mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-06-28 19:21:30 +08:00
chore: release v0.11.3
This commit is contained in:
@@ -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