fix(rendering+chip): 修 task d9c74a5a 三处独立缺陷——chip 粗体路径 404 / docx emoji 缺字 / 单文件被迫建 sections(bump 0.58.37)
一次"今日新闻 md→word"暴露三个不相干的坑,逐个定层修: 1) 前端文件 chip 点开弹框 404 —— media.js::extractArtifactRels 的 tail 字符集 _TAIL_CLS 没排除 `*`,助手把路径写成 markdown 粗体 `**<wd>/x.md**` 时前导 `**` 被 lead 边界吃掉、尾部 `**` 被贪婪吞进 rel(星号既不在 tail 集也不在尾部 strip 集),生成 `x.md**` → /v1/files/download 404。修:`*` 加进 _TAIL_CLS 排除集,同反引号一样 在 emphasis 处干净截断;文件名含 `*` 在 Windows 非法、误伤≈0。 2) 导出 word 图标(emoji)缺失 —— docx_manuscript 正文/标题字体是宋体/ 黑体/Times New Roman,无 emoji 字形,📰🔥🇨🇳📊 写进宋体 run → Word 渲染成豆腐块。修:common.py 加 EMOJI_RE + add_fonted_runs (按 emoji 边界拆 run,emoji 段走 Segoe UI Emoji、其余走原中西文字体), 接进 _emit_plain_with_chem(不破坏化学式下标)与 add_inline (bold/italic/code);标题走 add_inline 自动覆盖。 3) 过程中冒出 sections/ 文件夹 —— render.py docstring 承诺 src 可为 "目录或单个 .md",但 docx 三 profile 的 render_sections 全是 is_dir()+ glob 只认目录(只有 pdf 支持单文件),模型传单 .md 撞 `sections dir not found` → 被迫 mkdir sections && cp 兜底。修: common.py 抽 collect_md_files(文件→[src],目录→glob),docx_manuscript + docx_brief 共用,figures 基准取 src.parent;兑现 docstring 承诺。 验证(实跑非仅单测):emoji 满满的今日新闻 md 跑单文件 --profile report 退出 0(不再报 sections 缺失),解包 docx 校 run 字体——emoji 全 Segoe UI Emoji、中文仍宋体、SiO2/CaCO3 下标未被破坏;正则修复 4 组样本 (粗体/反引号+间隔号/裸路径+中文句号/斜体)全绿;目录多文件+brief+ 不存在输入三路回归无破坏。三处互不相干可独立修,均 additive,不碰对外 API/DB 契约。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1581b69bfa
commit
eb45e771fa
|
|
@ -5,6 +5,10 @@
|
|||
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
|
||||
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
||||
|
||||
## 0.58.37 — 2026-07-17
|
||||
|
||||
- 修三处文件/文档相关的小毛病:①助手把文件路径写成**加粗**形式时,对应的文件小标签点开不再报 404,能正常弹预览了;②把带 emoji 图标(📰🔥📊 等)的 Markdown 转成 Word 后,图标不再变成空白方块 / 缺字,能正常显示;③单个 Markdown 文件转 Word 时更顺,不会再在你的目录里多出一个 `sections` 文件夹。
|
||||
|
||||
## 0.58.32 — 2026-07-15
|
||||
|
||||
- 模型偶发返回空内容时不再"无声停下":此前极少数情况下模型会回一个空响应,系统当成"已答完"直接结束,任务看起来像自己中断了却没有任何提示。现在遇到空响应会自动重试(多数一次即恢复,你多半察觉不到);万一重试后仍为空,会明确显示「模型返回空响应,已停止,回复『继续』可重试」,不再让你对着停住的对话摸不着头脑。
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
### 2026-07
|
||||
|
||||
- **07-17 / 0.58.37**:**修 task d9c74a5a 复盘暴露的三处独立缺陷(chip 404 / docx emoji 缺失 / sections 文件夹被迫创建)**。一次"今日新闻 md→word"跑出三个不相干的坑,逐个定层修:**① 前端文件 chip 点开弹框 404**——`web/static/js/media.js::extractArtifactRels` 的路径 tail 字符集 `_TAIL_CLS` 没排除 `*`,助手把路径写成 markdown 粗体 `**<wd>/x.md**` 时,前导 `**` 被 lead 边界吃掉、但**尾部 `**` 被贪婪吞进 rel**(星号既不在 tail 集也不在尾部 strip 集),生成 `x.md**` 这种点开 `/v1/files/download?path=…md**` → 404 的坏 chip(用同款正则复现坐实结尾就是 `**`;反引号包裹的路径因反引号在排除集里天然无恙,偏这次用了粗体)。**修**:`*` 加进 `_TAIL_CLS` 排除集,同当初排除反引号一样在 emphasis 处干净截断;文件名含 `*` 在 Windows 非法、误伤≈0。**② 导出 word 里图标(emoji)缺失**——`rendering/docx_manuscript.py` 正文/标题字体是宋体/黑体/Times New Roman,**这些字体无 emoji 字形**,📰🔥🇨🇳📊 等直接写进宋体 run → Word 渲染成豆腐块/缺字。**修**:`rendering/common.py` 加 `EMOJI_RE`(圈主 emoji 区/区域指示符国旗/杂符+Dingbats/VS16/ZWJ,不含文本箭头等易误伤块)+ `add_fonted_runs`(按 emoji 边界拆 run,emoji 段走 `Segoe UI Emoji`、其余走原中西文字体),接进 `_emit_plain_with_chem`(正文,且不破坏化学式下标)与 `add_inline`(bold/italic/code);标题走 `add_inline` 自动覆盖。**③ 过程中冒出 sections/ 文件夹**——`render.py` docstring/argparse 都承诺 src 可为"目录**或单个 .md**",但 docx 三 profile(brief/paper/report/proposal)的 `render_sections` 全是 `is_dir()`+`glob("*.md")` **只认目录**(只有 pdf 真支持单文件),模型传单 .md → 撞 `sections dir not found` → 被迫 `mkdir sections && cp` 兜底留下垃圾目录。**修**:`common.py` 抽 `collect_md_files(src)`(文件→`[src]`,目录→glob),docx_manuscript + docx_brief 共用,figures 基准取 `src.parent`;兑现 docstring 承诺、模型不再需建 sections/。**验证**(/verify 精神,实跑非仅单测):还原 emoji 满满的今日新闻 md 跑单文件 `--profile report`,退出 0(不再报 sections 缺失),解包 docx 校 run 字体——📰🔥🇨🇳🏙️📊 全 `Segoe UI Emoji`、中文仍宋体、SiO₂/CaCO₃ 下标 `[SUB]` 未被破坏;正则修复用 4 组样本(粗体/反引号+间隔号/裸路径+中文句号/斜体)全绿。三处分属前端 chip 提取 / docx 字体 / render 契约,互不相干可独立修;均 additive,不碰对外 API/DB 契约(render.py 契约本就承诺单文件,现名副其实)。
|
||||
- **07-17 / 0.58.36**:**run_python 超时返回超时前的部分输出 + 无缓冲(-u),让模型据此续跑不整批重来**(失败面板 #8:`command timed out` 19 次 / 7 task / 6 用户,累计最多)。**定性**(查 19 条超时的原始 tool_call 代码):不是死循环 / chromium 老坑,全是**真慢的批量 I/O**——① 论文检索 `skills.research.paper.search()` 循环搜多个期刊 / 关键词(最多,~11 条,每次打 OpenAlex 外部 API 累计爆超时)② pdfplumber 多 PDF `extract_text` ③ 大文件下载(ffmpeg tar.xz / MDPI PDF)④ pandas 大 Excel。超时机制本身正常,真正浪费在 **`_run_subprocess` 超时分支把已捕获的部分输出全丢了**(kill 后 `communicate()` 已续读超时前 stdout,却只回一句 `[Error] command timed out` → 模型看不到「搜到第 6 个」、整批重搜,同 task 反复重跑)。**修**:① `tools/base.py::format_timeout_result` 把超时前的 stdout/stderr 一并返回 + 续跑提示(据已完成部分续跑 / 每项落盘 / 大操作用 `background=true` / 单次别塞太多),docker(`_run_subprocess`)+ host(`run_python.py` 的 `TimeoutExpired.stdout/stderr`)两执行器共用;② **`-u` 无缓冲**——Python 管道下 stdout 块缓冲,不 flush 的 `print` 超时被 kill 时缓冲区就丢了,故 run_python 的 python 调用全加 `-u`(docker 前台 inline / script_path + host 前台 + 两处后台 `procs.launch_host`),让部分输出真正落管道被捕获(不碰 shell)。这里提示指路(background / 续跑)与语法预检不同、**恰当**:超时就是「你这段太慢」的确定信号,建议对任何超时都可靠,不像语法→渲染器那样分不清场景。**不做**:不抬默认 timeout(120s 合理,抬高只延后同问题 + 多占 loop);不单独改 prompt(提示随超时结果反应式投放更精准)。测试 `tests/test_timeout_partial.py`(format 单测 + host 端到端:print 两项后 sleep,timeout=1 → 结果带两项 + 续跑提示)+ `test_executor_docker` argv 断言更新为 `python -u`,全 34 相邻测试绿。additive(超时结果加信息,不破坏 schema),不碰对外 API/DB 契约。至此失败面板 top 全部处置(② edit streak / ③ 畸形 / ① SyntaxError 预检+渲染器 / #8 超时);char-0 畸形由既有 salvage 覆盖。相关 memory:高轮数烧 token 三根因 / 沙箱 chromium 案(本次确认非 chromium)。
|
||||
- **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 案。
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||
# 改版本只动这一行。
|
||||
__version__ = "0.58.36"
|
||||
__version__ = "0.58.37"
|
||||
|
|
|
|||
|
|
@ -55,6 +55,49 @@ def set_subscript(run) -> None:
|
|||
rPr.append(va)
|
||||
|
||||
|
||||
# ───────────────────────── emoji 字体回退 ─────────────────────────
|
||||
|
||||
# 正文/标题字体是宋体/黑体/Times New Roman —— 这些字体**没有 emoji 字形**,直接把
|
||||
# emoji 写进这类 run,Word 里会渲染成豆腐块/缺字(用户视角"图标缺失",踩过 task
|
||||
# d9c74a5a 的今日新闻 docx)。做法:识别 emoji 码点段,单独开 run 走 Segoe UI Emoji
|
||||
# (Windows Word 自带彩色 emoji 字体),其余文字仍走原中西文字体。范围只圈真正的
|
||||
# emoji 常用块,不含文本箭头(2190-21FF)等易误伤的符号。
|
||||
EMOJI_RE = re.compile(
|
||||
"([\U0001F300-\U0001FAFF" # 主 emoji 区(符号图形/补充/扩展-A,含 📰🔥📊🔹)
|
||||
"\U0001F1E6-\U0001F1FF" # 区域指示符(国旗,如 🇨🇳)
|
||||
"\U00002600-\U000027BF" # 杂项符号 + Dingbats(☀✅✨✂ 等)
|
||||
"\U00002300-\U000023FF" # ⌚⏰⏱ 等
|
||||
"\U00002B00-\U00002BFF" # ⬆⭐ 等
|
||||
"\U0000FE00-\U0000FE0F" # 变体选择符(VS16 强制 emoji 呈现,如 🏙️)
|
||||
"\U0000200D]+)" # ZWJ(emoji 连字)
|
||||
)
|
||||
|
||||
_EMOJI_FONT = "Segoe UI Emoji"
|
||||
|
||||
|
||||
def add_fonted_runs(paragraph, text: str, *, size=None, cn_font: str = "宋体",
|
||||
en_font: str = "Times New Roman", bold: bool = False,
|
||||
italic: bool = False) -> None:
|
||||
"""把 text 写成若干 run:emoji 段走 Segoe UI Emoji(否则宋体/黑体缺字形→豆腐块),
|
||||
其余段走给定中西文字体。size/bold/italic 施加到每个 run。"""
|
||||
if not text:
|
||||
return
|
||||
for part in EMOJI_RE.split(text):
|
||||
if not part:
|
||||
continue
|
||||
r = paragraph.add_run(part)
|
||||
if size is not None:
|
||||
r.font.size = size
|
||||
if bold:
|
||||
r.bold = True
|
||||
if italic:
|
||||
r.italic = True
|
||||
if EMOJI_RE.fullmatch(part):
|
||||
set_run_fonts(r, cn_font=_EMOJI_FONT, en_font=_EMOJI_FONT)
|
||||
else:
|
||||
set_run_fonts(r, cn_font=cn_font, en_font=en_font)
|
||||
|
||||
|
||||
# ───────────────────────── 内联 markdown 切分 ─────────────────────────
|
||||
|
||||
# 顺序敏感:**bold** 必须先于 *italic* 匹配, 否则会被 italic 抢
|
||||
|
|
@ -141,3 +184,17 @@ def resolve_image_path(src: str, base_dir: Path) -> Path | None:
|
|||
if not p.is_absolute():
|
||||
p = (base_dir / p).resolve()
|
||||
return p if p.is_file() else None
|
||||
|
||||
|
||||
# ───────────────────────── sections 输入收集 ─────────────────────────
|
||||
|
||||
def collect_md_files(src: Path) -> list[Path]:
|
||||
"""把 render 入口的 src 归一为 md 文件列表:单个 .md → [src],目录 → 内部
|
||||
*.md 排序。空则 [](调用方报错)。render.py 三 docx profile 与 pdf 共用同一
|
||||
"目录或单文件"契约 —— 此前 docx 侧只认目录,单文件报 `sections dir not found`,
|
||||
逼模型 mkdir sections/ 再 cp 进去兜底(踩过 task d9c74a5a),现统一放开。"""
|
||||
if src.is_file():
|
||||
return [src]
|
||||
if src.is_dir():
|
||||
return sorted(src.glob("*.md"))
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ from .common import (
|
|||
is_separator_row as _is_sep_row,
|
||||
resolve_image_path as _resolve_image_path,
|
||||
MAX_IMG_WIDTH as _MAX_IMG_WIDTH,
|
||||
collect_md_files as _collect_md_files,
|
||||
)
|
||||
|
||||
# ───────────────────────── 主题色 ─────────────────────────
|
||||
|
|
@ -624,17 +625,15 @@ def render_md_block(doc: Document, md_text: str, ctx: dict) -> None:
|
|||
# ───────────────────────── 入口 ─────────────────────────
|
||||
|
||||
def render_sections(sections_dir: Path, out: Path, color: bool) -> None:
|
||||
if not sections_dir.is_dir():
|
||||
print(f"[ERR] sections dir not found: {sections_dir}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
md_files = sorted(sections_dir.glob("*.md"))
|
||||
md_files = _collect_md_files(sections_dir)
|
||||
if not md_files:
|
||||
print(f"[ERR] no .md found in {sections_dir}", file=sys.stderr)
|
||||
print(f"[ERR] no .md found: {sections_dir}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
base_dir = sections_dir if sections_dir.is_dir() else sections_dir.parent
|
||||
ctx = {
|
||||
"sections_dir": sections_dir,
|
||||
"figures_dir": sections_dir.parent / "figures",
|
||||
"sections_dir": base_dir,
|
||||
"figures_dir": base_dir / "figures",
|
||||
"fig_no": 0,
|
||||
"bm_id": 0,
|
||||
"color": color,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ from docx.oxml.ns import qn
|
|||
from docx.shared import Cm, Pt, RGBColor
|
||||
|
||||
from . import common
|
||||
from .common import set_run_fonts, set_style_fonts, set_subscript, CHEM_RE, parse_inline
|
||||
from .common import (set_run_fonts, set_style_fonts, set_subscript, CHEM_RE, parse_inline,
|
||||
add_fonted_runs, collect_md_files)
|
||||
|
||||
|
||||
# ───────────────────────── profile 配置 ─────────────────────────
|
||||
|
|
@ -150,46 +151,44 @@ def add_toc(doc: Document, prof: dict, depth: int = 3) -> None:
|
|||
# ───────────────────────── 内联(化学式下标)─────────────────────────
|
||||
|
||||
def _emit_plain_with_chem(paragraph, text: str, *, size, cn_font: str) -> None:
|
||||
"""plain 段:白名单化学式里的数字渲成下标,其余正常。无命中即一条普通 run。"""
|
||||
def _run(seg: str, sub: bool = False):
|
||||
if not seg:
|
||||
return
|
||||
r = paragraph.add_run(seg)
|
||||
"""plain 段:白名单化学式里的数字渲成下标,其余正常。非下标文字经 add_fonted_runs
|
||||
做 emoji 字体回退(宋体/黑体无 emoji 字形,否则显示成豆腐块)。"""
|
||||
def _plain(seg: str):
|
||||
add_fonted_runs(paragraph, seg, size=size, cn_font=cn_font, en_font="Times New Roman")
|
||||
|
||||
def _sub(ch: str):
|
||||
r = paragraph.add_run(ch)
|
||||
r.font.size = size
|
||||
set_run_fonts(r, cn_font=cn_font, en_font="Times New Roman")
|
||||
if sub:
|
||||
set_subscript(r)
|
||||
set_subscript(r)
|
||||
|
||||
pos = 0
|
||||
for m in CHEM_RE.finditer(text):
|
||||
_run(text[pos:m.start()])
|
||||
_plain(text[pos:m.start()])
|
||||
buf = ""
|
||||
for ch in m.group(0):
|
||||
if ch.isdigit():
|
||||
_run(buf); buf = ""
|
||||
_run(ch, sub=True)
|
||||
_plain(buf); buf = ""
|
||||
_sub(ch)
|
||||
else:
|
||||
buf += ch
|
||||
_run(buf)
|
||||
_plain(buf)
|
||||
pos = m.end()
|
||||
_run(text[pos:])
|
||||
_plain(text[pos:])
|
||||
|
||||
|
||||
def add_inline(paragraph, text: str, *, size: Pt = Pt(12), cn_font: str = "宋体") -> None:
|
||||
for style, seg in parse_inline(text):
|
||||
if style == "plain":
|
||||
_emit_plain_with_chem(paragraph, seg, size=size, cn_font=cn_font)
|
||||
continue
|
||||
run = paragraph.add_run(seg)
|
||||
run.font.size = size
|
||||
if style == "bold":
|
||||
run.bold = True
|
||||
set_run_fonts(run, cn_font=cn_font, en_font="Times New Roman")
|
||||
elif style == "bold":
|
||||
add_fonted_runs(paragraph, seg, size=size, cn_font=cn_font,
|
||||
en_font="Times New Roman", bold=True)
|
||||
elif style == "italic":
|
||||
run.italic = True
|
||||
set_run_fonts(run, cn_font=cn_font, en_font="Times New Roman")
|
||||
add_fonted_runs(paragraph, seg, size=size, cn_font=cn_font,
|
||||
en_font="Times New Roman", italic=True)
|
||||
elif style == "code":
|
||||
set_run_fonts(run, cn_font=cn_font, en_font="Consolas")
|
||||
add_fonted_runs(paragraph, seg, size=size, cn_font=cn_font, en_font="Consolas")
|
||||
|
||||
|
||||
# ───────────────────────── 段落 / 标题 / 列表 ─────────────────────────
|
||||
|
|
@ -419,15 +418,13 @@ def render_md_block(doc: Document, md_text: str, ctx: dict) -> None:
|
|||
def render_sections(profile: str, sections_dir: Path, out: Path, *,
|
||||
lang: str = "en", toc: bool = False, fund_type: str = "") -> None:
|
||||
prof = PROFILES[profile]
|
||||
if not sections_dir.is_dir():
|
||||
print(f"[ERR] sections dir not found: {sections_dir}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
md_files = sorted(sections_dir.glob("*.md"))
|
||||
md_files = collect_md_files(sections_dir)
|
||||
if not md_files:
|
||||
print(f"[ERR] no .md found in {sections_dir}", file=sys.stderr)
|
||||
print(f"[ERR] no .md found: {sections_dir}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
figures_dir = sections_dir.parent / "figures"
|
||||
base_dir = sections_dir if sections_dir.is_dir() else sections_dir.parent
|
||||
figures_dir = base_dir / "figures"
|
||||
ctx: dict = {
|
||||
"prof": prof,
|
||||
"sections_dir": sections_dir,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
"""Dump task d9c74a5a full row + messages (ASCII safe, prefix match on task_id)."""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
env = Path(__file__).resolve().parent.parent / ".env"
|
||||
for line in env.read_text(encoding="utf-8").splitlines():
|
||||
if line.strip().startswith("ZCBOT_DB_URL="):
|
||||
os.environ["ZCBOT_DB_URL"] = line.split("=", 1)[1].strip()
|
||||
from sqlalchemy import create_engine, text # noqa: E402
|
||||
|
||||
import builtins # noqa: E402
|
||||
|
||||
_out = open(Path(__file__).resolve().parent / "_task_d9c74a5a.txt", "w", encoding="utf-8")
|
||||
|
||||
|
||||
def print(*a, **k): # noqa: A001
|
||||
builtins.print(*a, **k, file=_out)
|
||||
|
||||
|
||||
engine = create_engine(os.environ["ZCBOT_DB_URL"])
|
||||
PREFIX = "d9c74a5a"
|
||||
|
||||
|
||||
def s(x, n=6000):
|
||||
t = str(x or "")
|
||||
return t if len(t) <= n else t[:n] + f"...[+{len(t)-n} chars]"
|
||||
|
||||
|
||||
with engine.connect() as conn:
|
||||
row = conn.execute(
|
||||
text("select * from tasks where task_id::text like :p"), {"p": PREFIX + "%"}
|
||||
).mappings().fetchone()
|
||||
if not row:
|
||||
print("[NO TASK]", PREFIX)
|
||||
sys.exit(1)
|
||||
TID = row["task_id"]
|
||||
print("[TASK ROW]")
|
||||
for k, v in dict(row).items():
|
||||
print(f" {k} = {s(v, 2000)}")
|
||||
|
||||
msgs = conn.execute(
|
||||
text("select idx,payload,model_profile,tokens_in,tokens_out,created_at from messages "
|
||||
"where task_id=:t order by idx"),
|
||||
{"t": TID},
|
||||
).fetchall()
|
||||
print(f"\n[MESSAGES] count={len(msgs)}\n")
|
||||
for idx, p, mp, ti, to, ca in msgs:
|
||||
if isinstance(p, str):
|
||||
try:
|
||||
p = json.loads(p)
|
||||
except Exception as e:
|
||||
print(f"[{idx}] !! payload JSON parse fail: {e}; raw head: {s(p, 500)}")
|
||||
continue
|
||||
role = p.get("role")
|
||||
head = f"[{idx}] {role}"
|
||||
if mp:
|
||||
head += f" ({mp})"
|
||||
if ti or to:
|
||||
head += f" tok={ti}/{to}"
|
||||
head += f" at={ca}"
|
||||
print(head)
|
||||
content = p.get("content")
|
||||
if content is not None:
|
||||
print(" content:", s(content, 2500))
|
||||
for tc in p.get("tool_calls") or []:
|
||||
fn = tc.get("function") or {}
|
||||
print(f" CALL {fn.get('name')}({s(fn.get('arguments'), 1200)})")
|
||||
if role == "tool":
|
||||
print(f" TOOL[{p.get('name')}] id={p.get('tool_call_id')}:", s(content, 1500))
|
||||
print()
|
||||
|
|
@ -94,7 +94,11 @@ export function extractMediaBanner(toolName, resultText) {
|
|||
// 注:间隔号 `·`(U+00B7)**不**排除 —— 它是文件名内部的合法分隔符(产品名
|
||||
// "AI赋能·xxx.pptx" 常见),几乎不当句尾标点用;排除它会在 `·` 处截断路径,把
|
||||
// 真产物 chip 吞没(踩过:含 `·` 的 pptx 不挂 chip)。尾部标点另有 strip 兜底。
|
||||
const _TAIL_CLS = "[^\\s\"'`<>(){}\\[\\]|,。、;:!?…“”‘’《》〈〉【】()「」『』]+";
|
||||
// `*` 排除:助手常把路径写成 markdown 粗体 `**<wd>/x.md**`,前导 `**` 已被 lead
|
||||
// 边界吃掉,但尾部 `**` 会被贪婪吞进 rel(星号既不在本集也不在尾部 strip 集),
|
||||
// 生成 `x.md**` 这种点开 404 的坏 chip(踩过:task d9c74a5a)。文件名含 `*` 在
|
||||
// Windows 非法、实际几乎不存在,排除误伤≈0,同反引号一样在 emphasis 处干净截断。
|
||||
const _TAIL_CLS = "[^\\s\"'`*<>(){}\\[\\]|,。、;:!?…“”‘’《》〈〉【】()「」『』]+";
|
||||
|
||||
export function extractArtifactRels(text, workingDir) {
|
||||
if (!text || !workingDir) return [];
|
||||
|
|
|
|||
Loading…
Reference in New Issue