diff --git a/PROGRESS.md b/PROGRESS.md index f7412ce..7b226fb 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 2-4 句:做了啥 + 关键判断 + 没动什么;细节查 `git log` / `git diff`。 -最后更新:2026-05-21(loop.py tool message append 补 `name` 字段 + 一次性 backfill 脚本回填历史 17 条 tool 消息 → 修历史 task 重开后 seedream banner/chip 不显示的 bug) +最后更新:2026-05-21(dev SPA chip 维度解绑产物白名单 → 通用工具结果里 echo 的路径也挂 chip,但图片/视频 inline 仍只对产物工具开;`renderArtifactBarHtml` 加 `allowInlineMedia` 参数) --- @@ -23,6 +23,8 @@ ### 2026-05-21 +- **dev SPA chip 维度解绑产物工具白名单 + `renderArtifactBarHtml` 加 `allowInlineMedia` 参数**:用户反馈"text 里必须挂 chip(不需要图片 banner,就是原先的 chip)"。复盘 `1e4548d`:它把"图片不该被无关工具误内联"和"chip 该不该挂"绑成同一个白名单 gate,砍多了 —— 用户看到 grep/read 结果里的路径直觉上想点开预览,但 chip 被一起锁了。**修法**:把 gate 降级到"图片/视频是否 inline"那层。`renderArtifactBarHtml(rels, allowInlineMedia=true)` 加第二参,false 时图片/视频也走 `.art-chip` 按钮(点开仍弹预览 modal,跟其它格式一致);4 处 tool 调用点(`renderMessages` tool 卡 + assistant tool_calls args + SSE tool_call + SSE tool_result)解绑 `ARTIFACT_PRODUCING_TOOLS.has(...)` gate,改成无条件 `extractArtifactRels`,只把 `inlineMedia = ARTIFACT_PRODUCING_TOOLS.has(name)` 透传给 `renderArtifactBarHtml` 的第二参;SSE 两处的 `upgradeMediaArtifacts(asstCard)` 也 gate 到 `if (inlineMedia)` 下,非产物工具就不发 blob fetch(不必要 / 也不会 inline);assistant 正文(行 1416)沿用默认 true,继续 inline。**两类白名单语义**:`ARTIFACT_PRODUCING_TOOLS` 现在专管"inline 大图/视频",chip 自身不受其限;`extractMediaBanner` 的"媒体 banner"(seedream/seedance tool_result 首行 model/size/cost/elapsed)仍单独白名单(它依赖工具协议的 key=value 格式,跟产物维度独立)。**Tradeoff**:① grep 一个老 PNG → 现在会挂 chip,但**不**会内联大图 — 用户能点开预览但屏幕不被无关老图占满;② 用户提了"绝对路径有些没挂 chip" — 推后再修,先验证当前方案符合预期。**没动**:`extractArtifactRels` 本身(regex 不变,现 4 处调用点都走它)、`_categorize` / chip 点击委托 / 媒体 blob 缓存、后端、DESIGN(纯前端 UX,无架构/schema 变化)、RUN(无对外行为变化)。 + - **loop.py tool message append 补 `name` 字段 + backfill 历史**:用户报"重开历史 task,seedream 生成的图既没有 elapsed banner 也没挂 chip"。根因:`core/loop.py` 第 161-167 行 append tool 消息时只写 `role/tool_call_id/content`,没存 `name`;前端 `dev.html` 历史渲染依赖 `payload.name` 判断是否产物工具(`ARTIFACT_PRODUCING_TOOLS.has(p.name)`)+ 抽 elapsed banner(`extractMediaBanner(p.name, ...)`),刷新后两者全黑。流式时正常 — SSE event 单独带 `name`(`_emit("tool_result", name=...)`),但 SSE 数据不入 DB,所以只有"刚生成那一刻"能看到。**修法**:loop.py 一行加 `"name": tc.function.name` 进 session.append 的 dict(OpenAI tool message spec 本来就有这字段,LiteLLM 接受);cancelled 占位那处(第 96-99 行)不动 —— 它的 content 是 `[cancelled by user]` 占位串,banner 正则匹配不上、chip 抽不出路径,挂 name 也无效果。**对比方案**:① 前端按 tool_call_id 反查上一条 assistant 的 tool_calls[].id → name(纯前端,不动后端 / DB)— 但每条 tool 消息渲染时都得线性扫之前所有 assistant 消息,O(n²) + 散在 5 个渲染点;② 用户提议的"路径带 user_id 前缀作为产物信号"— 不解决历史数据(已存内容里没 user_id 串)、判别力不够(grep/read echo 老图也会带)、违背 commit 9a7620f+5ff09b9 的 user_root-relative 简化方向。一行 fix + backfill 是改动最小同时彻底的方案。**Backfill 脚本** `scripts/backfill_tool_message_name.py`:按 task 分组扫 assistant.tool_calls 建 `tool_call_id → name` map,再扫该 task 的 tool 消息按 `tool_call_id` 查 map 补 name,`flag_modified` 标 JSONB 变更;默认 dry-run,`--apply` 真写;幂等(已有 name 跳过)。本地 17 条 tool 消息全部填上(seedream/glob/shell/read/write/load_skill 等),0 unresolved。**没动**:DB schema(payload 是 jsonb,加 key 无需 migration)、前端(已经在读 `p.name`,本来就支持只是历史数据没填)、其他 tool 调用路径、DESIGN(纯实现 bug,无架构变化)、RUN(无对外行为变化)。 ### 2026-05-20 diff --git a/web/static/dev.html b/web/static/dev.html index a290d85..672c429 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -1394,13 +1394,12 @@ function renderMessages(msgs) { const txt = typeof p.content === "string" ? p.content : JSON.stringify(p.content); const wd = _workingDirName(state.taskMeta && state.taskMeta.working_dir); const banner = extractMediaBanner(p.name || "", txt || ""); - const rels = ARTIFACT_PRODUCING_TOOLS.has(p.name || "") - ? pickFresh(extractArtifactRels(txt || "", wd)) - : []; + const rels = pickFresh(extractArtifactRels(txt || "", wd)); + const inlineMedia = ARTIFACT_PRODUCING_TOOLS.has(p.name || ""); card.innerHTML = `
${escapeHtml(txt || "")}${escapeHtml(args)}${escapeHtml(argsStr)}`;
asstCard.appendChild(det);
const wd = _workingDirName(state.taskMeta && state.taskMeta.working_dir);
- const fresh = ARTIFACT_PRODUCING_TOOLS.has(fn)
- ? extractArtifactRels(argsStr, wd).filter(r => !ctx.seenRels.has(r))
- : [];
+ const fresh = extractArtifactRels(argsStr, wd).filter(r => !ctx.seenRels.has(r));
fresh.forEach(r => ctx.seenRels.add(r));
- const barHtml = renderArtifactBarHtml(fresh);
+ const inlineMedia = ARTIFACT_PRODUCING_TOOLS.has(fn);
+ const barHtml = renderArtifactBarHtml(fresh, inlineMedia);
if (barHtml) {
asstCard.insertAdjacentHTML("beforeend", barHtml);
- upgradeMediaArtifacts(asstCard);
+ if (inlineMedia) upgradeMediaArtifacts(asstCard);
}
} else if (t === "tool_result") {
const txt = (ev.data && ev.data.result) || "";
@@ -1716,14 +1713,13 @@ function handleSseEvent(ev, asstCard, ctx) {
det.innerHTML = `${escapeHtml(txtStr)}`;
asstCard.appendChild(det);
const wd = _workingDirName(state.taskMeta && state.taskMeta.working_dir);
- const fresh = ARTIFACT_PRODUCING_TOOLS.has(toolName)
- ? extractArtifactRels(txtStr, wd).filter(r => !ctx.seenRels.has(r))
- : [];
+ const fresh = extractArtifactRels(txtStr, wd).filter(r => !ctx.seenRels.has(r));
fresh.forEach(r => ctx.seenRels.add(r));
- const barHtml = renderArtifactBarHtml(fresh);
+ const inlineMedia = ARTIFACT_PRODUCING_TOOLS.has(toolName);
+ const barHtml = renderArtifactBarHtml(fresh, inlineMedia);
if (barHtml) {
asstCard.insertAdjacentHTML("beforeend", barHtml);
- upgradeMediaArtifacts(asstCard);
+ if (inlineMedia) upgradeMediaArtifacts(asstCard);
}
scheduleFilesRefresh(); // 工具调用结果回来,FS 可能被改了,debounce 刷新右侧
} else if (t === "cancelled") {
@@ -2169,13 +2165,14 @@ function _workingDirName(workingDir) {
return segs[segs.length - 1] || "";
}
-// 产物工具白名单:只有这些工具的 tool_call args / tool_result 里 echo 的