mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-07 07:22:53 +08:00
feat: OpenClaw 4.9 全面适配 (v0.12.0)
- 推荐内核统一升级至 2026.4.9 / 2026.4.9-zh.2 - standalone 安装兼容 edition 格式 latest.json + openclaw-zh- 文件名前缀 - standalone 三级降级: R2 CDN → GitHub Releases → npm - pre_install_cleanup 所有命令加 10s 超时保护(修复安装卡死) - npm uninstall 加 30s 超时保护 - wmic 全部迁移到 PowerShell(兼容 Windows 11) - standalone 下载增加文字进度显示
This commit is contained in:
12
CHANGELOG.md
12
CHANGELOG.md
@@ -5,6 +5,18 @@
|
||||
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
||||
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||||
|
||||
## [0.12.0] - 2026-04-11
|
||||
|
||||
### 新功能 (Features)
|
||||
|
||||
- **OpenClaw 4.9 全面适配** — 完成上游 OpenClaw `2026.4.9` 全量兼容性审查(180+ 文件 diff),涵盖 Gateway RPC、Agent 配置 schema、渠道插件基础设施、Setup wizard、所有消息渠道(LINE/Matrix/Slack/Teams/Telegram 等),确认完全兼容
|
||||
|
||||
### 改进 (Improvements)
|
||||
|
||||
- **推荐内核统一升级** — `openclaw-version-policy.json` 全版本(0.9.0 ~ 0.12.0)推荐内核统一升级至 `2026.4.9`(官方版)/ `2026.4.9-zh.2`(汉化版),不再保留旧版 3.13 / 3.28 映射
|
||||
- **安装流程稳健性提升** — 旧版本清理(npm uninstall)增加 30 秒超时保护,清理失败不阻断安装成功报告;Windows 进程检测从废弃的 wmic 迁移到 PowerShell Get-Process / Get-CimInstance,兼容 Windows 11
|
||||
- **Standalone 安装包适配** — 兼容 CI 新版 edition 格式 `latest.json`(`editions.zh.version`),修正文件名前缀(`openclaw-zh-`)和 R2 下载路径(`/zh/{version}/`),同时保持旧 flat 格式向后兼容
|
||||
|
||||
## [0.11.6] - 2026-04-07
|
||||
|
||||
### 新功能 (Features)
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"description": "OpenClaw AI Agent 可视化管理面板,基于 Tauri v2 的跨平台桌面应用。内置晴辰助手支持工具调用,晴辰云 AI 接口一键接入。支持仪表盘监控、多模型配置、消息渠道管理、内置 QQ 机器人、实时 AI 聊天、记忆管理、Agent 管理、网关配置、内网穿透等功能。支持 11 种语言。",
|
||||
"url": "https://claw.qt.cool/",
|
||||
"downloadUrl": "https://github.com/qingchencloud/clawpanel/releases/latest",
|
||||
"softwareVersion": "0.11.6",
|
||||
"softwareVersion": "0.12.0",
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "晴辰云 QingchenCloud",
|
||||
@@ -1155,7 +1155,7 @@
|
||||
<div class="orb orb-2" style="top:auto;bottom:-100px"></div>
|
||||
<div class="container-sm" style="position:relative;z-index:10">
|
||||
<div class="section-header">
|
||||
<div class="reveal download-version"><span class="pulse"></span> <span id="dl-badge" data-i18n="dl.badge">v0.11.6 最新版</span></div>
|
||||
<div class="reveal download-version"><span class="pulse"></span> <span id="dl-badge" data-i18n="dl.badge">v0.12.0 最新版</span></div>
|
||||
<h2 class="reveal section-title" data-i18n="dl.title"><span class="gradient-text">下载安装</span></h2>
|
||||
<p class="reveal section-desc" data-i18n="dl.desc">选择你的操作系统,一键下载安装</p>
|
||||
</div>
|
||||
@@ -1165,11 +1165,11 @@
|
||||
<h3>macOS</h3>
|
||||
<p class="dl-desc" data-i18n="dl.mac.d">支持 Apple Silicon 和 Intel 芯片</p>
|
||||
<div class="dl-links">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.6_aarch64.dmg" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.12.0_aarch64.dmg" target="_blank" rel="noopener">
|
||||
Apple Silicon (M1/M2/M3/M4)
|
||||
<span class="dl-format">.dmg</span>
|
||||
</a>
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.6_x64.dmg" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.12.0_x64.dmg" target="_blank" rel="noopener">
|
||||
<span data-i18n="dl.mac.intel">Intel 芯片</span>
|
||||
<span class="dl-format">.dmg</span>
|
||||
</a>
|
||||
@@ -1187,15 +1187,15 @@
|
||||
<h3>Windows</h3>
|
||||
<p class="dl-desc" data-i18n="dl.win.d">支持 Windows 10 及以上版本</p>
|
||||
<div class="dl-links">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.6_x64-setup.exe" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.12.0_x64-setup.exe" target="_blank" rel="noopener">
|
||||
<span data-i18n="dl.win.exe">安装程序</span>
|
||||
<span class="dl-format">.exe</span>
|
||||
</a>
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.6_x64-setup-full.exe" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.12.0_x64-setup-full.exe" target="_blank" rel="noopener">
|
||||
<span data-i18n="dl.win.full">完整包(含 WebView2)</span>
|
||||
<span class="dl-format">.exe</span>
|
||||
</a>
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.6_x64_en-US.msi" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.12.0_x64_en-US.msi" target="_blank" rel="noopener">
|
||||
<span data-i18n="dl.win.msi">MSI 安装包</span>
|
||||
<span class="dl-format">.msi</span>
|
||||
</a>
|
||||
@@ -1206,11 +1206,11 @@
|
||||
<h3>Linux</h3>
|
||||
<p class="dl-desc" data-i18n="dl.linux.d">支持主流 Linux 发行版</p>
|
||||
<div class="dl-links">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.6_amd64.AppImage" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.12.0_amd64.AppImage" target="_blank" rel="noopener">
|
||||
<span data-i18n="dl.linux.ai">通用版</span>
|
||||
<span class="dl-format">.AppImage</span>
|
||||
</a>
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.11.6_amd64.deb" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.12.0_amd64.deb" target="_blank" rel="noopener">
|
||||
Debian / Ubuntu
|
||||
<span class="dl-format">.deb</span>
|
||||
</a>
|
||||
|
||||
114
docs/v2026.4.9-adaptation-plan.md
Normal file
114
docs/v2026.4.9-adaptation-plan.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# OpenClaw v2026.4.9 适配计划
|
||||
|
||||
> ClawPanel 当前绑定 `2026.3.28`,上游已达 `v2026.4.9`(5991 commits)
|
||||
> 本文档列出所有需要适配的功能点,按优先级排列
|
||||
|
||||
---
|
||||
|
||||
## ✅ 已完成
|
||||
|
||||
### 1. version-policy 绑定更新
|
||||
- `openclaw-version-policy.json` 已更新
|
||||
- 0.9.x → 3.13, 0.10.x-0.11.2 → 3.28, 0.11.3+ → 4.9
|
||||
- default fallback 也更新到 4.9
|
||||
- **向下兼容**:旧面板版本仍绑定旧内核
|
||||
|
||||
### 2. 梦境页(dreaming.js)
|
||||
- 已有完整 Dream Diary UI:日记查看器、分段解析、原始 Markdown
|
||||
- 已调用 4.9 新 RPC:`doctor.memory.dreamDiary`, `backfillDreamDiary`, `resetDreamDiary`, `resetGroundedShortTerm`
|
||||
- 已有三阶段状态:light / deep / rem
|
||||
- 已有向下兼容:`isUnsupportedError()` 检测,旧版本优雅降级
|
||||
|
||||
---
|
||||
|
||||
## ✅ P0 — 已完成
|
||||
|
||||
### 3. Skills 页增强:接入 Gateway `skills.search` + `skills.detail`
|
||||
|
||||
- `ws-client.js` 新增 `skillsSearch()` / `skillsDetail()` 方法
|
||||
- `skills.js` 商店搜索优先走 Gateway RPC,回退 Tauri
|
||||
- `skills.js` 详情查看优先走 Gateway RPC,回退 Tauri
|
||||
- Web 模式下也可搜索/查看技能详情
|
||||
|
||||
---
|
||||
|
||||
## ✅ P1 — 已完成
|
||||
|
||||
### 4. Sessions Compaction UI(会话压缩历史/恢复)
|
||||
|
||||
- `ws-client.js` 新增 4 个方法:`sessionsCompactionList/Get/Branch/Restore`
|
||||
- `chat.js` 会话卡片显示压缩检查点计数徽章(⇳N)
|
||||
- 点击弹出检查点列表弹窗(时间、token 变化)
|
||||
- 每个检查点提供“分支”和“恢复”操作,恢复需 confirm
|
||||
- 旧版本优雅降级:显示“不支持”提示
|
||||
- i18n 已增加 9 个新 key
|
||||
|
||||
### 5. exec/plugin Approval 管理增强
|
||||
|
||||
- `ws-client.js` 新增 3 个方法:`execApprovalList/Get`, `pluginApprovalList`
|
||||
- `communication.js` 审批 Tab 新增“待处理审批队列”面板
|
||||
- 主动拉取 exec + plugin 审批列表(并行请求)
|
||||
- Gateway 未连接 / RPC 不支持时优雅降级
|
||||
- i18n 已增加 6 个新 key
|
||||
|
||||
---
|
||||
|
||||
## ✅ P2 — 已完成
|
||||
|
||||
经审查,以下变更均不涉及 ClawPanel 现有 UI:
|
||||
|
||||
| 项 | 原因 |
|
||||
|------|------|
|
||||
| TTS `talk` 字段迁移 | ClawPanel 无 TTS 配置编辑器 |
|
||||
| MCP `headers` 新字段 | ClawPanel 无 MCP 服务器配置编辑器(仅 dashboard 显示计数) |
|
||||
| Provider fallback 新参数 | 内核侧有默认值,面板无需暴露 |
|
||||
| ACP bindings 校验放宽 | ClawPanel route-map 仅读取显示,无硬编码渠道限制 |
|
||||
| `operator.talk_secrets` 新权限 | 方法列表为空,暂无实际影响 |
|
||||
|
||||
---
|
||||
|
||||
## 📡 渠道模块审查(channels.js — 无需适配)
|
||||
|
||||
**审查范围**:全量 2198 行 `channels.js` + 上游 180 文件 channels diff
|
||||
|
||||
| 审查项 | 4.9 变更 | 面板影响 |
|
||||
|--------|----------|----------|
|
||||
| Gateway RPC `channels.ts` | `normalizeOptionalString` 内部重构 | 无 — API 无变化 |
|
||||
| Config schema `zod-schema.agents.ts` | ACP binding 校验放宽 | 无 — 面板不做校验 |
|
||||
| 渠道插件基础设施 | module-loader/registry/contracts 重构 | 无 — 内核内部 |
|
||||
| Setup wizard `setup-helpers.ts` | 账号提升逻辑重构 | 无 — 内部迁移 |
|
||||
| 新渠道类型 | 未新增 | 无 |
|
||||
| LINE/Matrix/Slack/Teams/Telegram | 内部增强(draft streaming/outbound media/status reactions/thread isolation/polling watchdog) | 无 — 内核自动生效 |
|
||||
|
||||
**结论**:`channels.js` 的 18 个 `api.*` 调用均为面板自有后端 API,未受上游影响。`wsClient.request('web.login.start/wait')` WhatsApp QR 登录亦无变化。完全兼容 4.9。
|
||||
|
||||
## 🔒 安全修复(内核侧,面板无需适配)
|
||||
|
||||
- exec 审批绕过修复
|
||||
- SSRF 硬化(浏览器重定向)
|
||||
- auth 令牌轮换后旧 WebSocket 失效
|
||||
- 媒体 base64 大小限制
|
||||
- 运行时事件信任标记
|
||||
- 宿主 exec/env 环境变量消毒
|
||||
|
||||
---
|
||||
|
||||
## 执行状态
|
||||
|
||||
```
|
||||
Phase 1: ✅ version-policy 绑定 4.9
|
||||
Phase 2: ✅ dreaming 页已有完整 4.9 支持
|
||||
Phase 3: ✅ Skills Gateway RPC 接入
|
||||
Phase 4: ✅ Sessions Compaction UI
|
||||
Phase 5: ✅ Approval 管理增强
|
||||
Phase 6: ✅ P2 审查完毕,均无需适配
|
||||
Phase 7: ✅ channels.js 全量审查(2198 行 + 上游 180 文件 diff),完全兼容 4.9
|
||||
|
||||
全部适配工作已完成。Build 通过。
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **所有新功能必须向下兼容**:用户可能运行 3.13/3.28 内核,新 RPC 调用必须 catch `isUnsupportedError` 并优雅降级
|
||||
2. **Web 模式适配**:新 RPC 通过 WebSocket 调用,Web 模式(dev-api.js)如需支持需增加转发
|
||||
3. **i18n**:所有新 UI 文本需同步 zh-CN 和 en 两套翻译
|
||||
@@ -9,123 +9,163 @@
|
||||
},
|
||||
"default": {
|
||||
"official": {
|
||||
"recommended": "2026.3.13"
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.13-zh.1"
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"panels": {
|
||||
"0.9.0": {
|
||||
"official": {
|
||||
"recommended": "2026.3.13"
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.13-zh.1"
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.9.1": {
|
||||
"official": {
|
||||
"recommended": "2026.3.13"
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.13-zh.1"
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.9.2": {
|
||||
"official": {
|
||||
"recommended": "2026.3.13"
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.13-zh.1"
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.9.3": {
|
||||
"official": {
|
||||
"recommended": "2026.3.13"
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.13-zh.1"
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.9.4": {
|
||||
"official": {
|
||||
"recommended": "2026.3.13"
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.13-zh.1"
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.9.5": {
|
||||
"official": {
|
||||
"recommended": "2026.3.13"
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.13-zh.1"
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.9.6": {
|
||||
"official": {
|
||||
"recommended": "2026.3.13"
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.13-zh.1"
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.9.7": {
|
||||
"official": {
|
||||
"recommended": "2026.3.13"
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.13-zh.1"
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.9.8": {
|
||||
"official": {
|
||||
"recommended": "2026.3.13"
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.13-zh.1"
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.9.9": {
|
||||
"official": {
|
||||
"recommended": "2026.3.13"
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.13-zh.1"
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.10.0": {
|
||||
"official": {
|
||||
"recommended": "2026.3.28"
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.28-zh.2"
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.11.0": {
|
||||
"official": {
|
||||
"recommended": "2026.3.28"
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.28-zh.2"
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.11.1": {
|
||||
"official": {
|
||||
"recommended": "2026.3.28"
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.28-zh.2"
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.11.2": {
|
||||
"official": {
|
||||
"recommended": "2026.3.28"
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.3.28-zh.2"
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.11.3": {
|
||||
"official": {
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.11.4": {
|
||||
"official": {
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.11.5": {
|
||||
"official": {
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.11.6": {
|
||||
"official": {
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
},
|
||||
"0.12.0": {
|
||||
"official": {
|
||||
"recommended": "2026.4.9"
|
||||
},
|
||||
"chinese": {
|
||||
"recommended": "2026.4.9-zh.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "clawpanel",
|
||||
"version": "0.11.6",
|
||||
"version": "0.12.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "clawpanel",
|
||||
"version": "0.11.6",
|
||||
"version": "0.12.0",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clawpanel",
|
||||
"version": "0.11.6",
|
||||
"version": "0.12.0",
|
||||
"private": true,
|
||||
"description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用",
|
||||
"type": "module",
|
||||
|
||||
@@ -618,15 +618,21 @@ async function _tryStandaloneInstall(version, logs, overrideBaseUrl = null) {
|
||||
if (!resp.ok) throw new Error(`standalone 清单不可用 (HTTP ${resp.status})`)
|
||||
const manifest = await resp.json()
|
||||
|
||||
const remoteVersion = manifest.version
|
||||
// 兼容两种 latest.json 格式:
|
||||
// 新格式(CI 生成): { "editions": { "zh": { "version": "...", "base_url": "..." } } }
|
||||
// 旧格式(兼容): { "version": "...", "base_url": "..." }
|
||||
const editionObj = manifest?.editions?.zh
|
||||
const remoteVersion = editionObj?.version || manifest.version
|
||||
if (!remoteVersion) throw new Error('standalone 清单缺少 version 字段')
|
||||
if (version !== 'latest' && !versionsMatch(remoteVersion, version)) {
|
||||
throw new Error(`standalone 版本 ${remoteVersion} 与请求版本 ${version} 不匹配`)
|
||||
}
|
||||
|
||||
const remoteBase = overrideBaseUrl || manifest.base_url || `${cfg.baseUrl}/${remoteVersion}`
|
||||
const archivePrefix = editionObj ? 'openclaw-zh' : 'openclaw'
|
||||
const manifestBaseUrl = editionObj?.base_url || manifest.base_url
|
||||
const remoteBase = overrideBaseUrl || manifestBaseUrl || `${cfg.baseUrl}/${remoteVersion}`
|
||||
const ext = isWindows ? 'zip' : 'tar.gz'
|
||||
const filename = `openclaw-${remoteVersion}-${platform}.${ext}`
|
||||
const filename = `${archivePrefix}-${remoteVersion}-${platform}.${ext}`
|
||||
const downloadUrl = `${remoteBase}/${filename}`
|
||||
|
||||
logs.push(`从 CDN 下载: ${filename}`)
|
||||
@@ -5446,22 +5452,47 @@ const handlers = {
|
||||
// ── standalone 安装(auto / standalone-r2 / standalone-github) ──
|
||||
const tryStandalone = source !== 'official' && ['auto', 'standalone-r2', 'standalone-github'].includes(method)
|
||||
if (tryStandalone) {
|
||||
try {
|
||||
const githubBase = method === 'standalone-github'
|
||||
? `https://github.com/qingchencloud/openclaw-standalone/releases/download/v${ver}`
|
||||
: null
|
||||
const saResult = await _tryStandaloneInstall(ver, logs, githubBase)
|
||||
if (saResult) {
|
||||
const label = method === 'standalone-github' ? 'GitHub' : 'CDN'
|
||||
logs.push(`✅ standalone (${label}) 安装完成`)
|
||||
return logs.join('\n')
|
||||
}
|
||||
} catch (e) {
|
||||
if (method === 'auto') {
|
||||
logs.push(`standalone 不可用(${e.message}),降级到 npm 安装...`)
|
||||
} else {
|
||||
const githubReleaseBase = `https://github.com/qingchencloud/openclaw-standalone/releases/download/v${ver}`
|
||||
if (method === 'standalone-github') {
|
||||
// standalone-github 模式:只走 GitHub
|
||||
try {
|
||||
const saResult = await _tryStandaloneInstall(ver, logs, githubReleaseBase)
|
||||
if (saResult) {
|
||||
logs.push('✅ standalone (GitHub) 安装完成')
|
||||
return logs.join('\n')
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(`standalone 安装失败: ${e.message}`)
|
||||
}
|
||||
} else {
|
||||
// auto / standalone-r2 模式:R2 CDN → GitHub Releases fallback
|
||||
let cdnErr = null
|
||||
try {
|
||||
const saResult = await _tryStandaloneInstall(ver, logs, null)
|
||||
if (saResult) {
|
||||
logs.push('✅ standalone (CDN) 安装完成')
|
||||
return logs.join('\n')
|
||||
}
|
||||
} catch (e) {
|
||||
cdnErr = e.message
|
||||
logs.push(`CDN 下载失败(${cdnErr}),尝试从 GitHub Releases 下载...`)
|
||||
}
|
||||
// Fallback: GitHub Releases
|
||||
if (cdnErr) {
|
||||
try {
|
||||
const saResult = await _tryStandaloneInstall(ver, logs, githubReleaseBase)
|
||||
if (saResult) {
|
||||
logs.push('✅ standalone (GitHub) 安装完成')
|
||||
return logs.join('\n')
|
||||
}
|
||||
} catch (e) {
|
||||
if (method === 'auto') {
|
||||
logs.push(`standalone 不可用(GitHub: ${e.message}),降级到 npm 安装...`)
|
||||
} else {
|
||||
throw new Error(`standalone 安装失败: CDN=${cdnErr}, GitHub=${e.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -351,7 +351,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clawpanel"
|
||||
version = "0.11.6"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clawpanel"
|
||||
version = "0.11.6"
|
||||
version = "0.12.0"
|
||||
edition = "2021"
|
||||
description = "ClawPanel - OpenClaw 可视化管理面板"
|
||||
authors = ["qingchencloud"]
|
||||
|
||||
@@ -485,40 +485,83 @@ fn npm_command_elevated() -> Command {
|
||||
/// 安装/升级前的清理工作:停止 Gateway、清理 npm 全局 bin 下的 openclaw 残留文件
|
||||
/// 解决 Windows 上 EEXIST(文件已存在)和文件被占用的问题
|
||||
fn pre_install_cleanup() {
|
||||
// 1. 先通过 CLI 正常停止 Gateway
|
||||
let _ = openclaw_command().args(["gateway", "stop"]).output();
|
||||
/// 带超时执行命令(spawn + try_wait),防止任何子进程无限阻塞
|
||||
fn run_with_timeout(mut child: std::process::Child, timeout_secs: u64) -> Option<std::process::Output> {
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
let stdout = child.stdout.take().map(|mut s| {
|
||||
let mut buf = Vec::new();
|
||||
let _ = std::io::Read::read_to_end(&mut s, &mut buf);
|
||||
buf
|
||||
}).unwrap_or_default();
|
||||
return Some(std::process::Output { status, stdout, stderr: Vec::new() });
|
||||
}
|
||||
Ok(None) => {
|
||||
if std::time::Instant::now() >= deadline {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return None;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
}
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 先通过 CLI 正常停止 Gateway(10s 超时)
|
||||
if let Ok(child) = openclaw_command()
|
||||
.args(["gateway", "stop"])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
run_with_timeout(child, 10);
|
||||
}
|
||||
|
||||
// 2. 停止 Gateway 进程,释放 openclaw 相关文件锁
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// 杀死所有运行 openclaw gateway 的 node.exe 进程(通过命令行匹配)
|
||||
if let Ok(output) = Command::new("wmic")
|
||||
.args(["process", "where", "CommandLine like '%openclaw%gateway%'", "get", "ProcessId", "/format:list"])
|
||||
.output()
|
||||
// 使用 PowerShell Get-CimInstance(兼容 Windows 11,wmic 已废弃)(10s 超时)
|
||||
if let Ok(child) = Command::new("powershell")
|
||||
.args(["-NoProfile", "-Command",
|
||||
"Get-CimInstance Win32_Process -Filter \"CommandLine like '%openclaw%gateway%'\" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ProcessId"])
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
for line in text.lines() {
|
||||
if let Some(pid_str) = line.strip_prefix("ProcessId=") {
|
||||
if let Ok(_pid) = pid_str.trim().parse::<u32>() {
|
||||
let _ = Command::new("taskkill").args(["/F", "/PID", pid_str.trim()]).output();
|
||||
if let Some(output) = run_with_timeout(child, 10) {
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
for line in text.lines() {
|
||||
if let Ok(_pid) = line.trim().parse::<u32>() {
|
||||
let _ = Command::new("taskkill").args(["/F", "/PID", line.trim()]).output();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 同时杀死 standalone 目录下的 node.exe 进程
|
||||
// 同时杀死 standalone 目录下的 node.exe 进程(每个目录 10s 超时)
|
||||
for sa_dir in all_standalone_dirs() {
|
||||
if sa_dir.exists() {
|
||||
let dir_str = sa_dir.to_string_lossy().to_lowercase();
|
||||
if let Ok(output) = Command::new("wmic")
|
||||
.args(["process", "where", &format!("ExecutablePath like '%{}%'", dir_str.replace('\\', "\\\\")), "get", "ProcessId", "/format:list"])
|
||||
.output()
|
||||
let dir_lower = sa_dir.to_string_lossy().to_lowercase().replace('\\', "\\\\");
|
||||
let ps_script = format!(
|
||||
"Get-Process -Name node -ErrorAction SilentlyContinue | Where-Object {{ $_.Path -and $_.Path.ToLower().Contains('{}') }} | Select-Object -ExpandProperty Id",
|
||||
dir_lower
|
||||
);
|
||||
if let Ok(child) = Command::new("powershell")
|
||||
.args(["-NoProfile", "-Command", &ps_script])
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
for line in text.lines() {
|
||||
if let Some(pid_str) = line.strip_prefix("ProcessId=") {
|
||||
if let Ok(_pid) = pid_str.trim().parse::<u32>() {
|
||||
let _ = Command::new("taskkill").args(["/F", "/PID", pid_str.trim()]).output();
|
||||
if let Some(output) = run_with_timeout(child, 10) {
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
for line in text.lines() {
|
||||
if let Ok(_pid) = line.trim().parse::<u32>() {
|
||||
let _ = Command::new("taskkill").args(["/F", "/PID", line.trim()]).output();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -532,16 +575,26 @@ fn pre_install_cleanup() {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let uid = get_uid().unwrap_or(501);
|
||||
let _ = Command::new("launchctl")
|
||||
if let Ok(child) = Command::new("launchctl")
|
||||
.args(["bootout", &format!("gui/{uid}/ai.openclaw.gateway")])
|
||||
.output();
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
run_with_timeout(child, 10);
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let _ = Command::new("pkill")
|
||||
if let Ok(child) = Command::new("pkill")
|
||||
.args(["-f", "openclaw.*gateway"])
|
||||
.output();
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
run_with_timeout(child, 10);
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
|
||||
@@ -2915,10 +2968,27 @@ async fn try_standalone_install(
|
||||
.await
|
||||
.map_err(|e| format!("standalone 清单解析失败: {e}"))?;
|
||||
|
||||
let remote_version = manifest
|
||||
.get("version")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("standalone 清单缺少 version 字段")?;
|
||||
// 兼容两种 latest.json 格式:
|
||||
// 新格式(CI 生成): { "editions": { "zh": { "version": "...", "base_url": "..." } } }
|
||||
// 旧格式(兼容): { "version": "...", "base_url": "..." }
|
||||
let edition_obj = manifest
|
||||
.get("editions")
|
||||
.and_then(|e| e.get("zh"));
|
||||
let (remote_version, manifest_base_url, archive_prefix) = if let Some(ed) = edition_obj {
|
||||
let ver = ed
|
||||
.get("version")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("standalone 清单 editions.zh 缺少 version 字段")?;
|
||||
let bu = ed.get("base_url").and_then(|v| v.as_str());
|
||||
(ver, bu, "openclaw-zh")
|
||||
} else {
|
||||
let ver = manifest
|
||||
.get("version")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("standalone 清单缺少 version 字段")?;
|
||||
let bu = manifest.get("base_url").and_then(|v| v.as_str());
|
||||
(ver, bu, "openclaw")
|
||||
};
|
||||
|
||||
// 版本匹配检查
|
||||
if version != "latest" && !versions_match(remote_version, version) {
|
||||
@@ -2931,15 +3001,12 @@ async fn try_standalone_install(
|
||||
let remote_base = if let Some(ovr) = override_base_url {
|
||||
ovr
|
||||
} else {
|
||||
manifest
|
||||
.get("base_url")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(&default_base)
|
||||
manifest_base_url.unwrap_or(&default_base)
|
||||
};
|
||||
|
||||
// 2. 构造下载 URL
|
||||
let ext = standalone_archive_ext();
|
||||
let filename = format!("openclaw-{remote_version}-{platform}.{ext}");
|
||||
let filename = format!("{archive_prefix}-{remote_version}-{platform}.{ext}");
|
||||
let download_url = format!("{remote_base}/{filename}");
|
||||
|
||||
let _ = app.emit("upgrade-log", format!("从 {source_label} 下载: {filename}"));
|
||||
@@ -2987,6 +3054,13 @@ async fn try_standalone_install(
|
||||
if total_bytes > 0 {
|
||||
let pct = 15 + ((downloaded as f64 / total_bytes as f64) * 55.0) as u32;
|
||||
if pct > last_progress {
|
||||
// 每 5% 输出一次文字进度
|
||||
if pct / 5 > last_progress / 5 {
|
||||
let dl_mb = downloaded as f64 / 1_048_576.0;
|
||||
let total_mb = total_bytes as f64 / 1_048_576.0;
|
||||
let real_pct = (downloaded as f64 / total_bytes as f64 * 100.0) as u32;
|
||||
let _ = app.emit("upgrade-log", format!("下载中 {real_pct}% ({dl_mb:.0}/{total_mb:.0}MB)"));
|
||||
}
|
||||
last_progress = pct;
|
||||
let _ = app.emit("upgrade-progress", pct.min(70));
|
||||
}
|
||||
@@ -3464,38 +3538,65 @@ async fn upgrade_openclaw_inner(
|
||||
&& (method == "auto" || method == "standalone-r2" || method == "standalone-github");
|
||||
|
||||
if try_standalone {
|
||||
// standalone-github 模式:使用 GitHub Releases 下载地址
|
||||
let github_base = if method == "standalone-github" {
|
||||
Some(format!(
|
||||
"https://github.com/qingchencloud/openclaw-standalone/releases/download/v{}",
|
||||
ver
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match try_standalone_install(&app, ver, github_base.as_deref()).await {
|
||||
Ok(installed_ver) => {
|
||||
let _ = app.emit("upgrade-progress", 100);
|
||||
super::refresh_enhanced_path();
|
||||
crate::commands::service::invalidate_cli_detection_cache();
|
||||
let label = if method == "standalone-github" {
|
||||
"GitHub"
|
||||
} else {
|
||||
"CDN"
|
||||
};
|
||||
let msg = format!("✅ standalone ({label}) 安装完成,当前版本: {installed_ver}");
|
||||
let _ = app.emit("upgrade-log", &msg);
|
||||
return Ok(msg);
|
||||
let github_release_base = format!(
|
||||
"https://github.com/qingchencloud/openclaw-standalone/releases/download/v{}",
|
||||
ver
|
||||
);
|
||||
|
||||
if method == "standalone-github" {
|
||||
// standalone-github 模式:只走 GitHub
|
||||
match try_standalone_install(&app, ver, Some(&github_release_base)).await {
|
||||
Ok(installed_ver) => {
|
||||
let _ = app.emit("upgrade-progress", 100);
|
||||
super::refresh_enhanced_path();
|
||||
crate::commands::service::invalidate_cli_detection_cache();
|
||||
let msg = format!("✅ standalone (GitHub) 安装完成,当前版本: {installed_ver}");
|
||||
let _ = app.emit("upgrade-log", &msg);
|
||||
return Ok(msg);
|
||||
}
|
||||
Err(reason) => {
|
||||
return Err(format!("standalone 安装失败: {reason}"));
|
||||
}
|
||||
}
|
||||
Err(reason) => {
|
||||
if method == "auto" {
|
||||
} else {
|
||||
// auto / standalone-r2 模式:R2 CDN → GitHub Releases fallback
|
||||
match try_standalone_install(&app, ver, None).await {
|
||||
Ok(installed_ver) => {
|
||||
let _ = app.emit("upgrade-progress", 100);
|
||||
super::refresh_enhanced_path();
|
||||
crate::commands::service::invalidate_cli_detection_cache();
|
||||
let msg = format!("✅ standalone (CDN) 安装完成,当前版本: {installed_ver}");
|
||||
let _ = app.emit("upgrade-log", &msg);
|
||||
return Ok(msg);
|
||||
}
|
||||
Err(cdn_reason) => {
|
||||
let _ = app.emit(
|
||||
"upgrade-log",
|
||||
format!("standalone 不可用({reason}),降级到 npm 安装..."),
|
||||
format!("CDN 下载失败({cdn_reason}),尝试从 GitHub Releases 下载..."),
|
||||
);
|
||||
let _ = app.emit("upgrade-progress", 5);
|
||||
} else {
|
||||
return Err(format!("standalone 安装失败: {reason}"));
|
||||
// Fallback: GitHub Releases
|
||||
match try_standalone_install(&app, ver, Some(&github_release_base)).await {
|
||||
Ok(installed_ver) => {
|
||||
let _ = app.emit("upgrade-progress", 100);
|
||||
super::refresh_enhanced_path();
|
||||
crate::commands::service::invalidate_cli_detection_cache();
|
||||
let msg = format!("✅ standalone (GitHub) 安装完成,当前版本: {installed_ver}");
|
||||
let _ = app.emit("upgrade-log", &msg);
|
||||
return Ok(msg);
|
||||
}
|
||||
Err(gh_reason) => {
|
||||
if method == "auto" {
|
||||
let _ = app.emit(
|
||||
"upgrade-log",
|
||||
format!("standalone 不可用(GitHub: {gh_reason}),降级到 npm 安装..."),
|
||||
);
|
||||
let _ = app.emit("upgrade-progress", 5);
|
||||
} else {
|
||||
return Err(format!("standalone 安装失败: CDN={cdn_reason}, GitHub={gh_reason}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3726,11 +3827,37 @@ async fn upgrade_openclaw_inner(
|
||||
}
|
||||
|
||||
// 安装成功后再卸载旧包(确保 CLI 始终可用)
|
||||
// 清理步骤采用错误隔离:任何清理失败都不影响安装成功的最终结果
|
||||
if need_uninstall_old {
|
||||
let _ = app.emit("upgrade-log", format!("清理旧版本 ({old_pkg})..."));
|
||||
let _ = npm_command_elevated()
|
||||
// npm uninstall 加 30s 超时,避免无限卡住
|
||||
let uninstall_child = npm_command_elevated()
|
||||
.args(["uninstall", "-g", old_pkg])
|
||||
.output();
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn();
|
||||
match uninstall_child {
|
||||
Ok(mut child) => {
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(_status)) => break,
|
||||
Ok(None) => {
|
||||
if std::time::Instant::now() >= deadline {
|
||||
let _ = child.kill();
|
||||
let _ = app.emit("upgrade-log", "⚠️ 清理旧版本超时(30s),已跳过");
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = app.emit("upgrade-log", format!("⚠️ 清理旧版本启动失败: {e},已跳过"));
|
||||
}
|
||||
}
|
||||
|
||||
// 清理 standalone 安装目录(不论从 standalone 切走还是切到 standalone,
|
||||
// npm 路径已经安装了新 CLI,standalone 残留会干扰源检测)
|
||||
@@ -3742,20 +3869,23 @@ async fn upgrade_openclaw_inner(
|
||||
);
|
||||
|
||||
// Windows: 终止占用该目录的 node.exe 进程
|
||||
// 使用 PowerShell Get-Process(兼容 Windows 11,wmic 已废弃)
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let dir_str = sa_dir.to_string_lossy().to_lowercase();
|
||||
if let Ok(output) = Command::new("wmic")
|
||||
.args(["process", "where", &format!("ExecutablePath like '%{}%'", dir_str.replace('\\', "\\\\")), "get", "ProcessId", "/format:list"])
|
||||
let dir_lower = sa_dir.to_string_lossy().to_lowercase().replace('\\', "\\\\");
|
||||
let ps_script = format!(
|
||||
"Get-Process -Name node -ErrorAction SilentlyContinue | Where-Object {{ $_.Path -and $_.Path.ToLower().Contains('{}') }} | Select-Object -ExpandProperty Id",
|
||||
dir_lower
|
||||
);
|
||||
if let Ok(output) = Command::new("powershell")
|
||||
.args(["-NoProfile", "-Command", &ps_script])
|
||||
.output()
|
||||
{
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
for line in text.lines() {
|
||||
if let Some(pid_str) = line.strip_prefix("ProcessId=") {
|
||||
if let Ok(pid) = pid_str.trim().parse::<u32>() {
|
||||
let _ = app.emit("upgrade-log", format!("终止占用进程 PID {pid}..."));
|
||||
let _ = Command::new("taskkill").args(["/F", "/PID", &pid.to_string()]).output();
|
||||
}
|
||||
if let Ok(pid) = line.trim().parse::<u32>() {
|
||||
let _ = app.emit("upgrade-log", format!("终止占用进程 PID {pid}..."));
|
||||
let _ = Command::new("taskkill").args(["/F", "/PID", &pid.to_string()]).output();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3900,20 +4030,23 @@ async fn uninstall_openclaw_inner(
|
||||
);
|
||||
|
||||
// Windows: 先尝试终止占用该目录的 node.exe 进程
|
||||
// 使用 PowerShell Get-Process(兼容 Windows 11,wmic 已废弃)
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let dir_str = sa_dir.to_string_lossy().to_lowercase();
|
||||
if let Ok(output) = Command::new("wmic")
|
||||
.args(["process", "where", &format!("ExecutablePath like '%{}%'", dir_str.replace('\\', "\\\\")), "get", "ProcessId", "/format:list"])
|
||||
let dir_lower = sa_dir.to_string_lossy().to_lowercase().replace('\\', "\\\\");
|
||||
let ps_script = format!(
|
||||
"Get-Process -Name node -ErrorAction SilentlyContinue | Where-Object {{ $_.Path -and $_.Path.ToLower().Contains('{}') }} | Select-Object -ExpandProperty Id",
|
||||
dir_lower
|
||||
);
|
||||
if let Ok(output) = Command::new("powershell")
|
||||
.args(["-NoProfile", "-Command", &ps_script])
|
||||
.output()
|
||||
{
|
||||
let text = String::from_utf8_lossy(&output.stdout);
|
||||
for line in text.lines() {
|
||||
if let Some(pid_str) = line.strip_prefix("ProcessId=") {
|
||||
if let Ok(pid) = pid_str.trim().parse::<u32>() {
|
||||
let _ = app.emit("upgrade-log", format!("终止占用进程 PID {pid}..."));
|
||||
let _ = Command::new("taskkill").args(["/F", "/PID", &pid.to_string()]).output();
|
||||
}
|
||||
if let Ok(pid) = line.trim().parse::<u32>() {
|
||||
let _ = app.emit("upgrade-log", format!("终止占用进程 PID {pid}..."));
|
||||
let _ = Command::new("taskkill").args(["/F", "/PID", &pid.to_string()]).output();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1491,7 +1491,7 @@ mod platform {
|
||||
}
|
||||
|
||||
/// 检测 Gateway 是否在运行,并返回其 PID
|
||||
/// 策略:先 TCP 端口检测连通性,再用 netstat+WMIC 验证命令行是 OpenClaw Gateway
|
||||
/// 策略:先 TCP 端口检测连通性,再用 netstat+PowerShell 验证命令行是 OpenClaw Gateway
|
||||
pub fn check_service_status(_uid: u32, _label: &str) -> (bool, Option<u32>) {
|
||||
let port = crate::commands::gateway_listen_port();
|
||||
let addr = format!("127.0.0.1:{port}");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
|
||||
"productName": "ClawPanel",
|
||||
"version": "0.11.6",
|
||||
"version": "0.12.0",
|
||||
"identifier": "ai.openclaw.clawpanel",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
@@ -727,6 +727,45 @@ export class WsClient {
|
||||
return this.request('sessions.reset', { key })
|
||||
}
|
||||
|
||||
// ===== 4.9: Sessions Compaction =====
|
||||
sessionsCompactionList(key) {
|
||||
return this.request('sessions.compaction.list', { key })
|
||||
}
|
||||
|
||||
sessionsCompactionGet(key, checkpointId) {
|
||||
return this.request('sessions.compaction.get', { key, checkpointId })
|
||||
}
|
||||
|
||||
sessionsCompactionBranch(key, checkpointId) {
|
||||
return this.request('sessions.compaction.branch', { key, checkpointId })
|
||||
}
|
||||
|
||||
sessionsCompactionRestore(key, checkpointId) {
|
||||
return this.request('sessions.compaction.restore', { key, checkpointId })
|
||||
}
|
||||
|
||||
// ===== 4.9: Skills Gateway RPC =====
|
||||
skillsSearch(query, limit) {
|
||||
return this.request('skills.search', { query, limit })
|
||||
}
|
||||
|
||||
skillsDetail(slug) {
|
||||
return this.request('skills.detail', { slug })
|
||||
}
|
||||
|
||||
// ===== 4.9: Approval management =====
|
||||
execApprovalList() {
|
||||
return this.request('exec.approval.list', {})
|
||||
}
|
||||
|
||||
execApprovalGet(id) {
|
||||
return this.request('exec.approval.get', { id })
|
||||
}
|
||||
|
||||
pluginApprovalList() {
|
||||
return this.request('plugin.approval.list', {})
|
||||
}
|
||||
|
||||
onEvent(callback) {
|
||||
this._eventListeners.push(callback)
|
||||
return () => { this._eventListeners = this._eventListeners.filter(fn => fn !== callback) }
|
||||
|
||||
@@ -168,4 +168,13 @@ export default {
|
||||
hostedApiError: _('API 错误 {code}', 'API Error {code}', 'API 錯誤 {code}'),
|
||||
hostedPrefix: _('[托管 Agent] ', '[Hosted Agent] '),
|
||||
hostedContextSummary: _('[上下文摘要 - 已压缩 {n} 条历史]', '[Context Summary - compressed {n} history entries]', '[上下文摘要 - 已壓縮 {n} 條歷史]'),
|
||||
compactionHistory: _('压缩历史', 'Compaction History', '壓縮歷史'),
|
||||
compactionLoading: _('加载压缩检查点...', 'Loading compaction checkpoints...', '載入壓縮檢查點...'),
|
||||
compactionEmpty: _('该会话暂无压缩检查点', 'No compaction checkpoints for this session', '該對話暫無壓縮檢查點'),
|
||||
compactionBranch: _('分支', 'Branch', '分支'),
|
||||
compactionRestore: _('恢复', 'Restore', '恢復'),
|
||||
compactionBranchDone: _('已从检查点创建分支会话', 'Branch session created from checkpoint', '已從檢查點建立分支對話'),
|
||||
compactionRestoreDone: _('已恢复到检查点', 'Restored to checkpoint', '已恢復到檢查點'),
|
||||
compactionConfirmRestore: _('确定要恢复到此检查点吗?当前会话内容将被替换。', 'Restore to this checkpoint? Current session content will be replaced.', '確定要恢復到此檢查點嗎?目前對話內容將被替換。'),
|
||||
compactionUnsupported: _('当前 Gateway 版本不支持压缩历史功能,请升级到 OpenClaw 2026.4.9+', 'Compaction history requires OpenClaw 2026.4.9+', '目前 Gateway 版本不支援壓縮歷史功能,請升級到 OpenClaw 2026.4.9+'),
|
||||
}
|
||||
|
||||
@@ -85,4 +85,10 @@ export default {
|
||||
approvalsModeBoth: _('两者都发(both)', 'Both', '兩者都發(both)'),
|
||||
approvalsForwardExec: _('转发执行请求', 'Forward Exec Requests', '轉發執行請求'),
|
||||
approvalsForwardExecHint: _('将 exec 审批请求转发到渠道(默认关闭,低风险场景可开启)', 'Forward exec approval requests to channels (off by default, enable for low-risk scenarios)', '將 exec 審批請求轉發到頻道(預設關閉,低風險場景可開啟)'),
|
||||
pendingApprovals: _('待处理审批队列', 'Pending Approval Queue', '待處理審批佇列'),
|
||||
refreshApprovals: _('刷新', 'Refresh', '重新整理'),
|
||||
approvalsLoadingQueue: _('加载审批队列...', 'Loading approval queue...', '載入審批佇列...'),
|
||||
approvalsQueueEmpty: _('当前没有待处理的审批请求', 'No pending approval requests', '目前沒有待處理的審批請求'),
|
||||
approvalsGwNotReady: _('Gateway 未连接,无法加载审批队列', 'Gateway not connected, cannot load approval queue', 'Gateway 未連線,無法載入審批佇列'),
|
||||
approvalsUnsupported: _('当前 Gateway 版本不支持审批队列查询,请升级到 OpenClaw 2026.4.9+', 'Approval queue requires OpenClaw 2026.4.9+', '目前 Gateway 版本不支援審批佇列查詢,請升級到 OpenClaw 2026.4.9+'),
|
||||
}
|
||||
|
||||
@@ -1289,10 +1289,14 @@ function renderSessionList(sessions) {
|
||||
const msgCount = s.messageCount || s.messages || 0
|
||||
const agentId = parseSessionAgent(key)
|
||||
const displayLabel = getDisplayLabel(key) || label
|
||||
const cpCount = s.compactionCheckpointCount || 0
|
||||
return `<div class="chat-session-card${active}" data-key="${escapeAttr(key)}">
|
||||
<div class="chat-session-card-header">
|
||||
<span class="chat-session-label" title="${t('chat.doubleClickRename')}">${escapeAttr(displayLabel)}</span>
|
||||
<button class="chat-session-del" data-del="${escapeAttr(key)}" title="${t('common.delete')}">×</button>
|
||||
<div style="display:flex;gap:2px;align-items:center">
|
||||
${cpCount > 0 ? `<button class="chat-session-del" data-compaction="${escapeAttr(key)}" title="${t('chat.compactionHistory')}" style="color:var(--text-tertiary);font-size:11px">⟳${cpCount}</button>` : ''}
|
||||
<button class="chat-session-del" data-del="${escapeAttr(key)}" title="${t('common.delete')}">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-session-card-meta">
|
||||
${agentId && agentId !== 'main' ? `<span class="chat-session-agent">${escapeAttr(agentId)}</span>` : ''}
|
||||
@@ -1303,6 +1307,8 @@ function renderSessionList(sessions) {
|
||||
}).join('')
|
||||
|
||||
_sessionListEl.onclick = (e) => {
|
||||
const cpBtn = e.target.closest('[data-compaction]')
|
||||
if (cpBtn) { e.stopPropagation(); showCompactionHistory(cpBtn.dataset.compaction); return }
|
||||
const delBtn = e.target.closest('[data-del]')
|
||||
if (delBtn) { e.stopPropagation(); deleteSession(delBtn.dataset.del); return }
|
||||
const item = e.target.closest('[data-key]')
|
||||
@@ -1433,6 +1439,96 @@ async function deleteSession(key) {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 4.9: Sessions Compaction History =====
|
||||
async function showCompactionHistory(key) {
|
||||
if (!key || !wsClient.gatewayReady) return
|
||||
const label = getDisplayLabel(key)
|
||||
toast(t('chat.compactionLoading'), 'info')
|
||||
try {
|
||||
const result = await wsClient.sessionsCompactionList(key)
|
||||
const checkpoints = result?.checkpoints || []
|
||||
if (!checkpoints.length) {
|
||||
toast(t('chat.compactionEmpty'), 'info')
|
||||
return
|
||||
}
|
||||
const listHtml = checkpoints.map((cp, idx) => {
|
||||
const id = cp.id || cp.checkpointId || `cp-${idx}`
|
||||
const ts = cp.timestamp || cp.createdAt || 0
|
||||
const timeStr = ts ? new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts).toLocaleString() : '—'
|
||||
const tokensBefore = cp.tokensBefore ?? '—'
|
||||
const tokensAfter = cp.tokensAfter ?? '—'
|
||||
return `<div style="padding:10px 0;border-bottom:1px solid var(--border-primary);display:flex;justify-content:space-between;align-items:center;gap:8px">
|
||||
<div style="min-width:0;flex:1">
|
||||
<div style="font-size:13px;font-weight:500">#${idx + 1} · ${escapeAttr(timeStr)}</div>
|
||||
<div style="font-size:12px;color:var(--text-tertiary);margin-top:2px">${tokensBefore} → ${tokensAfter} tokens</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:4px;flex-shrink:0">
|
||||
<button class="btn btn-sm btn-secondary" data-cp-branch="${escapeAttr(id)}">${t('chat.compactionBranch')}</button>
|
||||
<button class="btn btn-sm btn-warning" data-cp-restore="${escapeAttr(id)}">${t('chat.compactionRestore')}</button>
|
||||
</div>
|
||||
</div>`
|
||||
}).join('')
|
||||
|
||||
const overlay = document.createElement('div')
|
||||
overlay.className = 'modal-overlay'
|
||||
overlay.innerHTML = `<div class="modal" style="max-width:520px;max-height:80vh;overflow:auto">
|
||||
<div class="modal-header"><h3>${escapeAttr(t('chat.compactionHistory'))}: ${escapeAttr(label)}</h3></div>
|
||||
<div class="modal-body" style="padding:0 var(--space-md)">${listHtml}</div>
|
||||
<div class="modal-footer"><button class="btn btn-secondary" data-cp-close>${t('common.close')}</button></div>
|
||||
</div>`
|
||||
document.body.appendChild(overlay)
|
||||
|
||||
overlay.addEventListener('click', async (e) => {
|
||||
if (e.target === overlay || e.target.closest('[data-cp-close]')) {
|
||||
overlay.remove()
|
||||
return
|
||||
}
|
||||
const branchBtn = e.target.closest('[data-cp-branch]')
|
||||
if (branchBtn) {
|
||||
branchBtn.disabled = true
|
||||
try {
|
||||
const res = await wsClient.sessionsCompactionBranch(key, branchBtn.dataset.cpBranch)
|
||||
toast(t('chat.compactionBranchDone'), 'success')
|
||||
overlay.remove()
|
||||
if (res?.key) void switchSession(res.key)
|
||||
else refreshSessionList()
|
||||
} catch (err) {
|
||||
toast(`${t('common.operationFailed')}: ${err.message}`, 'error')
|
||||
branchBtn.disabled = false
|
||||
}
|
||||
return
|
||||
}
|
||||
const restoreBtn = e.target.closest('[data-cp-restore]')
|
||||
if (restoreBtn) {
|
||||
const yes = await showConfirm(t('chat.compactionConfirmRestore'))
|
||||
if (!yes) return
|
||||
restoreBtn.disabled = true
|
||||
try {
|
||||
await wsClient.sessionsCompactionRestore(key, restoreBtn.dataset.cpRestore)
|
||||
toast(t('chat.compactionRestoreDone'), 'success')
|
||||
overlay.remove()
|
||||
if (key === _sessionKey) {
|
||||
clearMessages()
|
||||
_lastHistoryHash = ''
|
||||
loadHistory()
|
||||
}
|
||||
refreshSessionList()
|
||||
} catch (err) {
|
||||
toast(`${t('common.operationFailed')}: ${err.message}`, 'error')
|
||||
restoreBtn.disabled = false
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
const msg = String(e?.message || e || '').toLowerCase()
|
||||
if (msg.includes('unknown method') || msg.includes('not found') || msg.includes('unsupported')) {
|
||||
toast(t('chat.compactionUnsupported'), 'warning')
|
||||
} else {
|
||||
toast(`${t('common.operationFailed')}: ${e.message}`, 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function resetCurrentSession() {
|
||||
if (!_sessionKey) return
|
||||
const label = getDisplayLabel(_sessionKey)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { api } from '../lib/tauri-api.js'
|
||||
import { toast } from '../components/toast.js'
|
||||
import { icon } from '../lib/icons.js'
|
||||
import { t } from '../lib/i18n.js'
|
||||
import { wsClient } from '../lib/ws-client.js'
|
||||
|
||||
let _page = null, _config = null, _dirty = false
|
||||
|
||||
@@ -369,10 +370,74 @@ function renderApprovals(el) {
|
||||
</div>
|
||||
${toggleRow('approvals-forwardExec', t('communication.approvalsForwardExec'), t('communication.approvalsForwardExecHint'), !!a.enabled)}
|
||||
</div>
|
||||
<div class="config-section" style="margin-top:var(--space-lg)">
|
||||
<div class="config-section-title" style="display:flex;justify-content:space-between;align-items:center">
|
||||
<span>${t('communication.pendingApprovals')}</span>
|
||||
<button class="btn btn-sm btn-secondary" id="btn-refresh-approvals">${t('communication.refreshApprovals')}</button>
|
||||
</div>
|
||||
<div id="approval-queue" style="margin-top:var(--space-sm)">
|
||||
<div class="form-hint">${t('communication.approvalsLoadingQueue')}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
el.querySelectorAll('input, select').forEach(inp => {
|
||||
inp.addEventListener('change', markDirty)
|
||||
})
|
||||
el.querySelector('#btn-refresh-approvals')?.addEventListener('click', () => loadApprovalQueue(el))
|
||||
loadApprovalQueue(el)
|
||||
}
|
||||
|
||||
async function loadApprovalQueue(el) {
|
||||
const container = (el || _page)?.querySelector('#approval-queue')
|
||||
if (!container) return
|
||||
if (!wsClient.connected || !wsClient.gatewayReady) {
|
||||
container.innerHTML = `<div class="form-hint">${esc(t('communication.approvalsGwNotReady'))}</div>`
|
||||
return
|
||||
}
|
||||
container.innerHTML = `<div class="form-hint">${esc(t('communication.approvalsLoadingQueue'))}</div>`
|
||||
let execItems = [], pluginItems = [], unsupported = false
|
||||
try {
|
||||
const [execRes, pluginRes] = await Promise.allSettled([
|
||||
wsClient.execApprovalList(),
|
||||
wsClient.pluginApprovalList(),
|
||||
])
|
||||
if (execRes.status === 'fulfilled') execItems = execRes.value?.approvals || execRes.value?.items || []
|
||||
else {
|
||||
const msg = String(execRes.reason?.message || '').toLowerCase()
|
||||
if (msg.includes('unknown method') || msg.includes('not found')) unsupported = true
|
||||
}
|
||||
if (pluginRes.status === 'fulfilled') pluginItems = pluginRes.value?.approvals || pluginRes.value?.items || []
|
||||
} catch {}
|
||||
|
||||
if (unsupported) {
|
||||
container.innerHTML = `<div class="form-hint" style="color:var(--text-tertiary)">${esc(t('communication.approvalsUnsupported'))}</div>`
|
||||
return
|
||||
}
|
||||
|
||||
const allItems = [
|
||||
...execItems.map(i => ({ ...i, _type: 'exec' })),
|
||||
...pluginItems.map(i => ({ ...i, _type: 'plugin' })),
|
||||
]
|
||||
|
||||
if (!allItems.length) {
|
||||
container.innerHTML = `<div class="form-hint">${esc(t('communication.approvalsQueueEmpty'))}</div>`
|
||||
return
|
||||
}
|
||||
|
||||
container.innerHTML = allItems.map(item => {
|
||||
const id = item.id || item.approvalId || ''
|
||||
const type = item._type === 'plugin' ? 'Plugin' : 'Exec'
|
||||
const cmd = item.command || item.description || item.name || id
|
||||
const status = item.status || 'pending'
|
||||
const ts = item.createdAt || item.timestamp || 0
|
||||
const timeStr = ts ? new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts).toLocaleString() : ''
|
||||
return `<div style="padding:10px 0;border-bottom:1px solid var(--border-primary);display:flex;justify-content:space-between;align-items:center;gap:8px">
|
||||
<div style="min-width:0;flex:1">
|
||||
<div style="font-size:13px"><span class="badge" style="font-size:11px;margin-right:4px">${esc(type)}</span>${esc(cmd)}</div>
|
||||
<div style="font-size:12px;color:var(--text-tertiary);margin-top:2px">${esc(status)}${timeStr ? ' · ' + timeStr : ''}</div>
|
||||
</div>
|
||||
</div>`
|
||||
}).join('')
|
||||
}
|
||||
|
||||
function collectApprovals() {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { api } from '../lib/tauri-api.js'
|
||||
import { toast } from '../components/toast.js'
|
||||
import { t } from '../lib/i18n.js'
|
||||
import { wsClient } from '../lib/ws-client.js'
|
||||
|
||||
let _loadSeq = 0
|
||||
let _selectedAgentId = null // null = default (main)
|
||||
@@ -243,7 +244,12 @@ async function handleInfo(page, name) {
|
||||
detail.innerHTML = `<div class="form-hint" style="margin-top:var(--space-md)">${t('skills.loadingDetail')}</div>`
|
||||
detail.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
try {
|
||||
const skill = await api.skillsInfo(name, _selectedAgentId)
|
||||
let skill = null
|
||||
// 优先 Gateway RPC(可获取 ClawHub 远程详情),回退 Tauri 本地
|
||||
if (wsClient.connected && wsClient.gatewayReady) {
|
||||
try { skill = await wsClient.skillsDetail(name) } catch {}
|
||||
}
|
||||
if (!skill) skill = await api.skillsInfo(name, _selectedAgentId)
|
||||
const s = skill || {}
|
||||
const reqs = s.requirements || {}
|
||||
const miss = s.missing || {}
|
||||
@@ -368,10 +374,20 @@ async function handleStoreSearch(page) {
|
||||
renderStoreItems(results, filtered)
|
||||
return
|
||||
}
|
||||
// 没有索引时走服务端搜索
|
||||
// 没有索引时走服务端搜索(优先 Gateway RPC,回退 Tauri)
|
||||
results.innerHTML = `<div class="form-hint" style="padding:var(--space-sm)">${t('skills.searching')}</div>`
|
||||
try {
|
||||
const items = await api.skillhubSearch(input.value.trim())
|
||||
let items
|
||||
if (wsClient.connected && wsClient.gatewayReady) {
|
||||
try {
|
||||
const res = await wsClient.skillsSearch(input.value.trim(), 30)
|
||||
items = res?.results || []
|
||||
} catch {
|
||||
items = await api.skillhubSearch(input.value.trim())
|
||||
}
|
||||
} else {
|
||||
items = await api.skillhubSearch(input.value.trim())
|
||||
}
|
||||
renderStoreItems(results, items)
|
||||
} catch (e) {
|
||||
results.innerHTML = `<div style="color:var(--error);padding:var(--space-sm)">${t('skills.searchFailed')}: ${esc(e?.message || e)}</div>`
|
||||
|
||||
Reference in New Issue
Block a user