Files
codex-proxy-rs/docs/database-schema-audit.md
T
2026-06-20 18:16:04 +08:00

32 KiB
Raw Blame History

数据库表设计与字段命名审计

审计日期:2026-06-20

本文档只审计当前 Rust 项目的 SQLite 表设计、字段设计、字段冗余和后续指纹持久化方向。本文不包含真实账号、token、cookie、API key 等敏感数据。

目标

  • 把当前表结构中不够清晰、不够稳定、容易制造重复数据或迁移债的问题固定下来。
  • 给出一套可以长期维护的命名规范和表职责边界。
  • 明确本轮指纹设计:config.yaml 只作为首次默认种子,数据库持久化后以数据库为准;后续自动更新只写数据库,运行时只从数据库加载。
  • 保持项目定位为 OpenAI / Codex 相关能力,不引入通用 proxy、VPN 或其它 provider 的泛化字段。

本轮实施结果

项目尚未上线,本轮按“新库最终结构”直接迁移,不写旧结构兼容代码。已经落地的结构变更:

  • schema.sql 成为新库最终结构,移除 connect_sqlite() 里的 ad hoc 轻量补丁逻辑。
  • 新增 schema_migrations 表作为后续上线后的迁移账本。
  • accounts 上游身份物理列改为 chatgpt_account_id / chatgpt_user_id,并增加 ux_accounts_chatgpt_identity 唯一索引。
  • account_usage 补齐并贯通 reasoning_tokens、total_tokens,协议解析、仓储写入、管理端列表和汇总都使用同一语义。
  • fingerprints 使用固定当前槽位 id = 'current',完整保存 originator、默认请求头、请求头顺序和更新时间;config.yaml 只做首次种子。
  • fingerprint_update_history 单独保存自动更新历史,不参与运行时当前指纹选择。
  • session_affinities.account_id 改为本地账号外键并级联删除。
  • event_logs 补齐 transport、attempt_index、upstream_status_code、failure_class、response_id、upstream_request_id 顶层排查字段,并保留 metadata_json 作为补充上下文。

仍然暂不拆 accounts、account_usage 等大表。拆表属于 P2 长期清洁项,等功能边界继续稳定后再做,避免为了形式拆表制造更多搬运代码。

审计基线

改造前 schema 已经能支撑基础运行,但还不是“教科书级”的表设计。主要问题不是缺表,而是职责边界和命名语义混在一起:

  1. accounts 同时承载账号身份、token、刷新计划、配额快照、配额冷却、Cloudflare 恢复状态,字段越来越像“运行时大杂烩”。
  2. accounts.account_id / accounts.user_id 命名过泛。它们实际是 ChatGPT/OpenAI 上游身份,不应该和本地账号主键 id 混淆。
  3. 上游账号身份只有普通索引,没有唯一约束。导入相同上游账号时,数据库层不能阻止重复记录。
  4. schema.sql 和实际运行库曾经出现 drift:运行库 account_usage 有 reasoning_tokens、total_tokens,但 schema.sql 没有;session_affinities.function_call_ids_json 的默认值表达也不一致。
  5. 迁移机制还是 ad hoc:connect_sqlite() 运行 schema.sql 后只补一个 accounts.next_refresh_at。缺少 schema_migrations 这种可追踪迁移账本。
  6. fingerprints 表只存了部分指纹字段。originator、默认请求头、请求头顺序仍从代码默认值补齐,不满足“后续都去数据库里拿”。
  7. 时间字段命名不统一:added_at 和 created_at 混用;fingerprints.created_at 在 upsert 时被覆盖,语义更像 updated_at。
  8. 多处 JSON 字段承担外部快照或小型结构化状态,这可以接受,但必须有命名、校验和索引边界,不能让核心查询长期依赖 metadata_json like ...。

表清单

