mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
Improve account and API key filtering, sorting, responsive layouts, table loading, usage reporting, and billing semantics across the admin interface. Remove obsolete compact-route handling, normalize model metadata, and add process-wide draining for HTTP streams and WebSockets during restart. Co-authored-by: Codex <noreply@openai.com>
576 lines
25 KiB
Markdown
576 lines
25 KiB
Markdown
# 架构说明
|
||
|
||
本文描述当前仓库的实际结构。目录调整、存储边界或请求链路发生变化时,代码和本文应在同一提交中更新。
|
||
|
||
## 系统边界
|
||
|
||
Codex Proxy RS 是一个单进程网关。Rust 进程同时提供:
|
||
|
||
- OpenAI Responses 兼容接口:`/v1/*`,包括 HTTP/SSE 和官方 Responses WebSocket
|
||
- 管理端接口:`/api/admin/*`
|
||
- Vue 管理端静态资源
|
||
- PostgreSQL、Redis 健康检查:`/healthz`
|
||
- 账号池调度、上游连接、会话恢复和遥测记录
|
||
|
||
网关接收客户端请求后,从账号池选择一个可用账号,补充该账号的凭据和上游要求的请求头,再把请求发送到 ChatGPT Codex 后端。账号错误可以触发换号,但候选顺序始终由当前配置的调度策略决定。项目不维护独立的业务协议;除模型别名、账号作用域身份、传输控制和历史恢复所需的改写外,请求与响应尽量保持上游语义。
|
||
|
||
运行时依赖:
|
||
|
||
- PostgreSQL:权威业务数据和遥测事实
|
||
- Redis:带 TTL 的运行态数据、分布式租约和模型快照
|
||
- ChatGPT Codex 后端:Responses、模型列表、配额和 token 刷新
|
||
- 本地数据目录:身份派生密钥、在线更新状态和文件日志
|
||
|
||
PostgreSQL 和 Redis 都是必需依赖。任一连接失败时进程不能完成启动;运行中 `/healthz` 检查任一依赖失败都会返回 `503`。
|
||
|
||
## 仓库目录
|
||
|
||
```text
|
||
.
|
||
├── backend/ Rust 后端和集成测试
|
||
│ ├── src/ 生产代码
|
||
│ ├── tests/ 后端测试,目录结构与 src 对应
|
||
│ ├── Cargo.toml
|
||
│ └── Cargo.lock
|
||
├── frontend/ Vue 管理端
|
||
│ ├── src/
|
||
│ ├── public/
|
||
│ └── dist/ 前端构建产物
|
||
├── deploy/ Dockerfile、Compose 和部署模板
|
||
├── docs/
|
||
│ └── architecture.md 当前架构说明
|
||
├── release/ 版本、目标平台和发布脚本
|
||
└── skills/ 仓库内开发约束
|
||
```
|
||
|
||
后端生产代码只放在 `backend/src/`。后端测试统一放在 `backend/tests/`,不在生产模块中加入 `#[cfg(test)]` 测试代码。
|
||
|
||
## 后端模块
|
||
|
||
`backend/src/lib.rs` 只声明顶层模块。项目没有 `application` 层;业务编排由各领域服务承担,跨领域对象只在 `bootstrap` 中装配。
|
||
|
||
```text
|
||
backend/src/
|
||
├── main.rs
|
||
├── lib.rs
|
||
├── infra/
|
||
├── upstream/
|
||
├── telemetry/
|
||
├── keys/
|
||
├── auth/
|
||
├── settings/
|
||
├── update/
|
||
├── fleet/
|
||
├── models/
|
||
├── dispatch/
|
||
├── api/
|
||
└── bootstrap/
|
||
```
|
||
|
||
依赖关系按以下规则维护:
|
||
|
||
1. `main.rs` 只解析子命令并进入 `bootstrap` 或一次性任务。
|
||
2. `bootstrap` 是进程装配根,可以依赖所有领域模块,但不承载业务规则。
|
||
3. `api` 负责 HTTP 契约、鉴权提取和响应映射,只调用领域服务,不直接写 SQL 或 Redis 命令。
|
||
4. `dispatch` 负责编排一次代理请求,可以调用 `fleet`、`models`、`upstream` 和 `telemetry`。
|
||
5. `fleet`、`keys`、`auth`、`settings`、`models`、`telemetry` 各自拥有本领域的服务与存储实现。
|
||
6. `upstream` 只处理上游协议和传输,不选择账号,也不处理管理端业务。
|
||
7. `infra` 提供数据库、Redis、日志、身份和通用格式工具,不依赖 HTTP 层。
|
||
|
||
### main.rs 与 lib.rs
|
||
|
||
- `main.rs`:命令行入口。
|
||
- `serve` 或无参数:启动 HTTP 服务。
|
||
- `rebuild-buckets`:从保留期内的成功、失败事实重建请求时间桶。
|
||
- `lib.rs`:导出后端顶层模块,供二进制和集成测试使用。
|
||
|
||
### infra
|
||
|
||
```text
|
||
infra/
|
||
├── database.rs
|
||
├── redis.rs
|
||
├── migrations/
|
||
│ └── 0001_initial.sql
|
||
├── identity.rs
|
||
├── paths.rs
|
||
├── logging.rs
|
||
├── time.rs
|
||
├── json.rs
|
||
└── format.rs
|
||
```
|
||
|
||
- `database.rs`:创建 PostgreSQL 连接池、执行迁移、校验迁移顺序和 SQL checksum、提供 ping。
|
||
- `migrations/0001_initial.sql`:PostgreSQL 终态基线。迁移版本由迁移目录在代码中的登记顺序派生。
|
||
- `redis.rs`:创建 Redis `ConnectionManager`,统一添加 `cpr:` 键前缀,提供 ping。
|
||
- `identity.rs`:管理员密码哈希、API Key 和会话令牌生成、账号作用域 HMAC 伪名。
|
||
- `paths.rs`:确定本地数据目录,读取或创建 `identity_hmac_secret`。
|
||
- `logging.rs`:结构化日志、stdout 输出和文件轮转。
|
||
- `time.rs`、`json.rs`、`format.rs`:跨模块使用的解析与格式化工具。
|
||
|
||
### upstream
|
||
|
||
```text
|
||
upstream/openai/
|
||
├── token_client.rs
|
||
├── protocol/
|
||
│ ├── schema.rs
|
||
│ ├── responses.rs
|
||
│ ├── events.rs
|
||
│ ├── sse.rs
|
||
│ ├── websocket.rs
|
||
│ └── websocket_errors.rs
|
||
├── transport/
|
||
│ ├── client.rs
|
||
│ ├── client_sse.rs
|
||
│ ├── endpoints.rs
|
||
│ ├── headers.rs
|
||
│ ├── tls.rs
|
||
│ ├── response_meta.rs
|
||
│ ├── usage.rs
|
||
│ ├── diagnostics.rs
|
||
│ ├── websocket.rs
|
||
│ ├── websocket_frames.rs
|
||
│ ├── websocket_pool.rs
|
||
│ └── websocket_pump.rs
|
||
└── fingerprint/
|
||
├── types.rs
|
||
├── store.rs
|
||
├── runtime.rs
|
||
└── updater.rs
|
||
```
|
||
|
||
- `protocol`:Responses 请求、SSE 事件、WebSocket 帧和响应聚合,不包含账号池逻辑。
|
||
- `transport`:HTTP/SSE 与 WebSocket 连接、请求头、TLS、响应诊断和连接池。
|
||
- `fingerprint`:Codex Desktop 指纹的 PostgreSQL 存储、运行时快照和更新。
|
||
- `token_client.rs`:OAuth token 刷新客户端。
|
||
|
||
当前只有 OpenAI/ChatGPT Codex provider。出现第二个 provider 时,共享的上游接口应放在 `upstream/mod.rs`,provider 特有实现仍留在各自子目录。
|
||
|
||
### fleet
|
||
|
||
`fleet` 是账号域,负责账号实体、持久化、账号池、调度、配额、token 刷新、Cookie 和管理操作。
|
||
|
||
```text
|
||
fleet/
|
||
├── account.rs
|
||
├── window.rs
|
||
├── import.rs
|
||
├── store/
|
||
│ ├── mod.rs
|
||
│ ├── queries.rs
|
||
│ ├── rows.rs
|
||
│ └── write.rs
|
||
├── pool/
|
||
│ ├── mod.rs
|
||
│ ├── filters.rs
|
||
│ └── state.rs
|
||
├── scheduler/
|
||
│ ├── mod.rs
|
||
│ ├── candidates.rs
|
||
│ ├── feedback.rs
|
||
│ └── strategy/
|
||
│ ├── smart.rs
|
||
│ ├── quota_reset.rs
|
||
│ ├── round_robin.rs
|
||
│ └── sticky.rs
|
||
├── quota/
|
||
│ ├── service.rs
|
||
│ └── runtime.rs
|
||
├── refresh/
|
||
│ ├── policy.rs
|
||
│ ├── lease.rs
|
||
│ └── service.rs
|
||
├── cookies/
|
||
│ └── store.rs
|
||
└── manage/
|
||
├── service.rs
|
||
├── types.rs
|
||
├── lifecycle.rs
|
||
├── import.rs
|
||
├── export.rs
|
||
├── oauth.rs
|
||
├── probe.rs
|
||
├── quota.rs
|
||
└── quota_view.rs
|
||
```
|
||
|
||
- `store`:`AccountStore` 接口和 PostgreSQL 实现。账号、刷新时间和 quota 状态按事务写入。
|
||
- `pool`:内存账号快照、并发槽位、请求间隔、状态同步和候选租用。
|
||
- `scheduler`:账号选择的唯一归属地。候选过滤和排序分离,支持 `smart`、`quota_reset_priority`、`round_robin`、`sticky`。
|
||
- `scheduler/feedback.rs`:保存进程内 EWMA 错误率和首字延迟,供 `smart` 策略打分。
|
||
- `quota`:配额查询、窗口状态和运行时更新。
|
||
- `refresh`:token 刷新策略、Redis 分布式租约和刷新服务。
|
||
- `cookies`:按账号保存 Cloudflare Cookie。
|
||
- `manage`:管理端账号导入、导出、OAuth、探测、刷新和生命周期操作。
|
||
|
||
### models
|
||
|
||
- `catalog.rs`:合并官方模型快照、解析运行时别名并提供目录查询,不维护静态模型或推理强度列表。
|
||
- `service.rs`:运行时模型服务、刷新和设置订阅。刷新任务使用可用账号请求官方 `/codex/models?client_version=...` 接口,并按订阅计划更新目录。
|
||
- `store.rs`:按订阅计划保存 Redis 模型快照。
|
||
- `types.rs`:模型和计划快照类型。
|
||
|
||
### dispatch
|
||
|
||
`dispatch` 是代理请求的编排边界。账号选择、历史恢复、上游调用、流生命周期和遥测归因在这里汇合。
|
||
|
||
```text
|
||
dispatch/
|
||
├── service.rs
|
||
├── upstream_call.rs
|
||
├── attempts.rs
|
||
├── errors.rs
|
||
├── recording.rs
|
||
├── affinity/
|
||
│ ├── identity.rs
|
||
│ ├── resolve.rs
|
||
│ ├── service.rs
|
||
│ ├── store.rs
|
||
│ └── types.rs
|
||
├── recovery/
|
||
│ ├── account_failure.rs
|
||
│ ├── auth.rs
|
||
│ ├── cloudflare.rs
|
||
│ ├── exhaustion.rs
|
||
│ └── history.rs
|
||
└── stream/
|
||
├── lifecycle.rs
|
||
├── live.rs
|
||
├── prefetch.rs
|
||
├── sse_failure.rs
|
||
└── trace.rs
|
||
```
|
||
|
||
- `service.rs`:非流式 Responses 主循环。
|
||
- `stream/lifecycle.rs`:流式 Responses 主循环。
|
||
- `upstream_call.rs`:把账号凭据、账号作用域身份和 Cookie 交给上游客户端,处理单账号有限重试。
|
||
- `attempts.rs`:在请求开始时冻结完整候选顺序,记录已尝试、忙碌和状态排除的账号。
|
||
- `errors.rs`:错误分类与客户端错误映射。
|
||
- `recording.rs`:把调度轨迹和上游诊断写入 `telemetry::Recorder`。
|
||
- `affinity`:会话亲和、账号作用域身份、Redis 索引和响应历史快照。
|
||
- `recovery`:账号风险隔离、Cloudflare 冷却、候选耗尽和 previous response 恢复。
|
||
- `stream`:首段预取、SSE 失败识别、下游流转发和结束时结算。
|
||
|
||
### telemetry
|
||
|
||
```text
|
||
telemetry/
|
||
├── recorder.rs
|
||
├── billing.rs
|
||
├── dashboard.rs
|
||
├── rebuild.rs
|
||
├── usage/
|
||
├── ops/
|
||
├── buckets/
|
||
└── account_usage/
|
||
```
|
||
|
||
- `recorder.rs`:真实 `/v1` 代理调用的统一记录入口。成功事实写入 `usage_records`,失败事实写入 `ops_error_logs`,同时更新请求时间桶。
|
||
- `billing.rs`:token 和模型计费口径。
|
||
- `dashboard.rs`:Dashboard 聚合查询,不处理 HTTP。
|
||
- `usage`、`ops`:事实表的类型、存储和查询。
|
||
- `buckets`:`request_time_buckets` 实时聚合、查询和重建。
|
||
- `account_usage`:账号累计用量和当前额度窗口统计。
|
||
- `rebuild.rs`:`rebuild-buckets` 子命令实现。
|
||
|
||
### keys、auth、settings、update
|
||
|
||
- `keys`:客户端 API Key 的创建、分页查询、更新、删除和完整 Key 校验。鉴权通过 PostgreSQL 唯一点查完成。
|
||
- `auth`:管理员用户、密码校验和登录会话。管理员用户在 PostgreSQL,登录会话在 Redis。
|
||
- `settings`:PostgreSQL `runtime_settings` 的读写和 `watch` 广播。模型别名、刷新参数、单账号并发、请求间隔和调度策略可在运行时更新。
|
||
- `update`:Release 查询、下载、checksum 校验、归档替换、更新状态和回滚。
|
||
|
||
### api
|
||
|
||
```text
|
||
api/
|
||
├── router.rs
|
||
├── assets.rs
|
||
├── middleware/
|
||
│ ├── connection_drain.rs
|
||
│ ├── request_id.rs
|
||
│ └── trace.rs
|
||
├── client/
|
||
│ ├── router.rs
|
||
│ ├── auth.rs
|
||
│ ├── responses/
|
||
│ │ ├── mod.rs
|
||
│ │ ├── sse.rs
|
||
│ │ └── websocket.rs
|
||
│ ├── models.rs
|
||
│ └── errors.rs
|
||
└── admin/
|
||
├── router.rs
|
||
├── response.rs
|
||
├── session.rs
|
||
├── auth_routes.rs
|
||
├── settings_routes.rs
|
||
├── system_routes.rs
|
||
├── keys_routes.rs
|
||
├── usage_routes.rs
|
||
├── ops_routes.rs
|
||
├── dashboard_routes.rs
|
||
└── accounts_routes/
|
||
```
|
||
|
||
- `router.rs`:组合客户端 API、管理端 API、SPA 静态资源和 `/healthz`。
|
||
- `middleware/connection_drain.rs`:统一结束 HTTP 流并追踪入站 WebSocket,不由业务接口分别处理进程关闭。
|
||
- `middleware/request_id.rs`:接收或生成请求 ID,并写入响应头。
|
||
- `middleware/trace.rs`:HTTP 访问日志。
|
||
- `client`:`/v1` 路由、Bearer API Key 鉴权和 Responses 入站协议。`responses/mod.rs` 负责共享请求构造,`sse.rs` 与 `websocket.rs` 只处理各自的下游传输。
|
||
- `admin`:`/api/admin` 路由、会话或管理员 API Key 鉴权、统一响应结构。所有管理端响应带 `Cache-Control: no-store`。
|
||
- `assets.rs`:静态文件服务和 Vue Router history fallback;未知 `/api`、`/v1` 路径不会回退到 SPA。
|
||
|
||
客户端请求体上限为 16 MiB。生产环境由同一个 Rust 进程托管 `web/dist`,不运行独立 Node 服务。
|
||
|
||
### bootstrap
|
||
|
||
```text
|
||
bootstrap/
|
||
├── config.rs
|
||
├── services.rs
|
||
├── state.rs
|
||
├── shutdown.rs
|
||
└── tasks/
|
||
├── coordinator.rs
|
||
├── periodic.rs
|
||
├── cleanup.rs
|
||
├── cookie_cleanup.rs
|
||
├── retention_trim.rs
|
||
├── model_refresh.rs
|
||
├── token_refresh.rs
|
||
├── quota_refresh.rs
|
||
└── fingerprint_update.rs
|
||
```
|
||
|
||
- `config.rs`:启动配置 schema、YAML 加载和密码环境变量覆盖。
|
||
- `services.rs`:创建存储、领域服务、上游客户端、路由和后台任务。
|
||
- `state.rs`:进程级运行配置和 PostgreSQL/Redis 健康检查。
|
||
- `shutdown.rs`:信号、管理端重启和进程替换协调。
|
||
- `tasks/coordinator.rs`:统一启动和关闭后台任务。
|
||
|
||
## 启动流程
|
||
|
||
`serve` 的启动顺序固定:
|
||
|
||
1. 从 `CPR_CONFIG_FILE` 指定路径加载 YAML;未设置时读取当前目录的 `config.yaml`。
|
||
2. 用 `CPR_ADMIN_DEFAULT_PASSWORD`、`CPR_POSTGRES_PASSWORD`、`CPR_REDIS_PASSWORD` 覆盖对应密码。
|
||
3. 初始化日志并绑定监听地址。
|
||
4. 连接 PostgreSQL,校验并执行迁移。
|
||
5. 连接 Redis,统一使用 `cpr:` 键前缀。
|
||
6. 读取或初始化 `runtime_settings`,把数据库设置应用到运行配置。
|
||
7. 创建各领域存储、服务和 OpenAI 上游客户端。
|
||
8. 从本地数据目录读取或创建 `identity_hmac_secret`。
|
||
9. 初始化默认指纹、管理员、模型运行时缓存和内存账号池。
|
||
10. 启动后台任务,挂载 HTTP 路由。
|
||
|
||
关闭时先停止接收新请求,再统一结束 HTTP 长流和入站 WebSocket,同时关闭上游 WebSocket 池与后台任务。全部连接最多排空 20 秒,单个后台任务最多等待 5 秒;超时后才强制结束剩余连接。
|
||
|
||
## 代理请求链路
|
||
|
||
一次 `POST /v1/responses` 请求或 WebSocket `response.create` 消息经过以下步骤:
|
||
|
||
1. 请求 ID 中间件建立全链路标识,访问日志记录基础 HTTP 信息。
|
||
2. `client::auth` 从 `Authorization: Bearer sk_...` 取出完整 Key,并在 PostgreSQL 校验是否存在且启用。WebSocket 在 HTTP upgrade 前完成鉴权。
|
||
3. API 层解析 HTTP JSON 或官方 `response.create` 文本帧,提取客户端 IP、User-Agent 和 transport-only 参数。
|
||
4. `models` 解析模型别名;`dispatch` 建立会话变体标识和 previous response 恢复计划。
|
||
5. `AccountAttemptLedger` 按当前调度策略冻结完整候选顺序。会话亲和账号可以排在前面,但不会绕过可用性检查。
|
||
6. 账号池租用并发槽位,必要时先刷新 quota;请求间隔在发送上游前执行。
|
||
7. `upstream_call` 注入账号 token、`chatgpt-account-id`、账号作用域身份、指纹请求头和 Cookie。
|
||
8. 上游通过 HTTP/SSE 或 WebSocket 返回结果。单账号可重试的 5xx 最多额外重试两次。
|
||
9. 在结果尚未提交给客户端时,账号级失败可继续尝试候选账本中的下一个账号。
|
||
10. 完成后更新账号用量、会话亲和、调度反馈和遥测事实,并释放账号槽位。
|
||
|
||
会话压缩不使用独立端点。Codex 通过 `/v1/responses` 请求中的 `compaction_trigger` 和 `request_kind=compaction` 表达压缩语义;代理沿用同一条调度链路,并在遥测元数据中记录 `compact`、`requestKind` 和 `subagentKind`。
|
||
|
||
## 账号调度与换号
|
||
|
||
候选生成分为两步:
|
||
|
||
- `scheduler::candidates` 过滤非 active、配额不可用、模型不匹配、处于 Cloudflare 冷却或明确排除的账号。
|
||
- 当前 `rotation_strategy` 对剩余账号排序。请求级快照保留所有符合条件的层级,层级只影响顺序,不缩小 failover 边界。
|
||
|
||
单个请求的候选顺序在开始时冻结。瞬时并发槽位占满的账号进入 busy 队列,等待槽位变化后再判断;状态已经变化的账号被排除。候选账本不会重复取出同一个账号。单账号内部的 5xx 重试,以及 previous response 失效后在原账号执行的完整重放,是账本之外的明确重试。
|
||
|
||
会触发账号隔离或换号的典型情况:
|
||
|
||
- quota 或 rate limit 已耗尽
|
||
- token 失效、账号过期或封禁
|
||
- 当前账号不支持请求模型
|
||
- Cloudflare challenge 或路径阻断
|
||
- 可重试的账号传输故障
|
||
- 未产生可见输出的空响应
|
||
|
||
这些失败只更新当前账号的状态、quota 或冷却时间,不启用独立熔断器,也不固定切到某一种策略。下一账号仍来自本次请求按当前设置生成的调度顺序。
|
||
|
||
请求本身不可修复的 4xx、协议错误或已经向客户端提交内容后的流错误不会通过换号掩盖。候选耗尽后,返回最后一个有业务意义的账号错误;没有候选时返回无可用账号错误。
|
||
|
||
## previous response 与流式恢复
|
||
|
||
成功响应写入 Redis 亲和记录,保存响应 ID、账号 ID、会话 ID、turn state、变体哈希和受限的 replay 节点。
|
||
|
||
previous response 分两类处理:
|
||
|
||
- 受管历史:Redis 能找到响应归属。先使用原账号和原 previous response;若上游返回 `previous_response_not_found`、连接续接忙碌或原连接不可复用,先在原账号执行完整历史重放。需要换号且 replay 完整时,其他账号也使用完整历史重放。
|
||
- 外部未知历史:Redis 没有该响应。只能在第一个账号原样尝试一次,因为服务端没有足够历史安全地转移到其他账号;失败时保留上游错误。
|
||
|
||
replay 按轮保存增量输入和输出,不保存每一轮累计的完整输入。写入前递归删除对象 ID 和 `encrypted_content`。容量边界:
|
||
|
||
- 单轮 snapshot:2 MiB
|
||
- 单会话累计 replay:16 MiB
|
||
- 最大深度:128 轮
|
||
- Redis TTL:4 小时
|
||
|
||
超过任一边界时不再生成新的 replay 节点,正常代理响应不因此失败。
|
||
|
||
流式请求只有在响应尚未提交给客户端时才能透明恢复或换号。首段预取会识别终止事件和账号级失败;一旦真实输出已经发给客户端,代理会用 `response.failed` 结束当前 SSE 或 WebSocket response,不能在另一账号上重放后拼接到已有输出。
|
||
|
||
Responses 协议没有按事件序号续传未完成 response 的请求字段。官方 Codex 在 turn 层持有完整会话历史和工具执行状态,收到可重试的断流后会建立新请求重新采样。代理保留这一责任边界:新请求仍进入账号池调度和会话恢复,但不会伪造同一个 response 的续传。
|
||
|
||
## 账号作用域身份
|
||
|
||
`identity_hmac_secret` 是实例级 256-bit 持久密钥。服务端用它对以下值做 HMAC 派生:
|
||
|
||
- prompt cache key
|
||
- session、thread、turn、window 和 parent thread ID
|
||
- client request ID
|
||
- installation ID
|
||
|
||
派生输入包含字段域和账号 ID。同一客户端标识在不同账号上得到不同值;同一账号、同一输入在密钥不变时保持稳定。installation ID 只由账号 ID 和持久密钥派生,因此每个账号有一个稳定值,不在每次请求时随机生成。
|
||
|
||
镜像更新或容器重建时必须保留数据目录中的 `identity_hmac_secret`。丢失该文件会为所有账号生成新的身份集合,已有会话亲和也失去连续性。
|
||
|
||
## 数据存储
|
||
|
||
### PostgreSQL
|
||
|
||
PostgreSQL 是权威持久化存储。
|
||
|
||
时间列统一使用 `timestamptz`,应用以 UTC 时间点读写。Compose 将 PostgreSQL 的会话显示时区和日志时区设为 `Asia/Shanghai`,因此直接查询时显示 `+08:00`;这不会改写已有数据,也不会改变数据库内部保存的绝对时间。API 输出、中国自然日统计和日志轮转仍由 `infra::time` 显式定义,不依赖宿主机时区。
|
||
|
||
| 数据 | 表 |
|
||
| --- | --- |
|
||
| 管理员 | `admin_users` |
|
||
| 客户端 API Key | `client_api_keys` |
|
||
| 运行时设置 | `runtime_settings` |
|
||
| 账号与 quota 状态 | `accounts` |
|
||
| 账号累计和窗口用量 | `account_usage` |
|
||
| 成功请求事实 | `usage_records` |
|
||
| 失败请求事实 | `ops_error_logs` |
|
||
| 请求聚合桶 | `request_time_buckets` |
|
||
| Cloudflare Cookie | `account_cookies` |
|
||
| Codex Desktop 指纹 | `fingerprints`、`fingerprint_update_history` |
|
||
| 迁移记录 | `schema_migrations` |
|
||
|
||
迁移框架要求版本严格递增,并校验已执行迁移的名称和 SQL checksum。修改已经发布的迁移会阻止启动;schema 变化必须新增迁移。
|
||
|
||
保留期清理任务每小时执行一次。默认值保存在 `runtime_settings`:
|
||
|
||
- `usage_records`:30 天
|
||
- `ops_error_logs`:30 天
|
||
- `request_time_buckets`:90 天
|
||
- `fingerprint_update_history`:最多 100 条
|
||
|
||
`account_usage` 是每账号一行的累计状态,账号删除时级联删除;账号、Key、设置、Cookie 和当前指纹按业务生命周期删除,不按日志保留期裁剪。
|
||
|
||
### Redis
|
||
|
||
Redis 使用统一的 `cpr:` 前缀。
|
||
|
||
| 键 | 内容 | 生命周期 |
|
||
| --- | --- | --- |
|
||
| `cpr:admin:session:<hash>` | 管理员登录会话 | `admin.session_ttl_minutes` |
|
||
| `cpr:lease:refresh:<account_id>` | token 刷新租约 | 租约 PX TTL |
|
||
| `cpr:models:plan_snapshots` | 各订阅计划模型快照 HASH | 下一次完整刷新替换 |
|
||
| `cpr:affinity:v2:resp:<response_id>` | 响应归属和 replay 节点 | 4 小时 |
|
||
| `cpr:affinity:v2:conv:<conversation_id>` | 会话响应 ZSET 索引 | 4 小时 |
|
||
| `cpr:affinity:v2:account:<account_id>` | 账号响应 ZSET 索引 | 4 小时 |
|
||
|
||
账号删除时会批量删除该账号的亲和响应并清理会话索引。Redis 写入失败不应改变已经得到的上游响应语义,但会失去对应的会话恢复、模型缓存或分布式协调能力。
|
||
|
||
### 本地文件
|
||
|
||
Docker 默认把宿主机 `.runtime/data` 挂载到 `/app/data`,并设置 `XDG_DATA_HOME=/app/data`。
|
||
|
||
```text
|
||
.runtime/
|
||
├── config.yaml
|
||
├── data/
|
||
│ ├── identity_hmac_secret
|
||
│ ├── update-state.json
|
||
│ ├── update.lock
|
||
│ └── update-tmp/
|
||
└── logs/
|
||
```
|
||
|
||
PostgreSQL 数据不在 `.runtime/data`。Compose 使用 `postgres-data` 命名卷;Redis 使用 `redis-data` 命名卷并启用 AOF。更换应用镜像不会删除这些卷,执行 `docker compose down -v` 才会删除命名卷。
|
||
|
||
## 后台任务
|
||
|
||
`TaskCoordinator` 管理以下任务:
|
||
|
||
- 过期 Cookie 清理
|
||
- PostgreSQL 事实表和指纹历史裁剪
|
||
- 模型目录周期刷新和 ETag 触发刷新
|
||
- token 提前刷新
|
||
- quota 周期刷新
|
||
- Codex Desktop 指纹更新
|
||
- WebSocket 连接池生命周期
|
||
- settings 的模型、账号池和刷新策略订阅
|
||
|
||
任务共享领域服务和存储,不通过 HTTP 回调自身。Redis TTL 已接管管理会话、刷新租约和会话亲和的过期,不存在单独的 session cleanup 或 affinity cleanup 轮询任务。
|
||
|
||
## 前端
|
||
|
||
```text
|
||
frontend/src/
|
||
├── api/ Axios 实例、错误处理和按领域划分的 API
|
||
├── components/base/ 基础控件
|
||
├── components/charts/ 图表封装
|
||
├── composables/ 跨页面组合逻辑
|
||
├── directives/ Vue 指令
|
||
├── layout/ 管理端框架和侧栏
|
||
├── router/ 路由表与登录守卫
|
||
├── stores/ Pinia 状态
|
||
├── styles/ 全局样式和设计 token
|
||
├── utils/ 通用前端工具
|
||
└── views/
|
||
├── login/
|
||
├── dashboard/
|
||
├── accounts/
|
||
├── api-keys/
|
||
├── usage/
|
||
└── settings/
|
||
```
|
||
|
||
前端只调用 `/api/admin/*`。开发服务器把 `/dev/*` 代理到本地后端并去掉 `/dev` 前缀;生产构建写入 `frontend/dist`,镜像构建时复制到后端的 `web/dist`。
|
||
|
||
## HTTP 路由
|
||
|
||
| 路径 | 作用 |
|
||
| --- | --- |
|
||
| `GET /healthz` | PostgreSQL 和 Redis 健康检查,成功返回 `204` |
|
||
| `POST /v1/responses` | Responses 非流式或 SSE 流式入口 |
|
||
| `GET /v1/responses` | 官方 Responses WebSocket upgrade 入口 |
|
||
| `POST /v1/responses/review` | Review 请求入口 |
|
||
| `GET /v1/models*` | 模型列表、详情、catalog 和运行信息 |
|
||
| `/api/admin/*` | 登录、账号、Key、设置、用量、错误、Dashboard 和系统更新 |
|
||
| 其他非 API 路径 | Vue SPA 静态资源或 `index.html` |
|
||
|
||
客户端接口使用 `sk_` API Key。管理端支持登录 Cookie,也支持设置中生成的管理员 API Key。
|
||
|
||
## 验证边界
|
||
|
||
CI 的质量门禁包括:
|
||
|
||
- Rust `fmt`
|
||
- Rust `clippy --all-targets --all-features --locked`
|
||
- 后端集成测试
|
||
- 前端 Prettier 检查与生产构建
|
||
- GitHub Actions workflow lint
|
||
- Docker 镜像构建和 Compose smoke test
|
||
- 依赖与镜像安全扫描
|
||
|
||
后端集成测试默认连接本机 PostgreSQL 和 Redis,也可通过 `CPR_TEST_DATABASE_URL`、`CPR_TEST_REDIS_URL` 指定。每个数据库测试创建独立数据库;Redis 测试使用独立键前缀并清理旧测试键。
|