Files
hermes-skills/skills/legal/wecom-file-send-receive/SKILL.md
T

20 KiB

name, description, version, tags
name description version tags
wecom-file-send-receive 企微群/私聊中发送和接收文件的完整方法+故障排查(846609、群消息丢失、发送vs接收问题区分)。发送用MEDIA标签,接收从cache/documents/读取。跨会话召回和文件接收工作流见references/。 1.0.0
企业微信
文件
MEDIA

企微文件发送与接收

📌 主动私信(proactive DM)找谁发、怎么发不串号:见 references/proactive-dm-clean-send.md —— 含 wecom_dm.py 干净直发原语、 错发根因(回复兜底串号)、以及三方交叉验证过的真实 userid 白名单。

📌 主动群消息(group notify)~/.hermes/scripts/wecom_group_notify.py —— 自开 WebSocket 直连,不经 gateway adapter,用于 workflow/脚本场景向群里发通知。 默认群 = 批量合同审查群(wrbAFkXAAAiWC3styKqNj0bZyH6BbJ_Q)。

python3 ~/.hermes/scripts/wecom_group_notify.py --text "消息内容"             # 默认群
python3 ~/.hermes/scripts/wecom_group_notify.py --group <群ID> --text "内容"  # 指定群

⚠️ 注意与私信脚本的区别:群通知不传 chat_type(群消息默认),私信脚本传 chat_type=1

已知问题:send_message无法向WeCom用户发送proactive私信

根因(2026-06-12定位)send_message_tool.py_parse_target_ref()函数只认纯数字chat_id为explicit target。WeCom的userid(如"QiuTing"、"doro")是字母混合的,不被识别→返回(None, None, False)→chat_id=None→fallback到home channel。

表现send_message(target="wecom:QiuTing")实际发送到了JiaQian(home channel),不是QiuTing的私信。返回的response显示"note": "Sent to wecom home channel"

为什么给doro发有时能成功:当前session是doro发起的,gateway有doro的reply_req_id缓存,走的是回复路径而非proactive发送。对没有活跃session的用户(如QiuTing)则必定失败。

修复方案:在tools/send_message_tool.py_parse_target_ref中为wecom添加explicit识别规则(约第408行return None, None, False之前):

if platform_name == "wecom" and target_ref and not target_ref.startswith("#"):
    return target_ref, None, True

临时绕过:通过直接WebSocket连接发送aibot_send_msg(chatid=userid, chat_type=1),但受proxy/网络环境限制也不稳定。

私信 vs 群消息 脚本选择(2026-07-01 铁律——发错目标=信息泄露)

目标 用什么脚本 绝对不能用
私信某人 wecom_dm.py --to <别名> wecom_group_notify.py(会发到群里)
群消息 wecom_group_notify.py --group <群ID> --text "..." wecom_dm.py(发不到群里)

2026-07-01 教训:Doro让私信邱律师,用了 wecom_group_notify.py(默认发到批量合同审查群),消息发到了群里所有人都看到了。workflow YAML(review-contract.yaml 第16-17行)里 classifier 的 not_a_contract 通知也写的是群通知脚本——应该改为 wecom_dm.py --to qiuting

铁律:看到"私信""私聊""单独告诉"等字眼,一律用 wecom_dm.py --to。只有明确说"在群里说""发到群里"时才用 wecom_group_notify.py

发送文件

⚠️ 铁律:不要用 send_message 工具发企微文件

send_message 的 MEDIA 功能不支持企微(仅支持 telegram/discord/matrix/weixin/signal/yuanbao/feishu)。调用 send_message(target="wecom:...", message="MEDIA:...") 会直接报错 send_message MEDIA delivery is currently only supported for...

企微发文件的唯一正确方式是在回复正文中写 MEDIA: 标签——由 gateway 拦截并调用 adapter.send_document()。不要绕道 send_message,也不要绕道邮件——直接在回复里写标签就行。

在回复文本中包含 MEDIA:/absolute/path/to/file.ext,gateway自动:

  1. 扫描回复中的 MEDIA: 标签
  2. 调用 adapter.send_document() 发送文件到当前对话
  3. 剥离标签,用户只看到文件附件

