mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-30 21:00:30 +08:00
feat: v0.6.0 — 公益AI接口 + Agent灵魂借尸还魂 + 知识库 + 全局AI诊断 + 官网改版
This commit is contained in:
211
src/components/ai-drawer.js
Normal file
211
src/components/ai-drawer.js
Normal 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 {}
|
||||
}
|
||||
@@ -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 = '完成'
|
||||
|
||||
@@ -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
91
src/lib/icons.js
Normal 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
254
src/lib/openclaw-kb.js
Normal 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** — WhatsApp(allowFrom, 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
|
||||
\`\`\`
|
||||
**Windows(WSL2 推荐):**
|
||||
\`\`\`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/responses(OpenAI 兼容)
|
||||
- **支持模型**: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()
|
||||
@@ -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'),
|
||||
|
||||
74
src/main.js
74
src/main.js
@@ -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)
|
||||
})()
|
||||
|
||||
@@ -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
@@ -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 = '一键修复配对' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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="知道了">×</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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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?.()
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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 ? '无视风险模式(无密码)' : '未设置密码')
|
||||
|
||||
@@ -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?.()
|
||||
|
||||
@@ -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
156
src/style/ai-drawer.css
Normal 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);
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user