Commit 5f2240d5ba8e2d24a2090309bd9816cc453087d5
1 parent
4cb28d27
docs: HANDOFF 加白名单门禁 + 授权流程章节(§十三)
- 角色(机器人本体 / owner)、门禁判定逻辑、授权交互(同意/拒绝) - .whitelist.json 格式 + 手动加白名单方法 - 引用回复授权的坑(type 57 appmsg,title + refermsg 提取) - 相关方法表 - TL;DR 加 step 0 白名单门禁;避雷清单加引用回复授权条目 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Showing
1 changed file
with
99 additions
and
1 deletions
server/services/wechat/HANDOFF.md
| @@ -3,7 +3,7 @@ | @@ -3,7 +3,7 @@ | ||
| 3 | > **谁会读这份文档**:刚接手这台 Pixel 部署的 claude-run / 开发者,没有上下文。 | 3 | > **谁会读这份文档**:刚接手这台 Pixel 部署的 claude-run / 开发者,没有上下文。 |
| 4 | > **目标**:5 分钟搞清楚正在跑什么、文件在哪、怎么改、怎么验证。 | 4 | > **目标**:5 分钟搞清楚正在跑什么、文件在哪、怎么改、怎么验证。 |
| 5 | > | 5 | > |
| 6 | -> **生成时间**:2026-05-25(v2,加 FSM + 流式 + hermes 补丁章节)。请定期核对实际状态, | 6 | +> **生成时间**:2026-05-25(v3,加 FSM / 流式 / hermes 补丁 / 白名单授权 章节)。请定期核对实际状态, |
| 7 | > 因为本文件不会随代码自动更新。 | 7 | > 因为本文件不会随代码自动更新。 |
| 8 | 8 | ||
| 9 | --- | 9 | --- |
| @@ -12,6 +12,7 @@ | @@ -12,6 +12,7 @@ | ||
| 12 | 12 | ||
| 13 | Pixel 手机本地的微信自动回复机器人。当好友(如 `yibooo`)给本机微信账号(小火子 / `wxid_51ap3m6p9xs722`)发消息,系统会: | 13 | Pixel 手机本地的微信自动回复机器人。当好友(如 `yibooo`)给本机微信账号(小火子 / `wxid_51ap3m6p9xs722`)发消息,系统会: |
| 14 | 14 | ||
| 15 | +0. **白名单门禁**(5/25 加):只对白名单的群/私聊自动回;非白名单先转给 owner 请示授权(详见 §十三) | ||
| 15 | 1. **轮询微信加密 DB**(sqlcipher)拿到新消息 | 16 | 1. **轮询微信加密 DB**(sqlcipher)拿到新消息 |
| 16 | 2. **拉最近 N 条上下文**喂给 hermes-agent (`POST 127.0.0.1:8642/v1/chat/completions`) | 17 | 2. **拉最近 N 条上下文**喂给 hermes-agent (`POST 127.0.0.1:8642/v1/chat/completions`) |
| 17 | 3. hermes 后端走 **Moonshot CN**(kimi-k2.6)生成回复(通过 Clash mixed port 出网) | 18 | 3. hermes 后端走 **Moonshot CN**(kimi-k2.6)生成回复(通过 Clash mixed port 出网) |
| @@ -299,6 +300,7 @@ pkill -f auto-reply.js | @@ -299,6 +300,7 @@ pkill -f auto-reply.js | ||
| 299 | | Hermes `/v1/runs` 长时间一片寂静、无任何 `[UTTER]` 事件输出,最后 `run.completed` 一次性给一大段总结 | Kimi-k2.6 在 hermes agentic mode 下 `assistant_message.content` 永远空,所有内容在 `reasoning_content`;`message.delta`/`interim_assistant_callback`/`message.complete` 都不 fire;只有 `reasoning.delta` 大量喷涌 | 短期开 `STREAM_REASONING=true` 让 reasoning 加 `🧠` 前缀作 utterance;长期改 hermes system prompt 让 Kimi 写 content 字段。详见 §十二.4 | | 300 | | Hermes `/v1/runs` 长时间一片寂静、无任何 `[UTTER]` 事件输出,最后 `run.completed` 一次性给一大段总结 | Kimi-k2.6 在 hermes agentic mode 下 `assistant_message.content` 永远空,所有内容在 `reasoning_content`;`message.delta`/`interim_assistant_callback`/`message.complete` 都不 fire;只有 `reasoning.delta` 大量喷涌 | 短期开 `STREAM_REASONING=true` 让 reasoning 加 `🧠` 前缀作 utterance;长期改 hermes system prompt 让 Kimi 写 content 字段。详见 §十二.4 | |
| 300 | | auto-reply node 进程被 keepalive 反复重启,CPU 占用高、log 文件被洪水般写入 | 流式场景下高频 `message.delta`/`reasoning.delta` 事件触发同步 `fs.appendFileSync` + 每条都 console.log,sync I/O 卡死 node;keepalive 检测心跳超时 → kill → restart | `log()` 去掉 `appendFileSync`(stdout 已由 start 脚本重定向到 log file);诊断事件加节流(高频事件 5 秒打一次) | | 301 | | auto-reply node 进程被 keepalive 反复重启,CPU 占用高、log 文件被洪水般写入 | 流式场景下高频 `message.delta`/`reasoning.delta` 事件触发同步 `fs.appendFileSync` + 每条都 console.log,sync I/O 卡死 node;keepalive 检测心跳超时 → kill → restart | `log()` 去掉 `appendFileSync`(stdout 已由 start 脚本重定向到 log file);诊断事件加节流(高频事件 5 秒打一次) | |
| 301 | | Hermes `/v1/runs` 只输出"最终一条回复",没多步进度 | hermes 默认 `/v1/runs` 只挂 `stream_delta_callback` + `tool_progress_callback`,缺 `reasoning_callback` + `interim_assistant_callback`(gateway 平台用 7+ 个回调) | 补丁 `gateway/platforms/api_server.py`,在 `_create_agent` 签名 + AIAgent 构造 + run handler 三处都加上缺的两个回调,emit `reasoning.delta` / `message.complete` SSE 事件。详见 §十二.3 | | 302 | | Hermes `/v1/runs` 只输出"最终一条回复",没多步进度 | hermes 默认 `/v1/runs` 只挂 `stream_delta_callback` + `tool_progress_callback`,缺 `reasoning_callback` + `interim_assistant_callback`(gateway 平台用 7+ 个回调) | 补丁 `gateway/platforms/api_server.py`,在 `_create_agent` 签名 + AIAgent 构造 + run handler 三处都加上缺的两个回调,emit `reasoning.delta` / `message.complete` SSE 事件。详见 §十二.3 | |
| 303 | +| owner 回「同意」授权后没生效、pending 没清 | owner 用微信**引用回复**发「同意」,content 是 type=57 appmsg XML(`<title>同意</title>` + `<refermsg>`),不是纯文本,`parseApprovalCommand` 正则匹配不上 | parseApprovalCommand 检测 `<appmsg>`/`<refermsg>` → 提取 `<title>` 作回复文本 + `<refermsg>` 作 quotedContext 匹配目标。详见 §十三.6 | | ||
| 302 | 304 | ||
| 303 | --- | 305 | --- |
| 304 | 306 | ||
| @@ -439,4 +441,100 @@ auto-reply.js hermes (/v1/runs) Moonshot CN | @@ -439,4 +441,100 @@ auto-reply.js hermes (/v1/runs) Moonshot CN | ||
| 439 | 441 | ||
| 440 | --- | 442 | --- |
| 441 | 443 | ||
| 444 | +## 十三、白名单门禁 + 授权流程(5/25 新增) | ||
| 445 | + | ||
| 446 | +### 13.1 目的 | ||
| 447 | + | ||
| 448 | +不再对任何人自动回复。只有**白名单**的群 / 私聊才自动回;非白名单的发信人, | ||
| 449 | +机器人先**转给主人(owner)请示**,owner 回「同意」才授权。 | ||
| 450 | + | ||
| 451 | +### 13.2 角色 | ||
| 452 | + | ||
| 453 | +| 角色 | wxid | 说明 | | ||
| 454 | +|------|------|------| | ||
| 455 | +| 机器人本体 | `wxid_51ap3m6p9xs722`(小火子) | 跑 auto-reply 的微信账号 | | ||
| 456 | +| **owner(主人/授权者)** | `yibooo`(火星人) | 收授权请求 + 发「同意/拒绝」指令;**永远在白名单** | | ||
| 457 | + | ||
| 458 | +ENV:`OWNER_WXID`(默认 `yibooo`)、`OWNER_NICKNAME`(默认 `火星人`,A11y 搜索用)。 | ||
| 459 | + | ||
| 460 | +### 13.3 门禁判定(`handleMessage` 内,replyType 判定之后) | ||
| 461 | + | ||
| 462 | +``` | ||
| 463 | +senderWxid = 群聊 ? content 前缀里的发送者 wxid : talker | ||
| 464 | +isFromOwner = senderWxid==OWNER || talker==OWNER | ||
| 465 | + | ||
| 466 | +if isFromOwner: | ||
| 467 | + parseApprovalCommand(content) 命中 → handleApproval(同意/拒绝),return | ||
| 468 | + 否则放行(owner 普通聊天) | ||
| 469 | +elif !whitelist.isAllowed(talker): | ||
| 470 | + requestAuthorization(转 owner 请示),return ← 不回复发信人 | ||
| 471 | +else: | ||
| 472 | + generateAndSendReply(正常流程) | ||
| 473 | +``` | ||
| 474 | + | ||
| 475 | +注意:**白名单 key 是 talker**(私聊=对方 wxid,群聊=chatroom id),即"授权这个会话"。 | ||
| 476 | +群里只要 chatroom 在白名单,任何人 @ 都会自动回。owner 在任何群 @ 都放行(按 senderWxid 判断)。 | ||
| 477 | + | ||
| 478 | +### 13.4 授权交互 | ||
| 479 | + | ||
| 480 | +- **请示**:`requestAuthorization` 给 owner 发 | ||
| 481 | + `【授权请求】X 给我发消息了:「...」回复「同意」我就自动回复 TA;回复「拒绝」忽略。` | ||
| 482 | + 并记入 pending(同一 talker 不重复请示)。 | ||
| 483 | +- **同意**:owner 回 `同意` / `同意 宜创AI全员群`(指定)→ 加白名单 + **自动回复 TA 刚才那条 pending 消息** + 给 owner 发确认。 | ||
| 484 | +- **拒绝**:owner 回 `拒绝` / `忽略` → 移除该 pending。 | ||
| 485 | +- 指令动词:`同意|授权|批准|approve|allow|yes` vs `拒绝|忽略|不同意|deny|no`。 | ||
| 486 | + | ||
| 487 | +### 13.5 `.whitelist.json` 格式 | ||
| 488 | + | ||
| 489 | +位于 `~/hermes-wechat/.whitelist.json`(持久化,重启不丢): | ||
| 490 | + | ||
| 491 | +```json | ||
| 492 | +{ | ||
| 493 | + "allowed": ["yibooo", "34576206251@chatroom"], | ||
| 494 | + "pending": { | ||
| 495 | + "wxid_g240cr5svks122": { | ||
| 496 | + "senderName": "", | ||
| 497 | + "content": "好的哈[社会社会]", | ||
| 498 | + "displayName": "马锐模", | ||
| 499 | + "ts": 1779684269591 | ||
| 500 | + } | ||
| 501 | + } | ||
| 502 | +} | ||
| 503 | +``` | ||
| 504 | + | ||
| 505 | +- `allowed`:已授权的 wxid(私聊)+ chatroom id(群)。owner wxid 启动时自动加入。 | ||
| 506 | +- `pending`:待授权请求,key 是 talker,value 含 senderName / 原始 content / displayName / 时间戳。 | ||
| 507 | +- 手动加白名单:直接编辑此文件加到 `allowed` 数组,重启 auto-reply 生效。 | ||
| 508 | + | ||
| 509 | +### 13.6 ⚠️ 引用回复授权的坑 | ||
| 510 | + | ||
| 511 | +owner 如果用微信**引用回复**发「同意」(而不是直接打字),content 不是纯文本 | ||
| 512 | +「同意」,而是 **type=57 appmsg XML**: | ||
| 513 | + | ||
| 514 | +```xml | ||
| 515 | +<msg><appmsg><title>同意</title><type>57</type> | ||
| 516 | + <refermsg>...被引用的"【授权请求】宜创AI全员群..."原文...</refermsg> | ||
| 517 | +</appmsg></msg> | ||
| 518 | +``` | ||
| 519 | + | ||
| 520 | +`parseApprovalCommand` 必须: | ||
| 521 | +1. 检测 `<appmsg>`/`<refermsg>` → 从 `<title>` 提取真正的回复文本("同意") | ||
| 522 | +2. 从 `<refermsg>` 提取 `quotedContext` → 匹配是"同意哪条 pending"(引用了哪条授权请求) | ||
| 523 | + | ||
| 524 | +`handleApproval` 目标匹配优先级:显式 target → quotedContext 里出现的 pending | ||
| 525 | +displayName → 最近一个 pending。 | ||
| 526 | + | ||
| 527 | +### 13.7 相关方法(auto-reply.js) | ||
| 528 | + | ||
| 529 | +| 方法 | 作用 | | ||
| 530 | +|------|------| | ||
| 531 | +| `WhitelistStore` | 持久化 allowed/pending | | ||
| 532 | +| `parseApprovalCommand(content)` | 解析授权指令(含引用回复 XML 提取),返回 `{approve, target, quotedContext}` | | ||
| 533 | +| `requestAuthorization(talker, senderName, content)` | 转 owner 请示 + 记 pending | | ||
| 534 | +| `handleApproval(cmd)` | 同意→加白名单+回复 pending;拒绝→移除 | | ||
| 535 | +| `generateAndSendReply({...})` | 抽出来的回复生成(白名单通过 / 授权后复用) | | ||
| 536 | +| `sendPlainTo(toUser, displayName, text)` | 给 owner 发纯文本(不走 hermes) | | ||
| 537 | + | ||
| 538 | +--- | ||
| 539 | + | ||
| 442 | **END OF HANDOFF** — 改动这份文档时记得更新顶部生成时间。 | 540 | **END OF HANDOFF** — 改动这份文档时记得更新顶部生成时间。 |