From 5cf92ec71a3d4451283d2a1547c4255a37562824 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 17 Jul 2026 09:32:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(rendering):=20=E9=80=9A=E7=94=A8=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E8=B5=B0=E5=B9=B3=E5=8F=B0=E6=B8=B2=E6=9F=93=E5=99=A8?= =?UTF-8?q?=20report=20profile=20+=20=E5=9F=BA=E5=BA=A7=E5=BC=95=E5=AF=BC,?= =?UTF-8?q?=E5=B0=91=E8=AE=A9=E6=A8=A1=E5=9E=8B=E6=89=8B=E6=92=B8=20python?= =?UTF-8?q?-docx(bump=200.58.35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 接 0.58.34(语法预检=止血),这条根治那一头。关键发现:平台早有成熟 md→docx(+pdf) 渲染器 rendering/(bind-mount /sandbox/rendering:ro,任何 run_python 可达,与 skill 无关),标题/正文/列表/表格/图片图题/mermaid/化学式下标/目录/house 字体全有, paper/proposal/brief 三 profile 在用。烧 token 的验收报告没走它、在裸手撸 python-docx, 唯一原因是没引导把"随手写 Word 报告"指向它。故不造轮子,只两件小事: 1. rendering/docx_manuscript.py 加 report profile(复用全套渲染;差异=报告不是申报书: 目录默认无[--toc 才带]/章节不强制分页/页边距 2.5-2.5/列表含"第X章节"不含"条"), render.py --profile 加 report 选项 + 路由。 2. core/agent_builder.py 基座 prompt 加软引导:出 Word 报告→正文写成 sections/*.md 调 render.py --profile report,别手撸 python-docx 内联中文。 取舍红线(与用户对齐):渲染器是首选加法不是替代——绝不硬拦 run_python 生成文档, 确需精细定制版式/改写已有 docx/处理 xlsx 等渲染器覆盖不了的照样自由写代码;软引导 让位于更具体的 skill render 指引(proposal/paper)。刻意不把渲染器指路塞进语法预检 hint(会 overload 只该管语法的检查、误导合法手撸),预检保持只诊断语法。三层叠加: 渲染器(根治)> 预检(0.58.34)> RepeatGuard err-streak(0.58.33),run_python 能力不削。 测试 tests/test_rendering.py(四 profile 端到端 + report 目录可选)+ CLI e2e (render.py --profile report 出 37KB docx)全绿;py_compile 校三文件无语法错。 DESIGN §8.6 记该 profile + 并存红线。additive,不碰对外 API/DB 契约。 Co-Authored-By: Claude Opus 4.8 (1M context) --- DESIGN.md | 4 +++- PROGRESS.md | 1 + core/__init__.py | 2 +- core/agent_builder.py | 10 ++++++++++ rendering/docx_manuscript.py | 15 +++++++++++++++ rendering/render.py | 6 +++++- tests/test_rendering.py | 26 ++++++++++++++++++++++++-- 7 files changed, 59 insertions(+), 5 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index a27a0ca..43f8278 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -278,7 +278,9 @@ scheduled_jobs(§8.5) channel_bindings(§8.7,判别列+JSONB) ### 8.6 平台渲染层 rendering/(✅ 2026-06-23) -**心智:文档渲染是平台能力不是 skill 内容**。起因:化学式白名单在三份 render_docx 逐字重复 + brief 缺 PDF 路径致线上手搓 weasyprint。**不放 `skills/_shared/`**:skill 走自包含/可 fork 标准,跨 skill import 破坏 fork。抽顶层 `rendering/` bind-mount `/sandbox/rendering:ro`:common(叶子原语单一事实源)+ docx_manuscript(paper/proposal 双 profile)+ docx_brief + pdf(chromium 不用 weasyprint——镜像已有,保真更高)+ render.py 统一 CLI。重构前后 docx 字节一致零回归;brief 不强并 manuscript(差异大,只共叶子)。与 §8.3 分工:pptx 预览在 host 面向用户,本层在沙盒面向 agent 交付物。 +**心智:文档渲染是平台能力不是 skill 内容**。起因:化学式白名单在三份 render_docx 逐字重复 + brief 缺 PDF 路径致线上手搓 weasyprint。**不放 `skills/_shared/`**:skill 走自包含/可 fork 标准,跨 skill import 破坏 fork。抽顶层 `rendering/` bind-mount `/sandbox/rendering:ro`:common(叶子原语单一事实源)+ docx_manuscript(paper/proposal/report 三 profile)+ docx_brief + pdf(chromium 不用 weasyprint——镜像已有,保真更高)+ render.py 统一 CLI。重构前后 docx 字节一致零回归;brief 不强并 manuscript(差异大,只共叶子)。与 §8.3 分工:pptx 预览在 host 面向用户,本层在沙盒面向 agent 交付物。 + +**`report` 通用 profile + 基座引导(2026-07-17)**:起因是工具失败面板最大头——建材院验收/技术/评审报告(无对应 skill)由模型裸手撸 python-docx 内联中文正文,引号/全角标点崩成 SyntaxError 反复返工烧 token(定层见 `core/pysyntax.py`)。根因不是缺渲染器(paper/proposal 早有全套 md→docx),而是**这类自由报告没有指路**。补 `report` profile(复用 manuscript 全套,差异:目录默认无 / 章节不强制分页 / 通用页边距)+ 基座 prompt 加软引导「出 Word 报告→正文写成 md sections 调 render.py --profile report,别手撸 python-docx」。**取舍(红线):渲染器是"首选加法"不是"替代"——绝不硬拦 run_python 生成文档**。确需精细定制版式 / 改写已有 docx / 处理 xlsx 等渲染器覆盖不了的,run_python 照样自由写代码;软引导让位于更具体的 skill render 指引(proposal/paper)。三层防御叠加:渲染器(根治,正文进 md 不进代码)> run_python 语法预检(`core/pysyntax.py`,手撸时的安全网,只诊断语法不限制工具选择)> RepeatGuard err-streak(撞墙兜底)。 ### 8.7 微信接入(双渠道)(✅ 均落地) diff --git a/PROGRESS.md b/PROGRESS.md index 1a9aa92..96a95be 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -23,6 +23,7 @@ ### 2026-07 +- **07-17 / 0.58.35**:**通用报告走平台渲染器(`report` profile)+ 基座引导——从根上少让模型手撸 python-docx**(接 0.58.34:语法预检是止血,这条是根治那一头)。**关键发现**:平台**早有**成熟 md→docx(+pdf)渲染器 `rendering/`(bind-mount `/sandbox/rendering:ro`,任何 run_python/shell 可达,与 skill 无关),标题/正文/列表/表格/图片图题/mermaid/内联/化学式下标/目录/house 字体(宋体小四·黑体·Times·1.5 行距)全有,paper/proposal/brief 三 profile 在用。烧 token 的验收报告没走它、在裸手撸 python-docx,**唯一原因是没 skill/引导把"随手写 Word 报告"指向它**(只有 proposal/paper 知道)。故**不造轮子**,只两件小事:① `rendering/docx_manuscript.py` 加 `report` profile(复用全套渲染,差异=报告不是申报书:目录默认无[`--toc` 才带]、章节不强制分页、通用页边距 2.5/2.5、列表含"第X章/节"不含"条"),`render.py --profile` 加 `report` 选项 + 路由;② `core/agent_builder.py` 基座 prompt 加软引导「出 Word 报告→正文写成 `/sections/*.md`(纯文本零转义)调 `render.py --profile report`,别手撸 python-docx 内联中文」。**取舍红线(与用户对齐)**:渲染器是**首选加法不是替代**——绝不硬拦 run_python 生成文档,确需精细定制版式/改写已有 docx/处理 xlsx 等渲染器覆盖不了的照样自由写代码;软引导让位于更具体的 skill render 指引(proposal/paper)。**刻意不做**:(c) 把渲染器指路塞进语法预检 hint——会把只该管语法的检查overload成它做不可靠的工具选择判断(分不清"报告正文该走渲染器"vs"定制 python-docx 就该手撸"),误导合法手撸,故预检保持只诊断语法。三层叠加:渲染器(根治)> 预检(0.58.34 安全网)> RepeatGuard err-streak(0.58.33 兜底),run_python 能力不削。测试 `tests/test_rendering.py`(四 profile 端到端 + report 目录可选)+ CLI e2e(`render.py --profile report` 出 37KB docx)全绿;py_compile 校 agent_builder/render/manuscript 无语法错。DESIGN §8.6 记该 profile + 并存红线。additive,不碰对外 API/DB 契约。相关 memory:skill 禁令不带配方 / 高轮数烧 token 三根因。 - **07-17 / 0.58.34**:**run_python 语法预检:宿主 compile() 拦下中文正文硬拼进源码的语法坏码**(失败面板最大头,20 条真实样本实证:模型手写 python-docx 生成 docx/报告时把中文正文内联进 `T("中文…")`,引号/标点崩坏 → SyntaxError → 改 → 再崩,`gen_acceptance_report_v3.py` 一分钟内连撞 6 次)。三类:**A 引号提前闭合**(最多,中文串里用 ASCII 引号 `"` 把外层字符串提前闭合;Python 却报 `Perhaps you forgot a comma?`/`unterminated string`,**主动误导**模型照着加逗号越修越错)、**B 全角标点漏进代码位**(`center=True、`/`采购(`,`invalid character 'X'(U+XXXX)`)、**C 括号/引号结构崩**。**根因判断**:症结不是"缺写长文的工具"(write 早能写长文进文件),而是**docx/报告没有渲染管线**——documents skill 只做文献检索、不管 docx 生成,模型裸手撸 python-docx 内联中文;而 ppt skill 有 SVG→pptx 管线(正文进 SVG 文本节点),所以 ppt 崩得少。**本轮先落机检止血**(对标 [[feedback_skill_ban_no_recipe]] "硬约束靠平台机检"):新增独立模块 `core/pysyntax.py`(不 import litellm,`tests/test_pysyntax.py` 本机可跑,11 用例覆盖 A/B/C + 合法中文串/中文引号不误伤)`precheck_python(code)`——host 上 `compile()`(host 3.12 ≡ sandbox python:3.12-slim,零版本偏差;只解析不执行、与依赖无关),命中 SyntaxError 返**锐化中文诊断**(出错行+光标+按错误类型定向:invalid-character→全角改半角、forgot-comma/unterminated+含中文→ASCII 引号提前闭合的三改法、含中文统一追加"正文别拼进 .py,write 进 .md/.txt 再 read"根治提示)。接进 **run_python 所有派发路径**(inline/script_path/background × docker+host 两执行器,`_prepare_script` 内嵌覆盖 script_path 全路径 + inline 分支各加一处):命中即 `ToolResult(hint, exit_code=2)` 早返、**跳过 docker 往返**。**零误伤**:只拦本就跑不了的码(compile 不过=运行必失败),通过则原样放行、行为不变;既有 30 个 executor/run_python 测试(合法码 fixture)全绿印证。**白捡协同**:预检把 run_python 语法失败从 `[stderr]…[exit 1]` 改成 `[Error]` 前缀+固定首行,`loop._RepeatGuard` 的 err-streak(0.58.33 ②)得以兜住"反复交语法坏码"的循环(旧 `[exit 1]` 形态它按非错误放行、抓不到)——① 诊断准(修得快)+ ② 撞墙拦(停得住)合围。**下一步(未做,单列选型)**:docx 渲染管线(正文 write 进纯文本文件 → 渲染工具 read 生成 docx,正文永不进 Python 源码,**也不能走"大正文当工具入参"**——那会把长中文塞进 tool_call arguments,触发 0.58.33 修的 wire 乱序畸形);沙箱现有 python-docx+markitdown(docx→md 单向)、无 pandoc/libreoffice,自建渲染器 vs 装 pandoc 待拍板。additive 内部机制,不碰对外 API/DB 契约。相关 memory:skill 禁令不带配方 / deepseek 畸形 salvage 案。 - **07-17 / 0.58.33**:**堵工具失败聚集里两条反复烧 token 的裂缝**(扫近 14 天失败面板 top13 定位,`scripts/standalone` 复现口径)。**裂缝①「必填 key 被吞」畸形(面板 #3 `edit 缺少必填参数 ['path']`,14 次 / 13 task / 9 用户,最广)**:定层查一条原始 tool_call 坐实是**流式 wire 乱序**——某键的值碎片被瞬移拼进相邻字符串(实证:path 的 `.../gen_final_report_v2.py` 被吞进 `old_str` 尾部,独立的 `"path"` 键随之消失),跨 provider(`call_00_` 网关档 + `toolu_` Anthropic 系),非模型漏参。这类与既有 char-0 前缀畸形的关键区别是 **JSON parse 成功** → 既不命中 `_malformed_tool_calls`(只抓 parse 失败)、salvage 也救不了(parse-to-end/key 白名单对合法但错位无能),一路漏到 executor 才在语义层报错、回 `[Error]` 喂回模型 → 再拼再乱序反复烧。**修**:`_toolcalls_partial_args`(解析为非空 dict 且 `0/sections/*.md`,纯文本、零转义 / 零语法风险)," + f"再调平台渲染器出 docx —— **别在 run_python 里手撸 python-docx 内联大段中文正文**" + f"(中文引号 / 全角标点混进源码极易 SyntaxError、反复返工烧 token):\n\n" + f" python /sandbox/rendering/render.py --profile report --format docx /sections/ -o /<报告名>.docx\n\n" + f"渲染器自动管字体(正文宋体小四 / 标题黑体 / 1.5 倍行距)、目录(加 `--toc`)、" + f"表格、`![](path)` 居中插图 + 图题编号、mermaid 图。已加载 proposal / paper 等 skill 时" + f"按其 render 指引的 profile 走(更具体),通用报告用 `--profile report`。\n" + f"这是**软建议不是强制**:确需精细定制版式 / 改写已有 docx / 处理 xlsx 等渲染器覆盖不了的," + f"照样用 run_python 自由写代码。\n" f"\n## task 级「宪法」文件命名约定(跨 skill 通用)\n" f"跟 task 1:1 绑定、后续步骤会**反复 read** 的「宪法」性文件(如 proposal/ppt 的 " f"spec、outline),统一落 task_dir 根、按此格式命名:\n\n" diff --git a/rendering/docx_manuscript.py b/rendering/docx_manuscript.py index 917df0d..6da591d 100644 --- a/rendering/docx_manuscript.py +++ b/rendering/docx_manuscript.py @@ -57,6 +57,21 @@ PROFILES = { "always_toc": True, "trailing_page_break": True, }, + # 通用报告(建材院验收/技术/评审报告等自由长文):复用 manuscript 全套渲染, + # 差异是"报告不是申报书"——目录默认无(--toc 才带)、章节间连续排版不强制分页、 + # 通用页边距。刻意让模型把正文写成 md 交给它渲,别再手撸 python-docx 内联中文 + # (失败面板最大头,见 core/pysyntax.py)。 + "report": { + "left_margin": Cm(2.5), + "right_margin": Cm(2.5), + "list_patterns": _BASE_LIST_PATTERNS + [ + re.compile(r"^第[一二三四五六七八九十百]+[章节]"), # 第一章 / 第一节(不含"条") + ], + "toc_title": "目 录", + "toc_placeholder": "[在 Word 中按 F9 或右键此处选择 “更新域” 即可生成完整目录]", + "always_toc": False, + "trailing_page_break": False, + }, } diff --git a/rendering/render.py b/rendering/render.py index b0d417b..9652078 100644 --- a/rendering/render.py +++ b/rendering/render.py @@ -26,7 +26,8 @@ from rendering import docx_brief, docx_manuscript, pdf # noqa: E402 def main(argv: list[str] | None = None) -> int: ap = argparse.ArgumentParser(description="md(sections 目录或单 .md)→ docx / pdf") ap.add_argument("src", type=Path, help="sections 目录(拼接其 *.md)或单个 .md") - ap.add_argument("--profile", required=True, choices=["brief", "paper", "proposal"]) + ap.add_argument("--profile", required=True, + choices=["brief", "paper", "proposal", "report"]) ap.add_argument("--format", default="docx", choices=["docx", "pdf"]) ap.add_argument("-o", "--output", type=Path, required=True, help="输出路径") ap.add_argument("--no-color", dest="color", action="store_false", @@ -53,6 +54,9 @@ def main(argv: list[str] | None = None) -> int: elif args.profile == "paper": docx_manuscript.render_sections("paper", args.src, args.output, lang=args.lang, toc=args.toc) + elif args.profile == "report": + docx_manuscript.render_sections("report", args.src, args.output, + lang=args.lang, toc=args.toc) else: # proposal docx_manuscript.render_sections("proposal", args.src, args.output, fund_type=args.fund_type) diff --git a/tests/test_rendering.py b/tests/test_rendering.py index 774a043..7bef935 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -51,7 +51,7 @@ def _docx_paragraphs(path: Path) -> int: class TestDocxProfiles(unittest.TestCase): - def test_three_profiles_render(self): + def test_four_profiles_render(self): with tempfile.TemporaryDirectory() as td: d = Path(td) sec = _write_sections(d) @@ -68,11 +68,33 @@ class TestDocxProfiles(unittest.TestCase): out_r = d / "proposal.docx" docx_manuscript.render_sections("proposal", sec, out_r, fund_type="key_rd") self.assertTrue(out_r.exists() and out_r.stat().st_size > 0) + # report(通用报告) + out_g = d / "report.docx" + docx_manuscript.render_sections("report", sec, out_g) + self.assertTrue(out_g.exists() and out_g.stat().st_size > 0) + self.assertGreater(_docx_paragraphs(out_g), 0) # 每份都应有 1 张表 - for f in (out_b, out_p, out_r): + for f in (out_b, out_p, out_r, out_g): with zipfile.ZipFile(f) as z: self.assertIn("word/document.xml", z.namelist()) + def test_report_toc_optional(self): + """report 默认无目录,--toc 才带(与 proposal 始终带目录相区分)。""" + with tempfile.TemporaryDirectory() as td: + d = Path(td) + sec = _write_sections(d) + out_no = d / "report_notoc.docx" + docx_manuscript.render_sections("report", sec, out_no, toc=False) + out_yes = d / "report_toc.docx" + docx_manuscript.render_sections("report", sec, out_yes, toc=True) + + def _has_toc_field(path: Path) -> bool: + with zipfile.ZipFile(path) as z: + xml = z.read("word/document.xml").decode("utf-8", "replace") + return "TOC \\o" in xml + self.assertFalse(_has_toc_field(out_no)) + self.assertTrue(_has_toc_field(out_yes)) + class TestChemSingleSource(unittest.TestCase): def test_pdf_uses_common_chem(self):