@@ -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 = `
<div class="page-header">
<h1 class="page-title">系统诊断</h1>
<p class="page-desc">全面检测系统状态,快速定位问题</p>
<div style="display:flex;gap:8px">
<button class="btn btn-primary btn-sm" id="btn-refresh">刷新状态</button>
<button class="btn btn-secondary btn-sm" id="btn-test-ws">测试 WebSocket</button>
<button class="btn btn-secondary btn-sm" id="btn-network-log">网络日志</button>
<button class="btn btn-warning btn-sm" id="btn-fix-pairing">一键修复配对</button>
</div>
</div>
<div id="debug-content"></div>
<div id="ws-test-log" style="display:none;margin-top:16px;background:var(--bg-secondary);border-radius:6px;padding:12px">
<div style="font-weight:600;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center">
<span>WebSocket 连接测试</span>
<button class="btn btn-sm" id="btn-clear-log" style="padding:4px 8px;font-size:11px">清空</button>
</div>
<pre id="ws-log-content" style="font-size:11px;line-height:1.5;max-height:400px;overflow:auto;margin:0;color:var(--text-primary)"></pre>
</div>
<div id="network-log" style="display:none;margin-top:16px;background:var(--bg-secondary);border-radius:6px;padding:12px">
<div style="font-weight:600;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center">
<span>网络请求日志(最近 100 条)</span>
<div style="display:flex;gap:8px">
<button class="btn btn-sm" id="btn-refresh-network" style="padding:4px 8px;font-size:11px">刷新</button>
<button class="btn btn-sm" id="btn-clear-network" style="padding:4px 8px;font-size:11px">清空</button>
</div>
</div>
<div id="network-log-content" style="font-size:11px;line-height:1.5;max-height:400px;overflow:auto"></div>
</div>
`
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 = '<div class="loading-text">检测中...</div>'
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 = ` <div style="font-family:monospace;font-size:12px;line-height:1.6"> `
// 总体状态概览
const allOk = info . appState . openclawReady && info . appState . gatewayRunning && info . wsClient . gatewayReady
html += ` <div class="config-section" style="background: ${ allOk ? 'var(--success-bg)' : 'var(--warning-bg)' } ;border-left:3px solid ${ allOk ? 'var(--success)' : 'var(--warning)' } ">
<div style="font-size:16px;font-weight:600;margin-bottom:8px"> ${ allOk ? '✅ 系统正常' : '⚠️ 发现问题' } </div>
<div style="color:var(--text-secondary);font-size:13px"> ${ allOk ? '所有核心功能运行正常' : '部分功能异常,请查看下方详情' } </div>
</div> `
// 应用状态
html += ` <div class="config-section">
<div class="config-section-title">应用状态</div>
<table class="debug-table">
<tr><td>OpenClaw 就绪</td><td> ${ info . appState . openclawReady ? '✅' : '❌' } </td></tr>
<tr><td>Gateway 运行中</td><td> ${ info . appState . gatewayRunning ? '✅' : '❌' } </td></tr>
</table>
</div> `
// WebSocket 状态
html += ` <div class="config-section">
<div class="config-section-title">WebSocket 连接</div>
<table class="debug-table">
<tr><td>连接状态</td><td> ${ info . wsClient . connected ? '✅ 已连接' : '❌ 未连接' } </td></tr>
<tr><td>握手状态</td><td> ${ info . wsClient . gatewayReady ? '✅ 已完成' : '❌ 未完成' } </td></tr>
<tr><td>会话密钥</td><td> ${ info . wsClient . sessionKey || '(空)' } </td></tr>
</table>
</div> `
// Node.js 环境
html += ` <div class="config-section">
<div class="config-section-title">Node.js 环境</div> `
if ( info . nodeError ) {
html += ` <div style="color:var(--error)">❌ ${ escapeHtml ( info . nodeError ) } </div> `
} else if ( info . node ) {
html += ` <table class="debug-table">
<tr><td>安装状态</td><td> ${ info . node . installed ? '✅ 已安装' : '❌ 未安装' } </td></tr>
<tr><td>版本</td><td> ${ info . node . version || '(未知)' } </td></tr>
</table> `
}
html += ` </div> `
// 版本信息
html += ` <div class="config-section">
<div class="config-section-title">版本信息</div> `
if ( info . versionError ) {
html += ` <div style="color:var(--error)">❌ ${ escapeHtml ( info . versionError ) } </div> `
} else if ( info . version ) {
html += ` <table class="debug-table">
<tr><td>当前版本</td><td> ${ info . version . current || '(未知)' } </td></tr>
<tr><td>最新版本</td><td> ${ info . version . latest || '(未检测)' } </td></tr>
<tr><td>更新可用</td><td> ${ info . version . update _available ? '⚠️ 有新版本' : '✅ 已是最新' } </td></tr>
</table> `
}
html += ` </div> `
// 配置文件
html += ` <div class="config-section">
<div class="config-section-title">配置文件</div> `
if ( info . configError ) {
html += ` <div style="color:var(--error)">❌ ${ escapeHtml ( info . configError ) } </div> `
} else if ( info . config ) {
const gw = info . config . gateway || { }
html += ` <table class="debug-table">
<tr><td>gateway.port</td><td> ${ gw . port || '(未设置)' } </td></tr>
<tr><td>gateway.auth.token</td><td> ${ gw . auth ? . token ? '✅ 已设置' : '⚠️ 未设置' } </td></tr>
<tr><td>gateway.enabled</td><td> ${ gw . enabled !== false ? '✅' : '❌' } </td></tr>
<tr><td>gateway.mode</td><td> ${ gw . mode || 'local' } </td></tr>
</table> `
}
html += ` </div> `
// 服务状态
html += ` <div class="config-section">
<div class="config-section-title">服务状态</div> `
if ( info . servicesError ) {
html += ` <div style="color:var(--error)">❌ ${ escapeHtml ( info . servicesError ) } </div> `
} else if ( info . services ? . length > 0 ) {
const svc = info . services [ 0 ]
html += ` <table class="debug-table">
<tr><td>CLI 安装</td><td> ${ svc . cli _installed !== false ? '✅ 已安装' : '❌ 未安装' } </td></tr>
<tr><td>运行状态</td><td> ${ svc . running ? '✅ 运行中' : '❌ 已停止' } </td></tr>
<tr><td>进程 PID</td><td> ${ svc . pid || '(无)' } </td></tr>
<tr><td>服务标签</td><td> ${ svc . label || '(未知)' } </td></tr>
</table> `
}
html += ` </div> `
// 设备密钥
html += ` <div class="config-section">
<div class="config-section-title">设备密钥 & 握手签名</div> `
if ( info . connectFrameError ) {
html += ` <div style="color:var(--error)">❌ ${ escapeHtml ( info . connectFrameError ) } </div> `
} else if ( info . connectFrame ) {
const device = info . connectFrame . params ? . device
html += ` <div style="color:var(--success);margin-bottom:8px">✅ 设备密钥生成成功</div>
<table class="debug-table">
<tr><td>设备 ID</td><td style="font-size:10px;word-break:break-all"> ${ device ? . id || '(无)' } </td></tr>
<tr><td>公钥</td><td style="font-size:10px;word-break:break-all"> ${ device ? . publicKey ? device . publicKey . substring ( 0 , 32 ) + '...' : '(无)' } </td></tr>
<tr><td>签名时间</td><td> ${ device ? . signedAt || '(无)' } </td></tr>
</table>
<details style="margin-top:8px">
<summary style="cursor:pointer;color:var(--text-secondary);font-size:12px">查看完整 Connect Frame</summary>
<pre style="background:var(--bg-secondary);padding:8px;border-radius:4px;overflow:auto;max-height:300px;font-size:11px"> ${ escapeHtml ( JSON . stringify ( info . connectFrame , null , 2 ) ) } </pre>
</details> `
}
html += ` </div> `
// 诊断建议
html += ` <div class="config-section">
<div class="config-section-title">诊断建议</div>
<ul style="margin:0;padding-left:20px;color:var(--text-secondary);font-size:13px"> `
if ( ! info . node ? . installed ) {
html += ` <li style="color:var(--error);margin-bottom:6px">❌ Node.js 未安装,请先安装 Node.js( <a href="https://nodejs.org/" target="_blank" rel="noopener">下载地址</a>) </li> `
}
if ( info . configError ) {
html += ` <li style="color:var(--error);margin-bottom:6px">❌ 配置文件不存在或损坏,请前往"初始设置"页面完成配置</li> `
}
if ( info . servicesError || ! info . services ? . length || info . services [ 0 ] ? . cli _installed === false ) {
html += ` <li style="color:var(--error);margin-bottom:6px">❌ OpenClaw CLI 未安装,请前往"初始设置"页面安装</li> `
}
if ( info . services ? . length > 0 && ! info . services [ 0 ] ? . running ) {
html += ` <li style="color:var(--warning);margin-bottom:6px">⚠️ Gateway 未启动,请前往"服务管理"页面启动服务</li> `
}
if ( info . config && ! info . config . gateway ? . auth ? . token ) {
html += ` <li style="color:var(--warning);margin-bottom:6px">⚠️ Gateway token 未设置(本地开发可选,生产环境建议设置)</li> `
}
if ( info . connectFrameError ) {
html += ` <li style="color:var(--error);margin-bottom:6px">❌ 设备密钥生成失败,请检查 Rust 后端日志</li> `
}
if ( ! info . wsClient . connected && info . services ? . length > 0 && info . services [ 0 ] ? . running ) {
html += ` <li style="color:var(--warning);margin-bottom:6px">⚠️ Gateway 已启动但 WebSocket 未连接,请检查端口 ${ info . config ? . gateway ? . port || 18789 } 是否被占用</li> `
}
if ( info . wsClient . connected && ! info . wsClient . gatewayReady ) {
html += ` <li style="color:var(--warning);margin-bottom:6px">⚠️ WebSocket 已连接但握手未完成,请检查 token 是否正确</li> `
}
if ( allOk ) {
html += ` <li style="color:var(--success);margin-bottom:6px">✅ 所有检测项正常,系统运行良好</li> `
}
html += ` </ul></div> `
html += ` <div style="margin-top:16px;padding:8px;background:var(--bg-secondary);border-radius:4px;font-size:11px;color:var(--text-tertiary)">检测时间: ${ info . timestamp } </div> `
html += ` </div> `
el . innerHTML = html
}
function escapeHtml ( str ) {
if ( ! str ) return ''
return String ( str )
. replace ( /&/g , '&' )
. 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 = '<div style="color:var(--text-secondary);padding:8px">暂无请求记录</div>'
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 = `
<div style="padding:8px;background:var(--bg-primary);border-radius:4px;margin-bottom:8px;font-size:12px">
<div style="display:flex;gap:16px">
<span>总请求: <strong> ${ total } </strong></span>
<span>缓存命中: <strong> ${ cached } </strong></span>
<span>平均耗时: <strong> ${ avgDuration . toFixed ( 0 ) } ms</strong></span>
</div>
</div>
<table class="debug-table" style="width:100%;font-size:11px">
<thead>
<tr style="background:var(--bg-primary)">
<th style="padding:6px;text-align:left;width:80px">时间</th>
<th style="padding:6px;text-align:left">命令</th>
<th style="padding:6px;text-align:left;max-width:200px">参数</th>
<th style="padding:6px;text-align:right;width:80px">耗时</th>
<th style="padding:6px;text-align:center;width:60px">缓存</th>
</tr>
</thead>
<tbody>
`
// 倒序显示(最新的在上面)
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 += `
<tr>
<td style="padding:4px;color:var(--text-tertiary)"> ${ log . time } </td>
<td style="padding:4px;font-family:monospace"> ${ escapeHtml ( log . cmd ) } </td>
<td style="padding:4px;font-family:monospace;font-size:10px;color:var(--text-secondary);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title=" ${ escapeHtml ( log . args ) } "> ${ escapeHtml ( log . args ) } </td>
<td style="padding:4px;text-align:right;color: ${ durationColor } "> ${ log . duration } </td>
<td style="padding:4px;text-align:center"> ${ cachedIcon } </td>
</tr>
`
}
html += ` </tbody></table> `
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' )
}
}