/** * 系统诊断页面 * 全面检测 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' import { isForeignGatewayError, showGatewayConflictGuidance } from '../lib/gateway-ownership.js' import { icon, statusIcon } from '../lib/icons.js' import { toast } from '../components/toast.js' import { navigate } from '../router.js' import { t } from '../lib/i18n.js' export async function render() { const page = document.createElement('div') page.className = 'page' page.innerHTML = `
${t('chatDebug.sectionAppState')}
${t('chatDebug.sectionWs')}
${t('chatDebug.sectionNode')}
${t('chatDebug.sectionVersion')}
` 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)) page.querySelector('#btn-doctor-check').addEventListener('click', () => handleDoctor(page, false)) page.querySelector('#btn-doctor-fix').addEventListener('click', () => handleDoctor(page, true)) loadDebugInfo(page) return page } async function openGatewayConflict(error = null) { const services = await api.getServicesStatus().catch(() => []) const gw = services?.find?.(s => s.label === 'ai.openclaw.gateway') || services?.[0] || null await showGatewayConflictGuidance({ error, service: gw }) } async function loadDebugInfo(page) { const el = page.querySelector('#debug-content') 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 rawToken = info.config?.gateway?.auth?.token const token = (typeof rawToken === 'string') ? rawToken : '' 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 ? `${statusIcon('ok')} ${t('chatDebug.systemOk')}` : `${statusIcon('warn')} ${t('chatDebug.issuesFound')}`}
${allOk ? t('chatDebug.allFunctionsOk') : t('chatDebug.someFunctionsError')}
` // 应用状态 html += `
${t('chatDebug.sectionAppState')}
${t('chatDebug.openclawReady')}${info.appState.openclawReady ? statusIcon('ok') : statusIcon('err')}
${t('chatDebug.gatewayRunning')}${info.appState.gatewayRunning ? statusIcon('ok') : statusIcon('err')}
` // WebSocket 状态 html += `
${t('chatDebug.sectionWs')}
${t('chatDebug.connStatus')}${info.wsClient.connected ? `${statusIcon('ok')} ${t('chatDebug.connected')}` : `${statusIcon('err')} ${t('chatDebug.notConnected')}`}
${t('chatDebug.handshakeStatus')}${info.wsClient.gatewayReady ? `${statusIcon('ok')} ${t('chatDebug.completed')}` : `${statusIcon('err')} ${t('chatDebug.notCompleted')}`}
${t('chatDebug.sessionKey')}${info.wsClient.sessionKey || t('chatDebug.empty')}
` // Node.js 环境 html += `
${t('chatDebug.sectionNode')}
` if (info.nodeError) { html += `
${statusIcon('err')} ${escapeHtml(info.nodeError)}
` } else if (info.node) { html += `
${t('chatDebug.installStatus')}${info.node.installed ? `${statusIcon('ok')} ${t('chatDebug.installed')}` : `${statusIcon('err')} ${t('chatDebug.notInstalled')}`}
${t('chatDebug.version')}${info.node.version || t('chatDebug.unknownLabel')}
` } html += `
` // 版本信息 html += `
${t('chatDebug.sectionVersion')}
` if (info.versionError) { html += `
${statusIcon('err')} ${escapeHtml(info.versionError)}
` } else if (info.version) { html += `
${t('chatDebug.currentVersion')}${info.version.current || t('chatDebug.unknownLabel')}
${t('chatDebug.recommendedVersion')}${info.version.recommended || t('chatDebug.notDetected')}
${t('chatDebug.panelVersion')}${info.version.panel_version || t('chatDebug.unknownLabel')}
${t('chatDebug.latestUpstream')}${info.version.latest || t('chatDebug.notDetected')}
${t('chatDebug.deviationFromRecommended')}${info.version.ahead_of_recommended ? `${statusIcon('warn')} ${t('chatDebug.versionTooHigh')}` : info.version.is_recommended ? `${statusIcon('ok')} ${t('chatDebug.versionAligned')}` : `${statusIcon('warn')} ${t('chatDebug.versionNeedSwitch')}`}
${t('chatDebug.latestAvailable')}${info.version.latest_update_available ? `${statusIcon('warn')} ${t('chatDebug.hasUpdate')}` : `${statusIcon('ok')} ${t('chatDebug.noUpdate')}`}
` } html += `
` // 配置文件 html += `
${t('chatDebug.sectionConfig')}
` if (info.configError) { html += `
${statusIcon('err')} ${escapeHtml(info.configError)}
` } else if (info.config) { const gw = info.config.gateway || {} html += `
gateway.port${gw.port || t('chatDebug.notSet')}
gateway.auth.token${gw.auth?.token ? `${statusIcon('ok')} ${t('chatDebug.set')}${typeof gw.auth.token === 'object' ? ' (SecretRef)' : ''}` : `${statusIcon('warn')} ${t('chatDebug.notSet')}`}
gateway.enabled${gw.enabled !== false ? statusIcon('ok') : statusIcon('err')}
gateway.mode${gw.mode || 'local'}
` } html += `
` // 服务状态 html += `
${t('chatDebug.sectionService')}
` if (info.servicesError) { html += `
${statusIcon('err')} ${escapeHtml(info.servicesError)}
` } else if (info.services?.length > 0) { const svc = info.services[0] html += `
${t('chatDebug.cliInstall')}${svc.cli_installed !== false ? `${statusIcon('ok')} ${t('chatDebug.installed')}` : `${statusIcon('err')} ${t('chatDebug.notInstalled')}`}
${t('chatDebug.runStatus')}${svc.running ? `${statusIcon('ok')} ${t('chatDebug.running')}` : `${statusIcon('err')} ${t('chatDebug.stopped')}`}
${t('chatDebug.processPid')}${svc.pid || t('chatDebug.none')}
${t('chatDebug.serviceLabel')}${svc.label || t('chatDebug.unknownLabel')}
` } html += `
` // 设备密钥 html += `
${t('chatDebug.sectionDevice')}
` if (info.connectFrameError) { html += `
${statusIcon('err')} ${escapeHtml(info.connectFrameError)}
` } else if (info.connectFrame) { const device = info.connectFrame.params?.device html += `
${statusIcon('ok')} ${t('chatDebug.deviceKeySuccess')}
${t('chatDebug.deviceId')}${device?.id || t('chatDebug.none')}
${t('chatDebug.publicKey')}${device?.publicKey ? device.publicKey.substring(0, 32) + '...' : t('chatDebug.none')}
${t('chatDebug.signTime')}${device?.signedAt || t('chatDebug.none')}
${t('chatDebug.viewConnectFrame')}
${escapeHtml(JSON.stringify(info.connectFrame, null, 2))}
` } html += `
` // 诊断建议 html += `
${t('chatDebug.sectionDiagnosis')}
` html += `
${t('chatDebug.checkTime', { time: info.timestamp })}
` html += `
` el.innerHTML = html } // 配置诊断 / 自动修复(openclaw doctor) async function handleDoctor(page, fix) { const btnCheck = page.querySelector('#btn-doctor-check') const btnFix = page.querySelector('#btn-doctor-fix') const outputDiv = page.querySelector('#doctor-output') const section = outputDiv?.querySelector('.config-section') const pre = outputDiv?.querySelector('pre') if (!outputDiv || !pre) return // 清除之前的提示 section?.querySelectorAll('.doctor-tip').forEach(el => el.remove()) if (btnCheck) btnCheck.disabled = true if (btnFix) btnFix.disabled = true if (fix && btnFix) btnFix.textContent = t('chatDebug.fixing') if (!fix && btnCheck) btnCheck.textContent = t('chatDebug.diagnosing') outputDiv.style.display = 'block' pre.textContent = fix ? t('chatDebug.runningDoctorFix') : t('chatDebug.runningDoctor') pre.style.color = 'var(--text-secondary)' try { const result = fix ? await api.doctorFix() : await api.doctorCheck() let text = result.output || '' if (result.errors) text += '\n' + result.errors const fullText = text.trim() pre.textContent = fullText || (result.success ? t('chatDebug.noIssues') : t('chatDebug.diagDone')) pre.style.color = result.success ? 'var(--success)' : 'var(--warning)' if (fullText.includes('ERR_MODULE_NOT_FOUND') || fullText.includes('Cannot find module')) { appendDoctorTip(section, t('chatDebug.installCorrupt'), t('chatDebug.installCorruptHint')) toast(t('chatDebug.installCorruptToast'), 'warning') } else if (fix && result.success) { toast(t('chatDebug.configFixDone'), 'success') } else if (fix) { toast(t('chatDebug.configFixPartial'), 'warning') } } catch (e) { const errMsg = e?.message || String(e) pre.textContent = t('chatDebug.execFailed') + errMsg pre.style.color = 'var(--error)' if (errMsg.includes('ERR_MODULE_NOT_FOUND') || errMsg.includes('Cannot find module') || errMsg.includes('未找到')) { appendDoctorTip(section, t('chatDebug.cliUnavailable'), t('chatDebug.cliUnavailableHint')) } toast(t('chatDebug.execFailed') + e, 'error') } finally { if (btnCheck) { btnCheck.disabled = false; btnCheck.textContent = t('chatDebug.btnDiagConfig') } if (btnFix) { btnFix.disabled = false; btnFix.textContent = t('chatDebug.btnAutoFix') } } } function appendDoctorTip(parent, title, body) { if (!parent) return const tip = document.createElement('div') tip.className = 'doctor-tip' tip.style.cssText = 'margin-top:var(--space-sm);padding:var(--space-sm);background:rgba(239,68,68,0.08);border-radius:var(--radius);font-size:var(--font-size-sm);color:var(--error);line-height:1.6' tip.innerHTML = `⚠ ${title}
${body}` tip.querySelector('[data-nav="about"]')?.addEventListener('click', (e) => { e.preventDefault() navigate('/about') }) parent.appendChild(tip) } 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.innerHTML = '' } addLog(`${icon('search', 14)} ${t('chatDebug.wsTestStart')}`) // 关闭旧连接 if (testWs) { testWs.close() testWs = null } // 读取配置 api.readOpenclawConfig().then(config => { const port = config?.gateway?.port || 18789 const rawToken = config?.gateway?.auth?.token const token = (typeof rawToken === 'string') ? rawToken : '' const wsHost = window.__TAURI_INTERNALS__ ? `127.0.0.1:${port}` : location.host const url = `ws://${wsHost}/ws?token=${encodeURIComponent(token)}` addLog(`${icon('radio', 14)} ${t('chatDebug.wsAddress', { url })}`) addLog(`${icon('key', 14)} ${t('chatDebug.wsToken', { token: token ? token.substring(0, 20) + '...' : t('chatDebug.empty') })}`) addLog(`${icon('clock', 14)} ${t('chatDebug.wsConnecting')}`) try { testWs = new WebSocket(url) testWs.onopen = () => { addLog(`${statusIcon('ok', 14)} ${t('chatDebug.wsConnected')}`) addLog(`${icon('clock', 14)} ${t('chatDebug.wsWaitChallenge')}`) } testWs.onmessage = (evt) => { try { const msg = JSON.parse(evt.data) addLog(`${icon('inbox', 14)} ${t('chatDebug.wsReceivedMsg')}: ${escapeHtml(JSON.stringify(msg, null, 2))}`) // 如果收到 challenge,尝试发送 connect frame if (msg.type === 'event' && msg.event === 'connect.challenge') { const nonce = msg.payload?.nonce || '' addLog(`${icon('lock', 14)} ${t('chatDebug.wsReceivedChallenge')}: ${nonce}`) addLog(`${icon('clock', 14)} ${t('chatDebug.wsGeneratingFrame')}`) api.createConnectFrame(nonce, token).then(frame => { addLog(`${statusIcon('ok', 14)} ${t('chatDebug.wsFrameGenerated')}`) addLog(`${icon('send', 14)} ${t('chatDebug.wsSendingFrame')}: ${escapeHtml(JSON.stringify(frame, null, 2))}`) testWs.send(JSON.stringify(frame)) }).catch(e => { addLog(`${statusIcon('err', 14)} ${t('chatDebug.wsFrameFailed')}: ${e}`) }) } // 如果收到 connect 响应 if (msg.type === 'res' && msg.id?.startsWith('connect-')) { if (msg.ok) { addLog(`${statusIcon('ok', 14)} ${t('chatDebug.wsHandshakeOk')}`) addLog(`${icon('bar-chart', 14)} Snapshot: ${escapeHtml(JSON.stringify(msg.payload, null, 2))}`) const sessionKey = msg.payload?.snapshot?.sessionDefaults?.mainSessionKey if (sessionKey) { addLog(`${icon('key', 14)} Session Key: ${sessionKey}`) } } else { addLog(`${statusIcon('err', 14)} ${t('chatDebug.wsHandshakeFailed')}: ${msg.error?.message || msg.error?.code || t('common.unknown')}`) } } } catch (e) { addLog(`${statusIcon('warn', 14)} ${t('chatDebug.wsParseFailed')}: ${e}`) addLog(`${icon('inbox', 14)} ${t('chatDebug.wsRawData')}: ${escapeHtml(evt.data)}`) } } testWs.onerror = (e) => { addLog(`${statusIcon('err', 14)} ${t('chatDebug.wsError')}: ${e.type}`) } testWs.onclose = (e) => { addLog(`${icon('plug', 14)} ${t('chatDebug.wsClosed')} - Code: ${e.code}, Reason: ${e.reason || t('chatDebug.empty')}`) if (e.code === 1008) { addLog(`${statusIcon('err', 14)} ${t('chatDebug.wsOriginRejected')}`) addLog(`${icon('lightbulb', 14)} ${t('chatDebug.wsOriginFix')}`) } else if (e.code === 4001) { addLog(`${statusIcon('err', 14)} ${t('chatDebug.wsAuthFailed')}`) } else if (e.code === 1006) { addLog(`${statusIcon('warn', 14)} ${t('chatDebug.wsAbnormalClose')}`) } testWs = null } } catch (e) { addLog(`${statusIcon('err', 14)} ${t('chatDebug.wsCreateFailed')}: ${e}`) } }).catch(e => { addLog(`${statusIcon('err', 14)} ${t('chatDebug.wsConfigReadFailed')}: ${e}`) }) function addLog(msg) { const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false }) const div = document.createElement('div') div.style.cssText = 'display:flex;gap:4px;align-items:flex-start;padding:1px 0;white-space:pre-wrap;word-break:break-all' div.innerHTML = `[${timestamp}] ${msg}` testLogs.push(div.textContent) contentEl.appendChild(div) 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 = `
${t('chatDebug.noRequests')}
` 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 = `
${t('chatDebug.totalRequests')}: ${total} ${t('chatDebug.cacheHit')}: ${cached} ${t('chatDebug.avgDuration')}: ${avgDuration.toFixed(0)}ms
` // 倒序显示(最新的在上面) for (let i = logs.length - 1; i >= 0; i--) { const log = logs[i] const cachedIcon = log.cached ? statusIcon('ok', 12) : '-' const durationColor = log.cached ? 'var(--text-tertiary)' : (parseInt(log.duration) > 1000 ? 'var(--error)' : (parseInt(log.duration) > 500 ? 'var(--warning)' : 'var(--text-primary)')) html += ` ` } html += `
${t('chatDebug.colTime')} ${t('chatDebug.colCommand')} ${t('chatDebug.colArgs')} ${t('chatDebug.colDuration')} ${t('chatDebug.colCache')}
${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') const fixBtn = page.querySelector('#btn-fix-pairing') if (fixBtn) { fixBtn.disabled = true; fixBtn.textContent = t('chatDebug.fixing') } logEl.style.display = 'block' testLogs = [] logEl.scrollIntoView({ behavior: 'smooth', block: 'start' }) 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(`${icon('wrench', 14)} ${t('chatDebug.fixStarting')}`) // 1. 写入 paired.json + controlUi.allowedOrigins addLog(`${icon('edit', 14)} ${t('chatDebug.fixWritingPair')}`) const result = await api.autoPairDevice() addLog(`${statusIcon('ok', 14)} ${result}`) addLog(`${statusIcon('ok', 14)} ${t('chatDebug.fixOriginAdded')}`) // 2. 停止 Gateway(确保旧进程完全退出,新进程能重新读取配置) addLog(`${icon('zap', 14)} ${t('chatDebug.fixStoppingGw')}`) try { await api.stopService('ai.openclaw.gateway') } catch (e) { if (isForeignGatewayError(e)) { await openGatewayConflict(e) throw e } } addLog(`${icon('clock', 14)} ${t('chatDebug.fixWaitExit')}`) await new Promise(resolve => setTimeout(resolve, 3000)) // 3. 启动 Gateway(重新加载 openclaw.json 配置) addLog(`${icon('zap', 14)} ${t('chatDebug.fixStartingGw')}`) try { await api.startService('ai.openclaw.gateway') } catch (e) { if (isForeignGatewayError(e)) { await openGatewayConflict(e) } throw e } addLog(`${statusIcon('ok', 14)} ${t('chatDebug.fixGwStartSent')}`) // 4. 等待 Gateway 就绪 addLog(`${icon('clock', 14)} ${t('chatDebug.fixWaitReady')}`) await new Promise(resolve => setTimeout(resolve, 5000)) // 5. 检查 Gateway 状态 addLog(`${icon('search', 14)} ${t('chatDebug.fixCheckStatus')}`) const services = await api.getServicesStatus() const running = services?.[0]?.running if (running) { addLog(`${statusIcon('ok', 14)} ${t('chatDebug.fixGwStarted')}`) } else { addLog(`${statusIcon('warn', 14)} ${t('chatDebug.fixGwMaybeStarting')}`) } // 6. 测试 WebSocket 连接 addLog(`${icon('plug', 14)} ${t('chatDebug.fixTestingWs')}`) const config = await api.readOpenclawConfig() const port = config?.gateway?.port || 18789 const rawToken = config?.gateway?.auth?.token const token = (typeof rawToken === 'string') ? rawToken : '' const wsHost = window.__TAURI_INTERNALS__ ? `127.0.0.1:${port}` : location.host const url = `ws://${wsHost}/ws?token=${encodeURIComponent(token)}` const ws = new WebSocket(url) ws.onopen = () => { addLog(`${statusIcon('ok', 14)} ${t('chatDebug.wsConnected')}`) } ws.onmessage = (evt) => { try { const msg = JSON.parse(evt.data) if (msg.type === 'event' && msg.event === 'connect.challenge') { addLog(`${statusIcon('ok', 14)} ${t('chatDebug.fixReceivedChallenge')}`) const nonce = msg.payload?.nonce || '' api.createConnectFrame(nonce, token).then(frame => { ws.send(JSON.stringify(frame)) addLog(`${icon('send', 14)} ${t('chatDebug.fixFrameSent')}`) }) } if (msg.type === 'res' && msg.id?.startsWith('connect-')) { if (msg.ok) { addLog(`${statusIcon('ok', 14)} ${t('chatDebug.fixPairSuccess')}`) addLog(`${icon('lightbulb', 14)} ${t('chatDebug.fixReconnecting')}`) ws.close(1000) // 触发主应用的 wsClient 重连,让主界面正常工作 wsClient.reconnect() setTimeout(() => loadDebugInfo(page), 2000) } else { const errMsg = msg.error?.message || msg.error?.code || t('common.unknown') addLog(`${statusIcon('err', 14)} ${t('chatDebug.wsHandshakeFailed')}: ${errMsg}`) if (errMsg.includes('origin not allowed')) { addLog(`${icon('lightbulb', 14)} ${t('chatDebug.fixOriginStillRejected')}`) } else { addLog(`${icon('lightbulb', 14)} ${t('chatDebug.fixSuggestManualRestart')}`) } } } } catch (e) { addLog(`${statusIcon('warn', 14)} ${t('chatDebug.wsParseFailed')}: ${e}`) } } ws.onerror = () => { addLog(`${statusIcon('err', 14)} ${t('chatDebug.fixWsConnFailed')}`) } ws.onclose = (e) => { if (e.code === 1008) { addLog(`${statusIcon('warn', 14)} ${t('chatDebug.fixOriginRejected1008')}`) addLog(`${icon('lightbulb', 14)} ${t('chatDebug.fixRetryHint')}`) } else if (e.code !== 1000) { addLog(`${statusIcon('warn', 14)} ${t('chatDebug.wsClosed')} - Code: ${e.code}`) } } } catch (e) { addLog(`${statusIcon('err', 14)} ${t('chatDebug.fixFailed')}: ${e}`) addLog(`${icon('lightbulb', 14)} ${t('chatDebug.fixSuggestManualRestart')}`) } finally { if (fixBtn) { fixBtn.disabled = false; fixBtn.textContent = t('chatDebug.btnFixPairing') } } }