Files
codex-app-server/docs/cloud-architecture.md
T

14 KiB
Raw Blame History

云端架构设计(第三阶段)

日期:2026-10-26。范围是用户 2026-10-26 01:06 列出的 8 项: 远程沙箱、云端派发、GitHub 提 PR 和 CI、Slack 触发、推送通知、 多用户协作和权限、跨线程记忆和项目知识库、模型 fallback 和 真实 token 成本统计。

先说一句总的:这 8 项不是 host.py 再加几个命令能长出来的, 它们共用一个前提——一个常驻的控制面。没有控制面,派发、 多用户、推送、GitHub 回调都无处落脚。所以下面先定控制面, 再按依赖关系分 4 个子阶段,每项都写清楚:复用现有 host.py 的什么、还缺什么外部条件、风险和诚实边界。

总架构

CLI / Web / Slack / GitHub webhook
        │  (HTTPS, 带用户身份)
        ▼
API 网关 ── 认证 (OAuth / API key) ── 权限 (RBAC, 见 §6)
        │
        ▼
控制面:任务队列 + 状态库 (Postgres / SQLite 起步)
  ├─ 派发器:选空闲 worker,按优先级、依赖、预算派发
  ├─ 记忆/KB 服务:线程摘要 + 项目索引 (§7)
  ├─ 计费服务:模型 fallback + 成本统计 (§8)
  └─ 通知服务:任务完成/失败/评审请求 → Slack/推送 (§5)
        │
        ▼
Worker 池(每个 worker 跑一个 host.py + 一个 codex app-server)
        │
        ▼
沙箱(每任务独立,见 §1)→ git worktree / 容器 → 产出分支
        │
        ▼
合并门:自动验测 → GitHub PR + CI (§3) → 合并 / 回滚

复用原则:控制面不重写 agent 逻辑。worker 就是现在的 host.py:dispatch / run / goal / review / merge / review-comment / fix-review / doctor / failures 都已实测可用,控制面只负责 “谁可以派、派给谁、状态在哪、结果通知谁”。重写 host 等于 把已验证的 19 个测试扔掉。

常驻前提(诚实边界):这个 VM 会被平台定时回收, WeChat 桥和 host daemon 都是前车之鉴。控制面跑在这台 VM 上只能算“演示”,生产要一台常驻机器(用户自己的 Mac/ 服务器)或一个托管服务(Fly.io / Render / 云服务器)。 Muse cron 只能做定时检查,不能替代控制面。

1. 远程沙箱

目标:每个任务在隔离环境里跑,跑坏了不污染宿主、不偷 别的任务或用户的密钥。

  • 现状:codex 自带沙箱(workspace-write / read-only, bubblewrap),host.py 已实测 read-only 确实挡写、审批 deny 确实挡工具。这是本地沙箱,不是远程沙箱。
  • 远程沙箱的三种实现,按成本递增:
    1. 同机容器:每任务一个容器(Docker),worktree 挂载进 容器,网络默认断、只放行模型 API。本 VM 没有 Docker, 这条路要先装 Docker 或换机器。
    2. 云容器:每任务起一个 Fly.io / 云服务器容器,跑完即毁。 隔离最好,成本按任务时长算,需要云账号和预算上限。
    3. 专用 worker 机器:每个 worker 一台轻量 VM,任务结束 快照/重建。隔离和成本介于两者之间。
  • 安全硬规则(不管选哪种):沙箱里不放宿主密钥,模型 key 只走 worker 的环境变量且不进日志;沙箱只能写自己的 worktree;网络 egress 白名单(模型 API + git 远端); 任务超时强制 kill;产出只以 git 分支形式出来,不直接 写主仓。

2. 云端派发

目标:任务不用在用户电脑上跑,提交后控制面排队、派发、 重试、取消,状态随时可查。

  • 队列:任务 = {id, 项目, 提示, 模型优先级, 依赖, 优先级, 预算, 幂等键}。幂等键防重复提交(Slack 重发、GitHub webhook 重发都只建一个任务)。
  • 状态机:queued → running → completed / failed / interrupted / merged / merge-failed,和 host.py 现有状态对齐; worker 心跳丢失超过阈值 → 标 interrupted,可续跑(已有的 recover 逻辑直接复用,thread 在 CODEX_HOME 里不丢)。
  • 派发策略:先依赖拓扑(已有的 topo_sort),再优先级, 再预算(max_daily_turns + 成本上限,见 §8),worker 空闲才派,不超发。
  • 取消:用户取消 → worker 收到 turn/interrupt → 状态 interrupted,分支和 worktree 保留,不静默丢。

3. GitHub 提 PR 和 CI

