UI 模块:嵌入方式与弹窗交互逻辑(v1.1.0 新增)
September 4, 2026 · View on GitHub
1. 架构总览
v1.1.0 在原有后端插件(cordis 插件,运行于 dsh 进程内)之上新增了浏览器端 UI 模块。一个包、一条 patch 行,两端各取所需:
dsh-autoupdate(单个 npm 包)
├── 后端半边 lib/index.js cordis 插件(检测/武装/回滚,原有逻辑)
├── 前端半边 lib/client.js 设置页 UI(本模块)
└── 通道 lib/channel.js 后端 RPC 通道注册(POST /dsh-autoupdate/*)
装载链:cordis.patch.yml 里的单行 name: dsh-autoupdate → cordis loader 加载后端入口;宿主(dsh-client-modules)扫描到 package.json 的 dsh.client 声明后,把 exports["./client"] 作为浏览器模块伺服到 /plugins/dsh-autoupdate/client.js,由 window.__ModuleLoader__ 懒加载执行。
2. UI 嵌入方式(插槽契约)
设置页是插槽式组合:壳插件(dsh-client-ui-settings-general)声明插槽树,各功能包向插槽注册内容。本插件注册的插槽:
| 插槽 | 类型 | 注册内容 |
|---|---|---|
settings.section | list(设置页左侧导航 + 内容区) | { id: "dsh-autoupdate", order: 100, label, locale } + Section 组件 |
注册代码(lib/client.js 的 apply(ctx)):
ctx.slots.inject("settings.section", () =>
ctx.slots.register({
name: "settings.section",
id: "dsh-autoupdate",
order: 100,
label: labelFn, // 走 locale 服务,失败回落 "自动更新"
locale: "dsh-autoupdate",
}, Section));
ctx.slots.inject(name, fn):等插槽声明出现在账本上再注册——与壳插件的加载顺序解耦。locale: "dsh-autoupdate":壳会给组件注入绑定到该命名空间的t函数;组件同时内置字典兜底(t缺失/抛错时用navigator.language选 zh/en)。- Section 组件用原生 React(
require("react"),ModuleLoader 静态表提供),不依赖 dsh 的组件库(primitives),样式用 dsw 设计变量 + 硬编码回退(var(--dsw-alias-bg-layer-2, #fff))。
3. 前后端通道
不引入 dsh 的 RPC 框架(typert/zod 描述符),直接用宿主连接服务的通用通道注册:
后端(lib/channel.js,cordis 侧):
ctx.inject(["connection"], (cctx) => {
cctx.connection.rpc.handle("/dsh-autoupdate", handler);
});
前端(lib/client.js,浏览器侧)——纯同源 fetch:
fetch(location.origin + "/dsh-autoupdate/autoupdate/check", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "client-request", rpcId, method: "autoupdate/check", payload: {} }),
});
// 响应: { type: "server-response", rpcId, result: { ok, value | error } }
三个端点:
| 端点 | payload | 返回 value |
|---|---|---|
autoupdate/status | {} | { installedVersion, pendingVersion, level, lastCheckAt, channel } |
autoupdate/check | {} | { state: "up-to-date" | "update-available", current, latest, channel, checkedAt } |
autoupdate/apply | { targetVersion } | { armed: true, targetVersion, fromVersion } |
安全边界:
apply的targetVersion必须等于最近一次check解析出的pendingVersion——UI 不能任意指定版本安装。- 通道信任检查(loopback / 同源 Origin / trustedHosts)由宿主连接层统一执行。
- 无
connection服务的 profile(headless/tui)里通道不注册,后端照常工作。
4. 弹窗交互逻辑
点击【检查更新】
│
▼
┌─ checking ────────────────────────► 文案「正在检查更新…」
│ │(POST autoupdate/check)
│ ▼
│ ┌─── state === "update-available" ──► ┌──────────────────────────────┐
│ │ │ 发现新版本 │
│ │ │ 当前版本 X / 最新版本 Y / 通道 │
│ │ │ [确认更新] [取消] │
│ │ └──────┬───────────┬───────────┘
│ │ 点击确认更新 │ │ 点击取消 → 关闭
│ │ ▼
│ │ arming →(POST autoupdate/apply)
│ │ ▼
│ │ armed:「更新已排定:dsh 退出后
│ │ 将自动完成安装与验证,重启后生效」
│ │ + 目标版本行 + [关闭]
│ │
│ └─── state === "up-to-date" ──────► 「当前已是最新版本,暂无可用更新」+ [关闭]
│
└─── 失败(网络/registry/断路器等)──► 红色错误文案 + [关闭]
实现要点:
- 弹窗本体:fixed 遮罩 + 居中面板,
role="dialog",点遮罩或按钮关闭。 - 并发防护:组件内
seq引用计数——每次请求自增,响应回来时序号不匹配直接丢弃(连续点击/关闭后再返回都不会写脏状态)。 - 后端并发防护:
checkForUi复用_checking锁,重复点击返回"已有检测正在进行"。 - armed 之后:更新在 dsh 进程退出后由 detached helper 执行(v1.0.0 原有事务:安装→双验证→失败回滚→断路器记账),UI 不做安装,只做排定。
- 通道暂时不可用:挂载时
autoupdate/status失败会显示提示,仍允许点击检查重试;明确禁用的插件以及正在执行的操作才会禁用按钮。
5. 抗破坏性设计(UI 部分)
| 依赖点 | 失稳后果 | 防护 |
|---|---|---|
settings.section 插槽被移除/改名 | UI 不显示 | slots.inject 永不触发,静默缺席;后端与手动 CLI 流程不受影响 |
| locale 服务 API 变化 | 文案异常 | t 调用全程 try/catch,内置 zh/en 字典兜底 |
| ModuleLoader 中 react 缺席 | 组件无法构建 | factory 内探测,缺失则跳过注册 |
| dsw 设计变量改名 | 样式走样 | 每个变量带硬编码回退值 |
| 通道信封格式(client-request/server-response)变化 | UI 请求 4xx | 前端报错文案明确提示"dsh 版本可能已变更",并给出手动命令 |
| primitives 组件库变化 | — | 不使用(原生 button/div) |
6. 触发规则变更(v1.0.0 → v1.1.0)
新增配置 autoCheck(默认 false):
false(默认):无任何静默检测——启动与周期定时器均不注册;检测只在用户点击【检查更新】时发生。true:恢复 v1.0.0 的启动 30s 首检 + 每 6 小时周期检测。
原有检测/武装/验证/回滚/断路器代码一行未改,仅定时器调度被 autoCheck 门控。想恢复自动检测,在 profile 的 cordis.patch.yml 里:
- id: auto-update
config:
autoCheck: true