Files
hermes-skills/skills/legal/wecom-file-send-receive/references/proactive-dm-clean-send.md
T

103 lines
6.6 KiB
Markdown

# 企微主动私信的"干净直发"原语 + 错发根因 + 账号白名单
> 背景:贾茜(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 验证。