fix(ui,loop): salvage 成功提示降噪——黄色警告降为灰色 info 行(bump 0.58.50)

用户反馈流内黄字「工具调用参数损坏但已就地抢救」吓人、疑影响后续对话。
核实零影响:salvage 只就地改写 arguments、当轮照常执行,对话历史与正常轮
无异;warn 是纯前端 SSE 展示事件,不入 messages、不回灌 LLM,刷新即消失。

处置取分级降噪而非全静默(上游返脏数据的事实要可见、可与 tool_salvaged
DB 留痕对上):
- core/loop.py:该 emit 加 level:"info",措辞软化为「已自动修复工具调用
  参数…继续执行」;
- web/static/js/chat.js:warn 渲染按 level 分流,info 走灰色 muted 行、
  不带 ⚠;黄色 ⚠ 只留给真打断本轮的路径(丢弃重试/熔断等)。

频发原因研究背书(DB 近7天,bad/chat 轮次):deepseek_v4.pro 14.5%、
flash 8.3%、unifyllm 5.9~7%,glm/sonnet 0 —— DeepSeek wire + unifyllm
网关两链路的流式 delta 乱序病,前缀实为同一调用自身内容的错位碎片
(与 0.58.24 定层一致);社区 opencode/goose/Cline/Roo-Code 同病且多数
closed-not-planned 未修,我方 salvage(85% 救回)+ 非流式重试已是最完整
处置 —— 频繁黄字本质是「救回成功」记录高频刷屏,降灰合理。

顺带:CLAUDE.md 文档节奏调整(bump/PROGRESS/CHANGELOG 改为一次 push
统一更新一次,不逐 commit;DESIGN 仍随触发它的 commit 走)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-07-21 08:47:44 +08:00
parent f535dbaa9a
commit 62f3ddb658
6 changed files with 23 additions and 10 deletions

View File

@ -5,6 +5,10 @@
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。 > 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
> 工程口径的完整记录见 `PROGRESS.md` / git log。 > 工程口径的完整记录见 `PROGRESS.md` / git log。
## 0.58.50 — 2026-07-21
- 对话过程中偶尔出现的黄色警告「工具调用参数损坏但已就地抢救」改为低调的灰色提示,措辞也更平和(「已自动修复工具调用参数」)。这种情况系统会自动修复,对回答质量和后续对话没有任何影响;黄色警告今后只用于真正需要留意的中断重试类情况。
## 0.58.43 — 2026-07-19 ## 0.58.43 — 2026-07-19
- 优化文件预览的观感:打开预览时整个页面背景统一压暗(半透明,背景内容仍隐约可辨、不挡死),聚焦到预览内容,不再出现"上半屏变暗、底部输入区一条通亮"的割裂感;底部输入框依然悬浮可用,可以边看文件边打字。 - 优化文件预览的观感:打开预览时整个页面背景统一压暗(半透明,背景内容仍隐约可辨、不挡死),聚焦到预览内容,不再出现"上半屏变暗、底部输入区一条通亮"的割裂感;底部输入框依然悬浮可用,可以边看文件边打字。

View File

