/**
* 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
`
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 ? '应用已锁定,请输入密码' : '请输入访问密码')}
${!hasDefault ? `
忘记密码?
${isTauri
? '删除配置文件中的 accessPassword 字段即可重置:
~/.openclaw/clawpanel.json'
: '编辑服务器上的配置文件,删除 accessPassword 字段后重启服务:
~/.openclaw/clawpanel.json'
}
` : ''}
`
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 = `
${t('dashboard.fixModalTitle')}
${t('dashboard.fixModalDesc')}
${t('dashboard.fixRunning')}\n
`
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 = `
更新到 v${ver}
在服务器上执行以下命令:
cd /opt/clawpanel
git pull origin main
npm install
npm run build
sudo systemctl restart clawpanel
如果 git pull 失败,可先执行 git checkout -- . 丢弃本地修改。
路径请替换为实际的 ClawPanel 安装目录。
`
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(/
`
}
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)
})()