Compare commits
No commits in common. "1e4548dd0cc5cb17b5bf98ff9e53f015f531e6a9" and "3c2e25d9125ba9b51e2bef78d9e5f1cbbcfe97d4" have entirely different histories.
1e4548dd0c
...
3c2e25d912
|
|
@ -320,8 +320,7 @@ tasks(task_id uuid pk, user_id fk, name text not null, working_dir text not null
|
|||
run_error text null,
|
||||
created_at, updated_at);
|
||||
create index on tasks (user_id, working_dir);
|
||||
-- working_dir 存储:相对 ROOT 的 posix 串(workspace/users/<uid>/<name>);写入入口
|
||||
-- 只接 simple name,越出 ROOT → to_db_path raise(不留 ROOT 外路径)
|
||||
-- working_dir 存储约定:ROOT 内 → 相对 ROOT posix 串;ROOT 外 → 保留绝对
|
||||
-- 读写边界统一过 core/paths.py::{to_db_path, from_db_path}
|
||||
-- 入口校验 validate_task_name():拒空 / 含 /\NUL / `.` 起头 / >255
|
||||
|
||||
|
|
|
|||
|
|
@ -23,14 +23,10 @@
|
|||
|
||||
### 2026-05-20
|
||||
|
||||
- **dev SPA chip 抽取改"产物工具白名单"门控(根因消 grep/read 类工具误挂无关文件 chip + 图片误 inline 预览)**:用户报"生成的图正常预览,但 grep 类工具的结果里 figures/ 下另一张老图也被 inline 出来了"。范围其实更大 — `extractArtifactRels` + `renderArtifactBarHtml` 是**通用产物展示**(image/video → inline,其他扩展名 → 可点 chip),所以 grep/read/shell/glob 等通用工具结果里 echo 的任何带扩展名路径(`.py`/`.md`/`.png`...)都会被当产物挂出来,图片只是其中最扎眼的一种;`seenRels` 只能去重同路径,挡不住"figures/ 下别的老图第一次出现"。**修法**:`web/static/dev.html` 新加 `ARTIFACT_PRODUCING_TOOLS = new Set(["seedream", "seedance"])` 白名单(产物维度,与 `extractMediaBanner` 的"媒体 banner 维度"解耦 — 将来若加"生成 docx 的工具",入这里但不入 banner 白名单),4 处工具 I/O 调用点全部用 `ARTIFACT_PRODUCING_TOOLS.has(toolName)` 三元短路:① `renderMessages` 的 `role==="tool"` 历史卡(行 1395-1404)② `renderMessages` 的 assistant `tool_calls` args(行 1422-1437)③ SSE `handleSseEvent` 的 `tool_call`(行 1692-1707)④ SSE `tool_result`(行 1714-1729);**assistant 正文(行 1417)不门控**,沿用 seenRels 兜底(助手主动 echo "刚生成的 xxx.png" 仍能挂 chip,seenRels 防同图重复)。**对比方案**:② 目录限制(regex 只匹配 `<wd>/figures/`)— 把通用 chip 系统降级为只服务 seedream,未来非媒体产物(pdf/docx/zip)就被锁死;③ 后端 tool_result 元信息带 `produced_files` 显式列表 — 最干净但 SSE / 历史回放 / seedream 都要改,改动量最大。**chip 系统的本意是"这次工具调用新产出的东西"**,grep/read 输出里的路径是"引用"不是"产物",white-list 在工具级过滤是正确语义,改动也最小。**Tradeoff**:`read figures/foo.png` 后老图不再挂 chip — 但这就是对的(读 ≠ 产);未来加新的产物生成工具需补白名单一行(成本极低,且本就该明确登记)。**没动**:`extractArtifactRels` / `renderArtifactBarHtml` 实现(它们仍 generic,只是调用入口被门控)、`_workingDirName` / 媒体 blob 缓存 / chip 点击委托、后端、DESIGN(纯前端 UX 修复,无架构/schema 变化)、RUN(无对外行为变化)。
|
||||
|
||||
- **dev SPA 输入区移除"⬆ 上传"按钮 + 加"✨ 润色"按钮(后端 `POST /v1/tasks/{id}/optimize_prompt`)**:用户反馈 ① 输入框下的"⬆ 上传"按钮意义不大(右侧文件面板已有同功能 `btn-upload`,完全重复) ② 加一个"润色"按钮 — 用户输简短草稿,根据当前对话模型 + 已选生图模型扩写/优化并替换原文本。**前端**:`web/static/dev.html` 删 `#chat-upload` 按钮 + 它的 onclick(line 1764);同位置加 `<button id="chat-optimize">✨ 润色</button>`(disabled 默认,textarea `input` 事件经 `syncOptimizeBtn` 联动启/禁,要求有内容 + 有 `state.taskId`)。点击 → `state.optimizing=true` + 按钮 disabled + 文案 "润色中…" + chat-hint "润色中…";POST 成功 → `textarea.focus(); textarea.select(); document.execCommand("insertText", false, optimized)` 把"全选 + 插入"作为一个 undo 单元接入 textarea **原生** undo 栈,Ctrl+Z 一次回到原文(execCommand 已 deprecated 但 textarea undo 集成所有浏览器仍支持);失败时贴 chat-hint 不动文本;execCommand 兜底失败(罕见旧浏览器)直接赋值 + 提示"本浏览器不支持撤销"。`sendMessage` 清空 textarea 后 + `renderChatMeta` 切 task 后各补一次 `syncOptimizeBtn`(value="" 不触发 input 事件,得手动 sync)。**后端**:`web/app.py` 加 `OptimizePromptRequest{text, image_model}` + `POST /v1/tasks/{tid}/optimize_prompt` handler — 校验 task 归属 user、text 非空 + ≤4000 字、用 `task.model_profile`(空 fallback default)装配 `LLM`,同步调 `chat()`(非 stream,短文本 3-5s 接受),meta-prompt 包含"当前对话模型 display_name + 选中 image variant 元数据(display_name / default_size / 适合画面细节描述)+ 4 条输出规则(只输出文本/保留原语言/补全模糊点不无中生有/长度合理)+ 用户草稿"。**计费决策**:写一行 `usage_events` **新 kind="prompt_optimize"**(`message_id=NULL`,`task_id` 仍挂当前 task,units 含 `tokens_in/out + usd_to_cny + image_model_hint`),**不** 调 `sync_task_tokens` → tasks 表的 `tokens_prompt/completion/cost_cny`(顶栏"N 条 · M tok"展示用)不被污染。心智:顶栏数字=主对话累计(用户感知),usage_events 全表 SUM=API 账单对账(润色也在内),按 `kind` GROUP BY 可单独评估"这个按钮值不值";单次成本 < ¥0.01(DeepSeek Pro)/ < ¥0.001(Flash)。**不与主对话 run 互斥**:它不写 messages 无 idx 竞争,允许 streaming 期间并行润色下一条草稿。**没动**:`record_chat_usage`(它 hardcode kind="chat",新增 kind 直接 `s.add(UsageEvent(...))` 内联,免给现有函数加参数污染)、DB schema(usage_events.kind 是 Text 列,加新值无需 migration)、image_model 处理(沿用 `_list_image_variants` 元数据)、视频模型(yaml 还没 video 段,等接 seedance 时同模式扩 video_model 字段)、DESIGN(无架构/schema 变化)。**Tradeoff**:① 不做用户偏好持久化(润色风格/温度);② 不接 streaming(短文本完整文本替换比逐字打印体验好,且 textarea undo 集成 execCommand 也只能一次性插入);③ 不接 prompt history(用户失败/退回的原文本可 Ctrl+Z 拿回,不需要服务端记历史)。**未浏览器实测**:HTML/JS 改动小但 textarea undo 行为需真浏览器跑一次确认 Ctrl+Z 链路;后端 `create_app()` import + route 注册通过(`/v1/tasks/{task_id}/optimize_prompt` 已在 routes 表)。
|
||||
|
||||
- **dev SPA 中间产物 chip / inline 图去重 + CLAUDE.md 新增"实施前先对方案"段**:用户报"工具结果里挂了一张图,后面 assistant 正文又挂了一张同图,有点重复"。根因:`renderArtifactBarHtml(extractArtifactRels(...))` 在 5 个渲染点都跑过 — `renderMessages` 里 tool 结果卡 / assistant 正文 / assistant tool_calls args 各一处,`handleSseEvent` 里 tool_call / tool_result 各一处。同一 rel 在 tool 结果与紧随 assistant 正文里同时出现(模型 echo 路径)→ 历史回放渲两次。修法:`renderMessages` 顶部建 `const seenRels = new Set()` + `pickFresh(rels)` 闭包,3 个调用点(tool 结果 / assistant 正文 / tool_calls args)全部包一层 — chronological 顺序,首次出现保留(tool 结果常在前),后续重复丢;SSE `ctx` 加 `seenRels: new Set()`,tool_call / tool_result 两 handler 共享去重。**对比 querySelector 版**:DOM 查询版 O(n²)(每条 card 渲染时扫 wrap 已有 `[data-rel]`),Set 版 O(n) 无查询,代码量相同还把"什么是 source of truth"明确(不依赖 DOM 已挂 chip 这个隐式状态)。**CLAUDE.md 增段**:开发期需求漂移快,非平凡改动(改 >1 文件 / 行为变化 / 多候选取舍)动手前先用自然语言把方案讲给用户确认,认可后再写代码;一次性 bug 修 / 字面量 / 样式微调可直接动手。方案描述要包含问题定位(文件 / 行号)+ 至少 1 个替代方案 + 涉及性能 / 兼容 / 数据迁移时主动说。**没动**:`extractArtifactRels` / `renderArtifactBarHtml` 实现(它们内部本身已 Set 去重单次调用内重复)、`_workingDirName` / chip 点击委托 / 媒体 blob 缓存、后端、DESIGN(纯前端 UX 修复)、RUN(无对外行为变化)。
|
||||
|
||||
- **2026-05-20 / paths.py 砍 ROOT 外路径**:`to_db_path` 越界 → raise(原 `str(pp)` 静默存绝对),`from_db_path` 删 `is_absolute()` 分支(只 `ROOT / s`)。**理由**:写入入口(`web/app.py POST /v1/tasks` → `working_dir_from_name`)只接 simple name join workspace,DB 里只可能存相对串(0002 migration 已清理历史绝对);ROOT 外分支是防御性死代码,符合 CLAUDE.md「不留兼容层」。**没动**:`core/storage/utils.py` no-subtask(`from_db_path` 接口同语义)、alembic(无数据需迁)、调用方(`web/app.py` / `core/agent_builder.py` / `core/export_docx.py` 全不改)。DESIGN §7.4 注释同步。
|
||||
|
||||
- **dev SPA 顶栏加生图模型下拉 + 中间产物图片/视频内联展示**:用户要 ① 项目栏右侧的模型选区加一个生图模型选择(目前只 seedream,默认选上),② 中间产物若是图片/视频直接在对话区展示(不只点击预览)。**生图选择范式判断**:不入 task 列(seedream/seedance 是 tool 范畴,non-chat,task 切粒度太粗;且现在仅一个 variant,加 DB 列纯负债)→ 走**消息级**:UI 下拉的选择跟 `POST /v1/tasks/{id}/messages` body 的 `image_model` 字段一起发,`_run_agent_bg` → `build_agent(image_variant=...)` → seedream tool 装配时按 key 挑 yaml 里 `image` 段的对应 variant_cfg;不入 DB,本 run 内多次 tool call 共用,下条消息可重选。**后端新接口** `GET /v1/image_models`(scan `config/media/doubao.yaml` image 段返 `{variant, display_name, model_id, price_cny_per_image, is_default}` 列表;不要求 `ARK_API_KEY` 已设 — UI 只展示元数据,真调时 `ArkConfig.load()` 那侧再过 key 检查),`_resolve_image_model(variant)` 校验存在性(空串 → 透传走 fallback,非空 → 必须命中 yaml,否则 400)。`agent_builder.build_agent` 新参 `image_variant: str = ""`:非空且命中 → 用它装 SeedreamTool;不命中(yaml 改动后旧选择 stale)静默回 fallback;空 → 沿用"取第一个 variant"。**前端**:`state.imageModels` + `state.imageModel`(per-session,不持久);`loadModels()` 同时拉 `/v1/image_models` 并锁第一个为默认;`renderImageModelDropdown()` 在 `renderModelDropdown` 旁画一个 `生图 [▾]`(yaml 无 variant 时不画);`onChangeImageModel` 纯前端 state 更新无 PATCH;`sendMessage` 把 `state.imageModel` 跟在 POST body 上发出去。**内联媒体**:`_EXT_GROUPS` 加 `video: {mp4,webm,mov,mkv,m4v}` 集合;`renderArtifactBarHtml` 按 `_categorize(rel)` 分支:image/video → 占位 `<span class="art-media[-image|-video]" data-rel="...">`,其他 → 沿用 `.art-chip`;新 `upgradeMediaArtifacts(root)` DOM walk 把占位异步换 `<img>`/`<video controls>`,经 `_fetchMediaBlobUrl(rel)`(Bearer header 不能直 `<img src=>` → fetch 拿 blob 转 `URL.createObjectURL`)+ `_mediaArtifactCache` 同 rel 复用;模态 `openFilePreview` 加 `_showVideo`(`<video controls autoplay>`);chip / `.art-media-image` 点击 → 弹模态放大,视频走原生 controls 不拦截(点击=暂停/播放,全屏走浏览器按钮,弹模态反而打断播放)。**缓存生命周期**:cache 走 `selectTask` 切换时 `_flushMediaArtifactCache` 撤销 blob URL;同 task 流式 / 历史回放复用,/clear 不清(FS 文件保留,rel 仍有效);logout 走 `location.reload()` 全清。**没动**:DESIGN(无架构/schema 变化)、Task / TaskState / TaskCreateRequest / TaskPatchRequest schema(早期一稿曾加 `Task.image_model` 列 + 0008 migration,用户复盘后判定 task 粒度不合适撤回 — 测试期 DB downgrade 不留 0008,模型/字段也清干净)、`record_image_usage`(model_profile 仍走 `doubao.<variant_key>`,自然跟着 build_agent 选的 variant)、artifact chip 抽取(其他类型仍走 chip + openFilePreview)。**Tradeoff**:① 不入 DB → 浏览器刷新会回到 yaml 第一个 variant 默认值,但用户中途切换的"上次选了什么"丢了 — 改用 localStorage 即可,先简单版;② 内联图片对每条带产物的消息都触发 fetch,blob URL 累积在 cache 里直到切 task 才回收,长会话场景 + 数十张图理论上内存吃几百 MB(普通 PNG 1-3MB),后续若问题再加 IntersectionObserver 懒加载。
|
||||
|
||||
- **LLM 调用切 streaming(cancel 秒退 + 前端打字机)+ 发送/停止合并单按钮**:用户反馈"点停止要等很久"+"发送/停止可以合并"。**问题 1 根因**:`litellm.completion(...)` 是同步阻塞,Python 没标准办法外部线程打断同步 IO;`broker.is_cancelled` 只在 `core/loop.py:run()` 每轮 LLM 前 + tool_calls 之间 poll,所以 cancel 必须等当前整轮 generation 跑完才生效(deepseek v4 + thinking + 长输出几十秒)。**修法**:切 `litellm.completion(stream=True)`,`core/llm.py` 加 `chat_stream()` generator(`stream_options={"include_usage": True}` 让最后 chunk 带 usage;`_build_kwargs` 抽出来给 chat/chat_stream 共用,免重复参数装配);`core/loop.py` 主循环改 `_stream_llm()` 流式迭代,chunk 间 poll cancel,命中 `break` + generator finally `stream.close()` 关底层 httpx 连接;chunks 攒齐用 `litellm.stream_chunk_builder(chunks, messages=...)` 拼回完整 response(自动处理 tool_call name/arguments 跨 chunk 拼接)给 tool_calls 解析 + usage 记账。**cancel 语义对齐**:stream 中途 cancel → 已收 chunk 丢弃不入库不记账(下次 resume 上下文干净);stream 完结后 tool_calls 之间 cancel → 沿用原 `_fill_cancelled_tool_results` 补 cancelled tool message。**前端打字机免费 bonus**:`dev.html:1500-1510` 早就备好接 `text` 事件的 `delta` 字段(rAF 节流 + nearBottom 不抢滚动 + 流中不跑 highlight),但后端原来发的是 `{"type":"text","content":"<整段>"}` 字段名对不上 → 前端永远 match 不到。新逻辑在 `_stream_llm` 里 chunk 到达即 `_emit({"type":"text","delta":...})`,前端自然激活打字机。loop.py 主流程末尾不再 emit 整段 text(content 已通过 delta 流过)。**问题 2 UI**:`web/static/dev.html` 把 `#chat-send`(发送)+ `#chat-cancel`(停止)合并为单 `#chat-action`,新 helper `setActionMode(mode)`(idle="发送" primary 红实心 / streaming="停止" danger 红边 / cancelling="停止中…" disabled);form submit + `chatAction()` 根据 `state.streaming` 分派 sendMessage / cancelCurrentTask;streaming 期间 Enter 不触发停止(textarea 编辑下一条草稿,误触发风险高)。**Smoke 验证**:① 18 chunks 流式 + 文本拼回 ✓ ② tool_call 49 chunks 跨片拼回 `{"a":7,"b":5}` 完整 ✓ ③ 提前 break + close 仅 0.7s(模拟"写 500 字散文中途 cancel")✓。**Tradeoffs**:① streaming 重试只在连接建立阶段(没拿到第一个 chunk 前)生效,中途断流不续 — 实务罕见;② timeout 行为从"整段 timeout"变"chunk 间隔 timeout",新模型接入要测 thinking 不吐 reasoning chunk 的极端情况;③ litellm `stream_chunk_builder` + `stream_options.include_usage` 在 deepseek/doubao/glm/openai 标准协议都正常,新接非主流 provider 时验证。**没动**:probe.py(仍用同步 `chat()`,离线探测不需要 cancel)、CLI 路径(probe 走 chat 不受影响)、broker / SSE 帧格式 / `record_chat_usage` 入参 / DB schema / messages 入库时机(拼回 response 跟非流式等价)。**文档**:`DESIGN.md` §3.1 翻转 tradeoff 表「LLM 同步 call 不可中断」→「LLM 调用走 streaming」+ §7 API 表 cancel 描述改 chunk-level 延迟;`RUN.md` cancel 接口 + 故障兜底表对应行同步;`web/app.py` 两条 docstring 同步。
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
"""working_dir 在 DB 与文件系统两种形态之间的归一(原 `task_dir` 已改名)。
|
||||
|
||||
存储约定(DESIGN §7.4):
|
||||
- working_dir → 相对 ROOT 的 posix 串(如 `workspace/users/<uid>/<name>`)
|
||||
- 空串 → 空串(legacy / 未绑项目;新建路径已 NOT NULL)
|
||||
- working_dir 在 ROOT 内 → 相对 ROOT 的 posix 串(如 `workspace/users/<uid>/<name>`)
|
||||
- working_dir 在 ROOT 外 → 绝对 str(如 `D:\\projects\\other\\proj` 或 `/home/u/proj`)
|
||||
- 空串 → 空串(legacy / 未绑项目)
|
||||
|
||||
写入入口(`web/app.py POST /v1/tasks` → `working_dir_from_name`)只接 simple name,
|
||||
拼到 `<workspace>/users/<uid>/<name>` 必在 ROOT 内 —— 越界视为 bug,`to_db_path` raise。
|
||||
跨机器迁移 / 切 OS / 移 repo 后,ROOT-内路径仍能 resolve;ROOT-外仍存绝对是务实选择
|
||||
—— 用户自指定的项目目录没有更好的归一基。
|
||||
|
||||
Read 端唯一入口:DB tasks.working_dir → `from_db_path(s)` → absolute Path。
|
||||
Write 端唯一入口:absolute Path → `to_db_path(p)` → DB 串。
|
||||
Read 端两种来源走两个入口:
|
||||
- DB tasks.working_dir → `from_db_path(s)` → absolute Path
|
||||
- 用户 CLI `--working-dir` / Web `/v1/tasks` 表单 → `Path(arg).expanduser().resolve()`
|
||||
|
||||
Write 端只通过 `to_db_path(absolute Path)` → DB 串。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -19,20 +23,28 @@ ROOT: Path = Path(__file__).resolve().parent.parent
|
|||
|
||||
|
||||
def to_db_path(p: Union[Path, str, None]) -> str:
|
||||
"""absolute Path / str → DB 串(相对 ROOT posix)。
|
||||
"""absolute Path / str → DB 串。
|
||||
|
||||
输入应已是绝对路径(build_agent / web 路由那一层都 .resolve() 过)。
|
||||
越出 ROOT 直接 raise —— 写入入口不允许外部目录(简单名 join workspace)。
|
||||
空 → ""。
|
||||
ROOT 内 → 相对 posix(`workspace/users/<uid>/<name>`)
|
||||
ROOT 外 → str(Path)(保留 OS 原生分隔符)
|
||||
空 → ""
|
||||
"""
|
||||
if not p:
|
||||
return ""
|
||||
pp = Path(p).resolve()
|
||||
return pp.relative_to(ROOT).as_posix()
|
||||
try:
|
||||
return pp.relative_to(ROOT).as_posix()
|
||||
except ValueError:
|
||||
return str(pp)
|
||||
|
||||
|
||||
def from_db_path(s: str) -> Path:
|
||||
"""DB 串(相对 ROOT posix)→ absolute Path。空 → Path("")(调用方判)。"""
|
||||
"""DB 串 → absolute Path。
|
||||
|
||||
相对串 → ROOT / s(再 resolve);绝对串 → resolve();空 → Path("")(调用方判)。
|
||||
"""
|
||||
if not s or not s.strip():
|
||||
return Path("")
|
||||
return (ROOT / s).resolve()
|
||||
p = Path(s)
|
||||
return p.resolve() if p.is_absolute() else (ROOT / p).resolve()
|
||||
|
|
|
|||
|
|
@ -1394,13 +1394,10 @@ 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))
|
||||
: [];
|
||||
card.innerHTML = `
|
||||
<div class="role">工具调用 · ${escapeHtml(p.name || "")}</div>
|
||||
<details class="tool-call"><summary>结果(${(txt || "").length} 字符)${banner}</summary><pre>${escapeHtml(txt || "")}</pre></details>
|
||||
${renderArtifactBarHtml(rels)}
|
||||
${renderArtifactBarHtml(pickFresh(extractArtifactRels(txt || "", wd)))}
|
||||
`;
|
||||
wrap.appendChild(card);
|
||||
continue;
|
||||
|
|
@ -1425,12 +1422,9 @@ function renderMessages(msgs) {
|
|||
try {
|
||||
args = JSON.stringify(JSON.parse((tc.function && tc.function.arguments) || "{}"), null, 2);
|
||||
} catch (e) { args = (tc.function && tc.function.arguments) || ""; }
|
||||
const rels = ARTIFACT_PRODUCING_TOOLS.has(fn)
|
||||
? pickFresh(extractArtifactRels(args, wd))
|
||||
: [];
|
||||
html += `
|
||||
<details class="tool-call"><summary>工具调用:${escapeHtml(fn)}</summary><pre>${escapeHtml(args)}</pre></details>
|
||||
${renderArtifactBarHtml(rels)}
|
||||
${renderArtifactBarHtml(pickFresh(extractArtifactRels(args, wd)))}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1697,9 +1691,7 @@ function handleSseEvent(ev, asstCard, ctx) {
|
|||
det.innerHTML = `<summary>工具调用:${escapeHtml(fn)}</summary><pre>${escapeHtml(argsStr)}</pre>`;
|
||||
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);
|
||||
if (barHtml) {
|
||||
|
|
@ -1716,9 +1708,7 @@ function handleSseEvent(ev, asstCard, ctx) {
|
|||
det.innerHTML = `<summary>工具结果${banner}</summary><pre>${escapeHtml(txtStr)}</pre>`;
|
||||
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);
|
||||
if (barHtml) {
|
||||
|
|
@ -2169,15 +2159,6 @@ function _workingDirName(workingDir) {
|
|||
return segs[segs.length - 1] || "";
|
||||
}
|
||||
|
||||
// 产物工具白名单:只有这些工具的 tool_call args / tool_result 里 echo 的 <wd>/...
|
||||
// 路径才挂 chip(图片/视频 inline 预览,其它类型为可点按钮)。通用工具
|
||||
// (grep/read/shell/glob 等)的 I/O 里出现的路径是引用而非新产物,放开会把命中
|
||||
// 的无关老文件全展示成"产物"。assistant 正文不受此限 —— 助手主动提到的路径仍
|
||||
// 挂 chip,seenRels 兜底防同文件重复。
|
||||
// 注:与 extractMediaBanner 的"媒体 banner"白名单是不同维度 —— 将来若新增
|
||||
// "生成 docx 的工具",入这里但不入 banner 白名单。
|
||||
const ARTIFACT_PRODUCING_TOOLS = new Set(["seedream", "seedance"]);
|
||||
|
||||
// 从 tool args / result / assistant 正文里抓 working_dir 下的文件路径,归一为 user_root 相对。
|
||||
// 启发式:把 \ 一律归 /,然后找以 `<wdName>/` 打头的串,要求最后一段含 . (像文件)。
|
||||
// 从 seedream/seedance tool_result 第一行 banner 抽 model/size/cost/elapsed,
|
||||
|
|
|
|||
Loading…
Reference in New Issue