From d3bd40fad752bd087a2a76693fd620083e6ff3c2 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Wed, 15 Jul 2026 13:40:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E5=AF=B9=E8=AF=9D=E6=AD=A3?= =?UTF-8?q?=E6=96=87=E6=96=87=E4=BB=B6=E9=94=9A=E7=82=B9=E4=B8=A4=E5=A4=84?= =?UTF-8?q?=E5=9B=9E=E5=BD=92=E2=80=94=E2=80=94=E5=90=AB=C2=B7=E7=9A=84=20?= =?UTF-8?q?pptx=20=E4=B8=A2=20chip=20+=20=E9=93=BE=E6=8E=A5=E7=82=B9?= =?UTF-8?q?=E5=87=BB=E6=95=B4=E9=A1=B5404(bump=200.58.29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 4 ++++ PROGRESS.md | 1 + core/__init__.py | 2 +- web/static/js/chat.js | 21 +++++++++++++++++++++ web/static/js/media.js | 5 ++++- 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c277c4..4ac5b3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ > 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。 > 工程口径的完整记录见 `PROGRESS.md` / git log。 +## 0.58.29 — 2026-07-15 + +- 修两处对话里文件相关的小毛病:助手回复正文里点文件路径链接不再整页报错(现在直接弹出预览,PPT 自动转成 PDF 看),文件名里带间隔号「·」的产物也能正常显示可点的文件小标签了。 + ## 0.58.27 — 2026-07-15 - 对话卡片新增「复制」按钮:鼠标悬停在任意一条消息上即出现,一键把该条正文原文(Markdown 源码,不带渲染样式)复制到剪贴板,复制成功会短暂提示「✓ 已复制」。 diff --git a/PROGRESS.md b/PROGRESS.md index 94f4bac..5e434e0 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -23,6 +23,7 @@ ### 2026-07 +- **07-15 / 0.58.29**:**修对话正文文件锚点两处回归(含 `·` 的 pptx 丢 chip + 正文文件链接点击整页 404)**。① chip 丢失:`media.js` 的 `_TAIL_CLS` 把间隔号 `·`(U+00B7)当尾部排除字符,而它是文件名合法分隔符(`AI赋能陶瓷产业·…-图标增强版.pptx`)→ 在 `·` 处截断成目录不挂 chip。去掉 `·`(尾部标点另有 strip 兜底);node 验证 OLD→`[]`、NEW→两路径全出。② 链接 404:模型把相对路径写成 `[]()`,`renderMd` 出 ``,但 `chat.js` 的 `#chat-stream` 点击委托无 `` 拦截 → 原生跳转打到 API 不存在路由返 FastAPI 默认 `{"detail":"Not Found"}`。加拦截 `.msg .body a[href]`:外链开新标签,内部相对路径 `openFilePreview(rel)`(复用预览 modal,pptx 转 PDF),`#` 锚点走默认。纯前端修复,不碰对外契约。 - **07-15 / 0.58.28**:**`mp_search_summary` 加批量入参 `formulas=[...]`(检索型烧 token 头号根因的定点解)**(诊断:`diag_tool_repeat.py` 全库扫,pymatgen task `fe2d8b73` 687 次 tool_call 里 `mp_search_summary` 占 **562 次**、重复率 91%——逐化学式一 formula 一轮走 agent loop 建氧化物表,562 份中间 JSON 全流经主上下文且每轮重发)。根因不是"检索发散"而是**工具只收单 `formula` + host-side 持 MP key(§7.5 #7 禁在 run_python 里循环)→ 被迫一式一调**。**这类"已知清单批量扇出"的正解是工具批量化,不是 delegate 子循环**(delegate 只把 562 轮搬进子上下文、基础 token 一分不省;批量化直接把 562 次并成 1 次工具调用+全部中间数据落盘不进对话)。实现:`formulas: list[str]`(≤300/次,超则报错分批)触发 batch 分支,复用**单个 MPRester 会话**逐式查(`num_chunks=1` fair-use 护栏不变)、每式按 `energy_above_hull` 升序(稳定相优先)、**单式出错不连坐整批**(记 `{"formula","error"}` 继续);结果聚合写 `working_dir/materials/mp_search_batch_.json`(内容寻址、同批幂等)、**只回紧凑 per-formula 摘要 + 路径**(N 个有结果/M 个空错 + 空错清单前 20),引导模型用 `read`/`run_python` 直读文件、勿逐条 dump。单查询路径(`formula`/`material_ids`/`elements`)**字节不变**保向后兼容;`working_dir` 设可选(裸构造 `MaterialsProjectSearchSummaryTool()` 不破,测试/smoke 无 working_dir 时降级内联返回)。`agent_builder` 注入 `working_dir_path`(同 `mp_get_structure`);pymatgen SKILL.md 加"多式用 `formulas` 批量"引导(WHY+WHAT,不写死步骤);`tests/test_secret_host_tools.py` +1(去重/落盘/稳定相排序/单式错误隔离/无 key 泄漏),原单查询测试补回 `chunk_size` 断言。additive 内部工具能力,不碰对外 API/DB 契约。**背景**:这是"检索型烧 token"三头(mp_search 562 批量扇出 / document_search 122 探索检索 / research code 试错抖动)里第一头的定点解;探索检索型留待 §8.11 delegate,代码试错归 RepeatGuard,不混为一谈。 - **07-15 / 0.58.27**:**对话卡片「复制」按钮(仿 GPT/DeepSeek 悬停操作栏)**(用户提议:卡片上加复制等按钮)。`renderMessages` 给每条 user 卡、以及有正文的 assistant 卡末尾挂 `.msg-actions` 栏(纯 tool_calls 轮 / error 卡不挂),默认 `opacity:0` 悬停/`focus-within` 才现、触屏(`hover:none`)常显淡态。复制取的是**正文原文**:点击经 `#chat-stream` 既有 click 委托据卡片 `data-idx` 回 `state.loadedMessages` 取 `payload.content`(Markdown 源,**不从渲染后的 `.body` HTML 反推**),`navigator.clipboard.writeText` 主路径 + `execCommand("copy")` 降级,按钮短暂反馈「✓ 已复制/复制失败」后复位。直播卡不带此栏——流结束后 `loadMessages` 整屏重渲成持久卡自然带上,省一处直播态特判。**范围收敛的理由**:后端是纯追加式会话,无「重跑最后一轮 / 截断历史 / 编辑分叉」语义,故本期只做零后端耦合的复制;重新生成 / 编辑重发若无后端截断支持会变成「复读上一句 / 历史里又追加一条」的语义欺骗,留待后端补重跑/截断能力再单独立项。只改 `chat.js`(生成+委托+剪贴板)+ `dev.html`(CSS),对外无契约变化。 - **07-15 / 0.58.26**:**用户停止后「已停止」持久提示(补齐 cancel 终态,与 error 卡对称)**(用户报:点停止后对话底部什么都不剩)。根因同 `2a1bc25d` 案:cancel 时前端 live-append 的 `已停止` badge 会被紧随的收尾 `loadMessages` 整屏重建(`renderMessages` 先 `innerHTML=""`)冲掉——闪一下即消失;而 error 之所以稳,是因为它落 `run_status="error"` 持久态 + `renderPersistedRunError` 每次重渲重派生,cancel 却在收尾直接回 `idle` 不留痕。**方案 B(架构对称):把 cancel 提升为一等持久终态**——`run_status` 是 `Text` 列(非 enum,**无 migration**),`agent.run` 在任一取消路径都 `return "[cancelled]"`(`loop.py`),`_run_agent_bg` 据此收尾写 `run_status="cancelled"`(否则 `idle`)。前端 `renderPersistedRunError`→`renderPersistedRunTerminal` 扩成同时认 error/cancelled:cancelled 渲居中 muted 虚线条「已停止 · 你中断了上次运行」(良性态,不用错误卡的醒目样式),刷新/切任务仍在,下次起新 run(`post_message` 写 running)覆盖清掉。删掉会被冲掉的 live badge(及其死 CSS `.cancelled-badge`)。**未选方案 A(客户端临时 flag)**:它会把刚被 `2a1bc25d` 修掉的「终态挂 live-only DOM」反模式请回来(刷新即死、跨标签页不通、对 broker 的 DB 态机制不可见),且与 10 行外的 error 路径不对称。兼容性:`run_status` 加新值是纯增量(不改既有值语义),蓝绿旧实例读到 `cancelled` 落到 `in ("running","cancelling")` 判断的 else 当良性态、旧前端只认 `error`→优雅退化到今天行为、不崩;全部 run_status 消费点(起 run 409 门/reaper/SSE is_active/cancel 端点/clear_messages)逐一核对 cancelled 均按 idle/error 类良性态处理。filter allowlist + DESIGN §7.4 尾句 + models.py 注释同步。 diff --git a/core/__init__.py b/core/__init__.py index 149f3ea..1bd0236 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.58.28" +__version__ = "0.58.29" diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 1d404eb..7d7d93d 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -1933,6 +1933,27 @@ $("chat-stream").addEventListener("click", (e) => { if (inlineImg) { const rel = inlineImg.dataset.rel; if (rel) openFilePreview(rel); + return; + } + // 正文里的 markdown 链接:模型常把工作区相对路径写成 [](),renderMd 出 。 + // 无拦截时原生跳转会打到 API server 不存在的路由 → FastAPI 默认 {"detail":"Not Found"} + // 整页糊脸。拦下来:外部链接开新标签,内部相对路径复用文件预览 modal(与 chip 同路径, + // pptx 自动转 PDF 预览)。href 取 getAttribute 原样值(模型写的相对路径正好是 user_root 相对)。 + const link = e.target.closest && e.target.closest(".msg .body a[href]"); + if (link) { + const raw = link.getAttribute("href") || ""; + if (!raw || raw.startsWith("#")) return; // 页内锚点 / 空 → 走默认 + if (/^(https?:|mailto:|tel:|blob:|data:)/i.test(raw)) { + e.preventDefault(); + window.open(raw, "_blank", "noopener"); + return; + } + e.preventDefault(); + let rel = raw.replace(/^sandbox:/i, "").replace(/^\.?\/+/, ""); + try { rel = decodeURIComponent(rel); } catch (_) {} + rel = rel.replace(/^\/+/, ""); + if (rel) openFilePreview(rel); + return; } }); diff --git a/web/static/js/media.js b/web/static/js/media.js index 321978e..b160220 100644 --- a/web/static/js/media.js +++ b/web/static/js/media.js @@ -91,7 +91,10 @@ export function extractMediaBanner(toolName, resultText) { // 路径 tail 排除集:空白/引号/括号类 + 常见中文标点 —— 中文正文里路径后常紧贴 // ",请查看"这类标点(无空格),不排除会把标点连后文一起吞进文件名,生成点开 404 // 的坏 chip。文件名几乎不含这些中文标点,排除的误伤面≈0。 -const _TAIL_CLS = "[^\\s\"'`<>(){}\\[\\]|,。、;:!?…·“”‘’《》〈〉【】()「」『』]+"; +// 注:间隔号 `·`(U+00B7)**不**排除 —— 它是文件名内部的合法分隔符(产品名 +// "AI赋能·xxx.pptx" 常见),几乎不当句尾标点用;排除它会在 `·` 处截断路径,把 +// 真产物 chip 吞没(踩过:含 `·` 的 pptx 不挂 chip)。尾部标点另有 strip 兜底。 +const _TAIL_CLS = "[^\\s\"'`<>(){}\\[\\]|,。、;:!?…“”‘’《》〈〉【】()「」『』]+"; export function extractArtifactRels(text, workingDir) { if (!text || !workingDir) return [];