表 当前职责 总体判断
admin_users 管理员账号 简单可用,命名基本清晰
admin_sessions 管理员登录会话 简单可用
client_api_keys 客户端访问 key 可用,但 name / label 语义重叠,唯一约束不足
accounts OpenAI/ChatGPT 账号、token、刷新、配额、恢复状态 已完成上游身份重命名和唯一约束;后续再评估拆表
account_refresh_leases 多任务刷新租约 职责清晰,字段可小幅命名优化
account_usage 账号累计用量和窗口用量 已补齐 reasoning/total;拆 totals/windows 属于 P2
account_cookies 账号级 cookie 持久化 基本合理,id 可选冗余
fingerprints Codex Desktop 请求指纹版本 已补齐当前槽位完整指纹字段
event_logs 管理端结构化事件日志 已补齐常用链路排查字段
model_plan_snapshots 套餐模型列表快照 简单可用
session_affinities response/session 到账号的亲和性 已补账号外键和默认值一致性

命名规范

后续新字段和迁移建议遵守以下规则。

标识符

  • 本地表主键统一用 id。
  • 外部系统 ID 必须加来源前缀,不使用裸 account_id / user_id:
    • 推荐:chatgpt_account_id、chatgpt_user_id
    • 可接受:openai_account_id、openai_user_id
    • 不推荐:account_id、user_id,因为它们和本地外键语义冲突。
  • 外键字段使用被引用实体名加 _id,例如 account_id 表示本地 accounts.id,不是上游账号 ID。

时间字段

  • 时间字段统一使用 RFC3339 UTC 字符串,字段名以 _at 或 _until 结尾。
  • 创建时间统一 created_at,更新时间统一 updated_at。
  • added_at 应逐步迁移为 created_at。
  • 如果字段表示“未来允许执行时间”,命名用 next_*_at,例如 next_refresh_at。
  • 如果字段表示“冷却截止时间”,命名用 *_cooldown_until,当前命名可保留。

数值字段

  • 计数用 _count,token 用 _tokens,毫秒用 _ms,秒用 _seconds。
  • 累计总量和窗口总量不要混在命名里:
    • 累计:request_count、input_tokens
    • 窗口:window_request_count、window_input_tokens
  • 如果保存 total_tokens,必须定义为“上游返回的原始 total”,不要同时把它当作 input_tokens + output_tokens 的派生值。

布尔字段

  • SQLite 仍用 integer check (value in (0, 1))。
  • 命名应表达肯定语义:
    • enabled 可接受,但 is_enabled 更清晰。
    • quota_verify_required 建议迁移为 requires_quota_verification。
    • quota_limit_reached 可接受。

密文和 JSON

  • 密文字段建议统一用 _ciphertext,比 _cipher 更准确。
    • 当前字段 access_token_cipher、refresh_token_cipher、value_cipher 可先保留,后续大迁移再统一。
  • JSON 字段必须以 _json 结尾。
  • _json 字段只用于以下情况:
    • 外部接口原始快照,短期不做复杂查询。
    • 小型原子结构,作为整体读写,例如请求头顺序。
    • 事件元数据,作为补充上下文。
  • 需要筛选、排序、聚合的字段不要长期只放在 JSON 里。

全局问题

1. 缺少迁移账本

改造前启动流程:

  1. connect_sqlite() 创建连接。
  2. 执行 schema.sql。
  3. 执行 apply_lightweight_migrations()。
  4. 只补 accounts.next_refresh_at。

问题:

  • 迁移没有版本号,不知道一个运行库应用过哪些结构变更。
  • 迁移散在 Rust 函数里,后续越补越难审计。
  • schema.sql 与运行库 drift 时,没有统一入口修正。

本轮处理:

  • 项目未上线,直接让 schema.sql 表达最终新库结构。
  • 移除 Rust 里的 ad hoc lightweight migration。
  • 增加 schema_migrations 表,为上线后的版本化迁移做准备。

后续上线后建议:

create table if not exists schema_migrations (
  version integer primary key,
  name text not null,
  applied_at text not null
);

后续每个结构变更用单独版本:

0001_initial_schema
0002_accounts_next_refresh_at
0003_account_usage_reasoning_total_tokens
0004_fingerprint_full_profile
0005_accounts_chatgpt_identity_unique

