13 KiB
MateClaw Desktop UI 热更新设计
背景
当前 mateclaw-desktop 的运行链路不是“Electron 直接加载 mateclaw-ui”,而是:
- Electron 启动并显示本地 Splash。
- Electron 用内置 JRE 启动
mateclaw-server.jar。 mateclaw-ui已提前构建到mateclaw-server/src/main/resources/static。BrowserWindow最终加载http://localhost:{动态端口}(由主进程在启动时随机选取)。
这意味着:
- 现在的 UI 资源和后端 JAR 强绑定。
- 任何
mateclaw-ui改动,都要重新打包mateclaw-server.jar,再跟着桌面安装包一起发布。 - 现有
electron-updater只能做“整包升级”,不能做“仅 UI 升级”。
因此这里要解决的问题,不是开发态 HMR,而是生产态 OTA:让 mateclaw-ui 可以脱离桌面安装包独立更新。
目标
- 支持
mateclaw-ui独立于mateclaw-desktop发布。 - UI 更新不要求用户下载新的桌面安装包。
- 保持当前本地
localhost架构,不把桌面应用直接改成远程网站壳子。 - 更新失败可回滚到内置 UI。
- 不影响现有
electron-updater的整包升级能力。
非目标
- 第一阶段不做后端 JAR 热更新。
- 第一阶段不允许 UI 任意突破当前后端 API 边界。
- 第一阶段不替换现有 Electron Splash / 自动升级通道。
现状约束
1. UI 当前被打进 Spring Boot JAR
mateclaw-ui 构建输出目录目前是:
../mateclaw-server/src/main/resources/static
也就是 UI 构建产物直接进入后端静态资源目录,最终随 JAR 发布。
2. Desktop 最终加载的是后端地址
mateclaw-desktop 主窗口业务页当前加载:
http://localhost:{动态端口}(端口由 Electron 主进程在启动时随机分配)
所以 UI 热更新不能只改 Electron dist,必须让后端在运行时能切换静态资源来源。
3. 现有整包升级已存在
桌面端已经接入 electron-updater,并通过 GitHub Releases 发布整包升级。
因此新方案应当与它并存:
- Shell / JRE / JAR 升级:继续走
electron-updater - 纯前端升级:新增 UI OTA 通道
方案结论
推荐采用:
方案 A:外置 UI Bundle + 本地优先加载 + Manifest 驱动的 OTA 更新
核心思路:
mateclaw-ui产出独立的静态包(zip)。mateclaw-desktop在启动时检查 UI 更新 Manifest。- 下载并校验新的 UI 包后,解压到
userData/ui-bundles/<version>/。 - Electron 启动后端时,通过环境变量把“当前启用的 UI 目录”传给 Spring Boot。
- Spring Boot 优先从外部目录提供静态资源;若外部目录不存在或损坏,则回退到 JAR 内置
classpath:/static/。 - UI 更新完成后刷新窗口即可生效,无需安装新桌面包。
这是对当前架构改动最小、兼容性最强的路径。
为什么不选其他方案
方案 B:Electron 直接加载远程站点
不推荐作为主方案。
缺点:
- 桌面应用退化成网站壳子,离线能力明显变差。
- 安全面更大,远程页面注入风险更高。
- 当前路由是
history模式,本地与远程混用会增加协议、资源路径和鉴权处理复杂度。 - 对现有
localhost + Spring Boot架构破坏过大。
方案 C:把 UI 更新继续塞进 JAR 差分包
不满足目标。
原因:
- UI 仍与后端耦合。
- 每次 UI 变更都要重新发 JAR 和桌面安装包。
- 无法做到真正的“仅 UI 热更新”。
目标架构
Electron Shell
├── Splash UI(本地 dist)
├── UI Update Manager(新增)
├── Bundled JRE
├── mateclaw-server.jar
└── BrowserWindow → http://localhost:{动态端口}
├── 优先读取 userData/ui-bundles/current/
└── fallback 到 classpath:/static/
运行时目录建议:
~/Library/Application Support/MateClaw/ # macOS 示例
├── data/
├── ui-bundles/
│ ├── current.json
│ ├── 1.0.3+20260405/
│ │ ├── index.html
│ │ ├── assets/...
│ │ └── meta.json
│ └── 1.0.4+20260410/
└── logs/
关键设计
1. UI 包格式
每个 UI 发布产物建议包含:
index.htmlassets/*logo/*icons/*meta.json
meta.json 示例:
{
"uiVersion": "1.0.4",
"buildId": "20260410.1",
"minDesktopVersion": "1.0.0",
"minServerApiVersion": "1.0",
"maxServerApiVersion": "1.x",
"sha256": "..."
}
说明:
uiVersion:前端语义版本。buildId:构建批次,便于排查。minDesktopVersion:限制旧 Electron shell。minServerApiVersion/maxServerApiVersion:约束 UI 与当前后端 API 兼容性。
2. 更新 Manifest
桌面端不直接猜测最新版本,而是请求一个 Manifest。
建议格式:
{
"channel": "stable",
"latest": {
"uiVersion": "1.0.4",
"buildId": "20260410.1",
"url": "https://download.example.com/mateclaw/ui/1.0.4/ui-bundle.zip",
"sha256": "..."
},
"minimumDesktopVersion": "1.0.0",
"compatibleServerApi": "1.x",
"signature": "base64..."
}
Manifest 最好放在稳定的静态地址,不要依赖 GitHub API 动态查询 release 列表。
发布源建议优先级:
- 自有 CDN / OSS / COS / R2
- GitHub Releases 直链
如果主要用户在国内,建议不要把 GitHub 当唯一源。
3. Spring Boot 静态资源加载改造
需要在 mateclaw-server 中新增静态资源优先级:
- 外部目录
file:${mateclaw.ui.dir}/ - 内置资源
classpath:/static/
实现建议:
- 新增配置项
mateclaw.ui.dir - 在
WebMvcConfigurer中注册资源处理器 - 对
/assets/**、/icons/**、/logo/**、/favicon.ico、/index.html和 SPA 路由统一转发 - 当外部目录不存在时自动回退内置资源
这样 BrowserWindow 仍然访问 http://localhost:{动态端口},但内容已经可由外置 UI 包覆盖。
4. Electron 侧 UI Update Manager
新增一个独立的 UI 更新管理器,职责:
- 读取当前启用 UI 版本。
- 拉取远程 Manifest。
- 判定兼容性。
- 下载 zip。
- 校验
sha256与签名。 - 解压到临时目录。
- 原子切换
current.json或current软链接。 - 通知渲染层“有可用 UI 更新”或“更新已完成”。
建议时机:
- 启动后 3 到 10 秒后台检查
- 用户手动点击“检查前端更新”
- 设置页允许切换更新通道(stable / beta)
5. 激活策略
建议采用“两阶段激活”:
启动前已下载完成
- Electron 在启动 Java 前先解析当前 UI 指针
- 将
MATECLAW_UI_DIR注入到 Java 进程环境变量 - 本次启动直接加载新 UI
运行中下载完成
- 下载成功后先不杀后端
- 标记“下次重启生效”是最稳妥方案
- 若要做到即时生效,可尝试:
- 切换
current指针 - 通知前端
window.location.reload()
- 切换
第一阶段推荐:
下载后提示“重启应用以应用前端更新”
原因是:
- 简化缓存一致性问题
- 避免运行态资源引用一半新一半旧
- 降低与长连接、SSE、登录态的耦合风险
6. 回滚策略
至少支持三层回退:
- 下载失败:保持当前 UI
- 解压或校验失败:丢弃新包,保持当前 UI
- 新 UI 启动异常:回退到上一版本 UI,最差回退到 JAR 内置 UI
建议机制:
current.json记录当前版本、上一版本、状态- UI 启动成功后,通过 preload IPC 上报“本次版本已健康启动”;如果后续选择 REST 方案,可新增
/api/v1/system/ui/boot-ok(当前源码尚未提供该端点) - 若启动后短时间内崩溃或白屏,下次启动自动回滚上一版本
7. 安全要求
UI 热更新本质上是在本地执行新的前端资源,必须做完整校验。
最低要求:
- HTTPS 下载
sha256校验- Manifest 签名校验
推荐增加:
- 使用 Ed25519 公钥验签
- 公钥随桌面端内置
- 不允许跳过签名校验加载生产更新包
否则该通道会成为远程代码注入入口。
8. 缓存与资源路径
需要处理几个细节:
index.html不应长缓存assets/*可使用内容 hash 长缓存- 外部 UI 包目录最好按版本隔离
current只做版本指针,不直接覆盖旧目录
这和 Vite 的产物模式天然兼容。
9. 版本兼容规则
建议明确一条产品规则:
- 只涉及 UI 表现、交互、文案、前端容错的变更,可以走 UI OTA
- 需要新增/修改后端 API、数据库结构、JRE 资源、Electron 权限能力的变更,必须走桌面整包升级
否则很容易出现:
- UI 已升级
- 本地 JAR 太旧
- 页面调用了不存在的 API
所以 Manifest 中必须带兼容约束。
发布链路设计
当前链路
mateclaw-ui构建到后端static/- Maven 打 JAR
- Electron 打包安装包
- GitHub Releases 发布
新链路
桌面整包发布
继续保持现状:
- 构建 UI
- 打进 JAR
- 打包桌面安装包
- 走
electron-updater
UI 独立发布
新增:
mateclaw-ui单独构建到临时目录- 生成
meta.json - 打 zip
- 计算
sha256 - 生成并发布
ui-manifest.json - 上传到 CDN / Release 资产
这样:
- 新装用户依然有 JAR 内置 UI 可用
- 老用户可在后续自动收到 UI OTA
推荐实施阶段
Phase 1:基础可用
目标:
- 支持下载 UI 包
- 支持启动时优先加载外部 UI
- 支持失败回退到内置 UI
- 下载完成后“下次重启生效”
需要改动:
mateclaw-server- 支持外部静态目录优先级
- 提供 SPA fallback
mateclaw-desktop- 增加 UI Update Manager
- 增加 Manifest 拉取、下载、校验、解压、指针切换
- 将 UI 版本信息暴露给 Splash / 设置页
mateclaw-ui- 构建时生成
meta.json - 设置页增加当前 UI 版本展示
- 构建时生成
这是最值得先落地的一版。
Phase 2:产品化
目标:
- 设置页支持“检查前端更新”
- 提示更新说明
- 支持 stable / beta 通道
- 支持启动失败自动回滚
Phase 3:增强体验
目标:
- 部分场景无需整应用重启即可刷新 UI
- 支持灰度发布
- 支持按平台分发不同 UI 包
建议新增模块
mateclaw-desktop
建议新增:
electron/main/ui-updater.tselectron/main/ui-runtime.ts
职责拆分:
ui-updater.ts:远程检查、下载、校验、解压、切换ui-runtime.ts:读取当前 UI 指针、提供给 Java 进程环境变量
mateclaw-server
建议新增:
vip.mate.config.ExternalUiPropertiesvip.mate.config.UiResourceConfigvip.mate.system.controller.DesktopRuntimeController
职责:
- 配置外部 UI 目录
- 注册静态资源与 SPA fallback
- 对前端暴露当前运行版本信息
mateclaw-ui
建议新增:
- 构建脚本:生成
meta.json - 设置页:显示
- Desktop 版本
- UI 版本
- 后端版本
- 更新通道
- 最近检查时间
接口建议
桌面 preload / IPC 可以新增:
uiUpdater.getState()uiUpdater.check()uiUpdater.download()uiUpdater.applyOnRestart()
后端接口建议新增(当前源码尚未提供):
- 建议新增:
GET /api/v1/runtime/version(当前源码尚未提供)
返回示例:
{
"desktopVersion": "1.0.0",
"serverVersion": "1.0.0",
"serverApiVersion": "1.0",
"uiVersion": "1.0.4",
"uiSource": "external"
}
这样前端可以明确展示当前正在跑的是哪个 UI 包。
风险点
1. 前后端版本漂移
这是最大风险。
控制手段:
- UI Manifest 增加兼容性约束
- 约定 API 破坏性变更只能走整包升级
2. 白屏回滚不完善
如果只做下载和切换,不做启动成功确认,坏包可能导致用户持续白屏。
所以至少要有:
- 上一版本指针
- 启动健康上报
3. 更新源可达性
若继续完全依赖 GitHub Releases,国内网络环境下成功率可能不稳定。
建议尽快切到稳定 CDN。
4. 安全边界扩大
远程前端资源可执行,签名和 hash 校验是必须项,不是可选优化。
最小落地建议
如果现在就开始做,建议按下面顺序推进:
- 先改
mateclaw-server,让它支持“外部目录覆盖 classpath static”。 - 再改
mateclaw-desktop,把 UI 包下载到userData/ui-bundles/,并通过环境变量传给 Java。 - 再补 Manifest、签名校验和版本展示。
- 最后再做“运行中更新提示”和“自动回滚”。
结论
对当前 MateClaw 架构,最合适的不是把桌面端改成远程站点壳,而是:
保留 Electron + localhost Spring Boot 架构,引入“外置 UI Bundle 覆盖内置 static 资源”的 OTA 机制。
这样可以:
- 保持离线可用
- 最大限度复用现有桌面端架构
- 将 UI 发布频率从桌面整包中解耦
- 把风险控制在“前端资源替换”这一层
第一阶段建议做到:
- 启动时自动检查 UI 更新
- 后台下载
- 校验后写入外部目录
- 下次重启生效
- 失败自动回退到 JAR 内置 UI
这版最稳,也最容易在现有代码上渐进落地。