diff --git a/CHANGELOG.md b/CHANGELOG.md index f93cc35d8..4ed4fc5b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -653,7 +653,7 @@ This beta release introduces the **Local API Proxy** feature, along with Skills ### Stats -- 51 commits since v3.7.1; 207 files changed; +17,297 / -6,870 lines. See [release-note-v3.8.0](docs/release-note-v3.8.0-en.md) for details. +- 51 commits since v3.7.1; 207 files changed; +17,297 / -6,870 lines. See [release-note-v3.8.0](docs/release-notes/v3.8.0-en.md) for details. --- diff --git a/README.md b/README.md index adefd2f85..4ea7f9022 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Modern AI-powered coding relies on CLI tools like Claude Code, Codex, Gemini CLI ## Features -[Full Changelog](CHANGELOG.md) | [Release Notes](docs/release-note-v3.11.1-en.md) +[Full Changelog](CHANGELOG.md) | [Release Notes](docs/release-notes/v3.11.1-en.md) ### Provider Management diff --git a/README_JA.md b/README_JA.md index 291b7ac1e..e6647844f 100644 --- a/README_JA.md +++ b/README_JA.md @@ -101,7 +101,7 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% / ## 特長 -[完全な更新履歴](CHANGELOG.md) | [リリースノート](docs/release-note-v3.11.1-ja.md) +[完全な更新履歴](CHANGELOG.md) | [リリースノート](docs/release-notes/v3.11.1-ja.md) ### プロバイダ管理 diff --git a/README_ZH.md b/README_ZH.md index c31dc82ac..f7c3647b2 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -102,7 +102,7 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更 ## 功能特性 -[完整更新日志](CHANGELOG.md) | [发布说明](docs/release-note-v3.11.1-zh.md) +[完整更新日志](CHANGELOG.md) | [发布说明](docs/release-notes/v3.11.1-zh.md) ### 供应商管理 diff --git a/docs/BACKEND_REFACTOR_PLAN.md b/docs/BACKEND_REFACTOR_PLAN.md deleted file mode 100644 index 9c8b75f10..000000000 --- a/docs/BACKEND_REFACTOR_PLAN.md +++ /dev/null @@ -1,169 +0,0 @@ -# CC Switch Rust 后端重构方案 - -## 目录 -- [背景与现状](#背景与现状) -- [问题确认](#问题确认) -- [方案评估](#方案评估) -- [渐进式重构路线](#渐进式重构路线) -- [测试策略](#测试策略) -- [风险与对策](#风险与对策) -- [总结](#总结) - -## 背景与现状 -- 前端已完成重构,后端 (Tauri + Rust) 仍维持历史结构。 -- 核心文件集中在 `src-tauri/src/commands.rs`、`lib.rs` 等超大文件中,业务逻辑与界面事件耦合严重。 -- 测试覆盖率低,只有零散单元测试,缺乏集成验证。 - -## 问题确认 - -| 提案问题 | 实际情况 | 严重程度 | -| --- | --- | --- | -| `commands.rs` 过长 | ✅ 1526 行,包含 32 个命令,职责混杂 | 🔴 高 | -| `lib.rs` 缺少服务层 | ✅ 541 行,托盘/事件/业务逻辑耦合 | 🟡 中 | -| `Result` 泛滥 | ✅ 118 处,错误上下文丢失 | 🟡 中 | -| 全局 `Mutex` 阻塞 | ✅ 31 处 `.lock()` 调用,读写不分离 | 🟡 中 | -| 配置逻辑分散 | ✅ 分布在 5 个文件 (`config`/`app_config`/`app_store`/`settings`/`codex_config`) | 🟢 低 | - -代码规模分布(约 5.4k SLOC): -- `commands.rs`: 1526 行(28%)→ 第一优先级 🎯 -- `lib.rs`: 541 行(10%)→ 托盘逻辑与业务耦合 -- `mcp.rs`: 732 行(14%)→ 相对清晰 -- `migration.rs`: 431 行(8%)→ 一次性逻辑 -- 其他文件合计:2156 行(40%) - -## 方案评估 - -### ✅ 优点 -1. **分层架构清晰** - - `commands/`:Tauri 命令薄层 - - `services/`:业务流程,如供应商切换、MCP 同步 - - `infrastructure/`:配置读写、外设交互 - - `domain/`:数据模型 (`Provider`, `AppType` 等) - → 提升可测试性、降低耦合度、方便团队协作。 - -2. **统一错误处理** - - 引入 `AppError`(`thiserror`),保留错误链和上下文。 - - Tauri 命令仍返回 `Result`,通过 `From` 自动转换。 - - 改善日志可读性,利于排查。 - -3. **并发优化** - - `AppState` 切换为 `RwLock`。 - - 读多写少的场景提升吞吐(如频繁查询供应商列表)。 - -### ⚠️ 风险 -1. **过度设计** - - 完整 DDD 四层在 5k 行项目中会增加 30-50% 维护成本。 - - Rust trait + repository 样板较多,收益不足。 - - 推荐“轻量分层”而非正统 DDD。 - -2. **迁移成本高** - - `commands.rs` 拆分、错误统一、锁改造触及多文件。 - - 测试缺失导致重构风险高,需先补测试。 - - 估算完整改造需 5-6 周;建议分阶段输出可落地价值。 - -3. **技术选型需谨慎** - - `parking_lot` 相比标准库 `RwLock` 提升有限,不必引入。 - - `spawn_blocking` 仅用于 >100ms 的阻塞任务,避免滥用。 - - 以现有依赖为主,控制复杂度。 - -## 实施进度 -- **阶段 1:统一错误处理 ✅** - - 引入 `thiserror` 并在 `src-tauri/src/error.rs` 定义 `AppError`,提供常用构造函数和 `From for String`,保留错误链路。 - - 配置、存储、同步等核心模块(`config.rs`、`app_config.rs`、`app_store.rs`、`store.rs`、`codex_config.rs`、`claude_mcp.rs`、`claude_plugin.rs`、`import_export.rs`、`mcp.rs`、`migration.rs`、`speedtest.rs`、`usage_script.rs`、`settings.rs`、`lib.rs` 等)已统一返回 `Result<_, AppError>`,避免字符串错误丢失上下文。 - - Tauri 命令层继续返回 `Result<_, String>`,通过 `?` + `Into` 统一转换,前端无需调整。 - - `cargo check` 通过,`rg "Result<[^>]+, String"` 巡检确认除命令层外已无字符串错误返回。 -- **阶段 2:拆分命令层 ✅** - - 已将单一 `src-tauri/src/commands.rs` 拆分为 `commands/{provider,mcp,config,settings,misc,plugin}.rs` 并通过 `commands/mod.rs` 统一导出,保持对外 API 不变。 - - 每个文件聚焦单一功能域(供应商、MCP、配置、设置、杂项、插件),命令函数平均 150-250 行,可读性与后续维护性显著提升。 - - 相关依赖调整后 `cargo check` 通过,静态巡检确认无重复定义或未注册命令。 -- **阶段 3:补充测试 ✅** - - `tests/import_export_sync.rs` 集成测试涵盖配置备份、Claude/Codex live 同步、MCP 投影与 Codex/Claude 双向导入流程,并新增启用项清理、非法 TOML 抛错等失败场景验证;统一使用隔离 HOME 目录避免污染真实用户环境。 - - 扩展 `lib.rs` re-export,暴露 `AppType`、`MultiAppConfig`、`AppError`、配置 IO 以及 Codex/Claude MCP 路径与同步函数,方便服务层及测试直接复用核心逻辑。 - - 新增负向测试验证 Codex 供应商缺少 `auth` 字段时的错误返回,并补充备份数量上限测试;顺带修复 `create_backup` 采用内存读写避免拷贝继承旧的修改时间,确保最新备份不会在清理阶段被误删。 - - 针对 `codex_config::write_codex_live_atomic` 补充成功与失败场景测试,覆盖 auth/config 原子写入与失败回滚逻辑(模拟目标路径为目录时的 rename 失败),降低 Codex live 写入回归风险。 - - 新增 `tests/provider_commands.rs` 覆盖 `switch_provider` 的 Codex 正常流程与供应商缺失分支,并抽取 `switch_provider_internal` 以复用 `AppError`,通过 `switch_provider_test_hook` 暴露测试入口;同时共享 `tests/support.rs` 提供隔离 HOME / 互斥工具函数。 - - 补充 Claude 切换集成测试,验证 live `settings.json` 覆写、新旧供应商快照回填以及 `.cc-switch/config.json` 持久化结果,确保阶段四提取服务层时拥有可回归的用例。 - - 增加 Codex 缺失 `auth` 场景测试,确认 `switch_provider_internal` 在关键字段缺失时返回带上下文的 `AppError`,同时保持内存状态未被污染。 - - 为配置导入命令抽取复用逻辑 `import_config_from_path` 并补充成功/失败集成测试,校验备份生成、状态同步、JSON 解析与文件缺失等错误回退路径;`export_config_to_file` 亦具备成功/缺失源文件的命令级回归。 - - 新增 `tests/mcp_commands.rs`,通过测试钩子覆盖 `import_default_config`、`import_mcp_from_claude`、`set_mcp_enabled` 等命令层行为,验证缺失文件/非法 JSON 的错误回滚以及成功路径落盘效果;阶段三目标达成,命令层关键边界已具备回归保障。 -- **阶段 4:服务层抽象 🚧(进行中)** - - 新增 `services/provider.rs` 并实现 `ProviderService::switch` / `delete`,集中处理供应商切换、回填、MCP 同步等核心业务;命令层改为薄封装并在 `tests/provider_service.rs`、`tests/provider_commands.rs` 中完成成功与失败路径的集成验证。 - - 新增 `services/mcp.rs` 提供 `McpService`,封装 MCP 服务器的查询、增删改、启用同步与导入流程;命令层改为参数解析 + 调用服务,`tests/mcp_commands.rs` 直接使用 `McpService` 验证成功与失败路径,阶段三测试继续适配。 - - `McpService` 在内部先复制内存快照、释放写锁,再执行文件同步,避免阶段五升级后的 `RwLock` 在 I/O 场景被长时间占用;`upsert/delete/set_enabled/sync_enabled` 均已修正。 - - 新增 `services/config.rs` 提供 `ConfigService`,统一处理配置导入导出、备份与 live 同步;命令层迁移至 `commands/import_export.rs`,在落盘操作前释放锁并复用现有集成测试。 - - 新增 `services/speedtest.rs` 并实现 `SpeedtestService::test_endpoints`,将 URL 校验、超时裁剪与网络请求封装在服务层,命令改为薄封装;补充单元测试覆盖空列表与非法 URL 分支。 - - 后续可选:应用设置(Store)命令仍较薄,可按需评估是否抽象;当前阶段四核心服务已基本齐备。 -- **阶段 5:锁与阻塞优化 ✅(首轮)** - - `AppState` 已由 `Mutex` 切换为 `RwLock`,托盘、命令与测试均按读写语义区分 `read()` / `write()`;`cargo test` 全量通过验证并未破坏现有流程。 - - 针对高开销 IO 的配置导入/导出命令提取 `load_config_for_import`,并通过 `tauri::async_runtime::spawn_blocking` 将文件读写与备份迁至阻塞线程,保持命令处理线程轻量。 - - 其余命令梳理后确认仍属轻量同步操作,暂不额外引入 `spawn_blocking`;若后续出现新的长耗时流程,再按同一模式扩展。 - -## 渐进式重构路线 - -### 阶段 1:统一错误处理(高收益 / 低风险) -- 新增 `src-tauri/src/error.rs`,定义 `AppError`。 -- 底层文件 IO、配置解析等函数返回 `Result`。 -- 命令层通过 `?` 自动传播,最终 `.map_err(Into::into)`。 -- 预估 3-5 天,立即启动。 - -### 阶段 2:拆分 `commands.rs`(高收益 / 中风险) -- 按业务拆分为 `commands/provider.rs`、`commands/mcp.rs`、`commands/config.rs`、`commands/settings.rs`、`commands/misc.rs`。 -- `commands/mod.rs` 统一导出和注册。 -- 文件行数降低到 200-300 行/文件,职责单一。 -- 预估 5-7 天,可并行进行部分重构。 - -### 阶段 3:补充测试(中收益 / 中风险) -- 引入 `tests/` 或 `src-tauri/tests/` 集成测试,覆盖供应商切换、MCP 同步、配置迁移。 -- 使用 `tempfile`/`tempdir` 隔离文件系统,组合少量回归脚本。 -- 预估 5-7 天,为后续重构提供安全网。 - -### 阶段 4:提取轻量服务层(中收益 / 中风险) -- 新增 `services/provider_service.rs`、`services/mcp_service.rs`。 -- 不强制使用 trait;直接以自由函数/结构体实现业务流程。 - ```rust - pub struct ProviderService; - impl ProviderService { - pub fn switch(config: &mut MultiAppConfig, app: AppType, id: &str) -> Result<(), AppError> { - // 业务流程:验证、回填、落盘、更新 current、触发事件 - } - } - ``` -- 命令层负责参数解析,服务层处理业务逻辑,托盘逻辑重用同一接口。 -- 预估 7-10 天,可在测试补齐后执行。 - -### 阶段 5:锁与阻塞优化(低收益 / 低风险) -- ✅ `AppState` 已从 `Mutex` 切换为 `RwLock`,命令与托盘读写按需区分,现有测试全部通过。 -- ✅ 配置导入/导出命令通过 `spawn_blocking` 处理高开销文件 IO;其他命令维持同步执行以避免不必要调度。 -- 🔄 持续监控:若后续引入新的批量迁移或耗时任务,再按相同模式扩展到阻塞线程;观察运行时锁竞争情况,必要时考虑进一步拆分状态或引入缓存。 - -## 测试策略 -- **优先覆盖场景** - - 供应商切换:状态更新 + live 配置同步 - - MCP 同步:enabled 服务器快照与落盘 - - 配置迁移:归档、备份与版本升级 -- **推荐结构** - ```rust - #[cfg(test)] - mod integration { - use super::*; - #[test] - fn switch_provider_updates_live_config() { /* ... */ } - #[test] - fn sync_mcp_to_codex_updates_claude_config() { /* ... */ } - #[test] - fn migration_preserves_backup() { /* ... */ } - } - ``` -- 目标覆盖率:关键路径 >80%,文件 IO/迁移 >70%。 - -## 风险与对策 -- **测试不足** → 阶段 3 强制补齐,建立基础集成测试。 -- **重构跨度大** → 按阶段在独立分支推进(如 `refactor/backend-step1` 等)。 -- **回滚困难** → 每阶段结束打 tag(如 `v3.6.0-backend-step1`),保留回滚点。 -- **功能回归** → 重构后执行手动冒烟流程:供应商切换、托盘操作、MCP 同步、配置导入导出。 - -## 总结 -- 当前规模下不建议整体引入完整 DDD/四层架构,避免过度设计。 -- 建议遵循“错误统一 → 命令拆分 → 补测试 → 服务层抽象 → 锁优化”的渐进式策略。 -- 完成阶段 1-3 后即可显著提升可维护性与可靠性;阶段 4-5 可根据资源灵活安排。 -- 重构过程中同步维护文档与测试,确保团队成员对架构演进保持一致认知。 diff --git a/docs/CODEX_MCP_RAW_TOML_PLAN.md b/docs/CODEX_MCP_RAW_TOML_PLAN.md deleted file mode 100644 index 0e99efa3e..000000000 --- a/docs/CODEX_MCP_RAW_TOML_PLAN.md +++ /dev/null @@ -1,1309 +0,0 @@ -# Codex MCP Raw TOML 重构方案 - -## 📋 目录 - -- [背景与目标](#背景与目标) -- [核心设计](#核心设计) -- [技术架构](#技术架构) -- [实施计划](#实施计划) -- [风险控制](#风险控制) -- [测试验证](#测试验证) - ---- - -## 背景与目标 - -### 当前问题 - -1. **数据丢失**:Codex MCP 配置在 TOML ↔ JSON 转换时丢失注释、格式、特殊值类型 -2. **配置复杂**:Codex TOML 支持复杂嵌套结构,强制结构化存储限制灵活性 -3. **用户体验差**:无法保留用户手写的注释和格式偏好 - -### 设计目标 - -1. **保真存储**:Codex MCP 使用 raw TOML 字符串存储,完全避免序列化损失 -2. **架构分离**:Claude/Gemini 继续用结构化 JSON,Codex 用原始文本 -3. **UI 解耦**:MCP 管理面板与当前 app 切换彻底分离 -4. **增量实施**:零改动现有 Claude/Gemini 逻辑,风险可控 - ---- - -## 核心设计 - -### 数据结构设计 - -#### config.json 顶层结构 - -```json -{ - "providers": [ - // 现有 provider 列表,不改 - ], - "mcp": { - // 统一 MCP 结构,仅用于 Claude & Gemini - // ✅ 完全移除 Codex 相关逻辑,apps 字段仅包含 claude/gemini - "servers": { - "fetch": { - "id": "fetch", - "name": "Fetch MCP", - "server": { - "type": "stdio", - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-fetch"] - }, - "apps": { - "claude": true, - "gemini": false - }, - "description": null, - "homepage": null, - "docs": null, - "tags": [] - } - } - }, - "codexMcp": { - "rawToml": "[mcp]\n# Codex 专用 MCP TOML 片段\n..." - } -} -``` - -#### Rust 数据结构 - -```rust -// src-tauri/src/app_config.rs -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CodexMcpConfig { - /// 完整的 MCP TOML 片段(包含 [mcp] 等) - pub raw_toml: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MultiAppConfig { - /// 版本号(v2 起) - #[serde(default = "default_version")] - pub version: u32, - - /// 应用管理器(claude/codex/gemini) - #[serde(flatten)] - pub apps: HashMap, - - /// MCP 配置(统一结构 + 旧结构,用于迁移) - #[serde(default)] - pub mcp: McpRoot, - - /// Prompt 配置(按客户端分治) - #[serde(default)] - pub prompts: PromptRoot, - - /// 通用配置片段(按应用分治) - #[serde(default)] - pub common_config_snippets: CommonConfigSnippets, - - /// Claude 通用配置片段(旧字段,用于向后兼容迁移) - #[serde(default, skip_serializing_if = "Option::is_none")] - pub claude_common_config_snippet: Option, - - /// Codex MCP raw TOML(新字段,仅 Codex 使用) - #[serde(default, skip_serializing_if = "Option::is_none")] - pub codex_mcp: Option, -} -``` - -### 分层架构 - -``` -┌─────────────────────────────────────────────────┐ -│ UI 层 │ -│ ┌─────────────────────────────────────────┐ │ -│ │ MCP 面板(与 app 切换完全解耦) │ │ -│ │ ├─ Tab1: Claude & Gemini (结构化 JSON) │ │ -│ │ │ - 仅管理 mcp.servers │ │ -│ │ │ - apps 字段仅含 claude/gemini │ │ -│ │ └─ Tab2: Codex (raw TOML 编辑器) │ │ -│ │ - 独立管理 codexMcp.rawToml │ │ -│ └─────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 应用层 │ -│ switch_app() 根据 app 类型选择数据源: │ -│ - Claude/Gemini → mcp.servers (过滤 apps) │ -│ - Codex → codexMcp.rawToml (完全独立) │ -│ │ -│ ✅ 无优先级冲突:两者完全隔离 │ -└─────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────┐ -│ 数据层 │ -│ config.json: │ -│ - mcp.servers: 仅 Claude & Gemini │ -│ - codexMcp.rawToml: 仅 Codex │ -│ │ -│ ✅ 单一职责:互不干扰 │ -└─────────────────────────────────────────────────┘ -``` - -### MCP 配置职责划分 - -| 配置源 | 职责 | 数据格式 | 管理方式 | -|--------|------|----------|----------| -| `mcp.servers` | Claude & Gemini MCP | 结构化 JSON | UI 表单(Tab1) | -| `codexMcp.rawToml` | Codex MCP | 原始 TOML 字符串 | 代码编辑器(Tab2) | - -**关键原则**: -- ✅ `mcp.servers` 中的 `apps` 字段**永不包含 `codex`** -- ✅ Codex MCP **仅存储**在 `codexMcp.rawToml` -- ✅ 切换逻辑**完全独立**,无优先级判断 - ---- - -## 技术架构 - -### 后端架构(Rust) - -#### 1. 配置管理 - -**文件**:`src-tauri/src/app_config.rs` - -```rust -impl MultiAppConfig { - pub fn load() -> Result { - // 1. 按 v2 结构加载 MultiAppConfig - let mut config = /* ... 现有 load 实现 ... */; - - let mut updated = false; - - // 2. 执行 Codex MCP → raw TOML 迁移 - // - 仅迁移 v3.6.2 的 mcp.codex.servers → codexMcp.rawToml - // - 迁移后清空 mcp.codex.servers,避免被后续 unified 迁移处理 - if migration::migrate_codex_mcp_to_raw_toml(&mut config)? { - updated = true; - } - - // 3. 执行 unified MCP 迁移(mcp.claude/gemini → mcp.servers) - // - ✅ 此时 mcp.codex 已清空,不会被迁移到 unified - // - unified 结构中 apps 字段仅包含 claude/gemini - if config.migrate_mcp_to_unified()? { - updated = true; - } - - // 4. 其他迁移(Prompt、通用片段等) - // ... - - if updated { - config.save()?; - } - - Ok(config) - } - - pub fn save(&self) -> Result<(), AppError> { - // 序列化时包含 codexMcp 字段 - // ... - } -} -``` - -#### 2. 数据迁移 - -**文件**:`src-tauri/src/migration.rs` - -```rust -/// 将 v3.6.2 的 mcp.codex.servers 迁移为 codexMcp.rawToml -/// -/// **关键行为**: -/// 1. 仅在 codex_mcp 为空且存在旧的 mcp.codex.servers 时执行 -/// 2. 转换后**立即清空 mcp.codex.servers**,避免被 unified 迁移重复处理 -/// 3. 返回 true 表示发生了迁移,需要保存配置 -pub fn migrate_codex_mcp_to_raw_toml( - config: &mut MultiAppConfig, -) -> Result { - // 已迁移过,跳过 - if config.codex_mcp.is_some() { - return Ok(false); - } - - let legacy_servers = &config.mcp.codex.servers; - if legacy_servers.is_empty() { - // 没有旧的 Codex MCP 配置,跳过 - return Ok(false); - } - - // 转换为 TOML - let toml = convert_legacy_codex_mcp_to_toml(legacy_servers)?; - config.codex_mcp = Some(CodexMcpConfig { raw_toml: toml }); - - // ✅ 关键:清空旧数据,确保 unified 迁移不会处理 Codex - config.mcp.codex.servers.clear(); - - log::info!( - "Migrated {} Codex MCP servers to raw TOML and cleared legacy storage", - legacy_servers.len() - ); - - Ok(true) -} - -/// 将 v3.6.2 时代的 mcp.codex.servers (HashMap) -/// 转换为 Codex 所需的 MCP TOML 片段 -fn convert_legacy_codex_mcp_to_toml( - servers: &HashMap, -) -> Result { - let mut toml = String::from("[mcp]\n\n"); - - for (id, entry) in servers { - // 旧结构:entry 是宽松 JSON 对象,包含 name/server/enabled 等字段 - let obj = entry - .as_object() - .ok_or_else(|| AppError::Config(format!( - "无效的 Codex MCP 条目 '{}': 必须为 JSON 对象", - id - )))?; - - let name = obj - .get("name") - .and_then(|v| v.as_str()) - .unwrap_or(id); - - let server = obj.get("server").ok_or_else(|| { - AppError::Config(format!( - "无效的 Codex MCP 条目 '{}': 缺少 server 字段", - id - )) - })?; - - let server_obj = server.as_object().ok_or_else(|| { - AppError::Config(format!( - "无效的 Codex MCP 条目 '{}': server 必须是 JSON 对象", - id - )) - })?; - - toml.push_str("[[mcp.servers]]\n"); - toml.push_str(&format!("name = \"{}\"\n", name)); - - // stdio 类型字段 - if let Some(cmd) = server_obj.get("command").and_then(|v| v.as_str()) { - toml.push_str(&format!("command = \"{}\"\n", cmd)); - } - - if let Some(args) = server_obj.get("args").and_then(|v| v.as_array()) { - let args_str = args - .iter() - .filter_map(|a| a.as_str()) - .map(|a| format!("\"{}\"", a)) - .collect::>() - .join(", "); - if !args_str.is_empty() { - toml.push_str(&format!("args = [{}]\n", args_str)); - } - } - - if let Some(env) = server_obj.get("env").and_then(|v| v.as_object()) { - if !env.is_empty() { - toml.push_str("\n[mcp.servers.env]\n"); - for (k, v) in env { - if let Some(val) = v.as_str() { - toml.push_str(&format!("{} = \"{}\"\n", k, val)); - } - } - } - } - - if let Some(cwd) = server_obj.get("cwd").and_then(|v| v.as_str()) { - toml.push_str(&format!("cwd = \"{}\"\n", cwd)); - } - - // http 类型字段 - if let Some(url) = server_obj.get("url").and_then(|v| v.as_str()) { - toml.push_str(&format!("url = \"{}\"\n", url)); - } - - if let Some(t) = server_obj.get("type").and_then(|v| v.as_str()) { - toml.push_str(&format!("type = \"{}\"\n", t)); - } - - if let Some(headers) = server_obj.get("headers").and_then(|v| v.as_object()) { - if !headers.is_empty() { - toml.push_str("\n[mcp.servers.headers]\n"); - for (k, v) in headers { - if let Some(val) = v.as_str() { - toml.push_str(&format!("{} = \"{}\"\n", k, val)); - } - } - } - } - - toml.push_str("\n"); - } - - Ok(toml) -} -``` - -#### 3. Tauri 命令 - -**文件**:`src-tauri/src/commands/mcp.rs` - -```rust -/// 获取 Codex MCP 配置 -#[tauri::command] -pub async fn get_codex_mcp_config( - state: State<'_, AppState> -) -> Result { - let config = state.config.read().unwrap(); - - if let Some(codex_mcp) = &config.codex_mcp { - Ok(codex_mcp.raw_toml.clone()) - } else { - // 返回默认模板 - Ok(String::from( - "[mcp]\n# 在这里填写 Codex MCP 配置\n# 示例:\n# [[mcp.servers]]\n# name = \"example\"\n# command = \"npx\"\n# args = [\"-y\", \"@modelcontextprotocol/server-example\"]\n" - )) - } -} - -/// 更新 Codex MCP 配置 -#[tauri::command] -pub async fn update_codex_mcp_config( - state: State<'_, AppState>, - raw_toml: String, -) -> Result<(), String> { - // 1. 语法验证 - toml::from_str::(&raw_toml) - .map_err(|e| format!("TOML syntax error: {}", e))?; - - // 2. 可选警告 - if !raw_toml.contains("[mcp") { - log::warn!("Codex MCP TOML doesn't contain [mcp] section"); - } - - // 3. 保存 - let mut config = state.config.write().unwrap(); - config.codex_mcp = Some(CodexMcpConfig { - raw_toml: raw_toml.clone(), - }); - config.save() - .map_err(|e| format!("Failed to save config: {}", e))?; - - Ok(()) -} - -/// 验证 TOML 语法(前端可在保存前调用) -#[tauri::command] -pub async fn validate_codex_mcp_toml( - raw_toml: String -) -> Result { - match toml::from_str::(&raw_toml) { - Ok(_) => Ok(ValidateResult { - valid: true, - error: None, - warnings: vec![], - }), - Err(e) => Ok(ValidateResult { - valid: false, - error: Some(e.to_string()), - warnings: vec![], - }), - } -} - -#[derive(Debug, Serialize)] -pub struct ValidateResult { - pub valid: bool, - pub error: Option, - pub warnings: Vec, -} - -/// 从 Codex live 配置导入 MCP 段 -#[tauri::command] -pub async fn import_codex_mcp_from_live() -> Result { - let config_path = get_codex_config_path() - .map_err(|e| e.to_string())?; - - if !config_path.exists() { - return Ok(String::from("[mcp]\n# No existing Codex config found\n")); - } - - let content = fs::read_to_string(&config_path) - .map_err(|e| format!("Failed to read Codex config: {}", e))?; - - let mcp_section = extract_mcp_section_from_toml(&content)?; - Ok(mcp_section) -} - -fn extract_mcp_section_from_toml(content: &str) -> Result { - use toml_edit::DocumentMut; - - let doc = content.parse::() - .map_err(|e| format!("Invalid TOML: {}", e))?; - - if let Some(mcp_item) = doc.get("mcp") { - let mut result = String::from("[mcp]\n"); - result.push_str(&mcp_item.to_string()); - Ok(result) - } else { - Ok(String::from("[mcp]\n# No MCP config found in live file\n")) - } -} -``` - -#### 3. 切换逻辑 - -**文件**:`src-tauri/src/services/provider.rs` - -```rust -impl ProviderService { - /// 切换到 Codex provider - pub fn switch_to_codex( - &self, - provider: &Provider - ) -> Result<(), AppError> { - // 1. 读取 Codex MCP 配置(完全独立于 unified) - let codex_mcp = { - let config = self.state.config.read().unwrap(); - config.codex_mcp.clone() - }; - - // 2. 生成最终配置(base + MCP) - let final_toml = self.apply_codex_config(provider, &codex_mcp)?; - - // 3. 写入 live 文件 - self.write_codex_config(&final_toml)?; - - Ok(()) - } - - fn apply_codex_config( - &self, - provider: &Provider, - codex_mcp: &Option, - ) -> Result { - // 1. 生成基础配置(不含 MCP) - let mut base_config = self.generate_codex_base_config(provider)?; - - // 2. 追加 MCP 配置(如果有) - if let Some(mcp_cfg) = codex_mcp { - let trimmed = mcp_cfg.raw_toml.trim(); - if !trimmed.is_empty() { - // 确保有换行分隔 - if !base_config.ends_with('\n') { - base_config.push('\n'); - } - base_config.push('\n'); - base_config.push_str(trimmed); - } - } - - // 3. 验证最终 TOML 可解析 - toml::from_str::(&base_config) - .map_err(|e| AppError::Config(format!( - "Generated Codex config is invalid: {}", - e - )))?; - - Ok(base_config) - } - - /// 切换到 Claude/Gemini - pub fn switch_to_claude_or_gemini( - &self, - provider: &Provider, - app_type: AppType, - ) -> Result<(), AppError> { - // 从 unified MCP 读取配置(apps 字段仅含 claude/gemini) - let mcp_servers = { - let config = self.state.config.read().unwrap(); - config.mcp.servers - .values() - .filter(|s| s.apps.get(&app_type.to_string()).unwrap_or(&false)) - .cloned() - .collect::>() - }; - - // 生成并写入配置 - // ... - - Ok(()) - } -} -``` - -**关键点**: -- ✅ Codex 切换**完全不读取** `mcp.servers` -- ✅ Claude/Gemini 切换**完全不读取** `codexMcp` -- ✅ 无优先级判断,逻辑简单清晰 - -### 前端架构(React + TypeScript) - -#### 1. API 层 - -**文件**:`src/lib/api/mcp.ts` - -```typescript -export const codexMcpApi = { - /** - * 获取 Codex MCP 配置(raw TOML) - */ - get: () => invoke('get_codex_mcp_config'), - - /** - * 更新 Codex MCP 配置 - */ - update: (rawToml: string) => - invoke('update_codex_mcp_config', { rawToml }), - - /** - * 验证 TOML 语法 - */ - validate: (rawToml: string) => - invoke('validate_codex_mcp_toml', { rawToml }), - - /** - * 从 Codex live 配置导入 - */ - importFromLive: () => - invoke('import_codex_mcp_from_live'), -}; - -export interface ValidateResult { - valid: boolean; - error?: string; - warnings: string[]; -} -``` - -#### 2. Hooks - -**文件**:`src/hooks/useCodexMcp.ts` - -```typescript -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { codexMcpApi } from '@/lib/api/mcp'; -import { toast } from 'sonner'; - -export function useCodexMcp() { - const queryClient = useQueryClient(); - - // 查询 - const query = useQuery({ - queryKey: ['codexMcp'], - queryFn: codexMcpApi.get, - }); - - // 更新 - const updateMutation = useMutation({ - mutationFn: codexMcpApi.update, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['codexMcp'] }); - toast.success('Codex MCP 配置已保存'); - }, - onError: (error: Error) => { - toast.error(`保存失败: ${error.message}`); - }, - }); - - // 验证 - const validateMutation = useMutation({ - mutationFn: codexMcpApi.validate, - }); - - // 导入 - const importMutation = useMutation({ - mutationFn: codexMcpApi.importFromLive, - onSuccess: (data) => { - queryClient.setQueryData(['codexMcp'], data); - toast.success('已从 Codex 配置导入 MCP'); - }, - onError: (error: Error) => { - toast.error(`导入失败: ${error.message}`); - }, - }); - - return { - rawToml: query.data ?? '', - isLoading: query.isLoading, - update: updateMutation.mutate, - // 保存前需要拿到校验结果,因此对外暴露 mutateAsync,便于 await - validate: validateMutation.mutateAsync, - importFromLive: importMutation.mutate, - }; -} -``` - -#### 3. UI 组件 - -**文件**:`src/components/mcp/McpPanel.tsx` - -```typescript -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { ClaudeGeminiMcpTab } from './ClaudeGeminiMcpTab'; -import { CodexMcpTab } from './CodexMcpTab'; - -export function McpPanel() { - return ( - - - - Claude & Gemini - - - Codex - - - - - - - - - - - - ); -} -``` - -**文件**:`src/components/mcp/ClaudeGeminiMcpTab.tsx` - -```typescript -import { Alert, AlertDescription } from '@/components/ui/alert'; -import { useMcp } from '@/hooks/useMcp'; - -/** - * Claude & Gemini 的 MCP 管理 Tab - * - * ✅ 仅操作 mcp.servers - * ✅ apps 字段仅含 claude/gemini(不含 codex) - * ✅ 完全独立于当前选中的 app - */ -export function ClaudeGeminiMcpTab() { - const { servers, addServer, updateServer, deleteServer } = useMcp(); - - return ( -
- - - 管理 Claude 和 Gemini 的 MCP 服务器。 -
- 注意:Codex MCP 在专用 Tab 管理(raw TOML 格式)。 -
-
- - {/* 现有 MCP 列表组件,但需确保: */} - {/* 1. 表单中 apps 选项仅显示 claude/gemini */} - {/* 2. 过滤掉可能的历史遗留 codex 数据 */} - !s.apps.codex)} - onAdd={addServer} - onUpdate={updateServer} - onDelete={deleteServer} - availableApps={['claude', 'gemini']} // ✅ 限制可选应用 - /> -
- ); -} -``` - -**文件**:`src/components/mcp/CodexMcpTab.tsx` - -```typescript -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { CodexMcpEditor } from './CodexMcpEditor'; -import { useCodexMcp } from '@/hooks/useCodexMcp'; -import { Alert, AlertDescription } from '@/components/ui/alert'; - -export function CodexMcpTab() { - const { rawToml, isLoading, update, validate, importFromLive } = useCodexMcp(); - const [localValue, setLocalValue] = useState(rawToml); - const [validationError, setValidationError] = useState(null); - - // 当后端数据加载完成或导入时,同步到本地编辑器 - useEffect(() => { - setLocalValue(rawToml); - }, [rawToml]); - - const handleSave = async () => { - // 保存前验证 - const result = await validate(localValue); - - if (!result.valid) { - setValidationError(result.error ?? 'Unknown error'); - return; - } - - setValidationError(null); - update(localValue); - }; - - const handleImport = () => { - importFromLive(); - }; - - if (isLoading) { - return
加载中...
; - } - - return ( -
- - - 直接编辑 Codex MCP TOML 配置。修改会在下次切换到 Codex 时生效。 - - - - {validationError && ( - - - TOML 语法错误: {validationError} - - - )} - - - -
- - - -
-
- ); -} -``` - -**文件**:`src/components/mcp/CodexMcpEditor.tsx` - -```typescript -import { useEffect, useRef } from 'react'; -import { EditorView, basicSetup } from 'codemirror'; -import { toml } from '@codemirror/lang-toml'; -import { oneDark } from '@codemirror/theme-one-dark'; -import { linter, Diagnostic } from '@codemirror/lint'; -import * as TOML from 'smol-toml'; - -const tomlLinter = linter((view) => { - const diagnostics: Diagnostic[] = []; - const content = view.state.doc.toString(); - - try { - TOML.parse(content); - } catch (e: any) { - diagnostics.push({ - from: 0, - to: content.length, - severity: 'error', - message: `TOML Syntax Error: ${e.message}`, - }); - } - - return diagnostics; -}); - -interface Props { - value: string; - onChange: (value: string) => void; -} - -export function CodexMcpEditor({ value, onChange }: Props) { - const editorRef = useRef(null); - const viewRef = useRef(); - - useEffect(() => { - if (!editorRef.current) return; - - const view = new EditorView({ - doc: value, - extensions: [ - basicSetup, - toml(), - oneDark, - tomlLinter, - EditorView.updateListener.of((update) => { - if (update.docChanged) { - onChange(update.state.doc.toString()); - } - }), - ], - parent: editorRef.current, - }); - - viewRef.current = view; - - return () => view.destroy(); - }, []); - - // 外部值变化时更新编辑器 - useEffect(() => { - if (!viewRef.current) return; - const currentValue = viewRef.current.state.doc.toString(); - if (currentValue !== value) { - viewRef.current.dispatch({ - changes: { - from: 0, - to: currentValue.length, - insert: value, - }, - }); - } - }, [value]); - - return ( -
- ); -} -``` - ---- - -## 实施计划 - -### Phase 0: 准备工作 - -**时间**:0.5 天 - -- [ ] 创建开发分支 `feature/codex-mcp-raw-toml` -- [ ] 安装前端依赖:`pnpm add @codemirror/lang-toml` -- [ ] 备份现有配置文件用于测试 - -### Phase 1: 后端基础(P0) - -**时间**:1.5 天 - -**任务**: - -- [ ] 在 `app_config.rs` 中定义 `CodexMcpConfig` -- [ ] 修改 `MultiAppConfig` 添加 `codex_mcp` 字段 -- [ ] 更新 `MultiAppConfig::load()` 和 `save()` 支持新字段 -- [ ] 编写迁移函数 `migrate_codex_mcp_to_raw_toml` - - [ ] 实现 `convert_servers_map_to_toml` - - [ ] 处理 stdio 类型服务器 - - [ ] 处理 http 类型服务器 -- [ ] 在 `lib.rs` 启动时执行迁移 -- [ ] 单元测试:迁移逻辑正确性 - -**验收标准**: - -- 现有配置可正确迁移为 raw TOML -- config.json 包含 `codexMcp` 字段 -- 迁移不影响 Claude/Gemini 配置 - -### Phase 2: 命令层(P0) - -**时间**:1 天 - -**任务**: - -- [ ] 在 `commands/mcp.rs` 实现命令: - - [ ] `get_codex_mcp_config` - - [ ] `update_codex_mcp_config` - - [ ] `validate_codex_mcp_toml` - - [ ] `import_codex_mcp_from_live` -- [ ] 实现 `extract_mcp_section_from_toml` 辅助函数 -- [ ] 在 `lib.rs` 注册新命令 -- [ ] 集成测试:命令调用正确性 - -**验收标准**: - -- 所有命令可通过 Tauri invoke 正常调用 -- TOML 语法验证准确 -- 从 live 配置导入功能正常 - -### Phase 3: 切换逻辑(P0) - -**时间**:1 天 - -**任务**: - -- [ ] 修改 `services/provider.rs` 的 Codex 切换逻辑 - - [ ] 实现 `switch_to_codex`(仅读取 `codexMcp`) - - [ ] 实现 `apply_codex_config`(拼接 base + raw TOML) - - [ ] 添加最终 TOML 验证 -- [ ] 确保 Claude/Gemini 切换逻辑不读取 `codexMcp` -- [ ] 原子写入机制验证 -- [ ] 集成测试:Codex 切换后配置正确 - -**验收标准**: - -- Codex 切换时,config.toml 包含 raw TOML 的 MCP 段 -- Claude/Gemini 切换时,仅使用 `mcp.servers` 中 `apps.claude/gemini=true` 的项 -- 生成的配置可被对应应用正确解析 -- 切换失败时不损坏现有配置 - -### Phase 4: 前端 API(P0) - -**时间**:0.5 天 - -**任务**: - -- [ ] 在 `lib/api/mcp.ts` 创建 `codexMcpApi` -- [ ] 定义 TypeScript 类型 `ValidateResult` -- [ ] 在 `hooks/useCodexMcp.ts` 创建 Hook - - [ ] useQuery 读取配置 - - [ ] useMutation 更新配置 - - [ ] useMutation 验证语法 - - [ ] useMutation 导入配置 - -**验收标准**: - -- API 调用成功返回数据 -- Hook 状态管理正确 -- 错误处理完善 - -### Phase 5: UI 实现(P0) - -**时间**:2 天 - -**任务**: - -- [ ] 重构 `McpPanel.tsx` 为 Tabs 布局 -- [ ] 创建 `ClaudeGeminiMcpTab.tsx` - - [ ] 移除对 `currentApp` 的依赖 - - [ ] 直接操作 `mcp.servers` - - [ ] **限制 `availableApps` 为 `['claude', 'gemini']`** - - [ ] **过滤掉 `apps.codex` 的历史数据** - - [ ] 添加提示:"Codex MCP 在专用 Tab 管理" -- [ ] 创建 `CodexMcpTab.tsx` - - [ ] 集成编辑器组件 - - [ ] 实现保存/导入/重置逻辑 - - [ ] 添加验证错误提示 -- [ ] 创建 `CodexMcpEditor.tsx` - - [ ] 集成 CodeMirror 6 - - [ ] 配置 TOML 语法高亮 - - [ ] 集成 TOML linter - - [ ] 实现双向绑定 -- [ ] 国际化:添加相关翻译 key -- [ ] **更新现有 MCP 表单组件,移除 `codex` 选项** - -**验收标准**: - -- MCP 面板有两个独立 Tab -- Tab1 (Claude & Gemini): - - `apps` 选项仅显示 claude/gemini - - 不显示任何 `apps.codex=true` 的服务器 - - 无法添加/编辑 Codex MCP -- Tab2 (Codex): - - 可正常编辑 raw TOML - - 语法错误有实时提示 - - 保存后配置持久化 - -### Phase 6: 增强功能(P1) - -**时间**:1 天 - -**任务**: - -- [ ] 添加 TOML 模板快捷插入功能 -- [ ] 导出到 Codex live 配置功能 -- [ ] 配置历史记录(可选) -- [ ] 改进错误提示(显示行号) - -**验收标准**: - -- 模板插入功能可用 -- 导出功能正常 - -### Phase 7: 测试与文档(P0) - -**时间**:1 天 - -**任务**: - -- [ ] 端到端测试: - - [ ] 新用户首次启动 - - [ ] 现有用户迁移场景(v3.6.2 → v3.7.0) - - [ ] 验证迁移后 `mcp.codex.servers` 被清空 - - [ ] 验证 unified MCP 不包含 `apps.codex` - - [ ] Claude ↔ Codex ↔ Gemini 切换 - - [ ] Codex MCP 编辑后切换生效 - - [ ] Tab1 无法操作 Codex MCP -- [ ] 更新 `CLAUDE.md` 文档 - - [ ] 明确 MCP 配置职责划分 - - [ ] 更新配置文件路径说明 -- [ ] 编写 migration guide -- [ ] 添加 CHANGELOG 条目 - -**验收标准**: - -- 所有测试用例通过 -- 文档完整准确 -- 迁移逻辑无数据丢失 - ---- - -## 风险控制 - -### 1. 数据丢失风险 - -**风险**:迁移过程中旧配置丢失 - -**控制措施**: - -- ✅ 迁移前自动备份 config.json(带时间戳) -- ✅ **迁移后清空 `mcp.codex.servers`,但不删除 `mcp.codex` 根节点**(保留结构用于回滚) -- ✅ 迁移日志记录详细信息(服务器数量、时间戳等) -- ✅ 提供回滚命令(Phase 6+) - -### 2. TOML 格式错误 - -**风险**:用户手写 TOML 导致 Codex 配置损坏 - -**控制措施**: - -- ✅ 保存前强制验证语法 -- ✅ 实时 linting 提示错误 -- ✅ 切换前再次验证最终配置 -- ✅ 写入失败时自动回滚(已有 `.bak` 机制) - -### 3. 并发写入 - -**风险**:多实例同时修改配置 - -**控制措施**: - -- ✅ 使用 RwLock 保护 config 访问 -- ✅ 使用 tauri-plugin-single-instance(已集成) - -### 4. Unified MCP 污染 - -**风险**:历史数据中存在 `apps.codex=true` 的服务器 - -**控制措施**: - -- ✅ **迁移时清空 `mcp.codex.servers`**,阻止 unified 迁移处理 Codex -- ✅ **前端过滤**:Tab1 显示时过滤掉 `apps.codex=true` 的项 -- ✅ **表单限制**:`availableApps` 仅包含 `['claude', 'gemini']` -- ✅ **后端验证**(可选):保存 unified MCP 时检查并拒绝包含 `codex` 的 apps - ---- - -## 测试验证 - -### 单元测试 - -```rust -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_convert_stdio_server_to_toml() { - let server = McpServer { - name: "test".into(), - server: ServerSpec::Stdio { - command: "npx".into(), - args: Some(vec!["-y".into(), "test".into()]), - env: Some(HashMap::from([ - ("KEY".into(), "value".into()) - ])), - cwd: None, - }, - // ... - }; - - let toml = convert_server_to_toml("test", &server).unwrap(); - - assert!(toml.contains("command = \"npx\"")); - assert!(toml.contains("args = [\"-y\", \"test\"]")); - assert!(toml.contains("KEY = \"value\"")); - } - - #[test] - fn test_toml_validation() { - let valid_toml = "[mcp]\n[[mcp.servers]]\nname = \"test\"\n"; - assert!(validate_toml(valid_toml).is_ok()); - - let invalid_toml = "[mcp\n[[mcp.servers]]\n"; - assert!(validate_toml(invalid_toml).is_err()); - } -} -``` - -### 集成测试场景 - -| 场景 | 步骤 | 预期结果 | -|------|------|----------| -| 新用户首次启动 | 1. 删除 config.json
2. 启动应用
3. 打开 Codex MCP Tab | 显示默认模板 | -| 现有用户迁移 | 1. 使用 v3.6.2 config.json(含 `mcp.codex.servers`)
2. 启动应用
3. 检查 config.json | - `codexMcp.rawToml` 存在且内容正确
- `mcp.codex.servers` 为空对象 `{}`
- `mcp.servers` 不含 `apps.codex` | -| 编辑 Codex MCP | 1. 在 Tab2 编辑 TOML
2. 保存
3. 检查 config.json | `codexMcp.rawToml` 更新 | -| 切换到 Codex | 1. 编辑 Codex MCP
2. 切换到 Codex provider
3. 检查 `~/.codex/config.toml` | MCP 段正确写入,与 raw TOML 一致 | -| 切换到 Claude | 1. 在 Tab1 添加 Claude MCP
2. 切换到 Claude provider
3. 检查 `~/.claude/settings.json` | 仅包含 `apps.claude=true` 的服务器 | -| TOML 语法错误 | 1. 在 Tab2 输入错误 TOML
2. 保存 | 显示错误提示,拒绝保存 | -| Tab1 隔离性 | 1. 打开 Tab1
2. 尝试添加服务器 | - `apps` 选项仅显示 claude/gemini
- 无法选择 codex | -| 历史数据过滤 | 1. 手动在 config.json 添加 `apps.codex=true` 的服务器
2. 打开 Tab1 | 该服务器不在列表中显示 | -| 从 live 导入 | 1. 手动编辑 `~/.codex/config.toml`
2. 点击 Tab2 "导入"
3. 检查编辑器 | 显示导入的 MCP 配置 | - -### 性能测试 - -- [ ] 大型 TOML(>10KB)编辑性能 -- [ ] CodeMirror 初始化时间(<500ms) -- [ ] 配置切换时间(<200ms) - ---- - -## 依赖项 - -### 前端新增 - -```bash -pnpm add @codemirror/lang-toml -``` - -### 后端(已有) - -- `toml = "0.8"` -- `toml_edit = "0.22"` - ---- - -## 时间线 - -| Phase | 工作量 | 累计 | -|-------|--------|------| -| Phase 0: 准备工作 | 0.5 天 | 0.5 天 | -| Phase 1: 后端基础 | 1.5 天 | 2 天 | -| Phase 2: 命令层 | 1 天 | 3 天 | -| Phase 3: 切换逻辑 | 1 天 | 4 天 | -| Phase 4: 前端 API | 0.5 天 | 4.5 天 | -| Phase 5: UI 实现 | 2 天 | 6.5 天 | -| Phase 6: 增强功能(可选)| 1 天 | 7.5 天 | -| Phase 7: 测试与文档 | 1 天 | 8.5 天 | - -**总计**:8.5 天(约 2 周) - -**MVP(最小可行产品)**:Phase 0-5 + Phase 7 = 7 天 - ---- - -## 回滚计划 - -如果重构出现严重问题,执行以下步骤: - -1. **恢复代码**: - ```bash - git checkout main - git branch -D feature/codex-mcp-raw-toml - ``` - -2. **恢复配置**: - ```bash - # 迁移时会自动备份为 config.v3.backup..json - cp ~/.cc-switch/config.v3.backup.*.json ~/.cc-switch/config.json - ``` - -3. **重启应用** - ---- - -## 成功标准 - -- ✅ 现有用户配置无损迁移 -- ✅ Codex MCP 配置保留注释和格式 -- ✅ MCP 面板与 app 切换完全解耦 -- ✅ Claude/Gemini 逻辑零改动 -- ✅ 所有测试用例通过 -- ✅ 文档完整更新 - ---- - -## 附录 - -### 示例配置 - -#### 迁移前(v3.6.2) - -```json -{ - "providers": [...], - "mcp": { - "codex": { - "servers": { - "fetch": { - "id": "fetch", - "name": "Fetch MCP", - "server": { - "type": "stdio", - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-fetch"] - }, - "enabled": true - } - } - } - } -} -``` - -#### 迁移后(v3.7.0) - -```json -{ - "providers": [...], - "mcp": { - "servers": { - "fetch": { - "id": "fetch", - "name": "Fetch MCP", - "server": { - "type": "stdio", - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-fetch"] - }, - "apps": { - "claude": true, - "gemini": false - } - } - }, - "codex": { - "servers": {} // ✅ 已清空,但保留结构用于回滚 - } - }, - "codexMcp": { - "rawToml": "[mcp]\n\n[[mcp.servers]]\nname = \"Fetch MCP\"\ncommand = \"npx\"\nargs = [\"-y\", \"@modelcontextprotocol/server-fetch\"]\n" - } -} -``` - -### 相关文档 - -- [Codex 官方 MCP 文档](https://codex.dev/docs/mcp) -- [TOML 规范](https://toml.io/en/) -- [CodeMirror 6 文档](https://codemirror.net/docs/) -- [项目 CLAUDE.md](../CLAUDE.md) - ---- - -**文档版本**:2.0 -**创建时间**:2025-11-18 -**最后更新**:2025-11-18 -**负责人**:Jason Young - ---- - -## 版本历史 - -### v2.0 (2025-11-18) -- ✅ **架构简化**:完全移除 unified MCP 中的 Codex 支持 -- ✅ **单一职责**:`mcp.servers` 仅用于 Claude/Gemini,`codexMcp.rawToml` 仅用于 Codex -- ✅ **迁移增强**:清空 `mcp.codex.servers` 避免重复处理 -- ✅ **UI 隔离**:Tab1 限制 `availableApps`,过滤 Codex 数据 -- ✅ **测试覆盖**:增加 Tab1 隔离性、历史数据过滤等场景 - -### v1.0 (2025-11-18) -- 初始版本 diff --git a/docs/REFACTORING_CHECKLIST.md b/docs/REFACTORING_CHECKLIST.md deleted file mode 100644 index 657a8e161..000000000 --- a/docs/REFACTORING_CHECKLIST.md +++ /dev/null @@ -1,490 +0,0 @@ -# CC Switch 重构实施清单 - -> 用于跟踪重构进度的详细检查清单 - -**开始日期**: ___________ -**预计完成**: ___________ -**当前阶段**: ___________ - ---- - -## 📋 阶段 0: 准备阶段 (预计 1 天) - -### 环境准备 - -- [ ] 创建新分支 `refactor/modernization` -- [ ] 创建备份标签 `git tag backup-before-refactor` -- [ ] 备份用户配置文件 `~/.cc-switch/config.json` -- [ ] 通知团队成员重构开始 - -### 依赖安装 - -```bash -pnpm add @tanstack/react-query -pnpm add react-hook-form @hookform/resolvers -pnpm add zod -pnpm add sonner -pnpm add next-themes -pnpm add @radix-ui/react-dialog @radix-ui/react-dropdown-menu -pnpm add @radix-ui/react-label @radix-ui/react-select -pnpm add @radix-ui/react-slot @radix-ui/react-switch @radix-ui/react-tabs -pnpm add class-variance-authority clsx tailwind-merge tailwindcss-animate -``` - -- [ ] 安装核心依赖 (上述命令) -- [ ] 验证依赖安装成功 `pnpm install` -- [ ] 验证编译通过 `pnpm typecheck` - -### 配置文件 - -- [ ] 创建 `components.json` -- [ ] 更新 `tsconfig.json` 添加路径别名 -- [ ] 更新 `vite.config.mts` 添加路径解析 -- [ ] 验证开发服务器启动 `pnpm dev` - -**完成时间**: ___________ -**遇到的问题**: ___________ - ---- - -## 📋 阶段 1: 基础设施 (预计 2-3 天) - -### 1.1 工具函数和基础组件 - -- [ ] 创建 `src/lib/utils.ts` (cn 函数) -- [ ] 创建 `src/components/ui/button.tsx` -- [ ] 创建 `src/components/ui/dialog.tsx` -- [ ] 创建 `src/components/ui/input.tsx` -- [ ] 创建 `src/components/ui/label.tsx` -- [ ] 创建 `src/components/ui/textarea.tsx` -- [ ] 创建 `src/components/ui/select.tsx` -- [ ] 创建 `src/components/ui/switch.tsx` -- [ ] 创建 `src/components/ui/tabs.tsx` -- [ ] 创建 `src/components/ui/sonner.tsx` -- [ ] 创建 `src/components/ui/form.tsx` - -**测试**: -- [ ] 验证所有 UI 组件可以正常导入 -- [ ] 创建一个测试页面验证组件样式 - -### 1.2 Query Client 设置 - -- [ ] 创建 `src/lib/query/queryClient.ts` -- [ ] 配置默认选项 (retry, staleTime 等) -- [ ] 导出 queryClient 实例 - -### 1.3 API 层 - -- [ ] 创建 `src/lib/api/providers.ts` - - [ ] getAll - - [ ] getCurrent - - [ ] add - - [ ] update - - [ ] delete - - [ ] switch - - [ ] importDefault - - [ ] updateTrayMenu - -- [ ] 创建 `src/lib/api/settings.ts` - - [ ] get - - [ ] save - -- [ ] 创建 `src/lib/api/mcp.ts` - - [ ] getConfig - - [ ] upsertServer - - [ ] deleteServer - -- [ ] 创建 `src/lib/api/index.ts` (聚合导出) - -**测试**: -- [ ] 验证 API 调用不会出现运行时错误 -- [ ] 确认类型定义正确 - -### 1.4 Query Hooks - -- [ ] 创建 `src/lib/query/queries.ts` - - [ ] useProvidersQuery - - [ ] useSettingsQuery - - [ ] useMcpConfigQuery - -- [ ] 创建 `src/lib/query/mutations.ts` - - [ ] useAddProviderMutation - - [ ] useSwitchProviderMutation - - [ ] useDeleteProviderMutation - - [ ] useUpdateProviderMutation - - [ ] useSaveSettingsMutation - -- [ ] 创建 `src/lib/query/index.ts` (聚合导出) - -**测试**: -- [ ] 在临时组件中测试每个 hook -- [ ] 验证 loading/error 状态正确 -- [ ] 验证缓存和自动刷新工作 - -**完成时间**: ___________ -**遇到的问题**: ___________ - ---- - -## 📋 阶段 2: 核心功能重构 (预计 3-4 天) - -### 2.1 主题系统 - -- [ ] 创建 `src/components/theme-provider.tsx` -- [ ] 创建 `src/components/mode-toggle.tsx` -- [ ] 更新 `src/index.css` 添加主题变量 -- [ ] 删除 `src/hooks/useDarkMode.ts` -- [ ] 更新所有组件使用新的主题系统 - -**测试**: -- [ ] 验证主题切换正常工作 -- [ ] 验证系统主题跟随功能 -- [ ] 验证主题持久化 - -### 2.2 更新 main.tsx - -- [ ] 引入 QueryClientProvider -- [ ] 引入 ThemeProvider -- [ ] 添加 Toaster 组件 -- [ ] 移除旧的 API 导入 - -**测试**: -- [ ] 验证应用可以正常启动 -- [ ] 验证 Context 正确传递 - -### 2.3 重构 App.tsx - -- [ ] 使用 useProvidersQuery 替代手动状态管理 -- [ ] 移除所有 loadProviders 相关代码 -- [ ] 移除手动 notification 状态 -- [ ] 简化事件监听逻辑 -- [ ] 更新对话框为新的 Dialog 组件 - -**目标**: 将 412 行代码减少到 ~100 行 - -**测试**: -- [ ] 验证供应商列表正常加载 -- [ ] 验证切换 Claude/Codex 正常工作 -- [ ] 验证事件监听正常工作 - -### 2.4 重构 ProviderList - -- [ ] 创建 `src/components/providers/ProviderList.tsx` -- [ ] 使用 mutation hooks 处理操作 -- [ ] 移除 onNotify prop -- [ ] 移除手动状态管理 - -**测试**: -- [ ] 验证供应商列表渲染 -- [ ] 验证切换操作 -- [ ] 验证删除操作 - -### 2.5 重构表单系统 - -- [ ] 创建 `src/lib/schemas/provider.ts` (Zod schema) -- [ ] 创建 `src/components/providers/ProviderForm.tsx` - - [ ] 使用 react-hook-form - - [ ] 使用 zodResolver - - [ ] 字段级验证 - -- [ ] 创建 `src/components/providers/AddProviderDialog.tsx` - - [ ] 使用新的 Dialog 组件 - - [ ] 集成 ProviderForm - - [ ] 使用 useAddProviderMutation - -- [ ] 创建 `src/components/providers/EditProviderDialog.tsx` - - [ ] 使用新的 Dialog 组件 - - [ ] 集成 ProviderForm - - [ ] 使用 useUpdateProviderMutation - -**测试**: -- [ ] 验证表单验证正常工作 -- [ ] 验证错误提示显示正确 -- [ ] 验证提交操作成功 -- [ ] 验证表单重置功能 - -### 2.6 清理旧组件 - -- [x] 删除 `src/components/AddProviderModal.tsx` -- [x] 删除 `src/components/EditProviderModal.tsx` -- [x] 更新所有引用这些组件的地方 -- [x] 删除 `src/components/ProviderForm.tsx` 及 `src/components/ProviderForm/` - -**完成时间**: ___________ -**遇到的问题**: ___________ - ---- - -## 📋 阶段 3: 设置和辅助功能 (预计 2-3 天) - -### 3.1 重构 SettingsDialog - -- [ ] 创建 `src/components/settings/SettingsDialog.tsx` - - [ ] 使用 Tabs 组件 - - [ ] 集成各个设置子组件 - -- [ ] 创建 `src/components/settings/GeneralSettings.tsx` - - [ ] 语言设置 - - [ ] 配置目录设置 - - [ ] 其他通用设置 - -- [ ] 创建 `src/components/settings/AboutSection.tsx` - - [ ] 版本信息 - - [ ] 更新检查 - - [ ] 链接 - -- [ ] 创建 `src/components/settings/ImportExportSection.tsx` - - [ ] 导入功能 - - [ ] 导出功能 - -**目标**: 将 643 行拆分为 4-5 个小组件,每个 100-150 行 - -**测试**: -- [ ] 验证设置保存功能 -- [ ] 验证导入导出功能 -- [ ] 验证更新检查功能 - -### 3.2 重构通知系统 - -- [ ] 在所有 mutations 中使用 `toast` 替代 `showNotification` -- [ ] 移除 App.tsx 中的 notification 状态 -- [ ] 移除自定义通知组件 - -**测试**: -- [ ] 验证成功通知显示 -- [ ] 验证错误通知显示 -- [ ] 验证通知自动消失 - -### 3.3 重构确认对话框 - -- [ ] 更新 `src/components/ConfirmDialog.tsx` 使用新的 Dialog -- [ ] 或者直接使用 shadcn/ui 的 AlertDialog - -**测试**: -- [ ] 验证删除确认对话框 -- [ ] 验证其他确认场景 - -**完成时间**: ___________ -**遇到的问题**: ___________ - ---- - -## 📋 阶段 4: 清理和优化 (预计 1-2 天) - -### 4.1 移除旧代码 - -- [x] 删除 `src/lib/styles.ts` -- [x] 从 `src/lib/tauri-api.ts` 移除 `window.api` 绑定 -- [x] 精简 `src/lib/tauri-api.ts`,只保留事件监听相关 -- [x] 删除或更新 `src/vite-env.d.ts` 中的过时类型 - -### 4.2 代码审查 - -- [ ] 检查所有 TODO 注释 -- [x] 检查是否还有 `window.api` 调用 -- [ ] 检查是否还有手动状态管理 -- [x] 统一代码风格 - -### 4.3 类型检查 - -- [x] 运行 `pnpm typecheck` 确保无错误 -- [x] 修复所有类型错误 -- [x] 更新类型定义 - -### 4.4 性能优化 - -- [ ] 检查是否有不必要的重渲染 -- [ ] 添加必要的 React.memo -- [ ] 优化 Query 缓存配置 - -**完成时间**: ___________ -**遇到的问题**: ___________ - ---- - -## 📋 阶段 5: 测试和修复 (预计 2-3 天) - -### 5.1 功能测试 - -#### 供应商管理 -- [ ] 添加供应商 (Claude) -- [ ] 添加供应商 (Codex) -- [ ] 编辑供应商 -- [ ] 删除供应商 -- [ ] 切换供应商 -- [ ] 导入默认配置 - -#### 应用切换 -- [ ] Claude <-> Codex 切换 -- [ ] 切换后数据正确加载 -- [ ] 切换后托盘菜单更新 - -#### 设置 -- [ ] 保存通用设置 -- [ ] 切换语言 -- [ ] 配置目录选择 -- [ ] 导入配置 -- [ ] 导出配置 - -#### UI 交互 -- [ ] 主题切换 (亮色/暗色) -- [ ] 对话框打开/关闭 -- [ ] 表单验证 -- [ ] Toast 通知 - -#### MCP 管理 -- [ ] 列表显示 -- [ ] 添加 MCP -- [ ] 编辑 MCP -- [ ] 删除 MCP -- [ ] 启用/禁用 MCP - -### 5.2 边界情况测试 - -- [ ] 空供应商列表 -- [ ] 无效配置文件 -- [ ] 网络错误 -- [ ] 后端错误响应 -- [ ] 并发操作 -- [ ] 表单输入边界值 - -### 5.3 兼容性测试 - -- [ ] Windows 测试 -- [ ] macOS 测试 -- [ ] Linux 测试 - -### 5.4 性能测试 - -- [ ] 100+ 供应商加载速度 -- [ ] 快速切换供应商 -- [ ] 内存使用情况 -- [ ] CPU 使用情况 - -### 5.5 Bug 修复 - -**Bug 列表** (发现后记录): - -1. ___________ - - [ ] 已修复 - - [ ] 已验证 - -2. ___________ - - [ ] 已修复 - - [ ] 已验证 - -**完成时间**: ___________ -**遇到的问题**: ___________ - ---- - -## 📋 最终检查 - -### 代码质量 - -- [ ] 所有 TypeScript 错误已修复 -- [ ] 运行 `pnpm format` 格式化代码 -- [ ] 运行 `pnpm typecheck` 通过 -- [ ] 代码审查完成 - -### 文档更新 - -- [ ] 更新 `CLAUDE.md` 反映新架构 -- [ ] 更新 `README.md` (如有必要) -- [ ] 添加 Migration Guide (可选) - -### 性能基准 - -记录性能数据: - -**旧版本**: -- 启动时间: _____ms -- 供应商加载: _____ms -- 内存占用: _____MB - -**新版本**: -- 启动时间: _____ms -- 供应商加载: _____ms -- 内存占用: _____MB - -### 代码统计 - -**代码行数对比**: - -| 文件 | 旧版本 | 新版本 | 减少 | -|------|--------|--------|------| -| App.tsx | 412 | ~100 | -76% | -| tauri-api.ts | 712 | ~50 | -93% | -| ProviderForm.tsx | 271 | ~150 | -45% | -| settings 模块 | 1046 | ~470 (拆分) | -55% | -| **总计** | 2038 | ~700 | **-66%** | - ---- - -## 📦 发布准备 - -### Pre-release 测试 - -- [ ] 创建 beta 版本 `v4.0.0-beta.1` -- [ ] 在测试环境验证 -- [ ] 收集用户反馈 - -### 正式发布 - -- [ ] 合并到 main 分支 -- [ ] 创建 Release Tag `v4.0.0` -- [ ] 更新 Changelog -- [ ] 发布 GitHub Release -- [ ] 通知用户更新 - ---- - -## 🚨 回滚触发条件 - -如果出现以下情况,考虑回滚: - -- [ ] 重大功能无法使用 -- [ ] 用户数据丢失 -- [ ] 严重性能问题 -- [ ] 无法修复的兼容性问题 - -**回滚命令**: -```bash -git reset --hard backup-before-refactor -# 或 -git revert -``` - ---- - -## 📝 总结报告 - -### 成功指标 - -- [ ] 所有现有功能正常工作 -- [ ] 代码量减少 40%+ -- [ ] 无用户数据丢失 -- [ ] 性能未下降 - -### 经验教训 - -**遇到的主要挑战**: -1. ___________ -2. ___________ -3. ___________ - -**解决方案**: -1. ___________ -2. ___________ -3. ___________ - -**未来改进**: -1. ___________ -2. ___________ -3. ___________ - ---- - -**重构完成日期**: ___________ -**总耗时**: _____ 天 -**参与人员**: ___________ diff --git a/docs/REFACTORING_MASTER_PLAN.md b/docs/REFACTORING_MASTER_PLAN.md deleted file mode 100644 index f38e19d3c..000000000 --- a/docs/REFACTORING_MASTER_PLAN.md +++ /dev/null @@ -1,1658 +0,0 @@ -# CC Switch 现代化重构完整方案 - -> Breaking Change 提醒(后续示例如仍出现 `app_type/appType` 字样,请按本规范理解与替换): -> -> - 后端 Tauri 命令统一仅接受 `app` 参数(值:`claude` 或 `codex`),不再接受 `app_type`/`appType`。 -> - 传入未知 `app` 会返回本地化错误,并提示“可选值: claude, codex”。 -> - 前端与文档中的旧示例如包含 `app_type`,一律替换为 `{ app }`。 - -## 📋 目录 - -- [第一部分: 战略规划](#第一部分-战略规划) - - [重构背景与目标](#重构背景与目标) - - [当前问题全面分析](#当前问题全面分析) - - [技术选型与理由](#技术选型与理由) -- [第二部分: 架构设计](#第二部分-架构设计) - - [新的目录结构](#新的目录结构) - - [数据流架构](#数据流架构) - - [组件拆分详细方案](#组件拆分详细方案) -- [第三部分: 实施计划](#第三部分-实施计划) - - [分阶段实施路线图](#分阶段实施路线图) - - [详细实施步骤](#详细实施步骤) -- [第四部分: 质量保障](#第四部分-质量保障) - - [测试策略](#测试策略) - - [风险控制](#风险控制) - - [回滚方案](#回滚方案) - ---- - -# 第一部分: 战略规划 - -## 🎯 重构背景与目标 - -### 为什么要重构? - -当前代码库存在以下核心问题: - -1. **状态管理混乱** - - 手动管理 20+ `useState` - - 大量复杂的 `useEffect` 依赖链 - - 数据同步逻辑分散 - -2. **组件过于臃肿** - - `SettingsModal.tsx`: **1046 行** 😱 - - `ProviderList.tsx`: **418 行** - - `ProviderForm.tsx`: **271 行** - -3. **代码重复严重** - - 相似的数据获取逻辑在多个组件重复 - - 表单验证逻辑手动编写 - - 错误处理不统一 - -4. **UI 缺乏统一性** - - 自定义样式分散 - - 缺乏设计系统 - - 响应式支持不足 - -5. **可维护性差** - - 组件职责不清晰 - - 耦合度高 - - 难以测试 - -### 重构目标 - -| 维度 | 目标 | 衡量标准 | -| -------------- | -------------------- | -------------- | -| **代码质量** | 减少 40-60% 样板代码 | 代码行数统计 | -| **开发效率** | 提升 50%+ 开发速度 | 新功能开发时间 | -| **用户体验** | 统一设计系统 | UI 一致性检查 | -| **可维护性** | 清晰的架构分层 | 代码审查时间 | -| **功能完整性** | 100% 功能无回归 | 全量测试通过 | - ---- - -## 🔍 当前问题全面分析 - -### 问题 1: App.tsx - 状态管理混乱 (412行) - -**现状**: - -```typescript -// 10+ 个 useState,状态管理混乱 -const [providers, setProviders] = useState>({}) -const [currentProviderId, setCurrentProviderId] = useState("") -const [notification, setNotification] = useState<{...} | null>(null) -const [isNotificationVisible, setIsNotificationVisible] = useState(false) -const [confirmDialog, setConfirmDialog] = useState<{...} | null>(null) -const [isSettingsOpen, setIsSettingsOpen] = useState(false) -const [isMcpOpen, setIsMcpOpen] = useState(false) -// ... 更多 - -// 手动数据加载,缺少 loading/error 状态 -const loadProviders = async () => { - const loadedProviders = await window.api.getProviders(activeApp) - const currentId = await window.api.getCurrentProvider(activeApp) - setProviders(loadedProviders) - setCurrentProviderId(currentId) -} - -// 复杂的 useEffect 依赖 -useEffect(() => { - loadProviders() -}, [activeApp]) -``` - -**核心问题**: - -- ❌ 状态同步困难 -- ❌ 没有 loading/error 处理 -- ❌ 错误处理不统一 -- ❌ 组件责任过重 - -**目标**: - -```typescript -// React Query: 3 行搞定 -const { data, isLoading, error } = useProvidersQuery(activeApp); -const providers = data?.providers || {}; -const currentProviderId = data?.currentProviderId || ""; -``` - ---- - -### 问题 2: SettingsModal.tsx - 超级巨无霸组件 (1046行) - -**现状结构**: - -``` -SettingsModal.tsx (1046 行) -├── 20+ useState (settings, configPath, version, isChecking...) -├── 15+ 处理函数 -│ ├── loadSettings() -│ ├── saveSettings() -│ ├── handleLanguageChange() -│ ├── handleCheckUpdate() -│ ├── handleExportConfig() -│ ├── handleImportConfig() -│ ├── handleBrowseConfigDir() -│ └── ... 更多 -├── 语言设置 UI -├── 窗口行为设置 UI -├── 配置文件位置 UI -├── 配置目录覆盖 UI (3个输入框) -├── 导入导出 UI -├── 关于和更新 UI -└── 2个子对话框 (ImportProgress, RestartConfirm) -``` - -**核心问题**: - -- ❌ 单个文件超过 1000 行 -- ❌ 多种职责混杂 -- ❌ 难以理解和维护 -- ❌ 无法并行开发 -- ❌ 难以测试 - -**目标**: 拆分为 **7 个小组件** (~470 行总计) - ---- - -### 问题 3: ProviderList.tsx - 内嵌组件和逻辑混杂 (418行) - -**现状结构**: - -``` -ProviderList.tsx (418 行) -├── SortableProviderItem (内嵌子组件, ~100行) -├── 拖拽排序逻辑 -├── 用量配置逻辑 -├── URL 处理逻辑 -├── Claude 插件同步逻辑 -└── 空状态 UI -``` - -**核心问题**: - -- ❌ 内嵌组件导致代码难读 -- ❌ 拖拽逻辑和 UI 混在一起 -- ❌ 业务逻辑分散 - -**目标**: 拆分为 **4 个独立组件** + **1 个自定义 Hook** - ---- - -### 问题 4: tauri-api.ts - 全局污染 (712行) - -**现状**: - -```typescript -// 问题 1: 污染全局命名空间 -if (typeof window !== "undefined") { - (window as any).api = tauriAPI; -} - -// 问题 2: 无缓存机制 -getProviders: async (app?: AppId) => { - try { - return await invoke("get_providers", { app }); - } catch (error) { - console.error("获取供应商列表失败:", error); - return {}; // 错误被吞掉 - } -}; -``` - -**核心问题**: - -- ❌ 全局 `window.api` 污染命名空间 -- ❌ 无缓存,重复请求 -- ❌ 无自动重试 -- ❌ 错误处理不统一 - -**目标**: - -- 封装为 API 层 (`lib/api/`) -- React Query 管理缓存和状态 - ---- - -### 问题 5: 表单验证 - 手动编写 (ProviderForm.tsx) - -**现状**: - -```typescript -const [name, setName] = useState(""); -const [nameError, setNameError] = useState(""); -const [apiKey, setApiKey] = useState(""); -const [apiKeyError, setApiKeyError] = useState(""); - -const validate = () => { - let valid = true; - if (!name) { - setNameError("请填写名称"); - valid = false; - } else { - setNameError(""); - } - if (!apiKey) { - setApiKeyError("请填写 API Key"); - valid = false; - } else if (apiKey.length < 10) { - setApiKeyError("API Key 长度不足"); - valid = false; - } else { - setApiKeyError(""); - } - return valid; -}; -``` - -**核心问题**: - -- ❌ 每个字段需要 2 个 state (值 + 错误) -- ❌ 验证逻辑手动编写 -- ❌ 代码冗长 - -**目标**: 使用 `react-hook-form` + `zod` - -```typescript -const schema = z.object({ - name: z.string().min(1, "请填写名称"), - apiKey: z.string().min(10, "API Key 长度不足"), -}); - -const form = useForm({ resolver: zodResolver(schema) }); -``` - ---- - -## 🛠 技术选型与理由 - -### 核心技术栈 - -| 技术 | 版本 | 用途 | 替代方案 | 为何选它? | -| ------------------------- | ------- | -------------- | --------------- | -------------------- | -| **@tanstack/react-query** | ^5.90.2 | 服务端状态管理 | SWR, RTK Query | 功能最全,生态最好 | -| **react-hook-form** | ^7.63.0 | 表单管理 | Formik | 性能更好,API 更简洁 | -| **zod** | ^4.1.11 | 运行时类型验证 | yup, joi | TypeScript 原生支持 | -| **shadcn/ui** | latest | UI 组件库 | Radix UI 原生 | 可定制,代码归属权 | -| **sonner** | ^2.0.7 | Toast 通知 | react-hot-toast | 更现代,动画更好 | -| **next-themes** | ^0.4.6 | 主题管理 | 自定义实现 | 开箱即用,SSR 友好 | - ---- - -# 第二部分: 架构设计 - -## 📁 新的目录结构 - -### 完整目录树 - -``` -src/ -├── components/ -│ ├── ui/ # shadcn/ui 基础组件 (由 CLI 生成) -│ │ ├── button.tsx -│ │ ├── dialog.tsx -│ │ ├── input.tsx -│ │ ├── label.tsx -│ │ ├── form.tsx -│ │ ├── select.tsx -│ │ ├── switch.tsx -│ │ ├── tabs.tsx -│ │ ├── card.tsx -│ │ ├── badge.tsx -│ │ └── sonner.tsx # Toast 组件 -│ │ -│ ├── providers/ # 供应商管理模块 -│ │ ├── ProviderList.tsx # 列表容器 (~100行) -│ │ ├── ProviderCard.tsx # 供应商卡片 (~120行) -│ │ ├── ProviderActions.tsx # 操作按钮组 (~80行) -│ │ ├── ProviderEmptyState.tsx # 空状态 (~30行) -│ │ ├── AddProviderDialog.tsx # 添加对话框 (~60行) -│ │ ├── EditProviderDialog.tsx # 编辑对话框 (~60行) -│ │ └── forms/ # 表单子模块 -│ │ ├── ProviderForm.tsx # 主表单 (~150行) -│ │ ├── PresetSelector.tsx # 预设选择器 (~60行) -│ │ ├── ApiKeyInput.tsx # API Key 输入 (~40行) -│ │ ├── ConfigEditor.tsx # 配置编辑器 (~80行) -│ │ └── KimiModelSelector.tsx # Kimi 模型选择器 (~40行) -│ │ -│ ├── settings/ # 设置管理模块 (拆分自 SettingsModal) -│ │ ├── SettingsDialog.tsx # 设置对话框容器 (~80行) -│ │ ├── LanguageSettings.tsx # 语言设置 (~40行) -│ │ ├── WindowSettings.tsx # 窗口行为设置 (~50行) -│ │ ├── ConfigPathDisplay.tsx # 配置路径显示 (~40行) -│ │ ├── DirectorySettings/ # 目录设置子模块 -│ │ │ ├── index.tsx # 目录设置容器 (~60行) -│ │ │ └── DirectoryInput.tsx # 单个目录输入组件 (~50行) -│ │ ├── ImportExportSection.tsx # 导入导出 (~120行) -│ │ ├── AboutSection.tsx # 关于和更新 (~100行) -│ │ └── RestartDialog.tsx # 重启确认对话框 (~40行) -│ │ -│ ├── usage/ # 用量查询模块 -│ │ ├── UsageFooter.tsx # 用量信息展示 -│ │ ├── UsageScriptModal.tsx # 用量脚本配置 -│ │ └── UsageEditor.tsx # 脚本编辑器 -│ │ -│ ├── mcp/ # MCP 管理模块 -│ │ ├── McpPanel.tsx # MCP 管理面板 -│ │ ├── McpList.tsx # MCP 列表 -│ │ ├── McpForm.tsx # MCP 表单 -│ │ └── McpTemplates.tsx # MCP 模板选择 -│ │ -│ ├── shared/ # 共享组件 -│ │ ├── AppSwitcher.tsx # Claude/Codex 切换器 -│ │ ├── ConfirmDialog.tsx # 确认对话框 -│ │ ├── UpdateBadge.tsx # 更新徽章 -│ │ ├── JsonEditor.tsx # JSON 编辑器 -│ │ ├── BrandIcons.tsx # 品牌图标 -│ │ └── ImportProgressModal.tsx # 导入进度 -│ │ -│ ├── theme-provider.tsx # 主题 Provider -│ └── mode-toggle.tsx # 主题切换按钮 -│ -├── hooks/ # 自定义 Hooks (业务逻辑层) -│ ├── useSettings.ts # 设置管理逻辑 -│ ├── useImportExport.ts # 导入导出逻辑 -│ ├── useDragSort.ts # 拖拽排序逻辑 -│ ├── useProviderActions.ts # 供应商操作 (可选) -│ ├── useVSCodeSync.ts # VS Code 同步 -│ ├── useClaudePlugin.ts # Claude 插件管理 -│ └── useAppVersion.ts # 版本信息 -│ -├── lib/ -│ ├── query/ # React Query 层 -│ │ ├── index.ts # 导出所有 hooks -│ │ ├── queryClient.ts # QueryClient 配置 -│ │ ├── queries.ts # 所有查询 hooks -│ │ └── mutations.ts # 所有变更 hooks -│ │ -│ ├── api/ # API 调用层 (封装 Tauri invoke) -│ │ ├── providers.ts # 供应商 API -│ │ ├── settings.ts # 设置 API -│ │ ├── mcp.ts # MCP API -│ │ ├── usage.ts # 用量查询 API -│ │ ├── vscode.ts # VS Code API -│ │ └── index.ts # 聚合导出 -│ │ -│ ├── schemas/ # Zod 验证 Schemas -│ │ ├── provider.ts # 供应商验证规则 -│ │ ├── settings.ts # 设置验证规则 -│ │ └── mcp.ts # MCP 验证规则 -│ │ -│ ├── utils/ # 工具函数 -│ │ ├── errorHandling.ts # 错误处理 -│ │ ├── providerUtils.ts # 供应商工具 -│ │ └── configUtils.ts # 配置工具 -│ │ -│ └── utils.ts # shadcn/ui 工具函数 (cn) -│ -├── types/ # TypeScript 类型定义 -│ └── index.ts -│ -├── contexts/ # React Contexts (保留现有) -│ └── UpdateContext.tsx # 更新管理 Context -│ -├── i18n/ # 国际化 (保留现有) -│ ├── index.ts -│ └── locales/ -│ -├── App.tsx # 主应用组件 (简化到 ~100行) -├── main.tsx # 入口文件 (添加 Providers) -└── index.css # 全局样式 -``` - -### 目录结构设计原则 - -1. **按功能模块分组** (providers/, settings/, mcp/) -2. **按技术层次分层** (components/, hooks/, lib/) -3. **UI 组件独立** (ui/ 目录) -4. **业务逻辑提取** (hooks/ 目录) -5. **数据层封装** (api/ 目录) - ---- - -## 🏗 数据流架构 - -### 分层架构图 - -``` -┌─────────────────────────────────────────┐ -│ UI 层 (Components) │ -│ ProviderList, SettingsDialog, etc. │ -└────────────────┬────────────────────────┘ - │ 使用 - ↓ -┌─────────────────────────────────────────┐ -│ 业务逻辑层 (Custom Hooks) │ -│ useSettings, useDragSort, etc. │ -└────────────────┬────────────────────────┘ - │ 调用 - ↓ -┌─────────────────────────────────────────┐ -│ 数据管理层 (React Query Hooks) │ -│ useProvidersQuery, useMutation, etc. │ -└────────────────┬────────────────────────┘ - │ 调用 - ↓ -┌─────────────────────────────────────────┐ -│ API 层 (API Functions) │ -│ providersApi, settingsApi, etc. │ -└────────────────┬────────────────────────┘ - │ invoke - ↓ -┌─────────────────────────────────────────┐ -│ Tauri Backend (Rust) │ -│ Commands, State, File System │ -└─────────────────────────────────────────┘ -``` - -### 数据流示例 - -**场景**: 切换供应商 - -``` -1. 用户点击按钮 - ↓ -2. ProviderCard 调用 onClick={() => switchMutation.mutate(id)} - ↓ -3. useSwitchProviderMutation (lib/query/mutations.ts) - - mutationFn: 调用 providersApi.switch(id, appType) - ↓ -4. providersApi.switch (lib/api/providers.ts) - - 调用 invoke('switch_provider', { id, app }) - ↓ -5. Tauri Backend (Rust) - - 执行切换逻辑 - - 更新配置文件 - - 返回结果 - ↓ -6. useSwitchProviderMutation - - onSuccess: invalidateQueries(['providers', appType]) - - onSuccess: updateTrayMenu() - - onSuccess: toast.success('切换成功') - ↓ -7. useProvidersQuery 自动重新获取数据 - ↓ -8. UI 自动更新 -``` - -### 关键设计原则 - -1. **单一职责**: 每层只做一件事 -2. **依赖倒置**: UI 依赖抽象 (hooks),不依赖具体实现 -3. **开闭原则**: 易于扩展,无需修改现有代码 -4. **状态分离**: - - 服务端状态 → React Query - - 客户端 UI 状态 → useState - - 全局状态 → Context - ---- - -## 🔧 组件拆分详细方案 - -### 拆分策略: SettingsModal (1046行 → 7个组件) - -#### 拆分前后对比 - -``` -┌───────────────────────────────────┐ -│ SettingsModal.tsx (1046 行) │ ❌ 过于臃肿 -│ │ -│ - 20+ useState │ -│ - 15+ 函数 │ -│ - 600+ 行 JSX │ -│ - 难以理解和维护 │ -└───────────────────────────────────┘ - - ↓ 重构 - -┌─────────────────────────────────────────────────┐ -│ settings/ 模块 (7个组件, ~470行) │ -│ │ -│ ├── SettingsDialog.tsx (容器, ~80行) │ -│ │ └── 使用 useSettings hook │ -│ │ │ -│ ├── LanguageSettings.tsx (~40行) │ -│ ├── WindowSettings.tsx (~50行) │ -│ ├── ConfigPathDisplay.tsx (~40行) │ -│ ├── DirectorySettings/ (~110行) │ -│ │ ├── index.tsx (~60行) │ -│ │ └── DirectoryInput.tsx (~50行) │ -│ ├── ImportExportSection.tsx (~120行) │ -│ │ └── 使用 useImportExport hook │ -│ └── AboutSection.tsx (~100行) │ -│ └── 使用 useAppVersion, useUpdate hooks │ -└─────────────────────────────────────────────────┘ - -✅ 每个组件 30-120 行 -✅ 职责清晰 -✅ 易于测试 -✅ 可独立开发 -``` - -#### 拆分详细方案 - -**1. SettingsDialog.tsx (容器组件, ~80行)** - -职责: 组织整体布局,协调子组件 - -```typescript -import { LanguageSettings } from './LanguageSettings' -import { WindowSettings } from './WindowSettings' -import { DirectorySettings } from './DirectorySettings' -import { ImportExportSection } from './ImportExportSection' -import { AboutSection } from './AboutSection' -import { useSettings } from '@/hooks/useSettings' - -export function SettingsDialog({ open, onOpenChange }) { - const { settings, updateSettings, saveSettings, isPending } = useSettings() - - return ( - - - - 设置 - - - - - 通用 - 高级 - 关于 - - - - updateSettings({ language: lang })} - /> - - - - - - - - - - - - - - - - - - - - - ) -} -``` - -**2. LanguageSettings.tsx (~40行)** - -职责: 语言切换 UI - -```typescript -interface LanguageSettingsProps { - value: 'zh' | 'en' - onChange: (lang: 'zh' | 'en') => void -} - -export function LanguageSettings({ value, onChange }: LanguageSettingsProps) { - return ( -
-

语言设置

-
- - -
-
- ) -} -``` - -**3. DirectoryInput.tsx (~50行)** - -职责: 可复用的目录选择输入框 - -```typescript -import { FolderSearch, Undo2 } from 'lucide-react' - -interface DirectoryInputProps { - label: string - description?: string - value?: string - onChange: (value: string | undefined) => void - type: 'app' | 'claude' | 'codex' -} - -export function DirectoryInput({ label, description, value, onChange }: DirectoryInputProps) { - const handleBrowse = async () => { - const selected = await window.api.selectConfigDirectory(value) - if (selected) onChange(selected) - } - - const handleReset = () => { - onChange(undefined) - } - - return ( -
- - {description &&

{description}

} -
- onChange(e.target.value)} - className="flex-1 font-mono text-xs" - /> - - -
-
- ) -} -``` - -**4. useSettings Hook (业务逻辑提取)** - -```typescript -export function useSettings() { - const queryClient = useQueryClient(); - - // 获取设置 - const { data: settings, isLoading } = useQuery({ - queryKey: ["settings"], - queryFn: async () => await settingsApi.get(), - }); - - // 保存设置 - const saveMutation = useMutation({ - mutationFn: async (newSettings: Settings) => - await settingsApi.save(newSettings), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["settings"] }); - toast.success("设置已保存"); - }, - }); - - // 本地临时状态 (保存前) - const [localSettings, setLocalSettings] = useState(null); - const currentSettings = localSettings || settings || {}; - - return { - settings: currentSettings, - updateSettings: (updates: Partial) => { - setLocalSettings((prev) => ({ ...prev, ...updates })); - }, - saveSettings: () => { - if (localSettings) saveMutation.mutate(localSettings); - }, - resetSettings: () => setLocalSettings(null), - isPending: saveMutation.isPending, - isLoading, - }; -} -``` - ---- - -### 拆分策略: ProviderList (418行 → 4个组件 + 1个Hook) - -#### 拆分方案 - -``` -ProviderList.tsx (418 行) ❌ 内嵌组件、逻辑混杂 - - ↓ 重构 - -providers/ 模块 (4个组件 + 1个Hook, ~330行) - -├── ProviderList.tsx (容器, ~100行) -│ └── 使用 useDragSort hook -│ -├── ProviderCard.tsx (~120行) -│ └── 显示单个供应商信息 -│ -├── ProviderActions.tsx (~80行) -│ └── 操作按钮组 (switch, edit, delete, usage) -│ -├── ProviderEmptyState.tsx (~30行) -│ └── 空状态提示 -│ -└── hooks/useDragSort.ts (~100行) - └── 拖拽排序逻辑 -``` - -#### 代码示例 - -**ProviderList.tsx (容器)** - -```typescript -import { ProviderCard } from './ProviderCard' -import { ProviderEmptyState } from './ProviderEmptyState' -import { useDragSort } from '@/hooks/useDragSort' - -export function ProviderList({ providers, currentProviderId, appType }) { - const { sortedProviders, handleDragEnd, sensors } = useDragSort(providers, appType) - - if (sortedProviders.length === 0) { - return - } - - return ( - - p.id)} - strategy={verticalListSortingStrategy} - > -
- {sortedProviders.map(provider => ( - - ))} -
-
-
- ) -} -``` - -**useDragSort.ts (逻辑提取)** - -```typescript -export function useDragSort( - providers: Record, - appType: AppId -) { - const queryClient = useQueryClient(); - const { t } = useTranslation(); - - // 排序逻辑 - const sortedProviders = useMemo(() => { - return Object.values(providers).sort((a, b) => { - if (a.sortIndex !== undefined && b.sortIndex !== undefined) { - return a.sortIndex - b.sortIndex; - } - const timeA = a.createdAt || 0; - const timeB = b.createdAt || 0; - if (timeA === 0 && timeB === 0) { - return a.name.localeCompare(b.name, "zh-CN"); - } - return timeA === 0 ? -1 : timeB === 0 ? 1 : timeA - timeB; - }); - }, [providers]); - - // 拖拽传感器 - const sensors = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), - useSensor(KeyboardSensor) - ); - - // 拖拽结束处理 - const handleDragEnd = useCallback( - async (event: DragEndEvent) => { - const { active, over } = event; - if (!over || active.id === over.id) return; - - const oldIndex = sortedProviders.findIndex((p) => p.id === active.id); - const newIndex = sortedProviders.findIndex((p) => p.id === over.id); - - const reordered = arrayMove(sortedProviders, oldIndex, newIndex); - const updates = reordered.map((p, i) => ({ id: p.id, sortIndex: i })); - - try { - await providersApi.updateSortOrder(updates, appType); - queryClient.invalidateQueries({ queryKey: ["providers", appType] }); - toast.success(t("provider.sortUpdated")); - } catch (error) { - toast.error(t("provider.sortUpdateFailed")); - } - }, - [sortedProviders, appType, queryClient, t] - ); - - return { sortedProviders, sensors, handleDragEnd }; -} -``` - ---- - -### 代码量对比总结 - -| 组件 | 重构前 | 重构后 | 变化 | -| -------------------- | ---------- | ---------------- | -------- | -| **SettingsModal** | 1046 行 | 7个组件 ~470行 | **-55%** | -| **ProviderList** | 418 行 | 4个组件 ~330行 | **-21%** | -| **业务逻辑 (Hooks)** | 混在组件中 | 5个 hooks ~400行 | 提取独立 | -| **总计** | 1464 行 | ~1200 行 | **-18%** | - -**注意**: 代码总量略有减少,但**可维护性大幅提升**: - -- ✅ 每个文件 30-120 行,易于理解 -- ✅ 关注点分离,职责清晰 -- ✅ 业务逻辑可复用 -- ✅ 易于测试和调试 - ---- - -# 第三部分: 实施计划 - -## 📅 分阶段实施路线图 - -### 总览 - -| 阶段 | 目标 | 工期 | 产出 | -| ---------- | -------------- | ------------ | ---------------------------- | -| **阶段 0** | 准备环境 | 1 天 | 依赖安装、配置完成 | -| **阶段 1** | 搭建基础设施(✅ 已完成) | 2-3 天 | API 层、Query Hooks 完成 | -| **阶段 2** | 重构核心功能(✅ 已完成) | 3-4 天 | App.tsx、ProviderList 完成 | -| **阶段 3** | 重构设置和辅助(✅ 已完成) | 2-3 天 | SettingsDialog、通知系统完成 | -| **阶段 4** | 清理和优化 | 1-2 天 | 旧代码删除、优化完成 | -| **阶段 5** | 测试和修复 | 2-3 天 | 测试通过、Bug 修复 | -| **总计** | - | **11-16 天** | v4.0.0 发布 | - ---- - -### 阶段 0: 准备阶段 (1天) - -**目标**: 环境准备和依赖安装 - -#### 任务清单 - -- [ ] 创建新分支 `refactor/modernization` -- [ ] 创建备份标签 `git tag backup-before-refactor` -- [ ] 安装核心依赖 -- [ ] 配置 shadcn/ui -- [ ] 配置 TypeScript 路径别名 -- [ ] 配置 Vite 路径解析 -- [ ] 验证开发服务器启动 - -#### 详细步骤 - -**1. 创建分支和备份** - -```bash -# 创建新分支 -git checkout -b refactor/modernization - -# 创建备份标签 -git tag backup-before-refactor - -# 推送标签到远程 (可选) -git push origin backup-before-refactor -``` - -**2. 安装依赖** - -```bash -# 核心依赖 -pnpm add @tanstack/react-query -pnpm add react-hook-form @hookform/resolvers -pnpm add zod -pnpm add sonner -pnpm add next-themes - -# Radix UI 组件 (shadcn/ui 依赖) -pnpm add @radix-ui/react-dialog -pnpm add @radix-ui/react-dropdown-menu -pnpm add @radix-ui/react-label -pnpm add @radix-ui/react-select -pnpm add @radix-ui/react-slot -pnpm add @radix-ui/react-switch -pnpm add @radix-ui/react-tabs -pnpm add @radix-ui/react-checkbox - -# 样式工具 -pnpm add class-variance-authority -pnpm add clsx -pnpm add tailwind-merge -``` - -**3. 创建 `components.json`** - -```json -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.js", - "css": "src/index.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "iconLibrary": "lucide", - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - } -} -``` - -**4. 更新 `tsconfig.json`** - -```json -{ - "compilerOptions": { - // ... 现有配置 - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - } -} -``` - -**5. 更新 `vite.config.mts`** - -```typescript -import path from "path"; -import react from "@vitejs/plugin-react"; -import { defineConfig } from "vite"; - -export default defineConfig({ - plugins: [react()], - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, -}); -``` - -**6. 验证** - -```bash -pnpm dev # 确保开发服务器正常启动 -pnpm typecheck # 确保类型检查通过 -``` - ---- - -### 阶段 1: 基础设施 (2-3天) - -**目标**: 搭建新架构的基础层 - -#### 任务清单 - -- [x] 创建工具函数 (`lib/utils.ts`) -- [x] 添加基础 UI 组件 (Button, Dialog, Input, Form 等) -- [x] 创建 Query Client 配置 -- [x] 封装 API 层 (providers, settings, mcp) -- [x] 创建 Query Hooks (queries, mutations) -- [x] 创建 Zod Schemas - -#### 详细步骤 - -**Step 1.1: 创建 `src/lib/utils.ts`** - -```typescript -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} -``` - -**Step 1.2: 添加 shadcn/ui 基础组件** - -创建 `src/components/ui/button.tsx`: - -```typescript -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", - destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", - outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", - icon: "h-9 w-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean -} - -const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" - return ( - - ) - } -) -Button.displayName = "Button" - -export { Button, buttonVariants } -``` - -类似地创建: - -- `dialog.tsx` -- `input.tsx` -- `label.tsx` -- `form.tsx` -- `select.tsx` -- `switch.tsx` -- `tabs.tsx` -- `textarea.tsx` -- `sonner.tsx` - -**参考**: https://ui.shadcn.com/docs/components - -**Step 1.3: 创建 Query Client** - -`src/lib/query/queryClient.ts`: - -```typescript -import { QueryClient } from "@tanstack/react-query"; - -export const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: 1, - refetchOnWindowFocus: false, - staleTime: 1000 * 60 * 5, // 5 分钟 - }, - mutations: { - retry: false, - }, - }, -}); -``` - -**Step 1.4: 封装 API 层** - -`src/lib/api/providers.ts`: - -```typescript -import { invoke } from "@tauri-apps/api/core"; -import { Provider } from "@/types"; -import type { AppId } from "@/lib/api"; - -export const providersApi = { - getAll: async (appId: AppId): Promise> => { - return await invoke("get_providers", { app: appId }); - }, - - getCurrent: async (appId: AppId): Promise => { - return await invoke("get_current_provider", { app: appId }); - }, - - add: async (provider: Provider, appId: AppId): Promise => { - return await invoke("add_provider", { provider, app: appId }); - }, - - update: async (provider: Provider, appId: AppId): Promise => { - return await invoke("update_provider", { provider, app: appId }); - }, - - delete: async (id: string, appId: AppId): Promise => { - return await invoke("delete_provider", { id, app: appId }); - }, - - switch: async (id: string, appId: AppId): Promise => { - return await invoke("switch_provider", { id, app: appId }); - }, - - importDefault: async (appId: AppId): Promise => { - return await invoke("import_default_config", { app: appId }); - }, - - updateTrayMenu: async (): Promise => { - return await invoke("update_tray_menu"); - }, - - updateSortOrder: async ( - updates: Array<{ id: string; sortIndex: number }>, - appId: AppId - ): Promise => { - return await invoke("update_providers_sort_order", { updates, app: appId }); - }, -}; -``` - -类似地创建: - -- `src/lib/api/settings.ts` -- `src/lib/api/mcp.ts` -- `src/lib/api/index.ts` (聚合导出) - -**Step 1.5: 创建 Query Hooks** - -`src/lib/query/queries.ts`: - -```typescript -import { useQuery } from "@tanstack/react-query"; -import { providersApi, type AppId } from "@/lib/api"; -import { Provider } from "@/types"; - -// 排序辅助函数 -const sortProviders = ( - providers: Record -): Record => { - return Object.fromEntries( - Object.values(providers) - .sort((a, b) => { - const timeA = a.createdAt || 0; - const timeB = b.createdAt || 0; - if (timeA === 0 && timeB === 0) { - return a.name.localeCompare(b.name, "zh-CN"); - } - if (timeA === 0) return -1; - if (timeB === 0) return 1; - return timeA - timeB; - }) - .map((provider) => [provider.id, provider]) - ); -}; - -export const useProvidersQuery = (appType: AppId) => { - return useQuery({ - queryKey: ["providers", appType], - queryFn: async () => { - let providers: Record = {}; - let currentProviderId = ""; - - try { - providers = await providersApi.getAll(appType); - } catch (error) { - console.error("获取供应商列表失败:", error); - } - - try { - currentProviderId = await providersApi.getCurrent(appType); - } catch (error) { - console.error("获取当前供应商失败:", error); - } - - // 自动导入默认配置 - if (Object.keys(providers).length === 0) { - try { - const success = await providersApi.importDefault(appType); - if (success) { - providers = await providersApi.getAll(appType); - currentProviderId = await providersApi.getCurrent(appType); - } - } catch (error) { - console.error("导入默认配置失败:", error); - } - } - - return { providers: sortProviders(providers), currentProviderId }; - }, - }); -}; -``` - -`src/lib/query/mutations.ts`: - -```typescript -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { providersApi, type AppId } from "@/lib/api"; -import { Provider } from "@/types"; -import { toast } from "sonner"; -import { useTranslation } from "react-i18next"; - -export const useAddProviderMutation = (appType: AppId) => { - const queryClient = useQueryClient(); - const { t } = useTranslation(); - - return useMutation({ - mutationFn: async (provider: Omit) => { - const newProvider: Provider = { - ...provider, - id: crypto.randomUUID(), - createdAt: Date.now(), - }; - await providersApi.add(newProvider, appType); - return newProvider; - }, - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ["providers", appType] }); - await providersApi.updateTrayMenu(); - toast.success(t("notifications.providerAdded")); - }, - onError: (error: Error) => { - toast.error(t("notifications.addFailed", { error: error.message })); - }, - }); -}; - -export const useSwitchProviderMutation = (appType: AppId) => { - const queryClient = useQueryClient(); - const { t } = useTranslation(); - - return useMutation({ - mutationFn: async (providerId: string) => { - return await providersApi.switch(providerId, appType); - }, - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ["providers", appType] }); - await providersApi.updateTrayMenu(); - toast.success( - t("notifications.switchSuccess", { appName: t(`apps.${appType}`) }) - ); - }, - onError: (error: Error) => { - toast.error(t("notifications.switchFailed") + ": " + error.message); - }, - }); -}; - -// 类似地创建: useDeleteProviderMutation, useUpdateProviderMutation -``` - -**Step 1.6: 创建 Zod Schemas** - -`src/lib/schemas/provider.ts`: - -```typescript -import { z } from "zod"; - -export const providerSchema = z.object({ - name: z.string().min(1, "请填写供应商名称"), - websiteUrl: z.string().url("请输入有效的网址").optional().or(z.literal("")), - settingsConfig: z - .string() - .min(1, "请填写配置内容") - .refine( - (val) => { - try { - JSON.parse(val); - return true; - } catch { - return false; - } - }, - { message: "配置 JSON 格式错误" } - ), -}); - -export type ProviderFormData = z.infer; -``` - ---- - -### 阶段 2: 核心功能重构 (3-4天) - -**目标**: 重构 App.tsx 和供应商管理 - -#### 任务清单 - -- [x] 更新 `main.tsx` (添加 Providers) -- [x] 创建主题 Provider -- [x] 重构 `App.tsx` (412行 → ~100行) -- [x] 拆分 ProviderList (4个组件) -- [x] 创建 `useDragSort` Hook -- [x] 重构表单组件 (使用 react-hook-form) -- [x] 创建 AddProvider / EditProvider Dialog - -#### 详细步骤 - -**Step 2.1: 更新 `main.tsx`** - -```typescript -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App' -import { UpdateProvider } from './contexts/UpdateContext' -import './index.css' -import './i18n' -import { QueryClientProvider } from '@tanstack/react-query' -import { queryClient } from '@/lib/query' -import { ThemeProvider } from '@/components/theme-provider' -import { Toaster } from '@/components/ui/sonner' - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - - - - - - -) -``` - -**Step 2.2: 创建 `theme-provider.tsx`** - -```typescript -import { createContext, useContext, useEffect, useState } from 'react' - -type Theme = 'dark' | 'light' | 'system' - -type ThemeProviderProps = { - children: React.ReactNode - defaultTheme?: Theme - storageKey?: string -} - -type ThemeProviderState = { - theme: Theme - setTheme: (theme: Theme) => void -} - -const ThemeProviderContext = createContext({ - theme: 'system', - setTheme: () => null, -}) - -export function ThemeProvider({ - children, - defaultTheme = 'system', - storageKey = 'ui-theme', - ...props -}: ThemeProviderProps) { - const [theme, setTheme] = useState( - () => (localStorage.getItem(storageKey) as Theme) || defaultTheme - ) - - useEffect(() => { - const root = window.document.documentElement - root.classList.remove('light', 'dark') - - if (theme === 'system') { - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light' - root.classList.add(systemTheme) - return - } - - root.classList.add(theme) - }, [theme]) - - const value = { - theme, - setTheme: (theme: Theme) => { - localStorage.setItem(storageKey, theme) - setTheme(theme) - }, - } - - return ( - - {children} - - ) -} - -export const useTheme = () => { - const context = useContext(ThemeProviderContext) - if (context === undefined) { - throw new Error('useTheme must be used within a ThemeProvider') - } - return context -} -``` - -**Step 2.3: 重构 `App.tsx`** - -(参考前面的代码示例,从 412 行简化到 ~100 行) - -**Step 2.4-2.7: 拆分 ProviderList** - -(参考前面的组件拆分详细方案) - ---- - -### 阶段 3: 设置和辅助功能 (2-3天) - -**目标**: 重构设置模块和通知系统 - -#### 任务清单 - -- [x] 拆分 SettingsDialog (7个组件) -- [x] 创建 `useSettings` Hook -- [x] 创建 `useImportExport` Hook -- [x] 替换通知系统为 Sonner -- [x] 重构 ConfirmDialog - -#### 详细步骤 - -(参考前面的组件拆分详细方案) - ---- - -### 阶段 4: 清理和优化 (1-2天) - -**目标**: 清理旧代码,优化性能 - -#### 任务清单 - -- [x] 删除 `lib/styles.ts` -- [x] 删除旧的 Modal 组件 -- [x] 移除 `window.api` 全局绑定 -- [x] 清理无用的 state 和函数 -- [x] 更新类型定义 -- [x] 代码格式化 -- [x] TypeScript 检查 - ---- - -### 阶段 5: 测试和修复 (2-3天) - -**目标**: 全面测试,修复 Bug - -#### 功能测试清单 - -- [ ] 添加供应商 (Claude/Codex) -- [ ] 编辑供应商 -- [ ] 删除供应商 -- [ ] 切换供应商 -- [ ] 拖拽排序 -- [ ] 设置保存 -- [ ] 导入导出配置 -- [ ] 主题切换 -- [ ] MCP 管理 -- [ ] 用量查询 -- [ ] 托盘菜单同步 - -#### 边界情况测试 - -- [ ] 空供应商列表 -- [ ] 网络错误 -- [ ] 表单验证 -- [ ] 并发操作 -- [ ] 大量数据 (100+ 供应商) - ---- - -# 第四部分: 质量保障 - -## 🧪 测试策略 - -### 手动测试 - -每完成一个阶段后进行全量功能测试。 - -### 自动化测试 (可选) - -可以考虑添加: - -- Vitest 单元测试 (hooks, utils) -- Testing Library 组件测试 - ---- - -## 🚨 风险控制 - -### 潜在风险 - -1. **功能回归**: 重构可能引入 bug -2. **用户数据丢失**: 配置文件操作失败 -3. **性能下降**: 新架构可能影响性能 -4. **兼容性问题**: 依赖库平台兼容性 - -### 缓解措施 - -1. **逐步重构**: 按阶段进行,每阶段后测试 -2. **保留备份**: Git tag + 配置文件备份 -3. **Beta 测试**: 先发布 beta 版本 -4. **回滚方案**: 准备快速回滚机制 - ---- - -## ⏪ 回滚方案 - -### 如果需要回滚 - -```bash -# 方案 1: 回到重构前 -git reset --hard backup-before-refactor - -# 方案 2: 创建回滚分支 -git checkout -b rollback-refactor -git revert -``` - -### 用户数据保护 - -在重构前自动备份配置: - -```rust -// Rust 后端 -fn backup_config_before_refactor() -> Result<()> { - let config_path = get_app_config_path()?; - let backup_path = config_path.with_extension("backup.json"); - fs::copy(config_path, backup_path)?; - Ok(()) -} -``` - ---- - -## 🎯 成功标准 - -### 必须达成 (Must Have) - -- ✅ 所有现有功能正常工作 -- ✅ 无用户数据丢失 -- ✅ 性能不下降 -- ✅ TypeScript 检查通过 - -### 期望达成 (Should Have) - -- ✅ 代码量减少 40%+ -- ✅ 用户反馈积极 -- ✅ 开发体验提升明显 - -### 可选达成 (Nice to Have) - -- ⭕ 添加自动化测试 -- ⭕ 性能优化 20%+ - ---- - -## 📊 预期成果 - -### 代码质量 - -- **代码行数**: 减少 40-60% -- **文件数量**: UI 组件增加,但单文件更小 -- **可维护性**: 大幅提升 - -### 开发效率 - -- **新功能开发**: 提升 50%+ -- **Bug 修复**: 提升 30%+ -- **代码审查**: 提升 40%+ - -### 用户体验 - -- **界面一致性**: 统一的设计语言 -- **响应速度**: 更好的加载反馈 -- **错误提示**: 更友好的错误信息 - ---- - -## 📚 参考资料 - -- [TanStack Query 文档](https://tanstack.com/query/latest) -- [react-hook-form 文档](https://react-hook-form.com/) -- [shadcn/ui 文档](https://ui.shadcn.com/) -- [Zod 文档](https://zod.dev/) -- [原始 PR #76](https://github.com/farion1231/cc-switch/pull/76) - ---- - -## 📝 注意事项 - -1. **分支管理**: 在新分支进行,不要直接在 main 上修改 -2. **提交粒度**: 每完成一小步就提交,便于回滚 -3. **文档更新**: 同步更新 CLAUDE.md -4. **依赖锁定**: 锁定依赖版本 -5. **沟通协作**: 定期同步进度 - ---- - -**祝重构顺利! 🚀** diff --git a/docs/REFACTORING_REFERENCE.md b/docs/REFACTORING_REFERENCE.md deleted file mode 100644 index 9102d0f0b..000000000 --- a/docs/REFACTORING_REFERENCE.md +++ /dev/null @@ -1,834 +0,0 @@ -# 重构快速参考指南 - -> 常见模式和代码示例的速查表 - ---- - -## 📑 目录 - -1. [React Query 使用](#react-query-使用) -2. [react-hook-form 使用](#react-hook-form-使用) -3. [shadcn/ui 组件使用](#shadcnui-组件使用) -4. [代码迁移示例](#代码迁移示例) - ---- - -## React Query 使用 - -### 基础查询 - -```typescript -// 定义查询 Hook -export const useProvidersQuery = (appId: AppId) => { - return useQuery({ - queryKey: ['providers', appId], - queryFn: async () => { - const data = await providersApi.getAll(appId) - return data - }, - }) -} - -// 在组件中使用 -function MyComponent() { - const { data, isLoading, error } = useProvidersQuery('claude') - - if (isLoading) return
Loading...
- if (error) return
Error: {error.message}
- - return
{/* 使用 data */}
-} -``` - -### Mutation (变更操作) - -```typescript -// 定义 Mutation Hook -export const useAddProviderMutation = (appId: AppId) => { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async (provider: Provider) => { - return await providersApi.add(provider, appId) - }, - onSuccess: () => { - // 重新获取数据 - queryClient.invalidateQueries({ queryKey: ['providers', appId] }) - toast.success('添加成功') - }, - onError: (error: Error) => { - toast.error(`添加失败: ${error.message}`) - }, - }) -} - -// 在组件中使用 -function AddProviderDialog() { -const mutation = useAddProviderMutation('claude') - - const handleSubmit = (data: Provider) => { - mutation.mutate(data) - } - - return ( - - ) -} -``` - -### 乐观更新 - -```typescript -export const useSwitchProviderMutation = (appId: AppId) => { - const queryClient = useQueryClient() - - return useMutation({ - mutationFn: async (providerId: string) => { - return await providersApi.switch(providerId, appId) - }, - // 乐观更新: 在请求发送前立即更新 UI - onMutate: async (providerId) => { - // 取消正在进行的查询 - await queryClient.cancelQueries({ queryKey: ['providers', appId] }) - - // 保存当前数据(以便回滚) - const previousData = queryClient.getQueryData(['providers', appId]) - - // 乐观更新 - queryClient.setQueryData(['providers', appId], (old: any) => ({ - ...old, - currentProviderId: providerId, - })) - - return { previousData } - }, - // 如果失败,回滚 - onError: (err, providerId, context) => { - queryClient.setQueryData(['providers', appId], context?.previousData) - toast.error('切换失败') - }, - // 无论成功失败,都重新获取数据 - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ['providers', appId] }) - }, - }) -} -``` - -### 依赖查询 - -```typescript -// 第二个查询依赖第一个查询的结果 -const { data: providers } = useProvidersQuery(appId) -const currentProviderId = providers?.currentProviderId - -const { data: currentProvider } = useQuery({ - queryKey: ['provider', currentProviderId], - queryFn: () => providersApi.getById(currentProviderId!), - enabled: !!currentProviderId, // 只有当 ID 存在时才执行 -}) -``` - ---- - -## react-hook-form 使用 - -### 基础表单 - -```typescript -import { useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import { z } from 'zod' - -// 定义验证 schema -const schema = z.object({ - name: z.string().min(1, '请输入名称'), - email: z.string().email('邮箱格式不正确'), - age: z.number().min(18, '年龄必须大于18'), -}) - -type FormData = z.infer - -function MyForm() { - const form = useForm({ - resolver: zodResolver(schema), - defaultValues: { - name: '', - email: '', - age: 0, - }, - }) - - const onSubmit = (data: FormData) => { - console.log(data) - } - - return ( -
- - {form.formState.errors.name && ( - {form.formState.errors.name.message} - )} - - -
- ) -} -``` - -### 使用 shadcn/ui Form 组件 - -```typescript -import { useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form' -import { Input } from '@/components/ui/input' -import { Button } from '@/components/ui/button' - -function MyForm() { - const form = useForm({ - resolver: zodResolver(schema), - }) - - return ( -
- - ( - - 名称 - - - - - - )} - /> - - - - - ) -} -``` - -### 动态表单验证 - -```typescript -// 根据条件动态验证 -const schema = z.object({ - type: z.enum(['official', 'custom']), - apiKey: z.string().optional(), - baseUrl: z.string().optional(), -}).refine( - (data) => { - // 如果是自定义供应商,必须填写 baseUrl - if (data.type === 'custom') { - return !!data.baseUrl - } - return true - }, - { - message: '自定义供应商必须填写 Base URL', - path: ['baseUrl'], - } -) -``` - -### 手动触发验证 - -```typescript -function MyForm() { - const form = useForm() - - const handleBlur = async () => { - // 验证单个字段 - await form.trigger('name') - - // 验证多个字段 - await form.trigger(['name', 'email']) - - // 验证所有字段 - const isValid = await form.trigger() - } - - return
...
-} -``` - ---- - -## shadcn/ui 组件使用 - -### Dialog (对话框) - -```typescript -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter, -} from '@/components/ui/dialog' -import { Button } from '@/components/ui/button' - -function MyDialog() { - const [open, setOpen] = useState(false) - - return ( - - - - 标题 - 描述信息 - - - {/* 内容 */} -
对话框内容
- - - - - -
-
- ) -} -``` - -### Select (选择器) - -```typescript -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' - -function MySelect() { - const [value, setValue] = useState('') - - return ( - - ) -} -``` - -### Tabs (标签页) - -```typescript -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' - -function MyTabs() { - return ( - - - 标签1 - 标签2 - 标签3 - - - -
标签1的内容
-
- - -
标签2的内容
-
- - -
标签3的内容
-
-
- ) -} -``` - -### Toast 通知 (Sonner) - -```typescript -import { toast } from 'sonner' - -// 成功通知 -toast.success('操作成功') - -// 错误通知 -toast.error('操作失败') - -// 加载中 -const toastId = toast.loading('处理中...') -// 完成后更新 -toast.success('处理完成', { id: toastId }) -// 或 -toast.dismiss(toastId) - -// 自定义持续时间 -toast.success('消息', { duration: 5000 }) - -// 带操作按钮 -toast('确认删除?', { - action: { - label: '删除', - onClick: () => handleDelete(), - }, -}) -``` - ---- - -## 代码迁移示例 - -### 示例 1: 状态管理迁移 - -**旧代码** (手动状态管理): - -```typescript -const [providers, setProviders] = useState>({}) -const [currentProviderId, setCurrentProviderId] = useState('') -const [loading, setLoading] = useState(false) -const [error, setError] = useState(null) - -useEffect(() => { - const load = async () => { - setLoading(true) - setError(null) - try { - const data = await window.api.getProviders(appType) - const currentId = await window.api.getCurrentProvider(appType) - setProviders(data) - setCurrentProviderId(currentId) - } catch (err) { - setError(err as Error) - } finally { - setLoading(false) - } - } - load() -}, [appId]) -``` - -**新代码** (React Query): - -```typescript -const { data, isLoading, error } = useProvidersQuery(appId) -const providers = data?.providers || {} -const currentProviderId = data?.currentProviderId || '' -``` - -**减少**: 从 20+ 行到 3 行 - ---- - -### 示例 2: 表单验证迁移 - -**旧代码** (手动验证): - -```typescript -const [name, setName] = useState('') -const [nameError, setNameError] = useState('') -const [apiKey, setApiKey] = useState('') -const [apiKeyError, setApiKeyError] = useState('') - -const validate = () => { - let valid = true - - if (!name.trim()) { - setNameError('请输入名称') - valid = false - } else { - setNameError('') - } - - if (!apiKey.trim()) { - setApiKeyError('请输入 API Key') - valid = false - } else if (apiKey.length < 10) { - setApiKeyError('API Key 长度不足') - valid = false - } else { - setApiKeyError('') - } - - return valid -} - -const handleSubmit = () => { - if (validate()) { - // 提交 - } -} - -return ( -
- setName(e.target.value)} /> - {nameError && {nameError}} - - setApiKey(e.target.value)} /> - {apiKeyError && {apiKeyError}} - - -
-) -``` - -**新代码** (react-hook-form + zod): - -```typescript -const schema = z.object({ - name: z.string().min(1, '请输入名称'), - apiKey: z.string().min(10, 'API Key 长度不足'), -}) - -const form = useForm({ - resolver: zodResolver(schema), -}) - -return ( -
- - ( - - - - - - - )} - /> - - ( - - - - - - - )} - /> - - - - -) -``` - -**减少**: 从 40+ 行到 30 行,且更健壮 - ---- - -### 示例 3: 通知系统迁移 - -**旧代码** (自定义通知): - -```typescript -const [notification, setNotification] = useState<{ - message: string - type: 'success' | 'error' -} | null>(null) -const [isVisible, setIsVisible] = useState(false) - -const showNotification = (message: string, type: 'success' | 'error') => { - setNotification({ message, type }) - setIsVisible(true) - setTimeout(() => { - setIsVisible(false) - setTimeout(() => setNotification(null), 300) - }, 3000) -} - -return ( - <> - {notification && ( -
- {notification.message} -
- )} - {/* 其他内容 */} - -) -``` - -**新代码** (Sonner): - -```typescript -import { toast } from 'sonner' - -// 在需要的地方直接调用 -toast.success('操作成功') -toast.error('操作失败') - -// 在 main.tsx 中只需添加一次 -import { Toaster } from '@/components/ui/sonner' - - -``` - -**减少**: 从 20+ 行到 1 行调用 - ---- - -### 示例 4: 对话框迁移 - -**旧代码** (自定义 Modal): - -```typescript -const [isOpen, setIsOpen] = useState(false) - -return ( - <> - - - {isOpen && ( -
setIsOpen(false)}> -
e.stopPropagation()}> -
-

