From db30f29abf0b83605011be7c716849d5ea058a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Fri, 13 Mar 2026 00:03:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v0.8.2=20=E2=80=94=2015=20fixes=20+=204?= =?UTF-8?q?=20features=20+=203=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: - Stop force-appending /v1 to API URLs (breaks Volcengine /v3 etc) - SSH upgrade: --unset-all + --add for 4 git insteadOf rules - Feishu: builtin detection, overlay→modal fix, select field, plugin version persistence - Docker: HTML response detection, Web mode guidance - Chat: runId dedup prevents duplicate messages - Cron: RPC params name→id - Channels: Gateway reload async (instant UI response), toggle cache invalidation - Linux: auto sudo for non-root npm installs (libc geteuid) - Control UI: dynamic hostname + auth token for remote access - npm: mirror fallback (npmmirror→npmjs.org) - QQBot: native binding friendly error message - Error diagnosis: SSH vs Git-not-installed, native binding detection Features: - About page: company info (武汉晴辰天下网络科技有限公司) - model-presets.js: shared module for models.js + assistant.js - Feishu: dual plugin support (builtin vs official @larksuiteoapi) - Assistant: provider preset quick-fill buttons Improvements: - Website: dynamic download links from latest.json + claw.qt.cool proxy - Linux deploy docs: upgrade guide, Gitee mirror, sudo notes - linux-deploy.sh: Gitee fallback + sudo npm + mirror retry --- CHANGELOG.md | 33 +++++++ docs/00.png | Bin 184668 -> 184091 bytes docs/01.png | Bin 231505 -> 266953 bytes docs/02.png | Bin 332082 -> 403053 bytes docs/03.png | Bin 171223 -> 181768 bytes docs/index.html | 37 ++++++-- docs/linux-deploy.md | 32 ++++++- package.json | 2 +- scripts/linux-deploy.sh | 18 +++- src-tauri/Cargo.lock | 3 +- src-tauri/Cargo.toml | 3 +- src-tauri/src/commands/config.rs | 138 ++++++++++++++++++++-------- src-tauri/src/commands/messaging.rs | 92 ++++++++++++++++++- src-tauri/tauri.conf.json | 2 +- src/lib/error-diagnosis.js | 30 ++++-- src/lib/model-presets.js | 83 +++++++++++++++++ src/lib/tauri-api.js | 7 +- src/pages/about.js | 45 ++++++++- src/pages/assistant.js | 61 ++++++------ src/pages/channels.js | 47 ++++++++-- src/pages/chat.js | 20 ++++ src/pages/cron.js | 8 +- src/pages/dashboard.js | 8 +- src/pages/docker.js | 31 ++++++- src/pages/models.js | 83 +---------------- 25 files changed, 587 insertions(+), 196 deletions(-) create mode 100644 src/lib/model-presets.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c94db1..fd18e1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,39 @@ 格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/), 版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 +## [0.8.2] - 2026-03-12 + +### 修复 (Fixes) + +- **接口地址不再强制拼接 /v1** — 火山引擎(/v3)等第三方 API 不再被错误追加 /v1,仅 Ollama(端口 11434)自动补全 +- **OpenClaw 升级 SSH 失败** — 增加 `git://` 和 `git+ssh://` 协议重定向到 HTTPS,`--unset-all` + `--add` 确保 4 条规则全部生效 +- **飞书插件安装失败** — 新增内置插件检测(`is_plugin_builtin`),已内置时自动跳过 npm install +- **飞书保存 ReferenceError** — 修复 `overlay is not defined`(应为 `modal`),修复表单收集不支持 ` + ${f.options.map(o => ``).join('')} + + + ` + } return `
@@ -379,7 +403,7 @@ async function openConfigDialog(pid, page, state) { const collectForm = () => { const obj = {} reg.fields.forEach(f => { - const el = modal.querySelector(`input[name="${f.key}"]`) + const el = modal.querySelector(`input[name="${f.key}"]`) || modal.querySelector(`select[name="${f.key}"]`) if (el) obj[f.key] = el.value.trim() }) return obj @@ -495,7 +519,18 @@ async function openConfigDialog(pid, page, state) { try { // 如果需要安装插件,先安装并显示日志 if (reg.pluginRequired) { - const pluginId = reg.pluginId || pid + // 飞书特殊处理:根据用户选择的插件版本决定安装包 + let pluginPackage = reg.pluginRequired + let pluginId = reg.pluginId || pid + if (pid === 'feishu') { + const pluginVersionField = modal.querySelector('[data-name="pluginVersion"]') + const pluginVersion = pluginVersionField?.value || 'builtin' + localStorage.setItem('clawpanel-feishu-plugin-version', pluginVersion) + if (pluginVersion === 'official') { + pluginPackage = '@larksuiteoapi/feishu-openclaw-plugin' + pluginId = 'feishu-openclaw-plugin' + } + } const pluginStatus = await api.getChannelPluginStatus(pluginId) // 跳过安装:插件已安装 或 已内置(新版 OpenClaw 内置了 feishu 等插件) if (!pluginStatus?.installed && !pluginStatus?.builtin) { @@ -534,7 +569,7 @@ async function openConfigDialog(pid, page, state) { if (pid === 'qqbot') { await api.installQqbotPlugin() } else { - await api.installChannelPlugin(reg.pluginRequired, pluginId) + await api.installChannelPlugin(pluginPackage, pluginId) } } catch (e) { toast('插件安装失败: ' + e, 'error') diff --git a/src/pages/chat.js b/src/pages/chat.js index ff76ce4..e02c51f 100644 --- a/src/pages/chat.js +++ b/src/pages/chat.js @@ -49,6 +49,7 @@ let _currentAiBubble = null, _currentAiText = '', _currentAiImages = [], _curren let _isStreaming = false, _isSending = false, _messageQueue = [], _streamStartTime = 0 let _lastRenderTime = 0, _renderPending = false, _lastHistoryHash = '' let _streamSafetyTimer = null, _unsubEvent = null, _unsubReady = null, _unsubStatus = null +let _seenRunIds = new Set() let _pageActive = false let _errorTimer = null, _lastErrorMsg = null let _attachments = [] @@ -886,6 +887,17 @@ function handleChatEvent(payload) { if (payload.sessionKey && payload.sessionKey !== _sessionKey && _sessionKey) return const { state } = payload + const runId = payload.runId + + // 重复 run 过滤:跳过已完成的 runId 的后续事件(Gateway 可能对同一消息触发多个 run) + if (runId && state === 'final' && _seenRunIds.has(runId)) { + console.log('[chat] 跳过重复 final, runId:', runId) + return + } + if (runId && state === 'delta' && _seenRunIds.has(runId) && !_isStreaming) { + console.log('[chat] 跳过已完成 run 的 delta, runId:', runId) + return + } if (state === 'delta') { const c = extractChatContent(payload.message) @@ -935,6 +947,14 @@ function handleChatEvent(payload) { const hasContent = finalText || _currentAiImages.length || _currentAiVideos.length || _currentAiAudios.length || _currentAiFiles.length // 忽略空 final(Gateway 会为一条消息触发多个 run,部分是空 final) if (!_currentAiBubble && !hasContent) return + // 标记 runId 为已处理,防止重复 + if (runId) { + _seenRunIds.add(runId) + if (_seenRunIds.size > 200) { + const first = _seenRunIds.values().next().value + _seenRunIds.delete(first) + } + } showTyping(false) // 如果流式阶段没有创建 bubble,从 final message 中提取 if (!_currentAiBubble && hasContent) { diff --git a/src/pages/cron.js b/src/pages/cron.js index 226f298..0de824d 100644 --- a/src/pages/cron.js +++ b/src/pages/cron.js @@ -247,7 +247,7 @@ function renderList(page, state) { const btn = e.currentTarget btn.disabled = true try { - await wsClient.request('cron.run', { name: jid }) + await wsClient.request('cron.run', { id: jid }) toast('任务已触发执行', 'success') setTimeout(() => fetchJobs(page, state), 2000) } catch (err) { toast('触发失败: ' + err, 'error') } @@ -259,7 +259,7 @@ function renderList(page, state) { btn.disabled = true btn.innerHTML = icon('refresh-cw', 14) try { - await wsClient.request('cron.update', { name: jid, patch: { enabled: !job.enabled } }) + await wsClient.request('cron.update', { id: jid, patch: { enabled: !job.enabled } }) toast(job.enabled ? '已暂停' : '已启用', 'info') await fetchJobs(page, state) } catch (err) { toast('操作失败: ' + err, 'error'); btn.disabled = false; btn.innerHTML = job.enabled ? icon('pause', 14) : icon('play', 14) } @@ -273,7 +273,7 @@ function renderList(page, state) { if (!yes) return if (btn) btn.disabled = true try { - await wsClient.request('cron.remove', { name: jid }) + await wsClient.request('cron.remove', { id: jid }) toast('已删除', 'info') await fetchJobs(page, state) } catch (err) { toast('删除失败: ' + err, 'error'); if (btn) btn.disabled = false } @@ -404,7 +404,7 @@ async function openTaskDialog(job, page, state) { patch.schedule = { kind: 'cron', expr: schedule } patch.payload = { kind: 'agentTurn', message } if (agentId) patch.agentId = agentId - await wsClient.request('cron.update', { name: job.id, patch }) + await wsClient.request('cron.update', { id: job.id, patch }) toast('任务已更新', 'success') } else { const params = { diff --git a/src/pages/dashboard.js b/src/pages/dashboard.js index 2934f89..d6bcdb2 100644 --- a/src/pages/dashboard.js +++ b/src/pages/dashboard.js @@ -301,7 +301,13 @@ function bindActions(page) { try { const config = await api.readOpenclawConfig() const port = config?.gateway?.port || 18789 - const url = `http://127.0.0.1:${port}` + // 远程部署时使用当前浏览器域名/IP,桌面版用 127.0.0.1 + const host = window.__TAURI_INTERNALS__ ? '127.0.0.1' : (location.hostname || '127.0.0.1') + const proto = location.protocol === 'https:' ? 'https' : 'http' + let url = `${proto}://${host}:${port}` + // 如果 Gateway 配置了 token 鉴权,附加到 URL 方便直接访问 + const authToken = config?.gateway?.auth?.token + if (authToken) url += `?token=${encodeURIComponent(authToken)}` // 尝试多种方式打开浏览器 if (window.__TAURI_INTERNALS__) { try { diff --git a/src/pages/docker.js b/src/pages/docker.js index b0fff5b..ccadd72 100644 --- a/src/pages/docker.js +++ b/src/pages/docker.js @@ -190,7 +190,11 @@ async function loadClusterOverview(page) { const detail = page.querySelector('#infra-detail') if (detail) detail.textContent = `${nodes.length} 节点 · ${runningContainers} 运行 / ${totalContainers} 总计` } catch (e) { - page.querySelector('#cluster-stats').innerHTML = `${icon('x-circle', 12)} Docker 未连接: ${esc(e.message)}` + const errMsg = String(e.message || e) + // 后端未运行(Tauri 桌面版不含 Docker 后端,或 Web 模式后端未启动) + const isBackendMissing = errMsg.includes('后端服务未运行') || errMsg.includes('is not valid JSON') || errMsg.includes('${icon('x-circle', 12)} Docker 未连接: ${esc(displayMsg)}` // ClawPanel 自身运行在 Docker 容器中时,显示容器内专属指引 if (isInDocker()) { @@ -218,6 +222,31 @@ async function loadClusterOverview(page) { return } + // 后端缺失时显示专属指引(桌面版需要 Web 部署模式) + if (isBackendMissing) { + page.querySelector('#workers-grid').innerHTML = ` +
+
+ +
+
龙虾军团需要 Web 部署模式
+
Docker 容器管理功能需要 ClawPanel Web 后端支持。桌面版暂不内置 Docker 管理后端。
+
+
${icon('info', 14)} 如何使用龙虾军团
+
    +
  1. 使用 Docker 部署 ClawPanel Web 版(推荐):docker run -d -p 3000:3000 -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/qingchencloud/openclaw:latest
  2. +
  3. 或使用开发模式启动:npm run dev,后端会自动启动 Docker 管理服务
  4. +
  5. 确保 Docker Desktop 已安装并运行
  6. +
+
桌面版的 Docker 管理功能正在开发中,敬请期待后续版本更新。
+
+
+ ` + page.querySelector('#docker-nodes').innerHTML = '' + page.querySelector('#docker-containers').innerHTML = '' + return + } + const isWin = navigator.userAgent.includes('Windows') const isMacOS = navigator.userAgent.includes('Mac') const installGuide = isWin diff --git a/src/pages/models.js b/src/pages/models.js index 061e072..6beac83 100644 --- a/src/pages/models.js +++ b/src/pages/models.js @@ -6,60 +6,7 @@ import { api } from '../lib/tauri-api.js' import { toast } from '../components/toast.js' import { showModal, showConfirm } from '../components/modal.js' import { icon, statusIcon } from '../lib/icons.js' - -// API 接口类型选项 -const API_TYPES = [ - { value: 'openai-completions', label: 'OpenAI 兼容 (最常用)' }, - { value: 'anthropic-messages', label: 'Anthropic 原生' }, - { value: 'openai-responses', label: 'OpenAI Responses' }, - { value: 'google-gemini', label: 'Google Gemini' }, -] - -// 服务商快捷预设 -const PROVIDER_PRESETS = [ - { key: 'openai', label: 'OpenAI 官方', baseUrl: 'https://api.openai.com/v1', api: 'openai-completions' }, - { key: 'anthropic', label: 'Anthropic 官方', baseUrl: 'https://api.anthropic.com', api: 'anthropic-messages' }, - { key: 'deepseek', label: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', api: 'openai-completions' }, - { key: 'google', label: 'Google Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1beta', api: 'google-gemini' }, - { key: 'ollama', label: 'Ollama (本地)', baseUrl: 'http://127.0.0.1:11434/v1', api: 'openai-completions' }, -] - -// gpt.qt.cool 推广配置 -const QTCOOL = { - baseUrl: 'https://gpt.qt.cool/v1', - defaultKey: 'sk-0JDu7hyc51ZKD4iNebpFu07EUEhXmVVc', - site: 'https://gpt.qt.cool/', - usageUrl: 'https://gpt.qt.cool/user?key=', - providerKey: 'qtcool', - api: 'openai-completions', - models: [] // 不使用硬编码模型列表,始终从 API 动态获取最新列表 -} - -// 常用模型预设(按服务商分组) -const MODEL_PRESETS = { - openai: [ - { id: 'gpt-4o', name: 'GPT-4o', contextWindow: 128000 }, - { id: 'gpt-4o-mini', name: 'GPT-4o Mini', contextWindow: 128000 }, - { id: 'o3-mini', name: 'o3 Mini', contextWindow: 200000, reasoning: true }, - ], - anthropic: [ - { id: 'claude-sonnet-4-5-20250514', name: 'Claude Sonnet 4.5', contextWindow: 200000 }, - { id: 'claude-haiku-3-5-20241022', name: 'Claude Haiku 3.5', contextWindow: 200000 }, - ], - deepseek: [ - { id: 'deepseek-chat', name: 'DeepSeek V3', contextWindow: 64000 }, - { id: 'deepseek-reasoner', name: 'DeepSeek R1', contextWindow: 64000, reasoning: true }, - ], - google: [ - { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', contextWindow: 1000000, reasoning: true }, - { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', contextWindow: 1000000 }, - ], - ollama: [ - { id: 'qwen2.5:7b', name: 'Qwen 2.5 7B', contextWindow: 32768 }, - { id: 'llama3.2', name: 'Llama 3.2', contextWindow: 8192 }, - { id: 'gemma3', name: 'Gemma 3', contextWindow: 32768 }, - ], -} +import { API_TYPES, PROVIDER_PRESETS, QTCOOL, MODEL_PRESETS, fetchQtcoolModels } from '../lib/model-presets.js' export async function render() { const page = document.createElement('div') @@ -431,13 +378,8 @@ function normalizeProviderUrls(config) { if (!url.endsWith('/v1')) url += '/v1' } else if (apiType !== 'google-gemini') { // Ollama 端口检测:11434 默认需要加 /v1 - if (/:11434$/.test(url)) url += '/v1' - // 其他 OpenAI 兼容: 确保有 /v1 - if (!url.endsWith('/v1')) { - const idx = url.indexOf('/v1/') - if (idx >= 0) url = url.slice(0, idx + 3) - else url += '/v1' - } + if (/:11434$/.test(url) && !url.endsWith('/v1')) url += '/v1' + // 不再强制追加 /v1,尊重用户填写的 URL(火山引擎等第三方用 /v3 等路径) } p.baseUrl = url } @@ -786,23 +728,8 @@ function bindTopActions(page, state) { btn.textContent = '获取模型列表...' btn.disabled = true - // 动态获取模型列表,失败则用静态 fallback - let models = QTCOOL.models - try { - const resp = await fetch(QTCOOL.baseUrl + '/models', { - headers: { 'Authorization': 'Bearer ' + QTCOOL.defaultKey }, - signal: AbortSignal.timeout(8000) - }) - if (resp.ok) { - const data = await resp.json() - if (data.data && data.data.length) { - models = data.data.map(m => ({ - id: m.id, name: m.id, contextWindow: 128000, - reasoning: m.id.includes('codex') - })).sort((a, b) => b.id.localeCompare(a.id)) - } - } - } catch { /* use fallback */ } + // 动态获取模型列表(共享逻辑) + const models = await fetchQtcoolModels() btn.innerHTML = `${icon('zap', 14)} 一键添加全部模型` btn.disabled = false