zcbot/PROGRESS.md

152 lines
20 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-19(dev SPA 登录从"邀请码/uuid5"撤回 邮箱+密码 — `users.email/password_hash` + UNIQUE + `main.py user add` CLI + 登录页两 tab)
---
## 状态
| 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-19
- **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 27
core/storage/engine.py 80
core/storage/models.py 98 ← 3 表(0004 删 runs/usage_events;0005 email UNIQUE)
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
web/__init__.py 5
web/app.py ~890 ← /v1 JSON API + user_id 隔离 + run lock + task-level cancel
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 ~1700 ← 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 并存不冲突。