diff --git a/DESIGN.md b/DESIGN.md index 2863aef..7250e0c 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -217,6 +217,184 @@ default_model: deepseek_v4.flash --- +## 7. Core / Platform 切分(草案,status=design,2026-05-09) + +> §1-§6 是 **personal-tool track**;本节是 **platform track**,目标把 core 包成多租户 SaaS。两者共享同一 core 代码,部署形态不同。本节落地前 §1-§6 路线照走,不阻塞 dogfood。 + +### 7.1 总原则 + +| | 形态 | 数据归属 | 接口 | +|---|---|---|---| +| Core(自己做) | 独立 **HTTP/SSE service** + sandbox 进程组 | 对话 / 文件 / 产物 / tokens / 用量 | `/v1/*` REST + SSE | +| Platform(团队做) | BFF + Web/Mobile UI + Auth + Billing | 终端用户 / 订阅 / 发票 | 调用 core `/v1/*` | + +CLI 也是 core 的一个客户端 —— 走同一 `/v1`,本地起 core 跑 localhost。dogfood 和平台走同一份代码路径,bug 先在自己身上发现。 + +参考蓝本:**OpenAI Assistants API** 形态(stateful agent service:`/threads /messages /files /runs`)。 + +### 7.2 资源模型与接口(/v1) + +``` +POST /v1/tasks 创建 task(mode/desc/model) +GET /v1/tasks 列表 +GET /v1/tasks/{id} 详情 +PATCH /v1/tasks/{id} 改 mode/desc/status +DELETE /v1/tasks/{id} 删 task(连带 messages/files/产物) + +POST /v1/tasks/{id}/files 上传(multipart,入 task_dir/source/) +GET /v1/tasks/{id}/files 列表 +GET /v1/tasks/{id}/files/{name} 下载(产物也走这里) +DELETE /v1/tasks/{id}/files/{name} + +POST /v1/tasks/{id}/messages 发消息,返回 {run_id} +GET /v1/tasks/{id}/messages 历史 +GET /v1/tasks/{id}/runs/{run_id}/events SSE 事件流 +POST /v1/tasks/{id}/runs/{run_id}/cancel + +GET /v1/skills 可用 skill 列表 +GET /v1/models 可用 model profile +POST /v1/probe (admin) 跑 capability probe + +GET /v1/usage tokens/cost/quota 状态(by tenant) +``` + +**SSE 事件格式**: +```json +{"type":"tool_call","run_id":"...","name":"read","args":{...},"ts":"..."} +{"type":"tool_result","run_id":"...","name":"read","preview":"...","truncated":false} +{"type":"text","run_id":"...","delta":"..."} ← LLM 流式 token +{"type":"usage","run_id":"...","prompt":1234,"completion":567,"cost_usd":0.012} +{"type":"done","run_id":"..."} +``` + +**版本化**:`/v1` 半年内 minor 向后兼容,major 6 个月 deprecation 窗口。 + +### 7.3 认证模型 + +Core 只信平台,**不直接对终端用户**。 + +``` +Authorization: Bearer ← 绑死平台租户 +X-Tenant-Id: ← 平台 sign 的 JWT claim +X-User-Id: ← 同上 +X-Request-Id: ← 跨服务 trace +``` + +平台对终端用户做 OIDC/Clerk auth,把租户/用户 ID 签进 JWT 给 core 验签 —— 平台**无法伪造租户 ID**。多租户隔离在 core 这一层强制,平台 bug 不会泄露跨租户数据。 + +### 7.4 存储:Postgres + 本地文件系统 + +**结构化数据走 Postgres**(service 形态多 worker 必须): + +``` +tenants(id, name, api_key_hash, plan, created_at, status) +tasks(id, tenant_id, user_id, mode, description, status, model_profile, + tokens_prompt, tokens_completion, cost_usd, created_at, updated_at) +messages(id, task_id, role, content, tool_calls, tool_call_id, + reasoning_content, created_at) +runs(id, task_id, status, started_at, finished_at, error, tokens_p, tokens_c, cost_usd) +files(id, task_id, name, path, size, content_type, uploaded_at, kind) + ← kind: source / intermediate / artifact +usage_events(id, tenant_id, task_id, run_id, kind, value, ts) + ← 计费/审计明细,append-only +quotas(tenant_id, max_concurrent_runs, max_tokens_month, max_storage_bytes, ...) +``` + +**文件走本地磁盘**: + +``` +/ + tenants/{tenant_id}/ + tasks/{task_id}/ + source/ ← 用户上传(PDF / 团队介绍等) + intermediate/← 中间产物(spec_lock.md / sections / slides) + artifact/ ← 最终产物(.docx / .pptx) +``` + +`files.path` 存相对 `` 的路径。`` 由部署配置决定(单机 = 一个目录,多 worker = 共享挂载点)。 + +为什么本地而不 S3:简化首版部署,运维门槛低;访问延迟低;tasks 文件总量 100GB 级别本地盘够用。规模真起来后,**files 表 + storage 抽象层只需换 backend,接口不动**。 + +### 7.5 沙盒:Per-task 容器 + Per-run exec + +| 选择 | 理由 | +|---|---| +| 每 task 一个长驻容器 | 起容器 ~300ms 太慢;一个 task 多轮 tool call 共享容器才划算 | +| 每 run 一次 `docker exec` | 进容器跑 run_python/shell;exec 级 timeout/资源限制 | +| 容器空闲 N 分钟回收 | 不浪费;下次 resume 重新拉起 | +| `task_dir` 直接 bind mount 进容器 | 宿主 `/.../tasks/{id}/` 挂到容器 `/workspace`,无同步开销 | + +**资源限制**:cgroup CPU/mem 上限、磁盘配额、网络 egress allowlist(只能出 LLM API 和 PyPI 镜像)、root 文件系统 read-only、no-new-privileges、drop ALL caps。 + +**选型**:Phase 起步用 **Docker**(运维门槛低);流量起来后视情况换 gVisor / Firecracker / e2b。Executor Protocol 抽象后切换成本低。 + +### 7.6 Core 代码改造(按依赖顺序) + +| # | 改造 | 影响文件 | 估时 | +|---|---|---|---| +| 1 | **事件流化 `loop.py`** —— `console.print` 改成 `yield Event`;CLI 接 console sink,API 接 SSE sink | `core/loop.py` `cli.py` | 半天 | +| 2 | **Storage 层** —— `Session/TaskState` 落 Postgres;`files` 走 `` | `core/session.py` `core/task.py` `main.py` 新增 `core/storage/` | 2 天 | +| 3 | **Executor 抽象** —— `run_python`/`shell` 走 `Executor.run(code, timeout, env)`;实现 = `docker exec` 到 per-task 容器 | `tools/run_python.py` `tools/shell.py` 新增 `core/executor/` | 2 天 | +| 4 | **Config + 多租户上下文** —— 每次请求带 `(tenant_id, user_id)`,所有 storage/executor 调用都 scoped | `main.py` `cli.py` 全链路 | 1 天 | +| 5 | **`api_key_env` 退役** —— 改 `KeyProvider`,运行时从 vault 取(平台 BYO 模式则逐请求注入) | `core/capabilities.py` `core/llm.py` | 半天 | +| 6 | **HTTP 外壳** —— FastAPI app,把上面五层包成 `/v1/*` + SSE | 新增 `core/api/` | 3-4 天 | + +代码量增量预估:**+800~1200 行**(API 层 + storage 层 + executor 层 + 配套测试)。 + +### 7.7 职责矩阵 + +| 事项 | Core | Platform | 备注 | +|---|---|---|---| +| 终端用户 auth | – | ✅ | OIDC/Clerk | +| 平台↔core 鉴权 | ✅ 验签 | ✅ 签发 | Bearer + JWT | +| 多租户数据隔离 | ✅ enforce | – | platform bug 不能跨租户 | +| Prompt injection 防护 | ✅ | – | 只有 core 看得见 LLM I/O | +| 敏感数据出站审计 | ✅ 产事件 | ✅ 消费转 SIEM | 双层 | +| 配额超限拒绝 | ✅ 计数 + 拒绝 | ✅ 展示给用户 | core 不能信平台传值 | +| 文件病毒扫描 | ✅ 入库再扫一次 | ✅ pre-upload 扫 | 双层 | +| GDPR 删除 | ✅ 提供 `DELETE` | ✅ 触发 | 数据在 core 这边 | +| LLM API key(BYO) | 运行时注入,**不持久化明文** | ✅ 加密存,逐请求传 | 降攻击面 | +| 计费 | ✅ 产 usage 事件 | ✅ 汇总 + Stripe | core 不碰钱 | +| 监控 / SLO | ✅ 自己负责 | – | 独立服务 | + +### 7.8 分阶段落地 + +| 阶段 | 目标 | 工作量 | 验收 | +|---|---|---|---| +| **A** 改造 §7.6 #1(事件流化) | CLI 不变,但 loop yield event;为后续铺路 | 半天 | CLI 走 event sink 跑通 dogfood | +| **B** 改造 #2-#5(storage / executor / DI / key)| 单进程仍可跑;接口齐备 | 1 周 | CLI 接 Postgres 跑通本地多 task | +| **C** 改造 #6(HTTP 外壳)| `/v1/*` 跑通,docker compose 起完整栈(core api + sandbox + PG) | 4 天 | curl / Postman 跑通主流程 | +| **D** 多租户 + Auth + Quota + Audit | 平台接得上 | 1 周 | 平台 demo 跑通 | +| **E** 上线打磨(限流/监控/告警/HA) | 可承接平台真实流量 | 持续 | SLO 99.5% | + +**A** 立刻可做,独立有价值。**B-D** 等平台团队 kickoff 时间锁定后开。**E** 上线后持续投入。 + +### 7.9 已知风险 + +| 风险 | 缓解 | +|---|---| +| 过早抽象违背 §5 哲学 | A 阶段单独有价值(支持 Web UI);B-E 等平台到位再做 | +| `/v1` 冻死后核心演化变慢 | minor 向后兼容窗口,major 6 个月 deprecation;内部加 `/v1internal` 用于实验 | +| Sandbox 网络出站限制不当 | Egress allowlist 起步只放 LLM endpoint + PyPI 镜像;skill 需要新出站靠申请 | +| Per-task 容器 ID 泄漏到对话 | tool 结果 sanitize,容器内 hostname/IP 不暴露 | +| 平台 bug 把 core 冲垮 | 平台维度限流(rate limit by Bearer key)+ 租户维度并发上限 | +| 文件计费/存储滥用 | 上传大小上限、月度配额、保留期(过期自动清) | +| LLM 成本失控 | BYO key 默认;平台代付要 per-tenant 月预算硬上限 | +| 本地盘容量瓶颈 | files 表 + storage 抽象层,换 backend 接口不动;LRU 清理冷 task | +| 多 worker 共享本地盘 | 起步单机部署即可;需要扩 worker 时上 NFS 或换对象存储 | + +### 7.10 取舍说明 + +**为什么 Docker 起步而不直接上 Firecracker/gVisor**:Docker 运维门槛最低,Executor Protocol 抽象后可平滑切换。提前上 microVM 是过度工程。 + +**为什么不复用 K8s Job per run**:Job 启停成本高(秒级),agent loop 一轮 tool call 才几百毫秒。Per-task 长驻容器 + per-run exec 是性能/隔离的最佳折中。 + +**为什么本地文件而不 S3**:首版部署/运维门槛低,访问延迟低,100GB 级别单机够用。storage 抽象层留好,真要切对象存储改 backend 不改接口。 + +**为什么 Postgres 而不 SQLite**:service 形态下 API + sandbox + 后续 worker 都要读写状态,SQLite 单写锁会成为瓶颈。Postgres 起步成本可接受。 + +--- + ## 附录:DeepSeek V4 关键事实(2026-04-24) - **V4-Pro**:1.6T 总 / 49B 激活,1M context,SWE-Bench 80.6 / Terminal-Bench 67.9 / MCPAtlas 73.6