mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-30 12:50:14 +08:00
fix: 修复所有页面 loading 动画未正确移除的问题
- chat-debug.js: loadDebugInfo 完成后正确调用 renderDebugInfo 移除 loading - agents.js: loadAgents 失败时显示错误信息替代 loading - dashboard.js: renderLogs 无日志时显示提示信息 - memory.js: loadFiles 失败时显示错误信息 - services.js: loadServices/loadRegistry/loadBackups 添加 loading 状态并在完成/失败时移除 - extensions.js: loadCftunnel/loadClawapp 添加 loading 状态并在完成/失败时移除 - models.js: loadConfig 添加 loading 状态并在失败时显示错误 - gateway.js: loadConfig 添加 loading 状态并在失败时显示错误 - logs.js: loadLog/searchLog 使用 loading-text 样式并在失败时显示错误 确保所有异步加载函数都: 1. 开始时显示 loading 状态 2. 成功时渲染数据(自动移除 loading) 3. 失败时显示错误信息(替代 loading)
This commit is contained in:
@@ -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: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/></svg>',
|
||||
extensions: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>',
|
||||
about: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
|
||||
debug: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/><circle cx="12" cy="12" r="3"/></svg>',
|
||||
}
|
||||
|
||||
let _delegated = false
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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'),
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
89
src/main.js
89
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() {
|
||||
|
||||
@@ -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 = '<div style="color:var(--error);padding:20px">加载失败: ' + e + '</div>'
|
||||
toast('加载 Agent 列表失败: ' + e, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
559
src/pages/chat-debug.js
Normal file
559
src/pages/chat-debug.js
Normal file
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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 = '<div style="color:var(--text-tertiary);padding:12px">暂无日志</div>'
|
||||
return
|
||||
}
|
||||
const lines = logs.trim().split('\n')
|
||||
logsEl.innerHTML = lines.map(l => `<div class="log-line">${escapeHtml(l)}</div>`).join('')
|
||||
logsEl.scrollTop = logsEl.scrollHeight
|
||||
|
||||
@@ -52,6 +52,7 @@ async function loadAll(page) {
|
||||
|
||||
async function loadCftunnel(page) {
|
||||
const el = page.querySelector('#cftunnel-content')
|
||||
el.innerHTML = '<div class="loading-text">加载中...</div>'
|
||||
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 = '<div class="loading-text">加载中...</div>'
|
||||
try {
|
||||
const status = await api.getClawappStatus()
|
||||
renderClawapp(el, status)
|
||||
|
||||
@@ -41,10 +41,13 @@ export async function render() {
|
||||
}
|
||||
|
||||
async function loadConfig(page, state) {
|
||||
const el = page.querySelector('#gateway-config')
|
||||
el.innerHTML = '<div class="loading-text">加载中...</div>'
|
||||
try {
|
||||
state.config = await api.readOpenclawConfig()
|
||||
renderConfig(page, state)
|
||||
} catch (e) {
|
||||
el.innerHTML = '<div style="color:var(--error);padding:20px">加载配置失败: ' + e + '</div>'
|
||||
toast('加载配置失败: ' + e, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export function cleanup() {
|
||||
|
||||
async function loadLog(page, logName) {
|
||||
const el = page.querySelector('#log-content')
|
||||
el.innerHTML = '<div style="color:var(--text-tertiary)">加载中...</div>'
|
||||
el.innerHTML = '<div class="loading-text">加载中...</div>'
|
||||
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 = '<div style="color:var(--error);padding:12px">加载日志失败: ' + e + '</div>'
|
||||
toast('加载日志失败: ' + e, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
async function searchLog(page, logName, query) {
|
||||
const el = page.querySelector('#log-content')
|
||||
el.innerHTML = '<div style="color:var(--text-tertiary)">搜索中...</div>'
|
||||
el.innerHTML = '<div class="loading-text">搜索中...</div>'
|
||||
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 => `<div class="log-line">${highlightMatch(escapeHtml(l), query)}</div>`).join('')
|
||||
} catch (e) {
|
||||
el.innerHTML = '<div style="color:var(--error);padding:12px">搜索失败: ' + e + '</div>'
|
||||
toast('搜索失败: ' + e, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ async function loadFiles(page, state) {
|
||||
}
|
||||
renderFileTree(page, state, files)
|
||||
} catch (e) {
|
||||
tree.innerHTML = '<div style="color:var(--error);padding:12px">加载失败: ' + e + '</div>'
|
||||
toast('加载文件列表失败: ' + e, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,11 +82,14 @@ export async function render() {
|
||||
}
|
||||
|
||||
async function loadConfig(page, state) {
|
||||
const listEl = page.querySelector('#providers-list')
|
||||
listEl.innerHTML = '<div class="loading-text">加载中...</div>'
|
||||
try {
|
||||
state.config = await api.readOpenclawConfig()
|
||||
renderDefaultBar(page, state)
|
||||
renderProviders(page, state)
|
||||
} catch (e) {
|
||||
listEl.innerHTML = '<div style="color:var(--error);padding:20px">加载配置失败: ' + e + '</div>'
|
||||
toast('加载配置失败: ' + e, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ const REGISTRIES = [
|
||||
|
||||
async function loadRegistry(page) {
|
||||
const bar = page.querySelector('#registry-bar')
|
||||
bar.innerHTML = '<div class="loading-text">加载中...</div>'
|
||||
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 = '<div class="loading-text">加载中...</div>'
|
||||
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 = '<div class="loading-text">加载中...</div>'
|
||||
try {
|
||||
const backups = await api.listBackups()
|
||||
renderBackups(list, backups)
|
||||
|
||||
@@ -164,7 +164,7 @@ function renderInstallSection() {
|
||||
function bindEvents(page, nodeOk) {
|
||||
// 进入面板
|
||||
page.querySelector('#btn-enter')?.addEventListener('click', () => {
|
||||
window.location.reload()
|
||||
window.location.hash = '/dashboard'
|
||||
})
|
||||
|
||||
// 一键安装
|
||||
|
||||
@@ -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]
|
||||
|
||||
58
src/style/debug.css
Normal file
58
src/style/debug.css
Normal file
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user