Files
codex-proxy-rs/deploy/README.md
T
ZyyandCodex c21633655e fix(api): keep SSE responses alive during upstream silence
Send SSE keepalive comments after 15 seconds without output while preserving
the pending execution future. Disable response buffering and transformation
so long compaction requests can maintain the downstream connection.

Cover four minutes of upstream silence, cancellation, first-event commitment,
and conflicting upstream headers. Document transport errors and the separate
Codex event idle timeout.

Validation: 259 gateway-api tests and Clippy passed with serde/rc enabled;
cargo fmt and git diff --check passed.

Co-authored-by: Codex <noreply@openai.com>
2026-09-06 16:49:35 +08:00

19 KiB
Raw Blame History

部署与运维

首次安装可按 快速开始 操作。 本文补充客户端配置、权限、备份和升级;命令从仓库根目录执行。

需要维护的配置文件:

  • config.yaml:应用行为与真实凭据,由 config.example.yaml 复制得到并被 Git 忽略。
  • compose.yaml:镜像、容器网络、端口、目录映射、健康检查和资源限制。

项目不使用 .env 配置文件。Compose 环境变量用于容器地址、镜像选择和构建发布; 应用设置与凭据保存在 config.yaml 中。已有部署不要重新复制模板覆盖配置。

准备

从仓库根目录执行:

mkdir -p .runtime/data .runtime/logs
install -d -m 0750 .runtime/postgres .runtime/redis
cp deploy/config.example.yaml deploy/config.yaml
sudo chown "$(id -u):10001" deploy/config.yaml
chmod 0640 deploy/config.yaml

为 PostgreSQL 与 Redis 分别生成一个密码:

openssl rand -hex 24
openssl rand -hex 24

把两个 48 位十六进制结果分别写入 deploy/config.yaml 的:

  • store.database.password
  • store.redis.password

另行设置 admin.default_password。它至少需要 12 个字符,不能是常见弱口令,也不能包含 $。

PostgreSQL 与 Redis 密码必须是 48 位十六进制字符。Compose 通过 config.yaml 的凭据桥接区 引用同一密码;三个值都不需要额外导出为环境变量,数据库和 Redis 密码也不能嵌入连接 URL。

Linux 上应用容器以 10001:10001 运行,需要允许该组写入应用数据和日志目录:

sudo chown -R "$(id -u):10001" .runtime/data .runtime/logs
chmod 0770 .runtime/data .runtime/logs

config.yaml 通过 Compose configs 只读挂载。普通 Compose 对本地文件保留宿主机的 UID/GID 和 mode,因此配置由当前用户持有,并只向容器组 10001 开放读取权限。

模板中的 openai / xai 只保留请求画像启动基线。OpenAI 的上游地址、WebSocket 池、额度刷新 与 OAuth 设置,以及 xAI 的 OAuth、额度和模型目录策略,均由各自 Provider 使用代码内默认值管理; 模板不重复列出这些默认项。运行后,Provider 检查官方版本并更新运行时请求画像, 不回写 config.yaml;检查失败时继续使用上一份有效画像。版本检查不等于重新核验 TLS。

启动

docker compose -f deploy/compose.yaml config --quiet
docker compose -f deploy/compose.yaml pull
docker compose -f deploy/compose.yaml up -d --no-build
docker compose -f deploy/compose.yaml ps

健康检查:

curl -i http://127.0.0.1:8080/healthz

204 No Content 表示应用、PostgreSQL、Redis 和后台任务的健康检查通过,不代表每个上游账号都可用。

不要把未脱敏的 docker compose config 或 docker inspect 输出上传到工单;它们会包含 PostgreSQL/Redis 启动密码。日常校验使用 config --quiet。

公网访问

Compose 默认只绑定 127.0.0.1。从其他设备访问时,在应用前配置 HTTPS 反向代理, 不要把 PostgreSQL 或 Redis 暴露到公网。