@ -45,12 +45,14 @@ PowerShell here-string `@'...'@` **只在 PowerShell 工具里有效**;用 Bash
## 文档维护 ## 文档维护
每完成一步实现(commit 前),**必须更新 `PROGRESS.md`**: **节奏:版本号 / CHANGELOG / PROGRESS 在 push 前统一更新一次,不用每次 commit 都动;DESIGN.md 例外 —— 某次 commit 触发了架构 / 取舍变化就随该 commit 一起改(见下)。**
- "已完成关键能力" 段加一条 `YYYY-MM-DD / <短标题>:<改了什么>`
**push 前必须更新 `PROGRESS.md`**(覆盖本次 push 包含的所有 commit):
- "已完成关键能力" 段加条目 `YYYY-MM-DD / <短标题>:<改了什么>`
- 状态表(§7 B Step 几 / Phase 几)若变化跟着改 - 状态表(§7 B Step 几 / Phase 几)若变化跟着改
- 文件清单若新增 / 删除模块跟着改 - 文件清单若新增 / 删除模块跟着改
**每次 commit / push 必须 bump 版本号** —— 单一事实源是 `core/__init__.py``__version__`(web/app.py 的 FastAPI version、`/healthz` 返回、前端左栏底部展示都引这里,改版本只动这一行): **push 必须 bump 版本号(一次 push 一 bump,不按 commit 数)** —— 单一事实源是 `core/__init__.py``__version__`(web/app.py 的 FastAPI version、`/healthz` 返回、前端左栏底部展示都引这里,改版本只动这一行):
- patch(`0.8.x`):bug 修复 / 重构 / 调参 / 新加 skill / 样式 - patch(`0.8.x`):bug 修复 / 重构 / 调参 / 新加 skill / 样式
- minor(`0.x.0`):成批新功能 / 明显的对外行为变化 - minor(`0.x.0`):成批新功能 / 明显的对外行为变化
- major(`x.0.0`):1.0 正式发版 / 不兼容大重构 - major(`x.0.0`):1.0 正式发版 / 不兼容大重构
@ -58,7 +60,7 @@ PowerShell here-string `@'...'@` **只在 PowerShell 工具里有效**;用 Bash
**bump 版本时若改动用户可感知 → 顺手在 `CHANGELOG.md` 顶部加条目**(格式 `## <版本> — <日期>` + bullet):口径面向用户("能干什么了"而不是"改了哪个模块"),纯内部重构 / 修复不记——不是每个版本都要有条目。这份由 `GET /v1/changelog` 解析、前端点版本号展示,别写内部模块名 / env 变量 / 部署细节。 **bump 版本时若改动用户可感知 → 顺手在 `CHANGELOG.md` 顶部加条目**(格式 `## <版本> — <日期>` + bullet):口径面向用户("能干什么了"而不是"改了哪个模块"),纯内部重构 / 修复不记——不是每个版本都要有条目。这份由 `GET /v1/changelog` 解析、前端点版本号展示,别写内部模块名 / env 变量 / 部署细节。
**只有以下情况才动 `DESIGN.md`**(避免把工程笔记沉淀成设计): **只有以下情况才动 `DESIGN.md`**(避免把工程笔记沉淀成设计)。DESIGN 不跟 push 节奏 —— 哪个 commit 触发了下列变化,就随那个 commit 一起改:
- 架构 / 心智模型变化(如 §7.1 task-primary 重写) - 架构 / 心智模型变化(如 §7.1 task-primary 重写)
- 取舍决策推翻或新增(§5 / §7.9 类内容) - 取舍决策推翻或新增(§5 / §7.9 类内容)
- API / schema 字段语义变化(§7.2 / §7.4) - API / schema 字段语义变化(§7.2 / §7.4)

View File

