fix(bridge): 拦截泄漏成正文的畸形工具调用,不再当回复外发 - #795
Open
liuhaoyang wants to merge 3 commits into
Open
Conversation
长会话中模型偶尔把本应是结构化 tool_use 的内容写进普通 text block (畸形 <invoke …> 片段),该回合随后以 stop_reason=end_turn 结束且不含 tool_use。Claude transcript fallback 会把这段序列化工具调用当作正常 final 原样转发到飞书,用户看到的就是一串没执行的工具调用 XML。 新增保守检测 looksLikeLeakedToolCall():仅在成对高置信特征 (<invoke name=…> 配 <parameter name=…> 或 </invoke>)时命中,避免 误杀用户正常讨论/粘贴 XML、代码里的 invoke()、单独标签等。 仅作用于 transcript fallback 外发点(worker emitReadyTurns,非 adopt): 命中时改发明确失败提示(新增 i18n worker.leaked_tool_call)并记日志, 不静默吞。显式 botmux send 不经过此路径,不受影响;adopt 模式保持透传。 补 10 条单测(真实泄漏形态命中 + 误杀防御)。
… fix/bridge-leaked-tool-call-guard
- 文案(zh/en): trailingAssistantText 取「最后一次真实 tool_use 之后」的尾部, 命中泄漏时前面的真实工具调用可能已执行并产生副作用。原文案「本轮未真正执行/ 建议重发」会误导用户重发→重复副作用。改为提示「本轮可能只完成部分操作,先检查 当前状态再决定补做或重试」,不再无条件劝重发。 - looksLikeLeakedToolCall 改两阶段 O(n) 扫描: 先线性定位首个合法 opener,再只在其 后缀查 <parameter name> 子标签或 </invoke> 闭合。原惰性 [\s\S]*? 中缀在「大量 未闭合 <invoke name> 起点」时灾难回溯 O(n²)(5万起点 28s),两阶段扫描根除(1ms)。 命中语义与旧正则完全等价(交叉验证覆盖 multi-opener/前后位置/大小写/命名空间前缀)。 - 测试: +4 用例(terminator 在 opener 前不命中/后置 opener 闭合命中/多 opener 全不 闭合不命中/病态输入线性时间守卫<200ms);原 59 全绿=语义等价;两处变异各精确杀死 对应新测试证有牙。
deepcoldy
force-pushed
the
fix/bridge-leaked-tool-call-guard
branch
from
August 10, 2026 13:25
f6daaa5 to
49f4373
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
修复 #794:长会话中模型偶尔把本应是结构化
tool_use的内容写进普通 text block(畸形<invoke …>片段),该回合随后以stop_reason=end_turn结束且不含tool_use。Claude transcript fallback 会把这段序列化工具调用当作正常 final 原样转发到飞书,用户看到的是一串没被执行的工具调用 XML。本 PR 实现 #794 里的 P0 防泄漏,不动 P1(注入格式改造留待单独灰度)。
改动
src/services/bridge-fallback-gate.ts— 新增纯函数looksLikeLeakedToolCall(text),采用两阶段 O(n) 扫描。<invoke name="…">配<parameter name="…">子标签,或配</invoke>闭合标签。光秃<invoke>、prose 里提到 "invoke"、代码里的invoke(...)、单独的<parameter>都不命中。exec定位首个合法 opener,再只在其后缀里查<parameter name>子标签或</invoke>闭合。命中语义 = 「最早的 opener 之后(任意靠后位置)存在终止符」。不用惰性[\s\S]*?中缀正则——那种写法在「大量未闭合<invoke name>起点」的病态输入下会灾难性回溯(O(n²))。<invoke name="…">…</invoke>(用户在代码/日志/bug 报告里粘贴或讨论)也会命中——仅凭文本形态无法与真实泄漏区分。这是纯文本启发式的既定取舍;把调用点限制在 transcript-drain 兜底路径(绝不作用于显式botmux send)是实际安全的保证。src/worker.ts(emitReadyTurns) — 在 transcript fallback 外发点接入检测。worker.leaked_tool_call)并log。不静默吞,避免用户误判任务完成。botmux send不经过此路径,不受影响;adopt 模式保持逐字透传(botmux-unaware CLI,transcript drain 是其唯一通道,且可能合法输出此类内容)。src/i18n/zh.ts/en.ts— 新增worker.leaked_tool_call提示(中英双语)。trailingAssistantText取的是「本轮最后一次真实tool_use之后」的尾部文本,命中时前面的真实工具调用可能已经执行并产生副作用。因此文案说明「本轮可能只完成了部分操作,这段之前的工具调用可能已真实执行,请先检查当前状态,再决定补做剩余部分还是重试,避免重复执行」,不无条件建议重发。test/bridge-fallback-gate.test.ts— 新增单测覆盖:count前缀 +<invoke>+<parameter>、prose+block、仅开合标签、截断无闭合)。<invoke>元素讨论、光秃<invoke>、代码里的invoke()、单独<parameter>—— 均不命中。验证
pnpm exec tsc --noEmit:通过。bridge-fallback-gate单文件:65 tests 全绿。/introduce、Pi/Codex App 状态时序、群路由 setup 等与本 PR 无关的模块(同类时序失败在未合本 PR 的 master 上亦可复现)。本 PR 完整 diff 仅 5 个 bridge/i18n 文件。说明
Closes #794