内容
# 把 Codex 接进现有聊天前端:App Server 网关适配、三引擎切换与 MCP 工具同步
> 文档状态:2026-08-22。本文聚焦 Codex;API 与 Claude Code `-p` 网络上有较多相关教程,这仅讲兼容边界。
>
> 本文在 gateway 的 mode 值中使用 `codex`,对应官方 Codex App Server 集成。`claude-p` 保留原名,因为它对应真实的 `claude -p` 执行模式。
这套方案适合已经拥有聊天前端、SSE 网关、会话数据库和自定义工具系统的人。最终效果是:同一前端可以按 conversation 在 API、Claude Code `-p`、Codex App Server 三条后端之间选择;三条后端共用消息存储、附件、流式 UI 和工具气泡;各 provider 的会话语义、提示词载体与原生事件留在各自 adapter 内。
## 1. 最终架构
```mermaid
flowchart LR
UI[Chat UI] -->|HTTP + SSE| GW[Chat Gateway]
GW --> DB[(Conversation DB)]
GW --> R{conversation.mode}
R -->|api| API[API Provider]
R -->|claude-p| CCP[Claude Code Adapter]
R -->|codex| CXP[Codex App Server Adapter]
CCP --> CC[Claude Code process/session]
CXP --> AS[Persistent Codex App Server]
AS --> T1[Codex thread A]
AS --> T2[Codex thread B]
REG[Canonical Tool Registry] --> RCC[CC renderer/consumer]
REG --> RCX[Codex renderer/consumer]
RCC --> CC
RCX --> AS
MEM[Optional memory service] -->|MCP or bounded injection| GW
MEM -->|MCP| CC
MEM -->|MCP| AS
```
最重要的边界有四条:
1. gateway 负责产品语义:conversation、消息树、附件、持久化、统一 SSE、后台任务。
2. provider adapter 负责协议语义:启动、认证、session/thread、工具事件、错误和中断。
3. 工具注册拥有单一真源,再为 CC 与 Codex 渲染各自配置;不要维护两份人工列表。
4. persona、style、记忆检索和 refill 各有独立职责;混成一个巨大 system prompt 会让更新、缓存与会话恢复一起失控。
本文把 `refill` 定义为“在新建、恢复、分叉或 compact 后,为 Codex thread 回填必要的历史上下文”。gateway 从自己的消息库中按预算取出历史消息摘要、最近几组完整对话和必要状态,组成一个临时 context block,放进下一次 `turn/start`。refill 只填当前 thread 缺少的部分,不重复整份 transcript,也不替代 Codex 原生的 thread history。
## 2. 为什么选 App Server
Codex 有三类常见接入面:
| 接入面 | 适合场景 | 长聊天前端中的局限 |
|---|---|---|
| `codex exec --json` | CI、脚本、一次性自动化 | 每轮进程与历史管理成本高,深度交互能力有限 |
| Codex SDK | 应用内编程调用、希望使用类型封装 | 很合适;仍需自行定义 gateway 事件与持久化契约 |
| `codex app-server` | 自定义富客户端、完整 thread/turn/event/approval 集成 | 协议面较宽,需要严谨 adapter 与版本测试 |
OpenAI 将 App Server 定位为富客户端集成接口,覆盖认证、conversation history、approvals 和 streamed agent events。自动化或 CI 更适合 SDK。[官方 App Server 文档](https://developers.openai.com/codex/app-server)
我们选择一个长期运行的 App Server 进程服务多个逻辑 conversation。这样做可以减少反复启动成本,也便于集中处理认证、模型目录、MCP 初始化与全局通知。每个前端 conversation 仍绑定独立 Codex thread,adapter 必须维护 ownership,严禁把一个 thread id 同时交给两个窗口。
## 3. 先冻结你自己的网关契约
先让前端只认识一套 provider-neutral contract。一个够用的最小集合如下:
```json
{"event":"start","conversation_id":"conv_demo","session_action":"resume"}
{"event":"session_bound","provider":"codex","session_id":"opaque-thread-id"}
{"event":"thinking_delta","text":"正在检查……","reasoning_source":"summary"}
{"event":"delta","text":"结论是……","phase":"final_answer"}
{"event":"tool_call_start","tool_call_id":"item_1","tool_name":"search","arguments":null,"tool_round":1}
{"event":"tool_call_result","tool_call_id":"item_1","tool_name":"search","arguments":{"q":"demo"},"result":"...","tool_round":1}
{"event":"status","kind":"rate_limit","detail":{}}
{"event":"server_request","request_id":"req_1","request_type":"user_input","payload":{}}
{"event":"done","text":"完整正文","usage":{}}
```
重要的细节:
- `session_bound` 在 native thread 创建或恢复成功后立即发出,gateway 收到后再落库。
- tool start 时参数可能尚未完整,UI 应允许 `arguments: null`,完成事件再回填。
- `done.text` 是整轮最终文本,方便断线恢复和一致性校验;前端实时展示仍消费 delta。
- reasoning 只展示官方提供的 summary 或明确可见的 reasoning text。不要把隐藏 chain-of-thought 写进产品承诺。
- provider 原生 id 只在 adapter 与数据库绑定层流转;公开日志可使用哈希或业务侧 id。
## 4. Codex runtime 与认证
### 4.1 固定版本,保存校验信息
App Server 仍在快速演化。部署时应固定 Codex 版本,并记录:
- CLI 版本;
- 安装包来源;
- artifact SHA-256;
- 该版本生成的 App Server schema;
- adapter 测试所用 fixture 版本。
升级流程应先生成/比较 schema,再跑 adapter parity tests,最后做真实 E2E。生产验证过某一版,不代表 `main` 分支或半年后的 CLI 会保持所有字段形状。
本文实际工程曾锁定并验证 Codex `0.148.0`。当前官方文档已经展示更丰富的 thread 字段和新能力,因此示例使用 opaque thread id,不使用 UUID 正则。若你的固定版本确实只返回 UUID,可以在版本专属 adapter 内收紧校验。当前官方 Python SDK 仍公开 `base_instructions` / `developer_instructions`,并在 wire 上映射到 camelCase;这两个字段可在 [SDK API reference](https://github.com/openai/codex/blob/main/sdk/python/docs/api-reference.md) 与 [SDK FAQ](https://github.com/openai/codex/blob/main/sdk/python/docs/faq.md) 核对。App Server 网页的简短 start 示例并非完整 schema。
### 4.2 认证目录必须持久化
远程或无头环境可以使用:
```bash
codex login --device-auth
codex login status
```
官方也支持先在有浏览器的机器登录,再复制 `~/.codex/auth.json`。该文件包含访问令牌,权限应设为 `0600`,放进持久卷,绝不能进入 Git、日志、issue 或教程截图。[官方认证文档](https://developers.openai.com/codex/auth)
使用 ChatGPT 登录身份时,启动 App Server 前清掉会偷偷改变 provider 或计费身份的环境变量,例如:
```python
for key in (
"OPENAI_API_KEY",
"CODEX_API_KEY",
"CODEX_ACCESS_TOKEN",
"OPENAI_BASE_URL",
"OPENAI_API_BASE",
):
child_env.pop(key, None)
```
若你明确选择 API key 模式,则反过来:把凭据放进 secret manager 或受限 env 文件,并在 health/log 中只输出“存在/缺失”,永远不输出值。
## 5. 启动 App Server 与 initialize 握手
最容易调试的 transport 是 stdio JSONL:
```bash
codex app-server --listen stdio://
```
父进程需要同时读取 stdout 和 stderr。stdout 每行是一条省略 `jsonrpc: "2.0"` 的 JSON-RPC 消息;stderr 只能当诊断日志,别混进协议 parser。
连接建立后顺序固定:
```json
{"method":"initialize","id":1,"params":{"clientInfo":{"name":"my_gateway","title":"My Gateway","version":"0.1.0"}}}
{"method":"initialized","params":{}}
```
任何 thread 请求都应等 initialize 成功。父进程需要维护:
- 自增 request id;
- `request_id -> Future` 的 pending map;
- `thread_id -> Queue` 的通知路由;
- reader task 与 stderr task;
- App Server epoch;
- 进程退出时对全部 pending future/queue 的失败广播。
精简实现见 [examples/codex_app_server_bridge.py](examples/codex_app_server_bridge.py)。
## 6. conversation 与 Codex thread 生命周期
数据库至少保存这些字段:
```sql
CREATE TABLE codex_window_sessions (
conversation_id TEXT PRIMARY KEY,
codex_thread_id TEXT UNIQUE,
codex_session_id TEXT,
runtime_hash TEXT,
resume_safe INTEGER NOT NULL DEFAULT 1,
generation INTEGER NOT NULL DEFAULT 0,
last_completed_message_id TEXT,
compact_refill_pending INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
updated_at TEXT NOT NULL
);
```
当前 App Server 还可能返回 `thread.sessionId`:root thread 通常以自己的 thread id 作为 session tree root,forked thread 会保留 root session id。`codex_thread_id` 负责具体 conversation ownership;`codex_session_id` 用于 fork 树审计和跨 thread 关联。固定版本没有该字段时保留 `NULL`,不要自行推导。
`resume_safe` 是关键状态位:
- turn 已被 App Server 接受后置为 `0`;
- 收到同一 turn 的 `turn/completed` 并完成持久化后置为 `1`;
- 连接在“请求可能已送达、响应未知”的窗口断掉时保持不安全;
- 不安全状态禁止盲目重试,否则可能产生双回复。
### 6.1 fresh
```json
{"method":"thread/start","id":10,"params":{
"cwd":"/workspace",
"approvalPolicy":"never",
"sandbox":"workspaceWrite",
"personality":"none",
"model":"YOUR_PINNED_MODEL",
"baseInstructions":"<identity prompt>",
"developerInstructions":"<output style>"
}}
```
字段名和 sandbox 枚举要以你的固定版本 schema 为准。当前 SDK 使用 snake_case Python 参数并在 wire 上转换为 camelCase;旧版与新版的 sandbox 结构也可能不同。
### 6.2 warm
同一 App Server epoch 内,conversation 已绑定 thread,且 runtime fingerprint 未变化时,直接在现有 thread 上 `turn/start`。这一条不重复执行完整 refill。
### 6.3 cold resume
App Server 重启、逻辑 session 被 idle reaper 回收,或 adapter 重新加载时:
```json
{"method":"thread/resume","id":11,"params":{
"threadId":"opaque-thread-id",
"baseInstructions":"<current identity>",
"developerInstructions":"<current style>"
}}
```
恢复成功后返回的 thread id 必须与目标一致。恢复时配置覆盖的首轮生效语义曾在部分版本出现过问题;生产系统应有“改变 style 后立即 cold resume”的回归用例,不能只测试 fresh。
### 6.4 fork
```json
{"method":"thread/fork","id":12,"params":{
"threadId":"source-thread-id",
"baseInstructions":"<identity>",
"developerInstructions":"<style>"
}}
```
新 thread id 必须与 source 不同。fork 期间锁住 source ownership 与 target slot,避免 source 正在前进时从错误 tip 分叉。若产品允许“从任意历史消息分支”,数据库的 source tip 和 native transcript 必须精确对齐;对齐证明不足时退回 fresh + frozen hydration。
### 6.5 runtime fingerprint
将这些值稳定序列化并哈希:
```text
model
+ reasoning effort
+ identity prompt hash
+ output style hash
+ MCP registry digest
+ provider-specific config version
```
fingerprint 变化时轮换逻辑 session,随后 resume/fork/fresh。继续把旧 warm thread 当成同一 runtime,会出现“前端已经改了 persona,模型还在读旧内容”的幽灵状态。
## 7. persona、style 与上下文窗口
推荐把提示词分为四层:
| 层 | 内容 | Codex 载体 |
|---|---|---|
| Identity | persona、用户关系背景、长期身份事实 | `baseInstructions` |
| Style | 语气、格式、输出偏好 | `developerInstructions` |
| Turn context | 当前时间、附件说明、动态状态 | 当前 `turn/start.input` |
| Refill | 历史消息摘要、最近完整 exchanges、分支上下文 | fresh/resume/fork/compact 时的受限输入块 |
`personality` 建议设为 `none`,由你自己的 style 完成输出控制。若希望保留 Codex 内置 personality,就把它纳入 runtime fingerprint,并做冲突测试。`baseInstructions` / `developerInstructions` 属于官方协议/SDK 字段;“identity 放前者、style 放后者”是本文 adapter 的映射策略,仍需对目标版本生成的 schema 与 fresh/resume/fork 首轮行为做测试。
### 7.1 为什么要把 style 单独拿出来
- 修改文风无需重建 identity 文本;
- 日志能分别显示 persona hash 和 style hash;
- API、CC、Codex 可以共用同一个前端 `prompt_blocks.style`,adapter 再映射到各自载体;
- provider 迁移时,关系身份和输出格式不会互相污染。
### 7.2 “start hook/启动提示”怎么处理
若你的项目在 thread 启动时加入自定义提示模板,把它当成一段可替换的 prompt resource:
- 不把模板硬编码进 adapter 状态机;
- 为有模板/无模板各跑一套功能回归;
- 保留工具调用、MCP、compact、resume、approval 的 canary;
- 模板失效时只替换资源层,不改协议层。
## 8. turn/start 与输入格式
```json
{"method":"turn/start","id":30,"params":{
"threadId":"opaque-thread-id",
"input":[
{"type":"text","text":"用户消息"},
{"type":"localImage","path":"/workspace/uploads/image.png"}
],
"effort":"high",
"summary":"detailed"
}}
```
输入 item 的类型随协议版本变化。把附件转换集中在一个函数里,并对未知类型 fail closed。远程前端上传文件后,seat 只能访问受控共享卷内的副本;不要接受用户直接传宿主绝对路径。
refill context 可以垫在当前用户消息前:
```xml
<conversation_refill kind="light">
<history_summary>...</history_summary>
<recent_history_gap>...</recent_history_gap>
</conversation_refill>
```
full refill 只用于 fresh 或无法安全恢复的情况。cold resume、fork 和 compact 后使用更小的 light refill;warm turn 不执行 refill。
## 9. App Server 事件翻译
App Server 的 `item/completed` 应作为该 item 的权威完成态;delta 用于实时 UI。当前官方事件说明见 [App Server events](https://developers.openai.com/codex/app-server)。
| App Server 事件/Item | Gateway 事件 | 处理要点 |
|---|---|---|
| `item/agentMessage/delta` | `delta` | 同 item 按顺序拼接 |
| `item/reasoning/summaryTextDelta` | `thinking_delta` | 稳定的可读 reasoning summary 通路 |
| `item/reasoning/textDelta` | `thinking_delta` | 只在模型支持时出现,不作产品保证 |
| `item/started` + `mcpToolCall` | `tool_call_start` | 记录 item id、tool、server、round |
| `item/completed` + tool item | `tool_call_result` | 最终参数与结果从 completed item 回填 |
| `item/commandExecution/outputDelta` | `status/tool_progress` | 按 `itemId` 顺序拼接;适合可折叠进度,不必写入正文 |
| `contextCompaction` item completed | `status/compact_boundary` | 给下一轮设置 refill pending |
| `thread/tokenUsage/updated` | 内部 usage snapshot | `done` 时附上最终快照 |
| `account/rateLimits/updated` | `status/rate_limit` | 只持久化安全字段 |
| `turn/completed` | `done` 或 `error` | 核对 turn id、status、error |
### 9.1 别把 reasoning summary 写成“完整 CoT”
官方说明 `summaryTextDelta` 提供 readable reasoning summaries;`textDelta` 只在模型支持时出现。教程、UI 和 marketing 都应使用“reasoning summary/思考摘要”这一表述。隐藏推理链没有稳定提取契约。
### 9.2 server request 是双向协议
App Server 可能向 client 发起 approval、user input 或 MCP elicitation 请求。adapter 要区分:
- 可由固定策略自动回答的请求;
- 必须转发前端、等待用户输入的请求;
- 该产品明确不支持、应返回 JSON-RPC error 的请求。
转发前端时记录 request 所属 thread/conversation。用户回填时再次校验 ownership,防止 A 窗口替 B 窗口批准操作。
## 10. 客户端断线之后继续排空
移动端切后台、网络抖动和用户离开页面都可能切断 SSE。若 gateway 的 adapter HTTP client 随前端断线一起关闭,native agent 仍可能继续工作,后半段正文和工具结果却无人读取。
稳定做法:
1. gateway 创建独立 producer task 读取 adapter;
2. producer 在收到 start 后插入一条空 assistant placeholder;
3. 每隔几秒原地覆写已累计正文、thinking 与 tool log;
4. 前端 SSE 只是 producer 的订阅者;
5. 订阅者断开时 producer 继续读到 `turn/completed`;
6. 最终一次原子写回并标记 session resume-safe。
adapter 自己也要有 detached drain:若 gateway 到 adapter 的 HTTP 连接中断,而 `turn/start` 已送达,adapter 继续消费该 thread queue 到 terminal event,再释放 lifecycle lock。这样下一条消息不会与仍在运行的 turn 叠到同一 thread。
## 11. 三引擎自由切换
conversation 创建时固定 `mode`:
```text
api | claude-p | codex
```
已有 conversation 的 mode 不允许被普通 chat 请求偷偷覆盖。若用户明确切换 provider,建议创建新 conversation 或执行显式 migration 流程;两种 native transcript 不能假装拥有同一条隐形历史。
路由骨架见 [examples/provider_router.py](examples/provider_router.py)。三条路径共享:
- conversation/message/attachment 表;
- parent/active-leaf 规则;
- 前端 SSE event contract;
- placeholder 与 detached persistence;
- memory preflight 的接口;
- 历史消息摘要生成器;
- tool UI 与 tool audit schema。
各自保留:
| 能力 | API | Claude Code `-p` | Codex App Server |
|---|---|---|---|
| 上游形态 | HTTP request/stream | CLI process + stream-json | 长期 App Server + JSON-RPC |
| 会话原语 | gateway 重放 messages | session resume/fork | thread start/resume/fork |
| Identity | system/developer message | system prompt file | `baseInstructions` |
| Style | system block | output style/append prompt | `developerInstructions` |
| 工具 | API tool schema + middleware executor | 内建工具 + MCP | App Server items + MCP |
| Context compact | gateway 历史消息摘要/裁剪 | CC 原生 compact + refill | `contextCompaction` + refill |
| 中断 | 取消 HTTP/background request | control request/process signal | `turn/interrupt` |
| 分支 | DB 截断后重放 | 依版本与 session 能力 | `thread/fork` 或 fresh hydration |
| 计费/额度 | token/cost | 订阅侧 usage | 订阅或 API 身份对应 usage |
前端只显示 provider-neutral 名称与能力开关。某 provider 暂不支持的能力应在 capability response 中明确关闭,别靠失败后猜。
### 11.1 CC 与 Codex 的原语对照
| 产品层语义 | Claude Code `-p` 常见原语 | Codex App Server 原语 |
|---|---|---|
| 新会话 | 启动 `claude -p` / 新 session | `thread/start` |
| 冷恢复 | `--resume <session-id>` | `thread/resume {threadId}` |
| 原生分叉 | `--resume ... --fork-session` | `thread/fork {threadId}` |
| 发一轮消息 | 写 stream-json stdin | `turn/start` |
| 中断 | control request / process control | `turn/interrupt {threadId, turnId}` |
| 正文增量 | content block text delta | `item/agentMessage/delta` |
| 思考显示 | provider thinking/summary event | `item/reasoning/summaryTextDelta`;可选 `textDelta` |
| 工具开始 | `tool_use` content block start | tool item 的 `item/started` |
| 工具完成 | replayed tool result/result event | tool item 的 `item/completed` |
| Turn 完成 | result event | `turn/completed` |
| Context 压缩 | CC compact/system event | completed `contextCompaction` item |
| Identity | `--system-prompt-file` 等 | `baseInstructions` |
| Output style | Output Style / append prompt | `developerInstructions` |
| MCP 配置 | JSON/CLI MCP config | `config.toml [mcp_servers.*]` |
这里对齐的是产品行为,字段与事件原样照搬会出错。adapter 负责把两套 native 原语翻译成同一 gateway contract。
## 12. MCP 工具注册如何同步给 CC 与 Codex
Codex 将 MCP 配置放在 `config.toml` 的 `[mcp_servers.<name>]` 下;CLI、桌面端和 IDE 扩展共享该配置。[官方 MCP 文档](https://developers.openai.com/codex/mcp)
要让 CC 与 Codex 的工具注册完全同步,核心是 canonical registry:
```mermaid
flowchart TD
A[App manifest / admin action] --> B[Canonical registry transaction]
B --> C[Validate names, URL, transport, health]
C --> D1[Render CC fragment]
C --> D2[Render Codex config block]
D1 --> E1[CC consumer verify]
D2 --> E2[Codex consumer verify]
E1 --> F[ACK: room + digest + heartbeat + ready]
E2 --> F
F --> G[Commit registry revision]
```
每个 revision 建议包含:
```json
{
"revision": 42,
"digest": "sha256-of-canonical-json",
"servers": {
"demo": {
"transport": "http",
"url_by_room": {
"cc": "http://cc-tools.example:8000/mcp",
"codex": "http://codex-tools.example:8000/mcp"
}
}
}
}
```
“同一工具”指 manifest、schema 与 revision 相同;两个 room 仍各用自己的 hostname/容器实例,避免跨房串流量或身份。
同一 canonical server 可渲染成两种 provider 配置。CC 的最小脱敏 fragment 例如:
```json
{
"mcpServers": {
"memory": {
"type": "http",
"url": "http://memory.example:8000/mcp"
}
}
}
```
启动时以 `claude -p --mcp-config /path/to/generated-mcp.json ...` 加载;registry digest 改变后轮换受影响的 CC session。Codex renderer 则把同一条 canonical record 写成:
```toml
[mcp_servers.memory]
url = "http://memory.example:8000/mcp"
enabled = true
```
| Canonical 字段 | CC renderer | Codex renderer |
|---|---|---|
| stable server name | `mcpServers.<name>` | `[mcp_servers.<name>]` |
| room-local URL | `url` | `url` |
| stdio command/args | `command` / `args` | `command` / `args` |
| secret header/token | 版本支持的 env/header 配置 | `bearer_token_env_var` / `env_http_headers` |
| enabled/required/timeout | 按 CC 目标版本渲染或由 wrapper 强制 | Codex config 对应键 |
| registry digest | session runtime fingerprint | App Server transition/consumer ACK |
两边原生配置格式会变化,canonical registry 只存 provider-neutral 意图;renderer 和验证器随目标版本更新。
### 12.1 consumer 应完成的验证
1. fragment 名称与 app id 合法;
2. fragment 声明的 room 与当前 consumer 一致;
3. URL hostname 只能属于当前 room allowlist;
4. TCP/HTTP 可达;
5. 原生 CLI 能列出预期 MCP server;
6. 生成配置使用原子替换;
7. 写入带 heartbeat 的 ACK,禁止伪造静态 `ready=true`。
### 12.2 Codex 动态变更为何常要轮换
修改 `config.toml` 并不保证运行中的 App Server、已有 thread、代理 `NO_PROXY` 和 MCP client 都立刻刷新。保守流程如下:
```text
关闭新 turn 入口
→ 等待/排空 active turns
→ 写入候选配置
→ 用 codex mcp list 或 schema-aware client 验证
→ 轮换逻辑 sessions
→ 重启 App Server
→ 等待 initialize + MCP ready
→ 写 ACK
→ 重新开放 turn 入口
```
任一步失败都保留 `ready=false` 与明确错误,并恢复上一 committed revision。别让聊天继续进入一个工具清单半新半旧的进程。
### 12.3 legacy SSE MCP
一些旧 MCP server 只实现 legacy SSE,Codex 侧常用 streamable HTTP。可放一个窄 bridge:
- 对 Codex 暴露 loopback streamable-HTTP endpoint;
- bridge 连接旧 SSE endpoint;
- session id、消息方向、超时和关闭语义逐项转译;
- 限制目标 allowlist,禁止把它做成任意 URL proxy。
若上游已支持当前 MCP transport,直接升级上游更省心。
## 13. 可选:路由中转与进程隔离
Codex App Server 不要求 Docker。若机器本身拥有合适且稳定的网络出口,可以直接以本地进程、`systemd` 或 `launchd` 服务运行 adapter 与 App Server,只需持久化 `CODEX_HOME`、限制工作目录权限并做好进程重启。
我们的部署位于远程 VPS,需要把 Codex 的登录、模型请求和 MCP 流量经过指定路由中转,同时禁止意外回落到 VPS 默认出口,所以增加了独立 gateway、隔离网络和 fail-closed probe。Docker 只是实现这一边界的一种方式;VM、network namespace 或 Kubernetes 也能完成同样目标。没有路由中转需求时,可以跳过本节。
### 13.1 Docker 路由中转示例
以下是可解析结构的脱敏片段。外部 bridge 名、代理端口和内部 service hostname 需按你的环境替换;不使用代理时删除 `HTTP_PROXY`、`HTTPS_PROXY` 和相关 gateway 配置:
```yaml
services:
codex-seat:
image: your-codex-seat:PINNED
command: ["python3", "/opt/example-adapter/codex_app_server_adapter.py"]
restart: unless-stopped
working_dir: /srv/example-codex/workspace
environment:
HOME: /srv/example-codex/home
CODEX_HOME: /srv/example-codex/config
CODEX_BIN: /opt/example-codex/bin/codex
HTTP_PROXY: http://egress.example:7890
HTTPS_PROXY: http://egress.example:7890
NO_PROXY: 127.0.0.1,localhost,egress.example,tools.example,memory.example
volumes:
- example_codex_home:/srv/example-codex/home
- example_codex_config:/srv/example-codex/config
- example_workspace:/srv/example-codex/workspace
cap_drop: ["ALL"]
security_opt: ["no-new-privileges:true"]
networks: [agent_internal, gateway_bridge, memory_bridge]
networks:
agent_internal:
internal: true
gateway_bridge:
external: true
name: REPLACE_WITH_GATEWAY_BRIDGE
memory_bridge:
external: true
name: REPLACE_WITH_MEMORY_BRIDGE
volumes:
example_codex_home: {}
example_codex_config: {}
example_workspace: {}
```
### 13.2 使用路由中转时的检查项
- Codex 进程没有绕过中转的默认外网回退;
- DNS、IPv4、IPv6 从进程所在的隔离边界归因;
- gateway health 只证明代理可用,额外 probe 还要证明 direct bypass 失败;
- 内部 MCP、gateway 和记忆服务地址加入 `NO_PROXY` 或等价 bypass;
- Docker 方案避免使用 host network,并将 auth/config/workspace 分卷;
- health 同时检查 App Server、认证状态和 MCP consumer freshness;
- 网络/代理变化先做只读检查和回滚设计。
## 14. 上下文与记忆系统只需要一个通用接口
每个人的记忆库、历史消息摘要、检索和归档设计都不同,Codex adapter 无需理解内部 bucket、embedding、画像或评分算法。它只消费一段带预算的 context,或把记忆服务作为 MCP 挂给 Codex。
Codex 侧的处理顺序可以固定为:
```text
fresh / 无法安全恢复 → full refill:带硬预算的 recovery context
cold resume / fork → light refill:历史消息摘要和最近缺口
warm → 不重复补历史
contextCompaction 完成 → 下一轮加入一次 post-compact light refill
普通记忆检索 → gateway preflight 注入,或由 Codex 主动调用 memory MCP
```
记忆接口只需提供 `recall(query, budget)`、可选的异步 `remember(turn)` 和 `health()`。返回内容应有字符预算、来源标记、超时和失败降级。
## 15. 流式 Markdown 前端
SSE adapter 只负责提供增量文本;SwiftUI 端可以用 [Microsoft SwiftStreamingMarkdown](https://github.com/microsoft/SwiftStreamingMarkdown) 渲染逐步增长的完整 Markdown snapshot。它采用 MIT License。
一个常见实现是:
```text
delta 到达
→ append 到 message buffer
→ 合并为“目前为止的完整 Markdown”
→ 推送给 StreamedMarkdownSource
→ tool/thinking 使用独立 UI block,避免塞进 Markdown 正文
```
不要每个字符都触发数据库写入。UI 可以高频刷新,gateway 持久化按时间窗口节流,并在 terminal event 强制 final flush。
## 16. 测试矩阵
fake App Server 可以覆盖绝大多数 adapter 状态机,真实 Codex E2E 负责证明协议 fixture 没有空转。
### 16.1 单元/协议测试
- initialize 之前拒绝 thread 请求;
- fresh 返回新 thread 并绑定;
- resume 必须返回目标 thread;
- fork 必须返回新 thread;
- thread ownership 唯一;
- agent/reasoning/tool delta 与 completed fallback;
- usage、rate limit、auth required;
- server request resolve 校验 conversation ownership;
- interrupt;
- 客户端断线后 detached drain;
- ghost turn 与迟到 terminal event 不得串进新 turn;
- App Server epoch 改变后 warm session 自动转 cold resume;
- compact pending 在消息被接受后清除,新 compact boundary 仍能留下;
- dynamic MCP config 写入失败时维持旧 committed revision。
### 16.2 真实 E2E
1. fresh 对话,验证 persona/style 与普通 streaming;
2. 调一个只读 MCP 工具,核对 start/result/UI/落库;
3. 重启 App Server 后 cold resume;
4. 相同 fingerprint 的 warm turn 不执行 refill;
5. 修改 effort/style 后轮换且首轮生效;
6. fork 后 child id 与 source 不同;
7. 触发或模拟 compact,下一轮只吃 light refill;
8. 前端中途断开,后台仍完整落库;
9. registry apply → 新工具可用 → down → 工具消失;
10. 若使用路由中转,从 Codex 进程所在隔离边界验证出口、DNS 与旁路策略。
## 17. 踩坑
### 1:每轮启动一个 App Server
能回字,长期会话、MCP 初始化、认证诊断和并发管理会变得很难看。优先维持一个 App Server runtime,再在其上管理逻辑 thread。
### 2:按 conversation id 猜 thread id
thread id 由 Codex 返回。数据库显式绑定,adapter 验证唯一 ownership。
### 3:请求发送后发生网络错误就自动重试
这段状态可能已经被模型接受。先标记 ambiguous/unsafe,恢复并查询 terminal state,确认安全后再决定。
### 4:把 delta 当最终事实
delta 可缺失、重复或在断线时丢一截。`item/completed` 和 `turn/completed` 才是完成态权威来源。
### 5:MCP 配置文件改了,运行态仍旧
同时考虑 App Server 进程、thread、环境变量、代理 bypass 与 consumer ACK。把变更做成带锁、可回滚的 transition。
### 6:把 API context assembly 原封不动塞进 native agent 每一轮
native thread 已持有历史。每轮重复整段 summary/persona 会浪费上下文并强化旧信息。使用 fresh/full、resume/light、warm/none、post-compact/light 的状态机。
### 7:升级 Codex 时不核对 schema
App Server 协议仍在演进。升级前先 diff 新旧 schema,再用保存的 JSONL fixture 重放 start、resume、fork、tool、compact 和 error 路径;字段变化应先在 adapter 兼容层消化。
## 18. 推荐的实现顺序
1. 冻结 gateway SSE 与 persistence contract。
2. 写 fake App Server,先完成 initialize/thread/turn 最小闭环。
3. 接 agent text delta 与 terminal completion。
4. 加 thread ownership、resume-safe 与 lifecycle lock。
5. 加 reasoning、tool、usage、rate limit、interrupt。
6. 加 detached drain 与 gateway background producer。
7. 加 persona/style/runtime fingerprint。
8. 加 fresh/resume/fork/compact refill。
9. 接静态 MCP,再做 canonical registry 和动态 transition。
10. 最后处理网络隔离、真实 E2E、可重复验收与回滚。
## 19. 官方资料
- [Codex App Server](https://developers.openai.com/codex/app-server)
- [Codex SDK](https://developers.openai.com/codex/sdk)
- [Codex MCP](https://developers.openai.com/codex/mcp)
- [Codex authentication](https://developers.openai.com/codex/auth)
- [Codex configuration reference](https://developers.openai.com/codex/config-reference)
- [Codex hooks](https://developers.openai.com/codex/hooks)
- [OpenAI Codex source](https://github.com/openai/codex)
## License
本仓库的原创文档、图表与示例代码采用 [MIT License](LICENSE)。
文中涉及的第三方项目及依赖保留各自许可证,详见 [ATTRIBUTION.md](ATTRIBUTION.md)。
连接信息
猜你喜欢
暂无推荐服务器