示例

审查完成,请查收修订版合同。

MEDIA:/tmp/contract-review/【修】合同名称.docx

注意事项

  • 路径必须是绝对路径,文件必须存在
  • 可以在一条回复中包含多个 MEDIA: 标签发送多个文件
  • 适用于所有平台(企微、Telegram等),不限于企微
  • MEDIA: 标签放在回复末尾即可,不影响正文显示

接收文件

企微AI Bot消息类型限制

AI Bot只接收msgtype=text的消息。纯图片、纯文件消息不触发回调(群聊和私信均如此)。

  • 私信中直接发图片/文件 → 不触发回调 → 小Maggie收不到
  • 合并转发 → API层面替换为[该消息类型暂不能展示] → 图片数据丢失
  • 唯一能传图/文件的方式:群里引用图片/文件消息并@小Maggie发文字

群Session隔离

配置 group_sessions_per_user: true(默认)时,同一群里不同用户@小Maggie走独立session。session key 格式:agent:main:wecom:group:<群id>:<用户id>。session 状态可查 ~/.hermes/sessions/sessions.json

触发方式(群里引用)

用户操作:

  1. 先发送文件/图片(此时小Maggie收不到)
  2. 引用/回复那条文件/图片消息并@小Maggie发文字说明需求

文件保存位置

~/.hermes/cache/documents/doc_<hash>_<原始文件名>

文件名经过URL编码,用 urllib.parse.unquote() 还原。

Gateway处理流程

  1. 收到引用消息 → 从quote中提取文件url和aes_key
  2. 下载文件 → AES解密(已修复base64 padding问题)
  3. 保存到 ~/.hermes/cache/documents/
  4. 消息中包含 The file is saved at: <路径>

读取文件

# docx
import zipfile, io
from lxml import etree
with open(path, 'rb') as f:
    # 正常docx操作

# 或用python-docx只读(不要用它save)
from docx import Document
doc = Document(path)

跨聊天发送消息/文件(发到非当前对话)

⚠️ 铁律:不要用 send_message 工具发企微私信

send_message(target='wecom:QiuTing') 等写法不可靠——实测会静默路由到 home channel(JiaQian)而不是目标用户,无报错。2026-06-12验证:连续两次 send_message(target='wecom:QiuTing') 均返回 chat_id: JiaQian,消息发给了Maggie而不是邱律师。

⚠️ 2026-06-21 重要修正_send_wecom / adapter.send() 内部有"回复兜底" 机制(_last_chat_req_ids → 退化成 APP_CMD_RESPONSE 回复历史消息),在长期 运行的 gateway 进程里多人并发时会串号错发(贾茜反映"给 Doro 的消息错发给 她"即源于此叠加旧脚本的 get_sender 误判)。单次 _send_wecom 命令行调用每次 new 一个空 adapter,通常不串号;但后台脚本/daemon 的主动通知不要依赖这个兜底 语义。最干净可靠的主动私信原语是 ~/.hermes/scripts/wecom_dm.py(自开 WebSocket,固定 chat_type=1,永不退化成回复,带账号白名单防呆 + errcode 真实 判定)。详见 references/proactive-dm-clean-send.md,含三方交叉验证的 userid 白名单。

python3 ~/.hermes/scripts/wecom_dm.py --to doro --text "内容"   # 推荐
python3 ~/.hermes/scripts/wecom_dm.py --list                    # 看核准账号

发私信/群消息到非当前对话的另一种方式是直接调用 _send_wecom(单次命令行调用可靠,后台 daemon 优先用上面的 wecom_dm.py):

cd /home/maggie/.hermes/hermes-agent && source venv/bin/activate && python -c "
import asyncio, yaml
from tools.send_message_tool import _send_wecom
with open('/home/maggie/.hermes/config.yaml') as f:
    cfg = yaml.safe_load(f)
extra = cfg.get('gateway', {}).get('platforms', {}).get('wecom', {}).get('extra', {})
result = asyncio.run(_send_wecom(extra, 'QiuTing', '消息内容'))
print(result)
"
  • _send_wecom(extra, '<用户ID>', msg) = 私信 可靠
  • send_message(target='wecom:<用户ID>') = 会误发到home channel

