diff --git a/README.md b/README.md index 15d877e..f0dc9f5 100644 --- a/README.md +++ b/README.md @@ -1,339 +1,44 @@ # cdxs -`cdxs` 是一个 Codex 账号与 `CODEX_HOME` 切换工具。它可以保存多个 Codex OAuth 账号或 API Key 账号,把指定账号写入 Codex 的 `auth.json`,并提供配额查询、会话查看/回收/修复、多 `CODEX_HOME` 管理和简单的配置同步服务。 +`cdxs` 是一个面向 Codex 的账号、home、会话和账号状态同步管理工具。 -## 功能概览 +它把多个 Codex OAuth / API Key 账号保存在本地,再按需写回 Codex 使用的 `auth.json`。同时,它可以管理多个独立 `CODEX_HOME`,避免手动搬运认证文件和会话状态。 -- 保存和切换多个 Codex 账号。 -- 支持从已有 `auth.json` 导入 OAuth 或 API Key 认证信息。 -- 支持通过 OpenAI OAuth 登录并保存 token。 -- 支持直接添加 API Key 账号。 -- 支持为不同项目创建独立的 `CODEX_HOME`,并绑定不同账号。 -- 支持用指定账号或 home 启动外部命令,例如 `codex`。 -- 支持查询 OAuth 账号的 Codex 使用配额。 -- 支持查看 Codex 会话、统计 token、移入垃圾箱、恢复和修复可见性。 -- 支持把本地 `cdxs.toml` 推送到自建同步服务,或从同步服务拉取。 +## 简介 -## 安装与构建 +Codex 的认证和会话状态都来自 `CODEX_HOME`。`cdxs` 做的是在这个目录外面加一层可操作的管理能力: -本项目是 Rust CLI。需要先安装 Rust 工具链。 +- 账号管理:导入已有 `auth.json`,通过 OAuth 登录,添加 API Key 账号,切换当前账号,并在需要时刷新 OAuth token。 +- 配额查看:查询 OAuth 账号的 Codex 使用配额,并缓存到本地。 +- Home 管理:创建命名的 `CODEX_HOME`,并绑定到指定账号。 +- 命令运行:用指定账号或 home 启动子进程,只为该进程设置 `CODEX_HOME`。 +- 会话管理:查看会话、统计 token 和文件信息、移入可恢复垃圾箱、恢复会话、修复缺失的会话索引。 +- 线程同步:在多个受管理 home 之间补齐缺失的会话线程。 +- 状态同步:运行轻量 HTTP 服务,在多台机器之间推送或拉取账号状态。 -```powershell -cargo build --release -``` +管理 Codex 登录信息本地文件。 -构建后的可执行文件位于: +## 存储 -```text -target\release\cdxs.exe -``` +Codex home 的解析顺序: -开发时可直接运行: - -```powershell -cargo run -- --help -``` - -如果希望全局使用,可以把 `target\release` 加入 `PATH`,或者把 `cdxs.exe` 复制到已有的命令目录。 - -## 数据文件 - -默认读取当前 `CODEX_HOME` 环境变量;如果没有设置,则使用用户目录下的 `.codex`: - -```text -%USERPROFILE%\.codex -``` +1. `--codex-home` +2. `CODEX_HOME` +3. `~/.codex` 主要文件: -- `auth.json`:Codex 原生认证文件,`cdxs switch` 会写入这里。 -- `cdxs.toml`:`cdxs` 自己的配置文件,保存账号、home、同步信息。 -- `cdxs-backups\`:写入 `auth.json`、`cdxs.toml`、会话索引或状态库前的备份目录。 -- `cdxs-trash\`:被 `cdxs session trash` 移入垃圾箱的会话。 - -多数命令支持通过 `CODEX_HOME` 控制配置位置;部分命令还提供 `--codex-home` 参数指定目标 Codex home。 - -## 快速开始 - -从当前 Codex 的 `auth.json` 导入账号: - -```powershell -cdxs import auth -``` - -导入后立即切换为当前账号: - -```powershell -cdxs import auth --switch -``` - -查看已保存账号: - -```powershell -cdxs list -``` - -切换账号: - -```powershell -cdxs switch <账号ID或邮箱前缀> -``` - -用指定账号启动 Codex: - -```powershell -cdxs run --account <账号ID或邮箱前缀> -- codex -``` - -## 账号管理 - -OAuth 登录: - -```powershell -cdxs login oauth -``` - -默认会监听 `127.0.0.1:1455` 等待浏览器回调。可指定端口: - -```powershell -cdxs login oauth --port 1456 -``` - -如果不能自动接收回调,可以手动粘贴回调 URL: - -```powershell -cdxs login oauth --manual -``` - -添加 API Key 账号: - -```powershell -cdxs account add-api-key --key sk-... -``` - -使用自定义 API base URL: - -```powershell -cdxs account add-api-key --key sk-... --base-url https://example.com/v1 -``` - -常用账号命令: - -```powershell -cdxs account list -cdxs account current -cdxs account show <账号ID或邮箱前缀> -cdxs account remove <账号ID或邮箱前缀> -cdxs refresh-token <账号ID或邮箱前缀> -``` - -支持 JSON 输出的命令: - -```powershell -cdxs list --json -cdxs account current --json -cdxs account show <账号> --json -``` - -说明:`switch --apply-fingerprint` 参数目前只会输出提示,实际不会应用设备指纹。 - -## 配额查询 - -查询当前账号或第一个账号的 Codex 配额: - -```powershell -cdxs quota -``` - -查询指定账号: - -```powershell -cdxs quota <账号ID或邮箱前缀> -``` - -查询所有账号: - -```powershell -cdxs quota --all -``` - -JSON 输出: - -```powershell -cdxs quota --all --json -``` - -注意:配额查询调用的是 ChatGPT/Codex OAuth 后端接口,只支持 OAuth 账号;API Key 账号不支持该配额查询。 - -## 多 CODEX_HOME 管理 - -创建一个独立 home: - -```powershell -cdxs home create work --path D:\codex-homes\work -``` - -创建时绑定账号,并把账号写入该 home 的 `auth.json`: - -```powershell -cdxs home create work --path D:\codex-homes\work --account <账号> -``` - -绑定已有 home 到账号: - -```powershell -cdxs home bind work <账号> -``` - -查看 home: - -```powershell -cdxs home list -cdxs home path work -``` - -用某个 home 启动命令: - -```powershell -cdxs run --home work -- codex -``` - -删除 home 记录: - -```powershell -cdxs home remove work -``` - -说明:`home remove` 只删除 `cdxs.toml` 里的 home 记录,不会删除实际目录;`default` home 不能删除。 - -## 会话管理 - -列出默认 home 的 Codex 会话: - -```powershell -cdxs session list -``` - -列出所有受管理 home 的会话: - -```powershell -cdxs session list --all-homes -``` - -查看某个会话的统计信息: - -```powershell -cdxs session stats -``` - -移入 `cdxs` 垃圾箱,并从 Codex 会话索引和 SQLite 状态库中隐藏: - -```powershell -cdxs session trash -``` - -查看垃圾箱: - -```powershell -cdxs session trash-list -``` - -恢复会话: - -```powershell -cdxs session restore -``` - -检查会话可见性问题: - -```powershell -cdxs session visibility check -``` - -自动修复可见性问题: - -```powershell -cdxs session visibility repair -``` - -把缺失的会话线程复制到其他受管理 home: - -```powershell -cdxs session sync-threads --all-homes -``` - -预览同步动作,不实际写入: - -```powershell -cdxs session sync-threads --all-homes --dry-run -``` - -会话相关命令会读取 Codex 的 `state_5.sqlite`、`session_index.jsonl` 以及 `sessions` / `archived_sessions` 下的 rollout 文件。写入前会尽量在 `cdxs-backups` 中备份相关文件。 +- `auth.json`:Codex 认证文件,由账号切换或运行命令时写入。 +- `cdxs.toml`:`cdxs` 的本地状态,保存账号、home、同步配置和元数据。 +- `state_5.sqlite`:Codex 会话数据库。 +- `session_index.jsonl`:Codex 会话列表索引。 +- `sessions/`:Codex rollout JSONL 文件。 +- `cdxs-trash/`:会话垃圾箱,用于可恢复删除。 + +会话修复、隐藏、恢复和同步可能修改 Codex 会话状态;写入前会备份相关状态文件。 ## 同步服务 -`cdxs` 内置一个简单同步服务,用于在多台机器之间同步 `cdxs.toml` 中的账号、home 等便携状态。 +内置同步服务只保存每个用户的账号状态,并提供登录、拉取、推送接口。它同步的是可迁移的 `cdxs` 账号状态,不同步整个 Codex home。 -在服务端添加用户: - -```powershell -cdxs server user add alice --password your-password -``` - -启动服务: - -```powershell -cdxs server run --bind 127.0.0.1:8765 -``` - -客户端登录: - -```powershell -cdxs sync login --server http://127.0.0.1:8765 --user alice --password your-password -``` - -推送本地状态到服务端: - -```powershell -cdxs sync push -``` - -从服务端拉取状态到本地: - -```powershell -cdxs sync pull -``` - -查看同步配置: - -```powershell -cdxs sync status -``` - -说明:服务端会保存用户密码哈希和登录 session;客户端拉取/推送的状态会排除服务端用户和同步 token。同步会覆盖账号、home 和 meta 状态,使用前建议先备份当前 `.codex\cdxs.toml`。 - -## 常用命令速查 - -```powershell -cdxs --help -cdxs list -cdxs import auth --switch -cdxs login oauth --switch -cdxs account add-api-key --key sk-... --switch -cdxs switch <账号> -cdxs run --account <账号> -- codex -cdxs quota --all -cdxs home list -cdxs session list --all-homes -cdxs session visibility check --all-homes -``` - -## 开发验证 - -当前代码没有单元测试,但可以运行: - -```powershell -cargo test -``` - -当前实际结果为 0 个测试通过,命令本身成功完成。 +项目包含 `Dockerfile` 和 `compose.yml`。默认容器监听 `8765`,数据目录为 `/data`。 diff --git a/docs/account-management.md b/docs/account-management.md deleted file mode 100644 index 967cb5d..0000000 --- a/docs/account-management.md +++ /dev/null @@ -1,422 +0,0 @@ -# 账号管理 - -本文说明 `cdxs` 当前账号管理功能的实现方式、调用机制和运行过程。 - -## 功能范围 - -账号管理主要覆盖以下能力: - -- 从 Codex 原生 `auth.json` 导入账号。 -- 添加 API Key 账号。 -- 列出所有已保存账号。 -- 查看当前账号。 -- 查看指定账号详情。 -- 删除指定账号。 -- 将指定账号切换为当前账号并写入目标 `auth.json`。 - -## 核心文件 - -- `src/main.rs`:CLI 入口,负责把命令分发到具体模块。 -- `src/cli.rs`:定义账号相关命令和参数。 -- `src/account.rs`:账号管理主逻辑。 -- `src/config_store.rs`:`cdxs.toml` 的数据模型、加载、保存和查询。 -- `src/auth_file.rs`:Codex 原生 `auth.json` 的读取和写入。 -- `src/paths.rs`:解析 `CODEX_HOME`、`auth.json`、`cdxs.toml` 路径。 -- `src/atomic.rs`:写入配置或认证文件前备份,并使用原子写入。 -- `src/jwt.rs`:从 OAuth `id_token` 中解析邮箱、计划、组织等账号元数据。 -- `src/token.rs`:切换 OAuth 账号前按需刷新 token。 - -## 数据保存方式 - -`cdxs` 自己的账号状态保存在当前 `CODEX_HOME` 下的 `cdxs.toml`: - -```text -\cdxs.toml -``` - -如果没有设置 `CODEX_HOME`,默认使用: - -```text -%USERPROFILE%\.codex -``` - -账号列表保存在 `Store.accounts` 中,当前账号 ID 保存在: - -```text -Store.meta.current_account_id -``` - -每个账号使用 `Account` 结构保存,关键字段包括: - -- `id`:稳定账号 ID。 -- `email`:显示用邮箱或 API Key 虚拟邮箱。 -- `auth_mode`:`oauth` 或 `api_key`。 -- `tokens`:OAuth 账号的 token。 -- `openai_api_key`:API Key 账号的 key。 -- `api_base_url`:API Key 账号的可选 base URL。 -- `plan_type`:账号套餐类型。 -- `account_id`:OAuth 账号 ID。 -- `organization_id`:OAuth 组织 ID。 -- `quota`:最近一次配额查询结果。 -- `requires_reauth`:OAuth refresh 失败后标记是否需要重新登录。 - -## 账号 ID 生成原理 - -账号 ID 由 `account.rs` 中的 `stable_id` 生成。 - -OAuth 账号使用以下信息生成稳定 ID: - -- 固定前缀:`oauth` -- 邮箱 -- `account_id` -- `organization_id` - -API Key 账号使用以下信息生成稳定 ID: - -- 固定前缀:`apikey` -- API Key -- `base_url` - -生成方式是对这些字段做 SHA-256,然后取前 16 位十六进制字符串: - -```text -oauth_xxxxxxxxxxxxxxxx -apikey_xxxxxxxxxxxxxxxx -``` - -这样重复导入同一个账号时,会更新原有记录,而不是创建重复账号。 - -## 命令调用机制 - -账号相关命令由 `src/cli.rs` 定义,再由 `src/main.rs` 分发到 `src/account.rs`。 - -```mermaid -flowchart TD - A[用户执行 cdxs 命令] --> B[Cli::parse] - B --> C[src/main.rs match Commands] - C --> D{账号相关命令} - D -->|cdxs import auth| E[account::import_auth] - D -->|cdxs account add-api-key| F[account::add_api_key] - D -->|cdxs list / account list| G[account::list_accounts] - D -->|cdxs account current| H[account::current_account] - D -->|cdxs account show| I[account::show_account] - D -->|cdxs account remove| J[account::remove_account] - D -->|cdxs switch| K[account::switch_account] -``` - -## 导入 auth.json 运行过程 - -命令: - -```powershell -cdxs import auth -``` - -可选参数: - -```powershell -cdxs import auth --file -cdxs import auth --codex-home <路径> -cdxs import auth --switch -``` - -运行过程: - -1. 解析主配置 home,也就是保存 `cdxs.toml` 的位置。 -2. 解析来源 home,用于定位要导入的 `auth.json`。 -3. 调用 `auth_file::read_auth_file` 读取并解析 `auth.json`。 -4. 调用 `account_from_auth` 判断是 OAuth 账号还是 API Key 账号。 -5. OAuth 账号会解析 token,并从 `id_token` 中读取邮箱、计划、账号 ID、组织 ID。 -6. API Key 账号会读取 `OPENAI_API_KEY` 和可选 base URL。 -7. 生成稳定账号 ID。 -8. 调用 `Store::upsert_account` 写入或更新账号。 -9. 如果带 `--switch`,同时写入目标 home 的 `auth.json`,并设置当前账号。 -10. 调用 `Store::save` 保存 `cdxs.toml`。 - -```mermaid -sequenceDiagram - participant U as 用户 - participant M as main.rs - participant A as account.rs - participant P as paths.rs - participant AF as auth_file.rs - participant S as config_store.rs - participant J as jwt.rs - - U->>M: cdxs import auth - M->>A: import_auth(file, codex_home, switch) - A->>P: codex_home(None) - P-->>A: 配置 home - A->>P: codex_home(codex_home) - P-->>A: 来源 home - A->>AF: read_auth_file(auth_path) - AF-->>A: CodexAuthFile - A->>A: account_from_auth - alt OAuth 账号 - A->>J: decode_payload(id_token) - J-->>A: email / plan / account_id / organization_id - A->>A: oauth_account - else API Key 账号 - A->>AF: extract_api_key / api_base_url - AF-->>A: key / base_url - A->>A: api_key_account - end - A->>S: Store::load - S-->>A: Store - A->>S: upsert_account - opt --switch - A->>AF: write_account_to_auth - A->>A: 设置 current_account_id - end - A->>S: save - S-->>U: 导入完成 -``` - -## 添加 API Key 账号运行过程 - -命令: - -```powershell -cdxs account add-api-key --key sk-... -``` - -可选参数: - -```powershell -cdxs account add-api-key --key sk-... --base-url https://example.com/v1 -cdxs account add-api-key --key sk-... --switch -``` - -运行过程: - -1. 解析当前 `CODEX_HOME`。 -2. 加载 `cdxs.toml`。 -3. 校验 API Key 非空。 -4. 根据 API Key 和 base URL 生成稳定账号 ID。 -5. 生成显示用邮箱,格式类似 `api-key-xxxxxxxx`。 -6. 调用 `Store::upsert_account` 保存账号。 -7. 如果带 `--switch`,写入当前 home 的 `auth.json`。 -8. 保存 `cdxs.toml`。 - -```mermaid -flowchart TD - A[cdxs account add-api-key] --> B[paths::codex_home] - B --> C[Store::load] - C --> D[api_key_account] - D --> E[stable_id] - E --> F[Store::upsert_account] - F --> G{是否 --switch} - G -->|是| H[auth_file::write_account_to_auth] - G -->|否| I[跳过 auth.json 写入] - H --> J[Store::save] - I --> J -``` - -## 列出账号运行过程 - -命令: - -```powershell -cdxs list -cdxs account list -``` - -JSON 输出: - -```powershell -cdxs list --json -cdxs account list --json -``` - -运行过程: - -1. 解析当前 `CODEX_HOME`。 -2. 加载 `cdxs.toml`。 -3. 如果 `--json`,直接输出 `store.accounts` 的 JSON。 -4. 如果不是 JSON,按表格输出账号 ID、邮箱、认证模式、套餐和配额。 -5. 当前账号会用 `*` 标记。 - -## 查看当前账号运行过程 - -命令: - -```powershell -cdxs account current -``` - -JSON 输出: - -```powershell -cdxs account current --json -``` - -运行过程: - -1. 加载 `cdxs.toml`。 -2. 读取 `Store.meta.current_account_id`。 -3. 如果没有当前账号,输出未设置账号。 -4. 如果当前账号 ID 不存在,返回错误。 -5. 找到账号后输出详情或 JSON。 - -## 查看指定账号运行过程 - -命令: - -```powershell -cdxs account show <账号ID或邮箱前缀> -``` - -查找账号使用 `Store::find_account`,支持三种匹配方式: - -- 完整账号 ID。 -- 完整邮箱。 -- 邮箱前缀。 - -运行过程: - -1. 加载 `cdxs.toml`。 -2. 调用 `find_account` 查找账号。 -3. 找不到则返回错误。 -4. 找到后输出详情或 JSON。 - -## 删除账号运行过程 - -命令: - -```powershell -cdxs account remove <账号ID或邮箱前缀> -``` - -运行过程: - -1. 加载 `cdxs.toml`。 -2. 用账号 ID、邮箱或邮箱前缀查找账号。 -3. 从 `Store.accounts` 中删除该账号。 -4. 如果该账号是当前账号,清空 `current_account_id`。 -5. 如果有 home 绑定该账号,清空对应 `bound_account_id`。 -6. 保存 `cdxs.toml`。 - -```mermaid -flowchart TD - A[cdxs account remove] --> B[Store::load] - B --> C[Store::find_account] - C --> D{是否存在} - D -->|否| E[返回账号不存在错误] - D -->|是| F[accounts.retain 删除账号] - F --> G{是否当前账号} - G -->|是| H[清空 current_account_id] - G -->|否| I[保持 current_account_id] - H --> J[清空 homes 中相关 bound_account_id] - I --> J - J --> K[Store::save] -``` - -## 切换账号运行过程 - -命令: - -```powershell -cdxs switch <账号ID或邮箱前缀> -``` - -可选指定目标 home: - -```powershell -cdxs switch <账号> --codex-home <路径> -``` - -运行过程: - -1. 加载主配置 home 的 `cdxs.toml`。 -2. 解析目标 home,用于确定要写入哪个 `auth.json`。 -3. 用账号 ID、邮箱或邮箱前缀查找账号。 -4. 如果是 OAuth 账号,调用 `token::refresh_account_if_needed` 检查 access token 是否即将过期。 -5. 如果 token 需要刷新,则先刷新并更新 `cdxs.toml` 中的账号 token。 -6. 调用 `auth_file::write_account_to_auth` 把账号写入目标 home 的 `auth.json`。 -7. 更新该账号的 `last_used_at`。 -8. 设置 `Store.meta.current_account_id`。 -9. 保存 `cdxs.toml`。 - -```mermaid -sequenceDiagram - participant U as 用户 - participant M as main.rs - participant A as account.rs - participant S as config_store.rs - participant T as token.rs - participant AF as auth_file.rs - - U->>M: cdxs switch - M->>A: switch_account - A->>S: Store::load - S-->>A: Store - A->>S: find_account - S-->>A: Account - A->>T: refresh_account_if_needed - alt OAuth token 即将过期 - T->>T: refresh_account - T-->>A: 已更新 tokens - else 不需要刷新或 API Key - T-->>A: 无需刷新 - end - A->>AF: write_account_to_auth - AF-->>A: 写入 auth.json - A->>A: 更新 last_used_at/current_account_id - A->>S: save - S-->>U: 切换完成 -``` - -## auth.json 写入规则 - -账号切换或带 `--switch` 保存账号时,会调用 `auth_file::write_account_to_auth`。 - -OAuth 账号写入格式: - -```json -{ - "OPENAI_API_KEY": null, - "tokens": { - "id_token": "...", - "access_token": "...", - "refresh_token": "...", - "account_id": "..." - }, - "last_refresh": "..." -} -``` - -API Key 账号写入格式: - -```json -{ - "auth_mode": "apikey", - "OPENAI_API_KEY": "sk-..." -} -``` - -## 写入安全机制 - -`cdxs.toml` 和 `auth.json` 写入时都使用同一套安全机制: - -1. 如果目标文件已存在,先备份到: - -```text -\cdxs-backups\ -``` - -2. 写入时先写到同目录临时文件: - -```text -.<原文件名>.tmp -``` - -3. 再用 rename 替换目标文件。 - -这样可以降低写入中断导致配置文件损坏的风险。 - -## 当前实现边界 - -- `switch --apply-fingerprint` 当前只输出提示,实际不会应用设备指纹。 -- API Key 账号没有 OAuth token,也不会参与 token refresh。 -- 账号查找支持邮箱前缀,但如果多个邮箱前缀相同,当前实现会返回第一个匹配项。 -- `remove_account` 只删除 `cdxs.toml` 中的账号记录,不会主动清理已经写入某个 home 的 `auth.json`。 diff --git a/docs/auth-token-quota.md b/docs/auth-token-quota.md deleted file mode 100644 index b5869e2..0000000 --- a/docs/auth-token-quota.md +++ /dev/null @@ -1,454 +0,0 @@ -# 认证与 Token、配额查询 - -本文说明 `cdxs` 当前“认证与 Token、配额查询”功能的实现方式、调用机制和运行过程。 - -## 功能范围 - -本功能主要覆盖以下能力: - -- 通过 OpenAI OAuth PKCE 流程登录 Codex 账号。 -- 支持浏览器本地回调和手动粘贴回调 URL 两种 OAuth 完成方式。 -- 将 OAuth 返回的 token 保存为 `cdxs` 账号。 -- 将已保存账号写入 Codex 原生 `auth.json`。 -- 在切换、运行、配额查询前按需刷新 OAuth access token。 -- 手动刷新指定 OAuth 账号 token。 -- 解码 JWT payload,用于读取邮箱、套餐、账号 ID、组织 ID 和过期时间。 -- 查询 OAuth 账号 Codex 配额,并缓存最近一次配额结果。 - -## 核心文件 - -- `src/main.rs`:CLI 入口,负责把认证、刷新和配额命令分发到具体模块。 -- `src/cli.rs`:定义 `login oauth`、`refresh-token`、`quota` 等命令参数。 -- `src/oauth.rs`:实现 OAuth PKCE 登录、回调解析和授权码换 token。 -- `src/token.rs`:实现 OAuth token 过期检查和 refresh token 刷新。 -- `src/quota.rs`:实现 Codex 配额接口调用、配额解析和展示。 -- `src/auth_file.rs`:负责 Codex 原生 `auth.json` 的读取和写入。 -- `src/jwt.rs`:本地解码 JWT payload,提取账号元数据和过期时间。 -- `src/config_store.rs`:定义账号、token、quota 的持久化模型,并读写 `cdxs.toml`。 - -## 数据保存方式 - -`cdxs` 自己管理的账号和 token 保存在当前 `CODEX_HOME` 下的 `cdxs.toml`: - -```text -\cdxs.toml -``` - -OAuth 账号的 token 保存在 `Account.tokens`: - -- `id_token`:主要用于本地解析账号元数据。 -- `access_token`:用于调用 ChatGPT/Codex 后端接口。 -- `refresh_token`:用于刷新 access token。 - -配额结果保存在 `Account.quota`: - -- `primary_remaining_percent`:主窗口剩余百分比。 -- `primary_reset_time`:主窗口重置时间戳,可为空。 -- `secondary_remaining_percent`:次窗口剩余百分比。 -- `secondary_reset_time`:次窗口重置时间戳,可为空。 -- `updated_at`:本地更新时间戳。 - -Codex CLI 实际读取的认证文件仍然是原生 `auth.json`: - -```text -\auth.json -``` - -`cdxs` 在切换账号或导入登录时,只负责把选中的账号转换成 Codex 兼容格式写入该文件。 - -## 命令调用机制 - -认证与配额相关命令由 `src/cli.rs` 定义,再由 `src/main.rs` 分发: - -```mermaid -flowchart TD - A[用户执行 cdxs 命令] --> B[Cli::parse] - B --> C[src/main.rs match Commands] - C --> D{认证与配额命令} - D -->|cdxs login oauth| E[oauth::login_oauth] - D -->|cdxs refresh-token| F[token::refresh_token_command] - D -->|cdxs quota| G[quota::quota_command] - D -->|cdxs switch| H[account::switch_account] - D -->|cdxs run| I[run_cmd::run_with_account_or_home] - H --> J[token::refresh_account_if_needed] - I --> J - G --> J -``` - -## OAuth 登录实现原理 - -`cdxs login oauth` 使用 PKCE 授权码流程,不需要本地保存 client secret。 - -关键常量在 `src/oauth.rs` 中定义: - -- `CLIENT_ID`:Codex 使用的 OAuth client id。 -- `AUTH_ENDPOINT`:`https://auth.openai.com/oauth/authorize`。 -- `TOKEN_ENDPOINT`:`https://auth.openai.com/oauth/token`。 -- `SCOPES`:`openid profile email offline_access`。 -- `ORIGINATOR`:`codex_vscode`。 - -运行时会生成三类临时值: - -- `code_verifier`:随机 32 字节 base64url 字符串。 -- `code_challenge`:对 `code_verifier` 做 SHA-256 后 base64url 编码。 -- `state`:随机字符串,用来校验回调是否属于本次登录。 - -然后拼出授权 URL,用户在浏览器打开 URL 完成登录。登录成功后,OpenAI 会把浏览器重定向到: - -```text -http://localhost:/auth/callback?code=...&state=... -``` - -默认端口是 `1455`。 - -## OAuth 登录运行过程 - -命令: - -```powershell -cdxs login oauth -``` - -可选参数: - -```powershell -cdxs login oauth --manual -cdxs login oauth --port 1455 -cdxs login oauth --switch -``` - -运行过程: - -1. 生成 `code_verifier`、`code_challenge` 和 `state`。 -2. 构造 OpenAI OAuth 授权 URL 并打印到终端。 -3. 如果没有 `--manual`,在 `127.0.0.1:` 启动一次性 HTTP listener 等待回调。 -4. 如果使用 `--manual`,从标准输入读取用户粘贴的完整回调 URL 或查询字符串。 -5. 调用 `parse_callback_code` 校验回调路径必须是 `/auth/callback`。 -6. 校验回调中的 `state` 必须和本次登录生成的 `state` 一致。 -7. 提取授权码 `code`。 -8. 调用 token endpoint,用 `authorization_code`、`client_id`、`redirect_uri`、`code_verifier` 换取 token。 -9. 要求响应中必须包含 `id_token` 和 `access_token`,`refresh_token` 可选。 -10. 调用 `account::upsert_oauth_tokens` 保存或更新账号。 -11. 如果带 `--switch`,保存账号后会切换到该账号。 - -```mermaid -sequenceDiagram - participant U as 用户 - participant M as main.rs - participant O as oauth.rs - participant B as 浏览器 - participant OA as OpenAI OAuth - participant A as account.rs - participant S as cdxs.toml - - U->>M: cdxs login oauth - M->>O: login_oauth(manual, port, switch) - O->>O: 生成 verifier/challenge/state - O-->>U: 打印授权 URL - U->>B: 打开授权 URL - B->>OA: 登录并授权 - OA-->>B: redirect 到 localhost callback - alt 自动回调模式 - B->>O: GET /auth/callback?code&state - O->>O: 校验 path/state 并提取 code - else --manual 模式 - U->>O: 粘贴回调 URL 或 query - O->>O: 校验 path/state 并提取 code - end - O->>OA: POST /oauth/token grant_type=authorization_code - OA-->>O: id_token/access_token/refresh_token - O->>A: upsert_oauth_tokens(tokens, None, switch) - A->>S: 保存账号和 token - opt --switch - A->>A: 写入 auth.json 并设置当前账号 - end -``` - -## 回调解析机制 - -`parse_callback_code` 支持三种输入形式: - -- 完整 URL,例如 `http://localhost:1455/auth/callback?code=...&state=...`。 -- 路径和查询,例如 `/auth/callback?code=...&state=...`。 -- 纯查询字符串,例如 `?code=...&state=...` 或 `code=...&state=...`。 - -当前实现会拒绝以下情况: - -- URL 格式无效。 -- 路径不是 `/auth/callback`。 -- `state` 不匹配。 -- 缺少 `code` 或 `code` 为空。 - -自动回调模式最多等待 300 秒,超时后返回错误。 - -## auth.json 写入机制 - -`auth_file::write_account_to_auth` 是 `cdxs` 和 Codex 原生认证文件之间的兼容边界。 - -OAuth 账号写入格式: - -```json -{ - "OPENAI_API_KEY": null, - "tokens": { - "id_token": "...", - "access_token": "...", - "refresh_token": "...", - "account_id": "..." - }, - "last_refresh": "..." -} -``` - -API Key 账号写入格式: - -```json -{ - "auth_mode": "apikey", - "OPENAI_API_KEY": "sk-..." -} -``` - -写入前会调用 `atomic::backup_if_exists` 备份已有文件,然后通过 `atomic::write_atomic` 原子替换目标文件。 - -## JWT 解析机制 - -`src/jwt.rs` 只解码 JWT payload,不验证签名。签名有效性由 OpenAI 服务端在 token 被使用时校验。 - -本地解析的字段包括: - -- `email`:账号邮箱。 -- `sub`:JWT subject。 -- `exp`:过期时间戳。 -- `https://api.openai.com/auth.chatgpt_plan_type`:套餐类型。 -- `https://api.openai.com/auth.account_id`:ChatGPT/Codex 账号 ID。 -- `https://api.openai.com/auth.organization_id`:组织 ID。 - -`token_expired` 使用 `exp` 判断 access token 是否过期。当前刷新提前量是 300 秒,也就是 token 距离过期不足 5 分钟时会被视为需要刷新。无法解析的 token 会被视为已过期。 - -## Token 刷新实现原理 - -`src/token.rs` 提供两类刷新入口: - -- `refresh_token_command`:用户显式执行 `cdxs refresh-token `。 -- `refresh_account_if_needed`:切换、运行、配额查询等流程内部按需调用。 - -只支持 OAuth 账号刷新。API Key 账号没有 OAuth token,`refresh_account_if_needed` 对 API Key 返回无需刷新,显式刷新 API Key 账号会返回错误。 - -刷新请求使用: - -```text -POST https://auth.openai.com/oauth/token -grant_type=refresh_token -refresh_token= -client_id= -``` - -刷新响应必须包含新的 `access_token`。如果响应没有新的 `id_token`,当前实现会沿用旧的 `id_token`,因为旧 `id_token` 仍可用于本地展示账号元数据。 - -如果响应没有新的 `refresh_token`,当前实现会继续保存旧的 `refresh_token`。 - -## 手动刷新 Token 运行过程 - -命令: - -```powershell -cdxs refresh-token <账号ID或邮箱前缀> -``` - -运行过程: - -1. 解析当前 `CODEX_HOME`。 -2. 加载 `cdxs.toml`。 -3. 通过账号 ID、完整邮箱或邮箱前缀查找账号。 -4. 校验账号必须是 OAuth 账号。 -5. 校验账号必须保存了 `refresh_token`。 -6. 调用 OpenAI token endpoint 刷新 token。 -7. 更新账号的 `tokens`、`updated_at`、`plan_type`、`account_id`、`organization_id`。 -8. 将 `requires_reauth` 设置为 `false`。 -9. 保存 `cdxs.toml`。 - -如果刷新失败,当前实现会把该账号的 `requires_reauth` 标记为 `true`,并返回错误。 - -```mermaid -flowchart TD - A[cdxs refresh-token account] --> B[Store::load] - B --> C[Store::find_account] - C --> D{OAuth 账号?} - D -->|否| E[返回 API Key 不支持刷新] - D -->|是| F{有 refresh_token?} - F -->|否| G[返回需要重新登录] - F -->|是| H[POST OAuth token endpoint] - H --> I{刷新成功?} - I -->|是| J[更新 tokens 和账号元数据] - I -->|否| K[标记 requires_reauth=true] - J --> L[Store::save] - K --> L -``` - -## 按需刷新运行过程 - -切换账号、运行命令、查询配额前会尽量避免写入或使用即将过期的 access token。 - -按需刷新逻辑: - -1. 找到目标账号。 -2. 如果账号不是 OAuth,直接返回无需刷新。 -3. 如果 OAuth 账号缺少 `tokens`,返回错误。 -4. 用 `jwt::token_expired(access_token, 300)` 判断 access token 是否即将过期。 -5. 如果没有过期,返回无需刷新。 -6. 如果即将过期,调用 `refresh_account` 刷新并更新 store。 - -```mermaid -sequenceDiagram - participant Caller as switch/run/quota - participant T as token.rs - participant J as jwt.rs - participant OA as OpenAI OAuth - participant S as cdxs.toml - - Caller->>T: refresh_account_if_needed(store, account_id) - T->>T: 查找账号并检查 auth_mode - alt API Key 账号 - T-->>Caller: false - else OAuth 账号 - T->>J: token_expired(access_token, 300) - alt 未过期 - J-->>T: false - T-->>Caller: false - else 即将过期或无法解析 - J-->>T: true - T->>OA: POST grant_type=refresh_token - OA-->>T: 新 tokens - T->>S: 更新内存 store,调用方之后保存 - T-->>Caller: true - end - end -``` - -## 配额查询实现原理 - -配额查询由 `src/quota.rs` 实现,只支持 OAuth 账号。API Key 账号会被拒绝,因为配额接口依赖 ChatGPT/Codex 账号上下文。 - -当前调用的后端接口是: - -```text -GET https://chatgpt.com/backend-api/wham/usage -``` - -请求头包括: - -- `Authorization: Bearer `。 -- `Accept: application/json`。 -- `ChatGPT-Account-Id: `,仅当账号保存了非空 `account_id` 时添加。 - -`ChatGPT-Account-Id` 用于多账号或组织场景,避免后端选择错误账号上下文。 - -接口响应中的 `used_percent` 会被转换成剩余百分比: - -```text -remaining_percent = 100 - clamp(used_percent, 0, 100) -``` - -主窗口来自 `rate_limit.primary_window`,次窗口来自 `rate_limit.secondary_window`。重置时间优先使用接口返回的 `reset_at`;如果没有 `reset_at`,则用当前时间加 `reset_after_seconds` 计算。 - -## 配额查询运行过程 - -命令: - -```powershell -cdxs quota -``` - -可选参数: - -```powershell -cdxs quota <账号ID或邮箱前缀> -cdxs quota --all -cdxs quota --json -``` - -账号选择顺序: - -1. 如果传入 `--all`,查询所有保存账号。 -2. 如果传入指定账号,查询该账号。 -3. 如果存在 `Store.meta.current_account_id`,查询当前账号。 -4. 否则查询保存的第一个账号。 - -运行过程: - -1. 解析当前 `CODEX_HOME`。 -2. 加载 `cdxs.toml`。 -3. 根据参数选择要查询的账号 ID 列表。 -4. 对每个账号调用 `refresh_one_quota`。 -5. 查询前先调用 `token::refresh_account_if_needed`。 -6. 校验账号必须是 OAuth 账号。 -7. 使用 access token 调用 usage 接口。 -8. 如果请求失败且错误文本包含 `401`、`token_invalidated` 或 `authentication token has been invalidated`,强制刷新一次 token 后重试。 -9. 解析配额并更新账号的 `plan_type`、`quota`、`updated_at`。 -10. 保存 `cdxs.toml`。 -11. 根据 `--json` 决定输出 JSON 或表格。 - -```mermaid -sequenceDiagram - participant U as 用户 - participant M as main.rs - participant Q as quota.rs - participant T as token.rs - participant API as ChatGPT Usage API - participant S as cdxs.toml - - U->>M: cdxs quota [account|--all|--json] - M->>Q: quota_command(account, all, json) - Q->>S: Store::load - Q->>Q: 选择账号列表 - loop 每个账号 - Q->>T: refresh_account_if_needed - T-->>Q: 已刷新或无需刷新 - Q->>Q: 校验 OAuth 账号和 tokens - Q->>API: GET /backend-api/wham/usage - alt token 失效类错误 - API-->>Q: 401/token_invalidated - Q->>T: refresh_account - T-->>Q: 新 tokens - Q->>API: 重新 GET usage - end - API-->>Q: usage JSON - Q->>Q: parse_quota - Q->>S: 更新账号 quota/plan/updated_at - end - Q->>S: Store::save - Q-->>U: 输出表格或 JSON -``` - -## 配额输出格式 - -默认表格输出字段: - -- `ID`:账号 ID,过长会被截断显示。 -- `Email`:账号邮箱,过长会被截断显示。 -- `Plan`:套餐类型。 -- `5h`:主窗口剩余百分比。 -- `Weekly`:次窗口剩余百分比。 -- `Status`:`ok` 或错误信息。 - -JSON 输出会输出数组,每个元素包括: - -- `id` -- `email` -- `plan_type` -- `quota` -- `error` - -如果查询多个账号时存在失败项,非 JSON 和 JSON 输出都会先展示可用结果,最后返回“部分账号配额刷新失败”的错误。 - -## 当前实现边界 - -- OAuth 登录不会自动打开浏览器,只打印授权 URL。 -- 自动 OAuth 回调只监听 `127.0.0.1:`,并且只处理一次回调请求。 -- 自动 OAuth 回调等待时间是 300 秒。 -- `id_token` 只做本地 payload 解码,不做签名校验。 -- 显式 `refresh-token` 只支持 OAuth 账号,不支持 API Key 账号。 -- 配额查询只支持 OAuth 账号,不支持 API Key 账号。 -- 配额接口错误信息不会输出完整响应体,只输出 HTTP status 和 body 长度,避免泄露响应内容。 -- 如果 token 刷新失败,账号会被标记为 `requires_reauth=true`,但不会自动重新发起 OAuth 登录。 diff --git a/docs/home-session.md b/docs/home-session.md deleted file mode 100644 index b397c89..0000000 --- a/docs/home-session.md +++ /dev/null @@ -1,319 +0,0 @@ -# 多 CODEX_HOME 管理与会话 - -本文说明当前代码中“多 CODEX_HOME 管理”和“会话管理”的实现方式、调用机制与运行过程。相关入口主要在 `src/main.rs`、`src/cli.rs`,实现分布在 `src/account.rs`、`src/run_cmd.rs`、`src/session.rs`、`src/config_store.rs`、`src/paths.rs`、`src/atomic.rs`。 - -## 功能边界 - -当前实现把 `CODEX_HOME` 当作 Codex 原生状态目录使用,每个 home 下可以有自己的 `auth.json`、`state_5.sqlite`、`session_index.jsonl`、`sessions/`、`archived_sessions/` 等文件。`cdxs` 额外在主 home 的 `cdxs.toml` 中保存已管理 home 列表,并支持把账号写入指定 home、用指定 home 启动子命令,以及检查、修复、回收、恢复和跨 home 补齐会话。 - -当前代码没有实现 `current_home` 的切换命令;`Store.meta.current_home` 只是配置模型字段,`list_homes` 会用它标记当前 home,但现有命令不会修改它。 - -## 数据模型 - -`src/config_store.rs` 中的 `Store` 是 `cdxs.toml` 的内存模型,其中和本功能相关的字段是: - -- `meta.current_home`:默认值为 `default`,当前没有命令写入它。 -- `homes`:已管理的 home 列表,每项是 `Home { name, path, bound_account_id }`。 -- `accounts`:账号列表,home 绑定账号时通过 `bound_account_id` 引用这里的账号。 - -配置加载逻辑会自动保证存在一个名为 `default` 的 home。这个默认 home 的路径来自 `paths::codex_home(None)`,也就是优先使用环境变量 `CODEX_HOME`,否则使用用户目录下的 `.codex`。 - -```mermaid -flowchart TD - A[paths::codex_home(None)] --> B{CODEX_HOME 是否存在} - B -->|存在且非空| C[展开并使用 CODEX_HOME] - B -->|不存在| D[使用用户目录/.codex] - C --> E[Store::load] - D --> E - E --> F{cdxs.toml 是否存在} - F -->|不存在或为空| G[Store::with_default_home] - F -->|存在| H[解析 TOML] - G --> I[ensure_default_home] - H --> I - I --> J[得到 homes 列表] -``` - -路径处理由 `src/paths.rs` 完成: - -- `codex_home(override_path)`:命令显式传入路径时优先使用;否则读取 `CODEX_HOME`;再否则使用 `~/.codex`。 -- `config_path(home)`:返回 `/cdxs.toml`。 -- `auth_path(home)`:返回 `/auth.json`。 -- `expand_home(path)`:支持把 `~`、`~/`、`~\` 展开为用户主目录。 - -写配置和关键状态文件时,`src/atomic.rs` 提供两类基础能力: - -- `write_atomic`:先写同目录临时文件,再 rename 到目标路径,避免半写入。 -- `backup_if_exists`:把已有文件复制到 `/cdxs-backups/`,文件名带时间戳。 - -## 多 CODEX_HOME 管理 - -### 命令入口 - -`src/cli.rs` 定义 `cdxs home` 子命令,`src/main.rs` 负责分发到 `account.rs`: - -- `cdxs home list [--json]` -> `account::list_homes` -- `cdxs home create --path [--account ]` -> `account::create_home` -- `cdxs home bind ` -> `account::bind_home` -- `cdxs home path ` -> `account::home_path` -- `cdxs home remove ` -> `account::remove_home` - -### 创建 home - -`create_home` 总是加载主 home 下的 `cdxs.toml`,不会把新 home 当作配置源。它会检查名称是否重复,解析并展开目标路径,创建目录,然后把新记录写入 `Store.homes`。 - -如果创建时传入 `--account`,代码会先在主配置的账号列表里解析账号,拿到稳定账号 ID,随后立即把该账号写入新 home 的 `auth.json`。这样新 home 创建完成后已经具备 Codex 可读取的认证文件。 - -```mermaid -sequenceDiagram - participant CLI as cdxs home create - participant Main as main.rs - participant Account as account::create_home - participant Store as cdxs.toml Store - participant FS as 文件系统 - participant Auth as auth.json - - CLI->>Main: HomeCommands::Create - Main->>Account: create_home(name, path, account) - Account->>Store: Store::load(main_home) - Account->>Store: 检查 home 名称是否已存在 - Account->>FS: expand_home(path) 并 create_dir_all - alt 传入 --account - Account->>Store: find_account(account) - Account->>Auth: write_account_to_auth(new_home/auth.json) - end - Account->>Store: push Home{name,path,bound_account_id} - Account->>Store: save(main_home) -``` - -### 绑定、查看和删除 home - -`bind_home` 只修改 `cdxs.toml` 中指定 home 的 `bound_account_id`,不会立即改写该 home 的 `auth.json`。真正运行命令时,`run` 会根据绑定账号刷新 token 并写入目标 home 的 `auth.json`。 - -`home_path` 只查找并打印配置中记录的路径。 - -`remove_home` 只从 `Store.homes` 中删除记录,不删除磁盘上的 home 目录。代码明确禁止删除名为 `default` 的 home。 - -## 用指定账号或 home 运行命令 - -### 命令入口 - -`cdxs run` 的参数定义在 `RunArgs`: - -- `--account `:指定账号,在目标 `CODEX_HOME` 中准备 `auth.json`。 -- `--home `:指定已管理 home,如果该 home 绑定了账号,则先准备认证。 -- `--codex-home `:和 `--account` 一起使用,用作目标 home 路径。 -- `-- `:实际启动的子命令,例如 `codex`。 - -`--account` 与 `--home` 互斥。没有命令参数时会报错。 - -### 运行机制 - -`src/run_cmd.rs` 的 `run_with_account_or_home` 只做三件事: - -1. 解析目标 home。 -2. 必要时调用 `account::prepare_account_in_home` 写入目标 home 的 `auth.json`。 -3. 启动子进程,并给子进程设置 `CODEX_HOME=`。 - -子进程环境被设置后,Codex 自己会从这个 home 读取 `auth.json` 和会话状态文件。父进程的 shell 环境不会被永久修改。 - -```mermaid -flowchart TD - A[cdxs run] --> B{使用 --home?} - B -->|是| C[resolve_home_for_run] - C --> D{home 是否绑定账号} - D -->|是| E[prepare_account_in_home] - D -->|否| F[使用 home.path] - E --> F - B -->|否| G{是否有 --account} - G -->|否| H[报错: run 需要 --account 或 --home] - G -->|是| I[paths::codex_home(--codex-home)] - I --> J[prepare_account_in_home] - J --> F - F --> K[Command::new] - K --> L[child.env CODEX_HOME=target_home] - L --> M[启动子命令并等待退出] -``` - -`prepare_account_in_home` 的流程是:从主 home 加载 `cdxs.toml`,按 ID、邮箱或邮箱前缀查找账号,调用 `token::refresh_account_if_needed` 按需刷新 OAuth token,然后把账号写入目标 home 的 `auth.json`,更新账号 `last_used_at`,最后保存主配置。 - -这里的设计重点是:账号库只存在于主 home 的 `cdxs.toml`,而运行时认证材料会被物化到目标 home 的 `auth.json`。 - -## 会话管理 - -### Codex 会话状态来源 - -`src/session.rs` 认为 Codex 会话可见性由三类文件共同决定: - -- `/state_5.sqlite`:SQLite 数据库,读取 `threads` 表。 -- `/session_index.jsonl`:会话选择器索引,每行 JSON 包含会话 ID 等信息。 -- `/sessions/` 和 `/archived_sessions/`:rollout JSONL 文件目录。 - -`SessionSummary` 由 SQLite 的 `threads` 行转换而来,包含 home 名称、home 路径、会话 ID、标题、cwd、更新时间、token 数、rollout 路径和 archived 标记。 - -### home 选择规则 - -会话命令都有 `--all-homes` 选项。底层通过 `homes(all_homes)` 选择扫描范围: - -- 不传 `--all-homes`:只扫描当前解析出来的默认 home,名称固定为 `default`。 -- 传 `--all-homes`:加载主 home 的 `cdxs.toml`,遍历 `Store.homes`,展开每个路径,并按路径去重。 - -```mermaid -flowchart TD - A[session 命令] --> B{--all-homes?} - B -->|否| C[只返回 paths::codex_home(None)] - B -->|是| D[Store::load(default_home)] - D --> E[遍历 store.homes] - E --> F[expand_home] - F --> G[按路径去重] - C --> H[扫描会话] - G --> H -``` - -### 会话命令入口 - -`src/cli.rs` 定义 `cdxs session` 子命令,`src/main.rs` 分发到 `session.rs`: - -- `cdxs session list [--all-homes] [--json]`:列出会话。 -- `cdxs session stats [--all-homes] [--json]`:显示单个会话的 token 和文件统计。 -- `cdxs session trash [--all-homes]`:把会话移入 `cdxs` 自己的垃圾箱。 -- `cdxs session trash-list [--all-homes] [--json]`:列出垃圾箱条目。 -- `cdxs session restore [--all-homes]`:从垃圾箱恢复会话。 -- `cdxs session visibility check [--all-homes] [--json]`:检查 SQLite、索引、rollout 的一致性。 -- `cdxs session visibility repair [--all-homes] [--json]`:修复可见性问题。 -- `cdxs session sync-threads [--all-homes] [--dry-run] [--json]`:在多个 home 之间补齐缺失线程。 - -### 列表与统计 - -`list_sessions` 遍历选中的 home,调用 `read_sessions_for_home` 打开 `/state_5.sqlite`,读取 `threads` 表,然后按 `updated_at_ms` 倒序输出。若数据库不存在,该 home 返回空列表,不报错。 - -`session_stats` 先用 `find_thread` 查找指定会话,再解析对应 rollout 文件: - -- 文件存在时读取大小和行数。 -- 遍历 JSONL 行,递归查找最后一个 `total_token_usage`,提取 total、input、output token。 -- SQLite 中的 `tokens_used` 和 rollout 中的 token 统计会分别显示。 - -### 垃圾箱与恢复 - -`trash_sessions` 对每个会话调用 `trash_one`。垃圾箱目录位于 `/cdxs-trash/--/`。 - -移动到垃圾箱时会保存三类可恢复材料: - -- rollout 文件副本,如果原 rollout 存在。 -- 从 `session_index.jsonl` 删除的原始行。 -- SQLite `threads` 原始行,写入 `manifest.json`。 - -随后代码会从 SQLite `threads` 表删除该会话,并删除原 rollout 文件。这样 Codex 不再能看到该会话。 - -```mermaid -sequenceDiagram - participant Cmd as cdxs session trash - participant S as session.rs - participant DB as state_5.sqlite - participant IDX as session_index.jsonl - participant Rollout as rollout jsonl - participant Trash as cdxs-trash - - Cmd->>S: trash_sessions(ids) - S->>DB: find_thread(session_id) - S->>Trash: 创建垃圾箱目录 - S->>Rollout: 复制 rollout 到垃圾箱 - S->>IDX: 删除匹配 session_id 的索引行 - S->>Trash: 写 manifest.json - S->>DB: DELETE FROM threads - S->>Rollout: 删除原 rollout 文件 -``` - -`restore_sessions` 会扫描垃圾箱 manifest,找到匹配会话后调用 `restore_one`: - -1. 把 manifest 中保存的 `ThreadRowData` 插回 SQLite。 -2. 把备份 rollout 复制回原始 rollout 路径。 -3. 把保存的 `session_index.jsonl` 行追加回索引,已有同 ID 时不会重复追加。 -4. 删除对应垃圾箱目录。 - -### 可见性检查与修复 - -`visibility_check` 会同时读取 SQLite thread、`session_index.jsonl` 和 rollout 文件,报告以下问题: - -- `missing_rollout`:SQLite 中有线程,但 `rollout_path` 指向的文件不存在。 -- `missing_index`:SQLite 中有线程,但 `session_index.jsonl` 没有该 ID。 -- `orphan_index`:索引里有 ID,但 SQLite 没有线程。 -- `orphan_rollout`:rollout 文件里有 `session_meta`,但 SQLite 没有线程。 - -rollout 扫描只处理 `sessions/` 和 `archived_sessions/` 下的 `.jsonl` 文件,并只读取每个文件前 25 行查找 `type == "session_meta"` 的记录。 - -`visibility_repair` 的修复策略是以 SQLite 现有线程为优先来源: - -- 如果 SQLite 线程的 rollout 路径不存在,但扫描到了同 ID 的 rollout,则更新 SQLite 中的 `rollout_path`。 -- 如果 SQLite 线程缺少索引行,则根据 thread 生成紧凑 JSONL 索引并追加。 -- 如果存在 orphan rollout,则从 rollout 的 `session_meta` 构造一个最小 `ThreadRowData` 并插入 SQLite,同时补索引。 - -修复前会备份 `state_5.sqlite` 和 `session_index.jsonl` 到 `/cdxs-backups/`。 - -```mermaid -flowchart TD - A[visibility repair] --> B[读取 threads] - B --> C[读取 session_index.jsonl] - C --> D[扫描 sessions 和 archived_sessions] - D --> E{线程 rollout 缺失但找到同 ID rollout?} - E -->|是| F[备份状态文件并更新 rollout_path] - E -->|否| G[继续] - F --> G - G --> H{线程缺少索引?} - H -->|是| I[备份并追加索引行] - H -->|否| J[继续] - I --> J - J --> K{rollout 没有 SQLite 线程?} - K -->|是| L[从 session_meta 构造最小 thread 并插入] - K -->|否| M[结束] - L --> M -``` - -### 跨 home 补齐会话 - -`sync_threads` 用于在多个已管理 home 之间复制缺失的会话线程。它要求选中 home 数量至少为 2;通常需要配合 `--all-homes` 使用,否则只会选到默认 home 并报错。 - -运行过程: - -1. 读取每个 home 的 SQLite threads 和索引 ID。 -2. 按 session ID 建立全局映射,第一次看到的线程作为源。 -3. 对每个目标 home,检查是否缺少某个 session ID。 -4. 如果源 rollout 不存在,则记录 `skip`。 -5. 如果不是 `--dry-run`,则备份目标状态文件,复制 rollout,插入 SQLite thread,并按需追加索引行。 - -当目标路径上已经存在同名 rollout 时,`portable_rollout_path` 会把目标路径改为 `sessions/cdxs-sync//`,避免覆盖目标 home 的已有文件。 - -```mermaid -sequenceDiagram - participant Cmd as cdxs session sync-threads - participant S as session.rs - participant Src as 源 home - participant Tgt as 目标 home - - Cmd->>S: sync_threads(all_homes, dry_run, json) - S->>S: homes(all_homes) 并要求至少两个 - S->>Src: 读取 threads 和 session_index - S->>Tgt: 读取 threads 和 session_index - S->>S: 第一次看到的 session 作为源 - alt 目标缺少 session 且源 rollout 存在 - S->>Tgt: 备份 state_5.sqlite 和 session_index.jsonl - S->>Tgt: 复制 rollout - S->>Tgt: INSERT OR REPLACE thread - S->>Tgt: 必要时追加 index - else dry-run - S->>S: 只记录 would_sync - else 源 rollout 缺失 - S->>S: 记录 skip - end -``` - -## 安全性与持久化细节 - -配置保存和会话修复会尽量避免直接覆盖关键文件: - -- `Store::save` 保存 `cdxs.toml` 前会先备份旧文件,再原子写入新文件。 -- `remove_session_index_entries` 修改 `session_index.jsonl` 前会备份旧索引。 -- `restore_session_index_entries` 恢复索引前会备份旧索引。 -- `backup_state_files` 会备份 `state_5.sqlite` 和 `session_index.jsonl`,供修复和同步使用。 - -需要注意的是,`trash_one` 删除 SQLite 行和 rollout 文件本身不是一个数据库事务加文件事务的整体原子操作;它通过先写 manifest 和备份 rollout 来保证后续可以用 `restore` 尝试恢复。 - diff --git a/docs/sync-server.md b/docs/sync-server.md deleted file mode 100644 index f0d9945..0000000 --- a/docs/sync-server.md +++ /dev/null @@ -1,438 +0,0 @@ -# 配置同步/服务端 - -本文说明 `cdxs` 当前配置同步和同步服务端功能的实现方式、调用机制和运行过程。 - -## 功能范围 - -配置同步/服务端主要覆盖以下能力: - -- 启动一个最小 HTTP 同步服务。 -- 在服务端配置文件中添加或更新登录用户。 -- 客户端使用用户名和密码登录同步服务。 -- 客户端把本地可移植配置推送到服务端。 -- 客户端从服务端拉取可移植配置并覆盖本地状态。 -- 查看当前客户端同步配置状态。 -- 使用 Docker 或 docker-compose 运行同步服务。 - -当前同步的数据只包含 `cdxs.toml` 中的可移植状态: - -- `meta` -- `accounts` -- `homes` - -不会同步客户端本地的 `sync` 配置,也不会把服务端的 `server.users`、`server.sessions` 下发给客户端。 - -## 核心文件 - -- `src/main.rs`:CLI 入口,负责把 `server` 和 `sync` 命令分发到具体模块。 -- `src/cli.rs`:定义同步服务端和客户端命令参数。 -- `src/server.rs`:HTTP 同步服务端实现,包含用户管理、登录、鉴权、状态读写。 -- `src/sync_client.rs`:客户端同步逻辑,包含登录、拉取、推送和状态查看。 -- `src/config_store.rs`:`cdxs.toml` 数据模型、加载、保存和默认 home 初始化。 -- `Dockerfile`:构建并运行 `cdxs server run` 的容器镜像。 -- `docker-compose.yml`:使用具名 volume 持久化 `/data/cdxs.toml` 并暴露 `8765` 端口。 - -## 数据保存方式 - -同步功能复用 `Store` 结构,也就是 `cdxs.toml` 的配置模型。 - -客户端默认读取当前 `CODEX_HOME` 下的配置: - -```text -\cdxs.toml -``` - -如果没有设置 `CODEX_HOME`,默认使用: - -```text -%USERPROFILE%\.codex\cdxs.toml -``` - -服务端有两种数据路径: - -- 如果 `server run --data <路径>` 被指定,服务端直接使用这个路径保存数据。 -- 如果未指定 `--data`,服务端使用当前 `CODEX_HOME` 下的 `cdxs.toml`。 - -`server user add` 也使用同一套路径规则,因此添加用户和启动服务必须指向同一个数据文件,才能让服务端读取到该用户。 - -## Store 中的相关字段 - -`Store` 中和同步相关的字段包括: - -- `meta`:当前账号、当前 home 等元数据。 -- `accounts`:账号列表。 -- `homes`:受管理的 `CODEX_HOME` 列表。 -- `server`:服务端用户和 bearer session,仅服务端使用。 -- `sync`:客户端保存的服务端地址、用户名、token、最近拉取/推送时间。 - -服务端用户保存在: - -```text -Store.server.users -``` - -服务端登录 session 保存在: - -```text -Store.server.sessions -``` - -客户端同步配置保存在: - -```text -Store.sync -``` - -## 服务端用户与 Token 原理 - -添加用户时,`server::add_user` 会生成随机 salt,并把密码保存为哈希值。 - -密码哈希方式: - -```text -sha256(salt + ":" + password) -``` - -登录成功后,服务端生成一个随机 bearer token,并只保存 token 的哈希值。 - -token 哈希方式: - -```text -sha256("cdxs-token:" + token) -``` - -原始 token 只在 `/v1/login` 响应中返回一次,由客户端保存到本地 `Store.sync.token`。后续 `pull` 和 `push` 使用: - -```http -Authorization: Bearer -``` - -服务端收到请求后,对请求中的 token 重新计算哈希,并检查是否存在于 `Store.server.sessions`。 - -```mermaid -flowchart TD - A[server user add] --> B[生成 salt] - B --> C[计算 password_hash] - C --> D[写入 Store.server.users] - E[sync login] --> F[校验 username/password] - F --> G[生成 bearer token] - G --> H[保存 token_hash 到 Store.server.sessions] - H --> I[原始 token 返回给客户端] - J[sync pull/push] --> K[Authorization Bearer token] - K --> L[服务端计算 token_hash] - L --> M{sessions 中是否存在} - M -->|是| N[允许访问 /v1/state] - M -->|否| O[返回 401] -``` - -## HTTP API - -同步服务端使用 `axum` 提供三个接口: - -- `GET /health`:健康检查,返回 `ok`。 -- `POST /v1/login`:使用用户名和密码登录,返回 bearer token。 -- `GET /v1/state`:鉴权后返回服务端保存的可移植状态。 -- `PUT /v1/state`:鉴权后用客户端提交的可移植状态替换服务端状态。 - -`/v1/state` 的读写都会经过 `authorize` 鉴权。 - -服务端返回给客户端前会调用 `sanitized_for_client`,把以下字段清空: - -- `server` -- `sync` - -这样客户端不会拿到服务端用户、密码哈希、已签发 session,也不会拿到服务端自己的同步配置。 - -## 命令调用机制 - -同步相关命令由 `src/cli.rs` 定义,再由 `src/main.rs` 分发。 - -```mermaid -flowchart TD - A[用户执行 cdxs 命令] --> B[Cli::parse] - B --> C[src/main.rs match Commands] - C --> D{命令类型} - D -->|cdxs server run| E[server::run_server] - D -->|cdxs server user add| F[server::add_user] - D -->|cdxs sync login| G[sync_client::login] - D -->|cdxs sync pull| H[sync_client::pull] - D -->|cdxs sync push| I[sync_client::push] - D -->|cdxs sync status| J[sync_client::status] -``` - -## 添加服务端用户运行过程 - -命令: - -```powershell -cdxs server user add --password -``` - -可选指定服务端数据文件: - -```powershell -cdxs server user add --password --data -``` - -运行过程: - -1. 调用 `resolve_data_path` 解析服务端数据文件路径。 -2. 调用 `Store::load_from_path` 加载服务端 `cdxs.toml`,文件不存在时生成默认 store。 -3. 校验用户名和密码不能为空。 -4. 生成随机 salt。 -5. 使用 salt 和密码计算 `password_hash`。 -6. 如果用户已存在,替换原用户记录。 -7. 如果用户不存在,追加到 `Store.server.users`。 -8. 调用 `Store::save_to_path` 保存服务端配置。 - -```mermaid -sequenceDiagram - participant U as 用户 - participant M as main.rs - participant S as server.rs - participant C as config_store.rs - - U->>M: cdxs server user add alice --password *** - M->>S: add_user(data, username, password) - S->>S: resolve_data_path - S->>C: Store::load_from_path - C-->>S: Store - S->>S: random_token 生成 salt - S->>S: hash_secret - S->>S: upsert ServerUser - S->>C: save_to_path - C-->>U: 用户已添加/更新 -``` - -## 启动服务端运行过程 - -命令: - -```powershell -cdxs server run -``` - -默认监听: - -```text -127.0.0.1:8765 -``` - -可选参数: - -```powershell -cdxs server run --bind 0.0.0.0:8765 -cdxs server run --data -``` - -运行过程: - -1. 调用 `resolve_data_path` 解析服务端数据文件和默认 home。 -2. 解析 `--bind` 为 `SocketAddr`。 -3. 创建 `AppState`,保存数据路径、默认 home 和一个异步 `Mutex`。 -4. 注册 `/health`、`/v1/login`、`/v1/state` 路由。 -5. 绑定 TCP listener。 -6. 调用 `axum::serve` 持续处理请求。 - -服务端在每个会读写配置文件的请求中都会先获取 `AppState.lock`。这个锁用于串行化文件读写,避免并发请求同时读写同一个 `cdxs.toml`。 - -```mermaid -flowchart TD - A[cdxs server run] --> B[resolve_data_path] - B --> C[解析 bind 地址] - C --> D[创建 AppState] - D --> E[注册 axum Router] - E --> F[绑定 TcpListener] - F --> G[axum::serve] - G --> H[处理 /health /v1/login /v1/state] -``` - -## 客户端登录运行过程 - -命令: - -```powershell -cdxs sync login --server http://127.0.0.1:8765 --user alice --password -``` - -运行过程: - -1. 客户端规范化 server URL,去掉末尾 `/`。 -2. 向 `/v1/login` 发送 JSON 请求。 -3. 服务端加载 `cdxs.toml`。 -4. 服务端按用户名查找 `Store.server.users`。 -5. 服务端校验密码哈希。 -6. 登录成功后生成 bearer token。 -7. 服务端保存 token 哈希到 `Store.server.sessions`。 -8. 客户端解析响应中的原始 token。 -9. 客户端加载本地 `cdxs.toml`。 -10. 客户端写入 `Store.sync.server_url`、`Store.sync.username`、`Store.sync.token`。 -11. 客户端保存本地配置。 - -```mermaid -sequenceDiagram - participant U as 用户 - participant C as sync_client.rs - participant H as HTTP - participant S as server.rs - participant FS as cdxs.toml - - U->>C: cdxs sync login - C->>H: POST /v1/login username/password - H->>S: login_handler - S->>FS: 加载服务端 Store - S->>S: 校验 password_hash - S->>S: 生成 bearer token 并保存 token_hash - S->>FS: 保存服务端 Store - S-->>C: token - C->>FS: 加载本地 Store - C->>FS: 保存 sync.server_url / username / token - C-->>U: sync login 成功 -``` - -## 拉取配置运行过程 - -命令: - -```powershell -cdxs sync pull -``` - -运行过程: - -1. 加载本地 `cdxs.toml`。 -2. 从 `Store.sync` 读取 server URL 和 token。 -3. 如果未登录或缺少 token,返回错误。 -4. 向 `/v1/state` 发送 `GET` 请求,并附带 `Authorization` header。 -5. 服务端校验 bearer token。 -6. 服务端加载服务端 `Store`。 -7. 服务端清空 `server` 和 `sync` 字段后返回 JSON。 -8. 客户端解析远端 `Store`。 -9. 客户端用远端 `meta`、`accounts`、`homes` 覆盖本地对应字段。 -10. 客户端保留本地 `sync` 配置,并更新 `last_pull_at`。 -11. 保存本地 `cdxs.toml`。 - -```mermaid -flowchart TD - A[cdxs sync pull] --> B[加载本地 Store] - B --> C[sync_endpoint 读取 server/token] - C --> D[GET /v1/state] - D --> E[服务端 authorize] - E --> F[服务端 sanitized_for_client] - F --> G[返回远端 Store] - G --> H[覆盖本地 meta/accounts/homes] - H --> I[更新 last_pull_at] - I --> J[保存本地 cdxs.toml] -``` - -## 推送配置运行过程 - -命令: - -```powershell -cdxs sync push -``` - -运行过程: - -1. 加载本地 `cdxs.toml`。 -2. 从 `Store.sync` 读取 server URL 和 token。 -3. 克隆本地 `Store` 作为上传 payload。 -4. 上传前把 payload 中的 `server` 和 `sync` 字段清空。 -5. 向 `/v1/state` 发送 `PUT` 请求,并附带 `Authorization` header。 -6. 服务端校验 bearer token。 -7. 服务端加载原服务端 `Store`。 -8. 服务端只用 payload 中的 `meta`、`accounts`、`homes` 替换服务端状态。 -9. 服务端把 `sync` 置为默认值,保留原服务端的 `server.users` 和 `server.sessions`。 -10. 服务端保存配置。 -11. 客户端更新本地 `last_push_at`。 -12. 客户端保存本地 `cdxs.toml`。 - -```mermaid -sequenceDiagram - participant C as sync_client.rs - participant S as server.rs - participant FS as cdxs.toml - - C->>FS: 加载本地 Store - C->>C: payload.server/default, payload.sync/default - C->>S: PUT /v1/state + Bearer token - S->>FS: 加载服务端 Store - S->>S: authorize - S->>S: 替换 meta/accounts/homes - S->>FS: 保存服务端 Store - S-->>C: sanitized Store - C->>FS: 更新 last_push_at 并保存 -``` - -## 查看同步状态 - -命令: - -```powershell -cdxs sync status -``` - -输出内容来自本地 `Store.sync`: - -- `server` -- `user` -- `token` -- `last_pull_at` -- `last_push_at` - -如果本地保存了 token,输出只显示: - -```text - -``` - -不会打印 token 明文。 - -## Docker 运行方式 - -`Dockerfile` 使用两阶段构建: - -1. 使用 `rust:1-bookworm` 构建 release 版本 `cdxs`。 -2. 使用 `debian:bookworm-slim` 作为运行镜像。 -3. 安装 `ca-certificates`。 -4. 把 `cdxs` 复制到 `/usr/local/bin/cdxs`。 -5. 暴露 `8765` 端口。 -6. 声明 `/data` volume。 - -容器默认命令: - -```text -cdxs server run --bind 0.0.0.0:8765 --data /data/cdxs.toml -``` - -`docker-compose.yml` 会: - -- 构建当前目录镜像。 -- 将容器命名为 `cdxs-server`。 -- 设置 `restart: unless-stopped`。 -- 映射宿主机 `8765` 到容器 `8765`。 -- 使用具名 volume `cdxs-data` 挂载到 `/data`。 - -## 写入安全机制 - -服务端和客户端保存 `cdxs.toml` 都调用 `Store::save` 或 `Store::save_to_path`。 - -保存时会: - -1. 如果目标文件已存在,先备份到对应 home 下的 `cdxs-backups`。 -2. 将 `Store` 序列化为 TOML。 -3. 写入同目录临时文件。 -4. 使用 rename 替换目标文件。 - -服务端请求还会使用 `tokio::sync::Mutex` 串行化文件读写,降低并发请求导致状态覆盖或文件损坏的风险。 - -## 当前实现边界 - -- 同步没有字段级合并或冲突解决,`pull` 会用远端 `meta/accounts/homes` 覆盖本地,`push` 会用本地 `meta/accounts/homes` 覆盖服务端。 -- bearer session 当前没有过期时间和撤销命令。 -- 服务端用户只有添加/更新命令,没有删除、列表或改密命令。 -- 服务端没有多租户隔离;所有通过鉴权的用户访问同一份服务端状态。 -- HTTP 服务没有在代码中直接配置 TLS,需要由外部反向代理或部署环境处理。 -- `sync push` 不上传客户端本地 `sync` token,也不上传客户端本地 `server` 配置。 -- `sync pull` 不会写入远端 `server` 或远端 `sync` 字段,只更新本地可移植状态并保留本地同步登录信息。