feat(paths): 对外路径统一全形式 <wd_name>/<rel> + UI 一次性兼容历史简写
system prompt 加硬约束: 助手 echo 产物文件路径必须用 user_root 相对全形式 <wd_name>/<rel> (<wd_name> = task_dir 末段, 如 生图测试/videos/xxx.mp4), 不简写为 videos/xxx.mp4 这种 task 内裸形式 -- Web UI 按 <wd_name>/ 前缀挂 chip, 简写 → chip 失效用户点不开。媒体 tool (seedream/seedance) 的 saved: 行已是规范形式可直接照抄, ppt/proposal/coding 等 run_python/write 写文件时 自己拼。跨所有产物 skill 统一生效。 imagegen/videogen SKILL 把"把 saved: xxx 告诉用户"重复教学改成"照抄 saved 行, 详见 system「路径」段" (避免协议漂移, 新产物 skill 不用重复教育)。 ppt/proposal 等 SKILL 不动 -- system 协议自动管。 dev.html extractArtifactRels 加一次性兼容兜底: 产物目录裸路径 videos/xxx.<ext> / figures/xxx.<ext> (协议刚性前历史简写) prepend <wdName>/ 拼成 user_root rel。**白名单显式枚举两项不扩展**, 长期老消息 归档后整段可删。 术语校准: 前缀叫 <wd_name> (working_dir 末段) 而非 <task_name> -- 用户允许 wd_name ≠ task_name, _display 锚 user_root 出来的是 <wd_name>。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5faff8a127
commit
eec7eb156f
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
||||
|
||||
最后更新:2026-05-22(dev SPA 手机自适应:tab 单列 + tablet rail 强制)
|
||||
最后更新:2026-05-22(对外路径协议刚性化:system 强约束 + UI 一次性兼容)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
### 2026-05-22
|
||||
|
||||
- **对外路径协议刚性化(system 强约束 + SKILL 简化 + UI 一次性兼容)**:`prompts/system/general_v1.md`「路径」段加规则 —— 助手对外 echo 产物路径必须用 user_root 相对全形式 `<wd_name>/<rel>`(`<wd_name>` = task_dir 末段,如 `生图测试/videos/xxx.mp4` / `基金申报/sections/01-绪论.md` / `公司汇报/slides/deck.pptx`),不简写为 `videos/xxx.mp4` 这种 task 内裸形式(Web UI 按 `<wd_name>/` 前缀挂 chip,简写 → chip 失效用户点不开)。媒体 tool(`seedream` / `seedance`)的 `saved:` 行已是规范全形式可直接照抄,其他场景(ppt / proposal / coding 等 run_python/write 写文件)自己拼。**跨所有产物 skill 统一生效**(不止 imagegen/videogen)。`skills/imagegen/SKILL.md` + `skills/videogen/SKILL.md` 把原有"把 `saved: xxx` 告诉用户"重复教学改成"照抄 saved 行,详见 system「路径」段"(消除 skill 内具体写法 → 协议归一到 system,新产物 skill 不用重复教育)。ppt/proposal/coding 等 SKILL **不动** —— 它们只泛说"告诉用户文件路径"没教错,system 协议升级后助手自然按全形式 echo,加 skill 提醒反而是协议漂移源。`web/static/dev.html::extractArtifactRels` 加一次性兼容兜底:产物目录裸路径 `videos/xxx.<ext>` / `figures/xxx.<ext>`(协议刚性前历史简写)prepend `<wdName>/` 拼成 user_root rel —— 白名单显式枚举两项不扩展、长期老消息归档后整段可删。**术语校准**:前缀叫 `<wd_name>`(working_dir 最后一段)而非 `<task_name>` —— 用户允许 `wd_name ≠ task_name`(`build_agent` wd_raw 走 working_dir 字段独立可指定),`_display` 锚 user_root 出来的是 `<wd_name>`,SKILL/system 早期写 `task_name` 在分叉场景下会误导助手拼错前缀。否决:(a) 后端 `_display` 改 task-relative 让 tool 输出本身就裸 —— `Tool` 基类 + fs/skill_tool/seedream/seedance/agent_builder/smoke 改 8 个文件,且 fs 跨 task 时要分层 fallback(working_dir → user_root → 绝对),复杂度超过收益;(b) 后端补 HEAD 探针让前端验文件存在再挂 chip —— 工程量与开发期需求不匹配;(c) 白名单常驻服务所有简写形式 —— 维护负担+清单可能膨胀,改成"一次性兼容历史消息"角色后边界清晰;(d) 每个写产物的 SKILL 各加一句"按 system 协议" —— 协议漂移源,违反"system 谈通用、SKILL 谈领域"边界。
|
||||
- **dev SPA 手机自适应:两档断点 + tab 单列**:`web/static/dev.html` 加 `@media`,**平板段(641-1024px)**纯 CSS 强制 rail(grid `40px 1fr 260px` + 左 pane 子项 `display:none` + 留 toggle 按钮),不写 localStorage —— 回桌面用户原偏好仍生效。**手机段(≤640px)**单列布局 grid `1fr` + `grid-template-areas:"head" "main"`,三 pane 都 `grid-area: main` 且默认 `display:none`,新加 `body.mv-{left,mid,right}` 控制当前可见 pane;header 加 `.mobile-tabs`(任务/对话/文件)桌面 `display:none`,手机段 `order:99 + flex-basis:100%` 换行铺底;`selectTask` 末加 `if (mqPhone.matches) setMobileView("mv-mid")` 选中任务自动切对话。`applyMobileMode()` 监听 `matchMedia("(max-width: 640px)")`,进手机时清 DOM 上的 `left-collapsed` class(localStorage 不动),回桌面再 `applyLeftCollapsed(读 localStorage)` 恢复。`100vh → 100dvh` 解决 iOS Safari 工具栏挤压;`textarea / input` 强制 ≥16px 防 focus 双指缩放;4 个 modal 卡片宽度从固定 px 改 `min(92vw, …)`,file-preview 改 `100vw × 100dvh` 全屏化。`#pane-toggle-left { display:none !important; }` 手机不允许折叠避免与 tab 切换语义冲突。否决:(a) 抽屉式(要写遮罩+手势 JS,自用工具不划算);(b) 只缩字号不改布局(三列在 360px 屏字段严重截断)。
|
||||
- **豆包 Seedance 2.0 Fast 视频生成接入(文生视频)+ videogen skill**:`config/media/doubao.yaml` 展开 video 段(`seedance_2_fast`:¥37/Mtok 文生 / ¥22/Mtok 图生,实测档位 480p 5s ¥1.86 / 720p 5s ¥4.00 — 由 token 公式 `(in+out)×W×H×fps/1024` 反推校验通过);`tools/seedance.py` 走 ark POST `/contents/generations/tasks` → 5s 间隔轮询 → succeeded 后 download mp4 + .meta.json 落 `<wd>/videos/<ts>-<rand>.mp4`,失败/cancel 不计费;`core/storage/usage.py::record_video_usage` 多态 units snapshot(resolution/duration/ratio/fps/tokens/单价);`build_agent` 加 `video_variant` + `cancel_check` 形参 — cancel_check 必须在 build 阶段传(SeedanceTool ctor 持有用于轮询期间响应停止按钮,改了原"build 后赋 agent.cancel_check"的延迟绑定,web 入口同步迁移);`web/app.py` 加 `_list_video_variants` / `_resolve_video_model` / `GET /v1/video_models` / `MessageRequest.video_model` / `OptimizePromptRequest.video_model`;`dev.html` 顶栏第三下拉 + `state.videoModels/videoModel` + 发消息一起 POST。前端 chip / inline `<video>` / `extractMediaBanner` / `_categorize` 在前期工作里已为 seedance 留好脚手架,几乎不动。`skills/videogen/SKILL.md` 六维诊断把 imagegen 的"光线"换成"运动+镜头"两维(运动必填,否则应该走 seedream 而非 seedance —— 差 18 倍价钱);BLOCKING 门槛比 imagegen 更严(¥4 vs ¥0.22)且要等 30-90s,贴 prompt+参数+预计花费+预计等待四件套等明确确认。`general_v1.md` 加 seedance 触发指引(平行 seedream)。phase 1 仅 t2v,**不支持 i2v**(skill 明示告诉用户)。fast 上限 720p,1080p+ 留给 pro variant(yaml 当前未配)。否决:(a) progress 事件流化(需要给 tool 加 sink 注入,phase 1 用 `run_status=running` 够了);(b) 远端 cgt-task DELETE(Volcengine 无明确 API,best-effort 不动);(c) i2v phase 1 拉进来(要图片转 URL + UI 选已有图,延后)。
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@
|
|||
## 路径
|
||||
默认工作目录在系统消息末尾,所有相对路径基于该目录。
|
||||
|
||||
**对外 echo 产物文件路径(回复用户、汇报产物)时**:用 user_root 相对的**全形式** `<wd_name>/<rel>` —— `<wd_name>` 就是上方 task_dir 字段的最后一段(如 task_dir = `D:\...\users\<uuid>\生图测试` → `<wd_name>` = `生图测试`)。例:`生图测试/videos/xxx.mp4`、`生图测试/figures/cover.png`、`基金申报/sections/01-绪论.md`、`公司汇报/slides/deck.pptx`。**不要简写**为 `videos/xxx.mp4` / `figures/cover.png` / `slides/deck.pptx` 这种只在 task 内成立的裸形式。
|
||||
|
||||
媒体 tool(`seedream` / `seedance`)输出的 `saved:` 那行**已经是规范全形式**,原样照抄就行(免去自己拼前缀);其他场景(ppt / proposal / coding 等 `run_python` / `write` / `shell` 写完文件后)自己按 `<wd_name>/<rel>` 拼。
|
||||
|
||||
**为什么硬性约束**:Web UI 按 `<wd_name>/...` 前缀识别产物路径挂可点 chip(预览 / 下载);简写形式 chip 失效,用户没法直接点开。跨所有产物 skill 统一生效。
|
||||
|
||||
## 平台
|
||||
当前是 Windows + cmd.exe。**避免用 unix-only flag**:
|
||||
- 建目录用 `run_python` 的 `os.makedirs(path, exist_ok=True)`,**不要** `shell mkdir -p`(cmd 不识别 -p,会创建名为 '-p' 的字面目录;shell 工具已对此做兜底但仍以 run_python 为优先)
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ seedream(
|
|||
## 产物处理
|
||||
|
||||
生图完成后:
|
||||
- 把 `saved: figures/xxx.png` 路径告诉用户
|
||||
- **原样照抄** tool 输出 `saved:` 那行的路径告诉用户(已是规范全形式,不要简写为 `figures/xxx.png` —— 详见 system prompt「路径」段,关系到 chip 能不能挂)
|
||||
- 如果是做 ppt / docx 配图,直接在后续 `add_picture` / `` 引用,不需要再问
|
||||
- 用户说"换张" → 走上节的"对齐改哪一维"流程,不要默认重发
|
||||
- 用户说"再来几张备选" → **先确认**:"备选 N 张会花 ¥{0.22 * N},确认?"(防止隐形烧钱)
|
||||
|
|
@ -216,7 +216,7 @@ seedream(
|
|||
## 输出
|
||||
|
||||
调完告诉用户:
|
||||
- 文件相对路径(`figures/xxx.png`)
|
||||
- 文件相对路径(照抄 tool `saved:` 行,全形式;见 system prompt「路径」段)
|
||||
- 本次成本(¥0.22 或 ¥0.27)
|
||||
- 用的 prompt 摘要(主体 / 风格 / 光线 3 件套)
|
||||
- 一句"要换方向 / 调风格 / 再来一张吗?"
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ seedance(
|
|||
## 产物处理
|
||||
|
||||
生视频完成后:
|
||||
- 把 `saved: videos/xxx.mp4` 路径告诉用户(SPA 会自动 inline 播放器,点击可全屏)
|
||||
- **原样照抄** tool 输出 `saved:` 那行的路径告诉用户(已是规范全形式,不要简写为 `videos/xxx.mp4` —— 详见 system prompt「路径」段,关系到 chip 能不能挂)。SPA 按全路径自动 inline 播放器,点击可全屏
|
||||
- 如果是做 ppt,提醒用户 `python-pptx` 可以 `add_movie` 嵌入 mp4(但导出 pdf 时视频会丢,改截关键帧 + 链接)
|
||||
- 用户说"换一段" → 走上节的"对齐改哪一维"流程,**不要默认重发**
|
||||
- 用户说"再来几段备选" → **先确认**:"备选 N 段会花 ¥{4 × N} + 等 {N × 60}s,确认?"
|
||||
|
|
@ -254,7 +254,7 @@ seedance(
|
|||
## 输出
|
||||
|
||||
调完告诉用户:
|
||||
- 文件相对路径(`videos/xxx.mp4`)
|
||||
- 文件相对路径(照抄 tool `saved:` 行,全形式;见 system prompt「路径」段)
|
||||
- 本次成本(¥X.XX,从 banner 抽)
|
||||
- 用的 prompt 摘要(主体 / 运动 / 镜头 3 件套)
|
||||
- 一句"要换方向 / 调镜头 / 加长时长再来一段吗?"
|
||||
|
|
|
|||
|
|
@ -2509,24 +2509,56 @@ function extractArtifactRels(text, workingDir) {
|
|||
if (!wd) return [];
|
||||
const norm = String(text).replace(/\\+/g, "/");
|
||||
const wdEsc = wd.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
// lead 边界:行首或非 path-字符;tail 截到空白/引号/括号等
|
||||
const re = new RegExp(
|
||||
"(?:^|[\\s\"'`/=:,()<>\\[\\]{}|])(" + wdEsc + "/[^\\s\"'`<>(){}\\[\\]|]+)",
|
||||
"g"
|
||||
);
|
||||
const seen = new Set();
|
||||
const out = [];
|
||||
let m;
|
||||
while ((m = re.exec(norm)) !== null) {
|
||||
let rel = m[1];
|
||||
rel = rel.replace(/[.,;:!?)\]}>。,;:!?)]+$/, ""); // 剥尾标点(中英)
|
||||
const tail = rel.slice(wd.length + 1);
|
||||
if (!tail) continue;
|
||||
const last = tail.split("/").pop() || "";
|
||||
if (!last.includes(".")) continue; // 看着像目录的不挂 chip
|
||||
if (seen.has(rel)) continue;
|
||||
seen.add(rel);
|
||||
out.push(rel);
|
||||
|
||||
// 规范形式:<wdName>/<...>/<file>.<ext> —— 当前协议(system prompt 强约束助手照抄 tool `saved:` 行)
|
||||
// lead 边界:行首或非 path-字符;tail 截到空白/引号/括号等
|
||||
{
|
||||
const re = new RegExp(
|
||||
"(?:^|[\\s\"'`/=:,()<>\\[\\]{}|])(" + wdEsc + "/[^\\s\"'`<>(){}\\[\\]|]+)",
|
||||
"g"
|
||||
);
|
||||
let m;
|
||||
while ((m = re.exec(norm)) !== null) {
|
||||
let rel = m[1];
|
||||
rel = rel.replace(/[.,;:!?)\]}>。,;:!?)]+$/, ""); // 剥尾标点(中英)
|
||||
const tail = rel.slice(wd.length + 1);
|
||||
if (!tail) continue;
|
||||
const last = tail.split("/").pop() || "";
|
||||
if (!last.includes(".")) continue; // 看着像目录的不挂 chip
|
||||
if (seen.has(rel)) continue;
|
||||
seen.add(rel);
|
||||
out.push(rel);
|
||||
}
|
||||
}
|
||||
|
||||
// ───── 一次性兼容:协议刚性化前的历史简写消息 ─────
|
||||
// system prompt 改硬约束之前,助手按 SKILL 旧文案 echo 过 `videos/xxx.mp4` /
|
||||
// `figures/xxx.png` 这种裸形式 —— 那些消息已存 DB 改不动,前端这里 prepend
|
||||
// <wdName> 把它们拼成 user_root rel 才能挂 chip。**白名单显式枚举不扩展**:
|
||||
// 新产物 skill 走 system 协议必出全形式,这一层只服务**历史消息**渲染。
|
||||
// 长期(老消息归档/不再回看)整段可删。
|
||||
const LEGACY_PRODUCT_DIRS = ["videos", "figures"];
|
||||
for (const dir of LEGACY_PRODUCT_DIRS) {
|
||||
const dirEsc = dir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
// lead 边界跟主规则一致但去掉 `/` —— 否则 <wd>/videos/xxx 里的 videos/xxx 会被重复
|
||||
// 匹配(虽然 seen 去重,但浪费 cycles)
|
||||
const re = new RegExp(
|
||||
"(?:^|[\\s\"'`=:,()<>\\[\\]{}|])(" + dirEsc + "/[^\\s\"'`<>(){}\\[\\]|]+)",
|
||||
"g"
|
||||
);
|
||||
let m;
|
||||
while ((m = re.exec(norm)) !== null) {
|
||||
let tail = m[1];
|
||||
tail = tail.replace(/[.,;:!?)\]}>。,;:!?)]+$/, "");
|
||||
const last = tail.split("/").pop() || "";
|
||||
if (!last.includes(".")) continue;
|
||||
const rel = wd + "/" + tail;
|
||||
if (seen.has(rel)) continue;
|
||||
seen.add(rel);
|
||||
out.push(rel);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue