From 5f2240d5ba8e2d24a2090309bd9816cc453087d5 Mon Sep 17 00:00:00 2001 From: yibo Date: Mon, 25 May 2026 12:53:02 +0800 Subject: [PATCH] docs: HANDOFF 加白名单门禁 + 授权流程章节(§十三) --- server/services/wechat/HANDOFF.md | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/server/services/wechat/HANDOFF.md b/server/services/wechat/HANDOFF.md index 53e56a4..3bc1baa 100644 --- a/server/services/wechat/HANDOFF.md +++ b/server/services/wechat/HANDOFF.md @@ -3,7 +3,7 @@ > **谁会读这份文档**:刚接手这台 Pixel 部署的 claude-run / 开发者,没有上下文。 > **目标**:5 分钟搞清楚正在跑什么、文件在哪、怎么改、怎么验证。 > -> **生成时间**:2026-05-25(v2,加 FSM + 流式 + hermes 补丁章节)。请定期核对实际状态, +> **生成时间**:2026-05-25(v3,加 FSM / 流式 / hermes 补丁 / 白名单授权 章节)。请定期核对实际状态, > 因为本文件不会随代码自动更新。 --- @@ -12,6 +12,7 @@ Pixel 手机本地的微信自动回复机器人。当好友(如 `yibooo`)给本机微信账号(小火子 / `wxid_51ap3m6p9xs722`)发消息,系统会: +0. **白名单门禁**(5/25 加):只对白名单的群/私聊自动回;非白名单先转给 owner 请示授权(详见 §十三) 1. **轮询微信加密 DB**(sqlcipher)拿到新消息 2. **拉最近 N 条上下文**喂给 hermes-agent (`POST 127.0.0.1:8642/v1/chat/completions`) 3. hermes 后端走 **Moonshot CN**(kimi-k2.6)生成回复(通过 Clash mixed port 出网) @@ -299,6 +300,7 @@ pkill -f auto-reply.js | 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 | | 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 秒打一次) | | 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 | +| owner 回「同意」授权后没生效、pending 没清 | owner 用微信**引用回复**发「同意」,content 是 type=57 appmsg XML(`同意` + ``),不是纯文本,`parseApprovalCommand` 正则匹配不上 | parseApprovalCommand 检测 ``/`` → 提取 `` 作回复文本 + `<refermsg>` 作 quotedContext 匹配目标。详见 §十三.6 | --- @@ -439,4 +441,100 @@ auto-reply.js hermes (/v1/runs) Moonshot CN --- +## 十三、白名单门禁 + 授权流程(5/25 新增) + +### 13.1 目的 + +不再对任何人自动回复。只有**白名单**的群 / 私聊才自动回;非白名单的发信人, +机器人先**转给主人(owner)请示**,owner 回「同意」才授权。 + +### 13.2 角色 + +| 角色 | wxid | 说明 | +|------|------|------| +| 机器人本体 | `wxid_51ap3m6p9xs722`(小火子) | 跑 auto-reply 的微信账号 | +| **owner(主人/授权者)** | `yibooo`(火星人) | 收授权请求 + 发「同意/拒绝」指令;**永远在白名单** | + +ENV:`OWNER_WXID`(默认 `yibooo`)、`OWNER_NICKNAME`(默认 `火星人`,A11y 搜索用)。 + +### 13.3 门禁判定(`handleMessage` 内,replyType 判定之后) + +``` +senderWxid = 群聊 ? content 前缀里的发送者 wxid : talker +isFromOwner = senderWxid==OWNER || talker==OWNER + +if isFromOwner: + parseApprovalCommand(content) 命中 → handleApproval(同意/拒绝),return + 否则放行(owner 普通聊天) +elif !whitelist.isAllowed(talker): + requestAuthorization(转 owner 请示),return ← 不回复发信人 +else: + generateAndSendReply(正常流程) +``` + +注意:**白名单 key 是 talker**(私聊=对方 wxid,群聊=chatroom id),即"授权这个会话"。 +群里只要 chatroom 在白名单,任何人 @ 都会自动回。owner 在任何群 @ 都放行(按 senderWxid 判断)。 + +### 13.4 授权交互 + +- **请示**:`requestAuthorization` 给 owner 发 + `【授权请求】X 给我发消息了:「...」回复「同意」我就自动回复 TA;回复「拒绝」忽略。` + 并记入 pending(同一 talker 不重复请示)。 +- **同意**:owner 回 `同意` / `同意 宜创AI全员群`(指定)→ 加白名单 + **自动回复 TA 刚才那条 pending 消息** + 给 owner 发确认。 +- **拒绝**:owner 回 `拒绝` / `忽略` → 移除该 pending。 +- 指令动词:`同意|授权|批准|approve|allow|yes` vs `拒绝|忽略|不同意|deny|no`。 + +### 13.5 `.whitelist.json` 格式 + +位于 `~/hermes-wechat/.whitelist.json`(持久化,重启不丢): + +```json +{ + "allowed": ["yibooo", "34576206251@chatroom"], + "pending": { + "wxid_g240cr5svks122": { + "senderName": "", + "content": "好的哈[社会社会]", + "displayName": "马锐模", + "ts": 1779684269591 + } + } +} +``` + +- `allowed`:已授权的 wxid(私聊)+ chatroom id(群)。owner wxid 启动时自动加入。 +- `pending`:待授权请求,key 是 talker,value 含 senderName / 原始 content / displayName / 时间戳。 +- 手动加白名单:直接编辑此文件加到 `allowed` 数组,重启 auto-reply 生效。 + +### 13.6 ⚠️ 引用回复授权的坑 + +owner 如果用微信**引用回复**发「同意」(而不是直接打字),content 不是纯文本 +「同意」,而是 **type=57 appmsg XML**: + +```xml +<msg><appmsg><title>同意57 + ...被引用的"【授权请求】宜创AI全员群..."原文... + +``` + +`parseApprovalCommand` 必须: +1. 检测 ``/`` → 从 `` 提取真正的回复文本("同意") +2. 从 `<refermsg>` 提取 `quotedContext` → 匹配是"同意哪条 pending"(引用了哪条授权请求) + +`handleApproval` 目标匹配优先级:显式 target → quotedContext 里出现的 pending +displayName → 最近一个 pending。 + +### 13.7 相关方法(auto-reply.js) + +| 方法 | 作用 | +|------|------| +| `WhitelistStore` | 持久化 allowed/pending | +| `parseApprovalCommand(content)` | 解析授权指令(含引用回复 XML 提取),返回 `{approve, target, quotedContext}` | +| `requestAuthorization(talker, senderName, content)` | 转 owner 请示 + 记 pending | +| `handleApproval(cmd)` | 同意→加白名单+回复 pending;拒绝→移除 | +| `generateAndSendReply({...})` | 抽出来的回复生成(白名单通过 / 授权后复用) | +| `sendPlainTo(toUser, displayName, text)` | 给 owner 发纯文本(不走 hermes) | + +--- + **END OF HANDOFF** — 改动这份文档时记得更新顶部生成时间。 -- libgit2 0.21.4