schema.sql 保持“新库最终结构”,迁移脚本负责“旧库升级路径”。本轮不写旧库升级路径。

2. 运行库和 schema.sql 曾经 drift

改造前观察到的 drift:

  • 实际运行库 account_usage 有 reasoning_tokens、total_tokens。
  • schema.sql 的 account_usage 没有这两个字段。
  • 实际运行库 session_affinities.function_call_ids_json 默认值显示为 "[]"。
  • schema.sql 写的是 '[]'。

风险:

  • 新装库、老运行库、测试库结构不一致。
  • 代码一旦开始读写这些字段,某些环境会失败。
  • 审计数据库时无法判断哪个结构才是事实来源。

本轮处理:

  • schema.sql 已直接补齐新库最终字段。
  • account_usage.reasoning_tokens / account_usage.total_tokens 已从协议解析贯通到 SQLite 写入、管理端列表和汇总。
  • 平台层 schema 测试已覆盖关键列、唯一索引和外键。

3. 账号唯一身份需要数据库兜底

改造前只有普通身份索引:

create index on accounts(account_id, user_id)
where account_id is not null;

这是普通索引,不是唯一索引。它能加速查找,但不能阻止重复。

真实问题已经出现:同一个上游账号身份导入后出现两条本地记录,而不是更新。

本轮已落地:

create unique index if not exists ux_accounts_chatgpt_identity
on accounts(chatgpt_account_id, coalesce(chatgpt_user_id, ''))
where chatgpt_account_id is not null;

本次没有旧线上库,不做重复数据兼容迁移。如果本地 .runtime 已有旧表,直接重建运行库。

导入适配器也必须遵守同一套身份优先级:

  1. 优先用 chatgpt_account_id + chatgpt_user_id 更新。
  2. 其次用导入文件中的稳定本地 id 更新。
  3. 仅邮箱不可作为唯一身份,只能作为展示或弱匹配辅助。

逐表审计

admin_users

改造前字段:

id, password_hash, created_at, updated_at

判断:职责清晰。

建议:

  • 保持当前结构即可。
  • 如果后续支持多管理员,可以补 is_enabled、last_login_at。
  • id 当前如果承载用户名,需要明确它就是登录名;如果未来支持独立用户 ID,应新增 username 并迁移。

admin_sessions

当前字段:

id, user_id, expires_at, created_at

判断:职责清晰。

建议:

  • user_id references admin_users(id) on delete cascade 是正确方向。
  • 可选补 last_seen_at,但不是当前必须项。

client_api_keys

当前字段:

id, name, prefix, key_hash, label, enabled, created_at, last_used_at

问题:

  • name 和 label 语义重叠。一个面向机器标识,一个面向展示标签时可以共存,但当前文档和字段名没有说明。
  • prefix 只有 enabled 条件索引,没有唯一约束。
  • key_hash 没有唯一约束。
  • 缺 updated_at,管理端修改名称、禁用 key 后无法统一排序或审计。

建议:

  • 如果只保留一个展示名,保留 name,删除或废弃 label。
  • 如果两个都保留,定义清楚:
    • name:用户可见名称,必填。
    • label:短标签或分组,可选。
  • 添加:
create unique index if not exists ux_client_api_keys_key_hash
on client_api_keys(key_hash);
  • enabled 可后续迁移为 is_enabled。
  • 补 updated_at、revoked_at 会更完整。

accounts

当前字段职责混合:

id
email, chatgpt_account_id, chatgpt_user_id, label, plan_type
access_token_cipher, refresh_token_cipher, access_token_expires_at, next_refresh_at
status
quota_json, quota_fetched_at, quota_limit_reached, quota_verify_required, quota_cooldown_until
cloudflare_cooldown_until
added_at, updated_at

已处理问题:

  • 上游身份物理列已改为 chatgpt_account_id / chatgpt_user_id。
  • 数据库已增加 ux_accounts_chatgpt_identity 唯一索引,防止同一上游身份重复导入。
  • 仓储 SQL 用 alias 继续向当前 Rust 领域结构返回 account_id / user_id,后续再做 Rust 字段大改名。