MEDIA标签只能发到当前对话。要发文件到其他聊天(如从群聊发文件到某人私信),需要直接调用WeComAdapter:

import asyncio

async def send_file_to_chat(chat_id: str, file_path: str, file_name: str = None):
    """发送文件到指定企微聊天(私信或群)"""
    import os, yaml
    from gateway.platforms.wecom import WeComAdapter
    from gateway.config import PlatformConfig

    with open(os.path.expanduser("~/.hermes/config.yaml")) as f:
        cfg = yaml.safe_load(f)
    wecom_cfg = cfg.get("gateway", {}).get("wecom", {})
    pconfig = PlatformConfig(extra=wecom_cfg)
    adapter = WeComAdapter(pconfig)

    connected = await adapter.connect()
    if not connected:
        raise RuntimeError(f"Failed to connect: {adapter.fatal_error_message}")
    try:
        result = await adapter.send_document(
            chat_id=chat_id,
            file_path=file_path,
            file_name=file_name or os.path.basename(file_path),
        )
        return result
    finally:
        await adapter.disconnect()

# 使用示例:从群聊中发文件到邱律师私信
# asyncio.run(send_file_to_chat("QiuTing", "/tmp/file.docx"))

注意事项

  • chat_id:私信用企微用户ID(如"QiuTing"),群聊用群ID(如"wrbAFk...")
  • 必须在hermes-agent的venv中运行(需要gateway模块)
  • 运行目录:cd /home/maggie/.hermes/hermes-agent && source venv/bin/activate

故障排查

先分清是发送问题还是接收问题

发送失败(errcode 846609)和接收不到是两个独立问题,不要混为一谈:

  • 发送失败:gateway.log 中有 Sending response 但紧跟 Send failed: errcode 846609(aibot websocket not subscribed)。消息收到了、处理了,只是回复发不出去。
  • 接收不到:gateway.log 中完全没有某个群/私信的 inbound message 记录。消息根本没到达 gateway。
  • 846609 是企微服务端的订阅态问题,通常重启 gateway 可恢复,但它不影响接收——可以收到消息但发不出回复。

群消息收不到但私信正常

⚠️ 第一反应不要重启 gateway。 企微群消息和私信走同一条 WebSocket 连接。如果私信能收到,WebSocket 没断,重启不解决问题。

⚠️ 不要先猜测原因再找证据。 先看日志事实,再得结论。

排查顺序(严格按此顺序,不要跳步):

  1. 查 gateway.log(首选,不是 journalctl):gateway.log 包含 INFO 级别的 inbound message 记录(含 chat_id 和 user),journalctl 通常只有 WARNING+,看不到消息是否到达。

    tail -200 ~/.hermes/logs/gateway.log | grep "inbound message"
    # 能看到哪些群/私信的消息被收到了,哪些完全没出现
    # 对比不同群的 chat_id,确认哪些群有消息哪些没有
    

    关键判断:如果某个群的消息在日志中完全没出现过(连 debug 级别的 policy/block 记录都没有),说明消息在企微服务端就没推过来,跟本地代码无关。

  2. 查本地代码改动(Hermes 源码有本地 patch)——当用户怀疑代码改动导致问题时,立即看代码,不要先猜:

    cd ~/.hermes/hermes-agent
    git diff                    # 未提交的改动
    git log --oneline -5        # 本地提交 vs 上游
    git stash list              # 暂存的改动
    git diff <upstream_tag>..HEAD -- gateway/platforms/wecom.py  # 与上游对比
    git show <commit> --stat    # 看影响了哪些文件
    git show <commit> -- gateway/platforms/wecom.py  # 看具体改动
    

    分析改动时必须精确分类到三条路径:

    • 接收路径_on_message, _dispatch_payload, _read_events, _extract_text, _extract_media, _derive_message_type
    • 发送路径_send_*, send_document, _send_reply_markdown, _send_media_message
    • 连接路径_listen_loop, _open_connection, _heartbeat_loop 不要笼统说"都不影响"——按路径逐条分析。
  3. 检查消息类型限制:AI Bot 只接收 msgtype=text,纯图片/文件消息不触发回调

  4. 检查 group_policy 配置~/.hermes/config.yaml 中 wecom.extra.group_policy(默认 open)

  5. 确认 bot 已被加入目标群 + @的是正确的 bot 名称

    • 同一个企微环境可能有多个 bot
    • 不同用户 @小Maggie 的效果可能不同(A能@到但B@不到——可能B的客户端上显示的bot名不同)
    • 如果某用户@的群消息完全没到达,但另一个用户@同一个群的消息能收到,问题在@的目标身份而非群本身