目标:任务做完不是只合到本地主分支,而是走标准 PR 流程, CI 过了才合。

  • 现状有利:本机有 git 和 gh,host.py 的 merge 已有 自动验测 + 回滚(本地 unittest),这套逻辑可以直接当 PR 的“本地预检”。
  • 流程:任务分支 → push 到远端 → gh pr create(标题、 任务描述、评审评论自动带上)→ CI 跑 .github/workflows/host-verify.yml(host.py 生成,跑 unittest)→ gh pr checks 轮询 → 绿了才 gh pr merge, 红了把 CI 日志贴回任务并标 merge-failed,不强行合。
  • 还需要用户给的:GitHub 仓库远端、一个 PAT 或 OAuth 授权(走 Secure Vault,绝不写进文件或聊天记录); PR 评论触发 fix-review 靠 GitHub webhook 回调到控制面 (需要控制面有公网地址,本地 VM 演示时可用 webhook 转发工具顶一下,生产不行)。
  • 边界:CI 是 GitHub Actions 的,不是 host.py 自己跑的; host.py 只负责生成 workflow、读 checks 结果、按结果 合或不合。

4. Slack 触发

目标:在 Slack 里就能派任务、查状态、收完成通知,不用 开终端。

  • 触发方式(从易到难):Slash 命令 /codex <任务> 派发; 频道里 @bot 查状态;交互按钮“批准合并 / 丢弃”直接调 控制面的 merge / discard(按钮点击要带用户身份和权限 校验,不能谁点都算数)。
  • 安全:Slack 签名密钥验签(防伪造请求),Slack 用户 ID 映射到 host 用户(见 §6),频道权限 = 项目权限, 私有项目不在公开频道里回贴 diff。
  • 还需要用户给的:一个 Slack workspace 管理员装 Slack App, 签名密钥和 Bot Token 走 Secure Vault;控制面要有公网 回调地址(同 GitHub webhook 的前提)。

5. 推送通知

目标:任务完成、失败、需要评审时,用户手机上能收到, 不用一直盯着终端。

  • 事件(只发状态变化,不每轮刷屏,和本地看门狗的通知 纪律一致):completed、failed、goal-unmet、merge-failed、 review-requested(有新评审评论)。
  • 通道(按接入成本排):Slack DM/频道(和 §4 同一套)、 Webhook(通用)、邮件、Web Push / APNs / FCM(真手机 推送,需要应用证书和用户订阅,成本最高)。
  • 现实一点的起点:对本用户,Muse 聊天通知 + Slack 已能 覆盖“手机收到”;真 APNs/FCM 是产品级才需要的最后 一步,不要一开始就做。
  • 用户偏好:每用户可设“即时 / 摘要 / 静默”,失败和 merge-failed 默认即时,completed 默认可摘要。

6. 多用户协作和权限

目标:多人用一个控制面,互相看不到不该看的项目,审批 和合并有据可查。

  • 认证:OAuth(GitHub / Google 登录,推荐 GitHub,因为 和 §3 同一套身份)或每用户 API key;会话用 JWT, 短期有效 + 刷新。
  • 权限(RBAC,每项目独立):
    • owner:管成员、管计费、删项目;
    • maintainer:派任务、批准合并、改配置;
    • contributor:派任务、写评审评论;
    • viewer:只读任务、diff、日志。
  • 隔离:状态库按项目过滤(行级过滤,不是靠前端藏), 沙箱按用户+任务隔离,模型 key 按用户隔离(不共用一 个中转站 key),日志里不出现别的用户的任务内容。
  • 审计:谁派了、谁批准了、谁合并了、谁改了配置,全部 记审计日志,不可删;这是多用户和单用户最大的区别, 单用户可以靠 host-run.log,多用户不行。

7. 跨线程记忆和项目知识库

目标:新任务不用从零开始,能用上以前线程的结论和项目 的既有知识,但不把无关历史全塞进上下文。

  • 两层,分开做:
    1. 线程记忆:每回合结束,host 写一条摘要(目标、 结论、改了哪些文件、遗留问题),存在记忆库;新任务 派发时,按项目 + 相关文件召回最近的摘要,注入提示 前缀,不注入全文。摘要可删、可纠正(用户纠正一条, 旧摘要要标废弃,不能留着误导下一轮)。
    2. 项目知识库:索引仓库文件、README、docs、历史 任务、评审评论。起步用 SQLite FTS(全文检索,零外 部依赖),够用;以后再上 embedding(需要向量库和 embedding 模型,成本和运维都上一个台阶)。
  • 检索时机:派发时 top-k 相关文件/文档注入提示;回合中 给 agent 一个 MCP search 工具,让它自己查 KB(比一次 性全注入省 token)。
  • 隐私边界:KB 按项目隔离,不跨项目召回;记忆是项目 的,不是某个用户的私人笔记,除非用户明确标私有。

8. 模型 fallback 和真实 token 成本统计

