diff --git a/DESIGN.md b/DESIGN.md index 9cce447..4611ac7 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -320,7 +320,8 @@ 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 内 → 相对 ROOT posix 串;ROOT 外 → 保留绝对 +-- working_dir 存储:相对 ROOT 的 posix 串(workspace/users//);写入入口 +-- 只接 simple name,越出 ROOT → to_db_path raise(不留 ROOT 外路径) -- 读写边界统一过 core/paths.py::{to_db_path, from_db_path} -- 入口校验 validate_task_name():拒空 / 含 /\NUL / `.` 起头 / >255 diff --git a/PROGRESS.md b/PROGRESS.md index 182ab25..212305d 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -27,6 +27,8 @@ - **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 → 占位 ``,其他 → 沿用 `.art-chip`;新 `upgradeMediaArtifacts(root)` DOM walk 把占位异步换 ``/`