feat: export core Hermes skills
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
# 企微主动私信的"干净直发"原语 + 错发根因 + 账号白名单
|
||||
|
||||
> 背景:贾茜(Maggie)反映"给 Doro 的消息错发给她"。排查发现根因在企微
|
||||
> adapter 的"回复兜底"机制。本文记录干净直发的可靠原语、错发机制、以及
|
||||
> 三方交叉验证过的真实 userid 白名单。
|
||||
|
||||
## 一、错发根因:adapter.send() 的"回复兜底"会串号
|
||||
|
||||
`gateway/platforms/wecom.py` 的 `WeComAdapter.send()`(约 1430–1445 行)有一条
|
||||
fallback:
|
||||
|
||||
```python
|
||||
reply_req_id = self._reply_req_id_for_message(reply_to)
|
||||
if not reply_req_id and chat_id in self._last_chat_req_ids:
|
||||
reply_req_id = self._last_chat_req_ids[chat_id] # ← 退化点
|
||||
if reply_req_id:
|
||||
response = await self._send_reply_markdown(reply_req_id, content) # 用历史 req_id "回复"
|
||||
else:
|
||||
# 才是真正的主动私信 aibot_send_msg + chat_type=1
|
||||
```
|
||||
|
||||
**含义**:在**长期运行的 gateway 进程**里,`_last_chat_req_ids` 会按 chat_id 缓存
|
||||
最近一条 inbound 的 req_id。多人并发时这个缓存可能串号——"发给 A 的主动消息"
|
||||
退化成"回复一条 req_id 绑定的历史消息",而那条历史消息的会话上下文可能属于 B,
|
||||
于是消息落到 B 头上。这是"给 Doro 的通知发到贾茜"的核心机制。
|
||||
|
||||
> 注意:`tools/send_message_tool.py` 里的 `_send_wecom()` 每次会 new 一个
|
||||
> **全新 adapter**(`_last_chat_req_ids` 为空),所以单次 `_send_wecom` 调用本身
|
||||
> 通常走 proactive 分支、不串号。真正高危的是**常驻 gateway 进程**内复用同一个
|
||||
> adapter 实例的发送,以及旧版 `auto_notify_new_file.sh` 的
|
||||
> "get_sender() 猜发件人 + 无差别 notify_doro" 叠加。结论:不要依赖
|
||||
> `_send_wecom`/`adapter.send()` 的兜底语义来保证"主动私信永远直达"——它不保证。
|
||||
|
||||
## 二、干净直发原语:~/.hermes/scripts/wecom_dm.py
|
||||
|
||||
这个脚本**完全绕开** `adapter.send()` 的回复兜底:自己开 WebSocket、
|
||||
`aibot_subscribe` 认证、直接 `aibot_send_msg` + **固定 `chat_type=1`**,
|
||||
永不退化成回复。一条消息 = 一次目标唯一确定的主动私信。已实测真发成功。
|
||||
|
||||
```bash
|
||||
# CLI
|
||||
python3 ~/.hermes/scripts/wecom_dm.py --to doro --text "内容"
|
||||
python3 ~/.hermes/scripts/wecom_dm.py --list # 白名单
|
||||
python3 ~/.hermes/scripts/wecom_dm.py --to doro --text "x" --dry-run
|
||||
|
||||
# 作为模块(注意:脚本在 ~/.hermes/scripts,需要时 sys.path.append)
|
||||
from wecom_dm import send_dm
|
||||
res = send_dm("doro", "内容") # res["success"], res["message_id"]
|
||||
```
|
||||
|
||||
特性:
|
||||
- **白名单防呆**:发给未核准账号会被拦截(除非 `--allow-raw`)。
|
||||
- **真实成功判定**:按企微 `errcode in {0, None}` 判断,不盲报成功;返回 `message_id`。
|
||||
- **无第三方依赖**:仅需 `aiohttp` + `pyyaml`(hermes venv 已有)。
|
||||
- 凭据从 `config.yaml` 的 `gateway.platforms.wecom.extra`(bot_id/secret)读取。
|
||||
|
||||
后续可把后台脚本(`workflow-watchdog.sh` 的 `notify()` 等)的通知改成调用此脚本,
|
||||
彻底脱离会串号的旧 `_send_wecom` 路径。
|
||||
|
||||
## 三、协议要点(自建发送时照此,已逐行核对源码)
|
||||
|
||||
- WS URL:`wss://openws.work.weixin.qq.com`(可被 `extra.websocket_url` 覆盖)
|
||||
- 认证:`cmd=aibot_subscribe`,body=`{bot_id, secret, device_id}`,等同 req_id 的 ack
|
||||
- 发送:`cmd=aibot_send_msg`,body=`{chatid, msgtype:"markdown", markdown:{content}, chat_type:1}`
|
||||
- 帧格式:`{"cmd":..., "headers":{"req_id":...}, "body":...}`,响应按 req_id 关联
|
||||
- 成功判定:响应顶层 `errcode in {0, None}` 即成功,否则读 `errmsg`
|
||||
- aiohttp 新版兼容:`ws_connect(timeout=...)` 用 float 会告警,优先
|
||||
`from aiohttp import ClientWSTimeout; ClientWSTimeout(ws_close=...)`,回退 float
|
||||
|
||||
## 四、已核准 userid 白名单(2026-06-21,三方交叉验证)
|
||||
|
||||
验证方法:state.db 的 `sessions.user_id` + `cache/documents/*.meta` 的 sender_id +
|
||||
`logs/*.log` 里 `platform=wecom user=X chat=Y` 三方对照。**日志里 user==chat 的记录
|
||||
即"私聊 DM"样本,证明该 userid 是企微可直达的真实私聊 chatid(chat_type=1)。**
|
||||
|
||||
| 别名 | 真实 userid | 身份 | 可信度 |
|
||||
|---|---|---|---|
|
||||
| doro | `doro` | Doro(律师·合同审查指导) | ★高 DB+meta+log,私聊×156 |
|
||||
| jiaqian | `JiaQian` | 贾茜 / Maggie(主人) | ★高 DB+log,私聊×108 |
|
||||
| qiuting | `QiuTing` | 邱律师(Doro 团队) | ★高 DB+log,私聊×71 |
|
||||
| weiwei | `WeiWei` | WeiWei(技术支持) | ★高 DB+log,私聊×49 |
|
||||
| shasha | `ShaSha` | 苌莎莎(律师·同团队) | △ DB+群 log,私聊无样本 |
|
||||
| yangayi | `YanGaYi` | 颜伽艺(架构师) | △ 单源 DB,低频 |
|
||||
| xiaonan | `XiaoNan` | XiaoNan | △ 单源 DB,低频 |
|
||||
|
||||
> userid 大小写敏感(`JiaQian` 不是 `jiaqian`)。脚本白名单同时接受小写别名和
|
||||
> 精确 userid。给真人发测试私信前先确认对象——优先发 WeiWei(技术支持,懂测试)。
|
||||
|
||||
## 五、★群聊不可替换(边界铁律)
|
||||
|
||||
`wecom_dm.py` 的 `chat_type=1` 是**主动私信专用**——企微 AI Bot 在**群聊**里**不能**主动 `aibot_send_msg`,只能走 adapter 的 RESPONSE 兜底(回复某条历史 inbound)。所以:
|
||||
- **本脚本只用于单聊私信,群聊发送绝不能改用它**(chat_type=1 在群里无效)。
|
||||
- 排查/替换发送点时,先用 chatid 前缀区分:**`wr` 开头 = 群聊(不动)**,其余 = 单聊(可迁移到 wecom_dm.py)。典型如 `notify_new_files.sh` 的群发 `adapter.send(chat_id='wrbAF...')` 必须**原样保留**。
|
||||
|
||||
## 六、已迁移调用点(单聊发送统一走本脚本)
|
||||
所有发企微**单聊私信**的后台脚本已从旧 `_send_wecom`/`adapter.send(chat_id='doro')` 改为调用 `wecom_dm.py`,每处加了"为什么不用 _send_wecom"的注释防回退,备份后缀 `.bak_replace_<时间戳>`:
|
||||
- `auto_notify_new_file.sh` → `notify_doro()`(当前唯一活跃运行)
|
||||
- `workflow-watchdog.sh` → `notify()`(历史脚本,当前未调度;注意它仍可能硬编码 `_send_wecom(extra,'doro',msg)`,是未拆隐患)
|
||||
- `notify_new_files.sh` → 单聊提醒部分(历史脚本,群发点原样保留)
|
||||
|
||||
### 改脚本前的标准流程
|
||||
1. `search_files` 搜 `_send_wecom|aibot_send_msg|adapter.send` 找全发送点 → 2. 逐个看 chatid 前缀判单聊/群聊 → 3. 备份 `cp 脚本 脚本.bak_replace_$(date +%Y%m%d_%H%M%S)` → 4. patch 替换单聊点、群聊点加注释保留 → 5. `bash -n` 语法 + grep 确认无残留单聊直调且群发点完好 → 6. 临时把目标改 weiwei 发一条测试拿 message_id 验证。
|
||||
Reference in New Issue
Block a user