From 7764a327996002deb122bf89459a5bfd3cf48da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Wed, 18 Mar 2026 15:02:04 +0800 Subject: [PATCH] fix: dashboard null crash, chat layout, markdown escaping, gzip, gateway banner delay feat: hosted agent with auto-stop, context compression, visual sliders feat: auto-reload gateway after config save (debounced 3s) style: toast solid bg, chat input enlargement, hosted agent panel CSS chore: fix dev.ps1 encoding, engagement share text --- dev.ps1 | 34 +-- src-tauri/Cargo.lock | 34 +++ src-tauri/Cargo.toml | 2 +- src-tauri/src/commands/mod.rs | 2 +- src/components/engagement.js | 2 +- src/lib/markdown.js | 6 +- src/lib/tauri-api.js | 9 +- src/pages/chat.js | 513 +++++++++++++++++++++++++++++++++- src/pages/dashboard.js | 2 +- src/style/chat.css | 178 +++++++++++- src/style/components.css | 16 +- 11 files changed, 757 insertions(+), 41 deletions(-) diff --git a/dev.ps1 b/dev.ps1 index 9121ed6..e0cb435 100644 --- a/dev.ps1 +++ b/dev.ps1 @@ -1,30 +1,12 @@ -#!/usr/bin/env pwsh -# ClawPanel 开发服务器启动脚本 +#!/usr/bin/env pwsh +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 -Write-Host "🚀 启动 ClawPanel 开发服务器..." -ForegroundColor Cyan +Write-Host '[*] Starting ClawPanel dev server...' -ForegroundColor Cyan -# 检查 Node.js -if (-not (Get-Command node -ErrorAction SilentlyContinue)) { - Write-Host "❌ 未找到 Node.js,请先安装" -ForegroundColor Red - exit 1 -} +if (-not (Get-Command node -ErrorAction SilentlyContinue)) { Write-Host '[ERR] Node.js not found' -ForegroundColor Red; exit 1 } +if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { Write-Host '[ERR] Rust not found' -ForegroundColor Red; exit 1 } +if (-not (Test-Path 'node_modules')) { Write-Host '[*] Installing deps...' -ForegroundColor Yellow; npm install } +if (-not (Test-Path 'src-tauri\target')) { Write-Host '[*] First Rust build (may take minutes)...' -ForegroundColor Yellow } -# 检查 Rust -if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { - Write-Host "❌ 未找到 Rust,请先安装" -ForegroundColor Red - exit 1 -} - -# 检查依赖 -if (-not (Test-Path "node_modules")) { - Write-Host "📦 安装前端依赖..." -ForegroundColor Yellow - npm install -} - -if (-not (Test-Path "src-tauri/target")) { - Write-Host "🦀 首次编译 Rust(可能需要几分钟)..." -ForegroundColor Yellow -} - -# 启动开发服务器 -Write-Host "✨ 启动中..." -ForegroundColor Green +Write-Host '[*] Launching...' -ForegroundColor Green npm run tauri dev diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2cf6eca..c10b22b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -56,6 +56,18 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "async-compression" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "atk" version = "0.18.2" @@ -359,6 +371,23 @@ dependencies = [ "memchr", ] +[[package]] +name = "compression-codecs" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + [[package]] name = "const-oid" version = "0.9.6" @@ -4206,13 +4235,18 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ + "async-compression", "bitflags 2.11.0", "bytes", + "futures-core", "futures-util", "http", "http-body", + "http-body-util", "iri-string", "pin-project-lite", + "tokio", + "tokio-util", "tower", "tower-layer", "tower-service", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8d7468c..f09936a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,7 +23,7 @@ serde_json = "1" dirs = "6" chrono = "0.4" zip = { version = "2", default-features = false, features = ["deflate"] } -reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"], default-features = false } +reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream", "gzip"], default-features = false } futures-util = "0.3" ed25519-dalek = { version = "2", features = ["rand_core"] } sha2 = "0.10" diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 948ff68..d40204e 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -92,7 +92,7 @@ fn build_http_client_opt( user_agent: Option<&str>, use_proxy: bool, ) -> Result { - let mut builder = reqwest::Client::builder().timeout(timeout); + let mut builder = reqwest::Client::builder().timeout(timeout).gzip(true); if let Some(ua) = user_agent { builder = builder.user_agent(ua); } diff --git a/src/components/engagement.js b/src/components/engagement.js index a07b91c..13bbcbe 100644 --- a/src/components/engagement.js +++ b/src/components/engagement.js @@ -66,7 +66,7 @@ export function tryShowEngagement() { _showing = true localStorage.setItem(KEYS.lastShown, String(Date.now())) - const shareText = '推荐一个开源的 OpenClaw 管理面板 — ClawPanel,一键搭建、便捷管理模型和 Agent,还内置 AI 助手帮你排查问题,小白也能轻松上手 👉 https://claw.qt.cool' + const shareText = '推荐一个开源的 OpenClaw 管理面板 — ClawPanel,一键搭建、便捷管理模型和 Agent,还内置 AI 助手帮你排查问题,小白也能轻松上手:https://claw.qt.cool' const overlay = document.createElement('div') overlay.className = 'engage-overlay' diff --git a/src/lib/markdown.js b/src/lib/markdown.js index a27d5dd..25e1cb0 100644 --- a/src/lib/markdown.js +++ b/src/lib/markdown.js @@ -25,7 +25,7 @@ function highlightCode(code, lang) { .replace(/\b(\d+\.?\d*)\b/g, `${S}0${E}$1${S}c${E}`) .replace(/(\/\/.*$|#.*$)/gm, `${S}1${E}$1${S}c${E}`) .replace(/(\/\*[\s\S]*?\*\/)/g, `${S}1${E}$1${S}c${E}`) - .replace(/("(?:[^&]|&(?!quot;))*?"|'(?:[^&]|&(?!#x27;))*?'|`[^`]*`)/g, + .replace(/("(?:[^&]|&(?!quot;))*?"|'[^'\n]*'|`[^`]*`)/g, `${S}2${E}$1${S}c${E}`) .replace(/\b([A-Z][a-zA-Z0-9_]*)\b/g, (m, w) => KEYWORDS.has(w) ? m : `${S}3${E}${w}${S}c${E}`) @@ -43,7 +43,6 @@ function escapeHtml(str) { .replace(//g, '>') .replace(/"/g, '"') - .replace(/'/g, ''') } // 预加载 Tauri convertFileSrc @@ -150,7 +149,8 @@ function inlineFormat(text) { .replace(/(?$1') .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => { const safeSrc = resolveImageSrc(src.trim()) - return `${alt}` + const escapedSrc = escapeHtml(src).replace(/\\/g, '\') + return `${alt}` }) .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => { const safe = /^https?:|^mailto:/i.test(url.trim()) ? url : '#' diff --git a/src/lib/tauri-api.js b/src/lib/tauri-api.js index f8ae335..67a6ee5 100644 --- a/src/lib/tauri-api.js +++ b/src/lib/tauri-api.js @@ -156,6 +156,13 @@ export async function checkBackendHealth() { } } +// 配置保存后防抖重载 Gateway(3 秒内多次写入只触发一次重载) +let _reloadTimer = null +function _debouncedReloadGateway() { + clearTimeout(_reloadTimer) + _reloadTimer = setTimeout(() => { invoke('reload_gateway').catch(() => {}) }, 3000) +} + // 导出 API export const api = { // 服务管理(状态用短缓存,操作不缓存) @@ -169,7 +176,7 @@ export const api = { getVersionInfo: () => cachedInvoke('get_version_info', {}, 30000), getStatusSummary: () => cachedInvoke('get_status_summary', {}, 60000), readOpenclawConfig: () => cachedInvoke('read_openclaw_config'), - writeOpenclawConfig: (config) => { invalidate('read_openclaw_config'); return invoke('write_openclaw_config', { config }) }, + writeOpenclawConfig: (config) => { invalidate('read_openclaw_config'); return invoke('write_openclaw_config', { config }).then(r => { _debouncedReloadGateway(); return r }) }, readMcpConfig: () => cachedInvoke('read_mcp_config'), writeMcpConfig: (config) => { invalidate('read_mcp_config'); return invoke('write_mcp_config', { config }) }, reloadGateway: () => invoke('reload_gateway'), diff --git a/src/pages/chat.js b/src/pages/chat.js index 6c39aaf..3e1c8ab 100644 --- a/src/pages/chat.js +++ b/src/pages/chat.js @@ -79,6 +79,32 @@ let _primaryModel = '' let _selectedModel = '' let _isApplyingModel = false +// ── 托管 Agent ── +const HOSTED_STATUS = { IDLE: 'idle', RUNNING: 'running', WAITING: 'waiting_reply', PAUSED: 'paused', ERROR: 'error' } +const HOSTED_SESSIONS_KEY = 'clawpanel-hosted-agent-sessions' +const HOSTED_SYSTEM_PROMPT = `你是一个托管调度 Agent。你的职责是:根据用户设定的目标,持续引导 OpenClaw AI Agent 完成任务。 +规则: +1. 你每一轮只输出一条简洁的指令(1-3 句话),发给 OpenClaw 执行 +2. 根据 OpenClaw 的回复评估进展,决定下一步指令 +3. 如果任务已完成或无法继续,回复包含"完成"或"停止"来结束循环 +4. 不要重复相同的指令,不要输出解释性文字,只输出下一步要执行的指令` +const HOSTED_DEFAULTS = { enabled: false, prompt: '', autoRunAfterTarget: true, stopPolicy: 'self', maxSteps: 50, stepDelayMs: 1200, retryLimit: 2, autoStopMinutes: 0 } +const HOSTED_RUNTIME_DEFAULT = { status: HOSTED_STATUS.IDLE, stepCount: 0, lastRunAt: 0, lastRunId: '', lastError: '', pending: false, errorCount: 0 } +const HOSTED_CONTEXT_MAX = 30 +const HOSTED_COMPRESS_THRESHOLD = 20 +let _hostedBtn = null, _hostedPanelEl = null, _hostedBadgeEl = null +let _hostedPromptEl = null, _hostedMaxStepsEl = null, _hostedStepDelayEl = null, _hostedRetryLimitEl = null +let _hostedAutoStopEl = null +let _hostedSaveBtn = null, _hostedStopBtn = null, _hostedCloseBtn = null +let _hostedDefaults = null +let _hostedSessionConfig = null +let _hostedRuntime = { ...HOSTED_RUNTIME_DEFAULT } +let _hostedBusy = false +let _hostedAbort = null +let _hostedLastTargetTs = 0 +let _hostedAutoStopTimer = null +let _hostedStartTime = 0 + export async function render() { const page = document.createElement('div') page.className = 'page chat-page' @@ -145,6 +171,48 @@ export async function render() { + + +