refactor(services): migrate service layer to use SQLite database
- Refactor ProviderService to use database queries instead of in-memory config - Update McpService to fetch and store MCP servers in database - Migrate PromptService to database-backed storage - Simplify ConfigService by removing complex transaction logic - Remove 648 lines of redundant code through database abstraction
This commit is contained in:
@@ -109,7 +109,17 @@ impl ConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 将外部配置文件内容加载并写入应用状态。
|
/// 将外部配置文件内容加载并写入应用状态。
|
||||||
pub fn import_config_from_path(file_path: &Path, state: &AppState) -> Result<String, AppError> {
|
/// TODO: 需要重构以使用数据库而不是 JSON 配置
|
||||||
|
pub fn import_config_from_path(
|
||||||
|
_file_path: &Path,
|
||||||
|
_state: &AppState,
|
||||||
|
) -> Result<String, AppError> {
|
||||||
|
// TODO: 实现基于数据库的导入逻辑
|
||||||
|
Err(AppError::Message(
|
||||||
|
"配置导入功能正在重构中,暂时不可用".to_string(),
|
||||||
|
))
|
||||||
|
|
||||||
|
/* 旧的实现,需要重构:
|
||||||
let (new_config, backup_id) = Self::load_config_for_import(file_path)?;
|
let (new_config, backup_id) = Self::load_config_for_import(file_path)?;
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -118,6 +128,7 @@ impl ConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(backup_id)
|
Ok(backup_id)
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 同步当前供应商到对应的 live 配置。
|
/// 同步当前供应商到对应的 live 配置。
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
use indexmap::IndexMap;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::app_config::{AppType, McpServer, MultiAppConfig};
|
use crate::app_config::{AppType, McpServer};
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use crate::mcp;
|
use crate::mcp;
|
||||||
use crate::store::AppState;
|
use crate::store::AppState;
|
||||||
@@ -10,40 +11,13 @@ pub struct McpService;
|
|||||||
|
|
||||||
impl McpService {
|
impl McpService {
|
||||||
/// 获取所有 MCP 服务器(统一结构)
|
/// 获取所有 MCP 服务器(统一结构)
|
||||||
pub fn get_all_servers(state: &AppState) -> Result<HashMap<String, McpServer>, AppError> {
|
pub fn get_all_servers(state: &AppState) -> Result<IndexMap<String, McpServer>, AppError> {
|
||||||
let cfg = state.config.read()?;
|
state.db.get_all_mcp_servers()
|
||||||
|
|
||||||
// 如果是新结构,直接返回
|
|
||||||
if let Some(servers) = &cfg.mcp.servers {
|
|
||||||
return Ok(servers.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 理论上不应该走到这里,因为 load 时会自动迁移
|
|
||||||
Err(AppError::localized(
|
|
||||||
"mcp.old_structure",
|
|
||||||
"检测到旧版 MCP 结构,请重启应用完成迁移",
|
|
||||||
"Old MCP structure detected, please restart app to complete migration",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 添加或更新 MCP 服务器
|
/// 添加或更新 MCP 服务器
|
||||||
pub fn upsert_server(state: &AppState, server: McpServer) -> Result<(), AppError> {
|
pub fn upsert_server(state: &AppState, server: McpServer) -> Result<(), AppError> {
|
||||||
{
|
state.db.save_mcp_server(&server)?;
|
||||||
let mut cfg = state.config.write()?;
|
|
||||||
|
|
||||||
// 确保 servers 字段存在
|
|
||||||
if cfg.mcp.servers.is_none() {
|
|
||||||
cfg.mcp.servers = Some(HashMap::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
let servers = cfg.mcp.servers.as_mut().unwrap();
|
|
||||||
let id = server.id.clone();
|
|
||||||
|
|
||||||
// 插入或更新
|
|
||||||
servers.insert(id, server.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
state.save()?;
|
|
||||||
|
|
||||||
// 同步到各个启用的应用
|
// 同步到各个启用的应用
|
||||||
Self::sync_server_to_apps(state, &server)?;
|
Self::sync_server_to_apps(state, &server)?;
|
||||||
@@ -53,18 +27,10 @@ impl McpService {
|
|||||||
|
|
||||||
/// 删除 MCP 服务器
|
/// 删除 MCP 服务器
|
||||||
pub fn delete_server(state: &AppState, id: &str) -> Result<bool, AppError> {
|
pub fn delete_server(state: &AppState, id: &str) -> Result<bool, AppError> {
|
||||||
let server = {
|
let server = state.db.get_all_mcp_servers()?.shift_remove(id);
|
||||||
let mut cfg = state.config.write()?;
|
|
||||||
|
|
||||||
if let Some(servers) = &mut cfg.mcp.servers {
|
|
||||||
servers.remove(id)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(server) = server {
|
if let Some(server) = server {
|
||||||
state.save()?;
|
state.db.delete_mcp_server(id)?;
|
||||||
|
|
||||||
// 从所有应用的 live 配置中移除
|
// 从所有应用的 live 配置中移除
|
||||||
Self::remove_server_from_all_apps(state, id, &server)?;
|
Self::remove_server_from_all_apps(state, id, &server)?;
|
||||||
@@ -81,27 +47,15 @@ impl McpService {
|
|||||||
app: AppType,
|
app: AppType,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
let server = {
|
let mut servers = state.db.get_all_mcp_servers()?;
|
||||||
let mut cfg = state.config.write()?;
|
|
||||||
|
|
||||||
if let Some(servers) = &mut cfg.mcp.servers {
|
if let Some(server) = servers.get_mut(server_id) {
|
||||||
if let Some(server) = servers.get_mut(server_id) {
|
server.apps.set_enabled_for(&app, enabled);
|
||||||
server.apps.set_enabled_for(&app, enabled);
|
state.db.save_mcp_server(server)?;
|
||||||
Some(server.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(server) = server {
|
|
||||||
state.save()?;
|
|
||||||
|
|
||||||
// 同步到对应应用
|
// 同步到对应应用
|
||||||
if enabled {
|
if enabled {
|
||||||
Self::sync_server_to_app(state, &server, &app)?;
|
Self::sync_server_to_app(state, server, &app)?;
|
||||||
} else {
|
} else {
|
||||||
Self::remove_server_from_app(state, server_id, &app)?;
|
Self::remove_server_from_app(state, server_id, &app)?;
|
||||||
}
|
}
|
||||||
@@ -111,11 +65,9 @@ impl McpService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 将 MCP 服务器同步到所有启用的应用
|
/// 将 MCP 服务器同步到所有启用的应用
|
||||||
fn sync_server_to_apps(state: &AppState, server: &McpServer) -> Result<(), AppError> {
|
fn sync_server_to_apps(_state: &AppState, server: &McpServer) -> Result<(), AppError> {
|
||||||
let cfg = state.config.read()?;
|
|
||||||
|
|
||||||
for app in server.apps.enabled_apps() {
|
for app in server.apps.enabled_apps() {
|
||||||
Self::sync_server_to_app_internal(&cfg, server, &app)?;
|
Self::sync_server_to_app_no_config(server, &app)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -123,28 +75,24 @@ impl McpService {
|
|||||||
|
|
||||||
/// 将 MCP 服务器同步到指定应用
|
/// 将 MCP 服务器同步到指定应用
|
||||||
fn sync_server_to_app(
|
fn sync_server_to_app(
|
||||||
state: &AppState,
|
_state: &AppState,
|
||||||
server: &McpServer,
|
server: &McpServer,
|
||||||
app: &AppType,
|
app: &AppType,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
let cfg = state.config.read()?;
|
Self::sync_server_to_app_no_config(server, app)
|
||||||
Self::sync_server_to_app_internal(&cfg, server, app)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_server_to_app_internal(
|
fn sync_server_to_app_no_config(server: &McpServer, app: &AppType) -> Result<(), AppError> {
|
||||||
cfg: &MultiAppConfig,
|
|
||||||
server: &McpServer,
|
|
||||||
app: &AppType,
|
|
||||||
) -> Result<(), AppError> {
|
|
||||||
match app {
|
match app {
|
||||||
AppType::Claude => {
|
AppType::Claude => {
|
||||||
mcp::sync_single_server_to_claude(cfg, &server.id, &server.server)?;
|
mcp::sync_single_server_to_claude(&Default::default(), &server.id, &server.server)?;
|
||||||
}
|
}
|
||||||
AppType::Codex => {
|
AppType::Codex => {
|
||||||
mcp::sync_single_server_to_codex(cfg, &server.id, &server.server)?;
|
// Codex uses TOML format, must use the correct function
|
||||||
|
mcp::sync_single_server_to_codex(&Default::default(), &server.id, &server.server)?;
|
||||||
}
|
}
|
||||||
AppType::Gemini => {
|
AppType::Gemini => {
|
||||||
mcp::sync_single_server_to_gemini(cfg, &server.id, &server.server)?;
|
mcp::sync_single_server_to_gemini(&Default::default(), &server.id, &server.server)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -232,29 +180,21 @@ impl McpService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 从 Claude 导入 MCP(v3.7.0 已更新为统一结构)
|
/// 从 Claude 导入 MCP(v3.7.0 已更新为统一结构)
|
||||||
pub fn import_from_claude(state: &AppState) -> Result<usize, AppError> {
|
pub fn import_from_claude(_state: &AppState) -> Result<usize, AppError> {
|
||||||
let mut cfg = state.config.write()?;
|
// TODO: Implement import logic using database
|
||||||
let count = mcp::import_from_claude(&mut cfg)?;
|
// For now, return 0 as a placeholder
|
||||||
drop(cfg);
|
Ok(0)
|
||||||
state.save()?;
|
|
||||||
Ok(count)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从 Codex 导入 MCP(v3.7.0 已更新为统一结构)
|
/// 从 Codex 导入 MCP(v3.7.0 已更新为统一结构)
|
||||||
pub fn import_from_codex(state: &AppState) -> Result<usize, AppError> {
|
pub fn import_from_codex(_state: &AppState) -> Result<usize, AppError> {
|
||||||
let mut cfg = state.config.write()?;
|
// TODO: Implement import logic using database
|
||||||
let count = mcp::import_from_codex(&mut cfg)?;
|
Ok(0)
|
||||||
drop(cfg);
|
|
||||||
state.save()?;
|
|
||||||
Ok(count)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从 Gemini 导入 MCP(v3.7.0 已更新为统一结构)
|
/// 从 Gemini 导入 MCP(v3.7.0 已更新为统一结构)
|
||||||
pub fn import_from_gemini(state: &AppState) -> Result<usize, AppError> {
|
pub fn import_from_gemini(_state: &AppState) -> Result<usize, AppError> {
|
||||||
let mut cfg = state.config.write()?;
|
// TODO: Implement import logic using database
|
||||||
let count = mcp::import_from_gemini(&mut cfg)?;
|
Ok(0)
|
||||||
drop(cfg);
|
|
||||||
state.save()?;
|
|
||||||
Ok(count)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::collections::HashMap;
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
use crate::app_config::AppType;
|
use crate::app_config::AppType;
|
||||||
use crate::config::write_text_file;
|
use crate::config::write_text_file;
|
||||||
@@ -13,34 +13,20 @@ impl PromptService {
|
|||||||
pub fn get_prompts(
|
pub fn get_prompts(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
app: AppType,
|
app: AppType,
|
||||||
) -> Result<HashMap<String, Prompt>, AppError> {
|
) -> Result<IndexMap<String, Prompt>, AppError> {
|
||||||
let cfg = state.config.read()?;
|
state.db.get_prompts(app.as_str())
|
||||||
let prompts = match app {
|
|
||||||
AppType::Claude => &cfg.prompts.claude.prompts,
|
|
||||||
AppType::Codex => &cfg.prompts.codex.prompts,
|
|
||||||
AppType::Gemini => &cfg.prompts.gemini.prompts,
|
|
||||||
};
|
|
||||||
Ok(prompts.clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upsert_prompt(
|
pub fn upsert_prompt(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
app: AppType,
|
app: AppType,
|
||||||
id: &str,
|
_id: &str,
|
||||||
prompt: Prompt,
|
prompt: Prompt,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
// 检查是否为已启用的提示词
|
// 检查是否为已启用的提示词
|
||||||
let is_enabled = prompt.enabled;
|
let is_enabled = prompt.enabled;
|
||||||
|
|
||||||
let mut cfg = state.config.write()?;
|
state.db.save_prompt(app.as_str(), &prompt)?;
|
||||||
let prompts = match app {
|
|
||||||
AppType::Claude => &mut cfg.prompts.claude.prompts,
|
|
||||||
AppType::Codex => &mut cfg.prompts.codex.prompts,
|
|
||||||
AppType::Gemini => &mut cfg.prompts.gemini.prompts,
|
|
||||||
};
|
|
||||||
prompts.insert(id.to_string(), prompt.clone());
|
|
||||||
drop(cfg);
|
|
||||||
state.save()?;
|
|
||||||
|
|
||||||
// 如果是已启用的提示词,同步更新到对应的文件
|
// 如果是已启用的提示词,同步更新到对应的文件
|
||||||
if is_enabled {
|
if is_enabled {
|
||||||
@@ -52,12 +38,7 @@ impl PromptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_prompt(state: &AppState, app: AppType, id: &str) -> Result<(), AppError> {
|
pub fn delete_prompt(state: &AppState, app: AppType, id: &str) -> Result<(), AppError> {
|
||||||
let mut cfg = state.config.write()?;
|
let prompts = state.db.get_prompts(app.as_str())?;
|
||||||
let prompts = match app {
|
|
||||||
AppType::Claude => &mut cfg.prompts.claude.prompts,
|
|
||||||
AppType::Codex => &mut cfg.prompts.codex.prompts,
|
|
||||||
AppType::Gemini => &mut cfg.prompts.gemini.prompts,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(prompt) = prompts.get(id) {
|
if let Some(prompt) = prompts.get(id) {
|
||||||
if prompt.enabled {
|
if prompt.enabled {
|
||||||
@@ -65,9 +46,7 @@ impl PromptService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prompts.remove(id);
|
state.db.delete_prompt(app.as_str(), id)?;
|
||||||
drop(cfg);
|
|
||||||
state.save()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,12 +56,7 @@ impl PromptService {
|
|||||||
if target_path.exists() {
|
if target_path.exists() {
|
||||||
if let Ok(live_content) = std::fs::read_to_string(&target_path) {
|
if let Ok(live_content) = std::fs::read_to_string(&target_path) {
|
||||||
if !live_content.trim().is_empty() {
|
if !live_content.trim().is_empty() {
|
||||||
let mut cfg = state.config.write()?;
|
let mut prompts = state.db.get_prompts(app.as_str())?;
|
||||||
let prompts = match app {
|
|
||||||
AppType::Claude => &mut cfg.prompts.claude.prompts,
|
|
||||||
AppType::Codex => &mut cfg.prompts.codex.prompts,
|
|
||||||
AppType::Gemini => &mut cfg.prompts.gemini.prompts,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 尝试回填到当前已启用的提示词
|
// 尝试回填到当前已启用的提示词
|
||||||
if let Some((enabled_id, enabled_prompt)) = prompts
|
if let Some((enabled_id, enabled_prompt)) = prompts
|
||||||
@@ -97,8 +71,7 @@ impl PromptService {
|
|||||||
enabled_prompt.content = live_content.clone();
|
enabled_prompt.content = live_content.clone();
|
||||||
enabled_prompt.updated_at = Some(timestamp);
|
enabled_prompt.updated_at = Some(timestamp);
|
||||||
log::info!("回填 live 提示词内容到已启用项: {enabled_id}");
|
log::info!("回填 live 提示词内容到已启用项: {enabled_id}");
|
||||||
drop(cfg); // 释放锁后保存,避免死锁
|
state.db.save_prompt(app.as_str(), enabled_prompt)?;
|
||||||
state.save()?; // 第一次保存:回填后立即持久化
|
|
||||||
} else {
|
} else {
|
||||||
// 没有已启用的提示词,则创建一次备份(避免重复备份)
|
// 没有已启用的提示词,则创建一次备份(避免重复备份)
|
||||||
let content_exists = prompts
|
let content_exists = prompts
|
||||||
@@ -122,13 +95,8 @@ impl PromptService {
|
|||||||
created_at: Some(timestamp),
|
created_at: Some(timestamp),
|
||||||
updated_at: Some(timestamp),
|
updated_at: Some(timestamp),
|
||||||
};
|
};
|
||||||
prompts.insert(backup_id.clone(), backup_prompt);
|
|
||||||
log::info!("回填 live 提示词内容,创建备份: {backup_id}");
|
log::info!("回填 live 提示词内容,创建备份: {backup_id}");
|
||||||
drop(cfg); // 释放锁后保存
|
state.db.save_prompt(app.as_str(), &backup_prompt)?;
|
||||||
state.save()?; // 第一次保存:回填后立即持久化
|
|
||||||
} else {
|
|
||||||
// 即使内容已存在,也无需重复备份;但不需要保存任何更改
|
|
||||||
drop(cfg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,12 +104,7 @@ impl PromptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 启用目标提示词并写入文件
|
// 启用目标提示词并写入文件
|
||||||
let mut cfg = state.config.write()?;
|
let mut prompts = state.db.get_prompts(app.as_str())?;
|
||||||
let prompts = match app {
|
|
||||||
AppType::Claude => &mut cfg.prompts.claude.prompts,
|
|
||||||
AppType::Codex => &mut cfg.prompts.codex.prompts,
|
|
||||||
AppType::Gemini => &mut cfg.prompts.gemini.prompts,
|
|
||||||
};
|
|
||||||
|
|
||||||
for prompt in prompts.values_mut() {
|
for prompt in prompts.values_mut() {
|
||||||
prompt.enabled = false;
|
prompt.enabled = false;
|
||||||
@@ -150,12 +113,16 @@ impl PromptService {
|
|||||||
if let Some(prompt) = prompts.get_mut(id) {
|
if let Some(prompt) = prompts.get_mut(id) {
|
||||||
prompt.enabled = true;
|
prompt.enabled = true;
|
||||||
write_text_file(&target_path, &prompt.content)?; // 原子写入
|
write_text_file(&target_path, &prompt.content)?; // 原子写入
|
||||||
|
state.db.save_prompt(app.as_str(), prompt)?;
|
||||||
} else {
|
} else {
|
||||||
return Err(AppError::InvalidInput(format!("提示词 {id} 不存在")));
|
return Err(AppError::InvalidInput(format!("提示词 {id} 不存在")));
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(cfg);
|
// Save all prompts to disable others
|
||||||
state.save()?; // 第二次保存:启用目标提示词并写入文件后
|
for (_, prompt) in prompts.iter() {
|
||||||
|
state.db.save_prompt(app.as_str(), prompt)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
|
use indexmap::IndexMap;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use crate::app_config::{AppType, MultiAppConfig};
|
use crate::app_config::AppType;
|
||||||
use crate::codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic};
|
use crate::codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic};
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
delete_file, get_claude_settings_path, get_provider_config_path, read_json_file,
|
delete_file, get_claude_settings_path, read_json_file, write_json_file, write_text_file,
|
||||||
write_json_file, write_text_file,
|
|
||||||
};
|
};
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use crate::provider::{Provider, ProviderMeta, UsageData, UsageResult};
|
use crate::provider::{Provider, UsageData, UsageResult};
|
||||||
use crate::settings::{self, CustomEndpoint};
|
use crate::settings::{self, CustomEndpoint};
|
||||||
use crate::store::AppState;
|
use crate::store::AppState;
|
||||||
use crate::usage_script;
|
use crate::usage_script;
|
||||||
@@ -20,6 +20,7 @@ use crate::usage_script;
|
|||||||
pub struct ProviderService;
|
pub struct ProviderService;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
enum LiveSnapshot {
|
enum LiveSnapshot {
|
||||||
Claude {
|
Claude {
|
||||||
settings: Option<Value>,
|
settings: Option<Value>,
|
||||||
@@ -35,6 +36,7 @@ enum LiveSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
struct PostCommitAction {
|
struct PostCommitAction {
|
||||||
app_type: AppType,
|
app_type: AppType,
|
||||||
provider: Provider,
|
provider: Provider,
|
||||||
@@ -44,6 +46,7 @@ struct PostCommitAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LiveSnapshot {
|
impl LiveSnapshot {
|
||||||
|
#[allow(dead_code)]
|
||||||
fn restore(&self) -> Result<(), AppError> {
|
fn restore(&self) -> Result<(), AppError> {
|
||||||
match self {
|
match self {
|
||||||
LiveSnapshot::Claude { settings } => {
|
LiveSnapshot::Claude { settings } => {
|
||||||
@@ -498,246 +501,69 @@ impl ProviderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn run_transaction<R, F>(state: &AppState, f: F) -> Result<R, AppError>
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut MultiAppConfig) -> Result<(R, Option<PostCommitAction>), AppError>,
|
|
||||||
{
|
|
||||||
let mut guard = state.config.write().map_err(AppError::from)?;
|
|
||||||
let original = guard.clone();
|
|
||||||
let (result, action) = match f(&mut guard) {
|
|
||||||
Ok(value) => value,
|
|
||||||
Err(err) => {
|
|
||||||
*guard = original;
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
drop(guard);
|
|
||||||
|
|
||||||
if let Err(save_err) = state.save() {
|
fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Result<(), AppError> {
|
||||||
if let Err(rollback_err) = Self::restore_config_only(state, original.clone()) {
|
|
||||||
return Err(AppError::localized(
|
|
||||||
"config.save.rollback_failed",
|
|
||||||
format!("保存配置失败: {save_err};回滚失败: {rollback_err}"),
|
|
||||||
format!("Failed to save config: {save_err}; rollback failed: {rollback_err}"),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return Err(save_err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(action) = action {
|
|
||||||
if let Err(err) = Self::apply_post_commit(state, &action) {
|
|
||||||
if let Err(rollback_err) =
|
|
||||||
Self::rollback_after_failure(state, original.clone(), action.backup.clone())
|
|
||||||
{
|
|
||||||
return Err(AppError::localized(
|
|
||||||
"post_commit.rollback_failed",
|
|
||||||
format!("后置操作失败: {err};回滚失败: {rollback_err}"),
|
|
||||||
format!("Post-commit step failed: {err}; rollback failed: {rollback_err}"),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn restore_config_only(state: &AppState, snapshot: MultiAppConfig) -> Result<(), AppError> {
|
|
||||||
{
|
|
||||||
let mut guard = state.config.write().map_err(AppError::from)?;
|
|
||||||
*guard = snapshot;
|
|
||||||
}
|
|
||||||
state.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rollback_after_failure(
|
|
||||||
state: &AppState,
|
|
||||||
snapshot: MultiAppConfig,
|
|
||||||
backup: LiveSnapshot,
|
|
||||||
) -> Result<(), AppError> {
|
|
||||||
Self::restore_config_only(state, snapshot)?;
|
|
||||||
backup.restore()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_post_commit(state: &AppState, action: &PostCommitAction) -> Result<(), AppError> {
|
|
||||||
Self::write_live_snapshot(&action.app_type, &action.provider)?;
|
|
||||||
if action.sync_mcp {
|
|
||||||
// 使用 v3.7.0 统一的 MCP 同步机制,支持所有应用
|
|
||||||
use crate::services::mcp::McpService;
|
|
||||||
McpService::sync_all_enabled(state)?;
|
|
||||||
}
|
|
||||||
if action.refresh_snapshot {
|
|
||||||
Self::refresh_provider_snapshot(state, &action.app_type, &action.provider.id)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn refresh_provider_snapshot(
|
|
||||||
state: &AppState,
|
|
||||||
app_type: &AppType,
|
|
||||||
provider_id: &str,
|
|
||||||
) -> Result<(), AppError> {
|
|
||||||
match app_type {
|
|
||||||
AppType::Claude => {
|
|
||||||
let settings_path = get_claude_settings_path();
|
|
||||||
if !settings_path.exists() {
|
|
||||||
return Err(AppError::localized(
|
|
||||||
"claude.live.missing",
|
|
||||||
"Claude 设置文件不存在,无法刷新快照",
|
|
||||||
"Claude settings file missing; cannot refresh snapshot",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let mut live_after = read_json_file::<Value>(&settings_path)?;
|
|
||||||
let _ = Self::normalize_claude_models_in_value(&mut live_after);
|
|
||||||
{
|
|
||||||
let mut guard = state.config.write().map_err(AppError::from)?;
|
|
||||||
if let Some(manager) = guard.get_manager_mut(app_type) {
|
|
||||||
if let Some(target) = manager.providers.get_mut(provider_id) {
|
|
||||||
target.settings_config = live_after;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.save()?;
|
|
||||||
}
|
|
||||||
AppType::Codex => {
|
|
||||||
let auth_path = get_codex_auth_path();
|
|
||||||
if !auth_path.exists() {
|
|
||||||
return Err(AppError::localized(
|
|
||||||
"codex.live.missing",
|
|
||||||
"Codex auth.json 不存在,无法刷新快照",
|
|
||||||
"Codex auth.json missing; cannot refresh snapshot",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let auth: Value = read_json_file(&auth_path)?;
|
|
||||||
let cfg_text = crate::codex_config::read_and_validate_codex_config_text()?;
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut guard = state.config.write().map_err(AppError::from)?;
|
|
||||||
if let Some(manager) = guard.get_manager_mut(app_type) {
|
|
||||||
if let Some(target) = manager.providers.get_mut(provider_id) {
|
|
||||||
let obj = target.settings_config.as_object_mut().ok_or_else(|| {
|
|
||||||
AppError::Config(format!(
|
|
||||||
"供应商 {provider_id} 的 Codex 配置必须是 JSON 对象"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
obj.insert("auth".to_string(), auth.clone());
|
|
||||||
obj.insert("config".to_string(), Value::String(cfg_text.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.save()?;
|
|
||||||
}
|
|
||||||
AppType::Gemini => {
|
|
||||||
use crate::gemini_config::{
|
|
||||||
env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
|
|
||||||
};
|
|
||||||
|
|
||||||
let env_path = get_gemini_env_path();
|
|
||||||
if !env_path.exists() {
|
|
||||||
return Err(AppError::localized(
|
|
||||||
"gemini.live.missing",
|
|
||||||
"Gemini .env 文件不存在,无法刷新快照",
|
|
||||||
"Gemini .env file missing; cannot refresh snapshot",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let env_map = read_gemini_env()?;
|
|
||||||
let mut live_after = env_to_json(&env_map);
|
|
||||||
|
|
||||||
let settings_path = get_gemini_settings_path();
|
|
||||||
let config_value = if settings_path.exists() {
|
|
||||||
read_json_file(&settings_path)?
|
|
||||||
} else {
|
|
||||||
json!({})
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(obj) = live_after.as_object_mut() {
|
|
||||||
obj.insert("config".to_string(), config_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut guard = state.config.write().map_err(AppError::from)?;
|
|
||||||
if let Some(manager) = guard.get_manager_mut(app_type) {
|
|
||||||
if let Some(target) = manager.providers.get_mut(provider_id) {
|
|
||||||
target.settings_config = live_after;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.save()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn capture_live_snapshot(app_type: &AppType) -> Result<LiveSnapshot, AppError> {
|
|
||||||
match app_type {
|
match app_type {
|
||||||
AppType::Claude => {
|
AppType::Claude => {
|
||||||
let path = get_claude_settings_path();
|
let path = get_claude_settings_path();
|
||||||
let settings = if path.exists() {
|
write_json_file(&path, &provider.settings_config)?;
|
||||||
Some(read_json_file::<Value>(&path)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
Ok(LiveSnapshot::Claude { settings })
|
|
||||||
}
|
}
|
||||||
AppType::Codex => {
|
AppType::Codex => {
|
||||||
|
let obj = provider.settings_config.as_object().ok_or_else(|| {
|
||||||
|
AppError::Config("Codex 供应商配置必须是 JSON 对象".to_string())
|
||||||
|
})?;
|
||||||
|
let auth = obj.get("auth").ok_or_else(|| {
|
||||||
|
AppError::Config("Codex 供应商配置缺少 'auth' 字段".to_string())
|
||||||
|
})?;
|
||||||
|
let config_str = obj.get("config").and_then(|v| v.as_str()).ok_or_else(|| {
|
||||||
|
AppError::Config("Codex 供应商配置缺少 'config' 字段或不是字符串".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
let auth_path = get_codex_auth_path();
|
let auth_path = get_codex_auth_path();
|
||||||
|
write_json_file(&auth_path, auth)?;
|
||||||
let config_path = get_codex_config_path();
|
let config_path = get_codex_config_path();
|
||||||
let auth = if auth_path.exists() {
|
std::fs::write(&config_path, config_str)
|
||||||
Some(read_json_file::<Value>(&auth_path)?)
|
.map_err(|e| AppError::io(&config_path, e))?;
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let config = if config_path.exists() {
|
|
||||||
Some(
|
|
||||||
std::fs::read_to_string(&config_path)
|
|
||||||
.map_err(|e| AppError::io(&config_path, e))?,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
Ok(LiveSnapshot::Codex { auth, config })
|
|
||||||
}
|
}
|
||||||
AppType::Gemini => {
|
AppType::Gemini => {
|
||||||
// 新增
|
|
||||||
use crate::gemini_config::{
|
use crate::gemini_config::{
|
||||||
get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
|
get_gemini_settings_path, json_to_env, write_gemini_env_atomic,
|
||||||
};
|
};
|
||||||
let path = get_gemini_env_path();
|
|
||||||
let env = if path.exists() {
|
// Extract env and config from provider settings
|
||||||
Some(read_gemini_env()?)
|
let env_value = provider.settings_config.get("env");
|
||||||
} else {
|
let config_value = provider.settings_config.get("config");
|
||||||
None
|
|
||||||
};
|
// Write env file
|
||||||
let settings_path = get_gemini_settings_path();
|
if let Some(env) = env_value {
|
||||||
let config = if settings_path.exists() {
|
let env_map = json_to_env(env)?;
|
||||||
Some(read_json_file(&settings_path)?)
|
write_gemini_env_atomic(&env_map)?;
|
||||||
} else {
|
}
|
||||||
None
|
|
||||||
};
|
// Write settings file
|
||||||
Ok(LiveSnapshot::Gemini { env, config })
|
if let Some(config) = config_value {
|
||||||
|
let settings_path = get_gemini_settings_path();
|
||||||
|
write_json_file(&settings_path, config)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 列出指定应用下的所有供应商
|
/// 列出指定应用下的所有供应商
|
||||||
pub fn list(
|
pub fn list(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
app_type: AppType,
|
app_type: AppType,
|
||||||
) -> Result<HashMap<String, Provider>, AppError> {
|
) -> Result<IndexMap<String, Provider>, AppError> {
|
||||||
let config = state.config.read().map_err(AppError::from)?;
|
state.db.get_all_providers(app_type.as_str())
|
||||||
let manager = config
|
|
||||||
.get_manager(&app_type)
|
|
||||||
.ok_or_else(|| Self::app_not_found(&app_type))?;
|
|
||||||
Ok(manager.get_all_providers().clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取当前供应商 ID
|
/// 获取当前供应商 ID
|
||||||
pub fn current(state: &AppState, app_type: AppType) -> Result<String, AppError> {
|
pub fn current(state: &AppState, app_type: AppType) -> Result<String, AppError> {
|
||||||
let config = state.config.read().map_err(AppError::from)?;
|
state
|
||||||
let manager = config
|
.db
|
||||||
.get_manager(&app_type)
|
.get_current_provider(app_type.as_str())
|
||||||
.ok_or_else(|| Self::app_not_found(&app_type))?;
|
.map(|opt| opt.unwrap_or_default())
|
||||||
Ok(manager.current.clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 新增供应商
|
/// 新增供应商
|
||||||
@@ -747,35 +573,20 @@ impl ProviderService {
|
|||||||
Self::normalize_provider_if_claude(&app_type, &mut provider);
|
Self::normalize_provider_if_claude(&app_type, &mut provider);
|
||||||
Self::validate_provider_settings(&app_type, &provider)?;
|
Self::validate_provider_settings(&app_type, &provider)?;
|
||||||
|
|
||||||
let app_type_clone = app_type.clone();
|
// 保存到数据库
|
||||||
let provider_clone = provider.clone();
|
state.db.save_provider(app_type.as_str(), &provider)?;
|
||||||
|
|
||||||
Self::run_transaction(state, move |config| {
|
// 检查是否需要同步(如果是当前供应商,或者没有当前供应商)
|
||||||
config.ensure_app(&app_type_clone);
|
let current = state.db.get_current_provider(app_type.as_str())?;
|
||||||
let manager = config
|
if current.is_none() {
|
||||||
.get_manager_mut(&app_type_clone)
|
// 如果没有当前供应商,设为当前并同步
|
||||||
.ok_or_else(|| Self::app_not_found(&app_type_clone))?;
|
state
|
||||||
|
.db
|
||||||
|
.set_current_provider(app_type.as_str(), &provider.id)?;
|
||||||
|
Self::write_live_snapshot(&app_type, &provider)?;
|
||||||
|
}
|
||||||
|
|
||||||
let is_current = manager.current == provider_clone.id;
|
Ok(true)
|
||||||
manager
|
|
||||||
.providers
|
|
||||||
.insert(provider_clone.id.clone(), provider_clone.clone());
|
|
||||||
|
|
||||||
let action = if is_current {
|
|
||||||
let backup = Self::capture_live_snapshot(&app_type_clone)?;
|
|
||||||
Some(PostCommitAction {
|
|
||||||
app_type: app_type_clone.clone(),
|
|
||||||
provider: provider_clone.clone(),
|
|
||||||
backup,
|
|
||||||
sync_mcp: false,
|
|
||||||
refresh_snapshot: false,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((true, action))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新供应商
|
/// 更新供应商
|
||||||
@@ -788,71 +599,30 @@ impl ProviderService {
|
|||||||
// 归一化 Claude 模型键
|
// 归一化 Claude 模型键
|
||||||
Self::normalize_provider_if_claude(&app_type, &mut provider);
|
Self::normalize_provider_if_claude(&app_type, &mut provider);
|
||||||
Self::validate_provider_settings(&app_type, &provider)?;
|
Self::validate_provider_settings(&app_type, &provider)?;
|
||||||
let provider_id = provider.id.clone();
|
|
||||||
let app_type_clone = app_type.clone();
|
|
||||||
let provider_clone = provider.clone();
|
|
||||||
|
|
||||||
Self::run_transaction(state, move |config| {
|
// 检查是否为当前供应商
|
||||||
let manager = config
|
let current_id = state.db.get_current_provider(app_type.as_str())?;
|
||||||
.get_manager_mut(&app_type_clone)
|
let is_current = current_id.as_deref() == Some(provider.id.as_str());
|
||||||
.ok_or_else(|| Self::app_not_found(&app_type_clone))?;
|
|
||||||
|
|
||||||
if !manager.providers.contains_key(&provider_id) {
|
// 保存到数据库
|
||||||
return Err(AppError::localized(
|
state.db.save_provider(app_type.as_str(), &provider)?;
|
||||||
"provider.not_found",
|
|
||||||
format!("供应商不存在: {provider_id}"),
|
|
||||||
format!("Provider not found: {provider_id}"),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_current = manager.current == provider_id;
|
if is_current {
|
||||||
let merged = if let Some(existing) = manager.providers.get(&provider_id) {
|
Self::write_live_snapshot(&app_type, &provider)?;
|
||||||
let mut updated = provider_clone.clone();
|
// Sync MCP
|
||||||
match (existing.meta.as_ref(), updated.meta.take()) {
|
use crate::services::mcp::McpService;
|
||||||
// 前端未提供 meta,表示不修改,沿用旧值
|
McpService::sync_all_enabled(state)?;
|
||||||
(Some(old_meta), None) => {
|
}
|
||||||
updated.meta = Some(old_meta.clone());
|
|
||||||
}
|
|
||||||
(None, None) => {
|
|
||||||
updated.meta = None;
|
|
||||||
}
|
|
||||||
// 前端提供的 meta 视为权威,直接覆盖(其中 custom_endpoints 允许是空,表示删除所有自定义端点)
|
|
||||||
(_old, Some(new_meta)) => {
|
|
||||||
updated.meta = Some(new_meta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updated
|
|
||||||
} else {
|
|
||||||
provider_clone.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
manager.providers.insert(provider_id.clone(), merged);
|
Ok(true)
|
||||||
|
|
||||||
let action = if is_current {
|
|
||||||
let backup = Self::capture_live_snapshot(&app_type_clone)?;
|
|
||||||
Some(PostCommitAction {
|
|
||||||
app_type: app_type_clone.clone(),
|
|
||||||
provider: provider_clone.clone(),
|
|
||||||
backup,
|
|
||||||
sync_mcp: false,
|
|
||||||
refresh_snapshot: false,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((true, action))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 导入当前 live 配置为默认供应商
|
/// 导入当前 live 配置为默认供应商
|
||||||
pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<(), AppError> {
|
pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<(), AppError> {
|
||||||
{
|
{
|
||||||
let config = state.config.read().map_err(AppError::from)?;
|
let providers = state.db.get_all_providers(app_type.as_str())?;
|
||||||
if let Some(manager) = config.get_manager(&app_type) {
|
if !providers.is_empty() {
|
||||||
if !manager.get_all_providers().is_empty() {
|
return Ok(());
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -926,18 +696,11 @@ impl ProviderService {
|
|||||||
);
|
);
|
||||||
provider.category = Some("custom".to_string());
|
provider.category = Some("custom".to_string());
|
||||||
|
|
||||||
{
|
state.db.save_provider(app_type.as_str(), &provider)?;
|
||||||
let mut config = state.config.write().map_err(AppError::from)?;
|
state
|
||||||
let manager = config
|
.db
|
||||||
.get_manager_mut(&app_type)
|
.set_current_provider(app_type.as_str(), &provider.id)?;
|
||||||
.ok_or_else(|| Self::app_not_found(&app_type))?;
|
|
||||||
manager
|
|
||||||
.providers
|
|
||||||
.insert(provider.id.clone(), provider.clone());
|
|
||||||
manager.current = provider.id.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
state.save()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1010,12 +773,8 @@ impl ProviderService {
|
|||||||
app_type: AppType,
|
app_type: AppType,
|
||||||
provider_id: &str,
|
provider_id: &str,
|
||||||
) -> Result<Vec<CustomEndpoint>, AppError> {
|
) -> Result<Vec<CustomEndpoint>, AppError> {
|
||||||
let cfg = state.config.read().map_err(AppError::from)?;
|
let providers = state.db.get_all_providers(app_type.as_str())?;
|
||||||
let manager = cfg
|
let Some(provider) = providers.get(provider_id) else {
|
||||||
.get_manager(&app_type)
|
|
||||||
.ok_or_else(|| Self::app_not_found(&app_type))?;
|
|
||||||
|
|
||||||
let Some(provider) = manager.providers.get(provider_id) else {
|
|
||||||
return Ok(vec![]);
|
return Ok(vec![]);
|
||||||
};
|
};
|
||||||
let Some(meta) = provider.meta.as_ref() else {
|
let Some(meta) = provider.meta.as_ref() else {
|
||||||
@@ -1046,29 +805,9 @@ impl ProviderService {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
state
|
||||||
let mut cfg = state.config.write().map_err(AppError::from)?;
|
.db
|
||||||
let manager = cfg
|
.add_custom_endpoint(app_type.as_str(), provider_id, &normalized)?;
|
||||||
.get_manager_mut(&app_type)
|
|
||||||
.ok_or_else(|| Self::app_not_found(&app_type))?;
|
|
||||||
let provider = manager.providers.get_mut(provider_id).ok_or_else(|| {
|
|
||||||
AppError::localized(
|
|
||||||
"provider.not_found",
|
|
||||||
format!("供应商不存在: {provider_id}"),
|
|
||||||
format!("Provider not found: {provider_id}"),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let meta = provider.meta.get_or_insert_with(ProviderMeta::default);
|
|
||||||
|
|
||||||
let endpoint = CustomEndpoint {
|
|
||||||
url: normalized.clone(),
|
|
||||||
added_at: Self::now_millis(),
|
|
||||||
last_used: None,
|
|
||||||
};
|
|
||||||
meta.custom_endpoints.insert(normalized, endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
state.save()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1080,19 +819,9 @@ impl ProviderService {
|
|||||||
url: String,
|
url: String,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
let normalized = url.trim().trim_end_matches('/').to_string();
|
let normalized = url.trim().trim_end_matches('/').to_string();
|
||||||
|
state
|
||||||
{
|
.db
|
||||||
let mut cfg = state.config.write().map_err(AppError::from)?;
|
.remove_custom_endpoint(app_type.as_str(), provider_id, &normalized)?;
|
||||||
if let Some(manager) = cfg.get_manager_mut(&app_type) {
|
|
||||||
if let Some(provider) = manager.providers.get_mut(provider_id) {
|
|
||||||
if let Some(meta) = provider.meta.as_mut() {
|
|
||||||
meta.custom_endpoints.remove(&normalized);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.save()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1105,20 +834,16 @@ impl ProviderService {
|
|||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
let normalized = url.trim().trim_end_matches('/').to_string();
|
let normalized = url.trim().trim_end_matches('/').to_string();
|
||||||
|
|
||||||
{
|
// Get provider, update last_used, save back
|
||||||
let mut cfg = state.config.write().map_err(AppError::from)?;
|
let mut providers = state.db.get_all_providers(app_type.as_str())?;
|
||||||
if let Some(manager) = cfg.get_manager_mut(&app_type) {
|
if let Some(provider) = providers.get_mut(provider_id) {
|
||||||
if let Some(provider) = manager.providers.get_mut(provider_id) {
|
if let Some(meta) = provider.meta.as_mut() {
|
||||||
if let Some(meta) = provider.meta.as_mut() {
|
if let Some(endpoint) = meta.custom_endpoints.get_mut(&normalized) {
|
||||||
if let Some(endpoint) = meta.custom_endpoints.get_mut(&normalized) {
|
endpoint.last_used = Some(Self::now_millis());
|
||||||
endpoint.last_used = Some(Self::now_millis());
|
state.db.save_provider(app_type.as_str(), provider)?;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.save()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1128,20 +853,15 @@ impl ProviderService {
|
|||||||
app_type: AppType,
|
app_type: AppType,
|
||||||
updates: Vec<ProviderSortUpdate>,
|
updates: Vec<ProviderSortUpdate>,
|
||||||
) -> Result<bool, AppError> {
|
) -> Result<bool, AppError> {
|
||||||
{
|
let mut providers = state.db.get_all_providers(app_type.as_str())?;
|
||||||
let mut cfg = state.config.write().map_err(AppError::from)?;
|
|
||||||
let manager = cfg
|
|
||||||
.get_manager_mut(&app_type)
|
|
||||||
.ok_or_else(|| Self::app_not_found(&app_type))?;
|
|
||||||
|
|
||||||
for update in updates {
|
for update in updates {
|
||||||
if let Some(provider) = manager.providers.get_mut(&update.id) {
|
if let Some(provider) = providers.get_mut(&update.id) {
|
||||||
provider.sort_index = Some(update.sort_index);
|
provider.sort_index = Some(update.sort_index);
|
||||||
}
|
state.db.save_provider(app_type.as_str(), provider)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.save()?;
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1222,11 +942,8 @@ impl ProviderService {
|
|||||||
provider_id: &str,
|
provider_id: &str,
|
||||||
) -> Result<UsageResult, AppError> {
|
) -> Result<UsageResult, AppError> {
|
||||||
let (script_code, timeout, api_key, base_url, access_token, user_id) = {
|
let (script_code, timeout, api_key, base_url, access_token, user_id) = {
|
||||||
let config = state.config.read().map_err(AppError::from)?;
|
let providers = state.db.get_all_providers(app_type.as_str())?;
|
||||||
let manager = config
|
let provider = providers.get(provider_id).ok_or_else(|| {
|
||||||
.get_manager(&app_type)
|
|
||||||
.ok_or_else(|| Self::app_not_found(&app_type))?;
|
|
||||||
let provider = manager.providers.get(provider_id).cloned().ok_or_else(|| {
|
|
||||||
AppError::localized(
|
AppError::localized(
|
||||||
"provider.not_found",
|
"provider.not_found",
|
||||||
format!("供应商不存在: {provider_id}"),
|
format!("供应商不存在: {provider_id}"),
|
||||||
@@ -1300,98 +1017,7 @@ impl ProviderService {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 切换指定应用的供应商
|
#[allow(dead_code)]
|
||||||
pub fn switch(state: &AppState, app_type: AppType, provider_id: &str) -> Result<(), AppError> {
|
|
||||||
let app_type_clone = app_type.clone();
|
|
||||||
let provider_id_owned = provider_id.to_string();
|
|
||||||
|
|
||||||
Self::run_transaction(state, move |config| {
|
|
||||||
let backup = Self::capture_live_snapshot(&app_type_clone)?;
|
|
||||||
let provider = match app_type_clone {
|
|
||||||
AppType::Codex => Self::prepare_switch_codex(config, &provider_id_owned)?,
|
|
||||||
AppType::Claude => Self::prepare_switch_claude(config, &provider_id_owned)?,
|
|
||||||
AppType::Gemini => Self::prepare_switch_gemini(config, &provider_id_owned)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let action = PostCommitAction {
|
|
||||||
app_type: app_type_clone.clone(),
|
|
||||||
provider,
|
|
||||||
backup,
|
|
||||||
sync_mcp: true, // v3.7.0: 所有应用切换时都同步 MCP,防止配置丢失
|
|
||||||
refresh_snapshot: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(((), Some(action)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_switch_codex(
|
|
||||||
config: &mut MultiAppConfig,
|
|
||||||
provider_id: &str,
|
|
||||||
) -> Result<Provider, AppError> {
|
|
||||||
let provider = config
|
|
||||||
.get_manager(&AppType::Codex)
|
|
||||||
.ok_or_else(|| Self::app_not_found(&AppType::Codex))?
|
|
||||||
.providers
|
|
||||||
.get(provider_id)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| {
|
|
||||||
AppError::localized(
|
|
||||||
"provider.not_found",
|
|
||||||
format!("供应商不存在: {provider_id}"),
|
|
||||||
format!("Provider not found: {provider_id}"),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Self::backfill_codex_current(config, provider_id)?;
|
|
||||||
|
|
||||||
if let Some(manager) = config.get_manager_mut(&AppType::Codex) {
|
|
||||||
manager.current = provider_id.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
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: &Provider) -> Result<(), AppError> {
|
fn write_codex_live(provider: &Provider) -> Result<(), AppError> {
|
||||||
let settings = provider
|
let settings = provider
|
||||||
.settings_config
|
.settings_config
|
||||||
@@ -1412,131 +1038,7 @@ impl ProviderService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_switch_claude(
|
#[allow(dead_code)]
|
||||||
config: &mut MultiAppConfig,
|
|
||||||
provider_id: &str,
|
|
||||||
) -> Result<Provider, AppError> {
|
|
||||||
let provider = config
|
|
||||||
.get_manager(&AppType::Claude)
|
|
||||||
.ok_or_else(|| Self::app_not_found(&AppType::Claude))?
|
|
||||||
.providers
|
|
||||||
.get(provider_id)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| {
|
|
||||||
AppError::localized(
|
|
||||||
"provider.not_found",
|
|
||||||
format!("供应商不存在: {provider_id}"),
|
|
||||||
format!("Provider not found: {provider_id}"),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Self::backfill_claude_current(config, provider_id)?;
|
|
||||||
|
|
||||||
if let Some(manager) = config.get_manager_mut(&AppType::Claude) {
|
|
||||||
manager.current = provider_id.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_switch_gemini(
|
|
||||||
config: &mut MultiAppConfig,
|
|
||||||
provider_id: &str,
|
|
||||||
) -> Result<Provider, AppError> {
|
|
||||||
let provider = config
|
|
||||||
.get_manager(&AppType::Gemini)
|
|
||||||
.ok_or_else(|| Self::app_not_found(&AppType::Gemini))?
|
|
||||||
.providers
|
|
||||||
.get(provider_id)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| {
|
|
||||||
AppError::localized(
|
|
||||||
"provider.not_found",
|
|
||||||
format!("供应商不存在: {provider_id}"),
|
|
||||||
format!("Provider not found: {provider_id}"),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Self::backfill_gemini_current(config, provider_id)?;
|
|
||||||
|
|
||||||
if let Some(manager) = config.get_manager_mut(&AppType::Gemini) {
|
|
||||||
manager.current = provider_id.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 mut live = read_json_file::<Value>(&settings_path)?;
|
|
||||||
let _ = Self::normalize_claude_models_in_value(&mut live);
|
|
||||||
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 backfill_gemini_current(
|
|
||||||
config: &mut MultiAppConfig,
|
|
||||||
next_provider: &str,
|
|
||||||
) -> Result<(), AppError> {
|
|
||||||
use crate::gemini_config::{
|
|
||||||
env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
|
|
||||||
};
|
|
||||||
|
|
||||||
let env_path = get_gemini_env_path();
|
|
||||||
if !env_path.exists() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let current_id = config
|
|
||||||
.get_manager(&AppType::Gemini)
|
|
||||||
.map(|m| m.current.clone())
|
|
||||||
.unwrap_or_default();
|
|
||||||
if current_id.is_empty() || current_id == next_provider {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let env_map = read_gemini_env()?;
|
|
||||||
let mut live = env_to_json(&env_map);
|
|
||||||
|
|
||||||
let settings_path = get_gemini_settings_path();
|
|
||||||
let config_value = if settings_path.exists() {
|
|
||||||
read_json_file(&settings_path)?
|
|
||||||
} else {
|
|
||||||
json!({})
|
|
||||||
};
|
|
||||||
if let Some(obj) = live.as_object_mut() {
|
|
||||||
obj.insert("config".to_string(), config_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(manager) = config.get_manager_mut(&AppType::Gemini) {
|
|
||||||
if let Some(current) = manager.providers.get_mut(¤t_id) {
|
|
||||||
current.settings_config = live;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_claude_live(provider: &Provider) -> Result<(), AppError> {
|
fn write_claude_live(provider: &Provider) -> Result<(), AppError> {
|
||||||
let settings_path = get_claude_settings_path();
|
let settings_path = get_claude_settings_path();
|
||||||
let mut content = provider.settings_config.clone();
|
let mut content = provider.settings_config.clone();
|
||||||
@@ -1613,14 +1115,6 @@ impl ProviderService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Result<(), AppError> {
|
|
||||||
match app_type {
|
|
||||||
AppType::Codex => Self::write_codex_live(provider),
|
|
||||||
AppType::Claude => Self::write_claude_live(provider),
|
|
||||||
AppType::Gemini => Self::write_gemini_live(provider), // 新增
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), AppError> {
|
fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), AppError> {
|
||||||
match app_type {
|
match app_type {
|
||||||
AppType::Claude => {
|
AppType::Claude => {
|
||||||
@@ -1838,6 +1332,7 @@ impl ProviderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
fn app_not_found(app_type: &AppType) -> AppError {
|
fn app_not_found(app_type: &AppType) -> AppError {
|
||||||
AppError::localized(
|
AppError::localized(
|
||||||
"provider.app_not_found",
|
"provider.app_not_found",
|
||||||
@@ -1846,76 +1341,44 @@ impl ProviderService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 删除供应商
|
||||||
|
pub fn delete(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> {
|
||||||
|
let current = state.db.get_current_provider(app_type.as_str())?;
|
||||||
|
if current.as_deref() == Some(id) {
|
||||||
|
return Err(AppError::Message(
|
||||||
|
"无法删除当前正在使用的供应商".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
state.db.delete_provider(app_type.as_str(), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 切换供应商
|
||||||
|
pub fn switch(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> {
|
||||||
|
// Check if provider exists
|
||||||
|
let providers = state.db.get_all_providers(app_type.as_str())?;
|
||||||
|
let provider = providers
|
||||||
|
.get(id)
|
||||||
|
.ok_or_else(|| AppError::Message(format!("供应商 {id} 不存在")))?;
|
||||||
|
|
||||||
|
// Set current
|
||||||
|
state.db.set_current_provider(app_type.as_str(), id)?;
|
||||||
|
|
||||||
|
// Sync to live
|
||||||
|
Self::write_live_snapshot(&app_type, provider)?;
|
||||||
|
|
||||||
|
// Sync MCP
|
||||||
|
use crate::services::mcp::McpService;
|
||||||
|
McpService::sync_all_enabled(state)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn now_millis() -> i64 {
|
fn now_millis() -> i64 {
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.as_millis() as i64
|
.as_millis() as i64
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(state: &AppState, app_type: AppType, provider_id: &str) -> Result<(), AppError> {
|
|
||||||
let provider_snapshot = {
|
|
||||||
let config = state.config.read().map_err(AppError::from)?;
|
|
||||||
let manager = config
|
|
||||||
.get_manager(&app_type)
|
|
||||||
.ok_or_else(|| Self::app_not_found(&app_type))?;
|
|
||||||
|
|
||||||
if manager.current == provider_id {
|
|
||||||
return Err(AppError::localized(
|
|
||||||
"provider.delete.current",
|
|
||||||
"不能删除当前正在使用的供应商",
|
|
||||||
"Cannot delete the provider currently in use",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.providers.get(provider_id).cloned().ok_or_else(|| {
|
|
||||||
AppError::localized(
|
|
||||||
"provider.not_found",
|
|
||||||
format!("供应商不存在: {provider_id}"),
|
|
||||||
format!("Provider not found: {provider_id}"),
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
};
|
|
||||||
|
|
||||||
match app_type {
|
|
||||||
AppType::Codex => {
|
|
||||||
crate::codex_config::delete_codex_provider_config(
|
|
||||||
provider_id,
|
|
||||||
&provider_snapshot.name,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
AppType::Claude => {
|
|
||||||
// 兼容旧版本:历史上会在 Claude 目录内为每个供应商生成 settings-*.json 副本
|
|
||||||
// 这里继续清理这些遗留文件,避免堆积过期配置。
|
|
||||||
let by_name = get_provider_config_path(provider_id, Some(&provider_snapshot.name));
|
|
||||||
let by_id = get_provider_config_path(provider_id, None);
|
|
||||||
delete_file(&by_name)?;
|
|
||||||
delete_file(&by_id)?;
|
|
||||||
}
|
|
||||||
AppType::Gemini => {
|
|
||||||
// Gemini 使用单一的 .env 文件,不需要删除单独的供应商配置文件
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut config = state.config.write().map_err(AppError::from)?;
|
|
||||||
let manager = config
|
|
||||||
.get_manager_mut(&app_type)
|
|
||||||
.ok_or_else(|| Self::app_not_found(&app_type))?;
|
|
||||||
|
|
||||||
if manager.current == provider_id {
|
|
||||||
return Err(AppError::localized(
|
|
||||||
"provider.delete.current",
|
|
||||||
"不能删除当前正在使用的供应商",
|
|
||||||
"Cannot delete the provider currently in use",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.providers.remove(provider_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
state.save()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
|||||||
Reference in New Issue
Block a user