--- id: codex-model-switch name: Codex 模型切换 status: planned progress: 0 --- ## 概述 在 DeepSeek 与官方模型之间安全切换 Codex 的模型提供方,并管理 DeepSeek API Key。 炼境侧**只读**:展示当前档位、Key 提示、冲突告警,所有写入动作转发给独立二进制 `codex-switch` 执行。 ## 决策记录 - **写入职责不放炼境**:`claude-autonomy-config` 已立下规则「炼境只读,不写第三方工具配置,避免意外覆盖用户的 settings.json」。模型切换必须写 `~/.codex/config.toml`,若放进炼境核心就与该规则直接冲突。故写入交给独立 CLI,炼境只做展示与转发。 - **不引入插件系统**:炼境「工具」页(`ToolsPage.tsx`)本就是「低频独立页合并为 tabs」的容器,`v2rayn-import`、`devtools-scan` 已确立「感知宿主开发工具状态」这一模块族。新需求是该族成员,加一个 tab 即可,无需先造扩展机制。 - **Key 归 CLI 管理,落炼境 SQLite**:写配置的是 CLI,密钥就该在写入方一侧,否则 CLI 独立使用时没有密钥来源。炼境因此不新增数据表,也不接触 DeepSeek 明文密钥。 - **只物化一处**:Key 只写 `[model_providers.deepseek].experimental_bearer_token`,并停止向 `[shell_environment_policy.set]` 写 `DEEPSEEK_API_KEY`。现状两处值不一致(`sk-599a66…` vs `sk-c12beb…`)正源于此。 --- ## 设计决策:Codex 模型切换工具 ### 背景 - 关联现有模块:`devtools-scan`(宿主开发工具扫描)、`wsl-proxy`(第三方工具配置同步)、`claude-autonomy-config`(外部 AI CLI 配置只读展示先例)、`api-keys`(密钥保管,职责不同不复用存储) - 现状事实:`~/.codex/config.toml` 已被 DeepSeek 官方脚本改写(`model = "deepseek-flash"`、`model_provider = "deepseek"`);`[model_providers.code-switch]` 由 cc-studio 托管且处于 `disabled_alias`;DeepSeek Key 同时存在于 provider 段与 `shell_environment_policy.set` 两处且值不同 - 用户痛点:官方脚本是一次性安装器(只有「装 flash / 装 v4-pro / 恢复到安装前」),无状态查询、无 Key 体检、无失败回滚 ### 方案选择 | 方案 | 说明 | 结论 | | --- | --- | --- | | A 纯炼境内置模块 | Rust 直接写 `config.toml` | 否决:违反 `claude-autonomy-config` 既有规则,且炼境承担第三方配置写坏的责任 | | B 独立 CLI + 炼境只读面板 | CLI 拥有写入与密钥;炼境只读探测 + 转发 | **选定** | | C 完整插件系统 | 先定义 manifest / 发现 / 生命周期 | 否决:当前仅一个候选插件,属过度设计 | ### 受影响文件 新增: - `src-tauri/src/commands/codex_switch.rs` — 只读探测 Codex 配置状态 + 定位并调用 `codex-switch` CLI - `src/components/tools/CodexSwitchPanel.tsx` — 工具页面板 修改: - `src-tauri/src/commands/mod.rs` — 声明 `pub mod codex_switch` - `src-tauri/src/lib.rs` — 注册 command(静态 invoke_handler) - `src/lib/commands.ts` — 前端封装(AGENTS.md 硬约定:新增 command 必须同步) - `src/components/tools/ToolsPage.tsx` — 新增 `codex` tab 同仓库内(独立子项目,自带 Cargo workspace,不纳入 src-tauri 构建): - `codex-switch/crates/core` — 纯逻辑:detect / config_edit / catalog / secret / verify / backup - `codex-switch/crates/cli` — 命令入口:status / preview / switch / undo / set-key / doctor ### 系统边界变更 - **不新增数据表**(Key 归 CLI,炼境不落密钥) - 新增 Tauri command: - `get_codex_switch_status() -> CodexSwitchStatus` — 只读;含当前档位、provider、Key hint(脱敏)、CLI 是否已安装、cc-studio 冲突、残留旧条目 - `run_codex_switch_action(action, target?) -> ActionResult` — 转发给 CLI,回传 stdout/stderr 与退出码 - CLI 契约(炼境与 CLI 之间的稳定接口): - `codex-switch status --json` - `codex-switch preview --to --json` - `codex-switch switch --to ` - `codex-switch undo` - `codex-switch set-key --stdin` - `codex-switch doctor --json` ### 风险点 - cc-studio 同时托管 `model_provider` / `preferred_auth_method` / `model_providers.code-switch`:写入会互相覆盖 → CLI preflight 检测托管文件,拒绝静默覆盖并告警 - CLI 未安装:面板降级为「未安装 + 获取引导」,不报错 - 配置被 CLI 改写后面板缓存陈旧 → 每次进入面板重新探测,不做前端缓存 - 二进制分发:CLI 需随炼境打包(Tauri xternalBin)或落 PATH。P0 阶段先按「PATH 查找 + 可配置路径」实现,打包留到发布前处理 - `toml_edit` 对含大量手写内容的 `config.toml` 格式保真:非拥有字段在写入前后做哈希断言,并补 fixture 回归测试 - 切换 provider 后历史会话按登录方式分组隐藏(官方文档已说明):必须由面板显式提示,否则新手误判为数据丢失 ### 拆解建议 建议拆为 5 张任务卡,依赖顺序: 1. **core 骨架**(M)— `codex-switch/crates/core`:detect + config_edit(toml_edit)+ backup 2. **CLI 切换**(M)— 依赖 1:status / preview / switch / undo 3. **CLI 密钥与体检**(M)— 依赖 2:set-key(凭据管理器)+ doctor 连通性验证 4. **炼境只读探测与转发**(M)— 依赖 2 的 CLI 契约:`codex_switch.rs` + `commands.ts` 5. **炼境工具页面板**(M)— 依赖 4:`ToolsPage` 新增 tab + 面板 UI + 新手护栏文案 --- ## 任务卡 > `codex-switch/` 指炼境仓库内的独立子目录,自带 Cargo workspace,不并入 `src-tauri` 的构建(对炼境现有编译零影响)。 ### 📋 core:配置探测与精细改写骨架 - status: todo - complexity: M - files: codex-switch/crates/core/Cargo.toml, codex-switch/crates/core/src/lib.rs, codex-switch/crates/core/src/detect.rs, codex-switch/crates/core/src/config_edit.rs, codex-switch/crates/core/src/backup.rs, codex-switch/crates/core/tests/fixtures/config.toml - acceptance: 对 fixture 执行档位切换后,非拥有字段(mcp_servers / plugins / projects / notify / shell_environment_policy)逐字节不变;config.toml 解析失败时中止且零文件修改;备份可原样还原 核心 crate 不得依赖 Tauri。`config_edit` 用 `toml_edit` 保留注释与字段顺序,写入走临时文件 + 同目录原子 rename。 ### 📋 CLI:status / preview / switch / undo - status: todo - complexity: M - files: codex-switch/crates/cli/Cargo.toml, codex-switch/crates/cli/src/main.rs, codex-switch/crates/cli/src/commands/status.rs, codex-switch/crates/cli/src/commands/preview.rs, codex-switch/crates/cli/src/commands/switch.rs, codex-switch/crates/cli/src/commands/undo.rs - depends: core:配置探测与精细改写骨架 - acceptance: `status --json` 报出当前档位与 provider;`switch --to official` 与 `switch --to deepseek-flash` 可往返切换;`undo` 回到上一状态;`preview --to X --json` 输出字段级 diff 且不含明文密钥 ### 📋 CLI:密钥托管与连通性体检 - status: todo - complexity: M - files: codex-switch/crates/core/src/secret.rs, codex-switch/crates/cli/src/commands/set_key.rs, codex-switch/crates/cli/src/commands/doctor.rs - depends: CLI:status / preview / switch / undo - acceptance: `set-key --stdin` 后密钥只物化到 `[model_providers.deepseek].experimental_bearer_token` 一处,且不再写入环境变量段;任何输出与日志中只出现 `sk-` + 前 6 + … + 后 4;`doctor --json` 能区分 Key 无效 / 限流 / 网络不通 / 模型名不存在 密钥存系统凭据管理器;`set-key` 接受 stdout 管道输入,不接受命令行明文参数。 ### 🔵 炼境:只读探测与 CLI 转发 - status: in_progress - complexity: M - files: src-tauri/src/commands/codex_switch.rs, src-tauri/src/commands/mod.rs, src-tauri/src/lib.rs, src/lib/commands.ts - depends: CLI:status / preview / switch / undo - acceptance: `get_codex_switch_status` 返回当前档位 / provider / 脱敏 Key hint / CLI 是否已安装 / cc-studio 托管冲突 / 旧条目残留;`run_codex_switch_action` 转发 CLI 并回传 stdout、stderr 与退出码;全流程炼境不写入任何 Codex 配置文件 CLI 调用统一走 `crate::silent_cmd`(Windows 隐藏窗口)。新增 command 必须同步 `src/lib/commands.ts`。 ### 📋 炼境:工具页面板与新手护栏 - status: todo - complexity: M - files: src/components/tools/CodexSwitchPanel.tsx, src/components/tools/ToolsPage.tsx - depends: 炼境:只读探测与 CLI 转发 - acceptance: 「工具」页新增 Codex 模型切换 tab;CLI 未安装时显示获取引导而非报错;切换成功后提示「历史会话按登录方式分组、未删除」与「桌面端需重启」;检测到 cc-studio 托管时给出告警 面板所有写入动作经确认弹窗后转发 CLI;每次进入面板重新探测,不做前端缓存。