@ -23,6 +23,7 @@
### 2026-07 ### 2026-07
- **07-21 / 0.58.50 / salvage 成功提示降噪(黄→灰)**:用户反馈"工具调用参数损坏但已就地抢救"黄字吓人、问是否影响后续对话——核实**零影响**(salvage 只就地改写 arguments,当轮照常执行,对话历史与正常轮无异;warn 是纯前端 SSE 展示事件,不入 messages 不回灌 LLM,刷新即消失)。处置:不全静默(上游返脏数据的事实要可见、可与 `tool_salvaged` DB 留痕对上),改**分级降噪**——`loop.py` 该 emit 加 `level:"info"` + 措辞软化「已自动修复工具调用参数…继续执行」;`chat.js` warn 渲染按 level 分流,info 走灰色 muted 无 ⚠,黄色 ⚠ 只留给真打断本轮的路径(丢弃重试/熔断等)。
- **07-21 / 0.58.49 / glm.pro52 空响应治本(禁 thinking + loop 区分截断)**:探针定层坐实根因链——①`max_output` 是**全仓死字段**(只在 capabilities.py 定义,`_build_kwargs` 零引用,`max_tokens` 从不发)→ 网关放任 glm-5.2 跑到自带 65536 输出上限;②glm-5.2 thinking **网关侧默认开**(线上探针实测 reasoning_content=766>0,尽管 config thinking_mode:false —— 那开关是 glm.yaml 未做的 TODO,根本没传);③重任务(100k 上下文)上思考膨胀烧穿 65536 被截断(finish_reason=length)、content 空 → loop 判空响应整轮丢弃 + **同上下文无效重试**(task 35744bea:5 次 empty 全 tokens_out=65536,事件4=attempt2 铁证);后果单任务烧 ~327k 输出 token。与 opus48/deepseek 网关 wire bug 不同根。**修**:`core/llm.py._build_kwargs` 加 family=="glm" 分支,据 thinking_mode 透传 `extra_body={"thinking":{"type":"enabled|disabled"}}`(GLM 协议,与 reasoning_effort 不同族)——线上探针 A/B 实测 disabled 后 reasoning_content 766→0、正文照常;`core/loop.py` 加 `_finish_reason`,空响应路径区分 length(截断,我方预算烧穿,重试无效)与 wire 吐空,warn 措辞据实 + `record_empty_response` units 记 finish_reason(JSON 免 migration,便于面板区分性质)。修好既坏的 test_loop_empty_response(mock 缺 executor)+ 补截断措辞用例,61 测试全绿。探针 `scripts/diag_glm_empty_probe.py` 留仓(monkeypatch _resolve_executor 绕沙箱池、PROBE_THINKING_OFF A/B 开关)。**遗留**:max_output 对所有模型仍未生效(本次只治 glm thinking 根因,没盲发 max_tokens——怕截断大 write 的 args);要不要让 max_output 全局生效需单独评估各档安全值。 - **07-21 / 0.58.49 / glm.pro52 空响应治本(禁 thinking + loop 区分截断)**:探针定层坐实根因链——①`max_output` 是**全仓死字段**(只在 capabilities.py 定义,`_build_kwargs` 零引用,`max_tokens` 从不发)→ 网关放任 glm-5.2 跑到自带 65536 输出上限;②glm-5.2 thinking **网关侧默认开**(线上探针实测 reasoning_content=766>0,尽管 config thinking_mode:false —— 那开关是 glm.yaml 未做的 TODO,根本没传);③重任务(100k 上下文)上思考膨胀烧穿 65536 被截断(finish_reason=length)、content 空 → loop 判空响应整轮丢弃 + **同上下文无效重试**(task 35744bea:5 次 empty 全 tokens_out=65536,事件4=attempt2 铁证);后果单任务烧 ~327k 输出 token。与 opus48/deepseek 网关 wire bug 不同根。**修**:`core/llm.py._build_kwargs` 加 family=="glm" 分支,据 thinking_mode 透传 `extra_body={"thinking":{"type":"enabled|disabled"}}`(GLM 协议,与 reasoning_effort 不同族)——线上探针 A/B 实测 disabled 后 reasoning_content 766→0、正文照常;`core/loop.py` 加 `_finish_reason`,空响应路径区分 length(截断,我方预算烧穿,重试无效)与 wire 吐空,warn 措辞据实 + `record_empty_response` units 记 finish_reason(JSON 免 migration,便于面板区分性质)。修好既坏的 test_loop_empty_response(mock 缺 executor)+ 补截断措辞用例,61 测试全绿。探针 `scripts/diag_glm_empty_probe.py` 留仓(monkeypatch _resolve_executor 绕沙箱池、PROBE_THINKING_OFF A/B 开关)。**遗留**:max_output 对所有模型仍未生效(本次只治 glm thinking 根因,没盲发 max_tokens——怕截断大 write 的 args);要不要让 max_output 全局生效需单独评估各档安全值。
- **07-20 / 0.58.48 / glm.pro52 空响应定层探针**:失败面板 empty 簇(15)主角从 opus48 换成 `glm.pro52`(11/15)。加 `scripts/diag_glm_empty_probe.py`——`build_agent(resume=True)` 复原真实 system prompt + 全工具 schema + **完整多轮上下文**(空响应发生在深层,单条 user 消息复现不了,这是与 2a1bc25d narrated 探针的关键差别),打 glm.pro52,三态判定 EMPTY(真吐空)/NARRATED(工具意图漏成正文)/STRUCTURED。默认锁 task 35744bea(近7天 empty 5 次最可复现)。本机 import litellm 卡 ~20min,交生产机 `.venv/Scripts/python.exe scripts/diag_glm_empty_probe.py` 跑,输出写 `scripts/_glm_empty_probe_*.txt`。定层后再决处置。 - **07-20 / 0.58.48 / glm.pro52 空响应定层探针**:失败面板 empty 簇(15)主角从 opus48 换成 `glm.pro52`(11/15)。加 `scripts/diag_glm_empty_probe.py`——`build_agent(resume=True)` 复原真实 system prompt + 全工具 schema + **完整多轮上下文**(空响应发生在深层,单条 user 消息复现不了,这是与 2a1bc25d narrated 探针的关键差别),打 glm.pro52,三态判定 EMPTY(真吐空)/NARRATED(工具意图漏成正文)/STRUCTURED。默认锁 task 35744bea(近7天 empty 5 次最可复现)。本机 import litellm 卡 ~20min,交生产机 `.venv/Scripts/python.exe scripts/diag_glm_empty_probe.py` 跑,输出写 `scripts/_glm_empty_probe_*.txt`。定层后再决处置。
- **07-20 / 0.58.47 / 工具失败面板 6 簇巡检 + svg 质量门签名诚实化**:扫近7天失败聚集逐簇定根因。①**write/edit malformed(45/32)**=100% DeepSeek v4 wire 把并行 tool_call 的 arguments 拼成 `{...}{...}`,salvage(0.58.24)复盘提前到 07-20:命中率稳 ~85%(post-launch salvaged 252/malformed 44)、残差涉事 task 全在跑无终态失败=落非流式重试自愈,**决策保持现状不再写代码**(面板绝对数被上线前 07-13/14 尾巴撑高,要看 salvaged/malformed 比)。②**shell/exit "Font issues"(20)**=`svg_quality_checker` 质量门按设计 exit-1,但 `toolfail._classify``[exit]` 前尾行当签名,而尾行永远是通用 tip「4. Font issues:…」→ 所有门失败(typography/alignment/spec_lock drift)误标成 Font issues 误导排查。修:checker 在 errors>0 时末尾多打确定性 `gate_verdict_line()`「[GATE FAIL] svg_quality_checker: N error file(s) / M total; top issues: …」作为最后一行 stdout(放 main() 里 export 之后),toolfail 现有取尾行逻辑自动拾取诚实签名、toolfail 零改动;`_categorize_issue` 加 `spec_lock/typography` 类避免真错落 "Other"。补 3 测试(GateVerdictLineTests),39+3 全绿。③**run_python 预检(31)**=pysyntax 按设计拦中文正文硬拼进 .py(引号/缩进崩),非平台 bug=提示词治理待办。④**empty(15)**=主角换成 `glm.pro52`(11/15,非旧 opus48 案),需生产跑 narrated 探针定层(本机 import litellm 卡,task e7c2f50b 备用)。⑤**web_fetch(6)**=外部 403 拒抓为主,非平台锅。 - **07-20 / 0.58.47 / 工具失败面板 6 簇巡检 + svg 质量门签名诚实化**:扫近7天失败聚集逐簇定根因。①**write/edit malformed(45/32)**=100% DeepSeek v4 wire 把并行 tool_call 的 arguments 拼成 `{...}{...}`,salvage(0.58.24)复盘提前到 07-20:命中率稳 ~85%(post-launch salvaged 252/malformed 44)、残差涉事 task 全在跑无终态失败=落非流式重试自愈,**决策保持现状不再写代码**(面板绝对数被上线前 07-13/14 尾巴撑高,要看 salvaged/malformed 比)。②**shell/exit "Font issues"(20)**=`svg_quality_checker` 质量门按设计 exit-1,但 `toolfail._classify``[exit]` 前尾行当签名,而尾行永远是通用 tip「4. Font issues:…」→ 所有门失败(typography/alignment/spec_lock drift)误标成 Font issues 误导排查。修:checker 在 errors>0 时末尾多打确定性 `gate_verdict_line()`「[GATE FAIL] svg_quality_checker: N error file(s) / M total; top issues: …」作为最后一行 stdout(放 main() 里 export 之后),toolfail 现有取尾行逻辑自动拾取诚实签名、toolfail 零改动;`_categorize_issue` 加 `spec_lock/typography` 类避免真错落 "Other"。补 3 测试(GateVerdictLineTests),39+3 全绿。③**run_python 预检(31)**=pysyntax 按设计拦中文正文硬拼进 .py(引号/缩进崩),非平台 bug=提示词治理待办。④**empty(15)**=主角换成 `glm.pro52`(11/15,非旧 opus48 案),需生产跑 narrated 探针定层(本机 import litellm 卡,task e7c2f50b 备用)。⑤**web_fetch(6)**=外部 403 拒抓为主,非平台锅。

