From de1531d1110eb784ca3a549378cfe90e601c1cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= Date: Sat, 6 Jun 2026 18:32:37 +0800 Subject: [PATCH] feat: prepare v0.18.0 release --- CONTRIBUTING.md | 4 +- README.de.md | 4 +- README.en.md | 10 +- README.es.md | 4 +- README.fr.md | 4 +- README.ja.md | 4 +- README.ko.md | 4 +- README.md | 19 +- README.pt.md | 4 +- README.ru.md | 4 +- README.vi.md | 4 +- README.zh-TW.md | 4 +- docs/linux-deploy.md | 5 +- scripts/dev-api.js | 99 ++-- scripts/translations/ja/settings.json | 2 +- src-tauri/src/commands/cli_conflict.rs | 77 +++ src-tauri/src/commands/config.rs | 101 ++-- src-tauri/src/commands/hermes.rs | 508 +++++++++++++++----- src-tauri/src/commands/hermes_providers.rs | 33 +- src-tauri/src/commands/service.rs | 82 +++- src/components/sidebar.js | 41 +- src/components/site-message-center.js | 85 +++- src/engines/hermes/lib/hermes-run-events.js | 7 + src/engines/hermes/pages/group-chat.js | 24 +- src/engines/xintian/index.js | 4 +- src/engines/xintian/pages/landing.js | 33 +- src/engines/xintian/style/xintian.css | 68 +++ src/lib/kernel-upgrade.js | 25 +- src/lib/model-presets.js | 5 +- src/lib/tauri-api.js | 1 + src/lib/ws-client.js | 18 +- src/locales/de.json | 4 +- src/locales/en.json | 4 +- src/locales/es.json | 4 +- src/locales/fr.json | 4 +- src/locales/ja.json | 4 +- src/locales/ko.json | 4 +- src/locales/modules/engine.js | 56 ++- src/locales/modules/kernel.js | 4 +- src/locales/modules/settings.js | 2 +- src/locales/modules/siteMessages.js | 7 +- src/locales/pt.json | 4 +- src/locales/ru.json | 4 +- src/locales/vi.json | 4 +- src/locales/zh-CN.json | 4 +- src/locales/zh-TW.json | 4 +- src/main.js | 56 ++- src/pages/about.js | 2 +- src/pages/chat.js | 12 +- src/style/components.css | 362 ++++++++++++-- src/style/site-message-center.css | 8 + tests/dev-api-cli-conflict.test.js | 47 ++ tests/hermes-group-chat-run-events.test.js | 16 + tests/model-presets.test.js | 18 +- tests/site-message-center.test.js | 34 ++ tests/site-update-source-policy.test.js | 24 + tests/web-headless-reload-policy.test.js | 65 +++ 57 files changed, 1591 insertions(+), 453 deletions(-) create mode 100644 src/engines/hermes/lib/hermes-run-events.js create mode 100644 tests/hermes-group-chat-run-events.test.js create mode 100644 tests/site-update-source-policy.test.js create mode 100644 tests/web-headless-reload-policy.test.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cdf3f25..12b3ef8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -136,7 +136,7 @@ clawpanel/ │ ├── linux-deploy.sh # Linux 服务器一键部署 │ └── sync-version.js # 版本号同步脚本 ├── docs/ # 文档、截图与更新清单 -│ ├── update/latest.json # 旧版前端热更新清单 +│ ├── update/latest.json # 旧版前端热更新兼容清单(新客户端不作为主更新入口) │ ├── linux-deploy.md # Linux 部署指南 │ └── docker-deploy.md # Docker 部署指南 ├── public/ # 静态资源(图标、Logo) @@ -451,7 +451,7 @@ ClawPanel 支持访问密码保护,**Web 模式和 Tauri 桌面端均可启用 ### 1. 桌面应用(Tauri) -面向 macOS / Windows / Linux 桌面用户,从 [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases) 下载安装包。 +面向 macOS / Windows / Linux 桌面用户,优先从 [官网下载中心](https://claw.qt.cool/download) 下载安装包;[GitHub Releases](https://github.com/qingchencloud/clawpanel/releases) 保留为备用入口。 ### 2. Linux 服务器(Web 版) diff --git a/README.de.md b/README.de.md index bc4f782..cefc0e7 100644 --- a/README.de.md +++ b/README.de.md @@ -30,7 +30,7 @@ ClawPanel ist ein visuelles Verwaltungspanel, das mehrere AI-Agent-Frameworks unterstützt, derzeit mit Dual-Engine-Unterstützung für [OpenClaw](https://github.com/1186258278/OpenClawChineseTranslation) und [Hermes Agent](https://github.com/nousresearch/hermes-agent). Mit einem **integrierten intelligenten KI-Assistenten**, der bei der Installation hilft, Konfigurationen automatisch diagnostiziert, Probleme behebt und Fehler korrigiert. 8 Werkzeuge + 4 Modi + interaktives Q&A — einfache Verwaltung für Anfänger und Experten. -> 🌐 **Website**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **Download**: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) +> 🌐 **Website**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **Download**: [Offizielles Download-Center](https://claw.qt.cool/download) | Fallback: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) ### 🎁 QingchenCloud AI API @@ -90,7 +90,7 @@ Eine Community leidenschaftlicher KI-Agenten-Entwickler und -Enthusiasten — tr ## Download & Installation -Besuchen Sie [Releases](https://github.com/qingchencloud/clawpanel/releases/latest) für die neueste Version: +Besuchen Sie das [offizielle Download-Center](https://claw.qt.cool/download) für die neueste Version. GitHub Releases bleibt als Fallback verfügbar: | Plattform | Installer | |----------|----------| diff --git a/README.en.md b/README.en.md index c768639..c47f665 100644 --- a/README.en.md +++ b/README.en.md @@ -33,7 +33,7 @@ ClawPanel is a visual management panel supporting multiple AI Agent frameworks, currently with [OpenClaw](https://github.com/1186258278/OpenClawChineseTranslation) and [Hermes Agent](https://github.com/nousresearch/hermes-agent) dual-engine support. It features a **built-in intelligent AI assistant** that helps you install, auto-diagnose configurations, troubleshoot issues, and fix errors. 8 tools + 4 modes + interactive Q&A — easy to manage for beginners and experts alike. -> 🌐 **Website**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **Download**: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) +> 🌐 **Website**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **Download**: [Official Download Center](https://claw.qt.cool/download) | Fallback: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) ### 🎁 QingchenCloud AI API @@ -97,7 +97,7 @@ A community of passionate AI Agent developers and enthusiasts — join us! ## Download & Install -Go to [Releases](https://github.com/qingchencloud/clawpanel/releases/latest) for the latest version: +Go to the [official download center](https://claw.qt.cool/download) for the latest version. It auto-detects your OS and also lets you pick Windows, macOS, or Linux packages manually. GitHub Releases remain available as a fallback. ### macOS @@ -203,16 +203,16 @@ npm run build && npm run serve # Production ## FAQ -### Hot Update Caused UI Issues / Rolling Back to Built-in Version +### Legacy Frontend Hot Update Files / Rolling Back to Built-in Version -ClawPanel desktop supports frontend hot updates. Update files are stored at: +Current ClawPanel desktop updates use full installers downloaded from the official site. The frontend hot update directory is kept only for compatibility with old installations and rollback handling. If you used the old hot update path, files are stored at: | OS | Path | |----|------| | Windows | `%USERPROFILE%\.openclaw\clawpanel\web-update\` | | macOS / Linux | `~/.openclaw/clawpanel/web-update/` | -If the UI looks broken after a hot update or you want to revert to the version bundled with the installer, simply delete that directory and restart: +If the UI looks broken or you want to revert to the frontend bundled with the installer, delete that directory and restart: ```bash # macOS / Linux diff --git a/README.es.md b/README.es.md index 7e80a7d..2361544 100644 --- a/README.es.md +++ b/README.es.md @@ -30,7 +30,7 @@ ClawPanel es un panel de gestión visual que soporta múltiples frameworks de AI Agent, actualmente con soporte dual para [OpenClaw](https://github.com/1186258278/OpenClawChineseTranslation) y [Hermes Agent](https://github.com/nousresearch/hermes-agent). Cuenta con un **asistente IA inteligente integrado** que te ayuda a instalar, diagnosticar configuraciones automáticamente, solucionar problemas y corregir errores. 8 herramientas + 4 modos + Q&A interactivo — fácil de gestionar para principiantes y expertos. -> 🌐 **Sitio web**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **Descargar**: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) +> 🌐 **Sitio web**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **Descargar**: [Centro de descargas oficial](https://claw.qt.cool/download) | Alternativa: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) ### 🎁 QingchenCloud AI API @@ -90,7 +90,7 @@ Una comunidad de desarrolladores y entusiastas apasionados por los AI Agents — ## Descargar e instalar -Visita [Releases](https://github.com/qingchencloud/clawpanel/releases/latest) para la última versión: +Visita el [centro de descargas oficial](https://claw.qt.cool/download) para la última versión. GitHub Releases sigue disponible como alternativa: | Plataforma | Instalador | |-----------|-----------| diff --git a/README.fr.md b/README.fr.md index e7c69ed..148aad3 100644 --- a/README.fr.md +++ b/README.fr.md @@ -30,7 +30,7 @@ ClawPanel est un panneau de gestion visuel supportant plusieurs frameworks d'agents IA, actuellement avec un double support pour [OpenClaw](https://github.com/1186258278/OpenClawChineseTranslation) et [Hermes Agent](https://github.com/nousresearch/hermes-agent). Il intègre un **assistant IA intelligent** qui vous aide à installer, diagnostiquer automatiquement les configurations, résoudre les problèmes et corriger les erreurs. 8 outils + 4 modes + Q&A interactif — facile à gérer pour débutants et experts. -> 🌐 **Site web** : [claw.qt.cool](https://claw.qt.cool/) | 📦 **Télécharger** : [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) +> 🌐 **Site web** : [claw.qt.cool](https://claw.qt.cool/) | 📦 **Télécharger** : [Centre de téléchargement officiel](https://claw.qt.cool/download) | Secours : [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) ### 🎁 QingchenCloud AI API @@ -90,7 +90,7 @@ Une communauté de développeurs et d'enthousiastes passionnés par les agents I ## Télécharger et installer -Rendez-vous sur [Releases](https://github.com/qingchencloud/clawpanel/releases/latest) pour la dernière version : +Rendez-vous sur le [centre de téléchargement officiel](https://claw.qt.cool/download) pour la dernière version. GitHub Releases reste disponible en secours : | Plateforme | Installateur | |-----------|-------------| diff --git a/README.ja.md b/README.ja.md index 49e454f..058482f 100644 --- a/README.ja.md +++ b/README.ja.md @@ -30,7 +30,7 @@ ClawPanel は複数の AI Agent フレームワークをサポートするビジュアル管理パネルで、現在 [OpenClaw](https://github.com/1186258278/OpenClawChineseTranslation) と [Hermes Agent](https://github.com/nousresearch/hermes-agent) のデュアルエンジンをサポートしています。**インテリジェント AI アシスタントを内蔵**し、ワンクリックインストール、設定の自動診断、問題の特定と修復をサポートします。8 つのツール + 4 つのモード + インタラクティブ Q&A で、初心者からエキスパートまで簡単に管理できます。 -> 🌐 **ウェブサイト**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **ダウンロード**: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) +> 🌐 **ウェブサイト**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **ダウンロード**: [公式ダウンロードセンター](https://claw.qt.cool/download) | 予備: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) ### 🎁 晴辰クラウド AI API @@ -90,7 +90,7 @@ AI Agent に情熱を持つ開発者とユーザーのコミュニティ — ぜ ## ダウンロードとインストール -[Releases](https://github.com/qingchencloud/clawpanel/releases/latest) から最新版をダウンロード: +[公式ダウンロードセンター](https://claw.qt.cool/download) から最新版をダウンロードしてください。GitHub Releases は予備のダウンロード先です: | プラットフォーム | インストーラー | |-----------------|---------------| diff --git a/README.ko.md b/README.ko.md index 327797d..54a22ef 100644 --- a/README.ko.md +++ b/README.ko.md @@ -30,7 +30,7 @@ ClawPanel은 여러 AI Agent 프레임워크를 지원하는 시각적 관리 패널으로, 현재 [OpenClaw](https://github.com/1186258278/OpenClawChineseTranslation) 및 [Hermes Agent](https://github.com/nousresearch/hermes-agent) 듀얼 엔진을 지원합니다. **지능형 AI 어시스턴트를 내장**하여 원클릭 설치, 자동 설정 진단, 문제 해결 및 오류 수정을 지원합니다. 8개 도구 + 4가지 모드 + 대화형 Q&A로 초보자부터 전문가까지 쉽게 관리할 수 있습니다. -> 🌐 **웹사이트**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **다운로드**: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) +> 🌐 **웹사이트**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **다운로드**: [공식 다운로드 센터](https://claw.qt.cool/download) | 예비: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) ### 🎁 칭천클라우드 AI API @@ -90,7 +90,7 @@ AI Agent에 열정적인 개발자와 사용자 커뮤니티 — 함께하세요 ## 다운로드 및 설치 -[Releases](https://github.com/qingchencloud/clawpanel/releases/latest)에서 최신 버전 다운로드: +[공식 다운로드 센터](https://claw.qt.cool/download)에서 최신 버전을 받으세요. GitHub Releases는 예비 다운로드 경로입니다: | 플랫폼 | 설치 파일 | |--------|----------| diff --git a/README.md b/README.md index 919c02a..36e3bf8 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ ClawPanel 是支持多 AI Agent 框架的可视化管理面板,目前支持 [OpenClaw](https://github.com/1186258278/OpenClawChineseTranslation) 和 [Hermes Agent](https://github.com/nousresearch/hermes-agent) 双引擎。**内置智能 AI 助手**,帮你一键安装、自动诊断配置、排查问题、修复错误。8 大工具 + 4 种模式 + 交互式问答,从新手到老手都能轻松管理。 -> 🌐 **官网**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **下载**: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) +> 🌐 **官网**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **下载**: [官网下载中心](https://claw.qt.cool/download) | 备用: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) ## ✨ Hermes Agent 第二引擎:会话、记忆、人格与工具全景管理 @@ -153,7 +153,7 @@ ClawPanel 提供**纯 Web 版部署模式**(零 GUI 依赖),天然兼容 A ## 下载安装 -前往 [Releases](https://github.com/qingchencloud/clawpanel/releases/latest) 页面下载最新版本,根据你的系统选择对应安装包: +前往 [官网下载中心](https://claw.qt.cool/download) 下载最新版本。页面会自动识别系统,也可以手动选择 Windows、macOS 或 Linux 安装包;GitHub Releases 作为备用下载入口保留。 ### macOS @@ -226,13 +226,14 @@ ClawPanel 提供多种升级方式,根据你的安装方式选择对应方案 ### macOS / Windows 桌面版升级 -桌面版内置**自动更新机制**,新版本发布后会自动提示升级: +桌面版会通过官网 API 检查新版本。发现新版本后,面板会推荐适合当前系统的完整安装包: 1. 打开 ClawPanel,如有新版本会弹出升级提示 -2. 点击「立即升级」,等待下载完成后自动安装重启 -3. 也可前往「关于」页面手动检查更新 +2. 点击「下载推荐安装包」,浏览器会从官网镜像下载文件 +3. 退出 ClawPanel,按系统安装器提示覆盖安装 +4. 也可前往「关于」页面手动检查更新 -> **手动升级**:如果自动更新失败,可前往 [Releases](https://github.com/qingchencloud/clawpanel/releases/latest) 下载最新安装包,覆盖安装即可。数据不会丢失。 +> **手动升级**:如果官网下载失败,可前往 [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) 下载最新安装包,覆盖安装即可。数据不会丢失。 ### Linux 桌面版升级 @@ -947,16 +948,16 @@ sudo systemctl restart clawpanel # 或 pm2 restart clawpanel 3. 飞书:私聊测试需在「工作台」搜索机器人名称;群聊需通过「群设置 → 智能群助手」添加 4. 钉钉:消息接收模式必须选择 **Stream 模式** -### 热更新后界面异常 / 想回退到内嵌版本 +### 旧版前端热更新残留 / 想回退到内嵌版本 -ClawPanel 桌面端支持前端热更新,更新文件存储在: +新版 ClawPanel 的主更新方式是下载完整安装包并覆盖安装。前端热更新目录仅用于兼容旧版本残留资源和回滚处理;如果你曾经使用过旧版热更新,相关文件会存储在: | 系统 | 路径 | |------|------| | Windows | `%USERPROFILE%\.openclaw\clawpanel\web-update\` | | macOS / Linux | `~/.openclaw/clawpanel/web-update/` | -如果热更新后界面显示异常或想回退到安装包自带的版本,删除该目录后重启即可: +如果界面显示异常或想回退到安装包自带的内嵌前端,删除该目录后重启即可: ```bash # macOS / Linux diff --git a/README.pt.md b/README.pt.md index e951cb6..ab2aa26 100644 --- a/README.pt.md +++ b/README.pt.md @@ -30,7 +30,7 @@ ClawPanel é um painel de gestão visual que suporta múltiplos frameworks de AI Agent, atualmente com suporte dual para [OpenClaw](https://github.com/1186258278/OpenClawChineseTranslation) e [Hermes Agent](https://github.com/nousresearch/hermes-agent). Possui um **assistente IA inteligente integrado** que ajuda a instalar, diagnosticar configurações automaticamente, resolver problemas e corrigir erros. 8 ferramentas + 4 modos + Q&A interativo — fácil de gerenciar para iniciantes e especialistas. -> 🌐 **Website**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **Download**: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) +> 🌐 **Website**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **Download**: [Centro de download oficial](https://claw.qt.cool/download) | Fallback: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) ### 🎁 QingchenCloud AI API @@ -90,7 +90,7 @@ Uma comunidade de desenvolvedores e entusiastas apaixonados por AI Agents — ju ## Download e instalação -Acesse [Releases](https://github.com/qingchencloud/clawpanel/releases/latest) para a versão mais recente: +Acesse o [centro de download oficial](https://claw.qt.cool/download) para a versão mais recente. GitHub Releases continua disponível como fallback: | Plataforma | Instalador | |-----------|-----------| diff --git a/README.ru.md b/README.ru.md index 06c1a52..7dc3a33 100644 --- a/README.ru.md +++ b/README.ru.md @@ -30,7 +30,7 @@ ClawPanel — это визуальная панель управления, поддерживающая несколько фреймворков AI-агентов, сейчас с двойной поддержкой [OpenClaw](https://github.com/1186258278/OpenClawChineseTranslation) и [Hermes Agent](https://github.com/nousresearch/hermes-agent). Со **встроенным интеллектуальным ИИ-ассистентом**, который помогает установить, автоматически диагностировать конфигурации, устранять неполадки и исправлять ошибки. 8 инструментов + 4 режима + интерактивный Q&A — удобное управление для новичков и экспертов. -> 🌐 **Сайт**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **Скачать**: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) +> 🌐 **Сайт**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **Скачать**: [официальный центр загрузки](https://claw.qt.cool/download) | Резерв: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) ### 🎁 QingchenCloud AI API @@ -90,7 +90,7 @@ ClawPanel — это визуальная панель управления, п ## Скачать и установить -Перейдите на [Releases](https://github.com/qingchencloud/clawpanel/releases/latest) для последней версии: +Перейдите в [официальный центр загрузки](https://claw.qt.cool/download) за последней версией. GitHub Releases остается резервным вариантом: | Платформа | Установщик | |----------|-----------| diff --git a/README.vi.md b/README.vi.md index 8cb1874..ef3f78a 100644 --- a/README.vi.md +++ b/README.vi.md @@ -30,7 +30,7 @@ ClawPanel là bảng quản lý trực quan hỗ trợ nhiều AI Agent framework, hiện tại hỗ trợ [OpenClaw](https://github.com/1186258278/OpenClawChineseTranslation) và [Hermes Agent](https://github.com/nousresearch/hermes-agent) động cơ kép. Tích hợp **trợ lý AI thông minh**, giúp bạn cài đặt, tự động chẩn đoán cấu hình, xử lý sự cố và sửa lỗi. 8 công cụ + 4 chế độ + hỏi đáp tương tác — dễ dàng quản lý cho cả người mới và chuyên gia. -> 🌐 **Website**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **Tải xuống**: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) +> 🌐 **Website**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **Tải xuống**: [Trung tâm tải xuống chính thức](https://claw.qt.cool/download) | Dự phòng: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) ### 🎁 QingchenCloud AI API @@ -90,7 +90,7 @@ Cộng đồng các nhà phát triển và người dùng đam mê AI Agent — ## Tải xuống & Cài đặt -Truy cập [Releases](https://github.com/qingchencloud/clawpanel/releases/latest) để tải phiên bản mới nhất: +Truy cập [trung tâm tải xuống chính thức](https://claw.qt.cool/download) để tải phiên bản mới nhất. GitHub Releases vẫn là đường dẫn dự phòng: | Nền tảng | Trình cài đặt | |----------|--------------| diff --git a/README.zh-TW.md b/README.zh-TW.md index e531e21..31c969a 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -30,7 +30,7 @@ ClawPanel 是支援多 AI Agent 框架的視覺化管理面板,目前支援 [OpenClaw](https://github.com/1186258278/OpenClawChineseTranslation) 和 [Hermes Agent](https://github.com/nousresearch/hermes-agent) 雙引擎。**內建智慧 AI 助手**,幫你一鍵安裝、自動診斷設定、排查問題、修復錯誤。8 大工具 + 4 種模式 + 互動式問答,從新手到老手都能輕鬆管理。 -> 🌐 **官網**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **下載**: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) +> 🌐 **官網**: [claw.qt.cool](https://claw.qt.cool/) | 📦 **下載**: [官網下載中心](https://claw.qt.cool/download) | 備用: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest) ### 🎁 晴辰雲 AI 介面 @@ -90,7 +90,7 @@ ClawPanel 是支援多 AI Agent 框架的視覺化管理面板,目前支援 [O ## 下載安裝 -前往 [Releases](https://github.com/qingchencloud/clawpanel/releases/latest) 下載最新版本: +前往 [官網下載中心](https://claw.qt.cool/download) 下載最新版本;GitHub Releases 保留為備用下載入口: | 平台 | 安裝檔 | |------|--------| diff --git a/docs/linux-deploy.md b/docs/linux-deploy.md index 8f56126..3d82c27 100644 --- a/docs/linux-deploy.md +++ b/docs/linux-deploy.md @@ -349,7 +349,7 @@ sudo npm install -g openclaw@2026.3.11 --registry https://registry.npmjs.org sudo npm install -g @qingchencloud/openclaw-zh@2026.3.7-zh.2 --registry https://registry.npmjs.org ``` -> **维护说明**:如果你是 ClawPanel 维护者,后续只需要更新仓库根目录的 `openclaw-version-policy.json`,即可统一调整不同面板版本对应的推荐 OpenClaw 版本。程序版本号、热更新清单、桌面图标的维护方式见 `docs/version-maintenance.md`。 +> **维护说明**:如果你是 ClawPanel 维护者,后续只需要更新仓库根目录的 `openclaw-version-policy.json`,即可统一调整不同面板版本对应的推荐 OpenClaw 版本。桌面端程序版本以 `package.json` 为唯一真相源,运行 `npm run version:sync` 同步到 Tauri 配置;`docs/update/latest.json` 仅保留给旧版前端热更新兼容链路。 > **权限说明**:Linux 全局 npm 包安装需要 root 权限。ClawPanel 现已自动检测非 root 用户并加 sudo,同时会自动补 GitHub HTTPS rewrite 规则;如仍遇权限问题,手动加 `sudo` 即可。 @@ -357,7 +357,8 @@ sudo npm install -g @qingchencloud/openclaw-zh@2026.3.7-zh.2 --registry https:// - **ClawPanel**:`git pull` 获取最新代码,无需重新安装依赖(除非 package.json 变了) - **OpenClaw**:优先通过面板切换到推荐稳定版;如需尝试其它版本,请在「关于」页手动切换 -- **前端热更新**:面板支持前端热更新(不需要 git pull),在「关于」页面点击「热更新」按钮即可 +- **ClawPanel 桌面版**:通过官网版本接口发现新版本,下载推荐完整安装包后覆盖安装 +- **前端热更新**:仅保留旧版本兼容和回滚目录处理,新版用户界面不再作为主更新入口展示 --- diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 546e812..8198a7f 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -31,7 +31,7 @@ const HERMES_PROVIDER_REGISTRY = [ hermesProvider('gemini', 'Google AI Studio', 'api_key', 'https://generativelanguage.googleapis.com/v1beta/openai', 'GEMINI_BASE_URL', ['GOOGLE_API_KEY', 'GEMINI_API_KEY'], 'openai_chat', 'openai', ['gemini-3.1-pro-preview', 'gemini-3-flash-preview', 'gemini-3.1-flash-lite-preview', 'gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemma-4-31b-it', 'gemma-4-26b-it']), hermesProvider('deepseek', 'DeepSeek', 'api_key', 'https://api.deepseek.com', 'DEEPSEEK_BASE_URL', ['DEEPSEEK_API_KEY'], 'openai_chat', 'openai', ['deepseek-chat', 'deepseek-reasoner']), hermesProvider('xai', 'xAI', 'api_key', 'https://api.x.ai/v1', 'XAI_BASE_URL', ['XAI_API_KEY'], 'openai_chat', 'openai', ['grok-4.20-reasoning', 'grok-4-1-fast-reasoning']), - hermesProvider('minimax', 'MiniMax (International)', 'api_key', 'https://api.minimax.io/anthropic/v1', 'MINIMAX_BASE_URL', ['MINIMAX_API_KEY'], 'anthropic_messages', 'anthropic', ['MiniMax-M2.7', 'MiniMax-M2.7-highspeed', 'MiniMax-M2.5', 'MiniMax-M2.5-highspeed', 'MiniMax-M2.1', 'MiniMax-M2.1-highspeed', 'MiniMax-M2', 'MiniMax-M2-highspeed']), + hermesProvider('minimax', 'MiniMax (International)', 'api_key', 'https://api.minimax.io/anthropic/v1', 'MINIMAX_BASE_URL', ['MINIMAX_API_KEY'], 'anthropic_messages', 'anthropic', ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed']), hermesProvider('huggingface', 'Hugging Face', 'api_key', 'https://router.huggingface.co/v1', 'HF_BASE_URL', ['HF_TOKEN'], 'openai_chat', 'openai', ['Qwen/Qwen3.5-397B-A17B', 'Qwen/Qwen3.5-35B-A3B', 'deepseek-ai/DeepSeek-V3.2', 'moonshotai/Kimi-K2.5', 'MiniMaxAI/MiniMax-M2.5', 'zai-org/GLM-5', 'XiaomiMiMo/MiMo-V2-Flash', 'moonshotai/Kimi-K2-Thinking'], true), hermesProvider('arcee', 'Arcee AI', 'api_key', 'https://api.arcee.ai/api/v1', 'ARCEE_BASE_URL', ['ARCEEAI_API_KEY'], 'openai_chat', 'openai', []), hermesProvider('azure-foundry', 'Azure Foundry', 'api_key', '', 'AZURE_FOUNDRY_BASE_URL', ['AZURE_FOUNDRY_API_KEY'], 'openai_chat', 'openai', [], true), @@ -45,7 +45,7 @@ const HERMES_PROVIDER_REGISTRY = [ hermesProvider('kimi-coding-cn', 'Kimi / Moonshot (China)', 'api_key', 'https://api.moonshot.cn/v1', '', ['KIMI_CN_API_KEY'], 'openai_chat', 'openai', ['kimi-for-coding', 'kimi-k2.6', 'kimi-k2.5', 'kimi-k2-thinking', 'kimi-k2-turbo-preview']), hermesProvider('alibaba', 'Alibaba Cloud (DashScope)', 'api_key', 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1', 'DASHSCOPE_BASE_URL', ['DASHSCOPE_API_KEY'], 'openai_chat', 'openai', ['qwen3.5-plus', 'qwen3-coder-plus', 'qwen3-coder-next', 'glm-5', 'glm-4.7', 'kimi-k2.5', 'MiniMax-M2.5']), hermesProvider('alibaba-coding-plan', 'Alibaba Cloud (Coding Plan)', 'api_key', 'https://coding-intl.dashscope.aliyuncs.com/v1', 'ALIBABA_CODING_PLAN_BASE_URL', ['ALIBABA_CODING_PLAN_API_KEY', 'DASHSCOPE_API_KEY'], 'openai_chat', 'openai', ['qwen3-coder-plus', 'qwen3-coder-next', 'qwen3.5-plus', 'qwen3.5-coder']), - hermesProvider('minimax-cn', 'MiniMax (China)', 'api_key', 'https://api.minimaxi.com/v1', 'MINIMAX_CN_BASE_URL', ['MINIMAX_CN_API_KEY'], 'anthropic_messages', 'anthropic', ['MiniMax-M2.7', 'MiniMax-M2.7-highspeed', 'MiniMax-M2.5', 'MiniMax-M2.5-highspeed', 'MiniMax-M2.1', 'MiniMax-M2.1-highspeed', 'MiniMax-M2', 'MiniMax-M2-highspeed']), + hermesProvider('minimax-cn', 'MiniMax (China)', 'api_key', 'https://api.minimaxi.com/v1', 'MINIMAX_CN_BASE_URL', ['MINIMAX_CN_API_KEY'], 'anthropic_messages', 'anthropic', ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed']), hermesProvider('xiaomi', 'Xiaomi MiMo', 'api_key', 'https://api.xiaomimimo.com/v1', 'XIAOMI_BASE_URL', ['XIAOMI_API_KEY'], 'openai_chat', 'openai', ['mimo-v2-pro', 'mimo-v2-omni', 'mimo-v2-flash']), hermesProvider('bedrock', 'AWS Bedrock', 'aws_sdk', 'https://bedrock-runtime.us-east-1.amazonaws.com', 'BEDROCK_BASE_URL', [], 'anthropic_messages', 'none', []), hermesProvider('openrouter', 'OpenRouter', 'api_key', 'https://openrouter.ai/api/v1', 'OPENAI_BASE_URL', ['OPENROUTER_API_KEY'], 'openai_chat', 'openai', [], true), @@ -57,7 +57,7 @@ const HERMES_PROVIDER_REGISTRY = [ hermesProvider('openai-codex', 'OpenAI Codex', 'oauth_external', 'https://chatgpt.com/backend-api/codex', '', [], 'codex_responses', 'none', ['gpt-5.5', 'gpt-5.4-mini', 'gpt-5.4', 'gpt-5.3-codex', 'gpt-5.2-codex', 'gpt-5.1-codex-max', 'gpt-5.1-codex-mini'], false, 'hermes auth login openai-codex'), hermesProvider('qwen-oauth', 'Qwen OAuth', 'oauth_external', 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1', '', [], 'openai_chat', 'none', ['qwen3.5-plus', 'qwen3-coder-plus', 'qwen3-coder-next'], false, 'hermes auth login qwen-oauth'), hermesProvider('google-gemini-cli', 'Google Gemini (OAuth)', 'oauth_external', 'https://generativelanguage.googleapis.com/v1beta/openai', '', [], 'openai_chat', 'none', ['gemini-2.5-pro', 'gemini-2.5-flash'], false, 'hermes auth login google-gemini-cli'), - hermesProvider('minimax-oauth', 'MiniMax (OAuth)', 'oauth_minimax', 'https://api.minimax.io/anthropic', '', [], 'anthropic_messages', 'none', ['MiniMax-M2.7', 'MiniMax-M2.7-highspeed', 'MiniMax-M2.5', 'MiniMax-M2.5-highspeed', 'MiniMax-M2.1', 'MiniMax-M2.1-highspeed', 'MiniMax-M2', 'MiniMax-M2-highspeed'], false, 'hermes auth login minimax-oauth'), + hermesProvider('minimax-oauth', 'MiniMax (OAuth)', 'oauth_minimax', 'https://api.minimax.io/anthropic', '', [], 'anthropic_messages', 'none', ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2.7-highspeed'], false, 'hermes auth login minimax-oauth'), hermesProvider('copilot-acp', 'GitHub Copilot ACP', 'external_process', 'http://127.0.0.1:0', 'COPILOT_ACP_BASE_URL', [], 'openai_chat', 'none', ['gpt-4o', 'gpt-4.1', 'claude-3.5-sonnet', 'claude-3.7-sonnet'], false, 'hermes auth login copilot-acp'), hermesProvider('custom', 'Custom OpenAI-Compatible', 'api_key', '', 'OPENAI_BASE_URL', ['OPENAI_API_KEY', 'CUSTOM_API_KEY'], 'openai_chat', 'openai', [], true), ] @@ -665,12 +665,12 @@ function resolveOpenclawCliPath() { } function scanAllOpenclawInstallations(activePath = resolveOpenclawCliPath()) { - const activeIdentity = scanCliIdentity(activePath) + const activeIdentity = cliIdentityKey(activePath) return collectAllCliCandidates().map(candidate => ({ path: candidate, source: classifyCliSource(candidate) || 'unknown', version: readVersionFromInstallation(candidate), - active: !!activeIdentity && scanCliIdentity(candidate) === activeIdentity, + active: !!activeIdentity && cliIdentityKey(candidate) === activeIdentity, })).sort((a, b) => { if (a.active !== b.active) return a.active ? -1 : 1 const sourceCmp = String(a.source || '').localeCompare(String(b.source || '')) @@ -702,6 +702,11 @@ function canonicalLowerPathForConflict(rawPath) { return text } +function cliIdentityKey(rawPath) { + const identity = scanCliIdentity(rawPath) + return identity ? canonicalLowerPathForConflict(identity) : '' +} + function standaloneConflictDirs() { const dirs = [] try { dirs.push(standaloneInstallDir()) } catch {} @@ -716,12 +721,46 @@ function isStandaloneConflictPath(cliPath, source = '') { return standaloneConflictDirs().some(dir => canon === dir || canon.startsWith(`${dir}/`)) } +function gatewayOwnerProtectedCliPath() { + const owner = readGatewayOwner() + if (!owner) return null + const startedBy = owner.startedBy || owner.started_by + if (startedBy && startedBy !== 'clawpanel') return null + const ownerDir = owner.openclawDir || owner.openclaw_dir + if (ownerDir && path.resolve(ownerDir) !== path.resolve(OPENCLAW_DIR)) return null + const ownerPort = Number(owner.port || 0) + if (ownerPort && ownerPort !== readGatewayPort()) return null + return owner.cliPath || owner.cli_path || null +} + +function activeOpenclawCliIdentityKeys(options = {}) { + const identities = new Set() + const add = rawPath => { + const key = cliIdentityKey(rawPath) + if (key) identities.add(key) + } + add(resolveOpenclawCliPath()) + add(gatewayOwnerProtectedCliPath()) + for (const rawPath of Array.isArray(options.activeCliPaths) ? options.activeCliPaths : []) { + add(rawPath) + } + return identities +} + +function isActiveOpenclawCliPath(cliPath, options = {}) { + const key = cliIdentityKey(cliPath) + return !!key && activeOpenclawCliIdentityKeys(options).has(key) +} + export function buildOpenclawPathConflictRecords(installations = scanAllOpenclawInstallations()) { const seen = new Set() const records = [] + const activeIdentities = activeOpenclawCliIdentityKeys() for (const item of Array.isArray(installations) ? installations : []) { const cliPath = item?.path if (!cliPath || isStandaloneConflictPath(cliPath, item.source)) continue + const cliIdentity = cliIdentityKey(cliPath) + if (item.active || (cliIdentity && activeIdentities.has(cliIdentity))) continue const key = canonicalLowerPathForConflict(cliPath) || String(cliPath) if (seen.has(key)) continue seen.add(key) @@ -752,6 +791,9 @@ export function quarantineOpenclawPathForWeb(rawPath, options = {}) { if (isStandaloneConflictPath(original, classifyCliSource(original))) { throw new Error('拒绝隔离 standalone 安装目录下的 OpenClaw(这是当前运行版本)') } + if (isActiveOpenclawCliPath(original, options)) { + throw new Error('拒绝隔离正在被 Gateway 使用的 OpenClaw(请先停止 Gateway 或切换 CLI 路径)') + } const fileName = path.basename(original) if (!fileName.toLowerCase().startsWith('openclaw')) { throw new Error(`拒绝隔离非 openclaw 文件: ${fileName}`) @@ -1612,6 +1654,18 @@ function detectInstalledSource() { return 'official' } +function detectActiveCliInstallMode() { + const activeCliPath = resolveOpenclawCliPath() + const activeCliSource = classifyCliSource(activeCliPath) + if (activeCliSource === 'standalone') return 'standalone' + if (['npm-zh', 'npm-official', 'npm-global'].includes(activeCliSource)) return 'npm' + return 'unknown' +} + +export function shouldFallbackStandaloneToNpm({ currentInstallMode = 'unknown', method = 'auto' } = {}) { + return method === 'auto' && currentInstallMode !== 'standalone' +} + function getLocalOpenclawVersion() { let current = readVersionFromInstallation(resolveOpenclawCliPath()) if (!current) { @@ -11379,6 +11433,7 @@ const handlers = { async upgrade_openclaw({ source = 'chinese', version, method = 'auto' } = {}) { const currentSource = detectInstalledSource() + const currentInstallMode = detectActiveCliInstallMode() const pkg = npmPackageName(source) const recommended = recommendedVersionFor(source) const ver = version || recommended || 'latest' @@ -11425,8 +11480,10 @@ const handlers = { return logs.join('\n') } } catch (e) { - if (method === 'auto') { + if (shouldFallbackStandaloneToNpm({ currentInstallMode, method })) { logs.push(`standalone 不可用(GitHub: ${e.message}),降级到 npm 安装...`) + } else if (method === 'auto') { + throw new Error(`当前 OpenClaw 使用 standalone 独立包模式,已阻止自动降级到 npm 全局安装。请稍后重试独立包升级,或在升级方式中手动选择 npm。standalone 安装失败: CDN=${cdnErr}, GitHub=${e.message}`) } else { throw new Error(`standalone 安装失败: CDN=${cdnErr}, GitHub=${e.message}`) } @@ -12166,31 +12223,17 @@ const handlers = { }, async check_panel_update() { - let lastErr = '' try { return await getSitePanelUpdate() } catch (e) { - lastErr = `site: ${e.message || e}` + return { + latest: null, + url: SITE_BASE_URL, + source: 'site', + downloadUrl: SITE_BASE_URL, + error: `site: ${e.message || e}`, + } } - - const sources = [ - { api: 'https://api.github.com/repos/qingchencloud/clawpanel/releases/latest', releases: 'https://github.com/qingchencloud/clawpanel/releases', name: 'github' }, - { api: 'https://gitee.com/api/v5/repos/QtCodeCreators/clawpanel/releases/latest', releases: 'https://gitee.com/QtCodeCreators/clawpanel/releases', name: 'gitee' }, - ] - for (const src of sources) { - try { - const resp = await globalThis.fetch(src.api, { - signal: AbortSignal.timeout(8000), - headers: { 'User-Agent': 'ClawPanel' }, - }) - if (!resp.ok) { lastErr = `${src.name}: HTTP ${resp.status}`; continue } - const json = await resp.json() - const tag = (json.tag_name || '').replace(/^v/, '').trim() - if (!tag) { lastErr = `${src.name}: 未找到版本号`; continue } - return { latest: tag, url: json.html_url || src.releases, source: src.name, downloadUrl: 'https://claw.qt.cool' } - } catch (e) { lastErr = `${src.name}: ${e.message}`; continue } - } - return { latest: null, url: 'https://github.com/qingchencloud/clawpanel/releases', error: lastErr } }, async check_site_announcements({ locale } = {}) { @@ -14934,7 +14977,7 @@ const handlers = { download_frontend_update() { throw new Error('Web 模式无需前端热更新,刷新浏览器即可') }, rollback_frontend_update() { throw new Error('Web 模式不支持前端热更新回滚') }, get_update_status() { return { status: 'idle', mode: 'web' } }, - // 注意:check_panel_update 的真实实现在前面 —— 走官网 API,失败后再回退 GitHub/Gitee。 + // 注意:check_panel_update 的真实实现在前面 —— 只走官网 API。 // 这里不能再 stub,否则 object literal 的后定义会覆盖前者,导致 Web 模式永远看不到新版。 // —— 应用重启(Web 端由 tauri-api.js 包装层直接调 location.reload,到这里说明绕过了包装)—— diff --git a/scripts/translations/ja/settings.json b/scripts/translations/ja/settings.json index 324d824..e73b9ff 100644 --- a/scripts/translations/ja/settings.json +++ b/scripts/translations/ja/settings.json @@ -17,7 +17,7 @@ "languageHint": "インターフェース言語を切り替えます。一部コンテンツは元の言語のまま表示される場合があります。", "testProxy": "接続テスト", "clearProxy": "プロキシ無効化", - "proxyHint": "設定後、npm インストール/アップグレード、バージョンチェック、GitHub/Gitee 更新チェック、ClawHub Skills ダウンロードはこのプロキシを使用します。localhost と LAN アドレスは自動バイパスされます。すぐに有効になります。Gateway が実行中の場合はサービスの再起動を検討してください。", + "proxyHint": "設定後、npm インストール/アップグレード、公式サイトのバージョン・お知らせチェック、ClawHub Skills ダウンロードはこのプロキシを使用します。localhost と LAN アドレスは自動バイパスされます。すぐに有効になります。Gateway が実行中の場合はサービスの再起動を検討してください。", "modelProxyToggle": "モデルテストとモデルリストリクエストもプロキシ経由", "modelProxyHint": "デフォルトオフ。一部のモデル API は国内中継または LAN アドレスのため、プロキシを使用すると接続に失敗する場合があります。モデルプロバイダーがプロキシを必要とする場合のみ有効にしてください。", "modelProxyNoProxy": "まず上のネットワークプロキシアドレスを設定してから、このオプションを有効にしてください。", diff --git a/src-tauri/src/commands/cli_conflict.rs b/src-tauri/src/commands/cli_conflict.rs index f88a07e..6bfe013 100644 --- a/src-tauri/src/commands/cli_conflict.rs +++ b/src-tauri/src/commands/cli_conflict.rs @@ -19,6 +19,7 @@ //! 隔离而非删除,是为了让用户/被影响的第三方软件可以恢复,避免 ClawPanel 越界破坏用户系统。 use serde::Serialize; +use std::collections::HashSet; use std::path::{Path, PathBuf}; #[derive(Debug, Clone, Serialize)] @@ -81,6 +82,73 @@ fn canonical_lower(path: &Path) -> String { s } +fn cli_identity(path: &Path) -> String { + #[cfg(target_os = "windows")] + if let Some(canonical) = crate::utils::canonicalize_windows_openclaw_cli_path(path) { + return canonical_lower(&canonical); + } + canonical_lower(path) +} + +fn gateway_owner_cli_path() -> Option { + let owner_path = crate::commands::openclaw_dir().join("gateway-owner.json"); + let content = std::fs::read_to_string(owner_path).ok()?; + let value = serde_json::from_str::(&content).ok()?; + let started_by = value + .get("started_by") + .or_else(|| value.get("startedBy")) + .and_then(|v| v.as_str()) + .unwrap_or_default(); + if !started_by.is_empty() && started_by != "clawpanel" { + return None; + } + if let Some(owner_dir) = value + .get("openclaw_dir") + .or_else(|| value.get("openclawDir")) + .and_then(|v| v.as_str()) + { + if canonical_lower(Path::new(owner_dir)) + != canonical_lower(&crate::commands::openclaw_dir()) + { + return None; + } + } + if let Some(port) = value.get("port").and_then(|v| v.as_u64()) { + if port != crate::commands::gateway_listen_port() as u64 { + return None; + } + } + value + .get("cli_path") + .or_else(|| value.get("cliPath")) + .and_then(|v| v.as_str()) + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(PathBuf::from) +} + +fn active_cli_identities() -> HashSet { + let mut identities = HashSet::new(); + if let Some(path) = crate::utils::resolve_openclaw_cli_path() { + let identity = cli_identity(Path::new(&path)); + if !identity.is_empty() { + identities.insert(identity); + } + } + if let Some(path) = gateway_owner_cli_path() { + let identity = cli_identity(&path); + if !identity.is_empty() { + identities.insert(identity); + } + } + identities +} + +fn is_active_gateway_cli_path(path: &Path) -> bool { + let identity = cli_identity(path); + !identity.is_empty() && active_cli_identities().contains(&identity) +} + /// 候选可执行文件名(带扩展名) fn executable_candidates(dir: &Path) -> Vec { #[cfg(target_os = "windows")] @@ -170,6 +238,7 @@ pub async fn scan_openclaw_path_conflicts() -> Result, String> .map(|p| canonical_lower(p)) .filter(|s| !s.is_empty()) .collect(); + let active_identities = active_cli_identities(); let path_var = crate::commands::enhanced_path(); #[cfg(target_os = "windows")] @@ -200,6 +269,9 @@ pub async fn scan_openclaw_path_conflicts() -> Result, String> if is_standalone { continue; } + if active_identities.contains(&cli_identity(&candidate)) { + continue; + } let (source, source_label) = detect_source(&candidate); let size_bytes = std::fs::metadata(&candidate).ok().map(|m| m.len()); @@ -238,6 +310,11 @@ pub async fn quarantine_openclaw_path(path: String) -> Result String { } } +fn detect_active_cli_install_mode() -> &'static str { + let Some(cli_path) = crate::utils::resolve_openclaw_cli_path() else { + return "unknown"; + }; + let resolved = std::fs::canonicalize(&cli_path) + .ok() + .unwrap_or_else(|| PathBuf::from(&cli_path)); + let source = crate::utils::classify_cli_source(&resolved.to_string_lossy()); + if source == "standalone" { + "standalone" + } else if source == "npm-zh" || source == "npm-official" || source == "npm-global" { + "npm" + } else { + "unknown" + } +} + +fn should_fallback_standalone_to_npm(current_install_mode: &str, method: &str) -> bool { + method == "auto" && current_install_mode != "standalone" +} + #[tauri::command] pub async fn get_version_info() -> Result { let current = get_local_version().await; @@ -3680,6 +3701,7 @@ async fn upgrade_openclaw_inner( let _guardian_pause = GuardianPause::new("upgrade"); let current_source = detect_installed_source(); + let current_install_mode = detect_active_cli_install_mode(); let pkg_name = npm_package_name(&source); let requested_version = version.clone(); let recommended_version = recommended_version_for(&source); @@ -3744,12 +3766,16 @@ async fn upgrade_openclaw_inner( return Ok(msg); } Err(gh_reason) => { - if method == "auto" { + if should_fallback_standalone_to_npm(current_install_mode, &method) { let _ = app.emit( "upgrade-log", format!("standalone 不可用(GitHub: {gh_reason}),降级到 npm 安装..."), ); let _ = app.emit("upgrade-progress", 5); + } else if method == "auto" { + return Err(format!( + "当前 OpenClaw 使用 standalone 独立包模式,已阻止自动降级到 npm 全局安装。请稍后重试独立包升级,或在升级方式中手动选择 npm。standalone 安装失败: CDN={cdn_reason}, GitHub={gh_reason}" + )); } else { return Err(format!( "standalone 安装失败: CDN={cdn_reason}, GitHub={gh_reason}" @@ -6463,77 +6489,12 @@ pub fn patch_model_vision() -> Result { Ok(changed) } -/// 检查 ClawPanel 自身是否有新版本(官网 → GitHub → Gitee 自动降级) +/// 检查 ClawPanel 自身是否有新版本(官网唯一发现源) #[tauri::command] pub async fn check_panel_update() -> Result { - if let Ok(site) = super::site_api::site_latest_for_panel_update().await { - return Ok(site); - } - - let client = - crate::commands::build_http_client(std::time::Duration::from_secs(8), Some("ClawPanel")) - .map_err(|e| format!("创建 HTTP 客户端失败: {e}"))?; - - // 先尝试 GitHub,失败后降级 Gitee - let sources = [ - ( - "https://api.github.com/repos/qingchencloud/clawpanel/releases/latest", - "https://github.com/qingchencloud/clawpanel/releases", - "github", - ), - ( - "https://gitee.com/api/v5/repos/QtCodeCreators/clawpanel/releases/latest", - "https://gitee.com/QtCodeCreators/clawpanel/releases", - "gitee", - ), - ]; - - let mut last_err = String::new(); - for (api_url, releases_url, source) in &sources { - match client.get(*api_url).send().await { - Ok(resp) if resp.status().is_success() => { - let json: Value = resp - .json() - .await - .map_err(|e| format!("解析响应失败: {e}"))?; - - let tag = json - .get("tag_name") - .and_then(|v| v.as_str()) - .unwrap_or("") - .trim_start_matches('v') - .to_string(); - - if tag.is_empty() { - last_err = format!("{source}: 未找到版本号"); - continue; - } - - let mut result = serde_json::Map::new(); - result.insert("latest".into(), Value::String(tag)); - result.insert( - "url".into(), - json.get("html_url") - .cloned() - .unwrap_or(Value::String(releases_url.to_string())), - ); - result.insert("source".into(), Value::String(source.to_string())); - result.insert( - "downloadUrl".into(), - Value::String("https://claw.qt.cool".into()), - ); - return Ok(Value::Object(result)); - } - Ok(resp) => { - last_err = format!("{source}: HTTP {}", resp.status()); - } - Err(e) => { - last_err = format!("{source}: {e}"); - } - } - } - - Err(last_err) + super::site_api::site_latest_for_panel_update() + .await + .map_err(|e| format!("官网版本接口不可用: {e}")) } // === 面板配置 (clawpanel.json) === diff --git a/src-tauri/src/commands/hermes.rs b/src-tauri/src/commands/hermes.rs index 8d5da33..2a66653 100644 --- a/src-tauri/src/commands/hermes.rs +++ b/src-tauri/src/commands/hermes.rs @@ -333,9 +333,20 @@ fn normalize_provider_url(raw: &str) -> String { out } +fn is_openai_codex_endpoint(base_url: &str) -> bool { + normalize_provider_url(base_url) + == normalize_provider_url("https://chatgpt.com/backend-api/codex") +} + fn normalize_hermes_provider_for_base_url(provider: &str, base_url: Option<&str>) -> String { let pid = provider.trim(); - if pid == "openrouter" { + if pid.eq_ignore_ascii_case("openai-codex") { + return "openai-codex".into(); + } + if base_url.map(is_openai_codex_endpoint).unwrap_or(false) { + return "openai-codex".into(); + } + if pid.eq_ignore_ascii_case("openrouter") { if let Some(url) = base_url { let base = normalize_provider_url(url); let expected = normalize_provider_url("https://openrouter.ai/api/v1"); @@ -347,6 +358,117 @@ fn normalize_hermes_provider_for_base_url(provider: &str, base_url: Option<&str> pid.to_string() } +#[derive(Default, Debug, Clone, PartialEq, Eq)] +struct HermesModelFields { + default_model: String, + provider: String, + base_url: String, +} + +fn read_top_level_hermes_model_fields(raw: &str) -> Result { + let config: serde_yaml::Value = + serde_yaml::from_str(raw).map_err(|e| format!("config.yaml YAML 格式错误: {e}"))?; + let root = config + .as_mapping() + .ok_or_else(|| "config.yaml 顶层必须是对象".to_string())?; + let Some(model_value) = yaml_get(root, "model") else { + return Ok(HermesModelFields::default()); + }; + let Some(model) = model_value.as_mapping() else { + return Ok(HermesModelFields { + default_model: model_value.as_str().unwrap_or_default().to_string(), + provider: String::new(), + base_url: String::new(), + }); + }; + Ok(HermesModelFields { + default_model: yaml_string_field(model, "default").unwrap_or_default(), + provider: yaml_string_field(model, "provider").unwrap_or_default(), + base_url: yaml_string_field(model, "base_url").unwrap_or_default(), + }) +} + +fn rewrite_top_level_hermes_model_provider(raw: &str, provider: &str) -> Result { + let mut out = Vec::new(); + let mut in_model = false; + let mut provider_written = false; + let mut saw_model = false; + let mut model_indent = 0usize; + let mut child_prefix: Option = None; + + for line in raw.lines() { + let trimmed = line.trim(); + let indent = line.len() - line.trim_start_matches([' ', '\t']).len(); + + if !in_model && indent == 0 && trimmed.starts_with("model:") { + in_model = true; + saw_model = true; + provider_written = false; + model_indent = indent; + child_prefix = None; + out.push(line.to_string()); + continue; + } + + if in_model { + if indent <= model_indent && !trimmed.is_empty() && !trimmed.starts_with('#') { + if !provider_written { + let prefix = child_prefix + .clone() + .unwrap_or_else(|| " ".repeat(model_indent + 2)); + out.push(format!("{prefix}provider: {provider}")); + provider_written = true; + } + in_model = false; + } else if indent > model_indent && trimmed.starts_with("provider:") { + let prefix: String = line + .chars() + .take_while(|c| *c == ' ' || *c == '\t') + .collect(); + out.push(format!("{prefix}provider: {provider}")); + provider_written = true; + continue; + } else if indent > model_indent + && child_prefix.is_none() + && !trimmed.is_empty() + && !trimmed.starts_with('#') + { + child_prefix = Some( + line.chars() + .take_while(|c| *c == ' ' || *c == '\t') + .collect(), + ); + } + } + + out.push(line.to_string()); + } + + if !saw_model { + return Err("config.yaml 中未找到顶层 model 字段".into()); + } + if in_model && !provider_written { + let prefix = child_prefix.unwrap_or_else(|| " ".repeat(model_indent + 2)); + out.push(format!("{prefix}provider: {provider}")); + } + + let mut fixed = out.join("\n"); + if !fixed.ends_with('\n') { + fixed.push('\n'); + } + Ok(fixed) +} + +fn should_alias_custom_openai_key(fields: &HermesModelFields) -> bool { + let provider = fields.provider.trim(); + let base = normalize_provider_url(&fields.base_url); + let expected = normalize_provider_url("https://openrouter.ai/api/v1"); + !is_openai_codex_endpoint(&fields.base_url) + && (provider.is_empty() || provider.eq_ignore_ascii_case("custom")) + && !base.is_empty() + && base != expected +} + fn env_file_has_value(raw: &str, key: &str) -> bool { raw.lines().any(|line| { let t = line.trim(); @@ -407,82 +529,47 @@ fn sanitize_hermes_openrouter_custom_mismatch() -> Result { if !config_path.exists() { return Ok(false); } - + let changed = sanitize_hermes_openrouter_custom_mismatch_at(&config_path)?; let raw = - std::fs::read_to_string(&config_path).map_err(|e| format!("读取 config.yaml 失败: {e}"))?; - let mut provider = String::new(); - let mut base_url = String::new(); - let mut in_model = false; - - for line in raw.lines() { - let trimmed = line.trim(); - if trimmed.starts_with("model:") { - in_model = true; - continue; - } - if in_model { - let indented = line.starts_with(' ') || line.starts_with('\t'); - if !indented && !trimmed.is_empty() && !trimmed.starts_with('#') { - break; - } - if let Some(v) = trimmed.strip_prefix("provider:") { - provider = v.trim().trim_matches('"').trim_matches('\'').to_string(); - } else if let Some(v) = trimmed.strip_prefix("base_url:") { - base_url = v.trim().trim_matches('"').trim_matches('\'').to_string(); - } - } - } - - let base = normalize_provider_url(&base_url); - let expected = normalize_provider_url("https://openrouter.ai/api/v1"); - let uses_custom_endpoint = !base.is_empty() && base != expected; - let alias_changed = if provider.is_empty() || provider == "custom" || uses_custom_endpoint { + std::fs::read_to_string(config_path).map_err(|e| format!("读取 config.yaml 失败: {e}"))?; + let fields = read_top_level_hermes_model_fields(&raw)?; + let alias_changed = if should_alias_custom_openai_key(&fields) { ensure_custom_openai_key_alias()? } else { false }; - if !uses_custom_endpoint { - return Ok(alias_changed); - } - if provider == "custom" { - return Ok(alias_changed); + Ok(changed || alias_changed) +} + +fn sanitize_hermes_openrouter_custom_mismatch_at( + config_path: &std::path::Path, +) -> Result { + if !config_path.exists() { + return Ok(false); } - let mut out = Vec::new(); - let mut in_model = false; - let mut provider_written = false; - for line in raw.lines() { - let trimmed = line.trim(); - if trimmed.starts_with("model:") { - in_model = true; - provider_written = false; - out.push(line.to_string()); - continue; - } - if in_model { - let indented = line.starts_with(' ') || line.starts_with('\t'); - if !indented && !trimmed.is_empty() && !trimmed.starts_with('#') { - in_model = false; - if !provider_written { - out.push(" provider: custom".to_string()); - provider_written = true; - } - } else if trimmed.starts_with("provider:") { - out.push(" provider: custom".to_string()); - provider_written = true; - continue; - } - } - out.push(line.to_string()); + let raw = + std::fs::read_to_string(config_path).map_err(|e| format!("读取 config.yaml 失败: {e}"))?; + let fields = read_top_level_hermes_model_fields(&raw)?; + let provider = fields.provider.trim(); + let base = normalize_provider_url(&fields.base_url); + let expected = normalize_provider_url("https://openrouter.ai/api/v1"); + let desired_provider = if provider.eq_ignore_ascii_case("openai-codex") + || is_openai_codex_endpoint(&fields.base_url) + { + "openai-codex" + } else if provider.eq_ignore_ascii_case("openrouter") && !base.is_empty() && base != expected { + "custom" + } else { + return Ok(false); + }; + + if provider.eq_ignore_ascii_case(desired_provider) { + return Ok(false); } - if in_model && !provider_written { - out.push(" provider: custom".to_string()); - } - let mut fixed = out.join("\n"); - if !fixed.ends_with('\n') { - fixed.push('\n'); - } - std::fs::write(&config_path, fixed).map_err(|e| format!("写入 config.yaml 失败: {e}"))?; + + let fixed = rewrite_top_level_hermes_model_provider(&raw, desired_provider)?; + std::fs::write(config_path, fixed).map_err(|e| format!("写入 config.yaml 失败: {e}"))?; Ok(true) } @@ -12585,57 +12672,19 @@ pub async fn hermes_read_config() -> Result { let home = hermes_home(); let config_path = home.join("config.yaml"); let env_path = home.join(".env"); - let _ = sanitize_hermes_openrouter_custom_mismatch(); + sanitize_hermes_openrouter_custom_mismatch()?; - // 读取 config.yaml - let config_raw = std::fs::read_to_string(&config_path).unwrap_or_default(); - let mut model_name = String::new(); - let mut base_url_from_yaml = String::new(); - let mut provider_from_yaml = String::new(); - let mut in_model = false; - for line in config_raw.lines() { - let trimmed = line.trim(); - if trimmed.starts_with("model:") { - in_model = true; - // `model: "xxx"` 单行格式 - if let Some(v) = trimmed - .strip_prefix("model:") - .map(|s| s.trim().trim_matches('"')) - { - if !v.is_empty() && !v.contains(':') { - model_name = v.to_string(); - } - } - continue; - } - if in_model { - if trimmed.starts_with("default:") { - model_name = trimmed - .strip_prefix("default:") - .unwrap() - .trim() - .trim_matches('"') - .to_string(); - } else if trimmed.starts_with("base_url:") { - base_url_from_yaml = trimmed - .strip_prefix("base_url:") - .unwrap() - .trim() - .trim_matches('"') - .to_string(); - } else if trimmed.starts_with("provider:") { - provider_from_yaml = trimmed - .strip_prefix("provider:") - .unwrap() - .trim() - .trim_matches('"') - .to_string(); - } else if !trimmed.is_empty() && !trimmed.starts_with('#') && !trimmed.starts_with('-') - { - in_model = false; - } - } - } + // 读取顶层 model 配置;不要让 auxiliary/x_search 等子配置污染仪表盘显示。 + let model_fields = if config_path.exists() { + let config_raw = std::fs::read_to_string(&config_path) + .map_err(|e| format!("读取 config.yaml 失败: {e}"))?; + read_top_level_hermes_model_fields(&config_raw)? + } else { + HermesModelFields::default() + }; + let model_name = model_fields.default_model; + let base_url_from_yaml = model_fields.base_url; + let provider_from_yaml = model_fields.provider; // 读取 .env 到 key→value map let env_raw = std::fs::read_to_string(&env_path).unwrap_or_default(); @@ -17474,6 +17523,227 @@ mod hermes_config_raw_tests { } } +#[cfg(test)] +mod hermes_dashboard_config_read_tests { + use super::read_top_level_hermes_model_fields; + + #[test] + fn dashboard_reader_uses_only_top_level_model_mapping() { + let raw = "\ +model: + provider: openai-codex + base_url: https://chatgpt.com/backend-api/codex + default: gpt-5.5 +auxiliary: + web_extract: + provider: custom + model: '' +x_search: + model: grok-4.20-reasoning + provider: custom +"; + + let fields = read_top_level_hermes_model_fields(raw).unwrap(); + + assert_eq!(fields.default_model, "gpt-5.5"); + assert_eq!(fields.provider, "openai-codex"); + assert_eq!(fields.base_url, "https://chatgpt.com/backend-api/codex"); + } + + #[test] + fn dashboard_reader_keeps_scalar_model_compatibility() { + let raw = "\ +model: gpt-5.5 +x_search: + model: grok-4.20-reasoning + provider: custom +"; + + let fields = read_top_level_hermes_model_fields(raw).unwrap(); + + assert_eq!(fields.default_model, "gpt-5.5"); + assert!(fields.provider.is_empty()); + assert!(fields.base_url.is_empty()); + } + + #[test] + fn dashboard_reader_rejects_invalid_yaml() { + let raw = "\ +model: + provider: openai-codex + default: gpt-5.5 +"; + + let err = read_top_level_hermes_model_fields(raw).unwrap_err(); + + assert!(err.contains("config.yaml YAML 格式错误")); + } +} + +#[cfg(test)] +mod hermes_sanitizer_tests { + use super::{ + normalize_hermes_provider_for_base_url, sanitize_hermes_openrouter_custom_mismatch_at, + yaml_key, + }; + use std::fs; + use std::path::PathBuf; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn write_config(name: &str, raw: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let dir = std::env::temp_dir().join(format!("clawpanel-hermes-sanitizer-{name}-{nanos}")); + fs::create_dir_all(&dir).unwrap(); + let path = dir.join("config.yaml"); + fs::write(&path, raw).unwrap(); + path + } + + fn top_level_section(raw: &str, key: &str) -> String { + let mut out = String::new(); + let mut in_section = false; + for line in raw.lines() { + let trimmed = line.trim(); + let indent = line.len() - line.trim_start_matches([' ', '\t']).len(); + if indent == 0 && trimmed.starts_with(&format!("{key}:")) { + in_section = true; + } else if in_section && indent == 0 && !trimmed.is_empty() && !trimmed.starts_with('#') + { + break; + } + if in_section { + out.push_str(line); + out.push('\n'); + } + } + out + } + + fn top_level_model_provider(raw: &str) -> Option { + let config: serde_yaml::Value = serde_yaml::from_str(raw).unwrap(); + let root = config.as_mapping().unwrap(); + root.get(yaml_key("model")) + .and_then(|model| model.as_mapping()) + .and_then(|model| model.get(yaml_key("provider"))) + .and_then(|provider| provider.as_str()) + .map(ToString::to_string) + } + + #[test] + fn sanitizer_keeps_openai_codex_with_codex_endpoint_unchanged() { + let raw = "\ +model: + provider: openai-codex + base_url: https://chatgpt.com/backend-api/codex + default: gpt-5.5 +auxiliary: + web_extract: + provider: custom + model: '' +"; + let path = write_config("codex-noop", raw); + + assert!(!sanitize_hermes_openrouter_custom_mismatch_at(&path).unwrap()); + assert_eq!(fs::read_to_string(&path).unwrap(), raw); + assert_eq!( + normalize_hermes_provider_for_base_url( + "openrouter", + Some("https://chatgpt.com/backend-api/codex"), + ), + "openai-codex" + ); + } + + #[test] + fn sanitizer_repairs_custom_codex_endpoint_without_touching_auxiliary() { + let raw = "\ +model: + provider: custom + base_url: https://chatgpt.com/backend-api/codex + default: gpt-5.5 +auxiliary: + vision: + provider: auto + model: '' + web_extract: + provider: custom + model: '' +display: + theme: system +"; + let path = write_config("custom-codex", raw); + let auxiliary_before = top_level_section(raw, "auxiliary"); + + assert!(sanitize_hermes_openrouter_custom_mismatch_at(&path).unwrap()); + let fixed = fs::read_to_string(&path).unwrap(); + + serde_yaml::from_str::(&fixed).unwrap(); + assert_eq!( + top_level_model_provider(&fixed).as_deref(), + Some("openai-codex") + ); + assert_eq!(top_level_section(&fixed, "auxiliary"), auxiliary_before); + } + + #[test] + fn sanitizer_rewrites_openrouter_custom_endpoint_only_at_top_level() { + let raw = "\ +model: + provider: openrouter + base_url: https://example.invalid/v1 + default: gpt-5.5 +auxiliary: + compression: + provider: openrouter + model: '' +"; + let path = write_config("openrouter-custom", raw); + let auxiliary_before = top_level_section(raw, "auxiliary"); + + assert!(sanitize_hermes_openrouter_custom_mismatch_at(&path).unwrap()); + let fixed = fs::read_to_string(&path).unwrap(); + + serde_yaml::from_str::(&fixed).unwrap(); + assert_eq!(top_level_model_provider(&fixed).as_deref(), Some("custom")); + assert_eq!(top_level_section(&fixed, "auxiliary"), auxiliary_before); + } + + #[test] + fn sanitizer_leaves_custom_non_codex_endpoint_unchanged() { + let raw = "\ +model: + provider: custom + base_url: https://example.invalid/v1 + default: gpt-5.5 +"; + let path = write_config("custom-non-codex", raw); + + assert!(!sanitize_hermes_openrouter_custom_mismatch_at(&path).unwrap()); + assert_eq!(fs::read_to_string(&path).unwrap(), raw); + } + + #[test] + fn sanitizer_rejects_invalid_yaml_without_writing() { + let raw = "\ +model: + provider: openrouter + base_url: https://example.invalid/v1 +auxiliary: + web_extract: + provider: custom + model: '' +"; + let path = write_config("invalid-yaml", raw); + let err = sanitize_hermes_openrouter_custom_mismatch_at(&path).unwrap_err(); + + assert!(err.contains("config.yaml YAML 格式错误")); + assert_eq!(fs::read_to_string(&path).unwrap(), raw); + } +} + #[cfg(test)] mod hermes_session_runtime_config_tests { use super::{build_hermes_session_runtime_config_values, merge_hermes_session_runtime_config}; diff --git a/src-tauri/src/commands/hermes_providers.rs b/src-tauri/src/commands/hermes_providers.rs index 2e2516e..bcf655d 100644 --- a/src-tauri/src/commands/hermes_providers.rs +++ b/src-tauri/src/commands/hermes_providers.rs @@ -221,16 +221,7 @@ const P_MINIMAX: HermesProvider = HermesProvider { api_key_env_vars: &["MINIMAX_API_KEY"], transport: TRANSPORT_ANTHROPIC, models_probe: PROBE_ANTHROPIC, - models: &[ - "MiniMax-M2.7", - "MiniMax-M2.7-highspeed", - "MiniMax-M2.5", - "MiniMax-M2.5-highspeed", - "MiniMax-M2.1", - "MiniMax-M2.1-highspeed", - "MiniMax-M2", - "MiniMax-M2-highspeed", - ], + models: &["MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed"], is_aggregator: false, cli_auth_hint: "", }; @@ -244,16 +235,7 @@ const P_MINIMAX_CN: HermesProvider = HermesProvider { api_key_env_vars: &["MINIMAX_CN_API_KEY"], transport: TRANSPORT_ANTHROPIC, models_probe: PROBE_ANTHROPIC, - models: &[ - "MiniMax-M2.7", - "MiniMax-M2.7-highspeed", - "MiniMax-M2.5", - "MiniMax-M2.5-highspeed", - "MiniMax-M2.1", - "MiniMax-M2.1-highspeed", - "MiniMax-M2", - "MiniMax-M2-highspeed", - ], + models: &["MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed"], is_aggregator: false, cli_auth_hint: "", }; @@ -267,16 +249,7 @@ const P_MINIMAX_OAUTH: HermesProvider = HermesProvider { api_key_env_vars: &[], transport: TRANSPORT_ANTHROPIC, models_probe: PROBE_NONE, - models: &[ - "MiniMax-M2.7", - "MiniMax-M2.7-highspeed", - "MiniMax-M2.5", - "MiniMax-M2.5-highspeed", - "MiniMax-M2.1", - "MiniMax-M2.1-highspeed", - "MiniMax-M2", - "MiniMax-M2-highspeed", - ], + models: &["MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed"], is_aggregator: false, cli_auth_hint: "hermes auth login minimax-oauth", }; diff --git a/src-tauri/src/commands/service.rs b/src-tauri/src/commands/service.rs index 7728aa9..ada85e6 100644 --- a/src-tauri/src/commands/service.rs +++ b/src-tauri/src/commands/service.rs @@ -2,10 +2,9 @@ /// /// 检测策略(跨平台统一): /// 1. TCP 连 127.0.0.1:{port},超时 1.5s -/// 2. 连通 → 认为 Gateway 在运行 +/// 2. 连通 → 认为 Gateway 在运行,并尽量解析监听 PID /// -/// 不依赖任何系统命令(无 netstat / PowerShell / launchctl / openclaw health), -/// 无权限问题,逻辑一致。 +/// 系统命令仅作为 PID 增强信息来源;命令不可用时不影响运行状态判断。 use std::collections::HashMap; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, OnceLock}; @@ -122,6 +121,54 @@ fn gateway_owner_pid_needs_refresh(owner: &GatewayOwnerRecord, pid: Option) && matches!(pid, Some(current_pid) if owner.pid != Some(current_pid)) } +#[cfg_attr(not(target_os = "linux"), allow(dead_code))] +fn parse_ss_listen_pid_output(text: &str) -> Option { + for line in text.lines() { + let Some(idx) = line.find("pid=") else { + continue; + }; + let digits: String = line[idx + 4..] + .chars() + .take_while(|c| c.is_ascii_digit()) + .collect(); + if let Ok(pid) = digits.parse::() { + if pid > 0 { + return Some(pid); + } + } + } + None +} + +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), allow(dead_code))] +fn parse_lsof_pid_output(text: &str) -> Option { + text.lines().find_map(|line| { + line.split_whitespace() + .next() + .and_then(|value| value.parse::().ok()) + .filter(|pid| *pid > 0) + }) +} + +#[cfg(test)] +mod gateway_pid_parse_tests { + use super::{parse_lsof_pid_output, parse_ss_listen_pid_output}; + + #[test] + fn parses_linux_ss_listener_pid() { + let output = "State Recv-Q Send-Q Local Address:Port Peer Address:Port Process\n\ +LISTEN 0 511 127.0.0.1:18789 0.0.0.0:* users:((\"node\",pid=4242,fd=23))"; + + assert_eq!(parse_ss_listen_pid_output(output), Some(4242)); + } + + #[test] + fn parses_lsof_listener_pid() { + assert_eq!(parse_lsof_pid_output("4242\n"), Some(4242)); + assert_eq!(parse_lsof_pid_output("not-a-pid\n4242\n"), Some(4242)); + } +} + fn read_gateway_owner() -> Option { let content = std::fs::read_to_string(gateway_owner_path()).ok()?; serde_json::from_str(&content).ok() @@ -933,7 +980,7 @@ mod platform { .output() .ok()?; let text = String::from_utf8_lossy(&output.stdout); - text.lines().next()?.trim().parse::().ok() + super::parse_lsof_pid_output(&text) } /// launchctl 失败时的回退:直接通过 CLI spawn Gateway 进程 @@ -1895,6 +1942,31 @@ mod platform { vec!["ai.openclaw.gateway".to_string()] } + fn get_pid_by_port(port: u16) -> Option { + let filter = format!("sport = :{port}"); + if let Ok(output) = std::process::Command::new("ss") + .args(["-ltnp", &filter]) + .output() + { + let text = String::from_utf8_lossy(&output.stdout); + if let Some(pid) = super::parse_ss_listen_pid_output(&text) { + return Some(pid); + } + } + + if let Ok(output) = std::process::Command::new("lsof") + .args(["-i", &format!("TCP:{port}"), "-sTCP:LISTEN", "-t"]) + .output() + { + let text = String::from_utf8_lossy(&output.stdout); + if let Some(pid) = super::parse_lsof_pid_output(&text) { + return Some(pid); + } + } + + None + } + /// 跨平台统一检测:TCP 连端口 #[allow(dead_code)] pub async fn check_service_status(_uid: u32, _label: &str) -> (bool, Option) { @@ -1912,7 +1984,7 @@ mod platform { .await .unwrap_or(false); if result { - (true, None) + (true, get_pid_by_port(port)) } else { (false, None) } diff --git a/src/components/sidebar.js b/src/components/sidebar.js index e7a9f36..a3cf316 100644 --- a/src/components/sidebar.js +++ b/src/components/sidebar.js @@ -9,12 +9,16 @@ import { toast } from './toast.js' const APP_VERSION = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0' import { t, getLang, setLang, getAvailableLangs } from '../lib/i18n.js' import { isFeatureAvailable } from '../lib/feature-gates.js' -import { getKernelSnapshot } from '../lib/kernel.js' +import { getKernelSnapshot, recommendedIsNewer } from '../lib/kernel.js' import { triggerKernelUpgrade } from '../lib/kernel-upgrade.js' import { getActiveEngine, getActiveEngineId, listEngines, needsInitialEngineChoice, isEngineSetupDeferred, switchEngine, onEngineChange } from '../lib/engine-manager.js' // 当用户点 "暂时不升级" 时,本地会话内不再显示升级提示 const SS_DISMISSED_KERNEL_UPGRADE = 'clawpanel_kernel_upgrade_dismissed' +const KERNEL_POLICY_TTL = 5 * 60 * 1000 +let _kernelPolicyInfo = null +let _kernelPolicyFetchedAt = 0 +let _kernelPolicyLoading = false function NAV_ITEMS_FULL() { return [ { @@ -282,6 +286,7 @@ export function renderSidebar(el) { el.innerHTML = html window.dispatchEvent(new CustomEvent('clawpanel:site-message-launcher-mounted')) + _ensureKernelPolicyInfo(el) // 应用折叠态(桌面端) _setDesktopSidebarCollapsed(collapsed) @@ -422,6 +427,34 @@ export function renderSidebar(el) { function _escSidebar(s) { return String(s || '').replace(//g, '>') } +function _kernelPolicyTarget(snap) { + return _kernelPolicyInfo?.recommended || snap?.target || '' +} + +function _isRunningGatewayBelowTarget(snap) { + if (!snap?.version) return false + const target = _kernelPolicyTarget(snap) + return target ? recommendedIsNewer(target, snap.version) : !snap.isLatest +} + +function _ensureKernelPolicyInfo(el) { + const snap = getKernelSnapshot() + if (getActiveEngineId() !== 'openclaw' || !snap?.version) return + const now = Date.now() + if (_kernelPolicyLoading) return + if (_kernelPolicyInfo && now - _kernelPolicyFetchedAt < KERNEL_POLICY_TTL) return + + _kernelPolicyLoading = true + api.getVersionInfo() + .then(info => { + _kernelPolicyInfo = info || null + _kernelPolicyFetchedAt = Date.now() + if (el?.isConnected) renderSidebar(el) + }) + .catch(() => {}) + .finally(() => { _kernelPolicyLoading = false }) +} + /** * 渲染"内核可升级"卡片。 * @@ -429,7 +462,7 @@ function _escSidebar(s) { return String(s || '').replace(/ diff --git a/src/components/site-message-center.js b/src/components/site-message-center.js index 073b2ac..005128a 100644 --- a/src/components/site-message-center.js +++ b/src/components/site-message-center.js @@ -65,7 +65,7 @@ export function normalizeSiteMessagePayload(payload = {}) { export function openSiteMessageCenter({ force = false, tab = null } = {}) { const visible = getVisibleMessages() if (!force && !visible.notifications.length && !visible.announcements.length) return - _activeTab = tab || (visible.notifications.length ? 'notifications' : 'announcements') + _activeTab = tab || getPreferredTab() renderModal() } @@ -81,9 +81,11 @@ function bindLaunchers() { } function updateLauncherBadge() { - const count = getVisibleMessages().notifications.length + getVisibleMessages().announcements.length + const visible = getVisibleMessages() + const count = isClosedToday() ? 0 : visible.notifications.length + visible.announcements.length document.querySelectorAll(LAUNCHER_SELECTOR).forEach((launcher) => { launcher.classList.toggle('has-unread', count > 0) + launcher.classList.toggle('is-muted-today', isClosedToday()) const badge = launcher.querySelector('.site-message-tool-badge, .site-message-fab-badge') if (badge) badge.textContent = count > 9 ? '9+' : String(count || '') }) @@ -94,6 +96,12 @@ function renderModal() { if (old) old.remove() const visible = getVisibleMessages() + const dismissed = getDismissedMessages() + const displayCounts = { + notifications: visible.notifications.length + dismissed.notifications.length, + announcements: visible.announcements.length + dismissed.announcements.length, + } + const activeVisibleCount = visible[_activeTab]?.length || 0 const overlay = document.createElement('div') overlay.id = 'site-message-overlay' @@ -107,21 +115,25 @@ function renderModal() {

