mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-28 03:40:09 +08:00
feat: v0.8.2 — 15 fixes + 4 features + 3 improvements
Fixes: - Stop force-appending /v1 to API URLs (breaks Volcengine /v3 etc) - SSH upgrade: --unset-all + --add for 4 git insteadOf rules - Feishu: builtin detection, overlay→modal fix, select field, plugin version persistence - Docker: HTML response detection, Web mode guidance - Chat: runId dedup prevents duplicate messages - Cron: RPC params name→id - Channels: Gateway reload async (instant UI response), toggle cache invalidation - Linux: auto sudo for non-root npm installs (libc geteuid) - Control UI: dynamic hostname + auth token for remote access - npm: mirror fallback (npmmirror→npmjs.org) - QQBot: native binding friendly error message - Error diagnosis: SSH vs Git-not-installed, native binding detection Features: - About page: company info (武汉晴辰天下网络科技有限公司) - model-presets.js: shared module for models.js + assistant.js - Feishu: dual plugin support (builtin vs official @larksuiteoapi) - Assistant: provider preset quick-fill buttons Improvements: - Website: dynamic download links from latest.json + claw.qt.cool proxy - Linux deploy docs: upgrade guide, Gitee mirror, sudo notes - linux-deploy.sh: Gitee fallback + sudo npm + mirror retry
This commit is contained in:
@@ -42,6 +42,7 @@ fn get_configured_registry() -> String {
|
||||
|
||||
/// 创建使用配置源的 npm Command
|
||||
/// Windows 上 npm 是 npm.cmd,需要通过 cmd /c 调用,并隐藏窗口
|
||||
/// Linux 非 root 用户全局安装需要 sudo
|
||||
fn npm_command() -> Command {
|
||||
let registry = get_configured_registry();
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -53,13 +54,29 @@ fn npm_command() -> Command {
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
cmd
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let mut cmd = Command::new("npm");
|
||||
cmd.args(["--registry", ®istry]);
|
||||
cmd.env("PATH", super::enhanced_path());
|
||||
cmd
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Linux 非 root 用户全局 npm install 需要 sudo
|
||||
let need_sudo = unsafe { libc::geteuid() } != 0;
|
||||
let mut cmd = if need_sudo {
|
||||
let mut c = Command::new("sudo");
|
||||
c.args(["npm", "--registry", ®istry]);
|
||||
c
|
||||
} else {
|
||||
let mut c = Command::new("npm");
|
||||
c.args(["--registry", ®istry]);
|
||||
c
|
||||
};
|
||||
cmd.env("PATH", super::enhanced_path());
|
||||
cmd
|
||||
}
|
||||
}
|
||||
|
||||
fn backups_dir() -> PathBuf {
|
||||
@@ -611,24 +628,28 @@ pub async fn upgrade_openclaw(
|
||||
let old_pkg = npm_package_name(¤t_source);
|
||||
let need_uninstall_old = current_source != source;
|
||||
|
||||
// 自动配置 git 使用 HTTPS 替代 SSH,避免用户没配 SSH Key 导致依赖安装失败
|
||||
// 自动配置 git 全面使用 HTTPS 替代 SSH/git 协议,避免用户没配 SSH Key 导致依赖安装失败
|
||||
let _ = app.emit("upgrade-log", "配置 Git HTTPS 模式...");
|
||||
// 先清除旧的 insteadOf 规则,再逐条添加(git config 不带 --add 会覆盖,只保留最后一条)
|
||||
let _ = Command::new("git")
|
||||
.args([
|
||||
"config",
|
||||
"--global",
|
||||
"url.https://github.com/.insteadOf",
|
||||
"ssh://git@github.com/",
|
||||
])
|
||||
.output();
|
||||
let _ = Command::new("git")
|
||||
.args([
|
||||
"config",
|
||||
"--global",
|
||||
"url.https://github.com/.insteadOf",
|
||||
"git@github.com:",
|
||||
])
|
||||
.args(["config", "--global", "--unset-all", "url.https://github.com/.insteadOf"])
|
||||
.output();
|
||||
for from in &[
|
||||
"ssh://git@github.com/",
|
||||
"git@github.com:",
|
||||
"git://github.com/",
|
||||
"git+ssh://git@github.com/",
|
||||
] {
|
||||
let _ = Command::new("git")
|
||||
.args([
|
||||
"config",
|
||||
"--global",
|
||||
"--add",
|
||||
"url.https://github.com/.insteadOf",
|
||||
from,
|
||||
])
|
||||
.output();
|
||||
}
|
||||
|
||||
let _ = app.emit("upgrade-log", format!("$ npm install -g {pkg}"));
|
||||
let _ = app.emit("upgrade-progress", 10);
|
||||
@@ -651,6 +672,8 @@ pub async fn upgrade_openclaw(
|
||||
|
||||
let mut child = npm_command()
|
||||
.args(["install", "-g", &pkg, "--registry", registry, "--verbose"])
|
||||
.env("GIT_TERMINAL_PROMPT", "0")
|
||||
.env("GIT_SSH_COMMAND", "ssh -o BatchMode=yes -o StrictHostKeyChecking=no")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
@@ -695,20 +718,66 @@ pub async fn upgrade_openclaw(
|
||||
.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}"));
|
||||
|
||||
// 如果使用了镜像源失败,自动降级到官方源重试
|
||||
let used_mirror = registry.contains("npmmirror.com") || registry.contains("taobao.org");
|
||||
if used_mirror {
|
||||
let _ = app.emit("upgrade-log", "");
|
||||
let _ = app.emit("upgrade-log", "⚠️ 镜像源安装失败,自动切换到官方源重试...");
|
||||
let _ = app.emit("upgrade-progress", 15);
|
||||
let fallback = "https://registry.npmjs.org";
|
||||
let mut child2 = npm_command()
|
||||
.args(["install", "-g", &pkg, "--registry", fallback, "--verbose"])
|
||||
.env("GIT_TERMINAL_PROMPT", "0")
|
||||
.env("GIT_SSH_COMMAND", "ssh -o BatchMode=yes -o StrictHostKeyChecking=no")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("执行重试命令失败: {e}"))?;
|
||||
let stderr2 = child2.stderr.take();
|
||||
let stdout2 = child2.stdout.take();
|
||||
let app3 = app.clone();
|
||||
let stderr_lines3 = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
|
||||
let stderr_lines4 = stderr_lines3.clone();
|
||||
let handle2 = std::thread::spawn(move || {
|
||||
if let Some(pipe) = stderr2 {
|
||||
let mut p: u32 = 20;
|
||||
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
|
||||
let _ = app3.emit("upgrade-log", &line);
|
||||
stderr_lines4.lock().unwrap().push(line);
|
||||
if p < 75 { p += 2; let _ = app3.emit("upgrade-progress", p); }
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Some(pipe) = stdout2 {
|
||||
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
|
||||
let _ = app.emit("upgrade-log", &line);
|
||||
}
|
||||
}
|
||||
let _ = handle2.join();
|
||||
let _ = app.emit("upgrade-progress", 80);
|
||||
let status2 = child2.wait().map_err(|e| format!("等待重试进程失败: {e}"))?;
|
||||
let _ = app.emit("upgrade-progress", 100);
|
||||
if !status2.success() {
|
||||
let code2 = status2.code().map(|c| c.to_string()).unwrap_or("unknown".into());
|
||||
let tail = stderr_lines3.lock().unwrap().iter().rev().take(15).rev().cloned().collect::<Vec<_>>().join("\n");
|
||||
return Err(format!("升级失败(镜像源和官方源均失败),exit code: {code2}\n{tail}"));
|
||||
}
|
||||
let _ = app.emit("upgrade-log", "✅ 官方源安装成功");
|
||||
} else {
|
||||
let _ = app.emit("upgrade-log", format!("❌ 升级失败 (exit code: {code})"));
|
||||
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 始终可用)
|
||||
@@ -1307,13 +1376,8 @@ fn normalize_base_url_for_api(raw: &str, api_type: &str) -> String {
|
||||
}
|
||||
"google-gemini" => base,
|
||||
_ => {
|
||||
if !base.ends_with("/v1") {
|
||||
if let Some(idx) = base.find("/v1/") {
|
||||
base.truncate(idx + 3);
|
||||
} else {
|
||||
base.push_str("/v1");
|
||||
}
|
||||
}
|
||||
// 不再强制追加 /v1,尊重用户填写的 URL(火山引擎等第三方用 /v3 等路径)
|
||||
// 仅 Ollama (端口 11434) 自动补 /v1
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,8 +416,11 @@ pub async fn save_messaging_platform(
|
||||
// 写回配置并重载 Gateway
|
||||
super::config::save_openclaw_json(&cfg)?;
|
||||
|
||||
// 触发 Gateway 重载使配置生效
|
||||
let _ = super::config::do_reload_gateway(&app).await;
|
||||
// Gateway 重载在后台进行,不阻塞 UI 响应
|
||||
let app2 = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _ = super::config::do_reload_gateway(&app2).await;
|
||||
});
|
||||
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
@@ -436,7 +439,11 @@ pub async fn remove_messaging_platform(
|
||||
}
|
||||
|
||||
super::config::save_openclaw_json(&cfg)?;
|
||||
let _ = super::config::do_reload_gateway(&app).await;
|
||||
// Gateway 重载在后台进行,不阻塞 UI 响应
|
||||
let app2 = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _ = super::config::do_reload_gateway(&app2).await;
|
||||
});
|
||||
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
@@ -462,7 +469,11 @@ pub async fn toggle_messaging_platform(
|
||||
}
|
||||
|
||||
super::config::save_openclaw_json(&cfg)?;
|
||||
let _ = super::config::do_reload_gateway(&app).await;
|
||||
// Gateway 重载在后台进行,不阻塞 UI 响应
|
||||
let app2 = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _ = super::config::do_reload_gateway(&app2).await;
|
||||
});
|
||||
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
@@ -519,6 +530,9 @@ pub async fn get_channel_plugin_status(plugin_id: String) -> Result<Value, Strin
|
||||
let installed = plugin_dir.is_dir() && plugin_install_marker_exists(&plugin_dir);
|
||||
let legacy_backup_detected = legacy_plugin_backup_dir(plugin_id).exists();
|
||||
|
||||
// 检测插件是否为 OpenClaw 内置(新版 openclaw/openclaw-zh 打包了 feishu 等插件)
|
||||
let builtin = is_plugin_builtin(plugin_id);
|
||||
|
||||
let cfg = super::config::load_openclaw_json().unwrap_or_else(|_| json!({}));
|
||||
let allowed = cfg
|
||||
.get("plugins")
|
||||
@@ -536,6 +550,7 @@ pub async fn get_channel_plugin_status(plugin_id: String) -> Result<Value, Strin
|
||||
|
||||
Ok(json!({
|
||||
"installed": installed,
|
||||
"builtin": builtin,
|
||||
"path": plugin_dir.to_string_lossy(),
|
||||
"allowed": allowed,
|
||||
"enabled": enabled,
|
||||
@@ -881,6 +896,50 @@ fn cleanup_failed_qqbot_install(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检测插件是否为 OpenClaw 内置(作为 npm 依赖打包在 openclaw/openclaw-zh 中)
|
||||
fn is_plugin_builtin(plugin_id: &str) -> bool {
|
||||
// 插件 ID → npm 包名映射
|
||||
let pkg_name = match plugin_id {
|
||||
"feishu" => "@openclaw/feishu",
|
||||
"dingtalk-connector" => "@dingtalk-real-ai/dingtalk-connector",
|
||||
_ => return false,
|
||||
};
|
||||
// 在全局 npm node_modules 中查找 openclaw 安装目录
|
||||
let npm_dirs: Vec<PathBuf> = {
|
||||
let mut dirs = Vec::new();
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(appdata) = std::env::var_os("APPDATA") {
|
||||
let base = PathBuf::from(appdata).join("npm").join("node_modules");
|
||||
dirs.push(base.join("@qingchencloud").join("openclaw-zh"));
|
||||
dirs.push(base.join("openclaw"));
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
dirs.push(PathBuf::from("/opt/homebrew/lib/node_modules/@qingchencloud/openclaw-zh"));
|
||||
dirs.push(PathBuf::from("/opt/homebrew/lib/node_modules/openclaw"));
|
||||
dirs.push(PathBuf::from("/usr/local/lib/node_modules/@qingchencloud/openclaw-zh"));
|
||||
dirs.push(PathBuf::from("/usr/local/lib/node_modules/openclaw"));
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
dirs.push(PathBuf::from("/usr/local/lib/node_modules/@qingchencloud/openclaw-zh"));
|
||||
dirs.push(PathBuf::from("/usr/local/lib/node_modules/openclaw"));
|
||||
dirs.push(PathBuf::from("/usr/lib/node_modules/@qingchencloud/openclaw-zh"));
|
||||
dirs.push(PathBuf::from("/usr/lib/node_modules/openclaw"));
|
||||
}
|
||||
dirs
|
||||
};
|
||||
// 插件包名拆分成路径片段,如 @openclaw/feishu → @openclaw/feishu
|
||||
let pkg_path: PathBuf = pkg_name.split('/').collect();
|
||||
for base in &npm_dirs {
|
||||
let candidate = base.join("node_modules").join(&pkg_path);
|
||||
if candidate.join("package.json").is_file() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn generic_plugin_dir(plugin_id: &str) -> PathBuf {
|
||||
super::openclaw_dir().join("extensions").join(plugin_id)
|
||||
}
|
||||
@@ -1117,10 +1176,13 @@ pub async fn install_qqbot_plugin(app: tauri::AppHandle) -> Result<String, Strin
|
||||
|
||||
let stderr = child.stderr.take();
|
||||
let app2 = app.clone();
|
||||
let qqbot_stderr_lines = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
|
||||
let qqbot_stderr_clone = qqbot_stderr_lines.clone();
|
||||
let handle = std::thread::spawn(move || {
|
||||
if let Some(pipe) = stderr {
|
||||
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
|
||||
let _ = app2.emit("plugin-log", &line);
|
||||
qqbot_stderr_clone.lock().unwrap().push(line);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1128,9 +1190,11 @@ pub async fn install_qqbot_plugin(app: tauri::AppHandle) -> Result<String, Strin
|
||||
let _ = app.emit("plugin-progress", 30);
|
||||
|
||||
let mut progress = 30;
|
||||
let mut qqbot_stdout_lines = Vec::new();
|
||||
if let Some(pipe) = child.stdout.take() {
|
||||
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
|
||||
let _ = app.emit("plugin-log", &line);
|
||||
qqbot_stdout_lines.push(line);
|
||||
if progress < 90 {
|
||||
progress += 10;
|
||||
let _ = app.emit("plugin-progress", progress);
|
||||
@@ -1145,6 +1209,26 @@ pub async fn install_qqbot_plugin(app: tauri::AppHandle) -> Result<String, Strin
|
||||
.wait()
|
||||
.map_err(|e| format!("等待安装进程失败: {}", e))?;
|
||||
|
||||
// 检测 native binding 缺失(macOS/Linux 上 OpenClaw CLI 自身启动失败)
|
||||
let all_output = {
|
||||
let stderr_guard = qqbot_stderr_lines.lock().unwrap();
|
||||
let mut combined = qqbot_stdout_lines.join("\n");
|
||||
combined.push('\n');
|
||||
combined.push_str(&stderr_guard.join("\n"));
|
||||
combined
|
||||
};
|
||||
if all_output.contains("native binding") || all_output.contains("Failed to start CLI") {
|
||||
let _ = app.emit("plugin-log", "");
|
||||
let _ = app.emit("plugin-log", "⚠️ 检测到 OpenClaw CLI 原生依赖问题(native binding 缺失)");
|
||||
let _ = app.emit("plugin-log", "这是 OpenClaw 的上游依赖问题,非 QQBot 插件本身的问题。");
|
||||
let _ = app.emit("plugin-log", "请在终端手动执行以下命令重装 OpenClaw:");
|
||||
let _ = app.emit("plugin-log", " npm i -g @qingchencloud/openclaw-zh@latest --registry https://registry.npmmirror.com");
|
||||
let _ = app.emit("plugin-log", "重装完成后再回来安装 QQBot 插件。");
|
||||
let _ = cleanup_failed_qqbot_install(had_existing_plugin, had_existing_config);
|
||||
let _ = app.emit("plugin-progress", 100);
|
||||
return Err("OpenClaw CLI 原生依赖缺失,请先在终端重装 OpenClaw(详见上方日志)".into());
|
||||
}
|
||||
|
||||
let finalize = (|| -> Result<(), String> {
|
||||
if !status.success() {
|
||||
let _ = app.emit(
|
||||
|
||||
Reference in New Issue
Block a user