mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-10 17:42:49 +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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -46,3 +46,4 @@ docs/promo-video.mp4
|
||||
|
||||
# Rust 开发工具
|
||||
src-tauri/.cargo/
|
||||
.codex/
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -484,7 +484,6 @@ function switchSession(newKey) {
|
||||
clearMessages()
|
||||
loadHistory()
|
||||
refreshSessionList()
|
||||
_page?.querySelector('#chat-sidebar')?.classList.remove('open')
|
||||
}
|
||||
|
||||
async function showNewSessionDialog() {
|
||||
|
||||
@@ -332,7 +332,7 @@ function _renderUnitCard(c, showAdopt) {
|
||||
</div>
|
||||
${isRunning && (ports.panel || ports.gateway) ? `
|
||||
<div class="unit-links">
|
||||
${ports.panel ? `<a href="http://${host}:${ports.panel}" target="_blank" rel="noopener" class="unit-link panel">${icon('monitor', 12)} 面板 :${ports.panel}</a>` : ''}
|
||||
${ports.panel ? `<a href="${location.protocol}//${host}:${ports.panel}" target="_blank" rel="noopener" class="unit-link panel">${icon('monitor', 12)} 面板 :${ports.panel}</a>` : ''}
|
||||
${ports.gateway ? `<span class="unit-link gateway" data-action="quick-chat" data-container-id="${esc(c.id)}" data-node-id="${esc(c.nodeId || '')}" data-name="${esc(c.name)}" title="发送测试消息">${icon('zap', 12)} 通讯 :${ports.gateway}</span>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
@@ -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) {
|
||||
<button class="btn" data-dismiss>关闭</button>
|
||||
</div>
|
||||
<div style="margin-top:16px;font-size:11px;color:var(--text-tertiary);font-family:var(--font-mono)">
|
||||
Panel: ${panelUrl} · Gateway: ws://${host}:${gatewayPort}
|
||||
Panel: ${panelUrl} · Gateway: ${location.protocol === 'https:' ? 'wss' : 'ws'}://${host}:${gatewayPort}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@@ -1543,18 +1543,18 @@ async function showInspectDialog(page, nodeId, containerId) {
|
||||
<div class="inspect-section">
|
||||
<div class="inspect-section-title">指挥通道</div>
|
||||
<div class="inspect-links">
|
||||
${ports.panel ? `<a href="http://${host}:${ports.panel}" target="_blank" rel="noopener" class="inspect-link-card">
|
||||
${ports.panel ? `<a href="${location.protocol}//${host}:${ports.panel}" target="_blank" rel="noopener" class="inspect-link-card">
|
||||
<span class="inspect-link-icon">${icon('monitor', 20)}</span>
|
||||
<span class="inspect-link-text">
|
||||
<strong>指挥台</strong>
|
||||
<span>http://${host}:${ports.panel}</span>
|
||||
<span>${location.protocol}//${host}:${ports.panel}</span>
|
||||
</span>
|
||||
</a>` : ''}
|
||||
${ports.gateway ? `<div class="inspect-link-card" style="cursor:default;opacity:0.85">
|
||||
<span class="inspect-link-icon">${icon('zap', 20)}</span>
|
||||
<span class="inspect-link-text">
|
||||
<strong>通讯链路 (WebSocket)</strong>
|
||||
<span>ws://${host}:${ports.gateway}/ws</span>
|
||||
<span>${location.protocol === 'https:' ? 'wss' : 'ws'}://${host}:${ports.gateway}/ws</span>
|
||||
</span>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user