diff --git a/PROGRESS.md b/PROGRESS.md index 12254a7..040a377 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-01(修 look_at_image/seedream 拒收容器 `/workspace` 绝对路径 + bump 0.34.6) +最后更新:2026-07-01(ppt 导出图标门升硬 + 修 CLI 退出码不传播 + 验收改全量 + bump 0.34.7) --- @@ -21,6 +21,9 @@ ## 已完成关键能力 +### 2026-07-01 / ppt skill 修复 ppt生成2(966041e5):图标门升硬 + CLI 退出码传播 + 验收改全量(bump 0.34.7) +诊断真实产出 `陶瓷资源节点建设方案.pptx`(deepseek-v4-flash 跑)两个缺陷:①23 页零图标(spec_lock 锁了 chunk-filled+inventory 却全 deck 0 个 ``);②不少错位。根因不是缺 gate 而是 gate 被打穿:(a) `svg_to_pptx.py:22` 只 `main()` 不 `sys.exit(main())`——**main() 里所有 `return 1`(图标门/无 SVG/坏路径)全被吞成退出 0**,这是最致命的一处;(b) 导出侧图标检查 `_warn_if_icons_unused` 按设计只软 WARN、照常产出;(c) 模型质检时 `svg_quality_checker.py ... | head -30`,管道吞非零退出码 + `head` 截掉打在最后的零图标 `[ERROR]` 结论;(d) 验收阶段 SKILL.md 本就只要求抽查 3 页,23 页里只肉眼看了 2 页,且封面 vision 已报"半成品/错位"仍未返工直接交付。改动:①`svg_to_pptx.py` → `sys.exit(main())`;②`pptx_cli.py` 把导出侧检查从软 WARN 升为**硬门**(锁图标却全 deck 零 `` → `[ERROR]` 退非零、不产出 pptx),加显式逃生口 `--allow-iconless`(应对 lock 过期/有意无图标);③SKILL.md 阶段六验收改「默认渲整本、逐页过目、差评即阻断返工」(废掉抽查 3 页),阶段四/五/反模式补「别用 `| head` 截断质检/导出输出」「别只看几页」「看到差评必返工」。合成测试三例(默认拒/`--allow-iconless` 放行/有图标正常)全过。**注:此修仅改 skill 侧,不改动线上跑法**;导出门只兜"锁了图标却零引用",正常有图标 deck 不受影响。 + ### 2026-07-01 / 修 look_at_image/seedream 拒收容器绝对路径(bump 0.34.6) 现象:docker backend 下主模型被系统提示告知一切都在 `/workspace` 下,自然产出容器绝对路径(如 `/workspace/ppt生成2/ceramic-node/images/cover_bg.png`)喂给 `look_at_image`,却报「图片找不到或越界」,只有改成 working_dir 相对路径才成功。根因:`tools/image_ref.py resolve_in_root`(look_at_image + seedream 共用)只吃「working_dir 相对 / user_root 相对 / 宿主绝对」三形态,唯独不把 `/workspace/` 翻回宿主 `user_root/`——而 host-side 的 send_email 早在 `Tool._resolve_user_file` 做了这翻译。改动:`resolve_in_root` 加容器根(`/workspace`)前缀翻译,**按字符串前缀判断而非 `is_absolute()`**(Windows 上 `/workspace/...` 缺盘符不算绝对);越界仍靠原 `relative_to(root)` 兜住(`/workspace/../secret`、`/workspace/../../etc/passwd` 实测仍拒)。这样 look_at_image/seedream 接受的路径形态与 send_email/wechat_push 及系统提示告诉 agent 的口径一致。 diff --git a/core/__init__.py b/core/__init__.py index 425b1bf..298463c 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.34.6" +__version__ = "0.34.7" diff --git a/skills/ppt/SKILL.md b/skills/ppt/SKILL.md index b808f19..d91315c 100644 --- a/skills/ppt/SKILL.md +++ b/skills/ppt/SKILL.md @@ -143,6 +143,7 @@ references/visual-styles/.md # 锁定的视觉风格 - **任何 `error`(禁用特性 / viewBox 不符 / spec_lock 漂移 / **锁了图标 inventory 却全 deck 0 图标** / **内容 deck 全是文字方块(≥6 页且零 ``/``/``/``)** 等)必须改:回阶段三重写该页再跑**,不放过。 - `warning`(低分辨率图 / 非 PPT 安全字体等):能顺手改就改,否则知会后放行。 - 跑 `svg_output/`(不要在 finalize 后跑 —— finalize 改写 SVG 会掩盖源级违规)。 +- ⚠️ **别用 `| head` / `| tail` 截断质检输出**:管道会把脚本的非零退出码换成 `head` 的 0(门形同虚设),`head` 还会截掉打在**最后**的 deck 级门结论(如零图标 `[ERROR]`)。原样跑,读完整输出、认它的退出码。 ## 阶段五:后处理 + 导出 @@ -156,16 +157,20 @@ references/visual-styles/.md # 锁定的视觉风格 .venv/Scripts/python.exe /scripts/svg_to_pptx.py # 产物:exports/_.pptx(原生,读 svg_output/)+ .build/backup/latest/svg_output/(源快照,只留最新) ``` +- 🚧 **导出边界图标门(硬)**:spec_lock 锁了 `icons.library` + 非空 `inventory` 但全 deck 零 `` → 导出**直接 `[ERROR]` 退非零、不产出 pptx**(这是最后一道,`| head` 绕不过)。正确做法是回阶段三给内容页补图标重跑;只有 lock 确实过期 / 有意做无图标 deck 才加 `--allow-iconless` 放行。 - ❌ 别用 `cp` 代替 finalize_svg(它做了多步关键处理);❌ 别加 `--only` / 强制 `-s output`。 - 动画可选:`-t fade`(翻页,默认)/ `-a auto`(逐元素入场,**默认 none**,用户要才开)。全表见 animations.md。 - 改稿:只改 `spec_lock.md` 的颜色/字体 → `update_spec.py ` 传播到所有 SVG;改版式/内容 → 重写对应页 SVG 再跑 5.2–5.3,**不要直接 edit 成品 .pptx**。 -## 阶段六:验收(渲图肉眼/vision 看) +## 阶段六:验收(渲图肉眼/vision 看)—— 全量,不抽查 ``` -.venv/Scripts/python.exe /scripts/svg_preview.py --pages 1,3,5 +.venv/Scripts/python.exe /scripts/svg_preview.py ``` -PNG 默认落 `.build/preview/`(隐藏);优先渲 `.build/svg_final/`(图标/配图已内嵌,最忠实),没有则渲 `svg_output/`(无 chromium 时走 cairosvg 兜底、会就地展开图标)。`read` 渲出的 PNG 亲眼过:封面、一个内容页、一个 breathing 页 —— 看标题层级、卡片过挤/过空、文字是否都正常、节奏是否单调、配图位置。不通过的回阶段三改对应页 SVG 重跑。 +- **默认渲整本,不带 `--pages`**。抽查 3 页只能覆盖 3 页,错位/文字溢出/元素重叠恰恰藏在没看的那些页里 —— 逐页手写绝对坐标,每页都可能翻车,所以**每页都要过目**。(页数多时可分批渲,但目标是 100% 覆盖,不是采样。) +- PNG 默认落 `.build/preview/`(隐藏);优先渲 `.build/svg_final/`(图标/配图已内嵌,最忠实),没有则渲 `svg_output/`(无 chromium 时走 cairosvg 兜底、会就地展开图标)。 +- `read` / `look_at_image` **逐页**亲眼过:标题层级、卡片过挤/过空、**文字是否溢出卡片/被裁**、**元素是否重叠错位**、图标在不在、节奏是否单调、配图位置。 +- 🚧 **差评即阻断**:任一页被判出排版/溢出/重叠/半成品问题(哪怕只是封面)→ **回阶段三改那一页 SVG、重渲、复看,直到通过才算验收完**。不许"看了一页差评、跳去看下一页好评就收尾"——那正是错位交付的来路。 > svg_preview 渲的是 SVG(视觉真相,与导出的 pptx 1:1),比渲最终 pptx 更早更准暴露观感问题。需要校验"SVG→DrawingML 转换是否保真",再开导出的 pptx 在 PowerPoint 里看。 @@ -199,6 +204,8 @@ PNG 默认落 `.build/preview/`(隐藏);优先渲 `.build/svg_final/`(图标/配 - **breathing 页堆多卡网格**(违节奏,显 AI 味) - 模板照搬不重上皮(直接用模板默认渐变/阴影/字号) - 质检没过就交付 / 直接 edit 成品 .pptx 改稿 +- **只渲/只看几页就收尾**(错位藏在没看的页里);**看到差评却不返工**(封面 vision 说"半成品/挤左侧"还继续导出交付) +- **用 `| head` 截断质检或导出输出**(吞非零退出码 + 截掉最后的门结论,门形同虚设) - 起名 `output.pptx` —— 按主题命名 ## 输出 diff --git a/skills/ppt/scripts/svg_to_pptx.py b/skills/ppt/scripts/svg_to_pptx.py index d785f2a..0b388bc 100644 --- a/skills/ppt/scripts/svg_to_pptx.py +++ b/skills/ppt/scripts/svg_to_pptx.py @@ -19,4 +19,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent)) from svg_to_pptx import main if __name__ == '__main__': - main() + # Propagate main()'s return code as the process exit code — otherwise every + # `return 1` guard in main() (icon gate / no-SVG / bad path) silently exits 0 + # and callers (and `&&` chains) can't tell success from a refused export. + sys.exit(main()) diff --git a/skills/ppt/scripts/svg_to_pptx/pptx_cli.py b/skills/ppt/scripts/svg_to_pptx/pptx_cli.py index 0f7e066..0e1bfc0 100644 --- a/skills/ppt/scripts/svg_to_pptx/pptx_cli.py +++ b/skills/ppt/scripts/svg_to_pptx/pptx_cli.py @@ -63,48 +63,46 @@ def _recorded_narration_on_click_slides( return blocked -def _warn_if_icons_unused(project_path: Path, svg_files: list[Path]) -> None: - """Export-boundary defense-in-depth (mirrors svg_quality_checker's icon gate). +def _deck_locks_icons_but_authors_none(project_path: Path, svg_files: list[Path]) -> bool: + """Detect the export-boundary icon violation. - If ``spec_lock.md`` locks an icon library + non-empty inventory but the source - SVGs carry zero ```` placeholders, the deck exports flat / - icon-less. Warn loudly on stderr so it isn't silent when someone exports - without first running ``svg_quality_checker.py`` (the hard gate). Non-fatal: - export still proceeds — the lock may be stale or icons intentionally absent. - Fully defensive: any failure here must never break the export. + Returns True when ``spec_lock.md`` locks an icon library + non-empty + inventory but the source SVGs carry ZERO ```` placeholders — + i.e. the deck would export flat / icon-less despite the strategist intending + icons. Returns False otherwise (including on any internal error: detection + must never itself break the export path). + + The caller turns a True into a fatal abort (unless ``--allow-iconless``). + This mirrors svg_quality_checker's deck-level icon gate, but at the export + boundary it is the LAST line of defense: the quality gate can be reordered + before export or have its non-zero exit swallowed by ``| head``, whereas a + refusal to write the pptx cannot be piped away. """ try: import re lock_path = project_path / 'spec_lock.md' if not lock_path.exists(): - return + return False try: from update_spec import parse_lock icons = (parse_lock(lock_path) or {}).get('icons') or {} except Exception: - return + return False library = (icons.get('library') or '').strip().lower() inventory = (icons.get('inventory') or '').strip().lower() _empty = ('', 'none', '(none)', '-', 'n/a') if library in _empty or inventory in _empty: - return + return False total = 0 for p in svg_files: try: total += len(re.findall(r']*\bdata-icon\s*=', p.read_text(encoding='utf-8'))) except Exception: continue - if total == 0: - print( - "[WARN] spec_lock locks an icon library + inventory, but the source SVGs " - "contain ZERO — this deck exports flat / icon-less. " - "Run svg_quality_checker.py and add inventory icons to content pages " - "before delivering.", - file=sys.stderr, - ) + return total == 0 except Exception: - return + return False def main(argv: list[str] | None = None) -> int: @@ -202,6 +200,12 @@ Recorded narration: parser.add_argument('--no-compat', action='store_true', help='Disable Office compatibility mode (pure SVG only, requires Office 2019+)') + parser.add_argument('--allow-iconless', action='store_true', default=False, + help='Allow export even when spec_lock locks an icon inventory but ' + 'the SVGs author zero (default: refuse — the deck ' + 'would render flat / icon-less). Use only for a stale lock or an ' + 'intentionally icon-less deck.') + mode_group = parser.add_mutually_exclusive_group() mode_group.add_argument('--only', type=str, choices=['native', 'legacy'], default=None, help='Only generate one version: native (editable shapes) or legacy (SVG image)') @@ -351,9 +355,30 @@ Recorded narration: print("Error: No SVG files found") return 1 - # Export-boundary icon check: warn (non-fatal) if an inventory is locked but - # no is authored — defense-in-depth behind the quality gate. - _warn_if_icons_unused(project_path, ref_files) + # Export-boundary icon gate: a locked icon inventory with ZERO authored + # means the deck exports flat / icon-less. This is the last + # line of defense (the quality gate can be reordered before export or its + # non-zero exit swallowed by `| head`), so it is FATAL by default — refuse to + # produce a pptx that the strategist's own spec_lock says is wrong. + # --allow-iconless is the explicit escape hatch (stale lock / intentional). + if _deck_locks_icons_but_authors_none(project_path, ref_files): + if args.allow_iconless: + print( + "[WARN] spec_lock locks an icon library + inventory but the source SVGs " + "contain ZERO — exporting flat / icon-less anyway " + "(--allow-iconless).", + file=sys.stderr, + ) + else: + print( + "[ERROR] spec_lock locks an icon library + inventory, but the source SVGs " + "contain ZERO — this deck would export flat / icon-less.\n" + " Add inventory icons to content pages (KPI / list / process /\n" + " comparison layouts especially), then re-run. If the lock is stale\n" + " or icons are intentionally absent, pass --allow-iconless.", + file=sys.stderr, + ) + return 1 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")