feat: 飞书官方插件迁移 + 配对审批 + Gateway防卡死 + 微信升级修复 + 更新检测修复

- 飞书渠道从 @openclaw/feishu 迁移到 @larksuite/openclaw-lark 官方插件
- 保存飞书配置时自动禁用旧 feishu 插件,防止新旧插件冲突
- 所有主要渠道(飞书/Telegram/Discord/Slack)启用配对审批UI
- gateway_command 增加20s超时,超时后force-kill+fresh start
- 全平台启动前端口占用检查,防止Guardian无限拉起
- Linux gateway_command 补齐 Duration 导入和 cleanup_zombie 实现
- Guardian自动守护在Tauri桌面端也启用,轮询间隔30s→15s
- 微信渠道:升级操作不再弹出扫码二维码,按钮文案区分安装/升级
- 版本更新检测:CI不再将minAppVersion写死为当前版本
- 部署脚本增强OpenClaw检测,支持已安装的官方版
- 日间/夜间模式圆形扩散切换动画(View Transitions API)
- API错误信息完整展示(429限流等),URL自动转可点击链接
- 第三方API接入引导优化:移除内置密钥,引导式流程
- 修复全平台 Clippy 警告(strip_prefix/dead_code/unnecessary_unwrap等)
- Rust代码格式化修复(cargo fmt)
- toast组件支持HTML内容渲染
- Rust后端test_model返回详细错误信息
This commit is contained in:
晴天
2026-03-23 20:37:48 +08:00
parent dccb4b4dbf
commit 3687e26d5d
50 changed files with 8055 additions and 2715 deletions

View File

