refactor(backend): phase 2 - split commands.rs by domain (100%)

Split monolithic commands.rs (1525 lines) into 7 domain-focused modules
to improve maintainability and readability while preserving the external API.

## Changes

### Module Structure

Created `commands/` directory with domain-based organization:

- **provider.rs** (946 lines, 15 commands)
  - Provider CRUD operations (get, add, update, delete, switch)
  - Usage query integration
  - Endpoint speed testing and custom endpoint management
  - Sort order management
  - Largest file but highly cohesive (all provider-related)

- **mcp.rs** (235 lines, 13 commands)
  - Claude MCP management (~/.claude.json)
  - SSOT MCP config management (config.json)
  - Sync operations (Claude ↔ Codex)
  - Import/export functionality

- **config.rs** (153 lines, 8 commands)
  - Config path queries (Claude/Codex)
  - Directory operations (open, pick)
  - Config status checks
  - Parameter compatibility layer (app_type/app/appType)

- **settings.rs** (40 lines, 5 commands)
  - App settings management
  - App restart functionality
  - app_config_dir override (Store integration)

- **plugin.rs** (36 lines, 4 commands)
  - Claude plugin management (~/.claude/config.json)
  - Plugin status and config operations

- **misc.rs** (45 lines, 3 commands)
  - External link handling
  - Update checks
  - Portable mode detection

- **mod.rs** (15 lines)
  - Module exports via `pub use`
  - Preserves flat API structure

### API Preservation

- Used `pub use` pattern to maintain external API
- All commands still accessible as `commands::function_name`
- Zero breaking changes for frontend code
- lib.rs invoke_handler unchanged (48 commands registered)

## Statistics

- Files: 1 → 7 (modular organization)
- Lines: 1525 → 1470 (net -55 lines, -3.6%)
- Commands: 48 → 48 (all preserved)
- Average file size: 210 lines (excluding provider.rs)
- Compilation:  Success (6.92s, 0 warnings)
- Tests:  4/4 passed

## Benefits

- **Maintainability**: Easier to locate and modify domain-specific code
- **Readability**: Smaller files (~200 lines) vs monolithic 1500+ lines
- **Testability**: Can unit test individual modules in isolation
- **Scalability**: Clear pattern for adding new command groups
- **Zero Risk**: No API changes, all tests passing

## Design Decisions

1. **Domain-based split**: Organized by business domain (provider, mcp, config)
   rather than technical layers (crud, query, sync)

2. **Preserved provider.rs size**: Kept at 946 lines to maintain high cohesion
   (all provider-related operations together). Can be further split in Phase 2.1
   if needed.

3. **Parameter compatibility**: Retained multiple parameter names (app_type, app,
   appType) for backward compatibility with different frontend call styles

## Phase 2 Status:  100% Complete

Ready for Phase 3: Adding integration tests.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jason
2025-10-27 22:18:05 +08:00
parent 4aa9512e36
commit 9f5c2b427f
8 changed files with 552 additions and 597 deletions

View File

