mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-06-07 16:49:42 +08:00
feat: multi-OpenClaw CLI detection/binding + i18n infrastructure
Multi-OpenClaw Detection & Binding: - Add resolve_openclaw_cli_path() and classify_cli_source() in utils.rs - Support openclawCliPath binding in clawpanel.json (user selects CLI) - VersionInfo now includes cli_path, cli_source, all_installations - scan_all_installations() detects all OpenClaw installs on system - Dashboard shows CLI source label + multi-install warning - Settings page: CLI binding UI with auto-detect and manual selection - dev-api.js synced with cli_path/cli_source fields for Web mode i18n Infrastructure: - Create src/lib/i18n.js core module (t(), setLang(), initI18n()) - Create src/locales/zh-CN.json and src/locales/en.json - Sidebar fully i18n-ized (nav labels, sections, instance switcher) - Dashboard stat cards fully i18n-ized - Settings page: language switcher UI (live reload) - initI18n() called in main.js on startup
This commit is contained in:
@@ -5,6 +5,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'
|
||||
import { t } from '../lib/i18n.js'
|
||||
|
||||
let _unsubGw = null
|
||||
|
||||
@@ -14,8 +15,8 @@ export async function render() {
|
||||
|
||||
page.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">仪表盘</h1>
|
||||
<p class="page-desc">OpenClaw 运行状态概览</p>
|
||||
<h1 class="page-title">${t('dashboard.title')}</h1>
|
||||
<p class="page-desc">${t('dashboard.desc')}</p>
|
||||
</div>
|
||||
<div class="stat-cards" id="stat-cards">
|
||||
<div class="stat-card loading-placeholder"></div>
|
||||
@@ -27,12 +28,12 @@ export async function render() {
|
||||
</div>
|
||||
<div id="dashboard-overview-container"></div>
|
||||
<div class="quick-actions">
|
||||
<button class="btn btn-secondary" id="btn-restart-gw">重启 Gateway</button>
|
||||
<button class="btn btn-secondary" id="btn-check-update">检查更新</button>
|
||||
<button class="btn btn-secondary" id="btn-create-backup">创建备份</button>
|
||||
<button class="btn btn-secondary" id="btn-restart-gw">${t('dashboard.restartGw')}</button>
|
||||
<button class="btn btn-secondary" id="btn-check-update">${t('dashboard.checkUpdate')}</button>
|
||||
<button class="btn btn-secondary" id="btn-create-backup">${t('dashboard.createBackup')}</button>
|
||||
</div>
|
||||
<div class="config-section">
|
||||
<div class="config-section-title">最近日志</div>
|
||||
<div class="config-section-title">${t('dashboard.recentLogs')}</div>
|
||||
<div class="log-viewer" id="recent-logs" style="max-height:300px"></div>
|
||||
</div>
|
||||
`
|
||||
@@ -144,8 +145,13 @@ function renderStatCards(page, services, version, agents, config) {
|
||||
const gw = services.find(s => s.label === 'ai.openclaw.gateway')
|
||||
const runningCount = services.filter(s => s.running).length
|
||||
const versionMeta = version.recommended
|
||||
? `${version.ahead_of_recommended ? `当前版本高于推荐稳定版 ${version.recommended},可能不稳定` : version.is_recommended ? '稳定版 ' + version.recommended : '推荐稳定版 ' + version.recommended}${version.latest_update_available && version.latest ? ' · 最新上游 ' + version.latest : ''}`
|
||||
: (version.latest_update_available && version.latest ? '最新上游: ' + version.latest : '版本信息未获取')
|
||||
? `${version.ahead_of_recommended ? t('dashboard.versionAhead', { version: version.recommended }) : version.is_recommended ? t('dashboard.versionStable', { version: version.recommended }) : t('dashboard.versionRecommend', { version: version.recommended })}${version.latest_update_available && version.latest ? ' · ' + t('dashboard.versionLatest', { version: version.latest }) : ''}`
|
||||
: (version.latest_update_available && version.latest ? t('dashboard.versionLatest', { version: version.latest }) : t('dashboard.versionUnknown'))
|
||||
|
||||
// CLI 路径信息
|
||||
const cliSourceLabel = { standalone: t('dashboard.cliSourceStandalone'), 'npm-zh': t('dashboard.cliSourceNpmZh'), 'npm-official': t('dashboard.cliSourceNpmOfficial'), 'npm-global': t('dashboard.cliSourceNpmGlobal') }[version.cli_source] || t('dashboard.cliSourceUnknown')
|
||||
const installCount = version.all_installations?.length || 0
|
||||
const multiInstall = installCount > 1
|
||||
|
||||
const defaultAgent = agents.find(a => a.id === 'main')?.name || 'main'
|
||||
const modelCount = config?.models?.providers ? Object.values(config.models.providers).reduce((acc, p) => acc + (p.models?.length || 0), 0) : 0
|
||||
@@ -154,47 +160,48 @@ function renderStatCards(page, services, version, agents, config) {
|
||||
cardsEl.innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">Gateway</span>
|
||||
<span class="stat-card-label">${t('dashboard.gateway')}</span>
|
||||
<span class="status-dot ${gw?.running ? 'running' : 'stopped'}"></span>
|
||||
</div>
|
||||
<div class="stat-card-value">${gw?.running ? '运行中' : '已停止'}</div>
|
||||
<div class="stat-card-meta">${gw?.pid ? 'PID: ' + gw.pid : (gw?.running ? '端口检测' : '未启动')}</div>
|
||||
<div class="stat-card-value">${gw?.running ? t('common.running') : t('common.stopped')}</div>
|
||||
<div class="stat-card-meta">${gw?.pid ? 'PID: ' + gw.pid : (gw?.running ? t('dashboard.portDetect') : t('dashboard.notStarted'))}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">版本 · ${version.source === 'official' ? '官方' : '汉化'}</span>
|
||||
<span class="stat-card-label">${t('dashboard.versionLabel')} · ${version.source === 'official' ? t('dashboard.versionOfficial') : t('dashboard.versionChinese')}</span>
|
||||
</div>
|
||||
<div class="stat-card-value">${version.current || '未知'}</div>
|
||||
<div class="stat-card-value">${version.current || t('common.unknown')}</div>
|
||||
<div class="stat-card-meta">${versionMeta}</div>
|
||||
${version.cli_path ? `<div class="stat-card-meta" style="margin-top:2px;font-size:11px;opacity:0.7" title="${escapeHtml(version.cli_path)}">${cliSourceLabel}${multiInstall ? ' · <span style="color:var(--warning)">' + t('dashboard.installCount', { count: installCount }) + '</span>' : ''}</div>` : ''}
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">Agent 舰队</span>
|
||||
<span class="stat-card-label">${t('dashboard.agentFleet')}</span>
|
||||
</div>
|
||||
<div class="stat-card-value">${agents.length} 个</div>
|
||||
<div class="stat-card-meta">默认: ${defaultAgent}</div>
|
||||
<div class="stat-card-value">${agents.length} ${t('common.unit')}</div>
|
||||
<div class="stat-card-meta">${t('dashboard.defaultAgent')}: ${defaultAgent}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">模型池</span>
|
||||
<span class="stat-card-label">${t('dashboard.modelPool')}</span>
|
||||
</div>
|
||||
<div class="stat-card-value">${modelCount} 个</div>
|
||||
<div class="stat-card-meta">基于 ${providerCount} 个渠道商</div>
|
||||
<div class="stat-card-value">${modelCount} ${t('common.unit')}</div>
|
||||
<div class="stat-card-meta">${t('dashboard.basedOnProviders', { count: providerCount })}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">基础服务</span>
|
||||
<span class="stat-card-label">${t('dashboard.baseServices')}</span>
|
||||
</div>
|
||||
<div class="stat-card-value">${runningCount}/${services.length}</div>
|
||||
<div class="stat-card-meta">存活率 ${services.length ? Math.round(runningCount / services.length * 100) : 0}%</div>
|
||||
<div class="stat-card-meta">${t('common.survivalRate')} ${services.length ? Math.round(runningCount / services.length * 100) : 0}%</div>
|
||||
</div>
|
||||
<div class="stat-card stat-card-clickable" id="card-control-ui" title="打开 OpenClaw 原生控制面板">
|
||||
<div class="stat-card stat-card-clickable" id="card-control-ui" title="${t('dashboard.controlUIDesc')}">
|
||||
<div class="stat-card-header">
|
||||
<span class="stat-card-label">Control UI</span>
|
||||
<span class="stat-card-label">${t('dashboard.controlUI')}</span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="opacity:0.5"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||
</div>
|
||||
<div class="stat-card-value" style="font-size:var(--font-size-sm)">OpenClaw 原生面板</div>
|
||||
<div class="stat-card-meta">${gw?.running ? '点击打开浏览器' : 'Gateway 未运行'}</div>
|
||||
<div class="stat-card-value" style="font-size:var(--font-size-sm)">${t('dashboard.controlUIDesc')}</div>
|
||||
<div class="stat-card-meta">${gw?.running ? t('dashboard.controlUIClick') : t('dashboard.controlUINotRunning')}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
import { api } from '../lib/tauri-api.js'
|
||||
import { toast } from '../components/toast.js'
|
||||
import { showConfirm } from '../components/modal.js'
|
||||
import { t, getLang, setLang, getAvailableLangs, onLangChange } from '../lib/i18n.js'
|
||||
import { renderSidebar } from '../components/sidebar.js'
|
||||
|
||||
const isTauri = !!window.__TAURI_INTERNALS__
|
||||
|
||||
@@ -45,10 +47,20 @@ export async function render() {
|
||||
</div>
|
||||
|
||||
<div class="config-section" id="openclaw-dir-section">
|
||||
<div class="config-section-title">OpenClaw 安装路径</div>
|
||||
<div class="config-section-title">${t('settings.openclawDir')}</div>
|
||||
<div id="openclaw-dir-bar"><div class="stat-card loading-placeholder" style="height:48px"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="config-section" id="cli-binding-section">
|
||||
<div class="config-section-title">${t('settings.openclawCli')}</div>
|
||||
<div id="cli-binding-bar"><div class="stat-card loading-placeholder" style="height:48px"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="config-section" id="language-section">
|
||||
<div class="config-section-title">${t('settings.language')}</div>
|
||||
<div id="language-bar"></div>
|
||||
</div>
|
||||
|
||||
`
|
||||
|
||||
bindEvents(page)
|
||||
@@ -57,9 +69,10 @@ export async function render() {
|
||||
}
|
||||
|
||||
async function loadAll(page) {
|
||||
const tasks = [loadProxyConfig(page), loadModelProxyConfig(page), loadOpenclawDir(page)]
|
||||
const tasks = [loadProxyConfig(page), loadModelProxyConfig(page), loadOpenclawDir(page), loadCliBinding(page)]
|
||||
tasks.push(loadRegistry(page))
|
||||
await Promise.all(tasks)
|
||||
loadLanguageSwitcher(page)
|
||||
}
|
||||
|
||||
// ===== 网络代理 =====
|
||||
@@ -243,6 +256,12 @@ function bindEvents(page) {
|
||||
case 'reset-openclaw-dir':
|
||||
await handleResetOpenclawDir(page)
|
||||
break
|
||||
case 'bind-cli':
|
||||
await handleBindCli(page, btn.dataset.path)
|
||||
break
|
||||
case 'unbind-cli':
|
||||
await handleUnbindCli(page)
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
toast(e.toString(), 'error')
|
||||
@@ -324,3 +343,109 @@ async function handleSaveRegistry(page) {
|
||||
await api.setNpmRegistry(registry)
|
||||
toast('npm 源已保存', 'success')
|
||||
}
|
||||
|
||||
// ===== CLI 绑定 =====
|
||||
|
||||
async function loadCliBinding(page) {
|
||||
const bar = page.querySelector('#cli-binding-bar')
|
||||
if (!bar) return
|
||||
try {
|
||||
const version = await api.getVersionInfo()
|
||||
const cfg = await api.readPanelConfig()
|
||||
const boundPath = cfg?.openclawCliPath || ''
|
||||
const installations = version.all_installations || []
|
||||
const currentPath = version.cli_path || ''
|
||||
|
||||
const sourceLabel = (src) => ({
|
||||
standalone: t('dashboard.cliSourceStandalone'),
|
||||
'npm-zh': t('dashboard.cliSourceNpmZh'),
|
||||
'npm-official': t('dashboard.cliSourceNpmOfficial'),
|
||||
'npm-global': t('dashboard.cliSourceNpmGlobal'),
|
||||
})[src] || t('dashboard.cliSourceUnknown')
|
||||
|
||||
let html = `<div class="form-hint" style="margin-bottom:var(--space-sm)">${t('settings.cliBindHint')}</div>`
|
||||
|
||||
if (currentPath) {
|
||||
html += `<div style="margin-bottom:var(--space-sm);font-size:var(--font-size-sm)">
|
||||
<span style="color:var(--text-secondary)">${t('settings.cliCurrent')}:</span>
|
||||
<code style="font-size:var(--font-size-xs)">${escapeHtml(currentPath)}</code>
|
||||
${boundPath ? `<span class="clawhub-badge" style="margin-left:var(--space-xs);background:rgba(99,102,241,0.14);color:#6366f1;font-size:var(--font-size-xs)">${t('settings.cliBound')}</span>` : ''}
|
||||
</div>`
|
||||
}
|
||||
|
||||
if (installations.length > 0) {
|
||||
html += '<div style="display:flex;flex-direction:column;gap:var(--space-xs)">'
|
||||
// Auto-detect option
|
||||
html += `<div style="display:flex;align-items:center;gap:var(--space-sm);padding:6px 10px;border-radius:var(--radius-sm);border:1px solid var(--border);${!boundPath ? 'background:var(--bg-active);border-color:var(--accent)' : ''}">
|
||||
<span style="flex:1;font-size:var(--font-size-sm)">${t('settings.cliAutoDetect')}</span>
|
||||
${boundPath ? '<button class="btn btn-secondary btn-xs" data-action="unbind-cli">' + t('common.reset') + '</button>' : '<span style="color:var(--success);font-size:var(--font-size-xs)">✓ ' + t('settings.cliActive') + '</span>'}
|
||||
</div>`
|
||||
for (const inst of installations) {
|
||||
const isActive = inst.active
|
||||
const isBound = boundPath && inst.path === boundPath
|
||||
html += `<div style="display:flex;align-items:center;gap:var(--space-sm);padding:6px 10px;border-radius:var(--radius-sm);border:1px solid var(--border);${isBound ? 'background:var(--bg-active);border-color:var(--accent)' : ''}">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:var(--font-size-xs);font-family:var(--font-mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escapeHtml(inst.path)}">${escapeHtml(inst.path)}</div>
|
||||
<div style="font-size:11px;color:var(--text-tertiary)">${sourceLabel(inst.source)}${inst.version ? ' · v' + inst.version : ''}</div>
|
||||
</div>
|
||||
${isBound ? '<span style="color:var(--success);font-size:var(--font-size-xs)">✓ ' + t('settings.cliBound') + '</span>' : `<button class="btn btn-secondary btn-xs" data-action="bind-cli" data-path="${escapeHtml(inst.path)}">${t('common.confirm')}</button>`}
|
||||
</div>`
|
||||
}
|
||||
html += '</div>'
|
||||
} else {
|
||||
html += `<div style="color:var(--text-tertiary);font-size:var(--font-size-sm)">${t('common.noData')}</div>`
|
||||
}
|
||||
|
||||
bar.innerHTML = html
|
||||
} catch (e) {
|
||||
bar.innerHTML = `<div style="color:var(--error)">${t('common.loadFailed')}: ${escapeHtml(String(e))}</div>`
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBindCli(page, path) {
|
||||
if (!path) return
|
||||
const ok = await showConfirm(t('settings.cliSwitchConfirm'))
|
||||
if (!ok) return
|
||||
const cfg = await api.readPanelConfig()
|
||||
cfg.openclawCliPath = path
|
||||
await api.writePanelConfig(cfg)
|
||||
toast(t('common.saveSuccess'), 'success')
|
||||
await loadCliBinding(page)
|
||||
}
|
||||
|
||||
async function handleUnbindCli(page) {
|
||||
const cfg = await api.readPanelConfig()
|
||||
delete cfg.openclawCliPath
|
||||
await api.writePanelConfig(cfg)
|
||||
toast(t('common.saveSuccess'), 'success')
|
||||
await loadCliBinding(page)
|
||||
}
|
||||
|
||||
// ===== 语言切换 =====
|
||||
|
||||
function loadLanguageSwitcher(page) {
|
||||
const bar = page.querySelector('#language-bar')
|
||||
if (!bar) return
|
||||
const langs = getAvailableLangs()
|
||||
const current = getLang()
|
||||
bar.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:var(--space-sm);flex-wrap:wrap">
|
||||
<select class="form-input" id="lang-select" style="max-width:200px">
|
||||
${langs.map(l => `<option value="${l.code}" ${l.code === current ? 'selected' : ''}>${l.label}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-hint" style="margin-top:var(--space-xs)">${t('settings.languageHint')}</div>
|
||||
`
|
||||
const select = bar.querySelector('#lang-select')
|
||||
select.onchange = () => {
|
||||
setLang(select.value)
|
||||
// Re-render sidebar + current page
|
||||
const sidebarEl = document.getElementById('sidebar')
|
||||
if (sidebarEl) renderSidebar(sidebarEl)
|
||||
// Re-render settings page
|
||||
const pageEl = page.closest('.page') || page
|
||||
render().then(newPage => {
|
||||
pageEl.replaceWith(newPage)
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user