标题

- -
-
- {/* 内容 */} -
-
- - -
-
-
- )} - -) -``` - -**新代码** (shadcn/ui Dialog): - -```typescript -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' - -const [isOpen, setIsOpen] = useState(false) - -return ( - <> - - - - - - 标题 - - {/* 内容 */} - - - - - - - -) -``` - -**优势**: -- 无需自定义样式 -- 内置无障碍支持 -- 自动管理焦点和 ESC 键 - ---- - -### 示例 5: API 调用迁移 - -**旧代码** (window.api): - -```typescript -// 添加供应商 -const handleAdd = async (provider: Provider) => { - try { - await window.api.addProvider(provider, appType) - await loadProviders() - showNotification('添加成功', 'success') - } catch (error) { - showNotification('添加失败', 'error') - } -} -``` - -**新代码** (React Query Mutation): - -```typescript -// 在组件中 -const addMutation = useAddProviderMutation(appId) - -const handleAdd = (provider: Provider) => { - addMutation.mutate(provider) - // 成功和错误处理已在 mutation 定义中处理 -} -``` - -**优势**: -- 自动处理 loading 状态 -- 统一的错误处理 -- 自动刷新数据 -- 更少的样板代码 - ---- - -## 常见问题 - -### Q: 如何在 mutation 成功后关闭对话框? - -```typescript -const mutation = useAddProviderMutation(appId) - -const handleSubmit = (data: Provider) => { - mutation.mutate(data, { - onSuccess: () => { - setIsOpen(false) // 关闭对话框 - }, - }) -} -``` - -### Q: 如何在表单中使用异步验证? - -```typescript -const schema = z.object({ - name: z.string().refine( - async (name) => { - // 检查名称是否已存在 - const exists = await checkNameExists(name) - return !exists - }, - { message: '名称已存在' } - ), -}) -``` - -### Q: 如何手动刷新 Query 数据? - -```typescript -const queryClient = useQueryClient() - -// 方式1: 使缓存失效,触发重新获取 -queryClient.invalidateQueries({ queryKey: ['providers', appId] }) - -// 方式2: 直接刷新 -queryClient.refetchQueries({ queryKey: ['providers', appId] }) - -// 方式3: 更新缓存数据 -queryClient.setQueryData(['providers', appId], newData) -``` - -### Q: 如何在组件外部使用 toast? - -```typescript -// 直接导入并使用即可 -import { toast } from 'sonner' - -export const someUtil = () => { - toast.success('工具函数中的通知') -} -``` - ---- - -## 调试技巧 - -### React Query DevTools - -```typescript -// 在 main.tsx 中添加 -import { ReactQueryDevtools } from '@tanstack/react-query-devtools' - - - - - -``` - -### 查看表单状态 - -```typescript -const form = useForm() - -// 在开发模式下打印表单状态 -console.log('Form values:', form.watch()) -console.log('Form errors:', form.formState.errors) -console.log('Is valid:', form.formState.isValid) -``` - ---- - -## 性能优化建议 - -### 1. 避免不必要的重渲染 - -```typescript -// 使用 React.memo -export const ProviderCard = React.memo(({ provider, onEdit }: Props) => { - // ... -}) - -// 或使用 useMemo -const sortedProviders = useMemo( - () => Object.values(providers).sort(...), - [providers] -) -``` - -### 2. Query 配置优化 - -```typescript -const { data } = useQuery({ - queryKey: ['providers', appId], - queryFn: fetchProviders, - staleTime: 1000 * 60 * 5, // 5分钟内不重新获取 - gcTime: 1000 * 60 * 10, // 10分钟后清除缓存 -}) -``` - -### 3. 表单性能优化 - -```typescript -// 使用 mode 控制验证时机 -const form = useForm({ - mode: 'onBlur', // 失去焦点时验证 - // mode: 'onChange', // 每次输入都验证(较慢) - // mode: 'onSubmit', // 提交时验证(最快) -}) -``` - ---- - -**提示**: 将此文档保存在浏览器书签或编辑器中,方便随时查阅! diff --git a/docs/TEST_DEVELOPMENT_PLAN.md b/docs/TEST_DEVELOPMENT_PLAN.md deleted file mode 100644 index dc5d87039..000000000 --- a/docs/TEST_DEVELOPMENT_PLAN.md +++ /dev/null @@ -1,73 +0,0 @@ -# 前端测试开发计划 - -## 1. 背景与目标 -- **背景**:v3.5.0 起前端功能快速扩张(供应商管理、MCP、导入导出、端点测速、国际化),缺失系统化测试导致回归风险与人工验证成本攀升。 -- **目标**:在 3 个迭代内建立覆盖关键业务的自动化测试体系,形成稳定的手动冒烟流程,并将测试执行纳入 CI/CD。 - -## 2. 范围与优先级 -| 范围 | 内容 | 优先级 | -| --- | --- | --- | -| 供应商管理 | 列表、排序、预设/自定义表单、切换、复制、删除 | P0 | -| 配置导入导出 | JSON 校验、备份、进度反馈、失败回滚 | P0 | -| MCP 管理 | 列表、启停、模板、命令校验 | P1 | -| 设置面板 | 主题/语言切换、目录设置、关于、更新检查 | P1 | -| 端点速度测试 & 使用脚本 | 启动测试、状态指示、脚本保存 | P2 | -| 国际化 | 中英切换、缺省文案回退 | P2 | - -## 3. 测试分层策略 -- **单元测试(Vitest)**:纯函数与 Hook(`useProviderActions`、`useSettingsForm`、`useDragSort`、`useImportExport` 等)验证数据处理、错误分支、排序逻辑。 -- **组件测试(React Testing Library)**:关键组件(`ProviderList`、`AddProviderDialog`、`SettingsDialog`、`McpPanel`)模拟交互、校验、提示;结合 MSW 模拟 API。 -- **集成测试(App 级别)**:挂载 `App.tsx`,覆盖应用切换、编辑模式、导入导出回调、语言切换,验证状态同步与 toast 提示。 -- **端到端测试(Playwright)**:依赖 `pnpm dev:renderer`,串联供应商 CRUD、排序拖拽、MCP 启停、语言切换即时刷新、更新检查跳转。 -- **手动冒烟**:Tauri 桌面包 + dev server 双通道,验证托盘、系统权限、真实文件写入。 - -## 4. 环境与工具 -- 依赖:Node 18+、pnpm 8+、Vitest、React Testing Library、MSW、Playwright、Testing Library User Event、Playwright Trace Viewer。 -- 配置要点: - - 在 `tsconfig` 中共享别名,Vitest 配合 `vite.config.mts`。 - - `setupTests.ts` 统一注册 MSW/RTL、自定义 matcher。 - - Playwright 使用多浏览器矩阵(Chromium 必选,WebKit 可选),并共享 `.env.test`。 - - Mock `@tauri-apps/api` 与 `providersApi`/`settingsApi`,隔离 Rust 层。 - -## 5. 自动化建设里程碑 -| 周期 | 目标 | 交付 | -| --- | --- | --- | -| Sprint 1 | Vitest 基础设施、核心 Hook 单测(P0) | `pnpm test:unit`、覆盖率报告、10+ 用例 | -| Sprint 2 | 组件/集成测试、MSW Mock 层 | `pnpm test:component`、App 主流程用例 | -| Sprint 3 | Playwright E2E、CI 接入 | `pnpm test:e2e`、CI job、冒烟脚本 | -| 持续 | 回归用例补齐、视觉比对探索 | Playwright Trace、截图基线 | - -## 6. 用例规划概览 -- **供应商管理**:新增(预设+自定义)、编辑校验、复制排序、切换失败回退、删除确认、使用脚本保存。 -- **导入导出**:成功、重复导入、校验失败、备份失败提示、导入后托盘刷新。 -- **MCP**:模板应用、协议切换(stdio/http)、命令校验、启停状态持久化。 -- **设置**:主题/语言即时生效、目录路径更新、更新检查按钮外链、关于信息渲染。 -- **端点速度测试**:触发测试、loading/成功/失败状态、指示器颜色、测速数据排序。 -- **国际化**:默认中文、切换英文后主界面/对话框文案变化、缺失 key fallback。 - -## 7. 数据与 Mock 策略 -- 在 `tests/fixtures/` 维护标准供应商、MCP、设置数据集。 -- 使用 MSW 拦截 `providersApi`、`settingsApi`、`providersApi.onSwitched` 等调用;提供延迟/错误注入接口以覆盖异常分支。 -- Playwright 端提供临时用户目录(`TMP_CC_SWITCH_HOME`)+ 伪配置文件,以验证真实文件交互路径。 - -## 8. 质量门禁与指标 -- 覆盖率目标:单元 ≥75%,分支 ≥70%,逐步提升至 80%+。 -- CI 阶段:`pnpm typecheck` → `pnpm format:check` → `pnpm test:unit` → `pnpm test:component` → `pnpm test:e2e`(可在 nightly 执行)。 -- 缺陷处理:修复前补充最小复现测试;E2E 冒烟必须陪跑重大功能发布。 - -## 9. 工作流与职责 -- **测试负责人**:前端工程师轮值;负责测试计划维护、PR 流水线健康。 -- **开发者职责**:提交功能需附新增/更新测试、列出手动验证步骤、如涉及 UI 提交截图。 -- **Code Review 检查**:测试覆盖说明、mock 合理性、易读性。 - -## 10. 风险与缓解 -| 风险 | 影响 | 缓解 | -| --- | --- | --- | -| Tauri API Mock 难度高 | 单测无法稳定 | 抽象 API 适配层 + MSW 统一模拟 | -| Playwright 运行时间长 | CI 变慢 | 拆分冒烟/完整版,冒烟只跑关键路径 | -| 国际化文案频繁变化 | 用例脆弱 | 优先断言 data-testid/结构,文案使用翻译 key | - -## 11. 输出与维护 -- 文档维护者:前端团队;每个版本更新后检查测试覆盖清单。 -- 交付物:测试报告(CI artifact)、Playwright Trace、覆盖率摘要。 -- 复盘:每次发布后召开 30 分钟测试复盘,记录缺陷、补齐用例。 diff --git a/docs/opencode-implementation-plan.md b/docs/opencode-implementation-plan.md deleted file mode 100644 index 6aecc4a7b..000000000 --- a/docs/opencode-implementation-plan.md +++ /dev/null @@ -1,485 +0,0 @@ -# OpenCode 第四应用支持实现计划 - -> **范围说明**:本计划暂不包含统一供应商(UniversalProvider)对 OpenCode 的支持,以降低初期实现复杂度。 - -## 概述 - -为 CC Switch 添加 OpenCode 支持,这是第四个受管理的 CLI 应用。OpenCode 的核心差异在于采用**累加式**供应商管理(多供应商共存,应用内热切换),而非现有三应用的**替换式**管理。 - -## 关键设计决策 - -| 特性 | Claude/Codex/Gemini | OpenCode | -|------|---------------------|----------| -| 供应商模式 | 替换式(单一活跃) | 累加式(多供应商共存) | -| UI 按钮 | 启用/切换 | 添加/删除 | -| is_current | 需要 | 不需要 | -| 代理/故障转移 | 支持 | 不支持 | -| API 格式字段 | 无 | 需要(npm 包名) | -| 配置文件 | 各自独立 | `~/.config/opencode/opencode.json` | - -## 配置文件格式 - -### 供应商配置 -```json -{ - "$schema": "https://opencode.ai/config.json", - "provider": { - "provider-id": { - "npm": "@ai-sdk/openai-compatible", - "name": "Provider Name", - "options": { - "baseURL": "https://api.example.com/v1", - "apiKey": "{env:API_KEY}" - }, - "models": { - "model-id": { "name": "Model Name" } - } - } - } -} -``` - -### MCP 配置 -```json -{ - "mcp": { - "remote-server": { - "type": "remote", - "url": "https://example.com/mcp", - "enabled": true - }, - "local-server": { - "type": "local", - "command": ["npx", "-y", "my-mcp-command"], - "enabled": true, - "environment": { "KEY": "value" } - } - } -} -``` - ---- - -## 实现步骤 - -### Phase 1: 后端数据结构扩展 - -#### 1.1 AppType 枚举扩展 -**文件**: `src-tauri/src/app_config.rs` - -```rust -pub enum AppType { - Claude, - Codex, - Gemini, - OpenCode, // 新增 -} -``` - -#### 1.2 McpApps / SkillApps 扩展 -**文件**: `src-tauri/src/app_config.rs` - -```rust -pub struct McpApps { - pub claude: bool, - pub codex: bool, - pub gemini: bool, - pub opencode: bool, // 新增 -} - -pub struct SkillApps { - pub claude: bool, - pub codex: bool, - pub gemini: bool, - pub opencode: bool, // 新增 -} -``` - -#### 1.3 数据库 Schema 迁移 -**文件**: `src-tauri/src/database/schema.rs` - -- `SCHEMA_VERSION` 递增 -- 添加迁移: - ```sql - ALTER TABLE mcp_servers ADD COLUMN enabled_opencode BOOLEAN NOT NULL DEFAULT 0; - ALTER TABLE skills ADD COLUMN enabled_opencode BOOLEAN NOT NULL DEFAULT 0; - ``` - -### Phase 2: OpenCode 供应商数据结构 - -#### 2.1 OpenCode 专属配置结构 -**文件**: `src-tauri/src/provider.rs`(或新建 `opencode_provider.rs`) - -```rust -/// OpenCode 供应商的 settings_config 结构 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OpenCodeProviderConfig { - /// AI SDK 包名,如 "@ai-sdk/openai-compatible" - pub npm: String, - /// 供应商选项 - pub options: OpenCodeProviderOptions, - /// 模型定义 - pub models: HashMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OpenCodeProviderOptions { - #[serde(rename = "baseURL", skip_serializing_if = "Option::is_none")] - pub base_url: Option, - #[serde(rename = "apiKey", skip_serializing_if = "Option::is_none")] - pub api_key: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub headers: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OpenCodeModel { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OpenCodeModelLimit { - pub context: Option, - pub output: Option, -} -``` - -### Phase 3: OpenCode Live 配置读写 - -#### 3.1 新建 OpenCode 配置模块 -**文件**: `src-tauri/src/opencode_config.rs` - -核心功能: -- `get_opencode_config_path()` → `~/.config/opencode/opencode.json` -- `read_opencode_config()` → 读取整个配置文件 -- `write_opencode_config()` → 原子写入配置文件 -- `get_providers()` → 获取 `provider` 对象 -- `set_provider(id, config)` → 添加/更新供应商 -- `remove_provider(id)` → 删除供应商 -- `get_mcp_servers()` → 获取 `mcp` 对象 -- `set_mcp_server(id, config)` → 添加/更新 MCP 服务器 -- `remove_mcp_server(id)` → 删除 MCP 服务器 - -### Phase 4: MCP 同步模块 - -#### 4.1 新建 OpenCode MCP 同步 -**文件**: `src-tauri/src/mcp/opencode.rs` - -```rust -/// 同步所有 enabled_opencode=true 的服务器到 OpenCode 配置 -pub fn sync_enabled_to_opencode(config: &MultiAppConfig) -> Result<(), AppError> - -/// 同步单个服务器 -pub fn sync_single_server_to_opencode( - config: &MultiAppConfig, - id: &str, - server_spec: &Value -) -> Result<(), AppError> - -/// 从 OpenCode 配置移除服务器 -pub fn remove_server_from_opencode(id: &str) -> Result<(), AppError> - -/// 从 OpenCode 配置导入服务器 -pub fn import_from_opencode(config: &mut MultiAppConfig) -> Result -``` - -**格式转换**: -| CC Switch 统一格式 | OpenCode 格式 | -|-------------------|---------------| -| `type: "stdio"` | `type: "local"` | -| `command` + `args` | `command: [cmd, ...args]` | -| `env` | `environment` | -| `type: "sse"/"http"` | `type: "remote"` | -| `url` | `url` | - -### Phase 5: 供应商服务层 - -#### 5.1 OpenCode 供应商服务 -**文件**: `src-tauri/src/services/provider/opencode.rs` - -核心方法: -```rust -/// 获取所有 OpenCode 供应商 -pub fn list(state: &AppState) -> Result, AppError> - -/// 添加供应商(同时写入 live 配置) -pub fn add(state: &AppState, provider: Provider) -> Result - -/// 更新供应商 -pub fn update(state: &AppState, provider: Provider) -> Result - -/// 删除供应商(同时从 live 配置移除) -pub fn delete(state: &AppState, id: &str) -> Result<(), AppError> - -/// 从 live 配置导入供应商到数据库 -pub fn import_from_live(state: &AppState) -> Result -``` - -**关键差异**: -- 不需要 `switch()` 方法 -- 不需要 `is_current` 管理 -- `add()` 自动写入 live -- `delete()` 自动从 live 移除 - -### Phase 6: Tauri 命令扩展 - -#### 6.1 更新现有命令 -**文件**: `src-tauri/src/commands/providers.rs` - -- 所有命令支持 `app_type = "opencode"` -- OpenCode 特定逻辑分支 - -#### 6.2 新增 OpenCode 专属命令(如需要) -```rust -#[tauri::command] -pub async fn opencode_sync_all_providers(state: State<'_, AppState>) -> Result<(), AppError> -``` - -### Phase 7: 前端类型定义 - -#### 7.1 TypeScript 类型扩展 -**文件**: `src/types.ts` - -```typescript -// AppId 扩展 -type AppId = "claude" | "codex" | "gemini" | "opencode"; - -// OpenCode 专属配置 -interface OpenCodeProviderConfig { - npm: string; // AI SDK 包名 - options: { - baseURL?: string; - apiKey?: string; - headers?: Record; - }; - models: Record; -} - -interface OpenCodeModel { - name: string; - limit?: { - context?: number; - output?: number; - }; -} -``` - -#### 7.2 MCP 应用状态扩展 -**文件**: `src/types.ts` - -```typescript -interface McpApps { - claude: boolean; - codex: boolean; - gemini: boolean; - opencode: boolean; // 新增 -} -``` - -### Phase 8: 前端预设配置 - -#### 8.1 新建 OpenCode 供应商预设 -**文件**: `src/config/opencodeProviderPresets.ts` - -```typescript -export const opencodeProviderPresets: ProviderPreset[] = [ - { - name: "OpenAI", - npmPackage: "@ai-sdk/openai", - settingsConfig: { - npm: "@ai-sdk/openai", - options: { apiKey: "{env:OPENAI_API_KEY}" }, - models: { - "gpt-4o": { name: "GPT-4o" }, - "gpt-4o-mini": { name: "GPT-4o Mini" }, - }, - }, - theme: { icon: "openai", iconColor: "#00A67E" }, - }, - { - name: "Anthropic", - npmPackage: "@ai-sdk/anthropic", - settingsConfig: { - npm: "@ai-sdk/anthropic", - options: { apiKey: "{env:ANTHROPIC_API_KEY}" }, - models: { - "claude-sonnet-4-20250514": { name: "Claude Sonnet 4" }, - }, - }, - }, - { - name: "OpenAI Compatible", - npmPackage: "@ai-sdk/openai-compatible", - settingsConfig: { - npm: "@ai-sdk/openai-compatible", - options: { - baseURL: "", - apiKey: "{env:API_KEY}", - }, - models: {}, - }, - isCustomTemplate: true, - }, - // ... 更多预设 -]; - -// npm 包选项 -export const opencodeNpmPackages = [ - { value: "@ai-sdk/openai", label: "OpenAI" }, - { value: "@ai-sdk/anthropic", label: "Anthropic" }, - { value: "@ai-sdk/openai-compatible", label: "OpenAI Compatible" }, - { value: "@ai-sdk/google", label: "Google" }, - { value: "@ai-sdk/azure", label: "Azure OpenAI" }, - { value: "@ai-sdk/amazon-bedrock", label: "Amazon Bedrock" }, - // ... 更多选项 -]; -``` - -### Phase 9: 前端 UI 组件 - -#### 9.1 OpenCode 供应商表单 -**文件**: `src/components/providers/forms/OpenCodeFormFields.tsx` - -新增字段: -- npm 包选择器(下拉框 + 自定义输入) -- options 编辑器(baseURL, apiKey, headers) -- models 编辑器(动态添加/删除模型) - -#### 9.2 供应商卡片按钮适配 -**文件**: `src/components/providers/ProviderActions.tsx` - -```tsx -// OpenCode 使用不同的主按钮 -if (appId === "opencode") { - return ( - - ); -} -``` - -#### 9.3 隐藏 OpenCode 不需要的功能 - -在以下组件中检查 `appId !== "opencode"`: -- 代理设置面板 -- 故障转移队列 -- 供应商切换逻辑 - -### Phase 10: 国际化 - -#### 10.1 新增翻译 Key -**文件**: `src/locales/zh/translation.json` & `en/translation.json` - -```json -{ - "app.opencode": "OpenCode", - "provider.addToConfig": "添加到配置", - "provider.removeFromConfig": "从配置移除", - "provider.inConfig": "已添加", - "provider.npmPackage": "AI SDK 包", - "provider.models": "模型配置", - // ... -} -``` - ---- - -## 关键文件清单 - -### 后端(Rust) -| 操作 | 文件路径 | -|------|---------| -| 修改 | `src-tauri/src/app_config.rs` | -| 修改 | `src-tauri/src/database/schema.rs` | -| 修改 | `src-tauri/src/database/dao/mcp.rs` | -| 修改 | `src-tauri/src/database/dao/providers.rs` | -| 修改 | `src-tauri/src/services/provider/mod.rs` | -| 修改 | `src-tauri/src/services/mcp.rs` | -| 修改 | `src-tauri/src/commands/providers.rs` | -| 修改 | `src-tauri/src/commands/mcp.rs` | -| 修改 | `src-tauri/src/mcp/mod.rs` | -| 新建 | `src-tauri/src/opencode_config.rs` | -| 新建 | `src-tauri/src/mcp/opencode.rs` | -| 新建 | `src-tauri/src/services/provider/opencode.rs` | - -### 前端(TypeScript/React) -| 操作 | 文件路径 | -|------|---------| -| 修改 | `src/types.ts` | -| 修改 | `src/lib/api/types.ts` | -| 修改 | `src/lib/api/providers.ts` | -| 修改 | `src/components/providers/ProviderActions.tsx` | -| 修改 | `src/components/providers/ProviderCard.tsx` | -| 修改 | `src/components/providers/AddProviderDialog.tsx` | -| 修改 | `src/components/providers/forms/ProviderForm.tsx` | -| 修改 | `src/App.tsx` | -| 新建 | `src/config/opencodeProviderPresets.ts` | -| 新建 | `src/components/providers/forms/OpenCodeFormFields.tsx` | - -### 国际化 -| 操作 | 文件路径 | -|------|---------| -| 修改 | `src/locales/zh/translation.json` | -| 修改 | `src/locales/en/translation.json` | -| 修改 | `src/locales/ja/translation.json` | - ---- - -## 验证计划 - -### 单元测试 -1. OpenCode 配置读写测试 -2. MCP 格式转换测试(stdio ↔ local, sse ↔ remote) -3. 供应商 CRUD 操作测试 - -### 集成测试 -1. 添加 OpenCode 供应商 → 验证写入 `~/.config/opencode/opencode.json` -2. 删除供应商 → 验证从配置文件移除 -3. MCP 同步测试 → 验证格式正确转换 -4. 从 live 配置导入 → 验证正确解析 - -### 手动测试 -1. UI 流程:添加预设 → 编辑 → 删除 -2. 切换应用 Tab → OpenCode 显示正确的 UI(无代理/故障转移) -3. 托盘菜单正确显示 OpenCode 供应商 -4. 深链接导入 OpenCode 供应商 - ---- - -## 风险评估 - -1. **数据库迁移**:需要在升级时自动执行 `ALTER TABLE` 语句 -2. **配置文件冲突**:OpenCode 可能有自己的配置,需要合并而非覆盖 -3. **MCP 格式差异**:`stdio` → `local` 转换需要处理边界情况 -4. **UI 一致性**:OpenCode 的"添加/删除"模式需要与其他应用的"启用/切换"清晰区分 - ---- - -## 补充说明 - -### 托盘菜单特殊处理 - -由于 OpenCode 采用累加式管理,托盘菜单行为需要调整: - -- **现有三应用**:托盘菜单显示 `CheckMenuItem`(单选,切换当前供应商) -- **OpenCode**:显示当前所有启用的供应商(普通 MenuItem,无勾选逻辑),点击打开主界面 - -**修改文件**:`src-tauri/src/tray.rs`(`TRAY_SECTIONS` 常量) - -### 数据库约束更新 - -`proxy_config` 表的 CHECK 约束需要扩展: -```sql -CHECK (app_type IN ('claude','codex','gemini','opencode')) -``` - -### Settings 结构体扩展 - -**文件**:`src-tauri/src/settings.rs` - -需要添加: -- `current_provider_opencode: Option` - 对 OpenCode 可能无意义,但保持结构一致 -- `opencode_config_dir: Option` - 自定义配置目录 diff --git a/docs/release-note-v3.10.0-en.md b/docs/release-notes/v3.10.0-en.md similarity index 98% rename from docs/release-note-v3.10.0-en.md rename to docs/release-notes/v3.10.0-en.md index e7a2117b0..801c88f9a 100644 --- a/docs/release-note-v3.10.0-en.md +++ b/docs/release-notes/v3.10.0-en.md @@ -2,7 +2,7 @@ > OpenCode Support, Global Proxy, Claude Rectifier & Multi-App Experience Enhancements -**[中文版 →](release-note-v3.10.0-zh.md) | [日本語版 →](release-note-v3.10.0-ja.md)** +**[中文版 →](v3.10.0-zh.md) | [日本語版 →](v3.10.0-ja.md)** --- diff --git a/docs/release-note-v3.10.0-ja.md b/docs/release-notes/v3.10.0-ja.md similarity index 99% rename from docs/release-note-v3.10.0-ja.md rename to docs/release-notes/v3.10.0-ja.md index 081e17c36..c8c89feaf 100644 --- a/docs/release-note-v3.10.0-ja.md +++ b/docs/release-notes/v3.10.0-ja.md @@ -2,7 +2,7 @@ > OpenCode サポート、グローバルプロキシ、Claude Rectifier とマルチアプリ体験の強化 -**[中文版 →](release-note-v3.10.0-zh.md) | [English →](release-note-v3.10.0-en.md)** +**[中文版 →](v3.10.0-zh.md) | [English →](v3.10.0-en.md)** --- diff --git a/docs/release-note-v3.10.0-zh.md b/docs/release-notes/v3.10.0-zh.md similarity index 98% rename from docs/release-note-v3.10.0-zh.md rename to docs/release-notes/v3.10.0-zh.md index 4df403de8..259a8fbc3 100644 --- a/docs/release-note-v3.10.0-zh.md +++ b/docs/release-notes/v3.10.0-zh.md @@ -2,7 +2,7 @@ > OpenCode 支持、全局代理、Claude Rectifier 与多应用体验增强 -**[English →](release-note-v3.10.0-en.md) | [日本語版 →](release-note-v3.10.0-ja.md)** +**[English →](v3.10.0-en.md) | [日本語版 →](v3.10.0-ja.md)** --- diff --git a/docs/release-note-v3.11.0-en.md b/docs/release-notes/v3.11.0-en.md similarity index 99% rename from docs/release-note-v3.11.0-en.md rename to docs/release-notes/v3.11.0-en.md index 4567d81b5..371e6bcfb 100644 --- a/docs/release-note-v3.11.0-en.md +++ b/docs/release-notes/v3.11.0-en.md @@ -2,7 +2,7 @@ > OpenClaw Support, Session Manager, Backup Management & 50+ Improvements -**[中文版 →](release-note-v3.11.0-zh.md) | [日本語版 →](release-note-v3.11.0-ja.md)** +**[中文版 →](v3.11.0-zh.md) | [日本語版 →](v3.11.0-ja.md)** --- diff --git a/docs/release-note-v3.11.0-ja.md b/docs/release-notes/v3.11.0-ja.md similarity index 99% rename from docs/release-note-v3.11.0-ja.md rename to docs/release-notes/v3.11.0-ja.md index ce5d03729..bdd664539 100644 --- a/docs/release-note-v3.11.0-ja.md +++ b/docs/release-notes/v3.11.0-ja.md @@ -2,7 +2,7 @@ > OpenClaw サポート、セッションマネージャー、バックアップ管理と 50 以上の改善 -**[中文版 →](release-note-v3.11.0-zh.md) | [English →](release-note-v3.11.0-en.md)** +**[中文版 →](v3.11.0-zh.md) | [English →](v3.11.0-en.md)** --- diff --git a/docs/release-note-v3.11.0-zh.md b/docs/release-notes/v3.11.0-zh.md similarity index 99% rename from docs/release-note-v3.11.0-zh.md rename to docs/release-notes/v3.11.0-zh.md index 07041c4bb..629df7c7b 100644 --- a/docs/release-note-v3.11.0-zh.md +++ b/docs/release-notes/v3.11.0-zh.md @@ -2,7 +2,7 @@ > OpenClaw 支持、会话管理器、备份管理与 50+ 项改进 -**[English →](release-note-v3.11.0-en.md) | [日本語版 →](release-note-v3.11.0-ja.md)** +**[English →](v3.11.0-en.md) | [日本語版 →](v3.11.0-ja.md)** --- diff --git a/docs/release-note-v3.11.1-en.md b/docs/release-notes/v3.11.1-en.md similarity index 98% rename from docs/release-note-v3.11.1-en.md rename to docs/release-notes/v3.11.1-en.md index 48ff18156..66ecca9ee 100644 --- a/docs/release-note-v3.11.1-en.md +++ b/docs/release-notes/v3.11.1-en.md @@ -2,7 +2,7 @@ > Revert Partial Key-Field Merging, Restore Common Config Snippet & Bug Fixes -**[中文版 →](release-note-v3.11.1-zh.md) | [日本語版 →](release-note-v3.11.1-ja.md)** +**[中文版 →](v3.11.1-zh.md) | [日本語版 →](v3.11.1-ja.md)** --- diff --git a/docs/release-note-v3.11.1-ja.md b/docs/release-notes/v3.11.1-ja.md similarity index 98% rename from docs/release-note-v3.11.1-ja.md rename to docs/release-notes/v3.11.1-ja.md index 3d4aa862e..321663db4 100644 --- a/docs/release-note-v3.11.1-ja.md +++ b/docs/release-notes/v3.11.1-ja.md @@ -2,7 +2,7 @@ > 部分キーフィールドマージの撤回、共通設定スニペットの復元とバグ修正 -**[中文版 →](release-note-v3.11.1-zh.md) | [English →](release-note-v3.11.1-en.md)** +**[中文版 →](v3.11.1-zh.md) | [English →](v3.11.1-en.md)** --- diff --git a/docs/release-note-v3.11.1-zh.md b/docs/release-notes/v3.11.1-zh.md similarity index 98% rename from docs/release-note-v3.11.1-zh.md rename to docs/release-notes/v3.11.1-zh.md index 9c2dc1590..1df412104 100644 --- a/docs/release-note-v3.11.1-zh.md +++ b/docs/release-notes/v3.11.1-zh.md @@ -2,7 +2,7 @@ > 回退部分键值合并、恢复通用配置片段与多项修复 -**[English →](release-note-v3.11.1-en.md) | [日本語版 →](release-note-v3.11.1-ja.md)** +**[English →](v3.11.1-en.md) | [日本語版 →](v3.11.1-ja.md)** --- diff --git a/docs/release-note-v3.6.0-en.md b/docs/release-notes/v3.6.0-en.md similarity index 99% rename from docs/release-note-v3.6.0-en.md rename to docs/release-notes/v3.6.0-en.md index e2512d298..45f61c1c4 100644 --- a/docs/release-note-v3.6.0-en.md +++ b/docs/release-notes/v3.6.0-en.md @@ -1,6 +1,6 @@ ## Major architecture refactoring with enhanced config sync and data protection -**[中文更新说明 Chinese Documentation →](https://github.com/farion1231/cc-switch/blob/main/docs/release-note-v3.6.0-zh.md)** +**[中文更新说明 Chinese Documentation →](https://github.com/farion1231/cc-switch/blob/main/docs/release-notes/v3.6.0-zh.md)** --- diff --git a/docs/release-note-v3.6.0-zh.md b/docs/release-notes/v3.6.0-zh.md similarity index 99% rename from docs/release-note-v3.6.0-zh.md rename to docs/release-notes/v3.6.0-zh.md index e56b1b83a..32066a42a 100644 --- a/docs/release-note-v3.6.0-zh.md +++ b/docs/release-notes/v3.6.0-zh.md @@ -2,7 +2,7 @@ > 全栈架构重构,增强配置同步与数据保护 -**[English Version →](../release-note-v3.6.0.md)** +**[English Version →](v3.6.0-en.md)** --- diff --git a/docs/release-note-v3.6.1-en.md b/docs/release-notes/v3.6.1-en.md similarity index 99% rename from docs/release-note-v3.6.1-en.md rename to docs/release-notes/v3.6.1-en.md index a291f927b..0fdd2b913 100644 --- a/docs/release-note-v3.6.1-en.md +++ b/docs/release-notes/v3.6.1-en.md @@ -2,7 +2,7 @@ > Stability improvements and user experience optimization (based on v3.6.0) -**[中文更新说明 Chinese Documentation →](https://github.com/farion1231/cc-switch/blob/main/docs/release-note-v3.6.1-zh.md)** +**[中文更新说明 Chinese Documentation →](https://github.com/farion1231/cc-switch/blob/main/docs/release-notes/v3.6.1-zh.md)** --- diff --git a/docs/release-note-v3.6.1-zh.md b/docs/release-notes/v3.6.1-zh.md similarity index 99% rename from docs/release-note-v3.6.1-zh.md rename to docs/release-notes/v3.6.1-zh.md index 76943f455..bc803d69d 100644 --- a/docs/release-note-v3.6.1-zh.md +++ b/docs/release-notes/v3.6.1-zh.md @@ -2,7 +2,7 @@ > 稳定性提升与用户体验优化(基于 v3.6.0) -**[English Version →](../release-note-v3.6.1.md)** +**[English Version →](v3.6.1-en.md)** --- diff --git a/docs/release-note-v3.7.0-en.md b/docs/release-notes/v3.7.0-en.md similarity index 99% rename from docs/release-note-v3.7.0-en.md rename to docs/release-notes/v3.7.0-en.md index a115e4b74..1f4ca2a9f 100644 --- a/docs/release-note-v3.7.0-en.md +++ b/docs/release-notes/v3.7.0-en.md @@ -2,7 +2,7 @@ > From Provider Switcher to All-in-One AI CLI Management Platform -**[中文更新说明 Chinese Documentation →](release-note-v3.7.0-zh.md)** +**[中文更新说明 Chinese Documentation →](v3.7.0-zh.md)** --- diff --git a/docs/release-note-v3.7.0-zh.md b/docs/release-notes/v3.7.0-zh.md similarity index 99% rename from docs/release-note-v3.7.0-zh.md rename to docs/release-notes/v3.7.0-zh.md index 78e025c24..85ae6afaa 100644 --- a/docs/release-note-v3.7.0-zh.md +++ b/docs/release-notes/v3.7.0-zh.md @@ -2,7 +2,7 @@ > 从供应商切换器到 AI CLI 一体化管理平台 -**[English Version →](release-note-v3.7.0-en.md)** +**[English Version →](v3.7.0-en.md)** --- diff --git a/docs/release-note-v3.7.1-en.md b/docs/release-notes/v3.7.1-en.md similarity index 99% rename from docs/release-note-v3.7.1-en.md rename to docs/release-notes/v3.7.1-en.md index b549ecf18..69e893da5 100644 --- a/docs/release-note-v3.7.1-en.md +++ b/docs/release-notes/v3.7.1-en.md @@ -2,7 +2,7 @@ > Stability Enhancements and User Experience Improvements -**[中文更新说明 Chinese Documentation →](release-note-v3.7.1-zh.md)** +**[中文更新说明 Chinese Documentation →](v3.7.1-zh.md)** --- diff --git a/docs/release-note-v3.7.1-zh.md b/docs/release-notes/v3.7.1-zh.md similarity index 99% rename from docs/release-note-v3.7.1-zh.md rename to docs/release-notes/v3.7.1-zh.md index 67cc0ed6a..1ad8459d5 100644 --- a/docs/release-note-v3.7.1-zh.md +++ b/docs/release-notes/v3.7.1-zh.md @@ -2,7 +2,7 @@ > 稳定性增强与用户体验改进 -**[English Version →](release-note-v3.7.1-en.md)** +**[English Version →](v3.7.1-en.md)** --- diff --git a/docs/release-note-v3.8.0-en.md b/docs/release-notes/v3.8.0-en.md similarity index 99% rename from docs/release-note-v3.8.0-en.md rename to docs/release-notes/v3.8.0-en.md index dc605d2a4..13012d8e1 100644 --- a/docs/release-note-v3.8.0-en.md +++ b/docs/release-notes/v3.8.0-en.md @@ -2,7 +2,7 @@ > Persistence Architecture Upgrade, Laying the Foundation for Cloud Sync -**[中文版 →](release-note-v3.8.0-zh.md) | [日本語版 →](release-note-v3.8.0-ja.md)** +**[中文版 →](v3.8.0-zh.md) | [日本語版 →](v3.8.0-ja.md)** --- diff --git a/docs/release-note-v3.8.0-ja.md b/docs/release-notes/v3.8.0-ja.md similarity index 99% rename from docs/release-note-v3.8.0-ja.md rename to docs/release-notes/v3.8.0-ja.md index 09ae00c87..718245a69 100644 --- a/docs/release-note-v3.8.0-ja.md +++ b/docs/release-notes/v3.8.0-ja.md @@ -2,7 +2,7 @@ > 永続化アーキテクチャを刷新し、クラウド同期の土台を構築 -**[English →](release-note-v3.8.0-en.md) | [中文版 →](release-note-v3.8.0-zh.md)** +**[English →](v3.8.0-en.md) | [中文版 →](v3.8.0-zh.md)** --- diff --git a/docs/release-note-v3.8.0-zh.md b/docs/release-notes/v3.8.0-zh.md similarity index 99% rename from docs/release-note-v3.8.0-zh.md rename to docs/release-notes/v3.8.0-zh.md index 07a110d87..506451e40 100644 --- a/docs/release-note-v3.8.0-zh.md +++ b/docs/release-notes/v3.8.0-zh.md @@ -2,7 +2,7 @@ > 持久化架构升级,为云同步奠定基础 -**[English Version →](release-note-v3.8.0-en.md)** +**[English Version →](v3.8.0-en.md)** --- diff --git a/docs/release-note-v3.9.0-en.md b/docs/release-notes/v3.9.0-en.md similarity index 98% rename from docs/release-note-v3.9.0-en.md rename to docs/release-notes/v3.9.0-en.md index 1614f7b66..470c7e58a 100644 --- a/docs/release-note-v3.9.0-en.md +++ b/docs/release-notes/v3.9.0-en.md @@ -2,7 +2,7 @@ > Local API Proxy, Auto Failover, Universal Provider, and a more complete multi-app workflow -**[中文版 →](release-note-v3.9.0-zh.md) | [日本語版 →](release-note-v3.9.0-ja.md)** +**[中文版 →](v3.9.0-zh.md) | [日本語版 →](v3.9.0-ja.md)** --- diff --git a/docs/release-note-v3.9.0-ja.md b/docs/release-notes/v3.9.0-ja.md similarity index 99% rename from docs/release-note-v3.9.0-ja.md rename to docs/release-notes/v3.9.0-ja.md index 7fa1b9cf7..b87d8896c 100644 --- a/docs/release-note-v3.9.0-ja.md +++ b/docs/release-notes/v3.9.0-ja.md @@ -2,7 +2,7 @@ > ローカル API プロキシ、自動フェイルオーバー、Universal Provider、多アプリ対応の強化 -**[English →](release-note-v3.9.0-en.md) | [中文版 →](release-note-v3.9.0-zh.md)** +**[English →](v3.9.0-en.md) | [中文版 →](v3.9.0-zh.md)** --- diff --git a/docs/release-note-v3.9.0-zh.md b/docs/release-notes/v3.9.0-zh.md similarity index 98% rename from docs/release-note-v3.9.0-zh.md rename to docs/release-notes/v3.9.0-zh.md index 9d13fa05d..2f98c0114 100644 --- a/docs/release-note-v3.9.0-zh.md +++ b/docs/release-notes/v3.9.0-zh.md @@ -2,7 +2,7 @@ > 本地 API 代理、自动故障切换、统一供应商与多应用工作流增强 -**[English →](release-note-v3.9.0-en.md) | [日本語版 →](release-note-v3.9.0-ja.md)** +**[English →](v3.9.0-en.md) | [日本語版 →](v3.9.0-ja.md)** --- diff --git a/docs/roadmap.md b/docs/roadmap.md deleted file mode 100644 index ede2d515f..000000000 --- a/docs/roadmap.md +++ /dev/null @@ -1,10 +0,0 @@ -- 自动升级自定义路径 ✅ -- win 绿色版报毒问题 ✅ -- mcp 管理器 ✅ -- i18n ✅ -- gemini cli -- homebrew 支持 ✅ -- memory 管理 -- codex 更多预设供应商 -- 云同步 -- 本地代理 diff --git a/docs/v3.7.0-unified-mcp-refactor.md b/docs/v3.7.0-unified-mcp-refactor.md deleted file mode 100644 index 0e5b0f001..000000000 --- a/docs/v3.7.0-unified-mcp-refactor.md +++ /dev/null @@ -1,863 +0,0 @@ -# v3.7.0 统一 MCP 管理重构计划 - -## 📋 项目概述 - -**目标**:将原有的按应用分离的 MCP 管理(Claude/Codex/Gemini 各自独立管理)重构为统一管理面板,每个 MCP 服务器通过多选框控制应用到哪些客户端。 - -**版本**:v3.6.2 → v3.7.0 - -**开始时间**:2025-11-14 - ---- - -## 🎯 核心需求 - -### 原有架构(v3.6.x) - -``` -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ Claude面板 │ │ Codex面板 │ │ Gemini面板 │ -│ MCP管理 │ │ MCP管理 │ │ MCP管理 │ -└─────────────┘ └─────────────┘ └─────────────┘ - ↓ ↓ ↓ - mcp.claude mcp.codex mcp.gemini - {servers} {servers} {servers} -``` - -### 新架构(v3.7.0) - -``` -┌───────────────────────────────────────┐ -│ 统一 MCP 管理面板 │ -│ ┌────────┬────────┬────────┬────┐ │ -│ │ 服务器 │ Claude │ Codex │Gem │ │ -│ ├────────┼────────┼────────┼────┤ │ -│ │ mcp-1 │ ✓ │ ✓ │ │ │ -│ │ mcp-2 │ ✓ │ │ ✓ │ │ -│ └────────┴────────┴────────┴────┘ │ -└───────────────────────────────────────┘ - ↓ - mcp.servers - { - "mcp-1": { - apps: {claude: true, codex: true, gemini: false} - } - } -``` - ---- - -## 📐 技术架构 - -### 数据结构设计 - -#### 新增:McpApps(应用启用状态) - -```rust -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] -pub struct McpApps { - pub claude: bool, - pub codex: bool, - pub gemini: bool, -} -``` - -#### 更新:McpServer(统一服务器定义) - -```rust -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct McpServer { - pub id: String, - pub name: String, - pub server: serde_json::Value, // 连接配置(stdio/http) - pub apps: McpApps, // 新增:标记应用到哪些客户端 - pub description: Option, - pub homepage: Option, - pub docs: Option, - pub tags: Vec, -} -``` - -#### 更新:McpRoot(新旧结构并存) - -```rust -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct McpRoot { - // v3.7.0 新结构 - #[serde(skip_serializing_if = "Option::is_none")] - pub servers: Option>, - - // v3.6.x 旧结构(保留用于迁移) - #[serde(default, skip_serializing_if = "McpConfig::is_empty")] - pub claude: McpConfig, - #[serde(default, skip_serializing_if = "McpConfig::is_empty")] - pub codex: McpConfig, - #[serde(default, skip_serializing_if = "McpConfig::is_empty")] - pub gemini: McpConfig, -} -``` - -### 迁移策略 - -``` -旧配置 (v3.6.x) 新配置 (v3.7.0) -───────────────── ───────────────── -mcp: mcp: - claude: servers: - servers: mcp-fetch: - mcp-fetch: {...} → id: "mcp-fetch" - codex: server: {...} - servers: apps: - mcp-filesystem: {...} claude: true - codex: true - gemini: false -``` - -**迁移逻辑**: -1. 检测 `mcp.servers` 是否存在 -2. 若不存在,从 `mcp.claude/codex/gemini.servers` 收集所有服务器 -3. 合并同 id 服务器的 apps 字段 -4. 清空旧结构字段 -5. 保存配置(自动触发) - ---- - -## ✅ 开发进度 - -### Phase 1: 后端数据结构与迁移 ✅ 已完成 - -#### 1.1 修改数据结构(app_config.rs)✅ - -**文件**:`src-tauri/src/app_config.rs` - -**变更**: -- ✅ 新增 `McpApps` 结构体(lines 30-62) -- ✅ 新增 `McpServer` 结构体(lines 64-79) -- ✅ 更新 `McpRoot` 支持新旧结构(lines 81-96) -- ✅ 添加辅助方法:`is_enabled_for`, `set_enabled_for`, `enabled_apps` - -**提交**:`c7b235b` - "feat(mcp): implement unified MCP management for v3.7.0" - -#### 1.2 实现迁移逻辑 ✅ - -**文件**:`src-tauri/src/app_config.rs` - -**实现**: -- ✅ `migrate_mcp_to_unified()` 方法(lines 380-509) - - 从旧结构收集所有服务器 - - 按 id 合并重复服务器 - - 处理冲突(合并 apps 字段) - - 清空旧结构 -- ✅ 集成到 `MultiAppConfig::load()` 方法(lines 252-257) - - 自动检测并执行迁移 - - 迁移后保存配置 - -**提交**:`c7b235b` - "feat(mcp): implement unified MCP management for v3.7.0" - ---- - -### Phase 2: 后端服务层重构 ✅ 已完成 - -#### 2.1 重写 McpService ✅ - -**文件**:`src-tauri/src/services/mcp.rs` - -**新增方法**: -- ✅ `get_all_servers()` - 获取所有服务器(lines 13-27) -- ✅ `upsert_server()` - 添加/更新服务器(lines 30-52) -- ✅ `delete_server()` - 删除服务器(lines 55-75) -- ✅ `toggle_app()` - 切换应用启用状态(lines 78-111) -- ✅ `sync_all_enabled()` - 同步所有启用的服务器(lines 180-188) - -**兼容层方法**(已废弃): -- ✅ `get_servers()` - 按应用过滤服务器(lines 196-210) -- ✅ `set_enabled()` - 委托到 toggle_app(lines 213-222) -- ✅ `sync_enabled()` - 同步特定应用(lines 225-236) -- ✅ `import_from_claude/codex/gemini()` - 导入包装(lines 239-266) - -**提交**:`c7b235b` - "feat(mcp): implement unified MCP management for v3.7.0" - -#### 2.2 新增同步函数(mcp.rs)✅ - -**文件**:`src-tauri/src/mcp.rs` - -**新增函数**(lines 800-965): -- ✅ `json_server_to_toml_table()` - JSON → TOML 转换助手(lines 828-889) -- ✅ `sync_single_server_to_claude()` - 同步单个服务器到 Claude(lines 800-814) -- ✅ `remove_server_from_claude()` - 从 Claude 移除服务器(lines 817-826) -- ✅ `sync_single_server_to_codex()` - 同步单个服务器到 Codex(lines 891-936) -- ✅ `remove_server_from_codex()` - 从 Codex 移除服务器(lines 939-965) -- ✅ `sync_single_server_to_gemini()` - 同步单个服务器到 Gemini(lines 967-977) -- ✅ `remove_server_from_gemini()` - 从 Gemini 移除服务器(lines 980-989) - -**关键修复**: -- ✅ 修复 toml_edit 类型转换(使用手动构建而非 serde 转换) -- ✅ 修复 get_codex_config_path() 调用(返回 PathBuf 而非 Result) - -**提交**:`c7b235b` - "feat(mcp): implement unified MCP management for v3.7.0" -**修复提交**:`7ae2a9f` - "fix(mcp): resolve compilation errors and add backward compatibility" - -#### 2.3 新增 Tauri Commands ✅ - -**文件**:`src-tauri/src/commands/mcp.rs` - -**新增命令**(lines 147-196): -- ✅ `get_mcp_servers()` - 获取所有服务器(lines 154-159) -- ✅ `upsert_mcp_server()` - 添加/更新服务器(lines 162-168) -- ✅ `delete_mcp_server()` - 删除服务器(lines 171-177) -- ✅ `toggle_mcp_app()` - 切换应用状态(lines 180-189) -- ✅ `sync_all_mcp_servers()` - 同步所有服务器(lines 192-195) - -**更新旧命令**(兼容层): -- ✅ `upsert_mcp_server_in_config()` - 转换为统一结构(lines 68-131) -- ✅ `delete_mcp_server_in_config()` - 忽略 app 参数(lines 134-141) - -**提交**:`c7b235b` - "feat(mcp): implement unified MCP management for v3.7.0" -**修复提交**:`7ae2a9f` - "fix(mcp): resolve compilation errors and add backward compatibility" - -#### 2.4 注册新命令(lib.rs)✅ - -**文件**:`src-tauri/src/lib.rs` - -**变更**: -- ✅ 导出 `McpServer` 类型(line 21) -- ✅ 导出新增的 mcp 同步函数(lines 26-31) -- ✅ 注册 5 个新命令到 invoke_handler(lines 550-555) - -**提交**:`c7b235b` - "feat(mcp): implement unified MCP management for v3.7.0" - -#### 2.5 添加缺失的函数(claude_mcp.rs & gemini_mcp.rs)✅ - -**文件**: -- `src-tauri/src/claude_mcp.rs` (lines 234-253) -- `src-tauri/src/gemini_mcp.rs` (lines 160-179) - -**新增**: -- ✅ `read_mcp_servers_map()` - 读取现有 MCP 服务器映射 - -**提交**:`7ae2a9f` - "fix(mcp): resolve compilation errors and add backward compatibility" - -#### 2.6 编译验证 ✅ - -**状态**:✅ 编译成功 -- ⚠️ 16 个警告(8 个废弃警告 + 8 个未使用函数警告 - 预期内) -- ✅ 0 个错误 - -**提交**:`7ae2a9f` - "fix(mcp): resolve compilation errors and add backward compatibility" - ---- - -### Phase 3: 前端开发 ⚠️ 部分完成 - -#### 3.1 TypeScript 类型定义 ✅ - -**文件**:`src/types.ts` - -**变更**: -- ✅ 新增 `McpApps` 接口(lines 129-133) -- ✅ 更新 `McpServer` 接口(lines 136-149) - - 新增 `apps: McpApps` 字段 - - `name` 改为必填 - - 标记 `enabled` 为废弃 -- ✅ 新增 `McpServersMap` 类型别名(line 152) -- ✅ 保持向后兼容(保留 `enabled`, `source` 等旧字段) - -**提交**:`ac09551` - "feat(frontend): add unified MCP types and API layer for v3.7.0" - -#### 3.2 API 层更新 ✅ - -**文件**:`src/lib/api/mcp.ts` - -**新增方法**(lines 99-141): -- ✅ `getAllServers()` - 获取所有服务器(lines 106-108) -- ✅ `upsertUnifiedServer()` - 添加/更新服务器(lines 113-115) -- ✅ `deleteUnifiedServer()` - 删除服务器(lines 120-122) -- ✅ `toggleApp()` - 切换应用状态(lines 127-133) -- ✅ `syncAllServers()` - 同步所有服务器(lines 138-140) - -**导入更新**: -- ✅ 导入 `McpServersMap` 类型(line 6) - -**提交**:`ac09551` - "feat(frontend): add unified MCP types and API layer for v3.7.0" - -#### 3.3 React Query Hooks 📝 待开发 - -**计划文件**:`src/hooks/useMcp.ts` - -**需要实现的 Hooks**: - -```typescript -// 查询 hooks -export function useAllMcpServers() { - return useQuery({ - queryKey: ['mcp', 'all'], - queryFn: () => mcpApi.getAllServers(), - }); -} - -// 变更 hooks -export function useUpsertMcpServer() { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: (server: McpServer) => mcpApi.upsertUnifiedServer(server), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['mcp', 'all'] }); - }, - }); -} - -export function useToggleMcpApp() { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: ({ serverId, app, enabled }: { - serverId: string; - app: AppId; - enabled: boolean; - }) => mcpApi.toggleApp(serverId, app, enabled), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['mcp', 'all'] }); - }, - }); -} - -export function useDeleteMcpServer() { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: (id: string) => mcpApi.deleteUnifiedServer(id), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['mcp', 'all'] }); - }, - }); -} - -export function useSyncAllMcpServers() { - return useMutation({ - mutationFn: () => mcpApi.syncAllServers(), - }); -} -``` - -**依赖**: -- `@tanstack/react-query` (已安装) -- `src/lib/api/mcp.ts` (✅ 已完成) -- `src/types.ts` (✅ 已完成) - -#### 3.4 统一 MCP 面板组件 📝 待开发 - -**计划文件**:`src/components/mcp/UnifiedMcpPanel.tsx` - -**组件结构**: - -```typescript -interface UnifiedMcpPanelProps { - className?: string; -} - -export function UnifiedMcpPanel({ className }: UnifiedMcpPanelProps) { - const { t } = useTranslation(); - const { data: servers, isLoading } = useAllMcpServers(); - const toggleApp = useToggleMcpApp(); - const deleteServer = useDeleteMcpServer(); - const syncAll = useSyncAllMcpServers(); - - // 组件实现... -} -``` - -**UI 设计**: - -``` -┌─────────────────────────────────────────────────────┐ -│ MCP 服务器管理 ┌──────────┐ │ -│ │ 添加服务器 │ │ -│ ┌─────┐ ┌──────────────┐ ┌─────────┐ └──────────┘ │ -│ │ 搜索 │ │ 导入自...▼ │ │ 同步全部 │ │ -│ └─────┘ └──────────────┘ └─────────┘ │ -├─────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ 名称 │ Claude │ Codex │ Gemini │操作│ │ -│ ├─────────────────────────────────────────────┤ │ -│ │ mcp-fetch │ ✓ │ ✓ │ │ ⚙️ │ │ -│ │ filesystem │ ✓ │ │ ✓ │ ⚙️ │ │ -│ │ brave-search │ │ ✓ │ ✓ │ ⚙️ │ │ -│ └─────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────┘ -``` - -**功能特性**: -- 📋 服务器列表展示(名称、描述、标签) -- ☑️ 三个复选框控制应用启用状态(Claude/Codex/Gemini) -- ➕ 添加新服务器(表单模态框) -- ✏️ 编辑服务器(表单模态框) -- 🗑️ 删除服务器(确认对话框) -- 📥 导入功能(从 Claude/Codex/Gemini 导入) -- 🔄 同步全部(手动触发同步到 live 配置) -- 🔍 搜索过滤 -- 🏷️ 标签过滤 - -**子组件**: - -1. **McpServerTable** (`McpServerTable.tsx`) - - 服务器列表表格 - - 应用复选框 - - 操作按钮(编辑、删除) - -2. **McpServerFormModal** (`McpServerFormModal.tsx`) - - 添加/编辑表单 - - stdio/http 类型切换 - - 应用选择(多选) - - 元信息编辑(描述、标签、链接) - -3. **McpImportDialog** (`McpImportDialog.tsx`) - - 选择导入来源(Claude/Codex/Gemini) - - 服务器预览 - - 批量导入 - -**依赖组件**(来自 shadcn/ui): -- `Table`, `TableBody`, `TableCell`, `TableHead`, `TableHeader`, `TableRow` -- `Checkbox` -- `Button` -- `Dialog`, `DialogContent`, `DialogHeader`, `DialogTitle` -- `Input`, `Textarea`, `Label` -- `Select`, `SelectContent`, `SelectItem`, `SelectTrigger`, `SelectValue` -- `Badge` -- `Tooltip` - -#### 3.5 主界面集成 📝 待开发 - -**文件**:`src/App.tsx` - -**变更计划**: - -```typescript -// 原有代码(v3.6.x) -{currentApp === 'claude' && } -{currentApp === 'codex' && } -{currentApp === 'gemini' && } - -// 新代码(v3.7.0) - -``` - -**移除的组件**: -- `ClaudeMcpPanel.tsx` -- `CodexMcpPanel.tsx` -- `GeminiMcpPanel.tsx` - -**注意**:保留旧组件文件备份,以便回滚 - -#### 3.6 国际化文本更新 📝 待开发 - -**文件**: -- `src/locales/zh/translation.json` -- `src/locales/en/translation.json` - -**需要添加的翻译键**: - -```json -{ - "mcp": { - "unifiedPanel": { - "title": "MCP 服务器管理 / MCP Server Management", - "addServer": "添加服务器 / Add Server", - "editServer": "编辑服务器 / Edit Server", - "deleteServer": "删除服务器 / Delete Server", - "deleteConfirm": "确定要删除此服务器吗?/ Are you sure to delete this server?", - "syncAll": "同步全部 / Sync All", - "syncAllSuccess": "已同步所有启用的服务器 / All enabled servers synced", - "importFrom": "导入自... / Import from...", - "search": "搜索服务器... / Search servers...", - "noServers": "暂无服务器 / No servers yet", - "enabledApps": "启用的应用 / Enabled Apps", - "apps": { - "claude": "Claude", - "codex": "Codex", - "gemini": "Gemini" - }, - "form": { - "id": "服务器 ID / Server ID", - "name": "显示名称 / Display Name", - "type": "类型 / Type", - "stdio": "本地进程 / Local Process", - "http": "远程服务 / Remote Service", - "command": "命令 / Command", - "args": "参数 / Arguments", - "env": "环境变量 / Environment Variables", - "cwd": "工作目录 / Working Directory", - "url": "URL", - "headers": "请求头 / Headers", - "description": "描述 / Description", - "tags": "标签 / Tags", - "homepage": "主页 / Homepage", - "docs": "文档 / Documentation", - "selectApps": "选择应用 / Select Apps", - "selectAppsHint": "勾选此服务器要应用到哪些客户端 / Check which clients this server applies to" - }, - "table": { - "name": "名称 / Name", - "type": "类型 / Type", - "apps": "应用 / Apps", - "actions": "操作 / Actions", - "edit": "编辑 / Edit", - "delete": "删除 / Delete" - }, - "import": { - "title": "导入 MCP 服务器 / Import MCP Servers", - "fromClaude": "从 Claude 导入 / Import from Claude", - "fromCodex": "从 Codex 导入 / Import from Codex", - "fromGemini": "从 Gemini 导入 / Import from Gemini", - "success": "成功导入 {{count}} 个服务器 / Successfully imported {{count}} server(s)", - "noServersFound": "未找到可导入的服务器 / No servers found to import" - } - } - } -} -``` - ---- - -## 🔄 迁移流程 - -### 用户体验 - -``` -1. 用户升级到 v3.7.0 - ↓ -2. 首次启动应用 - ↓ -3. 后端自动执行迁移 - - 检测旧结构 (mcp.claude/codex/gemini.servers) - - 合并到统一结构 (mcp.servers) - - 保存迁移后的配置 - - 日志记录迁移详情 - ↓ -4. 前端加载新面板 - - 显示所有服务器 - - 三个复选框显示各应用启用状态 - ↓ -5. 用户无缝使用 -``` - -### 数据完整性保证 - -1. **迁移前验证**: - - ✅ 校验旧结构合法性 - - ✅ 记录迁移前状态 - -2. **迁移中处理**: - - ✅ 合并同 id 服务器的 apps 字段 - - ✅ 处理 id 冲突(保留第一个,记录警告) - - ✅ 保留所有元信息(描述、标签、链接) - -3. **迁移后清理**: - - ✅ 清空旧结构(claude/codex/gemini) - - ✅ 自动保存新配置 - - ✅ 日志记录迁移完成 - -4. **回滚机制**: - - 配置文件有备份(`config.v1.backup..json`) - - 迁移失败时可手动回滚 - ---- - -## 🧪 测试计划 - -### 后端测试 ✅ 已验证 - -- [x] 编译测试(cargo check) -- [x] 数据结构序列化/反序列化 -- [ ] 迁移逻辑单元测试 -- [ ] 服务层方法测试 -- [ ] 同步函数测试 - -### 前端测试 ⏳ 待进行 - -- [ ] TypeScript 类型检查 -- [ ] API 调用测试 -- [ ] 组件渲染测试 -- [ ] 用户交互测试 -- [ ] 国际化文本检查 - -### 集成测试 ⏳ 待进行 - -- [ ] 完整迁移流程测试 - - [ ] 从空配置启动 - - [ ] 从 v3.6.x 配置升级 - - [ ] 多服务器合并场景 - - [ ] 冲突处理验证 -- [ ] 多应用同步测试 - - [ ] 启用单个应用 - - [ ] 启用多个应用 - - [ ] 动态切换应用 - - [ ] 同步到 live 配置验证 -- [ ] 边界情况测试 - - [ ] 空服务器列表 - - [ ] 超长服务器名称 - - [ ] 特殊字符处理 - - [ ] 并发操作 - ---- - -## 📦 交付清单 - -### 代码文件 - -#### 后端(Rust)✅ 已完成 - -- [x] `src-tauri/src/app_config.rs` - 数据结构定义与迁移 -- [x] `src-tauri/src/services/mcp.rs` - 服务层重构 -- [x] `src-tauri/src/mcp.rs` - 同步函数实现 -- [x] `src-tauri/src/commands/mcp.rs` - Tauri 命令 -- [x] `src-tauri/src/lib.rs` - 命令注册 -- [x] `src-tauri/src/claude_mcp.rs` - Claude MCP 操作 -- [x] `src-tauri/src/gemini_mcp.rs` - Gemini MCP 操作 - -#### 前端(TypeScript/React)⚠️ 部分完成 - -- [x] `src/types.ts` - 类型定义更新 -- [x] `src/lib/api/mcp.ts` - API 层更新 -- [ ] `src/hooks/useMcp.ts` - React Query Hooks -- [ ] `src/components/mcp/UnifiedMcpPanel.tsx` - 统一面板组件 -- [ ] `src/components/mcp/McpServerTable.tsx` - 服务器表格 -- [ ] `src/components/mcp/McpServerFormModal.tsx` - 表单模态框 -- [ ] `src/components/mcp/McpImportDialog.tsx` - 导入对话框 -- [ ] `src/App.tsx` - 主界面集成 -- [ ] `src/locales/zh/translation.json` - 中文翻译 -- [ ] `src/locales/en/translation.json` - 英文翻译 - -### 文档 - -- [x] 本重构计划文档 (`docs/v3.7.0-unified-mcp-refactor.md`) -- [ ] 用户升级指南 (`docs/upgrade-to-v3.7.0.md`) -- [ ] API 变更说明 (`docs/api-changes-v3.7.0.md`) - -### Git 提交记录 ✅ - -- [x] `c7b235b` - feat(mcp): implement unified MCP management for v3.7.0 -- [x] `7ae2a9f` - fix(mcp): resolve compilation errors and add backward compatibility -- [x] `ac09551` - feat(frontend): add unified MCP types and API layer for v3.7.0 - ---- - -## 🎯 下一步行动 - -### 立即任务(优先级 P0) - -1. ⬜ **实现 useMcp Hook** - - 文件:`src/hooks/useMcp.ts` - - 估时:1-2 小时 - - 依赖:API 层(已完成) - -2. ⬜ **创建 UnifiedMcpPanel 核心组件** - - 文件:`src/components/mcp/UnifiedMcpPanel.tsx` - - 估时:3-4 小时 - - 依赖:useMcp Hook - -3. ⬜ **添加国际化文本** - - 文件:`src/locales/{zh,en}/translation.json` - - 估时:30 分钟 - -4. ⬜ **集成到主界面** - - 文件:`src/App.tsx` - - 估时:30 分钟 - - 依赖:UnifiedMcpPanel 组件 - -### 次要任务(优先级 P1) - -5. ⬜ **实现子组件** - - McpServerTable - - McpServerFormModal - - McpImportDialog - - 估时:4-6 小时 - -6. ⬜ **编写测试用例** - - 后端单元测试 - - 前端组件测试 - - 集成测试 - - 估时:6-8 小时 - -7. ⬜ **编写用户文档** - - 升级指南 - - API 变更说明 - - 估时:2-3 小时 - -### 优化任务(优先级 P2) - -8. ⬜ **性能优化** - - 服务器列表虚拟滚动 - - 批量操作优化 - - 估时:2-3 小时 - -9. ⬜ **用户体验增强** - - 添加加载状态 - - 添加错误提示 - - 添加操作确认 - - 估时:2-3 小时 - -10. ⬜ **代码清理** - - 移除旧的分应用面板组件 - - 清理废弃代码 - - 代码格式化 - - 估时:1-2 小时 - ---- - -## 💡 技术亮点 - -### 1. 平滑迁移机制 - -- ✅ 自动检测旧配置并迁移 -- ✅ 新旧结构并存(过渡期) -- ✅ 无需用户手动操作 -- ✅ 保留所有历史数据 - -### 2. 向后兼容 - -- ✅ 旧命令继续可用(带废弃警告) -- ✅ 前端可增量更新 -- ✅ 渐进式重构策略 - -### 3. 类型安全 - -- ✅ Rust 强类型保证数据完整性 -- ✅ TypeScript 类型定义与后端一致 -- ✅ serde 序列化/反序列化自动处理 - -### 4. 清晰的架构分层 - -``` -Frontend (React) - ↓ (Tauri IPC) -Commands Layer - ↓ -Services Layer - ↓ -Data Layer (Config + Live Sync) -``` - -### 5. SSOT 原则 - -- 单一配置源:`~/.cc-switch/config.json` -- 统一管理:`mcp.servers` 字段 -- 按需同步:写入各应用 live 配置 - ---- - -## 📚 参考资源 - -### 内部文档 - -- [项目 README](../README.md) -- [CLAUDE.md](../CLAUDE.md) - Claude Code 工作指南 -- [架构文档](../CLAUDE.md#架构概述) - -### 相关 Issues/PRs - -- 无(新功能开发) - -### 技术栈文档 - -- [Tauri 2.0](https://tauri.app/v1/guides/) -- [React 18](https://react.dev/) -- [TanStack Query](https://tanstack.com/query/latest) -- [shadcn/ui](https://ui.shadcn.com/) -- [serde](https://serde.rs/) - ---- - -## 📝 变更日志 - -### 2025-11-14 - -- ✅ 完成后端 Phase 1 & 2(数据结构、服务层、命令层) -- ✅ 修复所有编译错误 -- ✅ 完成前端类型定义和 API 层 -- ✅ 创建本重构计划文档 - -### 待更新... - ---- - -## 👥 团队协作 - -**开发者**:Claude Code (AI Assistant) + User - -**审查者**:User - -**测试者**:User - ---- - -## ⚠️ 风险与对策 - -### 风险 1:迁移数据丢失 - -**概率**:低 -**影响**:高 -**对策**: -- ✅ 迁移前自动备份配置 -- ✅ 详细日志记录 -- ✅ 测试各种边界情况 - -### 风险 2:性能问题(大量服务器) - -**概率**:中 -**影响**:中 -**对策**: -- ⬜ 实现虚拟滚动 -- ⬜ 分页或懒加载 -- ⬜ 性能测试 - -### 风险 3:兼容性问题 - -**概率**:中 -**影响**:中 -**对策**: -- ✅ 保留旧命令兼容层 -- ✅ 前端增量更新 -- ⬜ 多版本测试 - -### 风险 4:用户学习成本 - -**概率**:低 -**影响**:低 -**对策**: -- ⬜ 清晰的 UI 设计 -- ⬜ 详细的升级指南 -- ⬜ 操作提示和引导 - ---- - -## 🎉 预期收益 - -### 用户体验提升 - -- ⭐ **简化操作**:不再需要在不同应用面板切换 -- ⭐ **统一视图**:一目了然看到所有 MCP 配置 -- ⭐ **灵活配置**:轻松控制每个 MCP 应用到哪些客户端 - -### 代码质量提升 - -- ⭐ **架构优化**:统一数据源,消除冗余 -- ⭐ **维护性**:单一面板组件,代码更简洁 -- ⭐ **扩展性**:未来添加新应用(如 Cursor)更容易 - -### 性能提升 - -- ⭐ **减少重复加载**:统一管理减少配置文件读写 -- ⭐ **更快同步**:批量操作更高效 - ---- - -## 📞 联系方式 - -**问题反馈**:[GitHub Issues](https://github.com/jasonyoungyang/cc-switch/issues) - -**功能建议**:[GitHub Discussions](https://github.com/jasonyoungyang/cc-switch/discussions) - ---- - -**文档版本**:v1.0 -**最后更新**:2025-11-14 -**状态**:🟡 开发中(后端完成 ✅,前端进行中 ⚠️) diff --git a/src-tauri/src/deeplink/mod.rs b/src-tauri/src/deeplink/mod.rs index c2d849686..4bd3c8036 100644 --- a/src-tauri/src/deeplink/mod.rs +++ b/src-tauri/src/deeplink/mod.rs @@ -7,7 +7,6 @@ //! - Prompts //! - Skills //! -//! See docs/ccswitch-deeplink-design.md for detailed design. mod mcp; mod parser;