diff --git a/package.json b/package.json index 9b2618e3f..377b171ff 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.17.4", + "version": "0.17.5", "type": "module", "repository": { "type": "git", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a20b0ca38..67c3ce1ec 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3377,7 +3377,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.17.4" +version = "0.17.5" dependencies = [ "anyhow", "async-stream", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ece71fe0b..29bd19bf8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.17.4" +version = "0.17.5" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/commands/switch_cmd.rs b/src-tauri/src/commands/switch_cmd.rs index 7feef299a..d28386b28 100644 --- a/src-tauri/src/commands/switch_cmd.rs +++ b/src-tauri/src/commands/switch_cmd.rs @@ -1,5 +1,6 @@ use crate::database::DbConnection; -use crate::models::Provider; +use crate::models::{AppType, Provider}; +use crate::services::live_sync::{check_config_sync, sync_from_external, SyncCheckResult}; use crate::services::switch::SwitchService; use serde_json::Value; use tauri::State; @@ -63,3 +64,48 @@ pub fn import_default_config( pub fn read_live_provider_settings(app_type: String) -> Result { SwitchService::read_live_settings(&app_type) } + +/// 检查配置同步状态 +#[tauri::command] +pub fn check_config_sync_status( + db: State<'_, DbConnection>, + app_type: String, +) -> Result { + // 解析 app_type + let app_type_enum: AppType = app_type + .parse() + .map_err(|e| format!("Invalid app type: {}", e))?; + + // 获取当前 ProxyCast 中设置的 provider + let current_provider = SwitchService::get_current_provider(&db, &app_type)? + .map(|p| p.id) + .unwrap_or_else(|| "unknown".to_string()); + + // 检查同步状态 + check_config_sync(&app_type_enum, ¤t_provider) + .map_err(|e| format!("Failed to check config sync: {}", e)) +} + +/// 从外部配置同步到 ProxyCast +#[tauri::command] +pub fn sync_from_external_config( + db: State<'_, DbConnection>, + app_type: String, +) -> Result { + // 解析 app_type + let app_type_enum: AppType = app_type + .parse() + .map_err(|e| format!("Invalid app type: {}", e))?; + + // 从外部配置获取 provider + let external_provider = sync_from_external(&app_type_enum) + .map_err(|e| format!("Failed to sync from external: {}", e))?; + + // 切换到外部检测到的 provider + SwitchService::switch_provider(&db, &app_type, &external_provider)?; + + Ok(format!( + "已同步到外部配置的 provider: {}", + external_provider + )) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5917ce2f6..c21bf255a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1751,15 +1751,61 @@ pub fn run() { let shared_flow_monitor = flow_monitor_clone.clone(); let app_handle = app.handle().clone(); tauri::async_runtime::spawn(async move { - // 先加载凭证 + // 先加载凭证池中的凭证 { + logs.write().await.add("info", "[启动] 正在加载凭证池..."); + + // 获取凭证池概览信息 + match pool_service.get_overview(&db) { + Ok(overview) => { + let mut loaded_types = Vec::new(); + let mut total_credentials = 0; + + for provider_overview in overview { + let count = provider_overview.stats.total_count; + if count > 0 { + total_credentials += count; + let provider_name = + match provider_overview.provider_type.as_str() { + "kiro" => "Kiro", + "gemini" => "Gemini", + "qwen" => "通义千问", + "antigravity" => "Antigravity", + "openai" => "OpenAI", + "claude" => "Claude", + "codex" => "Codex", + "claude_oauth" => "Claude OAuth", + "iflow" => "iFlow", + _ => &provider_overview.provider_type, + }; + loaded_types.push(format!("{} ({} 个)", provider_name, count)); + } + } + + if loaded_types.is_empty() { + logs.write().await.add("warn", "[启动] 未找到任何可用凭证"); + } else { + let message = format!( + "[启动] 凭证已加载: {} (共 {} 个)", + loaded_types.join(", "), + total_credentials + ); + logs.write().await.add("info", &message); + } + } + Err(e) => { + logs.write() + .await + .add("warn", &format!("[启动] 获取凭证池信息失败: {}", e)); + } + } + + // 兼容性:仍然尝试加载旧的 Kiro 凭证(如果存在) let mut s = state.write().await; if let Err(e) = s.kiro_provider.load_credentials().await { logs.write() .await - .add("warn", &format!("[启动] 加载 Kiro 凭证失败: {e}")); - } else { - logs.write().await.add("info", "[启动] Kiro 凭证已加载"); + .add("debug", &format!("[启动] 旧版 Kiro 凭证加载失败: {e}")); } } // 启动服务器(使用共享的遥测实例和 Flow Monitor) @@ -1897,6 +1943,8 @@ pub fn run() { commands::switch_cmd::switch_provider, commands::switch_cmd::import_default_config, commands::switch_cmd::read_live_provider_settings, + commands::switch_cmd::check_config_sync_status, + commands::switch_cmd::sync_from_external_config, // Config commands commands::config_cmd::get_config_status, commands::config_cmd::get_config_dir_path, diff --git a/src-tauri/src/services/live_sync.rs b/src-tauri/src/services/live_sync.rs index da91bde2f..98c0e4eba 100644 --- a/src-tauri/src/services/live_sync.rs +++ b/src-tauri/src/services/live_sync.rs @@ -267,3 +267,169 @@ pub fn read_live_settings( AppType::ProxyCast => Ok(json!({})), } } + +/// 同步状态枚举 +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum SyncStatus { + InSync, // 完全同步 + OutOfSync, // 有差异但无冲突 + Conflict, // 有冲突需要用户选择 +} + +/// 配置冲突信息 +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ConfigConflict { + pub field: String, + pub local_value: String, + pub external_value: String, +} + +/// 同步检查结果 +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct SyncCheckResult { + pub status: SyncStatus, + pub current_provider: String, + pub external_provider: String, + pub last_modified: Option, + pub conflicts: Vec, +} + +/// 从外部配置文件解析当前生效的 provider +pub fn parse_current_provider_from_live( + app_type: &AppType, + live_settings: &Value, +) -> Result> { + match app_type { + AppType::Claude => { + // 检查 Claude 配置中的认证信息来判断当前 provider + if let Some(env) = live_settings.get("env").and_then(|v| v.as_object()) { + // 优先检查 ANTHROPIC_AUTH_TOKEN (OAuth) + if let Some(token) = env.get("ANTHROPIC_AUTH_TOKEN").and_then(|v| v.as_str()) { + if !token.is_empty() { + return Ok("claude_oauth".to_string()); + } + } + + // 检查 ANTHROPIC_API_KEY (API Key) + if let Some(api_key) = env.get("ANTHROPIC_API_KEY").and_then(|v| v.as_str()) { + if !api_key.is_empty() { + return Ok("claude".to_string()); + } + } + } + + Ok("unknown".to_string()) + } + AppType::Codex => { + // 检查 Codex 认证信息 + if let Some(auth) = live_settings.get("auth").and_then(|v| v.as_object()) { + if auth + .get("access_token") + .and_then(|v| v.as_str()) + .map(|s| !s.is_empty()) + .unwrap_or(false) + { + return Ok("codex".to_string()); + } + } + + Ok("unknown".to_string()) + } + AppType::Gemini => { + // 检查 Gemini 环境变量 + if let Some(env) = live_settings.get("env").and_then(|v| v.as_object()) { + if let Some(api_key) = env.get("GOOGLE_API_KEY").and_then(|v| v.as_str()) { + if !api_key.is_empty() { + return Ok("gemini".to_string()); + } + } + } + + Ok("unknown".to_string()) + } + AppType::ProxyCast => Ok("proxycast".to_string()), + } +} + +/// 检查配置同步状态 +pub fn check_config_sync( + app_type: &AppType, + current_provider: &str, +) -> Result> { + // 读取外部配置文件 + let live_settings = read_live_settings(app_type)?; + + // 解析外部配置中的当前 provider + let external_provider = parse_current_provider_from_live(app_type, &live_settings)?; + + // 获取配置文件的修改时间 + let last_modified = get_config_last_modified(app_type); + + // 比较配置 + let status = if current_provider == external_provider { + SyncStatus::InSync + } else if external_provider == "unknown" { + SyncStatus::OutOfSync + } else { + SyncStatus::Conflict + }; + + // 检测具体的冲突字段 + let conflicts = if matches!(status, SyncStatus::Conflict) { + vec![ConfigConflict { + field: "provider".to_string(), + local_value: current_provider.to_string(), + external_value: external_provider.clone(), + }] + } else { + vec![] + }; + + Ok(SyncCheckResult { + status, + current_provider: current_provider.to_string(), + external_provider, + last_modified, + conflicts, + }) +} + +/// 获取配置文件的最后修改时间 +fn get_config_last_modified(app_type: &AppType) -> Option { + let home = dirs::home_dir()?; + let path = match app_type { + AppType::Claude => home.join(".claude").join("settings.json"), + AppType::Codex => home.join(".codex").join("auth.json"), + AppType::Gemini => home.join(".gemini").join(".env"), + AppType::ProxyCast => return None, + }; + + if let Ok(metadata) = std::fs::metadata(&path) { + if let Ok(modified) = metadata.modified() { + if let Ok(datetime) = modified.duration_since(std::time::UNIX_EPOCH) { + return Some(datetime.as_secs().to_string()); + } + } + } + + None +} + +/// 从外部配置同步到 ProxyCast 数据库 +/// 这个函数需要与 switch service 集成来更新数据库中的 provider 记录 +pub fn sync_from_external( + app_type: &AppType, +) -> Result> { + // 读取外部配置 + let live_settings = read_live_settings(app_type)?; + + // 解析当前生效的 provider + let external_provider = parse_current_provider_from_live(app_type, &live_settings)?; + + if external_provider == "unknown" { + return Err("无法识别外部配置中的 provider".into()); + } + + // 返回检测到的 provider,由调用方负责更新数据库 + Ok(external_provider) +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 233017bae..8ce8d5fd6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ProxyCast", - "version": "0.17.4", + "version": "0.17.5", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/clients/ConfigSyncDialog.tsx b/src/components/clients/ConfigSyncDialog.tsx new file mode 100644 index 000000000..d2d75c6d9 --- /dev/null +++ b/src/components/clients/ConfigSyncDialog.tsx @@ -0,0 +1,187 @@ +import { useState } from "react"; +import { AlertTriangle, CheckCircle, XCircle, RefreshCw } from "lucide-react"; +import { SyncCheckResult, SyncStatus } from "@/lib/api/switch"; + +interface ConfigSyncDialogProps { + isOpen: boolean; + syncResult: SyncCheckResult | null; + onClose: () => void; + onSyncFromExternal: () => Promise; + onRefreshCheck: () => Promise; +} + +export function ConfigSyncDialog({ + isOpen, + syncResult, + onClose, + onSyncFromExternal, + onRefreshCheck, +}: ConfigSyncDialogProps) { + const [syncing, setSyncing] = useState(false); + const [checking, setChecking] = useState(false); + + if (!isOpen || !syncResult) return null; + + const getSyncStatusIcon = (status: SyncStatus) => { + switch (status) { + case "InSync": + return ; + case "OutOfSync": + return ; + case "Conflict": + return ; + } + }; + + const getSyncStatusText = (status: SyncStatus) => { + switch (status) { + case "InSync": + return "配置已同步"; + case "OutOfSync": + return "配置有差异"; + case "Conflict": + return "配置冲突"; + } + }; + + const getSyncStatusColor = (status: SyncStatus) => { + switch (status) { + case "InSync": + return "text-green-700 bg-green-50 border-green-200"; + case "OutOfSync": + return "text-yellow-700 bg-yellow-50 border-yellow-200"; + case "Conflict": + return "text-red-700 bg-red-50 border-red-200"; + } + }; + + const handleSyncFromExternal = async () => { + setSyncing(true); + try { + await onSyncFromExternal(); + onClose(); + } catch (_e) { + // Error is handled in the hook + } finally { + setSyncing(false); + } + }; + + const handleRefreshCheck = async () => { + setChecking(true); + try { + await onRefreshCheck(); + } catch (_e) { + // Error is handled in the hook + } finally { + setChecking(false); + } + }; + + return ( +
+
+
+
+ {getSyncStatusIcon(syncResult.status)} +

配置同步状态

+
+ +
+
+ {getSyncStatusIcon(syncResult.status)} + + {getSyncStatusText(syncResult.status)} + +
+ +
+
+ ProxyCast 当前配置:{" "} + {syncResult.current_provider} +
+
+ 外部软件当前配置:{" "} + {syncResult.external_provider} +
+ {syncResult.last_modified && ( +
+ 配置文件修改时间:{" "} + {new Date( + parseInt(syncResult.last_modified) * 1000, + ).toLocaleString()} +
+ )} +
+ + {syncResult.conflicts.length > 0 && ( +
+
冲突详情:
+ {syncResult.conflicts.map((conflict, index) => ( +
+
字段: {conflict.field}
+
ProxyCast: {conflict.local_value}
+
外部软件: {conflict.external_value}
+
+ ))} +
+ )} +
+ + {syncResult.status !== "InSync" && ( +
+

+ {syncResult.status === "OutOfSync" && ( + <> + 检测到外部软件的配置与 ProxyCast + 不同。您可以选择同步外部配置到 ProxyCast。 + + )} + {syncResult.status === "Conflict" && ( + <> + 检测到配置冲突。建议选择使用外部软件的配置,或者手动在 + ProxyCast 中重新设置。 + + )} +

+
+ )} + +
+ + + {syncResult.status !== "InSync" && ( + + )} + + +
+
+
+
+ ); +} diff --git a/src/components/clients/ProviderList.tsx b/src/components/clients/ProviderList.tsx index f0d436dbb..52891617d 100644 --- a/src/components/clients/ProviderList.tsx +++ b/src/components/clients/ProviderList.tsx @@ -1,10 +1,11 @@ import { useState } from "react"; -import { Plus, RefreshCw, Eye } from "lucide-react"; -import { AppType } from "@/lib/api/switch"; +import { Plus, RefreshCw, Eye, GitCompare } from "lucide-react"; +import { AppType, SyncCheckResult } from "@/lib/api/switch"; import { useSwitch } from "@/hooks/useSwitch"; import { ProviderCard } from "./ProviderCard"; import { ProviderForm } from "./ProviderForm"; import { LiveConfigModal } from "./LiveConfigModal"; +import { ConfigSyncDialog } from "./ConfigSyncDialog"; import { ConfirmDialog } from "@/components/ConfirmDialog"; interface ProviderListProps { @@ -22,6 +23,8 @@ export function ProviderList({ appType }: ProviderListProps) { deleteProvider, switchToProvider, refresh, + checkConfigSync, + syncFromExternal, } = useSwitch(appType); const [showForm, setShowForm] = useState(false); @@ -30,6 +33,9 @@ export function ProviderList({ appType }: ProviderListProps) { >(null); const [showLiveConfig, setShowLiveConfig] = useState(false); const [deleteConfirm, setDeleteConfirm] = useState(null); + const [showSyncDialog, setShowSyncDialog] = useState(false); + const [syncResult, setSyncResult] = useState(null); + const [checkingSync, setCheckingSync] = useState(false); const handleAdd = () => { setEditingProvider(null); @@ -71,6 +77,31 @@ export function ProviderList({ appType }: ProviderListProps) { } }; + const handleCheckSync = async () => { + setCheckingSync(true); + try { + const result = await checkConfigSync(); + setSyncResult(result); + setShowSyncDialog(true); + } catch (_e) { + // Error is handled in the hook + } finally { + setCheckingSync(false); + } + }; + + const handleSyncFromExternal = async () => { + await syncFromExternal(); + // 重新检查同步状态 + const result = await checkConfigSync(); + setSyncResult(result); + }; + + const handleRefreshSyncCheck = async () => { + const result = await checkConfigSync(); + setSyncResult(result); + }; + if (loading) { return (
@@ -109,6 +140,16 @@ export function ProviderList({ appType }: ProviderListProps) {
+
); } diff --git a/src/hooks/useSwitch.ts b/src/hooks/useSwitch.ts index ee6146018..fa1366f6d 100644 --- a/src/hooks/useSwitch.ts +++ b/src/hooks/useSwitch.ts @@ -1,6 +1,11 @@ import { useState, useEffect, useCallback } from "react"; import { toast } from "sonner"; -import { switchApi, Provider, AppType } from "@/lib/api/switch"; +import { + switchApi, + Provider, + AppType, + SyncCheckResult, +} from "@/lib/api/switch"; export function useSwitch(appType: AppType) { const [providers, setProviders] = useState([]); @@ -62,6 +67,29 @@ export function useSwitch(appType: AppType) { toast.success("切换成功"); }; + const checkConfigSync = async (): Promise => { + try { + const result = await switchApi.checkConfigSync(appType); + return result; + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + toast.error("检查同步状态失败: " + message); + throw e; + } + }; + + const syncFromExternal = async (): Promise => { + try { + const message = await switchApi.syncFromExternal(appType); + await fetchProviders(); // 刷新数据 + toast.success(message); + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + toast.error("同步失败: " + message); + throw e; + } + }; + return { providers, currentProvider, @@ -72,5 +100,7 @@ export function useSwitch(appType: AppType) { deleteProvider, switchToProvider, refresh: fetchProviders, + checkConfigSync, + syncFromExternal, }; } diff --git a/src/lib/api/switch.ts b/src/lib/api/switch.ts index 177f2a3bc..9ff19305a 100644 --- a/src/lib/api/switch.ts +++ b/src/lib/api/switch.ts @@ -17,6 +17,25 @@ export interface Provider { // proxycast 保留用于内部配置存储,但不在 UI 的 Tab 中显示 export type AppType = "claude" | "codex" | "gemini" | "proxycast"; +// 同步状态枚举 +export type SyncStatus = "InSync" | "OutOfSync" | "Conflict"; + +// 配置冲突信息 +export interface ConfigConflict { + field: string; + local_value: string; + external_value: string; +} + +// 同步检查结果 +export interface SyncCheckResult { + status: SyncStatus; + current_provider: string; + external_provider: string; + last_modified?: string; + conflicts: ConfigConflict[]; +} + export const switchApi = { getProviders: (appType: AppType): Promise => invoke("get_switch_providers", { appType }), @@ -39,4 +58,12 @@ export const switchApi = { /** 读取当前生效的配置(从实际配置文件读取) */ readLiveSettings: (appType: AppType): Promise> => invoke("read_live_provider_settings", { appType }), + + /** 检查配置同步状态 */ + checkConfigSync: (appType: AppType): Promise => + invoke("check_config_sync_status", { appType }), + + /** 从外部配置同步到 ProxyCast */ + syncFromExternal: (appType: AppType): Promise => + invoke("sync_from_external_config", { appType }), };