Commit 5f2240d5ba8e2d24a2090309bd9816cc453087d5

Authored by yibo
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>
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** — 改动这份文档时记得更新顶部生成时间。