/** * ClawPanel 入口 */ // 模块已加载,取消 splash 超时回退(防止假阳性的 "页面加载失败" 提示) if (window._splashTimer) { clearTimeout(window._splashTimer); window._splashTimer = null } import { registerRoute, initRouter, navigate, setDefaultRoute } from './router.js' import { renderSidebar, openMobileSidebar } from './components/sidebar.js' import { initTheme } from './lib/theme.js' import { detectOpenclawStatus, isOpenclawReady, isUpgrading, isGatewayRunning, onGatewayChange, startGatewayPoll, onGuardianGiveUp, resetAutoRestart, loadActiveInstance, getActiveInstance, onInstanceChange } from './lib/app-state.js' import { wsClient } from './lib/ws-client.js' import { api, checkBackendHealth, isBackendOnline, onBackendStatusChange } from './lib/tauri-api.js' import { version as APP_VERSION } from '../package.json' import { statusIcon } from './lib/icons.js' import { tryShowEngagement } from './components/engagement.js' import { initI18n } from './lib/i18n.js' // 样式 import './style/variables.css' import './style/reset.css' import './style/layout.css' import './style/components.css' import './style/pages.css' import './style/chat.css' import './style/agents.css' import './style/debug.css' import './style/assistant.css' import './style/ai-drawer.css' // 初始化主题 + 国际化 initTheme() initI18n() /** HTML 转义,防止 XSS 注入 */ function escapeHtml(str) { return (str || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') } // === 访问密码保护(Web + 桌面端通用) === const isTauri = !!window.__TAURI_INTERNALS__ async function checkAuth() { if (isTauri) { // 桌面端:读 clawpanel.json,检查密码配置 try { const { api } = await import('./lib/tauri-api.js') const cfg = await api.readPanelConfig() if (!cfg.accessPassword) return { ok: true } if (sessionStorage.getItem('clawpanel_authed') === '1') return { ok: true } // 默认密码:直接传给登录页,避免二次读取 const defaultPw = (cfg.mustChangePassword && cfg.accessPassword) ? cfg.accessPassword : null return { ok: false, defaultPw } } catch { return { ok: true } } } // Web 模式 try { const resp = await fetch('/__api/auth_check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }) const data = await resp.json() if (!data.required || data.authenticated) return { ok: true } return { ok: false, defaultPw: data.defaultPassword || null } } catch { return { ok: true } } } const _logoSvg = `` function _hideSplash() { const splash = document.getElementById('splash') if (splash) { splash.classList.add('hide'); setTimeout(() => splash.remove(), 500) } } // === 后端离线检测(Web 模式) === let _backendRetryTimer = null function showBackendDownOverlay() { if (document.getElementById('backend-down-overlay')) return _hideSplash() const overlay = document.createElement('div') overlay.id = 'backend-down-overlay' overlay.innerHTML = `
${_logoSvg}
后端未启动
ClawPanel 后端服务未运行,无法获取真实数据。
请在服务器上启动后端服务后刷新页面。
# 开发模式
npm run dev
# 生产模式
npm run preview
claw.qt.cool ·v${APP_VERSION}
` document.body.appendChild(overlay) let retrying = false const btn = overlay.querySelector('#btn-backend-retry') const statusEl = overlay.querySelector('#backend-retry-status') const textEl = overlay.querySelector('#backend-retry-text') btn.addEventListener('click', async () => { if (retrying) return retrying = true btn.disabled = true textEl.textContent = '检测中...' statusEl.textContent = '' const ok = await checkBackendHealth() if (ok) { statusEl.textContent = '后端已连接,正在加载...' statusEl.style.color = 'var(--success,#22c55e)' overlay.classList.add('hide') setTimeout(() => { overlay.remove(); location.reload() }, 600) } else { statusEl.textContent = '后端仍未响应,请确认服务已启动' statusEl.style.color = 'var(--error,#ef4444)' textEl.textContent = '重新检测' btn.disabled = false retrying = false } }) // 自动轮询:每 5 秒检测一次 if (_backendRetryTimer) clearInterval(_backendRetryTimer) _backendRetryTimer = setInterval(async () => { const ok = await checkBackendHealth() if (ok) { clearInterval(_backendRetryTimer) _backendRetryTimer = null statusEl.textContent = '后端已连接,正在加载...' statusEl.style.color = 'var(--success,#22c55e)' overlay.classList.add('hide') setTimeout(() => { overlay.remove(); location.reload() }, 600) } }, 5000) } let _loginFailCount = 0 const CAPTCHA_THRESHOLD = 3 function _genCaptcha() { const a = Math.floor(Math.random() * 20) + 1 const b = Math.floor(Math.random() * 20) + 1 return { q: `${a} + ${b} = ?`, a: a + b } } function showLoginOverlay(defaultPw) { const hasDefault = !!defaultPw const overlay = document.createElement('div') overlay.id = 'login-overlay' let _captcha = _loginFailCount >= CAPTCHA_THRESHOLD ? _genCaptcha() : null overlay.innerHTML = `
${_logoSvg}
ClawPanel
${hasDefault ? '首次使用,默认密码已自动填充
登录后请前往「安全设置」修改密码' : (isTauri ? '应用已锁定,请输入密码' : '请输入访问密码')}
请先完成验证:${_captcha ? _captcha.q : ''}
${!hasDefault ? `
忘记密码?
${isTauri ? '删除配置文件中的 accessPassword 字段即可重置:
~/.openclaw/clawpanel.json' : '编辑服务器上的配置文件,删除 accessPassword 字段后重启服务:
~/.openclaw/clawpanel.json' }
` : ''}
claw.qt.cool ·v${APP_VERSION}
` document.body.appendChild(overlay) _hideSplash() return new Promise((resolve) => { overlay.querySelector('#login-form').addEventListener('submit', async (e) => { e.preventDefault() const pw = overlay.querySelector('#login-pw').value const btn = overlay.querySelector('.login-btn') const errEl = overlay.querySelector('#login-error') btn.disabled = true btn.textContent = '登录中...' errEl.textContent = '' // 验证码校验 if (_captcha) { const captchaVal = parseInt(overlay.querySelector('#login-captcha-input')?.value) if (captchaVal !== _captcha.a) { errEl.textContent = '验证码错误' _captcha = _genCaptcha() const qEl = overlay.querySelector('#captcha-q') if (qEl) qEl.textContent = _captcha.q overlay.querySelector('#login-captcha-input').value = '' btn.disabled = false btn.textContent = '登 录' return } } try { if (isTauri) { // 桌面端:本地比对密码 const { api } = await import('./lib/tauri-api.js') const cfg = await api.readPanelConfig() if (pw !== cfg.accessPassword) { _loginFailCount++ if (_loginFailCount >= CAPTCHA_THRESHOLD && !_captcha) { _captcha = _genCaptcha() const cEl = overlay.querySelector('#login-captcha') if (cEl) { cEl.style.display = 'block'; cEl.querySelector('#captcha-q').textContent = _captcha.q } } errEl.textContent = `密码错误${_loginFailCount >= CAPTCHA_THRESHOLD ? '' : ` (${_loginFailCount}/${CAPTCHA_THRESHOLD})`}` btn.disabled = false btn.textContent = '登 录' return } sessionStorage.setItem('clawpanel_authed', '1') // 同步建立 web session(WEB_ONLY_CMDS 需要 cookie 认证) try { await fetch('/__api/auth_login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: pw }), }) } catch {} overlay.classList.add('hide') setTimeout(() => overlay.remove(), 400) if (cfg.accessPassword === '123456') { sessionStorage.setItem('clawpanel_must_change_pw', '1') } resolve() } else { // Web 模式:调后端 const resp = await fetch('/__api/auth_login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: pw }), }) const data = await resp.json() if (!resp.ok) { _loginFailCount++ if (_loginFailCount >= CAPTCHA_THRESHOLD && !_captcha) { _captcha = _genCaptcha() const cEl = overlay.querySelector('#login-captcha') if (cEl) { cEl.style.display = 'block'; cEl.querySelector('#captcha-q').textContent = _captcha.q } } errEl.textContent = (data.error || '登录失败') + (_loginFailCount >= CAPTCHA_THRESHOLD ? '' : ` (${_loginFailCount}/${CAPTCHA_THRESHOLD})`) btn.disabled = false btn.textContent = '登 录' return } overlay.classList.add('hide') setTimeout(() => overlay.remove(), 400) if (data.mustChangePassword || data.defaultPassword === '123456') { sessionStorage.setItem('clawpanel_must_change_pw', '1') } resolve() } } catch (err) { errEl.textContent = '网络错误: ' + (err.message || err) btn.disabled = false btn.textContent = '登 录' } }) }) } // 全局 401 拦截:API 返回 401 时弹出登录 window.__clawpanel_show_login = async function() { if (document.getElementById('login-overlay')) return await showLoginOverlay() location.reload() } const sidebar = document.getElementById('sidebar') const content = document.getElementById('content') async function boot() { // 先注册所有路由,立即渲染 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('/skills', () => import('./pages/skills.js')) registerRoute('/security', () => import('./pages/security.js')) registerRoute('/about', () => import('./pages/about.js')) registerRoute('/assistant', () => import('./pages/assistant.js')) registerRoute('/setup', () => import('./pages/setup.js')) registerRoute('/channels', () => import('./pages/channels.js')) registerRoute('/cron', () => import('./pages/cron.js')) registerRoute('/usage', () => import('./pages/usage.js')) registerRoute('/communication', () => import('./pages/communication.js')) registerRoute('/settings', () => import('./pages/settings.js')) renderSidebar(sidebar) initRouter(content) // 移动端顶栏(汉堡菜单 + 标题) const mainCol = document.getElementById('main-col') const topbar = document.createElement('div') topbar.className = 'mobile-topbar' topbar.id = 'mobile-topbar' topbar.innerHTML = ` ClawPanel ` topbar.querySelector('.mobile-hamburger').addEventListener('click', openMobileSidebar) mainCol.prepend(topbar) // 隐藏启动加载屏 const splash = document.getElementById('splash') if (splash) { splash.classList.add('hide') setTimeout(() => splash.remove(), 500) } // 默认密码提醒横幅 if (sessionStorage.getItem('clawpanel_must_change_pw') === '1') { const banner = document.createElement('div') banner.id = 'pw-change-banner' banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:999;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;padding:10px 20px;display:flex;align-items:center;justify-content:center;gap:12px;font-size:13px;font-weight:500;box-shadow:0 2px 8px rgba(0,0,0,0.15)' banner.innerHTML = ` ${statusIcon('warn', 14)} 当前使用的是系统生成的默认密码,为了安全请尽快修改 前往安全设置 ` document.body.prepend(banner) } // Tauri 模式:确保 web session 存在(页面刷新后 cookie 可能丢失),然后加载实例和检测状态 const ensureWebSession = isTauri ? api.readPanelConfig().then(cfg => { if (cfg.accessPassword) { return fetch('/__api/auth_login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: cfg.accessPassword }), }).catch(() => {}) } }).catch(() => {}) : Promise.resolve() ensureWebSession.then(() => loadActiveInstance()).then(() => detectOpenclawStatus()).then(() => { // 重新渲染侧边栏(检测完成后 isOpenclawReady 状态已更新) renderSidebar(sidebar) if (!isOpenclawReady()) { setDefaultRoute('/setup') navigate('/setup') } else { if (window.location.hash === '#/setup') navigate('/dashboard') setupGatewayBanner() startGatewayPoll() // 自动连接 WebSocket(如果 Gateway 正在运行) if (isGatewayRunning()) { autoConnectWebSocket() } // 监听 Gateway 状态变化,自动连接/断开 WebSocket onGatewayChange((running) => { if (running) { autoConnectWebSocket() // 正向时机:Gateway 启动成功,延迟弹社区引导 setTimeout(tryShowEngagement, 5000) } else { wsClient.disconnect() } }) // 守护放弃时,弹出恢复选项 if (window.__TAURI_INTERNALS__) { import('@tauri-apps/api/event').then(async ({ listen }) => { await listen('guardian-event', (e) => { if (e.payload?.kind === 'give_up') showGuardianRecovery() }) }).catch(() => {}) api.guardianStatus().then(status => { if (status?.giveUp) showGuardianRecovery() }).catch(() => {}) } else { onGuardianGiveUp(() => { showGuardianRecovery() }) } // 实例切换时,重连 WebSocket + 重新检测状态 onInstanceChange(async () => { wsClient.disconnect() await detectOpenclawStatus() if (isGatewayRunning()) autoConnectWebSocket() }) } // 全局监听后台任务完成/失败事件,自动刷新安装状态和侧边栏 if (window.__TAURI_INTERNALS__) { import('@tauri-apps/api/event').then(async ({ listen }) => { const refreshAfterTask = async () => { // 清除 API 缓存,确保拿到最新状态 const { invalidate } = await import('./lib/tauri-api.js') invalidate('check_installation', 'get_services_status', 'get_version_info') await detectOpenclawStatus() renderSidebar(sidebar) // 如果安装完成后变为就绪,跳转到仪表盘 if (isOpenclawReady() && window.location.hash === '#/setup') { navigate('/dashboard') } // 如果卸载后变为未就绪,跳转到 setup if (!isOpenclawReady() && !isUpgrading()) { setDefaultRoute('/setup') navigate('/setup') } } await listen('upgrade-done', refreshAfterTask) await listen('upgrade-error', refreshAfterTask) }).catch(() => {}) } }) } async function autoConnectWebSocket() { try { const inst = getActiveInstance() console.log(`[main] 自动连接 WebSocket (实例: ${inst.name})...`) const config = await api.readOpenclawConfig() const port = config?.gateway?.port || 18789 const rawToken = config?.gateway?.auth?.token const token = (typeof rawToken === 'string') ? rawToken : '' // 启动前先确保设备已配对 + allowedOrigins 已写入,无需用户手动操作 let needReload = false try { const pairResult = await api.autoPairDevice() console.log('[main] 设备配对 + origins 已就绪:', pairResult) // 仅在配置实际变更时才需要 reload(dev-api 返回 {changed},Tauri 返回字符串) if (typeof pairResult === 'object' && pairResult.changed) { needReload = true } else if (typeof pairResult === 'string' && pairResult !== '设备已配对') { needReload = true } } catch (pairErr) { console.warn('[main] autoPairDevice 失败(非致命):', pairErr) } // 确保模型配置包含 vision 支持(input: ["text", "image"]) try { const patched = await api.patchModelVision() if (patched) { console.log('[main] 已为模型添加 vision 支持') needReload = true } } catch (visionErr) { console.warn('[main] patchModelVision 失败(非致命):', visionErr) } // 统一 reload Gateway(配对 origins + vision patch 合并为一次 reload) if (needReload) { try { await api.reloadGateway() console.log('[main] Gateway 已重载') } catch (reloadErr) { console.warn('[main] reloadGateway 失败(非致命):', reloadErr) } } let host const inst2 = getActiveInstance() if (inst2.type !== 'local' && inst2.endpoint) { try { const url = new URL(inst2.endpoint) host = `${url.hostname}:${inst2.gatewayPort || port}` } catch { host = window.__TAURI_INTERNALS__ ? `127.0.0.1:${port}` : location.host } } else { host = window.__TAURI_INTERNALS__ ? `127.0.0.1:${port}` : location.host } wsClient.connect(host, token) console.log(`[main] WebSocket 连接已启动 -> ${host}`) } catch (e) { console.error('[main] 自动连接 WebSocket 失败:', e) } } function setupGatewayBanner() { const banner = document.getElementById('gw-banner') if (!banner) return function update(running) { if (running || sessionStorage.getItem('gw-banner-dismissed')) { banner.classList.add('gw-banner-hidden') return } else { banner.classList.remove('gw-banner-hidden') banner.innerHTML = `
${statusIcon('info', 16)} Gateway 未运行 服务管理
` banner.querySelector('#btn-gw-dismiss')?.addEventListener('click', () => { banner.classList.add('gw-banner-hidden') sessionStorage.setItem('gw-banner-dismissed', '1') }) banner.querySelector('#btn-gw-start')?.addEventListener('click', async (e) => { const btn = e.target btn.disabled = true btn.classList.add('btn-loading') btn.textContent = '启动中...' try { await api.startService('ai.openclaw.gateway') } catch (err) { const errMsg = (err.message || String(err)).slice(0, 120) banner.innerHTML = `
${statusIcon('info', 16)} 启动失败 服务管理 查看日志
${escapeHtml(errMsg)}
` update(false) return } // 轮询等待实际启动 const t0 = Date.now() while (Date.now() - t0 < 30000) { try { const s = await api.getServicesStatus() const gw = s?.find?.(x => x.label === 'ai.openclaw.gateway') || s?.[0] if (gw?.running) { update(true); return } } catch {} const sec = Math.floor((Date.now() - t0) / 1000) btn.textContent = `启动中... ${sec}s` await new Promise(r => setTimeout(r, 1500)) } // 超时后尝试获取日志帮助排查 let logHint = '' try { const logs = await api.readLogTail('gateway', 5) if (logs?.trim()) logHint = `
${logs.trim().split('\n').slice(-3).join('\n')}
` } catch {} banner.innerHTML = `
${statusIcon('info', 16)} 启动超时,Gateway 可能仍在启动中 查看日志
${logHint} ` update(false) }) } } update(isGatewayRunning()) onGatewayChange(update) } function showGuardianRecovery() { const banner = document.getElementById('gw-banner') if (!banner) return banner.classList.remove('gw-banner-hidden') banner.innerHTML = `
${statusIcon('warn', 16)} ${t('dashboard.guardianFailed')} ${t('sidebar.logs')}
` banner.querySelector('#btn-gw-recover-fix')?.addEventListener('click', async (e) => { const btn = e.target btn.disabled = true btn.textContent = t('dashboard.fixing') // 弹出修复弹窗 const overlay = document.createElement('div') overlay.className = 'modal-overlay' overlay.innerHTML = ` ` document.body.appendChild(overlay) const logEl = overlay.querySelector('#fix-log') const statusEl = overlay.querySelector('#fix-status') const closeBtn = overlay.querySelector('#fix-close') closeBtn.onclick = () => overlay.remove() try { const result = await api.doctorFix() const output = result?.stdout || result?.output || JSON.stringify(result, null, 2) logEl.textContent = output || t('dashboard.fixDoneNoOutput') logEl.scrollTop = logEl.scrollHeight if (result?.errors) { statusEl.innerHTML = `${t('dashboard.fixDoneWarning')}${escapeHtml(String(result.errors).slice(0, 200))}` } else { statusEl.innerHTML = `${t('dashboard.fixDoneRestarting')}` resetAutoRestart() try { await api.startService('ai.openclaw.gateway') statusEl.innerHTML = `${t('dashboard.fixDoneRestarted')}` } catch { statusEl.innerHTML = `${t('dashboard.fixDoneRestartFail')}` } } } catch (err) { logEl.textContent += '\n❌ ' + (err.message || String(err)) statusEl.innerHTML = `${t('dashboard.fixFailed')}${escapeHtml(String(err.message || err).slice(0, 200))}` } closeBtn.style.display = '' btn.textContent = t('dashboard.autoFix') btn.disabled = false }) banner.querySelector('#btn-gw-recover-restart')?.addEventListener('click', async (e) => { const btn = e.target btn.disabled = true btn.textContent = t('dashboard.fixing') resetAutoRestart() try { await api.startService('ai.openclaw.gateway') btn.textContent = t('dashboard.startSent') } catch (err) { btn.textContent = t('dashboard.retryStart') btn.disabled = false } }) } // === 全局版本更新检测 === const UPDATE_CHECK_INTERVAL = 30 * 60 * 1000 // 30 分钟 let _updateCheckTimer = null async function checkGlobalUpdate() { const banner = document.getElementById('update-banner') if (!banner) return try { const info = await api.checkFrontendUpdate() if (!info.hasUpdate) return const ver = info.latestVersion || info.manifest?.version || '' if (!ver) return // 用户已忽略过该版本,不再打扰 const dismissed = localStorage.getItem('clawpanel_update_dismissed') if (dismissed === ver) return // 热更新已下载并重载过,不再重复提示同一版本 const hotApplied = localStorage.getItem('clawpanel_hot_update_applied') if (hotApplied === ver) return const changelog = info.manifest?.changelog || '' const isWeb = !window.__TAURI_INTERNALS__ banner.classList.remove('update-banner-hidden') banner.innerHTML = `
ClawPanel v${ver} 可用 ${changelog ? `· ${changelog}` : ''}
${isWeb ? ` Release Notes` : ` 完整安装包` }
` // 关闭按钮:记住忽略的版本 banner.querySelector('#btn-update-dismiss')?.addEventListener('click', () => { localStorage.setItem('clawpanel_update_dismissed', ver) banner.classList.add('update-banner-hidden') }) // Web 模式:显示更新命令弹窗 banner.querySelector('#btn-update-show-cmd')?.addEventListener('click', () => { const overlay = document.createElement('div') overlay.className = 'modal-overlay' overlay.innerHTML = ` ` document.body.appendChild(overlay) overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove() }) overlay.querySelector('[data-action="close"]').onclick = () => overlay.remove() overlay.addEventListener('keydown', (e) => { if (e.key === 'Escape') overlay.remove() }) }) // Tauri 热更新按钮 banner.querySelector('#btn-update-hot')?.addEventListener('click', async () => { const btn = banner.querySelector('#btn-update-hot') if (!btn) return btn.disabled = true btn.textContent = '下载中...' try { await api.downloadFrontendUpdate(info.manifest?.url || '', info.manifest?.hash || '') localStorage.setItem('clawpanel_hot_update_applied', ver) btn.textContent = '重载应用' btn.disabled = false btn.onclick = () => window.location.reload() } catch (e) { btn.textContent = '下载失败' btn.disabled = false const { toast } = await import('./components/toast.js') toast('更新下载失败: ' + (e.message || e), 'error') } }) } catch { // 检查失败静默忽略 } } function startUpdateChecker() { // 启动后 5 秒检查一次 setTimeout(checkGlobalUpdate, 5000) // 之后每 30 分钟检查一次 _updateCheckTimer = setInterval(checkGlobalUpdate, UPDATE_CHECK_INTERVAL) } // 启动:先检查后端 → 认证 → 加载应用 ;(async () => { // Web 模式:先检测后端是否在线(不在线则显示提示,不加载应用) if (!isTauri) { const backendOk = await checkBackendHealth() if (!backendOk) { showBackendDownOverlay() return } } const auth = await checkAuth() if (!auth.ok) await showLoginOverlay(auth.defaultPw) try { await boot() } catch (bootErr) { console.error('[main] boot() 失败:', bootErr) _hideSplash() const app = document.getElementById('app') if (app) app.innerHTML = `
⚠️
页面加载失败
${String(bootErr?.message || bootErr).replace(/
如果问题持续出现,请尝试重新安装 ClawPanel
或在 GitHub Issues 反馈
` } startUpdateChecker() // 初始化全局 AI 助手浮动按钮(延迟加载,不阻塞启动) setTimeout(async () => { const { initAIFab, registerPageContext, openAIDrawerWithError } = await import('./components/ai-drawer.js') initAIFab() // 注册各页面上下文提供器 registerPageContext('/chat-debug', async () => { const { isOpenclawReady, isGatewayRunning } = await import('./lib/app-state.js') const { wsClient } = await import('./lib/ws-client.js') const { api } = await import('./lib/tauri-api.js') const lines = ['## 系统诊断快照'] lines.push(`- OpenClaw: ${isOpenclawReady() ? '就绪' : '未就绪'}`) lines.push(`- Gateway: ${isGatewayRunning() ? '运行中' : '未运行'}`) lines.push(`- WebSocket: ${wsClient.connected ? '已连接' : '未连接'}`) try { const node = await api.checkNode() lines.push(`- Node.js: ${node?.version || '未知'}`) } catch {} try { const ver = await api.getVersionInfo() lines.push(`- 版本: 当前 ${ver?.current || '?'} / 推荐 ${ver?.recommended || '?'} / 最新 ${ver?.latest || '?'}${ver?.ahead_of_recommended ? ' / 当前版本高于推荐版' : ''}`) } catch {} return { detail: lines.join('\n') } }) registerPageContext('/services', async () => { const { isGatewayRunning } = await import('./lib/app-state.js') const { api } = await import('./lib/tauri-api.js') const lines = ['## 服务状态'] lines.push(`- Gateway: ${isGatewayRunning() ? '运行中' : '未运行'}`) try { const svc = await api.getServicesStatus() if (svc?.[0]) { lines.push(`- CLI: ${svc[0].cli_installed ? '已安装' : '未安装'}`) lines.push(`- PID: ${svc[0].pid || '无'}`) } } catch {} return { detail: lines.join('\n') } }) registerPageContext('/gateway', async () => { const { api } = await import('./lib/tauri-api.js') try { const config = await api.readOpenclawConfig() const gw = config?.gateway || {} const lines = ['## Gateway 配置'] lines.push(`- 端口: ${gw.port || 18789}`) lines.push(`- 模式: ${gw.mode || 'local'}`) lines.push(`- Token: ${gw.auth?.token ? '已设置' : '未设置'}`) if (gw.controlUi?.allowedOrigins) lines.push(`- Origins: ${JSON.stringify(gw.controlUi.allowedOrigins)}`) return { detail: lines.join('\n') } } catch { return null } }) registerPageContext('/setup', () => { return { detail: '用户正在进行 OpenClaw 初始安装,请帮助检查 Node.js 环境和网络状况' } }) // 挂到全局,供安装/升级失败时调用 window.__openAIDrawerWithError = openAIDrawerWithError }, 500) })()