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

211
src/components/ai-drawer.js Normal file
View File

@@ -0,0 +1,211 @@
/**
* 全局 AI 助手浮动按钮FAB
* 右下角可拖动按钮 → 点击导航到 AI 助手页面(复用完整功能)
* 自动注入当前页面上下文到 AI 助手会话
*/
const BOT_ICON = '<svg viewBox="0 0 24 24"><path d="M12 8V4H8"/><rect x="5" y="8" width="14" height="12" rx="2"/><path d="M9 13h0"/><path d="M15 13h0"/><path d="M10 17h4"/></svg>'
const POS_KEY = 'clawpanel-fab-pos'
// ── 页面上下文收集器注册表 ──
const _contextProviders = {}
/**
* 注册页面上下文提供器
* @param {string} route - 路由路径,如 '/chat-debug'
* @param {function} provider - 返回 { label, detail } 的函数(可 async
*/
export function registerPageContext(route, provider) {
_contextProviders[route] = provider
}
// ── 单例 ──
let _fab = null
/** 初始化 FAB */
export function initAIFab() {
if (_fab) return _fab
_fab = createFab()
return _fab
}
/** 导航到 AI 助手并注入错误上下文(显示为可操作的 banner而非自动发送 */
export function openAIDrawerWithError(errorCtx) {
sessionStorage.setItem('assistant-error-context', JSON.stringify({
scene: errorCtx.scene || '',
title: errorCtx.title || '操作失败',
hint: errorCtx.hint || '',
error: truncate(errorCtx.error || '', 3000),
ts: Date.now(),
}))
// 不自动导航 — FAB 按钮会出现红点提示,用户主动点击时跳转
// 如果用户已在助手页,也会实时检测到
if (getCurrentRoute() !== '/assistant') {
// 让 FAB 显示红点
if (_fab?.el) _fab.el.classList.add('has-error')
} else {
// 已在助手页 → 直接触发 banner 显示
window.dispatchEvent(new CustomEvent('assistant-error-injected'))
}
}
function truncate(str, max) {
if (!str || str.length <= max) return str
return str.slice(0, max) + '\n... (截断)'
}
// ── 创建 FAB ──
function createFab() {
const fab = document.createElement('button')
fab.className = 'ai-fab'
fab.title = 'AI 助手'
fab.innerHTML = BOT_ICON
document.body.appendChild(fab)
// 恢复保存的位置
restorePosition(fab)
// ── 拖动逻辑 ──
let _dragging = false
let _dragMoved = false
let _startX = 0, _startY = 0
let _fabX = 0, _fabY = 0
function onPointerDown(e) {
if (e.button !== 0) return
_dragging = true
_dragMoved = false
_startX = e.clientX
_startY = e.clientY
const rect = fab.getBoundingClientRect()
_fabX = rect.left
_fabY = rect.top
fab.style.transition = 'none'
fab.setPointerCapture(e.pointerId)
e.preventDefault()
}
function onPointerMove(e) {
if (!_dragging) return
const dx = e.clientX - _startX
const dy = e.clientY - _startY
if (!_dragMoved && Math.abs(dx) < 4 && Math.abs(dy) < 4) return
_dragMoved = true
fab.classList.add('dragging')
// 计算新位置(限制在视口内)
const vw = window.innerWidth
const vh = window.innerHeight
const size = 48
let newX = Math.max(8, Math.min(vw - size - 8, _fabX + dx))
let newY = Math.max(8, Math.min(vh - size - 8, _fabY + dy))
fab.style.left = newX + 'px'
fab.style.top = newY + 'px'
fab.style.right = 'auto'
fab.style.bottom = 'auto'
}
function onPointerUp(e) {
if (!_dragging) return
_dragging = false
fab.classList.remove('dragging')
fab.style.transition = ''
if (_dragMoved) {
// 吸附到最近的边(左/右)
const rect = fab.getBoundingClientRect()
const vw = window.innerWidth
const vh = window.innerHeight
const snapRight = rect.left > vw / 2
const y = Math.max(8, Math.min(vh - 56, rect.top))
if (snapRight) {
fab.style.left = 'auto'
fab.style.right = '24px'
} else {
fab.style.left = '24px'
fab.style.right = 'auto'
}
fab.style.top = y + 'px'
fab.style.bottom = 'auto'
// 保存位置
savePosition(snapRight ? 'right' : 'left', y)
} else {
// 没有拖动 → 点击
handleClick()
}
}
fab.addEventListener('pointerdown', onPointerDown)
document.addEventListener('pointermove', onPointerMove)
document.addEventListener('pointerup', onPointerUp)
// ── 点击 → 导航到 AI 助手 ──
async function handleClick() {
const route = getCurrentRoute()
// 已经在 AI 助手页面,不做任何操作
if (route === '/assistant') return
// 清除红点
fab.classList.remove('has-error')
// 如果没有错误上下文待处理,收集当前页面上下文
if (!sessionStorage.getItem('assistant-error-context')) {
const provider = _contextProviders[route]
if (provider) {
try {
const ctx = await provider()
if (ctx?.detail) {
const prompt = `以下是当前页面的上下文信息,请根据情况提供帮助:\n\n${ctx.detail}`
sessionStorage.setItem('assistant-auto-prompt', prompt)
}
} catch (e) {
console.warn('[ai-fab] 上下文收集失败:', e)
}
}
}
window.location.hash = '#/assistant'
}
// ── 路由变化时隐藏/显示 ──
function updateVisibility() {
const route = getCurrentRoute()
fab.style.display = route === '/assistant' ? 'none' : 'flex'
}
window.addEventListener('hashchange', updateVisibility)
updateVisibility()
return { el: fab }
}
function getCurrentRoute() {
return (window.location.hash.replace('#', '') || '/dashboard').split('?')[0]
}
function savePosition(side, top) {
try {
localStorage.setItem(POS_KEY, JSON.stringify({ side, top }))
} catch {}
}
function restorePosition(fab) {
try {
const raw = localStorage.getItem(POS_KEY)
if (!raw) return
const { side, top } = JSON.parse(raw)
if (side === 'left') {
fab.style.left = '24px'
fab.style.right = 'auto'
}
if (typeof top === 'number') {
fab.style.top = top + 'px'
fab.style.bottom = 'auto'
}
} catch {}
}

