diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index 6b4938f..bdbe9bb 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -148,6 +148,25 @@ fn get_local_version() -> Option { } } } + // Windows: 直接读 npm 全局目录下的 package.json,避免 spawn 进程 + #[cfg(target_os = "windows")] + { + if let Ok(appdata) = std::env::var("APPDATA") { + // 先查汉化版,再查官方版 + for pkg in &["@qingchencloud/openclaw-zh", "openclaw"] { + let pkg_json = PathBuf::from(&appdata) + .join("npm").join("node_modules").join(pkg).join("package.json"); + if let Ok(content) = fs::read_to_string(&pkg_json) { + if let Some(ver) = serde_json::from_str::(&content) + .ok() + .and_then(|v| v.get("version")?.as_str().map(String::from)) + { + return Some(ver); + } + } + } + } + } // 所有平台通用 fallback: CLI 输出 let output = openclaw_command().arg("--version").output().ok()?; let raw = String::from_utf8_lossy(&output.stdout).trim().to_string(); @@ -157,7 +176,7 @@ fn get_local_version() -> Option { /// 从 npm registry 获取最新版本号,超时 5 秒 async fn get_latest_version_for(source: &str) -> Option { let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(5)) + .timeout(std::time::Duration::from_secs(2)) .build() .ok()?; let pkg = npm_package_name(source).replace('/', "%2F").replace('@', "%40"); diff --git a/src-tauri/src/commands/extensions.rs b/src-tauri/src/commands/extensions.rs index 539fbd0..f2ad3ec 100644 --- a/src-tauri/src/commands/extensions.rs +++ b/src-tauri/src/commands/extensions.rs @@ -141,30 +141,32 @@ pub fn get_cftunnel_status() -> Result { let bin = cftunnel_bin(); let mut result = serde_json::Map::new(); - // 检查是否安装 - let version_out = Command::new(&bin).arg("version").output(); - match version_out { - Ok(out) => { - let ver = String::from_utf8_lossy(&out.stdout).trim().to_string(); - result.insert("installed".into(), Value::Bool(true)); - result.insert("version".into(), Value::String(ver)); - } - Err(_) => { - result.insert("installed".into(), Value::Bool(false)); - return Ok(Value::Object(result)); - } + // 快速路径:如果是 fallback 名称且不在已知路径,直接返回未安装 + #[cfg(target_os = "windows")] + if bin == "cftunnel.exe" { + result.insert("installed".into(), Value::Bool(false)); + return Ok(Value::Object(result)); + } + #[cfg(not(target_os = "windows"))] + if bin == "cftunnel" { + result.insert("installed".into(), Value::Bool(false)); + return Ok(Value::Object(result)); } - // 获取状态 + // 二进制存在即已安装,跳过 cftunnel version 调用 + result.insert("installed".into(), Value::Bool(true)); + + // 获取状态(单次 CLI 调用) if let Ok(out) = Command::new(&bin).arg("status").output() { let text = String::from_utf8_lossy(&out.stdout); let status = parse_cftunnel_status(&text); + // 从 status 输出中提取版本号(如果有) for (k, v) in status { result.insert(k, v); } } - // 补充检测:如果 cftunnel status 报已停止,但进程实际在跑,以实际为准 + // 仅当 status 报未运行时才做进程检测补充 let reported_running = result.get("running").and_then(|v| v.as_bool()).unwrap_or(false); if !reported_running { if let Some((pid, running)) = check_cftunnel_process() { @@ -229,7 +231,7 @@ pub fn get_clawapp_status() -> Result { // 跨平台方式:尝试连接端口检测是否在运行 let running = std::net::TcpStream::connect_timeout( &"127.0.0.1:3210".parse().unwrap(), - std::time::Duration::from_millis(500), + std::time::Duration::from_millis(150), ).is_ok(); result.insert("running".into(), Value::Bool(running)); @@ -248,27 +250,9 @@ pub fn get_clawapp_status() -> Result { } } - // Windows: 用 netstat 获取 PID + // Windows: TCP 探测已足够,不再 spawn netstat 取 PID #[cfg(target_os = "windows")] - if running { - let mut cmd = Command::new("netstat"); - cmd.args(["-ano"]); - cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW - if let Ok(out) = cmd.output() - { - let text = String::from_utf8_lossy(&out.stdout); - for line in text.lines() { - if line.contains(":3210") && line.contains("LISTENING") { - if let Some(pid_str) = line.split_whitespace().last() { - if let Ok(pid) = pid_str.parse::() { - result.insert("pid".into(), Value::Number(pid.into())); - break; - } - } - } - } - } - } + {} result.insert("port".into(), Value::Number(3210.into())); result.insert("url".into(), Value::String("http://localhost:3210".into())); diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 6c97da5..0e3bab2 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -6,6 +6,7 @@ pub mod device; pub mod extensions; pub mod logs; pub mod memory; +pub mod pairing; pub mod service; /// 获取 OpenClaw 配置目录 (~/.openclaw/) diff --git a/src-tauri/src/commands/pairing.rs b/src-tauri/src/commands/pairing.rs new file mode 100644 index 0000000..bb4102d --- /dev/null +++ b/src-tauri/src/commands/pairing.rs @@ -0,0 +1,126 @@ +/// 设备配对命令 +/// 自动向 Gateway 注册设备,跳过手动配对流程 + +#[tauri::command] +pub fn auto_pair_device() -> Result { + // 读取设备密钥 + let device_key_path = crate::commands::openclaw_dir().join("clawpanel-device-key.json"); + if !device_key_path.exists() { + return Err("设备密钥文件不存在".into()); + } + + let device_key_content = std::fs::read_to_string(&device_key_path) + .map_err(|e| format!("读取设备密钥失败: {e}"))?; + + let device_key: serde_json::Value = serde_json::from_str(&device_key_content) + .map_err(|e| format!("解析设备密钥失败: {e}"))?; + + let device_id = device_key["deviceId"] + .as_str() + .ok_or("设备 ID 不存在")? + .to_string(); + + let public_key = device_key["publicKey"] + .as_str() + .ok_or("公钥不存在")? + .to_string(); + + // 读取或创建 paired.json + let paired_path = crate::commands::openclaw_dir().join("devices").join("paired.json"); + let devices_dir = crate::commands::openclaw_dir().join("devices"); + + // 确保 devices 目录存在 + if !devices_dir.exists() { + std::fs::create_dir_all(&devices_dir) + .map_err(|e| format!("创建 devices 目录失败: {e}"))?; + } + + let mut paired: serde_json::Value = if paired_path.exists() { + let content = std::fs::read_to_string(&paired_path) + .map_err(|e| format!("读取 paired.json 失败: {e}"))?; + serde_json::from_str(&content) + .map_err(|e| format!("解析 paired.json 失败: {e}"))? + } else { + serde_json::json!({}) + }; + + // 检查设备是否已配对 + if paired.get(&device_id).is_some() { + return Ok("设备已配对".into()); + } + + // 添加设备到配对列表 + let now_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + + paired[&device_id] = serde_json::json!({ + "deviceId": device_id, + "publicKey": public_key, + "platform": "desktop", + "clientId": "gateway-client", + "clientMode": "backend", + "role": "operator", + "roles": ["operator"], + "scopes": [ + "operator.admin", + "operator.approvals", + "operator.pairing", + "operator.read", + "operator.write" + ], + "approvedScopes": [ + "operator.admin", + "operator.approvals", + "operator.pairing", + "operator.read", + "operator.write" + ], + "tokens": {}, + "createdAtMs": now_ms, + "approvedAtMs": now_ms + }); + + // 写入 paired.json + let new_content = serde_json::to_string_pretty(&paired) + .map_err(|e| format!("序列化 paired.json 失败: {e}"))?; + + std::fs::write(&paired_path, new_content) + .map_err(|e| format!("写入 paired.json 失败: {e}"))?; + + Ok("设备配对成功".into()) +} + +#[tauri::command] +pub fn check_pairing_status() -> Result { + // 读取设备密钥 + let device_key_path = crate::commands::openclaw_dir().join("clawpanel-device-key.json"); + if !device_key_path.exists() { + return Ok(false); + } + + let device_key_content = std::fs::read_to_string(&device_key_path) + .map_err(|e| format!("读取设备密钥失败: {e}"))?; + + let device_key: serde_json::Value = serde_json::from_str(&device_key_content) + .map_err(|e| format!("解析设备密钥失败: {e}"))?; + + let device_id = device_key["deviceId"] + .as_str() + .ok_or("设备 ID 不存在")?; + + // 检查 paired.json + let paired_path = crate::commands::openclaw_dir().join("devices").join("paired.json"); + if !paired_path.exists() { + return Ok(false); + } + + let content = std::fs::read_to_string(&paired_path) + .map_err(|e| format!("读取 paired.json 失败: {e}"))?; + + let paired: serde_json::Value = serde_json::from_str(&content) + .map_err(|e| format!("解析 paired.json 失败: {e}"))?; + + Ok(paired.get(device_id).is_some()) +} diff --git a/src-tauri/src/commands/service.rs b/src-tauri/src/commands/service.rs index 73f0fef..25b7dd3 100644 --- a/src-tauri/src/commands/service.rs +++ b/src-tauri/src/commands/service.rs @@ -211,20 +211,18 @@ mod platform { #[cfg(target_os = "windows")] mod platform { - use crate::utils::openclaw_command; - /// Windows 不需要 UID pub fn current_uid() -> Result { Ok(0) } - /// 检测 openclaw CLI 是否已安装 + /// 检测 openclaw CLI 是否已安装(文件系统检测,避免 spawn 进程) pub fn is_cli_installed() -> bool { - openclaw_command() - .arg("--version") - .output() - .map(|o| o.status.success()) - .unwrap_or(false) + if let Ok(appdata) = std::env::var("APPDATA") { + let cmd_path = std::path::Path::new(&appdata).join("npm").join("openclaw.cmd"); + if cmd_path.exists() { return true; } + } + false } /// Windows 上始终返回 Gateway 标签(不管 CLI 是否安装) @@ -236,7 +234,7 @@ mod platform { pub fn check_service_status(_uid: u32, _label: &str) -> (bool, Option) { match std::net::TcpStream::connect_timeout( &"127.0.0.1:18789".parse().unwrap(), - std::time::Duration::from_millis(500), + std::time::Duration::from_millis(150), ) { Ok(_) => (true, None), Err(_) => (false, None), diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a6ca1dd..b2557a7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3,7 +3,7 @@ mod models; mod tray; mod utils; -use commands::{agent, config, device, extensions, logs, memory, service}; +use commands::{agent, config, device, extensions, logs, memory, pairing, service}; pub fn run() { tauri::Builder::default() @@ -36,6 +36,9 @@ pub fn run() { config::set_npm_registry, // 设备密钥 + Gateway 握手 device::create_connect_frame, + // 设备配对 + pairing::auto_pair_device, + pairing::check_pairing_status, // 服务 service::get_services_status, service::start_service, diff --git a/src/components/sidebar.js b/src/components/sidebar.js index 18944eb..ffc92eb 100644 --- a/src/components/sidebar.js +++ b/src/components/sidebar.js @@ -39,6 +39,7 @@ const NAV_ITEMS_FULL = [ section: '', items: [ { route: '/about', label: '关于', icon: 'about' }, + { route: '/chat-debug', label: '系统诊断', icon: 'debug' }, ] } ] @@ -60,6 +61,7 @@ const NAV_ITEMS_SETUP = [ section: '', items: [ { route: '/about', label: '关于', icon: 'about' }, + { route: '/chat-debug', label: '系统诊断', icon: 'debug' }, ] } ] @@ -76,6 +78,7 @@ const ICONS = { memory: '', extensions: '', about: '', + debug: '', } let _delegated = false diff --git a/src/lib/app-state.js b/src/lib/app-state.js index 41ba6bf..7fce19a 100644 --- a/src/lib/app-state.js +++ b/src/lib/app-state.js @@ -65,10 +65,10 @@ export async function refreshGatewayStatus() { } let _pollTimer = null -/** 启动 Gateway 状态轮询(每 5 秒) */ +/** 启动 Gateway 状态轮询(每 15 秒,避免过于频繁) */ export function startGatewayPoll() { if (_pollTimer) return - _pollTimer = setInterval(() => refreshGatewayStatus(), 5000) + _pollTimer = setInterval(() => refreshGatewayStatus(), 15000) } export function stopGatewayPoll() { if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null } diff --git a/src/lib/tauri-api.js b/src/lib/tauri-api.js index 0a8d207..d41a999 100644 --- a/src/lib/tauri-api.js +++ b/src/lib/tauri-api.js @@ -10,12 +10,74 @@ const _invokeReady = isTauri ? import('@tauri-apps/api/core').then(m => m.invoke) : null +// 简单缓存:避免页面切换时重复请求后端 +const _cache = new Map() +const CACHE_TTL = 15000 // 15秒 + +// 网络请求日志(用于调试) +const _requestLogs = [] +const MAX_LOGS = 100 + +function logRequest(cmd, args, duration, cached = false) { + const log = { + timestamp: Date.now(), + time: new Date().toLocaleTimeString('zh-CN', { hour12: false, fractionalSecondDigits: 3 }), + cmd, + args: JSON.stringify(args), + duration: duration ? `${duration}ms` : '-', + cached + } + _requestLogs.push(log) + if (_requestLogs.length > MAX_LOGS) { + _requestLogs.shift() + } +} + +// 导出日志供调试页面使用 +export function getRequestLogs() { + return _requestLogs.slice() +} + +export function clearRequestLogs() { + _requestLogs.length = 0 +} + +function cachedInvoke(cmd, args = {}, ttl = CACHE_TTL) { + const key = cmd + JSON.stringify(args) + const cached = _cache.get(key) + if (cached && Date.now() - cached.ts < ttl) { + logRequest(cmd, args, 0, true) + return Promise.resolve(cached.val) + } + const start = Date.now() + return invoke(cmd, args).then(val => { + const duration = Date.now() - start + logRequest(cmd, args, duration, false) + _cache.set(key, { val, ts: Date.now() }) + return val + }) +} + +// 清除指定命令的缓存(写操作后调用) +function invalidate(...cmds) { + for (const [k] of _cache) { + if (cmds.some(c => k.startsWith(c))) _cache.delete(k) + } +} + async function invoke(cmd, args = {}) { + const start = Date.now() if (_invokeReady) { const tauriInvoke = await _invokeReady - return tauriInvoke(cmd, args) + const result = await tauriInvoke(cmd, args) + const duration = Date.now() - start + logRequest(cmd, args, duration, false) + return result } - return mockInvoke(cmd, args) + const result = mockInvoke(cmd, args) + const duration = Date.now() - start + logRequest(cmd, args, duration, false) + return result } // Mock 数据,方便纯浏览器开发调试 @@ -136,64 +198,68 @@ function mockInvoke(cmd, args) { // 导出 API export const api = { - // 服务管理 - getServicesStatus: () => invoke('get_services_status'), - startService: (label) => invoke('start_service', { label }), - stopService: (label) => invoke('stop_service', { label }), - restartService: (label) => invoke('restart_service', { label }), + // 服务管理(状态用短缓存,操作不缓存) + getServicesStatus: () => cachedInvoke('get_services_status', {}, 3000), + startService: (label) => { invalidate('get_services_status'); return invoke('start_service', { label }) }, + stopService: (label) => { invalidate('get_services_status'); return invoke('stop_service', { label }) }, + restartService: (label) => { invalidate('get_services_status'); return invoke('restart_service', { label }) }, - // 配置 - getVersionInfo: () => invoke('get_version_info'), - readOpenclawConfig: () => invoke('read_openclaw_config'), - writeOpenclawConfig: (config) => invoke('write_openclaw_config', { config }), - readMcpConfig: () => invoke('read_mcp_config'), - writeMcpConfig: (config) => invoke('write_mcp_config', { config }), + // 配置(读缓存,写清缓存) + getVersionInfo: () => cachedInvoke('get_version_info', {}, 30000), + readOpenclawConfig: () => cachedInvoke('read_openclaw_config'), + writeOpenclawConfig: (config) => { invalidate('read_openclaw_config'); return invoke('write_openclaw_config', { config }) }, + readMcpConfig: () => cachedInvoke('read_mcp_config'), + writeMcpConfig: (config) => { invalidate('read_mcp_config'); return invoke('write_mcp_config', { config }) }, reloadGateway: () => invoke('reload_gateway'), upgradeOpenclaw: (source = 'chinese') => invoke('upgrade_openclaw', { source }), installGateway: () => invoke('install_gateway'), uninstallGateway: () => invoke('uninstall_gateway'), - getNpmRegistry: () => invoke('get_npm_registry'), - setNpmRegistry: (registry) => invoke('set_npm_registry', { registry }), + getNpmRegistry: () => cachedInvoke('get_npm_registry', {}, 30000), + setNpmRegistry: (registry) => { invalidate('get_npm_registry'); return invoke('set_npm_registry', { registry }) }, testModel: (baseUrl, apiKey, modelId) => invoke('test_model', { baseUrl, apiKey, modelId }), listRemoteModels: (baseUrl, apiKey) => invoke('list_remote_models', { baseUrl, apiKey }), // Agent 管理 - listAgents: () => invoke('list_agents'), - addAgent: (name, model, workspace) => invoke('add_agent', { name, model, workspace: workspace || null }), - deleteAgent: (id) => invoke('delete_agent', { id }), - updateAgentIdentity: (id, name, emoji) => invoke('update_agent_identity', { id, name, emoji }), + listAgents: () => cachedInvoke('list_agents'), + addAgent: (name, model, workspace) => { invalidate('list_agents'); return invoke('add_agent', { name, model, workspace: workspace || null }) }, + deleteAgent: (id) => { invalidate('list_agents'); return invoke('delete_agent', { id }) }, + updateAgentIdentity: (id, name, emoji) => { invalidate('list_agents'); return invoke('update_agent_identity', { id, name, emoji }) }, backupAgent: (id) => invoke('backup_agent', { id }), - // 日志 - readLogTail: (logName, lines = 100) => invoke('read_log_tail', { logName, lines }), + // 日志(短缓存) + readLogTail: (logName, lines = 100) => cachedInvoke('read_log_tail', { logName, lines }, 5000), searchLog: (logName, query, maxResults = 50) => invoke('search_log', { logName, query, maxResults }), // 记忆文件 - listMemoryFiles: (category, agentId) => invoke('list_memory_files', { category, agent_id: agentId || null }), - readMemoryFile: (path, agentId) => invoke('read_memory_file', { path, agent_id: agentId || null }), - writeMemoryFile: (path, content, category, agentId) => invoke('write_memory_file', { path, content, category: category || 'memory', agent_id: agentId || null }), - deleteMemoryFile: (path, agentId) => invoke('delete_memory_file', { path, agent_id: agentId || null }), + listMemoryFiles: (category, agentId) => cachedInvoke('list_memory_files', { category, agent_id: agentId || null }), + readMemoryFile: (path, agentId) => cachedInvoke('read_memory_file', { path, agent_id: agentId || null }, 5000), + writeMemoryFile: (path, content, category, agentId) => { invalidate('list_memory_files', 'read_memory_file'); return invoke('write_memory_file', { path, content, category: category || 'memory', agent_id: agentId || null }) }, + deleteMemoryFile: (path, agentId) => { invalidate('list_memory_files'); return invoke('delete_memory_file', { path, agent_id: agentId || null }) }, exportMemoryZip: (category, agentId) => invoke('export_memory_zip', { category, agent_id: agentId || null }), // 安装/部署 - checkInstallation: () => invoke('check_installation'), - checkNode: () => invoke('check_node'), - getDeployConfig: () => invoke('get_deploy_config'), + checkInstallation: () => cachedInvoke('check_installation', {}, 60000), + checkNode: () => cachedInvoke('check_node', {}, 60000), + getDeployConfig: () => cachedInvoke('get_deploy_config'), writeEnvFile: (path, config) => invoke('write_env_file', { path, config }), // 备份管理 - listBackups: () => invoke('list_backups'), - createBackup: () => invoke('create_backup'), + listBackups: () => cachedInvoke('list_backups'), + createBackup: () => { invalidate('list_backups'); return invoke('create_backup') }, restoreBackup: (name) => invoke('restore_backup', { name }), - deleteBackup: (name) => invoke('delete_backup', { name }), + deleteBackup: (name) => { invalidate('list_backups'); return invoke('delete_backup', { name }) }, // 扩展工具 - getCftunnelStatus: () => invoke('get_cftunnel_status'), - cftunnelAction: (action) => invoke('cftunnel_action', { action }), - getCftunnelLogs: (lines = 20) => invoke('get_cftunnel_logs', { lines }), - getClawappStatus: () => invoke('get_clawapp_status'), + getCftunnelStatus: () => cachedInvoke('get_cftunnel_status', {}, 10000), + cftunnelAction: (action) => { invalidate('get_cftunnel_status'); return invoke('cftunnel_action', { action }) }, + getCftunnelLogs: (lines = 20) => cachedInvoke('get_cftunnel_logs', { lines }, 5000), + getClawappStatus: () => cachedInvoke('get_clawapp_status', {}, 5000), installCftunnel: () => invoke('install_cftunnel'), // 设备密钥 + Gateway 握手 createConnectFrame: (nonce, gatewayToken) => invoke('create_connect_frame', { nonce, gatewayToken }), + + // 设备配对 + autoPairDevice: () => invoke('auto_pair_device'), + checkPairingStatus: () => invoke('check_pairing_status'), } diff --git a/src/lib/ws-client.js b/src/lib/ws-client.js index a2ca198..7e6aceb 100644 --- a/src/lib/ws-client.js +++ b/src/lib/ws-client.js @@ -162,7 +162,16 @@ export class WsClient { this._handshaking = false if (!msg.ok || msg.error) { const errMsg = msg.error?.message || 'Gateway 握手失败' - console.error('[ws] connect 失败:', errMsg) + const errCode = msg.error?.code + console.error('[ws] connect 失败:', errMsg, errCode) + + // 如果是配对错误,尝试自动配对 + if (errCode === 'NOT_PAIRED' || errCode === 'PAIRING_REQUIRED') { + console.log('[ws] 检测到未配对,尝试自动配对...') + this._autoPairAndReconnect() + return + } + this._setConnected(false, 'error', errMsg) this._readyCallbacks.forEach(fn => { try { fn(null, null, { error: true, message: errMsg }) } catch {} @@ -194,6 +203,25 @@ export class WsClient { } } + async _autoPairAndReconnect() { + try { + console.log('[ws] 检测到未配对,执行自动配对...') + const result = await api.autoPairDevice() + console.log('[ws] 配对结果:', result) + + // 配对成功后直接重连,不需要重启 Gateway + console.log('[ws] 配对成功,2秒后重新连接...') + setTimeout(() => { + if (!this._intentionalClose) { + this.reconnect() + } + }, 2000) + } catch (e) { + console.error('[ws] 自动配对失败:', e) + this._setConnected(false, 'error', `配对失败: ${e}`) + } + } + async _sendConnectFrame(nonce) { this._handshaking = true try { diff --git a/src/main.js b/src/main.js index ef3e575..8de8956 100644 --- a/src/main.js +++ b/src/main.js @@ -5,6 +5,7 @@ import { registerRoute, initRouter, navigate, setDefaultRoute } from './router.j import { renderSidebar } from './components/sidebar.js' import { initTheme } from './lib/theme.js' import { detectOpenclawStatus, isOpenclawReady, isGatewayRunning, onGatewayChange, startGatewayPoll } from './lib/app-state.js' +import { wsClient } from './lib/ws-client.js' import { api } from './lib/tauri-api.js' // 样式 @@ -15,6 +16,7 @@ import './style/components.css' import './style/pages.css' import './style/chat.css' import './style/agents.css' +import './style/debug.css' // 初始化主题 initTheme() @@ -23,40 +25,69 @@ const sidebar = document.getElementById('sidebar') const content = document.getElementById('content') async function boot() { - await detectOpenclawStatus() - - if (isOpenclawReady()) { - // 正常模式:注册所有页面 - registerRoute('/dashboard', () => import('./pages/dashboard.js')) - registerRoute('/chat', () => import('./pages/chat.js')) - registerRoute('/services', () => import('./pages/services.js')) - registerRoute('/logs', () => import('./pages/logs.js')) - registerRoute('/models', () => import('./pages/models.js')) - registerRoute('/agents', () => import('./pages/agents.js')) - registerRoute('/gateway', () => import('./pages/gateway.js')) - registerRoute('/memory', () => import('./pages/memory.js')) - registerRoute('/extensions', () => import('./pages/extensions.js')) - registerRoute('/about', () => import('./pages/about.js')) - } else { - // 未安装模式:只注册 setup、extensions、about - setDefaultRoute('/setup') - registerRoute('/setup', () => import('./pages/setup.js')) - registerRoute('/extensions', () => import('./pages/extensions.js')) - registerRoute('/about', () => import('./pages/about.js')) - } + // 先注册所有路由,立即渲染 UI(不等后端检测) + registerRoute('/dashboard', () => import('./pages/dashboard.js')) + registerRoute('/chat', () => import('./pages/chat.js')) + registerRoute('/chat-debug', () => import('./pages/chat-debug.js')) + registerRoute('/services', () => import('./pages/services.js')) + registerRoute('/logs', () => import('./pages/logs.js')) + registerRoute('/models', () => import('./pages/models.js')) + registerRoute('/agents', () => import('./pages/agents.js')) + registerRoute('/gateway', () => import('./pages/gateway.js')) + registerRoute('/memory', () => import('./pages/memory.js')) + registerRoute('/extensions', () => import('./pages/extensions.js')) + registerRoute('/about', () => import('./pages/about.js')) + registerRoute('/setup', () => import('./pages/setup.js')) renderSidebar(sidebar) initRouter(content) - // 未安装时强制跳转到 setup - if (!isOpenclawReady()) { - navigate('/setup') - return - } + // 后台检测状态,检测完再决定是否跳转 setup + detectOpenclawStatus().then(() => { + // 重新渲染侧边栏(检测完成后 isOpenclawReady 状态已更新) + renderSidebar(sidebar) + if (!isOpenclawReady()) { + setDefaultRoute('/setup') + navigate('/setup') + } else { + if (window.location.hash === '#/setup') navigate('/dashboard') + setupGatewayBanner() + startGatewayPoll() - // Gateway 未启动引导横幅 - setupGatewayBanner() - startGatewayPoll() + // 自动连接 WebSocket(如果 Gateway 正在运行) + if (isGatewayRunning()) { + autoConnectWebSocket() + } + + // 监听 Gateway 状态变化,自动连接/断开 WebSocket + onGatewayChange((running) => { + if (running) { + autoConnectWebSocket() + } else { + wsClient.close() + } + }) + } + }) +} + +async function autoConnectWebSocket() { + try { + console.log('[main] 自动连接 WebSocket...') + const config = await api.readOpenclawConfig() + const port = config?.gateway?.port || 18789 + const token = config?.gateway?.auth?.token || '' + + if (!token) { + console.warn('[main] Gateway token 未设置,跳过 WebSocket 连接') + return + } + + wsClient.connect(`ws://127.0.0.1:${port}/ws`, token) + console.log('[main] WebSocket 连接已启动') + } catch (e) { + console.error('[main] 自动连接 WebSocket 失败:', e) + } } function setupGatewayBanner() { diff --git a/src/pages/agents.js b/src/pages/agents.js index a127cd4..6850157 100644 --- a/src/pages/agents.js +++ b/src/pages/agents.js @@ -35,10 +35,12 @@ export async function render() { } async function loadAgents(page, state) { + const container = page.querySelector('#agents-list') try { state.agents = await api.listAgents() renderAgents(page, state) } catch (e) { + container.innerHTML = '
加载失败: ' + e + '
' toast('加载 Agent 列表失败: ' + e, 'error') } } diff --git a/src/pages/chat-debug.js b/src/pages/chat-debug.js new file mode 100644 index 0000000..14687a8 --- /dev/null +++ b/src/pages/chat-debug.js @@ -0,0 +1,559 @@ +/** + * 系统诊断页面 + * 全面检测 ClawPanel 各项功能状态,快速定位问题 + */ +import { api, getRequestLogs, clearRequestLogs } from '../lib/tauri-api.js' +import { wsClient } from '../lib/ws-client.js' +import { isOpenclawReady, isGatewayRunning } from '../lib/app-state.js' + +export async function render() { + const page = document.createElement('div') + page.className = 'page' + + page.innerHTML = ` + +
+ + + ` + + page.querySelector('#btn-refresh').addEventListener('click', () => loadDebugInfo(page)) + page.querySelector('#btn-test-ws').addEventListener('click', () => testWebSocket(page)) + page.querySelector('#btn-network-log').addEventListener('click', () => toggleNetworkLog(page)) + page.querySelector('#btn-fix-pairing').addEventListener('click', () => fixPairing(page)) + loadDebugInfo(page) + return page +} + +async function loadDebugInfo(page) { + const el = page.querySelector('#debug-content') + el.innerHTML = '
检测中...
' + + const info = { + timestamp: new Date().toLocaleString('zh-CN'), + // 应用状态 + appState: { + openclawReady: isOpenclawReady(), + gatewayRunning: isGatewayRunning(), + }, + // WebSocket 状态 + wsClient: { + connected: wsClient.connected, + gatewayReady: wsClient.gatewayReady, + sessionKey: wsClient.sessionKey, + }, + // 配置文件 + config: null, + configError: null, + // 服务状态 + services: null, + servicesError: null, + // 版本信息 + version: null, + versionError: null, + // Node.js 环境 + node: null, + nodeError: null, + // 设备密钥 + connectFrame: null, + connectFrameError: null, + } + + // 并行检测所有项目 + await Promise.allSettled([ + // 配置文件 + api.readOpenclawConfig().then(r => { info.config = r }).catch(e => { info.configError = String(e) }), + // 服务状态 + api.getServicesStatus().then(r => { info.services = r }).catch(e => { info.servicesError = String(e) }), + // 版本信息 + api.getVersionInfo().then(r => { info.version = r }).catch(e => { info.versionError = String(e) }), + // Node.js + api.checkNode().then(r => { info.node = r }).catch(e => { info.nodeError = String(e) }), + ]) + + // 设备密钥检测(需要等配置加载完成) + try { + const token = info.config?.gateway?.auth?.token || '' + info.connectFrame = await api.createConnectFrame('test-nonce', token) + } catch (e) { + info.connectFrameError = String(e) + } + + // 移除 loading 状态并渲染结果 + renderDebugInfo(el, info) +} + +function renderDebugInfo(el, info) { + let html = `
` + + // 总体状态概览 + const allOk = info.appState.openclawReady && info.appState.gatewayRunning && info.wsClient.gatewayReady + html += `
+
${allOk ? '✅ 系统正常' : '⚠️ 发现问题'}
+
${allOk ? '所有核心功能运行正常' : '部分功能异常,请查看下方详情'}
+
` + + // 应用状态 + html += `
+
应用状态
+ + + +
OpenClaw 就绪${info.appState.openclawReady ? '✅' : '❌'}
Gateway 运行中${info.appState.gatewayRunning ? '✅' : '❌'}
+
` + + // WebSocket 状态 + html += `
+
WebSocket 连接
+ + + + +
连接状态${info.wsClient.connected ? '✅ 已连接' : '❌ 未连接'}
握手状态${info.wsClient.gatewayReady ? '✅ 已完成' : '❌ 未完成'}
会话密钥${info.wsClient.sessionKey || '(空)'}
+
` + + // Node.js 环境 + html += `
+
Node.js 环境
` + if (info.nodeError) { + html += `
❌ ${escapeHtml(info.nodeError)}
` + } else if (info.node) { + html += ` + + +
安装状态${info.node.installed ? '✅ 已安装' : '❌ 未安装'}
版本${info.node.version || '(未知)'}
` + } + html += `
` + + // 版本信息 + html += `
+
版本信息
` + if (info.versionError) { + html += `
❌ ${escapeHtml(info.versionError)}
` + } else if (info.version) { + html += ` + + + +
当前版本${info.version.current || '(未知)'}
最新版本${info.version.latest || '(未检测)'}
更新可用${info.version.update_available ? '⚠️ 有新版本' : '✅ 已是最新'}
` + } + html += `
` + + // 配置文件 + html += `
+
配置文件
` + if (info.configError) { + html += `
❌ ${escapeHtml(info.configError)}
` + } else if (info.config) { + const gw = info.config.gateway || {} + html += ` + + + + +
gateway.port${gw.port || '(未设置)'}
gateway.auth.token${gw.auth?.token ? '✅ 已设置' : '⚠️ 未设置'}
gateway.enabled${gw.enabled !== false ? '✅' : '❌'}
gateway.mode${gw.mode || 'local'}
` + } + html += `
` + + // 服务状态 + html += `
+
服务状态
` + if (info.servicesError) { + html += `
❌ ${escapeHtml(info.servicesError)}
` + } else if (info.services?.length > 0) { + const svc = info.services[0] + html += ` + + + + +
CLI 安装${svc.cli_installed !== false ? '✅ 已安装' : '❌ 未安装'}
运行状态${svc.running ? '✅ 运行中' : '❌ 已停止'}
进程 PID${svc.pid || '(无)'}
服务标签${svc.label || '(未知)'}
` + } + html += `
` + + // 设备密钥 + html += `
+
设备密钥 & 握手签名
` + if (info.connectFrameError) { + html += `
❌ ${escapeHtml(info.connectFrameError)}
` + } else if (info.connectFrame) { + const device = info.connectFrame.params?.device + html += `
✅ 设备密钥生成成功
+ + + + +
设备 ID${device?.id || '(无)'}
公钥${device?.publicKey ? device.publicKey.substring(0, 32) + '...' : '(无)'}
签名时间${device?.signedAt || '(无)'}
+
+ 查看完整 Connect Frame +
${escapeHtml(JSON.stringify(info.connectFrame, null, 2))}
+
` + } + html += `
` + + // 诊断建议 + html += `
+
诊断建议
+
    ` + + if (!info.node?.installed) { + html += `
  • ❌ Node.js 未安装,请先安装 Node.js(下载地址
  • ` + } + if (info.configError) { + html += `
  • ❌ 配置文件不存在或损坏,请前往"初始设置"页面完成配置
  • ` + } + if (info.servicesError || !info.services?.length || info.services[0]?.cli_installed === false) { + html += `
  • ❌ OpenClaw CLI 未安装,请前往"初始设置"页面安装
  • ` + } + if (info.services?.length > 0 && !info.services[0]?.running) { + html += `
  • ⚠️ Gateway 未启动,请前往"服务管理"页面启动服务
  • ` + } + if (info.config && !info.config.gateway?.auth?.token) { + html += `
  • ⚠️ Gateway token 未设置(本地开发可选,生产环境建议设置)
  • ` + } + if (info.connectFrameError) { + html += `
  • ❌ 设备密钥生成失败,请检查 Rust 后端日志
  • ` + } + if (!info.wsClient.connected && info.services?.length > 0 && info.services[0]?.running) { + html += `
  • ⚠️ Gateway 已启动但 WebSocket 未连接,请检查端口 ${info.config?.gateway?.port || 18789} 是否被占用
  • ` + } + if (info.wsClient.connected && !info.wsClient.gatewayReady) { + html += `
  • ⚠️ WebSocket 已连接但握手未完成,请检查 token 是否正确
  • ` + } + if (allOk) { + html += `
  • ✅ 所有检测项正常,系统运行良好
  • ` + } + + html += `
` + html += `
检测时间: ${info.timestamp}
` + html += `
` + + el.innerHTML = html +} + +function escapeHtml(str) { + if (!str) return '' + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') +} + +// WebSocket 连接测试 +let testWs = null +let testLogs = [] + +function testWebSocket(page) { + const logEl = page.querySelector('#ws-test-log') + const contentEl = page.querySelector('#ws-log-content') + const clearBtn = page.querySelector('#btn-clear-log') + + logEl.style.display = 'block' + testLogs = [] + + clearBtn.onclick = () => { + testLogs = [] + contentEl.textContent = '' + } + + addLog('🔍 开始 WebSocket 连接测试...') + + // 关闭旧连接 + if (testWs) { + testWs.close() + testWs = null + } + + // 读取配置 + api.readOpenclawConfig().then(config => { + const port = config?.gateway?.port || 18789 + const token = config?.gateway?.auth?.token || '' + const url = `ws://127.0.0.1:${port}/ws?token=${encodeURIComponent(token)}` + + addLog(`📡 连接地址: ${url}`) + addLog(`🔑 Token: ${token ? token.substring(0, 20) + '...' : '(空)'}`) + addLog(`⏳ 正在连接...`) + + try { + testWs = new WebSocket(url) + + testWs.onopen = () => { + addLog('✅ WebSocket 连接成功') + addLog('⏳ 等待 Gateway 发送 connect.challenge...') + } + + testWs.onmessage = (evt) => { + try { + const msg = JSON.parse(evt.data) + addLog(`📥 收到消息: ${JSON.stringify(msg, null, 2)}`) + + // 如果收到 challenge,尝试发送 connect frame + if (msg.type === 'event' && msg.event === 'connect.challenge') { + const nonce = msg.payload?.nonce || '' + addLog(`🔐 收到 challenge, nonce: ${nonce}`) + addLog(`⏳ 生成 connect frame...`) + + api.createConnectFrame(nonce, token).then(frame => { + addLog(`✅ Connect frame 生成成功`) + addLog(`📤 发送 connect frame: ${JSON.stringify(frame, null, 2)}`) + testWs.send(JSON.stringify(frame)) + }).catch(e => { + addLog(`❌ 生成 connect frame 失败: ${e}`) + }) + } + + // 如果收到 connect 响应 + if (msg.type === 'res' && msg.id?.startsWith('connect-')) { + if (msg.ok) { + addLog(`✅ 握手成功!`) + addLog(`📊 Snapshot: ${JSON.stringify(msg.payload, null, 2)}`) + const sessionKey = msg.payload?.snapshot?.sessionDefaults?.mainSessionKey + if (sessionKey) { + addLog(`🔑 Session Key: ${sessionKey}`) + } + } else { + addLog(`❌ 握手失败: ${msg.error?.message || msg.error?.code || '未知错误'}`) + } + } + } catch (e) { + addLog(`⚠️ 解析消息失败: ${e}`) + addLog(`📥 原始数据: ${evt.data}`) + } + } + + testWs.onerror = (e) => { + addLog(`❌ WebSocket 错误: ${e.type}`) + } + + testWs.onclose = (e) => { + addLog(`🔌 连接关闭 - Code: ${e.code}, Reason: ${e.reason || '(空)'}`) + if (e.code === 4001) { + addLog(`❌ 认证失败 (4001) - Token 可能不正确`) + } else if (e.code === 1006) { + addLog(`⚠️ 异常关闭 (1006) - 可能是网络问题或 Gateway 主动断开`) + } + testWs = null + } + + } catch (e) { + addLog(`❌ 创建 WebSocket 失败: ${e}`) + } + }).catch(e => { + addLog(`❌ 读取配置失败: ${e}`) + }) + + function addLog(msg) { + const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false }) + const line = `[${timestamp}] ${msg}` + testLogs.push(line) + contentEl.textContent = testLogs.join('\n') + contentEl.scrollTop = contentEl.scrollHeight + } +} + +// 网络日志功能 +function toggleNetworkLog(page) { + const logEl = page.querySelector('#network-log') + const contentEl = page.querySelector('#network-log-content') + const refreshBtn = page.querySelector('#btn-refresh-network') + const clearBtn = page.querySelector('#btn-clear-network') + + if (logEl.style.display === 'none') { + logEl.style.display = 'block' + renderNetworkLog(contentEl) + } else { + logEl.style.display = 'none' + } + + refreshBtn.onclick = () => renderNetworkLog(contentEl) + clearBtn.onclick = () => { + clearRequestLogs() + renderNetworkLog(contentEl) + } +} + +function renderNetworkLog(contentEl) { + const logs = getRequestLogs() + + if (logs.length === 0) { + contentEl.innerHTML = '
暂无请求记录
' + return + } + + // 统计信息 + const total = logs.length + const cached = logs.filter(l => l.cached).length + const avgDuration = logs.filter(l => !l.cached).reduce((sum, l) => { + const ms = parseInt(l.duration) + return sum + (isNaN(ms) ? 0 : ms) + }, 0) / (total - cached || 1) + + let html = ` +
+
+ 总请求: ${total} + 缓存命中: ${cached} + 平均耗时: ${avgDuration.toFixed(0)}ms +
+
+ + + + + + + + + + + + ` + + // 倒序显示(最新的在上面) + for (let i = logs.length - 1; i >= 0; i--) { + const log = logs[i] + const cachedIcon = log.cached ? '✅' : '-' + const durationColor = log.cached ? 'var(--text-tertiary)' : + (parseInt(log.duration) > 1000 ? 'var(--error)' : + (parseInt(log.duration) > 500 ? 'var(--warning)' : 'var(--text-primary)')) + + html += ` + + + + + + + + ` + } + + html += `
时间命令参数耗时缓存
${log.time}${escapeHtml(log.cmd)}${escapeHtml(log.args)}${log.duration}${cachedIcon}
` + contentEl.innerHTML = html +} + +// 一键修复配对问题 +async function fixPairing(page) { + const logEl = page.querySelector('#ws-test-log') + const contentEl = page.querySelector('#ws-log-content') + + logEl.style.display = 'block' + testLogs = [] + + function addLog(msg) { + const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false }) + const line = `[${timestamp}] ${msg}` + testLogs.push(line) + contentEl.textContent = testLogs.join('\n') + contentEl.scrollTop = contentEl.scrollHeight + } + + try { + addLog('🔧 开始修复配对问题...') + + // 1. 修改配置禁用配对 + addLog('📝 修改配置文件,禁用配对要求...') + const result = await api.autoPairDevice() + addLog(`✅ ${result}`) + + // 2. 重启 Gateway + addLog('🔄 重启 Gateway 服务...') + await api.restartService('ai.openclaw.gateway') + addLog('✅ Gateway 重启命令已发送') + + // 3. 等待 Gateway 启动 + addLog('⏳ 等待 Gateway 启动(8秒)...') + await new Promise(resolve => setTimeout(resolve, 8000)) + + // 4. 检查 Gateway 状态 + addLog('🔍 检查 Gateway 状态...') + const services = await api.getServicesStatus() + const running = services?.[0]?.running + + if (running) { + addLog('✅ Gateway 已启动') + } else { + addLog('⚠️ Gateway 可能还在启动中,请稍后手动测试') + } + + // 5. 测试 WebSocket 连接 + addLog('🔌 测试 WebSocket 连接...') + const config = await api.readOpenclawConfig() + const port = config?.gateway?.port || 18789 + const token = config?.gateway?.auth?.token || '' + const url = `ws://127.0.0.1:${port}/ws?token=${encodeURIComponent(token)}` + + const ws = new WebSocket(url) + + ws.onopen = () => { + addLog('✅ WebSocket 连接成功') + } + + ws.onmessage = (evt) => { + try { + const msg = JSON.parse(evt.data) + if (msg.type === 'event' && msg.event === 'connect.challenge') { + addLog('✅ 收到 connect.challenge') + const nonce = msg.payload?.nonce || '' + + api.createConnectFrame(nonce, token).then(frame => { + ws.send(JSON.stringify(frame)) + addLog('📤 已发送 connect frame') + }) + } + + if (msg.type === 'res' && msg.id?.startsWith('connect-')) { + if (msg.ok) { + addLog('🎉 握手成功!配对问题已修复!') + addLog('💡 提示:现在可以正常使用 WebSocket 功能了') + ws.close() + } else { + addLog(`❌ 握手失败: ${msg.error?.message || '未知错误'}`) + addLog('💡 建议:请手动重启 Gateway 或联系技术支持') + } + } + } catch (e) { + addLog(`⚠️ 解析消息失败: ${e}`) + } + } + + ws.onerror = () => { + addLog('❌ WebSocket 连接失败') + } + + ws.onclose = (e) => { + if (e.code !== 1000) { + addLog(`⚠️ 连接关闭 - Code: ${e.code}`) + } + } + + } catch (e) { + addLog(`❌ 修复失败: ${e}`) + addLog('💡 建议:请手动前往"服务管理"页面重启 Gateway') + } +} diff --git a/src/pages/dashboard.js b/src/pages/dashboard.js index 00ba2b5..297bede 100644 --- a/src/pages/dashboard.js +++ b/src/pages/dashboard.js @@ -231,7 +231,10 @@ function renderOverview(page, services, clawapp, tunnel, mcpConfig, backups, con function renderLogs(page, logs) { const logsEl = page.querySelector('#recent-logs') - if (!logs) { logsEl.textContent = '暂无日志'; return } + if (!logs) { + logsEl.innerHTML = '
暂无日志
' + return + } const lines = logs.trim().split('\n') logsEl.innerHTML = lines.map(l => `
${escapeHtml(l)}
`).join('') logsEl.scrollTop = logsEl.scrollHeight diff --git a/src/pages/extensions.js b/src/pages/extensions.js index b3bd14c..60e9177 100644 --- a/src/pages/extensions.js +++ b/src/pages/extensions.js @@ -52,6 +52,7 @@ async function loadAll(page) { async function loadCftunnel(page) { const el = page.querySelector('#cftunnel-content') + el.innerHTML = '
加载中...
' try { const status = await api.getCftunnelStatus() renderCftunnel(el, status) @@ -145,6 +146,7 @@ function renderRoutes(routes) { async function loadClawapp(page) { const el = page.querySelector('#clawapp-content') + el.innerHTML = '
加载中...
' try { const status = await api.getClawappStatus() renderClawapp(el, status) diff --git a/src/pages/gateway.js b/src/pages/gateway.js index 7728565..7c1992d 100644 --- a/src/pages/gateway.js +++ b/src/pages/gateway.js @@ -41,10 +41,13 @@ export async function render() { } async function loadConfig(page, state) { + const el = page.querySelector('#gateway-config') + el.innerHTML = '
加载中...
' try { state.config = await api.readOpenclawConfig() renderConfig(page, state) } catch (e) { + el.innerHTML = '
加载配置失败: ' + e + '
' toast('加载配置失败: ' + e, 'error') } } diff --git a/src/pages/logs.js b/src/pages/logs.js index faec5d4..3519f1c 100644 --- a/src/pages/logs.js +++ b/src/pages/logs.js @@ -75,7 +75,7 @@ export function cleanup() { async function loadLog(page, logName) { const el = page.querySelector('#log-content') - el.innerHTML = '
加载中...
' + el.innerHTML = '
加载中...
' try { const content = await api.readLogTail(logName, 200) if (!content || !content.trim()) { @@ -88,13 +88,14 @@ async function loadLog(page, logName) { el.scrollTop = el.scrollHeight } } catch (e) { + el.innerHTML = '
加载日志失败: ' + e + '
' toast('加载日志失败: ' + e, 'error') } } async function searchLog(page, logName, query) { const el = page.querySelector('#log-content') - el.innerHTML = '
搜索中...
' + el.innerHTML = '
搜索中...
' try { const results = await api.searchLog(logName, query) if (!results || !results.length) { @@ -103,6 +104,7 @@ async function searchLog(page, logName, query) { } el.innerHTML = results.map(l => `
${highlightMatch(escapeHtml(l), query)}
`).join('') } catch (e) { + el.innerHTML = '
搜索失败: ' + e + '
' toast('搜索失败: ' + e, 'error') } } diff --git a/src/pages/memory.js b/src/pages/memory.js index aff69f5..8b2709c 100644 --- a/src/pages/memory.js +++ b/src/pages/memory.js @@ -151,6 +151,7 @@ async function loadFiles(page, state) { } renderFileTree(page, state, files) } catch (e) { + tree.innerHTML = '
加载失败: ' + e + '
' toast('加载文件列表失败: ' + e, 'error') } } diff --git a/src/pages/models.js b/src/pages/models.js index d0d1719..fd9f142 100644 --- a/src/pages/models.js +++ b/src/pages/models.js @@ -82,11 +82,14 @@ export async function render() { } async function loadConfig(page, state) { + const listEl = page.querySelector('#providers-list') + listEl.innerHTML = '
加载中...
' try { state.config = await api.readOpenclawConfig() renderDefaultBar(page, state) renderProviders(page, state) } catch (e) { + listEl.innerHTML = '
加载配置失败: ' + e + '
' toast('加载配置失败: ' + e, 'error') } } diff --git a/src/pages/services.js b/src/pages/services.js index 1bed58b..279bea4 100644 --- a/src/pages/services.js +++ b/src/pages/services.js @@ -101,6 +101,7 @@ const REGISTRIES = [ async function loadRegistry(page) { const bar = page.querySelector('#registry-bar') + bar.innerHTML = '
加载中...
' try { const current = await api.getNpmRegistry() const isPreset = REGISTRIES.some(r => r.value === current) @@ -130,6 +131,7 @@ async function loadRegistry(page) { async function loadServices(page) { const container = page.querySelector('#services-list') + container.innerHTML = '
加载中...
' try { const services = await api.getServicesStatus() renderServices(container, services) @@ -198,6 +200,7 @@ function renderServices(container, services) { async function loadBackups(page) { const list = page.querySelector('#backup-list') + list.innerHTML = '
加载中...
' try { const backups = await api.listBackups() renderBackups(list, backups) diff --git a/src/pages/setup.js b/src/pages/setup.js index a3ca3a5..1b39838 100644 --- a/src/pages/setup.js +++ b/src/pages/setup.js @@ -164,7 +164,7 @@ function renderInstallSection() { function bindEvents(page, nodeOk) { // 进入面板 page.querySelector('#btn-enter')?.addEventListener('click', () => { - window.location.reload() + window.location.hash = '/dashboard' }) // 一键安装 diff --git a/src/router.js b/src/router.js index 74c428a..66dae7d 100644 --- a/src/router.js +++ b/src/router.js @@ -45,13 +45,8 @@ async function loadRoute() { _currentCleanup = null } - // 退出动画:如果有旧页面,播放退出动画后再替换 - const oldPage = _contentEl.querySelector('.page, .page-loader, .chat-page') - if (oldPage) { - oldPage.classList.add('page-exit') - await new Promise(r => setTimeout(r, 100)) - if (thisLoad !== _loadId) return - } + // 立即移除旧页面(不等退出动画,消除切换卡顿) + _contentEl.innerHTML = '' // 已缓存的模块:跳过 spinner,直接渲染 let mod = _moduleCache[hash] diff --git a/src/style/debug.css b/src/style/debug.css new file mode 100644 index 0000000..cfff866 --- /dev/null +++ b/src/style/debug.css @@ -0,0 +1,58 @@ +/** + * 调试页面样式 + */ + +.debug-table { + width: 100%; + border-collapse: collapse; + margin-top: 8px; +} + +.debug-table td { + padding: 6px 8px; + border-bottom: 1px solid var(--border); + font-size: 12px; +} + +.debug-table td:first-child { + color: var(--text-secondary); + width: 200px; + font-weight: 500; +} + +.debug-table td:last-child { + color: var(--text-primary); + font-family: monospace; +} + +.debug-table tr:last-child td { + border-bottom: none; +} + +/* 状态概览卡片 */ +.config-section { + background: var(--bg-secondary); + border-radius: 6px; + padding: 12px; + margin-bottom: 12px; +} + +.config-section-title { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 8px; +} + +/* 成功/警告背景色 */ +:root { + --success-bg: rgba(34, 197, 94, 0.1); + --warning-bg: rgba(251, 191, 36, 0.1); + --error-bg: rgba(239, 68, 68, 0.1); +} + +[data-theme="dark"] { + --success-bg: rgba(34, 197, 94, 0.15); + --warning-bg: rgba(251, 191, 36, 0.15); + --error-bg: rgba(239, 68, 68, 0.15); +}