feat: v0.6.0 — 公益AI接口 + Agent灵魂借尸还魂 + 知识库 + 全局AI诊断 + 官网改版

This commit is contained in:
晴天
2026-03-07 19:36:25 +08:00
parent b09f48f0dd
commit 0752dc2a71
55 changed files with 4346 additions and 480 deletions

10
src-tauri/Cargo.lock generated
View File

@@ -328,13 +328,14 @@ dependencies = [
[[package]]
name = "clawpanel"
version = "0.5.7"
version = "0.6.0"
dependencies = [
"base64 0.22.1",
"chrono",
"dirs",
"ed25519-dalek",
"rand 0.8.5",
"regex",
"reqwest 0.12.28",
"serde",
"serde_json",
@@ -343,6 +344,7 @@ dependencies = [
"tauri-build",
"tauri-plugin-shell",
"tokio",
"urlencoding",
"zip",
]
@@ -4361,6 +4363,12 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "urlpattern"
version = "0.3.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "clawpanel"
version = "0.5.7"
version = "0.6.0"
edition = "2021"
description = "ClawPanel - OpenClaw 可视化管理面板"
authors = ["qingchencloud"]
@@ -28,4 +28,6 @@ ed25519-dalek = { version = "2", features = ["rand_core"] }
sha2 = "0.10"
rand = "0.8"
base64 = "0.22"
urlencoding = "2"
regex = "1"
tokio = { version = "1", features = ["process", "time"] }

View File

@@ -357,6 +357,124 @@ async fn get_port_process(port: u16) -> String {
}
}
/// 联网搜索DuckDuckGo HTML
#[tauri::command]
pub async fn assistant_web_search(
query: String,
max_results: Option<usize>,
) -> Result<String, String> {
let max = max_results.unwrap_or(5);
let url = format!(
"https://html.duckduckgo.com/html/?q={}",
urlencoding::encode(&query)
);
let client = reqwest::Client::builder()
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| format!("创建 HTTP 客户端失败: {e}"))?;
let html = client
.get(&url)
.send()
.await
.map_err(|e| format!("搜索请求失败: {e}"))?
.text()
.await
.map_err(|e| format!("读取搜索结果失败: {e}"))?;
// 解析搜索结果
let mut results = Vec::new();
let re_result = regex::Regex::new(
r#"class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)</a>[\s\S]*?class="result__snippet"[^>]*>([\s\S]*?)</a>"#
).unwrap();
for cap in re_result.captures_iter(&html) {
if results.len() >= max {
break;
}
let raw_url = &cap[1];
let title = regex::Regex::new(r"<[^>]+>")
.unwrap()
.replace_all(&cap[2], "")
.trim()
.to_string();
let snippet = regex::Regex::new(r"<[^>]+>")
.unwrap()
.replace_all(&cap[3], "")
.trim()
.to_string();
// 解码 DuckDuckGo 的重定向 URL
let final_url = if let Some(pos) = raw_url.find("uddg=") {
let encoded = &raw_url[pos + 5..];
let end = encoded.find('&').unwrap_or(encoded.len());
urlencoding::decode(&encoded[..end])
.unwrap_or_else(|_| encoded[..end].into())
.to_string()
} else {
raw_url.to_string()
};
if !title.is_empty() && !final_url.is_empty() {
results.push((title, final_url, snippet));
}
}
if results.is_empty() {
return Ok(format!("搜索「{}」未找到相关结果。", query));
}
let mut output = format!("搜索「{}」找到 {} 条结果:\n\n", query, results.len());
for (i, (title, url, snippet)) in results.iter().enumerate() {
output.push_str(&format!(
"{}. **{}**\n {}\n {}\n\n",
i + 1,
title,
url,
snippet
));
}
Ok(output)
}
/// 抓取 URL 内容(通过 Jina Reader API
#[tauri::command]
pub async fn assistant_fetch_url(url: String) -> Result<String, String> {
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err("URL 必须以 http:// 或 https:// 开头".into());
}
let jina_url = format!("https://r.jina.ai/{}", url);
let client = reqwest::Client::builder()
.user_agent("Mozilla/5.0")
.timeout(std::time::Duration::from_secs(15))
.build()
.map_err(|e| format!("创建 HTTP 客户端失败: {e}"))?;
let content = client
.get(&jina_url)
.header("Accept", "text/plain")
.send()
.await
.map_err(|e| format!("抓取失败: {e}"))?
.text()
.await
.map_err(|e| format!("读取内容失败: {e}"))?;
if content.len() > 100_000 {
Ok(format!(
"{}\n\n[内容已截断,超过 100KB 限制]",
&content[..100_000]
))
} else if content.is_empty() {
Ok("(页面内容为空)".into())
} else {
Ok(content)
}
}
/// 列出目录内容
#[tauri::command]
pub async fn assistant_list_dir(path: String) -> Result<String, String> {

View File

@@ -560,12 +560,16 @@ pub async fn upgrade_openclaw(app: tauri::AppHandle, source: String) -> Result<S
let stdout = child.stdout.take();
// stderr 每行递增进度10→80 区间),让用户看到进度在动
// 同时收集 stderr 用于失败时返回给前端诊断
let app2 = app.clone();
let stderr_lines = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
let stderr_lines2 = stderr_lines.clone();
let handle = std::thread::spawn(move || {
let mut progress: u32 = 15;
if let Some(pipe) = stderr {
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
let _ = app2.emit("upgrade-log", &line);
stderr_lines2.lock().unwrap().push(line);
if progress < 75 {
progress += 2;
let _ = app2.emit("upgrade-progress", progress);
@@ -587,8 +591,13 @@ pub async fn upgrade_openclaw(app: tauri::AppHandle, source: String) -> Result<S
let _ = app.emit("upgrade-progress", 100);
if !status.success() {
let _ = app.emit("upgrade-log", "❌ 升级失败");
return Err("升级失败,请查看日志".into());
let code = status.code().map(|c| c.to_string()).unwrap_or("unknown".into());
let _ = app.emit("upgrade-log", format!("升级失败 (exit code: {code})"));
// 把 stderr 最后 15 行带进错误消息,确保前端诊断函数能匹配到
// npm 内部错误码(如 -4058 ENOENT、EPERM 等)
let tail = stderr_lines.lock().unwrap()
.iter().rev().take(15).rev().cloned().collect::<Vec<_>>().join("\n");
return Err(format!("升级失败exit code: {code}\n{tail}"));
}
// 安装成功后再卸载旧包(确保 CLI 始终可用)
@@ -1024,6 +1033,24 @@ pub async fn restart_gateway() -> Result<String, String> {
reload_gateway().await
}
/// 清理 base URL去掉尾部斜杠和已知端点路径防止用户粘贴完整端点 URL 导致路径重复
fn normalize_base_url(raw: &str) -> String {
let mut base = raw.trim_end_matches('/').to_string();
for suffix in &[
"/chat/completions",
"/completions",
"/responses",
"/messages",
"/models",
] {
if base.ends_with(suffix) {
base.truncate(base.len() - suffix.len());
break;
}
}
base.trim_end_matches('/').to_string()
}
/// 测试模型连通性:向 provider 发送一个简单的 chat completion 请求
#[tauri::command]
pub async fn test_model(
@@ -1031,7 +1058,7 @@ pub async fn test_model(
api_key: String,
model_id: String,
) -> Result<String, String> {
let url = format!("{}/chat/completions", base_url.trim_end_matches('/'));
let url = format!("{}/chat/completions", normalize_base_url(&base_url));
let body = serde_json::json!({
"model": model_id,
@@ -1100,7 +1127,7 @@ pub async fn test_model(
/// 获取服务商的远程模型列表(调用 /models 接口)
#[tauri::command]
pub async fn list_remote_models(base_url: String, api_key: String) -> Result<Vec<String>, String> {
let url = format!("{}/models", base_url.trim_end_matches('/'));
let url = format!("{}/models", normalize_base_url(&base_url));
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(15))

View File

@@ -364,13 +364,13 @@ echo "安装完成"
let mut child = {
let install_script = r#"
$ErrorActionPreference = 'Stop'
$binDir = Join-Path $env:USERPROFILE 'bin'
if (-not (Test-Path $binDir)) { New-Item -ItemType Directory -Path $binDir -Force | Out-Null }
Write-Output '下载 cftunnel...'
$url = 'https://github.com/qingchencloud/cftunnel/releases/latest/download/cftunnel-windows-amd64.exe'
$dest = Join-Path $binDir 'cftunnel.exe'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing
Write-Output '通过官方安装脚本安装 cftunnel...'
$tmp = Join-Path $env:TEMP 'install-cftunnel.ps1'
Write-Output '下载安装脚本到临时文件...'
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/qingchencloud/cftunnel/main/install.ps1' -OutFile $tmp -UseBasicParsing
Write-Output '执行安装脚本...'
& $tmp
Remove-Item $tmp -ErrorAction SilentlyContinue
Write-Output '安装完成'
"#;
// 使用完整路径调用 PowerShell避免 MSYS2/Git Bash 环境下找不到
@@ -434,7 +434,7 @@ Write-Output '安装完成'
Ok("安装成功".into())
}
/// 一键安装 ClawApp通过 npm
/// 一键安装 ClawApp通过官方安装脚本
#[tauri::command]
pub async fn install_clawapp(app: tauri::AppHandle) -> Result<String, String> {
use std::io::{BufRead, BufReader};
@@ -444,25 +444,58 @@ pub async fn install_clawapp(app: tauri::AppHandle) -> Result<String, String> {
let _ = app.emit("install-log", "开始安装 ClawApp...");
let _ = app.emit("install-progress", 10);
let _ = app.emit("install-log", "通过 npm 安装 clawapp...");
let _ = app.emit("install-log", "下载安装脚本...");
let _ = app.emit("install-progress", 30);
#[cfg(target_os = "windows")]
#[cfg(not(target_os = "windows"))]
let mut child = {
let mut cmd = Command::new("cmd");
cmd.args(["/c", "npm", "install", "-g", "clawapp"]);
cmd.creation_flags(0x08000000);
cmd.stdout(Stdio::piped())
let install_script = r#"
#!/bin/bash
set -e
echo "通过官方安装脚本安装 ClawApp..."
curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawapp/main/install.sh | bash
echo "安装完成"
"#;
Command::new("bash")
.arg("-c")
.arg(install_script)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("启动安装进程失败: {e}"))?
};
#[cfg(not(target_os = "windows"))]
#[cfg(target_os = "windows")]
let mut child = {
Command::new("npm")
.args(["install", "-g", "clawapp"])
.stdout(Stdio::piped())
let install_script = r#"
$ErrorActionPreference = 'Stop'
Write-Output '通过官方安装脚本安装 ClawApp...'
$tmp = Join-Path $env:TEMP 'install-clawapp.ps1'
Write-Output '下载安装脚本到临时文件...'
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/qingchencloud/clawapp/main/install.ps1' -OutFile $tmp -UseBasicParsing
Write-Output '执行安装脚本...'
& $tmp -Auto
Remove-Item $tmp -ErrorAction SilentlyContinue
Write-Output '安装完成'
"#;
let ps_path = std::env::var("SystemRoot")
.map(|root| {
format!(
"{}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
root
)
})
.unwrap_or_else(|_| "powershell.exe".to_string());
let mut cmd = Command::new(&ps_path);
cmd.args([
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
install_script,
]);
cmd.creation_flags(0x08000000);
cmd.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("启动安装进程失败: {e}"))?

View File

@@ -53,7 +53,12 @@ fn build_enhanced_path() -> String {
format!("{}/.volta/bin", home.display()),
format!("{}/.nodenv/shims", home.display()),
format!("{}/n/bin", home.display()),
format!("{}/.npm-global/bin", home.display()),
];
// NPM_CONFIG_PREFIX: 用户通过 npm config set prefix 自定义的全局安装路径
if let Ok(prefix) = std::env::var("NPM_CONFIG_PREFIX") {
extra.push(format!("{}/bin", prefix));
}
// 扫描 nvm 实际安装的版本目录(兼容无 current 符号链接的情况)
let nvm_versions = home.join(".nvm/versions/node");
if nvm_versions.is_dir() {
@@ -104,7 +109,12 @@ fn build_enhanced_path() -> String {
format!("{}/.volta/bin", home.display()),
format!("{}/.nodenv/shims", home.display()),
format!("{}/n/bin", home.display()),
format!("{}/.npm-global/bin", home.display()),
];
// NPM_CONFIG_PREFIX: 用户通过 npm config set prefix 自定义的全局安装路径
if let Ok(prefix) = std::env::var("NPM_CONFIG_PREFIX") {
extra.push(format!("{}/bin", prefix));
}
// NVM_DIR 环境变量(用户可能自定义了 nvm 安装目录)
let nvm_dir = std::env::var("NVM_DIR")
.ok()

View File

@@ -265,16 +265,37 @@ mod platform {
#[cfg(target_os = "windows")]
mod platform {
use std::os::windows::process::CommandExt;
use std::sync::Mutex;
use tokio::process::Command as TokioCommand;
/// 缓存 is_cli_installed 结果,避免每 15 秒 polling 都 spawn cmd.exe
static CLI_CACHE: Mutex<Option<(bool, std::time::Instant)>> = Mutex::new(None);
const CLI_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(60);
/// Windows 不需要 UID
pub fn current_uid() -> Result<u32, String> {
Ok(0)
}
/// 检测 openclaw CLI 是否已安装
/// 检测 openclaw CLI 是否已安装(带 60s 缓存,避免频繁 spawn 进程)
pub fn is_cli_installed() -> bool {
// 方式1: 检查常见文件路径
// 检查缓存
if let Ok(guard) = CLI_CACHE.lock() {
if let Some((val, ts)) = *guard {
if ts.elapsed() < CLI_CACHE_TTL {
return val;
}
}
}
let result = check_cli_installed_inner();
if let Ok(mut guard) = CLI_CACHE.lock() {
*guard = Some((result, std::time::Instant::now()));
}
result
}
fn check_cli_installed_inner() -> bool {
// 方式1: 检查常见文件路径(零进程,最快)
if let Ok(appdata) = std::env::var("APPDATA") {
let cmd_path = std::path::Path::new(&appdata)
.join("npm")

View File

@@ -84,6 +84,8 @@ pub fn run() {
assistant::assistant_system_info,
assistant::assistant_list_processes,
assistant::assistant_check_port,
assistant::assistant_web_search,
assistant::assistant_fetch_url,
// 数据目录 & 图片存储
assistant::assistant_ensure_data_dir,
assistant::assistant_save_image,

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
"productName": "ClawPanel",
"version": "0.5.7",
"version": "0.6.0",
"identifier": "ai.openclaw.clawpanel",
"build": {
"frontendDist": "../dist",