WebSocket 断开(群消息和私信都收不到)

  • 日志特征:WARNING [Wecom] WebSocket error: WeCom websocket closed
  • 修复:systemctl --user restart hermes-gateway
  • 本地 patch 加了 consecutive_failures 上限(MAX=20),超过会停止重连循环
  • ⚠️ 超过20次后gateway永不自动恢复——必须手动restart

网络层故障导致的WebSocket断连

当服务器本身网络短暂故障时(DNS解析失败、SSL证书错误),WeCom和Weixin会同时挂掉。这是区分网络问题 vs WeCom-specific问题的关键信号。

日志特征(三种错误交替出现):

WARNING [Wecom] Reconnect failed: Cannot connect to host openws.work.weixin.qq.com:443 ssl:True [SSLCertVerificationError: self-signed certificate in certificate chain]
WARNING [Wecom] Reconnect failed: Cannot connect to host openws.work.weixin.qq.com:443 ssl:default [Temporary failure in name resolution]
ERROR [Wecom] Too many consecutive reconnect failures (20), stopping listen loop

诊断和恢复步骤:

  1. 确认网络已恢复(不要盲目重启——网络没好重启也没用):
    curl -sI https://openws.work.weixin.qq.com 2>&1 | head -3  # 应返回HTTP响应
    dig openws.work.weixin.qq.com +short                        # 应返回IP
    
  2. 重启gatewaysystemctl --user restart hermes-gateway
  3. 验证所有平台重连成功:用 send_message(action='list') 确认WeCom targets出现,或查日志确认WebSocket connected

errcode 846609: aibot websocket not subscribed

  • 含义:企微服务端认为此 bot 的 WebSocket 订阅无效,拒绝发送/回复
  • 影响:只影响发送,不影响接收——可能同时还在收消息但回复全部失败
  • ⚠️ 不要因为 846609 就说"WebSocket 断了"——846609 是发送端的订阅态问题,接收端的 WebSocket 连接可能完全正常。如果私信能收到,WebSocket 连接没断。
  • 日志特征:ERROR [Wecom] Send failed: ... WeCom errcode 846609: aibot websocket not subscribed
  • 修复:systemctl --user restart hermes-gateway(重新建立 WebSocket 订阅)
  • 注意:重启期间正在处理的消息会丢失回复,但消息本身已被 gateway 处理过

主动往群里推消息的风险

企微 AI Bot 不能用 APP_CMD_SEND 主动发群消息,Hermes 代码通过复用旧 req_id_last_chat_req_ids)以 APP_CMD_RESPONSE 方式发送。这个 fallback 是唯一可行的做法,不是绕限制。

  • 但如果旧 req_id 已过期太久,发送会失败(846609)

⚠️ 铁律:后台脚本/daemon 禁止往群里发消息

所有自动化通知必须走私信(chat_id=用户ID, chat_type=1),不发群。

  • 2026-06-09教训:原脚本用notify_group同时往群里和私信发,Doro说"已经触发风险机制了"。已修复为notify_doro只走私信。任何修改此脚本的人必须遵守此规则。
  • 例外wecom_group_notify.py用于workflow中classifier识别到非合同文件后一次性通知邱律师(低频、事件驱动、非循环推送),不属于daemon定时推送,不触发风控。区分:daemon循环发=禁止;workflow事件触发一次=允许。
  • 排查"谁在往群里发消息"的标准流程:
    1. ps aux | grep -E "auto_notify|monitor|watchdog" 找后台脚本
    2. 检查其中是否有 _send_wecom(extra, '<群ID>', ...) 调用
    3. 检查 /tmp/auto_notify_new_file.log 看是否有 Group: 开头的发送记录
  • _send_wecom(extra, '<用户ID>', msg) = 私信
  • _send_wecom(extra, '<群ID>', msg) = 群消息 禁止