反向代理需要保留 Authorization,支持 /v1/responses 的 WebSocket Upgrade, 并关闭 SSE 响应缓冲。读取超时应覆盖长时间生成任务。 客户端使用 https://你的域名/v1,不要使用前端开发服务的 5173/dev/v1。 当前应用只支持单副本,不能通过复制容器扩容。

流式响应在首个上游事件提交后,每 15 秒无输出会发送一次 SSE 注释保活, 并设置 X-Accel-Buffering: no 和 Cache-Control: no-cache, no-transform。 反向代理仍需允许这些响应头生效;首个事件到达前的等待也需要足够的读取超时。

若 Codex 在压缩或长时间生成时出现 error decoding response body,这表示 客户端读取 HTTP 响应体失败。请结合网关请求诊断中的 upstream.read.failed、 downstream.body.closed 和反向代理日志判断断开位置,不能仅凭此消息认定是 JSON 格式错误。 SSE 注释保活用于防止传输链路空闲断开,不会重置 Codex 等待完整 SSE 事件的超时; 若报错为 idle timeout waiting for SSE,再检查客户端的 stream_idle_timeout_ms。

客户端配置

在管理端创建客户端密钥,打开「使用密钥」,按操作系统复制 config.toml 和 auth.json, 或通过 CCSwitch 导入。已有文件先备份,合并后完全退出并重启 Codex。

Linux/macOS 默认目录为 ~/.codex/,Windows 为 %USERPROFILE%\.codex\; 设置过 CODEX_HOME 时以该目录为准。Provider 设置应写入用户配置,不要只写到项目目录。 配置层级见 官方配置参考。

config.toml

以下示例与当前管理端模板的配置项一致。替换地址和密钥,模型可改成该密钥有权限使用的模型:

model_provider = "OpenAI"
model = "gpt-5.6-terra"
review_model = "gpt-5.6-terra"
model_reasoning_effort = "max"
service_tier = "default"

[model_providers.OpenAI]
name = "OpenAI"
base_url = "http://127.0.0.1:8080/v1"
wire_api = "responses"
supports_websockets = false
requires_openai_auth = false
# 填写代理密钥,真实账号由服务端管理。
experimental_bearer_token = "<client-api-key>"

[model_providers.OpenAI.http_headers]
# 供客户端识别服务端托管认证,本身不是密钥。
X-OpenAI-Actor-Authorization = "proxy-managed"

[features]
image_generation = true
goals = true

OpenAI 是自定义 Provider ID,大小写要与 model_provider 一致。合并配置时修改已有表, 不要重复添加 [features] 或 Provider 表。更换模型时也要检查其支持的推理强度。 密钥以明文保存,文件仅供本人读取,不要提交到 Git。

auth.json

{
  "OPENAI_API_KEY": "<client-api-key>"
}

这份文件可以保留,用于兼容已有客户端和 CCSwitch。新配置从 experimental_bearer_token 读取代理密钥,不会因为仅含 API Key 的 auth.json 存在而关闭生图。 真实 OpenAI OAuth 账号文件用于管理端账号导入,不要当作代理配置分发给客户端。

生图和 WebSocket

新模板已启用原生生图。Codex 可在任务需要图片时调用 image_gen.imagegen, 再通过代理的 Images 接口生成或编辑图片;也可以在需求中明确要求生成并使用图片。 需要支持该能力的客户端、支持图片输入的对话模型,以及有生图权限和额度的 OpenAI 账号。 代理不会增加上游权限,xAI 账号不能承接这些 Images 请求。

本项目已用 Codex CLI 0.153.4 验证过原生生图,这不是最低支持版本声明。 生图不要求开启 WebSocket。要启用客户端 WebSocket,把当前 Provider 的 supports_websockets 改为 true,并检查反向代理是否允许 Upgrade。

客户端到代理、代理到上游是两段独立连接。客户端关闭 WebSocket 后, 服务端仍可能用 WebSocket 访问上游;客户端开关不控制服务端连接池和 HTTP 回退策略。

旧版配置

