# 部署与运维 首次安装可按 [快速开始](../README.md#快速开始) 操作。 本文补充客户端配置、权限、备份和升级;部署命令从安装目录 `codex-proxy-rs/` 执行, 其中 `deploy/` 存放 Compose 文件和配置,`.runtime/` 存放持久化数据 | 任务 | 入口 | | --- | --- | | 安装与启动 | [手动安装](#手动安装) · [启动](#启动) · [公网访问](#公网访问) | | 接入客户端 | [客户端配置](#客户端配置) · [时区与客户端环境信息](#时区与客户端环境信息) · [登录与生图排查](#登录与生图排查) | | 日常运维 | [运行设置](#启动后的运行设置) · [小内存优化](#小内存优化) · [持久化与日志](#持久化与备份) · [请求排查](#请求错误排查) · [密码轮换](#密码语义) | | 更新与恢复 | [镜像升级](#镜像升级与源码构建) · [在线更新](#管理端在线更新) · [备份与恢复](#备份与恢复) · [优雅关停](#优雅关停) | | 运行源码 | [开发与源码联调](../docs/development.md) | ## 配置归属 配置按职责保存,不都在 YAML 中: | 位置 | 内容 | 修改方式 | | --- | --- | --- | | `deploy/config.yaml` | 监听、数据库/Redis 连接、部署凭据、时区、日志与可选内存分配策略等启动配置 | 从模板创建,已有部署合并修改,不覆盖原文件 | | `deploy/compose.yaml` | 镜像、容器网络、端口、挂载与资源限制 | 调整 Compose 并重建受影响容器 | | PostgreSQL | 账号、Key、运行设置、上游身份、插件配置等业务数据 | 管理端或管理 API | | 浏览器本地 | 主题和界面偏好 | 当前浏览器的主题设置 | 项目不使用 `.env` 配置文件。Compose 环境变量用于容器地址、镜像选择和构建发布; 可选的内存分配策略由 `config.yaml` 的 `services.app-runtime` 桥接到应用容器。 配置加载会忽略未知字段,并在启动控制台提示字段名;不输出对应值。缺少必填字段时会指出缺项并停止启动, 已知字段的类型和取值仍需合法;可选字段省略时使用默认值 后端从当前目录向上查找 `deploy/config.yaml`,相对数据、日志和静态资源路径以该文件所在目录解析 Compose 通过环境变量将监听和数据库地址设为容器内地址,并指定镜像中的静态资源目录 ### 部署时区 在 `deploy/config.yaml` 设置统一的 IANA 时区,省略时默认 `Asia/Shanghai`: ```yaml host: timezone: 'Asia/Shanghai' ``` 支持 `UTC`、`America/New_York`、`Asia/Kathmandu` 等名称;空值、未知名称和错误类型会阻止启动, 错误指出 `host.timezone`,不输出配置值。修改后重启网关生效,二进制与 Compose 使用同一配置 管理端与 Key 用量页的时间文本由后端生成;自然日筛选、日统计、Key 限额、账号预热和备份 Cron 使用该时区。 浏览器、进程 `TZ` 和 PostgreSQL 会话时区不覆盖业务配置;Compose 的基础设施时间基准为 UTC。 上游请求身份中的位置时区仍由对应运行设置或代理配置控制,与部署时区独立 切换时区保留已打开限额窗口的 UTC 边界与用量,到期后按新时区续接;不会因重启立即清零。 备份调度从切换后的当前时刻计算未来执行点,不补跑旧时区漏过的任务,已入队任务保持原状态。 夏令时中不存在的预热或 Cron 时刻跳过,重复时刻只选较早一次。日志日期与保留规则见[日志与保留窗口](#日志与保留窗口) ### 小内存优化 Linux/glibc 部署可选择更积极地归还请求结束后的空闲内存,适合内存紧张且请求后 RSS 长时间偏高的机器。 该选项默认关闭,官方 Linux 镜像支持;更频繁的映射与回收可能增加 CPU 开销,应结合实际负载观察 在 `deploy/config.yaml` 的 `services.app-runtime.environment` 中设置 `GLIBC_TUNABLES`,空字符串表示不启用额外调优: ```yaml services: app-runtime: environment: GLIBC_TUNABLES: 'glibc.malloc.mmap_threshold=131072:glibc.malloc.trim_threshold=131072' ``` Compose 从 `services.app-runtime` 取得参数;不开启优化时保留该段并将值设为 `''`。 PostgreSQL 和 Redis 的凭据桥接配置与该段并列,无需在 `compose.yaml` 重复填写参数 校验并重建应用容器使配置生效;`docker compose restart` 不会更新容器环境变量: ```bash docker compose -f deploy/compose.yaml config --quiet docker compose -f deploy/compose.yaml up -d --no-build --no-deps --force-recreate codex-proxy-rs ``` 此配置将 glibc 的 mmap 与 trim 阈值固定为 128 KiB,关闭对应的动态阈值调整。 如已有 `GLIBC_TUNABLES`,用冒号合并这两个参数,避免重复同名项;同时检查是否另设了 `MALLOC_MMAP_THRESHOLD_` 或 `MALLOC_TRIM_THRESHOLD_`,统一在一处维护。 参数含义见 [glibc 文档](https://sourceware.org/glibc/manual/latest/html_node/Memory-Allocation-Tunables.html) 关闭时删除这两个参数;没有其他 tunable 时将值恢复为 `''`,然后再次重建应用容器。 `services` 是 Compose 桥接区,直接运行二进制不会读取这一段。Linux/glibc 二进制部署需由启动器在进程启动前设置同一环境变量,例如: ```bash GLIBC_TUNABLES=glibc.malloc.mmap_threshold=131072:glibc.malloc.trim_threshold=131072 ./codex-proxy-rs ``` 该变量由 glibc 在进程启动时读取,不支持管理端热更新。 它不设置内存上限,不释放仍被请求或连接持有的缓冲,也不处理容器文件缓存;观察时分别比较进程 RSS、cgroup 的 `anon` 与 `file` ## 部署结构 ```mermaid flowchart LR Client[客户端 / 管理浏览器] -->|HTTPS| Proxy[反向代理] Proxy -->|HTTP / SSE / WebSocket| App[网关:单副本] App --> PG[(PostgreSQL)] App --> Redis[(Redis)] App --> Upstream[上游服务] App -. 数据库备份 .-> Backup[S3 / R2] ``` 网关端口默认只绑定本机;数据库和 Redis 不对公网开放。插件以网关同一系统身份运行,不能作为不可信代码沙箱 ## 手动安装 本节适用于 Linux amd64/arm64,需准备 Docker Engine、Docker Compose Plugin、curl 和 OpenSSL, 并确保当前用户能访问 Docker、可通过 `sudo` 或 root 设置目录权限。一键安装入口见 [快速开始](../README.md#一键安装),脚本会自动完成本节准备和服务启动 下载同一正式 Release 的部署文件并设置目录权限: ```bash mkdir -p codex-proxy-rs/deploy && cd codex-proxy-rs # 只解析一次最新正式版本,确保两个文件来自同一 Release。 CPR_RELEASE_URL="$(curl -fsSL -o /dev/null -w '%{url_effective}' https://github.com/zyycn/codex-proxy-rs/releases/latest)" CPR_RELEASE_TAG="${CPR_RELEASE_URL##*/}" curl -fsSL "https://github.com/zyycn/codex-proxy-rs/releases/download/${CPR_RELEASE_TAG}/compose.yaml" \ -o deploy/compose.yaml curl -fsSL "https://github.com/zyycn/codex-proxy-rs/releases/download/${CPR_RELEASE_TAG}/config.example.yaml" \ -o deploy/config.example.yaml install -d -m 0750 .runtime/postgres .runtime/redis sudo install -d -m 0770 -o "$(id -u)" -g 10001 .runtime/data .runtime/logs sudo install -m 0640 -o "$(id -u)" -g 10001 deploy/config.example.yaml deploy/config.yaml ``` 也可将 `CPR_RELEASE_TAG` 设置为指定的发布标签。Release 附带的 `compose.yaml` 默认使用该版本镜像, 配置模板和部署文件均包含在 `checksums.txt` 中;不要混用 `main` 分支模板与已发布镜像 为 PostgreSQL 与 Redis 分别生成一个密码: ```bash openssl rand -hex 24 openssl rand -hex 24 ``` 把两个 48 位十六进制结果分别写入 `deploy/config.yaml` 的: - `store.database.password` - `store.redis.password` 另行设置 `admin.default_password`:至少 12 个字符,不能使用常见弱口令或包含 `$` | 会话配置 | 含义 | 配置模板 / 默认值 | | --- | --- | --- | | `admin.session_ttl_minutes` | 管理员不活动期限 | 模板为 10080 分钟(7 天) | | `admin.session_absolute_ttl_minutes` | 从登录起计算的最长有效期 | 模板及省略时均为 43200 分钟(30 天) | | `client.session_ttl_minutes` | 密钥身份固定有效期,不随活动续期 | 默认 1440 分钟(1 天) | 修改配置需重启,不能延长已建立会话的最长有效期。浏览器会话与续期规则见[认证 API](../docs/api.md#统一登录与会话),独立于 `/v1/*` 鉴权和限额 PostgreSQL 与 Redis 密码必须是 48 位十六进制字符。Compose 通过 `config.yaml` 的凭据桥接区 引用同一密码;三个值都不需要额外导出为环境变量,数据库和 Redis 密码也不能嵌入连接 URL Linux 上应用容器以 `10001:10001` 运行。上述命令将应用数据和日志目录设为 `0770`, 配置设为 `0640`,均由当前用户持有、容器组 `10001` 访问。 `config.yaml` 通过 Compose `configs` 只读挂载,普通 Compose 保留宿主机文件的 UID/GID 和 mode ### Redis ACL 用户 连接已配置 ACL 用户的 Redis 时,把用户名写在 `store.redis.url` 中,密码仍单独填写: ```yaml store: redis: url: 'redis://u1@redis-host:6379/0' password: &redis_password '' ``` 将这两项合并到已有配置,保留其他配置和密码锚点;Redis 服务端需事先创建并授权该用户。 用户名中的 `@`、`:` 等特殊字符需要 URL 百分号编码。未填写用户名时使用 Redis 的 `default` 用户 环境变量 `CPR_REDIS_URL` 优先于 `store.redis.url`。默认 `deploy/compose.yaml` 已将它设为 `redis://redis:6379/`,使用 ACL 用户时还需把应用服务的该环境变量改为 `redis://u1@redis-host:6379/0`,其中主机名必须能从应用容器访问;只改 `config.yaml` 不会生效。 默认 Compose Redis 服务使用密码认证,不会自动创建 ACL 用户 ## 启动 ```bash 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 --wait docker compose -f deploy/compose.yaml ps ``` 健康检查: ```bash curl -i http://127.0.0.1:8080/healthz ``` `204 No Content` 表示应用、PostgreSQL、Redis 和后台任务的健康检查通过,不代表每个上游账号都可用 不要把未脱敏的 `docker compose config` 或 `docker inspect` 输出上传到工单;它们会包含 PostgreSQL/Redis 启动密码。日常校验使用 `config --quiet` ### 启动后的运行设置 在管理端修改以下设置,保存后对新请求生效,重启不会覆盖已保存的选择: | 设置 | 入口与边界 | | --- | --- | | 上游客户端身份 | “系统设置 → 上游配置 → 客户端身份”;Key 可按 Provider 覆盖,支持自动或固定版本 | | 全局请求位置 | “系统设置 → 上游配置 → 请求位置覆盖”;默认关闭,不从 YAML 导入 | | 代理位置 | “代理管理”;优先于全局位置,全局关闭时仍可独立生效 | 位置覆盖只影响 OpenAI 请求中受支持的位置、日期与时区信息,不改变真实出口 IP、epoch 时间戳或系统时区。 `openai.residency` 是独立的部署约束。身份与位置字段见 [运行设置 API](../docs/api.md#8-运行设置)。 后台账号操作使用 Provider 自己的官方画像;发布资料刷新不改写用户选择或 `config.yaml` ### 插件命令行 使用与网关相同的工作目录和 `deploy/config.yaml`,读取制品已接受且当前已启用的插件实例: ```bash codex-proxy-rs plugin --help codex-proxy-rs plugin '实例ID' '命令' --help codex-proxy-rs plugin '实例ID' '命令' --参数 '值' ``` 将示例中的实例 ID、命令、参数名和值替换为插件实际提供的内容 不带子命令或使用 `serve` 时启动网关;插件命令不监听 HTTP、不启动后台任务,也不恢复或清理正在运行的 网关准入状态。顶层 `--help/--version` 不读取配置。插件帮助只读数据库,不执行迁移、登录或账号保存, 因此需要先由正常部署完成数据库初始化。宿主诊断保留在配置的文件日志中,stdout/stderr 专用于命令结果 参数支持 `--名称=值` 或 `--名称 值`;布尔参数可直接写 `--名称`,关闭时写 `--名称=false`。 duration 使用 `500ms`、`1m30s` 等带单位形式。参数可能进入 shell 历史和系统进程列表,不能因为插件声明 了敏感参数就认为命令行是安全的密钥输入通道;真实凭据优先使用对应插件提供的受控登录或导入入口 成功命令原样返回插件退出码与输出。命令受总期限约束,`Ctrl-C` 或终止信号会取消调用并回收插件进程。 若命令产生账号,只有成功结果才按顺序通过 Admin 保存;调用仍检查期限、实例版本和当前资源事实, 模型执行 Key 由具体调用选择,不在实例中预绑定。 部分保存后失败或取消不会自动重放,先核对账号和审计记录,再决定是否重新执行 独立仓库 `codex-proxy-plugins` 的 `examples/workbench` 提供命令行、请求处理与管理页面示例;模型相关操作会产生真实上游用量 ## 公网访问 Compose 默认只绑定 `127.0.0.1`。从其他设备访问时,在应用前配置反向代理, 不要把 PostgreSQL 或 Redis 暴露到公网 同源登录页的管理员与 API Key 登录都支持 HTTP 和 HTTPS 登录。反向代理应原样保留浏览器的 `Origin`, 不要清除它或改写 Cookie 的 `Secure` 属性;HTTPS 反代可以使用 HTTP 回源。 HTTP 传输不加密,公网部署仍建议使用 HTTPS。 会话 Cookie 合同见 [管理接口鉴权](../docs/api.md#管理接口) 反向代理需要保留 `Authorization`,支持 `/v1/responses` 的 WebSocket Upgrade, 并关闭 SSE 响应缓冲。读取超时应覆盖长时间生成任务。 客户端使用部署地址下的 `/v1`,协议与部署一致,不要使用前端开发服务的 `5173/dev/v1`。 当前应用只支持单副本,不能通过复制容器扩容 流式响应在首个上游事件提交后,每 15 秒无输出会发送一次 SSE 注释保活, 并设置 `X-Accel-Buffering: no` 和 `Cache-Control: no-cache, no-transform`。 反向代理仍需允许这些响应头生效;首个事件到达前的等待也需要足够的读取超时 网关默认不限制模型请求的总执行时长;OpenAI 上游流默认有 300 秒空闲超时。 `api.request_timeout_seconds` 默认 `null`,只控制 HTTP 路由返回响应前的等待,不是流式正文或 WebSocket 每轮执行的总时限。 排队、插件显式执行期限与客户端断开的边界见 [请求期限](../docs/api.md#请求期限) OpenAI 上游池化 WebSocket 默认每 25 秒发送一次 Ping,发出后允许等待 30 秒; 收到 Pong 或其他入站帧即解除本次心跳截止,持续无响应则以 `pong_timeout` 关闭连接。 此策略也覆盖正在生成的请求,与等待下一条上游消息的 `stream_idle_timeout_ms` 分别计时 若 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`。 已有配置先备份,合并后完全退出并重启 Codex。代理密钥由 Provider 配置提供, 不需要另写 `auth.json`,已有官方登录文件可以保留 管理端生成的 CC Switch 一键导入包含连接信息和当前 Key 的日/周额度查询,默认每 30 分钟刷新。 查询地址和凭据随导入生成,在 CC Switch 中修改 Provider 的地址或 Key 后,需重新导入以同步用量查询。 导入不包含下方模板的原生生图和 WebSocket 设置。需要原生生图时,使用下方配置直连 CPR, 避免 CC Switch 切换或接管后重写 Provider 配置 Linux/macOS 默认目录为 `~/.codex/`,Windows 为 `%USERPROFILE%\.codex\`; 设置过 `CODEX_HOME` 时以该目录为准。Provider 设置应写入用户配置,不要只写到项目目录。 配置层级见 [官方配置参考](https://learn.chatgpt.com/docs/config-file/config-reference) ### config.toml 以下示例与当前管理端模板的配置项一致。替换地址和密钥,模型可改成该密钥有权限使用的模型: ```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 = "" [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。接入代理不需要扩大命令沙箱的联网或文件权限 需要指定完整模型目录时,在账号的模型列表中导出所选 Codex 模型,并在 `config.toml` 顶层设置 `model_catalog_json = "/absolute/path/to/cpr-model-catalog.json"`。导出文件不含账号凭据; 它是一次目录快照,调整选择或上游模型能力变化后需重新导出 ### 生图和 WebSocket 模板启用原生生图。Codex 可在任务需要图片时调用 `image_gen.imagegen`, 再通过代理的 Images 接口生成或编辑图片;也可以在需求中明确要求生成并使用图片。 需要支持该能力的客户端、支持图片输入的对话模型,以及有生图权限和额度的 OpenAI 账号。 代理不会增加上游权限,xAI 账号不能承接这些 Images 请求 生图不要求开启 WebSocket。要启用客户端 WebSocket,把当前 Provider 的 `supports_websockets` 改为 `true`,并检查反向代理是否允许 Upgrade 客户端到代理、代理到上游是两段独立连接。客户端关闭 WebSocket 后, 服务端仍可能用 WebSocket 访问上游;客户端开关不控制服务端连接池和 HTTP 回退策略。 OpenAI OAuth 账号的上游传输方式默认 WS;固定为 SSE 的账号不承接必须依赖 WS 的预热及连接内续接。 仅使用这类账号时,客户端保持 `supports_websockets = false`,避免先尝试 WS 再回退 ### 时区与客户端环境信息 要覆盖模型请求中的位置和时区,在管理端启用全局自定义位置,或为账号关联的代理设置位置; 代理位置优先。`host.timezone` 控制部署日历和管理端时间展示,不代替请求位置设置。 覆盖字段与环境消息识别条件见 [代理位置合同](../docs/api.md#独立代理管理--managed-proxies) Codex 回答仍显示本机时区时,先核对请求是否使用了预期账号和代理,再区分环境正文与工具结果: - 使用新会话询问环境上下文中的时区,避免已有工具输出或历史回答影响判断 - 检查实际环境消息是否满足完整上下文形状;显式普通文本分类或非法 XML 不会被覆盖 - 若模型调用 `date` 等工具查询本机,工具结果仍反映真实系统时区,覆盖不会修改客户端操作系统 网关的请求头画像和位置覆盖不提供完整匿名化。正文与工作区 metadata 的保留范围见 [正文兼容](../docs/api.md#正文兼容),xAI 请求不应用 OpenAI 的请求位置覆盖 Codex 客户端还有独立的出站请求。analytics 使用 `chatgpt_base_url`,可以包含真实系统类型、 系统版本和架构;模型 Provider 的 `base_url` 不改变该地址。官方发行配置默认启用 Statsig 指标,目标为 `https://ab.chatgpt.com/otlp/v1/metrics`,不由网关上游画像控制 需要关闭这两类遥测时,将以下字段合并到客户端 `config.toml`,避免重复创建已有表,完全退出并重启 Codex: ```toml [analytics] enabled = false [otel] metrics_exporter = "none" ``` 这两个开关不清理模型请求中的工作目录、Git metadata、用户内容或工具结果。 ### 登录与生图排查 - 仍提示登录:确认实际读取的用户配置目录、选中的 Provider 和客户端版本,再完全退出重启。 新 Provider 使用 `requires_openai_auth = false`,不依赖本地 ChatGPT 登录 - 没有生图工具:核对 Codex 实际读取的 Provider 配置,确认 Actor 标记、`requires_openai_auth = false` 和图片输入能力, 不只检查导出模板;CC Switch 导入不包含生图设置。客户端缓存的本地 Free 账号也可能影响工具展示。 先备份并区分本地真实账号文件与代理密钥文件,不要直接删除全部登录状态 - 已调用生图但失败:查看服务端账号的凭据、权限、额度和请求错误,不能只凭文本对话成功判断 - `401`:检查代理密钥是否正确、是否启用;Actor 标记不能代替密钥 - `426`:检查客户端版本及反向代理是否保留版本头,低于管理员设置的最低版本时需要更新客户端。 手机远程控制的版本识别范围见 [版本门禁规则](../docs/api.md#1-鉴权与公共约定) - 地址包含 `5173/dev/v1`:这是开发代理地址,依赖 Vite 服务。日常使用改成后端或 HTTPS 地址; 验证 WebSocket 时直接连接后端,当前 Vite 代理未显式开启 WebSocket 转发 其他客户端使用 Responses API、`/v1` Base URL 和代理密钥即可,不需要 Actor 标记。 路由与请求格式见 [API 参考](../docs/api.md#3-openai-数据面与模型目录) ## 优雅关停 收到停止信号后,应用先停止接收新连接并 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 ## 本地开发 源码环境、前后端启动与组件库联调见 [开发与源码联调](../docs/development.md) 本机开发可使用 Compose 提供 PostgreSQL 和 Redis,集成测试另用[专用测试库](../backend/migrations/README.md#本地测试库) ## 持久化与备份 ### 数据目录 Compose 使用以下绑定目录: | 目录 | 内容 | | --- | --- | | `.runtime/data` | OpenAI 会话锚点密钥、更新状态、临时更新目录与备份暂存区 | | `.runtime/logs` | 应用文件日志、按需启用的 OAuth 恢复记录与请求转储 | | `.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 备份应覆盖哪些目录、如何生成一致性数据库归档,见 [备份与恢复](#备份与恢复) ### 日志与保留窗口 文件日志以时间完整性为清理依据,**没有文件数量淘汰上限**: | 文件集 | 保留配置 | 默认完整窗口 | | --- | --- | --- | | 普通日志、OAuth 恢复日志 | `host.logging.file.retention_days` | 前 7 个完整自然日及当天 | | 全量请求/响应报文 | `host.logging.request_dump_retention_days` | 前 1 个完整自然日及当天 | 文件名统一为 `codex-proxy-rs-<类别>.YYYY-MM-DD[.N].log[.gz]`,类别分别为 `application`、`oauth-recovery`、`request-dump`。专用 tracing target 为 `oauth_recovery` 和 `request_dump`;普通日志保留各 Rust 模块的 target,便于按模块过滤。 程序只管理上述规范名称的日志,其他命名的文件由运维手动清理。 日志时间戳采用部署时区并携带数字偏移,文件按该时区的日期轮转和整组保留:例如 9 月 8 日配置 1 天,会保留 9 月 7 日全天及 9 月 8 日的所有分片, 到 9 月 9 日才允许清理 9 月 7 日,避免留下半天日志。夏令时日期允许为 23 或 25 小时。切换时区不重命名历史文件; 按文件名日期与部署时区中的实际修改日期取较近值,旧日期分片近期被写入时整组延后删除。 `max_file_size_mb` 仅决定分片大小(默认 20 MiB),不决定保存时长,单条大记录不会被截断。 关闭的分片压缩为 `.log.gz`;成功压缩、同步并发布归档后才删除原文件,保留原修改时间。 清理发生在启动和轮转时;空闲期间过期文件可能暂时多保留。检索时须同时读取 `.log` 与 `.log.gz` 报文开关为 `host.logging.request_dump`,默认关闭;开启时原始报文按块写入独立文件, 缓冲拒收或写入失败可能造成缺块,需结合 `file_logging` 健康状态判断完整性。 数据库请求诊断仍是有界摘要,不能用摘要事件数代替全量报文完整性 OAuth 恢复开关为 `host.logging.oauth_recovery`,默认关闭,与普通文件日志开关分别控制: - 开启后,OpenAI 每次成功取得 AT 和可用 RT 时,在资料补全及数据库写入前写入独立的 `oauth-recovery` 文件集 - 该通道不受普通日志级别过滤,关闭时也不会改写到普通日志或 stdout;接收端不等待后台写入 恢复记录和请求转储含原始凭据,`.runtime/logs` 须按敏感数据控制访问和备份 每个文件输出使用非阻塞队列,普通额度为 4096 条 / 64 MiB,WARN / ERROR 另有 512 条 / 8 MiB 预留, 额度包含正在写入的记录。后台批量缓冲写文件,正常退出会排空已接收的队列并同步。 持续拥堵、超大记录或输出关闭导致的拒收会损失整条记录,所有接口均不会为日志等待; `file_logging` 健康探针在拒收或写入/同步失败后报告 `Unhealthy`, 本次进程内恢复写入也不会清除已有缺口;压缩或清理失败报告 `Degraded`。 断电、强杀或磁盘故障可能造成日志丢失;需要恢复已删除文件时,应使用独立备份。 容量不足时不会提前删除保留窗口内日志,必须根据完整日期的压缩后实际用量规划空间,并监控磁盘余量和 健康探针。Docker stdout 的独立轮转不承担应用文件日志的完整保留承诺 完整运行时、Provider、revision 与恢复边界见 [架构文档](../docs/architecture.md) ## 请求错误排查 1. 先记录故障时间和时区、网关版本/提交、客户端名称与版本,以及 HTTP/SSE/WebSocket 传输。 区分“上游返回”“网关实际响应”和“客户端终端展示”,不要只凭终端的统一文案推断根因 2. 收集响应中的 `x-gateway-request-id`、`x-request-id` / `x-oai-request-id`;配置了 `api.request_id_header` 时也记录该入口头。WebSocket 合成错误的关联头位于本条错误的 `headers`。 在管理端错误列表按 ID 和时间搜索;检查平台条件并主动刷新,翻页不会推进查询时间。 只有入口 ID 时,改用入口日志和时间定位 3. 打开错误详情,核对上游/客户端状态、发送状态、attempt、失败阶段及后续恢复关联。 下载默认诊断包作为反馈材料,先看 `availability`、`attemptsComplete` 和淘汰计数; 该包不含原始错误正文或完整 trace data。分享前仍应检查关联 ID 等内部信息 4. 需要更细上下文时,在 `codex-proxy-rs-application.*.log` 及 `.log.gz` 中按网关/上游 ID 和时间检索,结合 `attempt.started`、`attempt.failed`、`request.finished` 与 transport 阶段判断。 **开启 `host.logging.file.enabled` 时,stdout 仅保留 `gateway_startup` 通道**; `docker logs` 看不到业务错误不代表没有错误。关闭普通文件日志且开启 `host.logging.stdout` 时,普通日志才按级别输出到 stdout;专用 dump/OAuth 恢复通道不会混入 5. 查不到请求记录时,先用模型执行 ID 在“全部平台”下搜索:已进入执行会话的建流前失败也会记录, 但其 attempt 为零,尚未确认的 Provider/账号/传输为空,不会命中具体平台或账号筛选。 再检查观测队列丢弃/写入失败告警及 `file_logging` 健康状态。鉴权、解析、路由和准入等入口拒绝 仍不保证进入错误列表;应结合入口状态与日志,不能据此认定请求未发生。异步投影有延迟, 刷新后仍需核对缺口,而不是反复重放可能已发送的请求 6. 仅在上述信息不足且能够控制访问范围时临时开启 `host.logging.request_dump` 复现一次。 它会记录完整凭据与用户正文,应限定访问、摘取最小片段并人工脱敏;复现后关闭开关, 按配置的保留窗口及组织的数据处理要求管理已生成文件。不要为普通请求排查开启 OAuth 恢复记录, 也不要直接上传整个日志目录或完整转储 请求问题反馈使用 [接口问题反馈表单](../.github/ISSUE_TEMPLATE/api-bug-report.yml);错误诊断与查询合同见 [API 文档](../docs/api.md#10-dashboard用量与错误) ### 插件取文与 DNS 插件受管 HTTP 由实际运行网关的主机或容器解析 DNS,再通过直连或账号代理访问目标。插件拥有完整网络访问能力,宿主不按插件身份限制私网、回环或保留地址 域名解析失败时先核对网关运行环境的 DNS。Clash / Mihomo 的 Fake-IP 地址需要相应代理接管;无法连通时可让目标域名返回真实地址,并检查所选代理。代理失败不会自动回退直连 排障区分域名解析失败、超时、连接失败和响应读取失败,避免在日志或截图中输出真实请求头、正文及凭据 ## 密码语义 - `admin.default_password` 只在首次创建管理员时使用 - 已有管理员在「系统设置 → 安全与访问 → 管理员密码」修改登录密码,需要验证当前密码;修改后所有管理员会话失效,使用新密码重新登录 - PostgreSQL 官方镜像只在空数据目录初始化时使用 `database.password` - Redis 在每次容器创建时使用 `redis.password` 已有 PostgreSQL 数据目录后,直接修改 `database.password` 不会修改数据库用户密码,只会导致 应用无法连接。轮换时必须先在 PostgreSQL 中修改用户密码,再同步更新 `config.yaml`。Redis 密码变更后需要用新配置重新创建 Redis 和应用容器,不需要删除 Redis 数据目录。 安排维护窗口,避免应用和 Redis 在过渡期间使用不同密码 ## 镜像升级与源码构建 每个 Release 独立提供 `config.example.yaml`、默认镜像固定到该版本的 `compose.yaml` 和校验和; 各平台归档也包含 `deploy/config.example.yaml`。配置模板来自构建该版本的同一提交。 使用二进制归档手动部署时,将模板中的 `api.asset_directory` 改为 `../web/dist`,指向归档内的静态资源。 在线更新默认使用同一目录;如显式设置 `host.system_update.web_dist_dir`,应确保它指向实际提供页面的目录。 升级时先阅读目标版本说明,下载同一 Release 的部署附件,对比模板并合并必要配置,保留已有凭据 和 Compose 自定义项。不要用模板覆盖 `config.yaml`,也不要从 `main` 下载模板搭配旧镜像 更新部署文件后,从安装目录拉取目标版本镜像并重建应用容器: ```bash docker compose -f deploy/compose.yaml pull codex-proxy-rs docker compose -f deploy/compose.yaml up -d --no-build --wait codex-proxy-rs ``` 源码构建需要克隆源码仓库并准备配置与数据目录,构建过程自动准备前端依赖,无需初始化子模块或准备额外 build context。以下命令从仓库根目录执行: ```bash docker compose -f deploy/compose.yaml build codex-proxy-rs docker compose -f deploy/compose.yaml up -d --no-build --wait ``` 升级后通过管理端版本接口或容器 image digest 确认运行实例的版本和 revision 构建元数据通过一次性进程环境传入: ```bash 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 ``` ### 版本命名与升级规则 管理端「系统更新」可临时选择通道检查更新,不保存选择,不会自动下载或安装。 每次打开弹窗按当前运行版本匹配通道:正式版为 Stable,预发行按版本后缀匹配。 升级并重启后按新运行版本匹配;侧栏更新提示始终使用当前运行通道。预发行编号 `N` 从 1 开始递增 | 通道 | 命名示例 | 接收的版本 | | --- | --- | --- | | Stable | `X.Y.Z` | 正式版 | | RC | `X.Y.Z-rc.N` | RC、正式版 | | Beta | `X.Y.Z-beta.N` | Beta、RC、正式版 | | Alpha | `X.Y.Z-alpha.N` | Alpha、Beta、RC、正式版 | | Exp | `X.Y.Z-exp.N` | 同一轮实验中编号更高的 exp,仅实验实例可选 | 普通通道在同一大版本内持续接收更高版本,包括后续小版本和补丁版本的预发行。 切换通道不会降级,Stable 候选也必须高于当前运行版本。 下载、安装或待重启期间不能切换通道 同一 `X.Y.Z-exp.N` 系列只用于同一轮实验,不能混用不同实验分支;变更基线即进入另一实验线,需手动迁移。 exp 不进入正式版、alpha、beta 或 rc,普通实例也不能选择 Exp。 从 exp 切换正式版时需先备份,在目标版本的独立数据库中迁移业务数据,不能直接复用或整库还原实验数据库 版本大小遵循 [SemVer](https://semver.org/),构建元数据 `+...` 不参与比较。未知预发行标识不提供在线更新。 更新器先过滤不允许的目标,再选择版本最高的候选;其他通道或更高大版本领先,不影响当前发行线的更新。 只有当前官方构建满足在线更新条件且存在允许安装的新版本时,才返回“有可用更新”。否则不显示更新标记、 其他通道的版本及发布说明,手动检查显示“当前没有可用更新”。检查失败单独报告,不能当作没有更新 alpha、beta、rc、exp 在 GitHub 标记为 Pre-release,不覆盖 GitHub Latest 或镜像 `latest`。 允许连续升级的发行线从首次发布起遵守[迁移冻结规则](../backend/migrations/README.md#冻结规则), 包括预发行到正式版的晋级;发版前验证对应升级路径,不能仅凭版本号认定数据库兼容 ### 管理端在线更新 官方构建按上述规则在线更新;源码等非官方构建返回不支持原因,不提供可用更新。 点击「下载并更新」后,操作日志显示下载、校验与安装过程,完成后显示「待重启」及待生效版本,重启后才使用新程序。 「当前版本」始终表示正在运行的版本,「通道最新」表示所选通道的检查结果 手动更换镜像或程序后,更新器会核对安装文件并校准历史状态,不以旧成功记录推断需要重启。 运行期间检测到外部文件变动或文件缺失时会报告异常,需要核对完整发行包并重启服务后再检查。 回滚入口只接受与记录指纹一致的完整备份;缺少指纹,或备份被替换、丢失时,不提供在线回滚, 但不会删除备份文件。此时需要按人工部署流程恢复已经核实的发行包 直接运行二进制时,若未设置 `CPR_ENABLE_SELF_RESTART=true`,管理端自重启默认关闭。 在现有 `deploy/config.yaml` 的 `host` 下合并以下配置: ```yaml host: system_update: deployment_mode: binary self_restart_enabled: true executable_path: '/opt/codex-proxy-rs/codex-proxy-rs' ``` `executable_path` 为可选项:未指定时,服务在启动时解析并固定当前程序路径; 显式配置时,将示例改为实际程序的绝对路径。`self_restart_enabled` 控制管理端的「立即重启」操作; 修改配置后需手动重启当前服务一次才会生效。 以上三个字段也可分别通过 `CPR_DEPLOYMENT_MODE`、`CPR_ENABLE_SELF_RESTART` 和 `CPR_UPDATE_EXE_PATH` 设置, YAML 中显式配置的值优先于对应环境变量 Compose 提供以下在线更新运行参数: - `CPR_UPDATE_REPOSITORY`:只接受 `owner/repository`;默认 `zyycn/codex-proxy-rs` - `CPR_GITHUB_API_BASE`:正式环境必须为 `https://api.github.com/repos` - `CPR_UPDATE_EXE_PATH`、`CPR_WEB_DIST_DIR`:分别指向容器内二进制和前端静态目录; `CPR_WEB_DIST_DIR` 同时供页面服务与更新器使用,相对路径以 `deploy/config.yaml` 所在目录为基准 - 更新临时目录、状态文件和锁文件默认由 `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___.tar.gz` 与 `checksums.txt`。服务会在替换前再次查询远端最新版本,校验下载 host、声明大小、SHA-256 和 归档路径;二进制或静态资源任一替换失败时恢复旧文件。成功后的旧二进制和旧静态目录分别保留为 `*.backup`,管理端 rollback 会交换当前文件与这份备份。更新状态和跨进程锁可在以下位置排查: ```text .runtime/data/update-state.json .runtime/data/update.lock .runtime/data/update-tmp/ ``` ### 插件兼容与发行目录 教学示例在 `codex-proxy-plugins` 独立构建和发布,通过[插件管理](../docs/plugins.md)安装。 宿主发行物不内置插件,包含用于兼容检查的封口清单 | 文件或目录 | 用途 | | --- | --- | | `plugin-release-manifest.json` | 固定宿主版本、提交、平台与支持的插件合同;插件列表当前为空 | | 可执行文件同目录的 `plugins/official` | 随受信宿主发行物部署的只读目录,与二进制、Web 资源成套更新或恢复 | - 下载更新时校验发行身份,插件合同不兼容不阻止安装。点击重启时检查已安装目标版本,列出不兼容插件的名称和原因;确认后保留启用配置并重启,逐个尝试启动插件,版本警告持续显示 - 回滚同样在重启前提示插件兼容风险,不因版本不匹配批量停用插件。更新或回滚期间的并发配置变更会阻止切换,文件替换失败或取消时恢复整组文件 - 非空官方清单中的包经身份、平台和摘要校验后幂等导入;导入不确认信任、不创建配置、不启用,也不删除旧包。管理员确认信任后才执行默认配置流程;重复摘要保留首次安装出处 - `sealed` 是构建封口标记,不是密码学签名。普通上传、URL 或 GitHub 安装不能获得 `builtin` 身份,独立教学示例也不例外 部署须保留完整的二进制、Web 资源与官方插件目录;不要手动修改制品元数据或迁移 checksum 绕过校验。 macOS arm64 提供构建产物;部署前需验证目标平台的插件运行与上游调用,不能只以打包成功判断可用 ## 备份与恢复 数据库可用管理端的 S3/R2 逻辑备份,或停止 PostgreSQL 后备份数据目录 不要在 PostgreSQL 写入期间直接复制数据目录作为一致性备份 | 需要保留的内容 | 备份方式 | | --- | --- | | 账号、Key、设置、请求、审计与插件数据 | 完整数据库归档,或停库后备份 `.runtime/postgres` | | 部署配置与凭据 | 单独备份 `deploy/config.yaml` 及 Compose 自定义配置 | | 应用日志与已启用的 OAuth 恢复记录 | 单独备份 `.runtime/logs`,不包含在数据库归档中 | | 会话锚点与节点运行文件 | 按需备份 `.runtime/data` | | 短期协调状态 | 按需备份 `.runtime/redis`,数据库冷恢复时使用空 Redis | OpenAI 主动额度重置卡及消费结果由上游持有,不属于本地备份内容 数据库、配置、插件敏感设置和 OAuth 恢复日志均按凭据保护 ### 备份内容与限制 - 插件包体、固定来源、下载凭据、制品接受事实、实例配置、版本配置快照、敏感配置、功能范围绑定和私有状态都保存在 PostgreSQL,随数据库一起备份。插件通过宿主回调读写的内置平台账号保存在同库的通用账号表。 来源使用的出站代理及其认证也须随库恢复,不能只备份 `plugin_*` 表;来源代理与账号代理独立选择, 不依赖进程代理环境,故障时不会自动回退直连。 包体单个上限为 32 MiB,需要计入数据库及备份容量;解压运行目录和 Redis 协调状态是可重建数据, 不作为插件安装或制品接受事实。恢复时保留制品接受状态、实例配置、账号引用和状态代次的一致性;插件降级仍须 通过状态兼容校验,不通过手工替换缓存目录回滚 - 官方运行镜像内置与 Compose 数据库服务版本一致的 `pg_dump` / `pg_restore`,无需额外安装。 自行替换 PostgreSQL 版本时,应同步核对备份工具版本 - 备份暂存目录为 `host.runtime_data_dir/backup-staging`;Compose 默认对应 `/app/.runtime/data/backup-staging`,由 `.runtime/data` 卷持久化,权限 `0700`(仅 `cpr` 用户可读写)。部署卷至少预留一个最大数据库归档的空间 - 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 工具离线恢复: ```bash pg_restore --no-owner --no-privileges --password \ --dbname='postgresql://restore_user@127.0.0.1:5432/restore_db' backup.dump ``` 示例中的账号、空目标库和归档路径需按实际情况替换,密码由命令交互输入 恢复期间与恢复后的边界见 [架构文档](../docs/architecture.md#11-生命周期安全与恢复)。 当前没有在线恢复 API,也没有供部署者直接启用的“维护模式”开关: 1. 先备份现有数据库,并停止应用;在独立的空数据库中恢复归档,检查数据和迁移版本 2. 应用保持停止,离线核对备份设置与记录。禁用快照中的旧计划,处理非终态记录 (`queued/dumping/uploading/deleting`)、旧调度游标和到期清理条件,再决定哪些记录保留 3. 使用空 Redis 和空插件解包缓存做一次冷启动;逐一核对已启用实例的包摘要/平台、制品接受状态、配置与绑定、账号引用、 私有状态 schema/代次,并执行实际插件操作。缺少或损坏必需包体、引用不完整或状态不兼容时,恢复不能 记为成功,也不能用相同插件 ID 的其他包替代 4. 确认不会误执行旧任务或删除恢复前的远端对象后,再接入业务流量;重新测试 S3 连接并设置计划 只关闭计划备份不会停止 Worker 的任务恢复和到期删除。无法确认这些记录的影响时, 不要把恢复后的数据库直接接入运行中的应用。具体处理应根据目标库数据制定,不提供清空生产记录的通用命令