From 795161adcdad50005db82b453471b7760690c7a3 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 20 Jul 2026 15:59:09 +0800 Subject: [PATCH] =?UTF-8?q?fix(ppt,tools):=20svg=20=E8=B4=A8=E9=87=8F?= =?UTF-8?q?=E9=97=A8=E6=9C=AB=E5=B0=BE=E5=87=BA=20[GATE=20FAIL]=20?= =?UTF-8?q?=E6=B1=87=E6=80=BB=E8=A1=8C=E2=80=94=E2=80=94=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E7=AD=BE=E5=90=8D=E4=BB=8E=E8=AF=AF=E6=A0=87"Font=20issues"?= =?UTF-8?q?=E5=8F=98=E8=AF=9A=E5=AE=9E(bump=200.58.47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 工具失败面板 shell/exit 第1簇「Font issues x20」是误标:svg_quality_checker 按设计 exit-1(质量门),但 core/toolfail 取 [exit] 前尾行当签名,而尾行永远是 通用 tip「4. Font issues:…」→ 所有门失败(typography/alignment/spec_lock drift) 全塌成一条"Font issues",误导排查方向。多文件门也没有可靠尾行(最后文件可能只有 WARN)。 修在工具侧(toolfail 保持通用零改动): - svg_quality_checker.gate_verdict_line():errors>0 时在 main() 末尾(export 之后) 多打一行确定性汇总 [GATE FAIL] svg_quality_checker: N error file(s) / M total; top issues: … 作为最后一行 stdout,toolfail 现有取尾行逻辑自动拾取诚实签名, 模型也多一行有用汇总。 - _categorize_issue 加 spec_lock/typography 类,避免真错(typography px)落 "Other"。 - 补 GateVerdictLineTests(3):无 error 不出行 / [GATE FAIL] 打头带 top issues / 是最后一行 stdout。56 测试全绿。 Co-Authored-By: Claude Opus 4.8 (1M context) --- PROGRESS.md | 3 +- core/__init__.py | 2 +- skills/ppt/scripts/svg_quality_checker.py | 30 +++++++++++++++- tests/test_svg_alignment_check.py | 43 +++++++++++++++++++++++ 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 4fb44a7..983f771 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。 -最后更新:2026-07-20(文件预览背景撤回毛玻璃、改回半透明压暗遮罩,bump 0.58.46) +最后更新:2026-07-20(工具失败面板巡检:salvage 复盘=保持 + svg 质量门签名诚实化,bump 0.58.47) --- @@ -23,6 +23,7 @@ ### 2026-07 +- **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.45 / glob 绝对路径崩溃(工具失败聚集 #2)**:巡检近7天工具失败聚集,`glob` 排第2(3次/3task/3用户、近24h 仍在发)。根因:`tools/fs.py` GlobTool 直接 `base.glob(pattern)`,模型传绝对 pattern(沙箱 cwd 就是 `/workspace`,自然写 `/workspace/**/*.py`)时 pathlib 抛 `NotImplementedError: Non-relative patterns are unsupported`,整个调用崩返 `[Error executing glob]`。改:绝对 pattern 先 `_split_abs_glob` 拆成「存在的静态前缀目录 + 相对 glob 串」(左起吃掉不含 `*?[` 的组件当 base)再匹配;补首字符 `/`\`\\` 判定(POSIX 前导斜杠在 Windows 宿主 `is_absolute()` 为 False,沙箱是 Linux,两处对齐)。顺手删掉原 `if "**"`/`else` 两分支代码完全相同的死重构。`/sandbox/tools/` 是 Dockerfile `COPY tools/` 拷贝,**需重建沙箱镜像**才生效。 - **07-20 / 0.58.45 / PPT 页脚漂移治本(工具失败聚集 #1,A+C)**:`shell/exit` 排第1(近7天16条 exit-1、近24h 6条,9task/7用户),实测拆解 15/16 是**逐页手写绝对坐标偏离 `spec_lock.layout_grid` 锁定值 2–16px 的「想对齐没对齐」**(footer_y 近失 27行为最大头,margin_x/content_top 各7行,content_bottom 溢出12行),字体/spec_lock 只是 WARN 不 fail。关键发现:`executor-base.md §180-185` 早有「HARD—checker-enforced」的 snap 纪律却仍被漂移,加 prose 无用,治本须机械化。**A**:spec_lock_reference 新增 `## footer` 锁定片段——x/y 用 `margin_x`/`footer_y` 预填好、页码 `text-anchor="end"` 在 `canvas_w−margin_x`、留 `{PAGE}` 占位,executor 每页**逐字粘贴只换页码**,把 footer_y 这个跨页漂移第一大头从「每页手算」收敛成「一次编写」(executor-base 加对应硬规则,SKILL.md 同步);冒烟验证锁定片段过质检 errors=0、无 footer_y/margin_x 报错。**C**:`svg_quality_checker._ALIGN_TOL` 2→3px(2px 对手写实测过严),同步刷 SKILL/spec_lock_reference/executor-base 的「2–15px/2–12px」表述与测试值(margin drift 用例 60/63/66→60/64/68,30 测试全绿)。content_bottom 溢出(装太多项,另一性质)与 grid-snap 后处理(B)本轮未做。 - **07-19 / 0.58.43**:文件预览遮罩改整屏、消除"上暗下亮"割裂——旧实现用 `#file-preview-modal { bottom: var(--preview-bottom-inset) }` 把遮罩顶到 chat-form 之上,好让预览时能边看边打字,代价是底部整条(输入区+左栏入口+右栏+状态)全不压暗,与上方压暗区形成一条硬边界的通亮带,很割裂。改为遮罩铺满整屏(`bottom`→`padding-bottom`,仍让居中卡片停在输入区之上不遮挡),底部随整屏统一压暗;仅 chat-form 由新 `body.fp-open` 类 `position:relative;z-index:95`(压过遮罩 z90,层叠上下文已核:`#app`/`#pane-mid` 均不建 context)+ 上向阴影,上浮成"悬浮活动条",继续可打字。预览时左栏/右栏/状态点击落到遮罩=关预览(符合"背景不可用"预期)。改 `web/static/dev.html` + `web/static/js/preview.js`(open/close 加/去 `fp-open`)。 diff --git a/core/__init__.py b/core/__init__.py index 1f691c0..f208205 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.58.46" +__version__ = "0.58.47" diff --git a/skills/ppt/scripts/svg_quality_checker.py b/skills/ppt/scripts/svg_quality_checker.py index a65633e..d832d3f 100644 --- a/skills/ppt/scripts/svg_quality_checker.py +++ b/skills/ppt/scripts/svg_quality_checker.py @@ -20,7 +20,7 @@ import re import json import html from pathlib import Path -from typing import List, Dict, Tuple +from typing import List, Dict, Tuple, Optional from collections import Counter, defaultdict from xml.etree import ElementTree as ET @@ -1095,6 +1095,8 @@ class SVGQualityChecker: return 'Alignment/grid' elif error_msg.startswith('Geometry:'): return 'Geometry' + elif 'spec_lock' in error_msg or 'typography' in error_msg: + return 'spec_lock/typography' elif 'font' in error_msg.lower(): return 'Font issues' else: @@ -2325,6 +2327,26 @@ class SVGQualityChecker: "(////) — verify dense content " "pages aren't just text + boxes.") + def gate_verdict_line(self) -> Optional[str]: + """One deterministic line summarizing the gate outcome, meant to be the + LAST stdout line on failure. + + Failure-cluster tooling (core/toolfail) signatures a non-zero shell exit + by the last content line before ``[exit N]``. Without this, that line is + always the generic trailing tip ``4. Font issues: ...`` — so every kind + of quality-gate failure (typography px, alignment drift, spec_lock, ...) + collapses onto one misleading "Font issues" cluster. This gives it a + stable, honest signature instead. ``None`` when there are no errors. + """ + if self.summary['errors'] <= 0: + return None + top = sorted(self.issue_types.items(), key=lambda kv: (-kv[1], kv[0]))[:3] + cats = ", ".join(k for k, _ in top) or "unspecified" + return ( + f"[GATE FAIL] svg_quality_checker: {self.summary['errors']} error " + f"file(s) / {self.summary['total']} total; top issues: {cats}" + ) + def print_summary(self): """Print check summary""" print("=" * 80) @@ -2576,6 +2598,12 @@ def main() -> None: output_file = sys.argv[idx + 1] checker.export_report(output_file) + # Deterministic gate verdict as the LAST stdout line on failure (after any + # --export [REPORT] line) so core/toolfail signatures this cluster honestly. + verdict = checker.gate_verdict_line() + if verdict: + print(verdict) + # Return exit code if checker.summary['errors'] > 0: sys.exit(1) diff --git a/tests/test_svg_alignment_check.py b/tests/test_svg_alignment_check.py index 59ef4fc..27274f6 100644 --- a/tests/test_svg_alignment_check.py +++ b/tests/test_svg_alignment_check.py @@ -331,5 +331,48 @@ class SpecContractTests(unittest.TestCase): self.assertFalse(any("CJK" in e for e in r["errors"])) +class GateVerdictLineTests(unittest.TestCase): + """gate_verdict_line() 给 core/toolfail 一条稳定诚实的失败签名, + 替掉「[exit] 前最后一行永远是通用 tip → 全塌成 Font issues」的误标。""" + + def _checker(self, body: str): + with TemporaryDirectory() as tmp: + page = _write_page(Path(tmp), "01_cover.svg", body) + checker = SVGQualityChecker() + checker.check_file(str(page)) + return checker + + def test_none_when_no_errors(self): + # 干净页面(无 error)不出 gate 行 —— exit 0 本就不进失败面板 + c = self._checker(_text(60, 100, "干净标题")) + self.assertEqual(c.summary["errors"], 0) + self.assertIsNone(c.gate_verdict_line()) + + def test_gate_line_on_error(self): + # foreignObject 是 error;gate 行须以 [GATE FAIL] svg_quality_checker 打头, + # 带文件计数与 top issues —— toolfail 归一后是诚实签名而非 "Font issues" + c = self._checker('') + self.assertGreater(c.summary["errors"], 0) + line = c.gate_verdict_line() + self.assertIsNotNone(line) + self.assertTrue(line.startswith("[GATE FAIL] svg_quality_checker:")) + self.assertIn("top issues:", line) + self.assertIn("foreignObject", line) + + def test_gate_line_is_last_stdout_line(self): + # 契约:gate 行是 print_summary 之后的最后一行 stdout(toolfail 取尾行) + import contextlib + import io + c = self._checker('') + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + c.print_summary() + line = c.gate_verdict_line() + if line: + print(line) + last = [ln for ln in buf.getvalue().splitlines() if ln.strip()][-1] + self.assertTrue(last.startswith("[GATE FAIL] svg_quality_checker:")) + + if __name__ == "__main__": unittest.main()