仅含代理密钥的 auth.json 配合 requires_openai_auth = true 仍可用于已有请求, 但 API Key 登录本身不会启用原生生图。需要生图时换用上述 Provider 配置。

以已核验的 Codex 0.153.4 为准,管理端已移除以下旧字段:

字段 处理
顶层 disable_response_storage 无有效配置定义,删除
顶层 network_access = "enabled" 无有效配置定义,删除;不是客户端 API 的联网开关
features.responses_websockets_v2 已标记移除,使用 Provider 的 supports_websockets

image_generation 和 goals 仍有效。不要为了接入代理,顺带扩大命令沙箱的联网或文件权限。

登录与生图排查

  • 仍提示登录:确认实际读取的用户配置目录、选中的 Provider 和客户端版本,再完全退出重启。 新 Provider 使用 requires_openai_auth = false,不依赖本地 ChatGPT 登录。
  • 没有生图工具:检查配置是否被覆盖、Actor 标记是否保留、模型是否支持图片输入。 官方客户端还会检查缓存登录状态;已核验版本在本地账号为 Free 时会隐藏生图。 先备份并区分本地真实账号文件与代理密钥文件,不要直接删除全部登录状态。
  • 已调用生图但失败:查看服务端账号的凭据、权限、额度和请求错误,不能只凭文本对话成功判断。
  • 401:检查代理密钥是否正确、是否启用;Actor 标记不能代替密钥。
  • 426:客户端低于管理员设置的最低版本,或版本无法识别,需要更新客户端。
  • 地址包含 5173/dev/v1:这是开发代理地址,依赖 Vite 服务。日常使用改成后端或 HTTPS 地址; 验证 WebSocket 时直接连接后端,当前 Vite 代理未显式开启 WebSocket 转发。

其他客户端使用 Responses API、/v1 Base URL 和代理密钥即可,不需要 Actor 标记。 路由与请求格式见 API 参考。

优雅关停

收到停止信号后,应用先停止接收新连接并 drain 存量连接;整个 drain 共享一个从停止信号 起算的绝对截止点(host.drain_timeout_seconds,默认 30 秒),逾期放弃等待,存量连接随 进程退出终止。drain 结束后才关停后台 worker,预算为 host.worker_shutdown_timeout_seconds(默认 30 秒),两段预算按最坏情况串联。

Compose 的 stop_grace_period 为 75 秒,覆盖默认 30 秒 HTTP drain、30 秒 worker 收尾和额外调度 余量。若调大任一应用超时,也必须把 stop_grace_period 调到大于两段超时之和;否则 Docker 会在 宽限期结束时 SIGKILL。

本地开发

本地 PostgreSQL 和 Redis 可继续由 Compose 启动:

docker compose -f deploy/compose.yaml up -d postgres redis
cd backend
cargo run -p codex-proxy-rs

后端会从当前目录向上查找 deploy/config.yaml。host.runtime_data_dir 是运行数据的统一根目录, 相对数据和日志目录均以该配置文件所在目录解析;Compose 把监听地址和数据库、Redis 地址固定 覆盖为容器内部服务名,并把前端静态目录指向容器内构建产物。PG/Redis 集成测试所需的 CPR_TEST_DATABASE_URL / CPR_TEST_REDIS_URL 约定见 迁移文档。

持久化与备份

Compose 使用以下绑定目录:

  • .runtime/data → OpenAI 会话锚点密钥、更新状态、临时更新目录与备份暂存区
  • .runtime/logs → 应用文件日志
  • .runtime/postgres → PostgreSQL
  • .runtime/redis → Redis AOF

普通 docker compose down 不会删除这些目录。删除 .runtime 会永久清除本地状态。

PostgreSQL 是账号、Client Key、运行设置、请求记录与审计的权威存储;账号 credential 当前按 Provider schema 以明文 JSON 保存于 PostgreSQL。Redis 只保存可重建、可过期的协调状态,例如 会话亲和、lease、cooldown、OAuth pending flow 与套餐模型目录 cache。

