31 KiB
设计文档
本地运行的个人任务 agent,覆盖三类工作:汇报 PPT、科研申报书、代码。 模型自由(LiteLLM 接 OpenAI-compatible),代码可控(目标 1500-2000 行 Python)。
1. 边界
做:PPT(python-pptx)/ 申报书(python-docx)/ 编码(读写文件 + shell + 迭代验证)。
不做:子 agent / IM 渠道 / 自定义 RAG / 锁定 Anthropic / Eval Suite(个人工具 dogfooding 替代)。多用户 / Web UI 归 §7。
关键约束:
- 模型自由:LiteLLM + OpenAI-compatible(默认 DeepSeek V4)
- 任务持久化:任意时刻关机,下次能恢复
- 演化性:模型升级不需要大改架构
- 形态兼容:本地 CLI 与 SaaS 共享同一份 core 和 storage(PG,无 SQLite / JSON 分支);CLI 长期保留(本地直跑 +
--remoteAPI client 双模式)
2. 架构
zcbot/
├── core/
│ ├── capabilities.py # ModelCapabilities,从 yaml 加载
│ ├── llm.py # LiteLLM 封装,按 capabilities 自动启 features
│ ├── loop.py # ReAct 主循环
│ ├── probe.py # 真实探测对账 yaml 声称的能力
│ ├── session.py # 消息列表 + meta + 落盘
│ ├── skills.py # SkillRegistry (Anthropic 渐进披露)
│ └── task.py # TaskState
├── tools/
│ ├── base.py # Tool 基类 + _resolve
│ ├── fs.py # read / write / edit (唯一匹配) / glob / grep
│ ├── shell.py # subprocess + 黑名单
│ ├── run_python.py # tmp .py + subprocess,过滤敏感 env
│ └── skill_tool.py # load_skill
├── skills/{coding,ppt,proposal}/ # SKILL.md + references / scripts / assets
├── prompts/system/general_v1.md
├── config/{agent.yaml, models/deepseek_v4.yaml}
├── workspace/
│ └── users/<user_id>/
│ ├── .memory/{core.md, extended/*.md} # 跨 task 共享记忆(user 级,dotfile 隔离)
│ └── <working_dir>/ # 工作目录,用户起名(同 working_dir 多 task 共享),仅 skill 产物
└── {main.py, cli.py}
工作目录(working_dir) = workspace/users/<user_id>/<working_dir>/,所有 skill 产物写到这里,绝对路径在 system prompt 显式给 agent(prompt 里仍叫 task_dir 占位符,跟 SKILL.md DSL 一致)。写错位置(cwd / skills/ / repo 根)git status 立刻报红。本地 CLI user_id 固定为 SENTINEL(00000000-...);web/JWT 路径用 sub。name(任务显示名)必填,working_dir 可选(留空 → 用 name 作目录名);两者都是简单名(不含 /\..、不以 . 起头,挡 .memory);同 working_dir 多 task 自动共享同目录(§7.1)。SaaS 化只是把 workspace/ 换 <storage_root>/,布局不变。
启动:读 agent.yaml → 加载 ModelCapabilities → LLM(caps) → 解析 task_dir → 拼 system prompt(general_v1.md + skill discovery + cwd + task_dir 绝对路径)→ 装配工具 → REPL。新建路径懒创建,不预占文件(§3.6)。ZCBOT_DB_URL 指 PG(本地 docker compose / 远端 dev / 生产)。
3. 核心组件
3.1 主循环(core/loop.py)
ReAct:LLM → 若有 tool_calls 就执行 → 结果塞回消息 → 再调 LLM。无 tool_call 即返回。
- 工具结果对模型截 16K 字符,用户预览 400 字符
- 后台 daemon 线程每 100ms 刷 spinner:
thinking... 1.3s ctx 12,345 tok - 每轮 LLM 返回追加 dim 一行
[in N out N t Xs] - assistant 文本走
rich.markdown.Markdown整段渲染(非流式) max_iterations从 capabilities 读
3.2 Model Profile(core/capabilities.py + config/models/*.yaml)
每模型一份 yaml,agent 行为按档案动态调整。新模型 5 分钟接入,不改代码。
字段:max/reliable_context、max_output、parallel_tools、tool_calling_quality、thinking_mode、reasoning_effort_levels、code_quality、enable_run_python、max_iterations、optimal_temperature、prompt_caching、extended_thinking、api_base、api_key_env。
LLM.chat 按 capabilities 自动启 parallel_tool_calls / reasoning_effort / Anthropic prompt-caching header。
3.3 Capability Probing(core/probe.py + cli.py probe)
yaml 是手填的,probe 用真实调用对账:basic_chat / parallel_tools / thinking_mode / long_context(opt-in)。不改 yaml,只出 rich Table 报告。显式触发,不进启动路径(避免烧 API)。
3.4 工具系统(Hybrid 范式)
JSON tool call(tools/):read / write / edit / glob / grep / shell / run_python / load_skill —— 离散操作。
Code execution(run_python):tmp .py + subprocess + 工作目录限制 + 敏感 env 过滤(*API_KEY *TOKEN *SECRET *PASSWORD *PRIVATE_KEY)—— 批处理 / 算数据 / 生成文档。
关键设计:edit 唯一匹配(CoreCoder 风格,old_str 重复即报错);工具按原子操作切分,不做 make_pptx() 这种高级封装。
3.5 Skill 系统(Anthropic 渐进披露)
对齐 Anthropic 2025-12 开放标准。三层加载:Discovery(name + description,几百 token)→ Activation(load_skill(name) 加载完整 SKILL.md,1-5K)→ Execution(SKILL.md 指 references/xxx 按需拉)。
原则:写 WHY+WHAT,不写 Step 1/2/3。description 决定模型能否触发。
3.6 Session 与 Task
Session(core/session.py)= 消息列表 + meta,直接 ORM 写 PG messages 表(append-only,jsonb 存 LiteLLM 原样 payload)。
Task(core/task.py)= Session 上层,含 name / working_dir / skill / description / status / model / reasoning_effort / 时间戳 / tokens。直接 ORM 写 PG tasks 表。working_dir FS 目录只存 skill 产物,无 state.json / messages.json。本地 + SaaS 同一份 schema 和 ORM,差别只在 ZCBOT_DB_URL。
字段三件套语义:
name(NOT NULL) = 任务显示名,UI 列表 / 标题 / docx 导出文件名用;独立于工作目录working_dir= 工作目录(相对 ROOT posix 串),同 working_dir 多 task 共享同物理目录skill= 智能体类型标签(coding / ppt / proposal / ...自由形式,后续可对齐skills/注册表强校验)
创建语义 —— working_dir 目录在 task 创建入口立即 mkdir(parents=True, exist_ok=True)(name 必填代表"显式声明项目";working_dir 留空 → fallback 用 name 作目录名)。Task 行在 web /v1/tasks POST 时即写;CLI 内仍走 Session.append 首条 user 消息触发的占位 INSERT(ensure_local_task_row idempotent,name 透传给 NOT NULL 列)—— REPL 启动后立刻 /exit 不留 DB 行(目录留着无害,跨 task 复用)。
REPL 内 task 切换 —— /new / /resume [last|<id>](无参列最近 10 个)/ /done /abandon / /desc。切走前 _cleanup_if_empty 守门:无 user message → DELETE DB 行;FS 一律不动(同 name 跨 task 共享,绝不 rmtree)。
原子性 —— PG INSERT 天然原子;skill 产物走 core.session.atomic_write_text(tmp + fsync + replace)。
CLI:chat --name "<任务名>" [--working-dir <目录名>] [--skill coding] [--desc "..."] [--resume last|<id>] [--remote <url>];tasks [--status ...]。
3.7 双层记忆(core/memory.py)
跨 task 共享的事实(用户偏好 / 项目约定 / 模型 quirk)放 workspace/users/<user_id>/.memory/(per-user,dotfile 隔离):
| 层 | 文件 | 加载 | 适合 |
|---|---|---|---|
| Core | core.md |
每次 build_agent 进 system prompt | 跨任务高频精炼事实(几百 token) |
| Extended | extended/*.md |
索引(标题+绝对路径)进 prompt,内容靠 read 工具按需拉 |
大量低频专题 |
system prompt 每次 build_agent 重建,resume 也走 _build_system_prompt 并覆盖 messages[0] —— memory 演化即时生效。
memory 由人填(也允许 agent 用 write 写),系统不自动维护 —— 关键差异:事实由用户判断,不由 LLM 自动总结。
memory 永远在 FS,不入 DB:统一 <workspace_or_storage_root>/users/<user_id>/.memory/(本地直接是 workspace/,SaaS 是 <storage_root>/,bind mount 进容器)。本地 CLI 走 SENTINEL user;web/JWT 走 sub。dotfile .memory/ 命名:跟用户起的项目目录(同样落 <uid>/ 下)区分,避免项目名取 memory 时撞名;validate_task_name 拒 . 起头双向防呆。理由:用户笔记语义,FS 读写 + 编辑器手编是产品的一部分;跨 task 共享靠"同一 user 同一目录"自动达成,无需 schema。
4. 模型路由
默认 default_model: deepseek_v4.flash。后续分模式路由思路:
| 模式 | 模型 | 理由 |
|---|---|---|
| 通用 / 编码 / PPT / 提案初稿 | flash | SWE-Bench 80.6,够用 |
| 复杂 bug / 提案终稿 | pro + reasoning_effort=max | 关键产出 |
| fallback | claude_4_7.opus | V4 不行时手动切 |
成本量级(对比):
| 任务 | flash | pro-max | Opus 4.7 |
|---|---|---|---|
| 修 bug(~10 轮) | $0.01 | $0.05 | $0.30 |
| 5 页 PPT | $0.05 | $0.20 | $1.50 |
| 完整申报书 | $0.30 | $1.50 | $10-15 |
99% 任务 flash 够用,关键终稿升 Pro。
5. 设计哲学
核心原则:Less Scaffolding, More Trust
老 agent 框架失败的核心:给 LLM 太多脚手架,模型升级后这些脚手架成枷锁。正确做法:把 LLM 当一个会持续变强的同事,告诉它目标,不告诉它步骤。
七条具体原则
- Prompt 用 WHY+WHAT 不用 HOW —— 教"怎么思考"会降智强模型
- Skill 渐进披露,不写完整流程
- 工具按原子操作切分,不做高级封装 —— 留组合空间
- Model Profile 化,不硬编码
- Capability Probing 对账实际行为
- 版本化 Prompt(等真要切版本时再做)
eval 评估—— 已删,dogfooding 更有效
借鉴
| 来源 | 借鉴 |
|---|---|
| CoreCoder | 主循环简洁实现 + Edit 唯一匹配 |
| Anthropic Skills | SKILL.md 渐进披露 |
| nanobot | Workspace + 任务隔离 |
| smolagents | LiteLLM + CodeAct 启发 run_python |
6. 风险与取舍
| 风险 | 缓解 |
|---|---|
| run_python sandbox 不够强(本地非真隔离) | 工作目录限制 + 敏感 env 过滤;SaaS 走 docker exec(§7.5);本地依赖用户最终审阅 |
| V4 某些复杂任务不如 Claude | dogfooding 判断,fallback 手动切 |
| Skill description 不准 → 触发不到 | Pro 优化描述,实战观察 |
| Long context 退化 | probe --long-context 探测可靠 ceiling |
| 本地 PG 离线 | docker compose up -d 起本地 PG 兜底;也可连远端 dev / staging PG |
Hybrid 范式而非纯 CodeAgent:V4 JSON tool call 已稳定;sandbox 成本只在需要时付;兼容 thinking。 Anthropic Skill 标准:行业标准已成,跨 SDK 兼容。 不做 subagent:状态管理爆炸;单 agent + skill 已覆盖 95% 场景。 不做 Eval Suite:个人单用户场景,dogfooding 信号比造作 case 强,probe 覆盖健康检查。
7. SaaS 化(草案,status=design,2026-05-12)
§1-§6 是本地 dogfood 形态;本节是SaaS 形态,把 core 包成多用户在线服务。 不引入 platform/core 切分 —— core 就是后端,直接对用户做 auth。两条形态共享同一份 core,差别只在 CLI 入口 vs HTTP 入口。本节落地前 §1-§6 路线照走,不阻塞 dogfood。
7.0 与本地形态的兼容性
SaaS 化不是"重写"也不是"取代 CLI",而是给同一份 core 加一个 HTTP 入口。落地过程中本地 CLI 始终可用。
共享:同一份 core/ / tools/ / SKILL.md / prompts。
差别:
| 维度 | 本地 | SaaS |
|---|---|---|
| 入口 | cli.py chat 直调 core |
HTTP /v1/... + SSE |
| Storage | PG(ZCBOT_DB_URL 指 docker compose / 远端 dev PG) |
PG(指生产 PG) |
| task_dir 派生 | workspace/users/<sentinel>/<name>/(name 必填,简单名) |
<storage_root>/users/<user_id>/<name>/(name 必填,简单名) |
| Memory | workspace/users/<sentinel>/.memory/(FS,dotfile) |
<storage_root>/users/<user_id>/.memory/(仍是 FS,dotfile) |
| Sandbox | subprocess + env 过滤 | per-task docker exec |
| Auth | 无(user_id='local') |
PLATFORM_KEY → JWT(过渡)→ OIDC |
CLI 长期双模式:本地直跑(默认,in-process,直连 PG,适合调内部状态)/ --remote https://...(HTTP 走 /v1,等价真实用户路径)。两模式共用 cli.py,差别只在 transport 层。
workspace/ 仅存 skill 产物,state / messages 全在 PG。本地 vs SaaS 共用 users/<user_id>/ 子树布局,差别只在外层根目录(workspace/ vs <storage_root>/),不在 storage 形态。
7.1 心智模型:Task 一等公民 + Dir 文件副视图
两个并列入口,正交不嵌套:
| 视图 | 入口语义 | 适用场景 | API |
|---|---|---|---|
| Task list(主) | "我的对话历史" | 任务驱动:"继续昨天那个 bug fix" | GET /v1/tasks?status=&task_dir= |
| Dir tree(辅) | "我的文件资产" | 项目驱动:"看汇报项目里所有素材 + 关联对话" | GET /v1/folders |
类比:macOS Finder + 最近使用 / Apple Notes 文件夹视图 + 全部备忘录。两个视图查同一份数据的不同切面,dir 不是 task 的父容器。
- Task = DB 一行,一等公民,自带
task_dir text字段:- 新建必给
name(简单名),task_dir = workspace/users/<user_id>/<name>/。同 name 多 task 共享 → "同一项目多对话"语义;不再支持空 task_dir / 自动 UUID 派生(原 ChatGPT thread 模式取消,纯对话也得起个项目名) - 指定 → 项目化 task,同 task_dir 多 task 自动共享
source//sections// 终稿(无需建"项目"实体)
- 新建必给
- Dir = FS 路径,无 DB 实体,path 即标识;无父子结构,改名走 prefix cascade(§7.4)
- No-subtask:同 task_dir 允许(同项目多对话),前缀嵌套拒
- Messages = DB 表,append-only,
jsonb存 LiteLLM 原样 payload - Skill 产物全落 task_dir,不引入 artifacts 表;SKILL.md 指示 agent 清中间件
- Skill 定义是项目代码,跟部署走,所有用户共享
空 dir(用户上传素材但还没开 task)在 dir tree 视图正常展示 —— 上传本身是有效产品行为;UI 上跟"有 task 的 dir"做轻量区分(如 task 数 badge)。
state / messages 两形态都在 PG,FS 只承担 skill 产物。多 task 共享同 task_dir 时由 §7.8 文件级悲观锁兜底。
7.2 资源模型(/v1)
Task 一等公民,files 是其副视图(经 task_dir 暴露,无独立 folder 实体)。所有路由统一 /v1 前缀,返 JSON;前端 / UI 由 platform 端实现,本仓库不维护(§7.9 取舍)。本地开发用 FastAPI 自带 /docs Swagger UI 自查;GET / 302 跳 /docs。
Tasks
POST /v1/tasks 创建 {name(必填), working_dir?, description?, skill?};
留空 working_dir → 用 name 作目录名;
working_dir 派生 workspace/users/<user_id>/<working_dir>/;
name/working_dir 不合法 → 400
GET /v1/tasks?status=&limit= 列表(updated_at 降序,?status=active|completed|abandoned)
GET /v1/tasks/{id} 单 task meta + 完整 messages
PATCH /v1/tasks/{id} {status?,description?,name?,skill?};status 从 web 不让切回 active(走 CLI)
DELETE /v1/tasks/{id} 硬删:DB 行 + messages(CASCADE);**FS working_dir 保留**
(同 working_dir 多 task 共享,文件由用户经 /files/delete 单独清)
GET /v1/folders 列当前 user 的 working_dir(FS 是 source of truth + 关联 task 计数 + 最后使用时间)
GET /v1/tasks/{id}/messages 历史(后续 ?search= 走 jsonb GIN / tsvector)
POST /v1/tasks/{id}/messages {content} 发消息 + 起 run,返 {run_id}
GET /v1/tasks/{id}/runs/{rid}/events SSE 流(见下)
POST /v1/tasks/{id}/runs/{rid}/cancel (待)
Files(per-task,task_dir 副视图)
GET /v1/tasks/{id}/files?path= 列子目录 {entries, crumbs, exists, root}
POST /v1/tasks/{id}/files/upload multipart;path 通过 query 或 form;严格拒含 / \\ .. 的 filename
GET /v1/tasks/{id}/files/download?path= 下载单文件;`..` / 绝对 / symlink 越界 400
POST /v1/tasks/{id}/files/delete {path} 文件或空目录;非空目录 400
Export
GET /v1/tasks/{id}/export docx 临时文件下载,BackgroundTask 删 tmp
Misc
GET /healthz {"status":"ok"}
GET / 302 → /docs (Swagger UI 自查,本地形态便利)
SSE 事件(Content-Type: text/event-stream,响应头带 X-Accel-Buffering: no 给 nginx 反代友好;每事件 event: <type> + data: <JSON>):
run_start {}
llm_start {}
text {"content":"<delta 或全量,取决于 model streaming 配置>"}
tool_call {"name":"...","args":{...},"args_preview":"..."}
tool_result {"name":"...","preview":"...","truncated":bool} # 完整 result 走 DB,SSE 只送预览给 UI
llm_end {"prompt_tokens":N,"completion_tokens":N}
error {"msg":"<type>: <detail>"}
done {}
订阅 fan-out:同 run 多订阅者(刷新 / 多 tab / 多设备)每订阅 1 独立 queue。订阅迟到(run 已 done)立刻收 done 不挂。事件不持久化 —— messages 走 PG,未来要"刷新继续看流式"再加 event log。
版本化:/v1 minor 半年向后兼容,major 6 个月 deprecation。/v1internal 实验位(未启)。
CORS:本地 dev allow_origins=["*"];部署 platform 时收紧到 platform 域名 allowlist。
Auth:PLATFORM_KEY → JWT 兑换(过渡形态,见 §7.3);Authorization: Bearer <jwt> 走所有 /v1/tasks*;/healthz、/docs、/openapi.json、/、/v1/auth/login、/static/* 豁免。
7.3 认证
当前形态(D' 过渡,2026-05-15 落地):platform 服务端(或 dev 浏览器)持有 PLATFORM_KEY 共享密钥,调 POST /v1/auth/login {user_id, platform_key} → 后端校验 key 匹配 → 签 HS256 JWT(sub=user_id,默 7d TTL,JWT_SECRET env 签)→ 返 {token, expires_at, user_id, ttl_seconds}。后续 Authorization: Bearer <jwt> 走所有 /v1/tasks*,FastAPI Depends(require_user) 验签 → 提取 user_id → SELECT/UPDATE 全带 Task.user_id == user_id 条件做隔离。PLATFORM_KEY / JWT_SECRET 任一缺失 → app 启动 fail-fast。信任模型:platform 是单点可信中间层(持 KEY = 可为任意 user_id 签 token,等同 user 身份由 platform 注入);风险与"platform 服务端泄漏 = 用户身份泄漏"同级,可接受。
未来形态(真 OIDC,D 阶段后期):OIDC / Clerk / 自建邮箱登录,Provider 签 ID token,zcbot /v1/auth/login 内部从"校验 PLATFORM_KEY"换成"校验 ID token 签名 + 提取 sub" —— 路由层 Depends 不动,Bearer JWT 契约不变。所有 storage/executor scoped by user_id。无 tenant 层 —— 个人 SaaS 用不上,做企业版再加 org_id 等价隔离。
7.4 存储:Postgres + 本地文件系统
users(user_id uuid pk, email null, password_hash | oidc_subject null, plan null, created_at)
-- 本地形态固定 INSERT sentinel: user_id = '00000000-...',email/auth/plan 全 NULL
tasks(task_id uuid pk, user_id fk, name text not null, working_dir text not null, skill, description,
status, model_profile, tokens_prompt, tokens_completion, cost_usd,
created_at, updated_at);
create index on tasks (user_id, working_dir);
-- working_dir 存储约定:本地 ROOT 内 → 相对 ROOT 的 posix 串
-- (`workspace/users/<user_id>/<name>`,name 是简单名,无 /\..);
-- 新建强制 `name` 必填,空串只可能在 legacy 数据(开发期已 wipe)。
-- SaaS 阶段同理(基础是 <storage_root>/users/<uid>/)。
-- 读写边界统一过 core/paths.py::{to_db_path,from_db_path}。
-- 入口校验 main.py::validate_task_name(): 拒空 / 含 /\NUL / `.` 起头 / >255。
messages(message_id uuid pk, task_id fk, idx int not null,
payload jsonb not null, tokens_in, tokens_out, created_at,
unique (task_id, idx));
create index on messages using gin (payload jsonb_path_ops);
-- 全文搜按需加 tsvector + GIN(中文 simple + pg_trgm 起步)
runs(run_id pk, task_id fk, status, started_at, finished_at, error, tokens_p, tokens_c)
usage_events(id, user_id, task_id uuid, run_id uuid, kind, value, ts)
-- append-only。task_id/run_id 不 FK,task 硬删后审计仍存活
No-subtask 校验(create_task):查同 user 下是否存在 new LIKE existing/% 或 existing LIKE new/%,中一则拒;同 task_dir 允许。两侧先用 from_db_path 归一到 absolute posix 再比前缀(混合存储形态 [相对+绝对] 不会漏判),数量小直接 Python 端比对,不在 SQL 里拼分隔符。
Folder rename(old → new,FS rename 成功后):UPDATE tasks SET task_dir = new || substring(task_dir from len(old)+1) WHERE user_id=? AND (task_dir = old OR task_dir LIKE old||'/%')。用 old/% 而非 old%,避免 project_a 误中 project_a_other。running task 引用时禁 rename / delete。
Folder delete:hard cascade,前端 modal 列影响面 + 输入 folder 名二确认。先 DELETE messages → DELETE tasks → FS 递归删;DB 成功 FS 失败由后台 GC 兜底清孤儿目录。usage_events 不参与 cascade。
文件系统(本地 <storage_root> = workspace/,SaaS 替换为部署根,布局不变):
<storage_root>/users/<user_id>/
.memory/{core.md, extended/} # per-user 记忆,dotfile 隔离,不入 DB
<name>/ # 项目目录,name 用户起(必填),task_dir 直接落这
<name>/... # 同 name 多 task 共享同目录(§7.1)
本地优先 S3(部署简化 / 低延迟),storage 抽象层留好后续可换。
Storage 实现:单一 PG ORM(本地 + SaaS 共用):一份 schema、一份 SQLAlchemy、一份查询,无 adapter,无 SQL 方言适配,无契约测试。alembic 管 migration;CLI 启动校验 schema 版本,落后报错让用户跑 cli db upgrade(本地)或部署管线自动 alembic upgrade head(SaaS)。cli migrate-from-fs --workspace ./workspace 一次性导旧 JSON。
7.5 沙盒:Per-task 容器 + Per-run exec
| 选择 | 理由 |
|---|---|
| 每 task 长驻容器 | 起容器 ~300ms 太慢;多轮 tool call 共享划算 |
每 run 一次 docker exec |
exec 级 timeout / 资源限制 |
| 空闲 N 分钟回收 | 不浪费,resume 时拉起 |
| bind mount = user root | <storage_root>/users/<user_id>/ → /workspace;同 user 多 task 不互隔(协作方便),跨 user 由独立实例隔离 |
资源限制:cgroup CPU/mem、磁盘配额、egress allowlist(只放 LLM + PyPI 镜像)、root fs read-only、no-new-privileges、drop ALL caps。
选型:起步 Docker;流量起来后视情况换 gVisor / Firecracker / e2b。Executor Protocol 抽象后切换成本低。
7.6 Core 代码改造(按依赖顺序)
| # | 项 | 估时 |
|---|---|---|
| 1 | loop.py375bb29) |
done |
| 2 | Storage 落 PG:Session / TaskState 改 SQLAlchemy 写 PG;alembic;cli migrate-from-fs;docker-compose.yml 起本地 PG |
3 天 |
| 3 | task_dir 字段语义:新建必给 name(简单名),task_dir 派生为 <storage_root>/users/<user_id>/<name>/(本地 <storage_root> = workspace/,sentinel user);同 name 多 task 共享同目录;tools/fs.py::_resolve 接 task_dir 注入;system prompt 注入 |
1 天 |
| 4 | Folder API:list / create / rename(cascade + 锁 running) / delete(hard cascade) / upload / download | 2 天 |
| 5 | No-subtask 校验:create_task 入口跑 §7.4 SQL |
0.5 天 |
| 6 | Executor + sandbox:run_python/shell → Executor.run(...);本地保留 subprocess executor,SaaS 走 docker;api_key_env → KeyProvider 运行时注入 |
2-3 天 |
| 7 | HTTP /v1:FastAPI + SSE + OIDC | 4 天 |
| 8 | CLI 双模式:transport 层抽象,默认 in-process;--remote 走 HTTP;本地直跑不删 |
1.5 天 |
| 9 | 已落 |
代码量增量:+1000~1500 行(单一 PG 比双 adapter 省 500-800 行;UI 不计入,本仓库只维护 API)。
7.7 分阶段落地
| 阶段 | 范围 | 工作量 | 验收 |
|---|---|---|---|
| A | #1 事件流化 | done ✅ | sink 协议铺 SSE 路 |
| B | #2 #3 #4 #5(Storage 落 PG + task_dir 双形态 + no-subtask) | done ✅ | 本地走 PG,messages 进 DB,任务/消息/状态全在 PG;task_dir 改相对存储(§7.4 注释) |
| D | #7 HTTP /v1 surface(无 auth) | done ✅ | /v1/tasks/* + SSE JSON + files 4 路由 + export + Swagger;本地形态 sentinel user 跑通 |
| D' 过渡 | PLATFORM_KEY → JWT 兑换 + user_id 数据隔离 + dev SPA | done ✅ | POST /v1/auth/login 拿 token,Authorization: Bearer 走全部 /v1/tasks*;web/static/dev.html 单文件 3 栏 SPA 给开发自验。详 §7.3 |
| D' 真 OIDC | 替换 /v1/auth/login 内部为 ID token 校验 + CORS allowlist 收紧 | 1 天 | 真发布给真实用户前补;路由层 Depends 不动,只换 login 内部 |
| C | #6 Executor + sandbox | 3 天 | 两本地账号互不可见对方 folder,本地 subprocess executor 仍可用 |
| E | #8 CLI transport 双模式 | 1.5 天 | 默认本地直跑保留,--remote 走 HTTP 跑通 |
| — | 本仓库不维护 UI | ||
| F | 上线打磨(限流 / 监控 / 告警 / HA) | 持续 | SLO 99.5% |
B 阶段一次性切换 —— 切到 PG 后本地与 SaaS 走相同代码路径,无回退、无双轨。dogfood 即生效(messages 进 DB → 全文搜、jsonb 查询立刻可用)。
D 落在 G 前面 —— 原排期 D 在 G 后(以为 dogfood 用 UI 跑),实际转向"platform 端联调"后,API surface 反而成阻塞;G 的 Jinja2+HTMX 投入(G1-G6 ~3 天)沉淀 = 删除前的 dogfood 价值,留下的 sink 协议 / broker / no-subtask / files 路径安全归一 / task_dir 相对存储仍被 D 复用。
7.8 已知风险
| 风险 | 缓解 |
|---|---|
| 过早抽象违背 §5 | B 阶段单一 PG 无 adapter;C-E 各阶段独立 dogfood 价值 |
| CLI 双模式分叉、本地直跑被忽略 | transport 层抽象统一接口;CI 跑两路径同一组用例 |
/v1 冻死后演化慢 |
minor 半年兼容,major 6 个月 deprecation;/v1internal 实验 |
| Rename 误中前缀 / 漏改子 task | cascade SQL 用 old/% + 单测覆盖 |
| Running task 被 rename / delete | 后端校验 + UI 禁按钮 |
| 误删 folder | 二确认 + 输入 folder 名;真要再加 trash bin |
| DB-then-FS 中断留孤儿目录 | 后台 GC 周期扫"FS 有但 DB 无引用" |
| 同 folder 多 task 并发写同名 | 文件级悲观锁,冲突早失败 |
| Sandbox 出站越权 | egress allowlist 起步只放 LLM + PyPI |
| 资源滥用 | BYO key 默认;月度配额;cold task LRU 清 |
7.9 取舍说明
path-as-identity 而非 folder_id:folder 真实存在于 FS,folder_id 等于造两份 source of truth。rename 是 UI 主动动作,cascade 单事务搞定。
user auth 而非 tenant 层:个人 SaaS 用不上。企业版加 org_id claim 等价。
skill 产物全落 cwd 不引入 artifacts 表:中间件是用户花 token 生成的资产,可下载可替换;artifacts 表是为不确定 UX 收益预付架构成本。真嫌乱 UI 加折叠视图。
hard cascade 而非 soft orphan:orphaned 让 list / resume / UI 都多一种特殊 case,"删 folder = 删项目"比"留对话残骸"自然。usage_events append-only 不 FK,task 硬删后月账仍存活。
本地也用 PG,不用 SQLite / JSON:① dogfood ≡ 真实用户路径,bug 在 dogfood 就能复现;② Docker 已是必然依赖(§7.5),docker compose up postgres 零增量门槛;③ 双 adapter 维护税远高于 PG 一次性配置成本;④ 本地 dev 也能连远端测试服。
API-only,UI 由 platform 实现(2026-05-15 决策):
- 原计划:Phase G 用 Jinja2 + HTMX 在本仓库做"简洁 Web UI",dogfood 用,真上线再做正经前端。已落地 G1-G6:task list / chat 流式 / files 浏览 / new / done/abandon/export/toast,共 ~600 行 HTML+CSS+SSE-HTML-片段。
- 触发:用户决定与已有 platform 联调,前端用 platform 的框架,本仓库再维护 HTML / CSS / HTMX 就是双套 UI 浪费。
- 取舍:
- 删
web/templates/*web/static/*+ jinja2/markdown-it-py/pygments/mdit-py-plugins 依赖 - SSE 事件 payload 从 HTML 片段切 JSON(
{"type":"text","content":"..."}等);前端自渲染 markdown / tool_call 折叠 - 路由统一
/v1前缀,响应全 JSON,FastAPI 自带/docsSwagger UI 接替"对内调试"角色(本地形态GET /302→/docs) - 本地 sentinel user 形态保留;auth 走 D' 过渡形态(PLATFORM_KEY → JWT,见 §7.3),真 OIDC 留到联调约定 token 形态后接
- CORS
allow_origins=["*"]本地宽松,platform 部署时按 platform 域名收紧
- 删
- 沉淀:G 阶段的 sink 协议(§7 A)/ RunBroker fan-out / no-subtask 校验 / files 路径安全归一 / task_dir 相对存储 全部保留,不在 UI 层不被牵连
dev SPA 留一份(2026-05-15 决策):web/static/dev.html 单文件 vanilla JS,3 栏布局(task list + chat + files),~600 行无构建链。与"UI 由 platform 实现"不冲突:platform UI 是给真用户的、生产形态;dev.html 是给本仓库开发者自验 /v1 API + SSE 流的开发期工具。platform 未上线 / 网络断 / 凌晨随手验时不需要拉 platform。理由:① SSE 调试在 curl 里看不到 UI 反应,需要可视端;② Swagger 不发 SSE 流也没流式视图;③ 一个静态文件维护成本可忽略,删了再补不如留着。形态:登录页填 user_id(默 sentinel)+ platform_key → localStorage 存 JWT → fetch+Bearer。
CLI 不被 API 取代,而是双模式共存:本地直跑调 core 内部状态比 HTTP roundtrip 顺手;前端用户路径靠 --remote 打通。离线靠本地 docker compose PG 兜底,不靠"全栈零依赖"幻觉。
Memory 不入 DB:跨 task 共享靠"同一 user 同一 FS 目录"自动达成。md 用户直接编辑器改,DB 化反而要造 UI、违反 §3.7"事实由用户判断"。
Web UI 走 server-render + HTMX 不上 SPA:① 与 §5 "Less Scaffolding" 一致,不引入 React/Vue 构建链 / node_modules / 双语言双 lint;② chat 主交互是 SSE 流式追加 + 表单提交,HTMX hx-swap / sse-swap 原生覆盖,无需客户端状态管理;③ FastAPI 单进程既出 /v1 JSON 也出 HTML 模板,部署单容器;④ 上限低(协作 / 实时多光标 / 复杂表单态做不动),真要做重前端再换栈,届时 /v1 已稳定可直接对接 SPA。
Tasks/Messages 在 PG 但 skill 产物在 FS:tasks / messages 需要查询、过滤、全文搜、跨 task 统计 —— DB 强项;skill 产物(*.pptx / *.docx / sections/*.md)终用户拿走,期望文件管理器看到、Office 打开、邮件发出 —— 进 DB 要做"导出"多余操作。FS 是产物天然存储,DB 是元数据 / 状态 / 索引天然存储。同理 §7.5 bind mount = user root,容器里 ≡ 用户在 Web UI 看到的目录,无中间层翻译。
附录:DeepSeek V4 关键事实(2026-04-24)
- V4-Pro:1.6T / 49B 激活,1M context,SWE-Bench 80.6 / Terminal-Bench 67.9 / MCPAtlas 73.6
- V4-Flash:284B / 13B 激活,1M context
- 推理模式:non-thinking / thinking / thinking-max
- 价格:in ~$0.145/M,out ~$1.74/M(约 Claude Opus 1/6 ~ 1/7)
deepseek-chat/deepseek-reasoner2026-07-24 下线 → 必须迁deepseek-v4-flash/deepseek-v4-pro