design: 加 §7 Core/Platform 切分草案 (SaaS 化方向)

讨论后定的方案:把 core 包成独立 HTTP/SSE service,平台只做 BFF+UI+Auth+Billing。
数据归属、/v1 接口、Postgres+本地文件存储、Docker 沙盒、6 步代码改造、5 阶段落地路线。
status=design,personal-tool track 不受影响。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-05-11 14:59:23 +08:00
parent 263cdb974a
commit f6c3492514
1 changed files with 178 additions and 0 deletions

178
DESIGN.md
View File

@ -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 <platform_api_key> ← 绑死平台租户
X-Tenant-Id: <tenant_uuid> ← 平台 sign 的 JWT claim
X-User-Id: <user_uuid> ← 同上
X-Request-Id: <uuid> ← 跨服务 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, ...)
```
**文件走本地磁盘**:
```
<storage_root>/
tenants/{tenant_id}/
tasks/{task_id}/
source/ ← 用户上传(PDF / 团队介绍等)
intermediate/← 中间产物(spec_lock.md / sections / slides)
artifact/ ← 最终产物(.docx / .pptx)
```
`files.path` 存相对 `<storage_root>` 的路径。`<storage_root>` 由部署配置决定(单机 = 一个目录,多 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 进容器 | 宿主 `<storage_root>/.../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` 走 `<storage_root>` | `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