OpenAI 主动额度重置卡及其消费结果由上游持有,不写入 PostgreSQL/Redis,也不属于本地备份内容; 管理端只在当前浏览器会话中保留最近一次查询结果。

数据库可用管理端的 S3/R2 逻辑备份,或停库后备份 .runtime/postgres。 不要在 PostgreSQL 写入期间直接复制数据目录作为一致性备份。要保留 OAuth 的 AT/RT 恢复记录,需保持 host.logging.file.enabled: true 并备份 .runtime/logs;恢复记录位于独立文件集,仍按普通日志的 retention_days 和 max_files 分别约束。若希望保留短期 Redis 状态和会话锚点,也同时备份 .runtime/redis 与 .runtime/data。

完整运行时、Provider、revision 与恢复边界见 架构文档。

密码语义

  • admin.default_password 只在首次创建管理员时使用。
  • PostgreSQL 官方镜像只在空数据目录初始化时使用 database.password。
  • Redis 在每次容器创建时使用 redis.password。

已有 PostgreSQL 数据目录后,直接修改 database.password 不会修改数据库用户密码,只会导致 应用无法连接。轮换时必须先在 PostgreSQL 中修改用户密码,再同步更新 config.yaml。Redis 密码变更后需要用新配置重新创建 Redis 和应用容器,不需要删除 Redis 数据目录。 安排维护窗口,避免应用和 Redis 在过渡期间使用不同密码。

镜像升级与源码构建

Warning

以下命令只适用于同一大版本内的升级,不支持跨大版本在线升级。跨大版本请使用全新的 .runtime/ 数据目录重新部署,并重新导入或重新授权 Provider 账号与客户端 Key。

docker compose -f deploy/compose.yaml build codex-proxy-rs
docker compose -f deploy/compose.yaml up -d

拉取发布镜像:

docker compose -f deploy/compose.yaml pull codex-proxy-rs
docker compose -f deploy/compose.yaml up -d --no-build

仓库维护者发布新版本时必须从干净且已同步上游的分支运行:

release/publish X.Y.Z

将 X.Y.Z 替换为待发布版本。脚本需要 Git、Ruby 和已登录的 GitHub CLI, 负责更新 release/version.yaml、创建版本提交和带注释 tag,并原子推送分支与 tag。 它不会登录任何服务器,也不会拉取镜像或调用管理端在线更新。

源码提交、仓库发版和运行实例升级是三种独立状态:本地 commit 不等于 Release,Release/tag 和镜像 已生成也不等于实例已升级。判断某项修复是否在线前,应先通过管理端版本接口或容器 image digest 确认运行实例的实际 revision;实例只有在执行上面的 Compose pull/up,或成功完成管理端在线更新后 才会改变。

构建元数据仍可作为一次性进程环境传入,不需要 .env 文件:

CPR_VERSION="$(ruby -ryaml -e 'puts YAML.load_file("release/version.yaml").fetch("version").delete_prefix("v")')" \
CPR_GIT_SHA="$(git rev-parse HEAD)" \
CPR_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
docker compose -f deploy/compose.yaml build codex-proxy-rs

管理端在线更新

Compose 已显式装配正式发布构建所需的运行参数:

  • CPR_UPDATE_REPOSITORY:只接受 owner/repository;默认 zyycn/codex-proxy-rs。
  • CPR_GITHUB_API_BASE:正式环境必须为 https://api.github.com/repos。
  • CPR_UPDATE_CHANNEL:stable 会拒绝 prerelease。
  • CPR_UPDATE_EXE_PATH、CPR_WEB_DIST_DIR:分别指向容器内二进制和前端静态目录。
  • 更新临时目录、状态文件和锁文件默认由 host.runtime_data_dir 派生; CPR_UPDATE_TEMP_DIR、CPR_UPDATE_STATE_FILE、CPR_UPDATE_LOCK_FILE 仅用于显式覆盖。
  • CPR_ENABLE_SELF_RESTART=true:更新或回滚完成后允许管理端请求重启;Docker 进程退出后由 Compose 的 restart: unless-stopped 拉起新进程。

