zcbot/PROGRESS.md

18 KiB
Raw Blame History

实施进度

配合 DESIGN.md。本文件只记 phase 状态、决策偏差、文件量、下一步。

最后更新:2026-05-15(Phase G G4)


状态

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 完工;Phase G Web UI 进行中(G1 脚手架 ;G2 task list ;G3 chat 只读 ;G4 SSE 流式 ;G5 文件浏览 待;G6 打磨)。下一阶 C(Executor) / D(HTTP /v1) 待。

已完成关键能力

  • Q1 → 05-06 / Phase 1-4:骨架 / 三 skill / run_python / Model Profile + Probing。ppt 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>/;.gitignore 删 bandaid。
  • 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 路。
  • 05-12 / §7 改写:platform/core 多租户方案废弃,改 user-direct(folder-centric、task/messages 入 PG、no-subtask、hard cascade)。
  • 05-14 / §7.1 心智模型修正:Folder-centricTask 一等公民 + Dir 文件副视图(双视图正交,dir 不是 task 父容器);task_dir 留空=一次性对话 / 指定=项目化二分语义入文。
  • 05-14 / §7 B Step 1 基建:core/storage/{engine,models}.py SQLAlchemy 2.x ORM(users/tasks/messages/runs/usage_events 5 表)+ alembic(初版 migration 0001_initial_schema,GIN/复合索引)+ cli db {upgrade,downgrade,current} 子命令组 + 本地 sentinel user(00000000-...)+ ZCBOT_DB_URL 必填(未设给清晰报错,不引导 docker)。已在远端测试 PG 跑通 db upgrade head
  • 05-14 / §7 B Step 2 Session ORM:core/session.py 重写,messages 走 PG(append-only,jsonb,idx 严格递增);system prompt 不入库(每次 build_agent 重建);Session.load(task_id, system_prompt=...) resume 接口;ensure_local_task_row idempotent UPSERT(INSERT ... ON CONFLICT DO NOTHING)在首条非 system 消息前打底 tasks 行。task_id 切换为 UUID(原时间戳格式废弃,旧 workspace 不做兼容)。main.py/cli.py 适配:resolve_task_id(UUID 前缀解析)、_cleanup_if_empty 双检查(DB messages + FS 产物)、_list_task_rows 改读 PG。core/export_docx.py 改从 PG 读 messages。端到端 build/append/resume/cleanup smoke 全绿。取消 Step 5 migrate-from-fs(用户决定不兼容旧 workspace)。
  • 05-14 / §7 B Step 3 TaskState ORM:core/task.py 重写,TaskState dataclass 保留为内存 DTO 但落地走 PG —— save()upsert_task(INSERT ON CONFLICT DO UPDATE,显式 set updated_at=func.now()),load(task_id) 走 SELECT;字段去掉 cwd(改读 task_dir,§7 SaaS task_dir-as-identity)。state.json 文件全面废除,task_dir 只承担 skill 产物。core/storage/utils.pyupsert_task / update_task 工具。main.py::sync_task_tokensupdate_task(tokens_p,tokens_c) 单字段 UPDATE(ORM-level update 自带 onupdate=func.now())。core/session.py::Session.append 的 ensure 调用补传 mode/description/reasoning_effort,避免首次 INSERT 后 _list_task_rows 看到空 meta。cli.py 全字段从 ORM Task 列读;_cleanup_if_empty 去 state.json 特例(任何 FS 文件 / 子目录都算实质痕迹);/done /abandon /desc 走 PG。core/export_docx.py meta 改从 TaskState.load(tid) 读(asdict 拿到 dict),去 CWD 字段。端到端 smoke:storage UPSERT/UPDATE round-trip + build_agent 懒创建 + Session.append 自动 INSERT 完整 meta + sync_task_tokens 局部 UPDATE + task_state.save UPSERT 保留 task_dir/tokens + export → .docx 37KB 全绿。
  • 05-14 / §7 B Step 4 task_dir 双形态:CLI chat --task-dir <path> 支持用户显式指定项目目录(§7.1 task-primary + dir 副视图心智模型落地)—— 留空走默认派生 workspace/tasks/<uuid>/,显式走用户路径(绝对或相对 cwd,Path.resolve())。main.py::resolve_task_idtask_dir_arg;resume 时从 PG tasks.task_dir 读(SELECT task_dir WHERE task_id=?),空则降级默认派生。新增 is_managed_task_dir(td, ws) 判断是否在 workspace/tasks/<uuid>/ 模板下,作 _cleanup_if_empty 保护开关 —— 用户自指定的项目目录绝不 rmtree(可能含用户已有文件);DB 行该删还是删。core/export_docx.py::export_chat_to_docx 重构:task_id 升一等参数(从 task_dir.name 提取改入参传入),task_dir 留空时自动从 PG 读,支持用户目录(非 UUID 命名)正常导出。cli /exportcli.py export 子命令均改走 _resolve_uuid_or_prefix + task_id 直传。Smoke 4 路径全绿:default-derived(managed=True, cleanup rmtree)/ --task-dir(managed=False, FS preserved)/ resume reads DB / export 自动 PG 查路径。
  • 05-14 / §7 B Step 6 no-subtask 校验:core/storage/utils.py::check_no_subtask(task_dir, user_id=SENTINEL) —— 同 user 下查 new LIKE existing||'/%' OR existing LIKE new||'/%'(task_dir != new 过滤掉同 task_dir 同项目多对话场景)。冲突抛 NoSubtaskError(ValueError 子类),消息带冲突 task 的 UUID 前 8 位 + 它的 task_dir。分隔符容差:SQL 里 replace(task_dir, :bs, '/') 把存的 Windows \ 在比较前归一,新值也 replace('\\', '/'),跨 OS / 历史数据混合分隔符不漏判;bs 通过 bind 参数传(绕开 SQL 字符串转义陷阱)。空 / whitespace task_dir 直接 return(legacy / 未绑项目)。main.py::build_agentresolve_task_id 后、TaskState 构造前调,if not resume 单层闸 —— resume 跳过(目录改名走未来 Folder API cascade,这里只拦新建)。cli.py 三处 build_agent 调用现有 try/except 直接接住 NoSubtaskError 并友好打印。Smoke 全绿:同 dir 允许 / child 拒 / parent 拒 / sibling 允许 / proj_a_other 不误中 proj_a(因为用 /% 而非 %)/ 空跳过 / Win \ 子目录拒 / 混合分隔符(\ 存 + / 查)仍拒 / build_agent 端到端三分支(child raise / same pass / resume bypass)。
  • 05-14 / §7 Phase G G1 Web UI 脚手架:新增 web/ 包(app.py FastAPI 工厂 + templates/{base,home}.html + static/style.css),cli.py web --host --port --reload 子命令(默认 127.0.0.1:8765,本地形态 sentinel user 无 auth,Phase D 才上 OIDC)。模板用 Jinja2 + HTMX/HTMX-SSE 走 CDN(无 node 链路),base.html{% block nav %} 让 G2+ 扩。Starlette 新版 TemplateResponse 签名:(request, name, context),旧式塞 context 里会让 jinja 用 dict 当 cache key 报 unhashable type,踩过修了。requirements 加 fastapi>=0.111 uvicorn[standard] jinja2>=3.1 python-multipart(后者为 G5 文件上传留)。Smoke 四路径全绿(in-process via Starlette TestClient):/healthz → "ok" / / → 1063B(title + static link + version) / /static/style.css → 1624B / /nonexistent → 404。Linux portability 顺手:模板里 path 显示约定用 Path.as_posix()(G3+ 模板落地);SSE 响应头 G4 上时带 X-Accel-Buffering: no(nginx 反代友好)。
  • 05-14 / §7 Phase G G2 task list 页:web/app.py::list_tasks(limit, status) 读 PG tasks + messages count(updated_at 降序),返回模板友好的 dict 列表;不复用 cli.py::_list_task_rows —— CLI 拿 tuple, Web 拿 dict,数据形状有别,等真有 schema 变更同步成本时再抽(避免预付抽象)。/ 路由换成 task 表渲染,filter via ?status=active|completed|abandoned(无效值静默降级为 all);/tasks/{task_id} 占位路由 UUID 校验 + DB 存在性校验,缺一则 404,有效则渲染 task_placeholder.html(G3 来填消息流)。Linux portability 落地:_norm_path() 把存的 backslash 在显示时全替成 forward slash(Path.as_posix() 在 Linux 读 Win backslash 串时不归一,所以直接 replace('\\','/'));Win Path.resolve() 存 D:\projects\...、Linux 存 /home/user/...,都能正确显示。template:home.html 表格(id/updated/status/mode/model/msgs/tokens/desc-dir),status 用 badge(status-active/completed/abandoned 配色),hover 高亮;空态文案。CSS:table 紧凑(.9rem)+ tabular-nums 对齐 + accent-soft placeholder note。Smoke 18 路径全绿(in-process):3 task seed(active/completed/abandoned)+ Win\Linux 双路径形态 → / 渲染对、status filter 正/反向、garbage status 静默 all、UUID 占位、notauuid 404、ghost UUID 404、limit 生效、/healthz 不退化。版本 0.1 → 0.2。
  • 05-15 / §7 Phase G G3 chat 只读页:web/app.py_get_md() 单例 MarkdownIt(gfm-like 预设 + linkify + breaks,html=False 禁内联 HTML 防 XSS),fenced code 走 pygments _pygments_highlight() 回调(codehilite cssclass)。load_chat_messages(tid) 读 PG idx asc;build_chat_blocks(messages) 聚合显示块 —— system / tool 不入 block(tool 内嵌进 assistant 的 tool_call.result),user / assistant text 走 markdown 渲染,assistant.tool_calls 配对 tool result(orphan tool_call → [no result])。_args_preview 60 字符截断,_pretty_json 解析失败 fallback 原串。/tasks/{id} 替换占位为 chat.html 渲染,删 task_placeholder.html。template:.msg 卡片(user 浅蓝 / assistant 白底),.body markdown 区(<pre> / <code> / <table> / <blockquote> / <s> 全 GFM 样式),tool_call 用 <details> 默认折叠(无 JS,浏览器原生开闭;summary 显示 tool 名 + args 前 60 字预览,展开看 args_pretty + result)。CSS 加 .codehilite 浅色 token 配色(keyword / string / comment / function / number / operator 6 类,余下黑色)。Smoke 28 路径全绿:4 display blocks(user/assistant×3,system/tool 跳过)+ markdown 特性(table / fence / autolink / strikethrough / bold)+ tool 配对(call_1 命中、orphan 走 [no result])+ HTML 含 <details>/tool-badge/codehilite/<s> + 空 task 文案 + invalid UUID 404 + util 单测(args_preview / pretty_json / render_md 边界)。版本 0.2 → 0.3。requirements 加 markdown-it-py[linkify] / mdit-py-plugins / pygments
  • 05-15 / §7 Phase G G6 部分:/new 入口(提前于 G5 落):用户反馈 Web 没"新建对话"入口 — 加 GET /new 表单页(description / mode / task_dir 三字段)+ POST /new 处理(strip 校验 + descriptiontask_dir 至少填一个否则 400 + check_no_subtask 同 CLI / build_agent 一致拦前缀嵌套 → 409 + ensure_local_task_row 写占位行 + 303 See Other 跳转 /tasks/{tid})。task_dir 空 → 默认派生 workspace/tasks/<uuid>/(同 _default_task_dir),显式 → Path.expanduser().resolve() 同 cli.py --task-dir。模板 new_task.html 加表单 + error 渲染(400/409 重渲带 form_state 不丢用户填的值);home.html 加 + new task 主按钮 + nav 加 new 链接;base.html 默认 nav 也带 tasks/new。CSS 加 .btn-primary / .new-task-form / .navlinks .active 配色。懒创建保留语义:Task 在 /new POST 时入库,后续 build_agent 走 resume 路径(已存在,不冲突);CLI REPL /new 仍走 build_agent 懒创建路径,不互相干扰。Smoke 21 路径全绿:GET 表单 200 + 三字段 / POST happy(description-only / custom task_dir)→ 303 + Location 正确 / DB 行字段对 + default-derived task_dir 含 uuid / 空描述空 task_dir → 400 重渲表单带 error / no-subtask 父子嵌套 → 409 + 错误文案 / home 页 + new task 按钮 + nav 链接 / /new nav 链接 active 标记。版本 0.4 → 0.5。
  • 05-15 / §7 Phase G G4 chat 发送 + SSE 流式:新增 web/broker.py::RunBroker(in-process pub/sub,subscribe/emit/close/unsubscribe)+ web/sinks.py::WebEventSink 实现 §7 A 的 sink 协议,把 AgentLoop._emit 桥到 broker。异步策略 = asyncio.to_thread(不改 core):POST /tasks/{tid}/messages async handler → 校验 task + INSERT runs 行 + asyncio.create_task(asyncio.to_thread(_run_agent_bg, ...)),_run_agent_bg 在工作线程跑 build_agent(resume=True) + agent.run,sink 通过 loop.call_soon_threadsafe(q.put_nowait, ev) 跨线程桥事件回 asyncio queue。多访问策略 = fan-out:每订阅一个独立 asyncio.Queue,同 run 多 tab / 刷新 / 桌面+移动都看得到流;_done 集合让晚到订阅者立即收 done(不挂)。GET /tasks/{tid}/runs/{rid}/eventsStreamingResponse async gen,响应头带 text/event-stream / Cache-Control: no-cache / X-Accel-Buffering: no(nginx 反代友好);第一帧发 : connected\nretry: 3000\n\n 让 EventSource 立即建立,30s 无 event 发 : ping 注释心跳。SSE multi-line data:HTML 片段含换行,每行加 data: 前缀(SSE spec),EventSource API 还原成 \n 拼接的 HTML 字符串。Event → HTML 片段:_render_event_fragment 渲染 text/tool_call/tool_result/error 四种,run_start/llm_start/llm_end/done 发空 data(只让客户端识别 event type)。新 fragment 模板 _frag_text.html / _frag_tool_call.html / _frag_tool_result.html / _frag_error.html + _send_response.html(POST 响应:user msg 卡 + msg-assistant streaming 容器带 sse-connect/sse-swap/sse-close)。chat.html 加 send 表单(Enter 发送、Shift+Enter 换行,HTMX hx-post / hx-target=#chat-stream / hx-swap=beforeend / hx-on::after-request reset);chat section 改 id="chat-stream" 让 SSE 追加进同一容器;非 active task 隐藏表单。CSS 加 .streaming .run-indicator 红点脉冲 / .send-form 表单样式 / .tool-result-inline 追加式样式 / .msg-error 错误卡。Run 状态写 PG runs:POST 时 status=running,正常完结 status=ok + tokens_p/c,异常 status=error + error 文本;DB 写失败不放大噪声(已 emit error 给前端)。lifespan bind_loop(asyncio.get_running_loop()) 让 broker 拿到 asyncio loop 引用。Smoke 双层全绿:broker 单元 8 case(subscribe/emit/get、fan-out 双订阅、跨 run_id 隔离、close 派 done、late subscribe 立刻收 done、unsubscribe 后失联、WebEventSink 桥、unbinded loop silent drop);端到端 24 case(POST 200 + HTML 含 sse-connect + run_id 抽出 + SSE stream content-type/x-accel-buffering/cache-control 头对、event types 序列 run_start/llm_start/text/tool_call/tool_result/llm_end/done、text fragment 含 <strong> markdown、tool_call 含 <details>、tool_result 含 preview、empty body 400、invalid/ghost UUID 404、late subscribe 立刻 done、PG runs 行 INSERT)。版本 0.3 → 0.4。TODO:并发同 task 多 run 互锁(messages idx UniqueConstraint 在并发 POST 下会冲突 — 用户连续点 send 暂时不会触发,但需要在 G6 或 D 阶段加 lock_for_update);event log 持久化(刷新继续看流式)留到未来。

关键决策与偏差

决策 备注
工具基目录 cwd(读)+ task_dir(写) system prompt 同时注入两者绝对路径
Workspace 布局 tasks/<id>/ + memory/{core.md, extended/} memory 跨 task 共享
Eval Suite 不做 个人工具 dogfooding
版本化 prompt 直接 general_v1.md Windows 软链接麻烦,真要切再做
run_python 沙盒 subprocess + env 过滤 Docker 在 §7 C 阶段

文件清单

core/capabilities.py        71
core/llm.py                 89
core/loop.py               152   ← §7 A: sink.emit
core/sinks.py              101   ← §7 A
core/ui.py                  38
core/probe.py              243
core/session.py            153   ← §7 B Step 2-3: ORM + ensure 补 meta
core/skills.py              81
core/task.py                82   ← §7 B Step 3: PG-backed TaskState,去 cwd
core/memory.py              76
core/export_docx.py        379   ← §7 B Step 2-4: task_id 升一等
core/storage/__init__.py    27   ← §7 B Step 1-3
core/storage/engine.py      80   ← §7 B Step 1
core/storage/models.py     124   ← §7 B Step 1
core/storage/utils.py      139   ← §7 B Step 3-6: +upsert_task/update_task/check_no_subtask
tools/base.py               34
tools/fs.py                182
tools/shell.py              94
tools/run_python.py         84
tools/skill_tool.py         45
main.py                    277   ← §7 B Step 4-6: +is_managed_task_dir / task_dir_arg / no-subtask check
cli.py                     558   ← §7 B Step 4 / Phase G G1: --task-dir / web 子命令
db/migrations/env.py        61   ← §7 B Step 1
db/migrations/versions/
  0001_initial_schema.py   125   ← §7 B Step 1
web/__init__.py              5   ← Phase G G1
web/app.py                 538   ← Phase G G1-G4 + G6/new: 工厂 + list/chat + SSE + /new
web/broker.py               88   ← Phase G G4: in-process pub/sub
web/sinks.py                20   ← Phase G G4: WebEventSink (§7 A sink 协议)
─────────────────────────────────
Python 合计              ~3732 行
+ web/templates/* ~249 行(base/home/chat/new_task + 5 个 _frag/_send_response)+ web/static/style.css 193 行(不计 Python 主仓库)

加 skills/ppt 脚本 ~600 行 + SKILL.md / references / config / prompts + alembic.ini,总仓库约 3500 行。


下一步候选(性价比排序)

  1. 真实 LLM 跑通 G4 + /new 全流程(~10 分钟)—— smoke 走的是 mock,需在浏览器开:cli.py web/new 建 task → 跳 chat → 发"你好" → 看真实流式 → 刷新看历史。
  2. §7 Phase G G5 文件浏览 + 上传下载(~半天)—— /tasks/{id}/files 渲染 task_dir 树,upload (multipart)/ download / 删。
  3. §7 Phase G G6 剩余打磨(~半天)—— /done /abandon 按钮、/export docx 下载、错误 toast、并发 run 互锁(messages idx 冲突)。/new 已提前完成。
  4. §7 C Executor + sandbox(~2-3 天)—— Phase G 完后再做,或穿插。
  5. Phase 6 context 三层压缩(~1 天)—— 兜底,V4 长上下文一般用不到。
  6. Proposal mermaid 预渲染(~半天)—— ASCII 透传不够用时再上 mmdc

§7 B 已完工。Phase G 进行中(G1 G2 G3 G4 G6/new )。剩余路线:G5 + G6 剩余 → C(Executor)→ D(HTTP /v1 + OIDC)→ E(CLI 双模式)→ F(deploy / billing)。