diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d3ede68..b922d9d 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -98,6 +98,49 @@ struct GuiConfigState { inner: Mutex, } +#[derive(Default)] +struct AgentConfigStatusCache { + refresh_lock: Mutex<()>, + entry: Mutex>, +} + +struct AgentConfigStatusCacheEntry { + port: u16, + statuses: Vec, +} + +impl AgentConfigStatusCache { + fn get(&self, port: u16) -> Result>, String> { + self.entry + .lock() + .map(|entry| { + entry + .as_ref() + .filter(|entry| entry.port == port) + .map(|entry| entry.statuses.clone()) + }) + .map_err(|_| "智能体配置状态缓存锁已损坏".to_string()) + } + + fn replace(&self, port: u16, statuses: Vec) -> Result<(), String> { + let mut current = self + .entry + .lock() + .map_err(|_| "智能体配置状态缓存锁已损坏".to_string())?; + *current = Some(AgentConfigStatusCacheEntry { port, statuses }); + Ok(()) + } + + fn clear(&self) -> Result<(), String> { + let mut current = self + .entry + .lock() + .map_err(|_| "智能体配置状态缓存锁已损坏".to_string())?; + *current = None; + Ok(()) + } +} + #[derive(Clone, Serialize)] #[serde(rename_all = "camelCase")] struct CorePlatform { @@ -864,16 +907,14 @@ fn get_gui_settings( Ok(GuiSettings::from(&config)) } -#[tauri::command] -fn get_agent_config_statuses( - app: tauri::AppHandle, - gui_config_state: tauri::State<'_, GuiConfigState>, +fn inspect_agent_config_statuses( + app: &tauri::AppHandle, + port: u16, ) -> Result, String> { let home = app .path() .home_dir() .map_err(|error| format!("无法获取用户目录: {error}"))?; - let port = gui_config_state.snapshot()?.port; Ok([ AgentClient::ClaudeCode, AgentClient::ClaudeDesktop, @@ -887,6 +928,49 @@ fn get_agent_config_statuses( .collect()) } +fn refresh_agent_config_status_cache( + app: &tauri::AppHandle, + gui_config_state: &GuiConfigState, + cache: &AgentConfigStatusCache, +) -> Result, String> { + let _refresh_guard = cache + .refresh_lock + .lock() + .map_err(|_| "智能体配置状态刷新锁已损坏".to_string())?; + let port = gui_config_state.snapshot()?.port; + let statuses = inspect_agent_config_statuses(app, port)?; + cache.replace(port, statuses.clone())?; + Ok(statuses) +} + +#[tauri::command] +fn get_agent_config_statuses( + app: tauri::AppHandle, + gui_config_state: tauri::State<'_, GuiConfigState>, + cache: tauri::State<'_, AgentConfigStatusCache>, +) -> Result, String> { + { + let _refresh_guard = cache + .refresh_lock + .lock() + .map_err(|_| "智能体配置状态刷新锁已损坏".to_string())?; + let port = gui_config_state.snapshot()?.port; + if let Some(statuses) = cache.get(port)? { + return Ok(statuses); + } + } + refresh_agent_config_status_cache(&app, gui_config_state.inner(), cache.inner()) +} + +#[tauri::command] +fn refresh_agent_config_statuses( + app: tauri::AppHandle, + gui_config_state: tauri::State<'_, GuiConfigState>, + cache: tauri::State<'_, AgentConfigStatusCache>, +) -> Result, String> { + refresh_agent_config_status_cache(&app, gui_config_state.inner(), cache.inner()) +} + #[tauri::command] async fn get_agent_models( gui_config_state: tauri::State<'_, GuiConfigState>, @@ -4254,12 +4338,17 @@ fn detect_lan_ipv4() -> Option { #[tauri::command] fn save_gui_settings( gui_config_state: tauri::State<'_, GuiConfigState>, + cache: tauri::State<'_, AgentConfigStatusCache>, settings: GuiNetworkSettings, ) -> Result { if settings.port == 0 { return Err("端口必须在 1 到 65535 之间".to_string()); } + let _refresh_guard = cache + .refresh_lock + .lock() + .map_err(|_| "智能体配置状态刷新锁已损坏".to_string())?; let previous = gui_config_state.snapshot()?; let mut next = previous.clone(); next.port = settings.port; @@ -4278,6 +4367,12 @@ fn save_gui_settings( } }; + if config.port != previous.port { + if let Err(error) = cache.clear() { + eprintln!("清空智能体配置状态缓存失败: {error}"); + } + } + Ok(GuiSettings::from(&config)) } @@ -8797,7 +8892,8 @@ fn main() { .manage(CoreDownloadState::default()) .manage(CoreProcessState::default()) .manage(usage::UsageCollectorState::default()) - .manage(GuiConfigState::new(gui_config)); + .manage(GuiConfigState::new(gui_config)) + .manage(AgentConfigStatusCache::default()); #[cfg(target_os = "macos")] let app = app.on_window_event(|window, event| { @@ -8828,6 +8924,19 @@ fn main() { usage::start_usage_collector(usage_app); }); + let agent_status_app = app.handle().clone(); + tauri::async_runtime::spawn_blocking(move || { + let gui_config_state = agent_status_app.state::(); + let cache = agent_status_app.state::(); + if let Err(error) = refresh_agent_config_status_cache( + &agent_status_app, + gui_config_state.inner(), + cache.inner(), + ) { + eprintln!("后台刷新智能体配置状态失败: {error}"); + } + }); + let core_app = app.handle().clone(); tauri::async_runtime::spawn_blocking(move || { let gui_config_state = core_app.state::(); @@ -8857,6 +8966,7 @@ fn main() { get_core_status, get_gui_settings, get_agent_config_statuses, + refresh_agent_config_statuses, get_agent_models, get_thinking_aliases, get_thinking_alias_sources, @@ -8913,6 +9023,18 @@ fn main() { mod tests { use super::*; + #[test] + fn agent_status_cache_requires_matching_port() { + let cache = AgentConfigStatusCache::default(); + cache.replace(8317, Vec::new()).unwrap(); + + assert!(cache.get(8317).unwrap().is_some_and(|statuses| statuses.is_empty())); + assert!(cache.get(8318).unwrap().is_none()); + + cache.clear().unwrap(); + assert!(cache.get(8317).unwrap().is_none()); + } + fn agent_test_home(name: &str) -> PathBuf { let path = std::env::temp_dir().join(format!( "cpa-gui-agent-{name}-{}-{}", diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index a121cbd..8873457 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -462,8 +462,11 @@ export function AgentsPage() { const [notice, setNotice] = useState(''); const [restoreConflict, setRestoreConflict] = useState(null); - const loadStatuses = useCallback(async () => { - const nextStatuses = await invoke('get_agent_config_statuses'); + const loadStatuses = useCallback(async (forceRefresh = false) => { + const command = forceRefresh + ? 'refresh_agent_config_statuses' + : 'get_agent_config_statuses'; + const nextStatuses = await invoke(command); setStatuses(nextStatuses); }, []); @@ -489,7 +492,7 @@ export function AgentsPage() { setLoading(true); setError(''); try { - await Promise.all([loadStatuses(), loadModels()]); + await Promise.all([loadStatuses(true), loadModels()]); } catch (requestError) { setError(String(requestError)); } finally { @@ -498,8 +501,12 @@ export function AgentsPage() { }, [loadModels, loadStatuses]); useEffect(() => { - void refresh(); - }, [refresh]); + setLoading(true); + setError(''); + void Promise.all([loadStatuses(), loadModels()]) + .catch((requestError) => setError(String(requestError))) + .finally(() => setLoading(false)); + }, [loadModels, loadStatuses]); useEffect(() => { writeSelectedAgentClient(selected); @@ -603,7 +610,7 @@ export function AgentsPage() { setNotice(result.enabled ? `${activeDefinition.name} 已启用配置修改;退出软件前建议关闭“修改智能体配置”,恢复原配置文件` : `${activeDefinition.name} 的原配置已恢复,请重启客户端`); - await loadStatuses(); + await loadStatuses(true); } catch (requestError) { setError(String(requestError)); } finally { @@ -640,7 +647,7 @@ export function AgentsPage() { : activeStatus?.modificationEnabled ? `${activeDefinition.name} 已启动;退出软件前建议关闭“修改智能体配置”,恢复原配置文件` : `${activeDefinition.name} 已启动`); - if (appliedBeforeLaunch) await loadStatuses(); + if (appliedBeforeLaunch) await loadStatuses(true); } catch (requestError) { setError(String(requestError)); } finally {