1 line
558 KiB
JSON
1 line
558 KiB
JSON
{"repo": "cacr92/wereply", "files": {"src-tauri/src/secret.rs": {"6a2debd152a2a8850bdb14c09a678e0c9ae5c792": {"t": 1785745833.5541017, "n": 1271, "h": "c59b368f3dae0425", "c": "use anyhow::{Context, Result};\nuse keyring::Entry;\n\nconst SERVICE_NAME: &str = \"wereply\";\nconst API_KEY_NAME: &str = \"deepseek_api_key\";\npub struct ApiKeyManager;\n\nimpl ApiKeyManager {\n pub fn get_deepseek_api_key() -> Result<String> {\n let entry = Entry::new(SERVICE_NAME, API_KEY_NAME)\n .context(\"初始化系统密钥链失败\")?;\n entry\n .get_password()\n .context(\"未找到 DeepSeek API 密钥,请在设置中配置\")\n }\n\n pub fn set_deepseek_api_key(api_key: &str) -> Result<()> {\n if !api_key.starts_with(\"sk-\") {\n anyhow::bail!(\"DeepSeek API 密钥格式错误\");\n }\n let entry = Entry::new(SERVICE_NAME, API_KEY_NAME)\n .context(\"初始化系统密钥链失败\")?;\n entry\n .set_password(api_key)\n .context(\"保存 API 密钥失败\")?;\n Ok(())\n }\n\n pub fn delete_deepseek_api_key() -> Result<()> {\n let entry = Entry::new(SERVICE_NAME, API_KEY_NAME)\n .context(\"初始化系统密钥链失败\")?;\n entry\n .delete_password()\n .context(\"删除 API 密钥失败\")?;\n Ok(())\n }\n\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn reject_invalid_key_format() {\n let result = ApiKeyManager::set_deepseek_api_key(\"invalid-key\");\n assert!(result.is_err());\n }\n\n}\n"}, "d78359994f5ebfd7511992cd4bf234e555af47cd": {"t": 1785757008.1618214, "n": 1271, "h": "c59b368f3dae0425", "c": "use anyhow::{Context, Result};\nuse keyring::Entry;\n\nconst SERVICE_NAME: &str = \"wereply\";\nconst API_KEY_NAME: &str = \"deepseek_api_key\";\npub struct ApiKeyManager;\n\nimpl ApiKeyManager {\n pub fn get_deepseek_api_key() -> Result<String> {\n let entry = Entry::new(SERVICE_NAME, API_KEY_NAME)\n .context(\"初始化系统密钥链失败\")?;\n entry\n .get_password()\n .context(\"未找到 DeepSeek API 密钥,请在设置中配置\")\n }\n\n pub fn set_deepseek_api_key(api_key: &str) -> Result<()> {\n if !api_key.starts_with(\"sk-\") {\n anyhow::bail!(\"DeepSeek API 密钥格式错误\");\n }\n let entry = Entry::new(SERVICE_NAME, API_KEY_NAME)\n .context(\"初始化系统密钥链失败\")?;\n entry\n .set_password(api_key)\n .context(\"保存 API 密钥失败\")?;\n Ok(())\n }\n\n pub fn delete_deepseek_api_key() -> Result<()> {\n let entry = Entry::new(SERVICE_NAME, API_KEY_NAME)\n .context(\"初始化系统密钥链失败\")?;\n entry\n .delete_password()\n .context(\"删除 API 密钥失败\")?;\n Ok(())\n }\n\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn reject_invalid_key_format() {\n let result = ApiKeyManager::set_deepseek_api_key(\"invalid-key\");\n assert!(result.is_err());\n }\n\n}\n"}, "35c2b998f21bc5e0a75fe2228ab6c54e0075287c": {"t": 1785757009.5194628, "n": 2589, "h": "b72b78d865dc8f37", "c": "use anyhow::{Context, Result};\nuse keyring::Entry;\n\nconst SERVICE_NAME: &str = \"wereply\";\nconst API_KEY_NAME: &str = \"deepseek_api_key\";\nconst WECHAT_DB_KEY_NAME: &str = \"wechat_db_key\";\n\npub struct ApiKeyManager;\n\nimpl ApiKeyManager {\n pub fn get_deepseek_api_key() -> Result<String> {\n let entry = Entry::new(SERVICE_NAME, API_KEY_NAME)\n .context(\"初始化系统密钥链失败\")?;\n entry\n .get_password()\n .context(\"未找到 DeepSeek API 密钥,请在设置中配置\")\n }\n\n pub fn set_deepseek_api_key(api_key: &str) -> Result<()> {\n if !api_key.starts_with(\"sk-\") {\n anyhow::bail!(\"DeepSeek API 密钥格式错误\");\n }\n let entry = Entry::new(SERVICE_NAME, API_KEY_NAME)\n .context(\"初始化系统密钥链失败\")?;\n entry\n .set_password(api_key)\n .context(\"保存 API 密钥失败\")?;\n Ok(())\n }\n\n pub fn delete_deepseek_api_key() -> Result<()> {\n let entry = Entry::new(SERVICE_NAME, API_KEY_NAME)\n .context(\"初始化系统密钥链失败\")?;\n entry\n .delete_password()\n .context(\"删除 API 密钥失败\")?;\n Ok(())\n }\n\n pub fn get_wechat_db_key() -> Result<String> {\n let entry = Entry::new(SERVICE_NAME, WECHAT_DB_KEY_NAME)\n .context(\"初始化系统密钥链失败\")?;\n entry\n .get_password()\n .context(\"未找到 WeChat 数据库密钥\")\n }\n\n pub fn set_wechat_db_key(key_hex: &str) -> Result<()> {\n if !is_hex_string(key_hex) {\n anyhow::bail!(\"WeChat 数据库密钥格式错误\");\n }\n let entry = Entry::new(SERVICE_NAME, WECHAT_DB_KEY_NAME)\n .context(\"初始化系统密钥链失败\")?;\n entry\n .set_password(key_hex)\n .context(\"保存 WeChat 数据库密钥失败\")?;\n Ok(())\n }\n\n #[allow(dead_code)]\n pub fn delete_wechat_db_key() -> Result<()> {\n let entry = Entry::new(SERVICE_NAME, WECHAT_DB_KEY_NAME)\n .context(\"初始化系统密钥链失败\")?;\n entry\n .delete_password()\n .context(\"删除 WeChat 数据库密钥失败\")?;\n Ok(())\n }\n}\n\nfn is_hex_string(input: &str) -> bool {\n if input.is_empty() || !input.len().is_multiple_of(2) {\n return false;\n }\n input.chars().all(|ch| ch.is_ascii_hexdigit())\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn reject_invalid_key_format() {\n let result = ApiKeyManager::set_deepseek_api_key(\"invalid-key\");\n assert!(result.is_err());\n }\n\n #[test]\n fn reject_invalid_wechat_key_format() {\n assert!(!is_hex_string(\"zz11\"));\n assert!(!is_hex_string(\"123\"));\n assert!(is_hex_string(\"aabbcc\"));\n }\n}\n"}, "49b771fb17bdfd82cbd11d5fe95044d8144d5094": {"t": 1785757010.8604944, "n": 1270, "h": "73406cc1226f3f18", "c": "use anyhow::{Context, Result};\nuse keyring::Entry;\n\nconst SERVICE_NAME: &str = \"wereply\";\nconst API_KEY_NAME: &str = \"deepseek_api_key\";\n\npub struct ApiKeyManager;\n\nimpl ApiKeyManager {\n pub fn get_deepseek_api_key() -> Result<String> {\n let entry = Entry::new(SERVICE_NAME, API_KEY_NAME)\n .context(\"初始化系统密钥链失败\")?;\n entry\n .get_password()\n .context(\"未找到 DeepSeek API 密钥,请在设置中配置\")\n }\n\n pub fn set_deepseek_api_key(api_key: &str) -> Result<()> {\n if !api_key.starts_with(\"sk-\") {\n anyhow::bail!(\"DeepSeek API 密钥格式错误\");\n }\n let entry = Entry::new(SERVICE_NAME, API_KEY_NAME)\n .context(\"初始化系统密钥链失败\")?;\n entry\n .set_password(api_key)\n .context(\"保存 API 密钥失败\")?;\n Ok(())\n }\n\n pub fn delete_deepseek_api_key() -> Result<()> {\n let entry = Entry::new(SERVICE_NAME, API_KEY_NAME)\n .context(\"初始化系统密钥链失败\")?;\n entry\n .delete_password()\n .context(\"删除 API 密钥失败\")?;\n Ok(())\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn reject_invalid_key_format() {\n let result = ApiKeyManager::set_deepseek_api_key(\"invalid-key\");\n assert!(result.is_err());\n }\n}\n"}}, "src-tauri/src/message_pipeline.rs": {"6a2debd152a2a8850bdb14c09a678e0c9ae5c792": {"t": 1785745866.176238, "n": 3080, "h": "f688fa985424df02", "c": "use crate::deepseek;\nuse crate::ipc::{validate_message_new, MessageNewPayload};\nuse crate::secret::ApiKeyManager;\nuse crate::state::{AppState, ChatMessage};\nuse crate::types::{ErrorPayload, RuntimeState, SuggestionsUpdated};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter};\nuse tokio::sync::Mutex;\nuse tracing::{info, warn};\n\npub async fn handle_incoming_message(\n app: &AppHandle,\n state: &Arc<Mutex<AppState>>,\n payload: MessageNewPayload,\n) {\n if let Err(err) = validate_message_new(&payload) {\n warn!(\"消息验证失败: {}\", err);\n return;\n }\n if is_duplicate_message(state, &payload).await {\n return;\n }\n record_message(state, &payload).await;\n info!(\"收到新消息,生成回复建议\");\n update_state(state, app, RuntimeState::Generating, \"\").await;\n let context = {\n let guard = state.lock().await;\n guard.context_for_chat(&payload.chat_id)\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let app_handle = app.clone();\n let state_handle = state.clone();\n tokio::spawn(async move {\n let api_key = ApiKeyManager::get_deepseek_api_key().ok();\n let suggestions = deepseek::generate_suggestions(&config, api_key, &context)\n .await\n .unwrap_or_else(|_| Vec::new());\n if suggestions.is_empty() {\n warn!(\"生成建议为空\");\n emit_error(\n &app_handle,\n ErrorPayload {\n code: \"SUGGESTION_EMPTY\".to_string(),\n message: \"未生成回复建议\".to_string(),\n recoverable: true,\n },\n );\n } else {\n info!(\"生成建议完成: {} 条\", suggestions.len());\n let payload = SuggestionsUpdated {\n chat_id: payload.chat_id.clone(),\n suggestions,\n };\n let _ = app_handle.emit(\"suggestions.updated\", payload);\n }\n update_state(&state_handle, &app_handle, RuntimeState::Listening, \"\").await;\n });\n}\n\nasync fn is_duplicate_message(state: &Arc<Mutex<AppState>>, payload: &MessageNewPayload) -> bool {\n let guard = state.lock().await;\n guard.is_duplicate(\n &payload.chat_id,\n &payload.msg_id,\n &payload.text,\n payload.timestamp,\n )\n}\n\nasync fn record_message(state: &Arc<Mutex<AppState>>, payload: &MessageNewPayload) {\n let mut guard = state.lock().await;\n guard.record_message(\n &payload.chat_id,\n ChatMessage {\n text: payload.text.clone(),\n timestamp: payload.timestamp,\n msg_id: payload.msg_id.clone(),\n },\n );\n}\n\nasync fn update_state(\n state: &Arc<Mutex<AppState>>,\n app: &AppHandle,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn emit_error(app: &AppHandle, payload: ErrorPayload) {\n let _ = app.emit(\"error.raised\", payload);\n}\n"}, "7dd77db3e172aaa1b719cf934bd9a426eb44665e": {"t": 1785757006.2631085, "n": 3080, "h": "f688fa985424df02", "c": "use crate::deepseek;\nuse crate::ipc::{validate_message_new, MessageNewPayload};\nuse crate::secret::ApiKeyManager;\nuse crate::state::{AppState, ChatMessage};\nuse crate::types::{ErrorPayload, RuntimeState, SuggestionsUpdated};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter};\nuse tokio::sync::Mutex;\nuse tracing::{info, warn};\n\npub async fn handle_incoming_message(\n app: &AppHandle,\n state: &Arc<Mutex<AppState>>,\n payload: MessageNewPayload,\n) {\n if let Err(err) = validate_message_new(&payload) {\n warn!(\"消息验证失败: {}\", err);\n return;\n }\n if is_duplicate_message(state, &payload).await {\n return;\n }\n record_message(state, &payload).await;\n info!(\"收到新消息,生成回复建议\");\n update_state(state, app, RuntimeState::Generating, \"\").await;\n let context = {\n let guard = state.lock().await;\n guard.context_for_chat(&payload.chat_id)\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let app_handle = app.clone();\n let state_handle = state.clone();\n tokio::spawn(async move {\n let api_key = ApiKeyManager::get_deepseek_api_key().ok();\n let suggestions = deepseek::generate_suggestions(&config, api_key, &context)\n .await\n .unwrap_or_else(|_| Vec::new());\n if suggestions.is_empty() {\n warn!(\"生成建议为空\");\n emit_error(\n &app_handle,\n ErrorPayload {\n code: \"SUGGESTION_EMPTY\".to_string(),\n message: \"未生成回复建议\".to_string(),\n recoverable: true,\n },\n );\n } else {\n info!(\"生成建议完成: {} 条\", suggestions.len());\n let payload = SuggestionsUpdated {\n chat_id: payload.chat_id.clone(),\n suggestions,\n };\n let _ = app_handle.emit(\"suggestions.updated\", payload);\n }\n update_state(&state_handle, &app_handle, RuntimeState::Listening, \"\").await;\n });\n}\n\nasync fn is_duplicate_message(state: &Arc<Mutex<AppState>>, payload: &MessageNewPayload) -> bool {\n let guard = state.lock().await;\n guard.is_duplicate(\n &payload.chat_id,\n &payload.msg_id,\n &payload.text,\n payload.timestamp,\n )\n}\n\nasync fn record_message(state: &Arc<Mutex<AppState>>, payload: &MessageNewPayload) {\n let mut guard = state.lock().await;\n guard.record_message(\n &payload.chat_id,\n ChatMessage {\n text: payload.text.clone(),\n timestamp: payload.timestamp,\n msg_id: payload.msg_id.clone(),\n },\n );\n}\n\nasync fn update_state(\n state: &Arc<Mutex<AppState>>,\n app: &AppHandle,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn emit_error(app: &AppHandle, payload: ErrorPayload) {\n let _ = app.emit(\"error.raised\", payload);\n}\n"}}, "src-tauri/src/lib.rs": {"6a2debd152a2a8850bdb14c09a678e0c9ae5c792": {"t": 1785745878.0914934, "n": 32131, "h": "166f439f9959d04a", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod message_pipeline;\nmod secret;\nmod state;\nmod types;\nmod ui_automation;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ui_automation::build_platform_automation;\nuse crate::ipc::{\n ChatsListPayload, InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload,\n};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status, UiPathStep, UiPathsStatus, UiTreeExport, UiTreeLearnResult,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, LogicalSize, Manager, Size, State};\nuse tokio::sync::{Mutex, oneshot, watch};\nuse tokio::time::{timeout, Duration};\nuse uuid::Uuid;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n let (automation, targets) = {\n let guard = state.lock().await;\n (guard.automation.clone(), guard.listen_targets.clone())\n };\n if automation.is_ready() {\n info!(\n \"使用本地自动化路径启动监听: targets={}\",\n targets.len()\n );\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"本地自动化监听已启动\");\n } else {\n warn!(\"本地自动化监听启动失败: {}\", res.message);\n }\n return Ok(res);\n }\n\n info!(\"使用 Agent 路径启动监听\");\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"Agent 已连接,发送监听指令\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.start\", true, true).await\n {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.stop_listening().await;\n if res.success {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n }\n return Ok(res);\n }\n\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.stop\", false, false).await\n {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n return Ok(api_ok(()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.pause\", false, false).await\n {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let targets = {\n let guard = state.lock().await;\n guard.listen_targets.clone()\n };\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n }\n return Ok(res);\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.resume\", true, true).await\n {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let sender = {\n let mut guard = state.lock().await;\n let mut next_config = guard.config.clone();\n next_config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &next_config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n guard.config = next_config;\n guard.listen_targets = normalized.clone();\n guard.agent.as_ref().map(|agent| agent.clone_sender())\n };\n\n if let Some(sender) = sender {\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n list_recent_chats_inner(state.inner().clone()).await\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn export_wechat_ui_tree(\n _state: State<'_, SharedState>,\n max_depth: Option<u32>,\n output_path: Option<String>,\n) -> Result<ApiResponse<UiTreeExport>, String> {\n #[cfg(not(target_os = \"macos\"))]\n {\n let _ = app;\n return Ok(api_err(\"仅支持 macOS\"));\n }\n\n #[cfg(target_os = \"macos\")]\n {\n let depth = max_depth.unwrap_or(10).min(32) as usize;\n let output_path = output_path\n .map(|path| path.trim().to_string())\n .filter(|path| !path.is_empty());\n let result = tokio::task::spawn_blocking(move || -> anyhow::Result<UiTreeExport> {\n if !crate::ui_automation::macos::ax::check_accessibility() {\n anyhow::bail!(\"缺少辅助功能权限\");\n }\n let client = crate::ui_automation::macos::AxClient::new()?;\n let window = client\n .front_window()\n .ok_or_else(|| anyhow::anyhow!(\"WeChat window not found\"))?;\n let tree = crate::ui_automation::macos::ax::snapshot_tree(&window, depth);\n let json = serde_json::to_string_pretty(&tree)?;\n if let Some(path) = output_path.as_ref() {\n std::fs::write(path, &json)?;\n }\n Ok(UiTreeExport {\n json,\n saved_to: output_path,\n })\n })\n .await;\n\n match result {\n Ok(Ok(payload)) => Ok(api_ok(payload)),\n Ok(Err(err)) => Ok(api_err(err.to_string())),\n Err(err) => Ok(api_err(format!(\"Automation task failed: {}\", err))),\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn learn_wechat_ui_paths(\n app: AppHandle,\n _state: State<'_, SharedState>,\n max_depth: Option<u32>,\n output_path: Option<String>,\n) -> Result<ApiResponse<UiTreeLearnResult>, String> {\n #[cfg(not(target_os = \"macos\"))]\n {\n return Ok(api_err(\"仅支持 macOS\"));\n }\n\n #[cfg(target_os = \"macos\")]\n {\n let depth = max_depth.unwrap_or(10).min(32) as usize;\n let output_path = output_path\n .map(|path| path.trim().to_string())\n .filter(|path| !path.is_empty());\n let result = tokio::task::spawn_blocking(move || -> anyhow::Result<UiTreeLearnResult> {\n if !crate::ui_automation::macos::ax::check_accessibility() {\n anyhow::bail!(\"缺少辅助功能权限\");\n }\n let client = crate::ui_automation::macos::AxClient::new()?;\n let window = client\n .front_window()\n .ok_or_else(|| anyhow::anyhow!(\"WeChat window not found\"))?;\n let tree = crate::ui_automation::macos::ax::snapshot_tree(&window, depth);\n let json = serde_json::to_string_pretty(&tree)?;\n let root = crate::ui_automation::macos::ax_learn::parse_snapshot(&tree)\n .ok_or_else(|| anyhow::anyhow!(\"解析 UI 树失败\"))?;\n let learned = crate::ui_automation::macos::ax_learn::learn_paths(&root)\n .map_err(|err| anyhow::anyhow!(err))?;\n let mut written_files = Vec::new();\n match crate::ui_automation::macos::ax_learn::find_repo_root() {\n Ok(repo_root) => {\n let rust_path = crate::ui_automation::macos::ax_learn::write_static_paths_rs(\n &repo_root,\n &learned,\n )\n .map_err(|err| anyhow::anyhow!(err))?;\n let swift_path =\n crate::ui_automation::macos::ax_learn::update_swift_paths(\n &repo_root,\n &learned,\n )\n .map_err(|err| anyhow::anyhow!(err))?;\n written_files.push(rust_path.to_string_lossy().to_string());\n written_files.push(swift_path.to_string_lossy().to_string());\n }\n Err(err) => {\n tracing::warn!(\"跳过静态 UI 路径更新: {}\", err);\n }\n }\n let saved_files =\n crate::ui_automation::macos::ui_paths_store::save_learned_paths(\n &app,\n &learned,\n &json,\n )\n .map_err(|err| anyhow::anyhow!(err))?;\n written_files.extend(saved_files);\n if let Some(path) = output_path.as_ref() {\n std::fs::write(path, &json)?;\n written_files.push(path.clone());\n }\n let session_list_path = learned\n .session_list\n .into_iter()\n .map(|step| UiPathStep {\n roles: step.roles,\n index: step.index as u32,\n title_contains: step.title_contains,\n })\n .collect();\n let message_list_path = learned\n .message_list\n .into_iter()\n .map(|step| UiPathStep {\n roles: step.roles,\n index: step.index as u32,\n title_contains: step.title_contains,\n })\n .collect();\n let input_path = learned\n .input\n .into_iter()\n .map(|step| UiPathStep {\n roles: step.roles,\n index: step.index as u32,\n title_contains: step.title_contains,\n })\n .collect();\n Ok(UiTreeLearnResult {\n json,\n session_list_path,\n message_list_path,\n input_path,\n written_files,\n })\n })\n .await;\n\n match result {\n Ok(Ok(payload)) => Ok(api_ok(payload)),\n Ok(Err(err)) => Ok(api_err(err.to_string())),\n Err(err) => Ok(api_err(format!(\"Automation task failed: {}\", err))),\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_wechat_ui_paths_status(\n app: AppHandle,\n _state: State<'_, SharedState>,\n) -> Result<ApiResponse<UiPathsStatus>, String> {\n #[cfg(not(target_os = \"macos\"))]\n {\n let _ = app;\n return Ok(api_err(\"仅支持 macOS\"));\n }\n\n #[cfg(target_os = \"macos\")]\n {\n match crate::ui_automation::macos::ui_paths_store::read_status(&app) {\n Ok(status) => Ok(api_ok(status)),\n Err(err) => Ok(api_err(err)),\n }\n }\n}\n\nasync fn list_recent_chats_inner(\n state: SharedState,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.list_recent_chats().await;\n if res.success {\n if let Some(chats) = res.data.clone() {\n let mut guard = state.lock().await;\n guard.recent_chats = chats;\n }\n }\n return Ok(res);\n }\n\n let request_id = Uuid::new_v4().to_string();\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some((request_id.clone(), tx));\n (sender, rx)\n };\n\n let payload_value =\n serde_json::to_value(ChatsListPayload { request_id: request_id.clone() })\n .map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.write_input(chat_id, text).await;\n return Ok(res);\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key() -> Result<ApiResponse<String>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => api_ok(key),\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n include_targets: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_targets {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n info!(\n \"发送监听控制指令: type={}, poll_interval_ms={}, targets={}\",\n message_type,\n poll_interval_ms\n .map(|value| value.to_string())\n .unwrap_or_else(|| \"-\".to_string()),\n targets.as_ref().map(|items| items.len()).unwrap_or(0)\n );\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nasync fn start_automation_polling(app: AppHandle, state: SharedState) {\n let (stop_tx, mut stop_rx) = watch::channel(false);\n let (automation, config, targets) = {\n let mut guard = state.lock().await;\n if let Some(stop) = guard.automation_stop.take() {\n let _ = stop.send(true);\n }\n guard.automation_stop = Some(stop_tx);\n (\n guard.automation.clone(),\n guard.config.clone(),\n guard.listen_targets.clone(),\n )\n };\n if !automation.is_ready() {\n return;\n }\n tokio::spawn(async move {\n let mut interval = tokio::time::interval(Duration::from_millis(config.poll_interval_ms));\n loop {\n tokio::select! {\n _ = stop_rx.changed() => {\n if *stop_rx.borrow() {\n break;\n }\n }\n _ = interval.tick() => {\n let res = automation.poll_latest_message().await;\n if !res.success {\n continue;\n }\n let Some(message) = res.data.flatten() else {\n continue;\n };\n if !should_handle_message(&message.chat_id, &targets) {\n continue;\n }\n let payload = crate::ipc::MessageNewPayload {\n chat_id: message.chat_id.clone(),\n chat_title: message.chat_id.clone(),\n is_group: infer_is_group(&message.chat_id, &targets),\n sender_name: String::new(),\n text: message.text.clone(),\n timestamp: message.timestamp,\n msg_id: message.msg_id.clone(),\n };\n crate::message_pipeline::handle_incoming_message(&app, &state, payload).await;\n }\n }\n }\n });\n}\n\nasync fn stop_automation_polling(state: SharedState) {\n let stop = {\n let mut guard = state.lock().await;\n guard.automation_stop.take()\n };\n if let Some(stop) = stop {\n let _ = stop.send(true);\n }\n}\n\nfn should_handle_message(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if targets.is_empty() {\n return true;\n }\n targets.iter().any(|target| target.name == chat_id)\n}\n\nfn infer_is_group(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if let Some(target) = targets.iter().find(|target| target.name == chat_id) {\n return matches!(target.kind, crate::types::ChatKind::Group);\n }\n chat_id.contains(\"\\u{7fa4}\")\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\nfn adjust_window_size(app: &AppHandle) {\n let Some(window) = app.get_webview_window(\"main\") else {\n return;\n };\n let monitor = window\n .current_monitor()\n .ok()\n .flatten()\n .or_else(|| window.primary_monitor().ok().flatten());\n let Some(monitor) = monitor else {\n warn!(\"获取显示器信息失败,跳过窗口尺寸调整\");\n return;\n };\n let scale_factor = monitor.scale_factor();\n let logical_size: LogicalSize<f64> = monitor.size().to_logical(scale_factor);\n let target_width = (logical_size.width * 0.42).round();\n let target_height = (logical_size.height * 0.6).round();\n let width = target_width.clamp(560.0, 960.0);\n let height = target_height.clamp(640.0, 900.0);\n if let Err(err) = window.set_size(Size::Logical(LogicalSize { width, height })) {\n warn!(\"窗口尺寸调整失败: {}\", err);\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let mut app_state = AppState::new(config, initial_status());\n let automation = build_platform_automation();\n app_state.automation = crate::ui_automation::AutomationManager::new(automation);\n let state = Arc::new(Mutex::new(app_state));\n app.manage(state);\n #[cfg(target_os = \"macos\")]\n if let Err(err) =\n crate::ui_automation::macos::ui_paths_store::load_from_disk(app.handle())\n {\n warn!(\"加载微信 UI 路径失败: {}\", err);\n }\n adjust_window_size(app.handle());\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n export_wechat_ui_tree,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n get_api_key,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n learn_wechat_ui_paths,\n get_wechat_ui_paths_status,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use crate::types::ChatKind;\n use crate::ui_automation::WeChatAutomation;\n use std::sync::atomic::{AtomicBool, Ordering};\n\n #[tokio::test]\n async fn list_recent_chats_requires_agent() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_rejects_when_pending() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let (tx, _rx) = oneshot::channel();\n {\n let mut guard = state.lock().await;\n guard.pending_chats_list = Some((\"req\".to_string(), tx));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_uses_automation() {\n struct MockAutomation {\n called: Arc<AtomicBool>,\n }\n\n impl WeChatAutomation for MockAutomation {\n fn platform(&self) -> Platform {\n Platform::Windows\n }\n\n fn list_recent_chats(&self) -> anyhow::Result<Vec<ChatSummary>> {\n self.called.store(true, Ordering::SeqCst);\n Ok(vec![ChatSummary {\n chat_id: \"id\".to_string(),\n chat_title: \"title\".to_string(),\n kind: ChatKind::Unknown,\n }])\n }\n\n fn start_listening(&self, _targets: Vec<ListenTarget>) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn stop_listening(&self) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn write_input(&self, _chat_id: &str, _text: &str) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn poll_latest_message(&self) -> anyhow::Result<Option<crate::ui_automation::IncomingMessage>> {\n Ok(None)\n }\n }\n\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let called = Arc::new(AtomicBool::new(false));\n {\n let mut guard = state.lock().await;\n guard.automation =\n crate::ui_automation::AutomationManager::new(Some(Arc::new(MockAutomation {\n called: Arc::clone(&called),\n })));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(result.success);\n assert!(called.load(Ordering::SeqCst));\n }\n}\n"}, "8637ff180ccb6fa9b476937538cc8af44662b0e0": {"t": 1785757008.0008485, "n": 30652, "h": "fd14c521e758b971", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod message_pipeline;\nmod secret;\nmod state;\nmod types;\nmod ui_automation;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ui_automation::build_platform_automation;\nuse crate::ipc::{\n ChatsListPayload, InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload,\n};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status, UiPathStep, UiTreeExport, UiTreeLearnResult,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, LogicalSize, Manager, Size, State};\nuse tokio::sync::{Mutex, oneshot, watch};\nuse tokio::time::{timeout, Duration};\nuse uuid::Uuid;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n let (automation, targets) = {\n let guard = state.lock().await;\n (guard.automation.clone(), guard.listen_targets.clone())\n };\n if automation.is_ready() {\n info!(\n \"使用本地自动化路径启动监听: targets={}\",\n targets.len()\n );\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"本地自动化监听已启动\");\n } else {\n warn!(\"本地自动化监听启动失败: {}\", res.message);\n }\n return Ok(res);\n }\n\n info!(\"使用 Agent 路径启动监听\");\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"Agent 已连接,发送监听指令\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.start\", true, true).await\n {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.stop_listening().await;\n if res.success {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n }\n return Ok(res);\n }\n\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.stop\", false, false).await\n {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n return Ok(api_ok(()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.pause\", false, false).await\n {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let targets = {\n let guard = state.lock().await;\n guard.listen_targets.clone()\n };\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n }\n return Ok(res);\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.resume\", true, true).await\n {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let sender = {\n let mut guard = state.lock().await;\n let mut next_config = guard.config.clone();\n next_config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &next_config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n guard.config = next_config;\n guard.listen_targets = normalized.clone();\n guard.agent.as_ref().map(|agent| agent.clone_sender())\n };\n\n if let Some(sender) = sender {\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n list_recent_chats_inner(state.inner().clone()).await\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn export_wechat_ui_tree(\n _state: State<'_, SharedState>,\n max_depth: Option<u32>,\n output_path: Option<String>,\n) -> Result<ApiResponse<UiTreeExport>, String> {\n #[cfg(not(target_os = \"macos\"))]\n {\n return Ok(api_err(\"仅支持 macOS\"));\n }\n\n #[cfg(target_os = \"macos\")]\n {\n let depth = max_depth.unwrap_or(10).min(32) as usize;\n let output_path = output_path\n .map(|path| path.trim().to_string())\n .filter(|path| !path.is_empty());\n let result = tokio::task::spawn_blocking(move || -> anyhow::Result<UiTreeExport> {\n if !crate::ui_automation::macos::ax::check_accessibility() {\n anyhow::bail!(\"缺少辅助功能权限\");\n }\n let client = crate::ui_automation::macos::AxClient::new()?;\n let window = client\n .front_window()\n .ok_or_else(|| anyhow::anyhow!(\"WeChat window not found\"))?;\n let tree = crate::ui_automation::macos::ax::snapshot_tree(&window, depth);\n let json = serde_json::to_string_pretty(&tree)?;\n if let Some(path) = output_path.as_ref() {\n std::fs::write(path, &json)?;\n }\n Ok(UiTreeExport {\n json,\n saved_to: output_path,\n })\n })\n .await;\n\n match result {\n Ok(Ok(payload)) => Ok(api_ok(payload)),\n Ok(Err(err)) => Ok(api_err(err.to_string())),\n Err(err) => Ok(api_err(format!(\"Automation task failed: {}\", err))),\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn learn_wechat_ui_paths(\n _state: State<'_, SharedState>,\n max_depth: Option<u32>,\n output_path: Option<String>,\n) -> Result<ApiResponse<UiTreeLearnResult>, String> {\n #[cfg(not(target_os = \"macos\"))]\n {\n return Ok(api_err(\"仅支持 macOS\"));\n }\n\n #[cfg(target_os = \"macos\")]\n {\n let depth = max_depth.unwrap_or(10).min(32) as usize;\n let output_path = output_path\n .map(|path| path.trim().to_string())\n .filter(|path| !path.is_empty());\n let result = tokio::task::spawn_blocking(move || -> anyhow::Result<UiTreeLearnResult> {\n if !crate::ui_automation::macos::ax::check_accessibility() {\n anyhow::bail!(\"缺少辅助功能权限\");\n }\n let client = crate::ui_automation::macos::AxClient::new()?;\n let window = client\n .front_window()\n .ok_or_else(|| anyhow::anyhow!(\"WeChat window not found\"))?;\n let tree = crate::ui_automation::macos::ax::snapshot_tree(&window, depth);\n let json = serde_json::to_string_pretty(&tree)?;\n let root = crate::ui_automation::macos::ax_learn::parse_snapshot(&tree)\n .ok_or_else(|| anyhow::anyhow!(\"解析 UI 树失败\"))?;\n let learned = crate::ui_automation::macos::ax_learn::learn_paths(&root)\n .map_err(|err| anyhow::anyhow!(err))?;\n let repo_root = crate::ui_automation::macos::ax_learn::find_repo_root()\n .map_err(|err| anyhow::anyhow!(err))?;\n let rust_path =\n crate::ui_automation::macos::ax_learn::write_static_paths_rs(&repo_root, &learned)\n .map_err(|err| anyhow::anyhow!(err))?;\n let swift_path =\n crate::ui_automation::macos::ax_learn::update_swift_paths(&repo_root, &learned)\n .map_err(|err| anyhow::anyhow!(err))?;\n let mut written_files = vec![\n rust_path.to_string_lossy().to_string(),\n swift_path.to_string_lossy().to_string(),\n ];\n if let Some(path) = output_path.as_ref() {\n std::fs::write(path, &json)?;\n written_files.push(path.clone());\n }\n let session_list_path = learned\n .session_list\n .into_iter()\n .map(|step| UiPathStep {\n roles: step.roles,\n index: step.index as u32,\n title_contains: step.title_contains,\n })\n .collect();\n let message_list_path = learned\n .message_list\n .into_iter()\n .map(|step| UiPathStep {\n roles: step.roles,\n index: step.index as u32,\n title_contains: step.title_contains,\n })\n .collect();\n let input_path = learned\n .input\n .into_iter()\n .map(|step| UiPathStep {\n roles: step.roles,\n index: step.index as u32,\n title_contains: step.title_contains,\n })\n .collect();\n Ok(UiTreeLearnResult {\n json,\n session_list_path,\n message_list_path,\n input_path,\n written_files,\n })\n })\n .await;\n\n match result {\n Ok(Ok(payload)) => Ok(api_ok(payload)),\n Ok(Err(err)) => Ok(api_err(err.to_string())),\n Err(err) => Ok(api_err(format!(\"Automation task failed: {}\", err))),\n }\n }\n}\n\nasync fn list_recent_chats_inner(\n state: SharedState,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.list_recent_chats().await;\n if res.success {\n if let Some(chats) = res.data.clone() {\n let mut guard = state.lock().await;\n guard.recent_chats = chats;\n }\n }\n return Ok(res);\n }\n\n let request_id = Uuid::new_v4().to_string();\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some((request_id.clone(), tx));\n (sender, rx)\n };\n\n let payload_value =\n serde_json::to_value(ChatsListPayload { request_id: request_id.clone() })\n .map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.write_input(chat_id, text).await;\n return Ok(res);\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key() -> Result<ApiResponse<String>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => api_ok(key),\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n include_targets: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_targets {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n info!(\n \"发送监听控制指令: type={}, poll_interval_ms={}, targets={}\",\n message_type,\n poll_interval_ms\n .map(|value| value.to_string())\n .unwrap_or_else(|| \"-\".to_string()),\n targets.as_ref().map(|items| items.len()).unwrap_or(0)\n );\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nasync fn start_automation_polling(app: AppHandle, state: SharedState) {\n let (stop_tx, mut stop_rx) = watch::channel(false);\n let (automation, config, targets) = {\n let mut guard = state.lock().await;\n if let Some(stop) = guard.automation_stop.take() {\n let _ = stop.send(true);\n }\n guard.automation_stop = Some(stop_tx);\n (\n guard.automation.clone(),\n guard.config.clone(),\n guard.listen_targets.clone(),\n )\n };\n if !automation.is_ready() {\n return;\n }\n tokio::spawn(async move {\n let mut interval = tokio::time::interval(Duration::from_millis(config.poll_interval_ms));\n loop {\n tokio::select! {\n _ = stop_rx.changed() => {\n if *stop_rx.borrow() {\n break;\n }\n }\n _ = interval.tick() => {\n let res = automation.poll_latest_message().await;\n if !res.success {\n continue;\n }\n let Some(message) = res.data.flatten() else {\n continue;\n };\n if !should_handle_message(&message.chat_id, &targets) {\n continue;\n }\n let payload = crate::ipc::MessageNewPayload {\n chat_id: message.chat_id.clone(),\n chat_title: message.chat_id.clone(),\n is_group: infer_is_group(&message.chat_id, &targets),\n sender_name: String::new(),\n text: message.text.clone(),\n timestamp: message.timestamp,\n msg_id: message.msg_id.clone(),\n };\n crate::message_pipeline::handle_incoming_message(&app, &state, payload).await;\n }\n }\n }\n });\n}\n\nasync fn stop_automation_polling(state: SharedState) {\n let stop = {\n let mut guard = state.lock().await;\n guard.automation_stop.take()\n };\n if let Some(stop) = stop {\n let _ = stop.send(true);\n }\n}\n\nfn should_handle_message(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if targets.is_empty() {\n return true;\n }\n targets.iter().any(|target| target.name == chat_id)\n}\n\nfn infer_is_group(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if let Some(target) = targets.iter().find(|target| target.name == chat_id) {\n return matches!(target.kind, crate::types::ChatKind::Group);\n }\n chat_id.contains(\"\\u{7fa4}\")\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\nfn adjust_window_size(app: &AppHandle) {\n let Some(window) = app.get_webview_window(\"main\") else {\n return;\n };\n let monitor = window\n .current_monitor()\n .ok()\n .flatten()\n .or_else(|| window.primary_monitor().ok().flatten());\n let Some(monitor) = monitor else {\n warn!(\"获取显示器信息失败,跳过窗口尺寸调整\");\n return;\n };\n let scale_factor = monitor.scale_factor();\n let logical_size: LogicalSize<f64> = monitor.size().to_logical(scale_factor);\n let target_width = (logical_size.width * 0.42).round();\n let target_height = (logical_size.height * 0.6).round();\n let width = target_width.clamp(560.0, 960.0);\n let height = target_height.clamp(640.0, 900.0);\n if let Err(err) = window.set_size(Size::Logical(LogicalSize { width, height })) {\n warn!(\"窗口尺寸调整失败: {}\", err);\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let mut app_state = AppState::new(config, initial_status());\n let automation = build_platform_automation();\n app_state.automation = crate::ui_automation::AutomationManager::new(automation);\n let state = Arc::new(Mutex::new(app_state));\n app.manage(state);\n adjust_window_size(app.handle());\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n export_wechat_ui_tree,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n get_api_key,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n learn_wechat_ui_paths,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use crate::types::ChatKind;\n use crate::ui_automation::WeChatAutomation;\n use std::sync::atomic::{AtomicBool, Ordering};\n\n #[tokio::test]\n async fn list_recent_chats_requires_agent() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_rejects_when_pending() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let (tx, _rx) = oneshot::channel();\n {\n let mut guard = state.lock().await;\n guard.pending_chats_list = Some((\"req\".to_string(), tx));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_uses_automation() {\n struct MockAutomation {\n called: Arc<AtomicBool>,\n }\n\n impl WeChatAutomation for MockAutomation {\n fn platform(&self) -> Platform {\n Platform::Windows\n }\n\n fn list_recent_chats(&self) -> anyhow::Result<Vec<ChatSummary>> {\n self.called.store(true, Ordering::SeqCst);\n Ok(vec![ChatSummary {\n chat_id: \"id\".to_string(),\n chat_title: \"title\".to_string(),\n kind: ChatKind::Unknown,\n }])\n }\n\n fn start_listening(&self, _targets: Vec<ListenTarget>) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn stop_listening(&self) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn write_input(&self, _chat_id: &str, _text: &str) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn poll_latest_message(&self) -> anyhow::Result<Option<crate::ui_automation::IncomingMessage>> {\n Ok(None)\n }\n }\n\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let called = Arc::new(AtomicBool::new(false));\n {\n let mut guard = state.lock().await;\n guard.automation =\n crate::ui_automation::AutomationManager::new(Some(Arc::new(MockAutomation {\n called: Arc::clone(&called),\n })));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(result.success);\n assert!(called.load(Ordering::SeqCst));\n }\n}\n"}, "cab12c3c06bacb90d7f511133906cfccc2098ba8": {"t": 1785757009.540267, "n": 26877, "h": "ce8b3f2330248398", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod message_pipeline;\nmod secret;\nmod state;\nmod types;\nmod ui_automation;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ui_automation::build_platform_automation;\nuse crate::ipc::{\n ChatsListPayload, InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload,\n};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status, UiTreeExport,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, LogicalSize, Manager, Size, State};\nuse tokio::sync::{Mutex, oneshot, watch};\nuse tokio::time::{timeout, Duration};\nuse uuid::Uuid;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n let (automation, targets) = {\n let guard = state.lock().await;\n (guard.automation.clone(), guard.listen_targets.clone())\n };\n if automation.is_ready() {\n info!(\n \"使用本地自动化路径启动监听: targets={}\",\n targets.len()\n );\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"本地自动化监听已启动\");\n } else {\n warn!(\"本地自动化监听启动失败: {}\", res.message);\n }\n return Ok(res);\n }\n\n info!(\"使用 Agent 路径启动监听\");\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"Agent 已连接,发送监听指令\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.start\", true, true).await\n {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.stop_listening().await;\n if res.success {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n }\n return Ok(res);\n }\n\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.stop\", false, false).await\n {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n return Ok(api_ok(()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.pause\", false, false).await\n {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let targets = {\n let guard = state.lock().await;\n guard.listen_targets.clone()\n };\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n }\n return Ok(res);\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.resume\", true, true).await\n {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let sender = {\n let mut guard = state.lock().await;\n let mut next_config = guard.config.clone();\n next_config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &next_config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n guard.config = next_config;\n guard.listen_targets = normalized.clone();\n guard.agent.as_ref().map(|agent| agent.clone_sender())\n };\n\n if let Some(sender) = sender {\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n list_recent_chats_inner(state.inner().clone()).await\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn export_wechat_ui_tree(\n _state: State<'_, SharedState>,\n max_depth: Option<u32>,\n output_path: Option<String>,\n) -> Result<ApiResponse<UiTreeExport>, String> {\n #[cfg(not(target_os = \"macos\"))]\n {\n return Ok(api_err(\"仅支持 macOS\"));\n }\n\n #[cfg(target_os = \"macos\")]\n {\n let depth = max_depth.unwrap_or(10).min(32) as usize;\n let output_path = output_path\n .map(|path| path.trim().to_string())\n .filter(|path| !path.is_empty());\n let result = tokio::task::spawn_blocking(move || -> anyhow::Result<UiTreeExport> {\n if !crate::ui_automation::macos::ax::check_accessibility() {\n anyhow::bail!(\"缺少辅助功能权限\");\n }\n let client = crate::ui_automation::macos::AxClient::new()?;\n let window = client\n .front_window()\n .ok_or_else(|| anyhow::anyhow!(\"WeChat window not found\"))?;\n let tree = crate::ui_automation::macos::ax::snapshot_tree(&window, depth);\n let json = serde_json::to_string_pretty(&tree)?;\n if let Some(path) = output_path.as_ref() {\n std::fs::write(path, &json)?;\n }\n Ok(UiTreeExport {\n json,\n saved_to: output_path,\n })\n })\n .await;\n\n match result {\n Ok(Ok(payload)) => Ok(api_ok(payload)),\n Ok(Err(err)) => Ok(api_err(err.to_string())),\n Err(err) => Ok(api_err(format!(\"Automation task failed: {}\", err))),\n }\n }\n}\n\nasync fn list_recent_chats_inner(\n state: SharedState,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.list_recent_chats().await;\n if res.success {\n if let Some(chats) = res.data.clone() {\n let mut guard = state.lock().await;\n guard.recent_chats = chats;\n }\n }\n return Ok(res);\n }\n\n let request_id = Uuid::new_v4().to_string();\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some((request_id.clone(), tx));\n (sender, rx)\n };\n\n let payload_value =\n serde_json::to_value(ChatsListPayload { request_id: request_id.clone() })\n .map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.write_input(chat_id, text).await;\n return Ok(res);\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key() -> Result<ApiResponse<String>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => api_ok(key),\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n include_targets: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_targets {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n info!(\n \"发送监听控制指令: type={}, poll_interval_ms={}, targets={}\",\n message_type,\n poll_interval_ms\n .map(|value| value.to_string())\n .unwrap_or_else(|| \"-\".to_string()),\n targets.as_ref().map(|items| items.len()).unwrap_or(0)\n );\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nasync fn start_automation_polling(app: AppHandle, state: SharedState) {\n let (stop_tx, mut stop_rx) = watch::channel(false);\n let (automation, config, targets) = {\n let mut guard = state.lock().await;\n if let Some(stop) = guard.automation_stop.take() {\n let _ = stop.send(true);\n }\n guard.automation_stop = Some(stop_tx);\n (\n guard.automation.clone(),\n guard.config.clone(),\n guard.listen_targets.clone(),\n )\n };\n if !automation.is_ready() {\n return;\n }\n tokio::spawn(async move {\n let mut interval = tokio::time::interval(Duration::from_millis(config.poll_interval_ms));\n loop {\n tokio::select! {\n _ = stop_rx.changed() => {\n if *stop_rx.borrow() {\n break;\n }\n }\n _ = interval.tick() => {\n let res = automation.poll_latest_message().await;\n if !res.success {\n continue;\n }\n let Some(message) = res.data.flatten() else {\n continue;\n };\n if !should_handle_message(&message.chat_id, &targets) {\n continue;\n }\n let payload = crate::ipc::MessageNewPayload {\n chat_id: message.chat_id.clone(),\n chat_title: message.chat_id.clone(),\n is_group: infer_is_group(&message.chat_id, &targets),\n sender_name: String::new(),\n text: message.text.clone(),\n timestamp: message.timestamp,\n msg_id: message.msg_id.clone(),\n };\n crate::message_pipeline::handle_incoming_message(&app, &state, payload).await;\n }\n }\n }\n });\n}\n\nasync fn stop_automation_polling(state: SharedState) {\n let stop = {\n let mut guard = state.lock().await;\n guard.automation_stop.take()\n };\n if let Some(stop) = stop {\n let _ = stop.send(true);\n }\n}\n\nfn should_handle_message(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if targets.is_empty() {\n return true;\n }\n targets.iter().any(|target| target.name == chat_id)\n}\n\nfn infer_is_group(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if let Some(target) = targets.iter().find(|target| target.name == chat_id) {\n return matches!(target.kind, crate::types::ChatKind::Group);\n }\n chat_id.contains(\"\\u{7fa4}\")\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\nfn adjust_window_size(app: &AppHandle) {\n let Some(window) = app.get_webview_window(\"main\") else {\n return;\n };\n let monitor = window\n .current_monitor()\n .ok()\n .flatten()\n .or_else(|| window.primary_monitor().ok().flatten());\n let Some(monitor) = monitor else {\n warn!(\"获取显示器信息失败,跳过窗口尺寸调整\");\n return;\n };\n let scale_factor = monitor.scale_factor();\n let logical_size: LogicalSize<f64> = monitor.size().to_logical(scale_factor);\n let target_width = (logical_size.width * 0.42).round();\n let target_height = (logical_size.height * 0.6).round();\n let width = target_width.clamp(560.0, 960.0);\n let height = target_height.clamp(640.0, 900.0);\n if let Err(err) = window.set_size(Size::Logical(LogicalSize { width, height })) {\n warn!(\"窗口尺寸调整失败: {}\", err);\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let mut app_state = AppState::new(config, initial_status());\n let automation = build_platform_automation();\n app_state.automation = crate::ui_automation::AutomationManager::new(automation);\n let state = Arc::new(Mutex::new(app_state));\n app.manage(state);\n adjust_window_size(app.handle());\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n export_wechat_ui_tree,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n get_api_key,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use crate::types::ChatKind;\n use crate::ui_automation::WeChatAutomation;\n use std::sync::atomic::{AtomicBool, Ordering};\n\n #[tokio::test]\n async fn list_recent_chats_requires_agent() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_rejects_when_pending() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let (tx, _rx) = oneshot::channel();\n {\n let mut guard = state.lock().await;\n guard.pending_chats_list = Some((\"req\".to_string(), tx));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_uses_automation() {\n struct MockAutomation {\n called: Arc<AtomicBool>,\n }\n\n impl WeChatAutomation for MockAutomation {\n fn platform(&self) -> Platform {\n Platform::Windows\n }\n\n fn list_recent_chats(&self) -> anyhow::Result<Vec<ChatSummary>> {\n self.called.store(true, Ordering::SeqCst);\n Ok(vec![ChatSummary {\n chat_id: \"id\".to_string(),\n chat_title: \"title\".to_string(),\n kind: ChatKind::Unknown,\n }])\n }\n\n fn start_listening(&self, _targets: Vec<ListenTarget>) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn stop_listening(&self) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn write_input(&self, _chat_id: &str, _text: &str) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn poll_latest_message(&self) -> anyhow::Result<Option<crate::ui_automation::IncomingMessage>> {\n Ok(None)\n }\n }\n\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let called = Arc::new(AtomicBool::new(false));\n {\n let mut guard = state.lock().await;\n guard.automation =\n crate::ui_automation::AutomationManager::new(Some(Arc::new(MockAutomation {\n called: Arc::clone(&called),\n })));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(result.success);\n assert!(called.load(Ordering::SeqCst));\n }\n}\n"}, "970a1467613ca280e8d0aa90a13789c4ca540094": {"t": 1785757011.309856, "n": 25226, "h": "251a0b67fd7421f5", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod message_pipeline;\nmod secret;\nmod state;\nmod types;\nmod ui_automation;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ui_automation::build_platform_automation;\nuse crate::ipc::{\n ChatsListPayload, InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload,\n};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, LogicalSize, Manager, Size, State};\nuse tokio::sync::{Mutex, oneshot, watch};\nuse tokio::time::{timeout, Duration};\nuse uuid::Uuid;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n let (automation, targets) = {\n let guard = state.lock().await;\n (guard.automation.clone(), guard.listen_targets.clone())\n };\n if automation.is_ready() {\n info!(\n \"使用本地自动化路径启动监听: targets={}\",\n targets.len()\n );\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"本地自动化监听已启动\");\n } else {\n warn!(\"本地自动化监听启动失败: {}\", res.message);\n }\n return Ok(res);\n }\n\n info!(\"使用 Agent 路径启动监听\");\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"Agent 已连接,发送监听指令\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.start\", true, true).await\n {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.stop_listening().await;\n if res.success {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n }\n return Ok(res);\n }\n\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.stop\", false, false).await\n {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n return Ok(api_ok(()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.pause\", false, false).await\n {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let targets = {\n let guard = state.lock().await;\n guard.listen_targets.clone()\n };\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n }\n return Ok(res);\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.resume\", true, true).await\n {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let sender = {\n let mut guard = state.lock().await;\n let mut next_config = guard.config.clone();\n next_config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &next_config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n guard.config = next_config;\n guard.listen_targets = normalized.clone();\n guard.agent.as_ref().map(|agent| agent.clone_sender())\n };\n\n if let Some(sender) = sender {\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n list_recent_chats_inner(state.inner().clone()).await\n}\n\nasync fn list_recent_chats_inner(\n state: SharedState,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.list_recent_chats().await;\n if res.success {\n if let Some(chats) = res.data.clone() {\n let mut guard = state.lock().await;\n guard.recent_chats = chats;\n }\n }\n return Ok(res);\n }\n\n let request_id = Uuid::new_v4().to_string();\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some((request_id.clone(), tx));\n (sender, rx)\n };\n\n let payload_value =\n serde_json::to_value(ChatsListPayload { request_id: request_id.clone() })\n .map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.write_input(chat_id, text).await;\n return Ok(res);\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key() -> Result<ApiResponse<String>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => api_ok(key),\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n include_targets: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_targets {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n info!(\n \"发送监听控制指令: type={}, poll_interval_ms={}, targets={}\",\n message_type,\n poll_interval_ms\n .map(|value| value.to_string())\n .unwrap_or_else(|| \"-\".to_string()),\n targets.as_ref().map(|items| items.len()).unwrap_or(0)\n );\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nasync fn start_automation_polling(app: AppHandle, state: SharedState) {\n let (stop_tx, mut stop_rx) = watch::channel(false);\n let (automation, config, targets) = {\n let mut guard = state.lock().await;\n if let Some(stop) = guard.automation_stop.take() {\n let _ = stop.send(true);\n }\n guard.automation_stop = Some(stop_tx);\n (\n guard.automation.clone(),\n guard.config.clone(),\n guard.listen_targets.clone(),\n )\n };\n if !automation.is_ready() {\n return;\n }\n tokio::spawn(async move {\n let mut interval = tokio::time::interval(Duration::from_millis(config.poll_interval_ms));\n loop {\n tokio::select! {\n _ = stop_rx.changed() => {\n if *stop_rx.borrow() {\n break;\n }\n }\n _ = interval.tick() => {\n let res = automation.poll_latest_message().await;\n if !res.success {\n continue;\n }\n let Some(message) = res.data.flatten() else {\n continue;\n };\n if !should_handle_message(&message.chat_id, &targets) {\n continue;\n }\n let payload = crate::ipc::MessageNewPayload {\n chat_id: message.chat_id.clone(),\n chat_title: message.chat_id.clone(),\n is_group: infer_is_group(&message.chat_id, &targets),\n sender_name: String::new(),\n text: message.text.clone(),\n timestamp: message.timestamp,\n msg_id: message.msg_id.clone(),\n };\n crate::message_pipeline::handle_incoming_message(&app, &state, payload).await;\n }\n }\n }\n });\n}\n\nasync fn stop_automation_polling(state: SharedState) {\n let stop = {\n let mut guard = state.lock().await;\n guard.automation_stop.take()\n };\n if let Some(stop) = stop {\n let _ = stop.send(true);\n }\n}\n\nfn should_handle_message(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if targets.is_empty() {\n return true;\n }\n targets.iter().any(|target| target.name == chat_id)\n}\n\nfn infer_is_group(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if let Some(target) = targets.iter().find(|target| target.name == chat_id) {\n return matches!(target.kind, crate::types::ChatKind::Group);\n }\n chat_id.contains(\"\\u{7fa4}\")\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\nfn adjust_window_size(app: &AppHandle) {\n let Some(window) = app.get_webview_window(\"main\") else {\n return;\n };\n let monitor = window\n .current_monitor()\n .ok()\n .flatten()\n .or_else(|| window.primary_monitor().ok().flatten());\n let Some(monitor) = monitor else {\n warn!(\"获取显示器信息失败,跳过窗口尺寸调整\");\n return;\n };\n let scale_factor = monitor.scale_factor();\n let logical_size: LogicalSize<f64> = monitor.size().to_logical(scale_factor);\n let target_width = (logical_size.width * 0.42).round();\n let target_height = (logical_size.height * 0.6).round();\n let width = target_width.clamp(560.0, 960.0);\n let height = target_height.clamp(640.0, 900.0);\n if let Err(err) = window.set_size(Size::Logical(LogicalSize { width, height })) {\n warn!(\"窗口尺寸调整失败: {}\", err);\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let mut app_state = AppState::new(config, initial_status());\n let automation = build_platform_automation();\n app_state.automation = crate::ui_automation::AutomationManager::new(automation);\n let state = Arc::new(Mutex::new(app_state));\n app.manage(state);\n adjust_window_size(app.handle());\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n get_api_key,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use crate::types::ChatKind;\n use crate::ui_automation::WeChatAutomation;\n use std::sync::atomic::{AtomicBool, Ordering};\n\n #[tokio::test]\n async fn list_recent_chats_requires_agent() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_rejects_when_pending() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let (tx, _rx) = oneshot::channel();\n {\n let mut guard = state.lock().await;\n guard.pending_chats_list = Some((\"req\".to_string(), tx));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_uses_automation() {\n struct MockAutomation {\n called: Arc<AtomicBool>,\n }\n\n impl WeChatAutomation for MockAutomation {\n fn platform(&self) -> Platform {\n Platform::Windows\n }\n\n fn list_recent_chats(&self) -> anyhow::Result<Vec<ChatSummary>> {\n self.called.store(true, Ordering::SeqCst);\n Ok(vec![ChatSummary {\n chat_id: \"id\".to_string(),\n chat_title: \"title\".to_string(),\n kind: ChatKind::Unknown,\n }])\n }\n\n fn start_listening(&self, _targets: Vec<ListenTarget>) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn stop_listening(&self) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn write_input(&self, _chat_id: &str, _text: &str) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn poll_latest_message(&self) -> anyhow::Result<Option<crate::ui_automation::IncomingMessage>> {\n Ok(None)\n }\n }\n\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let called = Arc::new(AtomicBool::new(false));\n {\n let mut guard = state.lock().await;\n guard.automation =\n crate::ui_automation::AutomationManager::new(Some(Arc::new(MockAutomation {\n called: Arc::clone(&called),\n })));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(result.success);\n assert!(called.load(Ordering::SeqCst));\n }\n}\n"}, "5c2f586fdb1baec47a8829bf2c657e0dce50e80a": {"t": 1785757012.904428, "n": 24688, "h": "b059ffd39176c022", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod message_pipeline;\nmod secret;\nmod state;\nmod types;\nmod ui_automation;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ui_automation::build_platform_automation;\nuse crate::ipc::{\n ChatsListPayload, InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload,\n};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, LogicalSize, Manager, Size, State};\nuse tokio::sync::{Mutex, oneshot, watch};\nuse tokio::time::{timeout, Duration};\nuse uuid::Uuid;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n let (automation, targets) = {\n let guard = state.lock().await;\n (guard.automation.clone(), guard.listen_targets.clone())\n };\n if automation.is_ready() {\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n }\n return Ok(res);\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.start\", true, true).await\n {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.stop_listening().await;\n if res.success {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n }\n return Ok(res);\n }\n\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.stop\", false, false).await\n {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n return Ok(api_ok(()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.pause\", false, false).await\n {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let targets = {\n let guard = state.lock().await;\n guard.listen_targets.clone()\n };\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n }\n return Ok(res);\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.resume\", true, true).await\n {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let sender = {\n let mut guard = state.lock().await;\n let mut next_config = guard.config.clone();\n next_config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &next_config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n guard.config = next_config;\n guard.listen_targets = normalized.clone();\n guard.agent.as_ref().map(|agent| agent.clone_sender())\n };\n\n if let Some(sender) = sender {\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n list_recent_chats_inner(state.inner().clone()).await\n}\n\nasync fn list_recent_chats_inner(\n state: SharedState,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.list_recent_chats().await;\n if res.success {\n if let Some(chats) = res.data.clone() {\n let mut guard = state.lock().await;\n guard.recent_chats = chats;\n }\n }\n return Ok(res);\n }\n\n let request_id = Uuid::new_v4().to_string();\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some((request_id.clone(), tx));\n (sender, rx)\n };\n\n let payload_value =\n serde_json::to_value(ChatsListPayload { request_id: request_id.clone() })\n .map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.write_input(chat_id, text).await;\n return Ok(res);\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key() -> Result<ApiResponse<String>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => api_ok(key),\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n include_targets: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_targets {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nasync fn start_automation_polling(app: AppHandle, state: SharedState) {\n let (stop_tx, mut stop_rx) = watch::channel(false);\n let (automation, config, targets) = {\n let mut guard = state.lock().await;\n if let Some(stop) = guard.automation_stop.take() {\n let _ = stop.send(true);\n }\n guard.automation_stop = Some(stop_tx);\n (\n guard.automation.clone(),\n guard.config.clone(),\n guard.listen_targets.clone(),\n )\n };\n if !automation.is_ready() {\n return;\n }\n tokio::spawn(async move {\n let mut interval = tokio::time::interval(Duration::from_millis(config.poll_interval_ms));\n loop {\n tokio::select! {\n _ = stop_rx.changed() => {\n if *stop_rx.borrow() {\n break;\n }\n }\n _ = interval.tick() => {\n let res = automation.poll_latest_message().await;\n if !res.success {\n continue;\n }\n let Some(message) = res.data.flatten() else {\n continue;\n };\n if !should_handle_message(&message.chat_id, &targets) {\n continue;\n }\n let payload = crate::ipc::MessageNewPayload {\n chat_id: message.chat_id.clone(),\n chat_title: message.chat_id.clone(),\n is_group: infer_is_group(&message.chat_id, &targets),\n sender_name: String::new(),\n text: message.text.clone(),\n timestamp: message.timestamp,\n msg_id: message.msg_id.clone(),\n };\n crate::message_pipeline::handle_incoming_message(&app, &state, payload).await;\n }\n }\n }\n });\n}\n\nasync fn stop_automation_polling(state: SharedState) {\n let stop = {\n let mut guard = state.lock().await;\n guard.automation_stop.take()\n };\n if let Some(stop) = stop {\n let _ = stop.send(true);\n }\n}\n\nfn should_handle_message(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if targets.is_empty() {\n return true;\n }\n targets.iter().any(|target| target.name == chat_id)\n}\n\nfn infer_is_group(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if let Some(target) = targets.iter().find(|target| target.name == chat_id) {\n return matches!(target.kind, crate::types::ChatKind::Group);\n }\n chat_id.contains(\"\\u{7fa4}\")\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\nfn adjust_window_size(app: &AppHandle) {\n let Some(window) = app.get_webview_window(\"main\") else {\n return;\n };\n let monitor = window\n .current_monitor()\n .ok()\n .flatten()\n .or_else(|| window.primary_monitor().ok().flatten());\n let Some(monitor) = monitor else {\n warn!(\"获取显示器信息失败,跳过窗口尺寸调整\");\n return;\n };\n let scale_factor = monitor.scale_factor();\n let logical_size: LogicalSize<f64> = monitor.size().to_logical(scale_factor);\n let target_width = (logical_size.width * 0.5).round();\n let target_height = (logical_size.height * 0.6).round();\n let width = target_width.clamp(600.0, 1100.0);\n let height = target_height.clamp(640.0, 900.0);\n if let Err(err) = window.set_size(Size::Logical(LogicalSize { width, height })) {\n warn!(\"窗口尺寸调整失败: {}\", err);\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let mut app_state = AppState::new(config, initial_status());\n let automation = build_platform_automation();\n app_state.automation = crate::ui_automation::AutomationManager::new(automation);\n let state = Arc::new(Mutex::new(app_state));\n app.manage(state);\n adjust_window_size(app.handle());\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n get_api_key,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use crate::types::ChatKind;\n use crate::ui_automation::WeChatAutomation;\n use std::sync::atomic::{AtomicBool, Ordering};\n\n #[tokio::test]\n async fn list_recent_chats_requires_agent() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_rejects_when_pending() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let (tx, _rx) = oneshot::channel();\n {\n let mut guard = state.lock().await;\n guard.pending_chats_list = Some((\"req\".to_string(), tx));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_uses_automation() {\n struct MockAutomation {\n called: Arc<AtomicBool>,\n }\n\n impl WeChatAutomation for MockAutomation {\n fn platform(&self) -> Platform {\n Platform::Windows\n }\n\n fn list_recent_chats(&self) -> anyhow::Result<Vec<ChatSummary>> {\n self.called.store(true, Ordering::SeqCst);\n Ok(vec![ChatSummary {\n chat_id: \"id\".to_string(),\n chat_title: \"title\".to_string(),\n kind: ChatKind::Unknown,\n }])\n }\n\n fn start_listening(&self, _targets: Vec<ListenTarget>) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn stop_listening(&self) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn write_input(&self, _chat_id: &str, _text: &str) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn poll_latest_message(&self) -> anyhow::Result<Option<crate::ui_automation::IncomingMessage>> {\n Ok(None)\n }\n }\n\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let called = Arc::new(AtomicBool::new(false));\n {\n let mut guard = state.lock().await;\n guard.automation =\n crate::ui_automation::AutomationManager::new(Some(Arc::new(MockAutomation {\n called: Arc::clone(&called),\n })));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(result.success);\n assert!(called.load(Ordering::SeqCst));\n }\n}\n"}, "acd5100908b078028686101304939a7a07634e86": {"t": 1785757014.748806, "n": 24960, "h": "1f0132499aab3679", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod message_pipeline;\nmod secret;\nmod state;\nmod types;\nmod ui_automation;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ui_automation::build_platform_automation;\nuse crate::ipc::{\n ChatsListPayload, InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload,\n};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, LogicalSize, Manager, Size, State};\nuse tokio::sync::{Mutex, oneshot, watch};\nuse tokio::time::{timeout, Duration};\nuse uuid::Uuid;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n let (automation, targets) = {\n let guard = state.lock().await;\n (guard.automation.clone(), guard.listen_targets.clone())\n };\n if automation.is_ready() {\n info!(\n \"使用本地自动化路径启动监听: targets={}\",\n targets.len()\n );\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"本地自动化监听已启动\");\n } else {\n warn!(\"本地自动化监听启动失败: {}\", res.message);\n }\n return Ok(res);\n }\n\n info!(\"使用 Agent 路径启动监听\");\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"Agent 已连接,发送监听指令\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.start\", true, true).await\n {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.stop_listening().await;\n if res.success {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n }\n return Ok(res);\n }\n\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.stop\", false, false).await\n {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n return Ok(api_ok(()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.pause\", false, false).await\n {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let targets = {\n let guard = state.lock().await;\n guard.listen_targets.clone()\n };\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n }\n return Ok(res);\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.resume\", true, true).await\n {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let sender = {\n let mut guard = state.lock().await;\n let mut next_config = guard.config.clone();\n next_config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &next_config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n guard.config = next_config;\n guard.listen_targets = normalized.clone();\n guard.agent.as_ref().map(|agent| agent.clone_sender())\n };\n\n if let Some(sender) = sender {\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n list_recent_chats_inner(state.inner().clone()).await\n}\n\nasync fn list_recent_chats_inner(\n state: SharedState,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.list_recent_chats().await;\n if res.success {\n if let Some(chats) = res.data.clone() {\n let mut guard = state.lock().await;\n guard.recent_chats = chats;\n }\n }\n return Ok(res);\n }\n\n let request_id = Uuid::new_v4().to_string();\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some((request_id.clone(), tx));\n (sender, rx)\n };\n\n let payload_value =\n serde_json::to_value(ChatsListPayload { request_id: request_id.clone() })\n .map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.write_input(chat_id, text).await;\n return Ok(res);\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n include_targets: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_targets {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n info!(\n \"发送监听控制指令: type={}, poll_interval_ms={}, targets={}\",\n message_type,\n poll_interval_ms\n .map(|value| value.to_string())\n .unwrap_or_else(|| \"-\".to_string()),\n targets.as_ref().map(|items| items.len()).unwrap_or(0)\n );\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nasync fn start_automation_polling(app: AppHandle, state: SharedState) {\n let (stop_tx, mut stop_rx) = watch::channel(false);\n let (automation, config, targets) = {\n let mut guard = state.lock().await;\n if let Some(stop) = guard.automation_stop.take() {\n let _ = stop.send(true);\n }\n guard.automation_stop = Some(stop_tx);\n (\n guard.automation.clone(),\n guard.config.clone(),\n guard.listen_targets.clone(),\n )\n };\n if !automation.is_ready() {\n return;\n }\n tokio::spawn(async move {\n let mut interval = tokio::time::interval(Duration::from_millis(config.poll_interval_ms));\n loop {\n tokio::select! {\n _ = stop_rx.changed() => {\n if *stop_rx.borrow() {\n break;\n }\n }\n _ = interval.tick() => {\n let res = automation.poll_latest_message().await;\n if !res.success {\n continue;\n }\n let Some(message) = res.data.flatten() else {\n continue;\n };\n if !should_handle_message(&message.chat_id, &targets) {\n continue;\n }\n let payload = crate::ipc::MessageNewPayload {\n chat_id: message.chat_id.clone(),\n chat_title: message.chat_id.clone(),\n is_group: infer_is_group(&message.chat_id, &targets),\n sender_name: String::new(),\n text: message.text.clone(),\n timestamp: message.timestamp,\n msg_id: message.msg_id.clone(),\n };\n crate::message_pipeline::handle_incoming_message(&app, &state, payload).await;\n }\n }\n }\n });\n}\n\nasync fn stop_automation_polling(state: SharedState) {\n let stop = {\n let mut guard = state.lock().await;\n guard.automation_stop.take()\n };\n if let Some(stop) = stop {\n let _ = stop.send(true);\n }\n}\n\nfn should_handle_message(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if targets.is_empty() {\n return true;\n }\n targets.iter().any(|target| target.name == chat_id)\n}\n\nfn infer_is_group(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if let Some(target) = targets.iter().find(|target| target.name == chat_id) {\n return matches!(target.kind, crate::types::ChatKind::Group);\n }\n chat_id.contains(\"\\u{7fa4}\")\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\nfn adjust_window_size(app: &AppHandle) {\n let Some(window) = app.get_webview_window(\"main\") else {\n return;\n };\n let monitor = window\n .current_monitor()\n .ok()\n .flatten()\n .or_else(|| window.primary_monitor().ok().flatten());\n let Some(monitor) = monitor else {\n warn!(\"获取显示器信息失败,跳过窗口尺寸调整\");\n return;\n };\n let scale_factor = monitor.scale_factor();\n let logical_size: LogicalSize<f64> = monitor.size().to_logical(scale_factor);\n let target_width = (logical_size.width * 0.42).round();\n let target_height = (logical_size.height * 0.6).round();\n let width = target_width.clamp(560.0, 960.0);\n let height = target_height.clamp(640.0, 900.0);\n if let Err(err) = window.set_size(Size::Logical(LogicalSize { width, height })) {\n warn!(\"窗口尺寸调整失败: {}\", err);\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let mut app_state = AppState::new(config, initial_status());\n let automation = build_platform_automation();\n app_state.automation = crate::ui_automation::AutomationManager::new(automation);\n let state = Arc::new(Mutex::new(app_state));\n app.manage(state);\n adjust_window_size(app.handle());\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use crate::types::ChatKind;\n use crate::ui_automation::WeChatAutomation;\n use std::sync::atomic::{AtomicBool, Ordering};\n\n #[tokio::test]\n async fn list_recent_chats_requires_agent() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_rejects_when_pending() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let (tx, _rx) = oneshot::channel();\n {\n let mut guard = state.lock().await;\n guard.pending_chats_list = Some((\"req\".to_string(), tx));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_uses_automation() {\n struct MockAutomation {\n called: Arc<AtomicBool>,\n }\n\n impl WeChatAutomation for MockAutomation {\n fn platform(&self) -> Platform {\n Platform::Windows\n }\n\n fn list_recent_chats(&self) -> anyhow::Result<Vec<ChatSummary>> {\n self.called.store(true, Ordering::SeqCst);\n Ok(vec![ChatSummary {\n chat_id: \"id\".to_string(),\n chat_title: \"title\".to_string(),\n kind: ChatKind::Unknown,\n }])\n }\n\n fn start_listening(&self, _targets: Vec<ListenTarget>) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn stop_listening(&self) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn write_input(&self, _chat_id: &str, _text: &str) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn poll_latest_message(&self) -> anyhow::Result<Option<crate::ui_automation::IncomingMessage>> {\n Ok(None)\n }\n }\n\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let called = Arc::new(AtomicBool::new(false));\n {\n let mut guard = state.lock().await;\n guard.automation =\n crate::ui_automation::AutomationManager::new(Some(Arc::new(MockAutomation {\n called: Arc::clone(&called),\n })));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(result.success);\n assert!(called.load(Ordering::SeqCst));\n }\n}\n"}, "29dce40fd708c9d3e67539c4035e030a9a207529": {"t": 1785757016.526066, "n": 24422, "h": "5eac6bc78b495919", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod message_pipeline;\nmod secret;\nmod state;\nmod types;\nmod ui_automation;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ui_automation::build_platform_automation;\nuse crate::ipc::{\n ChatsListPayload, InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload,\n};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, LogicalSize, Manager, Size, State};\nuse tokio::sync::{Mutex, oneshot, watch};\nuse tokio::time::{timeout, Duration};\nuse uuid::Uuid;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n let (automation, targets) = {\n let guard = state.lock().await;\n (guard.automation.clone(), guard.listen_targets.clone())\n };\n if automation.is_ready() {\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n }\n return Ok(res);\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.start\", true, true).await\n {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.stop_listening().await;\n if res.success {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n }\n return Ok(res);\n }\n\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.stop\", false, false).await\n {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n return Ok(api_ok(()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.pause\", false, false).await\n {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let targets = {\n let guard = state.lock().await;\n guard.listen_targets.clone()\n };\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n }\n return Ok(res);\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.resume\", true, true).await\n {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let sender = {\n let mut guard = state.lock().await;\n let mut next_config = guard.config.clone();\n next_config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &next_config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n guard.config = next_config;\n guard.listen_targets = normalized.clone();\n guard.agent.as_ref().map(|agent| agent.clone_sender())\n };\n\n if let Some(sender) = sender {\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n list_recent_chats_inner(state.inner().clone()).await\n}\n\nasync fn list_recent_chats_inner(\n state: SharedState,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.list_recent_chats().await;\n if res.success {\n if let Some(chats) = res.data.clone() {\n let mut guard = state.lock().await;\n guard.recent_chats = chats;\n }\n }\n return Ok(res);\n }\n\n let request_id = Uuid::new_v4().to_string();\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some((request_id.clone(), tx));\n (sender, rx)\n };\n\n let payload_value =\n serde_json::to_value(ChatsListPayload { request_id: request_id.clone() })\n .map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.write_input(chat_id, text).await;\n return Ok(res);\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n include_targets: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_targets {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nasync fn start_automation_polling(app: AppHandle, state: SharedState) {\n let (stop_tx, mut stop_rx) = watch::channel(false);\n let (automation, config, targets) = {\n let mut guard = state.lock().await;\n if let Some(stop) = guard.automation_stop.take() {\n let _ = stop.send(true);\n }\n guard.automation_stop = Some(stop_tx);\n (\n guard.automation.clone(),\n guard.config.clone(),\n guard.listen_targets.clone(),\n )\n };\n if !automation.is_ready() {\n return;\n }\n tokio::spawn(async move {\n let mut interval = tokio::time::interval(Duration::from_millis(config.poll_interval_ms));\n loop {\n tokio::select! {\n _ = stop_rx.changed() => {\n if *stop_rx.borrow() {\n break;\n }\n }\n _ = interval.tick() => {\n let res = automation.poll_latest_message().await;\n if !res.success {\n continue;\n }\n let Some(message) = res.data.flatten() else {\n continue;\n };\n if !should_handle_message(&message.chat_id, &targets) {\n continue;\n }\n let payload = crate::ipc::MessageNewPayload {\n chat_id: message.chat_id.clone(),\n chat_title: message.chat_id.clone(),\n is_group: infer_is_group(&message.chat_id, &targets),\n sender_name: String::new(),\n text: message.text.clone(),\n timestamp: message.timestamp,\n msg_id: message.msg_id.clone(),\n };\n crate::message_pipeline::handle_incoming_message(&app, &state, payload).await;\n }\n }\n }\n });\n}\n\nasync fn stop_automation_polling(state: SharedState) {\n let stop = {\n let mut guard = state.lock().await;\n guard.automation_stop.take()\n };\n if let Some(stop) = stop {\n let _ = stop.send(true);\n }\n}\n\nfn should_handle_message(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if targets.is_empty() {\n return true;\n }\n targets.iter().any(|target| target.name == chat_id)\n}\n\nfn infer_is_group(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if let Some(target) = targets.iter().find(|target| target.name == chat_id) {\n return matches!(target.kind, crate::types::ChatKind::Group);\n }\n chat_id.contains(\"\\u{7fa4}\")\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\nfn adjust_window_size(app: &AppHandle) {\n let Some(window) = app.get_webview_window(\"main\") else {\n return;\n };\n let monitor = window\n .current_monitor()\n .ok()\n .flatten()\n .or_else(|| window.primary_monitor().ok().flatten());\n let Some(monitor) = monitor else {\n warn!(\"获取显示器信息失败,跳过窗口尺寸调整\");\n return;\n };\n let scale_factor = monitor.scale_factor();\n let logical_size: LogicalSize<f64> = monitor.size().to_logical(scale_factor);\n let target_width = (logical_size.width * 0.42).round();\n let target_height = (logical_size.height * 0.6).round();\n let width = target_width.clamp(560.0, 960.0);\n let height = target_height.clamp(640.0, 900.0);\n if let Err(err) = window.set_size(Size::Logical(LogicalSize { width, height })) {\n warn!(\"窗口尺寸调整失败: {}\", err);\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let mut app_state = AppState::new(config, initial_status());\n let automation = build_platform_automation();\n app_state.automation = crate::ui_automation::AutomationManager::new(automation);\n let state = Arc::new(Mutex::new(app_state));\n app.manage(state);\n adjust_window_size(app.handle());\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use crate::types::ChatKind;\n use crate::ui_automation::WeChatAutomation;\n use std::sync::atomic::{AtomicBool, Ordering};\n\n #[tokio::test]\n async fn list_recent_chats_requires_agent() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_rejects_when_pending() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let (tx, _rx) = oneshot::channel();\n {\n let mut guard = state.lock().await;\n guard.pending_chats_list = Some((\"req\".to_string(), tx));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_uses_automation() {\n struct MockAutomation {\n called: Arc<AtomicBool>,\n }\n\n impl WeChatAutomation for MockAutomation {\n fn platform(&self) -> Platform {\n Platform::Windows\n }\n\n fn list_recent_chats(&self) -> anyhow::Result<Vec<ChatSummary>> {\n self.called.store(true, Ordering::SeqCst);\n Ok(vec![ChatSummary {\n chat_id: \"id\".to_string(),\n chat_title: \"title\".to_string(),\n kind: ChatKind::Unknown,\n }])\n }\n\n fn start_listening(&self, _targets: Vec<ListenTarget>) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn stop_listening(&self) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn write_input(&self, _chat_id: &str, _text: &str) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn poll_latest_message(&self) -> anyhow::Result<Option<crate::ui_automation::IncomingMessage>> {\n Ok(None)\n }\n }\n\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let called = Arc::new(AtomicBool::new(false));\n {\n let mut guard = state.lock().await;\n guard.automation =\n crate::ui_automation::AutomationManager::new(Some(Arc::new(MockAutomation {\n called: Arc::clone(&called),\n })));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(result.success);\n assert!(called.load(Ordering::SeqCst));\n }\n}\n"}, "cd02c73d7da839b178fae794c3cb83dd16fd0f8e": {"t": 1785757018.0792437, "n": 24422, "h": "42fc08f5d07c0f66", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod message_pipeline;\nmod secret;\nmod state;\nmod types;\nmod ui_automation;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ui_automation::build_platform_automation;\nuse crate::ipc::{\n ChatsListPayload, InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload,\n};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, LogicalSize, Manager, Size, State};\nuse tokio::sync::{Mutex, oneshot, watch};\nuse tokio::time::{timeout, Duration};\nuse uuid::Uuid;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n let (automation, targets) = {\n let guard = state.lock().await;\n (guard.automation.clone(), guard.listen_targets.clone())\n };\n if automation.is_ready() {\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n }\n return Ok(res);\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.start\", true, true).await\n {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.stop_listening().await;\n if res.success {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n }\n return Ok(res);\n }\n\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.stop\", false, false).await\n {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n return Ok(api_ok(()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.pause\", false, false).await\n {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let targets = {\n let guard = state.lock().await;\n guard.listen_targets.clone()\n };\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n }\n return Ok(res);\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.resume\", true, true).await\n {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let sender = {\n let mut guard = state.lock().await;\n let mut next_config = guard.config.clone();\n next_config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &next_config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n guard.config = next_config;\n guard.listen_targets = normalized.clone();\n guard.agent.as_ref().map(|agent| agent.clone_sender())\n };\n\n if let Some(sender) = sender {\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n list_recent_chats_inner(state.inner().clone()).await\n}\n\nasync fn list_recent_chats_inner(\n state: SharedState,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.list_recent_chats().await;\n if res.success {\n if let Some(chats) = res.data.clone() {\n let mut guard = state.lock().await;\n guard.recent_chats = chats;\n }\n }\n return Ok(res);\n }\n\n let request_id = Uuid::new_v4().to_string();\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some((request_id.clone(), tx));\n (sender, rx)\n };\n\n let payload_value =\n serde_json::to_value(ChatsListPayload { request_id: request_id.clone() })\n .map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.write_input(chat_id, text).await;\n return Ok(res);\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n include_targets: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_targets {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nasync fn start_automation_polling(app: AppHandle, state: SharedState) {\n let (stop_tx, mut stop_rx) = watch::channel(false);\n let (automation, config, targets) = {\n let mut guard = state.lock().await;\n if let Some(stop) = guard.automation_stop.take() {\n let _ = stop.send(true);\n }\n guard.automation_stop = Some(stop_tx);\n (\n guard.automation.clone(),\n guard.config.clone(),\n guard.listen_targets.clone(),\n )\n };\n if !automation.is_ready() {\n return;\n }\n tokio::spawn(async move {\n let mut interval = tokio::time::interval(Duration::from_millis(config.poll_interval_ms));\n loop {\n tokio::select! {\n _ = stop_rx.changed() => {\n if *stop_rx.borrow() {\n break;\n }\n }\n _ = interval.tick() => {\n let res = automation.poll_latest_message().await;\n if !res.success {\n continue;\n }\n let Some(message) = res.data.flatten() else {\n continue;\n };\n if !should_handle_message(&message.chat_id, &targets) {\n continue;\n }\n let payload = crate::ipc::MessageNewPayload {\n chat_id: message.chat_id.clone(),\n chat_title: message.chat_id.clone(),\n is_group: infer_is_group(&message.chat_id, &targets),\n sender_name: String::new(),\n text: message.text.clone(),\n timestamp: message.timestamp,\n msg_id: message.msg_id.clone(),\n };\n crate::message_pipeline::handle_incoming_message(&app, &state, payload).await;\n }\n }\n }\n });\n}\n\nasync fn stop_automation_polling(state: SharedState) {\n let stop = {\n let mut guard = state.lock().await;\n guard.automation_stop.take()\n };\n if let Some(stop) = stop {\n let _ = stop.send(true);\n }\n}\n\nfn should_handle_message(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if targets.is_empty() {\n return true;\n }\n targets.iter().any(|target| target.name == chat_id)\n}\n\nfn infer_is_group(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if let Some(target) = targets.iter().find(|target| target.name == chat_id) {\n return matches!(target.kind, crate::types::ChatKind::Group);\n }\n chat_id.contains(\"\\u{7fa4}\")\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\nfn adjust_window_size(app: &AppHandle) {\n let Some(window) = app.get_webview_window(\"main\") else {\n return;\n };\n let monitor = window\n .current_monitor()\n .ok()\n .flatten()\n .or_else(|| window.primary_monitor().ok().flatten());\n let Some(monitor) = monitor else {\n warn!(\"获取显示器信息失败,跳过窗口尺寸调整\");\n return;\n };\n let scale_factor = monitor.scale_factor();\n let logical_size: LogicalSize<f64> = monitor.size().to_logical(scale_factor);\n let target_width = (logical_size.width * 0.5).round();\n let target_height = (logical_size.height * 0.6).round();\n let width = target_width.clamp(600.0, 1100.0);\n let height = target_height.clamp(640.0, 900.0);\n if let Err(err) = window.set_size(Size::Logical(LogicalSize { width, height })) {\n warn!(\"窗口尺寸调整失败: {}\", err);\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let mut app_state = AppState::new(config, initial_status());\n let automation = build_platform_automation();\n app_state.automation = crate::ui_automation::AutomationManager::new(automation);\n let state = Arc::new(Mutex::new(app_state));\n app.manage(state);\n adjust_window_size(app.handle());\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use crate::types::ChatKind;\n use crate::ui_automation::WeChatAutomation;\n use std::sync::atomic::{AtomicBool, Ordering};\n\n #[tokio::test]\n async fn list_recent_chats_requires_agent() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_rejects_when_pending() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let (tx, _rx) = oneshot::channel();\n {\n let mut guard = state.lock().await;\n guard.pending_chats_list = Some((\"req\".to_string(), tx));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_uses_automation() {\n struct MockAutomation {\n called: Arc<AtomicBool>,\n }\n\n impl WeChatAutomation for MockAutomation {\n fn platform(&self) -> Platform {\n Platform::Windows\n }\n\n fn list_recent_chats(&self) -> anyhow::Result<Vec<ChatSummary>> {\n self.called.store(true, Ordering::SeqCst);\n Ok(vec![ChatSummary {\n chat_id: \"id\".to_string(),\n chat_title: \"title\".to_string(),\n kind: ChatKind::Unknown,\n }])\n }\n\n fn start_listening(&self, _targets: Vec<ListenTarget>) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn stop_listening(&self) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn write_input(&self, _chat_id: &str, _text: &str) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn poll_latest_message(&self) -> anyhow::Result<Option<crate::ui_automation::IncomingMessage>> {\n Ok(None)\n }\n }\n\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let called = Arc::new(AtomicBool::new(false));\n {\n let mut guard = state.lock().await;\n guard.automation =\n crate::ui_automation::AutomationManager::new(Some(Arc::new(MockAutomation {\n called: Arc::clone(&called),\n })));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(result.success);\n assert!(called.load(Ordering::SeqCst));\n }\n}\n"}, "7dd77db3e172aaa1b719cf934bd9a426eb44665e": {"t": 1785757019.53195, "n": 24420, "h": "f7402933333db342", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod message_pipeline;\nmod secret;\nmod state;\nmod types;\nmod ui_automation;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ui_automation::build_platform_automation;\nuse crate::ipc::{\n ChatsListPayload, InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload,\n};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, LogicalSize, Manager, Size, State};\nuse tokio::sync::{Mutex, oneshot, watch};\nuse tokio::time::{timeout, Duration};\nuse uuid::Uuid;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n let (automation, targets) = {\n let guard = state.lock().await;\n (guard.automation.clone(), guard.listen_targets.clone())\n };\n if automation.is_ready() {\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n }\n return Ok(res);\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.start\", true, true).await\n {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.stop_listening().await;\n if res.success {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n }\n return Ok(res);\n }\n\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.stop\", false, false).await\n {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n stop_automation_polling(state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n return Ok(api_ok(()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.pause\", false, false).await\n {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let targets = {\n let guard = state.lock().await;\n guard.listen_targets.clone()\n };\n let res = automation.start_listening(targets).await;\n if res.success {\n start_automation_polling(app.clone(), state.inner().clone()).await;\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n }\n return Ok(res);\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.resume\", true, true).await\n {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let sender = {\n let mut guard = state.lock().await;\n let mut next_config = guard.config.clone();\n next_config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &next_config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n guard.config = next_config;\n guard.listen_targets = normalized.clone();\n guard.agent.as_ref().map(|agent| agent.clone_sender())\n };\n\n if let Some(sender) = sender {\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n list_recent_chats_inner(state.inner().clone()).await\n}\n\nasync fn list_recent_chats_inner(\n state: SharedState,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.list_recent_chats().await;\n if res.success {\n if let Some(chats) = res.data.clone() {\n let mut guard = state.lock().await;\n guard.recent_chats = chats;\n }\n }\n return Ok(res);\n }\n\n let request_id = Uuid::new_v4().to_string();\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some((request_id.clone(), tx));\n (sender, rx)\n };\n\n let payload_value =\n serde_json::to_value(ChatsListPayload { request_id: request_id.clone() })\n .map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.write_input(chat_id, text).await;\n return Ok(res);\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n include_targets: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_targets {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nasync fn start_automation_polling(app: AppHandle, state: SharedState) {\n let (stop_tx, mut stop_rx) = watch::channel(false);\n let (automation, config, targets) = {\n let mut guard = state.lock().await;\n if let Some(stop) = guard.automation_stop.take() {\n let _ = stop.send(true);\n }\n guard.automation_stop = Some(stop_tx);\n (\n guard.automation.clone(),\n guard.config.clone(),\n guard.listen_targets.clone(),\n )\n };\n if !automation.is_ready() {\n return;\n }\n tokio::spawn(async move {\n let mut interval = tokio::time::interval(Duration::from_millis(config.poll_interval_ms));\n loop {\n tokio::select! {\n _ = stop_rx.changed() => {\n if *stop_rx.borrow() {\n break;\n }\n }\n _ = interval.tick() => {\n let res = automation.poll_latest_message().await;\n if !res.success {\n continue;\n }\n let Some(message) = res.data.flatten() else {\n continue;\n };\n if !should_handle_message(&message.chat_id, &targets) {\n continue;\n }\n let payload = crate::ipc::MessageNewPayload {\n chat_id: message.chat_id.clone(),\n chat_title: message.chat_id.clone(),\n is_group: infer_is_group(&message.chat_id, &targets),\n sender_name: String::new(),\n text: message.text.clone(),\n timestamp: message.timestamp,\n msg_id: message.msg_id.clone(),\n };\n crate::message_pipeline::handle_incoming_message(&app, &state, payload).await;\n }\n }\n }\n });\n}\n\nasync fn stop_automation_polling(state: SharedState) {\n let stop = {\n let mut guard = state.lock().await;\n guard.automation_stop.take()\n };\n if let Some(stop) = stop {\n let _ = stop.send(true);\n }\n}\n\nfn should_handle_message(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if targets.is_empty() {\n return true;\n }\n targets.iter().any(|target| target.name == chat_id)\n}\n\nfn infer_is_group(chat_id: &str, targets: &[ListenTarget]) -> bool {\n if let Some(target) = targets.iter().find(|target| target.name == chat_id) {\n return matches!(target.kind, crate::types::ChatKind::Group);\n }\n chat_id.contains(\"\\u7fa4\")\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\nfn adjust_window_size(app: &AppHandle) {\n let Some(window) = app.get_webview_window(\"main\") else {\n return;\n };\n let monitor = window\n .current_monitor()\n .ok()\n .flatten()\n .or_else(|| window.primary_monitor().ok().flatten());\n let Some(monitor) = monitor else {\n warn!(\"获取显示器信息失败,跳过窗口尺寸调整\");\n return;\n };\n let scale_factor = monitor.scale_factor();\n let logical_size: LogicalSize<f64> = monitor.size().to_logical(scale_factor);\n let target_width = (logical_size.width * 0.5).round();\n let target_height = (logical_size.height * 0.6).round();\n let width = target_width.clamp(600.0, 1100.0);\n let height = target_height.clamp(640.0, 900.0);\n if let Err(err) = window.set_size(Size::Logical(LogicalSize { width, height })) {\n warn!(\"窗口尺寸调整失败: {}\", err);\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let mut app_state = AppState::new(config, initial_status());\n let automation = build_platform_automation();\n app_state.automation = crate::ui_automation::AutomationManager::new(automation);\n let state = Arc::new(Mutex::new(app_state));\n app.manage(state);\n adjust_window_size(app.handle());\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use crate::types::ChatKind;\n use crate::ui_automation::WeChatAutomation;\n use std::sync::atomic::{AtomicBool, Ordering};\n\n #[tokio::test]\n async fn list_recent_chats_requires_agent() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_rejects_when_pending() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let (tx, _rx) = oneshot::channel();\n {\n let mut guard = state.lock().await;\n guard.pending_chats_list = Some((\"req\".to_string(), tx));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_uses_automation() {\n struct MockAutomation {\n called: Arc<AtomicBool>,\n }\n\n impl WeChatAutomation for MockAutomation {\n fn platform(&self) -> Platform {\n Platform::Windows\n }\n\n fn list_recent_chats(&self) -> anyhow::Result<Vec<ChatSummary>> {\n self.called.store(true, Ordering::SeqCst);\n Ok(vec![ChatSummary {\n chat_id: \"id\".to_string(),\n chat_title: \"title\".to_string(),\n kind: ChatKind::Unknown,\n }])\n }\n\n fn start_listening(&self, _targets: Vec<ListenTarget>) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn stop_listening(&self) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn write_input(&self, _chat_id: &str, _text: &str) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn poll_latest_message(&self) -> anyhow::Result<Option<crate::ui_automation::IncomingMessage>> {\n Ok(None)\n }\n }\n\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let called = Arc::new(AtomicBool::new(false));\n {\n let mut guard = state.lock().await;\n guard.automation =\n crate::ui_automation::AutomationManager::new(Some(Arc::new(MockAutomation {\n called: Arc::clone(&called),\n })));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(result.success);\n assert!(called.load(Ordering::SeqCst));\n }\n}\n"}, "f65bc758b2ee1655523ad9418df7263a9aa6b82c": {"t": 1785757020.88235, "n": 20044, "h": "23a39cf053209902", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod secret;\nmod state;\nmod types;\nmod ui_automation;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ipc::{\n ChatsListPayload, InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload,\n};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, LogicalSize, Manager, Size, State};\nuse tokio::sync::{Mutex, oneshot};\nuse tokio::time::{timeout, Duration};\nuse uuid::Uuid;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n let (automation, targets) = {\n let guard = state.lock().await;\n (guard.automation.clone(), guard.listen_targets.clone())\n };\n if automation.is_ready() {\n let res = automation.start_listening(targets).await;\n if res.success {\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n }\n return Ok(res);\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.start\", true, true).await\n {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.stop_listening().await;\n if res.success {\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n }\n return Ok(res);\n }\n\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.stop\", false, false).await\n {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.pause\", false, false).await\n {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.resume\", true, true).await\n {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let sender = {\n let mut guard = state.lock().await;\n let mut next_config = guard.config.clone();\n next_config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &next_config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n guard.config = next_config;\n guard.listen_targets = normalized.clone();\n guard.agent.as_ref().map(|agent| agent.clone_sender())\n };\n\n if let Some(sender) = sender {\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n list_recent_chats_inner(state.inner().clone()).await\n}\n\nasync fn list_recent_chats_inner(\n state: SharedState,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n return Ok(automation.list_recent_chats().await);\n }\n\n let request_id = Uuid::new_v4().to_string();\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some((request_id.clone(), tx));\n (sender, rx)\n };\n\n let payload_value =\n serde_json::to_value(ChatsListPayload { request_id: request_id.clone() })\n .map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n let res = automation.write_input(chat_id, text).await;\n return Ok(res);\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n include_targets: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_targets {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\nfn adjust_window_size(app: &AppHandle) {\n let Some(window) = app.get_webview_window(\"main\") else {\n return;\n };\n let monitor = window\n .current_monitor()\n .ok()\n .flatten()\n .or_else(|| window.primary_monitor().ok().flatten());\n let Some(monitor) = monitor else {\n warn!(\"获取显示器信息失败,跳过窗口尺寸调整\");\n return;\n };\n let scale_factor = monitor.scale_factor();\n let logical_size: LogicalSize<f64> = monitor.size().to_logical(scale_factor);\n let target_width = (logical_size.width * 0.5).round();\n let target_height = (logical_size.height * 0.6).round();\n let width = target_width.clamp(600.0, 1100.0);\n let height = target_height.clamp(640.0, 900.0);\n if let Err(err) = window.set_size(Size::Logical(LogicalSize { width, height })) {\n warn!(\"窗口尺寸调整失败: {}\", err);\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let state = Arc::new(Mutex::new(AppState::new(config, initial_status())));\n app.manage(state);\n adjust_window_size(app.handle());\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use crate::types::ChatKind;\n use crate::ui_automation::WeChatAutomation;\n use std::sync::atomic::{AtomicBool, Ordering};\n\n #[tokio::test]\n async fn list_recent_chats_requires_agent() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_rejects_when_pending() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let (tx, _rx) = oneshot::channel();\n {\n let mut guard = state.lock().await;\n guard.pending_chats_list = Some((\"req\".to_string(), tx));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_uses_automation() {\n struct MockAutomation {\n called: Arc<AtomicBool>,\n }\n\n impl WeChatAutomation for MockAutomation {\n fn platform(&self) -> Platform {\n Platform::Windows\n }\n\n fn list_recent_chats(&self) -> anyhow::Result<Vec<ChatSummary>> {\n self.called.store(true, Ordering::SeqCst);\n Ok(vec![ChatSummary {\n chat_id: \"id\".to_string(),\n chat_title: \"title\".to_string(),\n kind: ChatKind::Unknown,\n }])\n }\n\n fn start_listening(&self, _targets: Vec<ListenTarget>) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn stop_listening(&self) -> anyhow::Result<()> {\n Ok(())\n }\n\n fn write_input(&self, _chat_id: &str, _text: &str) -> anyhow::Result<()> {\n Ok(())\n }\n }\n\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let called = Arc::new(AtomicBool::new(false));\n {\n let mut guard = state.lock().await;\n guard.automation =\n crate::ui_automation::AutomationManager::new(Some(Arc::new(MockAutomation {\n called: Arc::clone(&called),\n })));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(result.success);\n assert!(called.load(Ordering::SeqCst));\n }\n}\n"}, "cc9e0bd873096218b82ad7ce07909b9be74b60e4": {"t": 1785757022.661403, "n": 17328, "h": "0fef34ca882b3fc9", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod secret;\nmod state;\nmod types;\nmod ui_automation;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ipc::{\n ChatsListPayload, InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload,\n};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, LogicalSize, Manager, Size, State};\nuse tokio::sync::{Mutex, oneshot};\nuse tokio::time::{timeout, Duration};\nuse uuid::Uuid;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.start\", true, true).await\n {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.stop\", false, false).await\n {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.pause\", false, false).await\n {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.resume\", true, true).await\n {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let sender = {\n let mut guard = state.lock().await;\n let mut next_config = guard.config.clone();\n next_config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &next_config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n guard.config = next_config;\n guard.listen_targets = normalized.clone();\n guard.agent.as_ref().map(|agent| agent.clone_sender())\n };\n\n if let Some(sender) = sender {\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n list_recent_chats_inner(state.inner().clone()).await\n}\n\nasync fn list_recent_chats_inner(\n state: SharedState,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let automation = {\n let guard = state.lock().await;\n guard.automation.clone()\n };\n if automation.is_ready() {\n return Ok(automation.list_recent_chats().await);\n }\n\n let request_id = Uuid::new_v4().to_string();\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some((request_id.clone(), tx));\n (sender, rx)\n };\n\n let payload_value =\n serde_json::to_value(ChatsListPayload { request_id: request_id.clone() })\n .map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n include_targets: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_targets {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\nfn adjust_window_size(app: &AppHandle) {\n let Some(window) = app.get_webview_window(\"main\") else {\n return;\n };\n let monitor = window\n .current_monitor()\n .ok()\n .flatten()\n .or_else(|| window.primary_monitor().ok().flatten());\n let Some(monitor) = monitor else {\n warn!(\"获取显示器信息失败,跳过窗口尺寸调整\");\n return;\n };\n let scale_factor = monitor.scale_factor();\n let logical_size: LogicalSize<f64> = monitor.size().to_logical(scale_factor);\n let target_width = (logical_size.width * 0.5).round();\n let target_height = (logical_size.height * 0.6).round();\n let width = target_width.clamp(600.0, 1100.0);\n let height = target_height.clamp(640.0, 900.0);\n if let Err(err) = window.set_size(Size::Logical(LogicalSize { width, height })) {\n warn!(\"窗口尺寸调整失败: {}\", err);\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let state = Arc::new(Mutex::new(AppState::new(config, initial_status())));\n app.manage(state);\n adjust_window_size(app.handle());\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[tokio::test]\n async fn list_recent_chats_requires_agent() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_rejects_when_pending() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let (tx, _rx) = oneshot::channel();\n {\n let mut guard = state.lock().await;\n guard.pending_chats_list = Some((\"req\".to_string(), tx));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n}\n"}, "57bdecbfee6dcbe0a7f39427af5724ea7617c37a": {"t": 1785757023.9858437, "n": 17111, "h": "d99f86cf4207cac7", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod secret;\nmod state;\nmod types;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ipc::{\n ChatsListPayload, InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload,\n};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, LogicalSize, Manager, Size, State};\nuse tokio::sync::{Mutex, oneshot};\nuse tokio::time::{timeout, Duration};\nuse uuid::Uuid;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.start\", true, true).await\n {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.stop\", false, false).await\n {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.pause\", false, false).await\n {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.resume\", true, true).await\n {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let sender = {\n let mut guard = state.lock().await;\n let mut next_config = guard.config.clone();\n next_config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &next_config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n guard.config = next_config;\n guard.listen_targets = normalized.clone();\n guard.agent.as_ref().map(|agent| agent.clone_sender())\n };\n\n if let Some(sender) = sender {\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n list_recent_chats_inner(state.inner().clone()).await\n}\n\nasync fn list_recent_chats_inner(\n state: SharedState,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let request_id = Uuid::new_v4().to_string();\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some((request_id.clone(), tx));\n (sender, rx)\n };\n\n let payload_value =\n serde_json::to_value(ChatsListPayload { request_id: request_id.clone() })\n .map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n include_targets: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_targets {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\nfn adjust_window_size(app: &AppHandle) {\n let Some(window) = app.get_webview_window(\"main\") else {\n return;\n };\n let monitor = window\n .current_monitor()\n .ok()\n .flatten()\n .or_else(|| window.primary_monitor().ok().flatten());\n let Some(monitor) = monitor else {\n warn!(\"获取显示器信息失败,跳过窗口尺寸调整\");\n return;\n };\n let scale_factor = monitor.scale_factor();\n let logical_size: LogicalSize<f64> = monitor.size().to_logical(scale_factor);\n let target_width = (logical_size.width * 0.5).round();\n let target_height = (logical_size.height * 0.6).round();\n let width = target_width.clamp(600.0, 1100.0);\n let height = target_height.clamp(640.0, 900.0);\n if let Err(err) = window.set_size(Size::Logical(LogicalSize { width, height })) {\n warn!(\"窗口尺寸调整失败: {}\", err);\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let state = Arc::new(Mutex::new(AppState::new(config, initial_status())));\n app.manage(state);\n adjust_window_size(app.handle());\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[tokio::test]\n async fn list_recent_chats_requires_agent() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_rejects_when_pending() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let (tx, _rx) = oneshot::channel();\n {\n let mut guard = state.lock().await;\n guard.pending_chats_list = Some((\"req\".to_string(), tx));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n}\n"}, "32fe3ce4dc3ed1e21664ba3faa3042595149d5f1": {"t": 1785757025.7909627, "n": 17112, "h": "92f710ccb965ab44", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod secret;\nmod state;\nmod types;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ipc::{\n ChatsListPayload, InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload,\n};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, LogicalSize, Manager, Size, State};\nuse tokio::sync::{Mutex, oneshot};\nuse tokio::time::{timeout, Duration};\nuse uuid::Uuid;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.start\", true, true).await\n {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.stop\", false, false).await\n {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.pause\", false, false).await\n {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.resume\", true, true).await\n {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let sender = {\n let mut guard = state.lock().await;\n let mut next_config = guard.config.clone();\n next_config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &next_config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n guard.config = next_config;\n guard.listen_targets = normalized.clone();\n guard.agent.as_ref().map(|agent| agent.clone_sender())\n };\n\n if let Some(sender) = sender {\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n list_recent_chats_inner(state.inner().clone()).await\n}\n\nasync fn list_recent_chats_inner(\n state: SharedState,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let request_id = Uuid::new_v4().to_string();\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some((request_id.clone(), tx));\n (sender, rx)\n };\n\n let payload_value =\n serde_json::to_value(ChatsListPayload { request_id: request_id.clone() })\n .map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n include_targets: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_targets {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\nfn adjust_window_size(app: &AppHandle) {\n let Some(window) = app.get_webview_window(\"main\") else {\n return;\n };\n let monitor = window\n .current_monitor()\n .ok()\n .flatten()\n .or_else(|| window.primary_monitor().ok().flatten());\n let Some(monitor) = monitor else {\n warn!(\"获取显示器信息失败,跳过窗口尺寸调整\");\n return;\n };\n let scale_factor = monitor.scale_factor();\n let logical_size: LogicalSize<f64> = monitor.size().to_logical(scale_factor);\n let target_width = (logical_size.width * 0.7).round();\n let target_height = (logical_size.height * 0.85).round();\n let width = target_width.clamp(640.0, 1200.0);\n let height = target_height.clamp(720.0, 900.0);\n if let Err(err) = window.set_size(Size::Logical(LogicalSize { width, height })) {\n warn!(\"窗口尺寸调整失败: {}\", err);\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let state = Arc::new(Mutex::new(AppState::new(config, initial_status())));\n app.manage(state);\n adjust_window_size(app.handle());\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[tokio::test]\n async fn list_recent_chats_requires_agent() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_rejects_when_pending() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let (tx, _rx) = oneshot::channel();\n {\n let mut guard = state.lock().await;\n guard.pending_chats_list = Some((\"req\".to_string(), tx));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n}\n"}, "2ed9c31b351f1b9586f35db9835ce1d096d251f5": {"t": 1785757027.0402775, "n": 16192, "h": "bf6bb92c95a2e4c0", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod secret;\nmod state;\nmod types;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ipc::{\n ChatsListPayload, InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload,\n};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, Manager, State};\nuse tokio::sync::{Mutex, oneshot};\nuse tokio::time::{timeout, Duration};\nuse uuid::Uuid;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.start\", true, true).await\n {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.stop\", false, false).await\n {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.pause\", false, false).await\n {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.resume\", true, true).await\n {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let sender = {\n let mut guard = state.lock().await;\n let mut next_config = guard.config.clone();\n next_config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &next_config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n guard.config = next_config;\n guard.listen_targets = normalized.clone();\n guard.agent.as_ref().map(|agent| agent.clone_sender())\n };\n\n if let Some(sender) = sender {\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n list_recent_chats_inner(state.inner().clone()).await\n}\n\nasync fn list_recent_chats_inner(\n state: SharedState,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let request_id = Uuid::new_v4().to_string();\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some((request_id.clone(), tx));\n (sender, rx)\n };\n\n let payload_value =\n serde_json::to_value(ChatsListPayload { request_id: request_id.clone() })\n .map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n include_targets: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_targets {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let state = Arc::new(Mutex::new(AppState::new(config, initial_status())));\n app.manage(state);\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[tokio::test]\n async fn list_recent_chats_requires_agent() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n\n #[tokio::test]\n async fn list_recent_chats_rejects_when_pending() {\n let state = Arc::new(Mutex::new(AppState::new(\n Config::default(),\n initial_status(),\n )));\n let (tx, _rx) = oneshot::channel();\n {\n let mut guard = state.lock().await;\n guard.pending_chats_list = Some((\"req\".to_string(), tx));\n }\n let result = list_recent_chats_inner(state).await.unwrap();\n assert!(!result.success);\n }\n}\n"}, "2946058c9aeaee7c839352916c95d45d718136ac": {"t": 1785757028.4881606, "n": 15173, "h": "6118c6f00173ab92", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod secret;\nmod state;\nmod types;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ipc::{\n ChatsListPayload, InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload,\n};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, Manager, State};\nuse tokio::sync::{Mutex, oneshot};\nuse tokio::time::{timeout, Duration};\nuse uuid::Uuid;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.start\", true, true).await\n {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.stop\", false, false).await\n {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.pause\", false, false).await\n {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.resume\", true, true).await\n {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let sender = {\n let mut guard = state.lock().await;\n let mut next_config = guard.config.clone();\n next_config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &next_config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n guard.config = next_config;\n guard.listen_targets = normalized.clone();\n guard.agent.as_ref().map(|agent| agent.clone_sender())\n };\n\n if let Some(sender) = sender {\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let request_id = Uuid::new_v4().to_string();\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some((request_id.clone(), tx));\n (sender, rx)\n };\n\n let payload_value =\n serde_json::to_value(ChatsListPayload { request_id: request_id.clone() })\n .map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n if matches!(guard.pending_chats_list.as_ref(), Some((pending_id, _)) if pending_id == &request_id) {\n guard.pending_chats_list = None;\n }\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n include_targets: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_targets {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let state = Arc::new(Mutex::new(AppState::new(config, initial_status())));\n app.manage(state);\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n"}, "4f83e9c909db5069ac744d0ed679771feeefcb81": {"t": 1785757029.928728, "n": 14608, "h": "6a8b204eaa8fb662", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod secret;\nmod state;\nmod types;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ipc::{InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, Manager, State};\nuse tokio::sync::{Mutex, oneshot};\nuse tokio::time::{timeout, Duration};\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.start\", true, true).await\n {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.stop\", false, false).await\n {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.pause\", false, false).await\n {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n if let Err(err) =\n send_listen_control(state.inner().clone(), \"listen.resume\", true, true).await\n {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let sender = {\n let mut guard = state.lock().await;\n guard.listen_targets = normalized.clone();\n guard.config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n guard.agent.as_ref().map(|agent| agent.clone_sender())\n };\n\n if let Some(sender) = sender {\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some(tx);\n (sender, rx)\n };\n\n let payload_value = serde_json::json!({});\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n include_targets: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_targets {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let state = Arc::new(Mutex::new(AppState::new(config, initial_status())));\n app.manage(state);\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n"}, "7e123798aa9afc99d2b3bc49ffe11e2991568093": {"t": 1785757031.305933, "n": 14741, "h": "db190a8ae09bcab6", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod secret;\nmod state;\nmod types;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ipc::{InputWritePayload, IpcEnvelope, ListenControlPayload, ListenTargetsPayload};\nuse crate::listen_targets::{normalize_listen_targets, MAX_LISTEN_TARGETS};\nuse crate::types::{\n api_err, api_ok, ApiResponse, ChatSummary, Config, DeepseekDiagnostics, ListenTarget, Platform,\n RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, Manager, State};\nuse tokio::sync::{Mutex, oneshot};\nuse tokio::time::{timeout, Duration};\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝开始监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.start\", true).await {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.stop\", false).await {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.pause\", false).await {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n {\n let guard = state.lock().await;\n if guard.listen_targets.is_empty() {\n warn!(\"未设置监听对象,拒绝恢复监听\");\n return Ok(api_err(\"请先设置监听对象\"));\n }\n }\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.resume\", true).await {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_listen_targets(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ListenTarget>>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.listen_targets.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_listen_targets(\n app: AppHandle,\n state: State<'_, SharedState>,\n targets: Vec<ListenTarget>,\n) -> Result<ApiResponse<()>, String> {\n let normalized = match normalize_listen_targets(targets, MAX_LISTEN_TARGETS) {\n Ok(targets) => targets,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n\n let (should_notify, sender) = {\n let mut guard = state.lock().await;\n guard.listen_targets = normalized.clone();\n guard.config.listen_targets = normalized.clone();\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n (\n guard.status.state == RuntimeState::Listening,\n guard.agent.as_ref().map(|agent| agent.clone_sender()),\n )\n };\n\n if should_notify {\n let Some(sender) = sender else {\n warn!(\"发送监听对象失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n let payload = ListenTargetsPayload {\n targets: normalized,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n if let Err(err) = sender.send(IpcEnvelope::new(\"listen.targets\", payload_value)).await {\n warn!(\"发送监听对象失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n }\n\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_recent_chats(\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<Vec<ChatSummary>>, String> {\n let (sender, receiver) = {\n let mut guard = state.lock().await;\n if guard.pending_chats_list.is_some() {\n return Ok(api_err(\"已有会话列表请求进行中\"));\n }\n let sender = match guard.agent.as_ref() {\n Some(agent) => agent.clone_sender(),\n None => return Ok(api_err(\"Agent 未连接\")),\n };\n let (tx, rx) = oneshot::channel();\n guard.pending_chats_list = Some(tx);\n (sender, rx)\n };\n\n let payload_value = serde_json::json!({});\n if let Err(err) = sender.send(IpcEnvelope::new(\"chats.list\", payload_value)).await {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n warn!(\"发送会话列表请求失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n match timeout(Duration::from_secs(3), receiver).await {\n Ok(Ok(chats)) => Ok(api_ok(chats)),\n Ok(Err(_)) => {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n Ok(api_err(\"会话列表获取失败\"))\n }\n Err(_) => {\n let mut guard = state.lock().await;\n guard.pending_chats_list = None;\n Ok(api_err(\"会话列表请求超时\"))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms, targets) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n if include_poll_interval {\n Some(guard.listen_targets.clone())\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let state = Arc::new(Mutex::new(AppState::new(config, initial_status())));\n app.manage(state);\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n get_listen_targets,\n set_listen_targets,\n list_recent_chats,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n"}, "7c7ff5f3e8bba2bfed1bf2dadacced162791b699": {"t": 1785757032.601599, "n": 10944, "h": "d0aa31429b062fac", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod listen_targets;\nmod logging;\nmod secret;\nmod state;\nmod types;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ipc::{InputWritePayload, ListenControlPayload};\nuse crate::types::{\n api_err, api_ok, ApiResponse, Config, DeepseekDiagnostics, Platform, RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, Manager, State};\nuse tokio::sync::Mutex;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.start\", true).await {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.stop\", false).await {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.pause\", false).await {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.resume\", true).await {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload {\n poll_interval_ms,\n targets: None,\n };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let state = Arc::new(Mutex::new(AppState::new(config, initial_status())));\n app.manage(state);\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n"}, "ed6b12c9e533c5c0d114a498eac00895153e16bf": {"t": 1785757034.2430792, "n": 10888, "h": "a4eb3109c2c36b36", "c": "mod agent;\npub mod bindings;\nmod config;\nmod deepseek;\nmod ipc;\nmod logging;\nmod secret;\nmod state;\nmod types;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ipc::{InputWritePayload, ListenControlPayload};\nuse crate::types::{\n api_err, api_ok, ApiResponse, Config, DeepseekDiagnostics, Platform, RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, Manager, State};\nuse tokio::sync::Mutex;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.start\", true).await {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.stop\", false).await {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.pause\", false).await {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.resume\", true).await {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload { poll_interval_ms };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let state = Arc::new(Mutex::new(AppState::new(config, initial_status())));\n app.manage(state);\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n"}, "5de993ddeedb39d449ba4f09e0f3e1efb6ebbef4": {"t": 1785757035.8569717, "n": 10870, "h": "c3df747acfaa364e", "c": "mod agent;\nmod config;\nmod deepseek;\nmod ipc;\nmod logging;\nmod secret;\nmod state;\nmod types;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ipc::{InputWritePayload, ListenControlPayload};\nuse crate::types::{\n api_err, api_ok, ApiResponse, Config, DeepseekDiagnostics, Platform, RuntimeState, Status,\n};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, Manager, State};\nuse tokio::sync::Mutex;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.start\", true).await {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.stop\", false).await {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.pause\", false).await {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.resume\", true).await {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn diagnose_deepseek(\n state: State<'_, SharedState>,\n api_key: Option<String>,\n) -> Result<ApiResponse<DeepseekDiagnostics>, String> {\n let key = match api_key {\n Some(key) if !key.trim().is_empty() => key,\n _ => match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n },\n };\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::diagnose(&config, &key).await {\n Ok(result) => Ok(api_ok(result)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload { poll_interval_ms };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let state = Arc::new(Mutex::new(AppState::new(config, initial_status())));\n app.manage(state);\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key,\n diagnose_deepseek,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n"}, "a05dcc3609dc13fd8cf08dbf4209f239775e8877": {"t": 1785757038.0257003, "n": 10131, "h": "02861a86a5a059c0", "c": "mod agent;\nmod config;\nmod deepseek;\nmod ipc;\nmod logging;\nmod secret;\nmod state;\nmod types;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::config::save_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ipc::{InputWritePayload, ListenControlPayload};\nuse crate::types::{api_err, api_ok, ApiResponse, Config, Platform, RuntimeState, Status};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, Manager, State};\nuse tokio::sync::Mutex;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn list_models(state: State<'_, SharedState>) -> Result<ApiResponse<Vec<String>>, String> {\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n let api_key = match ApiKeyManager::get_deepseek_api_key() {\n Ok(key) => key,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n match deepseek::list_models(&config, &api_key).await {\n Ok(models) => Ok(api_ok(models)),\n Err(err) => Ok(api_err(err.to_string())),\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.start\", true).await {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.stop\", false).await {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.pause\", false).await {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.resume\", true).await {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_deepseek_model(\n app: AppHandle,\n state: State<'_, SharedState>,\n model: String,\n) -> Result<ApiResponse<()>, String> {\n if !deepseek::is_supported_model(&model) {\n return Ok(api_err(\"不支持的模型\"));\n }\n let mut guard = state.lock().await;\n guard.config.deepseek_model = model;\n if let Err(err) = save_config(&app, &guard.config) {\n warn!(\"保存模型失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload { poll_interval_ms };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let state = Arc::new(Mutex::new(AppState::new(config, initial_status())));\n app.manage(state);\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key,\n list_models,\n set_deepseek_model\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n"}, "27e7c2c39423a0c95963cb515fe0ced0d37226e8": {"t": 1785757039.886555, "n": 8987, "h": "66a74af69c6d7b59", "c": "mod agent;\nmod config;\nmod deepseek;\nmod ipc;\nmod logging;\nmod secret;\nmod state;\nmod types;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ipc::{InputWritePayload, ListenControlPayload};\nuse crate::types::{api_err, api_ok, ApiResponse, Config, Platform, RuntimeState, Status};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, Manager, State};\nuse tokio::sync::Mutex;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到开始监听请求\");\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n info!(\"已在监听中,忽略重复请求\");\n return Ok(api_ok(()));\n }\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n warn!(\"启动 Agent 失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.start\", true).await {\n warn!(\"发送监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已启动\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到停止监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.stop\", false).await {\n warn!(\"发送停止监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n info!(\"监听已停止\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到暂停监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.pause\", false).await {\n warn!(\"发送暂停监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n info!(\"监听已暂停\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n info!(\"收到恢复监听请求\");\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.resume\", true).await {\n warn!(\"发送恢复监听指令失败: {}\", err);\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n info!(\"监听已恢复\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n warn!(\"写入建议失败: chat_id 为空\");\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n warn!(\"写入建议失败: 回复内容为空\");\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n warn!(\"写入建议失败: 回复内容过长\");\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n warn!(\"写入建议失败: Agent 未连接\");\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n warn!(\"写入建议失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n info!(\"写入建议完成\");\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n info!(\"保存 API 密钥\");\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n warn!(\"保存 API 密钥失败: {}\", err);\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => {\n info!(\"API 密钥验证成功\");\n Ok(api_ok(()))\n }\n Err(err) => {\n warn!(\"API 密钥验证失败: {}\", err);\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n info!(\"删除 API 密钥\");\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => {\n info!(\"API 密钥已删除\");\n api_ok(())\n }\n Err(err) => api_err(err.to_string()),\n })\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload { poll_interval_ms };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let state = Arc::new(Mutex::new(AppState::new(config, initial_status())));\n app.manage(state);\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n"}, "42aadc15b5ec862adb00093f5dadeeb20e6a134c": {"t": 1785757041.5157292, "n": 8157, "h": "036a148483103535", "c": "mod agent;\nmod config;\nmod deepseek;\nmod ipc;\nmod logging;\nmod secret;\nmod state;\nmod types;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ipc::{InputWritePayload, ListenControlPayload};\nuse crate::types::{api_err, api_ok, ApiResponse, Config, Platform, RuntimeState, Status};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, Manager, State};\nuse tokio::sync::Mutex;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n return Ok(api_ok(()));\n }\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.start\", true).await {\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.stop\", false).await {\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.pause\", false).await {\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n if let Err(err) = send_listen_control(state.inner().clone(), \"listen.resume\", true).await {\n return Ok(api_err(err));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => Ok(api_ok(())),\n Err(err) => {\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => api_ok(()),\n Err(err) => api_err(err.to_string()),\n })\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn send_listen_control(\n state: SharedState,\n message_type: &str,\n include_poll_interval: bool,\n) -> Result<(), String> {\n let (sender, poll_interval_ms) = {\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Err(\"Agent 未连接\".to_string());\n };\n (\n agent.clone_sender(),\n if include_poll_interval {\n Some(guard.config.poll_interval_ms)\n } else {\n None\n },\n )\n };\n let payload = ListenControlPayload { poll_interval_ms };\n let payload_value = serde_json::to_value(payload).map_err(|err| err.to_string())?;\n sender\n .send(crate::ipc::IpcEnvelope::new(message_type, payload_value))\n .await\n .map_err(|err| err.to_string())\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let state = Arc::new(Mutex::new(AppState::new(config, initial_status())));\n app.manage(state);\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n"}, "bd637b4b147ad1fec37e76f8006f64a7ed20a83c": {"t": 1785757042.8622563, "n": 6766, "h": "73bc04108d885a3d", "c": "mod agent;\nmod config;\nmod deepseek;\nmod ipc;\nmod logging;\nmod secret;\nmod state;\nmod types;\n\nuse crate::agent::start_agent;\nuse crate::config::load_config;\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ipc::InputWritePayload;\nuse crate::types::{api_err, api_ok, ApiResponse, Config, Platform, RuntimeState, Status};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, Manager, State};\nuse tokio::sync::Mutex;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n _app: AppHandle,\n _state: State<'_, SharedState>,\n _config: Config,\n) -> Result<ApiResponse<()>, String> {\n Ok(api_err(\"配置已固定为默认值\"))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n return Ok(api_ok(()));\n }\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n return Ok(api_err(err.to_string()));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(\n state: State<'_, SharedState>,\n api_key: String,\n) -> Result<ApiResponse<()>, String> {\n if let Err(err) = ApiKeyManager::set_deepseek_api_key(&api_key) {\n return Ok(api_err(err.to_string()));\n }\n\n let config = {\n let guard = state.lock().await;\n guard.config.clone()\n };\n match deepseek::validate_api_key(&config, &api_key).await {\n Ok(()) => Ok(api_ok(())),\n Err(err) => {\n let _ = ApiKeyManager::delete_deepseek_api_key();\n Ok(api_err(err.to_string()))\n }\n }\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => api_ok(()),\n Err(err) => api_err(err.to_string()),\n })\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let state = Arc::new(Mutex::new(AppState::new(config, initial_status())));\n app.manage(state);\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n"}, "49b771fb17bdfd82cbd11d5fe95044d8144d5094": {"t": 1785757044.8502946, "n": 6776, "h": "eff3c3e90aad4947", "c": "mod agent;\nmod config;\nmod deepseek;\nmod ipc;\nmod logging;\nmod secret;\nmod state;\nmod types;\n\nuse crate::agent::start_agent;\nuse crate::config::{load_config, save_config, validate_config};\nuse crate::secret::ApiKeyManager;\nuse crate::state::AppState;\nuse crate::ipc::InputWritePayload;\nuse crate::types::{api_err, api_ok, ApiResponse, Config, Platform, RuntimeState, Status};\nuse std::sync::Arc;\nuse tauri::{AppHandle, Emitter, Manager, State};\nuse tokio::sync::Mutex;\nuse tracing::{info, warn};\n\ntype SharedState = Arc<Mutex<AppState>>;\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_config(state: State<'_, SharedState>) -> Result<ApiResponse<Config>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.config.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn set_config(\n app: AppHandle,\n state: State<'_, SharedState>,\n config: Config,\n) -> Result<ApiResponse<()>, String> {\n if let Err(err) = validate_config(&config) {\n return Ok(api_err(err.to_string()));\n }\n if let Err(err) = save_config(&app, &config) {\n return Ok(api_err(err.to_string()));\n }\n {\n let mut guard = state.lock().await;\n guard.config = config.clone();\n }\n let _ = app.emit(\"config.updated\", config);\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_status(state: State<'_, SharedState>) -> Result<ApiResponse<Status>, String> {\n let guard = state.lock().await;\n Ok(api_ok(guard.status.clone()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn start_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n {\n let guard = state.lock().await;\n if guard.status.state == RuntimeState::Listening {\n return Ok(api_ok(()));\n }\n }\n\n if let Err(err) = ensure_agent_running(app.clone(), state.inner().clone()).await {\n return Ok(api_err(err.to_string()));\n }\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn stop_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Idle, \"\").await;\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn pause_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Paused, \"\").await;\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn resume_listening(\n app: AppHandle,\n state: State<'_, SharedState>,\n) -> Result<ApiResponse<()>, String> {\n set_runtime_state(&app, state.inner().clone(), RuntimeState::Listening, \"\").await;\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn write_suggestion(\n state: State<'_, SharedState>,\n chat_id: String,\n text: String,\n) -> Result<ApiResponse<()>, String> {\n if chat_id.trim().is_empty() {\n return Ok(api_err(\"chat_id 不能为空\"));\n }\n if text.trim().is_empty() {\n return Ok(api_err(\"回复内容不能为空\"));\n }\n if text.len() > 2000 {\n return Ok(api_err(\"回复内容过长\"));\n }\n\n let guard = state.lock().await;\n let Some(agent) = guard.agent.as_ref() else {\n return Ok(api_err(\"Agent 未连接\"));\n };\n\n let payload = InputWritePayload {\n chat_id,\n text,\n mode: Some(\"paste\".to_string()),\n restore_clipboard: Some(true),\n };\n let payload_value = match serde_json::to_value(payload) {\n Ok(value) => value,\n Err(err) => return Ok(api_err(err.to_string())),\n };\n if let Err(err) =\n agent\n .send(crate::ipc::IpcEnvelope::new(\"input.write\", payload_value))\n .await\n {\n return Ok(api_err(err.to_string()));\n }\n Ok(api_ok(()))\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn save_api_key(api_key: String) -> Result<ApiResponse<()>, String> {\n Ok(match ApiKeyManager::set_deepseek_api_key(&api_key) {\n Ok(()) => api_ok(()),\n Err(err) => api_err(err.to_string()),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn get_api_key_status() -> Result<ApiResponse<bool>, String> {\n Ok(match ApiKeyManager::get_deepseek_api_key() {\n Ok(_) => api_ok(true),\n Err(_) => api_ok(false),\n })\n}\n\n#[tauri::command]\n#[specta::specta]\nasync fn delete_api_key() -> Result<ApiResponse<()>, String> {\n Ok(match ApiKeyManager::delete_deepseek_api_key() {\n Ok(()) => api_ok(()),\n Err(err) => api_err(err.to_string()),\n })\n}\n\nasync fn ensure_agent_running(app: AppHandle, state: SharedState) -> anyhow::Result<()> {\n let exists = {\n let guard = state.lock().await;\n guard.agent.is_some()\n };\n if exists {\n return Ok(());\n }\n match start_agent(app.clone(), state.clone()).await {\n Ok(agent) => {\n let mut guard = state.lock().await;\n guard.agent = Some(agent);\n Ok(())\n }\n Err(err) => {\n warn!(\"启动 Agent 失败: {}\", err);\n Err(err)\n }\n }\n}\n\nasync fn set_runtime_state(\n app: &AppHandle,\n state: SharedState,\n runtime: RuntimeState,\n last_error: impl Into<String>,\n) {\n let mut guard = state.lock().await;\n guard.status.state = runtime;\n guard.status.last_error = last_error.into();\n let _ = app.emit(\"status.changed\", guard.status.clone());\n}\n\nfn initial_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n Platform::Windows\n } else if cfg!(target_os = \"macos\") {\n Platform::Macos\n } else {\n Platform::Unknown\n };\n Status {\n state: RuntimeState::Idle,\n platform,\n agent_connected: false,\n last_error: String::new(),\n }\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .setup(|app| {\n let config = load_config(app.handle())?;\n logging::init_logging(app.handle(), &config)?;\n let state = Arc::new(Mutex::new(AppState::new(config, initial_status())));\n app.manage(state);\n info!(\"WeReply 启动完成\");\n Ok(())\n })\n .invoke_handler(tauri::generate_handler![\n get_config,\n set_config,\n start_listening,\n stop_listening,\n pause_listening,\n resume_listening,\n write_suggestion,\n get_status,\n save_api_key,\n get_api_key_status,\n delete_api_key\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n"}, "bf06f6564f88b28e20cca543444126f1a21843ea": {"t": 1785757047.8006272, "n": 1202, "h": "0014f53042689b76", "c": "mod config;\nmod deepseek;\nmod agent;\nmod ipc;\n\nuse serde::Serialize;\n\n#[derive(Serialize)]\nstruct Status {\n state: String,\n platform: String,\n agent_connected: bool,\n last_error: String,\n}\n\n#[tauri::command]\nfn start_listening() -> bool {\n true\n}\n\n#[tauri::command]\nfn stop_listening() -> bool {\n true\n}\n\n#[tauri::command]\nfn get_status() -> Status {\n let platform = if cfg!(target_os = \"windows\") {\n \"windows\"\n } else if cfg!(target_os = \"macos\") {\n \"macos\"\n } else {\n \"unknown\"\n };\n Status {\n state: \"idle\".to_string(),\n platform: platform.to_string(),\n agent_connected: false,\n last_error: \"\".to_string(),\n }\n}\n\n#[tauri::command]\nfn write_suggestion(_chat_id: String, _text: String) -> bool {\n true\n}\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n tauri::Builder::default()\n .plugin(tauri_plugin_opener::init())\n .invoke_handler(tauri::generate_handler![\n start_listening,\n stop_listening,\n get_status,\n write_suggestion\n ])\n .run(tauri::generate_context!())\n .expect(\"error while running tauri application\");\n}\n"}}, "src-tauri/src/deepseek.rs": {"6a2debd152a2a8850bdb14c09a678e0c9ae5c792": {"t": 1785746250.0337152, "n": 13229, "h": "beb54bd8b803c273", "c": "use crate::types::{\n Config, DeepseekDiagnostics, DeepseekEndpointStatus, Suggestion, SuggestionStyle,\n};\nuse anyhow::{Context, Result};\nuse reqwest::Client;\nuse serde_json::{json, Value};\nuse std::time::Duration;\nuse tracing::{info, warn};\nuse uuid::Uuid;\n\nconst SYSTEM_PROMPT: &str = \"你是回复建议助手。请根据对话内容生成 3 条回复建议,分别为正式、\\\n中性、轻松风格。返回 JSON 数组,每个元素包含 style(formal|neutral|casual) 与 text。\";\nconst VALIDATION_PROMPT: &str = \"请回复一个简短确认词,用于验证连接。\";\nconst DEFAULT_MODELS: [&str; 2] = [\"deepseek-chat\", \"deepseek-reasoner\"];\n\nfn cap_timeout_ms(timeout_ms: u64) -> u64 {\n timeout_ms.clamp(2_000, 12_000)\n}\n\npub fn build_request(user_input: &str, model: &str) -> Value {\n json!({\n \"model\": model,\n \"stream\": false,\n \"messages\": [\n {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": user_input}\n ]\n })\n}\n\npub fn build_validation_request(user_input: &str, model: &str) -> Value {\n json!({\n \"model\": model,\n \"stream\": false,\n \"messages\": [\n {\"role\": \"system\", \"content\": VALIDATION_PROMPT},\n {\"role\": \"user\", \"content\": user_input}\n ]\n })\n}\n\npub fn is_supported_model(model: &str) -> bool {\n DEFAULT_MODELS.iter().any(|item| item == &model)\n}\n\nfn default_models() -> Vec<String> {\n DEFAULT_MODELS.iter().map(|model| (*model).to_string()).collect()\n}\n\nfn normalize_models(models: Vec<String>) -> Vec<String> {\n let mut normalized = Vec::new();\n for model in DEFAULT_MODELS {\n if models.iter().any(|item| item == model) {\n normalized.push(model.to_string());\n }\n }\n if normalized.is_empty() {\n default_models()\n } else {\n normalized\n }\n}\n\nfn build_chat_url(base_url: &str) -> String {\n format!(\"{}/chat/completions\", base_url.trim_end_matches('/'))\n}\n\nfn build_models_url(base_url: &str) -> String {\n format!(\"{}/models\", base_url.trim_end_matches('/'))\n}\n\nfn build_ok_status(status: reqwest::StatusCode) -> DeepseekEndpointStatus {\n DeepseekEndpointStatus {\n ok: true,\n status: Some(status.as_u16()),\n message: \"ok\".to_string(),\n }\n}\n\nfn build_error_status(status: Option<reqwest::StatusCode>, message: impl Into<String>) -> DeepseekEndpointStatus {\n DeepseekEndpointStatus {\n ok: false,\n status: status.map(|code| code.as_u16()),\n message: message.into(),\n }\n}\n\nfn format_http_error(status: reqwest::StatusCode, raw: &str) -> String {\n let detail: String = raw.chars().take(200).collect();\n if detail.is_empty() {\n format!(\"HTTP {}\", status)\n } else {\n format!(\"HTTP {} {}\", status, detail)\n }\n}\n\nfn parse_models(raw: &str) -> Result<Vec<String>> {\n let value: Value = serde_json::from_str(raw).context(\"响应 JSON 解析失败\")?;\n let Some(items) = value[\"data\"].as_array() else {\n return Ok(Vec::new());\n };\n let mut models = Vec::new();\n for item in items {\n if let Some(id) = item[\"id\"].as_str() {\n models.push(id.to_string());\n }\n }\n Ok(models)\n}\n\npub async fn validate_api_key(config: &Config, api_key: &str) -> Result<()> {\n let timeout_ms = cap_timeout_ms(config.timeout_ms);\n info!(\"开始验证 DeepSeek API 密钥\");\n let client = Client::builder()\n .timeout(Duration::from_millis(timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = build_chat_url(&config.base_url);\n let request = build_validation_request(\"ping\", &config.deepseek_model);\n\n let response = tokio::time::timeout(\n Duration::from_millis(timeout_ms),\n client\n .post(url)\n .bearer_auth(api_key)\n .json(&request)\n .send(),\n )\n .await\n .context(\"DeepSeek 连接超时\")?\n .context(\"DeepSeek 连接失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n if !status.is_success() {\n let detail: String = raw.chars().take(200).collect();\n warn!(\"DeepSeek 验证失败: {}\", status);\n anyhow::bail!(\"DeepSeek 验证失败: {} {}\", status, detail);\n }\n info!(\"DeepSeek 验证成功\");\n Ok(())\n}\n\npub async fn generate_suggestions(\n config: &Config,\n api_key: Option<String>,\n context_messages: &[String],\n) -> Result<Vec<Suggestion>> {\n let prompt = build_prompt(context_messages);\n let Some(key) = api_key else {\n return Ok(fallback_suggestions(&prompt));\n };\n\n let client = Client::builder()\n .timeout(Duration::from_millis(config.timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = build_chat_url(&config.base_url);\n let request = build_request(&prompt, &config.deepseek_model);\n\n let response = client\n .post(url)\n .bearer_auth(key)\n .json(&request)\n .send()\n .await\n .context(\"DeepSeek 请求失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n\n if !status.is_success() {\n warn!(\"DeepSeek 返回错误: {}\", status);\n return Ok(fallback_suggestions(&prompt));\n }\n\n match parse_response(&raw) {\n Ok(suggestions) if !suggestions.is_empty() => Ok(suggestions),\n Ok(_) => Ok(fallback_suggestions(&prompt)),\n Err(err) => {\n warn!(\"解析 DeepSeek 响应失败: {}\", err);\n Ok(fallback_suggestions(&prompt))\n }\n }\n}\n\npub async fn list_models(config: &Config, api_key: &str) -> Result<Vec<String>> {\n let timeout_ms = cap_timeout_ms(config.timeout_ms);\n let client = Client::builder()\n .timeout(Duration::from_millis(timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = build_models_url(&config.base_url);\n\n let response = tokio::time::timeout(\n Duration::from_millis(timeout_ms),\n client.get(url).bearer_auth(api_key).send(),\n )\n .await\n .context(\"DeepSeek 连接超时\")?\n .context(\"DeepSeek 连接失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n if !status.is_success() {\n let detail: String = raw.chars().take(200).collect();\n warn!(\"DeepSeek 拉取模型失败: {}\", status);\n anyhow::bail!(\"DeepSeek 拉取模型失败: {} {}\", status, detail);\n }\n let parsed = parse_models(&raw)?;\n Ok(normalize_models(parsed))\n}\n\npub async fn diagnose(config: &Config, api_key: &str) -> Result<DeepseekDiagnostics> {\n let timeout_ms = cap_timeout_ms(config.timeout_ms);\n let client = Client::builder()\n .timeout(Duration::from_millis(timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let chat = probe_chat(&client, config, api_key, timeout_ms).await;\n let models = probe_models(&client, config, api_key, timeout_ms).await;\n Ok(DeepseekDiagnostics {\n base_url: config.base_url.clone(),\n model: config.deepseek_model.clone(),\n chat,\n models,\n })\n}\n\nasync fn probe_chat(\n client: &Client,\n config: &Config,\n api_key: &str,\n timeout_ms: u64,\n) -> DeepseekEndpointStatus {\n let url = build_chat_url(&config.base_url);\n let request = build_validation_request(\"ping\", &config.deepseek_model);\n let response = tokio::time::timeout(\n Duration::from_millis(timeout_ms),\n client\n .post(url)\n .bearer_auth(api_key)\n .json(&request)\n .send(),\n )\n .await;\n\n let response = match response {\n Err(_) => return build_error_status(None, \"连接超时\"),\n Ok(Err(err)) => return build_error_status(None, err.to_string()),\n Ok(Ok(response)) => response,\n };\n\n let status = response.status();\n let raw = match response.text().await {\n Ok(raw) => raw,\n Err(err) => return build_error_status(Some(status), err.to_string()),\n };\n\n if status.is_success() {\n build_ok_status(status)\n } else {\n build_error_status(Some(status), format_http_error(status, &raw))\n }\n}\n\nasync fn probe_models(\n client: &Client,\n config: &Config,\n api_key: &str,\n timeout_ms: u64,\n) -> DeepseekEndpointStatus {\n let url = build_models_url(&config.base_url);\n let response = tokio::time::timeout(\n Duration::from_millis(timeout_ms),\n client.get(url).bearer_auth(api_key).send(),\n )\n .await;\n\n let response = match response {\n Err(_) => return build_error_status(None, \"连接超时\"),\n Ok(Err(err)) => return build_error_status(None, err.to_string()),\n Ok(Ok(response)) => response,\n };\n\n let status = response.status();\n let raw = match response.text().await {\n Ok(raw) => raw,\n Err(err) => return build_error_status(Some(status), err.to_string()),\n };\n\n if status.is_success() {\n build_ok_status(status)\n } else {\n build_error_status(Some(status), format_http_error(status, &raw))\n }\n}\n\nfn build_prompt(context_messages: &[String]) -> String {\n if context_messages.is_empty() {\n return \"用户未提供上下文,请生成礼貌的确认回复。\".to_string();\n }\n let mut lines = Vec::new();\n for (idx, message) in context_messages.iter().enumerate() {\n lines.push(format!(\"{}: {}\", idx + 1, message));\n }\n format!(\"最近对话:\\n{}\\n请生成 3 条回复建议。\", lines.join(\"\\n\"))\n}\n\nfn parse_response(raw: &str) -> Result<Vec<Suggestion>> {\n let json_value: Value = serde_json::from_str(raw).context(\"响应 JSON 解析失败\")?;\n let content = json_value[\"choices\"][0][\"message\"][\"content\"]\n .as_str()\n .unwrap_or_default()\n .trim();\n if content.is_empty() {\n return Ok(Vec::new());\n }\n\n let cleaned = content\n .trim_start_matches(\"```json\")\n .trim_end_matches(\"```\")\n .trim();\n if let Ok(items) = serde_json::from_str::<Vec<Value>>(cleaned) {\n let mut suggestions = Vec::new();\n for item in items {\n let style = match item[\"style\"].as_str().unwrap_or(\"neutral\") {\n \"formal\" => SuggestionStyle::Formal,\n \"casual\" => SuggestionStyle::Casual,\n _ => SuggestionStyle::Neutral,\n };\n let text = item[\"text\"].as_str().unwrap_or(\"\").trim().to_string();\n if !text.is_empty() {\n suggestions.push(Suggestion {\n id: Uuid::new_v4().to_string(),\n style,\n text,\n });\n }\n }\n return Ok(suggestions);\n }\n\n info!(\"DeepSeek 返回非 JSON 结构,使用降级解析\");\n let suggestions = cleaned\n .lines()\n .filter_map(|line| {\n let text = line.trim_matches(['-', ' ']).trim();\n if text.is_empty() {\n None\n } else {\n Some(Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Neutral,\n text: text.to_string(),\n })\n }\n })\n .collect();\n Ok(suggestions)\n}\n\nfn fallback_suggestions(prompt: &str) -> Vec<Suggestion> {\n let summary = summarize_text(prompt);\n vec![\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Formal,\n text: format!(\"好的,我了解了:{},稍后给您回复。\", summary),\n },\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Neutral,\n text: format!(\"收到,我看看 {} 再回复你。\", summary),\n },\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Casual,\n text: format!(\"好哒~{} 我等下回你。\", summary),\n },\n ]\n}\n\nfn summarize_text(text: &str) -> String {\n let trimmed: String = text.chars().take(20).collect();\n if trimmed.is_empty() {\n \"消息\".to_string()\n } else {\n trimmed\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_request_payload_is_minimal() {\n let req = build_request(\"hi\", \"deepseek-chat\");\n assert_eq!(req[\"model\"], \"deepseek-chat\");\n assert_eq!(req[\"messages\"].as_array().unwrap().len(), 2);\n assert_eq!(req[\"stream\"], false);\n assert!(req.get(\"temperature\").is_none());\n assert!(req.get(\"top_p\").is_none());\n assert!(req.get(\"n\").is_none());\n }\n\n #[test]\n fn fallback_has_three_styles() {\n let suggestions = fallback_suggestions(\"hi\");\n assert_eq!(suggestions.len(), 3);\n }\n\n #[test]\n fn build_validation_request_is_minimal() {\n let req = build_validation_request(\"ping\", \"deepseek-chat\");\n assert_eq!(req[\"model\"], \"deepseek-chat\");\n assert_eq!(req[\"stream\"], false);\n assert!(req.get(\"temperature\").is_none());\n assert!(req.get(\"top_p\").is_none());\n assert!(req.get(\"n\").is_none());\n }\n\n #[test]\n fn normalize_timeout_caps() {\n assert_eq!(cap_timeout_ms(20_000), 12_000);\n }\n\n #[test]\n fn build_chat_url_trims_slash() {\n let url = build_chat_url(\"https://api.deepseek.com/\");\n assert_eq!(url, \"https://api.deepseek.com/chat/completions\");\n }\n\n #[test]\n fn normalize_models_filters_and_fallbacks() {\n let models = normalize_models(vec![\"x\".to_string()]);\n assert_eq!(models, vec![\"deepseek-chat\", \"deepseek-reasoner\"]);\n }\n}\n"}, "5de993ddeedb39d449ba4f09e0f3e1efb6ebbef4": {"t": 1785757006.9375153, "n": 13229, "h": "beb54bd8b803c273", "c": "use crate::types::{\n Config, DeepseekDiagnostics, DeepseekEndpointStatus, Suggestion, SuggestionStyle,\n};\nuse anyhow::{Context, Result};\nuse reqwest::Client;\nuse serde_json::{json, Value};\nuse std::time::Duration;\nuse tracing::{info, warn};\nuse uuid::Uuid;\n\nconst SYSTEM_PROMPT: &str = \"你是回复建议助手。请根据对话内容生成 3 条回复建议,分别为正式、\\\n中性、轻松风格。返回 JSON 数组,每个元素包含 style(formal|neutral|casual) 与 text。\";\nconst VALIDATION_PROMPT: &str = \"请回复一个简短确认词,用于验证连接。\";\nconst DEFAULT_MODELS: [&str; 2] = [\"deepseek-chat\", \"deepseek-reasoner\"];\n\nfn cap_timeout_ms(timeout_ms: u64) -> u64 {\n timeout_ms.clamp(2_000, 12_000)\n}\n\npub fn build_request(user_input: &str, model: &str) -> Value {\n json!({\n \"model\": model,\n \"stream\": false,\n \"messages\": [\n {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": user_input}\n ]\n })\n}\n\npub fn build_validation_request(user_input: &str, model: &str) -> Value {\n json!({\n \"model\": model,\n \"stream\": false,\n \"messages\": [\n {\"role\": \"system\", \"content\": VALIDATION_PROMPT},\n {\"role\": \"user\", \"content\": user_input}\n ]\n })\n}\n\npub fn is_supported_model(model: &str) -> bool {\n DEFAULT_MODELS.iter().any(|item| item == &model)\n}\n\nfn default_models() -> Vec<String> {\n DEFAULT_MODELS.iter().map(|model| (*model).to_string()).collect()\n}\n\nfn normalize_models(models: Vec<String>) -> Vec<String> {\n let mut normalized = Vec::new();\n for model in DEFAULT_MODELS {\n if models.iter().any(|item| item == model) {\n normalized.push(model.to_string());\n }\n }\n if normalized.is_empty() {\n default_models()\n } else {\n normalized\n }\n}\n\nfn build_chat_url(base_url: &str) -> String {\n format!(\"{}/chat/completions\", base_url.trim_end_matches('/'))\n}\n\nfn build_models_url(base_url: &str) -> String {\n format!(\"{}/models\", base_url.trim_end_matches('/'))\n}\n\nfn build_ok_status(status: reqwest::StatusCode) -> DeepseekEndpointStatus {\n DeepseekEndpointStatus {\n ok: true,\n status: Some(status.as_u16()),\n message: \"ok\".to_string(),\n }\n}\n\nfn build_error_status(status: Option<reqwest::StatusCode>, message: impl Into<String>) -> DeepseekEndpointStatus {\n DeepseekEndpointStatus {\n ok: false,\n status: status.map(|code| code.as_u16()),\n message: message.into(),\n }\n}\n\nfn format_http_error(status: reqwest::StatusCode, raw: &str) -> String {\n let detail: String = raw.chars().take(200).collect();\n if detail.is_empty() {\n format!(\"HTTP {}\", status)\n } else {\n format!(\"HTTP {} {}\", status, detail)\n }\n}\n\nfn parse_models(raw: &str) -> Result<Vec<String>> {\n let value: Value = serde_json::from_str(raw).context(\"响应 JSON 解析失败\")?;\n let Some(items) = value[\"data\"].as_array() else {\n return Ok(Vec::new());\n };\n let mut models = Vec::new();\n for item in items {\n if let Some(id) = item[\"id\"].as_str() {\n models.push(id.to_string());\n }\n }\n Ok(models)\n}\n\npub async fn validate_api_key(config: &Config, api_key: &str) -> Result<()> {\n let timeout_ms = cap_timeout_ms(config.timeout_ms);\n info!(\"开始验证 DeepSeek API 密钥\");\n let client = Client::builder()\n .timeout(Duration::from_millis(timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = build_chat_url(&config.base_url);\n let request = build_validation_request(\"ping\", &config.deepseek_model);\n\n let response = tokio::time::timeout(\n Duration::from_millis(timeout_ms),\n client\n .post(url)\n .bearer_auth(api_key)\n .json(&request)\n .send(),\n )\n .await\n .context(\"DeepSeek 连接超时\")?\n .context(\"DeepSeek 连接失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n if !status.is_success() {\n let detail: String = raw.chars().take(200).collect();\n warn!(\"DeepSeek 验证失败: {}\", status);\n anyhow::bail!(\"DeepSeek 验证失败: {} {}\", status, detail);\n }\n info!(\"DeepSeek 验证成功\");\n Ok(())\n}\n\npub async fn generate_suggestions(\n config: &Config,\n api_key: Option<String>,\n context_messages: &[String],\n) -> Result<Vec<Suggestion>> {\n let prompt = build_prompt(context_messages);\n let Some(key) = api_key else {\n return Ok(fallback_suggestions(&prompt));\n };\n\n let client = Client::builder()\n .timeout(Duration::from_millis(config.timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = build_chat_url(&config.base_url);\n let request = build_request(&prompt, &config.deepseek_model);\n\n let response = client\n .post(url)\n .bearer_auth(key)\n .json(&request)\n .send()\n .await\n .context(\"DeepSeek 请求失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n\n if !status.is_success() {\n warn!(\"DeepSeek 返回错误: {}\", status);\n return Ok(fallback_suggestions(&prompt));\n }\n\n match parse_response(&raw) {\n Ok(suggestions) if !suggestions.is_empty() => Ok(suggestions),\n Ok(_) => Ok(fallback_suggestions(&prompt)),\n Err(err) => {\n warn!(\"解析 DeepSeek 响应失败: {}\", err);\n Ok(fallback_suggestions(&prompt))\n }\n }\n}\n\npub async fn list_models(config: &Config, api_key: &str) -> Result<Vec<String>> {\n let timeout_ms = cap_timeout_ms(config.timeout_ms);\n let client = Client::builder()\n .timeout(Duration::from_millis(timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = build_models_url(&config.base_url);\n\n let response = tokio::time::timeout(\n Duration::from_millis(timeout_ms),\n client.get(url).bearer_auth(api_key).send(),\n )\n .await\n .context(\"DeepSeek 连接超时\")?\n .context(\"DeepSeek 连接失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n if !status.is_success() {\n let detail: String = raw.chars().take(200).collect();\n warn!(\"DeepSeek 拉取模型失败: {}\", status);\n anyhow::bail!(\"DeepSeek 拉取模型失败: {} {}\", status, detail);\n }\n let parsed = parse_models(&raw)?;\n Ok(normalize_models(parsed))\n}\n\npub async fn diagnose(config: &Config, api_key: &str) -> Result<DeepseekDiagnostics> {\n let timeout_ms = cap_timeout_ms(config.timeout_ms);\n let client = Client::builder()\n .timeout(Duration::from_millis(timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let chat = probe_chat(&client, config, api_key, timeout_ms).await;\n let models = probe_models(&client, config, api_key, timeout_ms).await;\n Ok(DeepseekDiagnostics {\n base_url: config.base_url.clone(),\n model: config.deepseek_model.clone(),\n chat,\n models,\n })\n}\n\nasync fn probe_chat(\n client: &Client,\n config: &Config,\n api_key: &str,\n timeout_ms: u64,\n) -> DeepseekEndpointStatus {\n let url = build_chat_url(&config.base_url);\n let request = build_validation_request(\"ping\", &config.deepseek_model);\n let response = tokio::time::timeout(\n Duration::from_millis(timeout_ms),\n client\n .post(url)\n .bearer_auth(api_key)\n .json(&request)\n .send(),\n )\n .await;\n\n let response = match response {\n Err(_) => return build_error_status(None, \"连接超时\"),\n Ok(Err(err)) => return build_error_status(None, err.to_string()),\n Ok(Ok(response)) => response,\n };\n\n let status = response.status();\n let raw = match response.text().await {\n Ok(raw) => raw,\n Err(err) => return build_error_status(Some(status), err.to_string()),\n };\n\n if status.is_success() {\n build_ok_status(status)\n } else {\n build_error_status(Some(status), format_http_error(status, &raw))\n }\n}\n\nasync fn probe_models(\n client: &Client,\n config: &Config,\n api_key: &str,\n timeout_ms: u64,\n) -> DeepseekEndpointStatus {\n let url = build_models_url(&config.base_url);\n let response = tokio::time::timeout(\n Duration::from_millis(timeout_ms),\n client.get(url).bearer_auth(api_key).send(),\n )\n .await;\n\n let response = match response {\n Err(_) => return build_error_status(None, \"连接超时\"),\n Ok(Err(err)) => return build_error_status(None, err.to_string()),\n Ok(Ok(response)) => response,\n };\n\n let status = response.status();\n let raw = match response.text().await {\n Ok(raw) => raw,\n Err(err) => return build_error_status(Some(status), err.to_string()),\n };\n\n if status.is_success() {\n build_ok_status(status)\n } else {\n build_error_status(Some(status), format_http_error(status, &raw))\n }\n}\n\nfn build_prompt(context_messages: &[String]) -> String {\n if context_messages.is_empty() {\n return \"用户未提供上下文,请生成礼貌的确认回复。\".to_string();\n }\n let mut lines = Vec::new();\n for (idx, message) in context_messages.iter().enumerate() {\n lines.push(format!(\"{}: {}\", idx + 1, message));\n }\n format!(\"最近对话:\\n{}\\n请生成 3 条回复建议。\", lines.join(\"\\n\"))\n}\n\nfn parse_response(raw: &str) -> Result<Vec<Suggestion>> {\n let json_value: Value = serde_json::from_str(raw).context(\"响应 JSON 解析失败\")?;\n let content = json_value[\"choices\"][0][\"message\"][\"content\"]\n .as_str()\n .unwrap_or_default()\n .trim();\n if content.is_empty() {\n return Ok(Vec::new());\n }\n\n let cleaned = content\n .trim_start_matches(\"```json\")\n .trim_end_matches(\"```\")\n .trim();\n if let Ok(items) = serde_json::from_str::<Vec<Value>>(cleaned) {\n let mut suggestions = Vec::new();\n for item in items {\n let style = match item[\"style\"].as_str().unwrap_or(\"neutral\") {\n \"formal\" => SuggestionStyle::Formal,\n \"casual\" => SuggestionStyle::Casual,\n _ => SuggestionStyle::Neutral,\n };\n let text = item[\"text\"].as_str().unwrap_or(\"\").trim().to_string();\n if !text.is_empty() {\n suggestions.push(Suggestion {\n id: Uuid::new_v4().to_string(),\n style,\n text,\n });\n }\n }\n return Ok(suggestions);\n }\n\n info!(\"DeepSeek 返回非 JSON 结构,使用降级解析\");\n let suggestions = cleaned\n .lines()\n .filter_map(|line| {\n let text = line.trim_matches(['-', ' ']).trim();\n if text.is_empty() {\n None\n } else {\n Some(Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Neutral,\n text: text.to_string(),\n })\n }\n })\n .collect();\n Ok(suggestions)\n}\n\nfn fallback_suggestions(prompt: &str) -> Vec<Suggestion> {\n let summary = summarize_text(prompt);\n vec![\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Formal,\n text: format!(\"好的,我了解了:{},稍后给您回复。\", summary),\n },\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Neutral,\n text: format!(\"收到,我看看 {} 再回复你。\", summary),\n },\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Casual,\n text: format!(\"好哒~{} 我等下回你。\", summary),\n },\n ]\n}\n\nfn summarize_text(text: &str) -> String {\n let trimmed: String = text.chars().take(20).collect();\n if trimmed.is_empty() {\n \"消息\".to_string()\n } else {\n trimmed\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_request_payload_is_minimal() {\n let req = build_request(\"hi\", \"deepseek-chat\");\n assert_eq!(req[\"model\"], \"deepseek-chat\");\n assert_eq!(req[\"messages\"].as_array().unwrap().len(), 2);\n assert_eq!(req[\"stream\"], false);\n assert!(req.get(\"temperature\").is_none());\n assert!(req.get(\"top_p\").is_none());\n assert!(req.get(\"n\").is_none());\n }\n\n #[test]\n fn fallback_has_three_styles() {\n let suggestions = fallback_suggestions(\"hi\");\n assert_eq!(suggestions.len(), 3);\n }\n\n #[test]\n fn build_validation_request_is_minimal() {\n let req = build_validation_request(\"ping\", \"deepseek-chat\");\n assert_eq!(req[\"model\"], \"deepseek-chat\");\n assert_eq!(req[\"stream\"], false);\n assert!(req.get(\"temperature\").is_none());\n assert!(req.get(\"top_p\").is_none());\n assert!(req.get(\"n\").is_none());\n }\n\n #[test]\n fn normalize_timeout_caps() {\n assert_eq!(cap_timeout_ms(20_000), 12_000);\n }\n\n #[test]\n fn build_chat_url_trims_slash() {\n let url = build_chat_url(\"https://api.deepseek.com/\");\n assert_eq!(url, \"https://api.deepseek.com/chat/completions\");\n }\n\n #[test]\n fn normalize_models_filters_and_fallbacks() {\n let models = normalize_models(vec![\"x\".to_string()]);\n assert_eq!(models, vec![\"deepseek-chat\", \"deepseek-reasoner\"]);\n }\n}\n"}, "636e3a03025ebc89248e99e79da4c06b5642d62f": {"t": 1785757008.2211325, "n": 9922, "h": "8c462f52b9aca07d", "c": "use crate::types::{Config, Suggestion, SuggestionStyle};\nuse anyhow::{Context, Result};\nuse reqwest::Client;\nuse serde_json::{json, Value};\nuse std::time::Duration;\nuse tracing::{info, warn};\nuse uuid::Uuid;\n\nconst SYSTEM_PROMPT: &str = \"你是回复建议助手。请根据对话内容生成 3 条回复建议,分别为正式、\\\n中性、轻松风格。返回 JSON 数组,每个元素包含 style(formal|neutral|casual) 与 text。\";\nconst VALIDATION_PROMPT: &str = \"请回复一个简短确认词,用于验证连接。\";\nconst DEFAULT_MODELS: [&str; 2] = [\"deepseek-chat\", \"deepseek-reasoner\"];\n\nfn cap_timeout_ms(timeout_ms: u64) -> u64 {\n timeout_ms.clamp(2_000, 12_000)\n}\n\npub fn build_request(user_input: &str, model: &str) -> Value {\n json!({\n \"model\": model,\n \"stream\": false,\n \"messages\": [\n {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": user_input}\n ]\n })\n}\n\npub fn build_validation_request(user_input: &str, model: &str) -> Value {\n json!({\n \"model\": model,\n \"stream\": false,\n \"messages\": [\n {\"role\": \"system\", \"content\": VALIDATION_PROMPT},\n {\"role\": \"user\", \"content\": user_input}\n ]\n })\n}\n\npub fn is_supported_model(model: &str) -> bool {\n DEFAULT_MODELS.iter().any(|item| item == &model)\n}\n\nfn default_models() -> Vec<String> {\n DEFAULT_MODELS.iter().map(|model| (*model).to_string()).collect()\n}\n\nfn normalize_models(models: Vec<String>) -> Vec<String> {\n let mut normalized = Vec::new();\n for model in DEFAULT_MODELS {\n if models.iter().any(|item| item == model) {\n normalized.push(model.to_string());\n }\n }\n if normalized.is_empty() {\n default_models()\n } else {\n normalized\n }\n}\n\nfn build_chat_url(base_url: &str) -> String {\n format!(\"{}/chat/completions\", base_url.trim_end_matches('/'))\n}\n\nfn build_models_url(base_url: &str) -> String {\n format!(\"{}/models\", base_url.trim_end_matches('/'))\n}\n\nfn parse_models(raw: &str) -> Result<Vec<String>> {\n let value: Value = serde_json::from_str(raw).context(\"响应 JSON 解析失败\")?;\n let Some(items) = value[\"data\"].as_array() else {\n return Ok(Vec::new());\n };\n let mut models = Vec::new();\n for item in items {\n if let Some(id) = item[\"id\"].as_str() {\n models.push(id.to_string());\n }\n }\n Ok(models)\n}\n\npub async fn validate_api_key(config: &Config, api_key: &str) -> Result<()> {\n let timeout_ms = cap_timeout_ms(config.timeout_ms);\n info!(\"开始验证 DeepSeek API 密钥\");\n let client = Client::builder()\n .timeout(Duration::from_millis(timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = build_chat_url(&config.base_url);\n let request = build_validation_request(\"ping\", &config.deepseek_model);\n\n let response = tokio::time::timeout(\n Duration::from_millis(timeout_ms),\n client\n .post(url)\n .bearer_auth(api_key)\n .json(&request)\n .send(),\n )\n .await\n .context(\"DeepSeek 连接超时\")?\n .context(\"DeepSeek 连接失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n if !status.is_success() {\n let detail: String = raw.chars().take(200).collect();\n warn!(\"DeepSeek 验证失败: {}\", status);\n anyhow::bail!(\"DeepSeek 验证失败: {} {}\", status, detail);\n }\n info!(\"DeepSeek 验证成功\");\n Ok(())\n}\n\npub async fn generate_suggestions(\n config: &Config,\n api_key: Option<String>,\n context_messages: &[String],\n) -> Result<Vec<Suggestion>> {\n let prompt = build_prompt(context_messages);\n let Some(key) = api_key else {\n return Ok(fallback_suggestions(&prompt));\n };\n\n let client = Client::builder()\n .timeout(Duration::from_millis(config.timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = build_chat_url(&config.base_url);\n let request = build_request(&prompt, &config.deepseek_model);\n\n let response = client\n .post(url)\n .bearer_auth(key)\n .json(&request)\n .send()\n .await\n .context(\"DeepSeek 请求失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n\n if !status.is_success() {\n warn!(\"DeepSeek 返回错误: {}\", status);\n return Ok(fallback_suggestions(&prompt));\n }\n\n match parse_response(&raw) {\n Ok(suggestions) if !suggestions.is_empty() => Ok(suggestions),\n Ok(_) => Ok(fallback_suggestions(&prompt)),\n Err(err) => {\n warn!(\"解析 DeepSeek 响应失败: {}\", err);\n Ok(fallback_suggestions(&prompt))\n }\n }\n}\n\npub async fn list_models(config: &Config, api_key: &str) -> Result<Vec<String>> {\n let timeout_ms = cap_timeout_ms(config.timeout_ms);\n let client = Client::builder()\n .timeout(Duration::from_millis(timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = build_models_url(&config.base_url);\n\n let response = tokio::time::timeout(\n Duration::from_millis(timeout_ms),\n client.get(url).bearer_auth(api_key).send(),\n )\n .await\n .context(\"DeepSeek 连接超时\")?\n .context(\"DeepSeek 连接失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n if !status.is_success() {\n let detail: String = raw.chars().take(200).collect();\n warn!(\"DeepSeek 拉取模型失败: {}\", status);\n anyhow::bail!(\"DeepSeek 拉取模型失败: {} {}\", status, detail);\n }\n let parsed = parse_models(&raw)?;\n Ok(normalize_models(parsed))\n}\n\nfn build_prompt(context_messages: &[String]) -> String {\n if context_messages.is_empty() {\n return \"用户未提供上下文,请生成礼貌的确认回复。\".to_string();\n }\n let mut lines = Vec::new();\n for (idx, message) in context_messages.iter().enumerate() {\n lines.push(format!(\"{}: {}\", idx + 1, message));\n }\n format!(\"最近对话:\\n{}\\n请生成 3 条回复建议。\", lines.join(\"\\n\"))\n}\n\nfn parse_response(raw: &str) -> Result<Vec<Suggestion>> {\n let json_value: Value = serde_json::from_str(raw).context(\"响应 JSON 解析失败\")?;\n let content = json_value[\"choices\"][0][\"message\"][\"content\"]\n .as_str()\n .unwrap_or_default()\n .trim();\n if content.is_empty() {\n return Ok(Vec::new());\n }\n\n let cleaned = content\n .trim_start_matches(\"```json\")\n .trim_end_matches(\"```\")\n .trim();\n if let Ok(items) = serde_json::from_str::<Vec<Value>>(cleaned) {\n let mut suggestions = Vec::new();\n for item in items {\n let style = match item[\"style\"].as_str().unwrap_or(\"neutral\") {\n \"formal\" => SuggestionStyle::Formal,\n \"casual\" => SuggestionStyle::Casual,\n _ => SuggestionStyle::Neutral,\n };\n let text = item[\"text\"].as_str().unwrap_or(\"\").trim().to_string();\n if !text.is_empty() {\n suggestions.push(Suggestion {\n id: Uuid::new_v4().to_string(),\n style,\n text,\n });\n }\n }\n return Ok(suggestions);\n }\n\n info!(\"DeepSeek 返回非 JSON 结构,使用降级解析\");\n let suggestions = cleaned\n .lines()\n .filter_map(|line| {\n let text = line.trim_matches(['-', ' ']).trim();\n if text.is_empty() {\n None\n } else {\n Some(Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Neutral,\n text: text.to_string(),\n })\n }\n })\n .collect();\n Ok(suggestions)\n}\n\nfn fallback_suggestions(prompt: &str) -> Vec<Suggestion> {\n let summary = summarize_text(prompt);\n vec![\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Formal,\n text: format!(\"好的,我了解了:{},稍后给您回复。\", summary),\n },\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Neutral,\n text: format!(\"收到,我看看 {} 再回复你。\", summary),\n },\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Casual,\n text: format!(\"好哒~{} 我等下回你。\", summary),\n },\n ]\n}\n\nfn summarize_text(text: &str) -> String {\n let trimmed: String = text.chars().take(20).collect();\n if trimmed.is_empty() {\n \"消息\".to_string()\n } else {\n trimmed\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_request_payload_is_minimal() {\n let req = build_request(\"hi\", \"deepseek-chat\");\n assert_eq!(req[\"model\"], \"deepseek-chat\");\n assert_eq!(req[\"messages\"].as_array().unwrap().len(), 2);\n assert_eq!(req[\"stream\"], false);\n assert!(req.get(\"temperature\").is_none());\n assert!(req.get(\"top_p\").is_none());\n assert!(req.get(\"n\").is_none());\n }\n\n #[test]\n fn fallback_has_three_styles() {\n let suggestions = fallback_suggestions(\"hi\");\n assert_eq!(suggestions.len(), 3);\n }\n\n #[test]\n fn build_validation_request_is_minimal() {\n let req = build_validation_request(\"ping\", \"deepseek-chat\");\n assert_eq!(req[\"model\"], \"deepseek-chat\");\n assert_eq!(req[\"stream\"], false);\n assert!(req.get(\"temperature\").is_none());\n assert!(req.get(\"top_p\").is_none());\n assert!(req.get(\"n\").is_none());\n }\n\n #[test]\n fn normalize_timeout_caps() {\n assert_eq!(cap_timeout_ms(20_000), 12_000);\n }\n\n #[test]\n fn build_chat_url_trims_slash() {\n let url = build_chat_url(\"https://api.deepseek.com/\");\n assert_eq!(url, \"https://api.deepseek.com/chat/completions\");\n }\n\n #[test]\n fn normalize_models_filters_and_fallbacks() {\n let models = normalize_models(vec![\"x\".to_string()]);\n assert_eq!(models, vec![\"deepseek-chat\", \"deepseek-reasoner\"]);\n }\n}\n"}, "a05dcc3609dc13fd8cf08dbf4209f239775e8877": {"t": 1785757009.685507, "n": 9786, "h": "96049ca423666579", "c": "use crate::types::{Config, Suggestion, SuggestionStyle};\nuse anyhow::{Context, Result};\nuse reqwest::Client;\nuse serde_json::{json, Value};\nuse std::time::Duration;\nuse tracing::{info, warn};\nuse uuid::Uuid;\n\nconst SYSTEM_PROMPT: &str = \"你是回复建议助手。请根据对话内容生成 3 条回复建议,分别为正式、\\\n中性、轻松风格。返回 JSON 数组,每个元素包含 style(formal|neutral|casual) 与 text。\";\nconst VALIDATION_PROMPT: &str = \"请回复一个简短确认词,用于验证连接。\";\nconst DEFAULT_MODELS: [&str; 2] = [\"deepseek-chat\", \"deepseek-reasoner\"];\n\nfn cap_timeout_ms(timeout_ms: u64) -> u64 {\n timeout_ms.clamp(2_000, 8_000)\n}\n\npub fn build_request(user_input: &str, model: &str) -> Value {\n json!({\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": user_input}\n ]\n })\n}\n\npub fn build_validation_request(user_input: &str, model: &str) -> Value {\n json!({\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": VALIDATION_PROMPT},\n {\"role\": \"user\", \"content\": user_input}\n ]\n })\n}\n\npub fn is_supported_model(model: &str) -> bool {\n DEFAULT_MODELS.iter().any(|item| item == &model)\n}\n\nfn default_models() -> Vec<String> {\n DEFAULT_MODELS.iter().map(|model| (*model).to_string()).collect()\n}\n\nfn normalize_models(models: Vec<String>) -> Vec<String> {\n let mut normalized = Vec::new();\n for model in DEFAULT_MODELS {\n if models.iter().any(|item| item == model) {\n normalized.push(model.to_string());\n }\n }\n if normalized.is_empty() {\n default_models()\n } else {\n normalized\n }\n}\n\nfn build_chat_url(base_url: &str) -> String {\n format!(\"{}/chat/completions\", base_url.trim_end_matches('/'))\n}\n\nfn build_models_url(base_url: &str) -> String {\n format!(\"{}/models\", base_url.trim_end_matches('/'))\n}\n\nfn parse_models(raw: &str) -> Result<Vec<String>> {\n let value: Value = serde_json::from_str(raw).context(\"响应 JSON 解析失败\")?;\n let Some(items) = value[\"data\"].as_array() else {\n return Ok(Vec::new());\n };\n let mut models = Vec::new();\n for item in items {\n if let Some(id) = item[\"id\"].as_str() {\n models.push(id.to_string());\n }\n }\n Ok(models)\n}\n\npub async fn validate_api_key(config: &Config, api_key: &str) -> Result<()> {\n let timeout_ms = cap_timeout_ms(config.timeout_ms);\n info!(\"开始验证 DeepSeek API 密钥\");\n let client = Client::builder()\n .timeout(Duration::from_millis(timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = build_chat_url(&config.base_url);\n let request = build_validation_request(\"ping\", &config.deepseek_model);\n\n let response = tokio::time::timeout(\n Duration::from_millis(timeout_ms),\n client\n .post(url)\n .bearer_auth(api_key)\n .json(&request)\n .send(),\n )\n .await\n .context(\"DeepSeek 连接超时\")?\n .context(\"DeepSeek 连接失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n if !status.is_success() {\n let detail: String = raw.chars().take(200).collect();\n warn!(\"DeepSeek 验证失败: {}\", status);\n anyhow::bail!(\"DeepSeek 验证失败: {} {}\", status, detail);\n }\n info!(\"DeepSeek 验证成功\");\n Ok(())\n}\n\npub async fn generate_suggestions(\n config: &Config,\n api_key: Option<String>,\n context_messages: &[String],\n) -> Result<Vec<Suggestion>> {\n let prompt = build_prompt(context_messages);\n let Some(key) = api_key else {\n return Ok(fallback_suggestions(&prompt));\n };\n\n let client = Client::builder()\n .timeout(Duration::from_millis(config.timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = build_chat_url(&config.base_url);\n let request = build_request(&prompt, &config.deepseek_model);\n\n let response = client\n .post(url)\n .bearer_auth(key)\n .json(&request)\n .send()\n .await\n .context(\"DeepSeek 请求失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n\n if !status.is_success() {\n warn!(\"DeepSeek 返回错误: {}\", status);\n return Ok(fallback_suggestions(&prompt));\n }\n\n match parse_response(&raw) {\n Ok(suggestions) if !suggestions.is_empty() => Ok(suggestions),\n Ok(_) => Ok(fallback_suggestions(&prompt)),\n Err(err) => {\n warn!(\"解析 DeepSeek 响应失败: {}\", err);\n Ok(fallback_suggestions(&prompt))\n }\n }\n}\n\npub async fn list_models(config: &Config, api_key: &str) -> Result<Vec<String>> {\n let timeout_ms = cap_timeout_ms(config.timeout_ms);\n let client = Client::builder()\n .timeout(Duration::from_millis(timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = build_models_url(&config.base_url);\n\n let response = tokio::time::timeout(\n Duration::from_millis(timeout_ms),\n client.get(url).bearer_auth(api_key).send(),\n )\n .await\n .context(\"DeepSeek 连接超时\")?\n .context(\"DeepSeek 连接失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n if !status.is_success() {\n let detail: String = raw.chars().take(200).collect();\n warn!(\"DeepSeek 拉取模型失败: {}\", status);\n anyhow::bail!(\"DeepSeek 拉取模型失败: {} {}\", status, detail);\n }\n let parsed = parse_models(&raw)?;\n Ok(normalize_models(parsed))\n}\n\nfn build_prompt(context_messages: &[String]) -> String {\n if context_messages.is_empty() {\n return \"用户未提供上下文,请生成礼貌的确认回复。\".to_string();\n }\n let mut lines = Vec::new();\n for (idx, message) in context_messages.iter().enumerate() {\n lines.push(format!(\"{}: {}\", idx + 1, message));\n }\n format!(\"最近对话:\\n{}\\n请生成 3 条回复建议。\", lines.join(\"\\n\"))\n}\n\nfn parse_response(raw: &str) -> Result<Vec<Suggestion>> {\n let json_value: Value = serde_json::from_str(raw).context(\"响应 JSON 解析失败\")?;\n let content = json_value[\"choices\"][0][\"message\"][\"content\"]\n .as_str()\n .unwrap_or_default()\n .trim();\n if content.is_empty() {\n return Ok(Vec::new());\n }\n\n let cleaned = content\n .trim_start_matches(\"```json\")\n .trim_end_matches(\"```\")\n .trim();\n if let Ok(items) = serde_json::from_str::<Vec<Value>>(cleaned) {\n let mut suggestions = Vec::new();\n for item in items {\n let style = match item[\"style\"].as_str().unwrap_or(\"neutral\") {\n \"formal\" => SuggestionStyle::Formal,\n \"casual\" => SuggestionStyle::Casual,\n _ => SuggestionStyle::Neutral,\n };\n let text = item[\"text\"].as_str().unwrap_or(\"\").trim().to_string();\n if !text.is_empty() {\n suggestions.push(Suggestion {\n id: Uuid::new_v4().to_string(),\n style,\n text,\n });\n }\n }\n return Ok(suggestions);\n }\n\n info!(\"DeepSeek 返回非 JSON 结构,使用降级解析\");\n let suggestions = cleaned\n .lines()\n .filter_map(|line| {\n let text = line.trim_matches(['-', ' ']).trim();\n if text.is_empty() {\n None\n } else {\n Some(Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Neutral,\n text: text.to_string(),\n })\n }\n })\n .collect();\n Ok(suggestions)\n}\n\nfn fallback_suggestions(prompt: &str) -> Vec<Suggestion> {\n let summary = summarize_text(prompt);\n vec![\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Formal,\n text: format!(\"好的,我了解了:{},稍后给您回复。\", summary),\n },\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Neutral,\n text: format!(\"收到,我看看 {} 再回复你。\", summary),\n },\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Casual,\n text: format!(\"好哒~{} 我等下回你。\", summary),\n },\n ]\n}\n\nfn summarize_text(text: &str) -> String {\n let trimmed: String = text.chars().take(20).collect();\n if trimmed.is_empty() {\n \"消息\".to_string()\n } else {\n trimmed\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_request_payload_is_minimal() {\n let req = build_request(\"hi\", \"deepseek-chat\");\n assert_eq!(req[\"model\"], \"deepseek-chat\");\n assert_eq!(req[\"messages\"].as_array().unwrap().len(), 2);\n assert!(req.get(\"temperature\").is_none());\n assert!(req.get(\"top_p\").is_none());\n assert!(req.get(\"n\").is_none());\n }\n\n #[test]\n fn fallback_has_three_styles() {\n let suggestions = fallback_suggestions(\"hi\");\n assert_eq!(suggestions.len(), 3);\n }\n\n #[test]\n fn build_validation_request_is_minimal() {\n let req = build_validation_request(\"ping\", \"deepseek-chat\");\n assert_eq!(req[\"model\"], \"deepseek-chat\");\n assert!(req.get(\"temperature\").is_none());\n assert!(req.get(\"top_p\").is_none());\n assert!(req.get(\"n\").is_none());\n }\n\n #[test]\n fn normalize_timeout_caps() {\n assert_eq!(cap_timeout_ms(12_000), 8_000);\n }\n\n #[test]\n fn build_chat_url_trims_slash() {\n let url = build_chat_url(\"https://api.deepseek.com/\");\n assert_eq!(url, \"https://api.deepseek.com/chat/completions\");\n }\n\n #[test]\n fn normalize_models_filters_and_fallbacks() {\n let models = normalize_models(vec![\"x\".to_string()]);\n assert_eq!(models, vec![\"deepseek-chat\", \"deepseek-reasoner\"]);\n }\n}\n"}, "27e7c2c39423a0c95963cb515fe0ced0d37226e8": {"t": 1785757010.9205842, "n": 7475, "h": "683512bb6aad82b1", "c": "use crate::types::{Config, Suggestion, SuggestionStyle};\nuse anyhow::{Context, Result};\nuse reqwest::Client;\nuse serde_json::{json, Value};\nuse std::time::Duration;\nuse tracing::{info, warn};\nuse uuid::Uuid;\n\nconst SYSTEM_PROMPT: &str = \"你是回复建议助手。请根据对话内容生成 3 条回复建议,分别为正式、\\\n中性、轻松风格。返回 JSON 数组,每个元素包含 style(formal|neutral|casual) 与 text。\";\nconst VALIDATION_PROMPT: &str = \"请回复一个简短确认词,用于验证连接。\";\n\nfn cap_timeout_ms(timeout_ms: u64) -> u64 {\n timeout_ms.clamp(2_000, 8_000)\n}\n\npub fn build_request(\n user_input: &str,\n suggestion_count: u32,\n model: &str,\n temperature: f32,\n top_p: f32,\n) -> Value {\n json!({\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": user_input}\n ],\n \"temperature\": temperature,\n \"top_p\": top_p,\n \"n\": suggestion_count,\n \"stream\": false\n })\n}\n\npub fn build_validation_request(user_input: &str, model: &str) -> Value {\n json!({\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": VALIDATION_PROMPT},\n {\"role\": \"user\", \"content\": user_input}\n ],\n \"temperature\": 0.0,\n \"top_p\": 1.0,\n \"n\": 1,\n \"stream\": false\n })\n}\n\npub async fn validate_api_key(config: &Config, api_key: &str) -> Result<()> {\n let timeout_ms = cap_timeout_ms(config.timeout_ms);\n info!(\"开始验证 DeepSeek API 密钥\");\n let client = Client::builder()\n .timeout(Duration::from_millis(timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = format!(\n \"{}/v1/chat/completions\",\n config.base_url.trim_end_matches('/')\n );\n let request = build_validation_request(\"ping\", &config.deepseek_model);\n\n let response = tokio::time::timeout(\n Duration::from_millis(timeout_ms),\n client\n .post(url)\n .bearer_auth(api_key)\n .json(&request)\n .send(),\n )\n .await\n .context(\"DeepSeek 连接超时\")?\n .context(\"DeepSeek 连接失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n if !status.is_success() {\n let detail: String = raw.chars().take(200).collect();\n warn!(\"DeepSeek 验证失败: {}\", status);\n anyhow::bail!(\"DeepSeek 验证失败: {} {}\", status, detail);\n }\n info!(\"DeepSeek 验证成功\");\n Ok(())\n}\n\npub async fn generate_suggestions(\n config: &Config,\n api_key: Option<String>,\n context_messages: &[String],\n) -> Result<Vec<Suggestion>> {\n let prompt = build_prompt(context_messages);\n let Some(key) = api_key else {\n return Ok(fallback_suggestions(&prompt));\n };\n\n let client = Client::builder()\n .timeout(Duration::from_millis(config.timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = format!(\n \"{}/v1/chat/completions\",\n config.base_url.trim_end_matches('/')\n );\n let request = build_request(\n &prompt,\n config.suggestion_count,\n &config.deepseek_model,\n config.temperature,\n config.top_p,\n );\n\n let response = client\n .post(url)\n .bearer_auth(key)\n .json(&request)\n .send()\n .await\n .context(\"DeepSeek 请求失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n\n if !status.is_success() {\n warn!(\"DeepSeek 返回错误: {}\", status);\n return Ok(fallback_suggestions(&prompt));\n }\n\n match parse_response(&raw) {\n Ok(suggestions) if !suggestions.is_empty() => Ok(suggestions),\n Ok(_) => Ok(fallback_suggestions(&prompt)),\n Err(err) => {\n warn!(\"解析 DeepSeek 响应失败: {}\", err);\n Ok(fallback_suggestions(&prompt))\n }\n }\n}\n\nfn build_prompt(context_messages: &[String]) -> String {\n if context_messages.is_empty() {\n return \"用户未提供上下文,请生成礼貌的确认回复。\".to_string();\n }\n let mut lines = Vec::new();\n for (idx, message) in context_messages.iter().enumerate() {\n lines.push(format!(\"{}: {}\", idx + 1, message));\n }\n format!(\"最近对话:\\n{}\\n请生成 3 条回复建议。\", lines.join(\"\\n\"))\n}\n\nfn parse_response(raw: &str) -> Result<Vec<Suggestion>> {\n let json_value: Value = serde_json::from_str(raw).context(\"响应 JSON 解析失败\")?;\n let content = json_value[\"choices\"][0][\"message\"][\"content\"]\n .as_str()\n .unwrap_or_default()\n .trim();\n if content.is_empty() {\n return Ok(Vec::new());\n }\n\n let cleaned = content\n .trim_start_matches(\"```json\")\n .trim_end_matches(\"```\")\n .trim();\n if let Ok(items) = serde_json::from_str::<Vec<Value>>(cleaned) {\n let mut suggestions = Vec::new();\n for item in items {\n let style = match item[\"style\"].as_str().unwrap_or(\"neutral\") {\n \"formal\" => SuggestionStyle::Formal,\n \"casual\" => SuggestionStyle::Casual,\n _ => SuggestionStyle::Neutral,\n };\n let text = item[\"text\"].as_str().unwrap_or(\"\").trim().to_string();\n if !text.is_empty() {\n suggestions.push(Suggestion {\n id: Uuid::new_v4().to_string(),\n style,\n text,\n });\n }\n }\n return Ok(suggestions);\n }\n\n info!(\"DeepSeek 返回非 JSON 结构,使用降级解析\");\n let suggestions = cleaned\n .lines()\n .filter_map(|line| {\n let text = line.trim_matches(['-', ' ']).trim();\n if text.is_empty() {\n None\n } else {\n Some(Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Neutral,\n text: text.to_string(),\n })\n }\n })\n .collect();\n Ok(suggestions)\n}\n\nfn fallback_suggestions(prompt: &str) -> Vec<Suggestion> {\n let summary = summarize_text(prompt);\n vec![\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Formal,\n text: format!(\"好的,我了解了:{},稍后给您回复。\", summary),\n },\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Neutral,\n text: format!(\"收到,我看看 {} 再回复你。\", summary),\n },\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Casual,\n text: format!(\"好哒~{} 我等下回你。\", summary),\n },\n ]\n}\n\nfn summarize_text(text: &str) -> String {\n let trimmed: String = text.chars().take(20).collect();\n if trimmed.is_empty() {\n \"消息\".to_string()\n } else {\n trimmed\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_request_payload() {\n let req = build_request(\"hi\", 3, \"deepseek-chat\", 0.7, 1.0);\n assert_eq!(req[\"model\"], \"deepseek-chat\");\n assert_eq!(req[\"messages\"].as_array().unwrap().len(), 2);\n }\n\n #[test]\n fn fallback_has_three_styles() {\n let suggestions = fallback_suggestions(\"hi\");\n assert_eq!(suggestions.len(), 3);\n }\n\n #[test]\n fn build_validation_request_is_minimal() {\n let req = build_validation_request(\"ping\", \"deepseek-chat\");\n assert_eq!(req[\"n\"], 1);\n assert_eq!(req[\"temperature\"], 0.0);\n }\n\n #[test]\n fn normalize_timeout_caps() {\n assert_eq!(cap_timeout_ms(12_000), 8_000);\n }\n}\n"}, "bd637b4b147ad1fec37e76f8006f64a7ed20a83c": {"t": 1785757012.8368986, "n": 7025, "h": "a802d79676fe2e64", "c": "use crate::types::{Config, Suggestion, SuggestionStyle};\nuse anyhow::{Context, Result};\nuse reqwest::Client;\nuse serde_json::{json, Value};\nuse std::time::Duration;\nuse tracing::{info, warn};\nuse uuid::Uuid;\n\nconst SYSTEM_PROMPT: &str = \"你是回复建议助手。请根据对话内容生成 3 条回复建议,分别为正式、\\\n中性、轻松风格。返回 JSON 数组,每个元素包含 style(formal|neutral|casual) 与 text。\";\nconst VALIDATION_PROMPT: &str = \"请回复一个简短确认词,用于验证连接。\";\n\npub fn build_request(\n user_input: &str,\n suggestion_count: u32,\n model: &str,\n temperature: f32,\n top_p: f32,\n) -> Value {\n json!({\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": user_input}\n ],\n \"temperature\": temperature,\n \"top_p\": top_p,\n \"n\": suggestion_count,\n \"stream\": false\n })\n}\n\npub fn build_validation_request(user_input: &str, model: &str) -> Value {\n json!({\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": VALIDATION_PROMPT},\n {\"role\": \"user\", \"content\": user_input}\n ],\n \"temperature\": 0.0,\n \"top_p\": 1.0,\n \"n\": 1,\n \"stream\": false\n })\n}\n\npub async fn validate_api_key(config: &Config, api_key: &str) -> Result<()> {\n let client = Client::builder()\n .timeout(Duration::from_millis(config.timeout_ms.min(8_000)))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = format!(\n \"{}/v1/chat/completions\",\n config.base_url.trim_end_matches('/')\n );\n let request = build_validation_request(\"ping\", &config.deepseek_model);\n\n let response = client\n .post(url)\n .bearer_auth(api_key)\n .json(&request)\n .send()\n .await\n .context(\"DeepSeek 连接失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n if !status.is_success() {\n let detail: String = raw.chars().take(200).collect();\n anyhow::bail!(\"DeepSeek 验证失败: {} {}\", status, detail);\n }\n Ok(())\n}\n\npub async fn generate_suggestions(\n config: &Config,\n api_key: Option<String>,\n context_messages: &[String],\n) -> Result<Vec<Suggestion>> {\n let prompt = build_prompt(context_messages);\n let Some(key) = api_key else {\n return Ok(fallback_suggestions(&prompt));\n };\n\n let client = Client::builder()\n .timeout(Duration::from_millis(config.timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = format!(\n \"{}/v1/chat/completions\",\n config.base_url.trim_end_matches('/')\n );\n let request = build_request(\n &prompt,\n config.suggestion_count,\n &config.deepseek_model,\n config.temperature,\n config.top_p,\n );\n\n let response = client\n .post(url)\n .bearer_auth(key)\n .json(&request)\n .send()\n .await\n .context(\"DeepSeek 请求失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n\n if !status.is_success() {\n warn!(\"DeepSeek 返回错误: {}\", status);\n return Ok(fallback_suggestions(&prompt));\n }\n\n match parse_response(&raw) {\n Ok(suggestions) if !suggestions.is_empty() => Ok(suggestions),\n Ok(_) => Ok(fallback_suggestions(&prompt)),\n Err(err) => {\n warn!(\"解析 DeepSeek 响应失败: {}\", err);\n Ok(fallback_suggestions(&prompt))\n }\n }\n}\n\nfn build_prompt(context_messages: &[String]) -> String {\n if context_messages.is_empty() {\n return \"用户未提供上下文,请生成礼貌的确认回复。\".to_string();\n }\n let mut lines = Vec::new();\n for (idx, message) in context_messages.iter().enumerate() {\n lines.push(format!(\"{}: {}\", idx + 1, message));\n }\n format!(\"最近对话:\\n{}\\n请生成 3 条回复建议。\", lines.join(\"\\n\"))\n}\n\nfn parse_response(raw: &str) -> Result<Vec<Suggestion>> {\n let json_value: Value = serde_json::from_str(raw).context(\"响应 JSON 解析失败\")?;\n let content = json_value[\"choices\"][0][\"message\"][\"content\"]\n .as_str()\n .unwrap_or_default()\n .trim();\n if content.is_empty() {\n return Ok(Vec::new());\n }\n\n let cleaned = content\n .trim_start_matches(\"```json\")\n .trim_end_matches(\"```\")\n .trim();\n if let Ok(items) = serde_json::from_str::<Vec<Value>>(cleaned) {\n let mut suggestions = Vec::new();\n for item in items {\n let style = match item[\"style\"].as_str().unwrap_or(\"neutral\") {\n \"formal\" => SuggestionStyle::Formal,\n \"casual\" => SuggestionStyle::Casual,\n _ => SuggestionStyle::Neutral,\n };\n let text = item[\"text\"].as_str().unwrap_or(\"\").trim().to_string();\n if !text.is_empty() {\n suggestions.push(Suggestion {\n id: Uuid::new_v4().to_string(),\n style,\n text,\n });\n }\n }\n return Ok(suggestions);\n }\n\n info!(\"DeepSeek 返回非 JSON 结构,使用降级解析\");\n let suggestions = cleaned\n .lines()\n .filter_map(|line| {\n let text = line.trim_matches(['-', ' ']).trim();\n if text.is_empty() {\n None\n } else {\n Some(Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Neutral,\n text: text.to_string(),\n })\n }\n })\n .collect();\n Ok(suggestions)\n}\n\nfn fallback_suggestions(prompt: &str) -> Vec<Suggestion> {\n let summary = summarize_text(prompt);\n vec![\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Formal,\n text: format!(\"好的,我了解了:{},稍后给您回复。\", summary),\n },\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Neutral,\n text: format!(\"收到,我看看 {} 再回复你。\", summary),\n },\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Casual,\n text: format!(\"好哒~{} 我等下回你。\", summary),\n },\n ]\n}\n\nfn summarize_text(text: &str) -> String {\n let trimmed: String = text.chars().take(20).collect();\n if trimmed.is_empty() {\n \"消息\".to_string()\n } else {\n trimmed\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_request_payload() {\n let req = build_request(\"hi\", 3, \"deepseek-chat\", 0.7, 1.0);\n assert_eq!(req[\"model\"], \"deepseek-chat\");\n assert_eq!(req[\"messages\"].as_array().unwrap().len(), 2);\n }\n\n #[test]\n fn fallback_has_three_styles() {\n let suggestions = fallback_suggestions(\"hi\");\n assert_eq!(suggestions.len(), 3);\n }\n\n #[test]\n fn build_validation_request_is_minimal() {\n let req = build_validation_request(\"ping\", \"deepseek-chat\");\n assert_eq!(req[\"n\"], 1);\n assert_eq!(req[\"temperature\"], 0.0);\n }\n}\n"}, "49b771fb17bdfd82cbd11d5fe95044d8144d5094": {"t": 1785757014.1222465, "n": 5535, "h": "ffd8b2130a994e87", "c": "use crate::types::{Config, Suggestion, SuggestionStyle};\nuse anyhow::{Context, Result};\nuse reqwest::Client;\nuse serde_json::{json, Value};\nuse std::time::Duration;\nuse tracing::{info, warn};\nuse uuid::Uuid;\n\nconst SYSTEM_PROMPT: &str = \"你是回复建议助手。请根据对话内容生成 3 条回复建议,分别为正式、\\\n中性、轻松风格。返回 JSON 数组,每个元素包含 style(formal|neutral|casual) 与 text。\";\n\npub fn build_request(\n user_input: &str,\n suggestion_count: u32,\n model: &str,\n temperature: f32,\n top_p: f32,\n) -> Value {\n json!({\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": user_input}\n ],\n \"temperature\": temperature,\n \"top_p\": top_p,\n \"n\": suggestion_count,\n \"stream\": false\n })\n}\n\npub async fn generate_suggestions(\n config: &Config,\n api_key: Option<String>,\n context_messages: &[String],\n) -> Result<Vec<Suggestion>> {\n let prompt = build_prompt(context_messages);\n let Some(key) = api_key else {\n return Ok(fallback_suggestions(&prompt));\n };\n\n let client = Client::builder()\n .timeout(Duration::from_millis(config.timeout_ms))\n .build()\n .context(\"创建 HTTP 客户端失败\")?;\n let url = format!(\n \"{}/v1/chat/completions\",\n config.base_url.trim_end_matches('/')\n );\n let request = build_request(\n &prompt,\n config.suggestion_count,\n &config.deepseek_model,\n config.temperature,\n config.top_p,\n );\n\n let response = client\n .post(url)\n .bearer_auth(key)\n .json(&request)\n .send()\n .await\n .context(\"DeepSeek 请求失败\")?;\n let status = response.status();\n let raw = response.text().await.context(\"读取 DeepSeek 响应失败\")?;\n\n if !status.is_success() {\n warn!(\"DeepSeek 返回错误: {}\", status);\n return Ok(fallback_suggestions(&prompt));\n }\n\n match parse_response(&raw) {\n Ok(suggestions) if !suggestions.is_empty() => Ok(suggestions),\n Ok(_) => Ok(fallback_suggestions(&prompt)),\n Err(err) => {\n warn!(\"解析 DeepSeek 响应失败: {}\", err);\n Ok(fallback_suggestions(&prompt))\n }\n }\n}\n\nfn build_prompt(context_messages: &[String]) -> String {\n if context_messages.is_empty() {\n return \"用户未提供上下文,请生成礼貌的确认回复。\".to_string();\n }\n let mut lines = Vec::new();\n for (idx, message) in context_messages.iter().enumerate() {\n lines.push(format!(\"{}: {}\", idx + 1, message));\n }\n format!(\"最近对话:\\n{}\\n请生成 3 条回复建议。\", lines.join(\"\\n\"))\n}\n\nfn parse_response(raw: &str) -> Result<Vec<Suggestion>> {\n let json_value: Value = serde_json::from_str(raw).context(\"响应 JSON 解析失败\")?;\n let content = json_value[\"choices\"][0][\"message\"][\"content\"]\n .as_str()\n .unwrap_or_default()\n .trim();\n if content.is_empty() {\n return Ok(Vec::new());\n }\n\n let cleaned = content\n .trim_start_matches(\"```json\")\n .trim_end_matches(\"```\")\n .trim();\n if let Ok(items) = serde_json::from_str::<Vec<Value>>(cleaned) {\n let mut suggestions = Vec::new();\n for item in items {\n let style = match item[\"style\"].as_str().unwrap_or(\"neutral\") {\n \"formal\" => SuggestionStyle::Formal,\n \"casual\" => SuggestionStyle::Casual,\n _ => SuggestionStyle::Neutral,\n };\n let text = item[\"text\"].as_str().unwrap_or(\"\").trim().to_string();\n if !text.is_empty() {\n suggestions.push(Suggestion {\n id: Uuid::new_v4().to_string(),\n style,\n text,\n });\n }\n }\n return Ok(suggestions);\n }\n\n info!(\"DeepSeek 返回非 JSON 结构,使用降级解析\");\n let suggestions = cleaned\n .lines()\n .filter_map(|line| {\n let text = line.trim_matches(['-', ' ']).trim();\n if text.is_empty() {\n None\n } else {\n Some(Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Neutral,\n text: text.to_string(),\n })\n }\n })\n .collect();\n Ok(suggestions)\n}\n\nfn fallback_suggestions(prompt: &str) -> Vec<Suggestion> {\n let summary = summarize_text(prompt);\n vec![\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Formal,\n text: format!(\"好的,我了解了:{},稍后给您回复。\", summary),\n },\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Neutral,\n text: format!(\"收到,我看看 {} 再回复你。\", summary),\n },\n Suggestion {\n id: Uuid::new_v4().to_string(),\n style: SuggestionStyle::Casual,\n text: format!(\"好哒~{} 我等下回你。\", summary),\n },\n ]\n}\n\nfn summarize_text(text: &str) -> String {\n let trimmed: String = text.chars().take(20).collect();\n if trimmed.is_empty() {\n \"消息\".to_string()\n } else {\n trimmed\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_request_payload() {\n let req = build_request(\"hi\", 3, \"deepseek-chat\", 0.7, 1.0);\n assert_eq!(req[\"model\"], \"deepseek-chat\");\n assert_eq!(req[\"messages\"].as_array().unwrap().len(), 2);\n }\n\n #[test]\n fn fallback_has_three_styles() {\n let suggestions = fallback_suggestions(\"hi\");\n assert_eq!(suggestions.len(), 3);\n }\n}\n"}, "bf06f6564f88b28e20cca543444126f1a21843ea": {"t": 1785757016.5887058, "n": 755, "h": "221108b8e91c4b08", "c": "use serde_json::{json, Value};\n\npub fn build_request(\n user_input: &str,\n suggestion_count: u32,\n model: &str,\n temperature: f32,\n top_p: f32,\n) -> Value {\n json!({\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": \"你是回复建议助手\"},\n {\"role\": \"user\", \"content\": user_input}\n ],\n \"temperature\": temperature,\n \"top_p\": top_p,\n \"n\": suggestion_count,\n \"stream\": false\n })\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_request_payload() {\n let req = build_request(\"hi\", 3, \"deepseek-chat\", 0.7, 1.0);\n assert_eq!(req[\"model\"], \"deepseek-chat\");\n assert_eq!(req[\"messages\"].as_array().unwrap().len(), 2);\n }\n}\n"}}}} |