仍然存在的问题:

  • added_at 和其它表的 created_at 不统一。
  • token、刷新调度、配额、Cloudflare 恢复状态都塞在账号主表里。
  • status 同时表达可服务状态、刷新状态、终态禁用状态,需要明确状态机。

建议命名:

当前字段 建议字段 说明
account_id chatgpt_account_id 对应请求头 chatgpt-account-id 和上游账号身份
user_id chatgpt_user_id 上游用户身份
added_at created_at 与全库统一
access_token_cipher access_token_ciphertext 后续大迁移再统一
refresh_token_cipher refresh_token_ciphertext 后续大迁移再统一
quota_verify_required requires_quota_verification 布尔语义更自然

本轮方案:

  • 不拆表,先把数据库物理列名和唯一约束改干净。
  • Rust 领域结构暂时保留 account_id / user_id,仓储层用 SQL alias 做边界转换;这不是旧库兼容代码,而是避免本轮把本地外键 account_id 和上游身份重命名混在一起造成无意义 churn。

长期目标拆分:

accounts
  id
  email
  label
  plan_type
  status
  chatgpt_account_id
  chatgpt_user_id
  created_at
  updated_at

account_tokens
  account_id
  access_token_ciphertext
  refresh_token_ciphertext
  access_token_expires_at
  next_refresh_at
  token_updated_at

account_quota_state
  account_id
  quota_json
  quota_fetched_at
  quota_limit_reached
  requires_quota_verification
  quota_cooldown_until
  updated_at

account_recovery_state
  account_id
  cloudflare_cooldown_until
  updated_at

不是所有拆分都要一次做。优先级是:

  1. 先加唯一身份约束,解决重复导入。
  2. 再补 created_at 命名兼容。
  3. 最后考虑 token/quota/recovery 拆表。

状态机建议:

状态 语义
active access token 当前可用于请求
expired access token 不可用,但 refresh token 可能还能自救
refreshing 临时刷新中,必须有租约或短期超时兜底
quota_exhausted 当前配额不可用,但不是账号封禁
disabled 本地确认不可调度,例如 refresh token 永久失效
banned 上游封禁或账号停用,不可再参与刷新和请求

关键约束:

  • disabled / banned 启动时不应重新进入刷新调度。
  • expired 是否参与恢复,必须同时看 refresh_token_ciphertext 和 next_refresh_at。
  • next_refresh_at 到达前,启动扫描和后台扫描都不应刷新。

account_refresh_leases

当前字段:

account_id, owner, expires_at, updated_at

判断:职责清晰。

建议:

  • account_id 在这张表里表示本地账号外键,命名正确。
  • owner 可以更名为 lease_owner,但不是必须。
  • 保持 account_id primary key references accounts(id) on delete cascade。

account_usage

当前 schema.sql 字段:

account_id
request_count, empty_response_count
input_tokens, output_tokens, cached_tokens
reasoning_tokens, total_tokens
image_input_tokens, image_output_tokens
image_request_count, image_request_failed_count
window_*
window_started_at, window_reset_at, limit_window_seconds
last_used_at

问题:

  • 累计用量和窗口用量在同一行,写入路径集中,字段数量会继续膨胀。
  • total_tokens 如果只是 input_tokens + output_tokens,就是冗余;如果保存上游原始 total_tokens,必须在字段注释和代码里明确。
  • 图片用量、reasoning 用量、窗口用量混在一张表,后续每加一个计量维度都会扩字段。

本轮处理:

  • reasoning_tokens、total_tokens 已写入 schema.sql。
  • TokenUsage 已解析 output_tokens_details.reasoning_tokens 和上游 total_tokens。
  • AccountUsageDelta、SQLite upsert、管理端 usage list/summary 已全部贯通。
  • 明确定义:
    • input_tokens:上游 usage 输入 token。
    • output_tokens:上游 usage 输出 token。
    • cached_tokens:输入侧缓存 token。
    • reasoning_tokens:输出 token details 中的 reasoning token。
    • total_tokens:上游返回的原始 total token;缺失时才用输入加输出兜底。
  • 窗口字段继续保留,但只作为当前 rate-limit 窗口快照。

