feat: v0.6.0 — 公益AI接口 + Agent灵魂借尸还魂 + 知识库 + 全局AI诊断 + 官网改版

This commit is contained in:
晴天
2026-03-07 19:36:25 +08:00
parent b09f48f0dd
commit 0752dc2a71
55 changed files with 4346 additions and 480 deletions

View File

@@ -6,6 +6,7 @@ import { api } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { showUpgradeModal } from '../components/modal.js'
import { setUpgrading } from '../lib/app-state.js'
import { icon, statusIcon } from '../lib/icons.js'
export async function render() {
const page = document.createElement('div')
@@ -135,8 +136,23 @@ async function loadData(page) {
modal.setDone(typeof msg === 'string' ? msg : (msg?.message || '升级完成'))
loadData(page)
} catch (e) {
modal.appendLog(String(e))
modal.setError('升级失败')
const errStr = String(e)
modal.appendLog(errStr)
const { diagnoseInstallError } = await import('../lib/error-diagnosis.js')
const fullLog = modal.getLogText() + '\n' + errStr
const diagnosis = diagnoseInstallError(fullLog)
modal.setError(diagnosis.title)
if (diagnosis.hint) modal.appendLog('')
if (diagnosis.hint) modal.appendHtmlLog(`${statusIcon('info', 14)} ${diagnosis.hint}`)
if (diagnosis.command) modal.appendHtmlLog(`${icon('clipboard', 14)} ${diagnosis.command}`)
if (window.__openAIDrawerWithError) {
window.__openAIDrawerWithError({
title: diagnosis.title,
error: fullLog,
scene: '升级 OpenClaw',
hint: diagnosis.hint,
})
}
} finally {
setUpgrading(false)
unlistenLog?.()
@@ -173,11 +189,16 @@ function renderCommunity(page) {
<img src="/images/OpenClawWx.png" alt="微信交流群" style="width:140px;height:140px;border-radius:var(--radius-md);border:1px solid var(--border-primary)">
<div style="font-size:var(--font-size-sm);margin-top:8px;color:var(--text-secondary)">微信交流群</div>
</div>
<div style="text-align:center">
<img src="https://qt.cool/c/OpenClawDY/qr.png" alt="抖音交流群" style="width:140px;height:140px;border-radius:var(--radius-md);border:1px solid var(--border-primary);object-fit:contain;background:#fff">
<div style="font-size:var(--font-size-sm);margin-top:8px;color:var(--text-secondary)">抖音交流群</div>
</div>
<div style="flex:1;min-width:200px;display:flex;flex-direction:column;gap:8px;padding-top:4px">
<div style="font-size:var(--font-size-sm);color:var(--text-secondary)">扫码或点击链接加入交流群,反馈问题、获取帮助</div>
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px">
<a class="btn btn-primary btn-sm" href="https://qt.cool/c/OpenClaw" target="_blank" rel="noopener">加入 QQ 群</a>
<a class="btn btn-primary btn-sm" href="https://qt.cool/c/OpenClawWx" target="_blank" rel="noopener">加入微信群</a>
<a class="btn btn-primary btn-sm" href="https://qt.cool/c/OpenClawDY" target="_blank" rel="noopener">加入抖音群</a>
<a class="btn btn-secondary btn-sm" href="https://yb.tencent.com/gp/i/LsvIw7mdR7Lb" target="_blank" rel="noopener">元宝派社群</a>
</div>
<div style="font-size:var(--font-size-xs);color:var(--text-tertiary);margin-top:8px">
@@ -194,6 +215,11 @@ const PROJECTS = [
desc: 'AI Agent 框架,支持多模型协作、工具调用、记忆管理',
url: 'https://github.com/openclaw/openclaw',
},
{
name: 'OpenClaw-zh',
desc: 'AI Agent 框架,支持多模型协作、工具调用、记忆管理-中文优化版',
url: 'https://github.com/1186258278/OpenClawChineseTranslation',
},
{
name: 'ClawApp',
desc: '跨平台移动聊天客户端H5 + 代理服务器架构,支持离线和流式传输',

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@
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 { icon, statusIcon } from '../lib/icons.js'
export async function render() {
const page = document.createElement('div')
@@ -112,7 +113,7 @@ function renderDebugInfo(el, info) {
// 总体状态概览
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="font-size:16px;font-weight:600;margin-bottom:8px">${allOk ? `${statusIcon('ok')} 系统正常` : `${statusIcon('warn')} 发现问题`}</div>
<div style="color:var(--text-secondary);font-size:13px">${allOk ? '所有核心功能运行正常' : '部分功能异常,请查看下方详情'}</div>
</div>`
@@ -120,8 +121,8 @@ function renderDebugInfo(el, info) {
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>
<tr><td>OpenClaw 就绪</td><td>${info.appState.openclawReady ? statusIcon('ok') : statusIcon('err')}</td></tr>
<tr><td>Gateway 运行中</td><td>${info.appState.gatewayRunning ? statusIcon('ok') : statusIcon('err')}</td></tr>
</table>
</div>`
@@ -129,8 +130,8 @@ function renderDebugInfo(el, info) {
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.connected ? `${statusIcon('ok')} 已连接` : `${statusIcon('err')} 未连接`}</td></tr>
<tr><td>握手状态</td><td>${info.wsClient.gatewayReady ? `${statusIcon('ok')} 已完成` : `${statusIcon('err')} 未完成`}</td></tr>
<tr><td>会话密钥</td><td>${info.wsClient.sessionKey || '(空)'}</td></tr>
</table>
</div>`
@@ -139,10 +140,10 @@ function renderDebugInfo(el, info) {
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>`
html += `<div style="color:var(--error)">${statusIcon('err')} ${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.installed ? `${statusIcon('ok')} 已安装` : `${statusIcon('err')} 未安装`}</td></tr>
<tr><td>版本</td><td>${info.node.version || '(未知)'}</td></tr>
</table>`
}
@@ -152,12 +153,12 @@ function renderDebugInfo(el, info) {
html += `<div class="config-section">
<div class="config-section-title">版本信息</div>`
if (info.versionError) {
html += `<div style="color:var(--error)"> ${escapeHtml(info.versionError)}</div>`
html += `<div style="color:var(--error)">${statusIcon('err')} ${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>
<tr><td>更新可用</td><td>${info.version.update_available ? `${statusIcon('warn')} 有新版本` : `${statusIcon('ok')} 已是最新`}</td></tr>
</table>`
}
html += `</div>`
@@ -166,13 +167,13 @@ function renderDebugInfo(el, info) {
html += `<div class="config-section">
<div class="config-section-title">配置文件</div>`
if (info.configError) {
html += `<div style="color:var(--error)"> ${escapeHtml(info.configError)}</div>`
html += `<div style="color:var(--error)">${statusIcon('err')} ${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.auth.token</td><td>${gw.auth?.token ? `${statusIcon('ok')} 已设置` : `${statusIcon('warn')} 未设置`}</td></tr>
<tr><td>gateway.enabled</td><td>${gw.enabled !== false ? statusIcon('ok') : statusIcon('err')}</td></tr>
<tr><td>gateway.mode</td><td>${gw.mode || 'local'}</td></tr>
</table>`
}
@@ -182,12 +183,12 @@ function renderDebugInfo(el, info) {
html += `<div class="config-section">
<div class="config-section-title">服务状态</div>`
if (info.servicesError) {
html += `<div style="color:var(--error)"> ${escapeHtml(info.servicesError)}</div>`
html += `<div style="color:var(--error)">${statusIcon('err')} ${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>CLI 安装</td><td>${svc.cli_installed !== false ? `${statusIcon('ok')} 已安装` : `${statusIcon('err')} 未安装`}</td></tr>
<tr><td>运行状态</td><td>${svc.running ? `${statusIcon('ok')} 运行中` : `${statusIcon('err')} 已停止`}</td></tr>
<tr><td>进程 PID</td><td>${svc.pid || '(无)'}</td></tr>
<tr><td>服务标签</td><td>${svc.label || '(未知)'}</td></tr>
</table>`
@@ -198,10 +199,10 @@ function renderDebugInfo(el, info) {
html += `<div class="config-section">
<div class="config-section-title">设备密钥 & 握手签名</div>`
if (info.connectFrameError) {
html += `<div style="color:var(--error)"> ${escapeHtml(info.connectFrameError)}</div>`
html += `<div style="color:var(--error)">${statusIcon('err')} ${escapeHtml(info.connectFrameError)}</div>`
} else if (info.connectFrame) {
const device = info.connectFrame.params?.device
html += `<div style="color:var(--success);margin-bottom:8px"> 设备密钥生成成功</div>
html += `<div style="color:var(--success);margin-bottom:8px">${statusIcon('ok')} 设备密钥生成成功</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>
@@ -220,31 +221,31 @@ function renderDebugInfo(el, info) {
<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>`
html += `<li style="color:var(--error);margin-bottom:6px">${statusIcon('err')} 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>`
html += `<li style="color:var(--error);margin-bottom:6px">${statusIcon('err')} 配置文件不存在或损坏,请前往"初始设置"页面完成配置</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>`
html += `<li style="color:var(--error);margin-bottom:6px">${statusIcon('err')} OpenClaw CLI 未安装,请前往"初始设置"页面安装</li>`
}
if (info.services?.length > 0 && !info.services[0]?.running) {
html += `<li style="color:var(--warning);margin-bottom:6px">⚠️ Gateway 未启动,请前往"服务管理"页面启动服务</li>`
html += `<li style="color:var(--warning);margin-bottom:6px">${statusIcon('warn')} Gateway 未启动,请前往"服务管理"页面启动服务</li>`
}
if (info.config && !info.config.gateway?.auth?.token) {
html += `<li style="color:var(--warning);margin-bottom:6px">⚠️ Gateway token 未设置(本地开发可选,生产环境建议设置)</li>`
html += `<li style="color:var(--warning);margin-bottom:6px">${statusIcon('warn')} Gateway token 未设置(本地开发可选,生产环境建议设置)</li>`
}
if (info.connectFrameError) {
html += `<li style="color:var(--error);margin-bottom:6px"> 设备密钥生成失败,请检查 Rust 后端日志</li>`
html += `<li style="color:var(--error);margin-bottom:6px">${statusIcon('err')} 设备密钥生成失败,请检查 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 未连接,常见原因:<strong>origin not allowed</strong>Tauri origin 未在白名单)或端口 ${info.config?.gateway?.port || 18789} 被占用。点击“一键修复配对”可自动修复 origin 问题</li>`
html += `<li style="color:var(--warning);margin-bottom:6px">${statusIcon('warn')} Gateway 运行中但 WebSocket 未连接,常见原因:<strong>origin not allowed</strong>Tauri origin 未在白名单)或端口 ${info.config?.gateway?.port || 18789} 被占用。点击“一键修复配对”可自动修复 origin 问题</li>`
}
if (info.wsClient.connected && !info.wsClient.gatewayReady) {
html += `<li style="color:var(--warning);margin-bottom:6px">⚠️ WebSocket 已连接但握手未完成,请检查 token 是否正确</li>`
html += `<li style="color:var(--warning);margin-bottom:6px">${statusIcon('warn')} WebSocket 已连接但握手未完成,请检查 token 是否正确</li>`
}
if (allOk) {
html += `<li style="color:var(--success);margin-bottom:6px"> 所有检测项正常,系统运行良好</li>`
html += `<li style="color:var(--success);margin-bottom:6px">${statusIcon('ok')} 所有检测项正常,系统运行良好</li>`
}
html += `</ul></div>`
@@ -277,10 +278,10 @@ function testWebSocket(page) {
clearBtn.onclick = () => {
testLogs = []
contentEl.textContent = ''
contentEl.innerHTML = ''
}
addLog('🔍 开始 WebSocket 连接测试...')
addLog(`${icon('search', 14)} 开始 WebSocket 连接测试...`)
// 关闭旧连接
if (testWs) {
@@ -295,86 +296,88 @@ function testWebSocket(page) {
const wsHost = window.__TAURI_INTERNALS__ ? `127.0.0.1:${port}` : location.host
const url = `ws://${wsHost}/ws?token=${encodeURIComponent(token)}`
addLog(`📡 连接地址: ${url}`)
addLog(`🔑 Token: ${token ? token.substring(0, 20) + '...' : '(空)'}`)
addLog(` 正在连接...`)
addLog(`${icon('radio', 14)} 连接地址: ${url}`)
addLog(`${icon('key', 14)} Token: ${token ? token.substring(0, 20) + '...' : '(空)'}`)
addLog(`${icon('clock', 14)} 正在连接...`)
try {
testWs = new WebSocket(url)
testWs.onopen = () => {
addLog('✅ WebSocket 连接成功')
addLog('⏳ 等待 Gateway 发送 connect.challenge...')
addLog(`${statusIcon('ok', 14)} WebSocket 连接成功`)
addLog(`${icon('clock', 14)} 等待 Gateway 发送 connect.challenge...`)
}
testWs.onmessage = (evt) => {
try {
const msg = JSON.parse(evt.data)
addLog(`📥 收到消息: ${JSON.stringify(msg, null, 2)}`)
addLog(`${icon('inbox', 14)} 收到消息: ${escapeHtml(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...`)
addLog(`${icon('lock', 14)} 收到 challenge, nonce: ${nonce}`)
addLog(`${icon('clock', 14)} 生成 connect frame...`)
api.createConnectFrame(nonce, token).then(frame => {
addLog(` Connect frame 生成成功`)
addLog(`📤 发送 connect frame: ${JSON.stringify(frame, null, 2)}`)
addLog(`${statusIcon('ok', 14)} Connect frame 生成成功`)
addLog(`${icon('send', 14)} 发送 connect frame: ${escapeHtml(JSON.stringify(frame, null, 2))}`)
testWs.send(JSON.stringify(frame))
}).catch(e => {
addLog(` 生成 connect frame 失败: ${e}`)
addLog(`${statusIcon('err', 14)} 生成 connect frame 失败: ${e}`)
})
}
// 如果收到 connect 响应
if (msg.type === 'res' && msg.id?.startsWith('connect-')) {
if (msg.ok) {
addLog(` 握手成功!`)
addLog(`📊 Snapshot: ${JSON.stringify(msg.payload, null, 2)}`)
addLog(`${statusIcon('ok', 14)} 握手成功!`)
addLog(`${icon('bar-chart', 14)} Snapshot: ${escapeHtml(JSON.stringify(msg.payload, null, 2))}`)
const sessionKey = msg.payload?.snapshot?.sessionDefaults?.mainSessionKey
if (sessionKey) {
addLog(`🔑 Session Key: ${sessionKey}`)
addLog(`${icon('key', 14)} Session Key: ${sessionKey}`)
}
} else {
addLog(` 握手失败: ${msg.error?.message || msg.error?.code || '未知错误'}`)
addLog(`${statusIcon('err', 14)} 握手失败: ${msg.error?.message || msg.error?.code || '未知错误'}`)
}
}
} catch (e) {
addLog(`⚠️ 解析消息失败: ${e}`)
addLog(`📥 原始数据: ${evt.data}`)
addLog(`${statusIcon('warn', 14)} 解析消息失败: ${e}`)
addLog(`${icon('inbox', 14)} 原始数据: ${escapeHtml(evt.data)}`)
}
}
testWs.onerror = (e) => {
addLog(` WebSocket 错误: ${e.type}`)
addLog(`${statusIcon('err', 14)} WebSocket 错误: ${e.type}`)
}
testWs.onclose = (e) => {
addLog(`🔌 连接关闭 - Code: ${e.code}, Reason: ${e.reason || '(空)'}`)
addLog(`${icon('plug', 14)} 连接关闭 - Code: ${e.code}, Reason: ${e.reason || '(空)'}`)
if (e.code === 1008) {
addLog(` origin not allowed (1008) - Gateway 拒绝了当前应用的 origin`)
addLog(`💡 解决方法:点击“一键修复配对”,将自动将 tauri://localhost 加入白名单并重启 Gateway`)
addLog(`${statusIcon('err', 14)} origin not allowed (1008) - Gateway 拒绝了当前应用的 origin`)
addLog(`${icon('lightbulb', 14)} 解决方法:点击“一键修复配对”,将自动将 tauri://localhost 加入白名单并重启 Gateway`)
} else if (e.code === 4001) {
addLog(` 认证失败 (4001) - Token 可能不正确`)
addLog(`${statusIcon('err', 14)} 认证失败 (4001) - Token 可能不正确`)
} else if (e.code === 1006) {
addLog(`⚠️ 异常关闭 (1006) - 可能是网络问题或 Gateway 主动断开`)
addLog(`${statusIcon('warn', 14)} 异常关闭 (1006) - 可能是网络问题或 Gateway 主动断开`)
}
testWs = null
}
} catch (e) {
addLog(` 创建 WebSocket 失败: ${e}`)
addLog(`${statusIcon('err', 14)} 创建 WebSocket 失败: ${e}`)
}
}).catch(e => {
addLog(` 读取配置失败: ${e}`)
addLog(`${statusIcon('err', 14)} 读取配置失败: ${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')
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 = `<span style="color:var(--text-tertiary);flex-shrink:0">[${timestamp}]</span> ${msg}`
testLogs.push(div.textContent)
contentEl.appendChild(div)
contentEl.scrollTop = contentEl.scrollHeight
}
}
@@ -440,7 +443,7 @@ function renderNetworkLog(contentEl) {
// 倒序显示(最新的在上面)
for (let i = logs.length - 1; i >= 0; i--) {
const log = logs[i]
const cachedIcon = log.cached ? '✅' : '-'
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)'))
@@ -480,36 +483,36 @@ async function fixPairing(page) {
}
try {
addLog('🔧 开始修复配对问题...')
addLog(`${icon('wrench', 14)} 开始修复配对问题...`)
// 1. 写入 paired.json + controlUi.allowedOrigins
addLog('📝 正在写入设备配对信息 + Gateway origin 白名单...')
addLog(`${icon('edit', 14)} 正在写入设备配对信息 + Gateway origin 白名单...`)
const result = await api.autoPairDevice()
addLog(` ${result}`)
addLog('✅ 已将 tauri://localhost 加入 gateway.controlUi.allowedOrigins')
addLog(`${statusIcon('ok', 14)} ${result}`)
addLog(`${statusIcon('ok', 14)} 已将 tauri://localhost 加入 gateway.controlUi.allowedOrigins`)
// 2. 重启 Gateway
addLog('🔄 重启 Gateway 服务...')
addLog(`${icon('zap', 14)} 重启 Gateway 服务...`)
await api.restartService('ai.openclaw.gateway')
addLog('✅ Gateway 重启命令已发送')
addLog(`${statusIcon('ok', 14)} Gateway 重启命令已发送`)
// 3. 等待 Gateway 启动
addLog('⏳ 等待 Gateway 启动8秒...')
addLog(`${icon('clock', 14)} 等待 Gateway 启动8秒...`)
await new Promise(resolve => setTimeout(resolve, 8000))
// 4. 检查 Gateway 状态
addLog('🔍 检查 Gateway 状态...')
addLog(`${icon('search', 14)} 检查 Gateway 状态...`)
const services = await api.getServicesStatus()
const running = services?.[0]?.running
if (running) {
addLog('✅ Gateway 已启动')
addLog(`${statusIcon('ok', 14)} Gateway 已启动`)
} else {
addLog('⚠️ Gateway 可能还在启动中,请稍后手动测试')
addLog(`${statusIcon('warn', 14)} Gateway 可能还在启动中,请稍后手动测试`)
}
// 5. 测试 WebSocket 连接
addLog('🔌 测试 WebSocket 连接...')
addLog(`${icon('plug', 14)} 测试 WebSocket 连接...`)
const config = await api.readOpenclawConfig()
const port = config?.gateway?.port || 18789
const token = config?.gateway?.auth?.token || ''
@@ -519,62 +522,63 @@ async function fixPairing(page) {
const ws = new WebSocket(url)
ws.onopen = () => {
addLog('✅ WebSocket 连接成功')
addLog(`${statusIcon('ok', 14)} WebSocket 连接成功`)
}
ws.onmessage = (evt) => {
try {
const msg = JSON.parse(evt.data)
if (msg.type === 'event' && msg.event === 'connect.challenge') {
addLog('✅ 收到 connect.challenge')
addLog(`${statusIcon('ok', 14)} 收到 connect.challenge`)
const nonce = msg.payload?.nonce || ''
api.createConnectFrame(nonce, token).then(frame => {
ws.send(JSON.stringify(frame))
addLog('📤 已发送 connect frame')
addLog(`${icon('send', 14)} 已发送 connect frame`)
})
}
if (msg.type === 'res' && msg.id?.startsWith('connect-')) {
if (msg.ok) {
addLog('🎉 握手成功!配对问题已修复!')
addLog('💡 正在重新建立主应用 WebSocket 连接...')
addLog(`${statusIcon('ok', 14)} 握手成功!配对问题已修复!`)
addLog(`${icon('lightbulb', 14)} 正在重新建立主应用 WebSocket 连接...`)
ws.close(1000)
// 触发主应用的 wsClient 重连,让主界面正常工作
wsClient.reconnect()
setTimeout(() => loadDebugInfo(page), 2000)
} else {
const errMsg = msg.error?.message || msg.error?.code || '未知错误'
addLog(` 握手失败: ${errMsg}`)
addLog(`${statusIcon('err', 14)} 握手失败: ${errMsg}`)
if (errMsg.includes('origin not allowed')) {
addLog('💡 原因Gateway 拒绝了当前应用的 origin需要重启 Gateway 再试')
addLog(`${icon('lightbulb', 14)} 原因Gateway 拒绝了当前应用的 origin需要重启 Gateway 再试`)
} else {
addLog('💡 建议:请手动前往“服务管理”页面重启 Gateway')
addLog(`${icon('lightbulb', 14)} 建议:请手动前往“服务管理”页面重启 Gateway`)
}
}
}
} catch (e) {
addLog(`⚠️ 解析消息失败: ${e}`)
addLog(`${statusIcon('warn', 14)} 解析消息失败: ${e}`)
}
}
ws.onerror = () => {
addLog('❌ WebSocket 连接失败,请确认 Gateway 已在运行')
addLog(`${statusIcon('err', 14)} WebSocket 连接失败,请确认 Gateway 已在运行`)
}
ws.onclose = (e) => {
if (e.code === 1008) {
addLog(`⚠️ 连接被拒绝 (1008) - Gateway 拒绝了当前 origin`)
addLog('💡 该问题应已被本次修复流程处理,请再次点击“一键修复配对”')
addLog(`${statusIcon('warn', 14)} 连接被拒绝 (1008) - Gateway 拒绝了当前 origin`)
addLog(`${icon('lightbulb', 14)} 该问题应已被本次修复流程处理,请再次点击“一键修复配对”`)
} else if (e.code !== 1000) {
addLog(`⚠️ 连接关闭 - Code: ${e.code}`)
addLog(`${statusIcon('warn', 14)} 连接关闭 - Code: ${e.code}`)
}
}
} catch (e) {
addLog(` 修复失败: ${e}`)
addLog('💡 建议:请手动前往"服务管理"页面重启 Gateway')
addLog(`${statusIcon('err', 14)} 修复失败: ${e}`)
addLog(`${icon('lightbulb', 14)} 建议:请手动前往"服务管理"页面重启 Gateway`)
} finally {
if (fixBtn) { fixBtn.disabled = false; fixBtn.textContent = '一键修复配对' }
}
}

View File

@@ -9,6 +9,7 @@ import { renderMarkdown } from '../lib/markdown.js'
import { saveMessage, saveMessages, getLocalMessages, isStorageAvailable } from '../lib/message-db.js'
import { toast } from '../components/toast.js'
import { showModal, showConfirm } from '../components/modal.js'
import { icon as svgIcon } from '../lib/icons.js'
const RENDER_THROTTLE = 30
const STORAGE_SESSION_KEY = 'clawpanel-last-session'
@@ -134,11 +135,41 @@ export async function render() {
bindEvents(page)
bindConnectOverlay(page)
// 首次使用引导提示
showPageGuide(_messagesEl)
// 非阻塞:先返回 DOM后台连接 Gateway
connectGateway()
return page
}
const GUIDE_KEY = 'clawpanel-guide-chat-dismissed'
function showPageGuide(container) {
if (localStorage.getItem(GUIDE_KEY)) return
const guide = document.createElement('div')
guide.className = 'chat-page-guide'
guide.innerHTML = `
<div class="chat-guide-inner">
<div class="chat-guide-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="28" height="28"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
</div>
<div class="chat-guide-content">
<b>你正在使用「实时聊天」</b>
<p>此页面通过 <b>Gateway</b> 连接 OpenClaw 的 AI Agent对话由你部署的 OpenClaw 服务处理。</p>
<p style="opacity:0.7;font-size:11px">如需使用 ClawPanel 内置 AI 助手(独立于 OpenClaw请前往左侧菜单「AI 助手」页面。</p>
</div>
<button class="chat-guide-close" title="知道了">&times;</button>
</div>
`
guide.querySelector('.chat-guide-close').onclick = () => {
localStorage.setItem(GUIDE_KEY, '1')
guide.remove()
}
container.insertBefore(guide, container.firstChild)
}
// ── 事件绑定 ──
function bindEvents(page) {
@@ -1101,7 +1132,7 @@ function appendUserMessage(text, attachments = [], msgTime) {
} else if (att.fileName || att.name) {
const card = document.createElement('div')
card.className = 'msg-file-card'
card.innerHTML = `<span class="msg-file-icon">📎</span><span class="msg-file-name">${att.fileName || att.name}</span>`
card.innerHTML = `<span class="msg-file-icon">${svgIcon('paperclip', 16)}</span><span class="msg-file-name">${att.fileName || att.name}</span>`
mediaContainer.appendChild(card)
}
})
@@ -1212,10 +1243,10 @@ function appendFilesToEl(el, files) {
const card = document.createElement('div')
card.className = 'msg-file-card'
const ext = (f.name || '').split('.').pop().toLowerCase()
const iconMap = { pdf: '📄', doc: '📝', docx: '📝', txt: '📃', md: '📃', json: '📋', csv: '📊', zip: '📦', rar: '📦' }
const icon = iconMap[ext] || '📎'
const fileIconMap = { pdf: 'file', doc: 'file-text', docx: 'file-text', txt: 'file-plain', md: 'file-plain', json: 'clipboard', csv: 'bar-chart', zip: 'package', rar: 'package' }
const fileIcon = svgIcon(fileIconMap[ext] || 'paperclip', 16)
const size = f.size ? formatFileSize(f.size) : ''
card.innerHTML = `<span class="msg-file-icon">${icon}</span><div class="msg-file-info"><span class="msg-file-name">${f.name || '文件'}</span>${size ? `<span class="msg-file-size">${size}</span>` : ''}</div>`
card.innerHTML = `<span class="msg-file-icon">${fileIcon}</span><div class="msg-file-info"><span class="msg-file-name">${f.name || '文件'}</span>${size ? `<span class="msg-file-size">${size}</span>` : ''}</div>`
if (f.url) {
card.style.cursor = 'pointer'
card.onclick = () => window.open(f.url, '_blank')

View File

@@ -4,6 +4,7 @@
import { api } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { onGatewayChange } from '../lib/app-state.js'
import { navigate } from '../router.js'
let _unsubGw = null
@@ -36,6 +37,9 @@ export async function render() {
</div>
`
// 绑定事件(只绑一次)
bindActions(page)
// 异步加载数据
loadDashboardData(page)
@@ -94,7 +98,6 @@ async function loadDashboardData(page) {
}
renderStatCards(page, services, version, [], config, null)
bindActions(page)
// 第二波Agent、隧道、MCP、ClawApp、备份 → 更新卡片 + 渲染总览
const [agentsRes, tunnelRes, mcpRes, clawappRes, backupsRes] = await secondaryP
@@ -195,8 +198,14 @@ function renderOverview(page, services, clawapp, tunnel, mcpConfig, backups, con
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
Gateway 核心网关
</div>
<div class="overview-value" style="color: ${gw?.running ? 'var(--success)' : 'var(--error)'}">
${gw?.running ? '运行中' : '已停止'}
<div class="overview-actions">
<span class="overview-status" style="color: ${gw?.running ? 'var(--success)' : 'var(--error)'}">
${gw?.running ? '运行中' : '已停止'}
</span>
${gw?.running
? '<button class="btn btn-danger btn-xs" data-action="stop-gw">停止</button><button class="btn btn-secondary btn-xs" data-action="restart-gw">重启</button>'
: '<button class="btn btn-primary btn-xs" data-action="start-gw">启动</button>'
}
</div>
</div>
<div class="overview-item">
@@ -204,8 +213,16 @@ function renderOverview(page, services, clawapp, tunnel, mcpConfig, backups, con
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
ClawApp 守护进程
</div>
<div class="overview-value" style="color: ${clawapp?.running ? 'var(--success)' : 'var(--error)'}">
${clawapp?.running ? '端口 ' + clawapp.port : '未启动'}
<div class="overview-actions">
<span class="overview-status" style="color: ${clawapp?.running ? 'var(--success)' : 'var(--error)'}">
${clawapp?.running ? '端口 ' + clawapp.port : '未启动'}
</span>
${clawapp?.installed
? (clawapp?.running
? `<a class="btn btn-primary btn-xs" href="${clawapp.url || 'http://localhost:3210'}" target="_blank" rel="noopener">打开</a>`
: '<button class="btn btn-secondary btn-xs" data-action="goto-extensions">前往管理</button>')
: '<button class="btn btn-secondary btn-xs" data-action="goto-extensions">去安装</button>'
}
</div>
</div>
<div class="overview-item">
@@ -286,6 +303,44 @@ function bindActions(page) {
const btnUpdate = page.querySelector('#btn-check-update')
const btnCreateBackup = page.querySelector('#btn-create-backup')
// 概览区域的 Gateway 启动/停止/重启 + ClawApp 导航
page.addEventListener('click', async (e) => {
const actionBtn = e.target.closest('[data-action]')
if (!actionBtn) return
const action = actionBtn.dataset.action
if (action === 'start-gw') {
actionBtn.disabled = true; actionBtn.textContent = '启动中...'
try {
await api.startService('ai.openclaw.gateway')
toast('Gateway 启动指令已发送', 'success')
setTimeout(() => loadDashboardData(page), 2000)
} catch (err) { toast('启动失败: ' + err, 'error') }
finally { actionBtn.disabled = false; actionBtn.textContent = '启动' }
}
if (action === 'stop-gw') {
actionBtn.disabled = true; actionBtn.textContent = '停止中...'
try {
await api.stopService('ai.openclaw.gateway')
toast('Gateway 已停止', 'success')
setTimeout(() => loadDashboardData(page), 1500)
} catch (err) { toast('停止失败: ' + err, 'error') }
finally { actionBtn.disabled = false; actionBtn.textContent = '停止' }
}
if (action === 'restart-gw') {
actionBtn.disabled = true; actionBtn.textContent = '重启中...'
try {
await api.restartService('ai.openclaw.gateway')
toast('Gateway 重启指令已发送', 'success')
setTimeout(() => loadDashboardData(page), 3000)
} catch (err) { toast('重启失败: ' + err, 'error') }
finally { actionBtn.disabled = false; actionBtn.textContent = '重启' }
}
if (action === 'goto-extensions') {
navigate('/extensions')
}
})
btnRestart?.addEventListener('click', async () => {
btnRestart.disabled = true
btnRestart.classList.add('btn-loading')

View File

@@ -4,6 +4,7 @@
*/
import { api } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { statusIcon } from '../lib/icons.js'
// HTML 转义,防止 XSS
function escapeHtml(str) {
@@ -306,16 +307,24 @@ async function handleInstallCftunnel(page) {
await api.installCftunnel()
progressFill.classList.add('done')
progressText.textContent = '✅ 安装完成'
progressText.innerHTML = `${statusIcon('ok', 14)} 安装完成`
toast('cftunnel 安装成功', 'success')
// 3 秒后刷新状态
setTimeout(() => loadCftunnel(page), 3000)
} catch (e) {
progressFill.classList.add('error')
progressText.textContent = '❌ 安装失败'
progressText.innerHTML = `${statusIcon('err', 14)} 安装失败`
logBox.textContent += '\n错误: ' + e
toast('安装失败: ' + e, 'error')
if (window.__openAIDrawerWithError) {
window.__openAIDrawerWithError({
title: '安装 cftunnel 失败',
error: logBox.textContent,
scene: '安装 cftunnel 内网穿透工具',
hint: String(e),
})
}
} finally {
unlistenLog?.()
unlistenProgress?.()
@@ -364,15 +373,23 @@ async function handleInstallClawapp(page) {
await api.installClawapp()
progressFill.classList.add('done')
progressText.textContent = '✅ 安装完成'
progressText.innerHTML = `${statusIcon('ok', 14)} 安装完成`
toast('ClawApp 安装成功', 'success')
setTimeout(() => loadClawapp(page), 3000)
} catch (e) {
progressFill.classList.add('error')
progressText.textContent = '❌ 安装失败'
progressText.innerHTML = `${statusIcon('err', 14)} 安装失败`
logBox.textContent += '\n错误: ' + e
toast('安装失败: ' + e, 'error')
if (window.__openAIDrawerWithError) {
window.__openAIDrawerWithError({
title: '安装 ClawApp 失败',
error: logBox.textContent,
scene: '安装 ClawApp 手机客户端',
hint: String(e),
})
}
} finally {
unlistenLog?.()
unlistenProgress?.()

View File

@@ -5,6 +5,7 @@
import { api } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { showModal, showConfirm } from '../components/modal.js'
import { icon, statusIcon } from '../lib/icons.js'
// API 接口类型选项
const API_TYPES = [
@@ -22,6 +23,28 @@ const PROVIDER_PRESETS = [
{ key: 'google', label: 'Google Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1beta', api: 'google-gemini' },
]
// gpt.qt.cool 推广配置
const QTCOOL = {
baseUrl: 'https://gpt.qt.cool/v1',
defaultKey: 'sk-0JDu7hyc51ZKD4iNebpFu07EUEhXmVVc',
site: 'https://gpt.qt.cool/',
usageUrl: 'https://gpt.qt.cool/user?key=',
providerKey: 'qtcool',
api: 'openai-completions',
models: [
{ id: 'gpt-5.4', name: 'GPT-5.4', contextWindow: 128000 },
{ id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex', contextWindow: 128000, reasoning: true },
{ id: 'gpt-5.2-codex', name: 'GPT-5.2 Codex', contextWindow: 128000, reasoning: true },
{ id: 'gpt-5.2', name: 'GPT-5.2', contextWindow: 128000 },
{ id: 'gpt-5.1-codex-max', name: 'GPT-5.1 Codex Max', contextWindow: 128000, reasoning: true },
{ id: 'gpt-5.1-codex-mini', name: 'GPT-5.1 Codex Mini', contextWindow: 128000, reasoning: true },
{ id: 'gpt-5.1-codex', name: 'GPT-5.1 Codex', contextWindow: 128000, reasoning: true },
{ id: 'gpt-5.1', name: 'GPT-5.1', contextWindow: 128000 },
{ id: 'gpt-5-codex', name: 'GPT-5 Codex', contextWindow: 128000, reasoning: true },
{ id: 'gpt-5', name: 'GPT-5', contextWindow: 128000 },
]
}
// 常用模型预设(按服务商分组)
const MODEL_PRESETS = {
openai: [
@@ -60,6 +83,30 @@ export async function render() {
服务商是模型的来源(如 OpenAI、DeepSeek 等)。每个服务商下可添加多个模型。
标记为「主模型」的将优先使用,其余作为备选自动切换。配置修改后自动保存。
</div>
<div id="qtcool-promo" style="margin-bottom:var(--space-lg);border-radius:12px;background:linear-gradient(135deg,#0f0c29 0%,#302b63 50%,#24243e 100%);color:#fff;position:relative;overflow:hidden;box-shadow:0 4px 24px rgba(48,43,99,0.25)">
<div style="position:absolute;top:-50px;right:-50px;width:200px;height:200px;border-radius:50%;background:radial-gradient(circle,rgba(99,102,241,0.12) 0%,transparent 70%);pointer-events:none"></div>
<div style="position:absolute;bottom:-30px;left:20px;width:120px;height:120px;border-radius:50%;background:radial-gradient(circle,rgba(168,85,247,0.08) 0%,transparent 70%);pointer-events:none"></div>
<div style="padding:20px 24px 16px;display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:16px">
<div style="flex:1;min-width:240px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
<span style="font-size:20px">${icon('gift', 22)}</span>
<span style="font-weight:700;font-size:16px;letter-spacing:0.3px">ClawPanel 公益 AI 接口计划</span>
</div>
<div style="font-size:13px;color:rgba(255,255,255,0.65);line-height:1.7">
Token 费用我们帮你出了。调用成本由项目组内部承担GPT-5 全系列模型开箱即用。<br>
无需注册、无需付费、支持 OpenAI 兼容接口 — 点击即享。
</div>
</div>
<div style="display:flex;flex-direction:column;gap:10px;align-items:flex-end">
<button class="btn btn-sm" id="btn-qtcool-oneclick" style="background:linear-gradient(135deg,#6366f1,#a855f7);color:#fff;font-weight:600;border:none;padding:8px 22px;font-size:13px;white-space:nowrap;border-radius:8px;box-shadow:0 2px 12px rgba(99,102,241,0.4);cursor:pointer;transition:transform 0.15s">${icon('zap', 14)} 一键添加全部模型</button>
<div style="display:flex;gap:14px;font-size:11px">
<a href="https://gpt.qt.cool/checkin" target="_blank" style="color:rgba(168,133,247,0.9);text-decoration:none">${icon('target', 12)} 签到领密钥</a>
<a href="${QTCOOL.usageUrl}${QTCOOL.defaultKey}" target="_blank" style="color:rgba(168,133,247,0.9);text-decoration:none">${icon('bar-chart', 12)} 用量查询</a>
<a href="https://claw.qt.cool/" target="_blank" style="color:rgba(168,133,247,0.9);text-decoration:none">${icon('home', 12)} 官网</a>
</div>
</div>
</div>
</div>
<div id="default-model-bar"></div>
<div style="margin-bottom:var(--space-md)">
<input class="form-input" id="model-search" placeholder="搜索模型(按 ID 或名称过滤)" style="max-width:360px">
@@ -695,6 +742,71 @@ function applyDefaultModel(state) {
function bindTopActions(page, state) {
page.querySelector('#btn-add-provider').onclick = () => addProvider(page, state)
page.querySelector('#btn-undo').onclick = () => undo(page, state)
// gpt.qt.cool 一键添加(动态获取模型列表)
page.querySelector('#btn-qtcool-oneclick').onclick = async () => {
if (!state.config) { toast('配置未加载完成,请稍候', 'warning'); return }
const btn = page.querySelector('#btn-qtcool-oneclick')
btn.textContent = '获取模型列表...'
btn.disabled = true
// 动态获取模型列表,失败则用静态 fallback
let models = QTCOOL.models
try {
const resp = await fetch(QTCOOL.baseUrl + '/models', {
headers: { 'Authorization': 'Bearer ' + QTCOOL.defaultKey },
signal: AbortSignal.timeout(8000)
})
if (resp.ok) {
const data = await resp.json()
if (data.data && data.data.length) {
models = data.data.map(m => ({
id: m.id, name: m.id, contextWindow: 128000,
reasoning: m.id.includes('codex')
})).sort((a, b) => b.id.localeCompare(a.id))
}
}
} catch { /* use fallback */ }
btn.innerHTML = `${icon('zap', 14)} 一键添加全部模型`
btn.disabled = false
pushUndo(state)
if (!state.config.models) state.config.models = {}
if (!state.config.models.providers) state.config.models.providers = {}
const existing = state.config.models.providers[QTCOOL.providerKey]
if (existing) {
const existingIds = new Set((existing.models || []).map(m => typeof m === 'string' ? m : m.id))
let added = 0
for (const m of models) {
if (!existingIds.has(m.id)) {
existing.models.push({ ...m })
added++
}
}
toast(added ? `已添加 ${added} 个新模型到 qtcool` : 'qtcool 模型已是最新', added ? 'success' : 'info')
} else {
state.config.models.providers[QTCOOL.providerKey] = {
baseUrl: QTCOOL.baseUrl,
apiKey: QTCOOL.defaultKey,
api: QTCOOL.api,
models: models.map(m => ({ ...m })),
}
if (!getCurrentPrimary(state.config)) {
if (!state.config.agents) state.config.agents = {}
if (!state.config.agents.defaults) state.config.agents.defaults = {}
if (!state.config.agents.defaults.model) state.config.agents.defaults.model = {}
state.config.agents.defaults.model.primary = QTCOOL.providerKey + '/' + models[0].id
}
toast('已添加 gpt.qt.cool' + models.length + ' 个模型)', 'success')
}
renderProviders(page, state)
renderDefaultBar(page, state)
updateUndoBtn(page, state)
autoSave(state)
}
}
// 添加服务商(带预设快捷选择)
@@ -1073,7 +1185,7 @@ async function handleBatchTest(section, state, providerKey) {
renderDefaultBar(page, state)
}
// 进度 toast
const status = model?.testStatus === 'ok' ? '' : ''
const status = model?.testStatus === 'ok' ? '\u2713' : '\u2717'
const latStr = model?.latency != null ? ` ${(model.latency / 1000).toFixed(1)}s` : ''
toast(`${status} ${modelId}${latStr} (${ok + fail}/${ids.length})`, model?.testStatus === 'ok' ? 'success' : 'error')
}

View File

@@ -3,6 +3,7 @@
* 支持 Web 部署模式和 Tauri 桌面端
*/
import { toast } from '../components/toast.js'
import { statusIcon } from '../lib/icons.js'
const isTauri = !!window.__TAURI_INTERNALS__
let _tauriApi = null
@@ -114,7 +115,7 @@ function renderContent(container, status) {
let html = ''
// 当前状态
const stateIcon = status.hasPassword ? '✅' : (status.ignoreRisk ? '⚠️' : '⚠️')
const stateIcon = status.hasPassword ? statusIcon('ok', 20) : statusIcon('warn', 20)
const stateText = status.hasPassword
? (status.mustChangePassword ? '使用默认密码(需修改)' : '已设置自定义密码')
: (status.ignoreRisk ? '无视风险模式(无密码)' : '未设置密码')

View File

@@ -7,6 +7,7 @@ import { toast } from '../components/toast.js'
import { showConfirm, showUpgradeModal } from '../components/modal.js'
import { isMacPlatform, setUpgrading, setUserStopped, resetAutoRestart } from '../lib/app-state.js'
import { diagnoseInstallError } from '../lib/error-diagnosis.js'
import { icon, statusIcon } from '../lib/icons.js'
// HTML 转义,防止 XSS
function escapeHtml(str) {
@@ -424,11 +425,20 @@ async function doUpgradeWithModal(source, page) {
} catch (e) {
const errStr = String(e)
modal.appendLog(errStr)
const diagnosis = diagnoseInstallError(errStr)
const fullLog = modal.getLogText() + '\n' + errStr
const diagnosis = diagnoseInstallError(fullLog)
modal.setError(diagnosis.title)
if (diagnosis.hint) modal.appendLog('')
if (diagnosis.hint) modal.appendLog(' ' + diagnosis.hint)
if (diagnosis.command) modal.appendLog('💻 ' + diagnosis.command)
if (diagnosis.hint) modal.appendHtmlLog(`${statusIcon('info', 14)} ${diagnosis.hint}`)
if (diagnosis.command) modal.appendHtmlLog(`${icon('clipboard', 14)} ${diagnosis.command}`)
if (window.__openAIDrawerWithError) {
window.__openAIDrawerWithError({
title: diagnosis.title,
error: fullLog,
scene: '升级 OpenClaw',
hint: diagnosis.hint,
})
}
} finally {
setUpgrading(false)
unlistenLog?.()

View File

@@ -7,6 +7,7 @@ import { showUpgradeModal } from '../components/modal.js'
import { toast } from '../components/toast.js'
import { setUpgrading, isMacPlatform } from '../lib/app-state.js'
import { diagnoseInstallError } from '../lib/error-diagnosis.js'
import { icon, statusIcon } from '../lib/icons.js'
export async function render() {
const page = document.createElement('div')
@@ -107,7 +108,7 @@ function renderSteps(page, { node, cliOk, config }) {
: `安装 Node.js 后需要<strong>重启 ClawPanel</strong>,新的环境变量才能生效。`
}
<div style="margin-top:8px;display:flex;gap:6px;align-items:center;flex-wrap:wrap">
<button class="btn btn-secondary btn-sm" id="btn-scan-node" style="font-size:11px;padding:3px 10px">🔍 自动扫描</button>
<button class="btn btn-secondary btn-sm" id="btn-scan-node" style="font-size:11px;padding:3px 10px">${icon('search', 12)} 自动扫描</button>
<span style="color:var(--text-tertiary)">或手动指定路径:</span>
</div>
<div style="margin-top:6px;display:flex;gap:6px">
@@ -218,7 +219,7 @@ function renderInstallSection() {
<div style="margin-bottom:10px">
<div style="font-weight:600;margin-bottom:4px">WSL 中使用 Web 版:</div>
<div style="margin-bottom:2px;opacity:0.8">打开 WSL 终端,一键部署 ClawPanel Web 版:</div>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all">curl -fsSL https://claw.qt.cool/deploy.sh | bash</code>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all">curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/deploy.sh | bash</code>
<div style="margin-top:4px;opacity:0.7">部署后在浏览器访问 WSL 的 IP 即可管理。</div>
</div>
` : ''}
@@ -226,12 +227,12 @@ function renderInstallSection() {
<div style="font-weight:600;margin-bottom:4px">Docker 容器中使用:</div>
<div style="margin-bottom:2px;opacity:0.8">在容器内安装 OpenClaw + ClawPanel Web 版:</div>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all;margin-bottom:4px">npm i -g @qingchencloud/openclaw-zh</code>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all">curl -fsSL https://claw.qt.cool/deploy.sh | bash</code>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all">curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/deploy.sh | bash</code>
</div>
<div>
<div style="font-weight:600;margin-bottom:4px">远程服务器:</div>
<div style="margin-bottom:2px;opacity:0.8">SSH 登录服务器后执行:</div>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all">curl -fsSL https://claw.qt.cool/deploy.sh | bash</code>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all">curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/deploy.sh | bash</code>
</div>
</div>
</details>
@@ -363,7 +364,7 @@ function bindEvents(page, nodeOk, detectState) {
resultEl.innerHTML = `<span style="color:var(--danger)">扫描失败: ${e}</span>`
} finally {
btn.disabled = false
btn.textContent = '🔍 自动扫描'
btn.innerHTML = `${icon('search', 12)} 自动扫描`
}
})
@@ -425,9 +426,9 @@ function bindEvents(page, nodeOk, detectState) {
modal.appendLog('正在安装 Gateway 服务...')
try {
await api.installGateway()
modal.appendLog('✅ Gateway 服务已安装')
modal.appendHtmlLog(`${statusIcon('ok', 14)} Gateway 服务已安装`)
} catch (e) {
modal.appendLog('⚠️ Gateway 安装失败: ' + e)
modal.appendHtmlLog(`${statusIcon('warn', 14)} Gateway 安装失败: ${e}`)
}
// 确保 openclaw.json 有关键默认值,否则 Gateway 启动不了或功能受限
@@ -439,7 +440,7 @@ function bindEvents(page, nodeOk, detectState) {
if (!config.gateway.mode) {
config.gateway.mode = 'local'
patched = true
modal.appendLog('✅ 已设置 Gateway 运行模式为 local')
modal.appendHtmlLog(`${statusIcon('ok', 14)} 已设置 Gateway 运行模式为 local`)
}
if (!config.tools || config.tools.profile !== 'full') {
config.tools = { profile: 'full', sessions: { visibility: 'all' }, ...(config.tools || {}) }
@@ -447,12 +448,12 @@ function bindEvents(page, nodeOk, detectState) {
if (!config.tools.sessions) config.tools.sessions = {}
config.tools.sessions.visibility = 'all'
patched = true
modal.appendLog('✅ 已开启 Agent 工具全部权限')
modal.appendHtmlLog(`${statusIcon('ok', 14)} 已开启 Agent 工具全部权限`)
}
if (patched) await api.writeOpenclawConfig(config)
}
} catch (e) {
modal.appendLog('⚠️ 自动配置失败: ' + e)
modal.appendHtmlLog(`${statusIcon('warn', 14)} 自动配置失败: ${e}`)
}
toast('OpenClaw 安装成功', 'success')
@@ -460,11 +461,23 @@ function bindEvents(page, nodeOk, detectState) {
} catch (e) {
const errStr = String(e)
modal.appendLog(errStr)
const diagnosis = diagnoseInstallError(errStr)
// 等待 Tauri 事件队列中残留的 npm 日志行被 JS 处理完毕,
// 确保 getLogText() 包含完整输出(含 exit code / ENOENT 等关键行)
await new Promise(r => setTimeout(r, 150))
const fullLog = modal.getLogText() + '\n' + errStr
const diagnosis = diagnoseInstallError(fullLog)
modal.setError(diagnosis.title)
if (diagnosis.hint) modal.appendLog('')
if (diagnosis.hint) modal.appendLog(' ' + diagnosis.hint)
if (diagnosis.command) modal.appendLog('💻 ' + diagnosis.command)
if (diagnosis.hint) modal.appendHtmlLog(`${statusIcon('info', 14)} ${diagnosis.hint}`)
if (diagnosis.command) modal.appendHtmlLog(`${icon('clipboard', 14)} ${diagnosis.command}`)
if (window.__openAIDrawerWithError) {
window.__openAIDrawerWithError({
title: diagnosis.title,
error: fullLog,
scene: '初始安装 OpenClaw',
hint: diagnosis.hint,
})
}
} finally {
setUpgrading(false)
unlistenLog?.()