Files
hermes-skills/skills/communication/multichannel-messaging-discipline/references/wecom-proactive-notify-misrouting.md
T

6.0 KiB

企微主动通知错投到错误的人 — 根因与诊断

症状:一条本应私信给 A(如 Doro)的主动通知,落到了 B(如 JiaQian/Maggie)的企微私信里。 实证(2026-06-17,Maggie 飞书原话):私信收到"收到 JiaQian 发来的文件…请问如何处理?"——这条内容本应发给 Doro,却投到了 JiaQian 本人。

关键澄清:不是 userid 传错。 会话 DB 里 doro / JiaQian / WeiWei / ShaSha / QiuTing 都是各自独立的真实企微 useridchat_id="doro" 本身指向的就是 Doro。错投来自下面两个机制叠加,而非地址写错。

根因 1:发送者归属靠"猜"(旧 auto_notify_new_file.sh

旧版 get_sender() 查 session DB 取"最近 5 分钟最后一个会话":

SELECT user_id FROM sessions
WHERE source='wecom' AND started_at > (now-300)
ORDER BY started_at DESC LIMIT 1

多人并发时,这个"最近活跃会话"经常不是真正发文件的人 → 发件人张冠李戴。

已修复:企微 adapter 落盘缓存文件时写 .meta 边车文件(~/.hermes/cache/documents/<file>.meta,字段 sender_id / chat_id / chat_type)。脚本改读 ${filepath}.meta,不再靠"最近会话"猜。验证:.meta 内容形如 sender_id=doro chat_id=doro type=dm

根因 2:主动私信会"退化成回复"导致串号(adapter 层)

gateway/platforms/wecom.py WeComAdapter.send(chat_id, content) 在做主动 aibot_send_msg 前有一段回复优先兜底(约 1430–1445 行):

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)  # 回复某条历史消息,而非主动私信
else:
    payload = {"chatid": chat_id, "msgtype": "markdown", ...}
    if not chat_id.startswith("wr"):      # 群 ID 以 "wr" 开头;非 "wr" 当私聊
        payload["chat_type"] = 1          # 主动单聊
    response = await self._send_request(APP_CMD_SEND, payload)

_last_chat_req_ids[chat_id]入站流量填充(_remember_chat_req_id,约 533 行)。在长跑的 gateway 常驻进程里、多人并发时,某个 chat_id key 上记住的 req_id 可能绑定到归属于另一个人的会话上下文——于是"发给 doro"退化成"回复那条 req_id",落到错误的人头上。

注意两个独立的 dict:_reply_req_ids(按 message_id 存,给显式 reply_to 用)和 _last_chat_req_ids(按 chat_id 存,作群聊无 reply_to 时的兜底)。错投走的是后者这条 chat_id 兜底路径。

谁还在踩这条路径(主动私信脚本)

任何脚本调用 _send_wecom(extra, 'doro', msg)adapter.send() 都会经过上面的回复兜底,存在同样的串号风险。已知:

  • auto_notify_new_file.sh主循环已不再调用 notify_doro()(非 QiuTing 文件只记日志、不通知任何人,符合信息隔离铁律;QiuTing 走静默 workflow 不发中间通知)。notify_doro() 函数仍定义着但无调用点 → 主路径安全。
  • workflow-watchdog.sh:其 notify() 仍在用 _send_wecom(extra, 'doro', msg),workflow 崩溃重启时会触发,未拆除的隐患(低频但路径有风险)。

修复方向(动手前须经用户确认,勿擅改代码/脚本)

  • A(最稳):给 adapter 加"强制主动私信"参数,让脚本类通知绕过 _last_chat_req_ids 回复兜底,永远走 chat_type=1 proactive 直发。一次修复,所有脚本受益。涉及代码库,宜拉熟代码的人(WeiWei)评审。
  • B(最快):把 workflow-watchdog.sh 的通知目标改到本机日志/Scott,彻底不碰串号路径。改动小。
  • C(最保守):先出一页纸根因+修复评审文档,定了再动。

方向 A 已有现成实现:~/.hermes/scripts/wecom_dm.py(agent 可直接用)

不必等改 adapter——这个独立脚本已经实现了"强制主动私信、绕过回复兜底":自开 WebSocket,aibot_subscribe 认证后直接 aibot_send_msg + chat_type=1永不退化成回复,一条消息=一次干净 proactive send、目标唯一。带 --list 白名单(别名↔userid 三方交叉验证)、--dry-run、回执 message_id。

python3 ~/.hermes/scripts/wecom_dm.py --list
python3 ~/.hermes/scripts/wecom_dm.py --to doro --text "…" --dry-run
python3 ~/.hermes/scripts/wecom_dm.py --to doro --text "…"

凡是 agent 会话里要私信某个企微人(绕开 home channel)的场景,首选这个脚本,而不是 send_message(target='wecom:…')(会静默落 home channel)或 _send_wecom(extra,…)(需 gateway 内部 extra,会话里拿不到)。workflow-watchdog.sh 等仍硬编码 _send_wecom(extra,'doro',…) 的脚本,理想改法就是切到这个干净发送器。

诊断配方(只读,安全)

# 1. 脚本实际发了什么、发给谁(看 chat_id 与回执)
tail -n 80 /tmp/auto_notify_new_file.log
grep -aiE "Sending response .* to|aibot_send|chat_type|私信" ~/.hermes/logs/agent.log | tail -30

# 2. 真实 userid ↔ 人 映射(确认不是地址写错)
cd ~/.hermes/hermes-agent && source venv/bin/activate
python3 -c "import sqlite3;d=sqlite3.connect('$HOME/.hermes/state.db');[print(r) for r in d.execute(\"SELECT user_id,COUNT(*),MAX(started_at) FROM sessions WHERE source='wecom' GROUP BY user_id ORDER BY 2 DESC\")]"

# 3. .meta 边车归属是否正确
find ~/.hermes/cache/documents -name '*.meta' -exec cat {} \;

# 4. 谁还在用 doro 硬编码主动发送
# search_files pattern: _send_wecom|DORO_ID="doro"  在 ~/.hermes/scripts

一句话结论

"给 X 的消息错投给 Y"在企微里首查 adapter 的 _last_chat_req_ids 回复兜底脚本的发件人归属逻辑,不要一上来就怀疑 userid 写错——userid 往往是对的,错在"主动私信退化成回复历史消息"。