长期目标:

优先级说明:拆分 account_usage_totals / account_usage_windows 是 P2 长期清洁项,不进入本轮 P0/P1。本轮已解决 schema drift 和字段定义不清的问题。只有当窗口统计、历史统计、管理端聚合继续扩张时,才值得拆表。

account_usage_totals
  account_id
  request_count
  empty_response_count
  input_tokens
  output_tokens
  cached_tokens
  reasoning_tokens
  total_tokens
  image_input_tokens
  image_output_tokens
  image_request_count
  image_request_failed_count
  last_used_at

account_usage_windows
  account_id
  window_started_at
  window_reset_at
  limit_window_seconds
  request_count
  input_tokens
  output_tokens
  cached_tokens
  image_input_tokens
  image_output_tokens
  image_request_count
  image_request_failed_count
  updated_at

这样累计统计和限流窗口可以独立重置、独立迁移。

account_cookies

当前字段:

id, account_id, domain, name, value_cipher, path, expires_at, updated_at
unique(account_id, domain, name, path)

判断:整体合理。

问题:

  • id 对数据库唯一性不是必须,因为自然键已经是 (account_id, domain, name, path)。
  • value_cipher 建议后续统一为 value_ciphertext。
  • expires_at 可能来自 RFC2822 cookie 日期,也可能来自 RFC3339 max-age 计算值。代码读取时可解析两种格式,但清理 SQL 使用字符串比较,若混合格式会有边界风险。

建议:

  • 如果管理端不需要按 cookie 单条 ID 操作,可以考虑去掉 id,以自然键为主键。
  • 如果保留 id,当前设计也可接受。
  • 将入库 expires_at 统一规范化为 RFC3339 UTC,再做 SQL 清理。

fingerprints

当前字段:

id
app_version, build_number
platform, arch, chromium_version
user_agent_template
source
created_at

改造前代码行为:

  • Fingerprint::default_codex_desktop() 在 core 里硬编码默认值。
  • 旧仓储只从自动更新快照里读版本、平台、架构、Chromium 和 UA 模板。
  • originator、default_headers、header_order 仍由代码默认值补齐。
  • 旧自动更新写入会覆盖 created_at,这个字段实际承担了 updated_at 的语义。
  • 改造前 server/main.rs 构造 AppState 时没有加载数据库指纹,运行时仍可能使用硬编码默认指纹。

结论:改造前表不满足“指纹默认来自 config.yaml,持久化到数据库,后续都从数据库拿”。

本轮已按“当前槽位 + 可选历史”的语义落地,不再把运行时当前快照和历史记录混在同一套 source + created_at 查询里。

当前槽位表

request_fingerprints
  id
  originator
  app_version
  build_number
  platform
  arch
  chromium_version
  user_agent_template
  default_headers_json
  header_order_json
  source
  created_at
  updated_at

本轮保留现有表名 fingerprints,直接在该表上补齐同样字段。

字段定义:

字段 语义
id 指纹槽位。运行时当前槽位固定使用 current
originator originator 请求头值,例如 Codex Desktop
app_version Codex Desktop 应用版本
build_number Codex Desktop 构建号
platform UA 模板中的平台字段,例如 darwin
arch UA 模板中的架构字段,例如 arm64
chromium_version sec-ch-ua 的 Chromium 主版本
user_agent_template UA 模板
default_headers_json 默认请求头集合,建议保存为有序数组
header_order_json 最终请求头排序优先级
source 最近一次写入来源:config_seed、auto_update、manual
created_at 首次创建时间
updated_at 最近更新时间