@@ -0,0 +1,235 @@
#![allow(non_snake_case)]
use std::collections::HashMap;
use serde::Serialize;
use tauri::State;
use crate::app_config::AppType;
use crate::claude_mcp;
use crate::mcp;
use crate::store::AppState;
/// 获取 Claude MCP 状态
#[tauri::command]
pub async fn get_claude_mcp_status() -> Result<claude_mcp::McpStatus, String> {
claude_mcp::get_mcp_status().map_err(|e| e.to_string())
}
/// 读取 mcp.json 文本内容
#[tauri::command]
pub async fn read_claude_mcp_config() -> Result<Option<String>, String> {
claude_mcp::read_mcp_json().map_err(|e| e.to_string())
}
/// 新增或更新一个 MCP 服务器条目
#[tauri::command]
pub async fn upsert_claude_mcp_server(id: String, spec: serde_json::Value) -> Result<bool, String> {
claude_mcp::upsert_mcp_server(&id, spec).map_err(|e| e.to_string())
}
/// 删除一个 MCP 服务器条目
#[tauri::command]
pub async fn delete_claude_mcp_server(id: String) -> Result<bool, String> {
claude_mcp::delete_mcp_server(&id).map_err(|e| e.to_string())
}
/// 校验命令是否在 PATH 中可用(不执行)
#[tauri::command]
pub async fn validate_mcp_command(cmd: String) -> Result<bool, String> {
claude_mcp::validate_command_in_path(&cmd).map_err(|e| e.to_string())
}
#[derive(Serialize)]
pub struct McpConfigResponse {
pub config_path: String,
pub servers: HashMap<String, serde_json::Value>,
}
/// 获取 MCP 配置(来自 ~/.cc-switch/config.json
#[tauri::command]
pub async fn get_mcp_config(
state: State<'_, AppState>,
app: Option<String>,
) -> Result<McpConfigResponse, String> {
let config_path = crate::config::get_app_config_path()
.to_string_lossy()
.to_string();
let mut cfg = state
.config
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
let app_ty = AppType::from(app.as_deref().unwrap_or("claude"));
let (servers, normalized) = mcp::get_servers_snapshot_for(&mut cfg, &app_ty);
let need_save = normalized > 0;
drop(cfg);
if need_save {
state.save()?;
}
Ok(McpConfigResponse {
config_path,
servers,
})
}
/// 在 config.json 中新增或更新一个 MCP 服务器定义
#[tauri::command]
pub async fn upsert_mcp_server_in_config(
state: State<'_, AppState>,
app: Option<String>,
id: String,
spec: serde_json::Value,
sync_other_side: Option<bool>,
) -> Result<bool, String> {
let mut cfg = state
.config
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
let app_ty = AppType::from(app.as_deref().unwrap_or("claude"));
let mut sync_targets: Vec<AppType> = Vec::new();
let changed = mcp::upsert_in_config_for(&mut cfg, &app_ty, &id, spec.clone())?;
let should_sync_current = cfg
.mcp_for(&app_ty)
.servers
.get(&id)
.and_then(|entry| entry.get("enabled"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
if should_sync_current {
sync_targets.push(app_ty.clone());
}
if sync_other_side.unwrap_or(false) {
match app_ty {
AppType::Claude => sync_targets.push(AppType::Codex),
AppType::Codex => sync_targets.push(AppType::Claude),
}
}
drop(cfg);
state.save()?;
let cfg2 = state
.config
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
for app_ty_to_sync in sync_targets {
match app_ty_to_sync {
AppType::Claude => mcp::sync_enabled_to_claude(&cfg2)?,
AppType::Codex => mcp::sync_enabled_to_codex(&cfg2)?,
};
}
Ok(changed)
}
/// 在 config.json 中删除一个 MCP 服务器定义
#[tauri::command]
pub async fn delete_mcp_server_in_config(
state: State<'_, AppState>,
app: Option<String>,
id: String,
) -> Result<bool, String> {
let mut cfg = state
.config
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
let app_ty = AppType::from(app.as_deref().unwrap_or("claude"));
let existed = mcp::delete_in_config_for(&mut cfg, &app_ty, &id)?;
drop(cfg);
state.save()?;
let cfg2 = state
.config
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
match app_ty {
AppType::Claude => mcp::sync_enabled_to_claude(&cfg2)?,
AppType::Codex => mcp::sync_enabled_to_codex(&cfg2)?,
}
Ok(existed)
}
/// 设置启用状态并同步到客户端配置
#[tauri::command]
pub async fn set_mcp_enabled(
state: State<'_, AppState>,
app: Option<String>,
id: String,
enabled: bool,
) -> Result<bool, String> {
let mut cfg = state
.config
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
let app_ty = AppType::from(app.as_deref().unwrap_or("claude"));
let changed = mcp::set_enabled_and_sync_for(&mut cfg, &app_ty, &id, enabled)?;
drop(cfg);
state.save()?;
Ok(changed)
}
/// 手动同步:将启用的 MCP 投影到 ~/.claude.json
#[tauri::command]
pub async fn sync_enabled_mcp_to_claude(state: State<'_, AppState>) -> Result<bool, String> {
let mut cfg = state
.config
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
let normalized = mcp::normalize_servers_for(&mut cfg, &AppType::Claude);
mcp::sync_enabled_to_claude(&cfg)?;
let need_save = normalized > 0;
drop(cfg);
if need_save {
state.save()?;
}
Ok(true)
}
/// 手动同步:将启用的 MCP 投影到 ~/.codex/config.toml
#[tauri::command]
pub async fn sync_enabled_mcp_to_codex(state: State<'_, AppState>) -> Result<bool, String> {
let mut cfg = state
.config
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
let normalized = mcp::normalize_servers_for(&mut cfg, &AppType::Codex);
mcp::sync_enabled_to_codex(&cfg)?;
let need_save = normalized > 0;
drop(cfg);
if need_save {
state.save()?;
}
Ok(true)
}
/// 从 ~/.claude.json 导入 MCP 定义到 config.json
#[tauri::command]
pub async fn import_mcp_from_claude(state: State<'_, AppState>) -> Result<usize, String> {
let mut cfg = state
.config
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
let changed = mcp::import_from_claude(&mut cfg)?;
drop(cfg);
if changed > 0 {
state.save()?;
}
Ok(changed)
}
/// 从 ~/.codex/config.toml 导入 MCP 定义到 config.json
#[tauri::command]
pub async fn import_mcp_from_codex(state: State<'_, AppState>) -> Result<usize, String> {
let mut cfg = state
.config
.lock()
.map_err(|e| format!("获取锁失败: {}", e))?;
let changed = mcp::import_from_codex(&mut cfg)?;
drop(cfg);
if changed > 0 {
state.save()?;
}
Ok(changed)
}