View File

@ -1,3 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。 # 改版本只动这一行。
__version__ = "0.58.49" __version__ = "0.58.50"

View File

@ -821,7 +821,9 @@ class AgentLoop:
原样走丢弃 + 非流式重试(半抢救 = 一部分执行一部分丢弃,状态不一致,不如整轮重来) 原样走丢弃 + 非流式重试(半抢救 = 一部分执行一部分丢弃,状态不一致,不如整轮重来)
allowed_keys 从各工具 schema properties 现取(单一事实源,新增工具自动生效) allowed_keys 从各工具 schema properties 现取(单一事实源,新增工具自动生效)
改写用 ensure_ascii=False 保留中文原字节_execute_tool_call 会正常 json.loads 回来 改写用 ensure_ascii=False 保留中文原字节_execute_tool_call 会正常 json.loads 回来
每条成功抢救记一行 kind=tool_salvaged 留痕 + warn 让前端重建占位段 每条成功抢救记一行 kind=tool_salvaged 留痕 + level=info warn:抢救成功当轮
照常继续对后续对话零影响,前端按 info 渲成灰色 muted ;黄色显著标注只留给
真被打断的丢弃+重试路径
""" """
try: try:
msg = response.choices[0].message msg = response.choices[0].message
@ -874,9 +876,10 @@ class AgentLoop:
pass # 留痕失败绝不能打断:arguments 已改好,当轮照常执行 pass # 留痕失败绝不能打断:arguments 已改好,当轮照常执行
self._emit({ self._emit({
"type": "warn", "type": "warn",
"level": "info",
"msg": ( "msg": (
f"工具调用参数损坏但已就地抢救 " f"已自动修复工具调用参数 "
f"{[f'{tc.function.name}({o}->{s})' for tc, _, o, s, _h in pending]},本轮继续" f"{[f'{tc.function.name}({o}->{s})' for tc, _, o, s, _h in pending]},继续执行"
), ),
}) })
return True return True

View File

@ -2451,10 +2451,13 @@ function handleSseEvent(ev, asstCard, ctx) {
const msg = (ev.data && ev.data.msg) || ""; const msg = (ev.data && ev.data.msg) || "";
if (msg) { if (msg) {
closeTextSeg(ctx); closeTextSeg(ctx);
// level=info:已自愈、本轮未被打断(如参数就地抢救)→ 灰色低调行,不带 ⚠;
// 黄色显著标注只留给真打断本轮的情况(丢弃重试/熔断等)。
const info = ev.data.level === "info";
const w = document.createElement("div"); const w = document.createElement("div");
w.className = "run-warn muted"; w.className = "run-warn muted";
w.style.cssText = "margin:6px 0;font-size:12px;color:var(--warn,#b8860b);"; w.style.cssText = `margin:6px 0;font-size:12px;color:var(--${info ? "muted,#888" : "warn,#b8860b"});`;
w.textContent = `${msg}`; w.textContent = info ? msg : `${msg}`;
asstCard.appendChild(w); asstCard.appendChild(w);
} }
} else if (t === "cancelled") { } else if (t === "cancelled") {