feat: export core Hermes skills
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
---
|
||||
name: multichannel-messaging-discipline
|
||||
description: 跨渠道、多人环境下的通讯纪律——回复路由(在哪个渠道被找就回哪个渠道)、发送者身份核实、信息隔离、发送前核对。在向任何人或群发消息/回复、不确定对方是谁、或决定通知发往何处之前加载。覆盖企微 + 飞书的群聊与私聊。Load before sending any WeCom/Feishu message (group or DM), when unsure who the sender is, or when deciding where a reply/notification should go.
|
||||
---
|
||||
|
||||
# 多渠道通讯纪律 (Multichannel Messaging Discipline)
|
||||
|
||||
本环境同时有 **企微 + 飞书**、多个群、私聊、多个互不交叉的人(Doro、Maggie、邱律师、莎莎、洪总、Scott、WeiWei 等)。在这种环境里"把话说对人、发对地方"和把内容做对一样重要。任何一次发错渠道、认错人、跨人泄露,都是事故。
|
||||
|
||||
## 何时加载本技能
|
||||
- 向任何企微/飞书的**人或群**发消息、回复之前
|
||||
- 收到消息但**不能 100% 确定发送者是谁**时
|
||||
- 决定一条回复 / 通知 / 交付**应该发去哪里**时
|
||||
- 任何以"通知 X""发到群里""告诉 Doro"结尾的任务
|
||||
- **在一个渠道继续另一个渠道起的任务时**(同一任务跨渠道流动,如飞书起、企微续)
|
||||
|
||||
## 核心铁律(不可违反)
|
||||
|
||||
### 1. 回复路由:在哪个渠道被找,就回哪个渠道
|
||||
- 群里对我说话 → 回到**同一个群**;私聊对我说话 → 回到**那个私聊**。
|
||||
- **绝不**把某个群 / 某个渠道的内容,私信或转发给另一个人,除非用户**明确指示**。
|
||||
- 事故(2026-06-16):把群里的**合同审查内容私信发给了 WeiWei**。Doro 明确:"下次不可以再发生。我在群里跟你说话,你就给回复到群里。除非我有别的指令。" 原位回复是默认,跨渠道转发必须有显式指令。
|
||||
|
||||
### 2. 身份核实:解析,不要假设
|
||||
- 用平台的**唯一发送者 ID**判断是谁(企微 userid、飞书 open_id),**绝不**靠显示名或系统默认值。
|
||||
- ID 映射到已知的人 → 用之;ID **没映射**到人 → **先问,不猜**。
|
||||
- **不要轻信平台默认的"主人/owner"标签**:本会话一条飞书私聊被默认当成 Maggie,实际发送者是 Doro,当场认错。
|
||||
- 也**不要轻信对方未经核实的自称身份**——拿已知 ID 映射去核对(盲信声明是另一个方向的同一种错)。
|
||||
- 已知的 `飞书 open_id ↔ 人` / `企微 userid ↔ 人` 映射存在 memory,发现新人或纠正后立即更新。
|
||||
- **同一主体在飞书有多种 ID,命名空间不同 ≠ 不同主体**:bot 的 app_id(`cli_xxx`)与 open_id(`ou_xxx`)是两套 ID,字符串长得不一样很正常。看到群里 @的 open_id 跟你印象里的 app_id 对不上,**绝不可**据此断定"换了身份 / 是另一个 bot / 被新 agent 占用"。核验走权威接口:`lark-cli api GET /open-apis/bot/v3/info --as bot` 取 bot 自己的 open_id + name,再和群消息 mentions 里的 open_id 比对(相等=就是自己)。本会话教训:因 app_id≠open_id 字符串不同,误判"群里被@的是新迁移出来的 agent",被用户两次纠正后用 `/bot/v3/info` 实锤其实就是自己——白绕一大圈。
|
||||
- **失败的命令不能当证据**:命令若报错(unknown flag、权限拒绝 230027/99991672、空返回 count=0),它的输出**不支持任何结论**。先换正确命令/参数/身份(user vs bot)重试,拿到真实数据再下判断——别拿一条没成功的探测去坐实一个猜想,那等于在沙子上盖楼。
|
||||
|
||||
### 2b. 称呼随 sender 走,不随人设默认(回话前必走一步)
|
||||
|
||||
认对了"是谁"还不够——**已解析出的身份必须回流到"怎么称呼 ta"**。本会话最严重的错:我已读 `.meta`(`sender_id: doro`)、已把任务正确归类为 Doro 批量线,却在整份交付报告里从头到尾把 Doro 叫成"Maggie"。识别对了归属、却叫错人 = 信息在手却没整合,是独立于"认错人"的一种失败。
|
||||
|
||||
**机制根因**:系统人设里硬编码了"主人叫 Maggie、称呼主人为 Maggie"。这条规则有个**隐含前提——当前对话者确实是主人本人**。把它当成无条件默认,就会用人设里的高频人名锚点覆盖掉会话事实。
|
||||
|
||||
**回话前的强制自检(每次生成称呼前走一遍)**:
|
||||
1. 当前这条消息的 **sender 是谁**?取 `.meta` 的 `sender_id`,或会话 `Source / User`,**不取人设默认名**。
|
||||
2. sender **是主人本人吗**?是 → 才用主人称呼(Maggie);**否** → 用该 sender 的真实身份(`doro → Doro`、`qiuting → 邱律师` 等)。
|
||||
3. 已核实的任务归属身份与称呼**必须一致**——不能归属判对了、抬头却写错。
|
||||
|
||||
口诀:**人设默认名是"当对方是主人时"的条件值,不是无条件抬头。回话前先认 sender,再决定称呼。**
|
||||
|
||||
### 3. 信息隔离
|
||||
- 每个人 / 每个团队的文件和任务,只留在该人 / 该团队的上下文里。绝不把一方的内容带进另一方的渠道,也不在 A 的群里提 B 的任务。
|
||||
- Maggie 团队 与 Doro/邱律师 团队、各客户之间,严格不交叉。
|
||||
|
||||
### 3b. 跨渠道任务连续性(同一任务在两个渠道间流动时)
|
||||
同一个项目/任务可能在多个渠道接力推进——典型:飞书上确认了方法、更新了交付物,然后转到企微继续。**记忆是跨渠道通的(用户偏好、规则、文件位置都在),但每个渠道的对话上下文是隔离的**:飞书刚说的话不会自动进企微会话。所以\"接力续做\"前必须主动对齐,否则会拿旧认知在新渠道做事、重复已完成的工作、或漏掉刚确认的结论。
|
||||
|
||||
**续做铁律:先对齐,再动手。**
|
||||
1. **用 `session_search` 把另一渠道的最新成果拉回来**——搜项目名/关键结论(如\"世茂 千分号 汇总表\"),读出对方渠道刚确认了什么、交付物更新到哪一版。不要凭这边的旧记忆假设进度。
|
||||
2. **从权威源核实交付物现状**,不信任\"我记得\"——交付物(Nextcloud xlsx/docx)以实际文件为准,拉最新版打开看,确认版本/时间戳/内容与对方渠道的结论一致。
|
||||
3. **回应用户关心的具体修改/结论时,先复述对齐结果**(\"飞书那边刚确认的 X、更新的 Y 我已接收\"),让用户确认我们站在同一起点,再往下做。
|
||||
4. **渠道中断会留缺口**:若某渠道掉线过(如企微群订阅失效两小时),那段时间该渠道里别人发的内容这边是空白。续做前主动提示用户\"X 时段后若有人在该渠道发过东西,我没收到,请补给我\",避免任务断档——尤其客户对接渠道(隔离铁律下漏收=对接出窟窿)。
|
||||
|
||||
> 实证(2026-06-18 南通新东方/世茂):用户在飞书确认了 OCR 千分号/百分号识别法 + 更新了世茂汇总表,然后转企微说\"继续过世茂合同\"。正确做法是先 session_search 把飞书成果对齐、从 Nextcloud 拉最新版 xlsx 读出 4 份合同结构,再请用户给逐条指令——而不是在企微凭旧上下文直接开干。同期企微群因订阅失效(errcode 846609)静默两小时,须提示用户补回漏收的群消息。
|
||||
|
||||
### 4. 发送前三项核对(任何别人能看到的消息)
|
||||
发出去前确认:
|
||||
1. **收件人**——确切的人或群,解析出 chat_id / user_id(不要凭印象挑一个)
|
||||
2. **内容**——要发的正文
|
||||
3. **以谁的身份发**——bot 还是 user
|
||||
工具支持时**先 dry-run**,确认 payload 无误再正式发;发完把回执(message_id + 北京时间)报给用户便于核对。
|
||||
|
||||
### 5. 平台内账号/身份管理请求:把它当成“账号操作”,不是“消息回复”
|
||||
当用户让你**登录某个平台上的账号、修改密码、完成首次登录、维护个人账号资料**时,默认这是一个**账号操作任务**,不是跨人沟通任务。此时重点从“回哪个群/私聊”切换为:
|
||||
|
||||
1. **先区分是否是“当前 agent 自己要持有的账号”**
|
||||
- 用户明确说“这是你的账号”“你自己管理好密码” → 可由 agent 自行设置并保管该账号密码。
|
||||
- 如果是代表某个真人同事/用户持有的账号,且对方未授权 agent 自定密码 → 不能擅自决定长期密码。
|
||||
2. **执行后要给结果,不要停在征求式废话**
|
||||
- 用户已明确授权 agent 自主管理密码时,直接完成修改并回报“已完成”。
|
||||
- 不要在已获授权的情况下继续追问“你想设什么密码”。
|
||||
3. **账号管理与消息路由隔离**
|
||||
- 账号是平台内身份,不等于消息发送对象;不要因为会话对方是谁,就把账号密码策略误当成需要对方逐项确认的沟通动作。
|
||||
4. **汇报风格要结果导向**
|
||||
- 这类任务完成后,优先回:是否登录成功、是否已改密、页面确认信息。
|
||||
- 不展开无关解释,避免把简单账号操作说成审批流程。
|
||||
|
||||
### 5. 用户要求“重新查 / 不要用记忆 / 用文件夹里的问题件”时,立即切换到证据模式
|
||||
这类指令不是语气提醒,而是**明确纠偏**:之前的回答被认为混入了记忆、推断或口径漂移。后续必须把“回复别人”切回“基于当前文件/当前目录/当前记录的实查结果”。
|
||||
|
||||
执行要求:
|
||||
1. **停止沿用上一条摘要口径**——哪怕上一条是自己刚写的,也不能继续复述;
|
||||
2. **以用户指定的载体为准重新取证**:
|
||||
- 说“用文件夹里的问题件” → 先看该文件夹实际有哪些文件;
|
||||
- 说“打开文件查” → 必须打开文件或其内部结构(如 docx XML)再说;
|
||||
- 说“不要用记忆” → 禁止用 memory / 旧会话印象补缀事实;
|
||||
3. **结论按证据强弱分层表达**:
|
||||
- 能被当前文件直接坐实的,就说“可确认”;
|
||||
- 只能证明“文件存在”但不能证明“属于该批问题件/pass件”的,就明确写“目前只能确认存在,不能据此归类”;
|
||||
4. **不要把“在任务交付目录里存在”偷换成“就是问题件 / 已 pass / 属于同一批”**;
|
||||
5. **汇报时先交代证据来源**(查了哪个目录、哪份问题汇总、是否打开了文件),再给结论。
|
||||
6. **用户说“我就要一个结果”时,停止过程化解释**:这类话表示对方当前只接受最终答案,不要再补背景、过程、严谨性铺垫。先给一句结论;如对方追问,再展开证据链。
|
||||
|
||||
一句话:**用户说“重新查”时,先把脑子里的版本清空,回到文件本身;用户说“只要结果”时,先把话收成结论。**
|
||||
|
||||
## 实操配方
|
||||
- **飞书群里接收文件/图片**:飞书不允许文件和文字(@mention)在同一条消息里。解决方案:用户先发文件/图片,再对那条消息点「回复」并在回复里@小 Maggie。Gateway 的 `_fetch_parent_media` 方法会自动从被引用的 parent 消息中下载附件(2026-07-11 补丁)。文件缓存路径:`~/.hermes/cache/documents/`。图片同理——用户发图后回复+@即可。
|
||||
- 飞书群发消息 + @人(找群、@格式、dry-run、核对回执):见 `references/feishu-group-send-and-mention.md`
|
||||
- **飞书 bot 在群里不回、私聊却正常**的诊断配方(gateway inbound vs 群真实历史对照、查 `mentions[].id` 的 open_id 识别同名 bot 撞名、`--as user` 读群历史):见 `references/feishu-bot-silent-in-group-diagnosis.md`
|
||||
- 企微发文件 / 私信机制:见 `wecom-file-send-receive` 技能(MEDIA: 标签发文件;私信走独立 WS aibot_send_msg,chatid=userid+chat_type=1)
|
||||
- **企微私信非 home 的人(Doro/邱律师/WeiWei 等)→ 用 `~/.hermes/scripts/wecom_dm.py`,不要用 `send_message` 工具**:
|
||||
- `send_message(target='wecom:WeiWei')` 对非 home 的 wecom 用户会**静默回退到 home channel**(实测返回 `chat_id: JiaQian` + note `Sent to wecom home channel`),**不报错**——一条发给 WeiWei 的技术讨论就这样落到了 Maggie 渠道,同时违反信息隔离。
|
||||
- 正确做法(agent 可直接 terminal 调用,自开 WS、永不退化成回复、目标唯一):
|
||||
```bash
|
||||
python3 ~/.hermes/scripts/wecom_dm.py --list # 先看白名单别名↔userid
|
||||
python3 ~/.hermes/scripts/wecom_dm.py --to WeiWei --text "…" --dry-run # 演练
|
||||
python3 ~/.hermes/scripts/wecom_dm.py --to WeiWei --text "…" # 真发,回执含 message_id
|
||||
```
|
||||
- 别名:`doro / jiaqian / qiuting / weiwei / shasha / yangayi / xiaonan`(`--list` 为准)。这是 `references/wecom-proactive-notify-misrouting.md` 修复方向 A 在脚本层的现成实现,**比 `_send_wecom(extra,…)` 更可用**(后者需 gateway 内部 `extra`,agent 会话里拿不到)。
|
||||
- 企微主动通知**错投到错误的人**("给 Doro 的消息发给了 Maggie")的根因 + 只读诊断配方 + 修复方向:见 `references/wecom-proactive-notify-misrouting.md`
|
||||
|
||||
## Pitfalls
|
||||
- **把群任务的处理结果私信给"相关的人"**——即使你觉得对方该知道,也不行。原位回群,要不要另外通知由用户决定。
|
||||
- **靠会话默认值认人**——私聊默认 owner 不等于当前发送者,必须看 sender ID。
|
||||
- **拿\"两个长得不一样的 ID 字符串\"推断成\"两个身份/新迁移出的 agent\"**——同一个实体在不同命名空间有多种 ID,长相不同是常态:飞书机器人的 `app_id`(`cli_xxx`)与它的 `open_id`(`ou_xxx`)本就不一样,**两者是同一个 bot**。事故(2026-06-22 与 WeiWei 排障):我把群里被 @ 的 `ou_20bd8…`(open_id)和 gateway 配置里的 `cli_aaa4e77d27789bed`(app_id)当成两个 bot,进而臆断\"有个新迁移出来的 agent 占用了身份\",被纠正\"迁移还没开始\"。下结论前用**权威身份接口**核对:`LARK_CLI_NO_PROXY=1 lark-cli api GET /open-apis/bot/v3/info --as bot` 返回的 `open_id`/`app_name` 才是 bot 真身——拿它去比对,再判断是不是同一个。
|
||||
- **从一条 errored 的命令里读出\"结论\"**——同一次排障里,我那条查 bot open_id 的命令其实用错了 flag、根本没返回结果,我却继续往\"新身份\"上跳。**命令报错 = 没有证据,不是证据**;拿到真实返回再推理,别把工具失败当成支持自己假设的信号。
|
||||
- **同名 bot 撞名 = @ 显示名 ≠ @ 到你这个 app**:群里可能存在两个同名「小Maggie」(典型触发:做过新 Agent 迁移 / provisioning,新身份被拉进群)。用户 @ 显示名时飞书解析到的是某个 open_id,若那不是当前 gateway 跑的 app_id,事件流根本收不到,bot"静默不回",但 DM 仍正常(DM 按会话路由、不按 @ 身份)。诊断时必须查群消息 `mentions[].id` 的 open_id 和当前 bot app_id 是否一致,别一看不回就报"掉线"。完整配方见 `references/feishu-bot-silent-in-group-diagnosis.md`。
|
||||
- **认对了人却叫错称呼**——已解析出 sender 是 Doro,抬头却写"Maggie"。人设里"主人叫 Maggie"是"当对方是主人时"的条件值,不是无条件抬头;归属身份必须回流到称呼(见 2b)。
|
||||
- **session 无 .meta / sender 信息时,从文件内容或"印象"推断发件人**——事故(2026-07-13):session JSON 的 user message 里无 sender_id metadata,我从合同内容(青浦区卫生机构)推断是"刘婷律师",实际 gateway 日志明确显示 `user=QiuTing`(邱律师)。**当 session 记录不含 sender 信息时,必须查 gateway 日志(`~/.hermes/logs/gateway.log`)确认 `inbound message: platform=wecom user=XXX` 才能定身份**,绝不可从文件内容、以往经验、或"谁经常发这类合同"去猜。`grep "时间段" ~/.hermes/logs/gateway.log | grep "inbound"` 是唯一权威来源。
|
||||
- **猜 chat_id / group**——发前用 `lark-cli im +chat-list --as bot` 把 bot 实际所在的群列出来,挑出你和目标人共处的那个,别凭记忆。
|
||||
- **企微 @通知**:aibot_send_msg 只支持 markdown,不支持 text+mentioned_list,无法真正 @人;需要提醒时另发一条私信利用其消息提醒(仅在用户允许、且不违反路由铁律的前提下)。
|
||||
- **自动化脚本会替你发消息**:`auto_notify_new_file.sh` 等脚本会自动私信。处理任务前留意有没有自动化机制正在按旧规则推送,避免它替你违反路由 / 隔离铁律。
|
||||
- **`wecom_group_notify.py` 和 `wecom_dm.py` 是两个完全不同的脚本,不可混用(2026-07-01教训)**:Doro说"私信邱律师",用了 `wecom_group_notify.py`(默认发到批量合同审查群 `wrbAFkXAAAiWC3styKqNj0bZyH6BbJ_Q`),消息发到了群里而不是邱律师私信。**发前看清脚本名**:`wecom_dm.py` = 私信;`wecom_group_notify.py` = 群通知。workflow YAML 中通知邱律师的脚本也必须用 `wecom_dm.py --to qiuting`,不是 `wecom_group_notify.py`。
|
||||
- **`send_message` 工具私信企微人会静默投错**:`send_message(target='wecom:<user>')` 对非 home channel 的 wecom 用户**不报错、直接回退到 home channel**(实测发给 WeiWei 却落到 JiaQian/Maggie)。这是单点事故——既没到目标人、又跨团队泄露。私信非 home 的企微人一律走 `~/.hermes/scripts/wecom_dm.py --to <alias>`(见上"实操配方"),别用 `send_message`。发完核回执里的 userid 是不是目标人,发现是 home channel 立即用脚本补发。
|
||||
- **"给 X 的消息错投给 Y"先查 adapter 回复兜底,别先怪 userid**:企微 `WeComAdapter.send()` 在主动 `aibot_send_msg` 前有一段 `_last_chat_req_ids[chat_id]` 回复兜底——主动私信可能**退化成"回复某条历史消息"**,在多人并发的长跑 gateway 里串到别人头上。userid 往往是对的(`doro/JiaQian/QiuTing` 都是独立真实 ID),错在路径。诊断配方 + 根因 + 三个修复方向见 `references/wecom-proactive-notify-misrouting.md`。注意 `workflow-watchdog.sh` 仍硬编码 `_send_wecom(extra,'doro',msg)`,是未拆的隐患。
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# 飞书 bot 在群里静默、私聊却正常 —— 诊断配方
|
||||
|
||||
实证:2026-06-22 群「小Maggie工作群」(`oc_a927f86118216c36cb9394b0e95f2a11`)。WeiWei/颜伽艺在群里 @小Maggie 无反应,私聊正常。
|
||||
|
||||
## 症状
|
||||
- 用户在飞书**群**里 @小Maggie,bot 不回。
|
||||
- 但**私聊**(DM)发消息 bot 正常回复。
|
||||
- gateway 进程活着,飞书连接日志显示 `[Feishu] Connected in websocket mode`。
|
||||
|
||||
## 根因类别(按概率排序)
|
||||
1. **身份不匹配(display-name 撞名)** — 群里被 @ 的「小Maggie」其实是**另一个 bot 身份**(不同 open_id),不是当前 gateway 跑的那个 app。常见触发:做过「新 Agent 迁移 / provisioning」,新身份被拉进群并占用了群里「小Maggie」的 @ 目标。群 @ 解析到新身份的 open_id,旧 gateway(旧 app_id)的事件流里根本收不到这条,所以"不回"。**DM 仍正常,因为 DM 按会话路由、不按 @ 身份。** ← 本会话确诊就是这一类。
|
||||
2. 订阅/连接在空窗后失效(同类:企微 errcode 846609 静默两小时)。
|
||||
3. 发送侧失败:历史上见过 `[99992402] field validation failed`(连 plain-text 兜底也失败)——这是**发**不出去,不是**收**不到,必须区分。
|
||||
|
||||
## 诊断步骤(命令均已实测可用)
|
||||
所有 lark-cli 命令加 `LARK_CLI_NO_PROXY=1` 前缀,避免凭据走 HTTPS_PROXY。
|
||||
|
||||
**1. 确认 bot 在哪些群、拿群 chat_id:**
|
||||
```bash
|
||||
LARK_CLI_NO_PROXY=1 lark-cli im +chat-list --as bot
|
||||
```
|
||||
|
||||
**2. 查 gateway 实际收到了这个群的哪些 inbound:**
|
||||
```bash
|
||||
grep "oc_<群id>" ~/.hermes/logs/gateway.log | grep -iE "Inbound|inbound message"
|
||||
```
|
||||
若最后一条 inbound 停在很久以前、之后空白 → gateway 根本没收到新群消息(排除"收到但没回")。
|
||||
|
||||
**3. 拉群的真实消息历史,和第 2 步对照:**
|
||||
> bot 身份读群历史会因缺 scope 失败:`+chat-messages-list --as bot` → 230027 / 99991672,缺 `im:chat:readonly` `im:chat.members:read`。**改用 `--as user`**。
|
||||
```bash
|
||||
LARK_CLI_NO_PROXY=1 lark-cli im +chat-messages-list \
|
||||
--chat-id oc_<群id> --as user --order desc --page-size 30 --no-reactions --format json
|
||||
```
|
||||
返回 JSON 字段:`data.messages[]`,每条含 `content`(直接是文本)、`create_time`、`sender.{id,sender_type,name}`、`mentions[].{id,name}`。**注意不是 `items`/`body`** —— 用错字段会得到 `count=0` 的假空,误判成"群里没人发"。
|
||||
|
||||
**4. 关键判定 —— 看 @ 到的是谁的 open_id:**
|
||||
群消息的 `mentions[].id` 就是被 @ 对象的 open_id。和当前 gateway 的 bot 身份对比:
|
||||
```bash
|
||||
# 当前 gateway 用的飞书 app_id
|
||||
python3 -c "import yaml;c=yaml.safe_load(open('/home/maggie/.hermes/config.yaml'));print(c['gateway']['platforms']['feishu'].get('extra',{}).get('app_id'))"
|
||||
```
|
||||
- 对照法:第 2 步里 gateway 正常工作时段,bot 在群里发言的 sender 是 `app cli_<app_id>`。拿这个和第 4 步 mentions 里「小Maggie」的 open_id 比。
|
||||
- 若群历史里「小Maggie」被 @ 的 open_id ≠ 当前 gateway bot 的身份 → **确诊身份不匹配**:群里有两个同名「小Maggie」,大家 @ 错了。
|
||||
|
||||
**5. 旁证:DM 是否正常。** 私聊 inbound 在 gateway.log 里照常出现 → 证明连接/订阅活着,问题收敛到"群 @ 身份"这一层。
|
||||
|
||||
## 结论怎么报
|
||||
- 这是技术/配置问题,按铁律修复决策交给技术负责人(WeiWei/Scott),不自作主张改。
|
||||
- 给三个方向:①新 Agent 接管群(@ 目标对到新身份 / 把新身份订阅配通);②仍由旧 bot 管群(移除或换回群里的新「小Maggie」);③过渡期走私聊。
|
||||
|
||||
## Pitfalls
|
||||
- **别一看 bot 没回就说"掉线了"** —— 先分清"没收到(群 @ 身份不对 / 订阅失效)" vs "收到但没发出去(send 失败 99992402)"。
|
||||
- **lark-cli 读群历史/群成员要用 `--as user`**,bot 身份缺 scope。要让 bot 自己能读,得在开放平台给 `cli_xxx` 补 `im:chat:readonly` `im:chat.group_info:readonly` `im:chat.members:read`。
|
||||
- **JSON 字段名**:`+chat-messages-list` 返回 `data.messages[].content`,不是 `items[].body`。用错字段会得到误导性的 `count=0`。
|
||||
- **重启时 DNS 抽风是环境问题,不是代码问题** —— 重启日志里若有 `Temporary failure in name resolution`(open.feishu.cn / openws.work.weixin.qq.com),那是当时网络/DNS 短暂故障;飞书多半自己重连上了,企微可能没重连成功,单独核实企微在线状态即可,别当成代码 bug 去改。
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
# 飞书群发消息 + @人(已验证配方 2026-06-16)
|
||||
|
||||
目标:把消息发到**正确的飞书群**并 **@ 正确的人**,一次做对。依赖 `lark-cli`(lark-im 技能)。
|
||||
|
||||
## 步骤
|
||||
|
||||
### 1. 找出 bot 实际所在的群(不要猜 chat_id)
|
||||
```bash
|
||||
cd ~ && lark-cli im +chat-list --as bot
|
||||
```
|
||||
返回每个群的 `chat_id`、`name`、`description`、`owner_id`。挑出你和目标人**共处**的那个群。
|
||||
- 本环境已知群:"Doro, Maggie, 魏玮"(描述"小Maggie工作群")= `oc_a927f86118216c36cb9394b0e95f2a11`。
|
||||
- 若有多个候选群,按成员和描述确认,仍不确定就问用户,别赌。
|
||||
|
||||
### 2. @人的格式(两种已验证写法,均触发真实可点击 @ + 通知)
|
||||
- **text 模式**:`--msg-type text`,content 形如 `{"text":"<at>…</at> 正文"}`,@ 标签 `<at user_id="ou_xxx">显示名</at>`(用 **open_id**;嵌进 JSON 字符串时内层引号按 JSON 规则转义)。@所有人 `<at user_id="all"></at>`。
|
||||
- **post 模式**(多行/结构化汇报推荐,2026-06-16 已验证):post JSON 里 @ 用 at 元素 `{"tag":"at","user_id":"ou_xxx"}`,与文本元素 `{"tag":"text","text":"…"}` **同行**拼接,配 `--msg-type post`。结构:
|
||||
`{"zh_cn":{"content":[[{"tag":"at","user_id":"ou_757f053c9d7aff6c73b18aa60c337756"},{"tag":"text","text":" 进展汇报…"}],[{"tag":"text","text":"第二行"}]]}}`
|
||||
- **不要用 `--markdown`** 发 @:会强制转 post 且 @ 处理不可靠,用显式 post JSON 自己控制。
|
||||
|
||||
### 3. 先 dry-run 验证 payload
|
||||
```bash
|
||||
lark-cli im +messages-send --chat-id oc_xxx --as bot \
|
||||
--content '{"text":"<at user_id=\"ou_757f053c9d7aff6c73b18aa60c337756\">Doro</at> 正文…"}' \
|
||||
--msg-type text --dry-run
|
||||
```
|
||||
检查 body 里 chat_id、msg_type、@标签是否正确。
|
||||
|
||||
### 4. 去掉 --dry-run 正式发送
|
||||
成功返回 `message_id` + `create_time`。把 message_id 和**北京时间**报给用户便于核对。
|
||||
|
||||
## 发送前如何核实「这个 open_id 确实是目标本人」
|
||||
@错人是红线。理想是查通讯录,但本 bot **常缺 contact / chat 读权限**,按可靠性从高到低:
|
||||
|
||||
1. **gateway.log 取地面真值(最可靠,无需任何 scope)**——平台事件原始数据,比 API、比记忆都硬:
|
||||
```bash
|
||||
grep "oc_<群id>" ~/.hermes/logs/gateway.log | grep -oE "ou_[a-z0-9]{20,}" | sort | uniq -c | sort -rn
|
||||
```
|
||||
再看「当前这条消息」的入站行确认发送者:
|
||||
```bash
|
||||
grep "inbound message: platform=feishu" ~/.hermes/logs/gateway.log | tail
|
||||
# → user=ou_xxx chat=oc_xxx msg='…' 即是谁在这个群说了这句话
|
||||
```
|
||||
把刚收到那句话的 `user=ou_xxx` 与已知映射比对,一致才发。
|
||||
2. **已知 open_id 映射**(下方表 + memory),ID 没映射到人 → 先问不猜。
|
||||
3. **通讯录 API(常被 scope 挡)**:`lark-cli contact +get-user --user-id ou_xxx --user-id-type open_id --as user`。本环境 2026-06-16 报缺 `contact:user.basic_profile:readonly`;`im chat.members get` 也缺 `im:chat.members:read` 等。缺权限是**正常状态**,别卡在这里——退回方法 1。
|
||||
|
||||
## 已知 open_id(核对身份用,新增/纠正后同步到 memory)
|
||||
- Doro = `ou_757f053c9d7aff6c73b18aa60c337756`(与 bot 私聊 chat=`oc_9292e11bd98ddb5a69ea2c2da10d4f12`)
|
||||
- Scott(魏玮) = `ou_04fade9a9335c09ad09846da2051b3c0`
|
||||
|
||||
## 注意
|
||||
- `--as bot`:消息以应用 bot 名义发出,bot 必须已在目标群里。
|
||||
- lark-cli 是 API 工具,不是消息网关;bot 身份不代理用户(Scott 已纠正)。
|
||||
- 终端里 lark-cli 输出常带 HTTPS_PROXY 的 WARN 和版本更新 notice,是噪音,不影响结果;可 `grep -v "WARN\|proxy"` 过滤。
|
||||
|
||||
## 发文件附件到群里(md/pdf/docx 报告,已验证)
|
||||
|
||||
要把一份**文件**(错误分析 md、合同 docx、报告 pdf)发到群里,用 `--file`。常见组合:**先发文件附件,再发一条 @某人 的 post 说明**(文件本身不能 @ 人,说明消息负责 @)。
|
||||
|
||||
```bash
|
||||
# 先发文件(--file 接 cwd 相对路径),成功返回 message_id 并打印 "uploading file: xxx"
|
||||
cd ~/lark_send_tmp && lark-cli im +messages-send \
|
||||
--chat-id oc_a927f86118216c36cb9394b0e95f2a11 \
|
||||
--as bot --file "./报告.md" \
|
||||
--dry-run 2>&1 | grep -v "WARN\|proxy" # 先 dry-run,确认后去掉 --dry-run
|
||||
# 再发 @某人 的 post 说明(见上「@人的格式」post 模式),把文件背景+要点写清楚
|
||||
```
|
||||
|
||||
### ⚠️ 关键坑:`--file` 只接 cwd 相对路径,绝对路径被拒
|
||||
lark-cli 安全限制:`--file`(及 `--image`/`--video`/`--audio`)**拒绝绝对路径**(如 `/tmp/x.md`),路径解析 `..`/symlink 后必须仍在 cwd 内。
|
||||
**解法**:把文件复制到干净工作目录,`cd` 进去用 `./文件名` 发:
|
||||
```bash
|
||||
mkdir -p ~/lark_send_tmp && cp "/tmp/报告.md" ~/lark_send_tmp/
|
||||
cd ~/lark_send_tmp && lark-cli im +messages-send --chat-id oc_xxx --as bot --file "./报告.md"
|
||||
rm -rf ~/lark_send_tmp # 发完清理
|
||||
```
|
||||
- 本地文件 lark-cli 会**先自动上传**再发 file 消息,无需手动 `images.create`/拿 file_key。
|
||||
- dry-run 时 file_key 显示占位符 `file_dryrun_upload` 是正常的,正式发送才真上传。
|
||||
- 同理发图片用 `--image ./x.png`,视频 `--video ./x.mp4 --video-cover ./cover.png`。
|
||||
|
||||
## 已知群与人 ID(核对身份用)
|
||||
| 对象 | ID |
|
||||
|------|-----|
|
||||
| 飞书工作群"Doro, Maggie, 魏玮"(小Maggie工作群) | `oc_a927f86118216c36cb9394b0e95f2a11` |
|
||||
| Doro | `ou_757f053c9d7aff6c73b18aa60c337756`(私聊 chat `oc_9292e11bd98ddb5a69ea2c2da10d4f12`,别和群混) |
|
||||
| Scott / 魏玮 | `ou_04fade9a9335c09ad09846da2051b3c0` |
|
||||
| @所有人 | `all` |
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
# 企微主动通知错投到错误的人 — 根因与诊断
|
||||
|
||||
**症状**:一条本应私信给 A(如 Doro)的**主动通知**,落到了 B(如 JiaQian/Maggie)的企微私信里。
|
||||
实证(2026-06-17,Maggie 飞书原话):私信收到"收到 JiaQian 发来的文件…请问如何处理?"——这条内容本应发给 Doro,却投到了 JiaQian 本人。
|
||||
|
||||
**关键澄清:不是 userid 传错。** 会话 DB 里 `doro / JiaQian / WeiWei / ShaSha / QiuTing` 都是**各自独立的真实企微 userid**,`chat_id="doro"` 本身指向的就是 Doro。错投来自下面两个机制叠加,而非地址写错。
|
||||
|
||||
## 根因 1:发送者归属靠"猜"(旧 `auto_notify_new_file.sh`)
|
||||
旧版 `get_sender()` 查 session DB 取"最近 5 分钟最后一个会话":
|
||||
```sql
|
||||
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 行):
|
||||
```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) # 回复某条历史消息,而非主动私信
|
||||
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。
|
||||
```bash
|
||||
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',…)` 的脚本,理想改法就是切到这个干净发送器。
|
||||
|
||||
## 诊断配方(只读,安全)
|
||||
```bash
|
||||
# 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 往往是对的,错在"主动私信退化成回复历史消息"。
|
||||
Reference in New Issue
Block a user