From da0c7d453e2ab253063aae2650e5f3cc219335a1 Mon Sep 17 00:00:00 2001 From: host Date: Thu, 29 Oct 2026 10:37:30 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20README=20=E5=AF=B9=E9=BD=90=E7=94=9F?= =?UTF-8?q?=E4=BA=A7=E7=8E=B0=E7=8A=B6=EF=BC=88main=204c3420a=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 顶部新增项目现状:生产 Postgres + api.diante.me + React 命令台 GitHub 风格 - 新增云端控制面一节:状态机、worker/扩容、配额成本、记忆搜索、继续/计划/followup/fork、部署链路 - 纠正过期表述:云端不再是演示、PR/CI 本仓已走通(旧无远端/无登录已过期)、多机同步走中心库 - 新增诚实边界:内层 bwrap、40 人暂搁置、单机 HA 模拟、PR #127 未合并 --- README.md | 221 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 181 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 200b2f6..db2a8c3 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,62 @@ -# Codex app-server(无 GUI 版,照搬开源核心) +# Codex app-server(无 GUI 版,照搬开源核心 + 自建云端控制面) -目标:不做桌面 UI、不重写 agent 循环,把 Codex 开源能给到的全部 -原样接过来,做成一个可以被任何客户端(pi agent、你自己的 App、 -脚本)调用的 app-server。 +目标分两层,不要混成一层: + +1. **不重写 agent 循环**:把 Codex 开源能给到的全部原样接过来, + 做成一个可以被任何客户端(pi agent、你自己的 App、脚本) + 调用的 app-server。 +2. **自建控制面**:在官方 agent 之上加一层常驻服务——项目、 + 任务队列、worker 池、评审与合并、权限与配额、成本与记忆、 + 网页命令台——让多任务、多用户、远程派发能真正跑起来, + 而不是只停在本地单机演示。 + +## 项目现状(2026-10-29,main `4c3420a`,PR #129 已合并部署) + +旧版 README 只写到本地 host 层,云端还写着“演示、不是真实 +云服务”——那句话已经过期。本轮更新以生产现状为准: + +- **生产已上线**:控制面跑在常驻主机上,Postgres 为后端, + 公网入口 `https://api.diante.me`,命令台 + `https://api.diante.me/console`(`GET /health` 可查 + `backend=postgres`、`worker_ok` 与当前 commit)。 +- **命令台是 React 版**:源码在 `cloud/console-react/`(Vite + + React 19 + TypeScript),构建产物是单文件 `cloud/console.html`, + 只改 `src` 再 `npm run build:console`,产物绝不手改。 + 当前视觉是一页式 GitHub 风格(PR #129):细顶栏 + 6 张数据 + 卡片 + 派发、任务、对话、项目、环境、Workers、技能、自动化、 + 分诊、管理区、API Key 全部铺在一页;此前 Muse 风格四栏、 + React 三栏都已被它取代,不要再按旧截图改。 +- **执行层是 Docker + gVisor**:生产任务走 Docker 执行器, + 生产 autoscaler 起的 worker 带 `--docker-runtime runsc`;内层 codex 沙箱在生产容器里仍是 + `danger-full-access`(见下“诚实边界”),隔离靠外层容器 + + 租户目录 + egress 白名单,不是双层沙箱。 +- **模型走中转网关**:生产默认模型已改为 `gpt-5.6-terra` + (原 `gpt-6.1-sol` 在 2026-10-27 实测持续 503; + 价目表与 `/models` 优先级列表未动,terra 成本进 unpriced)。 + 系统 key 与中转站是设计本身,不做 OpenAI 官方账号对接, + 也不做 per-user 中转 key。 +- **已落地的控制面能力**:OAuth 真实登录(GitHub/Google)、 + 项目 RBAC 与 API key 管理、任务队列与 worker 心跳、自动扩容 + (协调员保护)、租户配额与 USD 上限、成本统计、自动记忆、 + 网页搜索 MCP(SearXNG 路)、任务继续消息持久化、子任务 + 默认模型/强度、计划模式、followup 与 fork、评审队列与 + PR/CI 门。细节与 API 见 `cloud/README.md` 与 `docs/`。 +- **仍未证明 / 暂搁置的**:40 人并行(2026-10-29 用户令先绕过, + 串行分波次或 18 人规模代替,40 人仍记未证明);第二台主机的 + 真跨机 HA(现有 HA 验证是同主机双进程模拟);G10 断点 + provider 修法只在 PR #127,未合并、未部署。 ## 一句话架构 ``` -你的客户端 / pi agent - │ stdio JSON-RPC(或 unix / ws,见 start.sh) +浏览器 / 客户端 / GitHub webhook + │ HTTPS + X-API-Key(OAuth 登录、webhook 靠签名) + ▼ +cloud/server.py(控制面,默认 127.0.0.1:8765,生产经 nginx + TLS) + │ 项目 / 任务 / 队列 / 权限 / 配额 / 成本 / 记忆 / 评审 + ▼ +cloud/worker.py + autoscaler(worker 池,local / docker 执行器) + │ 调 host.py:dispatch / run / goal / review / merge ▼ client.py ──► codex app-server(官方二进制,codex-core 原样) │ thread/start · turn/start · 事件流 · 审批请求 @@ -16,12 +64,12 @@ client.py ──► codex app-server(官方二进制,codex-core 原样) agent 循环 + 沙箱 + 工具 + MCP + skills + 线程持久化 ``` -本仓库自己写的只有宿主层:`client.py`(协议客户端)、 -`run_parallel.py`(多 thread 编排)、`start.sh`、`config.example.toml`。 +本仓库自己写的有三层:`client.py`(协议客户端)、`host.py` +(本地宿主编排)、`cloud/`(云端控制面 + worker + 命令台)。 agent 循环、沙箱、工具执行、上下文压缩、线程存储,全部是官方 codex 二进制里的,不是我复刻的简化版。 -实测版本:本机 `codex-cli 0.164.0-alpha.9`,协议定义就是从它 +实测版本:`codex-cli 0.164.0-alpha.9`,协议定义就是从它 导出的,见 `protocol/`。 ## 目录 @@ -29,6 +77,21 @@ codex 二进制里的,不是我复刻的简化版。 - `start.sh`:启动官方 app-server,支持 stdio / unix / ws 三种传输。 - `client.py`:零依赖 Python 客户端,演示完整链路。 - `run_parallel.py`:宿主层多 agent 编排(见下文“子代理”)。 +- `host.py`:本地宿主编排(dispatch / review / merge / goal / + automations / daemon / health,见下文“宿主编排”)。 +- `cloud/`:云端控制面(`server.py` 控制面、`worker.py` worker、 + `autoscaler.py` 自动扩容、`console.html` 命令台产物、 + `console-react/` 命令台源码、`sub2api.py` 中转网关、 + `websearch_mcp.py` 网页搜索 MCP、`db.py` SQLite/Postgres + 双后端)。控制面本身零第三方依赖,只用 Python 标准库。 +- `cloud/console-react/`:命令台源码。改页面只改 `src`, + 再 `npm run build:console` 生成 `cloud/console.html`。 +- `deploy/`:主机部署与运维脚本(`deploy.py` + `host_deploy.sh`, + tar 与 git 直拉两种模式;`deploy/README.md` 为准)。 +- `docs/`:架构与专题文档(云端架构、自动记忆、网页搜索 MCP、 + 线程生命周期等)。`cloud/README.md` 里仍写着“本地演示、 + 不是真实云服务”的旧诚实边界已过期,以本 README 顶部的 + 生产现状为准。 - `config.example.toml`:沙箱、审批、MCP、features 的配置示例。 - `protocol/`:`codex app-server generate-json-schema` 原样导出的 协议 schema(v1 + v2),客户端的方法名和字段以它为准。 @@ -121,6 +184,58 @@ python3 run_parallel.py --jobs examples/jobs.json 事后逐个 review diff 再合并,冲突仍需人工处理—— core 里没有自动解决冲突的调度器。 +## 云端控制面(cloud/,生产已跑,不再是演示) + +本地 `host.py` 只管一台机器上的任务;多用户、远程派发、 +排队、配额、评审门要靠 `cloud/`。它不是 host.py 的包装, +而是控制面只负责“谁可以派、派给谁、状态在哪、结果通知谁”, +执行仍走 worker 调 `host.py`,agent 逻辑不重写。 + +```bash +python3 -m cloud.server # 默认 127.0.0.1:8765,生产经 nginx + TLS 对外 +# 除 GET /health、/webhooks/*、/oauth/* 外,所有端点都要 X-API-Key 请求头 +# 任务实时链路:GET /tasks/{id}/trace(快照)与 GET /tasks/{id}/trace/stream(SSE) +# 命令台:GET /console(每次现读 cloud/console.html,无需重启) +``` + +- **任务状态机**:`queued → running → completed / failed / + interrupted / merged / merge-failed`;`merged` 只由 + `POST /tasks/{id}/merge` 同步状态,不执行合并本身(合并仍由 + host.py merge 执行,成功后再调本接口;仅 completed 可改 + merged,重复调用 409,越权 403)。 +- **worker 与扩容**:worker 心跳注册(`POST /workers/heartbeat`), + autoscaler 按队列扩缩;2026-10-29 已修协调员槽位泄漏 + (PR #118:协调员完成不清 `_running` 曾导致容量门静默拦单, + 40 人轮与 18 人轮的“认领停滞”都是它)与缩容误杀协调员 + (PR #117)。长 goal 任务异常退出不再一律判 failed, + 无结论的非 0 退出记 interrupted,断点在盘上可 `goal-resume` 续跑。 +- **权限、配额、成本**:项目 RBAC、API key 只存 sha256 哈希 + (明文只返回一次)、租户月度 USD 上限与建任务/分支/重试 + 准入、按任务与按项目/按日的成本(实际与估算分开)、 + 每 key 每分钟限流(环境变量可调,窗口与上限见部署 env, + 不要把生产调的值当成代码默认值写进文档)。 +- **记忆与搜索**:跨线程记忆与项目 KB(按项目隔离)、自动记忆 + (生产默认账号已开,worker 每轮读库)、网页搜索 MCP + (Perplexity → Exa → Tavily → Brave → SearXNG → DuckDuckGo + 自动回退;Docker 任务容器不继承 worker 环境,搜索 env 须经 + worker 的 extra_env 透传,见 PR #114)。 +- **继续、计划、followup、fork**:继续消息已持久化进会话历史 + (PR #126,此前失败任务只落任务记录、不进历史,网页重载后 + 看不到助手回复);建任务可带 `plan`(只读沙箱,不能与 + `approval=full` 或 goal 同用);`POST /tasks/{id}/followups` + (queued 排队不打断当前轮,direct 立即进链路且跑完当前轮后 + 优先于排队执行);`POST /tasks/{id}/fork`(仅终态任务可分支, + 继承 executor/模型/强度/权限/计划,goal 字段置空)。 +- **模型与网关**:子任务默认模型/强度(`child_model` / + `child_effort`,PR #121);模型路由走 sub2api 中转网关, + 断点命令的 model provider 须透传(PR #127 未合并,见顶部)。 +- **部署**:`deploy/deploy.py`(tar 默认,`--source git` 走主机 + 直拉 GitHub;产物唯一命名 + 三道 sha/commit 门 + 换目录 + 代替原地解包,health 须 `ok=true` 且 `backend=postgres` 且 + commit 对上才算成功;主干推送不自动部署生产,见 + `deploy/README.md`)。部署前先确认无人同时在推,并发部署 + 会撞目录换位;部署完须核 4 个 unit 全 active。 + ## 宿主编排(host.py,无 GUI,对齐 ChatGPT / Codex 桌面版宿主) ChatGPT / Codex 桌面版作为宿主,本质不是再写一个 agent,而是 @@ -170,14 +285,17 @@ turn 可以真正并发(不是轮流排队)。 | 多机器 state 冲突 | 锁只防同一台机器,多机器共享 state 文件会静默覆盖 | 部分修:state 加 version/last_writer/updated_at,保存时磁盘版本比加载时新就记 STATE-CONFLICT 日志并合并保留双方任务,不静默丢;真多机同步仍需中心服务(见下“没对齐”) | | 合并时提交缓存文件 | merge 的 git add -A 会把 __pycache__/*.pyc 也提交进主仓,下次合并被 .pyc 改动挡住 | 已修:merge 前清 worktree 的 __pycache__/*.pyc,主仓的 .pyc 改动先还原;单测覆盖 pyc 不入库 | | 守护进程 + 健康检查 + 自动恢复 | host.py 跑完就退出,不常驻;state 坏了静默当空 state;中断任务一直停在 running | 已修:daemon(双 fork 到 PID 1,循环 recover + run-due)、health(不跑模型,查二进制/配置/state/server/daemon)、restore-state(坏了从 .bak 恢复并留底)、recover(超过阈值的 running 标 interrupted,可用 run 续跑,不自动重跑模型回合) | -| GitHub PR / CI 一条线 | 本地 merge 完就完,没有 PR、没有 CI 门 | 已修(代码 + 本地可测部分):github-workflow 生成 .github/workflows/host-verify.yml(checkout + python + unittest,幂等);pr(push 分支 + gh pr create,成功后 pr_url/pr_number 写回任务记录);pr-checks(解析 gh pr checks,all_pass 仅全 pass 为真,空 checks 不算过);pr-merge(先查 checks,未全过拒绝且不调 merge,全过才 gh pr merge --merge 并标 merged,不动本地主分支)。gh 调用走 run_gh(),单测用假 gh。**真实 PR 本轮没跑**:本仓没有 git remote,gh 未登录,见下“GitHub PR/CI 的外部条件”。 | +| GitHub PR / CI 一条线 | 本地 merge 完就完,没有 PR、没有 CI 门 | 已修(代码 + 本地可测部分):github-workflow 生成 .github/workflows/host-verify.yml(checkout + python + unittest,幂等);pr(push 分支 + gh pr create,成功后 pr_url/pr_number 写回任务记录);pr-checks(解析 gh pr checks,all_pass 仅全 pass 为真,空 checks 不算过);pr-merge(先查 checks,未全过拒绝且不调 merge,全过才 gh pr merge --merge 并标 merged,不动本地主分支)。gh 调用走 run_gh(),单测用假 gh。本仓的真实 PR 链路现已走通,见下“GitHub PR/CI”一节。 | | 边界:cwd、沙箱、审批、模型 | dispatch / turn 参数里定,见 `config.example.toml` | 已实测 | | 任务登记 | `host-state.json`:task -> thread/worktree/状态/diff;agent 状态本身仍在 CODEX_HOME,不另存第二份 | 已实测 | **没对齐的部分(诚实说,不是偷工):** -- 云端派发、推送通知、Slack / GitHub 触发:那是 ChatGPT 托管服务, - 开源里没有。`run-due` 要靠你自己的 cron 定时调,才等于“定时自动化”。 +- 云端派发、推送、GitHub 触发:本地 host 层仍没有,`run-due` + 要靠你自己的 cron 定时调,才等于“定时自动化”;但云端控制面 + 层已经有了(见上文“云端控制面”):队列派发、webhook 入站、 + PR/CI 门、推送订阅与分发。Slack 触发未对真实 Slack 服务器 + 实测,不要把它写成已验证。 - GUI 的 inline 评论、side-by-side diff、点按钮批准:无 GUI 版 本来就不要这层,评审就是 git diff 文本 + merge/discard 命令。 - Computer Use:另一条管线,不在宿主编排里。 @@ -189,10 +307,10 @@ turn 可以真正并发(不是轮流排队)。 (gap2 实测拦下了一次 -X ours 丢 mul2 的情况);依赖任务 现在从依赖分支建 worktree,依赖先合、依赖任务后合时不再 重复加同一段。 -- 多机器同步:state 现在有 version 和冲突日志,不会静默丢 - 任务,但两台机器共享一个 state 文件仍不是 ChatGPT 云端 - 那种中心同步——要真多机,得另起一个中心 state 服务, - 这不在本地 host 的范围里。 +- 多机器同步:本地 host-state 仍是单机文件(有 version 和 + 冲突日志,不会静默丢任务),真多机同步要走云端控制面 + 的中心数据库(Postgres),不在本地 host 的范围里; + 同主机双进程模拟过的 HA 不等于第二台主机仍存活。 - 守护进程的边界:daemon 本身也活不过 VM 平台回收(和 微信桥 serve 一样,回收后要重拉),所以 VM 上更稳的定时 仍是 Muse cron 定时调 run-due/health,而不是只靠 daemon @@ -232,37 +350,36 @@ python3 host.py daemon-stop `python3 host.py automations --add --name <名> --task "<任务>" --repo <仓> --interval 86400` 登记自动化,再确认模型 key 已走环境变量提供,才让定时去调 `python3 host.py run-due`;未到期的自动化 run-due 时不重复派发。 -- 云端控制面演示另起一端(不跑模型回合): +- 云端控制面另起一端(生产上 worker 会跑真实模型回合, + 不再是“演示、不跑模型”): `python3 -m cloud.server`(默认 127.0.0.1:8765), 除 `GET /health`、`/webhooks/*`、`/oauth/*` 外都要 `X-API-Key` 请求头。 任务实时链路:`GET /tasks/{id}/trace`(已结束任务的快照)与 `GET /tasks/{id}/trace/stream`(运行中任务的 SSE:先 snapshot 再逐条 entry,终态发 done 关流;命令台详情的链路区用 fetch 流式读它, EventSource 不能带 X-API-Key 头故不用)。 - 新业务链路:建任务可带 `plan`(计划模式:只读沙箱,执行器传 - `--sandbox read-only`;不能与 `approval=full` 或 goal 同用); - `POST /tasks/{id}/followups`(body/mode,mode 为 queued 或 - direct;仅 queued/running 的非 goal 任务可接)与 - `GET /tasks/{id}/followups`(按创建时间排序)——排队不打断当前 - 轮、跑完按序再跑一轮,直接发送立即进链路(phase=followup-direct) - 且跑完当前轮后优先于排队执行;`POST /tasks/{id}/fork` - (message 必填;仅终态任务可分支,继承 executor/模型/强度/ - 权限/计划,goal 字段置空,首轮失败不自动重试、不重复分叉)。 + 新业务链路见上文“云端控制面”:`plan`、`followups` + (queued/direct)、`fork`,以及继续消息持久化。 -## GitHub PR/CI 的外部条件(真实 PR 仍缺这两样) +## GitHub PR/CI:本仓已走通,旧“无远端、无登录”已过期 -PR/CI 这条线本轮只做了代码和本地可测部分,真实 PR 没有跑, -原因如实写在这里,不是没做完就含糊过去: +旧版这里写着“本仓没有 git remote、gh 未登录、真实 PR 没跑”—— +那是 2026-10-25 左右的快照,现已过期。本仓现为 +`github.com/openqj/codex-app-server`,PR → CI(host-verify) +→ `--merge` 合并 → `deploy.py` git 模式部署已是常规链路 +(例如 #124 React 命令台、#125 Muse 风格、#126 继续消息持久化、 +#128 一页式排版、#129 GitHub 风格,均经此链路上线生产)。 +不要再拿旧段落去推断本仓的 PR 能力。 -- **仓库远端**:本仓(codex-app-server)目前没有 git remote - origin,`git remote -v` 为空。pr 命令会先查 remote,没有就 - 明确报错,不静默失败。以后给仓库加 remote 再跑: - `git remote add origin `。 -- **gh 登录**:本机有 `gh`,但 `gh auth status` 显示未登录。 - 不要在本轮登录,也不要把 PAT 写进任何文件、state、日志或 - 聊天记录;以后用户给 PAT 时走 Secure Vault,再跑 - `gh auth login`。没有登录时 pr/pr-checks/pr-merge 会把 gh - 的错误原文打印出来,不编造 PR URL。 +但要分清两条线: + +- **本仓的发布链路**:分支 → PR → CI 全过 → 合并进 main → + git 模式部署到生产主机,部署须验 health 与 commit 一致。 +- **host.py 的 pr/pr-checks/pr-merge**:那是给任意目标仓库用的 + 本地命令,仍要求目标仓有 remote 且运行机器上 `gh` 已登录; + 没有登录时它会把 gh 的错误原文打印出来,不编造 PR URL。 + 不要把 PAT 写进任何文件、state、日志或聊天记录;需要登录时 + 走 Secure Vault。 - **流程(远端和登录齐了之后)**: 1. `host.py github-workflow` 生成 CI workflow(已生成的文件 要提交进仓库,GitHub Actions 才会跑它); @@ -280,12 +397,36 @@ PR/CI 这条线本轮只做了代码和本地可测部分,真实 PR 没有跑 ## 开源没给到的(别指望照搬) - 模型本身、ChatGPT 登录、计费、Codex Cloud 云端沙箱。 -- GitHub / Slack 等渠道的托管派发。 -- 桌面版的 diff 评审 UI、多线程看板、通知。 +- GitHub / Slack 等渠道的托管派发:云端层已有 webhook 入站、 + 推送订阅与自动 PR,但 Slack 触发未对真实服务器实测, + 托管派发不等于 ChatGPT 托管服务。 +- 桌面版的 inline 评论、side-by-side diff、点按钮批准: + 命令台有评审队列与对话时间线,但评审仍是 git diff 文本 + + merge/discard 命令,不是桌面版评审 UI。 - Computer Use(屏幕操作)是另一条管线,不在 CLI 中间层里; 本版 features 里虽然有 `computer_use` 开关,但那不是 “复制 app-server 就自动得到一个桌面操作 agent”。 +## 诚实边界(2026-10-29,更新 README 时仍成立的) + +- **内层沙箱**:Docker 容器嵌套 pidns 下 bwrap 的 fresh proc + mount 被内核拒绝(宿主级 + 带 userns 的 AppArmor profile + 才能跑通 workspace-write),所以生产是 Docker 隔离 + + 容器内 danger-full-access,不是双层沙箱;宿主级 workspace-write + 下 agent 只能写自己 worktree,但 `/tmp` 不隔离,写 /tmp 会落到 + 宿主 /tmp 上。 +- **并行规模**:18 人并行已探过,40 人并行未证明且已暂搁置; + 需要 40 人规模时走串行分波次或拆小批次,不要把 18 人的通过 + 写成 40 人的通过。 +- **HA**:多机 HA 0→4 的跨机部分是同主机双进程模拟验证, + 真“杀一台主机仍存活”需要第二台 VPS;VM 会被平台定时回收, + 不能作 HA 节点。 +- **未合并的在途工作**:G10 断点 provider(PR #127)只推了 PR, + 未合并、未部署;不要在 README 里把它写成已上线。 +- **密钥**:API key、OAuth secret、中转 key 只走环境变量或 + Secure Vault,绝不写进仓库、state、日志或聊天记录; + key 明文只在签发时返回一次。 + ## 安全 - 常驻服务不要用 `danger-full-access`,也不要把