@@ -3,6 +3,12 @@ use std::path::PathBuf;
use std::sync::RwLock;
use std::time::Duration;
/// 缓存 gateway 端口避免频繁读文件5秒有效期
static GATEWAY_PORT_CACHE: std::sync::LazyLock<std::sync::Mutex<(u16, std::time::Instant)>> =
std::sync::LazyLock::new(|| {
std::sync::Mutex::new((18789, std::time::Instant::now() - Duration::from_secs(60)))
});
pub mod agent;
pub mod assistant;
pub mod config;
@@ -39,6 +45,41 @@ pub fn openclaw_dir() -> PathBuf {
default_openclaw_dir()
}
/// Gateway 监听端口:读取 `openclaw.json` 的 `gateway.port`,缺省 **18789**。
/// 与面板「Gateway 配置」、服务状态检测netstat / TCP / launchctl 兜底)共用同一来源,
/// 并尊重 `clawpanel.json` 中的 `openclawDir` 自定义配置目录。
pub fn gateway_listen_port() -> u16 {
// 5秒内返回缓存值避免服务状态检测时频繁读文件
if let Ok(cache) = GATEWAY_PORT_CACHE.lock() {
if cache.1.elapsed() < Duration::from_secs(5) {
return cache.0;
}
}
let port = read_gateway_port_from_config();
if let Ok(mut cache) = GATEWAY_PORT_CACHE.lock() {
*cache = (port, std::time::Instant::now());
}
port
}
fn read_gateway_port_from_config() -> u16 {
let config_path = openclaw_dir().join("openclaw.json");
if let Ok(content) = std::fs::read_to_string(&config_path) {
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(port) = val
.get("gateway")
.and_then(|g| g.get("port"))
.and_then(|p| p.as_u64())
{
if port > 0 && port < 65536 {
return port as u16;
}
}
}
}
18789
}
fn panel_config_path() -> PathBuf {
// ClawPanel 自身配置始终在默认目录,不随 openclawDir 变化
default_openclaw_dir().join("clawpanel.json")
@@ -341,16 +382,28 @@ fn build_enhanced_path() -> String {
let mut extra: Vec<String> = vec![];
// 1. NVM_SYMLINKnvm-windows 活跃版本符号链接,如 D:\nodejs—— 最高优先级
// 增强:尝试解析符号链接目标
if let Ok(nvm_symlink) = std::env::var("NVM_SYMLINK") {
let symlink_path = std::path::Path::new(&nvm_symlink);
if symlink_path.is_dir() {
extra.push(nvm_symlink.clone());
}
// 如果是符号链接,尝试读取其实际指向的目标
#[cfg(target_os = "windows")]
if symlink_path.is_symlink() {
if let Ok(target) = std::fs::read_link(symlink_path) {
if target.is_dir() {
extra.push(target.to_string_lossy().to_string());
}
}
}
}
// 2. NVM_HOME用户自定义 nvm 安装目录)
if let Ok(nvm_home) = std::env::var("NVM_HOME") {
let nvm_path = std::path::Path::new(&nvm_home);
if nvm_path.is_dir() {
// 扫描所有已安装的版本目录
if let Ok(entries) = std::fs::read_dir(nvm_path) {
for entry in entries.flatten() {
let p = entry.path();
@@ -359,13 +412,34 @@ fn build_enhanced_path() -> String {
}
}
}
// 尝试从 settings.json 读取当前激活版本
let settings_path = nvm_path.join("settings.json");
if settings_path.exists() {
if let Ok(content) = std::fs::read_to_string(&settings_path) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
// settings.json 中有 "path" 字段指向当前版本
if let Some(current_version) = json.get("path").and_then(|v| v.as_str())
{
let version_path = nvm_path.join(current_version);
if version_path.is_dir() {
// 将当前激活版本移到更高优先级
let version_bin = version_path.to_string_lossy().to_string();
if !extra.contains(&version_bin) {
extra.insert(0, version_bin);
}
}
}
}
}
}
}
}
// 3. %APPDATA%\nvmnvm-windows 默认安装目录)
if !appdata.is_empty() {
extra.push(format!(r"{}\nvm", appdata));
let nvm_dir = std::path::Path::new(&appdata).join("nvm");
if nvm_dir.is_dir() {
// 扫描所有已安装的版本
if let Ok(entries) = std::fs::read_dir(&nvm_dir) {
for entry in entries.flatten() {
let p = entry.path();
@@ -374,10 +448,35 @@ fn build_enhanced_path() -> String {
}
}
}
// 尝试从 settings.json 读取当前激活版本
let settings_path = nvm_dir.join("settings.json");
if settings_path.exists() {
if let Ok(content) = std::fs::read_to_string(&settings_path) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(current_version) = json.get("path").and_then(|v| v.as_str())
{
let version_path = nvm_dir.join(current_version);
if version_path.is_dir() {
let version_bin = version_path.to_string_lossy().to_string();
if !extra.contains(&version_bin) {
extra.insert(0, version_bin);
}
}
}
}
}
}
}
}
// 4. volta
extra.push(format!(r"{}\.volta\bin", home.display()));
// volta 的活跃版本
let volta_bin = std::path::Path::new(&home).join(".volta/bin");
if volta_bin.is_dir() && !extra.contains(&volta_bin.to_string_lossy().to_string()) {
extra.insert(0, volta_bin.to_string_lossy().to_string());
}
// 5. fnm
if !localappdata.is_empty() {
extra.push(format!(r"{}\fnm_multishells", localappdata));
@@ -388,30 +487,53 @@ fn build_enhanced_path() -> String {
.unwrap_or_else(|| std::path::Path::new(&appdata).join("fnm"));
let fnm_versions = fnm_base.join("node-versions");
if fnm_versions.is_dir() {
// 尝试找到 fnm 的当前活跃版本
let fnm_current = fnm_base.join("current");
if fnm_current.is_dir() {
let current_inst = fnm_current.join("installation");
if current_inst.is_dir()
&& current_inst.join("node.exe").exists()
&& !extra.contains(&current_inst.to_string_lossy().to_string())
{
extra.insert(0, current_inst.to_string_lossy().to_string());
}
}
// 扫描所有版本
if let Ok(entries) = std::fs::read_dir(&fnm_versions) {
for entry in entries.flatten() {
let inst = entry.path().join("installation");
if inst.is_dir() && inst.join("node.exe").exists() {
extra.push(inst.to_string_lossy().to_string());
let inst_str = inst.to_string_lossy().to_string();
if !extra.contains(&inst_str) {
extra.push(inst_str);
}
}
}
}
}
// 6. npm 全局openclaw.cmd 通常在这里)
if !appdata.is_empty() {
extra.push(format!(r"{}\npm", appdata));
}
// 7. 系统默认 Node.js 安装路径(优先级最低)
extra.push(format!(r"{}\nodejs", pf));
extra.push(format!(r"{}\nodejs", pf86));
if !localappdata.is_empty() {
extra.push(format!(r"{}\Programs\nodejs", localappdata));
}
// 8. 扫描常见盘符下的 Node 安装(用户可能装在 D:\、F:\ 等)
for drive in &["C", "D", "E", "F"] {
extra.push(format!(r"{}:\nodejs", drive));
extra.push(format!(r"{}:\Node", drive));
extra.push(format!(r"{}:\Program Files\nodejs", drive));
// 常见 AI/Dev 工具目录
extra.push(format!(r"{}:\AI\Node", drive));
extra.push(format!(r"{}:\AI\nodejs", drive));
extra.push(format!(r"{}:\Dev\nodejs", drive));
extra.push(format!(r"{}:\Tools\nodejs", drive));
}
let mut parts: Vec<&str> = vec![];
@@ -419,9 +541,10 @@ fn build_enhanced_path() -> String {
if let Some(ref cp) = custom_path {
parts.push(cp.as_str());
}
// 然后是默认扫描到的路径
// 然后是默认扫描到的路径(去重)
let mut seen = std::collections::HashSet::new();
for p in &extra {
if std::path::Path::new(p).exists() {
if std::path::Path::new(p).exists() && seen.insert(p.clone()) {
parts.push(p.as_str());
}
}