# 实施进度 > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 2-4 句:做了啥 + 关键判断 + 没动什么;细节查 `git log` / `git diff`。 最后更新:2026-05-20(dev SPA 主页轻量美化:header 加 brand logo / 左 pane filter 行轻分隔 / 顶栏语义按钮改 hover 上色 / 圆角阴影微调) --- ## 状态 | Phase | 标题 | 状态 | 备注 | |---|---|---|---| | 1-3 | 骨架 + Skill + run_python | ✅ | 三个 skill;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(邮箱密码 + platform_key → JWT)+ dev SPA ✅**;**单活 run 锁 + cancel ✅**;**0004 schema 瘦身 ✅**(删 runs/usage_events);**入口归位 ✅**(`cli.py`→`main.py`,装配 lib 挪 `core/agent_builder.py`);真 OIDC 待;C(Executor)待。 | --- ## 已完成关键能力 ### 2026-05-20 - **dev SPA 主页轻量美化(纯 CSS / HTML,不动 JS / 路由)**:用户要"简洁美化主页"。改四处:① header 从裸 "zcbot" 文字 → brand wrapper(24px 红渐变 "Z" logo + 标题字号 14→15 + letter-spacing + 顶栏 1px 极淡阴影),沿用登录页 brand 视觉但缩小;② 左 pane 三行 pane-head(任务标签/搜索/排序)用 `#pane-left .pane-head + .pane-head` 选择器把 filter / sort 子行换白底 + `--border-soft #ececec` 分隔,弱化为子层级,把两条 inline `border-top` 顺手去掉(与新 `border-bottom` 重叠会出双线);③ 顶栏 4 个语义按钮(完成/导出/废弃/删除)+ 选入弹框的复制/移动按钮从"常态彩边 + hover 加底色"改"常态中性 + hover 一次性上语义色(color + border + bg)",给 button 基础类加 transition 让色变平滑(沿用现有 `button.danger` 的同款 hover-only 范式);④ 圆角统一:button / input / textarea / select / floating-menu / .msg 4→6,三个 modal 卡片 6→8 + 阴影 `0 8px 24px → 0 12px 32px` 略深显悬浮感。**没动**:布局 / 交互逻辑 / 任何 JS / 后端 / DESIGN(纯视觉)/ RUN(无对外接口变化);dd-item 菜单的语义色保留(菜单内本来就靠色区分动作类型,不属于"顶栏中性"范畴)。 - **加 `config/models/glm.yaml`:智谱 GLM 5.1 接入(litellm zai provider + 国内站 bigmodel.cn)**:用户要加 GLM。litellm 1.83.14 内置 `zai` provider(PR #17307 早就 merge,我初次 grep 漏了 — 只搜了 zhipu/glm/doubao),`zai/glm-5.1` 自动路由到 z.ai 国际站(`api.z.ai`,env `ZAI_API_KEY`)。**用户用国内站 bigmodel.cn**(账号 / key 跟 z.ai 国际站不通用),YAML 走 `api_base: https://open.bigmodel.cn/api/paas/v4` 覆盖 litellm 默认(`core/llm.py:71-72` 已有 `if self.api_base: kwargs["api_base"]=...` 透传通道),env 命名 `ZHIPUAI_API_KEY` 跟国际站 `ZAI_API_KEY` 分开。family=`glm`,单 variant `pro`,context 200K / reliable 100K / max_out 8192,tool calling 标 good,run_python 开。**`thinking_mode: false`**:GLM 的 thinking 协议是 body `{"type":"enabled"}` 开关 +(可选)budget,与 OpenAI/DeepSeek 的 `reasoning_effort` int 等级不同;`core/llm.py:77-78` 只透传 `reasoning_effort`,要接 GLM thinking 得加 family 分支(`if family.startswith("glm"): kwargs["extra_body"]={"thinking":{"type":"enabled"}}`),不在加 YAML 范围,留 TODO。smoke:`ModelCapabilities.load('glm.pro', ...)` 正常 + `litellm.get_llm_provider('zai/glm-5.1')` 返 `(model=glm-5.1, provider=zai, default_base=https://api.z.ai/api/paas/v4)`,YAML override 生效后实际打 bigmodel.cn;`/v1/models` 扫描结果含 `glm.pro / 'GLM 5.1' / thinking=False`。**没动**:`core/llm.py`(避免半成品 thinking 分支)、DESIGN.md(只加模型档案,非架构变更)、`default_model`(仍 `deepseek_v4.flash`,GLM 是可选项,前端下拉里出现)。**已知待办**:① 接 GLM thinking 透传;② 豆包图像/视频生成(seedream/seedance,完全不同 API 形态,要单独管线)。 - **files SPA UX 翻面 + 拖拽上传 + 修 checkbox 全局 width bug**:沿用上条新加的两路由,但前端 UX 整套换。**原模型**(select-then-pick-dest):主区行带 checkbox + 顶栏全选三态 + 黄 bar(复制到 / 移动到 / 取消)→ 弹框选目标目录。**新模型**(at-dest-pull-sources):主区只读浏览,顶栏加 `[选入…]` 按钮 → 弹框内浏览任意目录 + 跨目录勾文件 / 子目录(`Set` 跨切换保留)+ 底部 `[复制到此处]` `[移动到此处]` 两按钮直接落到主区当前 `state.filesPath`。**理由**:用户切任务时主区自动跳 task working_dir,绝大多数操作是"把外面素材喂进当前 working_dir",destination-first 比 source-first 少一次心智切换,且主区干净。**附带**:① 主区 `` 被全局 `input{ width:100%; }` 撑成全行宽 → 把 `.name`(`flex: 1; flex-basis: 0`)挤成 0 宽,行里只剩看不见的文字 + 居中的 checkbox(用户报"看不到文字"),根因不修永远埋雷,改 selector 排除 checkbox/radio/file。② 拖拽上传:`#pane-right` 监听 dragenter/over/leave/drop,有 `Files` 才响应(忽略文本拖拽),`#file-droparea` 红色虚线 overlay,落点 = `state.filesPath`,沿用 `/v1/files/upload`。**删了**:`state.selectedFiles` + `syncBulkBar` + `dirPicker` 模块 + 顶栏 selall + 黄 bar 整块 + 行 checkbox 渲染(按 CLAUDE.md 不留旧 UX)。**没动**:后端 `/v1/files/copy` `/v1/files/move`(同样的 `paths` + `dest_dir`)、DESIGN、RUN。 - **`POST /v1/files/copy` + `/v1/files/move` 跨目录批量搬动**(原"+ dev SPA 多选 + 目录选择弹框"已被上一条翻面替换):用户要"在文件夹间复制/移动文件"。后端两路由共用 `_validate_transfer` 预检 helper(批量原子校验:源存在、不能等于/含 dest、不在 dest 直接子级、批内重名、target 已存 409,任一失败整批 abort,无 FS 副作用)。**move 加额外闸**:任一源是顶层目录且为某 task `working_dir` → 409(维持"working_dir = 顶层目录"invariant — 允许沉到子目录后,rename 顶层只更新当前层 task 的 DB-aware 逻辑会失效,代码复杂度翻倍才能扛住嵌套场景;用户想归档项目目录:先 DELETE task)。**copy 无此闸**,新副本无 task 关联。dev SPA:`.file-row` 加 `` 列 + 顶栏 `#files-selall` 三态(全/半/无),选中 ≥1 出黄底 toolbar(`复制到…` / `移动到…` / `取消选中`)。目录选择弹框 `#dir-picker-modal` 复用 `/v1/files` 浏览(只列目录,面包屑可点回上层,源目录灰禁),底部按钮文案随 mode 切。`state.selectedFiles` 切 task / 切 filesPath 时清,refresh 后剔除已不存在的 rel 保 view 一致。**部分失败**:沿用现有 rename / delete 单向语义,FS 中途失败抛 500 + 已成功项保留(`shutil.move/copytree` 失败几乎只在跨卷断连 / 磁盘满,workspace 同盘罕见)。**没动**:DESIGN(API 添加非语义变更)、RUN(无 CLI / env 变化)、DB schema。 - **working_dir 视为可重生 FS 视图**:DB 是 source of truth,FS 目录可独立删 / 用户手动 rmtree / 跨机器迁移丢失,**下次跑就自动 mkdir 重建**。三处改:① `DELETE /v1/tasks/{id}` 删完后若同 user 下再无 task 引用此 working_dir 且 FS 目录为空 → best-effort `rmdir` 清孤儿(非空 / 不存在 / 外部 --working-dir 静默跳过)。② `POST /v1/files/delete` 顶层目录去掉「有 task 引用就 409」闸,允许独立删空目录,task.working_dir 字段不动。③ `core/agent_builder.py::build_agent` 把 `working_dir_path.mkdir(parents=True, exist_ok=True)` 从 `if not resume:` 里挪出,resume 也兜底建目录(用户手删 FS 后再 send message 不会炸)。smoke `scripts/smoke_files_rename.py` 增 case 4 (200 + working_dir 不变) / case 8 (DELETE task 空目录自动清) / case 9 (非空目录保留),全 9 pass。**没动**:DB schema、rename 顶层目录的同步 UPDATE 逻辑(rename 是明确改名,和"删后重生"语义不同)、外部 --working-dir(DB 绝对串)的清理(避免误删用户外部项目)。 ### 2026-05-19 - **0006 模型切换(c 模式 task 级 A 粒度)+ usage_events v2 表**:`tasks.model_profile` 从死字段变 source-of-truth,顶栏下拉 → `PATCH /v1/tasks/{id}` 即换,**A 粒度下条 send 生效**(当前 run 不受影响;running 中切 UI 提示"跑完后生效")。`build_agent` resume 时优先 task.model_profile,新建 task POST body 加可选 `model_profile`(留空 → `cfg["default_model"]`)。`GET /v1/models` 扫 `config/models/*.yaml` 列可选项(含 display_name / thinking_mode / is_default),`ModelCapabilities` 加 `display_name` 字段,deepseek_v4.yaml 两 variant 各填名。**前端**:chat-meta 加下拉(切了 PATCH+提示)、新建对话框 modal 加 ``;`tool_result` 事件 debounce 500ms 刷新文件 panel。仅前端,不动后端 / DESIGN / RUN。 - **proposal skill mermaid hash→caption + quality_check 加图相关 4 拦截 + SKILL.md 精简 + `/v1/files/download` 加 `Cache-Control: no-cache`**:用户反馈"申报 skill 图没渲染到 docx",诊断双层 bug:① 模型写满 ASCII 字符画从未用 mermaid + `![]()`;② SPA 预览命中浏览器启发式缓存(Starlette FileResponse 无 Cache-Control)。修法:render_diagrams 改 caption 强制必填 + 同 task 唯一(撞名退 2);quality_check 加 4 条(figures/ 有 png 但 sections 0 引用 / 围栏含 box-drawing 字符 / mermaid 缺首行 `%% caption:` / caption 撞名);SKILL.md ~193→~160 行。 - **dev SPA 文件预览弹框**:点击文件不再直接下载,弹 90vw 模态按扩展名分派(image/pdf/text/md→已有 renderMd / docx 用 docx-preview / xlsx 用 SheetJS / pptx 等 fallback "请下载查看")。库懒加载 + blob URL 全局 track + 弹框关时 revoke 防漏;vendor 入 git(jszip / docx-preview / xlsx,~1MB,无 npm 链路就直 vendor 锁版本)。**没动**:后端 app.py(blob URL 路径足够)。 ### 2026-05-18 - **入口归位:`cli.py`→`main.py`,原 `main.py`→`core/agent_builder.py`,删 CLI REPL,§7 E 撤**:`main.py` 混三角色(装配 lib + utility + cli/web 共 import 的事实入口),按 SoC 拆。`git mv` 两次(覆盖)+ 5 处 `from main import` → `from core.agent_builder import`。删 `chat / tasks / export` 三命令 + REPL 主循环 + 内部 helpers(~400 行);新 `main.py` 只剩 `db / probe / web`(后来再加 `user`)。失:CLI 无 auth 直跑 core 通道;补:dev SPA 走同条 web 路径,临时调试写几行 ad-hoc script。 - **0004 schema 大瘦身:删 runs / usage_events,合 run_status / run_error 入 tasks;路由 run_id → task_id**:`usage_events` 全代码库零写零读,`runs` 表 tokens_p/c 写但从未读(真 tokens 走 tasks 累计),started_at/finished_at/error 也只写不读,`run_id` 唯二实用是 broker 频道键 + cancel 参数 — 单活 run 形态下客户端只需 task_id 就够。`tasks` 加 `run_status text default 'idle'`(idle/running/cancelling/error,error 是唯一持久终态)+ `run_error text`。Broker 全 task_id 索引 + 加 `start(task_id)` 清上轮 done 标记。**dev SPA**:`state.currentRunId` → `state.streaming` bool;cancel POST `/v1/tasks/{tid}/cancel` 去 `/runs/{rid}/`。 - **`POST /v1/files/rename` + 顶层目录 delete 加 task 引用闸**:**`/v1/files/*` 升格为唯一目录树 mutation 入口,DB-FS 一致性作服务端不变量内化**;`GET /v1/folders` 定位"项目聚合视图",只读。顶层目录(`target.parent.resolve() == root.resolve() and is_dir()`)走 DB-aware 分支:事务内 `SELECT ... FOR UPDATE` 锁关联 task + 任一 running/cancelling → 409 + `check_no_subtask(exclude=被改名 tids)` 防嵌套 + UPDATE 在 FS rename 之前(FS 失败可回滚)。**架构教训(§7.9)**:此前提的双命名空间 `/v1/folders/rename` vs `/v1/files/rename` 反了 — `is_top_level` 分支是**从数据状态派生**(path 恰好是 working_dir),不是客户端意图派生,放服务端是更安全的位置。 - **task-level cancel + AgentLoop 协作式 cancel + dev SPA stop 按钮**:Broker 加 `request_cancel / is_cancelled / clear_cancel`(per-task `threading.Event`,`setdefault` 保证 BG 还没 register 也能 set)。Loop 加 `cancel_check` callable + `_fill_cancelled_tool_results` 给未执行 tool_call 补 `[cancelled]` tool message(LiteLLM 协议要求 assistant tool_call 必须有匹配 tool result,否则 resume 报错)。**LLM 同步 call 本身不可中断**(litellm 阻塞,无原生 cancel)— 最坏等当前一轮跑完几十秒。Gate 同步扩:`post_message` 单活 run 检查 `status in ('running', 'cancelling')` 避免新旧 BG 撞 messages.idx。 - **`POST /v1/tasks/{id}/messages` 单活 run 锁 + 孤儿 reaper**:同事务 `SELECT Task ... FOR UPDATE` + 活跃状态检查 + 标 running,三步原子完成避免 TOCTOU(用户连点 send / 多 tab 同时发 → 两 BG 线程争 `messages.idx`)。lifespan 加 reaper:启动时 `UPDATE Task SET run_status='error' WHERE run_status IN ('running','cancelling')` 清进程 crash 留下的孤儿。**未来 TODO**:multi-worker 部署 reaper 不能简单全表清(会误清其他 worker 的真在跑),换 heartbeat + lease。 - **proposal skill 流程图/结构图管线**:`render_diagrams.py` 扫 sections/*.md mermaid 块 → mmdc(本地)或 mermaid.ink(公网) → png;render_docx 加 `add_picture` 识别 `![](...)` 单行 + mermaid 围栏特判;templates 三处占位换成完整 mermaid 例子。图编号 `ctx['fig_no']` 调用链递增不重不漏;mmdc/网络都没的极端环境 docx 仍能产(ASCII 退化)。 - **system prompt skill 机制改"可选辅助"**:接 GET /v1/skills + 下拉落地后,prompt 第 14 行从 `"永远 load 一下"` 改 `"简单问答/读代码/改 bug/文件操作直接用通用工具就够,不必为每个任务硬套 skill"`;一旦决定要用仍 load 完整指引。**Tradeoff**:边缘场景(用户提"整理大纲")agent 偏向不 load 可能漏掉好的模板,比"什么都套 coding"的噪音更可接受。 - **`GET /v1/skills` + dev SPA skill 字段改下拉**:lifespan 启动 `SkillRegistry` 扫一次挂 `app.state`(FS 静态运行中不变);返 `{skills:[{name,description}]}` 按 name 升序。前端 `` 换 `