⚠️ auto_notify_new_file.shget_sender() 不可靠

脚本用 get_sender()sessions 表最近5分钟内的最后一个session来判断发件人。这不准——如果多人同时在聊天或session时间不吻合,就会误判(实测:邱律师的文件被误判为WeiWei)。

  • 准确查发件人的方法:查 messages 表中 [The user sent a document: 开头的用户消息,JOIN sessions 表取 user_id
  • 手动确认时直接查 state.db:
    SELECT s.user_id, m.content FROM messages m
    JOIN sessions s ON m.session_id = s.id
    WHERE m.role='user' AND m.content LIKE '%sent a document%'
    ORDER BY m.timestamp DESC LIMIT 5
    

Nextcloud桌面客户端同步失败诊断(Cloudflare Tunnel)

当用户报告"同步不流畅/更新同步不了"时,查以下两个日志:

  1. Nextcloud日志docker exec nextcloud-nextcloud-1 tail -100 /var/www/html/data/nextcloud.log | grep '"level":3'
    • 典型错误:预期文件大小为X字节,实际写入Y字节(BadRequest) → 上传被中途截断
  2. Cloudflared日志journalctl -u cloudflared-nextcloud.service --since today | grep ERR
    • 典型错误:Incoming request ended abruptly: context canceled → Cloudflare超时掐断

根因:Cloudflare免费版单次HTTP请求100秒超时硬限制(不可配置)。桌面客户端默认整体传输>10MB文件,上传带宽不足时被超时掐断。

解法:桌面客户端开启分块上传,把大文件拆成小块传输:

  • Windows: %APPDATA%\Nextcloud\nextcloud.cfg[General] 段加 chunkSize=5242880
  • macOS: ~/Library/Preferences/Nextcloud/nextcloud.cfg
  • Linux: ~/.config/Nextcloud/nextcloud.cfg
  • 改完需退出并重启客户端

服务端验证(确认分块上传端点正常):

curl -s -o /dev/null -w "%{http_code}" -u doro:PASS http://localhost:5000/remote.php/dav/uploads/doro/
# 返回200 = 正常

注意:tunnel配置的connectTimeout只管建立连接超时,不管传输过程超时。加noChunkedEncoding让cloudflared先缓存再转发理论上有帮助,但治不了CF边缘100秒硬限制——分块上传才是正解。参见 references/nextcloud-sync-diagnosis.md

大文件传输失败的fallback路径

当用户需要传大文件(>30MB),常规渠道可能都失败:

  • 企微群/私信:文件消息无法同时@人,且AI Bot只收text类型
  • Nextcloud上传:大文件上传可能中断(.part文件出现后消失,无最终文件)
  • 诊断Nextcloud上传中断:在容器内查 .part 文件和 uploads/ 目录
    sudo docker exec nextcloud-nextcloud-1 bash -c 'find /var/www/html/data/<user>/ -name "*.part" 2>/dev/null'
    sudo docker exec nextcloud-nextcloud-1 bash -c 'find /var/www/html/data/<user>/uploads/ -type f 2>/dev/null'
    

fallback方案:让用户发邮件。提供 maggiejunior@shazhou.work,用himalaya skill下载附件。注意大附件(>20MB)IMAP下载也可能很慢,参见himalaya skill的"Pitfalls: Large Attachments"部分。

常见文件问题

  • 找不到文件:搜 ~/.hermes/cache/documents/,不要只搜 /tmp
  • 文件乱码:检查 file 命令输出,正常应显示 Microsoft Word 2007+
  • 发送失败:确认文件路径存在且为绝对路径
  • 文件名太长:中文文件名 URL 编码后可能超 255 字节 ext4 限制,本地 patch 已加截断逻辑