chore: release v0.11.3

This commit is contained in:
晴天
2026-04-03 00:16:50 +08:00
parent 61400397ec
commit ce4e9ee8b0
19 changed files with 269 additions and 76 deletions

View File

@@ -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)

View File

@@ -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>

View File

@@ -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
View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "clawpanel",
"version": "0.11.2",
"version": "0.11.3",
"private": true,
"description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用",
"type": "module",

View File

@@ -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
View File

@@ -351,7 +351,7 @@ dependencies = [
[[package]]
name = "clawpanel"
version = "0.11.2"
version = "0.11.3"
dependencies = [
"base64 0.22.1",
"chrono",

View File

@@ -1,6 +1,6 @@
[package]
name = "clawpanel"
version = "0.11.2"
version = "0.11.3"
edition = "2021"
description = "ClawPanel - OpenClaw 可视化管理面板"
authors = ["qingchencloud"]

View File

@@ -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())

View File

@@ -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",

View File

@@ -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>

View File

@@ -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

View File

@@ -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} 條歷史]'),

View File

@@ -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}'),

View File

@@ -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 服務已卸載'),

View File

@@ -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 = `

View File

@@ -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)

View File

@@ -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')

View File

@@ -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">