Release 必须提供当前 OS/架构的 codex-proxy-rs_<version>_<os>_<arch>.tar.gz 与 checksums.txt。服务会在替换前再次查询远端最新版本,校验下载 host、声明大小、SHA-256 和 归档路径;二进制或静态资源任一替换失败时恢复旧文件。成功后的旧二进制和旧静态目录分别保留为 *.backup,管理端 rollback 会交换当前文件与这份备份。更新状态和跨进程锁可在以下位置排查:

.runtime/data/update-state.json
.runtime/data/update.lock
.runtime/data/update-tmp/

备份与恢复

备份内容与限制

  • 运行时镜像的 pg_dump/pg_restore 在构建期从与 compose 服务端同一固定 postgres:18-bookworm 镜像(Dockerfile 的 POSTGRES_IMAGE)COPY 到 /usr/lib/postgresql/18/bin 并加入容器 PATH,版本与服务端严格一致;运行时只从 Debian 官方源补 libpq5 依赖,不引入第三方 APT 源,也不依赖运行时动态安装。
  • 备份暂存目录为 host.runtime_data_dir/backup-staging;Compose 默认对应 /app/.runtime/data/backup-staging,由 .runtime/data 卷持久化,权限 0700(仅 cpr 用户可读写)。部署卷至少预留一个最大数据库归档的空间。
  • OAuth 恢复记录通道使用 oauth_recovery 结构化日志事件;当前 OpenAI Provider 每次成功取得 AT(以及存在时的 RT)后,都会在账号资料补全、过期时间计算和数据库写入前写入独立的 codex-proxy-rs-oath.YYYY-MM-DD[.N].log 文件集。事件含原始 AT/RT,并以 provider 字段标记来源,且与普通 .runtime/logs 完全相同地按日、按大小分割,并分别按相同 retention_days 与 max_files 清理;文件日志开启时不会被普通日志级别筛掉。它遵循普通日志现有的非阻塞写入机制,不会阻断 导入、授权或刷新。 .runtime/logs 因此属于敏感数据,必须按现有普通日志的访问控制和加密备份策略处理。
  • S3/R2 存储、Cron 计划、保留策略与备份记录都保存在 PostgreSQL(backup_settings / backup_records),备份记录行在删除成功后硬删除,操作历史进入 admin_audit_events。
  • 手工备份的 expiresInDays 在创建时生成独立 expires_at;计划备份按当前 retentionDays 生成 expires_at,并同时受 retentionCount 清理规则约束。到期只进入删除流程,不构成在线恢复点。

人工恢复数据库

备份归档由 pg_dump --format=custom --no-owner --no-privileges 生成,可通过标准 PostgreSQL 工具离线恢复:

pg_restore --no-owner --no-privileges --password \
  --dbname='postgresql://restore_user@127.0.0.1:5432/restore_db' backup.dump

示例中的账号、空目标库和归档路径需按实际情况替换,密码由命令交互输入。

恢复期间与恢复后的边界见 架构文档。 当前没有在线恢复 API,也没有供部署者直接启用的“维护模式”开关:

  1. 先备份现有数据库,并停止应用;在独立的空数据库中恢复归档,检查数据和迁移版本。
  2. 应用保持停止,离线核对备份设置与记录。禁用快照中的旧计划,处理非终态记录 (queued/dumping/uploading/deleting)、旧调度游标和到期清理条件,再决定哪些记录保留。
  3. 确认不会误执行旧任务或删除恢复前的远端对象后,再启动应用;重新测试 S3 连接并设置计划。

只关闭计划备份不会停止 Worker 的任务恢复和到期删除。无法确认这些记录的影响时, 不要把恢复后的数据库直接接入运行中的应用。具体处理应根据目标库数据制定,不提供清空生产记录的通用命令。