mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-06 20:02:49 +08:00
- dashboard.js: 版本/状态信息持久化缓存,实例切换时自动清空 (#145) - service.rs: macOS is_cli_installed 改为真实路径探测,无plist时返回默认Gateway条目 (#144) - utils.rs: 新增 common_non_windows_cli_candidates,resolve_openclaw_cli_path 优先检测 standalone/手动安装路径 (#144) - config.rs: macOS get_local_version 增加 resolve_cli + canonicalize,detect_installed_source 增加 CLI 分类 + standalone 检测 (#144) - dev-api.js: macOS CLI/版本/来源检测补齐 Intel Homebrew + standalone + findOpenclawBin (#144) - main.js: 更新 banner dismiss 从 sessionStorage 改 localStorage (#146) - assistant.js: Web 模式 AI 测试走后端代理 api.testModel 绕过 CORS (#148)
This commit is contained in:
@@ -425,11 +425,30 @@ function buildGitInstallEnv() {
|
||||
|
||||
function detectInstalledSource() {
|
||||
if (isMac) {
|
||||
// ARM Homebrew
|
||||
try {
|
||||
const target = fs.readlinkSync('/opt/homebrew/bin/openclaw')
|
||||
if (String(target).includes('openclaw-zh')) return 'chinese'
|
||||
return 'official'
|
||||
} catch {}
|
||||
// Intel Homebrew
|
||||
try {
|
||||
const target = fs.readlinkSync('/usr/local/bin/openclaw')
|
||||
if (String(target).includes('openclaw-zh')) return 'chinese'
|
||||
return 'official'
|
||||
} catch {}
|
||||
// standalone (~/.openclaw-bin)
|
||||
const saDir = path.join(homedir(), '.openclaw-bin')
|
||||
if (fs.existsSync(path.join(saDir, 'openclaw')) || fs.existsSync(path.join(saDir, 'VERSION'))) return 'chinese'
|
||||
if (fs.existsSync('/opt/openclaw/openclaw')) return 'chinese'
|
||||
// findOpenclawBin fallback
|
||||
const bin = findOpenclawBin()
|
||||
if (bin) {
|
||||
const lower = bin.replace(/\\/g, '/').toLowerCase()
|
||||
if (lower.includes('openclaw-zh') || lower.includes('@qingchencloud') || lower.includes('/openclaw-bin/') || lower.includes('/opt/openclaw/')) return 'chinese'
|
||||
return 'official'
|
||||
}
|
||||
return 'official'
|
||||
}
|
||||
if (isWindows) {
|
||||
try {
|
||||
@@ -452,11 +471,34 @@ function detectInstalledSource() {
|
||||
function getLocalOpenclawVersion() {
|
||||
let current = null
|
||||
if (isMac) {
|
||||
// ARM Homebrew
|
||||
try {
|
||||
const target = fs.readlinkSync('/opt/homebrew/bin/openclaw')
|
||||
const pkgPath = path.resolve('/opt/homebrew/bin', target, '..', 'package.json')
|
||||
current = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version
|
||||
} catch {}
|
||||
// Intel Homebrew
|
||||
if (!current) {
|
||||
try {
|
||||
const target = fs.readlinkSync('/usr/local/bin/openclaw')
|
||||
const pkgPath = path.resolve('/usr/local/bin', target, '..', 'package.json')
|
||||
current = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version
|
||||
} catch {}
|
||||
}
|
||||
// standalone (~/.openclaw-bin)
|
||||
if (!current) {
|
||||
try {
|
||||
const vf = path.join(homedir(), '.openclaw-bin', 'VERSION')
|
||||
if (fs.existsSync(vf)) {
|
||||
const lines = fs.readFileSync(vf, 'utf8').split('\n')
|
||||
for (const l of lines) { if (l.startsWith('openclaw_version=')) { current = l.split('=')[1]?.trim(); break } }
|
||||
}
|
||||
if (!current) {
|
||||
const pkg = path.join(homedir(), '.openclaw-bin', 'node_modules', '@qingchencloud', 'openclaw-zh', 'package.json')
|
||||
if (fs.existsSync(pkg)) current = JSON.parse(fs.readFileSync(pkg, 'utf8')).version
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
if (!current && isWindows) {
|
||||
try {
|
||||
@@ -1530,7 +1572,11 @@ const handlers = {
|
||||
|
||||
let cliInstalled = false
|
||||
if (isMac) {
|
||||
cliInstalled = fs.existsSync('/opt/homebrew/bin/openclaw') || fs.existsSync('/usr/local/bin/openclaw')
|
||||
cliInstalled = fs.existsSync('/opt/homebrew/bin/openclaw')
|
||||
|| fs.existsSync('/usr/local/bin/openclaw')
|
||||
|| fs.existsSync(path.join(homedir(), '.openclaw-bin', 'openclaw'))
|
||||
|| fs.existsSync('/opt/openclaw/openclaw')
|
||||
|| !!findOpenclawBin()
|
||||
} else if (isWindows) {
|
||||
try {
|
||||
const paths = [
|
||||
|
||||
@@ -1158,10 +1158,19 @@ pub fn write_mcp_config(config: Value) -> Result<(), String> {
|
||||
/// macOS: 优先从 npm 包的 package.json 读取(含完整后缀),fallback 到 CLI
|
||||
/// Windows/Linux: 优先读文件系统,fallback 到 CLI
|
||||
async fn get_local_version() -> Option<String> {
|
||||
// macOS: 通过 symlink 找到包目录,读 package.json 的 version
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// 兼容 ARM (/opt/homebrew) 和 Intel (/usr/local) 两种 Homebrew 安装路径
|
||||
if let Some(cli_path) = crate::utils::resolve_openclaw_cli_path() {
|
||||
let resolved = std::fs::canonicalize(&cli_path)
|
||||
.ok()
|
||||
.unwrap_or_else(|| PathBuf::from(&cli_path));
|
||||
if let Some(ver) = read_version_from_installation(&resolved)
|
||||
.or_else(|| read_version_from_installation(std::path::Path::new(&cli_path)))
|
||||
{
|
||||
return Some(ver);
|
||||
}
|
||||
}
|
||||
|
||||
for brew_prefix in &["/opt/homebrew/bin", "/usr/local/bin"] {
|
||||
let openclaw_path = format!("{}/openclaw", brew_prefix);
|
||||
if let Ok(target) = fs::read_link(&openclaw_path) {
|
||||
@@ -1182,10 +1191,9 @@ async fn get_local_version() -> Option<String> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Windows: 先查 standalone 安装,再查 npm 全局目录
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// 检查所有 standalone 安装目录
|
||||
for sa_dir in all_standalone_dirs() {
|
||||
let version_file = sa_dir.join("VERSION");
|
||||
if let Ok(content) = fs::read_to_string(&version_file) {
|
||||
@@ -1212,7 +1220,7 @@ async fn get_local_version() -> Option<String> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// npm 全局目录 — 根据 CLI 来源决定检查顺序,避免读到非活跃包的版本
|
||||
|
||||
if let Ok(appdata) = std::env::var("APPDATA") {
|
||||
let cli_is_zh = crate::utils::resolve_openclaw_cli_path()
|
||||
.map(|p| crate::utils::classify_cli_source(&p) == "npm-zh")
|
||||
@@ -1239,7 +1247,8 @@ async fn get_local_version() -> Option<String> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 所有平台通用 fallback: CLI 输出(异步)
|
||||
|
||||
// 所有平台通用 fallback: CLI 输出
|
||||
// Windows: 先确认 openclaw 不是第三方程序(如 CherryStudio)
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@@ -1259,6 +1268,7 @@ async fn get_local_version() -> Option<String> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::utils::openclaw_command_async;
|
||||
let output = openclaw_command_async()
|
||||
.arg("--version")
|
||||
@@ -1296,6 +1306,18 @@ fn detect_installed_source() -> String {
|
||||
// macOS: 检查 openclaw bin 的 symlink 指向
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Some(cli_path) = crate::utils::resolve_openclaw_cli_path() {
|
||||
let resolved = std::fs::canonicalize(&cli_path)
|
||||
.ok()
|
||||
.unwrap_or_else(|| PathBuf::from(&cli_path));
|
||||
let source = crate::utils::classify_cli_source(&resolved.to_string_lossy());
|
||||
if source == "npm-zh" || source == "standalone" {
|
||||
return "chinese".into();
|
||||
}
|
||||
if source == "npm-official" || source == "npm-global" {
|
||||
return "official".into();
|
||||
}
|
||||
}
|
||||
// 兼容 ARM (/opt/homebrew) 和 Intel (/usr/local) 两种 Homebrew 路径
|
||||
for brew_prefix in &["/opt/homebrew/bin/openclaw", "/usr/local/bin/openclaw"] {
|
||||
if let Ok(target) = std::fs::read_link(brew_prefix) {
|
||||
@@ -1305,6 +1327,11 @@ fn detect_installed_source() -> String {
|
||||
return "official".into();
|
||||
}
|
||||
}
|
||||
for sa_dir in all_standalone_dirs() {
|
||||
if sa_dir.join("openclaw").exists() || sa_dir.join("VERSION").exists() {
|
||||
return "chinese".into();
|
||||
}
|
||||
}
|
||||
"official".into()
|
||||
}
|
||||
// Windows: 优先通过 CLI 路径判断,fallback 到文件系统检测
|
||||
|
||||
@@ -322,13 +322,26 @@ pub fn guardian_status() -> Result<GuardianStatus, String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
mod platform {
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
const OPENCLAW_PREFIXES: &[&str] = &["ai.openclaw."];
|
||||
|
||||
/// macOS 上 CLI 是否安装(检查 plist 是否存在即可)
|
||||
fn common_cli_candidates() -> Vec<PathBuf> {
|
||||
let mut candidates = Vec::new();
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
candidates.push(home.join(".openclaw-bin").join("openclaw"));
|
||||
}
|
||||
candidates.push(PathBuf::from("/opt/openclaw/openclaw"));
|
||||
candidates.push(PathBuf::from("/opt/homebrew/bin/openclaw"));
|
||||
candidates.push(PathBuf::from("/usr/local/bin/openclaw"));
|
||||
candidates
|
||||
}
|
||||
|
||||
/// macOS 上 CLI 是否安装(兼容手动安装 / standalone / Homebrew)
|
||||
pub fn is_cli_installed() -> bool {
|
||||
true // macOS 通过 plist 扫描,不依赖 CLI 检测
|
||||
crate::utils::resolve_openclaw_cli_path().is_some()
|
||||
|| common_cli_candidates().into_iter().any(|p| p.exists())
|
||||
}
|
||||
|
||||
pub fn current_uid() -> Result<u32, String> {
|
||||
@@ -361,6 +374,9 @@ mod platform {
|
||||
}
|
||||
}
|
||||
labels.sort();
|
||||
if labels.is_empty() {
|
||||
labels.push("ai.openclaw.gateway".to_string());
|
||||
}
|
||||
labels
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,20 @@ fn find_openclaw_cmd() -> Option<std::path::PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn common_non_windows_cli_candidates() -> Vec<std::path::PathBuf> {
|
||||
let mut candidates = Vec::new();
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
candidates.push(home.join(".openclaw-bin").join("openclaw"));
|
||||
candidates.push(home.join(".local").join("bin").join("openclaw"));
|
||||
}
|
||||
candidates.push(std::path::PathBuf::from("/opt/openclaw/openclaw"));
|
||||
candidates.push(std::path::PathBuf::from("/opt/homebrew/bin/openclaw"));
|
||||
candidates.push(std::path::PathBuf::from("/usr/local/bin/openclaw"));
|
||||
candidates.push(std::path::PathBuf::from("/usr/bin/openclaw"));
|
||||
candidates
|
||||
}
|
||||
|
||||
/// 解析当前实际使用的 openclaw CLI 完整路径(跨平台)
|
||||
pub fn resolve_openclaw_cli_path() -> Option<String> {
|
||||
// 优先使用用户绑定的路径
|
||||
@@ -54,6 +68,11 @@ pub fn resolve_openclaw_cli_path() -> Option<String> {
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
for candidate in common_non_windows_cli_candidates() {
|
||||
if candidate.exists() {
|
||||
return Some(candidate.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
let path = crate::commands::enhanced_path();
|
||||
let sep = ':';
|
||||
for dir in path.split(sep) {
|
||||
|
||||
@@ -639,7 +639,7 @@ async function checkGlobalUpdate() {
|
||||
if (!ver) return
|
||||
|
||||
// 用户已忽略过该版本,不再打扰
|
||||
const dismissed = sessionStorage.getItem('clawpanel_update_dismissed')
|
||||
const dismissed = localStorage.getItem('clawpanel_update_dismissed')
|
||||
if (dismissed === ver) return
|
||||
|
||||
const changelog = info.manifest?.changelog || ''
|
||||
@@ -665,7 +665,7 @@ async function checkGlobalUpdate() {
|
||||
|
||||
// 关闭按钮:记住忽略的版本
|
||||
banner.querySelector('#btn-update-dismiss')?.addEventListener('click', () => {
|
||||
sessionStorage.setItem('clawpanel_update_dismissed', ver)
|
||||
localStorage.setItem('clawpanel_update_dismissed', ver)
|
||||
banner.classList.add('update-banner-hidden')
|
||||
})
|
||||
|
||||
|
||||
@@ -3168,6 +3168,22 @@ function showSettings() {
|
||||
btn.disabled = true
|
||||
btn.textContent = t('assistant.testing')
|
||||
resultEl.innerHTML = '<span style="color:var(--text-tertiary)">' + t('assistant.testSending') + '</span>'
|
||||
|
||||
// Web 模式下浏览器 fetch 受 CORS 限制,优先走后端代理
|
||||
if (!window.__TAURI_INTERNALS__) {
|
||||
const t0 = Date.now()
|
||||
try {
|
||||
const reply = await api.testModel(baseUrl, apiKey, model, selApiType)
|
||||
const elapsed = Date.now() - t0
|
||||
resultEl.innerHTML = buildTestResult({ success: true, elapsed, usedApi: selApiType, reqUrl: baseUrl, reqBody: {}, respStatus: 200, respBody: '', reply: reply || '(ok)' })
|
||||
} catch (err) {
|
||||
const elapsed = Date.now() - t0
|
||||
resultEl.innerHTML = buildTestResult({ success: false, elapsed, usedApi: selApiType, reqUrl: baseUrl, reqBody: {}, respStatus: 0, respBody: '', error: err.message || String(err) })
|
||||
}
|
||||
btn.disabled = false; btn.textContent = t('assistant.testBtn')
|
||||
return
|
||||
}
|
||||
|
||||
const base = cleanBaseUrl(baseUrl, selApiType)
|
||||
const hdrs = authHeaders(selApiType, apiKey)
|
||||
const t0 = Date.now()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { api } from '../lib/tauri-api.js'
|
||||
import { toast } from '../components/toast.js'
|
||||
import { onGatewayChange } from '../lib/app-state.js'
|
||||
import { getActiveInstance, onGatewayChange } from '../lib/app-state.js'
|
||||
import { navigate } from '../router.js'
|
||||
import { t } from '../lib/i18n.js'
|
||||
|
||||
@@ -65,8 +65,22 @@ export function cleanup() {
|
||||
}
|
||||
|
||||
let _dashboardInitialized = false
|
||||
let _dashboardVersionCache = null
|
||||
let _dashboardStatusSummaryCache = null
|
||||
let _dashboardInstanceId = ''
|
||||
|
||||
function syncDashboardInstanceScope() {
|
||||
const instanceId = getActiveInstance()?.id || 'local'
|
||||
if (_dashboardInstanceId && _dashboardInstanceId !== instanceId) {
|
||||
_dashboardInitialized = false
|
||||
_dashboardVersionCache = null
|
||||
_dashboardStatusSummaryCache = null
|
||||
}
|
||||
_dashboardInstanceId = instanceId
|
||||
}
|
||||
|
||||
async function loadDashboardData(page, fullRefresh = false) {
|
||||
syncDashboardInstanceScope()
|
||||
// 分波加载:关键数据先渲染,次要数据后填充,减少白屏等待
|
||||
// 轻量调用(读文件)每次都做;重量调用(spawn CLI/网络请求)只在首次或手动刷新时做
|
||||
const withTimeout = (promise, ms) => Promise.race([
|
||||
@@ -77,21 +91,23 @@ async function loadDashboardData(page, fullRefresh = false) {
|
||||
api.getServicesStatus(),
|
||||
api.readOpenclawConfig(),
|
||||
// 版本信息:首次加载或手动刷新时才查询(避免 ARM 设备上频繁查 npm registry)
|
||||
(!_dashboardInitialized || fullRefresh) ? api.getVersionInfo() : Promise.resolve(null),
|
||||
(!_dashboardInitialized || fullRefresh || !_dashboardVersionCache) ? api.getVersionInfo() : Promise.resolve(_dashboardVersionCache),
|
||||
]), 15000)
|
||||
const secondaryP = withTimeout(Promise.allSettled([
|
||||
api.listAgents(),
|
||||
api.readMcpConfig(),
|
||||
api.listBackups(),
|
||||
// getStatusSummary 是最重的调用(spawn openclaw status --json),只在首次加载时调用
|
||||
(!_dashboardInitialized || fullRefresh) ? api.getStatusSummary() : Promise.resolve(null),
|
||||
(!_dashboardInitialized || fullRefresh || !_dashboardStatusSummaryCache) ? api.getStatusSummary() : Promise.resolve(_dashboardStatusSummaryCache),
|
||||
]), 15000).catch(() => [{ status: 'rejected' }, { status: 'rejected' }, { status: 'rejected' }, { status: 'rejected' }])
|
||||
const logsP = api.readLogTail('gateway', 20).catch(() => '')
|
||||
|
||||
// 第一波:服务状态 + 配置 + 版本 → 立即渲染统计卡片
|
||||
const [servicesRes, configRes, versionRes] = await coreP
|
||||
const services = servicesRes.status === 'fulfilled' ? servicesRes.value : []
|
||||
const version = (versionRes.status === 'fulfilled' && versionRes.value) ? versionRes.value : {}
|
||||
const version = (versionRes.status === 'fulfilled' && versionRes.value)
|
||||
? (_dashboardVersionCache = versionRes.value)
|
||||
: (_dashboardVersionCache || {})
|
||||
const config = configRes.status === 'fulfilled' ? configRes.value : null
|
||||
if (servicesRes.status === 'rejected') toast(t('dashboard.servicesLoadFail'), 'error')
|
||||
if (versionRes.status === 'rejected') toast(t('dashboard.versionLoadFail'), 'error')
|
||||
@@ -128,7 +144,9 @@ async function loadDashboardData(page, fullRefresh = false) {
|
||||
const agents = agentsRes.status === 'fulfilled' ? agentsRes.value : []
|
||||
const mcpConfig = mcpRes.status === 'fulfilled' ? mcpRes.value : null
|
||||
const backups = backupsRes.status === 'fulfilled' ? backupsRes.value : []
|
||||
const statusSummary = statusRes.status === 'fulfilled' ? statusRes.value : null
|
||||
const statusSummary = (statusRes.status === 'fulfilled' && statusRes.value)
|
||||
? (_dashboardStatusSummaryCache = statusRes.value)
|
||||
: _dashboardStatusSummaryCache
|
||||
|
||||
renderStatCards(page, services, version, agents, config)
|
||||
renderOverview(page, services, mcpConfig, backups, config, agents, statusSummary)
|
||||
@@ -475,6 +493,7 @@ function bindActions(page) {
|
||||
btnUpdate.textContent = t('dashboard.checking')
|
||||
try {
|
||||
const info = await api.getVersionInfo()
|
||||
_dashboardVersionCache = info
|
||||
if (info.ahead_of_recommended && info.recommended) {
|
||||
toast(t('dashboard.versionAheadWarn', { current: info.current || '', recommended: info.recommended }), 'warning')
|
||||
} else if (info.update_available && info.recommended) {
|
||||
|
||||
Reference in New Issue
Block a user