${t('siteMessages.title')}

-

${formatSummary(visible)}

+

${formatSummary(displayCounts)}

- ${renderTab('notifications', t('siteMessages.notifications'), ICON_BELL, visible.notifications.length)} - ${renderTab('announcements', t('siteMessages.announcements'), ICON_SEND, visible.announcements.length)} + ${renderTab('notifications', t('siteMessages.notifications'), ICON_BELL, displayCounts.notifications)} + ${renderTab('announcements', t('siteMessages.announcements'), ICON_SEND, displayCounts.announcements)}
- ${_activeTab === 'notifications' ? renderNotifications(visible.notifications) : renderAnnouncements(visible.announcements)} + ${_activeTab === 'notifications' + ? renderNotifications(visible.notifications, dismissed.notifications) + : renderAnnouncements(visible.announcements, dismissed.announcements)}
- + ${activeVisibleCount + ? `` + : ``}
` @@ -142,8 +154,9 @@ function renderTab(tab, label, icon, count) { ` } -function renderNotifications(items) { +function renderNotifications(items, dismissedItems = []) { if (!items.length) { + if (dismissedItems.length) return renderDismissedState('notifications', dismissedItems.length) return `
@@ -177,8 +190,9 @@ function renderNotifications(items) { ` } -function renderAnnouncements(items) { +function renderAnnouncements(items, dismissedItems = []) { if (!items.length) { + if (dismissedItems.length) return renderDismissedState('announcements', dismissedItems.length) return `
@@ -213,6 +227,18 @@ function renderAnnouncements(items) { ` } +function renderDismissedState(tab, count) { + const icon = tab === 'notifications' ? ICON_BELL : ICON_SEND + return ` +
+ + ${t('siteMessages.dismissedTitle')} +

${t('siteMessages.dismissedHint', { count })}

+ +
+ ` +} + function renderMessageCta(item, prominent = false) { if (!item.ctaText || !item.ctaUrl) return '' return `${escapeHtml(item.ctaText)}` @@ -223,12 +249,19 @@ function bindModalEvents(overlay) { overlay.querySelector('[data-site-message-today]')?.addEventListener('click', () => { localStorage.setItem(TODAY_CLOSE_KEY, todayKey()) closeModal() + updateLauncherBadge() }) overlay.querySelector('[data-site-message-dismiss]')?.addEventListener('click', () => { dismissItems(getVisibleMessages()[_activeTab]) closeModal() updateLauncherBadge() }) + overlay.querySelector('[data-site-message-close-current]')?.addEventListener('click', closeModal) + overlay.querySelector('[data-site-message-restore]')?.addEventListener('click', () => { + restoreItems(_messages[_activeTab]) + renderModal() + updateLauncherBadge() + }) overlay.querySelectorAll('[data-site-message-tab]').forEach((btn) => { btn.addEventListener('click', () => { _activeTab = btn.dataset.siteMessageTab || 'notifications' @@ -255,12 +288,23 @@ function dismissItems(items) { } } +function restoreItems(items) { + for (const item of items || []) { + const key = dismissStorageKey(item) + if (key) localStorage.removeItem(key) + } +} + function shouldAutoOpen() { - if (localStorage.getItem(TODAY_CLOSE_KEY) === todayKey()) return false + if (isClosedToday()) return false const visible = getVisibleMessages() return visible.notifications.length > 0 || visible.announcements.length > 0 } +function isClosedToday() { + return localStorage.getItem(TODAY_CLOSE_KEY) === todayKey() +} + function getVisibleMessages() { return { notifications: _messages.notifications.filter(item => !isDismissed(item)), @@ -268,6 +312,27 @@ function getVisibleMessages() { } } +function getDismissedMessages() { + return { + notifications: _messages.notifications.filter(isDismissed), + announcements: _messages.announcements.filter(isDismissed), + } +} + +function getPreferredTab() { + const visible = getVisibleMessages() + if (visible.notifications.length) return 'notifications' + if (visible.announcements.length) return 'announcements' + const dismissed = getDismissedMessages() + if (dismissed.notifications.length) return 'notifications' + return 'announcements' +} + +function getDismissButtonLabel() { + if (_activeTab === 'notifications') return t('siteMessages.dismissCurrentNotifications') + return t('siteMessages.dismissCurrentAnnouncements') +} + function normalizePayload(payload = {}) { const notifications = [] const announcements = [] diff --git a/src/engines/hermes/lib/hermes-run-events.js b/src/engines/hermes/lib/hermes-run-events.js new file mode 100644 index 0000000..17ab37a --- /dev/null +++ b/src/engines/hermes/lib/hermes-run-events.js @@ -0,0 +1,7 @@ +/** + * Predicate for Hermes `hermes-run-*` Tauri events. + * Requires a known run_id so concurrent runs cannot leak output across listeners. + */ +export function matchesHermesRun(runId, eventRunId) { + return runId != null && eventRunId === runId +} diff --git a/src/engines/hermes/pages/group-chat.js b/src/engines/hermes/pages/group-chat.js index d24c70d..610f9be 100644 --- a/src/engines/hermes/pages/group-chat.js +++ b/src/engines/hermes/pages/group-chat.js @@ -13,6 +13,7 @@ */ import { t } from '../../../lib/i18n.js' import { api, isTauriRuntime, safeTauriListen } from '../../../lib/tauri-api.js' +import { matchesHermesRun } from '../lib/hermes-run-events.js' import { svgIcon } from '../lib/svg-icons.js' /** @@ -58,27 +59,27 @@ async function runHermesAgentAndWaitFinal(input) { cleanup() reject(err) } - const matchesRun = (rid) => !runId || !rid || rid === runId ;(async () => { try { unsubs.push(await safeTauriListen('hermes-run-started', (e) => { - if (!runId && e?.payload?.run_id) runId = e.payload.run_id + const rid = e?.payload?.run_id + if (!runId && rid) runId = rid })) unsubs.push(await safeTauriListen('hermes-run-delta', (e) => { - if (!matchesRun(e?.payload?.run_id)) return + if (!matchesHermesRun(runId, e?.payload?.run_id)) return accumulated += e?.payload?.delta || '' })) unsubs.push(await safeTauriListen('hermes-run-done', (e) => { - if (!matchesRun(e?.payload?.run_id)) return + if (!matchesHermesRun(runId, e?.payload?.run_id)) return const out = (e?.payload?.output || accumulated || '').trim() finish(out) })) unsubs.push(await safeTauriListen('hermes-run-error', (e) => { - if (!matchesRun(e?.payload?.run_id)) return + if (!matchesHermesRun(runId, e?.payload?.run_id)) return fail(new Error(e?.payload?.error || 'unknown error')) })) unsubs.push(await safeTauriListen('hermes-run-cancelled', (e) => { - if (!matchesRun(e?.payload?.run_id)) return + if (!matchesHermesRun(runId, e?.payload?.run_id)) return finish(accumulated.trim() || '(cancelled)') })) @@ -89,7 +90,7 @@ async function runHermesAgentAndWaitFinal(input) { // 防御:如果 done 事件因为顺序问题尚未派发(理论上不会发生),等一拍兜底 setTimeout(() => { - if (!settled) finish(accumulated.trim()) + if (!settled && runId) finish(accumulated.trim()) }, 300) } catch (e) { fail(e) @@ -303,11 +304,13 @@ export function render() { // 每个 profile run 完后切到下一个。 // 这是个 trade-off — 真正的并发需要后端改造支持 per-call profile。 let activeProfile = null + let initialProfile = null try { // 记下当前 active profile 用于最后还原 const curResp = await api.hermesProfilesList().catch(() => null) const curArr = Array.isArray(curResp) ? curResp : (curResp?.profiles || []) - activeProfile = curResp?.active || curArr.find(p => p.active)?.name || 'default' + initialProfile = curResp?.active || curArr.find(p => p.active)?.name || 'default' + activeProfile = initialProfile } catch {} for (let i = 0; i < targets.length; i++) { @@ -333,6 +336,11 @@ export function render() { } // 还原 active profile(如果改了)— 静默尝试 + if (initialProfile && activeProfile !== initialProfile) { + try { + await api.hermesProfileUse(initialProfile) + } catch {} + } sending = false draw() } diff --git a/src/engines/xintian/index.js b/src/engines/xintian/index.js index c390a0f..f842968 100644 --- a/src/engines/xintian/index.js +++ b/src/engines/xintian/index.js @@ -2,7 +2,7 @@ * 心甜Claw 引擎(产品宣传入口) * ------------------------------------------------------------------ * 这不是一个本地引擎,而是「心甜Claw」产品的一个产品落地页入口: - * - 桌面客户端 + SaaS 后端,Windows 安装即用 + * - 桌面客户端 + SaaS 后端,Windows / macOS / Linux 安装即用 * - ClawPanel 里只承载宣传 + 跳转下载 * * 因此它的 detect/boot/cleanup 都是 no-op,永远 ready, @@ -19,7 +19,7 @@ let _listeners = [] export default { id: 'xintian', name: '心甜Claw', - description: 'Xintian Claw · Worry-free AI Companion for Windows', + description: 'Xintian Claw · Worry-free AI Companion for desktop', icon: XINTIAN_ICON, async detect() { diff --git a/src/engines/xintian/pages/landing.js b/src/engines/xintian/pages/landing.js index 4f6a5ea..cae5478 100644 --- a/src/engines/xintian/pages/landing.js +++ b/src/engines/xintian/pages/landing.js @@ -1,7 +1,7 @@ /** * 心甜Claw · 产品落地页 * ------------------------------------------------------------------ - * 面向 Windows 桌面客户端的产品宣传 + 下载引导页。 + * 面向 Windows / macOS / Linux 桌面客户端的产品宣传 + 下载引导页。 * 所有可见文本走 i18n(engine.xt*),对外链接统一经过 openExternal() * 在 Tauri 桌面端走 @tauri-apps/plugin-shell,Web 端回退到 window.open。 */ @@ -27,6 +27,10 @@ const ICON = { channels: ``, shield: ``, windows: ``, + monitor: ``, + token: ``, + layers: ``, + model: ``, download: ``, external: ``, check: ``, @@ -55,9 +59,9 @@ function getFeatures() { { icon: ICON.brain, title: t('engine.xtFeatMemoryTitle'), desc: t('engine.xtFeatMemoryDesc') }, { icon: ICON.book, title: t('engine.xtFeatRagTitle'), desc: t('engine.xtFeatRagDesc') }, { icon: ICON.clock, title: t('engine.xtFeatCronTitle'), desc: t('engine.xtFeatCronDesc') }, - { icon: ICON.skills, title: t('engine.xtFeatSkillsTitle'), desc: t('engine.xtFeatSkillsDesc') }, + { icon: ICON.model, title: t('engine.xtFeatSkillsTitle'), desc: t('engine.xtFeatSkillsDesc') }, { icon: ICON.channels, title: t('engine.xtFeatChannelTitle'), desc: t('engine.xtFeatChannelDesc') }, - { icon: ICON.shield, title: t('engine.xtFeatOfflineTitle'), desc: t('engine.xtFeatOfflineDesc') }, + { icon: ICON.token, title: t('engine.xtFeatOfflineTitle'), desc: t('engine.xtFeatOfflineDesc') }, ] } @@ -89,6 +93,15 @@ function getCompareCards() { ] } +function getHeroProofs() { + return [ + { icon: ICON.monitor, title: t('engine.xtProofPlatformTitle'), desc: t('engine.xtProofPlatformDesc') }, + { icon: ICON.token, title: t('engine.xtProofTokenTitle'), desc: t('engine.xtProofTokenDesc') }, + { icon: ICON.layers, title: t('engine.xtProofPlanTitle'), desc: t('engine.xtProofPlanDesc') }, + { icon: ICON.model, title: t('engine.xtProofModelTitle'), desc: t('engine.xtProofModelDesc') }, + ] +} + /** 亮点清单(CTA 区下方) */ function getChecklist() { return [ @@ -135,6 +148,17 @@ export async function render() { const bullets = getChecklist() .map(b => `
  • ${ICON.check}${esc(b)}
  • `).join('') + const heroProofs = getHeroProofs() + .map(p => ` +
    + ${p.icon} + + ${esc(p.title)} + ${esc(p.desc)} + +
    + `).join('') + root.innerHTML = `
    @@ -158,7 +182,7 @@ export async function render() {

    ${esc(t('engine.xtHeroSub'))}

    +
    ${heroProofs}
    diff --git a/src/engines/xintian/style/xintian.css b/src/engines/xintian/style/xintian.css index 8ede42e..1bee38f 100644 --- a/src/engines/xintian/style/xintian.css +++ b/src/engines/xintian/style/xintian.css @@ -271,6 +271,74 @@ } [data-engine="xintian"] .xt-hero-meta-sep { opacity: 0.5; } +[data-engine="xintian"] .xt-hero-proof { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; + margin: 26px auto 0; + max-width: 760px; +} + +[data-engine="xintian"] .xt-proof { + display: flex; + align-items: flex-start; + gap: 10px; + min-width: 0; + padding: 12px 13px; + border: 1px solid var(--xt-border); + border-radius: 14px; + background: color-mix(in srgb, var(--xt-surface-1) 82%, transparent); + box-shadow: 0 10px 24px -18px rgba(27, 20, 16, 0.18); + text-align: left; +} + +[data-engine="xintian"] .xt-proof-ico { + width: 24px; + height: 24px; + display: inline-grid; + place-items: center; + flex: 0 0 24px; + border-radius: 8px; + background: var(--xt-accent-soft); + color: var(--xt-accent); +} + +[data-engine="xintian"] .xt-proof-ico svg { + width: 14px; + height: 14px; +} + +[data-engine="xintian"] .xt-proof-copy { + min-width: 0; + display: grid; + gap: 3px; +} + +[data-engine="xintian"] .xt-proof-copy strong { + color: var(--xt-ink-primary); + font-size: 12px; + line-height: 1.2; +} + +[data-engine="xintian"] .xt-proof-copy span { + color: var(--xt-ink-tertiary); + font-size: 11px; + line-height: 1.35; +} + +@media (max-width: 980px) { + [data-engine="xintian"] .xt-hero-proof { + grid-template-columns: repeat(2, minmax(0, 1fr)); + max-width: 520px; + } +} + +@media (max-width: 520px) { + [data-engine="xintian"] .xt-hero-proof { + grid-template-columns: 1fr; + } +} + /* ========================================================================== * 4 · Buttons * ========================================================================== */ diff --git a/src/lib/kernel-upgrade.js b/src/lib/kernel-upgrade.js index d84bd19..8ebf6c9 100644 --- a/src/lib/kernel-upgrade.js +++ b/src/lib/kernel-upgrade.js @@ -11,6 +11,28 @@ import { toast } from '../components/toast.js' import { t } from './i18n.js' import { getKernelSnapshot } from './kernel.js' +async function resolveKernelUpgradePolicy(snap) { + try { + const info = await api.getVersionInfo() + const source = String(info?.source || '').toLowerCase() + const cliSource = String(info?.cli_source || '').toLowerCase() + const variant = source === 'chinese' || cliSource === 'standalone' || cliSource === 'npm-zh' + ? 'chinese' + : source === 'official' || cliSource === 'npm-official' || cliSource === 'npm-global' + ? 'official' + : (snap.variant === 'chinese' ? 'chinese' : 'official') + return { + variant, + targetVersion: info?.recommended || snap.target || '', + } + } catch { + return { + variant: snap.variant === 'chinese' ? 'chinese' : 'official', + targetVersion: snap.target || '', + } + } +} + /** * 触发一键升级。会自动检测当前内核 variant(官方/汉化)选择源,调推荐版本。 * @@ -26,8 +48,7 @@ export async function triggerKernelUpgrade(opts = {}) { return false } - const variant = snap.variant === 'chinese' ? 'chinese' : 'official' - const targetVersion = snap.target || '' + const { variant, targetVersion } = await resolveKernelUpgradePolicy(snap) // 1. 确认对话框 if (!opts.skipConfirm) { diff --git a/src/lib/model-presets.js b/src/lib/model-presets.js index 5a55037..03a0541 100644 --- a/src/lib/model-presets.js +++ b/src/lib/model-presets.js @@ -23,7 +23,7 @@ export const PROVIDER_PRESETS = [ { key: 'volcengine', label: '火山引擎', baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', api: 'openai-completions', site: 'https://volcengine.com/L/Ph1OP5I3_GY', desc: '字节跳动旗下云平台,支持豆包等模型' }, { key: 'aliyun', label: '阿里云百炼', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', api: 'openai-completions', site: 'https://www.aliyun.com/benefit/ai/aistar?userCode=keahn2zr&clubBiz=subTask..12435175..10263..', desc: '阿里云 AI 大模型平台,支持通义千问全系列' }, { key: 'zhipu', label: '智谱 AI', baseUrl: 'https://open.bigmodel.cn/api/paas/v4', api: 'openai-completions', site: 'https://www.bigmodel.cn/glm-coding?ic=3F6F9XYKTS', desc: '国产大模型领军企业,支持 GLM-4 全系列' }, - { key: 'minimax', label: 'MiniMax', baseUrl: 'https://api.minimax.io/v1', api: 'openai-completions', site: 'https://platform.minimaxi.com/user-center/basic-information/interface-key', desc: '国产多模态大模型,支持 MiniMax-M2.7 / M2.5 系列' }, + { key: 'minimax', label: 'MiniMax', baseUrl: 'https://api.minimax.io/v1', api: 'openai-completions', site: 'https://platform.minimaxi.com/user-center/basic-information/interface-key', desc: '国产多模态大模型,支持 MiniMax-M3 / M2.7 系列' }, { key: 'moonshot', label: 'Moonshot / Kimi', baseUrl: 'https://api.moonshot.ai/v1', api: 'openai-completions', site: 'https://platform.moonshot.ai/console/api-keys', desc: 'Kimi 大模型平台,支持超长上下文' }, { key: 'openai', label: 'OpenAI 官方', baseUrl: 'https://api.openai.com/v1', api: 'openai-completions', site: 'https://platform.openai.com/api-keys' }, { key: 'anthropic', label: 'Anthropic 官方', baseUrl: 'https://api.anthropic.com/v1', api: 'anthropic-messages', site: 'https://console.anthropic.com/settings/keys' }, @@ -78,10 +78,9 @@ export const MODEL_PRESETS = { { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', contextWindow: 1000000 }, ], minimax: [ + { id: 'MiniMax-M3', name: 'MiniMax M3', contextWindow: 524288 }, { id: 'MiniMax-M2.7', name: 'MiniMax M2.7', contextWindow: 1000000 }, { id: 'MiniMax-M2.7-highspeed', name: 'MiniMax M2.7 Highspeed', contextWindow: 1000000 }, - { id: 'MiniMax-M2.5', name: 'MiniMax M2.5', contextWindow: 204000 }, - { id: 'MiniMax-M2.5-highspeed', name: 'MiniMax M2.5 Highspeed', contextWindow: 204000 }, ], moonshot: [ { id: 'kimi-k2.5', name: 'Kimi K2.5', contextWindow: 131072 }, diff --git a/src/lib/tauri-api.js b/src/lib/tauri-api.js index c50c886..7a1c3cc 100644 --- a/src/lib/tauri-api.js +++ b/src/lib/tauri-api.js @@ -255,6 +255,7 @@ export async function checkBackendHealth() { let _reloadTimer = null function _debouncedReloadGateway() { clearTimeout(_reloadTimer) + if (!isTauriRuntime()) return _reloadTimer = setTimeout(() => { invoke('reload_gateway').catch(() => {}) }, 3000) } diff --git a/src/lib/ws-client.js b/src/lib/ws-client.js index f7559dc..e51c7a0 100644 --- a/src/lib/ws-client.js +++ b/src/lib/ws-client.js @@ -9,7 +9,7 @@ * 5. 从 snapshot.sessionDefaults.mainSessionKey 获取 sessionKey * 6. 开始正常通信 */ -import { api } from './tauri-api.js' +import { api, isTauriRuntime } from './tauri-api.js' import { t } from './i18n.js' import { KERNEL_TARGET } from './feature-catalog.js' @@ -590,12 +590,16 @@ export class WsClient { const result = await api.autoPairDevice() console.log('[ws] 配对结果:', result) - // 配对后需要 reload Gateway 使 allowedOrigins 生效 - try { - await api.reloadGateway() - console.log('[ws] Gateway 已重载') - } catch (e) { - console.warn('[ws] reloadGateway 失败(非致命):', e) + // 配对后桌面端需要 reload Gateway 使 allowedOrigins 生效;Web/headless 不能隐式重载反代后的服务。 + if (isTauriRuntime()) { + try { + await api.reloadGateway() + console.log('[ws] Gateway 已重载') + } catch (e) { + console.warn('[ws] reloadGateway 失败(非致命):', e) + } + } else { + console.log('[ws] Web/headless 模式跳过自动 reload Gateway') } // 修复 #160: 不调用 reconnect()(它会重置 _autoPairAttempts 导致无限循环), diff --git a/src/locales/de.json b/src/locales/de.json index 130b48a..30dc1a5 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -2132,8 +2132,8 @@ "failurePrefix": "Kernel-Update fehlgeschlagen:" }, "upgradeHint": { - "title": "Neuer Kernel verfügbar", - "subtitle": "{from} → {to}, hier klicken zum Aktualisieren", + "title": "Running Gateway can be upgraded", + "subtitle": "Gateway {from} → recommended {to}", "dismissTooltip": "In dieser Sitzung nicht erneut anzeigen" } }, diff --git a/src/locales/en.json b/src/locales/en.json index 0985378..cccbf73 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2275,8 +2275,8 @@ "failurePrefix": "Kernel upgrade failed:" }, "upgradeHint": { - "title": "New kernel available", - "subtitle": "{from} → {to}, click to upgrade", + "title": "Running Gateway can be upgraded", + "subtitle": "Gateway {from} → recommended {to}", "dismissTooltip": "Don't remind me this session" }, "tooOldForProtocol": "Gateway kernel is too old and does not support the handshake protocol used by this ClawPanel. Please upgrade the OpenClaw kernel to the recommended version ({recommended}) and retry. You can upgrade from \"Services → OpenClaw → Upgrade\"." diff --git a/src/locales/es.json b/src/locales/es.json index 4d71f63..3c0ef7b 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -2132,8 +2132,8 @@ "failurePrefix": "Falló la actualización del kernel:" }, "upgradeHint": { - "title": "Nuevo kernel disponible", - "subtitle": "{from} → {to}, haz clic para actualizar", + "title": "Running Gateway can be upgraded", + "subtitle": "Gateway {from} → recommended {to}", "dismissTooltip": "No avisar en esta sesión" } }, diff --git a/src/locales/fr.json b/src/locales/fr.json index 41ae7b8..e7c9a48 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -2132,8 +2132,8 @@ "failurePrefix": "Échec de la mise à jour du noyau :" }, "upgradeHint": { - "title": "Nouveau noyau disponible", - "subtitle": "{from} → {to}, cliquez pour mettre à jour", + "title": "Running Gateway can be upgraded", + "subtitle": "Gateway {from} → recommended {to}", "dismissTooltip": "Ne plus avertir pendant cette session" } }, diff --git a/src/locales/ja.json b/src/locales/ja.json index 8b6c868..d15cabc 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -2132,8 +2132,8 @@ "failurePrefix": "カーネルのアップグレードに失敗:" }, "upgradeHint": { - "title": "新しいカーネルが利用可能", - "subtitle": "{from} → {to}、クリックでアップグレード", + "title": "実行中の Gateway をアップグレードできます", + "subtitle": "Gateway {from} → 推奨 {to}", "dismissTooltip": "このセッションでは再表示しない" } }, diff --git a/src/locales/ko.json b/src/locales/ko.json index 131de8f..17bfbd6 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -2132,8 +2132,8 @@ "failurePrefix": "커널 업그레이드 실패:" }, "upgradeHint": { - "title": "새 커널 사용 가능", - "subtitle": "{from} → {to}, 클릭하여 업그레이드", + "title": "실행 중인 Gateway 업그레이드 가능", + "subtitle": "Gateway {from} → 권장 {to}", "dismissTooltip": "이 세션에서는 알리지 않음" } }, diff --git a/src/locales/modules/engine.js b/src/locales/modules/engine.js index 415356b..3057fe5 100644 --- a/src/locales/modules/engine.js +++ b/src/locales/modules/engine.js @@ -1865,33 +1865,41 @@ export default { // Hero xtHeroEyebrow: _('心甜Claw · 跨平台 AI 省心助手', 'Xintian Claw · Worry-free AI Companion', '心甜Claw · 跨平台 AI 省心助手', '心甜Claw · 手間いらずの AI コンパニオン', '心甜Claw · 근심 없는 AI 동반자'), - xtHeroTitleLead: _('WINDOWS 安装即用', 'READY FOR WINDOWS', 'WINDOWS 安裝即用', 'WINDOWS 用すぐに使える', 'WINDOWS에서 바로 사용'), + xtHeroTitleLead: _('全平台下载安装到即用', 'DESKTOP APPS READY', '全平台下載安裝即用', 'デスクトップ版ですぐに使える', '데스크톱 앱 바로 사용'), xtHeroTitleA: _('不只是对话,是会', 'Not just chat —', '不只是對話,是會'), xtHeroTitleB: _('记得你', 'an AI that remembers you', '記得你'), xtHeroTitleC: _('的 AI 管家', '.', '的 AI 管家'), xtHeroSub: _( - '桌面客户端 + SaaS 后端 + 长期记忆 + 多渠道,一次安装,让 AI 真正长期为你干活。', - 'Desktop client, SaaS backend, persistent memory, and multi-channel delivery — install once and let AI keep working for you.', - '桌面客戶端 + SaaS 後端 + 長期記憶 + 多頻道,一次安裝,讓 AI 真正長期為你幹活。', + 'Windows / macOS / Linux 桌面客户端 + SaaS 后端 + 长期记忆 + 多渠道,每天赠送免费 Token,也支持接入你的自定义模型。', + 'Windows, macOS and Linux desktop apps with a SaaS backend, persistent memory, multi-channel delivery, daily free tokens, and custom model support.', + 'Windows / macOS / Linux 桌面客戶端 + SaaS 後端 + 長期記憶 + 多頻道,每天贈送免費 Token,也支援接入你的自訂模型。', ), - xtCtaDownloadWin: _('下载 Windows 版', 'Download for Windows', '下載 Windows 版', 'Windows 版をダウンロード', 'Windows 버전 다운로드'), + xtCtaDownloadWin: _('打开下载中心', 'Open download center', '打開下載中心', 'ダウンロードセンターを開く', '다운로드 센터 열기'), xtCtaVisitSite: _('访问官网', 'Visit website', '訪問官網', '公式サイトへ', '공식 웹사이트'), xtHeroPlatformWin: _('Windows 10 / 11 · x64', 'Windows 10 / 11 · x64', 'Windows 10 / 11 · x64'), - xtHeroPlatformRest: _('macOS / Linux 即将上线', 'macOS / Linux coming soon', 'macOS / Linux 即將上線', 'macOS / Linux 近日公開', 'macOS / Linux 곧 출시'), - xtHeroFreeTrial: _('预置 2 个免费 Agent', '2 free agents included', '預置 2 個免費 Agent', '2 つの無料エージェント付き', '무료 에이전트 2개 포함'), + xtHeroPlatformRest: _('macOS / Linux 已支持', 'macOS / Linux supported', 'macOS / Linux 已支援', 'macOS / Linux 対応済み', 'macOS / Linux 지원'), + xtHeroFreeTrial: _('每日免费 Token · 可接自定义模型', 'Daily free tokens · custom models', '每日免費 Token · 可接自訂模型', '毎日無料 Token · カスタムモデル対応', '매일 무료 토큰 · 사용자 모델 지원'), + xtProofPlatformTitle: _('三端桌面版', 'Three desktop platforms', '三端桌面版'), + xtProofPlatformDesc: _('Win / Mac / Linux', 'Windows / macOS / Linux', 'Win / Mac / Linux'), + xtProofTokenTitle: _('每日免费 Token', 'Daily free tokens', '每日免費 Token'), + xtProofTokenDesc: _('免费会员每日 300 万', '3M/day on free plan', '免費會員每日 300 萬'), + xtProofPlanTitle: _('套餐可扩容', 'Plans for more usage', '套餐可擴容'), + xtProofPlanDesc: _('1600 万 / 4000 万月卡', '16M / 40M monthly tiers', '1600 萬 / 4000 萬月卡'), + xtProofModelTitle: _('自定义模型', 'Custom models', '自訂模型'), + xtProofModelDesc: _('Base URL / API Key', 'Base URL / API Key', 'Base URL / API Key'), // Features 区域 xtFeaturesEyebrow: _('核心能力', 'CORE CAPABILITIES', '核心能力'), xtFeaturesTitle: _('八种能力,一个助手', 'Eight capabilities, one companion', '八種能力,一個助手'), xtFeaturesSub: _( - '从聊天、记忆到定时自动化、多渠道通知——把 AI 做到生产可用。', - 'From chat and memory to scheduled automation and multi-channel delivery — AI ready for real work.', - '從聊天、記憶到定時自動化、多頻道通知——把 AI 做到生產可用。', + '从聊天、记忆、定时自动化到模型接入、用量套餐和多渠道通知——把 AI 做到生产可用。', + 'From chat, memory and automation to model access, usage plans and multi-channel delivery — AI ready for real work.', + '從聊天、記憶、定時自動化到模型接入、用量套餐和多頻道通知——把 AI 做到生產可用。', ), // 8 个特性卡片 xtFeatChatTitle: _('流式对话 × 思维链', 'Streaming chat × CoT', '串流對話 × 思維鏈'), - xtFeatChatDesc: _('工具调用与思考过程全程可见,Markdown / 代码 / 表格原生渲染。', 'Full visibility into tool calls and reasoning, with native Markdown / code / table rendering.', '工具調用與思考過程全程可見,Markdown / 程式碼 / 表格原生渲染。'), + xtFeatChatDesc: _('工具调用与思考过程全程可见,Markdown / 代码 / 表格原生渲染,平台模型开箱即用。', 'Full visibility into tool calls and reasoning, native Markdown/code/table rendering, and hosted models ready out of the box.', '工具調用與思考過程全程可見,Markdown / 程式碼 / 表格原生渲染,平台模型開箱即用。'), xtFeatAgentTitle: _('多智能体 Agent 体系', 'Multi-agent roster', '多智能體 Agent 體系'), xtFeatAgentDesc: _('预置心甜 + 晴辰两个助手,独立人设与记忆,可随时自定义。', 'Bundled Xintian & Qingchen assistants, each with its own persona and memory — fully customizable.', '預置心甜 + 晴辰兩個助手,獨立人設與記憶,可隨時自定義。'), xtFeatMemoryTitle: _('心甜智脑 · 长期记忆', 'Sweet Brain · Long-term memory', '心甜智腦 · 長期記憶'), @@ -1900,12 +1908,12 @@ export default { xtFeatRagDesc: _('拖拽上传 PDF / Word / Markdown,回答自动附带引用与跳转链接。', 'Drag-and-drop PDF / Word / Markdown — answers come with citations and jump links.', '拖放上傳 PDF / Word / Markdown,回答自動附帶引用與跳轉連結。'), xtFeatCronTitle: _('定时任务 × 后台任务', 'Scheduled & background tasks', '定時任務 × 背景任務'), xtFeatCronDesc: _('到点自动跑,长调研一轮一轮来,进度条可暂停可恢复。', 'Cron-triggered runs and multi-round background research with pause/resume progress.', '到點自動跑,長調研一輪一輪來,進度條可暫停可恢復。'), - xtFeatSkillsTitle: _('技能中心 · SkillForge', 'Skill Hub · SkillForge', '技能中心 · SkillForge'), - xtFeatSkillsDesc: _('把常用流程打包成技能 @ 调用,内置抓取 / 日报 / 总结。', 'Package prompts into reusable skills — invoke with @, with built-in scraping, reporting, summarization.', '把常用流程打包成技能 @ 調用,內建抓取 / 日報 / 總結。'), + xtFeatSkillsTitle: _('平台模型 × 自定义模型', 'Hosted × custom models', '平台模型 × 自訂模型'), + xtFeatSkillsDesc: _('可直接使用平台 AI 模型,也支持填写 Base URL / API Key 接入自己的模型服务。', 'Use hosted AI models out of the box, or connect your own model service with Base URL / API Key.', '可直接使用平台 AI 模型,也支援填寫 Base URL / API Key 接入自己的模型服務。'), xtFeatChannelTitle: _('多消息渠道', 'Multi-channel delivery', '多訊息頻道'), xtFeatChannelDesc: _('飞书 / 微信 / Telegram 等消息渠道互通,一套记忆跟你到每个对话窗。', 'Feishu / WeChat / Telegram all connected — one memory follows you to every conversation.', '飛書 / 微信 / Telegram 等訊息頻道互通,一套記憶跟你到每個對話窗。'), - xtFeatOfflineTitle: _('离线 × 本地优先', 'Offline × local-first', '離線 × 本地優先'), - xtFeatOfflineDesc: _('核心数据存本地 ~/.xintian-claw,断网队列补发,多后端容灾。', 'Core data stored locally at ~/.xintian-claw, offline queue + multi-backend failover.', '核心資料存本地 ~/.xintian-claw,斷網佇列補發,多後端容災。'), + xtFeatOfflineTitle: _('免费 Token × 灵活套餐', 'Free tokens × flexible plans', '免費 Token × 彈性套餐'), + xtFeatOfflineDesc: _('免费会员每日 300 万 Token,基础 / 高级月卡可扩至每日 1600 万 / 4000 万,并解锁更多 Agent 与工具额度。', 'Free members get 3M tokens per day; Basic and Pro tiers scale to 16M / 40M daily tokens and unlock more agents and tools.', '免費會員每日 300 萬 Token,基礎 / 高級月卡可擴至每日 1600 萬 / 4000 萬,並解鎖更多 Agent 與工具額度。'), // Compare 区域 xtCompareEyebrow: _('产品定位', 'POSITIONING', '產品定位'), @@ -1923,7 +1931,7 @@ export default { xtCompareBForWho: _('面向 Python 工程师', 'For Python engineers', '面向 Python 工程師'), xtComparePosC: _('所有普通用户', 'EVERYONE', '所有普通使用者'), xtCompareCTitle: _('心甜Claw', 'Xintian Claw', '心甜Claw'), - xtCompareCDesc: _('Windows 双击安装即可用,内置 Agent 与记忆,不写一行代码也能上手。', 'Double-click install on Windows — agents and memory out of the box, zero code required.', 'Windows 雙擊安裝即可用,內建 Agent 與記憶,不寫一行程式碼也能上手。'), + xtCompareCDesc: _('Windows / macOS / Linux 都有下载入口,内置 Agent、记忆和平台模型,不写一行代码也能上手。', 'Download on Windows, macOS or Linux — agents, memory and hosted models out of the box, zero code required.', 'Windows / macOS / Linux 都有下載入口,內建 Agent、記憶和平台模型,不寫一行程式碼也能上手。'), xtCompareCForWho: _('面向日常使用者', 'For everyday users', '面向日常使用者'), xtCompareRecommend: _('推荐', 'RECOMMENDED', '推薦'), @@ -1931,15 +1939,15 @@ export default { xtCtaEyebrow: _('立即开始', 'GET STARTED', '立即開始'), xtCtaTitle: _('今天装上 · 明天就离不开', 'Install today, depend on it tomorrow', '今天裝上 · 明天就離不開'), xtCtaSub: _( - '下载 Windows 安装包、双击运行,登录账号即可开始使用。无需配置 Python、无需命令行。', - 'Download the Windows installer, double-click, sign in — ready to chat. No Python, no terminal.', - '下載 Windows 安裝包、雙擊執行,登入帳號即可開始使用。無需配置 Python、無需命令列。', + '进入下载中心,按系统选择 Windows、macOS 或 Linux 安装包。登录账号即可使用每日免费 Token,也可以接入自己的模型服务。', + 'Open the download center, choose the Windows, macOS or Linux installer, sign in for daily free tokens, or connect your own model service.', + '進入下載中心,按系統選擇 Windows、macOS 或 Linux 安裝包。登入帳號即可使用每日免費 Token,也可以接入自己的模型服務。', ), - xtBulletInstall: _('一次安装 · 自动更新', 'One-click install · auto update', '一次安裝 · 自動更新'), - xtBulletLogin: _('微信 / 邮箱登录', 'WeChat / Email sign-in', '微信 / 信箱登入'), - xtBulletSync: _('多设备记忆同步', 'Multi-device memory sync', '多裝置記憶同步'), - xtBulletSafe: _('核心数据本地加密', 'Core data encrypted locally', '核心資料本地加密'), - xtCtaPrimary: _('立即下载 Windows 版', 'Download for Windows', '立即下載 Windows 版'), + xtBulletInstall: _('Windows / macOS / Linux 桌面端', 'Windows / macOS / Linux desktop apps', 'Windows / macOS / Linux 桌面端'), + xtBulletLogin: _('每日免费 Token 起步可用', 'Daily free tokens included', '每日免費 Token 起步可用'), + xtBulletSync: _('基础 / 高级套餐按需扩容', 'Basic / Pro plans scale as needed', '基礎 / 高級套餐按需擴容'), + xtBulletSafe: _('支持自定义 Base URL / API Key / 模型', 'Custom Base URL / API Key / models supported', '支援自訂 Base URL / API Key / 模型'), + xtCtaPrimary: _('打开下载中心', 'Open download center', '打開下載中心'), xtCtaSecondary: _('了解更多', 'Learn more', '了解更多'), xtCtaLinkLabel: _('官网', 'WEBSITE', '官網'), diff --git a/src/locales/modules/kernel.js b/src/locales/modules/kernel.js index 4dfa019..2e958b5 100644 --- a/src/locales/modules/kernel.js +++ b/src/locales/modules/kernel.js @@ -25,8 +25,8 @@ export default { failurePrefix: _("内核升级失败:", "Kernel upgrade failed:", "核心升級失敗:", "カーネルのアップグレードに失敗:", "커널 업그레이드 실패:", "Nâng cấp kernel thất bại:", "Falló la actualización del kernel:", "Falha na atualização do kernel:", "Ошибка обновления ядра:", "Échec de la mise à jour du noyau :", "Kernel-Update fehlgeschlagen:"), }, upgradeHint: { - title: _("有新内核可用", "New kernel available", "有新核心可用", "新しいカーネルが利用可能", "새 커널 사용 가능", "Có kernel mới", "Nuevo kernel disponible", "Novo kernel disponível", "Доступно новое ядро", "Nouveau noyau disponible", "Neuer Kernel verfügbar"), - subtitle: _("{from} → {to},点此一键升级", "{from} → {to}, click to upgrade", "{from} → {to},點此一鍵升級", "{from} → {to}、クリックでアップグレード", "{from} → {to}, 클릭하여 업그레이드", "{from} → {to}, nhấn để nâng cấp", "{from} → {to}, haz clic para actualizar", "{from} → {to}, clique para atualizar", "{from} → {to}, нажмите для обновления", "{from} → {to}, cliquez pour mettre à jour", "{from} → {to}, hier klicken zum Aktualisieren"), + title: _("运行中 Gateway 可升级", "Running Gateway can be upgraded", "執行中的 Gateway 可升級", "実行中の Gateway をアップグレードできます", "실행 중인 Gateway 업그레이드 가능", "Running Gateway can be upgraded", "Running Gateway can be upgraded", "Running Gateway can be upgraded", "Running Gateway can be upgraded", "Running Gateway can be upgraded", "Running Gateway can be upgraded"), + subtitle: _("Gateway {from} → 推荐 {to}", "Gateway {from} → recommended {to}", "Gateway {from} → 推薦 {to}", "Gateway {from} → 推奨 {to}", "Gateway {from} → 권장 {to}", "Gateway {from} → recommended {to}", "Gateway {from} → recommended {to}", "Gateway {from} → recommended {to}", "Gateway {from} → recommended {to}", "Gateway {from} → recommended {to}", "Gateway {from} → recommended {to}"), dismissTooltip: _("本次会话不再提醒", "Don't remind me this session", "本次工作階段不再提醒", "このセッションでは再表示しない", "이 세션에서는 알리지 않음", "Không nhắc trong phiên này", "No avisar en esta sesión", "Não avisar nesta sessão", "Не напоминать в этой сессии", "Ne plus avertir pendant cette session", "In dieser Sitzung nicht erneut anzeigen"), }, tooOldForProtocol: _("Gateway 内核版本过旧,不兼容当前 ClawPanel 使用的握手协议。请把 OpenClaw 内核升级到推荐版本({recommended})后重试。可在「服务管理 → OpenClaw → 一键升级」中完成升级。", "Gateway kernel is too old and does not support the handshake protocol used by this ClawPanel. Please upgrade the OpenClaw kernel to the recommended version ({recommended}) and retry. You can upgrade from \"Services → OpenClaw → Upgrade\".", "Gateway 核心版本過舊,不相容目前 ClawPanel 使用的握手協議。請將 OpenClaw 核心升級到推薦版本({recommended})後重試。可在「服務管理 → OpenClaw → 一鍵升級」中完成升級。", "Gateway kernel is too old and does not support the handshake protocol used by this ClawPanel. Please upgrade the OpenClaw kernel to the recommended version ({recommended}) and retry. You can upgrade from \"Services → OpenClaw → Upgrade\".", "Gateway kernel is too old and does not support the handshake protocol used by this ClawPanel. Please upgrade the OpenClaw kernel to the recommended version ({recommended}) and retry. You can upgrade from \"Services → OpenClaw → Upgrade\".", "Gateway kernel is too old and does not support the handshake protocol used by this ClawPanel. Please upgrade the OpenClaw kernel to the recommended version ({recommended}) and retry. You can upgrade from \"Services → OpenClaw → Upgrade\".", "Gateway kernel is too old and does not support the handshake protocol used by this ClawPanel. Please upgrade the OpenClaw kernel to the recommended version ({recommended}) and retry. You can upgrade from \"Services → OpenClaw → Upgrade\".", "Gateway kernel is too old and does not support the handshake protocol used by this ClawPanel. Please upgrade the OpenClaw kernel to the recommended version ({recommended}) and retry. You can upgrade from \"Services → OpenClaw → Upgrade\".", "Gateway kernel is too old and does not support the handshake protocol used by this ClawPanel. Please upgrade the OpenClaw kernel to the recommended version ({recommended}) and retry. You can upgrade from \"Services → OpenClaw → Upgrade\".", "Gateway kernel is too old and does not support the handshake protocol used by this ClawPanel. Please upgrade the OpenClaw kernel to the recommended version ({recommended}) and retry. You can upgrade from \"Services → OpenClaw → Upgrade\".", "Gateway kernel is too old and does not support the handshake protocol used by this ClawPanel. Please upgrade the OpenClaw kernel to the recommended version ({recommended}) and retry. You can upgrade from \"Services → OpenClaw → Upgrade\"."), diff --git a/src/locales/modules/settings.js b/src/locales/modules/settings.js index df62a26..e9f95ac 100644 --- a/src/locales/modules/settings.js +++ b/src/locales/modules/settings.js @@ -28,7 +28,7 @@ export default { languageHint: _('切换界面显示语言,部分内容可能仍为中文', 'Switch the interface language. Some content may remain in the original language.', '切換界面顯示語言,部分內容可能仍為中文', 'インターフェース言語を切り替えます。一部コンテンツは元の言語のまま表示される場合があります。', '인터페이스 언어를 전환합니다. 일부 콘텐츠는 원래 언어로 표시될 수 있습니다.', 'Chuyển đổi ngôn ngữ giao diện.', 'Cambiar el idioma de la interfaz.', 'Alterar o idioma da interface.', 'Переключить язык интерфейса.', 'Changer la langue de l\'interface.', 'Sprache der Benutzeroberfläche wechseln.'), testProxy: _('测试连通', 'Test Connection', '測試連通', '接続テスト', '연결 테스트', 'Kiểm tra kết nối', 'Probar conexión', 'Testar conexão', 'Проверить подключение', 'Tester la connexion', 'Verbindung testen'), clearProxy: _('关闭代理', 'Disable Proxy', '關閉代理', 'プロキシ無効化', '프록시 비활성화', 'Tắt proxy', 'Desactivar proxy', 'Desativar proxy', 'Отключить прокси', 'Désactiver le proxy', 'Proxy deaktivieren'), - proxyHint: _('设置后,npm 安装/升级、版本检测、GitHub/Gitee 更新检查、ClawHub Skills 等下载类操作会走此代理。自动绕过 localhost 和内网地址。保存后新请求立即生效;如 Gateway 正在运行,建议重启一次服务。', 'Once set, npm install/upgrade, version checks, GitHub/Gitee update checks, ClawHub Skills downloads will use this proxy. Localhost and LAN addresses are auto-bypassed. Takes effect immediately; if Gateway is running, consider restarting the service.', '設定后,npm 安裝/升級、版本檢測、GitHub/Gitee 更新檢查、ClawHub Skills 等下載類操作會走此代理。自動繞過 localhost 和內網位址。儲存后新請求立即生效;如 Gateway 正在執行,建議重啟一次服務。', '設定後、npm インストール/アップグレード、バージョンチェック、GitHub/Gitee 更新チェック、ClawHub Skills ダウンロードはこのプロキシを使用します。localhost と LAN アドレスは自動バイパスされます。すぐに有効になります。Gateway が実行中の場合はサービスの再起動を検討してください。'), + proxyHint: _('设置后,npm 安装/升级、官网版本与公告检查、ClawHub Skills 等下载类操作会走此代理。自动绕过 localhost 和内网地址。保存后新请求立即生效;如 Gateway 正在运行,建议重启一次服务。', 'Once set, npm install/upgrade, official site version and announcement checks, ClawHub Skills downloads will use this proxy. Localhost and LAN addresses are auto-bypassed. Takes effect immediately; if Gateway is running, consider restarting the service.', '設定後,npm 安裝/升級、官網版本與公告檢查、ClawHub Skills 等下載類操作會走此代理。自動繞過 localhost 和內網位址。儲存後新請求立即生效;如 Gateway 正在執行,建議重啟一次服務。', '設定後、npm インストール/アップグレード、公式サイトのバージョン・お知らせチェック、ClawHub Skills ダウンロードはこのプロキシを使用します。localhost と LAN アドレスは自動バイパスされます。すぐに有効になります。Gateway が実行中の場合はサービスの再起動を検討してください。'), modelProxyToggle: _('模型测试和模型列表请求也走代理', 'Route model test and model list requests through proxy', '模型測試和模型列表請求也走代理', 'モデルテストとモデルリストリクエストもプロキシ経由'), modelProxyHint: _('默认关闭。部分用户的模型 API 地址本身就是国内中转或内网地址,走代理反而会连接失败。只有当你的模型服务商需要翻墙访问时才建议开启。', 'Off by default. Some model API endpoints are already domestic or LAN addresses — proxying them may cause connection failures. Only enable if your model provider requires a proxy.', '預設關閉。部分使用者的模型 API 位址本身就是國內中轉或內網位址,走代理反而會連線失敗。只有當你的模型服務商需要翻牆訪問時才建議開啟。', 'デフォルトオフ。一部のモデル API は国内中継または LAN アドレスのため、プロキシを使用すると接続に失敗する場合があります。モデルプロバイダーがプロキシを必要とする場合のみ有効にしてください。'), modelProxyNoProxy: _('请先在上方设置网络代理地址后,才能启用此选项。', 'Please set up a network proxy above before enabling this option.', '請先在上方設定網路代理位址后,才能啟用此選項。', 'まず上のネットワークプロキシアドレスを設定してから、このオプションを有効にしてください。'), diff --git a/src/locales/modules/siteMessages.js b/src/locales/modules/siteMessages.js index 572ccb3..ef50f44 100644 --- a/src/locales/modules/siteMessages.js +++ b/src/locales/modules/siteMessages.js @@ -14,12 +14,17 @@ export default { levelWarning: _('重要', 'Important'), levelSuccess: _('正常', 'Normal'), levelInfo: _('提示', 'Info'), - closeToday: _('今日关闭', 'Close today'), + closeToday: _('今日不再提醒', "Don't remind today"), closeCurrent: _('关闭公告', 'Close notices'), + dismissCurrentNotifications: _('忽略当前通知', 'Dismiss current notifications'), + dismissCurrentAnnouncements: _('忽略当前公告', 'Dismiss current announcements'), emptyNotifications: _('暂无通知', 'No notifications'), emptyNotificationsHint: _('重要通知会按时间显示在这里。', 'Important notices will appear here in chronological order.'), emptyAnnouncements: _('暂无系统公告', 'No announcements'), emptyAnnouncementsHint: _('官网固定公告会显示在这里。', 'Pinned official announcements will appear here.'), + dismissedTitle: _('本机已关闭这些公告', 'These notices are closed on this device'), + dismissedHint: _('官网返回了 {count} 条内容,但已被本机关闭。可重新显示用于调试或查看。', 'The official site returned {count} item(s), but they were closed on this device. Restore them to debug or review.'), + restoreDismissed: _('重新显示', 'Show again'), defaultTitle: _('ClawPanel 通知', 'ClawPanel notice'), timeUnknown: _('时间未知', 'Unknown time'), today: _('今天', 'Today'), diff --git a/src/locales/pt.json b/src/locales/pt.json index bc37c20..abbaa1c 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -2132,8 +2132,8 @@ "failurePrefix": "Falha na atualização do kernel:" }, "upgradeHint": { - "title": "Novo kernel disponível", - "subtitle": "{from} → {to}, clique para atualizar", + "title": "Running Gateway can be upgraded", + "subtitle": "Gateway {from} → recommended {to}", "dismissTooltip": "Não avisar nesta sessão" } }, diff --git a/src/locales/ru.json b/src/locales/ru.json index 077669b..8b87779 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -2132,8 +2132,8 @@ "failurePrefix": "Ошибка обновления ядра:" }, "upgradeHint": { - "title": "Доступно новое ядро", - "subtitle": "{from} → {to}, нажмите для обновления", + "title": "Running Gateway can be upgraded", + "subtitle": "Gateway {from} → recommended {to}", "dismissTooltip": "Не напоминать в этой сессии" } }, diff --git a/src/locales/vi.json b/src/locales/vi.json index 6a81aeb..5ff8823 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -2132,8 +2132,8 @@ "failurePrefix": "Nâng cấp kernel thất bại:" }, "upgradeHint": { - "title": "Có kernel mới", - "subtitle": "{from} → {to}, nhấn để nâng cấp", + "title": "Running Gateway can be upgraded", + "subtitle": "Gateway {from} → recommended {to}", "dismissTooltip": "Không nhắc trong phiên này" } }, diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index ad653fe..c0deab5 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -2358,8 +2358,8 @@ "failurePrefix": "内核升级失败:" }, "upgradeHint": { - "title": "有新内核可用", - "subtitle": "{from} → {to},点此一键升级", + "title": "运行中 Gateway 可升级", + "subtitle": "Gateway {from} → 推荐 {to}", "dismissTooltip": "本次会话不再提醒" }, "tooOldForProtocol": "Gateway 内核版本过旧,不兼容当前 ClawPanel 使用的握手协议。请把 OpenClaw 内核升级到推荐版本({recommended})后重试。可在「服务管理 → OpenClaw → 一键升级」中完成升级。" diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index 701f445..df1ab7d 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -2133,8 +2133,8 @@ "failurePrefix": "核心升級失敗:" }, "upgradeHint": { - "title": "有新核心可用", - "subtitle": "{from} → {to},點此一鍵升級", + "title": "執行中的 Gateway 可升級", + "subtitle": "Gateway {from} → 推薦 {to}", "dismissTooltip": "本次工作階段不再提醒" } }, diff --git a/src/main.js b/src/main.js index 7863b76..e0e022c 100644 --- a/src/main.js +++ b/src/main.js @@ -443,6 +443,7 @@ function _genCaptcha() { function renderLoginLanguageSwitch() { const langs = getAvailableLangs() const current = getLang() + const currentLang = langs.find(lang => lang.code === current) || langs[0] const options = langs.map(lang => `