diff --git a/.gitignore b/.gitignore index 3fcd949..7176f8e 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ docs/promo-video.mp4 # Rust 开发工具 src-tauri/.cargo/ +.codex/ \ No newline at end of file diff --git a/scripts/dev-api.js b/scripts/dev-api.js index f4e3aca..e676ed9 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -153,7 +153,7 @@ function parseCookies(req) { const obj = {} ;(req.headers.cookie || '').split(';').forEach(pair => { const [k, ...v] = pair.trim().split('=') - if (k) obj[k] = decodeURIComponent(v.join('=')) + if (k) try { obj[k] = decodeURIComponent(v.join('=')) } catch (_) { obj[k] = v.join('=') } }) return obj } diff --git a/src-tauri/src/commands/assistant.rs b/src-tauri/src/commands/assistant.rs index 0776654..f5194e6 100644 --- a/src-tauri/src/commands/assistant.rs +++ b/src-tauri/src/commands/assistant.rs @@ -2,6 +2,9 @@ use base64::{engine::general_purpose, Engine as _}; /// AI 助手工具命令 /// 提供终端执行、文件读写、目录列表等能力 /// 仅在用户主动开启工具后由 AI 调用 +#[cfg(target_os = "windows")] +#[allow(unused_imports)] +use std::os::windows::process::CommandExt; use std::path::PathBuf; /// 审计日志:记录 AI 助手的敏感操作(exec / read / write) @@ -267,18 +270,23 @@ pub async fn assistant_system_info() -> Result { /// 列出运行中的进程(按名称过滤) #[tauri::command] pub async fn assistant_list_processes(filter: Option) -> Result { - let output = if cfg!(target_os = "windows") { - tokio::process::Command::new("powershell") + let output; + #[cfg(target_os = "windows")] + { + output = tokio::process::Command::new("powershell") .args(["-NoProfile", "-Command", "Get-Process | Select-Object Id, ProcessName, CPU, WorkingSet64 | Sort-Object ProcessName | Format-Table -AutoSize | Out-String -Width 200"]) + .creation_flags(0x08000000) .output() - .await - } else { - tokio::process::Command::new("ps") + .await; + } + #[cfg(not(target_os = "windows"))] + { + output = tokio::process::Command::new("ps") .args(["aux", "--sort=-%mem"]) .output() - .await - }; + .await; + } let output = output.map_err(|e| format!("获取进程列表失败: {e}"))?; let stdout = String::from_utf8_lossy(&output.stdout).to_string(); @@ -331,18 +339,23 @@ pub async fn assistant_check_port(port: u16) -> Result { } async fn get_port_process(port: u16) -> String { - let output = if cfg!(target_os = "windows") { - tokio::process::Command::new("powershell") + let output; + #[cfg(target_os = "windows")] + { + output = tokio::process::Command::new("powershell") .args(["-NoProfile", "-Command", &format!("Get-NetTCPConnection -LocalPort {} -ErrorAction SilentlyContinue | Select-Object OwningProcess | ForEach-Object {{ (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName }}", port)]) + .creation_flags(0x08000000) .output() - .await - } else { - tokio::process::Command::new("lsof") + .await; + } + #[cfg(not(target_os = "windows"))] + { + output = tokio::process::Command::new("lsof") .args(["-i", &format!(":{}", port), "-t"]) .output() - .await - }; + .await; + } match output { Ok(o) => { diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index d2b8c81..21202d7 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -1309,25 +1309,37 @@ pub async fn test_model( .map(String::from) }) .unwrap_or_else(|| format!("HTTP {status}")); - return Err(msg); + // 401/403 是认证错误,一定要报错 + if status.as_u16() == 401 || status.as_u16() == 403 { + return Err(msg); + } + // 其他错误(400/422 等):服务器可达、认证通过,仅模型对简单测试不兼容 + // 返回成功但带提示,避免误导用户认为模型不可用 + return Ok(format!("⚠ 连接正常(API 返回 {status},部分模型对简单测试不兼容,不影响实际使用)")); } - // 提取回复内容(兼容 reasoning 模型的 reasoning_content 字段) + // 提取回复内容(兼容多种响应格式) let reply = serde_json::from_str::(&text) .ok() .and_then(|v| { - let msg = v.get("choices")?.get(0)?.get("message")?; - // 优先取 content,为空则取 reasoning_content - let content = msg.get("content").and_then(|c| c.as_str()).unwrap_or(""); - if !content.is_empty() { - return Some(content.to_string()); + // 标准 OpenAI 格式: choices[0].message.content + if let Some(msg) = v.get("choices").and_then(|c| c.get(0)).and_then(|c| c.get("message")) { + let content = msg.get("content").and_then(|c| c.as_str()).unwrap_or(""); + if !content.is_empty() { + return Some(content.to_string()); + } + // reasoning 模型 + if let Some(rc) = msg.get("reasoning_content").and_then(|c| c.as_str()).filter(|s| !s.is_empty()) { + return Some(format!("[reasoning] {rc}")); + } } - msg.get("reasoning_content") - .and_then(|c| c.as_str()) - .filter(|s| !s.is_empty()) - .map(|s| format!("[reasoning] {s}")) + // DashScope 格式: output.text + if let Some(t) = v.get("output").and_then(|o| o.get("text")).and_then(|t| t.as_str()).filter(|s| !s.is_empty()) { + return Some(t.to_string()); + } + None }) - .unwrap_or_else(|| "(无回复内容)".into()); + .unwrap_or_else(|| "(模型已响应)".into()); Ok(reply) } diff --git a/src-tauri/src/commands/pairing.rs b/src-tauri/src/commands/pairing.rs index 5a8f779..fe36b93 100644 --- a/src-tauri/src/commands/pairing.rs +++ b/src-tauri/src/commands/pairing.rs @@ -115,14 +115,14 @@ fn patch_gateway_origins() { return; }; - // 仅允许 Tauri 应用 + 本地开发服务器的 origin - let origins = serde_json::json!([ - "tauri://localhost", - "https://tauri.localhost", - "http://tauri.localhost", - "http://localhost:1420", - "http://127.0.0.1:1420" - ]); + // Tauri 应用 + 本地开发服务器必须存在的 origin + let required: Vec = vec![ + "tauri://localhost".into(), + "https://tauri.localhost".into(), + "http://tauri.localhost".into(), + "http://localhost:1420".into(), + "http://127.0.0.1:1420".into(), + ]; if let Some(obj) = config.as_object_mut() { let gateway = obj @@ -133,7 +133,26 @@ fn patch_gateway_origins() { .entry("controlUi") .or_insert_with(|| serde_json::json!({})); if let Some(cui) = control_ui.as_object_mut() { - cui.insert("allowedOrigins".to_string(), origins); + // 合并:保留用户已有的 origin,追加缺失的 Tauri origin + let existing: Vec = cui + .get("allowedOrigins") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|s| s.as_str().map(String::from)) + .collect() + }) + .unwrap_or_default(); + let mut merged = existing; + for r in &required { + if !merged.iter().any(|e| e == r) { + merged.push(r.clone()); + } + } + cui.insert( + "allowedOrigins".to_string(), + serde_json::json!(merged), + ); } } } diff --git a/src-tauri/src/commands/skills.rs b/src-tauri/src/commands/skills.rs index e89cb9a..56f5c3e 100644 --- a/src-tauri/src/commands/skills.rs +++ b/src-tauri/src/commands/skills.rs @@ -1,6 +1,10 @@ use crate::utils::openclaw_command_async; use serde_json::Value; +#[cfg(target_os = "windows")] +#[allow(unused_imports)] +use std::os::windows::process::CommandExt; + /// 列出所有 Skills 及其状态(openclaw skills list --json) #[tauri::command] pub async fn skills_list() -> Result { @@ -104,11 +108,11 @@ pub async fn skills_install_dep(kind: String, spec: Value) -> Result return Err(format!("不支持的安装类型: {other}")), }; - let output = tokio::process::Command::new(&program) - .args(&args) - .env("PATH", &path_env) - .output() - .await + let mut cmd = tokio::process::Command::new(&program); + cmd.args(&args).env("PATH", &path_env); + #[cfg(target_os = "windows")] + cmd.creation_flags(0x08000000); + let output = cmd.output().await .map_err(|e| format!("执行 {program} 失败: {e}"))?; let stdout = String::from_utf8_lossy(&output.stdout).to_string(); @@ -140,12 +144,13 @@ pub async fn skills_clawhub_install(slug: String) -> Result { std::fs::create_dir_all(&skills_dir).map_err(|e| format!("创建 skills 目录失败: {e}"))?; } - let output = tokio::process::Command::new("npx") - .args(["-y", "clawhub", "install", &slug]) + let mut cmd = tokio::process::Command::new("npx"); + cmd.args(["-y", "clawhub", "install", &slug]) .env("PATH", &path_env) - .current_dir(&home) - .output() - .await + .current_dir(&home); + #[cfg(target_os = "windows")] + cmd.creation_flags(0x08000000); + let output = cmd.output().await .map_err(|e| format!("执行 clawhub 失败: {e}"))?; let stdout = String::from_utf8_lossy(&output.stdout).to_string(); @@ -171,11 +176,12 @@ pub async fn skills_clawhub_search(query: String) -> Result { } let path_env = super::enhanced_path(); - let output = tokio::process::Command::new("npx") - .args(["-y", "clawhub", "search", &q]) - .env("PATH", &path_env) - .output() - .await + let mut cmd = tokio::process::Command::new("npx"); + cmd.args(["-y", "clawhub", "search", &q]) + .env("PATH", &path_env); + #[cfg(target_os = "windows")] + cmd.creation_flags(0x08000000); + let output = cmd.output().await .map_err(|e| format!("执行 clawhub 失败: {e}"))?; if !output.status.success() { diff --git a/src/lib/ws-client.js b/src/lib/ws-client.js index 18d1a3d..c5f7ad2 100644 --- a/src/lib/ws-client.js +++ b/src/lib/ws-client.js @@ -66,11 +66,13 @@ export class WsClient { return () => { this._readyCallbacks = this._readyCallbacks.filter(cb => cb !== fn) } } - connect(host, token) { + connect(host, token, opts = {}) { this._intentionalClose = false this._autoPairAttempts = 0 this._token = token || '' - this._url = `ws://${host}/ws?token=${encodeURIComponent(this._token)}` + // 自动检测协议:如果页面通过 HTTPS 加载(反代场景),使用 wss:// + const proto = opts.secure ?? (typeof location !== 'undefined' && location.protocol === 'https:') ? 'wss' : 'ws' + this._url = `${proto}://${host}/ws?token=${encodeURIComponent(this._token)}` this._doConnect() } diff --git a/src/pages/chat.js b/src/pages/chat.js index e410275..d1dc526 100644 --- a/src/pages/chat.js +++ b/src/pages/chat.js @@ -484,7 +484,6 @@ function switchSession(newKey) { clearMessages() loadHistory() refreshSessionList() - _page?.querySelector('#chat-sidebar')?.classList.remove('open') } async function showNewSessionDialog() { diff --git a/src/pages/docker.js b/src/pages/docker.js index 861e44f..32b7d35 100644 --- a/src/pages/docker.js +++ b/src/pages/docker.js @@ -332,7 +332,7 @@ function _renderUnitCard(c, showAdopt) { ${isRunning && (ports.panel || ports.gateway) ? ` ` : ''} @@ -1474,7 +1474,7 @@ async function showDeployDialog(page, nodeId) { // 成功页面 const host = location.hostname || 'localhost' - const panelUrl = `http://${host}:${panelPort}` + const panelUrl = `${location.protocol}//${host}:${panelPort}` const selectedRole = overlay.querySelector('#dd-role')?.value || 'general' const roleInfo = MILITARY.roles[selectedRole] || MILITARY.roles.general @@ -1488,7 +1488,7 @@ async function showDeployDialog(page, nodeId) {
- Panel: ${panelUrl} · Gateway: ws://${host}:${gatewayPort} + Panel: ${panelUrl} · Gateway: ${location.protocol === 'https:' ? 'wss' : 'ws'}://${host}:${gatewayPort}
` @@ -1543,18 +1543,18 @@ async function showInspectDialog(page, nodeId, containerId) {
指挥通道