mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-28 03:40:09 +08:00
- #32: parseCookies decodeURIComponent crash with malformed cookies (Authelia) - #31: Gateway restart no longer overwrites user CORS allowedOrigins (merge instead) - #25: Windows terminal flashing - add CREATE_NO_WINDOW to skills.rs + assistant.rs - #33: Model test tolerates non-auth HTTP errors (Ali Coding Plan compatibility) - #29: Auto-detect ws/wss protocol for reverse proxy + protocol-aware Docker URLs - #23: Chat session sidebar stays open when switching sessions
This commit is contained in:
@@ -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<String, String> {
|
||||
/// 列出运行中的进程(按名称过滤)
|
||||
#[tauri::command]
|
||||
pub async fn assistant_list_processes(filter: Option<String>) -> Result<String, 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",
|
||||
"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<String, String> {
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
||||
@@ -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::<serde_json::Value>(&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)
|
||||
}
|
||||
|
||||
@@ -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<String> = 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<String> = 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Value, String> {
|
||||
@@ -104,11 +108,11 @@ pub async fn skills_install_dep(kind: String, spec: Value) -> Result<Value, Stri
|
||||
other => 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<Value, String> {
|
||||
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<Value, String> {
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
Reference in New Issue
Block a user