目标:主模型挂了自动换备选,成本是真数不是估算,且 每项成本能对到具体任务。

  • 先纠正一个旧说法:之前说“协议本身不暴露 token 数” 是不准确的。查 2026-10-26 的 v2 schema:协议里有 account/usage/read 方法,返回 ThreadUsage (estimatedUsageCreditsMicros / estimatedUsageUsdMicros) 和 TokenUsageBreakdown(input / output / reasoning / cached / total)。但有两个前提:(a) 它是 ChatGPT 账号 的计费路线,(b) 中转站(xcmapi)走的是 API key 路线, 计费路线“可用时”才返回——中转站上到底能不能读到, 必须实测,不能先写进承诺。
  • 落地顺序:
    1. host 的 Server 事件循环先接住回合里的 usage 通知 (如果有),按回合记 input/output/total 到任务记录;
    2. 每回合结束调一次 account/usage/read(threadId), 记 estimatedUsageUsdMicros;这个调用本身不跑模型, 不花模型钱;
    3. 如果中转站上读不到(返回空/不可用),再走 provider 计费接口:OpenAI 的 /usage、xcmapi 的账单接口(如果 有,拿用户的中转站 key 临时测一次,不存文件);
    4. 都读不到时才用估算:prompt + 输出字符数 / 4,并 明确标“估算”,绝不冒充真数。
  • Fallback:配置里写模型优先级列表,例如 [gpt-6.1-sol, gpt-6-sol, gpt-5.6-sol];主模型返回 404 / 429 / 5xx / 超时时,自动试下一个,任务记录里 记“实际用的模型”,fallback 发生时通知一次,不静默 降级;预算(回合数 + 成本上限)按实际模型累计,超 预算就停,不自动换更贵的模型续跑。
  • 成本报告:按任务 / 按项目 / 按天汇总 token、美元、 回合数、fallback 次数,报告里真数和估算分开列。

分阶段落地(按依赖,不按热闹程度)

  • 3A 控制面 + 多用户 + 记忆/KB:API + 认证 + RBAC + 状态库 + 任务队列 + 线程摘要 + SQLite FTS 知识库。 不需要云账号、GitHub、Slack,可在本机先跑通。这是 后面所有项的地基,先做它。
  • 3B 远程沙箱 + 云端派发:worker 池 + 容器沙箱 + 心跳/中断/取消。需要 Docker 或云容器账号,是 3A 之后 的第一块硬骨头。
  • 3C GitHub PR/CI + Slack + 推送:外部集成三件套, 需要 GitHub PAT、Slack App、公网回调地址,三者可以 并行,但都依赖 3A 的认证和权限。
  • 3D 模型 fallback + 真实成本:usage 读取实测 + fallback 列表 + 成本报告。依赖 3A 的任务记录,不依赖 3B/3C,可以和 3C 并行。

补齐进度(2026-10-26 第二轮,上一轮留下的未完成项)

已做完(不要再列为未完成):

  • 记忆 / 知识库 / 成本 / 模型已接成 server.py 正式端点, 全部走 X-API-Key + 项目 RBAC(除 /health、/webhooks/、 /oauth/ pre-auth 外),单测覆盖 401/403/404 与跨项目隔离。
  • SQLite 并发:连接统一走 cloud/db.py(WAL + busy_timeout 5000ms + 写锁有限重试 3 次指数退避),多线程并发写单测已覆盖; 但单机上限仍在,高并发要换 Postgres,见下“仍留”。
  • OAuth 接线脚手架:cloud/oauth.py(state 10 分钟过期一次性、 授权 URL 生成、回调验 state + 换 token,token 换取可注入 fake 客户端,secret 只走环境变量),端点 GET /oauth/{provider}/login 与 POST /oauth/{provider}/callback 已接;真实应用注册与绑定仍缺。
  • 推送订阅与分发:cloud/push.py + 端点(订阅增删查、分发按 偏好过滤、只发状态变化事件),webhook/slack 走可注入 sender, webpush/apns/fcm 只做接口 + 缺凭证明确报错;真实凭证仍缺。

仍留(需要外部条件,不是代码能假装解决的):

  • 常驻主机 / 托管服务(VM 回收问题没变)。
  • GitHub 远端 + PAT(真实 PR 仍缺这两样)。
  • Slack App(真实签名密钥、斜杠命令、事件订阅)。
  • 云容器账号(CloudExecutor 仍是占位,Docker 未接通挂载 / 网络隔离 / 资源限额)。
  • 模型 key 供给方式(定时/worker 进程手里没有中转站 key, 上次 real-nightly-tests 就卡在这里)。
  • OAuth 真实应用注册、回调公网地址、已有账号绑定流程。
  • 推送真实证书(VAPID / APNs / FCM)与真实发送端。
  • worker 执行、云端执行、真实通知与真实模型回合的接通; memory/KB/成本/模型路由的 HTTP 端点已接,但 worker 回写 completed/merged 与成本记录仍需接线。

需要用户定的事(现在就可以定,不用等代码)

  1. 控制面跑在哪:常驻机器(自己的 Mac/服务器)还是 托管服务(Fly.io / Render / 云服务器)?这决定 3B 的沙箱选型和 3C 的回调地址。
  2. GitHub:用哪个仓库、给不给 PAT(走 Secure Vault)?
  3. Slack:有没有 workspace 管理员权限装 App?
  4. 推送:Slack + Muse 通知够不够,还是要真手机推送 (APNs/FCM)?
  5. 计费实测:同不同意用中转站 key 临时测一次 account/usage/read(不存 key,只看返回有没有数)?