mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-07 04:42:46 +08:00
fix hosted session binding and harden panel config resolution
This commit is contained in:
@@ -1040,7 +1040,7 @@ function scanLocalSkillsFallback(agentSkillsDir = null) {
|
||||
status: 'scanned',
|
||||
scannedAt: new Date().toISOString(),
|
||||
scannedRoots,
|
||||
cli: cliError ? { status: 'exec-failed', message: String(cliError?.message || cliError) } : null,
|
||||
cli: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,78 @@ fn default_openclaw_dir() -> PathBuf {
|
||||
dirs::home_dir().unwrap_or_default().join(".openclaw")
|
||||
}
|
||||
|
||||
fn panel_path_key(path: &std::path::Path) -> String {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
return path
|
||||
.to_string_lossy()
|
||||
.replace('/', "\\")
|
||||
.to_lowercase();
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
path.to_string_lossy().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn push_unique_panel_config_path(paths: &mut Vec<PathBuf>, path: PathBuf) {
|
||||
let key = panel_path_key(&path);
|
||||
if paths.iter().any(|existing| panel_path_key(existing) == key) {
|
||||
return;
|
||||
}
|
||||
paths.push(path);
|
||||
}
|
||||
|
||||
fn panel_config_candidate_paths() -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
push_unique_panel_config_path(&mut paths, default_openclaw_dir().join("clawpanel.json"));
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Ok(profile) = std::env::var("USERPROFILE") {
|
||||
let trimmed = profile.trim();
|
||||
if !trimmed.is_empty() {
|
||||
push_unique_panel_config_path(
|
||||
&mut paths,
|
||||
PathBuf::from(trimmed).join(".openclaw").join("clawpanel.json"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let (Ok(home_drive), Ok(home_path)) = (
|
||||
std::env::var("HOMEDRIVE"),
|
||||
std::env::var("HOMEPATH"),
|
||||
) {
|
||||
let combined = format!("{}{}", home_drive.trim(), home_path.trim());
|
||||
let trimmed = combined.trim();
|
||||
if !trimmed.is_empty() {
|
||||
push_unique_panel_config_path(
|
||||
&mut paths,
|
||||
PathBuf::from(trimmed).join(".openclaw").join("clawpanel.json"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(appdata) = std::env::var("APPDATA") {
|
||||
let appdata_path = PathBuf::from(appdata.trim());
|
||||
if let Some(profile_dir) = appdata_path.parent().and_then(|p| p.parent()) {
|
||||
push_unique_panel_config_path(
|
||||
&mut paths,
|
||||
profile_dir.join(".openclaw").join("clawpanel.json"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
fn read_panel_config_from(path: &std::path::Path) -> Option<serde_json::Value> {
|
||||
std::fs::read_to_string(path)
|
||||
.ok()
|
||||
.and_then(|content| serde_json::from_str(&content).ok())
|
||||
}
|
||||
|
||||
fn normalize_custom_openclaw_dir(raw: &str) -> Option<PathBuf> {
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
@@ -121,8 +193,21 @@ fn read_gateway_port_from_config() -> u16 {
|
||||
}
|
||||
|
||||
fn panel_config_path() -> PathBuf {
|
||||
// ClawPanel 自身配置始终在默认目录,不随 openclawDir 变化
|
||||
default_openclaw_dir().join("clawpanel.json")
|
||||
let candidates = panel_config_candidate_paths();
|
||||
for path in &candidates {
|
||||
if read_panel_config_from(path).is_some() {
|
||||
return path.clone();
|
||||
}
|
||||
}
|
||||
for path in &candidates {
|
||||
if path.exists() {
|
||||
return path.clone();
|
||||
}
|
||||
}
|
||||
candidates
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap_or_else(|| default_openclaw_dir().join("clawpanel.json"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -151,9 +236,12 @@ pub(crate) fn windows_npm_global_prefix() -> Option<String> {
|
||||
}
|
||||
|
||||
pub fn read_panel_config_value() -> Option<serde_json::Value> {
|
||||
std::fs::read_to_string(panel_config_path())
|
||||
.ok()
|
||||
.and_then(|content| serde_json::from_str(&content).ok())
|
||||
for path in panel_config_candidate_paths() {
|
||||
if let Some(value) = read_panel_config_from(&path) {
|
||||
return Some(value);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn configured_proxy_url() -> Option<String> {
|
||||
|
||||
@@ -102,6 +102,7 @@ let _hostedAutoStopEl = null
|
||||
let _hostedSaveBtn = null, _hostedStopBtn = null, _hostedCloseBtn = null
|
||||
let _hostedDefaults = null
|
||||
let _hostedSessionConfig = null
|
||||
let _hostedBoundSessionKey = null
|
||||
let _hostedRuntime = { ...HOSTED_RUNTIME_DEFAULT }
|
||||
let _hostedBusy = false
|
||||
let _hostedAbort = null
|
||||
@@ -1692,8 +1693,39 @@ function handleEvent(msg) {
|
||||
}
|
||||
|
||||
function handleChatEvent(payload) {
|
||||
// sessionKey 过滤
|
||||
if (payload.sessionKey && payload.sessionKey !== _sessionKey && _sessionKey) return
|
||||
const hostedSessionKey = getHostedBoundSessionKey()
|
||||
const isCurrentSession = !payload.sessionKey || !_sessionKey || payload.sessionKey === _sessionKey
|
||||
const isHostedSession = !!payload.sessionKey && !!hostedSessionKey && payload.sessionKey === hostedSessionKey
|
||||
|
||||
// sessionKey 过滤:当前会话照常渲染;托管绑定会话在后台继续驱动循环
|
||||
if (!isCurrentSession && !isHostedSession) return
|
||||
|
||||
if (!isCurrentSession && isHostedSession) {
|
||||
if (payload.state === 'final' && shouldCaptureHostedTarget(payload)) {
|
||||
const c = extractChatContent(payload.message)
|
||||
const capturedText = c?.text || ''
|
||||
if (capturedText) {
|
||||
appendHostedTarget(capturedText)
|
||||
if (detectStopFromText(capturedText)) {
|
||||
stopHostedAgent()
|
||||
} else {
|
||||
maybeTriggerHostedRun()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.state === 'error' && _hostedSessionConfig?.enabled) {
|
||||
_hostedRuntime.errorCount = (_hostedRuntime.errorCount || 0) + 1
|
||||
_hostedRuntime.lastError = payload.errorMessage || payload.error?.message || t('common.error')
|
||||
_hostedRuntime.pending = false
|
||||
if (_hostedRuntime.errorCount >= _hostedSessionConfig.retryLimit) {
|
||||
_hostedRuntime.status = HOSTED_STATUS.ERROR
|
||||
}
|
||||
persistHostedRuntime()
|
||||
updateHostedBadge()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const { state } = payload
|
||||
const runId = payload.runId
|
||||
@@ -2814,6 +2846,10 @@ function getHostedSessionKey() {
|
||||
return _sessionKey || localStorage.getItem(STORAGE_SESSION_KEY) || 'agent:main:main'
|
||||
}
|
||||
|
||||
function getHostedBoundSessionKey() {
|
||||
return _hostedSessionConfig?.boundSessionKey || _hostedBoundSessionKey || ''
|
||||
}
|
||||
|
||||
async function loadHostedDefaults() {
|
||||
try {
|
||||
const panel = await api.readPanelConfig()
|
||||
@@ -2827,23 +2863,28 @@ function loadHostedSessionConfig() {
|
||||
const key = getHostedSessionKey()
|
||||
const current = data[key] || {}
|
||||
_hostedSessionConfig = { ...HOSTED_DEFAULTS, ..._hostedDefaults, ...current }
|
||||
if (_hostedSessionConfig.enabled && !_hostedSessionConfig.boundSessionKey) {
|
||||
_hostedSessionConfig.boundSessionKey = key
|
||||
}
|
||||
_hostedBoundSessionKey = _hostedSessionConfig.boundSessionKey || null
|
||||
if (!_hostedSessionConfig.state) _hostedSessionConfig.state = { ...HOSTED_RUNTIME_DEFAULT }
|
||||
if (!_hostedSessionConfig.history) _hostedSessionConfig.history = []
|
||||
_hostedRuntime = { ...HOSTED_RUNTIME_DEFAULT, ..._hostedSessionConfig.state }
|
||||
updateHostedBadge()
|
||||
}
|
||||
|
||||
function saveHostedSessionConfig(nextConfig) {
|
||||
function saveHostedSessionConfig(nextConfig, key = null) {
|
||||
let data = {}
|
||||
try { data = JSON.parse(localStorage.getItem(HOSTED_SESSIONS_KEY) || '{}') } catch { data = {} }
|
||||
data[getHostedSessionKey()] = nextConfig
|
||||
data[key || getHostedSessionKey()] = nextConfig
|
||||
localStorage.setItem(HOSTED_SESSIONS_KEY, JSON.stringify(data))
|
||||
}
|
||||
|
||||
function persistHostedRuntime() {
|
||||
function persistHostedRuntime(persistKey = null) {
|
||||
if (!_hostedSessionConfig) return
|
||||
_hostedSessionConfig.state = { ..._hostedRuntime }
|
||||
saveHostedSessionConfig(_hostedSessionConfig)
|
||||
const key = persistKey || getHostedBoundSessionKey() || getHostedSessionKey()
|
||||
saveHostedSessionConfig(_hostedSessionConfig, key)
|
||||
}
|
||||
|
||||
function updateHostedBadge() {
|
||||
@@ -2943,7 +2984,9 @@ async function startHostedAgent() {
|
||||
const retryLimit = Math.max(0, parseInt(_hostedRetryLimitEl?.value || HOSTED_DEFAULTS.retryLimit, 10))
|
||||
const timerOn = _page?.querySelector('#hosted-agent-timer-on')?.checked
|
||||
const autoStopMinutes = timerOn ? Math.max(0, parseInt(_hostedAutoStopEl?.value || 0, 10)) : 0
|
||||
_hostedSessionConfig = { ..._hostedSessionConfig, prompt, enabled: true, maxSteps, stepDelayMs, retryLimit, autoStopMinutes }
|
||||
const boundSessionKey = getHostedSessionKey()
|
||||
_hostedBoundSessionKey = boundSessionKey
|
||||
_hostedSessionConfig = { ..._hostedSessionConfig, prompt, enabled: true, maxSteps, stepDelayMs, retryLimit, autoStopMinutes, boundSessionKey }
|
||||
const sysContent = HOSTED_SYSTEM_PROMPT + '\n\nUser goal: ' + prompt
|
||||
if (!_hostedSessionConfig.history?.length) _hostedSessionConfig.history = [{ role: 'system', content: sysContent }]
|
||||
else if (_hostedSessionConfig.history[0]?.role === 'system') _hostedSessionConfig.history[0].content = sysContent
|
||||
@@ -2968,6 +3011,7 @@ async function startHostedAgent() {
|
||||
|
||||
function stopHostedAgent() {
|
||||
if (!_hostedSessionConfig) return
|
||||
const boundSessionKey = getHostedBoundSessionKey() || getHostedSessionKey()
|
||||
if (_hostedAbort) { _hostedAbort.abort(); _hostedAbort = null }
|
||||
clearTimeout(_hostedAutoStopTimer); _hostedAutoStopTimer = null
|
||||
clearInterval(_countdownInterval); _countdownInterval = null
|
||||
@@ -2979,7 +3023,8 @@ function stopHostedAgent() {
|
||||
_hostedRuntime.lastError = ''
|
||||
_hostedRuntime.errorCount = 0
|
||||
_hostedStartTime = 0
|
||||
persistHostedRuntime()
|
||||
persistHostedRuntime(boundSessionKey)
|
||||
_hostedBoundSessionKey = null
|
||||
renderHostedPanel()
|
||||
updateHostedBadge()
|
||||
toast(t('chat.hostedStopped'), 'info')
|
||||
@@ -2987,6 +3032,8 @@ function stopHostedAgent() {
|
||||
|
||||
function shouldCaptureHostedTarget(payload) {
|
||||
if (!_hostedSessionConfig?.enabled) return false
|
||||
const hostedSessionKey = getHostedBoundSessionKey()
|
||||
if (payload?.sessionKey && hostedSessionKey && payload.sessionKey !== hostedSessionKey) return false
|
||||
if (_hostedRuntime.status === HOSTED_STATUS.PAUSED || _hostedRuntime.status === HOSTED_STATUS.ERROR || _hostedRuntime.status === HOSTED_STATUS.IDLE) return false
|
||||
if (payload?.message?.role && payload.message.role !== 'assistant') return false
|
||||
const ts = payload?.timestamp || Date.now()
|
||||
@@ -3050,8 +3097,9 @@ function detectStopFromText(text) {
|
||||
async function runHostedAgentStep() {
|
||||
if (_hostedBusy || !_hostedSessionConfig?.enabled) return
|
||||
const prompt = (_hostedSessionConfig.prompt || '').trim()
|
||||
const hostedSessionKey = getHostedBoundSessionKey() || getHostedSessionKey()
|
||||
if (!prompt) return
|
||||
if (!wsClient.gatewayReady || !_sessionKey) {
|
||||
if (!wsClient.gatewayReady || !hostedSessionKey) {
|
||||
_hostedRuntime.status = HOSTED_STATUS.PAUSED
|
||||
_hostedRuntime.lastError = 'Gateway not ready'
|
||||
persistHostedRuntime(); updateHostedBadge()
|
||||
@@ -3099,7 +3147,7 @@ async function runHostedAgentStep() {
|
||||
_hostedRuntime.pending = false
|
||||
persistHostedRuntime(); updateHostedBadge()
|
||||
// 将指令发给 Gateway Agent
|
||||
try { await wsClient.chatSend(_sessionKey, instruction) } catch {}
|
||||
try { await wsClient.chatSend(hostedSessionKey, instruction) } catch {}
|
||||
} else {
|
||||
_hostedRuntime.status = HOSTED_STATUS.IDLE
|
||||
_hostedRuntime.pending = false
|
||||
@@ -3221,6 +3269,8 @@ function normalizeHostedBaseUrl(raw, apiType) {
|
||||
|
||||
function appendHostedOutput(text) {
|
||||
if (!text || !_messagesEl) return
|
||||
const hostedSessionKey = getHostedBoundSessionKey()
|
||||
if (hostedSessionKey && _sessionKey && hostedSessionKey !== _sessionKey) return
|
||||
const wrap = document.createElement('div')
|
||||
wrap.className = 'msg msg-system msg-hosted'
|
||||
wrap.textContent = `[${t('chat.hostedAgent')}] ${text}`
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 自动检测环境 → 版本选择 → 一键安装 → 自动跳转
|
||||
*/
|
||||
import { api, invalidate } from '../lib/tauri-api.js'
|
||||
import { showUpgradeModal } from '../components/modal.js'
|
||||
import { showConfirm, showUpgradeModal } from '../components/modal.js'
|
||||
import { toast } from '../components/toast.js'
|
||||
import { setUpgrading, isMacPlatform } from '../lib/app-state.js'
|
||||
import { diagnoseInstallError } from '../lib/error-diagnosis.js'
|
||||
@@ -111,6 +111,51 @@ export async function render() {
|
||||
return page
|
||||
}
|
||||
|
||||
async function maybeRefreshGatewayServiceBinding() {
|
||||
if (!isMacPlatform()) return false
|
||||
|
||||
const [versionInfo, dirInfo] = await Promise.all([
|
||||
api.getVersionInfo().catch(() => null),
|
||||
api.getOpenclawDir().catch(() => null),
|
||||
])
|
||||
if (!versionInfo?.cli_path || dirInfo?.configExists === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
const shouldRefresh = await showConfirm(t('settings.gatewayServiceRefreshConfirm'))
|
||||
if (!shouldRefresh) return false
|
||||
|
||||
toast(t('settings.gatewayServiceRefreshing'), 'info')
|
||||
try {
|
||||
const services = await api.getServicesStatus().catch(() => [])
|
||||
const gw = services?.find?.(s => s.label === 'ai.openclaw.gateway') || services?.[0] || null
|
||||
const shouldStartAgain = gw?.running === true && gw?.owned_by_current_instance !== false
|
||||
|
||||
await api.uninstallGateway().catch(() => {})
|
||||
await api.installGateway()
|
||||
if (shouldStartAgain) {
|
||||
await api.startService('ai.openclaw.gateway')
|
||||
}
|
||||
|
||||
toast(t('settings.gatewayServiceRefreshed'), 'success')
|
||||
return true
|
||||
} catch (e) {
|
||||
toast(`${t('settings.gatewayServiceRefreshFailed')}: ${e?.message || e}`, 'warning')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function promptRestart(msg) {
|
||||
if (!window.__TAURI_INTERNALS__) { toast(msg, 'success'); return }
|
||||
const ok = await showConfirm(`${msg}\n\n${t('settings.restartConfirm')}`)
|
||||
if (ok) {
|
||||
toast(t('settings.restarting'), 'info')
|
||||
try { await api.relaunchApp() } catch { toast(t('settings.restartFailed'), 'warning') }
|
||||
} else {
|
||||
toast(`${msg}, ${t('settings.effectNextLaunch')}`, 'success')
|
||||
}
|
||||
}
|
||||
|
||||
async function runDetect(page) {
|
||||
const stepsEl = page.querySelector('#setup-steps')
|
||||
stepsEl.innerHTML = `
|
||||
@@ -620,7 +665,10 @@ function bindEvents(page, nodeOk, detectState) {
|
||||
await api.writePanelConfig(cfg)
|
||||
invalidate()
|
||||
if (dirResultEl) dirResultEl.innerHTML = `<span style="color:var(--success)">✓ ${t('setup.pathSaved')}</span>`
|
||||
toast(t('setup.customPathSaved'), 'success')
|
||||
const savedMsg = t('setup.customPathSaved')
|
||||
const refreshed = await maybeRefreshGatewayServiceBinding()
|
||||
if (refreshed) toast(savedMsg, 'success')
|
||||
else await promptRestart(savedMsg)
|
||||
setTimeout(() => runDetect(page), 500)
|
||||
} catch (e) {
|
||||
if (dirResultEl) dirResultEl.innerHTML = `<span style="color:var(--error)">${t('setup.saveFailed', { err: e })}</span>`
|
||||
@@ -673,7 +721,10 @@ function bindEvents(page, nodeOk, detectState) {
|
||||
invalidate()
|
||||
if (dirInput) dirInput.value = ''
|
||||
if (dirResultEl) { dirResultEl.style.display = 'block'; dirResultEl.innerHTML = `<span style="color:var(--success)">✓ ${t('setup.defaultRestored')}</span>` }
|
||||
toast(t('setup.defaultRestoredToast'), 'success')
|
||||
const restoredMsg = t('setup.defaultRestoredToast')
|
||||
const refreshed = await maybeRefreshGatewayServiceBinding()
|
||||
if (refreshed) toast(restoredMsg, 'success')
|
||||
else await promptRestart(restoredMsg)
|
||||
setTimeout(() => runDetect(page), 500)
|
||||
} catch (e) {
|
||||
toast(t('setup.restoreFailed', { err: e }), 'error')
|
||||
@@ -779,7 +830,9 @@ function bindEvents(page, nodeOk, detectState) {
|
||||
resultEl.style.display = 'block'
|
||||
resultEl.innerHTML = `<span style="color:var(--success)">✓ ${successText}</span>`
|
||||
}
|
||||
toast(successText, 'success')
|
||||
const refreshed = await maybeRefreshGatewayServiceBinding()
|
||||
if (refreshed) toast(successText, 'success')
|
||||
else await promptRestart(successText)
|
||||
setTimeout(() => runDetect(page), 300)
|
||||
return true
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user