合并受阻
变更摘要
本 PR 针对微信通道审批链路的全程非阻塞改造,修复一次事故中"同一 turn 并发多个工具调用导致会话永久卡死"的三个根因:并发 permission_request 被"已有挂起"守卫丢弃、sendPermission 决定不带 call_id 导致 daemon 对不上具体调用、以及 5 分钟超时默认拒绝与审批挂起语义矛盾且静默清空队列。改动核心是新增仓库内实现的本地审批队列 approval-queue.js(替代 file:/// 绝对路径本地依赖),让 bridge.js 的 onPermission 将并发审批并入同一条挂起记录、handleInbound 直接落决定或"跳过老操作"解锁,并扩展 atomcode-backend.js 的 sendPermission(decision, sessionId, callId) 支持按 call_id 回填,使对话在审批未决时也不会卡死。
主要改动
- 新增本地审批队列
approval-queue.js:审批记录模型支持多个call_id并入(createApproval创建、appendCallId追加),采用临时文件 +rename原子写,沿用~/.atomcode/weixin/approval的pending/decided目录,并提供list/status/decideCLI 供本机模型/用户操作。 - 并发审批并入同一条挂起记录:
bridge.js的onPermission在存在挂起审批时调用appendCallId把后续call_id并入现有记录并直接返回,不再丢弃第 2、3 条permission_request;审批通知中嵌入真实审批 id 与可复制的decide命令(approval-format.js支持多工具合并展示)。 - 扩展
sendPermission签名:atomcode-backend.js的sendPermission(decision, sessionId, callId)新增可选的sessionId/callId参数,请求体带上session_id和可选的call_id,旧调用形态保持不变、向后兼容。 - 审批决定按
call_id逐条回填且取消超时自动拒绝:bridge.js的watchApproval改为按审批 id 定向轮询pollDecided,取到决定后遍历hit.call_ids逐个调用sendPermission回填,不再 5 分钟超时自动拒绝。 handleInbound审批闸门改造:挂起审批期间,微信用户回复「同意/拒绝」直接调用decideApproval落决定并即时回话(不进 turn 队列);普通消息则触发"跳过老操作"(deny 落决定、回填复用 watcher 单一路径),避免审批未决时对话被卡死。


代码审查
我已完成全部 4 个变更文件的逐一审查。以下为审查结论与发现清单。
审查结论
各文件审查情况
plugins/weixin/adapter/src/approval-format.js — 3 项发现
- P2: 通知文案"5 分钟未回复自动拒绝"与本 PR 移除超时自动拒绝的行为矛盾(第 124 行)
- P2:
DECIDE命令硬编码机器专属绝对路径且指向被替换的旧approval.ts(第 3 行) - P2: 输出契约变更未同步测试,
approval-format.test.js现有断言必挂(第 111-128 行)
plugins/weixin/adapter/src/approval-queue.js — 2 项发现
- P3: CLI 入口自检对相对路径
argv[1]失效,文档用法node approval-queue.js list静默无效(第 170 行) - P3: 未使用的
dirname导入(第 22 行,可选清理)
plugins/weixin/adapter/src/bridge.js — 5 项发现
- P2: 审批记录
call_ids为空时 watcher 一条决定都不回填,daemon turn 永久卡死——在无call_id事件场景下原样复现本 PR 要修的故障(第 102-104 行) - P2: 旧审批的 watcher 无条件
clearPending,竞态下抹掉新审批的挂起状态,审批回复失效并可重新死锁(第 105 行) - P2:
notifyToast硬编码C:/Users/GO2se/.../toast.ps1且无 spawnerror监听(第 27-28 行) - P3:
pollDecided先删决定文件,回填失败时决定永久丢失、无重试(第 96-107 行) - P2: 审批异步化后未更新
bridge.test.js,现有审批用例多条断言失败且测试会写真实审批目录(第 142-150 行)
plugins/weixin/atomcode/src/atomcode-backend.js — 无问题。sendPermission 第三参数可选、向后兼容,现有 atomcode-backend.test.js 断言(decision、session_id 捕获)均不受影响。
按优先级统计
- P0: 0 项
- P1: 0 项
- P2: 7 项
- P3: 3 项
总体风险判断
本 PR 的修复方向正确(队列化 + 多 call_id 并入 + 按 id 定向回填 + 去超时),但存在多处可重新触发"会话卡死"的缺陷:最关键是 call_ids 为空时 watcher 不回填任何决定(旧 approval.ts/无 call_id 事件场景下 turn 依旧永久挂起)与 旧 watcher 无条件清 pending 可能抹掉新审批(竞态下审批回复失效、重新死锁);其次是用户可见文案与实际行为矛盾、硬编码机器专属路径带来的可移植性问题,以及未同步更新的既有测试会导致 CI 必挂。建议在合入前优先修复上述 P2 正确性问题并同步测试。
| 类型 | 数量 |
|---|---|
| 🔴 阻塞 | 1 |
| 🟡 建议 | 7 |
⛔ 需要修改


🔵 Low Priority
变更行:approval-queue.js:22 import { dirname } from 'node:path';。
受影响行为/契约:该模块内 dirname 从未被引用(路径构造全部用 join 与 fileURLToPath),属死导入。
失败模式:无运行时失败,但属于声而未用的死代码,后续阅读者会误以为存在目录定位逻辑;按仓库 JS 规范应清理。
建议:删除未使用的 dirname 导入,保持模块干净。


🟡 Medium Priority
变更行:approval-format.js 重写了 formatApproval 输出(第 111-128 行)及 actionLine(第 85-109 行)。
受影响行为/契约:新输出不再包含旧文案「回复 y 同意 | n 拒绝」,write_file/create_file 的提示从「写文件」改为「📄 新建文件」。
失败模式:仓库内未修改的 plugins/weixin/adapter/test/approval-format.test.js 有多条断言必然失败,导致 CI 红:
(第 17 行 …(共5000字)、第 36 行 /weird_tool/ 仍可通过。)
建议:同步更新 plugins/weixin/adapter/test/approval-format.test.js,将断言改为匹配新输出格式(「新建文件」、「回复「同意」= 允许」等);本次改动改动了对外输出契约,应一并提交测试更新。


🟡 Medium Priority
变更行:approval-format.js:124 新增文案「👉 直接回复:「同意」= 允许,「拒绝」= 拒绝(5 分钟未回复自动拒绝)」。
受影响行为/契约:本 PR 的核心变更之一就是去掉 5 分钟超时自动拒绝(approval-queue.js:10「无超时自动拒绝:审批未决就一直挂起」,bridge.js watchApproval 的轮询没有任何超时分支,PR 背景也明确说明删除该语义)。
失败模式:微信用户看到提示后,若 5 分钟内不回复,会以为操作已被自动拒绝、事情已了结;实际上决定永远不会产生,该 turn 一直挂起,直到用户再发一条普通消息才会被"跳过"解锁。这是一条直接面向用户的、与代码实际行为相反的错误指引,由本 diff 引入。
建议:将第 124 行文案改为与新行为一致,去掉"5 分钟未回复自动拒绝"的说法,改为「未决定将一直挂起,直接发任意消息可跳过」。若确实需要超时,应在 watchApproval 中实现超时拒绝并同步文案。
| 124
| - '👉 直接回复:「同意」= 允许,「拒绝」= 拒绝( |
|
124 | + '👉 直接回复:「同意」= 允许,「拒绝」= 拒绝(未决定将一直挂起,直接发消息可跳过)', |


🟡 Medium Priority
变更行:bridge.js:142 sessions.setPending(fromUserId, { pending: true, approvalId: rec.id })、第 146-150 行新通知与 watcher、第 177-188 行审批回复改为落队列由 watcher 异步回填。
受影响行为/契约:审批从"同步 sendPermission"改为"落队列 + 700ms 轮询异步回填",pending 结构从 {pending:true} 变为 {pending:true, approvalId}。
失败模式:仓库内未修改的 plugins/weixin/adapter/test/bridge.test.js 第 38-58 行的"审批"用例多条断言必然失败,CI 变红:
另外该用例的 onPermission 事件无 call_id,watcher 的 for 循环一条决定也不发(见 bridge.js:102 独立问题),即便等待也不会产生 'allow'。同时测试会真实写入 ~/.atomcode/weixin/approval 目录(污染开发机真实队列,若桥在运行可能被真实 watcher 取走),且 setInterval 会让测试进程多挂 700ms。
建议:本次改动改变了审批回复的处理时序与 pending 数据结构,必须同步更新 bridge.test.js 对应用例(用临时审批目录、缩短/可控轮询间隔、等待 watcher 回填后断言),否则该测试必挂并可能拖住测试进程。


🟠 High Priority
变更行:bridge.js watchApproval(94-108 行)中 pollDecided(approvalId) 在 approval-queue.js 119-124 行执行 rmSync(decidedPath(id))(拿走即删),随后才逐 call_id 调用 sendPermission;clearInterval(timer) 只在成功路径(98 行)执行,catch(107 行)只打日志。
影响行为/契约:决定文件的消费(删除)先于决定的投递,且失败路径不停止定时器。
失败模式:(1) 若 sendPermission 内 this.fetch 网络异常抛出,决定文件已被删除,决定永久丢失;catch 后定时器继续运行,而 pollDecided 已返回 null,if (!hit) return 使回调永久空转——700ms 一次的无用轮询永不停止(CPU 泄漏、定时器泄漏);(2) 异常发生在 sessions.clearPending(user)(105 行)之前,该用户的 pending 审批状态永远不清理,此后每条消息都被 handleInbound 的审批闸门拦截成"审批回复/跳过",用户无法正常对话;(3) 即便不抛异常,sendPermission 对非 2xx 只 console.error 不抛错(atomcode-backend.js 75 行),watcher 仍会 clearPending 并回"✅ 已允许"假成功,而 daemon 实际未收到决定,老 turn 不解锁,per-user 串行队列里后续 turn 全部排在永不结束的老 turn 之后,形成死锁。
建议:在 try/catch/finally 中确保任何路径都 clearInterval;将 pollDecided 的删除语义改为"发送成功后再删"或失败时把记录写回 decided 目录以便重试;失败时同样清理 pending 闸门,避免用户被永久锁在审批状态。


🔵 Low Priority
变更行:approval-queue.js 22 行 import { dirname } from 'node:path';。经全文检索,dirname 在文件中从未被使用(路径拼接全部走 join/fileURLToPath)。
影响行为/契约:无功能影响,属残留死代码。
建议:删除 import { dirname } from 'node:path'; 这一未使用导入。
| 22
| - import { |
|
22 | + import { join } from 'node:path'; |


🔵 Low Priority
变更行:approval-format.js 124 行 '👉 直接回复:「同意」= 允许,「拒绝」= 拒绝(5 分钟未回复自动拒绝)'。
影响行为/契约:本 PR 在 bridge.js 明确"去掉超时自动拒绝:审批未决就一直挂起,由用户回复或下一条普通消息触发跳过解锁",但发送给微信用户的通知文案仍承诺"5 分钟未回复自动拒绝"。
失败模式:用户看到文案后可能等待 5 分钟等自动拒绝,实际永远不会有;审批一直挂起,只有再发一条消息才会被"跳过"解锁。误导用户、与修复后的真实行为不一致,属于本 PR 改动引入的契约与文案矛盾。
建议:同步更新通知文案,去掉"5 分钟未回复自动拒绝",改为说明"未决定则持续等待,发新消息自动跳过"。
| 124
| - '👉 直接回复:「同意」= 允许,「拒绝」= 拒绝( |
|
124 | + '👉 直接回复:「同意」= 允许,「拒绝」= 拒绝(未决定会一直等待,发新消息可跳过)', |


🔵 Low Priority
变更行:approval-queue.js 为全新文件(172 行),实现 createApproval/appendCallId/decideApproval/pollDecided/atomicWrite 等核心逻辑;本 PR 未新增任何针对该模块的测试(变更清单仅 4 个 src 文件)。
影响行为/契约:该模块是本 PR 修复"并发审批丢弃/决定丢 call_id"的关键路径,包含并发并入、临时文件+rename 原子写、decided 取走即删、跨进程(CLI 与 bridge)共享目录等高风险语义;PR 描述声称"队列模块 13/13 冒烟全绿",但这些冒烟测试未随仓库提交,无法回归。
失败模式:上述并发并入、空 call_ids、decided 记录兼容旧 approval.ts(无 call_ids 字段)等边界/竞态问题(见本批次其它 finding)没有任何测试拦截,后续改动极易回归到 turn 卡死。属于关键路径缺失关键测试。
建议:新增 approval-queue.test.js,覆盖空 call_id、多 call_id 并入、decide/poll 生命周期、旧格式记录兼容等用例,并把 PR 描述中的"13/13 冒烟"纳入仓库测试。


变更摘要
本 PR 修复微信通道审批链路导致的会话卡死问题:同一 turn 内并发多个工具调用时,后续 permission_request 被"已有挂起"守卫丢弃、决定回填不带 call_id 且 5 分钟超时默认拒绝,导致 daemon 等不齐决定、turn 永久卡死。核心思路是让审批链路全程非阻塞:新增仓库内实现的 approval-queue.js 本地审批队列(替代 file:/// 绝对路径依赖),审批记录支持多个 call_id 并入、原子写入并提供 list/status/decide CLI;sendPermission 签名扩展为 (decision, sessionId, callId) 向后兼容;bridge.js 的 onPermission 将并发审批并入同一条挂起、watchApproval 去掉超时自动拒绝并在决定到达后按 call_id 逐个回填;handleInbound 配合新增的 phrase-gate.js 短语闸门,使审批挂起时用户回复「同意/拒绝」直接落决定,普通消息触发"跳过老操作"解锁,对话永不因审批卡死。同时新增「同意会话」工具白名单机制(sessions.js 的 getWhitelist/whitelist/isWhitelisted/clearWhitelist),并在 approval-format.js 中让审批通知携带真实 id、合并展示多工具信息。
主要改动
-
新增
approval-queue.js审批队列模块:审批记录支持同一 turn 并发多工具调用的多个call_id并入(createApproval/appendCallId),采用临时文件 +renameSync原子写,提供list/status/decideCLI,复用~/.atomcode/weixin/approval的pending/decided目录保持兼容。 -
sendPermission支持call_id回填:atomcode-backend.js中签名扩展为sendPermission(decision, sessionId, callId),请求体显式携带session_id并在有callId时附加call_id,第三参数可选、旧调用形态不变,修复决定对不上具体调用导致的 turn 卡死。 -
并发审批并入与无超时回填:
bridge.js的onPermission在已有挂起审批时通过appendCallId并入新call_id(不再丢弃后续permission_request);watchApproval移除超时自动拒绝,轮询pollDecided定向取走决定后按call_ids逐个调用sendPermission回填。 -
审批回复短语闸门与自动跳过:新增
phrase-gate.js,以归一化+整消息全等的锚点表(ANCHORS)判定allow/deny/allow_session/allow_batch/hold/pass,数字与单字符永不误判为授权;handleInbound中审批挂起时命中锚点直接decideApproval落决定,普通消息则自动以no落决定跳过老操作并清理挂起,配合 per-user 串行队列(enqueue)保证不并发不卡死。 -
审批通知与会话白名单:
approval-format.js重构为"人话优先"渲染,通知携带真实审批 id(可直接复制进 decide 命令)与模型意图(said)、多工具合并展示,尾行提示锚点与ANCHOR_HINTS同源;sessions.js新增会话级工具白名单存储,「同意会话」后同类工具免审(危险工具经isDangerousTool把关后入列),/cd切目录开新会话时clearWhitelist清空授权。


追加提交 abe2741:审批交互四模块
在根因修复(406d68a)之上,追加四个交互层模块:
1. phrase-gate.js 短语闸门(字节级确定性)
- 归一化(去标点/全角转半角/小写)后整消息全等查表,绝无子串误判("我不同意"≠同意);
- 数字与单字符(1/0/y,举手/测试惯用语)永不锚定,原样透传对话队列;
- 锚点必须 ≥2 汉字;危险工具(rm/taskkill/format 等)黑名单永不免审;
- 锚点表与通知尾行同源渲染(表即文档,用户每次都看到合法回复词)。
2. 溯源通知
审批通知升级为三要素:做什么(工具+参数)+为什么(模型审批前刚说的话)+怎么批(锚点行)。公理:每条消息自包含全部操作知识,用户默认失忆。
3. 思考心跳
iLink 无流式,思考期盲等——15s/40s/90s 三级心跳提示,首个 text 事件或 turn 结束即取消,零打断零 token。
4. 会话白名单
- 「同意会话」→同类工具本会话免审(危险工具除外,/cd 切会话自动清空);
- 「同意打包」→放行当前批次全部 call_id;
- 「先别/等等」→保持挂起转模型解释。
验证
- phrase-gate 31/31、approval-queue 13/13 沙箱冒烟全绿(含子串假阳性/数字透传/全角归一化反例);
- 全部桥源文件
node --check通过; - 桥重启加载实测:启动日志干净,运行中。
🤖 Generated with AtomCode


代码审查
审查结论
共报告 12 个问题:P1 × 1,P2 × 5,P3 × 6。
逐文件确认
- plugins/weixin/adapter/src/approval-format.js — 发现问题 3 个:输出契约变更未同步测试(P2)、"5 分钟未回复自动拒绝"文案与实现矛盾(P3)、DECIDE 硬编码用户绝对路径且指向已被替换的旧 approval.ts(P3)。
- plugins/weixin/adapter/src/approval-queue.js — 发现问题 3 个:pollDecided 先删后发送导致决定丢失(P1,锚定在 bridge 消费端)、call_ids 为空时零回填(P2,锚定在 bridge watcher)、未使用 dirname 导入(P3)、缺单元测试(P3)。
- plugins/weixin/adapter/src/bridge.js — 发现问题 6 个:hold 分支缺 return 导致「先别」被自动拒绝(P2)、call_ids 为空时 watcher 零次回填(P2)、pollDecided 先删后发送(P1)、bridge.test.js 未同步(P2)、watcher 定时器永不清理(P3)、/cd 无参分支未清空白名单(P3)。
- plugins/weixin/adapter/src/phrase-gate.js — 发现问题 1 个:isDangerousTool 正则过宽误伤且漏网
rd /s /q等 Windows 删除命令,白名单旁路存在安全缺口(P2)。 - plugins/weixin/adapter/src/sessions.js — 无问题(白名单存取逻辑与既有
_entry模式一致,新增方法均被 bridge 正确消费)。 - plugins/weixin/atomcode/src/atomcode-backend.js — 无问题(sendPermission 第三参数可选、body 构造向后兼容,
this.sessionId兜底链完整)。
总体风险判断
本 PR 修复方向正确(并发并入、call_id 回填、非阻塞审批、短语闸门),但存在两个足以复现"事故"级别的回归点:① watcher 在 call_ids 为空时零次回填,决定发不出、turn 重蹈卡死;② decided 记录先删后发送,任何一次回填失败即永久丢失决定并卡死串行队列。二者均落在线程最核心的审批回填路径上,建议合并前优先修复。其余为契约/测试未同步(CI 必红)、安全旁路正则缺口与文案/硬编码等中低危问题。整体风险:中等偏高,建议修复 P1/P2 后合入。
| 类型 | 数量 |
|---|---|
| 🔴 阻塞 | 0 |
| 🟡 建议 | 6 |
💬 仅评论


🔵 Low Priority
变更行:approval-format.js 第 3 行 const DECIDE = 'node C:/Users/GO2se/.atomcode/weixin/approval/approval.ts decide';bridge.js 第 24 行 toast 脚本 'C:/Users/GO2se/.atomcode/weixin/toast.ps1'。
受影响行为/契约:这两处把单个机器用户(GO2se)的绝对路径硬编码进发送给每个微信用户的审批通知和本地 toast 调用;且 DECIDE 指向的是旧版本地 approval.ts——本 PR 正是把它替换为仓库内 approval-queue.js。
失败模式:在其他机器/其他用户部署时,通知里的"本机备用"命令与 toast 脚本路径必然不存在,功能失效;新装环境没有旧 approval.ts,通知中的备用命令是不可用的假入口。
建议:DECIDE 命令改为从 import.meta.url 推导本文件路径(即 approval-queue.js),toast.ps1 路径改为配置/环境变量注入。


变更摘要
本 PR 修复微信通道审批链路的三个根因问题:并发审批被"已有挂起"守卫丢弃、决定不带 call_id 导致 daemon 对不上调用、超时默认拒绝与挂起语义矛盾。核心思路是让审批链路全程非阻塞——新增仓库内实现的 approval-queue.js 本地审批队列(支持多 call_id 并入与原子写),改造 bridge.js 的 onPermission/watchApproval/handleInbound 使并发审批合并、决定逐 call_id 回填、非审批消息自动"跳过老操作"解锁;同时扩展 atomcode-backend.js 的 sendPermission 签名、新增 phrase-gate.js 短语闸门与 sessions.js 会话级工具白名单,并升级 approval-format.js 审批通知展示。
主要改动
- 新增
approval-queue.js本地审批队列:审批记录支持多个call_id(并发工具调用并入同一条挂起审批,不再丢弃后续permission_request);采用临时文件 +rename原子写;提供list/status/decideCLI,沿用~/.atomcode/weixin/approval的 pending/decided 目录,与旧版approval.ts兼容。 bridge.js审批链路全程非阻塞:onPermission对已有挂起审批的并发事件用appendCallId并入而非丢弃,并新增白名单免审与notifyToast本地提醒;watchApproval移除超时自动拒绝,改为轮询pollDecided后按call_ids逐个回填sendPermission。handleInbound审批闸门与自动跳过:有挂起审批时,用户回复「同意/拒绝/同意会话/先别」经phrase-gate.js归一化+整消息全等匹配后直接落决定;普通消息(数字/单字符/表外内容)触发decideApproval(..., 'no')自动跳过卡死的老操作,对话永不因审批卡死。atomcode-backend.jssendPermission签名扩展:由(decision)扩展为(decision, sessionId, callId),session_id显式传入避免多用户并发串号,call_id可选(不带则 daemon 回退旧行为),保持向后兼容。- 新增
phrase-gate.js短语闸门与sessions.js白名单:matchGate提供字节级确定性匹配(锚点表ANCHORS与通知尾行ANCHOR_HINTS同源);「同意会话」将工具写入sessions.js新增的会话级白名单实现本会话免审(危险工具如rm/taskkill/format经isDangerousTool把关永不入列,/cd切会话时清空)。


追加提交 a6fbd32:代码审查修复
对 abe2741 做逐行审查(结合 PR 页自动审查报告核实),修复三处:
- hold 分支缺 return(真 bug):「先别/等等」分支发完提示后 fall-through 到下方"自动跳过"块,审批被 deny——与"保持挂起转模型解释"的意图完全相反。已补
return; - 空 call_ids 零回填(核实 PR 页审查 P2):事件不带 call_id 时 watcher 拿到决定也一个都不回填,turn 卡死。兜底改为发一次不带 call_id 的决定;
- watcher 竞态清 pending(核实 PR 页审查 P2):旧 watcher 无条件
clearPending会抹掉新审批的挂起状态。改为仅当挂起的仍是本审批时才清。
验证
- phrase-gate 31/31、approval-queue 13/13 复跑全绿;
node --check通过;桥重启加载实测运行中。
PR 页自动审查报告中的其余发现(approval-format 文案未同步、toast 硬编码路径、pollDecided 先删后发无重试、测试文件缺失)已记录,作为合并后 follow-up 处理。
🤖 Generated with AtomCode


变更摘要
本 PR 修复微信通道审批链路的三个根因:同一 turn 内并发 permission_request 被"已有挂起"守卫丢弃、sendPermission 决定不带 call_id 导致 daemon 对不上具体调用、5 分钟超时自动拒绝与审批挂起语义矛盾且静默清队列。核心改动是新增仓库内实现的本地审批队列 approval-queue.js(替代 file:/// 绝对路径依赖,支持多个 call_id 并入同一条审批),扩展 sendPermission 签名支持 call_id 定向回填,去除超时自动拒绝,并通过 phrase-gate.js 短语闸门让微信回复「同意/拒绝」直接落决定、普通消息自动跳过卡死的老操作,使审批链路全程非阻塞,会话不再因审批卡死。
主要改动
- 新增审批队列模块
approval-queue.js:审批记录支持多个call_id并入同一条挂起记录(appendCallId),采用临时文件 +rename原子写,提供list/status/decideCLI,沿用~/.atomcode/weixin/approval的 pending/decided 目录,不再有超时自动拒绝。 atomcode-backend.jssendPermission签名扩展:由(decision)扩展为(decision, sessionId, callId),callId可选、向后兼容;显式携带session_id,多用户并发不串号,daemon 可按call_id对号入座,修复决定对不上调用导致的 turn 卡死。bridge.js并发审批并入与定向回填:onPermission中同 turn 的后续permission_request通过appendCallId并入既有挂起而不丢弃;watchApproval轮询到决定后按记录里的全部call_ids逐个回填sendPermission,且只在挂起仍属于本审批时才清理状态,避免旧 watcher 抹掉新审批。handleInbound审批闸门与自动跳过策略:挂起审批时,回复经phrase-gate.js短语闸门(归一化 + 整消息全等,数字/单字符永不安锚)命中「同意/拒绝」直接落决定并立刻回话;普通消息则自动 deny 落决定跳过老操作,回填复用 watcher 单一路径,对话永不因审批卡死。sessions.js会话级工具白名单与危险工具把关:新增whitelist/isWhitelisted/clearWhitelist,「同意会话」可将同类工具加入本会话免审;phrase-gate.js的isDangerousTool黑名单(rm/taskkill/format 等)保证危险工具永不免审,/cd切会话时清空白名单。


代码审查
审查结论
逐文件核查结果(6/6):
- plugins/weixin/adapter/src/approval-format.js — 报告 3 项:输出契约变更破坏既有测试(P1)、「5 分钟自动拒绝」文案矛盾(P3)、硬编码家目录路径且 DECIDE 指向旧 approval.ts(P3)
- plugins/weixin/adapter/src/bridge.js — 报告 3 项:合并审批静默放行未见工具(P2)、watcher 回填中途失败致 turn 永久卡死(P2)、/cd 无参分支未清空白名单(P2)
- plugins/weixin/adapter/src/phrase-gate.js — 报告 1 项:口语词「好的/行/可以」被直接视为审批同意,可能放行危险工具(P2)
- plugins/weixin/adapter/src/approval-queue.js — 报告 2 项:未使用 dirname 导入(P3)、高危逻辑无单元测试(P3)
- plugins/weixin/adapter/src/sessions.js — 无问题(白名单存取实现正确;唯一分歧点在 bridge.js 调用侧,已在上文报告)
- plugins/weixin/atomcode/src/atomcode-backend.js — 无问题(sendPermission 第三参数可选,向后兼容,既有调用与测试均不受影响)
上一轮问题回查: hold 分支缺 return 与 call_ids 空数组零次回填两项已被本次 diff 修复;文案矛盾、硬编码路径、dirname 死导入、队列无测试四项仍存在,已在本次重新报告。
按优先级统计: P1 ×1(测试套件确定性失败,CI 阻塞)、P2 ×4(静默合并放行、回填失败无重试、白名单残留、口语词误授权)、P3 ×4(文案矛盾、硬编码路径、死导入、缺测试)。
总体风险判断: 本次改动的核心修复方向(并发审批并入、按 call_id 回填、审批回复不经过 turn 队列)逻辑自洽,正常路径可解原事故;但存在两处会重新引入"turn 卡死"的高风险路径(watcher 回填失败无重试、通知抛错后审批无 watcher),以及静默合并放行与口语词误授权两个授权边界安全缺口;同时既有测试套件被确定性地破坏而未同步更新,合入前应优先修复 P1/P2 项并补跑全部测试。
| 类型 | 数量 |
|---|---|
| 🔴 阻塞 | 1 |
| 🟡 建议 | 4 |
⛔ 需要修改


🔵 Low Priority
建议:将注释中的「见 clearAll」改为「见 clearWhitelist」,与实际方法名保持一致。
| 26
| - // 由调用方在入列前把关,这里只做存储。/cd 切目录开新会话时应一并清空(见 clear |
|
26 | + // 由调用方在入列前把关,这里只做存储。/cd 切目录开新会话时应一并清空(见 clearWhitelist)。 |


代码审查
审查结论
共报告 11 个问题:P2 × 3,P3 × 8,无 P0/P1。整体判断:本 PR 的核心思路(审批落本地队列、并发并入、按 call_id 回填、去掉超时自动拒绝、普通消息自动跳过解锁)方向正确,上一轮报告的"hold 缺 return"与"call_ids 为空零次回填"两个问题已在本次修复;但仍存在若干可能重新触发"turn 卡死/决定丢失"的路径,以及多处与自身注释/文案矛盾的实现,建议合入前处理 P2 三项。
各文件审查情况
- plugins/weixin/adapter/src/approval-format.js:发现问题 2 个——审批通知文案仍称"5 分钟未回复自动拒绝"(P3,与已移除的超时矛盾);DECIDE 命令硬编码机器特定绝对路径且指向已被替换的旧 approval.ts(P3)。其余格式化/截断逻辑无实质缺陷。
- plugins/weixin/adapter/src/approval-queue.js:发现问题 2 个——未使用的
dirname导入(P3,死代码);并发并入/原子写/跨进程读写等高危逻辑无任何单元测试(P3)。其pollDecided 先删后发的设计缺陷与 bridge.js 的回填丢失问题合并报告于 bridge.js。 - plugins/weixin/adapter/src/bridge.js:发现问题 6 个——决定在回填成功前被删除、发送失败时决定永久丢失致 turn 重新卡死(P2);
this.sessionId跨用户共享兜底在记录缺 sessionId 时把决定发到别的用户会话(P2);hold 分支后下一条普通消息会静默 deny 挂起审批、「说同意继续」无法兑现(P2);toast spawn 缺 'error' 监听致进程崩溃风险(P3);多个无 call_id 请求合并后只回填 1 次(P3);嵌套三元违反规范(P3);「同意会话」仅首个工具入白名单(P3)。 - plugins/weixin/adapter/src/phrase-gate.js:无问题(归一化+整消息全等的判定逻辑、危险工具黑名单审查通过,数字/单字符守卫有效)。
- plugins/weixin/adapter/src/sessions.js:无问题(白名单存取与 /cd 清空逻辑一致)。
- plugins/weixin/atomcode/src/atomcode-backend.js:发现问题 1 个(与 bridge.js:101 合并报告的跨用户
this.sessionId兜底,根因在 sendPermission 第 69 行)。
风险判断
P2 三项(决定丢失、会话串号、hold 语义矛盾)均与本 PR 修复的"turn 卡死"事故直接相关,属于修复不完整或引入的变体,建议优先处理;其余 P3 项为文案/可移植性/规范/测试缺口,可随迭代修复。
| 类型 | 数量 |
|---|---|
| 🔴 阻塞 | 0 |
| 🟡 建议 | 3 |
💬 仅评论


追加提交 426e376:思维链摘要推送
SSE 抓流实测:daemon 已流式转发思考模型的 reasoning delta(一次 turn 28 条,如 qwen3.8-27b 的思考原文),桥此前直接丢弃——本提交打通最后一环,纯桥侧实现、零 daemon 改动。
实现
atomcode-backend.js:dispatch补reasoning分支,透传给onReasoning回调;bridge.js里程碑式聚合:delta攒缓冲,每约 220 字切一块,思考期最多推 3 条(💭 前缀,绝不逐 delta 倾倒);- 与心跳协同:首条摘要替代 15s 心跳,40s/90s 心跳保留兜底;首个 text 事件即停;
- 每条摘要自带锚点行(有挂起审批时),审批通知优先级不受影响。
终态消息序列
0s 🤔 已收到
~15s 💭 正在分析:「xxx」(+锚点行,如有挂起审批)
~40s 💭 下一段思路…
?s 🔐 审批通知(独占,如有)
终答 整段回复
验证
node --check双文件通过;dispatch reasoning 透传冒烟 OK;- 桥重启加载实测运行中。
可回退性
本提交独立成 commit,git revert 426e376 即可单独撤除思维链功能,不影响前序修复。
🤖 Generated with AtomCode


变更摘要
本 PR 修复微信通道审批链路的三类事故根因(并发审批被"已有挂起"守卫丢弃、sendPermission 决定不带 call_id、超时自动拒绝与挂起语义矛盾导致 turn 永久卡死),将审批链路改为全程非阻塞:新增仓库内实现、支持多 call_id 并入与原子写的本地审批队列模块 approval-queue.js(替代 file:/// 绝对路径依赖);sendPermission 签名扩展为 (decision, sessionId, callId) 并向后兼容;bridge.js 并发审批并入同一条挂起记录、决定到达后按 call_id 逐个回填、去掉超时自动拒绝;审批回复经新增的短语闸门 phrase-gate.js 直接落决定,普通消息自动跳过挂起的老操作,并在 sessions.js 增加会话级工具白名单(「同意会话」免审、危险工具永不免审),同时 atomcode-backend.js 新增 reasoning 思维链事件转发。
主要改动
- 新增审批队列模块
approval-queue.js:审批记录支持多个call_id并入(appendCallId),原子写(临时文件 +renameSync),沿用~/.atomcode/weixin/approval的pending/decided目录并移除超时自动拒绝,提供list/status/decideCLI,取代file:///绝对路径的本地依赖。 atomcode-backend.js的sendPermission签名扩展为(decision, sessionId, callId):决定体新增call_id字段,使 daemon 能对号入座具体调用;sessionId显式传入避免多用户并发串号,第三参数可选保持旧调用形态兼容。bridge.js的onPermission并发审批并入:同一 turn 后续permission_request通过appendCallId并入现有挂起审批(不再丢弃),并新增会话白名单免审逻辑(isDangerousTool判定的危险工具永不自动放行)。bridge.js的watchApproval与handleInbound解除卡死:watchApproval按 id 定向轮询决定并逐call_id回填sendPermission;handleInbound在审批挂起时按phrase-gate.js闸门结果直接落决定(「同意/拒绝/同意会话/先别」),非审批消息自动 deny 跳过老操作并交由 watcher 单一路径回填,对话不再因审批卡死。- 新增
phrase-gate.js短语闸门与sessions.js会话白名单:归一化 + 整消息全等判定(数字/单字符/表外内容透传不误判授权),锚点表与通知提示同源;「同意会话」将同类工具记入sessions.js的会话级工具白名单,/cd切会话时通过clearWhitelist清空,危险工具永不免审。


追加提交 426e376:思维链摘要推送
SSE 抓流实测:daemon 已流式转发思考模型的 reasoning delta(一次 turn 28 条,如 qwen3.8-27b 的思考原文),桥此前直接丢弃——本提交打通最后一环,纯桥侧实现、零 daemon 改动。
实现
atomcode-backend.js:dispatch补reasoning分支,透传给onReasoning回调;bridge.js里程碑式聚合:delta攒缓冲,每约 220 字切一块,思考期最多推 3 条(💭 前缀,绝不逐 delta 倾倒);- 与心跳协同:首条摘要替代 15s 心跳,40s/90s 心跳保留兜底;首个 text 事件即停;
- 每条摘要自带锚点行(有挂起审批时),审批通知优先级不受影响。
验证
node --check双文件通过;dispatch reasoning 透传冒烟 OK;桥重启加载实测运行中。
可回退性
本提交独立成 commit,git revert 426e376 可单独撤除思维链功能,不影响前序修复。
🤖 Generated with AtomCode


追加提交 426e376:思维链摘要推送
SSE 抓流实测:daemon 已流式转发思考模型的 reasoning delta(一次 turn 28 条),桥此前直接丢弃——本提交打通最后一环,纯桥侧实现、零 daemon 改动。
- atomcode-backend.js: dispatch 补 reasoning 分支,透传 onReasoning 回调;
- bridge.js: 里程碑式聚合,每约 220 字切一块,思考期最多 3 条(💭);首条摘要替代 15s 心跳,40s/90s 兜底;首个 text 事件即停;摘要自带锚点行(有挂起审批时)。
验证:node --check 通过;dispatch reasoning 透传冒烟 OK;桥重启实测运行中。可回退:git revert 426e376 可单独撤除本功能。
🤖 Generated with AtomCode


追加提交 426e376:思维链摘要推送。SSE 实测 daemon 已流式转发 reasoning delta,桥补 onReasoning 透传 + 里程碑式聚合(约 220 字/块,思考期最多 3 条,首条替代 15s 心跳,text 即停,自带锚点行)。node --check 通过,透传冒烟 OK,桥重启实测运行中。可单独 revert。


代码审查
审查结论
各文件审查确认
- plugins/weixin/adapter/src/approval-format.js — 已审查:输出格式重写导致既有测试必然失败(P2);「5 分钟自动拒绝」文案与实现矛盾(P3);DECIDE 硬编码机器路径且指向旧 approval.ts(P3)。
- plugins/weixin/adapter/src/approval-queue.js — 已审查:队列模块本身逻辑(原子写、多 call_id 并入、CLI)未见新问题;遗留未使用的 dirname 导入(P3)。
- plugins/weixin/adapter/src/bridge.js — 已审查:notifyToast 未监听 spawn error 事件导致非 Windows 环境崩溃(P2);审批决定先消费后发送、失败无重试会复现 turn 死锁(P2);非锚点肯定答复被自动 deny(P2);/cd 无参分支未清空白名单(P2);hold 分支消息丢弃与语义冲突(P3);watcher 定时器泄漏(P3)。
- plugins/weixin/adapter/src/phrase-gate.js — 已审查:锚点全等与归一化逻辑本身正确,但"表外→pass→auto-skip deny"的策略在 bridge.js 层造成肯定答复误拒(见 bridge.js P2 条目);本文件无独立新问题。
- plugins/weixin/adapter/src/sessions.js — 已审查:no issues(白名单存取逻辑正确;其与 /cd 的不一致在 bridge.js 有参/无参分支,已归入 bridge.js 的 P2 条目)。
- plugins/weixin/atomcode/src/atomcode-backend.js — 已审查:no issues(sendPermission 第三参数可选、向后兼容,reasoning 事件转发有可选链保护,既有 atomcode-backend 测试兼容)。
按优先级统计
- P0:0
- P1:0
- P2:5(bridge.js ×4、approval-format.js ×1)
- P3:5(回查遗留 3 项 + 新增 2 项)
总体风险判断
本次改动的核心目标(并发审批并入、call_id 回填、非阻塞队列)方向正确,并发审批不再被丢弃、决定带 call_id 回填这两条主线修复是成立的。但改动仍存在实质性风险:①审批决定"先消费后发送、无重试"的路径在 daemon 短暂不可用时会让决定永久丢失,恰可复现本 PR 声称修复的 turn 死锁;②"非锚点消息自动 deny"策略会把高频的「好/嗯/ok/同意吧」等肯定答复误判为拒绝,属于高概率的用户意图误执行;③notifyToast 的 spawn error 事件未处理,在非 Windows 环境(含 CI)首个审批到达即进程崩溃;④formatApproval 输出重写未同步测试,CI 必然挂 4 个用例。整体判定:功能修复方向正确但可靠性/回归防护不足,建议合并前先修复 P2 各项(尤其决定消费时序与 spawn 崩溃),并同步测试与文案。
| 类型 | 数量 |
|---|---|
| 🔴 阻塞 | 0 |
| 🟡 建议 | 6 |
💬 仅评论


🟡 Medium Priority
建议:把"非锚点消息自动 deny"收敛为"确属新对话消息才跳过":可在 phrase-gate 增加近义肯定/否定集合,或将 pass 消息改为仅当包含明确否定词时才 deny,否则保持挂起并回复锚点提示。


🟡 Medium Priority
建议:在 handleControlCommand 的无参 /cd 分支 sessions.setSessionId(user, undefined) 之后补上 sessions.clearWhitelist(user);,与有参分支保持一致。


背景
2026-09-15 12:48 微信通道事故:一个 turn 内并发 3 个 bash 工具调用,daemon 连发 3 条 permission_request,桥只注册了 1 个审批且回填不带 call_id,turn 永久卡死,该用户后续所有消息排队无响应。
三个根因
permission_request被"已有挂起"守卫直接丢掉,daemon 等满员决定才放行 → turn 卡死;sendPermission只发{session_id, decision},daemon 对不上具体调用,兜底"拒绝"也解不开锁;修复
approval-queue.js(仓库内实现,替代file:///绝对路径的本地依赖):审批记录支持多个call_id并入;原子写(临时文件 + rename);提供list/status/decideCLI;沿用~/.atomcode/weixin/approval的 pending/decided 目录,本机既有 CLI 不受影响;bridge.jsonPermission:并发审批并入同一条挂起(不丢弃后续事件);atomcode-backend.jssendPermission:签名扩展为(decision, sessionId, callId),可选参数向后兼容;bridge.jswatchApproval:去掉超时自动拒绝;决定到达后逐call_id回填;bridge.jshandleInbound:审批挂起时,用户回复「同意/拒绝」直接落决定;普通消息触发"跳过老操作"(deny 落决定,回填复用 watcher 单一路径),对话永不因审批卡死;approval-format.js:审批通知带真实 id(可直接复制进 decide 命令)、多工具合并展示。兼容性
sendPermission第三参数可选,旧调用形态不变;approval.ts相同,CLI 用法一致;node --check通过,队列模块 13/13 冒烟全绿,桥重启加载新代码运行正常。🤖 Generated with AtomCode