mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-07 05:52:54 +08:00
feat(ui): 收口导航并优化实例切换与离线提示
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* 侧边导航栏
|
||||
*/
|
||||
import { navigate, getCurrentRoute } from '../router.js'
|
||||
import { navigate, getCurrentRoute, reloadCurrentRoute } from '../router.js'
|
||||
import { toggleTheme, getTheme } from '../lib/theme.js'
|
||||
import { isOpenclawReady, getActiveInstance, switchInstance, onInstanceChange } from '../lib/app-state.js'
|
||||
import { api } from '../lib/tauri-api.js'
|
||||
import { toast } from './toast.js'
|
||||
import { version as APP_VERSION } from '../../package.json'
|
||||
|
||||
const NAV_ITEMS_FULL = [
|
||||
@@ -36,7 +37,6 @@ const NAV_ITEMS_FULL = [
|
||||
{
|
||||
section: '扩展',
|
||||
items: [
|
||||
{ route: '/extensions', label: '扩展工具', icon: 'extensions' },
|
||||
{ route: '/skills', label: 'Skills', icon: 'skills' },
|
||||
]
|
||||
},
|
||||
@@ -63,12 +63,6 @@ const NAV_ITEMS_SETUP = [
|
||||
{ route: '/assistant', label: 'AI 助手', icon: 'assistant' },
|
||||
]
|
||||
},
|
||||
{
|
||||
section: '',
|
||||
items: [
|
||||
{ route: '/extensions', label: '扩展工具', icon: 'extensions' },
|
||||
]
|
||||
},
|
||||
{
|
||||
section: '',
|
||||
items: [
|
||||
@@ -215,8 +209,11 @@ export function renderSidebar(el) {
|
||||
if (id !== getActiveInstance().id) {
|
||||
opt.style.opacity = '0.5'
|
||||
switchInstance(id).then(() => {
|
||||
const inst = getActiveInstance()
|
||||
const desc = inst.type === 'local' ? '本机' : inst.name
|
||||
toast(`已切换到 ${desc} — 模型配置、Agent 等将管理该实例`, 'success')
|
||||
renderSidebar(el)
|
||||
navigate(getCurrentRoute())
|
||||
reloadCurrentRoute()
|
||||
})
|
||||
}
|
||||
return
|
||||
@@ -281,16 +278,20 @@ async function _toggleInstanceDropdown(sidebarEl) {
|
||||
const [data, health] = await Promise.all([api.instanceList(), api.instanceHealthAll()])
|
||||
const healthMap = Object.fromEntries((health || []).map(h => [h.id, h]))
|
||||
const activeId = getActiveInstance().id
|
||||
let html = ''
|
||||
let html = '<div class="instance-hint">切换后,模型配置、Agent 等页面将管理对应实例</div>'
|
||||
for (const inst of data.instances) {
|
||||
const h = healthMap[inst.id] || {}
|
||||
const active = inst.id === activeId ? ' active' : ''
|
||||
const dot = h.online !== false ? 'online' : 'offline'
|
||||
const badge = inst.type === 'docker' ? '<span class="instance-badge docker">🦞 龙虾</span>' : inst.type === 'remote' ? '<span class="instance-badge remote">远程</span>' : ''
|
||||
const port = inst.endpoint ? inst.endpoint.match(/:(\d+)/)?.[1] : ''
|
||||
const portTag = port ? `<span class="instance-port">:${port}</span>` : ''
|
||||
html += `<div class="instance-option${active}" data-id="${inst.id}">
|
||||
<span class="instance-dot ${dot}"></span>
|
||||
<span class="instance-opt-name">${_escSidebar(inst.name)}</span>
|
||||
${portTag}
|
||||
${badge}
|
||||
${active ? '<span class="instance-active-tag">当前</span>' : ''}
|
||||
</div>`
|
||||
}
|
||||
html += '<div class="instance-divider"></div>'
|
||||
|
||||
@@ -64,6 +64,7 @@ const PATHS = {
|
||||
'gear': '<circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>',
|
||||
'plus-circle': '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/>',
|
||||
'crown': '<path d="M2 20h20L19 9l-5 5-2-7-2 7-5-5-3 11z"/><path d="M2 20h20v2H2z"/>',
|
||||
'hammer': '<path d="M15 12l-8.5 8.5c-.83.83-2.17.83-3 0 0 0 0 0 0 0a2.12 2.12 0 010-3L12 9"/><path d="M17.64 15L22 10.64"/><path d="M20.91 11.7l-1.25-1.25c-.6-.6-.93-1.4-.93-2.25V6.5a.5.5 0 01.5-.5H20a2.77 2.77 0 001.95-.82l.05-.05a1 1 0 00-.7-1.7h-4.19A3 3 0 0015 5.59V8"/>',
|
||||
'send': '<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>',
|
||||
}
|
||||
|
||||
|
||||
107
src/main.js
107
src/main.js
@@ -6,7 +6,7 @@ import { renderSidebar, openMobileSidebar } from './components/sidebar.js'
|
||||
import { initTheme } from './lib/theme.js'
|
||||
import { detectOpenclawStatus, isOpenclawReady, isGatewayRunning, onGatewayChange, startGatewayPoll, onGuardianGiveUp, resetAutoRestart, loadActiveInstance, getActiveInstance, onInstanceChange } from './lib/app-state.js'
|
||||
import { wsClient } from './lib/ws-client.js'
|
||||
import { api } from './lib/tauri-api.js'
|
||||
import { api, checkBackendHealth, isBackendOnline, onBackendStatusChange } from './lib/tauri-api.js'
|
||||
import { version as APP_VERSION } from '../package.json'
|
||||
import { statusIcon } from './lib/icons.js'
|
||||
import { tryShowEngagement } from './components/engagement.js'
|
||||
@@ -61,6 +61,82 @@ function _hideSplash() {
|
||||
if (splash) { splash.classList.add('hide'); setTimeout(() => splash.remove(), 500) }
|
||||
}
|
||||
|
||||
// === 后端离线检测(Web 模式) ===
|
||||
let _backendRetryTimer = null
|
||||
|
||||
function showBackendDownOverlay() {
|
||||
if (document.getElementById('backend-down-overlay')) return
|
||||
_hideSplash()
|
||||
const overlay = document.createElement('div')
|
||||
overlay.id = 'backend-down-overlay'
|
||||
overlay.innerHTML = `
|
||||
<div class="login-card" style="text-align:center">
|
||||
${_logoSvg}
|
||||
<div class="login-title" style="color:var(--error,#ef4444)">后端未启动</div>
|
||||
<div class="login-desc" style="line-height:1.8">
|
||||
ClawPanel 后端服务未运行,无法获取真实数据。<br>
|
||||
<span style="font-size:12px;color:var(--text-tertiary)">请在服务器上启动后端服务后刷新页面。</span>
|
||||
</div>
|
||||
<div style="background:var(--bg-tertiary);border-radius:var(--radius-md,8px);padding:14px 18px;margin:16px 0;text-align:left;font-family:var(--font-mono,monospace);font-size:12px;line-height:1.8;user-select:all;color:var(--text-secondary)">
|
||||
<div style="color:var(--text-tertiary);margin-bottom:4px"># 开发模式</div>
|
||||
npm run dev<br>
|
||||
<div style="color:var(--text-tertiary);margin-top:8px;margin-bottom:4px"># 生产模式</div>
|
||||
npm run preview
|
||||
</div>
|
||||
<button class="login-btn" id="btn-backend-retry" style="margin-top:8px">
|
||||
<span id="backend-retry-text">重新检测</span>
|
||||
</button>
|
||||
<div id="backend-retry-status" style="font-size:12px;color:var(--text-tertiary);margin-top:12px"></div>
|
||||
<div style="margin-top:16px;font-size:11px;color:#aaa">
|
||||
<a href="https://claw.qt.cool" target="_blank" rel="noopener" style="color:#aaa;text-decoration:none">claw.qt.cool</a>
|
||||
<span style="margin:0 6px">·</span>v${APP_VERSION}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
document.body.appendChild(overlay)
|
||||
|
||||
let retrying = false
|
||||
const btn = overlay.querySelector('#btn-backend-retry')
|
||||
const statusEl = overlay.querySelector('#backend-retry-status')
|
||||
const textEl = overlay.querySelector('#backend-retry-text')
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
if (retrying) return
|
||||
retrying = true
|
||||
btn.disabled = true
|
||||
textEl.textContent = '检测中...'
|
||||
statusEl.textContent = ''
|
||||
|
||||
const ok = await checkBackendHealth()
|
||||
if (ok) {
|
||||
statusEl.textContent = '后端已连接,正在加载...'
|
||||
statusEl.style.color = 'var(--success,#22c55e)'
|
||||
overlay.classList.add('hide')
|
||||
setTimeout(() => { overlay.remove(); location.reload() }, 600)
|
||||
} else {
|
||||
statusEl.textContent = '后端仍未响应,请确认服务已启动'
|
||||
statusEl.style.color = 'var(--error,#ef4444)'
|
||||
textEl.textContent = '重新检测'
|
||||
btn.disabled = false
|
||||
retrying = false
|
||||
}
|
||||
})
|
||||
|
||||
// 自动轮询:每 5 秒检测一次
|
||||
if (_backendRetryTimer) clearInterval(_backendRetryTimer)
|
||||
_backendRetryTimer = setInterval(async () => {
|
||||
const ok = await checkBackendHealth()
|
||||
if (ok) {
|
||||
clearInterval(_backendRetryTimer)
|
||||
_backendRetryTimer = null
|
||||
statusEl.textContent = '后端已连接,正在加载...'
|
||||
statusEl.style.color = 'var(--success,#22c55e)'
|
||||
overlay.classList.add('hide')
|
||||
setTimeout(() => { overlay.remove(); location.reload() }, 600)
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
let _loginFailCount = 0
|
||||
const CAPTCHA_THRESHOLD = 3
|
||||
|
||||
@@ -221,7 +297,6 @@ async function boot() {
|
||||
registerRoute('/agents', () => import('./pages/agents.js'))
|
||||
registerRoute('/gateway', () => import('./pages/gateway.js'))
|
||||
registerRoute('/memory', () => import('./pages/memory.js'))
|
||||
registerRoute('/extensions', () => import('./pages/extensions.js'))
|
||||
registerRoute('/skills', () => import('./pages/skills.js'))
|
||||
registerRoute('/security', () => import('./pages/security.js'))
|
||||
registerRoute('/about', () => import('./pages/about.js'))
|
||||
@@ -307,9 +382,20 @@ async function boot() {
|
||||
})
|
||||
|
||||
// 守护放弃时,弹出恢复选项
|
||||
onGuardianGiveUp(() => {
|
||||
showGuardianRecovery()
|
||||
})
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
import('@tauri-apps/api/event').then(async ({ listen }) => {
|
||||
await listen('guardian-event', (e) => {
|
||||
if (e.payload?.kind === 'give_up') showGuardianRecovery()
|
||||
})
|
||||
}).catch(() => {})
|
||||
api.guardianStatus().then(status => {
|
||||
if (status?.giveUp) showGuardianRecovery()
|
||||
}).catch(() => {})
|
||||
} else {
|
||||
onGuardianGiveUp(() => {
|
||||
showGuardianRecovery()
|
||||
})
|
||||
}
|
||||
|
||||
// 实例切换时,重连 WebSocket + 重新检测状态
|
||||
onInstanceChange(async () => {
|
||||
@@ -601,8 +687,17 @@ function startUpdateChecker() {
|
||||
_updateCheckTimer = setInterval(checkGlobalUpdate, UPDATE_CHECK_INTERVAL)
|
||||
}
|
||||
|
||||
// 启动:先检查认证,再加载应用
|
||||
// 启动:先检查后端 → 认证 → 加载应用
|
||||
;(async () => {
|
||||
// Web 模式:先检测后端是否在线(不在线则显示提示,不加载应用)
|
||||
if (!isTauri) {
|
||||
const backendOk = await checkBackendHealth()
|
||||
if (!backendOk) {
|
||||
showBackendDownOverlay()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const auth = await checkAuth()
|
||||
if (!auth.ok) await showLoginOverlay(auth.defaultPw)
|
||||
boot()
|
||||
|
||||
@@ -473,6 +473,11 @@ const PROJECTS = [
|
||||
desc: 'AI Agent 框架,支持多模型协作、工具调用、记忆管理-中文优化版',
|
||||
url: 'https://github.com/1186258278/OpenClawChineseTranslation',
|
||||
},
|
||||
{
|
||||
name: 'ClawPanel',
|
||||
desc: 'OpenClaw 可视化管理面板,Tauri v2 桌面应用',
|
||||
url: 'https://github.com/qingchencloud/clawpanel',
|
||||
},
|
||||
{
|
||||
name: 'ClawApp',
|
||||
desc: '跨平台移动聊天客户端,H5 + 代理服务器架构,支持离线和流式传输',
|
||||
@@ -483,11 +488,6 @@ const PROJECTS = [
|
||||
desc: '全协议内网穿透工具,Cloud 模式免费 HTTP/WS + Relay 模式自建中继',
|
||||
url: 'https://github.com/qingchencloud/cftunnel',
|
||||
},
|
||||
{
|
||||
name: 'ClawPanel',
|
||||
desc: 'OpenClaw 可视化管理面板,Tauri v2 桌面应用',
|
||||
url: 'https://github.com/qingchencloud/clawpanel',
|
||||
},
|
||||
]
|
||||
|
||||
function renderProjects(page) {
|
||||
@@ -509,10 +509,9 @@ function renderProjects(page) {
|
||||
|
||||
const LINKS = [
|
||||
{ label: 'Claw 项目官网', url: 'https://claw.qt.cool', primary: true },
|
||||
{ label: 'cftunnel 官网', url: 'https://cftunnel.qt.cool' },
|
||||
{ label: 'cftunnel 桌面客户端', url: 'https://github.com/qingchencloud/cftunnel-app/releases' },
|
||||
{ label: 'OpenClaw 中文翻译', url: 'https://github.com/1186258278/OpenClawChineseTranslation' },
|
||||
{ label: 'ClawApp 文档', url: 'https://github.com/qingchencloud/clawapp#readme' },
|
||||
{ label: 'ClawApp 手机客户端', url: 'https://clawapp.qt.cool' },
|
||||
{ label: 'cftunnel 内网穿透', url: 'https://cftunnel.qt.cool' },
|
||||
]
|
||||
|
||||
function renderContribute(page) {
|
||||
|
||||
@@ -96,14 +96,11 @@ ${personality}
|
||||
- **开源项目**:
|
||||
- **ClawPanel** — OpenClaw 可视化管理面板(Tauri v2),官网 https://claw.qt.cool
|
||||
- **OpenClaw 汉化版** — AI Agent 平台中文版,npm install -g @qingchencloud/openclaw-zh
|
||||
- **ClawApp** — OpenClaw 手机聊天客户端(H5/PWA),通过一键脚本部署,GitHub: https://github.com/qingchencloud/clawapp
|
||||
- **cftunnel** — 全协议内网穿透 CLI(Cloudflare Tunnel + frp 双引擎,Go 编写),GitHub: https://github.com/qingchencloud/cftunnel
|
||||
- **cfsite** — Cloudflare Pages 部署 CLI
|
||||
- **WebToEXE** — 网站打包成桌面应用
|
||||
|
||||
## ClawPanel 是什么
|
||||
- OpenClaw 的可视化管理面板,基于 Tauri v2 的跨平台桌面应用(Windows/macOS/Linux)
|
||||
- 支持仪表盘监控、模型配置、Agent 管理、实时聊天、记忆文件管理、内网穿透、AI 助手工具调用等
|
||||
- 支持仪表盘监控、模型配置、Agent 管理、实时聊天、记忆文件管理、AI 助手工具调用等
|
||||
- 官网: https://claw.qt.cool | GitHub: https://github.com/qingchencloud/clawpanel
|
||||
|
||||
## OpenClaw 是什么
|
||||
@@ -169,14 +166,7 @@ ${personality}
|
||||
当用户问到如何安装其他产品时,推荐以下安装方式:
|
||||
- **OpenClaw 汉化版**: npm install -g @qingchencloud/openclaw-zh(推荐国内用户)
|
||||
- **OpenClaw 官方版**: npm install -g openclaw
|
||||
- **ClawApp 手机客户端**:
|
||||
- Mac/Linux: curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawapp/main/install.sh | bash
|
||||
- Windows: irm https://raw.githubusercontent.com/qingchencloud/clawapp/main/install.ps1 | iex
|
||||
- 或手动: git clone https://github.com/qingchencloud/clawapp.git && cd clawapp && npm run install:all && npm run build:h5
|
||||
- **ClawPanel**: 从 https://github.com/qingchencloud/clawpanel/releases 下载
|
||||
- **cftunnel 内网穿透**:
|
||||
- Mac/Linux: curl -fsSL https://raw.githubusercontent.com/qingchencloud/cftunnel/main/install.sh | bash
|
||||
- Windows: irm https://raw.githubusercontent.com/qingchencloud/cftunnel/main/install.ps1 | iex
|
||||
- **更多项目**: 访问 https://github.com/qingchencloud
|
||||
|
||||
## 社区贡献指引
|
||||
@@ -186,7 +176,6 @@ ${personality}
|
||||
引导用户到对应仓库提交 Issue,帮用户整理好格式:
|
||||
- **ClawPanel**: https://github.com/qingchencloud/clawpanel/issues/new
|
||||
- **OpenClaw 汉化版**: https://github.com/qingchencloud/openclaw-zh/issues/new
|
||||
- **cftunnel**: https://github.com/qingchencloud/cftunnel/issues/new
|
||||
|
||||
Issue 模板(帮用户填好):
|
||||
\`\`\`
|
||||
@@ -659,8 +648,7 @@ const BUILTIN_SKILLS = [
|
||||
6. 用代码块展示完整 Issue 内容,给出对应仓库的 Issue 链接:
|
||||
- ClawPanel: https://github.com/qingchencloud/clawpanel/issues/new
|
||||
- OpenClaw: https://github.com/qingchencloud/openclaw-zh/issues/new
|
||||
- cftunnel: https://github.com/qingchencloud/cftunnel/issues/new
|
||||
- ClawApp: https://github.com/qingchencloud/clawapp/issues/new`,
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'pr-assistant',
|
||||
|
||||
@@ -65,9 +65,7 @@ async function loadDashboardData(page) {
|
||||
])
|
||||
const secondaryP = Promise.allSettled([
|
||||
api.listAgents(),
|
||||
api.getCftunnelStatus(),
|
||||
api.readMcpConfig(),
|
||||
api.getClawappStatus(),
|
||||
api.listBackups(),
|
||||
])
|
||||
const logsP = api.readLogTail('gateway', 20).catch(() => '')
|
||||
@@ -97,25 +95,23 @@ async function loadDashboardData(page) {
|
||||
if (patched) api.writeOpenclawConfig(config).catch(() => {})
|
||||
}
|
||||
|
||||
renderStatCards(page, services, version, [], config, null)
|
||||
renderStatCards(page, services, version, [], config)
|
||||
|
||||
// 第二波:Agent、隧道、MCP、ClawApp、备份 → 更新卡片 + 渲染总览
|
||||
const [agentsRes, tunnelRes, mcpRes, clawappRes, backupsRes] = await secondaryP
|
||||
// 第二波:Agent、MCP、备份 → 更新卡片 + 渲染总览
|
||||
const [agentsRes, mcpRes, backupsRes] = await secondaryP
|
||||
const agents = agentsRes.status === 'fulfilled' ? agentsRes.value : []
|
||||
const tunnel = tunnelRes.status === 'fulfilled' ? tunnelRes.value : null
|
||||
const mcpConfig = mcpRes.status === 'fulfilled' ? mcpRes.value : null
|
||||
const clawapp = clawappRes.status === 'fulfilled' ? clawappRes.value : null
|
||||
const backups = backupsRes.status === 'fulfilled' ? backupsRes.value : []
|
||||
|
||||
renderStatCards(page, services, version, agents, config, tunnel)
|
||||
renderOverview(page, services, clawapp, tunnel, mcpConfig, backups, config, agents)
|
||||
renderStatCards(page, services, version, agents, config)
|
||||
renderOverview(page, services, mcpConfig, backups, config, agents)
|
||||
|
||||
// 第三波:日志(最低优先级)
|
||||
const logs = await logsP
|
||||
renderLogs(page, logs)
|
||||
}
|
||||
|
||||
function renderStatCards(page, services, version, agents, config, tunnel) {
|
||||
function renderStatCards(page, services, version, agents, config) {
|
||||
const cardsEl = page.querySelector('#stat-cards')
|
||||
const gw = services.find(s => s.label === 'ai.openclaw.gateway')
|
||||
const runningCount = services.filter(s => s.running).length
|
||||
@@ -154,14 +150,6 @@ function renderStatCards(page, services, version, agents, config, tunnel) {
|
||||
<div class="stat-card-value">${modelCount} 个</div>
|
||||
<div class="stat-card-meta">基于 ${providerCount} 个渠道商</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">内网穿透隧道</span>
|
||||
<span class="status-dot ${tunnel?.running ? 'running' : 'stopped'}"></span>
|
||||
</div>
|
||||
<div class="stat-card-value">${tunnel?.running ? '运行中' : (tunnel?.installed ? '已停止' : '未配置')}</div>
|
||||
<div class="stat-card-meta">${tunnel?.routes ? tunnel.routes.length + ' 个路由映射' : '——'}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">基础服务</span>
|
||||
@@ -172,7 +160,7 @@ function renderStatCards(page, services, version, agents, config, tunnel) {
|
||||
`
|
||||
}
|
||||
|
||||
function renderOverview(page, services, clawapp, tunnel, mcpConfig, backups, config, agents) {
|
||||
function renderOverview(page, services, mcpConfig, backups, config, agents) {
|
||||
const containerEl = page.querySelector('#dashboard-overview-container')
|
||||
const gw = services.find(s => s.label === 'ai.openclaw.gateway')
|
||||
const mcpCount = mcpConfig?.mcpServers ? Object.keys(mcpConfig.mcpServers).length : 0
|
||||
@@ -208,32 +196,6 @@ function renderOverview(page, services, clawapp, tunnel, mcpConfig, backups, con
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-item">
|
||||
<div class="overview-label">
|
||||
<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-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">
|
||||
<div class="overview-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
||||
Cloudflare 隧道
|
||||
</div>
|
||||
<div class="overview-value" style="color: ${tunnel?.running ? 'var(--success)' : (tunnel?.installed ? 'var(--warning)' : 'var(--text-tertiary)')}">
|
||||
${tunnel?.running ? (tunnel.tunnel_name || '运行中') : (tunnel?.installed ? '已停止' : '未安装')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-item">
|
||||
<div class="overview-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>
|
||||
@@ -336,9 +298,6 @@ function bindActions(page) {
|
||||
} catch (err) { toast('重启失败: ' + err, 'error') }
|
||||
finally { actionBtn.disabled = false; actionBtn.textContent = '重启' }
|
||||
}
|
||||
if (action === 'goto-extensions') {
|
||||
navigate('/extensions')
|
||||
}
|
||||
})
|
||||
|
||||
btnRestart?.addEventListener('click', async () => {
|
||||
|
||||
@@ -150,3 +150,7 @@ function escHtml(s) {
|
||||
export function getCurrentRoute() {
|
||||
return window.location.hash.slice(1) || _defaultRoute
|
||||
}
|
||||
|
||||
export function reloadCurrentRoute() {
|
||||
loadRoute()
|
||||
}
|
||||
|
||||
@@ -151,6 +151,29 @@
|
||||
background: rgba(6,182,212,.1);
|
||||
color: #06b6d4;
|
||||
}
|
||||
.instance-hint {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
padding: 6px 10px 4px;
|
||||
line-height: 1.4;
|
||||
border-bottom: 1px solid var(--border-secondary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.instance-port {
|
||||
font-size: 10px;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text-tertiary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.instance-active-tag {
|
||||
font-size: 10px;
|
||||
padding: 0 5px;
|
||||
border-radius: 3px;
|
||||
background: rgba(99,102,241,.12);
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.instance-divider {
|
||||
height: 1px;
|
||||
background: var(--border-secondary);
|
||||
|
||||
46
tests/docker-tasking.test.js
Normal file
46
tests/docker-tasking.test.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
DOCKER_TASK_TIMEOUT_MS,
|
||||
buildDockerDispatchTargets,
|
||||
buildDockerInstanceSwitchContext,
|
||||
} from '../src/lib/docker-tasking.js'
|
||||
|
||||
test('Docker 异步任务默认超时提升到 10 分钟', () => {
|
||||
assert.equal(DOCKER_TASK_TIMEOUT_MS, 10 * 60 * 1000)
|
||||
})
|
||||
|
||||
test('Docker 派发目标会保留容器和节点信息', () => {
|
||||
const targets = buildDockerDispatchTargets([
|
||||
{ id: 'container-1234567890ab', name: 'openclaw-coder', nodeId: 'node-a' },
|
||||
{ id: 'container-bbbbbbbbbbbb', name: 'openclaw-writer', nodeId: 'node-b' },
|
||||
])
|
||||
|
||||
assert.deepEqual(targets, [
|
||||
{ containerId: 'container-1234567890ab', containerName: 'openclaw-coder', nodeId: 'node-a' },
|
||||
{ containerId: 'container-bbbbbbbbbbbb', containerName: 'openclaw-writer', nodeId: 'node-b' },
|
||||
])
|
||||
})
|
||||
|
||||
test('Docker 实例切换上下文会要求整页重载并生成正确注册参数', () => {
|
||||
const ctx = buildDockerInstanceSwitchContext({
|
||||
containerId: 'abcdef1234567890',
|
||||
name: 'openclaw-coder',
|
||||
port: '21420',
|
||||
gatewayPort: '28789',
|
||||
nodeId: 'node-a',
|
||||
})
|
||||
|
||||
assert.equal(ctx.instanceId, 'docker-abcdef123456')
|
||||
assert.equal(ctx.reloadRoute, true)
|
||||
assert.deepEqual(ctx.registration, {
|
||||
name: 'openclaw-coder',
|
||||
type: 'docker',
|
||||
endpoint: 'http://127.0.0.1:21420',
|
||||
gatewayPort: 28789,
|
||||
containerId: 'abcdef1234567890',
|
||||
nodeId: 'node-a',
|
||||
note: 'Added from Docker page',
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user