zcbot/PROGRESS.md

31 KiB
Raw Blame History

实施进度

配合 DESIGN.md。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 2-4 句:做了啥 + 关键判断 + 没动什么;细节查 git log / git diff

最后更新:2026-05-20(dev SPA 任务列表行 meta 加最近操作时间相对显示 + 新建弹框工作目录改下拉)


状态

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.pymain.py,装配 lib 挪 core/agent_builder.py);真 OIDC 待;C(Executor)待。

已完成关键能力

2026-05-20

  • dev SPA 左侧任务列表行加「最近操作时间」:用户要"显示最新操作时间"。renderTaskList 行 meta 区(badge / skill / N 条 / N tok / id-slice)在 id-slice 之前插一个 <span class="muted" style="margin-left:auto;">,文案用新加的 fmtTimeAgo(iso) 相对时间 helper:<60s→刚刚 / <1h→N 分钟前 / 同日→N 小时前 / 昨日→昨天 HH:MM / 同年→MM-DD HH:MM / 跨年→YYYY-MM-DD,title= hover 出完整 fmtTime locale 串。margin-left:auto 从 id-slice 挪到时间 span(让两者一起靠右,中间 8px .meta gap 自然分隔)。字段用 updated_at(任务任何写操作 — 改名 / 新消息 / 状态切 — 都会更新,贴合"最新操作"语义),/v1/tasks payload 早已包含,后端零改。没动:左 pane 列表默认排序仍 -created_at(用户改排序顺序时另说);id-slice 保留(调试参考)。
  • dev SPA 新建任务弹框「工作目录」从 input + datalist 改 <select> 下拉:用户要"做成下拉选择"。原 <input list="folders-datalist"> autocomplete 改 <select id="nt-wd-sel">,选项 = (留空 · 用任务名作目录) + 既有目录(name — N 个任务 / 空目录) + + 新建目录… sentinel(__new__)。选 __new__ → 显示备用 <input id="nt-wd-new"> 输入新目录名 + autofocus,提交时 working_dir = sel === "__new__" ? nt-wd-new.value : sel。hint 区改 updateWdHint() 三分支(新建 / 留空 / 复用),change + new-input + name-input 三事件触发。<datalist id="folders-datalist"> 留在 modal 内但不再被它消费,只供左 pane 顶部 #filter-wd 筛选 autocomplete(datalist 按 id 引用,DOM 位置无关);loadFolderSuggestions() 同次拉取灌两边。没动:/v1/folders API、提交 body 形态(仍 working_dir: string,空串语义不变 → 后端 fallback 用任务名)、左 pane filter-wd 仍用 input + datalist(用户只点名"任务弹框")、DESIGN / RUN。Tradeoff:纯 select 实现最直接但会失"新名则新建",改两段式(select 含 + 新建…,触发后展开 text input)保留所有原能力。
  • 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_agentworking_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/modelsconfig/models/*.yaml 列可选项(含 display_name / thinking_mode / is_default),ModelCapabilitiesdisplay_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_agentuser_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/downloadCache-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.pymain.py,原 main.pycore/agent_builder.py,删 CLI REPL,§7 E 撤:main.py 混三角色(装配 lib + utility + cli/web 共 import 的事实入口),按 SoC 拆。git mv 两次(覆盖)+ 5 处 from main importfrom 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 就够。tasksrun_status text default 'idle'(idle/running/cancelling/error,error 是唯一持久终态)+ run_error text。Broker 全 task_id 索引 + 加 start(task_id) 清上轮 done 标记。dev SPA:state.currentRunIdstate.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-14→05-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 + 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>/
  • 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/shellExecutor.run(...),本地保留 subprocess、SaaS 走 docker;api_key_envKeyProvider 运行时注入。多用户在线跑代码前置。
  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 并存不冲突。