槽位规则:

  • config_seed 首次写入时使用 id = 'current'。
  • auto_update 更新时覆盖同一行 id = 'current',只更新远端更新源明确提供的字段,例如 app_version、build_number、chromium_version、source、updated_at。
  • manual 管理端调整也覆盖同一行 id = 'current'。
  • load_current() 固定执行 select ... from fingerprints where id = 'current',不按 source 或 created_at desc 猜测当前指纹。
  • 项目尚未上线,本轮不保留旧自动更新槽位的兼容路径;实现时直接使用 CURRENT_FINGERPRINT_ID = 'current'。
  • 本地开发库里如果已有旧自动更新行,可以直接清空 fingerprints 或重建 .runtime 数据库,不写生产兼容迁移。
  • source 只是“最近一次写入来源”,不是槽位选择条件。

这样可以回答三个关键问题:

  • 首次配置种子写哪一行:写 id = 'current'。
  • 自动更新写哪一行:覆盖 id = 'current'。
  • 运行时读哪一行:只读 id = 'current'。

可选历史表

如果需要保留每次 appcast 检查或手动变更历史,应单独建追加表,不建议继续把历史记录塞进当前槽位表:

fingerprint_update_history
  id
  current_fingerprint_id
  app_version
  build_number
  chromium_version
  source
  manifest_json
  created_at

历史表规则:

  • id 使用 UUID。
  • 每次检查到更新可以 insert 一行历史。
  • 历史只用于审计和排查,不参与运行时 load_current()。
  • insert_update() 已改写到历史表,不写入当前槽位表。

不推荐的方案:

  • 不推荐在同一张 fingerprints 表里一会儿用固定 ID 表示当前,一会儿用 UUID 表示历史,再靠 source = 'runtime' order by created_at desc 推断当前。这会让 created_at / updated_at / source 三个字段语义互相污染。

default_headers_json 建议格式:

[
  {"name": "Accept-Encoding", "value": "gzip, deflate, br, zstd"},
  {"name": "Accept-Language", "value": "en-US,en;q=0.9"},
  {"name": "sec-ch-ua-mobile", "value": "?0"},
  {"name": "sec-ch-ua-platform", "value": "\"macOS\""}
]

不用单独建 fingerprint_headers 表的理由:

  • 请求头集合是小型原子配置。
  • 查询不会按单个 header 聚合。
  • 保持 JSON 原子读写更简单,也更符合“运行时指纹快照”的语义。

如果后续要做管理端逐项编辑、审计每个 header 的历史,再考虑拆表。

启动流程建议:

  1. 读取 config.yaml 的 fingerprint 默认配置。
  2. 连接 SQLite 并初始化当前 schema。由于尚未上线,本轮可以直接更新 schema.sql 并重建本地运行库。
  3. 调用 fingerprints.ensure_current_seed(config_fingerprint)。
  4. 如果 id = 'current' 不存在,插入 config 默认值,source = config_seed。
  5. 如果 id = 'current' 已存在,不用 config 覆盖数据库。
  6. 从 id = 'current' 读取完整指纹,构造 AppState。
  7. 后台自动更新只覆盖 id = 'current' 中明确由更新源提供的字段。
  8. 请求链路和诊断接口只读运行时加载的数据库指纹。

重要约束:

  • config.yaml 是首次默认值,不是持续覆盖源。
  • 数据库是启动后的事实来源。
  • 自动更新不得把 originator、platform、arch、default_headers_json、header_order_json 重置成代码默认值。
  • core 层不应依赖 platform 配置类型。转换应在 runtime 或 adapters 层完成,避免依赖倒置。

event_logs

当前字段:

id, request_id, kind, level, account_id, route, model, status_code,
transport, attempt_index, upstream_status_code, failure_class, response_id, upstream_request_id,
latency_ms, message, metadata_json, created_at

判断:基础可用,常用排查字段已从 metadata_json 提升为顶层列。

仍需注意:

  • attempt_index 只有请求重试上下文明示时才会有值;不会凭空生成。
  • upstream_request_id 需要上游响应头或 metadata 提供;没有来源时保持空。

已补充高频查询字段:

transport
attempt_index
upstream_status_code
failure_class
response_id
upstream_request_id

字段来源建议:

