refactor(backend): phase 4 - extract provider service layer
Architecture improvements: - Extract ProviderService with switch/backfill/write methods - Reduce command layer from 160 to 13 lines via delegation - Separate business logic (services) from state management (commands) - Introduce precise error handling with structured validation Refactoring details: - Split Codex/Claude switching into symmetric private methods - Add multi-layer validation for Codex auth field (existence + type) - Extract import_config_from_path for command and test reuse - Expose export_config_to_file and ProviderService in public API Test coverage: - Add 10+ integration tests for Claude/Codex switching flows - Cover import/export success and failure scenarios (JSON parse, missing file) - Verify state consistency on error paths (current remains unchanged) - Test snapshot backfill for both old and new providers after switching
This commit is contained in:
@@ -10,6 +10,7 @@ use crate::codex_config;
|
||||
use crate::config::get_claude_settings_path;
|
||||
use crate::error::AppError;
|
||||
use crate::provider::{Provider, ProviderMeta};
|
||||
use crate::services::ProviderService;
|
||||
use crate::speedtest;
|
||||
use crate::store::AppState;
|
||||
|
||||
@@ -312,160 +313,15 @@ pub async fn delete_provider(
|
||||
|
||||
/// 切换供应商
|
||||
fn switch_provider_internal(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> {
|
||||
use serde_json::Value;
|
||||
|
||||
let mut config = state
|
||||
.config
|
||||
.lock()
|
||||
.map_err(|e| AppError::Message(format!("获取锁失败: {}", e)))?;
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
let provider = {
|
||||
let manager = config
|
||||
.get_manager_mut(&app_type)
|
||||
.ok_or_else(|| AppError::Message(format!("应用类型不存在: {:?}", app_type)))?;
|
||||
|
||||
manager
|
||||
.providers
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| AppError::Message(format!("供应商不存在: {}", id)))?
|
||||
};
|
||||
|
||||
match app_type {
|
||||
AppType::Codex => {
|
||||
if !{
|
||||
let cur = config
|
||||
.get_manager_mut(&app_type)
|
||||
.ok_or_else(|| AppError::Message(format!("应用类型不存在: {:?}", app_type)))?;
|
||||
cur.current.is_empty()
|
||||
} {
|
||||
let auth_path = codex_config::get_codex_auth_path();
|
||||
let config_path = codex_config::get_codex_config_path();
|
||||
if auth_path.exists() {
|
||||
let auth: Value = crate::config::read_json_file(&auth_path)?;
|
||||
let config_str = if config_path.exists() {
|
||||
std::fs::read_to_string(&config_path).map_err(|e| {
|
||||
AppError::Message(format!(
|
||||
"读取 config.toml 失败: {}: {}",
|
||||
config_path.display(),
|
||||
e
|
||||
))
|
||||
})?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let live = serde_json::json!({
|
||||
"auth": auth,
|
||||
"config": config_str,
|
||||
});
|
||||
|
||||
let cur_id2 = {
|
||||
let m = config
|
||||
.get_manager(&app_type)
|
||||
.ok_or_else(|| AppError::Message(format!("应用类型不存在: {:?}", app_type)))?;
|
||||
m.current.clone()
|
||||
};
|
||||
let m = config
|
||||
.get_manager_mut(&app_type)
|
||||
.ok_or_else(|| AppError::Message(format!("应用类型不存在: {:?}", app_type)))?;
|
||||
if let Some(cur) = m.providers.get_mut(&cur_id2) {
|
||||
cur.settings_config = live;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let auth = provider
|
||||
.settings_config
|
||||
.get("auth")
|
||||
.ok_or_else(|| AppError::Message("目标供应商缺少 auth 配置".to_string()))?;
|
||||
let cfg_text = provider
|
||||
.settings_config
|
||||
.get("config")
|
||||
.and_then(|v| v.as_str());
|
||||
crate::codex_config::write_codex_live_atomic(auth, cfg_text)?;
|
||||
}
|
||||
AppType::Claude => {
|
||||
use crate::config::{read_json_file, write_json_file};
|
||||
|
||||
let settings_path = get_claude_settings_path();
|
||||
|
||||
if settings_path.exists() {
|
||||
let cur_id = {
|
||||
let m = config
|
||||
.get_manager(&app_type)
|
||||
.ok_or_else(|| AppError::Message(format!("应用类型不存在: {:?}", app_type)))?;
|
||||
m.current.clone()
|
||||
};
|
||||
if !cur_id.is_empty() {
|
||||
if let Ok(live) = read_json_file::<serde_json::Value>(&settings_path) {
|
||||
let m = config
|
||||
.get_manager_mut(&app_type)
|
||||
.ok_or_else(|| AppError::Message(format!("应用类型不存在: {:?}", app_type)))?;
|
||||
if let Some(cur) = m.providers.get_mut(&cur_id) {
|
||||
cur.settings_config = live;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(parent) = settings_path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| AppError::Message(format!("创建目录失败: {}", e)))?;
|
||||
}
|
||||
|
||||
write_json_file(&settings_path, &provider.settings_config)?;
|
||||
|
||||
if settings_path.exists() {
|
||||
if let Ok(live_after) = read_json_file::<serde_json::Value>(&settings_path) {
|
||||
let m = config
|
||||
.get_manager_mut(&app_type)
|
||||
.ok_or_else(|| AppError::Message(format!("应用类型不存在: {:?}", app_type)))?;
|
||||
if let Some(target) = m.providers.get_mut(id) {
|
||||
target.settings_config = live_after;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let manager = config
|
||||
.get_manager_mut(&app_type)
|
||||
.ok_or_else(|| AppError::Message(format!("应用类型不存在: {:?}", app_type)))?;
|
||||
manager.current = id.to_string();
|
||||
}
|
||||
|
||||
if let AppType::Codex = app_type {
|
||||
crate::mcp::sync_enabled_to_codex(&config)?;
|
||||
|
||||
let cfg_text_after = crate::codex_config::read_and_validate_codex_config_text()?;
|
||||
|
||||
let cur_id = {
|
||||
let m = config
|
||||
.get_manager(&app_type)
|
||||
.ok_or_else(|| AppError::Message(format!("应用类型不存在: {:?}", app_type)))?;
|
||||
m.current.clone()
|
||||
};
|
||||
let m = config
|
||||
.get_manager_mut(&app_type)
|
||||
.ok_or_else(|| AppError::Message(format!("应用类型不存在: {:?}", app_type)))?;
|
||||
if let Some(p) = m.providers.get_mut(&cur_id) {
|
||||
if let Some(obj) = p.settings_config.as_object_mut() {
|
||||
obj.insert(
|
||||
"config".to_string(),
|
||||
serde_json::Value::String(cfg_text_after),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("成功切换到供应商");
|
||||
ProviderService::switch(&mut config, app_type, id)?;
|
||||
|
||||
drop(config);
|
||||
state.save()?;
|
||||
|
||||
Ok(())
|
||||
state.save()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::provider::Provider;
|
||||
use chrono::Utc;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
// 默认仅保留最近 10 份备份,避免目录无限膨胀
|
||||
const MAX_BACKUPS: usize = 10;
|
||||
@@ -223,38 +223,41 @@ pub async fn import_config_from_file(
|
||||
file_path: String,
|
||||
state: tauri::State<'_, crate::store::AppState>,
|
||||
) -> Result<Value, String> {
|
||||
// 读取导入的文件
|
||||
let file_path_ref = std::path::Path::new(&file_path);
|
||||
let import_content = fs::read_to_string(file_path_ref)
|
||||
.map_err(|e| AppError::io(file_path_ref, e).to_string())?;
|
||||
import_config_from_path(Path::new(&file_path), &state)
|
||||
.map_err(|e| e.to_string())
|
||||
.map(|backup_id| {
|
||||
json!({
|
||||
"success": true,
|
||||
"message": "Configuration imported successfully",
|
||||
"backupId": backup_id
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// 从文件导入配置的核心逻辑,供命令及测试复用。
|
||||
pub fn import_config_from_path(
|
||||
file_path: &Path,
|
||||
state: &crate::store::AppState,
|
||||
) -> Result<String, AppError> {
|
||||
let import_content =
|
||||
fs::read_to_string(file_path).map_err(|e| AppError::io(file_path, e))?;
|
||||
|
||||
// 验证并解析为配置对象
|
||||
let new_config: crate::app_config::MultiAppConfig =
|
||||
serde_json::from_str(&import_content)
|
||||
.map_err(|e| AppError::json(file_path_ref, e).to_string())?;
|
||||
.map_err(|e| AppError::json(file_path, e))?;
|
||||
|
||||
// 备份当前配置
|
||||
let config_path = crate::config::get_app_config_path();
|
||||
let backup_id = create_backup(&config_path).map_err(|e| e.to_string())?;
|
||||
let backup_id = create_backup(&config_path)?;
|
||||
|
||||
// 写入新配置到磁盘
|
||||
fs::write(&config_path, &import_content)
|
||||
.map_err(|e| AppError::io(&config_path, e).to_string())?;
|
||||
.map_err(|e| AppError::io(&config_path, e))?;
|
||||
|
||||
// 更新内存中的状态
|
||||
{
|
||||
let mut config_state = state
|
||||
.config
|
||||
.lock()
|
||||
.map_err(|e| AppError::from(e).to_string())?;
|
||||
*config_state = new_config;
|
||||
let mut guard = state.config.lock().map_err(AppError::from)?;
|
||||
*guard = new_config;
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
"success": true,
|
||||
"message": "Configuration imported successfully",
|
||||
"backupId": backup_id
|
||||
}))
|
||||
Ok(backup_id)
|
||||
}
|
||||
|
||||
/// 同步当前供应商配置到对应的 live 文件
|
||||
|
||||
@@ -13,17 +13,21 @@ mod provider;
|
||||
mod settings;
|
||||
mod speedtest;
|
||||
mod store;
|
||||
mod services;
|
||||
mod usage_script;
|
||||
|
||||
pub use app_config::{AppType, MultiAppConfig};
|
||||
pub use codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic};
|
||||
pub use config::{get_claude_mcp_path, get_claude_settings_path, read_json_file};
|
||||
pub use import_export::{create_backup, sync_current_providers_to_live};
|
||||
pub use import_export::{
|
||||
create_backup, export_config_to_file, import_config_from_path, sync_current_providers_to_live,
|
||||
};
|
||||
pub use provider::Provider;
|
||||
pub use settings::{update_settings, AppSettings};
|
||||
pub use mcp::{import_from_claude, import_from_codex, sync_enabled_to_claude, sync_enabled_to_codex};
|
||||
pub use error::AppError;
|
||||
pub use store::AppState;
|
||||
pub use services::ProviderService;
|
||||
pub use commands::*;
|
||||
|
||||
use tauri::{
|
||||
|
||||
3
src-tauri/src/services/mod.rs
Normal file
3
src-tauri/src/services/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod provider;
|
||||
|
||||
pub use provider::ProviderService;
|
||||
184
src-tauri/src/services/provider.rs
Normal file
184
src-tauri/src/services/provider.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::app_config::{AppType, MultiAppConfig};
|
||||
use crate::config::{get_claude_settings_path, read_json_file, write_json_file};
|
||||
use crate::codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic};
|
||||
use crate::error::AppError;
|
||||
use crate::mcp;
|
||||
|
||||
/// 供应商相关业务逻辑
|
||||
pub struct ProviderService;
|
||||
|
||||
impl ProviderService {
|
||||
/// 切换指定应用的供应商
|
||||
pub fn switch(
|
||||
config: &mut MultiAppConfig,
|
||||
app_type: AppType,
|
||||
provider_id: &str,
|
||||
) -> Result<(), AppError> {
|
||||
match app_type {
|
||||
AppType::Codex => Self::switch_codex(config, provider_id),
|
||||
AppType::Claude => Self::switch_claude(config, provider_id),
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_codex(config: &mut MultiAppConfig, provider_id: &str) -> Result<(), AppError> {
|
||||
let provider = config
|
||||
.get_manager(&AppType::Codex)
|
||||
.ok_or_else(|| AppError::Message("应用类型不存在: Codex".into()))?
|
||||
.providers
|
||||
.get(provider_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| AppError::ProviderNotFound(provider_id.to_string()))?;
|
||||
|
||||
Self::backfill_codex_current(config, provider_id)?;
|
||||
Self::write_codex_live(&provider)?;
|
||||
|
||||
if let Some(manager) = config.get_manager_mut(&AppType::Codex) {
|
||||
manager.current = provider_id.to_string();
|
||||
}
|
||||
|
||||
// 同步启用的 MCP 服务器
|
||||
mcp::sync_enabled_to_codex(config)?;
|
||||
|
||||
// 更新持久化快照
|
||||
let cfg_text_after = crate::codex_config::read_and_validate_codex_config_text()?;
|
||||
if let Some(manager) = config.get_manager_mut(&AppType::Codex) {
|
||||
if let Some(target) = manager.providers.get_mut(provider_id) {
|
||||
if let Some(obj) = target.settings_config.as_object_mut() {
|
||||
obj.insert(
|
||||
"config".to_string(),
|
||||
Value::String(cfg_text_after),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn backfill_codex_current(
|
||||
config: &mut MultiAppConfig,
|
||||
next_provider: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let current_id = config
|
||||
.get_manager(&AppType::Codex)
|
||||
.map(|m| m.current.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
if current_id.is_empty() || current_id == next_provider {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let auth_path = get_codex_auth_path();
|
||||
if !auth_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let auth: Value = read_json_file(&auth_path)?;
|
||||
let config_path = get_codex_config_path();
|
||||
let config_text = if config_path.exists() {
|
||||
std::fs::read_to_string(&config_path).map_err(|e| AppError::io(&config_path, e))?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let live = json!({
|
||||
"auth": auth,
|
||||
"config": config_text,
|
||||
});
|
||||
|
||||
if let Some(manager) = config.get_manager_mut(&AppType::Codex) {
|
||||
if let Some(current) = manager.providers.get_mut(¤t_id) {
|
||||
current.settings_config = live;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_codex_live(provider: &crate::provider::Provider) -> Result<(), AppError> {
|
||||
let settings = provider
|
||||
.settings_config
|
||||
.as_object()
|
||||
.ok_or_else(|| AppError::Config("Codex 配置必须是 JSON 对象".into()))?;
|
||||
let auth = settings.get("auth").ok_or_else(|| {
|
||||
AppError::Config(format!("供应商 {} 缺少 auth 配置", provider.id))
|
||||
})?;
|
||||
if !auth.is_object() {
|
||||
return Err(AppError::Config(format!(
|
||||
"供应商 {} 的 auth 必须是对象",
|
||||
provider.id
|
||||
)));
|
||||
}
|
||||
let cfg_text = settings.get("config").and_then(Value::as_str);
|
||||
|
||||
write_codex_live_atomic(auth, cfg_text)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn switch_claude(config: &mut MultiAppConfig, provider_id: &str) -> Result<(), AppError> {
|
||||
let provider = {
|
||||
let manager = config
|
||||
.get_manager(&AppType::Claude)
|
||||
.ok_or_else(|| AppError::Message("应用类型不存在: Claude".into()))?;
|
||||
manager
|
||||
.providers
|
||||
.get(provider_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| AppError::ProviderNotFound(provider_id.to_string()))?
|
||||
};
|
||||
|
||||
Self::backfill_claude_current(config, provider_id)?;
|
||||
Self::write_claude_live(&provider)?;
|
||||
|
||||
if let Some(manager) = config.get_manager_mut(&AppType::Claude) {
|
||||
manager.current = provider_id.to_string();
|
||||
|
||||
if let Some(target) = manager.providers.get_mut(provider_id) {
|
||||
let settings_path = get_claude_settings_path();
|
||||
let live_after = read_json_file::<Value>(&settings_path)?;
|
||||
target.settings_config = live_after;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn backfill_claude_current(
|
||||
config: &mut MultiAppConfig,
|
||||
next_provider: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let settings_path = get_claude_settings_path();
|
||||
if !settings_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let current_id = config
|
||||
.get_manager(&AppType::Claude)
|
||||
.map(|m| m.current.clone())
|
||||
.unwrap_or_default();
|
||||
if current_id.is_empty() || current_id == next_provider {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let live = read_json_file::<Value>(&settings_path)?;
|
||||
if let Some(manager) = config.get_manager_mut(&AppType::Claude) {
|
||||
if let Some(current) = manager.providers.get_mut(¤t_id) {
|
||||
current.settings_config = live;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_claude_live(provider: &crate::provider::Provider) -> Result<(), AppError> {
|
||||
let settings_path = get_claude_settings_path();
|
||||
if let Some(parent) = settings_path.parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
|
||||
}
|
||||
|
||||
write_json_file(&settings_path, &provider.settings_config)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user