mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-06 20:02:49 +08:00
chore: release v0.11.3
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -5,6 +5,19 @@
|
||||
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
||||
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||||
|
||||
## [0.11.3] - 2026-04-03
|
||||
|
||||
### 修复 (Fixes)
|
||||
|
||||
- **推荐稳定版映射** — `0.11.1` / `0.11.2` 现在会正确继承并显示 OpenClaw `2026.3.28` / `2026.3.28-zh.2`,不再错误回退到旧的 `2026.3.13` 默认值;桌面端 Rust 后端和 Web 模式 `dev-api` 统一采用同一套 panel patch 版本兜底匹配逻辑
|
||||
- **多安装提示误报** — 仪表盘检测到多个 OpenClaw 安装时,若用户已经显式绑定 CLI,则不再继续显示橙色告警和“去配置”按钮,避免误导已经完成绑定的用户
|
||||
- **托管 Agent 地址校验** — 托管 Agent 现在会规范化并校验第三方模型地址,自动清理 `/chat/completions`、`/models`、`/api` 等尾部路径,兼容 OpenAI / Anthropic / Gemini / Ollama,并拦截无效的 `tauri.localhost` 或非 `http(s)` 地址
|
||||
|
||||
### 改进 (Improvements)
|
||||
|
||||
- **多安装引导弹窗重构** — “检测到多个 OpenClaw 安装”对话框改为更适合新手理解的卡片式布局,新增“为什么会看到这个提示”、当前绑定/自动检测信息卡、步骤化建议和高亮的安装列表
|
||||
- **Tauri 运行时检测统一** — 抽出 `isTauriRuntime()` 统一判断桌面端环境,减少 `window.__TAURI_INTERNALS__` 直判带来的分支分散问题,改善主入口、聊天页和调试页的 WebSocket/桌面环境兼容性
|
||||
|
||||
## [0.11.2] - 2026-04-02
|
||||
|
||||
### 修复 (Fixes)
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"description": "OpenClaw AI Agent 可视化管理面板,基于 Tauri v2 的跨平台桌面应用。内置晴辰助手支持工具调用,晴辰云 AI 接口一键接入。支持仪表盘监控、多模型配置、消息渠道管理、内置 QQ 机器人、实时 AI 聊天、记忆管理、Agent 管理、网关配置、内网穿透等功能。支持 11 种语言。",
|
||||
"url": "https://claw.qt.cool/",
|
||||
"downloadUrl": "https://github.com/qingchencloud/clawpanel/releases/latest",
|
||||
"softwareVersion": "0.11.2",
|
||||
"softwareVersion": "0.11.3",
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "晴辰云 QingchenCloud",
|
||||
@@ -1155,7 +1155,7 @@
|
||||
<div class="orb orb-2" style="top:auto;bottom:-100px"></div>
|
||||
<div class="container-sm" style="position:relative;z-index:10">
|
||||
<div class="section-header">
|
||||
<div class="reveal download-version"><span class="pulse"></span> <span id="dl-badge" data-i18n="dl.badge">v0.11.2 最新版</span></div>
|
||||
<div class="reveal download-version"><span class="pulse"></span> <span id="dl-badge" data-i18n="dl.badge">v0.11.3 最新版</span></div>
|
||||
<h2 class="reveal section-title" data-i18n="dl.title"><span class="gradient-text">下载安装</span></h2>
|
||||
<p class="reveal section-desc" data-i18n="dl.desc">选择你的操作系统,一键下载安装</p>
|
||||
</div>
|
||||
@@ -1165,11 +1165,11 @@
|
||||
<h3>macOS</h3>
|
||||
<p class="dl-desc" data-i18n="dl.mac.d">支持 Apple Silicon 和 Intel 芯片</p>
|
||||
<div class="dl-links">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.2_aarch64.dmg" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.3_aarch64.dmg" target="_blank" rel="noopener">
|
||||
Apple Silicon (M1/M2/M3/M4)
|
||||
<span class="dl-format">.dmg</span>
|
||||
</a>
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.2_x64.dmg" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.3_x64.dmg" target="_blank" rel="noopener">
|
||||
<span data-i18n="dl.mac.intel">Intel 芯片</span>
|
||||
<span class="dl-format">.dmg</span>
|
||||
</a>
|
||||
@@ -1187,15 +1187,15 @@
|
||||
<h3>Windows</h3>
|
||||
<p class="dl-desc" data-i18n="dl.win.d">支持 Windows 10 及以上版本</p>
|
||||
<div class="dl-links">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.2_x64-setup.exe" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.3_x64-setup.exe" target="_blank" rel="noopener">
|
||||
<span data-i18n="dl.win.exe">安装程序</span>
|
||||
<span class="dl-format">.exe</span>
|
||||
</a>
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.2_x64-setup-full.exe" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.3_x64-setup-full.exe" target="_blank" rel="noopener">
|
||||
<span data-i18n="dl.win.full">完整包(含 WebView2)</span>
|
||||
<span class="dl-format">.exe</span>
|
||||
</a>
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.2_x64_en-US.msi" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.3_x64_en-US.msi" target="_blank" rel="noopener">
|
||||
<span data-i18n="dl.win.msi">MSI 安装包</span>
|
||||
<span class="dl-format">.msi</span>
|
||||
</a>
|
||||
@@ -1206,11 +1206,11 @@
|
||||
<h3>Linux</h3>
|
||||
<p class="dl-desc" data-i18n="dl.linux.d">支持主流 Linux 发行版</p>
|
||||
<div class="dl-links">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.2_amd64.AppImage" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.3_amd64.AppImage" target="_blank" rel="noopener">
|
||||
<span data-i18n="dl.linux.ai">通用版</span>
|
||||
<span class="dl-format">.AppImage</span>
|
||||
</a>
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.2_amd64.deb" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.3_amd64.deb" target="_blank" rel="noopener">
|
||||
Debian / Ubuntu
|
||||
<span class="dl-format">.deb</span>
|
||||
</a>
|
||||
|
||||
@@ -111,6 +111,22 @@
|
||||
"chinese": {
|
||||
"recommended": "2026.3.28-zh.2"
|
||||
}
|
||||
},
|
||||
"0.11.1": {
|
||||
"official": {
|
||||
"recommended": "2026.3.28"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.28-zh.2"
|
||||
}
|
||||
},
|
||||
"0.11.2": {
|
||||
"official": {
|
||||
"recommended": "2026.3.28"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.28-zh.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "clawpanel",
|
||||
"version": "0.11.2",
|
||||
"version": "0.11.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "clawpanel",
|
||||
"version": "0.11.2",
|
||||
"version": "0.11.3",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clawpanel",
|
||||
"version": "0.11.2",
|
||||
"version": "0.11.3",
|
||||
"private": true,
|
||||
"description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用",
|
||||
"type": "module",
|
||||
|
||||
@@ -511,6 +511,38 @@ function standaloneConfig() {
|
||||
return policy?.standalone || { enabled: false }
|
||||
}
|
||||
|
||||
function findPanelPolicyEntry(policy, currentVersion) {
|
||||
const exact = policy?.panels?.[currentVersion]
|
||||
if (exact) return exact
|
||||
|
||||
const currentParts = parseVersion(currentVersion)
|
||||
if (currentParts.length < 2) return null
|
||||
|
||||
let matched = null
|
||||
let matchedParts = null
|
||||
for (const [version, entry] of Object.entries(policy?.panels || {})) {
|
||||
const parts = parseVersion(version)
|
||||
if (parts.length < 2) continue
|
||||
if (parts[0] !== currentParts[0] || parts[1] !== currentParts[1]) continue
|
||||
if (versionCompare(version, currentVersion) > 0) continue
|
||||
if (!matchedParts || compareParsedVersion(parts, matchedParts) > 0) {
|
||||
matched = entry
|
||||
matchedParts = parts
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
function compareParsedVersion(a = [], b = []) {
|
||||
const len = Math.max(a.length, b.length)
|
||||
for (let i = 0; i < len; i++) {
|
||||
const av = Number(a[i] || 0)
|
||||
const bv = Number(b[i] || 0)
|
||||
if (av !== bv) return av > bv ? 1 : -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function standalonePlatformKey() {
|
||||
const arch = process.arch
|
||||
const plat = process.platform
|
||||
@@ -746,7 +778,8 @@ async function _tryR2Install(version, source, logs) {
|
||||
|
||||
function recommendedVersionFor(source = 'chinese') {
|
||||
const policy = loadVersionPolicy()
|
||||
return policy?.panels?.[PANEL_VERSION]?.[source]?.recommended
|
||||
const panelEntry = findPanelPolicyEntry(policy, PANEL_VERSION)
|
||||
return panelEntry?.[source]?.recommended
|
||||
|| policy?.default?.[source]?.recommended
|
||||
|| null
|
||||
}
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -351,7 +351,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clawpanel"
|
||||
version = "0.11.2"
|
||||
version = "0.11.3"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clawpanel"
|
||||
version = "0.11.2"
|
||||
version = "0.11.3"
|
||||
edition = "2021"
|
||||
description = "ClawPanel - OpenClaw 可视化管理面板"
|
||||
authors = ["qingchencloud"]
|
||||
|
||||
@@ -101,6 +101,36 @@ fn panel_version() -> &'static str {
|
||||
env!("CARGO_PKG_VERSION")
|
||||
}
|
||||
|
||||
fn find_panel_policy_entry<'a>(policy: &'a VersionPolicy, current_version: &str) -> Option<&'a VersionPolicyEntry> {
|
||||
if let Some(entry) = policy.panels.get(current_version) {
|
||||
return Some(entry);
|
||||
}
|
||||
|
||||
let current_parts = parse_version(current_version);
|
||||
if current_parts.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
policy
|
||||
.panels
|
||||
.iter()
|
||||
.filter_map(|(version, entry)| {
|
||||
let parts = parse_version(version);
|
||||
if parts.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
if parts[0] != current_parts[0] || parts[1] != current_parts[1] {
|
||||
return None;
|
||||
}
|
||||
if parts > current_parts {
|
||||
return None;
|
||||
}
|
||||
Some((parts, entry))
|
||||
})
|
||||
.max_by(|(left, _), (right, _)| left.cmp(right))
|
||||
.map(|(_, entry)| entry)
|
||||
}
|
||||
|
||||
fn parse_version(value: &str) -> Vec<u32> {
|
||||
value
|
||||
.split(|c: char| !c.is_ascii_digit())
|
||||
@@ -231,7 +261,7 @@ pub(crate) fn all_standalone_dirs() -> Vec<PathBuf> {
|
||||
|
||||
fn recommended_version_for(source: &str) -> Option<String> {
|
||||
let policy = load_version_policy();
|
||||
let panel_entry = policy.panels.get(panel_version());
|
||||
let panel_entry = find_panel_policy_entry(&policy, panel_version());
|
||||
match source {
|
||||
"official" => panel_entry
|
||||
.and_then(|entry| entry.official.recommended.clone())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
|
||||
"productName": "ClawPanel",
|
||||
"version": "0.11.2",
|
||||
"version": "0.11.3",
|
||||
"identifier": "ai.openclaw.clawpanel",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
@@ -125,47 +125,78 @@ export async function showGatewayConflictGuidance({ error = null, service = null
|
||||
const suggestionThree = t('services.guidanceSuggestionInstallations')
|
||||
const settingsButtonLabel = hasUnboundForeignGateway ? t('services.guidanceBindCliBtn') : t('sidebar.settings')
|
||||
|
||||
const whyText = hasUnboundForeignGateway
|
||||
? t('services.guidanceWhyForeignUnbound')
|
||||
: hasForeignGateway
|
||||
? t('services.guidanceWhyForeign')
|
||||
: t('services.guidanceWhyMultiInstall')
|
||||
|
||||
const installationHtml = installations.length
|
||||
? installations.map(inst => {
|
||||
const badges = [
|
||||
inst.active ? `<span class="clawhub-badge" style="font-size:11px">${escapeHtml(t('settings.cliActive'))}</span>` : '',
|
||||
inst.version ? `<span class="clawhub-badge" style="font-size:11px">${escapeHtml(t('settings.cliVersion'))}: ${escapeHtml(inst.version)}</span>` : '',
|
||||
inst.source ? `<span class="clawhub-badge" style="font-size:11px">${escapeHtml(cliSourceLabel(inst.source))}</span>` : '',
|
||||
].filter(Boolean).join(' ')
|
||||
const isActive = !!inst.active
|
||||
const borderColor = isActive ? 'rgba(34,197,94,0.4)' : 'var(--border-light)'
|
||||
const bgColor = isActive ? 'rgba(34,197,94,0.06)' : 'var(--bg-secondary)'
|
||||
const activeBadge = isActive
|
||||
? `<span style="display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;background:rgba(34,197,94,0.14);color:#16a34a">● ${escapeHtml(t('services.guidanceActiveBadge'))}</span>`
|
||||
: ''
|
||||
const versionBadge = inst.version
|
||||
? `<span style="display:inline-flex;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:500;background:rgba(99,102,241,0.10);color:var(--text-secondary)">${escapeHtml(inst.version)}</span>`
|
||||
: ''
|
||||
const sourceBadge = inst.source
|
||||
? `<span style="display:inline-flex;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:500;background:var(--bg-tertiary, rgba(0,0,0,0.06));color:var(--text-tertiary)">${escapeHtml(cliSourceLabel(inst.source))}</span>`
|
||||
: ''
|
||||
return `
|
||||
<div style="padding:10px 12px;border:1px solid var(--border-light);border-radius:10px;background:var(--bg-secondary);margin-top:8px">
|
||||
<div style="font-size:12px;word-break:break-all;font-family:var(--font-mono)">${escapeHtml(inst.path)}</div>
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">${badges}</div>
|
||||
<div style="padding:10px 14px;border:1px solid ${borderColor};border-radius:10px;background:${bgColor};margin-top:8px;transition:border-color .15s">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||||
<span style="font-size:15px">📂</span>
|
||||
<code style="font-size:12px;word-break:break-all;flex:1;min-width:0">${escapeHtml(inst.path)}</code>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">${activeBadge}${versionBadge}${sourceBadge}</div>
|
||||
</div>`
|
||||
}).join('')
|
||||
: `<div style="padding:10px 12px;border:1px dashed var(--border-light);border-radius:10px;background:var(--bg-secondary);margin-top:8px;color:var(--text-secondary)">${escapeHtml(t('services.guidanceNoInstallations', { settings: settingsLabel }))}</div>`
|
||||
: `<div style="padding:14px;border:1px dashed var(--border-light);border-radius:10px;background:var(--bg-secondary);margin-top:8px;color:var(--text-tertiary);text-align:center">${escapeHtml(t('services.guidanceNoInstallations', { settings: settingsLabel }))}</div>`
|
||||
|
||||
const infoCard = (icon, label, value, sub) => `
|
||||
<div style="display:flex;gap:10px;padding:10px 14px;border-radius:10px;background:var(--bg-secondary);border:1px solid var(--border-light)">
|
||||
<span style="font-size:16px;flex-shrink:0;margin-top:1px">${icon}</span>
|
||||
<div style="min-width:0;flex:1">
|
||||
<div style="font-size:11px;color:var(--text-tertiary);font-weight:600;text-transform:uppercase;letter-spacing:0.3px">${escapeHtml(label)}</div>
|
||||
<div style="margin-top:3px;font-size:13px;word-break:break-all;font-family:var(--font-mono);color:var(--text-primary)">${escapeHtml(value)}</div>
|
||||
${sub ? `<div style="margin-top:2px;font-size:11px;color:var(--text-tertiary)">${escapeHtml(sub)}</div>` : ''}
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
const stepCard = (n, text) => `
|
||||
<div style="display:flex;gap:10px;align-items:flex-start">
|
||||
<span style="display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;background:var(--primary, #6366f1);color:#fff;font-size:12px;font-weight:700;flex-shrink:0;margin-top:1px">${n}</span>
|
||||
<div style="font-size:13px;color:var(--text-secondary);line-height:1.6;flex:1">${escapeHtml(text)}</div>
|
||||
</div>`
|
||||
|
||||
const content = `
|
||||
<div style="display:flex;flex-direction:column;gap:12px;font-size:var(--font-size-sm);color:var(--text-secondary);line-height:1.7">
|
||||
<div style="padding:12px;border-radius:10px;background:rgba(245,158,11,0.12);color:var(--warning)">
|
||||
${escapeHtml(summaryText)}
|
||||
<div style="display:flex;flex-direction:column;gap:14px;font-size:var(--font-size-sm);color:var(--text-secondary);line-height:1.7">
|
||||
<div style="display:flex;gap:10px;padding:12px 14px;border-radius:10px;background:rgba(245,158,11,0.10);border:1px solid rgba(245,158,11,0.2)">
|
||||
<span style="font-size:18px;flex-shrink:0">⚠️</span>
|
||||
<div style="color:var(--warning);font-size:13px;line-height:1.6">${escapeHtml(summaryText)}</div>
|
||||
</div>
|
||||
${message ? `<div style="padding:10px 12px;border-radius:10px;background:var(--bg-secondary);font-family:var(--font-mono);word-break:break-all">${message}</div>` : ''}
|
||||
<div style="display:grid;grid-template-columns:1fr;gap:8px">
|
||||
<div><strong>${escapeHtml(t('services.guidanceCurrentBindingTitle'))}</strong><div style="margin-top:4px;font-family:var(--font-mono);word-break:break-all">${escapeHtml(displayBoundCliPath)}</div></div>
|
||||
<div><strong>${escapeHtml(t('settings.openclawCli'))}</strong><div style="margin-top:4px;font-family:var(--font-mono);word-break:break-all">${escapeHtml(currentCli)}</div><div style="margin-top:4px;color:var(--text-tertiary)">${escapeHtml(currentCliSource)}</div></div>
|
||||
<div><strong>${escapeHtml(t('settings.openclawDir'))}</strong><div style="margin-top:4px;font-family:var(--font-mono);word-break:break-all">${escapeHtml(currentDir)}</div></div>
|
||||
${pid ? `<div><strong>PID</strong><div style="margin-top:4px">${escapeHtml(pid)}</div></div>` : ''}
|
||||
${message ? `<div style="padding:10px 14px;border-radius:10px;background:var(--bg-secondary);border:1px solid var(--border-light);font-family:var(--font-mono);font-size:12px;word-break:break-all;color:var(--text-tertiary)">${message}</div>` : ''}
|
||||
<details style="border-radius:10px;background:var(--bg-secondary);border:1px solid var(--border-light);overflow:hidden">
|
||||
<summary style="padding:10px 14px;cursor:pointer;font-size:13px;font-weight:600;color:var(--text-primary);user-select:none">${escapeHtml(t('services.guidanceWhyTitle'))}</summary>
|
||||
<div style="padding:0 14px 12px;font-size:13px;color:var(--text-secondary);line-height:1.6">${escapeHtml(whyText)}</div>
|
||||
</details>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||||
${infoCard('🔗', t('services.guidanceInfoCliBinding'), displayBoundCliPath)}
|
||||
${infoCard('🛠️', t('services.guidanceInfoCliDetected'), currentCli, currentCliSource)}
|
||||
${infoCard('📁', t('services.guidanceInfoDataDir'), currentDir)}
|
||||
${pid ? infoCard('⚡', t('services.guidanceInfoProcess'), `PID ${pid}`) : ''}
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:8px">
|
||||
<div style="font-size:13px;font-weight:600;color:var(--text-primary)">${escapeHtml(t('services.guidanceHandlingTitle'))}</div>
|
||||
${stepCard(1, suggestionOne.replace(/^1\.\s*/, ''))}
|
||||
${stepCard(2, suggestionTwo.replace(/^2\.\s*/, ''))}
|
||||
${stepCard(3, suggestionThree.replace(/^3\.\s*/, ''))}
|
||||
</div>
|
||||
<div>
|
||||
<strong>${escapeHtml(t('services.guidanceHandlingTitle'))}</strong>
|
||||
<div style="margin-top:6px">
|
||||
${escapeHtml(suggestionOne)}
|
||||
</div>
|
||||
<div style="margin-top:6px">
|
||||
${escapeHtml(suggestionTwo)}
|
||||
</div>
|
||||
<div style="margin-top:6px">
|
||||
${escapeHtml(suggestionThree)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<strong>${escapeHtml(t('services.guidanceInstallationsTitle'))}</strong>
|
||||
<div style="font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:4px">${escapeHtml(t('services.guidanceInstallationsTitle'))}</div>
|
||||
${installationHtml}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
|
||||
import { t } from './i18n.js'
|
||||
|
||||
const isTauri = !!window.__TAURI_INTERNALS__
|
||||
export function isTauriRuntime() {
|
||||
return !!window.__TAURI_INTERNALS__ || !!window.__TAURI__ || window.location?.hostname === 'tauri.localhost'
|
||||
}
|
||||
|
||||
// 仅在 Node.js 后端实现的命令(Tauri Rust 不处理),强制走 webInvoke
|
||||
const WEB_ONLY_CMDS = new Set([
|
||||
@@ -19,10 +21,15 @@ const WEB_ONLY_CMDS = new Set([
|
||||
'get_deploy_mode',
|
||||
])
|
||||
|
||||
// 预加载 Tauri invoke,避免每次 API 调用都做动态 import
|
||||
const _invokeReady = isTauri
|
||||
? import('@tauri-apps/api/core').then(m => m.invoke)
|
||||
: null
|
||||
let _invokeReady = null
|
||||
|
||||
async function getTauriInvoke() {
|
||||
if (!isTauriRuntime()) return null
|
||||
if (!_invokeReady) {
|
||||
_invokeReady = import('@tauri-apps/api/core').then(m => m.invoke)
|
||||
}
|
||||
return _invokeReady
|
||||
}
|
||||
|
||||
// 简单缓存:避免页面切换时重复请求后端
|
||||
const _cache = new Map()
|
||||
@@ -97,8 +104,8 @@ export { invalidate }
|
||||
|
||||
async function invoke(cmd, args = {}) {
|
||||
const start = Date.now()
|
||||
if (_invokeReady && !WEB_ONLY_CMDS.has(cmd)) {
|
||||
const tauriInvoke = await _invokeReady
|
||||
const tauriInvoke = WEB_ONLY_CMDS.has(cmd) ? null : await getTauriInvoke()
|
||||
if (tauriInvoke) {
|
||||
const result = await tauriInvoke(cmd, args)
|
||||
const duration = Date.now() - start
|
||||
logRequest(cmd, args, duration, false)
|
||||
@@ -120,7 +127,7 @@ async function webInvoke(cmd, args) {
|
||||
})
|
||||
if (resp.status === 401) {
|
||||
// Tauri 模式下不触发登录浮层(Tauri 有自己的认证流程)
|
||||
if (!isTauri && window.__clawpanel_show_login) window.__clawpanel_show_login()
|
||||
if (!isTauriRuntime() && window.__clawpanel_show_login) window.__clawpanel_show_login()
|
||||
throw new Error(t('common.loginRequired'))
|
||||
}
|
||||
// 检测后端是否可用:如果返回的是 HTML(非 JSON),说明后端未运行
|
||||
@@ -155,7 +162,7 @@ function _setBackendOnline(v) {
|
||||
|
||||
// 后端健康检查
|
||||
export async function checkBackendHealth() {
|
||||
if (isTauri) { _setBackendOnline(true); return true }
|
||||
if (isTauriRuntime()) { _setBackendOnline(true); return true }
|
||||
try {
|
||||
const resp = await fetch('/__api/health', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
||||
const ok = resp.ok
|
||||
|
||||
@@ -153,6 +153,7 @@ export default {
|
||||
hostedGwNotReady: _('Gateway 未就绪', 'Gateway not ready', 'Gateway 未就緒'),
|
||||
hostedErrorThreshold: _('连续错误超过阈值', 'Consecutive errors exceeded threshold', '連續錯誤超過阈值'),
|
||||
hostedModelNotConfigured: _('托管 Agent 未配置模型(请在 AI 助手页面配置)', 'Hosted Agent model not configured (please configure in AI Assistant page)', '托管 Agent 未設定模型(請在 AI 助手頁面設定)'),
|
||||
hostedModelUrlInvalid: _('托管 Agent 模型地址无效,请在 AI 助手页面填写完整的 http(s):// 地址', 'Hosted Agent model URL is invalid. Please enter a full http(s):// URL on the AI Assistant page.', '托管 Agent 模型位址無效,請在 AI 助手頁面填寫完整的 http(s):// 位址'),
|
||||
hostedApiError: _('API 错误 {code}', 'API Error {code}', 'API 錯誤 {code}'),
|
||||
hostedPrefix: _('[托管 Agent] ', '[Hosted Agent] '),
|
||||
hostedContextSummary: _('[上下文摘要 - 已压缩 {n} 条历史]', '[Context Summary - compressed {n} history entries]', '[上下文摘要 - 已壓縮 {n} 條歷史]'),
|
||||
|
||||
@@ -38,6 +38,7 @@ export default {
|
||||
multiInstall: _('检测到多个安装', 'Multiple installations detected', '檢測到多個安裝', '複数のインストールを検出', '여러 설치가 감지됨'),
|
||||
multiInstallHint: _('在「面板设置」中可选择使用哪个', 'Choose which one to use in Settings', '在「面板設定」中可選擇使用哪個', '設定で使用するものを選択できます', '설정에서 사용할 설치를 선택할 수 있습니다'),
|
||||
multiInstallCardHint: _('检测到多个安装,建议确认当前绑定的 CLI 与 OpenClaw 目录。', 'Multiple installations detected. Confirm the bound CLI and OpenClaw directory first.', '檢測到多個安裝,建議先確認目前綁定的 CLI 與 OpenClaw 目錄。', '複数のインストールが検出されました。まず CLI バインドと OpenClaw ディレクトリを確認してください。', '여러 설치가 감지되었습니다. 먼저 바인딩된 CLI와 OpenClaw 디렉터리를 확인하세요.'),
|
||||
multiInstallBoundOk: _('已绑定 CLI,{count} 个安装共存', '{count} installations coexist, CLI is bound', '已綁定 CLI,{count} 個安裝共存'),
|
||||
foreignGatewayHint: _('检测到外部 Gateway,建议先查看引导或进入设置修正绑定。', 'External Gateway detected. Review the guidance or open Settings to correct the binding.', '檢測到外部 Gateway,建議先查看引導或進入設定修正綁定。', '外部 Gateway を検出しました。ガイドを確認するか設定を開いて関連付けを修正してください。', '외부 Gateway가 감지되었습니다. 안내를 확인하거나 설정에서 바인딩을 수정하세요.'),
|
||||
externalInstance: _('外部实例', 'External instance', '外部實例', '外部インスタンス', '외부 인스턴스', 'Phiên bản bên ngoài'),
|
||||
externalGatewayDetected: _('检测到外部 Gateway{pid}', 'External Gateway detected{pid}', '檢測到外部 Gateway{pid}', '外部 Gateway を検出{pid}', '외부 Gateway 감지됨{pid}', 'Đã phát hiện Gateway bên ngoài{pid}'),
|
||||
|
||||
@@ -35,6 +35,16 @@ export default {
|
||||
guidanceCliBindingAuto: _('未显式绑定(当前为自动检测)', 'Not explicitly bound (currently auto-detected)', '未明確綁定(目前為自動檢測)'),
|
||||
guidanceBindCliBtn: _('去绑定 CLI', 'Bind CLI', '前往綁定 CLI'),
|
||||
guidanceInstallationsTitle: _('已检测到的 OpenClaw 安装', 'Detected OpenClaw installations', '已檢測到的 OpenClaw 安裝'),
|
||||
guidanceStepLabel: _('步骤 {n}', 'Step {n}', '步驟 {n}'),
|
||||
guidanceInfoCliBinding: _('当前绑定', 'Current Binding', '目前綁定'),
|
||||
guidanceInfoCliDetected: _('自动检测到', 'Auto-detected', '自動檢測到'),
|
||||
guidanceInfoDataDir: _('数据目录', 'Data Directory', '資料目錄'),
|
||||
guidanceInfoProcess: _('进程', 'Process', '進程'),
|
||||
guidanceActiveBadge: _('正在使用', 'Active', '正在使用'),
|
||||
guidanceWhyTitle: _('为什么会看到这个提示?', 'Why am I seeing this?', '為什麼會看到這個提示?'),
|
||||
guidanceWhyMultiInstall: _('你的电脑上安装了多个 OpenClaw,面板需要知道应该管理哪一个,以免操作到错误的实例。', 'Multiple OpenClaw installations exist on your computer. The panel needs to know which one to manage so it doesn\'t accidentally control the wrong instance.', '你的電腦上安裝了多個 OpenClaw,面板需要知道應該管理哪一個,以免操作到錯誤的實例。'),
|
||||
guidanceWhyForeign: _('面板发现端口上正在运行一个不属于它的 Gateway 实例。为了安全,面板不会直接操作它,需要你先确认绑定关系。', 'The panel detected a Gateway instance on the port that it does not own. For safety, the panel will not operate on it directly until you confirm the binding.', '面板發現連接埠上正在運行一個不屬於它的 Gateway 實例。為了安全,面板不會直接操作它,需要你先確認綁定關係。'),
|
||||
guidanceWhyForeignUnbound: _('端口上已有 Gateway 在运行,但你还没告诉面板应该管理哪个 OpenClaw。请先绑定再操作。', 'A Gateway is already running on the port, but you haven\'t told the panel which OpenClaw to manage. Please bind first.', '連接埠上已有 Gateway 在運行,但你還沒告訴面板應該管理哪個 OpenClaw。請先綁定再操作。'),
|
||||
gwNotInstalled: _('Gateway 服务未安装', 'Gateway service not installed', 'Gateway 服務未安裝'),
|
||||
gwInstalled: _('Gateway 服务已安装', 'Gateway service installed', 'Gateway 服務已安裝'),
|
||||
gwUninstalled: _('Gateway 服务已卸载', 'Gateway service uninstalled', 'Gateway 服務已卸載'),
|
||||
|
||||
14
src/main.js
14
src/main.js
@@ -10,7 +10,7 @@ import { renderSidebar, openMobileSidebar } from './components/sidebar.js'
|
||||
import { initTheme } from './lib/theme.js'
|
||||
import { detectOpenclawStatus, isOpenclawReady, isUpgrading, isGatewayRunning, onGatewayChange, startGatewayPoll, onGuardianGiveUp, resetAutoRestart, loadActiveInstance, getActiveInstance, onInstanceChange } from './lib/app-state.js'
|
||||
import { wsClient } from './lib/ws-client.js'
|
||||
import { api, checkBackendHealth, isBackendOnline, onBackendStatusChange } from './lib/tauri-api.js'
|
||||
import { api, checkBackendHealth, isBackendOnline, isTauriRuntime, onBackendStatusChange } from './lib/tauri-api.js'
|
||||
import { version as APP_VERSION } from '../package.json'
|
||||
import { statusIcon } from './lib/icons.js'
|
||||
import { isForeignGatewayError, showGatewayConflictGuidance } from './lib/gateway-ownership.js'
|
||||
@@ -45,7 +45,7 @@ async function openGatewayConflict(error = null) {
|
||||
}
|
||||
|
||||
// === 访问密码保护(Web + 桌面端通用) ===
|
||||
const isTauri = !!window.__TAURI_INTERNALS__
|
||||
const isTauri = isTauriRuntime()
|
||||
|
||||
async function checkAuth() {
|
||||
if (isTauri) {
|
||||
@@ -408,7 +408,7 @@ async function boot() {
|
||||
})
|
||||
|
||||
// 守护放弃时,弹出恢复选项
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
if (isTauriRuntime()) {
|
||||
import('@tauri-apps/api/event').then(async ({ listen }) => {
|
||||
await listen('guardian-event', (e) => {
|
||||
if (e.payload?.kind === 'give_up') showGuardianRecovery()
|
||||
@@ -432,7 +432,7 @@ async function boot() {
|
||||
}
|
||||
|
||||
// 全局监听后台任务完成/失败事件,自动刷新安装状态和侧边栏
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
if (isTauriRuntime()) {
|
||||
import('@tauri-apps/api/event').then(async ({ listen }) => {
|
||||
const refreshAfterTask = async () => {
|
||||
// 清除 API 缓存,确保拿到最新状态
|
||||
@@ -509,10 +509,10 @@ async function autoConnectWebSocket() {
|
||||
const url = new URL(inst2.endpoint)
|
||||
host = `${url.hostname}:${inst2.gatewayPort || port}`
|
||||
} catch {
|
||||
host = window.__TAURI_INTERNALS__ ? `127.0.0.1:${port}` : location.host
|
||||
host = isTauriRuntime() ? `127.0.0.1:${port}` : location.host
|
||||
}
|
||||
} else {
|
||||
host = window.__TAURI_INTERNALS__ ? `127.0.0.1:${port}` : location.host
|
||||
host = isTauriRuntime() ? `127.0.0.1:${port}` : location.host
|
||||
}
|
||||
wsClient.connect(host, token)
|
||||
console.log(`[main] WebSocket 连接已启动 -> ${host}`)
|
||||
@@ -712,7 +712,7 @@ async function checkGlobalUpdate() {
|
||||
if (hotApplied === ver) return
|
||||
|
||||
const changelog = info.manifest?.changelog || ''
|
||||
const isWeb = !window.__TAURI_INTERNALS__
|
||||
const isWeb = !isTauriRuntime()
|
||||
|
||||
banner.classList.remove('update-banner-hidden')
|
||||
banner.innerHTML = `
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 系统诊断页面
|
||||
* 全面检测 ClawPanel 各项功能状态,快速定位问题
|
||||
*/
|
||||
import { api, getRequestLogs, clearRequestLogs } from '../lib/tauri-api.js'
|
||||
import { api, getRequestLogs, clearRequestLogs, isTauriRuntime } from '../lib/tauri-api.js'
|
||||
import { wsClient } from '../lib/ws-client.js'
|
||||
import { isOpenclawReady, isGatewayRunning } from '../lib/app-state.js'
|
||||
import { isForeignGatewayError, showGatewayConflictGuidance } from '../lib/gateway-ownership.js'
|
||||
@@ -396,7 +396,7 @@ function testWebSocket(page) {
|
||||
const port = config?.gateway?.port || 18789
|
||||
const rawToken = config?.gateway?.auth?.token
|
||||
const token = (typeof rawToken === 'string') ? rawToken : ''
|
||||
const wsHost = window.__TAURI_INTERNALS__ ? `127.0.0.1:${port}` : location.host
|
||||
const wsHost = isTauriRuntime() ? `127.0.0.1:${port}` : location.host
|
||||
const url = `ws://${wsHost}/ws?token=${encodeURIComponent(token)}`
|
||||
|
||||
addLog(`${icon('radio', 14)} ${t('chatDebug.wsAddress', { url })}`)
|
||||
@@ -640,7 +640,7 @@ async function fixPairing(page) {
|
||||
const port = config?.gateway?.port || 18789
|
||||
const rawToken = config?.gateway?.auth?.token
|
||||
const token = (typeof rawToken === 'string') ? rawToken : ''
|
||||
const wsHost = window.__TAURI_INTERNALS__ ? `127.0.0.1:${port}` : location.host
|
||||
const wsHost = isTauriRuntime() ? `127.0.0.1:${port}` : location.host
|
||||
const url = `ws://${wsHost}/ws?token=${encodeURIComponent(token)}`
|
||||
|
||||
const ws = new WebSocket(url)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 聊天页面 - 完整版,对接 OpenClaw Gateway
|
||||
* 支持:流式响应、Markdown 渲染、会话管理、Agent 选择、快捷指令
|
||||
*/
|
||||
import { api, invalidate } from '../lib/tauri-api.js'
|
||||
import { api, invalidate, isTauriRuntime } from '../lib/tauri-api.js'
|
||||
import { navigate } from '../router.js'
|
||||
import { wsClient, uuid } from '../lib/ws-client.js'
|
||||
import { renderMarkdown } from '../lib/markdown.js'
|
||||
@@ -1233,7 +1233,7 @@ async function connectGateway() {
|
||||
// 未连接,发起新连接
|
||||
const config = await api.readOpenclawConfig()
|
||||
const gw = config?.gateway || {}
|
||||
const host = window.__TAURI_INTERNALS__ ? `127.0.0.1:${gw.port || 18789}` : location.host
|
||||
const host = isTauriRuntime() ? `127.0.0.1:${gw.port || 18789}` : location.host
|
||||
const token = gw.auth?.token || gw.authToken || ''
|
||||
wsClient.connect(host, token)
|
||||
} catch (e) {
|
||||
@@ -2970,7 +2970,8 @@ async function callHostedAI(messages, onChunk) {
|
||||
|
||||
if (!config.baseUrl || !config.model) throw new Error(t('chat.hostedModelNotConfigured'))
|
||||
|
||||
let base = config.baseUrl.replace(/\/+$/, '').replace(/\/chat\/completions\/?$/, '').replace(/\/completions\/?$/, '').replace(/\/messages\/?$/, '').replace(/\/models\/?$/, '')
|
||||
const apiType = normalizeHostedApiType(config.apiType)
|
||||
const base = normalizeHostedBaseUrl(config.baseUrl, apiType)
|
||||
if (_hostedAbort) { _hostedAbort.abort(); _hostedAbort = null }
|
||||
_hostedAbort = new AbortController()
|
||||
const signal = _hostedAbort.signal
|
||||
@@ -3010,6 +3011,51 @@ async function callHostedAI(messages, onChunk) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeHostedApiType(raw) {
|
||||
const type = (raw || '').trim()
|
||||
if (type === 'anthropic' || type === 'anthropic-messages') return 'anthropic-messages'
|
||||
if (type === 'google-gemini' || type === 'google-generative-ai') return 'google-generative-ai'
|
||||
if (type === 'ollama') return 'ollama'
|
||||
return 'openai-completions'
|
||||
}
|
||||
|
||||
function normalizeHostedBaseUrl(raw, apiType) {
|
||||
let base = (raw || '').trim()
|
||||
if (!base) throw new Error(t('chat.hostedModelNotConfigured'))
|
||||
if (/^\/\//.test(base)) base = `http:${base}`
|
||||
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(base) && /^(localhost|(?:\d{1,3}\.){3}\d{1,3}|\[[0-9a-f:.]+\]|[^/\s]+:\d+)(?:\/|$)/i.test(base)) {
|
||||
base = `http://${base}`
|
||||
}
|
||||
let url
|
||||
try {
|
||||
url = new URL(base)
|
||||
} catch {
|
||||
throw new Error(t('chat.hostedModelUrlInvalid'))
|
||||
}
|
||||
if (!/^https?:$/.test(url.protocol) || url.hostname === 'tauri.localhost') {
|
||||
throw new Error(t('chat.hostedModelUrlInvalid'))
|
||||
}
|
||||
base = `${url.origin}${url.pathname}`
|
||||
.replace(/\/+$/, '')
|
||||
.replace(/\/api\/chat\/?$/, '')
|
||||
.replace(/\/api\/generate\/?$/, '')
|
||||
.replace(/\/api\/tags\/?$/, '')
|
||||
.replace(/\/api\/?$/, '')
|
||||
.replace(/\/chat\/completions\/?$/, '')
|
||||
.replace(/\/completions\/?$/, '')
|
||||
.replace(/\/responses\/?$/, '')
|
||||
.replace(/\/messages\/?$/, '')
|
||||
.replace(/\/models\/?$/, '')
|
||||
const type = normalizeHostedApiType(apiType)
|
||||
if (type === 'anthropic-messages') {
|
||||
if (!base.endsWith('/v1')) base += '/v1'
|
||||
return base
|
||||
}
|
||||
if (type === 'google-generative-ai') return base
|
||||
if (/:(11434)$/i.test(base) && !base.endsWith('/v1')) return `${base}/v1`
|
||||
return base
|
||||
}
|
||||
|
||||
function appendHostedOutput(text) {
|
||||
if (!text || !_messagesEl) return
|
||||
const wrap = document.createElement('div')
|
||||
|
||||
@@ -118,6 +118,7 @@ async function loadDashboardData(page, fullRefresh = false) {
|
||||
api.readOpenclawConfig(),
|
||||
// 版本信息:首次加载或手动刷新时才查询(避免 ARM 设备上频繁查 npm registry)
|
||||
(!_dashboardInitialized || fullRefresh || !_dashboardVersionCache) ? api.getVersionInfo() : Promise.resolve(_dashboardVersionCache),
|
||||
api.readPanelConfig(),
|
||||
]), 15000)
|
||||
const secondaryP = withTimeout(Promise.allSettled([
|
||||
api.listAgents(),
|
||||
@@ -127,12 +128,13 @@ async function loadDashboardData(page, fullRefresh = false) {
|
||||
const logsP = api.readLogTail('gateway', 20).catch(() => '')
|
||||
|
||||
// 第一波:服务状态 + 配置 + 版本 → 立即渲染统计卡片
|
||||
const [servicesRes, configRes, versionRes] = await coreP
|
||||
const [servicesRes, configRes, versionRes, panelConfigRes] = await coreP
|
||||
const services = servicesRes.status === 'fulfilled' ? servicesRes.value : []
|
||||
const version = (versionRes.status === 'fulfilled' && versionRes.value)
|
||||
? (_dashboardVersionCache = versionRes.value)
|
||||
: (_dashboardVersionCache || {})
|
||||
const config = configRes.status === 'fulfilled' ? configRes.value : null
|
||||
const panelConfig = panelConfigRes.status === 'fulfilled' ? panelConfigRes.value : null
|
||||
const gw = services.find(s => s.label === 'ai.openclaw.gateway')
|
||||
const shouldLoadStatusSummary = gw?.running === true
|
||||
if (!shouldLoadStatusSummary) {
|
||||
@@ -166,7 +168,7 @@ async function loadDashboardData(page, fullRefresh = false) {
|
||||
}
|
||||
}
|
||||
|
||||
renderStatCards(page, services, version, [], config)
|
||||
renderStatCards(page, services, version, [], config, panelConfig)
|
||||
if (gw) {
|
||||
maybeShowForeignGatewayBindingPrompt({
|
||||
service: gw,
|
||||
@@ -191,7 +193,7 @@ async function loadDashboardData(page, fullRefresh = false) {
|
||||
}
|
||||
}
|
||||
|
||||
renderStatCards(page, services, version, agents, config)
|
||||
renderStatCards(page, services, version, agents, config, panelConfig)
|
||||
renderOverview(page, services, mcpConfig, backups, config, agents, statusSummary)
|
||||
|
||||
// 第三波:日志(最低优先级)
|
||||
@@ -212,7 +214,7 @@ async function openGatewayConflict(page, error = null, reason = null) {
|
||||
})
|
||||
}
|
||||
|
||||
function renderStatCards(page, services, version, agents, config) {
|
||||
function renderStatCards(page, services, version, agents, config, panelConfig) {
|
||||
const cardsEl = page.querySelector('#stat-cards')
|
||||
const gw = services.find(s => s.label === 'ai.openclaw.gateway')
|
||||
const foreignGateway = isForeignGatewayService(gw)
|
||||
@@ -225,6 +227,7 @@ function renderStatCards(page, services, version, agents, config) {
|
||||
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 = dedupeOpenclawInstallations(version.all_installations).length
|
||||
const multiInstall = installCount > 1
|
||||
const cliBound = !!(panelConfig?.openclawCliPath && String(panelConfig.openclawCliPath).trim())
|
||||
|
||||
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
|
||||
@@ -252,13 +255,15 @@ function renderStatCards(page, services, version, agents, config) {
|
||||
</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>` : ''}
|
||||
${multiInstall
|
||||
${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' + (cliBound ? '' : ' style="color:var(--warning)"') + '>' + t('dashboard.installCount', { count: installCount }) + '</span>' : ''}</div>` : ''}
|
||||
${multiInstall && !cliBound
|
||||
? `<div class="stat-card-meta" style="margin-top:8px;color:var(--warning);line-height:1.6">${t('dashboard.multiInstallCardHint')}</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">
|
||||
<button class="btn btn-secondary btn-xs" data-action="resolve-multi-install">${t('dashboard.viewGuidance')}</button>
|
||||
<button class="btn btn-primary btn-xs" data-action="open-settings">${t('dashboard.goSettings')}</button>
|
||||
</div>`
|
||||
: multiInstall && cliBound
|
||||
? `<div class="stat-card-meta" style="margin-top:4px;color:var(--text-tertiary);font-size:11px">✓ ${t('dashboard.multiInstallBoundOk', { count: installCount })}</div>`
|
||||
: ''}
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
|
||||
Reference in New Issue
Block a user