字段 数据来源
transport runtime 请求分发结果或上游客户端返回的 transport 标记,例如 http_sse、websocket、websocket_fallback_http_sse
attempt_index 账号请求重试循环里的第几次尝试,从 runtime dispatch / retry context 递增产生
upstream_status_code CodexClientError::Upstream 或 HTTP response status;成功响应可为空或等于最终 status_code
failure_class runtime 现有错误分类结果,例如 auth、quota、cloudflare、rate_limit、transport、empty_response、history_recovery 等
response_id 上游 Responses / Chat / WebSocket completed 事件里的 response id;失败前没有 response id 时为空
upstream_request_id 上游响应头中能拿到的 request id,例如 x-request-id、cf-ray 或 OpenAI 相关 request id;没有则为空

实施原则:

  • 字段从现有请求上下文、上游响应头、错误类型中提取,不新增模拟值。
  • 同一个用户请求的每次账号尝试都应能用同一个 request_id 串起来。
  • metadata_json 继续保存完整上下文,但管理端常用筛选项必须升为顶层列。

保留 metadata_json,但它只做补充上下文,不承担主要筛选维度。

model_plan_snapshots

当前字段:

plan_type, models_json, fetched_at

判断:简单合理。

建议:

  • plan_type 作为自然主键可保留。
  • 如果后续需要区分来源,可加 source,但当前项目只做 OpenAI/Codex,不需要泛化 provider 字段。

session_affinities

当前字段:

response_id
account_id
conversation_id
turn_state
instructions_hash
input_tokens
function_call_ids_json
variant_hash
expires_at
created_at

判断:职责清晰。

问题:

  • account_id 没有在 schema 中声明为 references accounts(id) on delete cascade。
  • function_call_ids_json 的默认值在运行库和 schema.sql 表达不一致。
  • turn_state 如果包含上游续链敏感状态,后续需要确认是否要加密或至少严格控制日志输出。

建议:

  • 增加账号外键,账号删除时 cascade 删除 affinity。
  • 统一 function_call_ids_json default '[]'。
  • conversation_id、variant_hash 可以保留当前命名,它们表达的是本项目内部续链匹配键。

指纹配置和数据库持久化方案

用户要求:

  • 本次指纹默认值写在 config.yaml。
  • 启动时持久化到数据库。
  • 后续更新只更新数据库。
  • 后续运行都从数据库拿。

建议配置结构:

fingerprint:
  originator: Codex Desktop
  app_version: 26.519.81530
  build_number: "3178"
  platform: darwin
  arch: arm64
  chromium_version: "146"
  user_agent_template: "Codex Desktop/{version} ({platform}; {arch})"
  default_headers:
    - name: Accept-Encoding
      value: gzip, deflate, br, zstd
    - name: Accept-Language
      value: en-US,en;q=0.9
    - name: sec-ch-ua-mobile
      value: "?0"
    - name: sec-ch-ua-platform
      value: "\"macOS\""
    - name: sec-fetch-site
      value: same-origin
    - name: sec-fetch-mode
      value: cors
    - name: sec-fetch-dest
      value: empty
  header_order:
    - authorization
    - chatgpt-account-id
    - originator
    - x-openai-internal-codex-residency
    - x-client-request-id
    - x-codex-installation-id
    - x-codex-turn-state
    - openai-beta
    - user-agent
    - sec-ch-ua
    - sec-ch-ua-mobile
    - sec-ch-ua-platform
    - accept-encoding
    - accept-language
    - sec-fetch-site
    - sec-fetch-mode
    - sec-fetch-dest
    - content-type
    - accept
    - cookie

说明:

  • default_headers 用数组,不用 map,是为了保留顺序和大小写。
  • header_order 统一使用小写 header name,因为排序匹配通常按大小写无关处理。
  • 配置文件中不放任何 token、cookie 或账号身份。

本轮数据库结构调整:

项目尚未上线,不需要为旧 fingerprints 表写兼容迁移。直接更新 schema.sql 的最终结构,并让新库包含完整字段即可。

建议最终字段约束:

