diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 928d47c..2ebc330 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -1815,10 +1815,11 @@ const handlers = { return { valid: true, warnings: ['该平台暂不支持在线校验'] } }, - install_qqbot_plugin() { + install_qqbot_plugin({ version } = {}) { const bin = findOpenclawBin() || 'openclaw' + const spec = version ? `@tencent-connect/openclaw-qqbot@${version}` : '@tencent-connect/openclaw-qqbot@latest' try { - execSync(`${bin} plugins install @tencent-connect/openclaw-qqbot@latest`, { timeout: 600000, cwd: homedir() }) + execSync(`${bin} plugins install ${spec}`, { timeout: 600000, cwd: homedir() }) return '安装成功' } catch (e) { throw new Error('QQBot 插件安装失败: ' + (e.message || e)) @@ -1851,11 +1852,12 @@ const handlers = { } }, - install_channel_plugin({ packageName, pluginId }) { + install_channel_plugin({ packageName, pluginId, version }) { if (!packageName || !pluginId) throw new Error('packageName 和 pluginId 不能为空') const bin = findOpenclawBin() || 'openclaw' + const spec = version ? `${packageName.trim()}@${version}` : packageName.trim() try { - execSync(`${bin} plugins install ${packageName.trim()}`, { timeout: 120000, cwd: homedir() }) + execSync(`${bin} plugins install ${spec}`, { timeout: 120000, cwd: homedir() }) return '安装成功' } catch (e) { throw new Error(`插件 ${pluginId} 安装失败: ` + (e.message || e)) diff --git a/src-tauri/src/commands/messaging.rs b/src-tauri/src/commands/messaging.rs index b78e1a5..11dd6c7 100644 --- a/src-tauri/src/commands/messaging.rs +++ b/src-tauri/src/commands/messaging.rs @@ -1104,6 +1104,7 @@ pub async fn run_channel_action( app: tauri::AppHandle, platform: String, action: String, + version: Option, ) -> Result { use std::io::{BufRead, BufReader}; use std::process::Stdio; @@ -1118,10 +1119,17 @@ pub async fn run_channel_action( // weixin install 走 npx 而非 openclaw CLI if platform == "weixin" && action == "install" { - let _ = app.emit("channel-action-log", json!({ - "platform": &platform, "action": &action, "kind": "info", - "message": "开始安装微信插件: npx -y @tencent-weixin/openclaw-weixin-cli@latest install", - })); + let weixin_spec = match &version { + Some(v) if !v.is_empty() => format!("@tencent-weixin/openclaw-weixin-cli@{}", v), + _ => "@tencent-weixin/openclaw-weixin-cli@latest".to_string(), + }; + let _ = app.emit( + "channel-action-log", + json!({ + "platform": &platform, "action": &action, "kind": "info", + "message": format!("开始安装微信插件: npx -y {} install", weixin_spec), + }), + ); let _ = app.emit( "channel-action-progress", json!({ "platform": &platform, "action": &action, "progress": 5 }), @@ -1133,24 +1141,14 @@ pub async fn run_channel_action( use std::os::windows::process::CommandExt; const CREATE_NO_WINDOW: u32 = 0x08000000; let mut c = std::process::Command::new("cmd"); - c.args([ - "/c", - "npx", - "-y", - "@tencent-weixin/openclaw-weixin-cli@latest", - "install", - ]); + c.args(["/c", "npx", "-y", &weixin_spec, "install"]); c.creation_flags(CREATE_NO_WINDOW); c }; #[cfg(not(target_os = "windows"))] let mut cmd = { let mut c = std::process::Command::new("npx"); - c.args([ - "-y", - "@tencent-weixin/openclaw-weixin-cli@latest", - "install", - ]); + c.args(["-y", &weixin_spec, "install"]); c }; cmd.env("PATH", &path_env); @@ -1522,7 +1520,7 @@ pub async fn diagnose_channel( pub async fn repair_qqbot_channel_setup(app: tauri::AppHandle) -> Result { let (installed, _loc) = qqbot_extension_installed(); if !installed { - install_qqbot_plugin(app.clone()).await?; + install_qqbot_plugin(app.clone(), None).await?; return Ok(json!({ "ok": true, "action": "installed", @@ -2475,6 +2473,7 @@ pub async fn install_channel_plugin( app: tauri::AppHandle, package_name: String, plugin_id: String, + version: Option, ) -> Result { use std::io::{BufRead, BufReader}; use std::process::Stdio; @@ -2485,6 +2484,11 @@ pub async fn install_channel_plugin( if package_name.is_empty() || plugin_id.is_empty() { return Err("package_name 和 plugin_id 不能为空".into()); } + // 拼接版本号:package@version(兼容用户 OpenClaw 版本的插件) + let install_spec = match &version { + Some(v) if !v.is_empty() => format!("{}@{}", package_name, v), + _ => package_name.to_string(), + }; let plugin_dir = generic_plugin_dir(plugin_id); let plugin_backup = generic_plugin_backup_dir(plugin_id); let config_path = super::openclaw_dir().join("openclaw.json"); @@ -2518,8 +2522,9 @@ pub async fn install_channel_plugin( fs::copy(&config_path, &config_backup).map_err(|e| format!("备份配置失败: {e}"))?; } + let _ = app.emit("plugin-log", format!("安装规格: {}", install_spec)); let spawn_result = crate::utils::openclaw_command() - .args(["plugins", "install", package_name]) + .args(["plugins", "install", &install_spec]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn(); @@ -2637,11 +2642,19 @@ pub async fn install_channel_plugin( } #[tauri::command] -pub async fn install_qqbot_plugin(app: tauri::AppHandle) -> Result { +pub async fn install_qqbot_plugin( + app: tauri::AppHandle, + version: Option, +) -> Result { use std::io::{BufRead, BufReader}; use std::process::Stdio; use tauri::Emitter; + let install_spec = match &version { + Some(v) if !v.is_empty() => format!("{}@{}", TENCENT_OPENCLAW_QQBOT_PACKAGE, v), + _ => TENCENT_OPENCLAW_QQBOT_PACKAGE.to_string(), + }; + let plugin_dir = generic_plugin_dir(OPENCLAW_QQBOT_EXTENSION_FOLDER); let plugin_backup = generic_plugin_backup_dir(OPENCLAW_QQBOT_EXTENSION_FOLDER); let config_path = super::openclaw_dir().join("openclaw.json"); @@ -2678,8 +2691,9 @@ pub async fn install_qqbot_plugin(app: tauri::AppHandle) -> Result cachedInvoke('list_configured_platforms', {}, 5000), getChannelPluginStatus: (pluginId) => invoke('get_channel_plugin_status', { pluginId }), - installQqbotPlugin: () => invoke('install_qqbot_plugin'), - installChannelPlugin: (packageName, pluginId) => invoke('install_channel_plugin', { packageName, pluginId }), - runChannelAction: (platform, action) => invoke('run_channel_action', { platform, action }), + installQqbotPlugin: (version = null) => invoke('install_qqbot_plugin', { version }), + installChannelPlugin: (packageName, pluginId, version = null) => invoke('install_channel_plugin', { packageName, pluginId, version }), + runChannelAction: (platform, action, version = null) => invoke('run_channel_action', { platform, action, version }), checkWeixinPluginStatus: () => invoke('check_weixin_plugin_status'), // Agent 渠道绑定管理 diff --git a/src/pages/channels.js b/src/pages/channels.js index 407267d..08d9f4c 100644 --- a/src/pages/channels.js +++ b/src/pages/channels.js @@ -1441,7 +1441,15 @@ async function openConfigDialog(pid, page, state, accountId) { if (progressText) progressText.textContent = `${pct}%` }) - const output = await api.runChannelAction(pid, actionId) + // 自动 pin 插件版本到用户的 OpenClaw 版本(仅 install 动作) + let actionVersion = null + if (actionId === 'install') { + try { + const vInfo = await api.getVersionInfo() + if (vInfo?.current) actionVersion = vInfo.current.split('-')[0] + } catch {} + } + const output = await api.runChannelAction(pid, actionId, actionVersion) _flushQr() // 命令结束后刷新残留 QR 缓冲 if (progressBar) progressBar.style.width = '100%' if (progressText) progressText.textContent = '100%' @@ -1772,7 +1780,15 @@ async function openConfigDialog(pid, page, state, accountId) { } }) - const output = await api.runChannelAction(pid, actionId) + // 自动 pin 插件版本(仅 install 动作) + let actionVer2 = null + if (actionId === 'install') { + try { + const vInfo = await api.getVersionInfo() + if (vInfo?.current) actionVer2 = vInfo.current.split('-')[0] + } catch {} + } + const output = await api.runChannelAction(pid, actionId, actionVer2) toast(t('channels.actionDone'), 'success') if (logBox && output && !String(output).includes(logBox.textContent)) { logBox.textContent += (logBox.textContent ? '\n' : '') + String(output) @@ -1930,11 +1946,17 @@ async function openConfigDialog(pid, page, state, accountId) { } catch {} try { + // 自动 pin 插件版本到用户的 OpenClaw 版本,避免 minHostVersion 不兼容 + let pluginVersion = null + try { + const vInfo = await api.getVersionInfo() + if (vInfo?.current) pluginVersion = vInfo.current.split('-')[0] + } catch {} // QQ 必须用专用安装命令:官方包目录为 openclaw-qqbot,与 install_channel_plugin(…, "qqbot") 的备份路径不一致 if (pid === 'qqbot') { - await api.installQqbotPlugin() + await api.installQqbotPlugin(pluginVersion) } else { - await api.installChannelPlugin(pluginPackage, pluginId) + await api.installChannelPlugin(pluginPackage, pluginId, pluginVersion) } } catch (e) { toast(t('channels.pluginInstallFailed') + ': ' + e, 'error')