This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
## 初始化与管理员
|
||||
|
||||
新数据库始终创建账号表中的默认 Codex 账号,但只有 `settings.initialized` 存在才视为完成安装。`POST /api/v1/setup` 在事务中创建唯一 `admin(id=1)`、通用设置和安装标记;用户名至少 3 位、密码至少 10 位。首次初始化没有额外安装令牌,因此初始化完成前不得把实例直接暴露到不可信网络。
|
||||
新数据库不预建 Codex 账号;管理员初始化后通过 CPA `authIndex` 添加绑定。只有 `settings.initialized` 存在才视为完成安装。`POST /api/v1/setup` 在事务中创建唯一 `admin(id=1)`、通用设置和安装标记;用户名至少 3 位、密码至少 10 位。首次初始化没有额外安装令牌,因此初始化完成前不得把实例直接暴露到不可信网络。
|
||||
|
||||
管理员密码使用 argon2id(3 次、64 MiB、2 lanes、32 字节结果和随机 salt)保存。当前没有改密或找回接口;不要通过新增旁路直接写入明文或弱摘要。
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
|
||||
`security.OpenVault` 首次启动创建权限 `0600` 的 `/data/secret.key`。SMTP 密码和 Telegram Bot Token 使用该 32 字节密钥经 AES-GCM 加密,密文写入 SQLite;GET 和保存响应不得返回秘密明文,空密码/Token 表示保留旧值。
|
||||
|
||||
Codex OAuth 凭据由 app-server 写入各账号隔离的 `CODEX_HOME`,不经过浏览器或 SQLite。`secret.key`、数据库、Codex 目录和日志都可能包含敏感运行信息,不得提交 Git、加入镜像层或复制到前端。
|
||||
Codex OAuth 凭据由外部 CLIProxyAPI 保存和刷新,不进入 Codex Helper。每条本地账号记录只保存非秘密的 CPA `authIndex`;`CLIPROXY_API_MANAGEMENT_KEY` 只从服务端环境变量读取,不写入 SQLite、日志、前端状态或响应。`secret.key`、数据库、部署 `.env`、CPA management key 和 CPA 凭据目录都属于敏感运行信息,不得提交 Git、加入镜像层或复制到前端。
|
||||
|
||||
## HTTP 边界
|
||||
|
||||
初始化完成后,标记为公开的账号列表和 Dashboard 以匿名只读方式开放,供公开总览加载;未标记账号对匿名请求不可见。匿名响应不返回邮箱、Codex 认证方式、账号配置校验字段或内部错误。新增账号、设备码登录、同步、删除、公开状态修改、提醒和所有设置接口仍必须经过 `require` 的 session 与来源校验。
|
||||
初始化完成后,标记为公开的账号列表和 Dashboard 以匿名只读方式开放,供公开总览加载;未标记账号对匿名请求不可见。匿名响应不返回邮箱、CPA authIndex、Codex 认证方式、账号配置校验字段或内部错误。新增或修改 CPA 绑定、同步、删除、公开状态修改、提醒和所有设置接口仍必须经过 `require` 的 session 与来源校验。
|
||||
|
||||
JSON 解码限制为 1 MiB 并拒绝未知字段。统一安全头包括限制性 CSP、`nosniff`、禁止 iframe 和 same-origin referrer。前端路由、按钮禁用和邮箱掩码均不是服务端授权边界;所有新敏感端点必须在后端经过 `require`,改变 API 方法时还要核对来源头逻辑。
|
||||
JSON 解码限制为 1 MiB 并拒绝未知字段。统一安全头包括限制性 CSP、`nosniff`、禁止 iframe 和 same-origin referrer。前端路由、按钮禁用和邮箱掩码均不是服务端授权边界;所有新敏感端点必须在后端经过 `require`,改变 API 方法时还要核对来源头逻辑。公开账号响应必须清空 `authIndex`,避免把 CPA 内部凭据标识暴露给匿名访问者。
|
||||
|
||||
@@ -1,39 +1,53 @@
|
||||
# Codex 集成与用量同步
|
||||
# CLIProxyAPI 与 Codex 用量同步
|
||||
|
||||
## 账号隔离与进程生命周期
|
||||
## 外部依赖与凭据边界
|
||||
|
||||
每个 `accounts` 记录对应一个 `accountRuntime` 和独立 `codex app-server` 子进程。账号 1 为升级兼容固定使用 `/data/codex`;其他账号使用 `/data/accounts/<id>/codex`。该目录通过子进程 `CODEX_HOME` 注入,保存并刷新 ChatGPT 登录凭据。
|
||||
Codex Helper 不再启动 `codex app-server`,也不保存 Codex OAuth token。所有 Codex 登录、凭据持久化和刷新均由外部 CLIProxyAPI(CPA)负责;本项目只通过环境变量读取:
|
||||
|
||||
`ensureReady` 用 lifecycle mutex 串行冷启动:创建目录、启动子进程、在 20 秒内调用 `initialize`,成功后才标记 ready。仅子进程存活不代表协议已初始化;失败进程必须关闭后重试。`syncing` mutex 串行同账号同步并保护 Dashboard,`stateMu` 保护 ready/stopped。删除账号先从应用 map 移除并停止进程,再删除数据库和精确凭据目录。
|
||||
- `CLIPROXY_API_BASE_URL`:CPA Management API 根地址。
|
||||
- `CLIPROXY_API_MANAGEMENT_KEY`:CPA management key,只进入服务端请求头,不写入 SQLite、日志或前端响应。
|
||||
|
||||
## JSONL 协议与登录
|
||||
每条 `accounts` 记录保存一个唯一的非空 `auth_index`。添加或修改绑定时,后端先调用 CPA `GET /v0/management/auth-files?auth_index=...`,确认索引唯一匹配、provider/type 为 `codex`,且凭据未 disabled。CPA 的临时 `unavailable`/error 状态可能正是额度耗尽导致,不能据此拒绝读取,否则仪表盘无法显示 100% 用量和重置时间。账号邮箱、套餐和 ChatGPT account ID 来自该认证条目的安全元数据与 ID token claims;原始 token 不会进入 Codex Helper。
|
||||
|
||||
客户端以 stdio 启动 `codex app-server`,每行一个 JSON 消息。递增 ID 将响应关联到 buffered channel;请求受调用 context 或 20 秒 timeout 控制。旧进程晚退出时不能把替代进程标记为断开。
|
||||
旧数据库迁移会为已有账号增加空 `auth_index`。这些账号会保持未连接,直到管理员在设置页绑定 CPA authIndex。新数据库不再自动创建无凭据的默认账号。
|
||||
|
||||
设备码登录调用:
|
||||
## Management API 代请求
|
||||
|
||||
```text
|
||||
account/login/start {type:"chatgptDeviceCode"}
|
||||
```
|
||||
后端通过 CPA `POST /v0/management/api-call` 发起上游请求,请求体使用 `auth_index` 选择凭据,并在 Authorization 中传递 `Bearer $TOKEN$` 占位符,由 CPA 注入和刷新真实 access token。关键上游请求是:
|
||||
|
||||
前端展示 `verificationUrl` 与 `userCode`。收到 `account/login/completed`、`account/updated` 或 `account/rateLimits/updated` 后异步重拉数据;登录完成最多以 0、1、3 秒退避等待套餐分类可用。官方流程和字段见 [Codex App Server 文档](https://learn.chatgpt.com/docs/app-server)。Dockerfile 固定的 CLI 版本及项目测试仍是实际兼容基线。
|
||||
- `GET https://chatgpt.com/backend-api/wham/usage`:套餐、普通限额、代码审查限额、`additional_rate_limits` 和可用重置卡摘要。
|
||||
- `GET https://chatgpt.com/backend-api/wham/profiles/me`:Token 活动摘要与每日 bucket。
|
||||
- `GET https://chatgpt.com/backend-api/wham/rate-limit-reset-credits`:可选的重置卡详情及到期时间。
|
||||
|
||||
## 同步数据流
|
||||
CPA HTTP 层和包装的上游状态分别校验。错误只包含安全的阶段、HTTP 状态和上游错误类型,不包含 management key、OAuth token、原始响应正文或 CPA 凭据内容。请求均有超时和响应体大小限制。
|
||||
|
||||
一次同步依次读取 `account/read`、`account/rateLimits/read` 和 `account/usage/read`:
|
||||
这些 `/wham/*` 路径属于 ChatGPT/Codex 内部接口,并非稳定公开 API;CLIProxyAPI 版本、当前解析代码和测试是本项目的实际兼容基线。
|
||||
|
||||
- `account/read` 决定连接、邮箱、认证模式和套餐;读取失败会使整次同步失败。
|
||||
- 限额读取兼容 `rateLimitsByLimitId` 多 bucket 和旧 `rateLimits` 单 bucket,再把 primary/secondary 展平;同时读取 `rateLimitResetCredits`,只在有可用卡时保留数量和到期时间。失败时保留空限额而不令整次同步失败。
|
||||
- 套餐未知时,可从所有可分类且一致的限额 bucket 回填;冲突或未知时必须保持 unknown。
|
||||
- 用量读取保存 summary 和每日 Token bucket;接口失败时使用空摘要和空历史,不令整次同步失败。同步成功后,以当前仍有效的最长限额窗口作为当前重置周期,按每日 bucket 汇总周期起点至当前日期的 Token,写入 Dashboard 的 `currentCycle`;周期外和未来日期不会计入。
|
||||
- 每次限额同步写入快照、检测用量百分比显著回落,并更新账号元数据及内存 Dashboard。
|
||||
## 限额归一化
|
||||
|
||||
`account/usage/read` 的 optional 指标和 daily buckets 可能暂未提供。仅 API Key 或 Bedrock 登录不能保证读取 ChatGPT 用量;不得在缺失数据时合成调用次数、输入/输出 Token、价格或账单日期。
|
||||
一次成功同步会把 `/wham/usage` 转换为现有 `Dashboard.Limits`:
|
||||
|
||||
重置卡来自 `account/rateLimits/read` 的 `rateLimitResetCredits`。该信息只保存在内存 Dashboard 中,下一次同步会重新读取;没有可用卡时不返回到 Dashboard。当前项目只展示数量和到期时间,不执行消耗操作。
|
||||
- 顶层 `rate_limit` 使用稳定 `limitId=codex`。
|
||||
- `code_review_rate_limit` 使用稳定 `limitId=code_review`。
|
||||
- `additional_rate_limits[]` 优先使用 `metered_feature` 作为 limit ID,`limit_name` 作为显示名称;例如 Spark 的 `codex_bengalfox`。
|
||||
- `primary_window` 和 `secondary_window` 分别保留为 `windowType=primary|secondary`,前端依据窗口时长识别小时和周额度,不假定字段顺序。
|
||||
- `limit_window_seconds` 向上取整转换为分钟。
|
||||
- `used_percent` 限制在 0–100,但保留小数。
|
||||
- 重置时间优先使用 Unix 秒 `reset_at`;缺失时以本次响应时间加 `reset_after_seconds` 推算。
|
||||
- `plan_type` 未知时按原值保留,并由现有套餐分类逻辑安全显示为 unknown。
|
||||
|
||||
`currentCycle` 的周期长度和重置时间来自 `account/rateLimits/read`,不硬编码为七天;通常会选择 secondary 周窗口。由于 daily bucket 只有日期没有每次请求时间,周期边界所在日期按整日汇总,因此该值是当前周期的日级统计,不是 Credits 或美元估值。
|
||||
可用重置卡数量优先以详情接口的 `available_count` 为准,详情接口失败时可退回 `/wham/usage` 的 `rate_limit_reset_credits.available_count`。只有状态为 available 且能解析 RFC3339 `expires_at` 的记录才进入到期时间列表;详情失败不影响主同步。
|
||||
|
||||
## 套餐类型
|
||||
## Token 活动与当前周期
|
||||
|
||||
用户期望类型仅为连接后的校验提示:`any`、`personal`、`team`。当前 personal 包括 free/go/plus/pro/prolite;team 包括 team/business 及两种 self-serve business 标识。未知新套餐必须显示 unknown,不能静默当作某一类型。相同邮箱和相同已知实际类型的多个账号标记 `possibleDuplicate`,但不会自动合并或删除。
|
||||
`/wham/profiles/me` 的 `stats` 提供 lifetime、单日峰值、最长任务、连续天数以及 `daily_usage_buckets`。每日 bucket 继续写入按 `account_id` 隔离的 `daily_usage`;optional、`null`、未来新增字段和暂缺 bucket 必须安全降级,不能合成调用次数、输入/输出 Token、价格或账单日期。
|
||||
|
||||
同步成功后,以当前仍有效的最长限额窗口作为当前重置周期,按每日 bucket 汇总周期起点至当前日期的 Token,写入 `Dashboard.currentCycle`。周期边界只有日期粒度,因此边界日按整日汇总。额度和 Profile Token 活动来自不同上游聚合链路,数据新鲜度可能不同。
|
||||
|
||||
## 调度、并发与失败
|
||||
|
||||
应用启动后异步同步全部已绑定账号,之后固定每五分钟同步;手动同步和 Telegram 立即刷新使用同一链路。每个账号的 `syncing` mutex 串行同步并保护内存 Dashboard,不同账号仍保持数据库、历史和提醒隔离。
|
||||
|
||||
`auth_index` 缺失、CPA 未配置、认证条目不可用或核心 `/wham/usage` 失败会使本次同步失败:已有 Dashboard 保留并标记 `stale=true`,内部错误仅对已登录管理员可见。`/wham/profiles/me` 的 Token 活动和重置卡详情都是可选数据;Profile 失败时同一进程内保留上次成功的摘要和每日历史,没有旧值时降级为空,重置卡详情失败时退回用量接口的可用数量。两者都不会隐藏已成功读取的额度窗口。成功同步会清除 stale/error、写入限额快照、检测百分比回落并更新账号元数据。
|
||||
|
||||
删除账号只删除 Codex Helper 中的绑定、数据库历史和内存 Dashboard,不调用 CPA 删除认证文件。CPA OAuth 凭据的删除和恢复必须在 CLIProxyAPI 中单独执行。
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
|
||||
- `settings` 保存通用、SMTP、Telegram、绑定码及安装标记;秘密单独以密文 key 保存。
|
||||
- `admin` 与 `sessions` 保存唯一管理员和登录会话。
|
||||
- `accounts` 保存 Codex 连接元数据、期望套餐类型和 `public_visible`;该字段默认 `0`,旧账号迁移后保持私有,只有管理员明确开启后才进入匿名总览。
|
||||
- `accounts` 保存唯一 CPA `auth_index`、Codex 连接元数据、期望套餐类型和 `public_visible`;公开响应会隐藏 `auth_index`。旧账号迁移后索引为空且保持私有,只有管理员绑定 CPA 凭据并明确开启后才进入匿名总览。
|
||||
- `daily_usage` 和 `limit_snapshots` 按 `account_id` 保存历史,删除账号时级联删除。
|
||||
- `notifications` 保存稳定去重键、调度时间、结构化消息、状态、次数和错误。
|
||||
- `telegram_updates` 保存 Bot API offset。
|
||||
|
||||
启动迁移必须幂等并兼容早期单账号库:创建默认账号 1,把旧用量和限额数据迁入该账号,补 `expected_kind` 和通知 body。schema 变化应增加覆盖旧结构且保留已有行的测试。
|
||||
启动迁移必须幂等并兼容早期单账号库:已有默认账号 1 及其旧用量、限额数据继续保留,账号行补空 `auth_index`、`expected_kind` 和通知 body;新数据库不再主动创建默认账号。schema 变化应增加覆盖旧结构且保留已有行的测试。
|
||||
|
||||
## 清理与备份
|
||||
|
||||
每分钟调度器根据 `retentionDays` 清理旧限额、通知和每日用量;允许范围为 30–365 天。`maintenance/backup` 使用 SQLite `VACUUM INTO` 创建独立一致性快照,包含已提交 WAL 数据且不中断写入。
|
||||
|
||||
数据库快照不包含 `/data/secret.key`、`/data/codex` 或 `/data/accounts/*/codex`,因此不能单独恢复通知凭据和 Codex 登录。完整灾难恢复必须停止容器并备份、恢复整个 `/data`,同时保持 UID `10001` 可读。不得把 `docker compose down -v` 写成普通升级步骤。
|
||||
数据库快照不包含 `/data/secret.key`,因此不能单独恢复通知凭据。Codex OAuth 凭据位于外部 CLIProxyAPI,不属于 Codex Helper 备份;两套服务必须分别备份。恢复 Codex Helper 必须停止容器并备份、恢复整个 `/data`,同时保持 UID `10001` 可读。不得把 `docker compose down -v` 写成普通升级步骤。
|
||||
|
||||
## 提醒生成与重试
|
||||
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
# 后端运行与 API
|
||||
# 后端运行时与 API
|
||||
|
||||
## 启动与关闭
|
||||
|
||||
入口为 `backend/cmd/server/main.go`。普通启动调用 `app.New`:打开 `/data/codex-helper.db` 并执行兼容迁移、打开或创建 `/data/secret.key`、为数据库中的每个账号创建运行时对象,再组装 HTTP server。`Run` 启动 app-server 保活、后台调度器和 Telegram long polling;SIGINT/SIGTERM 触发五秒 HTTP 优雅关闭、停止所有账号进程并关闭数据库。
|
||||
入口为 `backend/cmd/server/main.go`。普通启动调用 `app.New`:打开 `/data/codex-helper.db` 并执行兼容迁移、打开或创建 `/data/secret.key`、创建 CLIProxyAPI 客户端、为数据库中的每个账号创建内存 Dashboard 运行时,再组装 HTTP server。`Run` 启动首次账号同步、后台调度器和 Telegram long polling;SIGINT/SIGTERM 触发五秒 HTTP 优雅关闭并关闭数据库。
|
||||
|
||||
`codex-helper healthcheck` 将 `${LISTEN_ADDR:-:8080}` 的通配地址转换为 `127.0.0.1` 并请求 `/health/live`,供 Docker `HEALTHCHECK` 使用。
|
||||
|
||||
## 请求链路
|
||||
## 组件职责
|
||||
|
||||
- `internal/app/app.go`:应用生命周期、账号运行时、路由、静态前端和通用 HTTP 辅助函数。
|
||||
- `internal/app/api.go`:`/api/v1/` 分派、初始化、会话、账号和用量同步。
|
||||
- `internal/app/app.go`:应用生命周期、账号内存运行时、调度和 HTTP 路由。
|
||||
- `internal/app/api.go`:认证后的业务 API、CPA 账号绑定与 Dashboard 同步。
|
||||
- `internal/app/notify.go`:SMTP、Telegram、提醒生成和发送。
|
||||
- `internal/store/store.go`:SQLite schema、兼容迁移和数据方法。
|
||||
- `internal/codex/client.go`:与 `codex app-server` 的 JSONL 请求/响应关联。
|
||||
- `internal/cliproxy/`:CLIProxyAPI Management API、`api-call` 包装及 `/wham/*` 响应解析。
|
||||
|
||||
所有路由由 `http.ServeMux` 承载。API 先处理 status、setup、login 和初始化后的账号/Dashboard 匿名只读入口,再统一调用 `require`;前端资源从 Go `embed.FS` 提供,未知浏览器路径回退到 `index.html`。完整端点以 [`backend/CONTRACT.md`](../../backend/CONTRACT.md) 为准。
|
||||
|
||||
## 后台任务
|
||||
|
||||
- `keepCodex` 每秒检查未就绪的账号,串行完成进程启动与协议初始化,成功后立即同步。
|
||||
- `scheduler` 每五分钟固定触发全账号同步;每分钟清理过期历史并异步处理提醒。通用设置中的 `syncMinutes` 仅为旧客户端兼容字段,不改变固定调度周期。
|
||||
- 应用启动后异步同步全部已绑定 CPA authIndex,避免外部服务暂时不可用阻止 HTTP 启动。
|
||||
- 独立同步调度器每五分钟固定触发全账号同步,最多并发同步四个账号;通用设置中的 `syncMinutes` 仅为旧客户端兼容字段,不改变固定周期。
|
||||
- 独立维护调度器每分钟清理过期历史并异步处理提醒,CPA 同步变慢不得阻塞该分钟任务。
|
||||
- `telegramLoop` 使用 Bot API long polling;仅已配置 Token 才请求更新。
|
||||
- app-server 的登录、账号和限额通知会触发带短退避的同步;多次失败将内存 Dashboard 标为 stale 并记录 `lastError`。
|
||||
|
||||
这些 goroutine 都以应用 context 为退出边界。修改调度时必须避免无限阻塞、重复启动、停止后重启和同一账号并发同步。
|
||||
这些 goroutine 都以应用 context 为退出边界。每账号 `syncing` mutex 串行手动同步、定时同步和 Dashboard 读取;修改调度时必须避免无限阻塞、重复启动和同一账号并发同步。CPA 同步失败会保留旧 Dashboard 并标记 stale,不得清空仍有效的历史快照。
|
||||
|
||||
## 状态语义
|
||||
|
||||
`/health/live` 只表示 HTTP 进程存活;`/health/ready` 还检查 SQLite,并返回 `cpa` 布尔值。`/api/v1/system/status` 也返回同一 `cpa` 配置状态。该字段只表示 CPA base URL 和 management key 均已配置,不代表每个 authIndex 或上游 `/wham/*` 当前可用;具体账号状态由账号列表和 Dashboard 的 `connected`、`stale`、`lastError` 表达。
|
||||
|
||||
Reference in New Issue
Block a user