//! IPC 命令:CLI 全链路(detectCli / previewAction / runAction / readConfig / //! writeConfig / authStatus / authorize / diagnose,架构 §2 IPC 契约) //! //! 编排全部走 `agentdock-core::Engine`。runAction 通过 Tauri 事件流式回传 //! stdout/stderr;敏感输出经 `agentdock-secrets::redact` 脱敏后才进事件。 use std::collections::BTreeMap; use agentdock_adapter::{AdapterAction, DryRunPlan}; use agentdock_core::{ActionEvent, ActionOpts, AuthFlowEvent, AuthStatus, ConfigFormState, ConfigVerifyResult, DetectResult, Engine, WriteResult}; use agentdock_diag::DiagnosticReport; use serde_json::json; use tauri::Emitter; /// 把 action 字符串映射为 AdapterAction(snake_case,与 IPC 契约一致)。 fn parse_action(action: &str) -> Result { match action { "install" => Ok(AdapterAction::Install), "update" => Ok(AdapterAction::Update), "uninstall" => Ok(AdapterAction::Uninstall), "write_config" => Ok(AdapterAction::WriteConfig), "authorize" => Ok(AdapterAction::Authorize), "repair" => Ok(AdapterAction::Repair), other => Err(format!("未知动作: {other}")), } } /// 读取单个适配器完整定义(CLI 详情页展示文档/渠道/许可用)。 #[tauri::command(rename = "getAdapter")] pub fn get_adapter(id: String, state: tauri::State<'_, Engine>) -> Result { state.adapter(&id).map_err(|e| e.to_string()) } /// 检测单个 CLI(detectCli)。 #[tauri::command(rename = "detectCli")] pub fn detect_cli(id: String, state: tauri::State<'_, Engine>) -> Result { state.detect(&id).map_err(|e| e.to_string()) } /// 批量检测全部 CLI(detectCliAll),供总览/目录/我的 CLI 接真机状态。 #[tauri::command(rename = "detectCliAll")] pub fn detect_cli_all(state: tauri::State<'_, Engine>) -> Result, String> { state.detect_all().map_err(|e| e.to_string()) } /// 干燥运行(previewAction):返回将执行的命令与影响面,不真正执行。 #[tauri::command(rename = "previewAction")] pub fn preview_action( id: String, action: String, channel: Option, state: tauri::State<'_, Engine>, ) -> Result { let action = parse_action(&action)?; let mut plan = state.preview(&id, action).map_err(|e| e.to_string())?; // 渠道覆盖时调整计划里的命令为所选渠道(供确认弹窗展示) if let Some(ch) = channel { if let Some(adapter) = state.adapter(&id).ok() { if let Some(install) = adapter.install.as_ref() { if let Some(target) = install.channels.iter().find(|c| c.id == ch) { if !target.command.is_empty() { plan.commands = vec![target.command.clone()]; plan.elevate = matches!(target.elevate.as_deref(), Some("if_needed") | Some("required")); plan.elevate_reason_zh = target.elevate_reason_zh.clone(); } } } } } Ok(plan) } /// 流式执行动作(runAction):事件经 `cli-action-event` 回传。 #[tauri::command(rename = "runAction")] pub fn run_action( app: tauri::AppHandle, id: String, action: String, channel: Option, state: tauri::State<'_, Engine>, ) -> Result<(), String> { let action = parse_action(&action)?; let engine = state.inner().clone(); let opts = ActionOpts { channel }; std::thread::spawn(move || { let cli_id = id.clone(); let result = engine.run(&id, action, &opts, |ev| { let _ = app.emit("cli-action-event", json!({ "cli_id": cli_id, "event": ev })); }); if let Err(e) = result { let _ = app.emit( "cli-action-event", json!({ "cli_id": cli_id, "event": ActionEvent::error(e.to_string()) }), ); } }); Ok(()) } /// 读取配置表单状态(readConfig)。 #[tauri::command(rename = "readConfig")] pub fn read_config(id: String, state: tauri::State<'_, Engine>) -> Result { state.read_config(&id).map_err(|e| e.to_string()) } /// 写配置(writeConfig):patch 为 { field_id: value }。 #[tauri::command(rename = "writeConfig")] pub fn write_config( id: String, patch: BTreeMap, state: tauri::State<'_, Engine>, ) -> Result { state.write_config(&id, &patch).map_err(|e| e.to_string()) } /// 配置写入后的「生效检查」(verifyConfig):重读配置文件 + CLI 版本探测。 #[tauri::command(rename = "verifyConfig")] pub fn verify_config(id: String, state: tauri::State<'_, Engine>) -> Result { state.verify_config(&id).map_err(|e| e.to_string()) } /// 授权状态(authStatus)。 #[tauri::command(rename = "authStatus")] pub fn auth_status(id: String, state: tauri::State<'_, Engine>) -> Result { state.auth_status(&id).map_err(|e| e.to_string()) } /// 软件内授权(authorize):按官方授权方式运行登录命令,事件经 `cli-auth-event` 流式回传。 #[tauri::command(rename = "authorize")] pub fn authorize( app: tauri::AppHandle, id: String, mode: String, state: tauri::State<'_, Engine>, ) -> Result<(), String> { let engine = state.inner().clone(); std::thread::spawn(move || { let cli_id = id.clone(); let m = mode.clone(); let result = engine.authorize_stream(&id, &mode, |ev| { let _ = app.emit("cli-auth-event", &ev); }); if let Err(e) = result { let _ = app.emit("cli-auth-event", AuthFlowEvent::error(&cli_id, &m, e.to_string())); } }); Ok(()) } /// 取消正在进行的授权流程。 #[tauri::command(rename = "cancelAuthorize")] pub fn cancel_authorize(id: String, mode: String, state: tauri::State<'_, Engine>) { state.cancel_authorize(&id, &mode); } /// 诊断(diagnose):内部实时取本机环境快照。 #[tauri::command(rename = "diagnose")] pub fn diagnose(id: String, state: tauri::State<'_, Engine>) -> Result { let env = agentdock_platform::detect::detect_env(); state.diagnose(&id, &env).map_err(|e| e.to_string()) } /// 批量诊断全部已装工具(总览「立即诊断」入口用),内部实时取本机环境快照。 #[tauri::command(rename = "diagnoseAll")] pub fn diagnose_all(state: tauri::State<'_, Engine>) -> Result, String> { let env = agentdock_platform::detect::detect_env(); state.diagnose_all(&env).map_err(|e| e.to_string()) }