Move account and dashboard read orchestration into their existing admin API modules, and remove the top-level admin_queries package and service wrapper. Update composition wiring, dependency guards, and architecture documentation to reflect feature-local query ownership. Co-authored-by: Codex <noreply@openai.com>
29 KiB
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。
架构不变量
v1/*使用 Request → Attempt → Stream 三层静态洋葱生命周期;管理端、健康检查和静态资源不进入业务洋葱。- 一个功能只有一个规则 owner。跨入口的账号失败语义属于
fleet/account_failure.rs;v1/*的 retry、exhaustion 和 commit decision 属于对应 controller。 - API 只解析和编码;lifecycle 只编排顺序、retry、commit 和 finalize;transport 只建立连接并产出 typed facts。
- 上游响应一旦可能收到 payload,transport 或账号都不得自动重放同一请求。
- 账号认证身份按 attempt 重建;客户端会话拓扑按原值透传;两者不能共用一个“身份”抽象。
v1/*热路径中的账号明确失效先在线性化的内存路径生效并驱逐 WebSocket,再异步持久化 PostgreSQL;管理端和后台任务可等待同一状态 effect 持久化完成。- 每项生产能力只有一条执行路径,不设置兼容 wrapper、双状态机或运行时切换的新旧实现。
backend/src/只放生产代码;所有测试和 test-only helper 位于backend/tests/。- 功能端口由消费方定义,provider 仅在
bootstrap绑定;fleet不依赖telemetry/upstream,models不依赖upstream。 - 前端外部响应先按 endpoint 在 API adapter 运行时收窄,内部类型以实现和组合式函数返回值推导为主。
仓库结构
.
├── backend/
│ ├── src/ Rust 生产代码
│ ├── tests/ 按生产边界组织的集成与契约测试
│ ├── Cargo.toml
│ └── Cargo.lock
├── frontend/ Vue 管理端
├── deploy/ Dockerfile、Compose 和运行配置
├── docs/
│ └── architecture.md 当前架构
├── release/ 版本、平台和发布脚本
└── skills/ 仓库内开发约束
后端分层
backend/src/lib.rs 只声明顶层模块。项目不设置通用 application 层,跨领域对象仅在 bootstrap 装配。
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 |
backend/tests/architecture/dependencies.rs 使用 Rust AST 检查全局禁止依赖,不用源码字面量模拟模块图。新增模块或移动端口时必须先更新真实依赖,再讨论是否调整守卫规则。
infra
infra 提供:
database.rs:PostgreSQL 连接池、迁移、迁移 checksum 和 ping。redis.rs:RedisConnectionManager、统一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
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/
│ ├── mod.rs
│ ├── audit.rs
│ ├── breaker.rs
│ ├── coordinator.rs
│ ├── error.rs
│ ├── handshake.rs
│ ├── model.rs
│ ├── pump.rs
│ ├── pool/
│ │ ├── mod.rs
│ │ ├── lease.rs
│ │ ├── state.rs
│ │ └── supervisor.rs
│ └── exchange/
│ ├── mod.rs
│ ├── collect.rs
│ ├── io.rs
│ ├── reducer.rs
│ └── stream.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,并自动把合法的 Desktop 版本与构建号应用到运行时画像。新 HTTP 请求立即使用新画像;WebSocket 连接池按实际 opening 画像分代,不复用旧 UA 连接,也不改写在途请求。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。
Dashboard 查询归入 api/admin/dashboard_routes.rs,账号列表查询归入 api/admin/accounts_routes/query.rs。HTTP handler 负责参数、鉴权和响应,文件内 query service 负责跨域读取,presenter 负责 casing、显示文本和格式化。上游请求画像与 Desktop 发布观测通过 Dashboard 模块定义的快照端口注入,不直接引用 OpenAI adapter 类型。
v1 请求生命周期
dispatch 是 v1/* 的业务编排边界:
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:
- 解析模型和请求不变量。
- 建立 history、cyber policy、identity、usage 等 request scope。
- 并行读取互不依赖的 Redis 状态,读取有 100ms 上限并 fail-open。
- 合并 preferred/excluded 账号集合。
- 按当前 scheduler 冻结本请求的完整候选顺序。
Request scope 在 attempt 之间传递,但 controller 只能访问自己的状态。
Attempt
dispatch/lifecycle/attempt.rs 对每个候选账号执行:
- 从账号池获取 lease,并在真正获取时重读运行时可用状态。
- 由 quota、Cookie、history 和 identity controller 构造
AccountScopedRequest。 - 并行执行账号请求间隔等待与 prepared transport。
- 发送上游请求,将 HTTP、SSE、WebSocket 结果规范化为
AttemptObservation。 - 由
ControllerSet按静态优先级调用 owner 分类和 effect。 - 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 时创建一个
Connectingslot;前台从该 slot 的统一起点最多等待 800ms。 - 800ms 内握手成功时,连接原子执行
Connecting → Busy(owner),connection 与 lease 直接交给当前请求,再发送 WS payload。 - 800ms 耗尽时,当前请求不发送 WS payload,立即在同账号使用 HTTP/2;未完成的握手由 pool 在后台继续,仍受 15 秒硬连接超时约束。
- 当前请求耗尽 800ms、handoff receiver 已关闭后,后台握手成功才进入
Idle,供后续请求复用;最终失败则删除Connectingslot。相同 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 都计入 slot 上限:ws_pool.max_per_account 默认单账号 8 个,ws_pool.max_total 默认全局 64 个;ws_pool.max_connecting 的全局 semaphore 默认只允许 16 个 opening 同时执行。总量满且存在 Idle 时淘汰最久未使用的 socket;Busy/Connecting 不抢占,无法释放容量或获取 opening permit 时,同账号当前请求按既有策略使用 HTTP/2。账号驱逐会删除该账号全部 slot、关闭 idle socket 并取消 connecting;busy socket 在归还时因 reservation 已失效而关闭。lease 的 Drop 同步释放状态,不依赖当前线程存在 Tokio runtime。进程关闭会取消并等待 pool-owned opening、流式 forwarder 和维护任务,迟到连接不能在驱逐或 shutdown 后重新入池。pool 在 runtime 外构造时,会在首次 acquire 时补启动维护任务;零维护间隔按禁用处理,不创建 panic task。
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 协商,配置为:
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
成功与失败事实记录:
transportDecisionwsConnectMstransportDecisionWaitMsupstreamHeadersMsfirstEventMsfirstTokenMshttpVersionwebsocketPool.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 集合。候选账本不会重复租用同一账号。
明确上游事实的处理顺序:
- transport 提取 status、code、type、message、retry-after 等事实。
fleet/account_failure.rs将跨入口账号事实统一分类为 quota、rate limit、expired、disabled、banned 或 model unsupported;Cloudflare 与 cyber policy 保持各自 owner。- v1 controller 把共享分类翻译为 retry/exhaustion decision。
- 对账号全局事实,账号池 mutex 内立即失效 runtime state 并移除 slot。
- 驱逐该账号的 idle/busy/connecting WebSocket entry。
- lifecycle 从冻结候选中选择下一账号。
- 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 后记录实际响应账号,并以
ClientInput、SanitizedOutput、AccountOutput(account_id)三种来源类型追加本轮 input 与 output。 - 普通客户端 input 按值保留;input 中显式携带的已知 Responses output item 按当前来源账号标记。清洗只作用于这些 item 顶层,不递归删除用户消息、工具输出或工具参数中的同名字段。
- 同账号 full replay 删除 upstream output 顶层
id,保留该账号返回的encrypted_content;跨账号 full replay 同时删除顶层id和encrypted_content,并丢弃纯 opaque 的 compaction item。 - B 成功后,快照会永久清洗旧账号的
AccountOutput,随后只把 B 新返回的 output 作为账号绑定状态保存。 - 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 | 同账号可复用,换号必须删除 |
turnMetadata |
客户端语义与 installation 投影 | 换号时类型化清除账号字段并改写 installation;解析失败直接删除 |
previous_response_id |
上游历史句柄 | 只在 owner 账号/socket 使用;full replay 时删除 |
conversation |
Responses 服务端会话句柄 | 换号时删除 |
session/thread/conversation_id/turn/window/parent ID |
客户端会话拓扑 | 原样透传,不按账号改写 |
prompt_cache_key、x-client-request-id |
会话缓存与请求关联 | 客户端提供时原样透传 |
output item 顶层 id |
上游输出引用 | full replay 时删除,嵌套同名字段不动 |
output item 顶层 encrypted_content |
上游加密状态 | 同账号保留,换号删除 |
tool call_id |
工具输入输出配对 | full replay 时保留 |
跨账号重放的不变量是:目标账号收到完整的客户端会话语义,但请求中不存在任何由来源账号认证、签发、加密、路由或缓存的账号绑定状态。
CrossAccountReplay(source, target) 是 HistoryController 产生的显式边界;同一请求内发生普通账号 failover 时也执行相同的 Responses input 类型化清洗。AccountScopedIdentity 只包含 installation ID。AccountScopedRequest 只有在当前账号认证、Cookie、installation ID、turn-state 和 metadata 边界处理完成后才能进入 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,并在写入时惰性裁剪。
本地目录
.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 按以下顺序启动:
- 定位并严格解析
deploy/config.yaml。 - 解析相对路径并注入 PostgreSQL/Redis secret。
- 初始化日志和监听地址。
- 连接 PostgreSQL,校验并执行迁移。
- 连接 Redis。
- 读取
runtime_settings。 - 创建领域 store、service、OpenAI client 和账号池。
- 读取或创建
identity_hmac_secret。 - 构造上游请求画像并初始化管理员和模型快照。
- 启动后台任务并挂载 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 进程托管。
固定依赖方向:
view / component
-> feature composable / explicit Pinia store
-> api module -> request transport
-> VueUse SSE
api/request.ts只负责请求与管理端 envelope 解包;endpoint module 不声明响应 DTO,直接透传data/params,管理端写操作统一使用POST。- 401 处理由
main.ts注入,request transport 不依赖 router 或 store。 - SSE 由对应 feature composable / Pinia store 使用 VueUse
useEventSource管理,连接随 feature 生命周期关闭,不保留手写api/streamstransport。 - 账号、API Key、usage 列表使用 latest-request-wins 的
usePagedQuery,页内选择统一由usePageSelection管理。 - system update 是 layout 持有的单例 Pinia 状态;桌面和移动 Sidebar 只发出打开意图,不挂载或清理共享 stream。
- API 结果、composable 返回值和局部 view model 默认依赖 TypeScript 推导;显式类型只用于 Props/Emits、领域判别联合和可复用泛型边界。
- 移动导航入口位于滚动容器内的 sticky 工具栏,不以悬浮控件遮挡页面数据。
- ESLint dependency restriction 禁止 API 反向依赖 layout/router/store/view,并禁止 UI 层直接导入 Axios。
| 路径 | 作用 |
|---|---|
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。