View File

@@ -163,6 +163,7 @@ export function showUpgradeModal() {
const text = overlay.querySelector('.upgrade-progress-text')
const logBox = overlay.querySelector('.upgrade-log-box')
const closeBtn = overlay.querySelector('[data-action="close"]')
const _logLines = []
closeBtn.onclick = () => overlay.remove()
overlay.addEventListener('keydown', (e) => {
@@ -171,11 +172,20 @@ export function showUpgradeModal() {
return {
appendLog(line) {
_logLines.push(line)
const div = document.createElement('div')
div.textContent = line
logBox.appendChild(div)
logBox.scrollTop = logBox.scrollHeight
},
appendHtmlLog(line) {
_logLines.push(line)
const div = document.createElement('div')
div.innerHTML = line
logBox.appendChild(div)
logBox.scrollTop = logBox.scrollHeight
},
getLogText() { return _logLines.join('\n') },
setProgress(pct) {
fill.style.width = pct + '%'
if (pct >= 100) text.textContent = '完成'

View File

@@ -3,13 +3,17 @@
* 解析 npm 错误信息,返回用户友好的提示和修复建议
*/
const NPM_CMD = 'npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com'
/**
* @param {string} errStr - npm 错误输出
* @param {string} errStr - npm 错误输出(可含流式日志)
* @returns {{ title: string, hint?: string, command?: string }}
*/
export function diagnoseInstallError(errStr) {
const s = errStr.toLowerCase()
// ===== 1. Git 相关 =====
// git SSH 权限问题(有 git 但没配 SSH Key
if (s.includes('permission denied (publickey)') || s.includes('ssh://git@github')) {
return {
@@ -28,12 +32,44 @@ export function diagnoseInstallError(errStr) {
}
}
// EPERM文件被占用/权限问题)
// ===== 2. 文件 / 权限 =====
// EPERM文件被占用/权限问题)— 放在 ENOENT 前面,优先匹配
if (s.includes('eperm') || s.includes('operation not permitted')) {
return {
title: '安装失败 — 文件被占用',
hint: '有文件被锁定无法写入。先关闭所有 ClawPanel 和 Node.js 进程,然后在管理员终端手动安装:',
command: 'npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com',
title: '安装失败 — 文件被占用或权限不足',
hint: '常见原因杀毒软件拦截、Gateway 进程未关闭、或终端缺少管理员权限。\n请先关闭 Gateway再以管理员身份打开终端手动安装:',
command: NPM_CMD,
}
}
// ENOENT文件找不到 / -4058
if (s.includes('enoent') || s.includes('-4058') || s.includes('code -4058')) {
// 尝试从日志中提取具体缺失的路径
const pathMatch = errStr.match(/enoent[^']*'([^']+)'/i) || errStr.match(/path\s+'([^']+)'/i)
const missingPath = pathMatch?.[1] || ''
if (missingPath.includes('node_modules') || missingPath.includes('npm')) {
return {
title: '安装失败 — npm 全局目录异常',
hint: `npm 全局安装目录可能不存在或损坏(${missingPath})。\n请先修复 npm 目录,再重试安装:`,
command: 'npm config set prefix "%APPDATA%\\npm" && ' + NPM_CMD,
}
}
return {
title: '安装失败 — 文件或目录不存在',
hint: '常见原因npm 全局目录未创建、杀毒软件隔离了文件、或磁盘权限问题。\n建议步骤\n1. 关闭杀毒软件的实时防护\n2. 以管理员身份打开 PowerShell\n3. 手动运行安装命令:',
command: NPM_CMD,
}
}
// EACCES权限不足
if (s.includes('eacces') || s.includes('permission denied')) {
const isMac = navigator.platform?.includes('Mac') || navigator.userAgent?.includes('Mac')
return {
title: '安装失败 — 权限不足',
hint: isMac ? '请在终端使用 sudo 安装:' : '请以管理员身份打开 PowerShell 安装:',
command: isMac ? 'sudo ' + NPM_CMD : NPM_CMD,
}
}
@@ -42,53 +78,68 @@ export function diagnoseInstallError(errStr) {
return {
title: '安装不完整',
hint: '上次安装可能中断了。先清理残留再重装:',
command: 'npm cache clean --force && npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com',
command: 'npm cache clean --force && ' + NPM_CMD,
}
}
// ENOENT文件找不到
if (s.includes('enoent') || s.includes('-4058') || s.includes('code -4058')) {
return {
title: '安装失败 — 文件访问错误',
hint: '尝试以管理员身份运行 ClawPanel或在终端手动安装',
command: 'npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com',
}
}
// ===== 3. 网络 =====
// 权限不足EACCES / EPERM
if (s.includes('eacces') || s.includes('eperm') || s.includes('permission denied')) {
const isMac = navigator.platform?.includes('Mac') || navigator.userAgent?.includes('Mac')
return {
title: '安装失败 — 权限不足',
hint: isMac ? '请在终端使用 sudo 安装:' : '请以管理员身份打开终端安装:',
command: isMac
? 'sudo npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com'
: 'npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com',
}
}
// 网络错误
if (s.includes('etimedout') || s.includes('econnrefused') || s.includes('enotfound')
|| s.includes('network') || s.includes('fetch failed') || s.includes('socket hang up')) {
|| s.includes('fetch failed') || s.includes('socket hang up')
|| s.includes('econnreset') || s.includes('unable to get local issuer')) {
const isProxy = s.includes('proxy') || s.includes('unable to get local issuer')
return {
title: '安装失败 — 网络连接错误',
hint: '请检查网络连接,或尝试切换 npm 镜像源后重试。',
hint: isProxy
? '检测到代理/证书问题。如果你使用了 VPN 或公司代理,请尝试关闭后重试,或设置 npm 信任证书:'
: '无法连接到 npm 仓库。请检查网络连接,或尝试使用国内镜像源:',
command: isProxy
? 'npm config set strict-ssl false && ' + NPM_CMD
: NPM_CMD,
}
}
// ===== 4. npm 自身问题 =====
// npm 缓存损坏
if (s.includes('integrity') || s.includes('sha512') || s.includes('cache')) {
return {
title: '安装失败 — npm 缓存异常',
hint: '尝试清理 npm 缓存后重试:',
command: 'npm cache clean --force',
hint: '本地缓存可能损坏。清理缓存后重试:',
command: 'npm cache clean --force && ' + NPM_CMD,
}
}
// 通用 fallback
// Node.js 版本过低
if (s.includes('engine') || s.includes('unsupported') || s.includes('required:')) {
return {
title: '安装失败 — Node.js 版本不兼容',
hint: '当前 Node.js 版本过低OpenClaw 需要 Node.js 18 或更高版本。\n请升级 Node.js',
command: '下载最新版: https://nodejs.org/',
}
}
// npm 版本过低或损坏
if (s.includes('npm err') && (s.includes('cb() never called') || s.includes('code 1'))) {
return {
title: '安装失败 — npm 异常',
hint: 'npm 自身可能异常。尝试更新 npm 后重试:',
command: 'npm install -g npm@latest && ' + NPM_CMD,
}
}
// ===== 5. 磁盘空间 =====
if (s.includes('enospc') || s.includes('no space')) {
return {
title: '安装失败 — 磁盘空间不足',
hint: '磁盘空间不足,请清理磁盘后重试。',
}
}
// ===== fallback =====
return {
title: '安装失败',
hint: '请在终端手动尝试安装,查看完整错误信息:',
command: 'npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com',
command: NPM_CMD,
}
}

91
src/lib/icons.js Normal file
View File

@@ -0,0 +1,91 @@
/**
* 统一 SVG 图标库 — 替代所有 Emoji保持视觉一致性
* 基于 Lucide/Feather 风格,使用 currentColor 继承颜色
*/
const PATHS = {
// 状态图标
'check-circle': '<path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>',
'x-circle': '<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>',
'alert-triangle': '<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>',
'info': '<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"/>',
// 简单指示符
'check': '<polyline points="20 6 9 17 4 12"/>',
'x': '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
// 操作图标
'search': '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
'gift': '<polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/><path d="M12 7H7.5a2.5 2.5 0 010-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 000-5C13 2 12 7 12 7z"/>',
'zap': '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>',
'target': '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
'bar-chart': '<line x1="12" y1="20" x2="12" y2="10"/><line x1="18" y1="20" x2="18" y2="4"/><line x1="6" y1="20" x2="6" y2="16"/>',
'home': '<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>',
'paperclip': '<path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/>',
'clipboard': '<path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2"/><rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>',
'file': '<path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/><polyline points="13 2 13 9 20 9"/>',
'file-text': '<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>',
'file-plain': '<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/>',
'package': '<line x1="16.5" y1="9.4" x2="7.5" y2="4.21"/><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>',
'terminal': '<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>',
'edit': '<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>',
'folder': '<path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/>',
'monitor': '<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>',
'plug': '<path d="M12 22v-5"/><path d="M9 8V1h6v7"/><path d="M7 8h10a0 0 0 010 0 5 5 0 01-10 0 0 0 0 010 0z"/><path d="M12 8v5"/>',
'wrench': '<path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/>',
'bug': '<path d="M8 2l1.88 1.88M14.12 3.88L16 2M9 7.13v-1a3 3 0 116 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 014-4h4a4 4 0 014 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9M6.53 9C4.6 8.8 3 7.1 3 5M6 13H2M3 21c0-2.1 1.7-3.9 3.8-4M20.97 5c0 2.1-1.6 3.8-3.5 4M22 13h-4M17.2 17c2.1.1 3.8 1.9 3.8 4"/>',
'fire': '<path d="M8.5 14.5A2.5 2.5 0 0011 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 11-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 002.5 2.5z"/>',
'key': '<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 11-7.778 7.778 5.5 5.5 0 017.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>',
'lock': '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0110 0v4"/>',
'clock': '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
'send': '<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>',
'download': '<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>',
'inbox': '<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11z"/>',
'radio': '<circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 010 8.49m-8.48-.01a6 6 0 010-8.49m11.31-2.82a10 10 0 010 14.14m-14.14 0a10 10 0 010-14.14"/>',
'lightbulb': '<line x1="9" y1="18" x2="15" y2="18"/><line x1="10" y1="22" x2="14" y2="22"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0018 8 6 6 0 006 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 018.91 14"/>',
'globe': '<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/>',
'shield': '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
'list': '<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>',
}
/**
* 生成内联 SVG 图标
* @param {string} name 图标名称
* @param {number} [size=16] 图标尺寸px
* @param {string} [className] 可选 CSS 类名
* @returns {string} SVG HTML 字符串
*/
export function icon(name, size = 16, className) {
const paths = PATHS[name]
if (!paths) return ''
const cls = className ? ` class="${className}"` : ''
return `<svg${cls} width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-0.125em;flex-shrink:0">${paths}</svg>`
}
/**
* 状态图标(带颜色)
* @param {'ok'|'err'|'warn'|'info'} type 状态类型
* @param {number} [size=16] 图标尺寸
* @returns {string} 带颜色的 SVG 字符串
*/
export function statusIcon(type, size = 16) {
const map = {
ok: { name: 'check-circle', color: 'var(--success)' },
err: { name: 'x-circle', color: 'var(--danger, var(--error))' },
warn: { name: 'alert-triangle', color: 'var(--warning)' },
info: { name: 'info', color: 'var(--info, var(--primary))' },
}
const cfg = map[type]
if (!cfg) return ''
const paths = PATHS[cfg.name]
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${cfg.color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-0.125em;flex-shrink:0">${paths}</svg>`
}
/**
* 用于日志前缀的图标(纯文本环境也能回退)
* @param {string} name 图标名称
* @returns {string} 小尺寸 SVG 字符串
*/
export function logIcon(name, size = 14) {
return icon(name, size)
}

254
src/lib/openclaw-kb.js Normal file
View File

@@ -0,0 +1,254 @@
/**
* OpenClaw 内置知识库
* 来源https://openclawcn.com/docs/
* 供 ClawPanel AI 助手在系统提示词中使用
*/
export const OPENCLAW_KB = `
# OpenClaw 知识库(内置参考)
## 一、架构概览
OpenClaw 是开源个人 AI 助手平台,核心组件:
- **Gateway 网关**核心后端服务处理消息路由、Agent 执行、渠道连接
- **CLI**:命令行工具,用于安装/配置/管理 OpenClaw
- **Agent智能体**:独立的 AI 角色实例,有自己的工作区、身份、模型配置
- **Workspace工作区**Agent 的个性化存储Skills、提示、记忆
- **Channel渠道**消息通道WhatsApp/Telegram/Discord/Mattermost 等)
- **Control UI / Dashboard**:内置 Web 管理界面,端口 18789
## 二、目录结构
\`\`\`
~/.openclaw/
├── openclaw.json # 主配置文件JSON5支持注释
├── .env # 全局环境变量
├── workspace/ # 默认(main) Agent 的工作区
│ ├── IDENTITY.md # Agent 身份定义
│ ├── SOUL.md # Agent 灵魂/人格
│ ├── USER.md # 用户信息
│ ├── AGENTS.md # 操作规则
│ └── ... # Skills、记忆等
├── agents/
│ ├── main/
│ │ └── agent/
│ │ ├── auth-profiles.json # 认证配置OAuth + API Key
│ │ ├── models.json # 模型提供商配置
│ │ └── auth.json # 运行时认证缓存(自动管理)
│ └── <agentId>/
│ ├── agent/ # 同上
│ └── workspace/ # 自定义 Agent 的工作区
├── credentials/
│ ├── oauth.json # 旧版 OAuth 导入
│ ├── whatsapp/<accountId>/ # WhatsApp 凭证
│ └── <channel>-allowFrom.json # 配对白名单
└── logs/ # 日志文件
\`\`\`
**重要路径规则:**
- main Agent 工作区:\`~/.openclaw/workspace\`(根级别)
- 自定义 Agent 工作区:\`~/.openclaw/agents/<agentId>/workspace\`
- Agent 配置目录:\`~/.openclaw/agents/<agentId>/agent/\`
## 三、CLI 常用命令
| 命令 | 说明 |
|------|------|
| \`openclaw onboard\` | 新手引导向导(推荐首次使用) |
| \`openclaw onboard --install-daemon\` | 引导 + 安装后台服务 |
| \`openclaw setup\` | 初始化/配置工作区 |
| \`openclaw gateway\` | 启动 Gateway前台 |
| \`openclaw gateway --port 18789 --verbose\` | 指定端口启动 |
| \`openclaw gateway status\` | 查看 Gateway 状态 |
| \`openclaw dashboard\` | 打开 Web Dashboard |
| \`openclaw status\` | 系统状态概览 |
| \`openclaw status --all\` | 完整调试报告(可粘贴) |
| \`openclaw health\` | 健康检查 |
| \`openclaw doctor\` | 诊断配置问题 |
| \`openclaw doctor --fix\` | 自动修复配置问题 |
| \`openclaw security audit --deep\` | 深度安全审计 |
| \`openclaw channels login\` | 登录渠道(如 WhatsApp QR |
| \`openclaw pairing list <channel>\` | 列出配对请求 |
| \`openclaw pairing approve <channel> <code>\` | 批准配对 |
| \`openclaw configure --section web\` | 配置 Web 搜索Brave API |
| \`openclaw config set <key> <value>\` | 设置单个配置项 |
| \`openclaw logs\` | 查看日志 |
| \`openclaw service start/stop/restart\` | 管理后台服务 |
| \`openclaw message send --target <num> --message "text"\` | 发送测试消息 |
## 四、配置文件openclaw.json
配置位于 \`~/.openclaw/openclaw.json\`JSON5 格式(支持注释和尾逗号)。
不存在时使用安全默认值。严格 schema 验证,未知键会阻止启动。
### 最小配置示例
\`\`\`json5
{
agents: {
defaults: {
workspace: "~/.openclaw/workspace"
}
},
channels: {
whatsapp: {
allowFrom: ["+15555550123"]
}
}
}
\`\`\`
### 关键配置项
- **agents.defaults.workspace** — 默认工作区路径
- **agents.defaults.model.primary** — 默认模型(格式 "provider/model"
- **agents.defaults.sandbox** — 沙箱配置mode: "off"|"non-main"|"all"
- **agents.list[]** — 多 Agent 配置id, name, workspace, model, identity, groupChat, sandbox
- **channels.whatsapp** — WhatsAppallowFrom, groups, dmPolicy, accounts
- **channels.telegram** — Telegram Bot
- **channels.discord** — Discord Bot
- **channels.mattermost** — Mattermost 插件
- **gateway.auth.token** — Gateway 认证令牌
- **gateway.port** — Gateway 端口(默认 18789
- **models.providers** — 自定义模型提供商baseUrl, apiKey, api, models[]
- **env.vars** — 内联环境变量
- **bindings[]** — 消息路由绑定channel→agentId
### 配置管理 RPC
- \`config.get\` — 获取当前配置(含 hash
- \`config.apply\` — 全量替换配置并重启(需 baseHash
- \`config.patch\` — 部分更新配置并重启JSON merge patch 语义)
- \`config.schema\` — 获取配置的 JSON Schema
### 环境变量
- \`~/.openclaw/.env\` — 全局 .env
- 配置中支持 \`\${VAR_NAME}\` 语法引用环境变量
- env.shellEnv.enabled=true 可从 shell 导入环境变量
## 五、多 Agent 路由
\`\`\`json5
{
agents: {
list: [
{ id: "main", workspace: "~/.openclaw/workspace", sandbox: { mode: "off" } },
{ id: "helper", name: "Helper Bot", workspace: "~/.openclaw/agents/helper/workspace" }
]
},
bindings: [
{ match: { channel: "telegram" }, agentId: "helper" },
{ match: { channel: "whatsapp" }, agentId: "main" }
]
}
\`\`\`
- main Agent 的工作区默认 \`~/.openclaw/workspace\`
- 其他 Agent 默认 \`~/.openclaw/workspace-<agentId>\`
- Agent 配置目录固定为 \`~/.openclaw/agents/<agentId>/agent/\`
## 六、模型配置
模型配置存储在 \`~/.openclaw/agents/<agentId>/agent/models.json\`
也可在 openclaw.json 的 \`models.providers\` 中定义自定义提供商。
自定义提供商示例:
\`\`\`json5
{
models: {
providers: {
"my-proxy": {
baseUrl: "http://localhost:4000/v1",
apiKey: "sk-...",
api: "openai-completions",
models: [
{ id: "gpt-4o", name: "GPT-4o", reasoning: false, input: ["text", "image"],
contextWindow: 128000, maxTokens: 16384 }
]
}
}
},
agents: {
defaults: {
model: { primary: "my-proxy/gpt-4o" }
}
}
}
\`\`\`
## 七、认证
- **OAuth推荐**:通过 \`openclaw onboard\` 设置,支持 Anthropic、OpenAI Codex
- **API Key**:直接在 auth-profiles.json 或环境变量中设置
- **凭证位置**\`~/.openclaw/agents/<agentId>/agent/auth-profiles.json\`
- **旧版导入**\`~/.openclaw/credentials/oauth.json\`
## 八、安装
**macOS/Linux**
\`\`\`bash
curl -fsSL https://openclaw.ai/install.sh | bash
\`\`\`
**WindowsWSL2 推荐):**
\`\`\`powershell
iwr -useb https://openclaw.ai/install.ps1 | iex
\`\`\`
**npm 全局安装:**
\`\`\`bash
npm install -g openclaw@latest
\`\`\`
**前置条件:** Node.js >= 22
## 九、后台服务
- **macOS**launchd 服务openclaw 应用管理)
- **Linux**systemd 用户服务(需 \`sudo loginctl enable-linger $USER\`
- **Windows**WSL2 内运行 Linux 步骤
## 十、渠道配置
### WhatsApp
- \`openclaw channels login\` → 扫描 QR 登录
- 配置 allowFrom 白名单限制私聊
- groups 配置群组行为requireMention 等)
- 首次私信会返回配对码,需 approve
### Telegram
- 使用 Bot Token
- \`channels.telegram.tokenFile\` 或环境变量
- 群组支持 @提及触发
### Discord
- 使用 Bot Token
- 环境变量或配置中设置
- 支持 guild 级别配置
## 十一、故障排查
1. \`openclaw doctor\` — 诊断所有已知问题
2. \`openclaw doctor --fix\` — 自动修复
3. \`openclaw status --all\` — 完整状态报告
4. \`openclaw health\` — 健康检查
5. \`openclaw logs\` — 查看日志
6. 配置验证失败 → Gateway 拒绝启动,仅允许诊断命令
7. WhatsApp 不回消息 → 检查配对是否已 approve
8. 认证错误 → 检查 auth-profiles.json 或重新 \`openclaw onboard\`
## 十二、ClawPanel 公益 AI 接口计划
ClawPanel 项目组正式开放公益 AI 接口测试计划,降低用户接入 AI 能力的门槛。
### 核心信息
- **GPT-AI 网关地址**https://gpt.qt.cool/v1
- **支持接口**/v1/chat/completions、/v1/responsesOpenAI 兼容)
- **支持模型**OpenAI 全系列GPT-5、GPT-5.1、GPT-5.2、GPT-5.3-codex、GPT-5.4 等)
- **费用**:测试期间所有调用成本由项目组内部承担,用户无需付费
- **限制**:无请求限制
- **用户后台**https://gpt.qt.cool/user查看用量、提交工单
### 官方入口
- **ClawPanel 官网**https://claw.qt.cool/
- **GPT-AI 网关官网**https://gpt.qt.cool/
- **用户后台**https://gpt.qt.cool/user
- **晴辰导航站**https://qt.cool/
### 测试密钥
- ClawPanel 已内置公共体验密钥,开箱即用
- 用户也可前往 gpt.qt.cool 签到获取独立密钥
- 独立密钥可在用户后台管理和查询用量
### 接入方式
已兼容 OpenAI API 的项目,只需替换:
1. Base URL → https://gpt.qt.cool/v1
2. API Key → 测试密钥
即可完成接入。
### 在 ClawPanel 中配置
- **助手设置**:打开 AI 助手设置 → 模型配置 → 使用「一键接入」按钮
- **模型配置页**:进入模型配置 → 使用「一键添加全部模型」按钮
- 两处均自动填入网关地址和内置密钥
`.trim()

View File

@@ -249,6 +249,8 @@ function mockInvoke(cmd, args) {
assistant_system_info: () => `OS: ${navigator.platform.includes('Win') ? 'windows' : navigator.platform.includes('Mac') ? 'macos' : 'linux'}\nArch: x86_64\nHome: ${navigator.platform.includes('Win') ? 'C:\\Users\\user' : '/Users/user'}\nHostname: mock-host\nShell: ${navigator.platform.includes('Win') ? 'powershell / cmd' : 'zsh'}\nPath separator: ${navigator.platform.includes('Win') ? '\\\\' : '/'}`,
assistant_list_processes: ({ filter }) => filter ? `Id ProcessName\n-- -----------\n1234 ${filter}\n5678 ${filter}-helper` : 'Id ProcessName\n-- -----------\n1 System\n1234 node\n5678 openclaw',
assistant_check_port: ({ port }) => port === 18789 ? `端口 ${port} 已被占用(正在监听)\n占用进程: node` : `端口 ${port} 未被占用(空闲)`,
assistant_web_search: ({ query }) => `搜索「${query}」找到 3 条结果:\n\n1. **${query} - 文档**\n https://example.com/docs\n 这是关于 ${query} 的文档页面\n\n2. **${query} 常见问题**\n https://example.com/faq\n 常见问题解答\n\n3. **${query} GitHub**\n https://github.com/example\n 开源仓库`,
assistant_fetch_url: ({ url }) => `# ${url}\n\n这是从 ${url} 抓取的网页内容mock\n\n## 主要内容\n\n示例文本...`,
// 数据目录 & 图片存储
assistant_ensure_data_dir: () => (navigator.platform.includes('Win') ? 'C:\\Users\\user\\.openclaw\\clawpanel' : '/Users/user/.openclaw/clawpanel'),
assistant_save_image: ({ id }) => `/mock/images/${id}.jpg`,
@@ -347,6 +349,8 @@ export const api = {
assistantSystemInfo: () => invoke('assistant_system_info'),
assistantListProcesses: (filter) => invoke('assistant_list_processes', { filter: filter || null }),
assistantCheckPort: (port) => invoke('assistant_check_port', { port }),
assistantWebSearch: (query, maxResults) => invoke('assistant_web_search', { query, max_results: maxResults || 5 }),
assistantFetchUrl: (url) => invoke('assistant_fetch_url', { url }),
// 数据目录 & 图片存储
ensureDataDir: () => invoke('assistant_ensure_data_dir'),

View File

@@ -8,6 +8,7 @@ import { detectOpenclawStatus, isOpenclawReady, isGatewayRunning, onGatewayChang
import { wsClient } from './lib/ws-client.js'
import { api } from './lib/tauri-api.js'
import { version as APP_VERSION } from '../package.json'
import { statusIcon } from './lib/icons.js'
// 样式
import './style/variables.css'
@@ -19,6 +20,7 @@ import './style/chat.css'
import './style/agents.css'
import './style/debug.css'
import './style/assistant.css'
import './style/ai-drawer.css'
// 初始化主题
initTheme()
@@ -183,7 +185,7 @@ async function boot() {
banner.id = 'pw-change-banner'
banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:999;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;padding:10px 20px;display:flex;align-items:center;justify-content:center;gap:12px;font-size:13px;font-weight:500;box-shadow:0 2px 8px rgba(0,0,0,0.15)'
banner.innerHTML = `
<span>⚠️ 当前使用的是系统生成的默认密码,为了安全请尽快修改</span>
<span>${statusIcon('warn', 14)} 当前使用的是系统生成的默认密码,为了安全请尽快修改</span>
<a href="#/security" style="color:#fff;background:rgba(255,255,255,0.2);padding:4px 14px;border-radius:6px;text-decoration:none;font-size:12px;font-weight:600" onclick="document.getElementById('pw-change-banner').remove();sessionStorage.removeItem('clawpanel_must_change_pw')">前往安全设置</a>
<button onclick="this.parentElement.remove()" style="background:none;border:none;color:rgba(255,255,255,0.7);cursor:pointer;font-size:16px;padding:0 4px;margin-left:4px">✕</button>
`
@@ -286,7 +288,7 @@ function setupGatewayBanner() {
banner.classList.remove('gw-banner-hidden')
banner.innerHTML = `
<div class="gw-banner-content">
<span class="gw-banner-icon"></span>
<span class="gw-banner-icon">${statusIcon('warn', 16)}</span>
<span>Gateway 未启动,部分功能不可用</span>
<button class="btn btn-sm btn-primary" id="btn-gw-start">启动 Gateway</button>
</div>
@@ -302,7 +304,7 @@ function setupGatewayBanner() {
const errMsg = err.message || String(err)
banner.innerHTML = `
<div class="gw-banner-content">
<span class="gw-banner-icon"></span>
<span class="gw-banner-icon">${statusIcon('warn', 16)}</span>
<span>启动失败: ${errMsg}</span>
<button class="btn btn-sm btn-primary" id="btn-gw-start">重试</button>
<a class="btn btn-sm btn-ghost" href="#/logs" style="color:inherit;text-decoration:underline">查看日志</a>
@@ -331,7 +333,7 @@ function setupGatewayBanner() {
} catch {}
banner.innerHTML = `
<div class="gw-banner-content">
<span class="gw-banner-icon"></span>
<span class="gw-banner-icon">${statusIcon('warn', 16)}</span>
<span>启动超时Gateway 可能仍在启动中</span>
<button class="btn btn-sm btn-primary" id="btn-gw-start">重试</button>
<a class="btn btn-sm btn-ghost" href="#/logs" style="color:inherit;text-decoration:underline">查看日志</a>
@@ -353,7 +355,7 @@ function showGuardianRecovery() {
banner.classList.remove('gw-banner-hidden')
banner.innerHTML = `
<div class="gw-banner-content" style="flex-wrap:wrap;gap:8px">
<span class="gw-banner-icon">🛠</span>
<span class="gw-banner-icon">${statusIcon('warn', 16)}</span>
<span>Gateway 反复启动失败,可能配置有误</span>
<button class="btn btn-sm btn-primary" id="btn-gw-recover-restart">重试启动</button>
<button class="btn btn-sm btn-secondary" id="btn-gw-recover-backup">从备份恢复</button>
@@ -384,4 +386,66 @@ function showGuardianRecovery() {
const auth = await checkAuth()
if (!auth.ok) await showLoginOverlay(auth.defaultPw)
boot()
// 初始化全局 AI 助手浮动按钮(延迟加载,不阻塞启动)
setTimeout(async () => {
const { initAIFab, registerPageContext, openAIDrawerWithError } = await import('./components/ai-drawer.js')
initAIFab()
// 注册各页面上下文提供器
registerPageContext('/chat-debug', async () => {
const { isOpenclawReady, isGatewayRunning } = await import('./lib/app-state.js')
const { wsClient } = await import('./lib/ws-client.js')
const { api } = await import('./lib/tauri-api.js')
const lines = ['## 系统诊断快照']
lines.push(`- OpenClaw: ${isOpenclawReady() ? '就绪' : '未就绪'}`)
lines.push(`- Gateway: ${isGatewayRunning() ? '运行中' : '未运行'}`)
lines.push(`- WebSocket: ${wsClient.connected ? '已连接' : '未连接'}`)
try {
const node = await api.checkNode()
lines.push(`- Node.js: ${node?.version || '未知'}`)
} catch {}
try {
const ver = await api.getVersionInfo()
lines.push(`- 版本: ${ver?.current || '?'}${ver?.latest || '?'}`)
} catch {}
return { detail: lines.join('\n') }
})
registerPageContext('/services', async () => {
const { isGatewayRunning } = await import('./lib/app-state.js')
const { api } = await import('./lib/tauri-api.js')
const lines = ['## 服务状态']
lines.push(`- Gateway: ${isGatewayRunning() ? '运行中' : '未运行'}`)
try {
const svc = await api.getServicesStatus()
if (svc?.[0]) {
lines.push(`- CLI: ${svc[0].cli_installed ? '已安装' : '未安装'}`)
lines.push(`- PID: ${svc[0].pid || '无'}`)
}
} catch {}
return { detail: lines.join('\n') }
})
registerPageContext('/gateway', async () => {
const { api } = await import('./lib/tauri-api.js')
try {
const config = await api.readOpenclawConfig()
const gw = config?.gateway || {}
const lines = ['## Gateway 配置']
lines.push(`- 端口: ${gw.port || 18789}`)
lines.push(`- 模式: ${gw.mode || 'local'}`)
lines.push(`- Token: ${gw.auth?.token ? '已设置' : '未设置'}`)
if (gw.controlUi?.allowedOrigins) lines.push(`- Origins: ${JSON.stringify(gw.controlUi.allowedOrigins)}`)
return { detail: lines.join('\n') }
} catch { return null }
})
registerPageContext('/setup', () => {
return { detail: '用户正在进行 OpenClaw 初始安装,请帮助检查 Node.js 环境和网络状况' }
})
// 挂到全局,供安装/升级失败时调用
window.__openAIDrawerWithError = openAIDrawerWithError
}, 500)
})()

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?.()

156
src/style/ai-drawer.css Normal file
View File

@@ -0,0 +1,156 @@
/* ── 全局 AI 助手浮动按钮 ── */
.ai-fab {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 9000;
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--accent);
color: #fff;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35);
transition: transform 0.2s, box-shadow 0.2s;
touch-action: none; /* 拖动时禁止浏览器默认手势 */
user-select: none;
}
.ai-fab:hover {
transform: scale(1.08);
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.45);
}
.ai-fab.dragging {
opacity: 0.85;
transform: scale(1.12);
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.5);
cursor: grabbing;
}
.ai-fab svg {
width: 22px;
height: 22px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
pointer-events: none; /* 防止 SVG 吃掉 pointer 事件 */
}
/* FAB 红点:有待处理的错误上下文 */
.ai-fab.has-error::after {
content: '';
position: absolute;
top: 2px;
right: 2px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--error, #ef4444);
border: 2px solid #fff;
animation: fab-pulse 1.5s ease-in-out infinite;
}
@keyframes fab-pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.8; }
}
/* ── 错误上下文 Banner助手页面内 ── */
.ast-error-banner {
margin: 0 0 12px;
padding: 10px 14px;
background: var(--error-muted, rgba(239, 68, 68, 0.08));
border: 1px solid var(--error-border, rgba(239, 68, 68, 0.25));
border-radius: var(--radius-lg, 12px);
font-size: 12px;
line-height: 1.5;
color: var(--text-secondary);
position: relative;
}
.ast-error-banner-header {
display: flex;
align-items: center;
gap: 8px;
}
.ast-error-banner-icon {
flex-shrink: 0;
color: var(--error, #ef4444);
font-size: 16px;
}
.ast-error-banner-title {
flex: 1;
font-weight: 600;
color: var(--text-primary);
font-size: 13px;
}
.ast-error-banner-actions {
display: flex;
gap: 6px;
flex-shrink: 0;
}
.ast-error-banner-actions button {
padding: 3px 10px;
border-radius: 6px;
border: 1px solid transparent;
font-size: 11px;
cursor: pointer;
transition: background 0.15s;
}
.ast-error-banner .btn-analyze {
background: var(--error, #ef4444);
color: #fff;
border-color: var(--error, #ef4444);
}
.ast-error-banner .btn-analyze:hover {
opacity: 0.9;
}
.ast-error-banner .btn-dismiss {
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.ast-error-banner .btn-dismiss:hover {
background: var(--bg-secondary);
}
.ast-error-banner-hint {
margin-top: 6px;
color: var(--text-tertiary);
}
.ast-error-banner-detail {
margin-top: 8px;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.ast-error-banner-detail.expanded {
max-height: 300px;
}
.ast-error-banner-detail pre {
margin: 0;
padding: 8px 10px;
background: var(--bg-tertiary);
border-radius: 6px;
font-size: 11px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
max-height: 280px;
overflow-y: auto;
color: var(--text-secondary);
}
.ast-error-toggle {
background: none;
border: none;
color: var(--text-tertiary);
font-size: 11px;
cursor: pointer;
padding: 0;
margin-top: 4px;
text-decoration: underline;
}
.ast-error-toggle:hover {
color: var(--text-primary);
}

View File

@@ -426,6 +426,57 @@
border-color: var(--accent);
}
/* 首次引导提示 */
.ast-page-guide {
display: flex;
align-items: center;
gap: 10px;
margin-top: 16px;
padding: 10px 14px;
background: var(--accent-muted, rgba(99, 102, 241, 0.08));
border: 1px solid var(--accent-border, rgba(99, 102, 241, 0.2));
border-radius: var(--radius-lg);
font-size: 12px;
line-height: 1.5;
color: var(--text-secondary);
text-align: left;
width: 100%;
max-width: 560px;
position: relative;
}
.ast-guide-badge {
flex-shrink: 0;
background: var(--accent);
color: #fff;
font-size: 11px;
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
white-space: nowrap;
}
.ast-guide-text {
flex: 1;
min-width: 0;
}
.ast-guide-text b {
color: var(--text-primary);
}
.ast-guide-close {
flex-shrink: 0;
background: none;
border: none;
color: var(--text-tertiary);
font-size: 18px;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
line-height: 1;
}
.ast-guide-close:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
/* ── Skills 网格 ── */
.ast-skills-grid {
display: grid;
@@ -1520,3 +1571,112 @@
justify-content: flex-end;
gap: 8px;
}
/* ── 灵魂移植卡片 ── */
.ast-soul-card {
border: 1px solid var(--border-secondary);
border-radius: var(--radius-md);
background: var(--bg-primary);
overflow: hidden;
}
.ast-soul-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-secondary);
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
}
.ast-soul-header svg { flex-shrink: 0; }
.ast-soul-files {
display: flex;
flex-direction: column;
}
.ast-soul-file {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 14px;
font-size: 12px;
border-bottom: 1px solid var(--border-secondary);
transition: background var(--transition-fast);
}
.ast-soul-file:last-child { border-bottom: none; }
.ast-soul-file:hover { background: var(--bg-card-hover); }
.ast-soul-file-icon {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.ast-soul-file.loaded .ast-soul-file-icon {
background: var(--success-muted, rgba(34,197,94,0.1));
color: var(--success);
}
.ast-soul-file.missing .ast-soul-file-icon {
background: var(--error-muted, rgba(239,68,68,0.1));
color: var(--error);
}
.ast-soul-file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.ast-soul-file-name {
font-weight: 600;
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 12px;
}
.ast-soul-file.missing .ast-soul-file-name {
color: var(--text-tertiary);
}
.ast-soul-file-desc {
font-size: 11px;
color: var(--text-tertiary);
line-height: 1.2;
}
.ast-soul-file-size {
font-size: 11px;
color: var(--text-tertiary);
font-family: var(--font-mono);
flex-shrink: 0;
}
/* 按钮样式补充 */
.btn-ghost {
background: transparent;
border: 1px solid var(--border-secondary);
color: var(--text-secondary);
}
.btn-ghost:hover {
background: var(--bg-card-hover);
border-color: var(--border-primary);
color: var(--text-primary);
}
.btn svg { vertical-align: -2px; }
/* 加载旋转动画 */
.ast-spin {
animation: ast-spin 1s linear infinite;
}
@keyframes ast-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}

View File

@@ -783,6 +783,54 @@
color: var(--text-tertiary);
}
/* 首次引导提示 */
.chat-page-guide {
margin: 0 16px 8px;
flex-shrink: 0;
}
.chat-guide-inner {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 14px;
background: var(--accent-muted, rgba(99, 102, 241, 0.08));
border: 1px solid var(--accent-border, rgba(99, 102, 241, 0.2));
border-radius: var(--radius-lg);
font-size: 12px;
line-height: 1.6;
color: var(--text-secondary);
position: relative;
}
.chat-guide-icon {
flex-shrink: 0;
color: var(--accent);
margin-top: 2px;
}
.chat-guide-content b {
color: var(--text-primary);
font-size: 13px;
}
.chat-guide-content p {
margin: 4px 0 0;
}
.chat-guide-close {
position: absolute;
top: 6px;
right: 8px;
background: none;
border: none;
color: var(--text-tertiary);
font-size: 18px;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
line-height: 1;
}
.chat-guide-close:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.chat-lightbox-img {
max-width: 90%;
max-height: 90%;

View File

@@ -140,6 +140,12 @@
font-size: var(--font-size-xs);
}
.btn-xs {
padding: 2px 8px;
font-size: 11px;
border-radius: var(--radius-sm);
}
/* 表单 */
.form-group {
margin-bottom: var(--space-lg);
@@ -275,8 +281,16 @@ mark {
padding: var(--space-xl);
min-width: 360px;
max-width: 500px;
max-height: calc(100vh - 60px);
display: flex;
flex-direction: column;
box-shadow: var(--shadow-lg);
}
.modal-body {
flex: 1;
overflow-y: auto;
min-height: 0;
}
.modal-title {
font-size: var(--font-size-lg);

View File

@@ -50,6 +50,17 @@
color: var(--text-tertiary);
}
.overview-actions {
display: flex;
align-items: center;
gap: 8px;
}
.overview-status {
font-size: var(--font-size-sm);
font-weight: 600;
margin-right: 4px;
}
.overview-value {
display: flex;
align-items: center;