feat(rendering): 通用报告走平台渲染器 report profile + 基座引导,少让模型手撸 python-docx(bump 0.58.35)
接 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) <noreply@anthropic.com>
This commit is contained in:
parent
2a60243c02
commit
5cf92ec71a
|
|
@ -278,7 +278,9 @@ scheduled_jobs(§8.5) channel_bindings(§8.7,判别列+JSONB)
|
||||||
|
|
||||||
### 8.6 平台渲染层 rendering/(✅ 2026-06-23)
|
### 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 微信接入(双渠道)(✅ 均落地)
|
### 8.7 微信接入(双渠道)(✅ 均落地)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
### 2026-07
|
### 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 报告→正文写成 `<task_dir>/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.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<len(missing_required)<len(required)`,即至少一个必填在场、至少一个缺失=错位吞键特征;空 `{}`/必填全缺不算,仍交 executor+RepeatGuard)接进 `_stream_llm` 的 attempt 循环,和畸形/空响应同类:丢弃本轮走**非流式重试**(服务端一次拼好绕开流式 delta 乱序),耗尽仍缺才落回 executor 返「缺必填参数」(多为真漏参,不再空转);`_log_partial_args` 落 `usage_events`(kind=`tool_malformed`,error 签名固定 `missing required keys [...]`,和 char-0 型区分)——这类此前伪装成普通 messages `[Error]`、面板完全看不见,现纳入可观测。**裂缝②「换参数撞同一堵墙」(面板 #2 `edit old_str not found` 单 task 9 次 / 24h 7 次)**:模型每次微调 old_str 重试,精确 args 指纹每次不同 → `_RepeatGuard` 的 arg 判据不累计,只能等 `_STALL_LIMIT` 慢慢兜。**修**:`_RepeatGuard` 补第二道判据——按工具的**连续同类错误 streak**(`_norm_err` 抹平路径/数字后签名相同、跨 ≥2 个不同 args),SOFT 注入定向提示(先 read/grep 看确切内容再动手)、HARD 拦截一次并重置到 SOFT(非永久封死,换路后重试留活口);任一非错误结果立即清零该工具 streak。测试 `tests/test_loop_repeat_guard.py` +12(err-streak 5 + partial-args 7),全 23 相邻测试(salvage/toolfail_malformed)绿。**本机验证坑**:`core/loop.py` 顶层 `import litellm` 本机导入死卡(GitHub cost map),纯逻辑测试用 `sys.modules` 注入 litellm+子模块 stub 绕开跑真代码;失败面板扫描脚本因 import 链同样卡,改直连 psycopg 复刻 `scan_tool_failures` 口径查(结果与走 `core` 的交叉验证一致)。盲区:provider-wire 抖动本机复现不出、/verify 端到端跑不了,靠单测 + 生产灰度。additive 内部机制,不碰对外 API/DB 契约。剩 ①run_python 中文全角标点/未闭合字符串致 SyntaxError(面板最大头,产物质量,待单独立项)、③超时 19 次未动。相关 memory:deepseek 畸形 salvage 案 / 高轮数烧 token 三根因。
|
- **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<len(missing_required)<len(required)`,即至少一个必填在场、至少一个缺失=错位吞键特征;空 `{}`/必填全缺不算,仍交 executor+RepeatGuard)接进 `_stream_llm` 的 attempt 循环,和畸形/空响应同类:丢弃本轮走**非流式重试**(服务端一次拼好绕开流式 delta 乱序),耗尽仍缺才落回 executor 返「缺必填参数」(多为真漏参,不再空转);`_log_partial_args` 落 `usage_events`(kind=`tool_malformed`,error 签名固定 `missing required keys [...]`,和 char-0 型区分)——这类此前伪装成普通 messages `[Error]`、面板完全看不见,现纳入可观测。**裂缝②「换参数撞同一堵墙」(面板 #2 `edit old_str not found` 单 task 9 次 / 24h 7 次)**:模型每次微调 old_str 重试,精确 args 指纹每次不同 → `_RepeatGuard` 的 arg 判据不累计,只能等 `_STALL_LIMIT` 慢慢兜。**修**:`_RepeatGuard` 补第二道判据——按工具的**连续同类错误 streak**(`_norm_err` 抹平路径/数字后签名相同、跨 ≥2 个不同 args),SOFT 注入定向提示(先 read/grep 看确切内容再动手)、HARD 拦截一次并重置到 SOFT(非永久封死,换路后重试留活口);任一非错误结果立即清零该工具 streak。测试 `tests/test_loop_repeat_guard.py` +12(err-streak 5 + partial-args 7),全 23 相邻测试(salvage/toolfail_malformed)绿。**本机验证坑**:`core/loop.py` 顶层 `import litellm` 本机导入死卡(GitHub cost map),纯逻辑测试用 `sys.modules` 注入 litellm+子模块 stub 绕开跑真代码;失败面板扫描脚本因 import 链同样卡,改直连 psycopg 复刻 `scan_tool_failures` 口径查(结果与走 `core` 的交叉验证一致)。盲区:provider-wire 抖动本机复现不出、/verify 端到端跑不了,靠单测 + 生产灰度。additive 内部机制,不碰对外 API/DB 契约。剩 ①run_python 中文全角标点/未闭合字符串致 SyntaxError(面板最大头,产物质量,待单独立项)、③超时 19 次未动。相关 memory:deepseek 畸形 salvage 案 / 高轮数烧 token 三根因。
|
||||||
- **07-15 / 0.58.32**:**空响应防御:provider 吐空不再被当正常收尾静默 done**(复盘 task 2a1bc25d "ppt3":unifyllm 网关对某档 Claude 偶发把 tool_use 漏成正文 / 直接吐空,回来的轮 `tool_calls=[]` 且正文空,`loop.py` 的 `if not tool_calls: done` 当"模型答完"收尾——run_status=idle、无报错、无终态失败,表现为"自己中断",只能人肉挖 DB 才发现;复测该 bug 现已消失/瞬态,但"空 tool_calls 直接 done"这个失败模式本身太隐蔽,任何 provider 未来吐一次空都会静默卡死,故加防御)。**两层**(对称既有畸形 salvage 链):① 自动重试——`_is_empty_response`(tc 空且 content 去空白为空;纯 tool_call 轮不误判)接进 `_stream_llm` 的 attempt 循环,和畸形同类:丢弃本轮走非流式重试(多数瞬态重发一次即好,用户无感),`_log_empty_response` stdout+`usage_events`(kind=`empty_response`,cost 0)双写留痕、DB 失败静默不阻塞;② 重试耗尽仍空——`run()` 收尾点分空-空分支:不发静默 `done`,改发**可见 warn**「模型返回空响应(已自动重试仍为空),已停止,回复『继续』可重试」+ done(复用 stall 熔断的 warn+自停话术,run_status 落 idle 可续),**不引入终态 error**(空响应可恢复,红 error 过重且与既有路径不对称)。③ 可观测——`core/toolfail.py` 加第四段扫描 kind=empty_response 聚成 tool=`(empty)`/kind=`empty` 的 cluster(sample=model_profile 看哪个网关档在吐空),即便自动救回也留痕、跨 task 系统性吐空才冒头(单次瞬态 <阈值不触发)。**刻意不做**内容嗅探式"narrated tool_call 特征→重试":marker 每次变、且与助手正常解释/贴代码的正文高度重叠,假阳性(把合法回答判坏反复重试)比它防的罕见静默停更糟——那类只靠面板留痕兜、不改热路径行为。测试:`tests/test_loop_empty_response.py`(_is_empty_response 四态含纯 tool_call 不误判 + 重试恢复/耗尽返空/正常文本不重试)、`tests/test_toolfail_malformed.py` +2(空响应聚集 / 单次瞬态不触发),顺手补回 `test_loop_malformed_retry.py` 因 salvage(0.58.24)落地后失修的 `loop.executor` 桩(空 schemas → salvage 返 False 走既有重试)。全 247 测试绿。盲区:provider-wire 抖动本机复现不出、/verify 端到端跑不了,靠单测+生产灰度。additive 内部机制,不碰对外 API/DB 契约。相关 memory:unifyllm tool_use 漏正文案 / deepseek 畸形 salvage 案。
|
- **07-15 / 0.58.32**:**空响应防御:provider 吐空不再被当正常收尾静默 done**(复盘 task 2a1bc25d "ppt3":unifyllm 网关对某档 Claude 偶发把 tool_use 漏成正文 / 直接吐空,回来的轮 `tool_calls=[]` 且正文空,`loop.py` 的 `if not tool_calls: done` 当"模型答完"收尾——run_status=idle、无报错、无终态失败,表现为"自己中断",只能人肉挖 DB 才发现;复测该 bug 现已消失/瞬态,但"空 tool_calls 直接 done"这个失败模式本身太隐蔽,任何 provider 未来吐一次空都会静默卡死,故加防御)。**两层**(对称既有畸形 salvage 链):① 自动重试——`_is_empty_response`(tc 空且 content 去空白为空;纯 tool_call 轮不误判)接进 `_stream_llm` 的 attempt 循环,和畸形同类:丢弃本轮走非流式重试(多数瞬态重发一次即好,用户无感),`_log_empty_response` stdout+`usage_events`(kind=`empty_response`,cost 0)双写留痕、DB 失败静默不阻塞;② 重试耗尽仍空——`run()` 收尾点分空-空分支:不发静默 `done`,改发**可见 warn**「模型返回空响应(已自动重试仍为空),已停止,回复『继续』可重试」+ done(复用 stall 熔断的 warn+自停话术,run_status 落 idle 可续),**不引入终态 error**(空响应可恢复,红 error 过重且与既有路径不对称)。③ 可观测——`core/toolfail.py` 加第四段扫描 kind=empty_response 聚成 tool=`(empty)`/kind=`empty` 的 cluster(sample=model_profile 看哪个网关档在吐空),即便自动救回也留痕、跨 task 系统性吐空才冒头(单次瞬态 <阈值不触发)。**刻意不做**内容嗅探式"narrated tool_call 特征→重试":marker 每次变、且与助手正常解释/贴代码的正文高度重叠,假阳性(把合法回答判坏反复重试)比它防的罕见静默停更糟——那类只靠面板留痕兜、不改热路径行为。测试:`tests/test_loop_empty_response.py`(_is_empty_response 四态含纯 tool_call 不误判 + 重试恢复/耗尽返空/正常文本不重试)、`tests/test_toolfail_malformed.py` +2(空响应聚集 / 单次瞬态不触发),顺手补回 `test_loop_malformed_retry.py` 因 salvage(0.58.24)落地后失修的 `loop.executor` 桩(空 schemas → salvage 返 False 走既有重试)。全 247 测试绿。盲区:provider-wire 抖动本机复现不出、/verify 端到端跑不了,靠单测+生产灰度。additive 内部机制,不碰对外 API/DB 契约。相关 memory:unifyllm tool_use 漏正文案 / deepseek 畸形 salvage 案。
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||||
# 改版本只动这一行。
|
# 改版本只动这一行。
|
||||||
__version__ = "0.58.34"
|
__version__ = "0.58.35"
|
||||||
|
|
|
||||||
|
|
@ -398,6 +398,16 @@ def _build_system_prompt(
|
||||||
f"普通产物(sections / slides / 终稿 .docx/.pptx)按 SKILL 文档落路径;"
|
f"普通产物(sections / slides / 终稿 .docx/.pptx)按 SKILL 文档落路径;"
|
||||||
f"「宪法」性文件(spec 等)按下面《task 级「宪法」文件命名约定》拼路径。\n"
|
f"「宪法」性文件(spec 等)按下面《task 级「宪法」文件命名约定》拼路径。\n"
|
||||||
f"⛔ 不要把产物写到 cwd / `skills/` / repo 根 —— 只写到 task_dir。\n"
|
f"⛔ 不要把产物写到 cwd / `skills/` / repo 根 —— 只写到 task_dir。\n"
|
||||||
|
f"\n## 生成 Word 报告 / 文档(验收 / 技术 / 评审报告等自由长文)\n"
|
||||||
|
f"**优先**把正文写成 Markdown(`<task_dir>/sections/*.md`,纯文本、零转义 / 零语法风险),"
|
||||||
|
f"再调平台渲染器出 docx —— **别在 run_python 里手撸 python-docx 内联大段中文正文**"
|
||||||
|
f"(中文引号 / 全角标点混进源码极易 SyntaxError、反复返工烧 token):\n\n"
|
||||||
|
f" python /sandbox/rendering/render.py --profile report --format docx <task_dir>/sections/ -o <task_dir>/<报告名>.docx\n\n"
|
||||||
|
f"渲染器自动管字体(正文宋体小四 / 标题黑体 / 1.5 倍行距)、目录(加 `--toc`)、"
|
||||||
|
f"表格、`` 居中插图 + 图题编号、mermaid 图。已加载 proposal / paper 等 skill 时"
|
||||||
|
f"按其 render 指引的 profile 走(更具体),通用报告用 `--profile report`。\n"
|
||||||
|
f"这是**软建议不是强制**:确需精细定制版式 / 改写已有 docx / 处理 xlsx 等渲染器覆盖不了的,"
|
||||||
|
f"照样用 run_python 自由写代码。\n"
|
||||||
f"\n## task 级「宪法」文件命名约定(跨 skill 通用)\n"
|
f"\n## task 级「宪法」文件命名约定(跨 skill 通用)\n"
|
||||||
f"跟 task 1:1 绑定、后续步骤会**反复 read** 的「宪法」性文件(如 proposal/ppt 的 "
|
f"跟 task 1:1 绑定、后续步骤会**反复 read** 的「宪法」性文件(如 proposal/ppt 的 "
|
||||||
f"spec、outline),统一落 task_dir 根、按此格式命名:\n\n"
|
f"spec、outline),统一落 task_dir 根、按此格式命名:\n\n"
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,21 @@ PROFILES = {
|
||||||
"always_toc": True,
|
"always_toc": True,
|
||||||
"trailing_page_break": 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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ from rendering import docx_brief, docx_manuscript, pdf # noqa: E402
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
ap = argparse.ArgumentParser(description="md(sections 目录或单 .md)→ docx / pdf")
|
ap = argparse.ArgumentParser(description="md(sections 目录或单 .md)→ docx / pdf")
|
||||||
ap.add_argument("src", type=Path, help="sections 目录(拼接其 *.md)或单个 .md")
|
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("--format", default="docx", choices=["docx", "pdf"])
|
||||||
ap.add_argument("-o", "--output", type=Path, required=True, help="输出路径")
|
ap.add_argument("-o", "--output", type=Path, required=True, help="输出路径")
|
||||||
ap.add_argument("--no-color", dest="color", action="store_false",
|
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":
|
elif args.profile == "paper":
|
||||||
docx_manuscript.render_sections("paper", args.src, args.output,
|
docx_manuscript.render_sections("paper", args.src, args.output,
|
||||||
lang=args.lang, toc=args.toc)
|
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
|
else: # proposal
|
||||||
docx_manuscript.render_sections("proposal", args.src, args.output,
|
docx_manuscript.render_sections("proposal", args.src, args.output,
|
||||||
fund_type=args.fund_type)
|
fund_type=args.fund_type)
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ def _docx_paragraphs(path: Path) -> int:
|
||||||
|
|
||||||
|
|
||||||
class TestDocxProfiles(unittest.TestCase):
|
class TestDocxProfiles(unittest.TestCase):
|
||||||
def test_three_profiles_render(self):
|
def test_four_profiles_render(self):
|
||||||
with tempfile.TemporaryDirectory() as td:
|
with tempfile.TemporaryDirectory() as td:
|
||||||
d = Path(td)
|
d = Path(td)
|
||||||
sec = _write_sections(d)
|
sec = _write_sections(d)
|
||||||
|
|
@ -68,11 +68,33 @@ class TestDocxProfiles(unittest.TestCase):
|
||||||
out_r = d / "proposal.docx"
|
out_r = d / "proposal.docx"
|
||||||
docx_manuscript.render_sections("proposal", sec, out_r, fund_type="key_rd")
|
docx_manuscript.render_sections("proposal", sec, out_r, fund_type="key_rd")
|
||||||
self.assertTrue(out_r.exists() and out_r.stat().st_size > 0)
|
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 张表
|
# 每份都应有 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:
|
with zipfile.ZipFile(f) as z:
|
||||||
self.assertIn("word/document.xml", z.namelist())
|
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):
|
class TestChemSingleSource(unittest.TestCase):
|
||||||
def test_pdf_uses_common_chem(self):
|
def test_pdf_uses_common_chem(self):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue