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:
晴天
2026-03-13 00:03:09 +08:00
parent f707b2301c
commit db30f29abf
25 changed files with 587 additions and 196 deletions

View File

@@ -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", &registry]);
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", &registry]);
c
} else {
let mut c = Command::new("npm");
c.args(["--registry", &registry]);
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(&current_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
}
}

View File

@@ -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(