id text primary key
originator text not null
app_version text not null
build_number text not null
platform text not null
arch text not null
chromium_version text not null
user_agent_template text not null
default_headers_json text not null
header_order_json text not null
source text not null
created_at text not null
updated_at text not null

本地已有 .runtime 数据库时,处理方式是删除运行库、重建运行库,或只清空并重建 fingerprints 表;不写旧槽位到当前槽位的兼容升级逻辑。

已落地仓储 API:

CURRENT_FINGERPRINT_ID = "current"
ensure_current_seed(default_fingerprint) -> Fingerprint
load_current() -> Option<Fingerprint>
update_current_version(app_version, build_number, chromium_version) -> ()
insert_update_history(update) -> ()  # 可选;写 fingerprint_update_history,不参与运行时读取

旧自动更新槽位相关 API 已从运行时代码移除。运行时需要的是“当前指纹”,不是“最新自动更新历史”。

建议目标结构

如果允许做一次较完整的 schema v2,目标结构如下。

admin_users
admin_sessions
client_api_keys

accounts
account_tokens
account_quota_state
account_recovery_state
account_refresh_leases
account_usage_totals
account_usage_windows
account_cookies

request_fingerprints
event_logs
model_plan_snapshots
session_affinities
schema_migrations

本轮已实现这些:

  1. 增加 schema_migrations 作为未来上线后的迁移纪律;本轮未上线,本地库可以直接重建。
  2. 对齐 schema.sql 和运行库 drift。
  3. 补全 fingerprints 字段和 id = 'current' 当前槽位加载链路。
  4. 解决 accounts 上游身份唯一约束和重复导入。
  5. 数据库物理列改为 chatgpt_account_id / chatgpt_user_id;Rust 领域结构暂时通过仓储 alias 过渡。
  6. 补齐 event_logs 顶层排查字段。

实施优先级

P0:必须先做

  • 已完成:新增迁移账本表,停止继续堆 ad hoc 迁移;本轮本地库可直接重建,不写旧结构兼容升级。
  • 已完成:直接对齐 schema.sql 与运行库 drift。
  • 已完成:补全 fingerprints 表,使 id = 'current' 的完整指纹可以从数据库恢复。
  • 已完成:config.yaml 增加 fingerprint 默认种子。
  • 已完成:启动时 seed 数据库,运行时从数据库加载指纹。
  • 已完成:账号导入按上游身份更新,数据库增加唯一约束。

P1:命名和职责边界

  • 已完成:数据库物理列使用 chatgpt_account_id、chatgpt_user_id。
  • 已完成:event_logs 补结构化排查字段。
  • 待做:逐步把 added_at 迁移为 created_at。
  • 待做:明确 status 状态机和刷新调度不变量。
  • 待做:如后续确实需要,再把 Rust 领域结构字段也改为 chatgpt_account_id / chatgpt_user_id。

P2:拆表和长期清洁

  • 从 accounts 拆出 account_tokens、account_quota_state、account_recovery_state。
  • 从 account_usage 拆出 totals / windows。
  • 统一 _ciphertext 命名。
  • 评估 account_cookies.id 是否保留。

不建议做

  • 不引入 provider、provider_id、proxy_api_key 之类泛化字段。当前项目只做 OpenAI/Codex。
  • 不把配置文件当作每次启动的覆盖源。配置只是首次种子,数据库才是运行时事实来源。
  • 不把请求头字段继续硬编码在 core 里再由数据库补一部分。要么完整默认,要么完整落库。
  • 不先加唯一索引再清理历史重复数据。否则迁移会直接失败。
  • 不把真实 token、cookie、账号邮箱写进文档或测试 fixture。

下一步建议

下一步不再补旧库兼容迁移,优先继续做真实链路验证和状态机收敛:

  1. 继续用真实账号跑请求链路,验证刷新调度、禁用账号、封禁账号、配额冷却和 websocket/http_sse fallback。
  2. 明确 status 状态机,把 disabled/banned/expired/refreshing 的启动扫描和后台调度不变量固化。
  3. 如管理端长期需要窗口级 reasoning/total,再评估是否把 account_usage 拆成 totals/windows。