ci / test (push) Successful in 45m25s
- Vendor codex-ui-stack host/ as cloud/codex-ui-host/ (original/ not vendored, 650MB) - Bridge emulates 7 methods missing in codex 0.164.0-alpha.9, exposes 29 experimental-protocol methods (150 -> 179), Linux binary candidates, read-only POST /__codex_git - GET /console 302s to the UI host (:8095) when reachable, else falls back to the classic React console; GET /console-classic always serves it - Alignment tables: docs/codex-native-ui-alignment.md - Tests: test_console 7, new test_codex_ui_host 4, handler method pin 247 -> 249
94 lines
19 KiB
Markdown
94 lines
19 KiB
Markdown
# Codex 原生 UI ↔ codex-app-server 双向对齐表
|
||
|
||
- 基线:Gitea main `37ba6876bf326ebd6b1e01ae7d2c5684dafe439b`(2026-11-06,线上 code.tose.top 生产基线)
|
||
- 工作区:`~/workspace/codex-app-server-codex-ui`,分支 `feat/codex-native-ui`
|
||
- UI 栈:`~/workspace/user/downloads/codex-ui-stack/`(zip sha256 `668bcd82…3125e`);`original/` 2026-10-27 自 `/tmp/codex-asar/webview` 原样 vendored(20918 文件,`index.html` sha `85e5c5b4…`),**必须保持字节一致**,改动只发生在 `host/` 响应层与 app-server 侧
|
||
- 清点依据:A(UI host 清点)、C(codex 0.164.0-alpha.9 全量 150 方法实测探活)、D(37ba687 基线重核:控制面 **179 个端点**,控制台为 `cloud/console-react/` 源码构建的 353KB React 包)。B 路按旧分支 5ba4d35 清点的数字(60 端点、820 行 console、resident 为 0)**在本基线全部作废**,未采用。
|
||
- codex 二进制:`codex-cli 0.164.0-alpha.9`(`/opt/hatch-image/bin/codex`)
|
||
- 约定:凡原生 UI 没有对应表面可承载某功能时,本表如实写“原生无对应表面”,不自造页面/组件。
|
||
|
||
## 集成架构(结论先行)
|
||
|
||
1. **线程共享是集成点**:`host.py` 一个 app-server 进程托管多 thread,线程状态持久化在 `CODEX_HOME`(默认 `~/.codex`)。UI host 的 `app-server-bridge.mjs` 自己 spawn 一个 app-server,但因共享同一 CODEX_HOME,`thread/list` 能看到 host.py 任务的 thread(C 实测:`thread/list` 返回了既有 thread)。因此 UI 不需要直连 host.py 的 app-server 进程,也不需要新增 socket/ws 附着逻辑。
|
||
2. **入口替换方式**:`GET /console` 改为按需 302 到 UI host(默认 `http://127.0.0.1:8095/`,可配 `CODEX_UI_URL`);UI host 不可达时回落到旧控制台。旧 React 控制台**保留**,新路由 `GET /console-classic` 继续 serve `cloud/console.html`(理由见 §3)。
|
||
3. **original/ 不进仓**:解压后 650MB / 20918 文件,不可提交。入仓的只有 `host/` 适配层(约 104KB)与启动脚本;`original/` 由 `CODEX_UI_ORIGINAL_ROOT` 指向已下载的栈目录,部署时以 zip sha256 校验(见 §4)。
|
||
4. **模型走 relay,不走 mock**:栈自带的 `relay-config.json` 里 `baseUrl` 指向本地 mock(`http://127.0.0.1:8095/mockrelay/v1`),真实 Bumian 地址只写在 JSON 里、host 代码无引用,`apiKey` 为空。当前配置下模型调用走确定性 mock 替身。接真实 relay 须把 `baseUrl` 改为 `relayBaseUrl` 并经环境变量给 key(只写 envKey 名,key 值绝不入文件)。
|
||
|
||
---
|
||
|
||
## A 表——UI 要、codex-app-server / 官方 app-server 缺的
|
||
|
||
| # | UI 需要的方法/功能 | 缺口现状(实测) | 补齐方案与落点 |
|
||
|---|---|---|---|
|
||
| A1 | `thread/stop` | 官方 0.164.0-alpha.9 不存在(`unknown variant`,C 实测;实验 schema 枚举 172 方法里也没有它) | 桥接层仿真:`app-server-bridge.mjs` 拦截 `thread/stop`,转调 `turn/interrupt`(host.py:939 已确认:对齐 Codex App 停止按钮就是发 `turn/interrupt`)。落点:入仓的 `cloud/codex-ui-host/host/app-server-bridge.mjs` |
|
||
| A2 | `thread/rollback` | 同上,不存在 | 桥接层仿真:转调 `thread/revert`(实测存在)。同上落点 |
|
||
| A3 | `thread/startAeon` | 同上,不存在 | 桥接层仿真:转调 `thread/start`(Aeon 为实验模式,本二进制无此入口;降级为普通 start,并在方法注释里写明)。同上落点 |
|
||
| A4 | `interactive/liveSessions/list`、`interactive/session/upload`、`interactive/sessionSandbox/list`、`interactive/sessionSandbox/read`(4 个) | 同上,均不存在 | 桥接层仿真:`list` 类回空列表、`upload`/`read` 回结构化 `-32601 method not found (interactive sessions not available in codex 0.164.0-alpha.9)`。不伪造会话数据。UI 的 live-session 面若调用,即表现为“无 live 会话”,不静默成功。落点同上 |
|
||
| A5 | 实验协议 29 个方法(UI 清单漏列,二进制实测 schema 里有) | UI 清单只有 150 个;`local-host.js` 按清单动态生成 services,漏列的 29 个 UI 调不到。29 个:`account/bedrock/checkGovCloudRequirements`、`account/sendAddCreditsNudgeEmail`、`account/workspaceMessages/read`、`command/exec/resize|terminate|write`、`environment/add`、`environment/status`、`externalAgentConfig/import/recordHistory`、`plugin/share/checkout`、`server/diagnostics`、`thread/backgroundTerminals/list|terminate`、`thread/decrement_elicitation|increment_elicitation`、`thread/items/read`、`thread/prediction/request`、`thread/readState/update`、`thread/realtime/appendAudio|appendSpeech|appendText|listVoices|start|stop`、`userVerification/cancel|delete|enroll|status|verify` | 把 29 个追加进 `appserver-methods.json`(150 → 179 个,与控制面端点数巧合相同,无关联)。只加清单、不改 `original/`。其中 `realtime/*` 与 `userVerification/*` 在本机未逐个探活,标注“仅 schema 有名,未探活”。落点:入仓的 `cloud/codex-ui-host/host/appserver-methods.json` |
|
||
| A6 | 桥接在 Linux 上找到 codex 二进制 | `app-server-bridge.mjs` 的默认候选只有 `$CODEX_APP_SERVER_BIN` 与两个 macOS ChatGPT 内路径;在 Linux 上不设环境变量即报 `codex binary not found`(C 实测) | 入仓的 bridge 增加候选:`$CODEX_BIN`(host.py 同款环境变量名)→ `/opt/hatch-image/bin/codex` → PATH 里的 `codex`。落点同 A1 |
|
||
| A7 | UI 要 `git.request("stable-metadata")`(Send 门控靠它非空) | `local-host.js` 里 git 是 stub,固定回 `{available:false, …}`,其余回 null | 桥接层补只读 git 能力:`local-host.js` 的 `git.request` 改为调 host 新增端点 `POST /__codex_git`(只接受 `stable-metadata` / `branch` / `status` 三种只读 op,在给定 cwd 内跑 `git` 只读命令,拒绝写操作与 cwd 外路径)。落点:入仓的 `cloud/codex-ui-host/host/local-host.js` + `web-server.mjs` |
|
||
| A8 | UI 要终端面 | `local-host.js:274` 把 `terminal` 服务显式置 null;进程类能力只剩 app-server 的 `process/*` / `thread/shellCommand` | 不补终端面:原生 UI 的终端服务在本栈是 stub,app-server 的 `process/spawn|writeStdin|resizePty|kill` 实测存在(缺参但方法存在),UI 若经 `process/*` 调用即走真实进程。实现时核实 UI 是否调用 `terminal` 服务:若调用,记为“原生终端面在本栈不可用”,不造终端组件。落点:验证阶段核实,不写代码 |
|
||
| A9 | UI 要账号 / 用量 / 远控 | `account/rateLimits/read`、`account/usage/read`、`plugin/share/list`、`remoteControl/enable|disable|pairing/start` 需 ChatGPT 账号登录(C 实测);当前 provider(relay/apikey)不用 gateway OAuth,`account/gatewayOAuth/login` 不可用 | **不可补**:这是 provider/账号限制,不是代码缺口。UI 的 `mode-decide` 改写已强制跳过登录直进 app,本栈账号面实际不可达。实现不改动,验收时以实测报错为准 |
|
||
| A10 | UI host 无鉴权 vs 控制面要求 `X-API-Key` | UI host 全部端点同源、无鉴权,只绑 `127.0.0.1:8095`;控制面除 `/health`、`/console`、`/oauth/*`、`/webhooks/*` 外一律要求 key | 不给 UI host 加自造登录面:保持 loopback-only 绑定;`/console` 的 302 只发生在控制面已鉴权(或本机回环)的请求之后——实现方案:`cloud/server.py` 的 `/console` 在 302 前先做一次 UI host 健康探测(`GET /` 2 秒超时),探测失败则回落旧控制台;UI host 本身不暴露到外网。密钥不参与 UI host 鉴权 |
|
||
| A11 | UI 要 `config/read` 含 relay 覆盖、`model/list` | C 实测均 ok;`config/read` 返回 `model=gpt-6.1-sol`、`model_provider=openkun`(与本机 `~/.codex/config.toml` 一致);`model/list` 仅一条 | 无缺口:桥接的 `-c` 覆盖(`relayArgs`)保持原样;`verify.mjs --deep` 第 13 项即校验此点。实现不改动 |
|
||
|
||
## B 表——app-server 有、UI 缺的(控制面 179 个端点 → 原生 UI 表面)
|
||
|
||
控制面分组(D 实测,179 = 任务 66 + 项目/环境/上传 17 + 租户配额/管理 11 + OAuth/身份/key 14 + GitHub App/webhook 7 + resident 7 + automations 10 + 记忆/KB/成本/推送 23 + worker/监控/模型 10 + 其它 full-auto 9 + todos 5):
|
||
|
||
| # | 控制面功能(端点) | 原生 UI 对应表面 | 结论 |
|
||
|---|---|---|---|
|
||
| B1 | 任务创建 / 列表 / 详情(`POST /tasks`、`GET /tasks`、`GET /tasks/{id}`) | 线程列表(`thread/list`)与线程阅读(`thread/read`、`thread/items/list`)是**线程**,不是**任务**;任务含 prompt/执行器/模型/goal/分支/worktree 等宿主字段,原生 UI 无任务队列面 | 原生无对应表面(任务列表、建任务表单)。任务的线程内容可经 `thread/read`、`thread/items/list` 看到,属部分映射,不等同 |
|
||
| B2 | 任务操作:取消/暂停/重试/合并/发布/提 PR/分叉/继续(`POST /tasks/{id}/{cancel,pause,retry,merge,release,pr,fork,continue}`) | 取消→`turn/interrupt`(+ A1 的 `thread/stop` 仿真);重试/继续→`thread/resume`、`thread/start`;分叉→`thread/fork` | 部分映射:cancel/retry/fork/continue 有方法级对应;pause/release/merge/pr **原生无对应表面**(合并与 PR 是宿主 Git 流程,原生 UI 无此概念) |
|
||
| B3 | 任务 trace(`GET /tasks/{id}/trace`、`/trace/stream` SSE)、时间线、成本、结果全文、agents、文件/目录树(`/files`、`/tree`) | `thread/timeline/list`(trace 的部分映射)、`thread/items/list`(内容)、`fs/readDirectory`、`fs/readFile`(文件浏览的部分映射) | 部分映射:trace/时间线/文件有近似方法;成本(`GET /tasks/{id}/cost`)、agents 分工、SSE trace 流 **原生无对应表面** |
|
||
| B4 | 任务线程管理(`/threads`、`/thread`、`/thread/{rename,archive,unarchive,compact,message}`、`/chat-history`、`/messages`、followups、reviews、approvals、handoff、refs、worktree、audit-loop、goal、sleep/wait) | `thread/name/set`、`thread/archive|unarchive`、`thread/compact/start`、`thread/items/list`、`review/start`、`thread/approveGuardianDeniedAction` | 部分映射:改名/归档/压缩/评审启动/审批有方法级对应;followups、handoff、refs、worktree 快照、audit-loop 裁决/修复、goal 续跑、sleep/wait、任务间 messages **原生无对应表面** |
|
||
| B5 | 浏览器远控(`/tasks/{id}/browser/{state,stream,input,control}`) | 无。原生 UI 的浏览器面是它自己的渲染面,不接管外部浏览器 | 原生无对应表面 |
|
||
| B6 | 项目与成员(`POST/GET /projects`、`PATCH /projects/{id}`、`/projects/{id}/members`) | `project/list|read|create|import|update|delete|move` 是 codex 本地工程目录,不是云端多租户项目;成员/角色模型(viewer/contributor/maintainer/owner)原生 UI 没有 | 原生无对应表面(云端项目制与成员管理) |
|
||
| B7 | 环境(`POST/GET /environments`、`/environments/{id}/archive`) | `environment/info` 是单机执行环境信息,不是“项目仓库命名基准检出” | 原生无对应表面 |
|
||
| B8 | 租户配额 / 管理(`GET /users/me/quota`、`/admin/quotas`、`/admin/sweep`、`/admin/backup`、`/admin/audit-ledger/*`、`/admin/push/dispatch`、`/admin/cost-by-creator`、`/admin/ops/approval-timeout`、`/users/me/cost`) | `account/usage/read`、`account/rateLimits/read` 需 ChatGPT 登录且不可用(A9);其余无 | 原生无对应表面 |
|
||
| B9 | OAuth / 身份 / API key(`/oauth/{provider}/*`、`/users/me/keys`、`/users/me/profile`、`/users/me/gateway/rotate`、`/github-app/*`) | `account/login/start`、`account/logout` 存在,但本栈 `mode-decide` 改写跳过登录直进 app,账号面实际不可达;key 管理无 | 原生无对应表面(OAuth 与本站 key 管理) |
|
||
| B10 | GitHub webhook / Slack webhook(`POST /webhooks/github|slack`)、GitHub 仓库列表(`GET /github/repos`) | 无 | 原生无对应表面 |
|
||
| B11 | resident 常驻监听(7 个端点,`POST/GET/PATCH /projects/{id}/resident-watches`、pause/resume/end) | 无。控制台 `console-react/src/` 全文 0 处 `resident`,连旧控制台都没有这个面 | 原生无对应表面 |
|
||
| B12 | automations 与分诊(10 个端点,`/projects/{id}/automations`、bind/heartbeat/cancel、`/triage`) | 无。`automations.py` 的自动化是云端定时建任务;原生 UI 无自动化面(host.py 的本地 automations 是另一套) | 原生无对应表面(bind/heartbeat 连旧控制台都没调) |
|
||
| B13 | full-auto(9 个端点,`/full-auto/{status,config-plan,validate-action,merge-gate,ci-fix,release-plan,rollback-plan,execute,audit}`) | 无 | 原生无对应表面 |
|
||
| B14 | 项目记忆 / KB / 成本 / 推送(23 个端点,`/projects/{id}/{memory,kb,cost,push/subscriptions}`、`/users/me/memories`、`/users/me/identity-context`、`/users/me/auto-memory*`) | `memory/status|reset` 是 codex 本地记忆状态,不是项目记忆库;`skills/list` 与项目技能清单(`GET /projects/{id}/skills`)部分对应 | 部分映射:项目技能→`skills/list`;其余(项目记忆/KB/成本/推送订阅/自动记忆)**原生无对应表面** |
|
||
| B15 | 技能 / 应用 / MCP(`GET /projects/{id}/{skills,apps,mcp-servers}`、`POST …/mcp-servers/reload|oauth-login`) | `skills/list`、`plugin/list|installed`、`app/list|read`、`mcpServerStatus/list`、`mcpServer/tool/call`、`config/mcpServer/reload` | 覆盖:技能/插件/应用/MCP 有方法级对应(`plugin/read` 失败时 host 有离线回落) |
|
||
| B16 | worker / 监控 / 模型(`GET /workers`、`/workers/heartbeat`、`/metrics`、`/metrics-report`、`/audit-scorecard`、`/routing-shadow`、`/models`、`/relay/models`) | `model/list`、`modelProvider/capabilities/read` 覆盖模型面;其余无 | 部分映射:模型列表有对应;worker 池、指标、评分卡、路由影子 **原生无对应表面** |
|
||
| B17 | 待办(5 个端点,`/projects/{id}/todos`) | 无 | 原生无对应表面 |
|
||
| B18 | 模型总览与中转模型列表(`GET /models`、`GET /relay/models`) | `model/list`(C 实测仅一条 `gpt-6.1-sol`) | 部分映射:单模型列表有对应;中转站多模型下拉无原生面(旧控制台用 `/relay/models` 做模型下拉,新 UI 用 `model/list`) |
|
||
|
||
## §3 旧 console.html 的去留决定
|
||
|
||
**决定:保留旧控制台,改路由为 `GET /console-classic`,`GET /console` 改为新 UI 入口(302 到 UI host,不可达时回落旧控制台)。**
|
||
|
||
理由:
|
||
1. 旧控制台是 React 构建产物(`cloud/console-react/`,46 个源码文件),承载 B 表里大量“原生无对应表面”的管理面(配额/管理、resident、automations、full-auto、审计、成员、待办等)。悄悄删除等于删除功能,违反“不删除较新工作”。
|
||
2. 原生 UI 是单用户线程界面,无法承载多租户控制面;“替换”指的是默认入口,不是不留后路。
|
||
3. `test_console.py`(5 个测试)只验壳与路由,不跑浏览器 JS;路由改动须同步更新这 5 个测试(`/console` 无 key 200 出 HTML 的断言要改为 302/回落行为,`/console-classic` 承接原断言),并新增 UI host 相关测试。
|
||
|
||
## §4 入仓清单(实现范围)
|
||
|
||
1. `cloud/codex-ui-host/`:自 `~/workspace/user/downloads/codex-ui-stack/` 拷入 `host/` 全部 12 个文件 + `start.sh` + `verify.mjs`(原样,改动只在下述适配处;`original/` 不入仓)。
|
||
- 适配 A1–A4:`app-server-bridge.mjs` 增加 7 个缺失方法的仿真分支。
|
||
- 适配 A5:`appserver-methods.json` 追加 29 个方法(150 → 179)。
|
||
- 适配 A6:`app-server-bridge.mjs` 增加 `$CODEX_BIN`、`/opt/hatch-image/bin/codex`、PATH 候选。
|
||
- 适配 A7:`local-host.js` 的 `git.request` 走新增 `POST /__codex_git`(只读三 op);`web-server.mjs` 增加该端点。
|
||
- 适配:`relay-config.json` 保持栈自带 Bumian 配置原样(`apiKey` 为空),另加 `relay-config.README.md` 说明 mock 现状与接真实 relay 的两步(`baseUrl` 改为 `relayBaseUrl` + 设 `BUMIAN_API_KEY` 环境变量);不换 provider、不写 key。
|
||
- 适配:`projectless.mjs` 的默认工作区根在 Linux 上改为 `$CODEX_WEB_WORKSPACE_ROOT` 或 `~/Codex`(原 `~/Documents/Codex` 在服务器上不存在)。
|
||
2. `cloud/server.py`:`GET /console` 改为 302 到 `CODEX_UI_URL`(默认 `http://127.0.0.1:8095/`),2 秒探测失败回落旧控制台;新增 `GET /console-classic` serve `cloud/console.html`(原 `/console` 行为整体平移)。
|
||
3. `scripts/run-codex-ui.sh`(新建):启动 UI host 的唯一入口,设置 `CODEX_APP_SERVER_BIN`、`CODEX_UI_ORIGINAL_ROOT`(默认已下载的栈目录),`exec node cloud/codex-ui-host/host/web-server.mjs --port ${UI_PORT:-8095}`。
|
||
4. 测试:`cloud/tests/test_console.py` 按 §3 更新;新增 `cloud/tests/test_codex_ui_host.py`(Python 侧:方法表计数 179、7 个仿真方法在 bridge 里存在、original/ 未被入仓——即仓内无 `cloud/codex-ui-host/original/`、启动脚本存在且可执行)。
|
||
5. 验证:`python3 run_tests.py cloud` 相关子集 + `node verify.mjs`(shallow)+ `node verify.mjs --deep`(真实 app-server 可用时)+ 真实联调冒烟(起 :8095,`thread/list` 与等价调用,日志为证)。
|
||
|
||
## §4.1 实现注记(实现后补记,与规范同等效力)
|
||
|
||
- A5:`appserver-methods.json` 是纯 JSON,加文件头注释会破坏合法性,故注释未写进 JSON 本身;“`realtime/*` 与 `userVerification/*` 仅 schema 有名、未逐个探活”记在这里(本节),与 A5 表格同效。
|
||
- A4 口径:以本表 A4 为准——`list` 类(`interactive/liveSessions/list` 与 `interactive/sessionSandbox/list`)回空列表;`upload` 与 `sessionSandbox/read` 回结构化 `-32601`。实现初版曾把 `sessionSandbox/list` 划入 `-32601` 组,协调员已按本表纠正。
|
||
- `web-server.mjs` 的 `--root` 默认值增加了 `CODEX_UI_ORIGINAL_ROOT` 回落(1 行):不加则启动脚本设该变量无效;接受该改动,启动脚本无需改传 `--root`。
|
||
|
||
## §5 剩余缺口(实现后仍存在,如实记录)
|
||
|
||
- B 表里标记“原生无对应表面”的功能(B1 任务队列、B6 项目制、B7 环境、B8 配额/管理、B9 OAuth/key、B10 webhook、B11 resident、B12 automations、B13 full-auto、B14 项目记忆/KB/成本/推送大部、B17 待办、B5 浏览器远控)**没有**原生界面承载,仍走 `/console-classic`(旧 React 控制台)。这是按用户要求不自造界面的直接结果,不是遗漏。
|
||
- A9(ChatGPT 登录类方法)不可补;A8 终端面待验证阶段核实。
|
||
- `original/` 不进仓:部署时须另行提供与 zip sha256 一致的 `original/` 目录,否则 UI host 无法启动;这一步属于部署(须用户点头),不在本轮本地完成范围内。
|