diff --git a/README.md b/README.md index 7fff6a09b..a5bb0a130 100644 --- a/README.md +++ b/README.md @@ -12,31 +12,35 @@ [![GHCR](https://img.shields.io/badge/GHCR-codex--proxy--rs-2496ED?logo=docker&logoColor=white&style=flat-square)](https://github.com/zyycn/codex-proxy-rs/pkgs/container/codex-proxy-rs) [![MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://opensource.org/license/mit) -[快速开始](#快速开始) · [客户端接入](#客户端接入) · [运维](#运维) · [部署](deploy/README.md) · [接口](docs/api.md) · [架构](docs/architecture.md) +[快速开始](#快速开始) · [客户端接入](#客户端接入) · [运行维护](#运行维护) · [部署](deploy/README.md) · [接口](docs/api.md) · [架构](docs/architecture.md) +Codex Proxy RS 在一个网关中管理 OpenAI 与 xAI 账号,为客户端提供统一鉴权、模型路由、额度状态、 +请求观测和管理控制面。 + > [!NOTE] -> 只支持 OpenAI Responses API,不提供 `/v1/chat/completions`。 +> 数据面支持 OpenAI Responses、Images 和模型目录协议,不提供 `/v1/chat/completions`。 -## 能力 +## 功能 -| 领域 | 实现 | -| -------- | ----------------------------------------------------------------------------------------------------- | -| 协议 | OpenAI Responses JSON、SSE、WebSocket 与模型目录 | -| Provider | 固定 OpenAI、xAI 两个 Provider;各自管理 OAuth、账号、额度与模型目录 | -| 路由 | Client Key 绑定 Provider、全局模型映射、会话亲和与同 Provider 安全 fallback | -| 透明边界 | OpenAI 请求保留未知字段和字段顺序,SSE 与 WebSocket 原样转发业务字节;xAI 在 Provider 内做协议转换 | -| 延续 | OpenAI native/replay continuation;xAI 使用客户端提交的完整历史 | -| 账号 | 导入、OAuth、credential 刷新、主动额度刷新,以及正常响应中的被动额度/状态观测 | -| 管理 | Client Key、账号、模型目录、设置、观测、系统更新与 OAuth | -| 计量 | 模型请求 Token、费用、延迟、账号与 Provider 归因,并记录响应确认的实际 `service_tier` | +| 领域 | 当前能力 | +| --- | --- | +| 数据面 | Responses JSON、SSE、WebSocket、review 子代理、Images 生成/编辑与模型目录 | +| Provider | 固定 `openai`、`xai` 两个 Provider,各自拥有 OAuth、credential、额度、目录与 transport | +| 路由 | Client Key 限定账号分组;按模型能力编译 Provider 候选;支持全局精确模型映射、会话亲和和安全 fallback | +| 透明边界 | OpenAI 保留未知请求字段及字段顺序,并原样转发 SSE、WebSocket 与 Images 业务字节;xAI 在 Provider 内转换协议 | +| 账号 | OAuth、OpenAI AT/RT、账号文件与 Agent Identity 导入,credential/额度刷新、账号恢复及主动额度重置卡 | +| 管理 | 账号与分组、Client Key、运行设置、用量/错误观测、S3/R2 备份、系统更新与回滚 | +| 计量 | Token、图片、费用、延迟、账号与 Provider 归因,并记录上游响应确认的实际 `service_tier` | + +架构边界、状态所有权和请求不变量见 [架构文档](docs/architecture.md)。 ## 快速开始 需要 Docker Engine 与 Docker Compose Plugin。 -### 1. 准备 +### 1. 准备配置和目录 ```bash git clone https://github.com/zyycn/codex-proxy-rs.git @@ -56,16 +60,16 @@ openssl rand -hex 24 openssl rand -hex 24 ``` -把两个结果写入 `deploy/config.yaml`,并单独设置管理员初始密码: +把两个结果分别写入 `deploy/config.yaml`,再设置管理员初始密码: -| 配置 | 约束 | -| -------------------------- | ---------------------------- | -| `store.database.password` | 48 位十六进制 | -| `store.redis.password` | 48 位十六进制 | -| `admin.default_password` | 至少 12 位,不能包含 `$` | +| 配置 | 约束 | +| --- | --- | +| `store.database.password` | 48 位十六进制 | +| `store.redis.password` | 48 位十六进制 | +| `admin.default_password` | 至少 12 位,不能包含 `$` | -PostgreSQL、Redis 容器和应用通过同一份 `config.yaml` 使用这两个基础设施密码,不需要额外 -导出环境变量。管理员密码只在首次创建管理员时生效。 +PostgreSQL、Redis 和应用从同一份 `config.yaml` 读取基础设施密码,不需要额外导出环境变量。 +管理员密码只在首次创建管理员时生效。 Linux 需要允许容器组写入运行目录: @@ -83,37 +87,36 @@ docker compose -f deploy/compose.yaml up -d --no-build curl -i http://127.0.0.1:8080/healthz ``` -`204 No Content` 表示应用、PostgreSQL 与 Redis 均可用。管理端地址: +`204 No Content` 表示应用、PostgreSQL 与 Redis 均可用。管理端地址为 `http://127.0.0.1:8080`。 -### 3. 初始化 +### 3. 初始化网关 1. 使用 `admin@cpr.local` 与初始密码登录。 -2. 在账号页选择对应 Provider:OpenAI 支持 OAuth、账号文件与 Agent Identity;xAI 支持 OAuth - 和兼容账号文件导入。 -3. 按需配置客户端模型到上游模型的全局映射;未命中映射的模型名原样透传。 -4. 创建 `sk_...` Client Key,绑定 `openai` 或 `xai`,并设置速率与并发限制。 +2. 导入或授权 Provider 账号:OpenAI 支持 OAuth、逐行 AT/RT、OAuth JSON 与 Agent Identity; + xAI 支持 OAuth 和账号 JSON。 +3. 创建账号分组并按需配置全局模型映射。未命中的模型名原样交给候选 Provider。 +4. 创建 `sk_...` Client Key,选择可用账号分组,并设置速率和并发限制;空分组范围表示全部账号。 -OpenAI 账号导入和首次 OAuth 创建在 credential 提交后会立即尝试一次额度观测;额度接口临时失败不会 -回滚已提交的账号。Free、K12 等套餐共用同一套额度与状态转换逻辑,套餐字段只作为账号事实并隔离 -对应的模型目录 cache。credential 刷新、额度刷新和正常请求中的被动额度观测是三条独立链路,详见 -[账号接口](docs/api.md#5-账号)。 +OpenAI RT 导入会先向官方 token endpoint 换取 AT;仅 AT 的账号不具备自动续期能力, +能否参与调度仍取决于 token 中可验证的身份与后续上游事实。账号导入和首次 OAuth 创建会尽力读取一次额度, +额度上游暂时失败不会回滚已提交的账号。 + +OpenAI 账号的主动重置卡只在用户打开相应弹窗或手工刷新时查询。消费成功后管理端重新查询卡片与额度; +系统不会通过修改本地 `reset_at` 伪造重置。完整账号合同见 [账号接口](docs/api.md#5-账号)。 > [!IMPORTANT] > xAI 使用 OAuth session,不支持把 xAI API Key 作为上游 credential。 -> [!NOTE] -> `admin.default_password` 只在首次创建管理员时生效。 - -完整部署、权限和密码轮换规则见 [deploy/README.md](deploy/README.md)。 +完整的权限、密码轮换、备份和升级规则见 [部署文档](deploy/README.md)。 ## 客户端接入 -| 配置 | 值 | -| -------- | ---------------------------------------- | -| Base URL | `http://127.0.0.1:8080/v1` | -| API Key | 管理端创建的 `sk_...` | -| 鉴权 | `Authorization: Bearer ` | +| 配置 | 值 | +| --- | --- | +| Base URL | `http://127.0.0.1:8080/v1` | +| API Key | 管理端创建的 `sk_...` | +| 鉴权 | `Authorization: Bearer ` | ```bash curl http://127.0.0.1:8080/v1/models \ @@ -130,69 +133,70 @@ curl http://127.0.0.1:8080/v1/responses \ ```
-API 路由 +公开数据面路由 -| 路由 | 用途 | -| ---------------------------------- | ------------------------------ | -| `POST /v1/responses` | JSON 响应或 SSE Responses 流 | -| `GET /v1/responses` | Responses WebSocket 升级 | -| `POST /v1/responses/review` | review 子代理请求 | -| `POST /v1/images/generations` | 图像生成 JSON 透明转发 | -| `POST /v1/images/edits` | 图像编辑 JSON 透明转发 | -| `GET /v1/models` | 当前 Client Key 可用的模型列表 | -| `GET /v1/models/catalog` | Codex 客户端展示用模型目录 | -| `GET /v1/models/{model_id}/info` | Codex 客户端展示用模型详情 | -| `GET /v1/models/{model_id}` | OpenAI 兼容模型详情 | +| 路由 | 用途 | +| --- | --- | +| `POST /v1/responses` | JSON 响应或 SSE Responses 流 | +| `GET /v1/responses` | Responses WebSocket 升级 | +| `POST /v1/responses/review` | review 子代理请求 | +| `POST /v1/images/generations` | OpenAI Images 生成 JSON 透明转发 | +| `POST /v1/images/edits` | OpenAI Images 编辑 JSON 透明转发 | +| `GET /v1/models` | 当前 Client Key 账号范围内的模型列表 | +| `GET /v1/models/catalog` | Codex 客户端展示用模型目录 | +| `GET /v1/models/{model_id}/info` | Codex 客户端展示用模型详情 | +| `GET /v1/models/{model_id}` | OpenAI 兼容模型详情 | -所有 `/v1/*` 路由都需要客户端 API Key。 +所有 `/v1/*` 路由都需要 Client Key。Images 是 OpenAI Provider 自有端点,不读取模型字段,也不参与 +文本模型映射。
-完整的客户端与管理端路由、鉴权和 mutation 规则见 [接口文档](docs/api.md)。 +完整的客户端与管理端路由、鉴权、请求字段和 mutation 语义见 [接口文档](docs/api.md)。 -## 运维 +## 运行维护 ```bash -# 实例升级 +# 同一大版本内升级发布镜像 docker compose -f deploy/compose.yaml pull codex-proxy-rs docker compose -f deploy/compose.yaml up -d --no-build -# 日志 +# 查看日志 docker compose -f deploy/compose.yaml logs -f codex-proxy-rs -# 从源码构建 +# 从当前源码构建 docker compose -f deploy/compose.yaml build codex-proxy-rs docker compose -f deploy/compose.yaml up -d ``` > [!IMPORTANT] -> `.runtime/` 保存 PostgreSQL、Redis、日志、OpenAI 会话锚点密钥和自更新状态。删除该目录会永久清除 -> 运行状态。 +> `.runtime/` 保存 PostgreSQL、Redis、日志、OpenAI 会话锚点和在线更新状态。删除该目录会永久清除 +> 本地运行状态。 > [!WARNING] -> 不支持跨大版本在线升级。升级到新的大版本时,请使用全新的 `.runtime/` 数据目录重新部署, -> 并重新导入或重新授权 Provider 账号与客户端 Key。 +> 不支持跨大版本在线升级。升级到新的大版本时请使用全新的 `.runtime/` 数据目录,并重新导入或授权 +> Provider 账号与 Client Key。 -Compose 默认只绑定 `127.0.0.1`。公网接入应使用 HTTPS 反向代理,转发 WebSocket -upgrade 与真实客户端 IP;不要暴露 PostgreSQL 或 Redis。 +Compose 默认只绑定 `127.0.0.1`。公网接入应使用 HTTPS 反向代理,转发 WebSocket upgrade 与真实客户端 +IP;不要暴露 PostgreSQL 或 Redis。当前运行拓扑为单副本,不能通过复制应用容器扩容。 ## 维护与发布 -发布仓库版本必须从干净、已同步上游的发布分支运行: +仓库版本只能从干净、已同步上游的发布分支发布: ```bash release/publish ``` -脚本依赖已登录的 GitHub CLI(`gh auth login`)。它会更新 `release/version.yaml`、创建约定提交与带 -注释的 `v` tag,原子推送分支和 tag,再从发布分支触发 Release 构建,使构建缓存可以跨版本 -复用。不要手工修改版本后单独打 tag。该脚本不会登录服务器,也不会改变任何运行实例;源码提交、 -仓库发版和实例升级是三个独立状态。 +脚本依赖已登录的 GitHub CLI(`gh auth login`),会更新 `release/version.yaml`、创建约定提交和带注释的 +`v` tag,原子推送分支与 tag,再触发 Release 构建。源码提交、仓库发版和运行实例升级是三个 +独立状态;脚本不会登录服务器或改变任何运行实例。 ## 文档 -- [部署](deploy/README.md) +- [部署与运维](deploy/README.md) - [客户端与管理端接口](docs/api.md) -- [架构与数据边界](docs/architecture.md) +- [系统架构](docs/architecture.md) +- [数据库迁移规则](backend/migrations/README.md) - [Release](https://github.com/zyycn/codex-proxy-rs/releases) - [容器镜像](https://github.com/zyycn/codex-proxy-rs/pkgs/container/codex-proxy-rs) diff --git a/backend/crates/gateway-admin/src/backup/policy.rs b/backend/crates/gateway-admin/src/backup/policy.rs index ac71063b8..a1c2d248f 100644 --- a/backend/crates/gateway-admin/src/backup/policy.rs +++ b/backend/crates/gateway-admin/src/backup/policy.rs @@ -119,11 +119,11 @@ pub struct RetentionDecision { /// 从按完成时间倒序的 completed 计划备份中决定纳入删除流程的记录。 /// -/// 规则(`docs/s3-backup-design-audit.md` §4.4): +/// 自动保留规则: /// - `retentionDays = 0` 时不按天数清理;`retentionCount = 0` 时不按份数清理。 /// - 自动计划备份满足任一启用阈值即可进入删除。 /// - 无论阈值如何,至少保留最近一个成功的计划备份(倒序下标 0)。 -/// - 手动备份不受自动阈值影响;无 TTL,仅由管理员手动删除。 +/// - 手动备份不进入本规则;手工/计划记录各自的 `expires_at` 由到期扫描处理。 #[must_use] pub fn decide_retention( retention_days: u32, diff --git a/backend/crates/gateway-admin/src/model/backup.rs b/backend/crates/gateway-admin/src/model/backup.rs index 2dc39bcf8..c31e3bbbe 100644 --- a/backend/crates/gateway-admin/src/model/backup.rs +++ b/backend/crates/gateway-admin/src/model/backup.rs @@ -288,7 +288,7 @@ pub struct BackupRecord { pub error_message: Option, pub started_at: Option>, pub completed_at: Option>, - /// 手动过期时间;创建时确定,到期自动清理。 + /// 创建时确定的过期时间;手工和计划备份均可设置,到期自动清理。 pub expires_at: Option>, pub created_at: DateTime, pub updated_at: DateTime, diff --git a/backend/crates/gateway-api/src/admin/presenter.rs b/backend/crates/gateway-api/src/admin/presenter.rs index b80cf114f..a77b2775d 100644 --- a/backend/crates/gateway-api/src/admin/presenter.rs +++ b/backend/crates/gateway-api/src/admin/presenter.rs @@ -1,7 +1,7 @@ //! Admin API 的展示层辅助:数值格式化。 //! -//! 按 docs/project-redundancy-boundary-audit.md BE-02 的目标,display 字段最终 -//! 迁移到前端;过渡期统一由本模块提供,避免 accounts/observability 各自维护。 +//! 当前 wire 仍保留 display 字段;统一由本模块提供,避免 accounts/observability +//! 各自维护格式化规则。 use gateway_core::accounting::Decimal; diff --git a/backend/crates/gateway-store/src/lib.rs b/backend/crates/gateway-store/src/lib.rs index fab901ebd..afacfa48b 100644 --- a/backend/crates/gateway-store/src/lib.rs +++ b/backend/crates/gateway-store/src/lib.rs @@ -1,6 +1,6 @@ //! 多 Provider 网关的 PostgreSQL 持久化与 Redis 协调 adapter。 //! -//! 业务规则与 port 由 `gateway-core` 拥有。本 crate 只负责把终态的七张业务表 +//! 业务规则与 port 由 `gateway-core` / `gateway-admin` 拥有。本 crate 只负责把当前十二张业务表 //! 和可丢失 Redis 状态映射为明确的基础设施操作。 use std::sync::Arc; diff --git a/backend/crates/gateway-store/src/postgres/mod.rs b/backend/crates/gateway-store/src/postgres/mod.rs index ffd81f526..b831bd22e 100644 --- a/backend/crates/gateway-store/src/postgres/mod.rs +++ b/backend/crates/gateway-store/src/postgres/mod.rs @@ -1,4 +1,4 @@ -//! 九张终态业务表的 PostgreSQL adapters。 +//! 当前十二张业务表的 PostgreSQL adapters。 use async_trait::async_trait; use sqlx::{PgPool, postgres::PgPoolOptions}; diff --git a/backend/migrations/README.md b/backend/migrations/README.md index fb912de0e..78cd3a450 100644 --- a/backend/migrations/README.md +++ b/backend/migrations/README.md @@ -4,12 +4,15 @@ sqlx 在服务启动时(`serve` 监听之前)按编号顺序执行本目录 文件的 checksum 记入 `_sqlx_migrations`。此后每次启动都会重新校验:**已应用 迁移的文件字节与记录不一致时,服务直接拒绝启动**。 +当前大版本只有两份冻结迁移:`0001_initial.sql` 建立十二张业务表的完整新库基线, +`0002_nullable_requested_model.sql` 允许 Images 等无模型端点把请求模型保存为 `NULL`。 + ## 冻结规则 - 同一大版本内,已合入 main 的迁移文件**永久冻结**,一个字节都不能改——包括注释和 空白。任何改动都会让该大版本的已部署实例在下次重启时因 checksum 失配而无法启动, 且没有自动恢复路径。 -- 同一大版本内的 schema 变更一律新增编号迁移(`0002_...`、`0003_...`),永远不回改 +- 同一大版本内的 schema 变更一律新增编号迁移(下一份从 `0003_...` 开始),永远不回改 旧文件。切换大版本时允许把已完成升级的 schema 折叠成新的 `0001` 基线,因为跨大版本 不支持在线升级,目标版本必须使用全新数据目录部署。 - `.frozen-sha256` 是冻结清单(`sha256sum` 格式)。CI 会校验:清单内文件 diff --git a/deploy/README.md b/deploy/README.md index 6c8cf4ad1..21fab2e7e 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -112,6 +112,9 @@ PostgreSQL 是账号、Client Key、运行设置、请求记录与审计的权 Provider schema 以明文 JSON 保存于 PostgreSQL。Redis 只保存可重建、可过期的协调状态,例如 会话亲和、lease、cooldown、OAuth pending flow 与套餐模型目录 cache。 +OpenAI 主动额度重置卡及其消费结果由上游持有,不写入 PostgreSQL/Redis,也不属于本地备份内容; +管理端只在当前浏览器会话中保留最近一次查询结果。 + 备份必须包含 `.runtime/postgres`。要保留 OAuth 的 AT/RT 恢复能力,必须保持 `host.logging.file.enabled: true` 并备份 `.runtime/logs`;恢复记录位于独立文件集,仍按普通日志的 `retention_days` 和 `max_files` 分别约束。若希望保留短期 Redis 状态和会话锚点,也同时备份 `.runtime/redis` 与 @@ -205,8 +208,8 @@ Release 必须提供当前 OS/架构的 `codex-proxy-rs___.ta - 备份暂存目录为 `host.runtime_data_dir/backup-staging`;Compose 默认对应 `/app/.runtime/data/backup-staging`,由 `.runtime/data` 卷持久化,权限 `0700`(仅 `cpr` 用户可读写)。部署卷至少预留一个最大数据库归档的空间。 -- OAuth 恢复记录通道使用 `oauth_recovery` 结构化日志事件;当前 OpenAI provider 每次成功取得 - AT/RT 后,都会在账号资料补全、过期时间计算和数据库写入前写入独立的 +- 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` 清理;文件日志开启时不会被普通日志级别筛掉。它遵循普通日志现有的非阻塞写入机制,不会阻断 @@ -214,6 +217,8 @@ Release 必须提供当前 OS/架构的 `codex-proxy-rs___.ta `.runtime/logs` 因此属于敏感数据,必须按现有普通日志的访问控制和加密备份策略处理。 - S3/R2 存储、Cron 计划、保留策略与备份记录都保存在 PostgreSQL(`backup_settings` / `backup_records`),备份记录行在删除成功后硬删除,操作历史进入 `admin_audit_events`。 +- 手工备份的 `expiresInDays` 在创建时生成独立 `expires_at`;计划备份按当前 `retentionDays` 生成 + `expires_at`,并同时受 `retentionCount` 清理规则约束。到期只进入删除流程,不构成在线恢复点。 ### 人工恢复数据库 @@ -224,7 +229,7 @@ Release 必须提供当前 OS/架构的 `codex-proxy-rs___.ta pg_restore --no-owner --no-privileges --dbname= .dump ``` -恢复期间与恢复后的边界(见 `docs/s3-backup-design-audit.md` §8): +恢复期间与恢复后的边界见 [架构文档](../docs/architecture.md#11-生命周期安全与恢复)。操作顺序为: 1. 恢复期间以维护模式启动,不运行备份 Worker(避免把快照中的中间状态当作当前任务继续处理)。 2. 恢复完成后清理备份控制记录的非终态(`queued/dumping/uploading/deleting`),并重新计算 diff --git a/docs/api.md b/docs/api.md index 0a184c9c5..b40aa3c1f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -47,7 +47,9 @@ Client Key 通过账号分组限定路由范围:未绑定分组时可使用全 ### 管理写入一致性 -Admin API 不暴露全局配置版本,mutation 请求也不要求客户端提供全局配置版本。会改变路由快照或安全配置的写入由后端在事务内推进内部配置 revision,并用于快照发布与审计。 +mutation 请求不要求客户端提供全局配置版本。会改变路由快照或安全配置的写入由后端在事务内推进 +内部 `config_revision`,并用于快照发布与审计。账号更新和分组查询/写入的部分响应会返回 +`configRevision` 作为已提交事实,但它不是客户端 mutation 的前置条件。 ## 2. 健康检查 @@ -64,8 +66,8 @@ Responses 和 Images HTTP body、WebSocket message 和 frame 不设置网关私 | `POST` | `/v1/responses` | OpenAI Responses JSON;`stream=true` 返回 SSE,否则返回完整 JSON | | `GET` | `/v1/responses` | 通过 HTTP Upgrade 建立 Responses WebSocket | | `POST` | `/v1/responses/review` | 使用同一 Responses 合同发起 review 子代理请求 | -| `POST` | `/v1/images/generations` | 按当前 Codex Provider 发起图像生成;JSON 请求与响应正文原样转发 | -| `POST` | `/v1/images/edits` | 按当前 Codex Provider 发起图像编辑;JSON 请求与响应正文原样转发 | +| `POST` | `/v1/images/generations` | 通过 OpenAI Provider 发起图像生成;JSON 请求与响应正文原样转发 | +| `POST` | `/v1/images/edits` | 通过 OpenAI Provider 发起图像编辑;JSON 请求与响应正文原样转发 | | `GET` | `/v1/models` | 返回当前 Client Key 账号范围内各 Provider 的可用公开模型并集;有两种响应形态,见下 | | `GET` | `/v1/models/catalog` | 返回 Codex 客户端使用的模型目录 | | `GET` | `/v1/models/{model_id}/info` | 返回 Codex 客户端使用的单模型信息 | @@ -77,9 +79,9 @@ Responses 和 Images HTTP body、WebSocket message 和 frame 不设置网关私 OpenAI 路径保留客户端 Responses wire 语义:请求 body 的未知字段和字段顺序保持不变(受控模型 映射除外),HTTP SSE 与 WebSocket 的上游业务事件字节原样转发,response ID 按 opaque 值处理而不 假设 UUID 或固定长度;OpenAI 上游错误 envelope 和允许下发的 opaque header 值也不由 canonical -观测结果重写。Images 请求不读取或重建 JSON,只在原始字节之外完成账号选择、鉴权头替换和端点路由; -成功与失败响应正文同样保持原始字节。xAI 是 Grok wire 与 Responses wire 之间的协议转换层,转换只在 -xAI Provider 内完成。 +观测结果重写。Images 请求不读取或重建 JSON,也不要求或映射模型字段;它固定使用 OpenAI Provider, +只在原始字节之外完成账号选择、鉴权头替换和端点路由,成功与失败响应正文同样保持原始字节。xAI 是 +Grok wire 与 Responses wire 之间的协议转换层,转换只在 xAI Provider 内完成。 上游结构化错误的 message/code/type 会透传给客户端,其中内嵌的账号指纹 UUID 已脱敏。模型映射是 全局精确映射,未命中时模型名原样交给候选 Provider;分组只限定账号集合,不参与模型改名。 @@ -103,12 +105,15 @@ xAI Provider 内完成。 | `GET` | `/api/admin/accounts/export` | `accountIds`、`confirm=export_sensitive_accounts` | 显式导出最多 200 个账号的敏感 Provider 文档 | | `POST` | `/api/admin/accounts/import` | `{ provider, data }` | 导入或按上游身份更新账号;新账号保持未分组,已有账号保留所属分组 | | `POST` | `/api/admin/accounts/refresh` | `{ accountId }` | 手工刷新 OAuth credential(`idToken` / `accessToken` / `refreshToken`),不刷新额度 | +| `POST` | `/api/admin/accounts/recover` | `{ accountId }` | 管理员显式清除该账号的本地错误/额度/cooldown 事实并重新启用,不访问上游 | | `POST` | `/api/admin/accounts/rotate` | OpenAI rotation 字段 | 手工替换 OpenAI OAuth token | | `POST` | `/api/admin/accounts/update` | `{ accountId, enabled, concurrencyLimit, weight, groupIds }` | 一次更新账号调度状态、并发上限(`null` 表示继承运行参数)、权重(1–100)与所属分组 | | `POST` | `/api/admin/accounts/batch-update` | `{ accountIds, enabled, concurrencyLimit, weight, groupIds }` | 一次事务统一更新所选账号的全部调度字段与完整分组集合 | | `POST` | `/api/admin/accounts/delete` | `{ provider, accountIds }` | 批量删除 1–200 个账号 | | `GET` | `/api/admin/accounts/quota` | `accountId` | 读取当前额度,不强制访问上游 | | `POST` | `/api/admin/accounts/quota/refresh` | `{ accountId }` | 访问 Provider 并刷新额度,同时同步额度所属状态 | +| `GET` | `/api/admin/accounts/reset-credits` | `accountId` | 查询 OpenAI 上游主动额度重置卡,不读取本地库存 | +| `POST` | `/api/admin/accounts/reset-credits` | `{ accountId, creditId?, redeemRequestId }` | 使用 UUIDv4 幂等键消费一张 OpenAI 上游重置卡 | | `GET` | `/api/admin/accounts/models` | `accountId` | 优先读取该 Provider + 套餐的模型 cache,缺失时有限实时拉取 | | `POST` | `/api/admin/accounts/models/refresh` | `{ accountId }` | 强制拉取最新模型并覆盖 cache | | `GET` | `/api/admin/accounts/connection-test` | `accountId`、`modelId` | 通过 SSE 返回实时连接测试事件,不作为业务 Responses 用量记录 | @@ -123,14 +128,34 @@ xAI Provider 内完成。 - `sortBy`: `email`、`status`、`planType`、`usage`、`lastUsedAt`、`expiresAt`; - `sortDirection`: `asc`、`desc`。 -导入的 `data` 必须是 JSON object,内部 schema 由目标 Provider 独占解释: +导入的 `data` 必须是 JSON object,Admin API 请求上限为 64 MiB;Provider 可以收紧限制, +当前 xAI 导入上限为 16 MiB。内部 schema 由目标 Provider 独占解释: -- OpenAI 接受 OAuth 账号文档、账号 bundle 和 Agent Identity; +- OpenAI 接受单账号 OAuth 文档、`accounts` 数组(最多 200 项)、CPR 账号 bundle 和 Agent Identity; +- OpenAI OAuth token 字段只识别 `accessToken`、`refreshToken`、`idToken`,可以嵌套在账号 object 内; + 每项至少包含 AT 或 RT。RT-only 会在导入时换取 AT,AT-only 不具备自动续期能力; - xAI 从单账号 object 或 `accounts` 数组中提取 OAuth token;包装中的代理、并发、优先级等字段不参与认证; - xAI 批量导入逐条独立校验:失败条目跳过并记录日志,不中断其余条目,仅当没有任何条目成功时整个导入才报错; - xAI API Key 不是受支持的账号 credential; - 导入不会只凭文件外形写入账号;目标 Provider 使用认证材料完成必要的 token exchange 或已认证账号资料补全。 +管理端的 OpenAI `AT` / `RT` 标签是同一导入 API 的输入便利层:每行一个 token,最多 200 行,提交前 +转换为对应的 `accounts` JSON。Admin API 本身不接收纯文本 token 列表。例如: + +```json +{ + "provider": "openai", + "data": { + "accounts": [ + { "accessToken": "eyJ..." }, + { "accessToken": "eyJ...", "refreshToken": "rt_...", "idToken": "eyJ..." } + ] + } +} +``` + +RT-only 使用同一形状,只提交 `refreshToken`。不得把真实 token 写入日志、issue、fixture 或文档。 + 账号导入与 OAuth complete 不接收 `groupIds`。首次创建的账号保持未分组;按既有上游身份重新导入、 重新授权以及普通 credential refresh/rotation 均保留已有分组。分组关系只通过账号编辑维护。 账号列表的每个 item 返回轻量 `groups: [{ id, name, enabled }]`。 @@ -200,6 +225,9 @@ OAuth start 使用: 但上游 authorization code 本身通常只能交换一次;已完成过 token exchange 时应重新创建 OAuth flow。 - `GET /accounts/quota` 只读取最后一次落库快照;`POST /accounts/quota/refresh` 才访问上游。access token 已过期时,额度刷新要求先走 credential 刷新或重新授权,不会拿过期 token 探测额度。 +- `POST /accounts/recover` 是管理员对本地事实的强制恢复:它清除 Redis cooldown 和已保存的额度/错误, + 把账号重新启用并恢复为可调度 credential;它不验证上游账号是否已经恢复,下一次真实请求仍可重新写入 + 失败事实。 - 成功额度观测会 revision-fenced 写入 quota;明确 `Allowed` 投影为 `normal`,明确耗尽投影为 `quota_exhausted`。额度观测不会清除凭据过期、无效或封禁事实;这些事实统一投影为 `error`,并由 `errorReason` 区分。额度接口的 401/403 也不足以判定 refresh token 永久失效,credential 终态只由 @@ -214,6 +242,43 @@ OAuth start 使用: 新状态不符合当前筛选,该行从当前页移除。请求驱动或后台任务产生的状态变化,需要下一次显式查询账号 列表后才会显示。 +### OpenAI 主动额度重置卡 + +`GET /api/admin/accounts/reset-credits?accountId=...` 每次都查询 OpenAI 上游;后端不把卡片列表写入 +PostgreSQL 或 Redis。管理端只在用户打开弹窗或点击刷新时调用,并在当前浏览器会话内缓存最近一次成功 +结果,用于账号行上的 `xN` 提示。 + +查询响应: + +```json +{ + "availableCount": 1, + "credits": [{ + "id": "credit_...", + "status": "available", + "title": "...", + "expiresAt": "2026-08-31T12:00:00Z", + "resetType": "..." + }] +} +``` + +消费请求的 `redeemRequestId` 必须是小写、带连字符的 canonical UUIDv4;`creditId` 可省略,由上游选择 +可用卡。一次请求发出后若传输结果不明确,重试必须复用完全相同的 `redeemRequestId`、`creditId` 和 +账号。服务在单副本进程内按账号串行消费,并在 credential 需要刷新时以同一命令重试一次;它不会对不明 +结果自动创建新消费。 + +```json +{ + "accountId": "acct_...", + "creditId": "credit_...", + "redeemRequestId": "8fbf302d-11df-4bd5-82e4-08e4b3df7874" +} +``` + +消费响应只返回上游结果 `code` 和可选 `credit`。消费端确认成功后应重新 GET 卡片列表,并显式调用 +`POST /api/admin/accounts/quota/refresh` 回读官方额度;不得直接改写本地 `resetAt`。xAI 不支持该能力。 + ## 6. 账号分组 分组是 Provider-neutral 的账号集合;一个组可包含任意 Provider 账号,一个账号也可属于多个组。 @@ -327,7 +392,8 @@ id, triggerKind, status, scheduledAt, objectKey, sizeBytes, sha256, attemptCount errorCode, errorMessage, startedAt, completedAt, expiresAt, createdAt, updatedAt ``` -`expiresAt` 为创建时确定的手动过期时间;到期后由 Worker 自动清理(手动与计划备份均可设置)。 +`expiresAt` 在创建时确定:手动备份来自 `expiresInDays`,计划备份来自当时的 +`retentionDays`;到期后由 Worker 进入删除流程。 连接测试响应: diff --git a/docs/architecture.md b/docs/architecture.md index 3e5c0700a..cecdd7a09 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,306 +1,259 @@ # Codex Proxy RS 架构 -本文是仓库唯一架构文档,只描述当前架构。 +本文回答系统由哪些边界组成、请求如何流动、状态由谁拥有,以及修改时必须保持哪些不变量。 +具体 HTTP 字段见 [接口文档](api.md),部署参数见 [部署文档](../deploy/README.md);上游 URL、超时、 +重试间隔和 UI 布局属于源码或配置,不在架构文档重复维护。 -## 1. 系统边界 +## 1. 系统定位 -Codex Proxy RS 是单进程、单副本部署的多 Provider AI 网关: +Codex Proxy RS 是单进程、单副本运行的多 Provider AI 网关,同时提供: -- 客户端面提供 OpenAI Responses 兼容的 JSON、SSE、WebSocket 与模型目录协议。 -- 管理面提供 `/api/admin/*` 和 Vue 静态管理端。 -- 当前 Provider 为 OpenAI 与 xAI;账号 credential 均由所属 Provider 独占解释。 -- OpenAI 行为以重构前正式实现为语义基准;xAI/Grok 行为以 `grok2api` 验证结果为基准。 -- 两个 Provider 的请求画像都由启动配置提供基线,并通过进程级共享状态向每次新请求发布一致快照。OpenAI Desktop 与 xAI CLI 的官方版本检查会分别原子更新自己负责的版本字段。 -- OpenAI Desktop 使用官方 appcast,并从同一 ZIP 的内嵌 Core 做有界 Range 探测;xAI CLI 使用官方 npm 包 `@xai-official/grok`。检查失败保留上一份成功画像;检查成功后 OAuth、catalog、quota/billing、inference 与 Dashboard 自动采用最新版本。OpenAI 制品画像写入 Redis 单键并使用 TTL,避免按发布次数堆积。 -- PostgreSQL 是业务与配置事实的唯一权威存储。 -- Redis 只保存可丢失、可重建或有自然过期时间的协调状态。 +- 面向客户端的 OpenAI Responses、Images 和模型目录协议; +- 面向管理员的 `/api/admin/*` 控制面和 Vue 管理端; +- OpenAI 与 xAI 两个编译期 Provider; +- PostgreSQL 持久化、Redis 协调状态以及 S3/R2 数据库备份。 -部署假定:单副本。带 lease 的周期任务只做单周期互斥而非跨周期 leader 选举,N 副本会让同一任务最坏以 N 倍频率执行(见第 10 节);自更新只替换并重启处理该请求的那个副本,其余副本停留在旧版本。不要以多副本方式运行本网关。 +系统不提供 `/v1/chat/completions`,不存在 Provider Instance 层,也不支持通过复制应用容器进行多副本 +扩容。Client Key 限定账号分组,而不是绑定某个 Provider;一次请求的 Provider 候选由账号范围、模型 +能力和运行时健康共同决定。 -Gateway Engine 不识别具体 Provider 协议;Provider 不拥有客户端 admission、跨 Provider 路由或业务重试预算。 +## 2. 运行拓扑 -## 2. Workspace 与依赖方向 +```mermaid +flowchart LR + Client[API Client] --> API[gateway-api] + Browser[Vue Admin] --> API -```text -backend/ -├── apps/gateway/ composition root(Bundle 装配) -├── crates/gateway-core/ operation、routing、engine、policy、accounting -├── crates/gateway-protocol/ 可共享 wire contract 与 canonical event -├── crates/gateway-admin/ 管理领域、用例、抽象端口与备份 Worker 策略 -├── crates/gateway-store/ PostgreSQL、Redis、S3/R2 与 pg_dump adapter -├── crates/gateway-api/ OpenAI Responses 与 Admin HTTP adapter -├── crates/gateway-host/ 配置与日志、HTTP serve/drain、worker 运行时、自更新 -├── crates/providers/openai/ OpenAI credential、catalog、transport -├── crates/providers/xai/ xAI/Grok credential、catalog、transport -└── migrations/ 编号迁移与冻结清单 .frozen-sha256 + API --> Core[gateway-core] + API --> Admin[gateway-admin] + + Core --> Registry[Provider Registry] + Admin --> Registry + Registry --> OpenAI[provider-openai] + Registry --> XAI[provider-xai] + OpenAI --> OpenAIUpstream[OpenAI upstream] + XAI --> XAIUpstream[xAI upstream] + + Core --> Store[gateway-store] + Admin --> Store + OpenAI --> Store + XAI --> Store + Store --> PG[(PostgreSQL)] + Store --> Redis[(Redis)] + Store --> Object[(S3 / R2)] + + Host[gateway-host] -. lifecycle / workers / update .-> API ``` -依赖规则: +`backend/apps/gateway` 是唯一组合根,按 Host → Store → Provider → Core → Admin → API → Worker 的顺序 +初始化具体实现。其余 crate 只暴露自己的配置、端口和 Bundle,不自行定位别的实现。 -- `gateway-core` 不依赖 HTTP、数据库、Redis 或具体 Provider。 -- `gateway-protocol` 不依赖其他 workspace crate。 -- Provider crate 之间禁止互相依赖。 -- `gateway-api` 只面向 Admin/Core/Protocol 抽象,不导入具体 Provider。 -- `gateway-store` 实现 Admin/Core 端口与备份基础设施(S3/R2、`pg_dump`、受控暂存),但不拥有业务策略。 -- 只有 `apps/gateway` 组合具体实现。 +## 3. Workspace 边界 -依赖 DAG 主要由上述约定与评审维护,机器校验只覆盖两处:`gateway-api` 的 architecture 测试冻结自身源码树与测试树,并禁止 manifest 出现 store/Provider/基础设施依赖(禁止清单不含 `gateway-host`);`apps/gateway` 的 architecture 测试冻结应用源码树、禁止生产测试挂载并约束 bootstrap 只做装配。其余 crate 没有 architecture 测试。 +| 路径 | 责任 | +| --- | --- | +| `backend/apps/gateway` | 读取顶层配置、连接 Bundle、注册 Provider 与 Worker | +| `gateway-protocol` | 跨层共享的 wire contract 和 canonical event,不依赖其他 workspace crate | +| `gateway-core` | operation、请求快照、路由、admission、attempt 协调、交付边界和计量 | +| `gateway-admin` | 管理领域、用例、Provider/Store 端口、审计语义和备份策略 | +| `gateway-api` | HTTP/WS/SSE 解码与交付、Admin wire、静态管理端;不直接访问 Store 或具体 Provider | +| `gateway-store` | PostgreSQL、Redis、S3/R2、`pg_dump` 适配器;不拥有业务策略 | +| `gateway-host` | 配置加载、日志、HTTP 生命周期、Worker 监督和系统更新 | +| `providers/openai` | OpenAI OAuth/Agent Identity、账号选择、目录、额度、Responses/Images transport | +| `providers/xai` | xAI OAuth session、账号选择、目录、额度和 Grok/Responses 转换 | +| `frontend` | Vue 管理端,仅通过 Admin API 读写状态 | -## 3. 请求执行链 +依赖方向遵守四条规则: -```text -API decode/auth - -> freeze RuntimeSnapshot - -> compile RoutePlan - -> admission - -> enqueue model_request observation - -> select Provider/account - -> enqueue attempt/send observations - -> cross upstream send barrier - -> canonical stream - -> cross in-memory downstream delivery barrier and enqueue observation - -> enqueue terminal observation and accounting +1. Core 不依赖 HTTP、数据库、Redis 或具体 Provider。 +2. Provider 之间不互相依赖,也不决定跨 Provider fallback。 +3. API 只调用 Core/Admin 抽象;Store 只实现端口。 +4. 具体实现只在组合根相遇。 + +`gateway-api` 与 `apps/gateway` 的 architecture tests 冻结关键源码树和依赖禁区;其余边界由 manifest、 +集成测试和评审共同维护。 + +## 4. 数据面请求生命周期 + +```mermaid +sequenceDiagram + participant C as Client + participant A as API Adapter + participant E as Core Engine + participant P as Provider + participant S as Store / Ops queue + + C->>A: authenticated request + A->>E: Operation + client context + E->>E: freeze snapshot and compile routing plan + E->>S: enqueue request / attempt observations + E->>P: one candidate, one credential, one attempt + P-->>E: cold canonical stream + raw wire + E->>E: enforce send and downstream commit barriers + E-->>A: committed response stream + A-->>C: JSON / SSE / WebSocket + E->>S: enqueue terminal observation and accounting ``` +请求开始时冻结 `RuntimeSnapshot`、Client Key 的账号范围、模型映射、Provider 候选顺序和调度策略。 +运行中的请求不再拼接新旧配置,也不在热路径查询分组关系。 + 核心不变量: -1. 每个客户端请求只有一条 `model_requests`;attempt 明细保存在其 JSON 事实中,不另建 attempt 表。 -2. 任何可能到达上游的调用都必须先按序提交 request/attempt 观测到有界队列;PostgreSQL 投影是可恢复 - 观测,不是数据面发送的同步前置条件。3. `not_sent`、`sent`、`ambiguous` 是不同的上游发送边界;发送状态是请求级单调水位(`sent` > `ambiguous` > `not_sent`),跨 attempt 只升不降,终态写回不得低于水位;`ambiguous` 不自动重放。 -4. 内存中的 downstream commit 表示网关已作出不可撤回的交付承诺,从该时刻禁止 retry/fallback; - 它在实际首字节写出前越过并按序入队。`downstream_committed_at` 是该事实的 PostgreSQL 观测投影, - 不宣称字节已经到达客户端,也不反向决定协议交付。 -5. Provider 每次 `execute` 只能选择一个 credential 并准备一个 cold stream,不得隐藏换号或业务 retry。 -6. 下游 commit 前,Core 先在当前 Provider 内按冻结策略换账号;只有调用仍处于 `not_sent`、错误属于 - 可安全推进的资源/基础设施类别且 operation 允许重放时,才推进到路由计划中的下一个 Provider。 -7. 本架构不通过隐式连接复用承载业务身份。HTTP client 可安全复用 transport 连接,但账号、credential revision、cookie/session binding 必须显式绑定到本次调用。 +- 一个客户端请求对应一条 `model_requests`;attempt 是请求内事实,不建立第二张权威表。 +- Provider 的一次 `execute` 只选择一个 credential 并返回一个冷流;换号、重试和 fallback 由 Core 决定。 +- `not_sent`、`sent`、`ambiguous` 是单调的上游发送边界;结果不明确时不能假定上游未收到请求。 +- downstream commit 是不可撤回的交付承诺。commit 后禁止换号、重试和 fallback。 +- 跨 Provider 只在账号范围和能力都允许,且请求尚未到达上游或已被证明可安全重放时发生。 +- 可恢复观测写入失败不能替换已经确定的客户端协议结果。 -管理端 Usage 详情中的 attempt 列表是 best-effort 观测(见 adr-002):中间失败来自 `ops_events`,最终尝试由 `model_requests` 合成,二者都不承诺完整(队列丢弃、写入失败、非失败观测不落库会造成缺口)。API 通过 `attemptsComplete: false` 显式标注这一语义,不再声称「全部尝试」。 +Responses 按模型目录编译候选;全局模型映射是精确映射,未命中时模型名原样交给候选 Provider。 +Images 是 OpenAI Provider 自有端点:它不要求模型字段、不参与文本模型映射,只在 Client Key 的账号范围 +确实包含 OpenAI 账号时生成单一 OpenAI 候选。 -## 4. 路由、fallback 与错误处理 +## 5. Provider 与协议边界 -RuntimeSnapshot 冻结 Provider 集合、模型能力、全部账号及 membership 索引、每个 Client Key 的账号 -范围、运行时策略和全局 `config_revision`。Key 零分组关联表示 `AllAccounts`;存在关联时表示 -`Restricted`,只允许已启用绑定组成员的并集。已绑定组全部禁用或为空会得到空池,绝不回退到全部账号。 -一次请求始终使用认证时取得的同一账号范围和快照,不在执行中拼接新旧配置,热路径也不查询 membership。 -账号调度事实包含可空的并发上限覆盖与 1–100 的权重;并发上限为空时继承运行参数,权重默认 1。 -管理员单账号或批量编辑在同一事务中替换启停、并发上限、权重和完整分组集合;credential 导入、 -重新授权与后台刷新不覆盖已有调度事实。 +Core 只理解 `Operation`、能力要求、Provider 候选、稳定错误和 canonical event,不读取 Provider SDK +类型。Provider 独占 credential schema、OAuth、账号选择、模型目录、额度投影和上游 transport。 -模型先经过全局精确映射,再按账号范围内实际存在的 Provider 及其明确模型/operation 能力编译有序候选。 -普通请求和 `ReplayAny` 只有在 Provider 尚未发出请求,且返回无可用账号、账号容量不足或 Provider -基础设施不可用时,才可推进到下一 Provider;已建立流后的重试先在当前 Provider 内换账号。任何发送结果 -不明确或 downstream 已 commit 的请求都不跨目标重放。 +- OpenAI 是透明边界。Responses 请求保留未知字段和字段顺序;SSE、WebSocket 与 Images 的业务正文 + 按原始字节转发。canonical facts 从同一数据旁路提取,只用于路由、观测和计费。 +- xAI 是翻译边界。Provider 把 Grok wire 转换为 Responses wire;上游结构化错误的 message/code/type + 可以透出,但账号指纹会先脱敏。 +- response ID 是不透明 UTF-8 bytes,不假设 UUID、固定长度或跨 Provider 可复用。 +- 请求画像以配置为启动基线。OpenAI Desktop 与 xAI CLI 的官方版本检查只更新各自负责的运行时画像, + 不回写 `config.yaml`。 -明确的上游认证、封禁、额度或 cooldown 错误会更新对应账号状态,使下一次选择排除该账号;满足重放安全条件且尚未 downstream commit 时,可以换号。传输结果不明确时不得假定请求未到达上游。 +## 6. 路由、账号范围与 continuation -重试矩阵属于独立策略,OpenAI 保持原正式行为、xAI 保持参考行为,不在 Provider transport 内增加额外重试。 +Client Key 与账号分组形成授权范围: -## 5. Continuation +- 没有分组关联表示 `AllAccounts`; +- 有关联时只允许已启用分组成员的并集; +- 已绑定分组为空或全部禁用时得到空池,绝不回退为全部账号; +- 分组可以包含多个 Provider,账号也可以属于多个分组。 -系统在 `model_requests.client_response_id` 与 `upstream_response_id` 中记录响应生命周期确认的 ID, -两者都按 opaque UTF-8 bytes 保存,不做格式、长度或唯一性推断;不新增 conversation、transcript、 -continuation 或 claim 表。 +账号选择综合启停状态、credential/quota 事实、Redis cooldown、并发上限、权重、请求间隔和会话亲和。 +账号编辑在一个事务中替换完整调度事实;导入、重新授权和后台刷新不会覆盖已有分组、权重或并发设置。 -- `store=true`:使用 Provider 持久化的 native handle。 -- `store=false`:opaque replay state 仅存在于活连接内,不落 PostgreSQL。 -- OpenAI continuation 顺序为 native、replay owner、replay any。`replay any` 是 - Provider 定义的恢复阶段,不代表 Core 已持有完整 input:OpenAI 在跨账号时保留 - canonical `previous_response_id` 与原始增量 input 做 continuation probe;上游返回 - `previous_response_not_found` 时原样交给官方客户端触发 full retry。只有确实持有 - 可携带 transcript 的 Provider 才能清除 native handle 后在代理内部重放。 -- xAI 使用客户端提交的完整历史作为已验证 continuation 路径。 -- native continuation pin 同时绑定创建它的 Client Key、冻结账号范围、Provider 和账号;跨 Key 复用、 - scope 外账号或计划中不存在该 Provider 都 fail closed。`ReplayOwner` 同样固定原 Provider 和账号, - 不参与跨 Provider fallback。 -- continuation 失败仍受 send barrier、downstream commit 和 Provider kind 边界约束。 +Continuation 仍受原请求的 Client Key、账号范围、Provider 和发送/交付边界约束: -## 6. Provider 与 credential owner +- native continuation 固定创建它的 Provider 与账号; +- OpenAI 按 native → replay owner → replay any 推进,并保留官方 `previous_response_id` 语义; +- xAI 使用客户端提交的完整历史作为重放输入; +- scope 外账号、跨 Key 复用或不明确发送结果均 fail closed。 -`Provider` 接收 canonical `Operation + ProviderCandidate + AttemptContext`,返回携带冻结 metadata 的 canonical cold stream。Registry 使用稳定 `ProviderKind` 查找实现。 +会话亲和是优先选择提示,不是硬账号绑定;native continuation 才携带不可跨越的 owner 约束。 -Provider 独占以下职责: +## 7. 控制面与 revision -- credential 文档的编码、解码与校验; -- OAuth 登录、导入、刷新与轮换; -- 账号选择所需的 Provider 私有事实; -- catalog 查询与能力编译; -- HTTP/WebSocket transport 和错误分类; -- quota 投影与 Provider 私有观测字段。 - -客户端可见正文的立场按 Provider 不同: - -- OpenAI 是逐字节透明代理:请求 body 的未知字段和字段顺序保持不变(受控模型映射除外),上游 wire 是客户端可见的事实来源,HTTP SSE 帧以原始字节原样下发,WebSocket 上游 JSON 文本原样下发;response ID 以 opaque bytes 存储,不假设 UUID 或固定长度。canonical facts 只从同一帧旁路解析,用于观测、亲和与计费。仅网关内部帧(rate limits、metadata)被就地消费,不向客户端转发。 -- xAI 是翻译层:把 Grok wire 解码后重新合成 Responses wire 事件,不承诺字节透明;上游错误按结构化 message/code/type 透出,message 中的 UUID 账号指纹替换为占位符。 - -credential 更新使用 `credential_revision` CAS。PostgreSQL 分别保存凭据事实与额度访问事实;带截止时间的 -429 cooldown 只保存在 Redis,是唯一的临时冷却运行时事实,不进入 Provider quota 展示模型。Store 把三类 -事实交给 Core 的唯一解析器,投影为 `normal`、`quota_exhausted`、`rate_limited`、`disabled` 或 `error`; -投影为 `rate_limited` 时同时携带有效的 `rate_limited_until`,到期重算后该时间和状态一起消失。若没有其它 -阻塞事实,账号才恢复为 `normal`。成功推理或明确的成功额度观测可以写入 `Allowed`;本地时钟与不确定响应 -不能伪造恢复。推理请求的账号/model cooldown 只约束数据面调度,不参与 OAuth refresh;OpenAI 与 xAI 的 -OAuth refresh 请求状态不明确时只按 OAuth 自身的有界退避推进该 credential 的 `next_refresh_at`:前五次 -约为 5/15/45/135/300 秒,之后约每 10 分钟恢复;仍有效 Access Token 时不晚于其到期时刻,Access Token -到期后仍按该节奏恢复,至“到期 + 24 小时”才终态化。自动 refresh 的瞬态失败(限流、5xx、超时、畸形 -响应)保留现有凭据并按指数退避推迟下一次刷新,不把账号标记为失效;上游明确的永久错误 -(`invalid_grant`、封禁)仍立即写入终态失效。 - -`refresh_token_expires_at` 不是公共 SQL 列或 Core 权威状态。xAI 可在 `provider_credentials_json` 内保存它作为 Provider 私有提示;真正失效以 refresh endpoint 返回的永久错误为准。 - -OpenAI OAuth 导入先取得 access token(仅有 refresh token 时先交换),再复用官方 -`token_data.rs::parse_chatgpt_jwt_claims` 的本地 JWT payload 解析逻辑:优先读取 `idToken`,字段缺失时 -由 `accessToken` 补齐。解析器只读取 `email`、`https://api.openai.com/profile` 与 -`https://api.openai.com/auth` 中的官方 claims,不调用 `whoami`,也不使用导入文档顶层的 -`userId/accountId` 补身份。两个令牌都没有用户身份 claims 时,账号以未验证错误状态入库。首次 OAuth -返回与当前 Codex Desktop 一致的两层登录地址:内层使用 `auth.openai.com/oauth/authorize` 的 -官方参数顺序,外层使用 `https://chatgpt.com/codex/desktop-auth` 并开启 -`codex_streamlined_login`。`codex_app_version` 每次从共享 Desktop 画像快照读取; -首次授权在 10 分钟 pending flow 中生成账号级 `installation_id`,换码成功后把同一个值写入 credential。 -`source_surface_stable_id` 与 `codex_origin_stable_id` 由该 ID 经过带版本域的 SHA-256 确定性派生,并映射为 -UUIDv4;重新授权读取既有 `installation_id`,因此得到相同的 surface ID。系统不额外持久化 surface ID, -也不再从全局 HMAC 身份锚点派生;它不等同于 DeviceCheck attestation。为了对齐 macOS Desktop -交给外部浏览器的最终 `chatgpt.com` URL,Provider 同样携带 `no_universal_links=1` 浏览器路由参数。 -服务端随后仍以 `state`、PKCE -与官方 token exchange 成功后解析并保存同一 token set。重新授权的目标绑定 -只保存目标账号 ID,不接受或冻结客户端提供的 credential revision;complete 阶段重新读取目标账号及其当前 revision,并以当前 revision 做最终 CAS。 -重新授权和手工或后台 RT refresh 只轮换目标账号的 token,保留既有账号资料与 OAuth principal。 - -credential 与 quota 是两组独立事实,不是两套对外状态。主动 quota refresh 会拒绝过期 Access Token; -quota 探测的 401/403 不具备判定 Refresh Token 永久失效的证据。成功主动观测和正常推理响应中的 -rate-limit header 观测都 revision-fenced 写入同一 quota;只有明确的访问证据才写 `Allowed` 或 -`Exhausted`。只有明确的 deactivated workspace 才能由额度链路写入 `Banned` 凭据事实,且额度观测不能 -清除 `Invalid`、`Expired`、`Banned`。这些凭据事实对外统一投影为 `error` 并携带稳定原因。Free、K12 等 -套餐复用该逻辑;`plan_type` 在此不参与额度分支,只作为 Provider 事实并用于隔离不同套餐的 catalog cache。 -账号展开用量不读取 retention 范围累计值,而是选择带完整边界且可归属到账号的代表性额度窗口,并按 -`[reset_at - window_seconds, reset_at)` 聚合请求、Token、模型和成本;额度重置边界变化后,同一查询随刷新 -结果重算。模型专属窗口不会错误复用账号总用量,边界不完整时也不伪造“当前窗口”统计。 - -## 7. 控制面与内部 revision - -Admin API 不要求客户端提交配置 revision,也不向客户端暴露配置 revision。会改变调度快照或安全配置的写入,必须在同一 PostgreSQL 事务中: - -- 执行业务 mutation; -- 推进 `runtime_settings.config_revision`; -- 写入脱敏 `admin_audit_events`。 - -推进全局 revision 的 mutation 包括 runtime settings、账号分组 CRUD、客户端 Key 及其分组 -替换、账号编辑、账号导入/创建/删除、管理员显式启停和管理员 credential rotation。上述分组关系 -写入与业务 mutation、单次 revision 推进、脱敏审计处于同一 PostgreSQL 事务;审计会明确记录 -`routing_scope: groups -> all` 等权限范围变化。 - -不推进全局 revision 的运行时观测包括 quota、cooldown、catalog generation、请求统计以及自动 credential refresh;自动 refresh 只推进 `credential_revision`。提交后 Redis 通知只负责缩短其他副本的收敛延迟,周期性 PostgreSQL 对账才是正确性基础。 - -## 8. PostgreSQL 终态 - -当前 schema 由冻结的编号迁移演进:`0001_initial.sql` 创建九张基线业务表, -`0005_account_groups.sql` 再增加三张 Provider-neutral 分组关系表,合计十二张业务表: - -| 表 | 权威事实 | -| --- | --- | -| `admin_users` | 管理员身份与密码摘要 | -| `admin_audit_events` | 管理 mutation 审计 | -| `client_api_keys` | 客户端鉴权、限额与授权范围 | -| `account_groups` | Provider-neutral 分组资料与启停事实 | -| `account_group_accounts` | 分组到账号的多对多 membership | -| `client_api_key_groups` | Client Key 到分组的多对多授权;零行表示全部账号 | -| `runtime_settings` | 全局配置与 `config_revision` | -| `provider_accounts` | 账号资料、Provider-owned 明文 credential JSON、revision、凭据事实与规范化 quota 事实 | -| `model_requests` | 请求、attempt、计费、交付、恢复事实及请求开始时冻结的路由范围快照 | -| `ops_events` | 脱敏运行事件 | -| `backup_settings` | S3/R2 存储、Cron 计划与保留策略单例配置 | -| `backup_records` | 备份任务状态与归档事实(`queued/dumping/uploading/completed/failed/deleting`) | - -备份设计边界见 `docs/s3-backup-design-audit.md`:单副本部署下备份由单个可取消 -`DaemonTask` 承担调度、执行、删除收敛与保留清理,不需要 leader lease、fencing token 或 -heartbeat;计划时间点冲突只记录日志/指标并推进游标,不产生 skipped 记录;删除成功后 -记录硬删除,操作历史进入 `admin_audit_events`。管理员身份事实不重复写入 -`backup_records`。存储身份在存在记录时锁定:endpoint/region/bucket/path-style 不允许 -变化,只允许轮换凭据与修改 prefix。每份备份可设置创建时确定的手动过期时间 -`expires_at`(手动与计划备份均可,`expiresInDays` 天数生成),到期由 Worker 自动清理。 - -设计规则: - -- 一个事实只存一次;可表达关系使用真实 FK 与支持索引。 -- Provider 差异只进入受 schema/version 校验的 JSONB 边界。 -- credential JSON 以 Provider 自己的 schema 明文保存在 `provider_credentials_json`;日志、Debug、API 和 audit - 不输出其中的 secret,Provider 只在需要发往上游时把值包装为运行时 secret。 -- stale recovery 只把超时 `running` 请求收敛为 `incomplete`,不重放业务请求。 -- 数据面 execution observation 通过有界、带总字节预算的进程内队列交给 PostgreSQL OpsFlush worker; - 入队不等待数据库,队列满、worker 不可用或写入失败时记录累计统计并丢弃该可恢复观测,不能替换 - 客户端可见协议结果。队列不是第二份业务权威,也不做无幂等键的盲目重试。 -- retention 只删除已满足保留规则的历史事实,不改变运行中请求。 - -业务 schema 由编号迁移定义;`0001_initial.sql` 是当前大版本基线,后续冻结迁移只向前演进。其中 -response ID 使用无格式假设的 UTF-8 bytes,`model_requests.service_tier` 保存响应观测到的服务档位, -`routing_scope` 与 group ID/name 快照保存请求开始时的 `all`/`groups` 授权事实,迁移前历史明确标为 -`legacy_provider`, -账号对外五态由 PostgreSQL 凭据/额度事实与 Redis 429 冷却通过 Core 唯一解析器动态派生, -不保存重复的 `status` 列。同一大版本内已合入的迁移永久冻结—— -`.frozen-sha256` 是冻结清单,CI 校验清单相对 PR base 只增不改,启动时 sqlx 重校验已应用 -迁移的 checksum,不一致直接拒绝启动。规则见 `backend/migrations/README.md`。 - -## 9. Redis 终态 - -Redis 保存的协调状态恰好是:客户端 admission 热状态、credential lease/fencing 与调度信号、账号 429 cooldown 与 credential state 可重建缓存、catalog 缓存、Provider circuit、会话亲和与会话级账号排除、continuation pin、OAuth pending flow、worker leader lease以及 runtime change 通知。会话亲和和 round-robin 游标按 Client Key + Provider 隔离;continuation payload 带 Client Key 所有权并在读取时校验。账号 lease、quota、cooldown 和请求间隔仍按真实账号全局共享。Redis 丢失后必须从 PostgreSQL 或 Provider 事实恢复;恢复完成前需要保护的 acquire 路径 fail closed。 - -多副本部署时,跨副本协调面只有上述 Redis 状态与 PostgreSQL 事实;WebSocket 连接池、请求画像与 RuntimeSnapshot 副本都是进程内状态,其中 RuntimeSnapshot 依靠周期对账收敛,请求画像依靠各自的官方版本检查收敛。 - -OAuth pending key: +管理写入遵循统一流程: ```text -codex-proxy-rs:oauth-pending:v1:{provider_kind}:{flow_fingerprint} -flow_fingerprint = SHA-256(provider_kind || 0x00 || raw_flow_binding) +HTTP validation + -> Admin use case + -> Provider prepare/verify when needed + -> PostgreSQL transaction + audit + -> publish committed runtime snapshot + -> notify Provider-derived caches ``` -创建时的基础 Hash 字段为: +会改变路由快照或安全配置的 mutation 在同一 PostgreSQL 事务中提交业务事实、推进内部 +`config_revision` 并写入脱敏审计。Admin mutation 不要求客户端提交 revision;少数账号/分组响应 +返回已提交的 `configRevision`,不将它当作乐观并发前置条件。 -- `owner_fingerprint` -- `expires_at_epoch_seconds` -- `provider_payload` +额度、cooldown、目录 generation、请求统计和自动 credential refresh 属于运行时观测,不推进全局 +revision;credential 轮换只推进账号自己的 `credential_revision`。Redis 通知用于缩短收敛延迟, +PostgreSQL 周期对账才是正确性基础。 -complete 先通过 Lua 原子 claim,并临时增加 `claim_fingerprint` 与 -`claim_expires_at_epoch_millis`。owner mismatch 不删除记录;有效 claim 阻止并发 complete,失败时只有 -claim owner 可以释放,账号事务提交后才原子删除整个 pending key。OpenAI TTL 为 10 分钟,xAI TTL -为 30 分钟。 +## 8. 状态所有权 -## 10. 后台任务与恢复 +| 状态 | 唯一权威 | 说明 | +| --- | --- | --- | +| 账号、credential、分组、Client Key、设置、审计、请求与备份记录 | PostgreSQL | 业务持久化事实 | +| admission、lease、cooldown、circuit、会话亲和、continuation、OAuth pending、目录 cache | Redis | 可重建、可过期的协调状态 | +| 日志、OAuth 恢复记录、在线更新状态、备份暂存 | `.runtime/` | 部署节点本地运行文件 | +| 重置卡库存与消费结果 | OpenAI upstream | 后端不建立本地卡库存;前端只保留当前浏览器会话的最近查询 | +| Provider 公开模型与请求画像 | Provider/runtime cache | 由官方目录或发布源刷新,不写成第二份业务配置 | -HTTP serve/drain 与 worker 运行时都在 `gateway-host`:Host 负责注册校验、leader lease、失败退避、健康暴露与关闭;`apps/gateway` 的 bootstrap 只装配。WorkerContribution 由 Store、Core、Admin 与两个 Provider bundle 显式贡献,host bundle 自身不贡献任何 worker: +账号对外状态不是独立列,而是 PostgreSQL credential/quota 事实与 Redis cooldown 的统一投影: +`normal`、`quota_exhausted`、`rate_limited`、`disabled`、`error`。只有明确上游证据才能恢复或终态化账号, +本地时钟和不确定响应不能伪造事实。 -- Store:stale execution recovery、retention,以及 PostgreSQL execution observation、Redis client - admission release 和 Provider circuit feedback 三个 OpsFlush daemon。native continuation 直接写 Redis, - 单次 Lua 记录只清理有界数量的过期/超限索引项,避免无界阻塞。 -- Core:RuntimeSnapshot revision 周期对账(无 lease,逐副本运行)与 Redis change 长驻订阅(daemon,逐副本运行)。 -- Admin:备份 daemon(`WorkerKind::Backup`)。单副本边界下它是一个可取消 `DaemonTask`,内部循环推进 - Cron 游标、恢复中间状态、完成删除收敛、领取并执行一个 queued 任务、执行一小批保留清理;长时间 - `pg_dump` 与 multipart 上传由 daemon 自身持有,Host 只负责 panic 后重启、健康与关闭。它不使用 - Redis leader lease(单副本无竞争),只依赖 PostgreSQL 部分唯一约束防御并发 API/调度创建。 -- Provider:credential refresh、quota/catalog 健康,以及模型 etag 与官方版本 release 检查(OpenAI Desktop、xAI CLI)。 +当前大版本的十二张业务表由 `0001_initial.sql` 建立;`0002_nullable_requested_model.sql` 允许 Images +等无模型端点把 `requested_model_id` 保存为 `NULL`。已应用迁移按字节冻结,后续 schema 变化只能新增 +编号迁移,详见 [迁移规则](../backend/migrations/README.md)。 -带 lease 的周期任务在每个周期开始向 Redis 申请一次 leader lease,周期内续租,周期结束即释放。这只是单周期互斥,不是跨周期 leader 选举:各副本独立计时,N 副本部署下同一任务的实际执行频率最坏可达单副本的 N 倍。 +## 9. Credential、额度与主动重置 -自更新、回滚与进程重启不是 worker:它们是 Admin API 触发的 Host 系统操作,只替换并重启处理该请求的那个副本。 +credential 与 quota 是两组独立事实:credential refresh 不等于 quota refresh,额度接口的 401/403 +也不能单独证明 refresh token 永久失效。 -关闭时序:收到取消信号后,HTTP drain(axum 优雅关闭与游离连接等待)共享同一个从信号时刻起算的绝对截止点,逾期放弃等待存量连接;随后 worker 在独立的 shutdown 超时内 join。有界观测队列会在该阶段按各自预算 drain,逾期的可恢复观测被计数并丢弃。 +- OpenAI 支持 OAuth、AT/RT、OAuth JSON 与 Agent Identity;RT-only 导入先换取 AT,AT-only 导入没有 + 自动续期能力。OAuth 身份只从官方 JWT claims 投影,不信任导入文档顶层身份字段。 +- xAI 使用 OAuth session;API Key 不是受支持的账号 credential。 +- 新账号导入和首次 OAuth 在 credential 提交后尽力读取一次额度;失败只留下观测,不回滚账号事务。 +- quota refresh、正常推理返回的 rate-limit headers 和后台健康任务汇入同一额度事实;套餐只用于展示与 + 目录 cache 隔离,不创建套餐专属状态机。 -worker 不得通过导入 Admin use case 绕过边界,也不得维护第二份业务状态。 +主动额度重置是 OpenAI Provider 的不可逆上游操作:列表查询和消费都直接使用当前 Desktop 请求画像; +卡片不写 PostgreSQL/Redis。消费请求携带调用方生成的 UUIDv4 幂等键,同一账号的消费在进程内串行; +发送结果不明确时必须复用原键。确认成功后管理端再显式刷新卡片与 quota,不能直接改本地重置时间。 -## 11. 测试与验收 +## 10. 观测与后台任务 -- 生产 `src` 禁止 `#[cfg(test)]`、`#[path]` 和 `include!` 测试挂载。 -- 测试位于各 package 的 `tests/`,目录镜像生产模块。 -- Core 规则使用确定性测试;Provider 使用 contract/fixture 测试。 -- FK、CAS、revision、recovery 使用真实 PostgreSQL 测试。 -- admission、lease、cooldown、OAuth pending 使用真实 Redis 测试。 -- 自动化测试不得发送真实对话或轮换生产 refresh token;仓库不提供破坏性 live fixture。 -- 可选的外部 `CODEX_REAL_ACCOUNT_FILE` 官方 JWKS 签名检查仅用于显式测试,环境变量未设置时直接跳过; - 它不属于 OpenAI OAuth 导入、首次授权、重新授权或 RT refresh 流程,也不经过 selector/transport。 +请求观测通过有界进程内队列异步投影到 PostgreSQL。队列满、Store 暂不可用或进程退出超时时,允许丢失 +可恢复观测并累计指标,但不允许改变客户端响应。Usage 详情中的 attempt 因此是 best-effort,并通过 +`attemptsComplete: false` 明示不完整性。 -终态门禁: +只有完整交付客户端的成功响应进入 Token、延迟和成本聚合。实际 `service_tier` 只接受上游响应事件确认, +不能用请求期望值替代。 + +Worker 由各 Bundle 贡献、由 Host 统一监督: + +- Store:过期请求恢复、历史保留和 PostgreSQL/Redis 观测队列; +- Core:RuntimeSnapshot 周期对账和 Redis change 订阅; +- Admin:S3/R2 备份 daemon,负责调度、执行、删除收敛与保留清理; +- Provider:credential refresh、quota/catalog 健康和官方版本/etag 检查。 + +周期任务的 Redis lease 只保证单周期互斥,不构成多副本 leader 选举。备份 daemon 依赖单副本部署边界, +自更新也只替换处理请求的当前进程,因此整个应用必须保持单副本。 + +## 11. 生命周期、安全与恢复 + +启动只有在配置、PostgreSQL、Redis、Provider、Core、Admin、API 和 Worker 全部初始化成功后才进入服务。 +健康检查综合 Core、Store 与 Worker 状态,但不会把单个 Provider 的业务降级等同于整个进程失活。 + +关闭分为两段:先停止接收新连接并 drain HTTP/WS,再取消并等待 Worker;两段各有独立预算,Compose 的 +`stop_grace_period` 必须覆盖二者之和。超时后只丢弃仍未落盘的可恢复观测,不执行隐式业务重放。 + +安全边界: + +- Provider credential 以 Provider schema 的明文 JSON 保存在 PostgreSQL;数据库和备份必须按敏感数据保护。 +- OAuth 恢复日志包含原始 AT/RT,`.runtime/logs` 同样属于敏感数据。 +- 真实 secret 不进入普通日志、Debug、fixture 或 audit details;明文只能通过账号导出、Key reveal、 + 备份设置等明确的敏感 Admin 合同返回。 +- OAuth pending flow 使用有期限、带 owner 的一次性 claim;事务成功后才消费,失败释放 claim。 +- 在线更新校验 Release host、大小、SHA-256 和归档路径,并只允许同一大版本内更新。 + +PostgreSQL 备份恢复属于人工维护操作:恢复时停用备份 Worker,清理快照中的非终态任务,重算计划游标并 +重新验证对象存储;不会自动删除恢复前已经存在的远端对象。 + +## 12. 修改与验收 + +变更应落在拥有该事实的边界:协议适配进 API,执行策略进 Core,Provider 差异进对应 Provider,持久化 +实现进 Store,生命周期进 Host,管理规则进 Admin。不要用兼容 shim、第二套状态机或跨层旁路绕开 owner。 + +后端验证从 `backend/Cargo.toml` 执行,仓库根目录没有 Cargo manifest: ```bash -cd backend -cargo fmt --check -cargo clippy --all-targets --all-features --locked -- -D warnings -cargo test --test main --locked +cargo +1.97.0 fmt --all --manifest-path backend/Cargo.toml -- --check +cargo +1.97.0 clippy --manifest-path backend/Cargo.toml --all-targets --all-features --locked -- -D warnings +cargo +1.97.0 test --manifest-path backend/Cargo.toml --test main --locked +pnpm --dir frontend format:check +pnpm --dir frontend build +docker compose -f deploy/compose.yaml config --quiet ``` + +行为、配置或边界变化必须同步其唯一文档 owner:用户入口写入根 README,HTTP 合同写入 `docs/api.md`, +部署操作写入 `deploy/README.md`,架构不变量保留在本文。 diff --git a/frontend/src/views/accounts/components/AccountResetCredits.vue b/frontend/src/views/accounts/components/AccountResetCredits.vue index bd7839f1f..f163bb509 100644 --- a/frontend/src/views/accounts/components/AccountResetCredits.vue +++ b/frontend/src/views/accounts/components/AccountResetCredits.vue @@ -48,10 +48,11 @@ const modalDescription = computed(() => showConfirm.value const triggerLabel = computed(() => { if (ambiguous.value) return '查看主动重置卡,有一项操作待确认' - if (loadError.value) + if (loadError.value) { return hasSnapshot.value ? `查看主动重置卡,查询失败,最近查询 ${availableCount.value} 张可用` : '查看主动重置卡,查询失败' + } return hasSnapshot.value ? `查看主动重置卡,最近查询 ${availableCount.value} 张可用` : '查看主动重置卡' diff --git a/skills/codex-proxy-rs/SKILL.md b/skills/codex-proxy-rs/SKILL.md index 3990b8af8..bb0c1df43 100644 --- a/skills/codex-proxy-rs/SKILL.md +++ b/skills/codex-proxy-rs/SKILL.md @@ -1,46 +1,71 @@ --- name: codex-proxy-rs -description: Codex Proxy RS 仓库开发指南。Use when changing or auditing its Rust backend, Vue frontend, PostgreSQL/Redis state, account scheduling, Responses transport and recovery, telemetry, Docker deployment, CI, release, configuration, or project documentation. +description: Codex Proxy RS 仓库开发与审计指南。Use for its Rust gateway, Vue admin, OpenAI/xAI providers, account routing, Responses/Images transport, PostgreSQL/Redis state, workers, deployment, release, CI, or project documentation. --- # Codex Proxy RS -## 事实来源 +## 开始工作 -1. 以当前源码、配置和测试为准。 -2. 非简单改动读取 `references/repo-guide.md`。 -3. 涉及目录、生命周期、存储或后台任务时读取 `docs/architecture.md`。 -4. skill 或文档与实现冲突时,在同一改动中修正。 +1. 从仓库根目录检查 `git status --short`,保留用户已有改动。 +2. `.codegraph/` 存在时先用 CodeGraph 定位 owner、调用链和动态分发,再读未覆盖的源码。 +3. 先确定事实属于 API、Core、Admin、Store、Host、Provider 还是 frontend,不跨 owner 打补丁。 +4. 行为与文档冲突时以当前源码、配置和测试为准,并在同一任务中修正文档。 -存在 `.codegraph/` 时可先定位调用链;结论必须回到源码和测试验证。 +仓库根目录没有 Cargo manifest;后端命令必须进入 `backend/` 或显式传 +`--manifest-path backend/Cargo.toml`。 -## 边界 +## 文档路由 -- `gateway-api` 只做协议适配;`gateway-core` 编排;Provider 独占 credential、catalog 与 transport;`gateway-store` 实现持久化端口。 -- Provider 每次调用只选择一个 credential,不隐藏换号、业务 retry 或跨 Provider fallback。 -- PostgreSQL 是持久化权威;Redis 只保存可恢复协调状态和 OAuth pending flow。 -- 已应用的数据库迁移按字节冻结(清单 `backend/migrations/.frozen-sha256`,CI 做 append-only 校验); - schema 变更一律新增编号迁移,规则见 `backend/migrations/README.md`。 -- 当前只有 `openai` 与 `xai` 两个 Provider;账号归属 Provider,不存在 Provider Instance 层。 -- OpenAI 的 OAuth 与 Agent Identity credential 由 `providers/openai` 独占解析;账号导出使用 CPR 文档,Agent Identity - 只输出运行所需身份材料,不伪造 OAuth 字段。 -- 测试放在各 package 的 `tests/`,统一挂在单一 `main` 集成测试目标下(`--test main`)。禁止在生产 `src/` 写 test-only 代码。 -- Vue 使用 `