zcbot/PROGRESS.md

163 lines
29 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 状态、决策偏差、文件量、下一步。每条 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<rel>` 跨切换保留)+ 底部 `[复制到此处]` `[移动到此处]` 两按钮直接落到主区当前 `state.filesPath`。**理由**:用户切任务时主区自动跳 task working_dir,绝大多数操作是"把外面素材喂进当前 working_dir",destination-first 比 source-first 少一次心智切换,且主区干净。**附带**:① 主区 `<input type=checkbox class=row-cb>` 被全局 `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` 加 `<input type=checkbox class=row-cb>` 列 + 顶栏 `#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 加 `<select id="nt-model">`、message 历史按 `messages.model_profile` 切换点画小标(`── DeepSeek V4 Pro ──`,连续同 model 不重复)。**统计 schema**:0004 删掉的简陋 usage_events 字段不够多态,本次重建 v2 形态(`event_id/user_id/task_id/message_id/kind/model_profile/units jsonb/cost_usd`),chat 已接入(`core/storage/usage.py::record_chat_usage`,`loop.py` 在 assistant message 入库后调,litellm cost map 算钱),媒体扩展位(image/video/audio kind)预留不动 schema。**双写**:同时回填 `messages.tokens_in/out/model_profile`,查 message 详情时不需 JOIN。**索引**:`(user_id, created_at)` / `(task_id)` / `(model_profile, created_at)`,用户级配额 query JOIN-free。**没动**:CLI / RUN.md(无 env / 命令变化)、`tasks.tokens_prompt/completion/cost_usd` 保留作 task 级粗概览。
- **dev SPA 登录撤回 邮箱+密码,删 invites 表**:前两条"邀请码 env → invites 表(0005)"一日游撤回,复用 users 表本来就有的 email/password_hash 列(0001 schema)+ 0005 加 UNIQUE(email)。`bcrypt` 哈希,新 `/v1/auth/login_password` 路由,新 `main.py user add --email --password` CLI 发用户。dev SPA 登录两 tab(邮箱密码 默认 / UUID+PLATFORM_KEY 备用,last-used 持久化 LS)。判定:邀请码 uuid5(NS,name) 推导对外是黑盒(改 name = 换身份),复用 users 列语义清晰也对齐生产路径。**没动**:JWT 签发 / platform_key 路径 / DB users 表列结构。
- **邀请码后端 env → invites 表(0005)** _(已撤,见上条;原条目已删,有需要看 git history)_
- **SENTINEL user 彻底撤(数据 + 代码)**:`SENTINEL_USER_ID = UUID('00000000-...')` 在 web 必从 JWT 拿 user_id 后已无角色,按 CLAUDE.md "不写兼容层" 连根拔。DB CASCADE 删 sentinel user + workspace dotfile 目录;代码 10 处删 import / 默认参数 / fallback,`utils.py` 三函数和 `build_agent``user_id` 从可选变必填(`build_agent` 加 `*,` 转 KEYWORD_ONLY 规避默认参数顺序)。**Bonus**:把"操作 user 数据的函数必须显式传 user_id"作为 Python 必填参数固化,以后多 user 函数 typechecker 会拦到。
- **dev SPA 邀请码登录(env 形态)** _(已撤,见 SENTINEL user 撤之后两条,路径整体改邮箱密码)_
- **任务/文件行 `⋯` 下拉菜单 + 文件顶栏长名截断 + 聊天框上传按钮 + 工具调用 debounce 刷新右侧**:单例浮层菜单(`#floating-menu` position:fixed)避开 pane overflow 裁剪。任务行 4 项(complete/abandon/export/delete,不同颜色,非 active 自动 disable);文件行 3 项(改名/下载/删除);聊天框加上传按钮共用 `<input type="file">`;`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 升序。前端 `<input>``<select>` + 首项 `(默认 · 不限定)` 空值;option 文案 `name — description`,失败静默退化为只剩默认项。**没动**:`POST /v1/tasks` body 不校验 `skill ∈ registry`(留空 / 任意串都允许)。
- **dev SPA 全套 UI 中文化**:静态文案(login / header / pane-head / 操作按钮 / new task modal)+ 动态文案(status badges / role 标签 / SSE 流式提示 / confirm/alert)全面本地化。技术字段(user_id / UUID / token / SSE event 名 / API 字段名)不动 — 都是 schema 层不影响 UI 中文。
### 2026-05-17
- **0003 schema:name + working_dir + skill 三件套**:用户要任务标识和工作目录解耦(原 name 实际是目录名)。`TRUNCATE tasks CASCADE` + `task_dir → working_dir` + `mode → skill` + 加 `name TEXT NOT NULL`(空表 NOT NULL 不需要 backfill)。新建必传 `name`(显示名,DB NOT NULL,UI 标题用);`working_dir` 可选(留空 fallback 用 name);两者都过 `validate_task_name`。新增 `GET /v1/folders`(FS 非 dotfile 子目录 + 关联 task 计数 + 最后使用时间)给 dev SPA modal 的 datalist 补全用。
- **`GET /v1/tasks` 分页 + 多维筛选 + ordering(DRF 风格)**:标准分页壳 `{page,page_size,count,results}`;6 个 query(page/page_size/status/skill/working_dir 末段名/q ILIKE name+desc/ordering);`-field` 倒序,allowlist `created_at/updated_at/name/status`,非法字段静默忽略,**默认 `-created_at`**(用户要求,创建时间倒序更稳)。dev SPA 加翻页按钮 + 搜索 debounce 300ms + working_dir datalist autocomplete。
- **task 硬删 API + dev SPA delete 按钮 + 文件 per-row 删**:`DELETE /v1/tasks/{id}` user_id ownership 校验 + DB 行删(messages CASCADE)+ **FS task_dir 不动**(同 name 多 task 共享时"最后删了顺便 rmtree"易擦用户素材,经 /files/delete 显式清更安全)。dev SPA chat 面板加 `btn-delete-task`(任何 status 都可删,confirm 带项目名 + 消息条数二次确认);file 面板 per-row 加红 `×`
- **files API 全面 user-rooted(去掉 task_id 前置)**:原 API 用 task_id 拐杖间接拿 working_dir,迫使前端先选 task。`/v1/files/*` 4 路由改 user-rooted(`workspace/users/<uid>/` 为边界),`_safe_join` 边界改 user_root + 加 dotfile 过滤(`.memory/` 隐藏);dev SPA `loadFiles()` 不再 gate on task_id,enterApp 时直接拉。**架构**:与 §7.1 "task / dir 双视图正交"心智对齐,files 操作不该依赖 task。
- **files 面板 UX 项目名 + 修 root crumb bug**:用户混淆"空目录"为"看不到文件夹本身",修两处:① 后端 `cur_rel == "."` 不再追加无意义 "." crumb;② 前端 crumbs 第一格 label 从 "/" 改项目名,整条路径直观 `水泥申报 / 草稿 / draft.md`
- **task_dir 改 eager mkdir**:原"懒 mkdir(skill 首次写产物时建)"是 UUID-named 时代设计,现在 task_dir 是用户给的项目名,**name = 项目声明**,目录就该 task 创建时存在(用户可立刻塞素材文件)。`build_agent` 新建分支 + `web/app.py::create_task` 都加 `mkdir(parents=True, exist_ok=True)`;同 name 多 task 共享 + 已有内容不被擦。
- **task = name-based 项目目录 + memory dotfile**:废弃自动 UUID 派生 + `tasks/` 中间层。新建必给 `name`(简单名,项目目录名);`task_dir = workspace/users/<uid>/<name>/`;同 name 多 task 自动共享同目录(§7.1)。memory 搬 dotfile(`workspace/users/<uid>/.memory/{core.md, extended/}`)跟项目目录扁平共存不撞名;`validate_task_name` 拒 `.` 起头双向防呆。`_cleanup_if_empty` 简化:FS 一律不动(跨 task 复用绝不 rmtree),空 task 只删 DB 行。
### 2026-05-15
- **§7 D 阶段:`/v1` JSON API 落地;Phase G Jinja2/HTMX UI 路线撤**:用户决定与已有 platform 联调,前端用 platform 框架,本仓库再维护 HTML/CSS 就是双套浪费。删 `web/templates/*` + `web/static/*` CSS + jinja2/markdown-it-py/pygments 依赖;重写 `web/app.py``/v1/` 前缀 JSON;SSE event payload 由 HTML 片段切 JSON(`event: <type>` + `data: <JSON>`)。**沉淀**:G 阶段的 sink 协议 / RunBroker fan-out / no-subtask / files 路径安全归一 / task_dir 相对存储全部保留,不被 UI 层牵连。dev SPA `web/static/dev.html` 留一份升级为本地 dogfood 主路径(单文件 vanilla JS,3 栏)。
- **§7 D' 过渡 auth(PLATFORM_KEY → JWT)+ dev SPA**:`pyjwt` HS256,`AuthConfig.from_env()` 启动校验 `PLATFORM_KEY` / `JWT_SECRET` 必填(任一缺失 fail-fast);`HTTPBearer` Depends + `make_require_user(cfg)` 工厂闭包持 cfg。数据隔离全 `Task.user_id == user_id` + `_assert_owns_task` helper;跨 user 视为 404 不暴露存在性。**SSE 走 fetch + ReadableStream**(`EventSource` 不支持自定义 header,token 没法塞,手解 SSE frame)。**没动 core**(本地 CLI 路径不进 web auth);**TODO**:真 OIDC 接入(替换 /v1/auth/login 内部为 ID token 校验,路由层不动)。
- **task_dir 改相对存储**:DB `tasks.task_dir` 原存绝对(`D:\projects\...`),改为 **ROOT 内→相对 posix、ROOT 外→保留绝对**(用户 `--task-dir` 指外部项目场景)。新增 `core/paths.py::{ROOT, to_db_path, from_db_path}` 三出口,所有读写边界统一过这里;alembic 0002 一次 UPDATE 把现有 ROOT-prefix 行转相对。`CLAUDE.md` 加"开发阶段不写兼容层"心智(用户指示)。
- **workspace 布局统一 per-user**:`workspace/tasks/<uuid>/` + 全局 `workspace/memory/`**`workspace/users/<user_id>/{tasks/<uuid>,memory/}/`**。`build_agent` / memory / web `create_task` 全程透传 user_id;**清旧数据不留兼容**(DELETE tasks CASCADE + `rm -rf workspace/tasks/`)。
- **litellm 启动 cost map 网络警告兜底**:`import litellm` 之前 `os.environ.setdefault("LITELLM_LOCAL_MODEL_COST_MAP", "True")` 走打包的本地 cost map,跳过 httpx.get;冷启动从 ~5s SSL 超时降到 <1s
- **Phase G G1-G6 Jinja2/HTMX Web UI(05-1405-15)** _(全撤,被 D 阶段 `/v1` JSON API + 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(基建 + Session/TaskState ORM + task_dir 双形态 + no-subtask)**:`core/storage/{engine,models}.py` SQLAlchemy 2.x ORM(5 )+ alembic + `cli db {upgrade,downgrade,current}` + `ZCBOT_DB_URL` 必填;`core/session.py` messages PG(append-only,jsonb,idx 递增);`core/task.py` TaskState 保留内存 DTO 落地走 PG;`state.json` 全废;`check_no_subtask` user 下查前缀嵌套(Python fetch 后归一比对, OS 分隔符容差)。**取消** Step 5 migrate-from-fs(用户决定不兼容旧 workspace)。
### 2026-05-12
- **§7 改写**:platform/core 多租户方案废弃, user-direct(folder-centric 后续 §7.1 修为 task-primary;task/messages PG;no-subtask;hard cascade)。
### 历史(2026-Q1 → 05-11)
- **Phase 1-4**:骨架 / skill / run_python / Model Profile + Probingppt v3 加商务红 + apply_brand + Iconify;素材摄取改 markitdown CLI
- **05-06 Phase 6 部分**:task + state.json + tokens 累计;CLI `tasks` + REPL `/status /done /abandon /desc`;移除 legacy `workspace/sessions/`
- **05-07 TUI + task_dir**:rich Markdown 渲染;spinner 显实时耗时 + 累计 token;system prompt 注入 task_dir 绝对路径,产物收敛 `workspace/tasks/<id>/`
- **05-08 REPL 切换 + 懒创建**:`/resume [last|<id>]`;`build_agent` 不预占文件;`_cleanup_if_empty` 三条件守门。
- **05-09→05-10 §7 草案 + 导出**:DESIGN §7 初版(05-12 重写);`cli.py export <task_id>` + `core/export_docx.py`
- **05-11 原子写 + 双层记忆 + §7 A**:`atomic_write_text` 接管 save;`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>` 用户起项目名,同 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(SELECT FOR UPDATE + check_no_subtask + 事务回滚) |
| 单活 run | task 同时最多 1 个活 run | gate 在 `post_message` 同事务 `SELECT FOR UPDATE`,挡连点 send / 多 tab |
| LLM 同步 call 不可中断 | cancel 协作式 check 在 LLM 之间 + tool_call 之间 | 最坏等当前一轮跑完(几十秒) |
---
## 文件清单
```
core/capabilities.py 71
core/llm.py 93 ← litellm 离线 cost map env
core/loop.py 182 ← §7 A sink.emit + cancel_check 协作式 cancel
core/sinks.py 101 ← §7 A
core/ui.py 38
core/paths.py 50 ← task_dir db form 归一(to_db_path / from_db_path)
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 ← record_chat_usage 出口(0006)
core/storage/engine.py 80
core/storage/models.py 130 ← 4 表(0004 删 runs;0005 email UNIQUE;0006 加 usage_events v2 + messages.model_profile)
core/storage/usage.py 70 ← 0006:record_chat_usage(litellm cost map + 双写 messages + insert usage_events)
core/storage/utils.py 136
core/agent_builder.py 307 ← 装配 lib(原 main.py 内容,05-18 改名归位)
tools/{base,fs,shell,run_python,skill_tool}.py ~440 行
main.py ~210 ← 入口:web / db / probe / user(05-19 加 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 ← 0005 一日游 invites 已撤,接 users.email UNIQUE
0006_usage_events_v2_and_message_model.py 60 ← messages.model_profile 列 + usage_events v2 表(多态 units jsonb)
web/__init__.py 5
web/app.py ~1320 ← /v1 JSON API + user_id 隔离 + run lock + cancel + files copy/move
web/auth.py ~190 ← D' 过渡:邮箱密码 + platform_key → JWT
web/broker.py 121 ← in-process pub/sub + cancel signal(全 task_id 索引)
web/sinks.py 21
web/static/dev.html ~2140 ← D' dev SPA(3 栏 + 文件预览弹框 + 两 tab 登录 + 多选 + 目录选择弹框)
web/static/vendor/ ~1 MB ← jszip / docx-preview / xlsx(office 预览)
─────────────────────────────────
Python 合计 ~3400 行(+ dev.html 1700 静态 + vendor 1MB)
```
`skills/ppt|proposal|coding/` 脚本 ~600 行 + SKILL.md / references / config / prompts + alembic.ini,总仓库约 3500 行。
---
## 下一步候选(性价比排序)
1. **真 OIDC 接入 + CORS 收紧**(~1 天)—— `/v1/auth/login` 内部从 platform_key 校验换成 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'(过渡 auth)+ 单活 run 锁 + cancel + 0004 schema 瘦身 + 入口归位 主体已完工。剩余路线:真 OIDC → C(Executor)→ F(deploy / billing)。**§7 E CLI 双模式撤**(2026-05-18,§7.9):dev SPA 已是本地 dogfood 主路径,CLI REPL 删,无 `--remote` 双 transport 维护税。原 Phase G Web UI 路线撤(§7.9),UI 改 platform 端实现;`web/static/dev.html` 是开发期单文件 SPA,跟 platform UI 并存不冲突。