mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-07 00:05:49 +00:00
492 lines
24 KiB
Markdown
492 lines
24 KiB
Markdown
# Codex Proxy RS 架构
|
||
|
||
本文定义仓库当前生效的系统结构、责任边界和运行时不变量。设计讨论、故障排查、迁移过程和阶段性验收不属于架构文档;代码边界发生变化时,本文必须在同一变更中同步。
|
||
|
||
## 系统边界
|
||
|
||
Codex Proxy RS 是一个单进程 Rust 网关,同时提供:
|
||
|
||
- OpenAI Responses 兼容接口:`/v1/*`,包括 HTTP JSON、HTTP SSE 和官方 Responses WebSocket。
|
||
- 管理端接口:`/api/admin/*`。
|
||
- Vue 管理端静态资源。
|
||
- PostgreSQL 与 Redis 健康检查:`/healthz`。
|
||
- Codex 账号池调度、上游传输、历史恢复、用量和运维遥测。
|
||
|
||
网关为每次代理请求选择一个可用上游账号,注入该账号的认证身份和安装身份,再调用 ChatGPT Codex 后端。请求和响应默认保持上游语义;只有模型别名、账号身份隔离、transport control 和具备完整上下文的历史重放允许改写。
|
||
|
||
运行时依赖:
|
||
|
||
| 依赖 | 责任 |
|
||
| --- | --- |
|
||
| PostgreSQL | 账号、Key、设置、Cookie 和遥测事实的权威存储 |
|
||
| Redis | 管理会话、刷新租约、模型快照、响应归属和会话级短期状态 |
|
||
| ChatGPT Codex | Responses、模型目录、配额和 OAuth token 刷新 |
|
||
| `.runtime/` | 身份派生密钥、更新状态、日志及本地 PostgreSQL/Redis 数据 |
|
||
|
||
PostgreSQL 和 Redis 都是启动必需依赖。运行中任一健康检查失败时,`/healthz` 返回 `503`。
|
||
|
||
## 架构不变量
|
||
|
||
1. `v1/*` 使用 Request → Attempt → Stream 三层静态洋葱生命周期;管理端、健康检查和静态资源不进入业务洋葱。
|
||
2. 一个功能只有一个规则 owner。跨入口的账号失败语义属于 `fleet/account_failure.rs`;`v1/*` 的 retry、exhaustion 和 commit decision 属于对应 controller。
|
||
3. API 只解析和编码;lifecycle 只编排顺序、retry、commit 和 finalize;transport 只建立连接并产出 typed facts。
|
||
4. 上游响应一旦可能收到 payload,transport 或账号都不得自动重放同一请求。
|
||
5. 账号认证身份按 attempt 重建;客户端会话拓扑按原值透传;两者不能共用一个“身份”抽象。
|
||
6. `v1/*` 热路径中的账号明确失效先在线性化的内存路径生效并驱逐 WebSocket,再异步持久化 PostgreSQL;管理端和后台任务可等待同一状态 effect 持久化完成。
|
||
7. 每项生产能力只有一条执行路径,不设置兼容 wrapper、双状态机或运行时切换的新旧实现。
|
||
8. `backend/src/` 只放生产代码;所有测试和 test-only helper 位于 `backend/tests/`。
|
||
|
||
## 仓库结构
|
||
|
||
```text
|
||
.
|
||
├── backend/
|
||
│ ├── src/ Rust 生产代码
|
||
│ ├── tests/ 按生产边界组织的集成与契约测试
|
||
│ ├── Cargo.toml
|
||
│ └── Cargo.lock
|
||
├── frontend/ Vue 管理端
|
||
├── deploy/ Dockerfile、Compose 和运行配置
|
||
├── docs/
|
||
│ └── architecture.md 当前架构
|
||
├── release/ 版本、平台和发布脚本
|
||
└── skills/ 仓库内开发约束
|
||
```
|
||
|
||
## 后端分层
|
||
|
||
`backend/src/lib.rs` 只声明顶层模块。项目不设置通用 `application` 层,跨领域对象仅在 `bootstrap` 装配。
|
||
|
||
```text
|
||
backend/src/
|
||
├── main.rs
|
||
├── lib.rs
|
||
├── infra/
|
||
├── upstream/
|
||
├── fleet/
|
||
├── models/
|
||
├── dispatch/
|
||
├── telemetry/
|
||
├── keys/
|
||
├── auth/
|
||
├── settings/
|
||
├── update/
|
||
├── api/
|
||
└── bootstrap/
|
||
```
|
||
|
||
依赖方向:
|
||
|
||
| 模块 | 责任与依赖边界 |
|
||
| --- | --- |
|
||
| `main` | 解析子命令,只进入 `bootstrap` 或一次性任务 |
|
||
| `bootstrap` | 进程 composition root,可装配所有领域模块,不拥有业务规则 |
|
||
| `api` | HTTP/WebSocket 契约、鉴权提取和最终响应编码,不写 SQL/Redis |
|
||
| `dispatch` | 编排一次 `v1/*` 请求,调用 `fleet`、`models`、`upstream`、`telemetry` |
|
||
| `fleet` | 账号、调度、quota、token、Cookie 和管理操作 |
|
||
| `upstream` | Codex 协议与 HTTP/WebSocket transport,不选择账号 |
|
||
| `telemetry` | 保存已经确定的成功、失败和聚合事实,不参与调度 |
|
||
| `infra` | PostgreSQL、Redis、日志、时间、路径和身份基础设施,不依赖 API |
|
||
|
||
### infra
|
||
|
||
`infra` 提供:
|
||
|
||
- `database.rs`:PostgreSQL 连接池、迁移、迁移 checksum 和 ping。
|
||
- `redis.rs`:Redis `ConnectionManager`、统一 `cpr:` 前缀和 ping。
|
||
- `identity.rs`:密码哈希、API Key、会话令牌和 HMAC 伪名。
|
||
- `paths.rs`:数据目录与 `identity_hmac_secret`。
|
||
- `logging.rs`:TTY compact 日志与非 TTY/文件 JSON 日志。
|
||
- `time.rs`、`json.rs`、`format.rs`:无业务语义的通用值处理。
|
||
|
||
### upstream
|
||
|
||
```text
|
||
upstream/openai/
|
||
├── token_client.rs
|
||
├── failure.rs
|
||
├── profile.rs
|
||
├── desktop_release.rs
|
||
├── protocol/
|
||
│ ├── responses.rs
|
||
│ ├── events.rs
|
||
│ ├── sse.rs
|
||
│ ├── websocket.rs
|
||
│ └── schema.rs
|
||
├── transport/
|
||
│ ├── client.rs
|
||
│ ├── client_sse.rs
|
||
│ ├── headers.rs
|
||
│ ├── tls.rs
|
||
│ ├── response_meta.rs
|
||
│ ├── diagnostics.rs
|
||
│ ├── usage.rs
|
||
│ ├── websocket.rs
|
||
│ ├── websocket_frames.rs
|
||
│ ├── websocket_pool.rs
|
||
│ ├── websocket_breaker.rs
|
||
│ └── websocket_pump.rs
|
||
```
|
||
|
||
- `protocol` 定义 Responses body、canonical SSE/WS 事实和序列化,不包含账号策略。
|
||
- `failure.rs` 将各 transport 错误规范化为稳定的 `UpstreamFailureFacts`,不解释账号状态。
|
||
- `transport` 拥有 HTTP/SSE、WebSocket、TLS、header、连接池、熔断和 transport metrics。
|
||
- `profile.rs` 定义配置驱动、启动后不可变的 Codex Desktop 上游请求画像。
|
||
- `desktop_release.rs` 只观测官方 appcast;发布变化不会自动改写生效请求画像。
|
||
- `token_client.rs` 只实现 OAuth token 刷新协议。
|
||
|
||
### fleet
|
||
|
||
`fleet` 是账号领域:
|
||
|
||
- `store`:账号与 quota 的 PostgreSQL 映射和事务写入。
|
||
- `pool`:进程内账号快照、并发 slot、请求间隔和候选 lease。
|
||
- `account_failure.rs`:HTTP、SSE、手动额度查询和后台额度刷新共享的账号失败分类与状态 effect。
|
||
- `scheduler`:候选过滤与排序的唯一 owner,支持 `smart`、`quota_reset_priority`、`round_robin`、`sticky`。
|
||
- `scheduler/feedback`:进程内 EWMA 错误率和首字延迟。
|
||
- `quota`:配额查询、窗口与运行时限制。
|
||
- `refresh`:token 刷新策略、Redis 租约和刷新服务。
|
||
- `cookies`:账号级 Cloudflare Cookie。
|
||
- `manage`:导入、导出、OAuth、探测、刷新和账号生命周期。
|
||
|
||
### models
|
||
|
||
- `catalog.rs` 合并官方模型快照与运行时别名。
|
||
- `service.rs` 请求 `/codex/models`,按订阅计划维护模型目录。
|
||
- `store.rs` 在 Redis 中保存计划模型快照。
|
||
- `types.rs` 定义模型、计划和目录值对象。
|
||
|
||
### telemetry、keys、auth、settings、update
|
||
|
||
- `telemetry`:成功事实 `usage_records`、失败事实 `ops_error_logs`、请求时间桶、账号用量和计费。
|
||
- `keys`:客户端 API Key 生命周期与 PostgreSQL 鉴权。
|
||
- `auth`:管理员用户、密码校验和 Redis 登录会话。
|
||
- `settings`:`runtime_settings` 读写及 `watch` 广播。
|
||
- `update`:Release 查询、checksum 校验、文件替换、状态和回滚。
|
||
|
||
### api 与 bootstrap
|
||
|
||
`api` 组合客户端接口、管理端接口、健康检查和 SPA:
|
||
|
||
- `api/client/responses` 解析 HTTP JSON/SSE 与入站 Responses WebSocket。
|
||
- `api/client/errors.rs` 将 typed dispatch error 编码为稳定 OpenAI 错误。
|
||
- `api/admin` 提供管理端路由和统一 `no-store` 响应。
|
||
- `api/middleware` 负责 request ID、访问日志和连接排空。
|
||
- `api/assets.rs` 提供静态资源与 Vue Router fallback,未知 API 路径不回退 SPA。
|
||
|
||
`bootstrap` 负责配置、依赖装配、启动任务和关闭协调;它不解释账号错误、history 或 transport outcome。
|
||
|
||
## v1 请求生命周期
|
||
|
||
`dispatch` 是 `v1/*` 的业务编排边界:
|
||
|
||
```text
|
||
API adapter
|
||
-> Request lifecycle
|
||
-> AttemptRunner(account A)
|
||
-> controller enter
|
||
-> request interval || transport prepare
|
||
-> upstream exchange
|
||
<- typed observation / decision
|
||
-> AttemptRunner(account B) [仅 OpenAttempt 可 retry]
|
||
-> Stream lifecycle [CommittedAttempt 无 retry API]
|
||
<- controller finalize / API encoding
|
||
```
|
||
|
||
### Request
|
||
|
||
`dispatch/lifecycle/request.rs`:
|
||
|
||
1. 解析模型和请求不变量。
|
||
2. 建立 history、cyber policy、identity、usage 等 request scope。
|
||
3. 并行读取互不依赖的 Redis 状态,读取有 100ms 上限并 fail-open。
|
||
4. 合并 preferred/excluded 账号集合。
|
||
5. 按当前 scheduler 冻结本请求的完整候选顺序。
|
||
|
||
Request scope 在 attempt 之间传递,但 controller 只能访问自己的状态。
|
||
|
||
### Attempt
|
||
|
||
`dispatch/lifecycle/attempt.rs` 对每个候选账号执行:
|
||
|
||
1. 从账号池获取 lease,并在真正获取时重读运行时可用状态。
|
||
2. 由 quota、Cookie、history 和 identity controller 构造 `AccountScopedRequest`。
|
||
3. 并行执行账号请求间隔等待与 prepared transport。
|
||
4. 发送上游请求,将 HTTP、SSE、WebSocket 结果规范化为 `AttemptObservation`。
|
||
5. 由 `ControllerSet` 按静态优先级调用 owner 分类和 effect。
|
||
6. lifecycle 只解释 `Accept`、`RetrySameAccount`、`RetryNextCandidate`、`Return`。
|
||
|
||
`OpenAttempt` 暴露 decision/retry;`CommittedAttempt` 只允许建立结果或 stream。提交边界后的类型没有 retry 方法。
|
||
|
||
### Stream
|
||
|
||
`dispatch/transport/canonical.rs` 对上游事件只解码一次,同时保留 raw bytes 和 typed facts。prefetch 在首个真实输出或 terminal 前保持请求可恢复;提交后由 `dispatch/stream/live.rs` 转发事件,并由 consuming finalizer 保证所有结束路径只 finalize 一次。
|
||
|
||
Stream terminal 包括 completed、incomplete、failed、上游断开、下游取消、shutdown 和 capture limit。已提交流中的明确账号事实仍会更新账号状态,但只影响后续请求。
|
||
|
||
### Controller 所有权
|
||
|
||
| 功能 | 唯一 owner |
|
||
| --- | --- |
|
||
| previous response 所有权、scope、full replay | `controllers/history.rs` |
|
||
| cyber policy 会话排除和 CAS 清理 | `controllers/cyber_policy/` |
|
||
| v1 quota 与 rate limit attempt decision | `controllers/quota.rs` |
|
||
| v1 封禁、过期、模型不支持的 exhaustion decision | `controllers/account_failure.rs` |
|
||
| transport 错误到稳定上游事实 | `upstream/openai/failure.rs` |
|
||
| 跨入口账号失败分类、状态 effect 与 WS 驱逐 | `fleet/account_failure.rs` |
|
||
| Cloudflare Cookie 与冷却 | `controllers/cloudflare.rs` |
|
||
| 会话亲和写入 | `controllers/affinity.rs` |
|
||
| usage 结算 | `controllers/usage.rs` |
|
||
| 代理事件与 transport metrics | `controllers/telemetry/` |
|
||
|
||
Controller 之间不直接调用。跨功能优先级只在 `ControllerSet` 声明;API、transport、store 和 lifecycle 不重复识别功能语义。Controller 只把共享账号分类翻译为 v1 生命周期 decision;手动额度查询和后台额度刷新执行同一状态 effect,但各自保留自己的返回与调度职责。
|
||
|
||
## 上游双热传输
|
||
|
||
Responses transport 使用会话级 WebSocket 池与 origin 级 HTTP/2 池两条常热通道。WebSocket 保存 connection-local response 状态;HTTP/2 提供新链和可持久化续链的低延迟后备。transport 只竞速连接准备,不并行发送业务 payload。
|
||
|
||
### TransportRequirement
|
||
|
||
`upstream/openai/protocol/responses.rs` 将请求事实归一化为:
|
||
|
||
| Requirement | 语义 |
|
||
| --- | --- |
|
||
| `HttpRequired` | 客户端明确要求 HTTP |
|
||
| `ExplicitWebSocketWarmup` | `generate=false + store=false`,必须使用可保留 socket 的 WebSocket |
|
||
| `ExactWebSocketContinuation` | 只能使用持有指定 connection-local response ID 的 socket |
|
||
| `PersistedContinuation` | previous response 已持久化,可使用 WS 或 HTTP/2 |
|
||
| `ExternalUnknown` | previous response owner 未知,只允许选定账号原样尝试 |
|
||
| `NewChain` | 无 previous response 的普通新链 |
|
||
|
||
`generate=false + store=false` 的约束优先于本地 HTTP hint,避免产生无法续接的 warmup response。
|
||
|
||
### Prepared typestate 与 fallback
|
||
|
||
`PreparedResponseTransport` 只表示“账号、header 和连接已准备,但 payload 尚未发送”。WebSocket opening 与 `response.create` 发送是单向类型边界:
|
||
|
||
- 热 WS 可用时立即租用并发送 WS payload。
|
||
- 普通新链、persisted continuation 和 external unknown 没有热 WS 时创建一个 `Connecting` slot;前台从该 slot 的统一起点最多等待 800ms。
|
||
- 800ms 内握手成功时,连接先原子进入 `Idle`,当前请求再租用为 `Busy` 并发送 WS payload。
|
||
- 800ms 耗尽时,当前请求不发送 WS payload,立即在同账号使用 HTTP/2;未完成的握手由 pool 在后台继续,仍受 15 秒硬连接超时约束。
|
||
- 后台握手成功后只进入 `Idle`,供后续请求复用;最终失败则删除 `Connecting` slot。相同 key 的后续请求沿用原 opening 的绝对 deadline,不会重新获得一段 800ms。
|
||
- DNS/TCP/TLS 立即失败、pool 不可用或 breaker 打开时,同账号使用 HTTP/2。
|
||
- opening 已返回明确上游响应时,事实直接进入 controller,不追加 HTTP 请求。
|
||
- warmup 与 exact continuation 必须等待所需 WS,不使用 HTTP fallback。
|
||
- payload 发送后的连接断开、发送超时或首事件超时统一为 `PostSendAmbiguous`,不得换 transport 或账号。
|
||
|
||
AttemptRunner 将 prepared transport 与账号请求间隔并行,800ms、熔断阈值和连接状态只存在于 `upstream/openai/transport`。
|
||
|
||
### WebSocket pool
|
||
|
||
Pool key 为 `(base_url, account_id, local_conversation_id)`,slot 状态为:
|
||
|
||
- `Idle`:可租用的热 socket。
|
||
- `Busy`:单 socket 上已有一个 in-flight response。
|
||
- `Connecting`:同 key opening 单飞,包含统一 started-at、结果通知和取消令牌。
|
||
|
||
每条连接记录最新 completed response ID。Exact continuation 只有 ID 完全匹配时才能租用;socket 缺失、busy、失活或 ID 不匹配时不新建连接碰运气,由 HistoryController 决定 full replay 或失败。
|
||
|
||
池化 socket 默认每 25 秒发送带唯一序列的 Ping,匹配 Pong 的 deadline 为 5 秒,最大寿命为 55 分钟。任意业务入站帧同样证明链路存活;下游背压导致 pump 无法读取 socket 时暂停主动探活。后台 pump 统一处理 ping/pong、EOF、close 和 liveness,复用前只读取原子连接状态,不增加网络往返。
|
||
|
||
`Idle`、`Busy` 和 `Connecting` 都计入 `ws_pool.max_per_account`,默认单账号最多 8 个 slot;后台续建不设置另一套队列或全局连接上限。账号驱逐会删除该账号全部 slot、关闭 idle socket 并取消 connecting;busy socket 在归还时因 reservation 已失效而关闭。进程关闭先取消所有 pool-owned opening,再等待维护任务和建连任务退出,迟到连接不能在驱逐或 shutdown 后重新入池。
|
||
|
||
### Origin breaker
|
||
|
||
Breaker key 为 origin 与 TLS profile,不按账号区分:
|
||
|
||
- 30 秒窗口内 3 次退化 opening 后打开 30 秒;退化包括 800ms 快路径预算耗尽、transport 失败和 5xx opening 响应。
|
||
- 同一次 opening 的前台预算耗尽与后台最终失败只计一次;迟到成功也不抹去已经发生的快路径退化。
|
||
- 预算内成功 opening 会清空该 origin 的历史退化记录。
|
||
- 打开期间热 WS 仍可使用;普通冷 opening 直接走 HTTP/2。
|
||
- 到期后只允许一个 half-open probe。
|
||
- 账号或请求级 4xx opening 响应证明 origin 可达并关闭 half-open。
|
||
- 5xx 或 transport probe 失败重新打开 breaker。
|
||
- 账号驱逐和进程 shutdown 触发的主动取消不计为 origin 故障。
|
||
|
||
Breaker 不更新账号状态;账号失效只由 controller 的明确 typed fact 触发。
|
||
|
||
### HTTP/2
|
||
|
||
Reqwest client 按 TLS profile 缓存并自动 ALPN 协商,配置为:
|
||
|
||
```rust
|
||
Client::builder()
|
||
.pool_idle_timeout(None::<Duration>)
|
||
.http2_keep_alive_interval(Duration::from_secs(30))
|
||
.http2_keep_alive_timeout(Duration::from_secs(5))
|
||
.http2_keep_alive_while_idle(true)
|
||
```
|
||
|
||
HTTP/2 连接按 origin/TLS profile 共享,账号 Authorization 只属于单个 stream。系统没有强制 HTTP/1.1 配置或第二套 transport builder。
|
||
|
||
### Transport metrics
|
||
|
||
成功与失败事实记录:
|
||
|
||
- `transportDecision`
|
||
- `wsConnectMs`
|
||
- `transportDecisionWaitMs`
|
||
- `upstreamHeadersMs`
|
||
- `firstEventMs`
|
||
- `firstTokenMs`
|
||
- `httpVersion`
|
||
- `websocketPool.kind`
|
||
|
||
`wsConnectMs` 只表示已经成功的 WS 握手耗时;复用、预算耗尽和其他 HTTP fallback 不填写该字段。`transportDecisionWaitMs` 表示当前请求为 transport 选择实际等待的时间。后台 opening 另外记录 `ws_preconnect_duration_ms`、`ws_preconnect_outcome` 和 `foreground_waiting`,不把未成功握手的 800ms 伪装成连接耗时。
|
||
|
||
稳定 decision 包括 `ws_reused`、`ws_connected_fast`、`ws_exact_required`、`ws_required`、`http2_ws_budget_exhausted`、`http2_breaker_open`、`http2_pool_unavailable` 和 `http2_ws_pre_send_failure`。
|
||
|
||
## 账号调度与失效
|
||
|
||
Scheduler 先过滤不可用账号,再按运行时 `rotation_strategy` 排序。候选顺序在 Request 阶段冻结;层级只影响顺序,不缩小 failover 集合。候选账本不会重复租用同一账号。
|
||
|
||
明确上游事实的处理顺序:
|
||
|
||
1. transport 提取 status、code、type、message、retry-after 等事实。
|
||
2. `fleet/account_failure.rs` 将跨入口账号事实统一分类为 quota、rate limit、expired、disabled、banned 或 model unsupported;Cloudflare 与 cyber policy 保持各自 owner。
|
||
3. v1 controller 把共享分类翻译为 retry/exhaustion decision。
|
||
4. 对账号全局事实,账号池 mutex 内立即失效 runtime state 并移除 slot。
|
||
5. 驱逐该账号的 idle/busy/connecting WebSocket entry。
|
||
6. lifecycle 从冻结候选中选择下一账号。
|
||
6. PostgreSQL 状态写入后台有序队列。
|
||
|
||
数据库延迟不会延长当前换号路径,也不会使并发请求重新租到已失效账号。
|
||
|
||
`cyber_policy` 是会话级负亲和,不是账号全局状态。它按客户端 API Key 和会话身份隔离,以 revision token 做 CAS 清理,TTL 为 1 小时;不设置功能私有的重试次数,候选耗尽由通用 lifecycle 返回。
|
||
|
||
## Previous response 与重放
|
||
|
||
成功 response 的 Redis 归属记录保存账号、conversation、turn state、variant 和 continuation scope,不保存请求/响应正文。
|
||
|
||
Continuation scope:
|
||
|
||
| Scope | 含义 |
|
||
| --- | --- |
|
||
| `Persisted` | `store=true`,上游可在新 transport hydration |
|
||
| `ConnectionLocal` | `store=false` 且 completed socket 仍在池中 |
|
||
| `ReplayRequired` | 没有可续接 socket,但当前入站 WS 持有完整 canonical transcript |
|
||
| `Unavailable` | 没有 socket,也没有完整 replay snapshot |
|
||
| `ExternalUnknown` | owner 无法由代理确认 |
|
||
|
||
完整 transcript 只存在于当前入站 Responses WebSocket:
|
||
|
||
- 每次 completed 后追加本轮 input 与 output。
|
||
- full replay 前递归删除 item `id` 和 `encrypted_content`,保留工具配对所需的 `call_id`。
|
||
- transcript 不写 PostgreSQL/Redis,不跨下游连接共享。
|
||
- 有完整 transcript 时,HistoryController 可以删除 previous ID 并在同账号或后续候选全量重放。
|
||
- 只有归属而没有 transcript 时,只允许 owner 账号续接。
|
||
- owner 与 transcript 都未知时,只在首个选定账号原样尝试。
|
||
|
||
真实输出提交后不执行透明 history recovery。客户端负责在新的 turn 中重新提交完整上下文。
|
||
|
||
## 身份隔离
|
||
|
||
| 字段 | 所属语义 | attempt 行为 |
|
||
| --- | --- | --- |
|
||
| access token、`chatgpt-account-id`、Cookie | 上游账号认证 | 每次从当前账号重建 |
|
||
| `x-codex-installation-id` 及其 metadata 投影 | 安装/设备指纹 | 由实例 HMAC secret 按账号稳定派生 |
|
||
| `x-codex-turn-state` | 上游 opaque sticky state | 同账号可复用,换号必须删除 |
|
||
| `previous_response_id` | 上游历史句柄 | 只在 owner 账号/socket 使用;full replay 时删除 |
|
||
| session/thread/conversation/turn/window/parent ID | 客户端会话拓扑 | 原样透传,不按账号改写 |
|
||
| `prompt_cache_key`、`x-client-request-id` | 会话缓存与请求关联 | 客户端提供时原样透传 |
|
||
| output item `id`、`encrypted_content` | 上游输出引用 | full replay 时删除 |
|
||
| tool `call_id` | 工具输入输出配对 | full replay 时保留 |
|
||
|
||
`AccountScopedIdentity` 只包含 installation ID;`AccountScopedRequest` 只有在当前账号认证、Cookie、installation ID 和 turn-state 边界处理完成后才能进入 upstream transport。
|
||
|
||
## 数据存储
|
||
|
||
### PostgreSQL
|
||
|
||
PostgreSQL 保存:
|
||
|
||
| 数据 | 表 |
|
||
| --- | --- |
|
||
| 管理员 | `admin_users` |
|
||
| 客户端 Key | `client_api_keys` |
|
||
| 运行时设置 | `runtime_settings` |
|
||
| 账号与 quota | `accounts` |
|
||
| 账号累计用量 | `account_usage` |
|
||
| 成功请求事实 | `usage_records` |
|
||
| 失败请求事实 | `ops_error_logs` |
|
||
| 请求聚合桶 | `request_time_buckets` |
|
||
| Cloudflare Cookie | `account_cookies` |
|
||
| 迁移记录 | `schema_migrations` |
|
||
|
||
时间列使用 `timestamptz`,应用按 UTC 时间点读写,中国自然日和展示时区由 `infra::time` 定义。迁移版本严格递增并校验名称与 SQL checksum。
|
||
|
||
### Redis
|
||
|
||
Redis 使用统一 `cpr:` 前缀:
|
||
|
||
| Key | 内容 | 生命周期 |
|
||
| --- | --- | --- |
|
||
| `admin:session:<hash>` | 管理员会话 | 配置 TTL |
|
||
| `lease:refresh:<account_id>` | token 刷新租约 | PX TTL |
|
||
| `models:plan_snapshots` | 计划模型快照 | 刷新替换 |
|
||
| `affinity:v3:resp:<response_id>` | response owner | 4 小时 |
|
||
| `affinity:v3:conv:<conversation_id>` | conversation 索引 | 4 小时 |
|
||
| `affinity:v3:account:<account_id>` | account 索引 | 4 小时 |
|
||
| `affinity:v3:global:*` | 全局裁剪索引和字节计数 | 4 小时 |
|
||
| `cyber-policy:v2:session:<hash>` | 会话级账号排除 | 1 小时 |
|
||
|
||
Affinity metadata 单条上限 64 KiB、全局上限 128 MiB,并在写入时惰性裁剪。
|
||
|
||
### 本地目录
|
||
|
||
```text
|
||
.runtime/
|
||
├── data/
|
||
│ ├── identity_hmac_secret
|
||
│ ├── update-state.json
|
||
│ ├── update.lock
|
||
│ └── update-tmp/
|
||
├── logs/
|
||
├── postgres/
|
||
└── redis/
|
||
```
|
||
|
||
Compose 将应用配置只读挂载到 `/app/deploy/config.yaml`。应用数据、日志、PostgreSQL 和 Redis 均使用独立 bind mount;Redis 启用 AOF。
|
||
|
||
## 启动、任务与关闭
|
||
|
||
`serve` 按以下顺序启动:
|
||
|
||
1. 定位并严格解析 `deploy/config.yaml`。
|
||
2. 解析相对路径并注入 PostgreSQL/Redis secret。
|
||
3. 初始化日志和监听地址。
|
||
4. 连接 PostgreSQL,校验并执行迁移。
|
||
5. 连接 Redis。
|
||
6. 读取 `runtime_settings`。
|
||
7. 创建领域 store、service、OpenAI client 和账号池。
|
||
8. 读取或创建 `identity_hmac_secret`。
|
||
9. 构造上游请求画像并初始化管理员和模型快照。
|
||
10. 启动后台任务并挂载 HTTP router。
|
||
|
||
后台任务由 `TaskCoordinator` 管理:
|
||
|
||
- Cookie 清理和事实表 retention。
|
||
- 模型目录刷新。
|
||
- token 与 quota 刷新。
|
||
- Codex Desktop 官方发布观测。
|
||
- settings 订阅。
|
||
- WebSocket pool 生命周期。
|
||
|
||
关闭时停止接收新请求,结束 HTTP 长流和入站 WebSocket,关闭上游 WebSocket pool 与后台任务。连接总排空上限为 20 秒,单任务等待上限为 5 秒。
|
||
|
||
## 前端与路由
|
||
|
||
Vue 管理端位于 `frontend/src`,使用 Vue Router、Pinia 和按领域划分的 API client。生产构建写入 `frontend/dist` 并由 Rust 进程托管。
|
||
|
||
| 路径 | 作用 |
|
||
| --- | --- |
|
||
| `GET /healthz` | PostgreSQL/Redis 健康检查 |
|
||
| `POST /v1/responses` | Responses JSON 或 SSE |
|
||
| `GET /v1/responses` | Responses WebSocket upgrade |
|
||
| `POST /v1/responses/review` | Review 入口 |
|
||
| `GET /v1/models*` | 模型目录和运行信息 |
|
||
| `/api/admin/*` | 管理端 API |
|
||
| 其他非 API 路径 | Vue SPA 静态资源 |
|
||
|
||
客户端接口使用 `sk_` API Key。管理端支持登录 Cookie 和管理员 API Key。请求 body 与 WebSocket 文本帧上限均为 16 MiB。
|