dsh-selfupdater 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/settings-card.js +786 -0
- package/cordis.patch.yml +5 -0
- package/lib/index.js +494 -0
- package/lib/plugin-updater.mjs +424 -0
- package/lib/updater.mjs +391 -0
- package/package.json +40 -0
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-selfupdater 浏览器端入口:在设置页注入一张"版本更新"卡片,
|
|
3
|
+
* 卡片内部分为"DSH 更新"与"插件更新"两个小节。
|
|
4
|
+
*
|
|
5
|
+
* 【加载协议 - 重要】DSH 的 ModuleLoader 动态 import 本 bundle 后,
|
|
6
|
+
* 会核对注册表中是否存在本插件的注册记录;bundle 必须在模块执行期间
|
|
7
|
+
* 同步调用 window.__ModuleLoader__.load({ id, factory }) 完成自注册,
|
|
8
|
+
* 否则报 "loaded without registering ... via __ModuleLoader__.load"。
|
|
9
|
+
*
|
|
10
|
+
* 格式完全对照官方 dshmarket@1.29.2 编译产物(client/client.js):
|
|
11
|
+
* window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => { … } });
|
|
12
|
+
* factory 接收宿主注入的 require 函数,用于获取外部依赖
|
|
13
|
+
* (react / @deepseek-ai/dsh-client-runtime 等,由 package.json 中
|
|
14
|
+
* dsh.client.inject 列表声明);内部用 CommonJS module.exports
|
|
15
|
+
* 返回 { name, inject, apply } 三件套。
|
|
16
|
+
*
|
|
17
|
+
* 主题适配策略(亮暗双模式):
|
|
18
|
+
* - 不硬编码任何颜色;所有颜色走 --dshsu-* CSS 变量;
|
|
19
|
+
* - 启动时注入一份 <style>,规则只引用变量;
|
|
20
|
+
* - 通过 MutationObserver 监听宿主根节点的 class/data-theme 变化,
|
|
21
|
+
* 结合 prefers-color-scheme 媒体查询推断当前是亮色还是暗色,
|
|
22
|
+
* 把对应调色板写到根节点的 --dshsu-* 变量上;
|
|
23
|
+
* - 所有小节自动跟随主题,无需各自感知明暗。
|
|
24
|
+
*/
|
|
25
|
+
window.__ModuleLoader__.load({
|
|
26
|
+
id: 'dsh-selfupdater',
|
|
27
|
+
// 工厂函数:宿主传入 require 用于获取注入的外部模块(等价编译前的 import)。
|
|
28
|
+
factory: (require) => {
|
|
29
|
+
const module = { exports: {} };
|
|
30
|
+
const exports = module.exports;
|
|
31
|
+
|
|
32
|
+
const react = require('react');
|
|
33
|
+
// 保留原代码里的 h 短名(createElement 的别名),避免大面积改动。
|
|
34
|
+
const h = react.createElement;
|
|
35
|
+
const useState = react.useState;
|
|
36
|
+
|
|
37
|
+
// ModuleLoader 约定的插件三件套:id、依赖的服务、入口函数。
|
|
38
|
+
// slots:插槽服务(必需);locale:文案服务(缺失时 apply 内部会兜底降级)。
|
|
39
|
+
const name = 'dsh-selfupdater';
|
|
40
|
+
const inject = ['slots', 'locale'];
|
|
41
|
+
|
|
42
|
+
const NS = 'dsh-selfupdater';
|
|
43
|
+
const API_BASE = '/dsh-selfupdater';
|
|
44
|
+
/** 状态轮询间隔(升级进行中 2 秒,空闲 30 秒)。 */
|
|
45
|
+
const POLL_ACTIVE_MS = 2000;
|
|
46
|
+
const POLL_IDLE_MS = 30000;
|
|
47
|
+
/** 插件清单的空闲刷新间隔(比状态轮询慢一档,避免无谓请求)。 */
|
|
48
|
+
const PLUGIN_REFRESH_MS = 60000;
|
|
49
|
+
|
|
50
|
+
/* ------------------------------------------------------------------ *
|
|
51
|
+
* 工具
|
|
52
|
+
* ------------------------------------------------------------------ */
|
|
53
|
+
|
|
54
|
+
/** 调用插件后端接口的统一封装。 */
|
|
55
|
+
async function api(path, options) {
|
|
56
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
57
|
+
headers: { 'content-type': 'application/json' },
|
|
58
|
+
...options,
|
|
59
|
+
});
|
|
60
|
+
const data = await res.json().catch(() => ({}));
|
|
61
|
+
if (!res.ok && res.status !== 202) {
|
|
62
|
+
throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
63
|
+
}
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ------------------------------------------------------------------ *
|
|
68
|
+
* 主题系统:探测宿主亮暗模式并注入 --dshsu-* CSS 变量
|
|
69
|
+
* ------------------------------------------------------------------ */
|
|
70
|
+
|
|
71
|
+
/** 亮色调色板:白底、浅灰边框、高饱和强调色。 */
|
|
72
|
+
const THEME_LIGHT = {
|
|
73
|
+
'--dshsu-surface': '#ffffff',
|
|
74
|
+
'--dshsu-subtle': '#f6f8fa',
|
|
75
|
+
'--dshsu-border': 'rgba(15, 23, 42, 0.10)',
|
|
76
|
+
'--dshsu-muted': 'rgba(15, 23, 42, 0.55)',
|
|
77
|
+
'--dshsu-accent': '#2f6bff',
|
|
78
|
+
'--dshsu-accent-soft': 'rgba(47, 107, 255, 0.10)',
|
|
79
|
+
'--dshsu-success': '#15803d',
|
|
80
|
+
'--dshsu-success-soft': 'rgba(21, 128, 61, 0.10)',
|
|
81
|
+
'--dshsu-danger': '#dc2626',
|
|
82
|
+
'--dshsu-danger-soft': 'rgba(220, 38, 38, 0.10)',
|
|
83
|
+
'--dshsu-warn': '#b45309',
|
|
84
|
+
'--dshsu-warn-soft': 'rgba(217, 119, 6, 0.12)',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** 暗色调色板:半透明表面(适配任意暗色宿主背景)、亮化文字与强调色。 */
|
|
88
|
+
const THEME_DARK = {
|
|
89
|
+
'--dshsu-surface': 'rgba(255, 255, 255, 0.05)',
|
|
90
|
+
'--dshsu-subtle': 'rgba(255, 255, 255, 0.07)',
|
|
91
|
+
'--dshsu-border': 'rgba(255, 255, 255, 0.13)',
|
|
92
|
+
'--dshsu-muted': 'rgba(255, 255, 255, 0.58)',
|
|
93
|
+
'--dshsu-accent': '#7aa2ff',
|
|
94
|
+
'--dshsu-accent-soft': 'rgba(122, 162, 255, 0.16)',
|
|
95
|
+
'--dshsu-success': '#4ade80',
|
|
96
|
+
'--dshsu-success-soft': 'rgba(74, 222, 128, 0.13)',
|
|
97
|
+
'--dshsu-danger': '#f87171',
|
|
98
|
+
'--dshsu-danger-soft': 'rgba(248, 113, 113, 0.14)',
|
|
99
|
+
'--dshsu-warn': '#fbbf24',
|
|
100
|
+
'--dshsu-warn-soft': 'rgba(251, 191, 36, 0.14)',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 读取单个元素上的显式主题标记(data-theme 属性或 dark/light 类)。
|
|
105
|
+
* 返回 true=暗色、false=亮色、undefined=该元素未声明。
|
|
106
|
+
* 额外兼容 Semi Design 的 theme-dark/theme-light 类名。
|
|
107
|
+
*/
|
|
108
|
+
function elementThemeFlag(el) {
|
|
109
|
+
if (!el) return undefined;
|
|
110
|
+
const attr = (el.getAttribute('data-theme') ?? '').toLowerCase();
|
|
111
|
+
if (attr === 'dark' || el.classList?.contains('theme-dark')) return true;
|
|
112
|
+
if (attr === 'light' || el.classList?.contains('theme-light')) return false;
|
|
113
|
+
if (el.classList?.contains('dark')) return true;
|
|
114
|
+
if (el.classList?.contains('light')) return false;
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 兜底手段:沿 DOM 向上找第一个不透明的背景色,按亮度判断明暗。
|
|
120
|
+
* 解决宿主不写任何主题标记、只换背景配色的实现(本次"深色模式下卡片
|
|
121
|
+
* 区域仍是白底黑字"正是探测不到标记导致的)。跳过插件自身节点,
|
|
122
|
+
* 避免读到自家调色板造成"鸡生蛋"死循环。
|
|
123
|
+
*/
|
|
124
|
+
function backgroundIsDark(startEl) {
|
|
125
|
+
let node = startEl;
|
|
126
|
+
while (node && node !== document.documentElement) {
|
|
127
|
+
// 插件自己的卡片/容器不算数,向上找宿主的真实背景。
|
|
128
|
+
if (typeof node.className === 'string' && node.className.includes(NS)) {
|
|
129
|
+
node = node.parentElement;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const bg = getComputedStyle(node).backgroundColor;
|
|
133
|
+
const m = /^rgba?\(([^)]+)\)$/i.exec(bg);
|
|
134
|
+
if (m) {
|
|
135
|
+
const [r, g, b, a = 1] = m[1].split(',').map((v) => parseFloat(v));
|
|
136
|
+
if (a > 0) {
|
|
137
|
+
// 经验加权亮度公式:< 0.45 视为深色底。
|
|
138
|
+
return (0.299 * r + 0.587 * g + 0.114 * b) / 255 < 0.45;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
node = node.parentElement; // 透明背景继续向上
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 推断宿主当前是否为暗色模式。判定顺序:
|
|
148
|
+
* 1. 卡片所在容器 → body → html 逐级找显式主题标记(右侧容器可能单独带 .dark);
|
|
149
|
+
* 2. 系统偏好 prefers-color-scheme;
|
|
150
|
+
* 3. 最终兜底:读宿主实际渲染的背景色亮度。
|
|
151
|
+
*/
|
|
152
|
+
function detectDark() {
|
|
153
|
+
if (typeof document === 'undefined') return false;
|
|
154
|
+
// 从卡片父级开始向上扫(排除卡片本身),找不到再退回 body/html 全局标记。
|
|
155
|
+
const anchor = document.querySelector('.dshsu-card')?.parentElement ?? document.body;
|
|
156
|
+
for (let node = anchor; node; node = node.parentElement) {
|
|
157
|
+
const flag = elementThemeFlag(node);
|
|
158
|
+
if (flag !== undefined) return flag;
|
|
159
|
+
}
|
|
160
|
+
const rootFlag = elementThemeFlag(document.documentElement);
|
|
161
|
+
if (rootFlag !== undefined) return rootFlag;
|
|
162
|
+
if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) return true;
|
|
163
|
+
return backgroundIsDark(anchor ?? document.body);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 已应用到根节点的主题名(防止 Observer 观察到自己写入的 style 造成死循环)。 */
|
|
167
|
+
let appliedTheme = '';
|
|
168
|
+
|
|
169
|
+
/** 把当前主题对应的调色板写到根节点;主题未变化时不重复写入。 */
|
|
170
|
+
function syncThemeVars() {
|
|
171
|
+
if (typeof document === 'undefined') return;
|
|
172
|
+
const theme = detectDark() ? 'dark' : 'light';
|
|
173
|
+
if (theme === appliedTheme) return;
|
|
174
|
+
appliedTheme = theme;
|
|
175
|
+
const palette = theme === 'dark' ? THEME_DARK : THEME_LIGHT;
|
|
176
|
+
for (const [key, value] of Object.entries(palette)) {
|
|
177
|
+
document.documentElement.style.setProperty(key, value);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** 卡片全部静态样式:只引用 --dshsu-* 变量,随主题切换整体换肤。 */
|
|
182
|
+
const CARD_CSS = `
|
|
183
|
+
.dshsu-card{display:grid;gap:12px;padding:16px;border:1px solid var(--dshsu-border);
|
|
184
|
+
border-radius:12px;background:var(--dshsu-surface);max-width:520px}
|
|
185
|
+
.dshsu-head{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
186
|
+
.dshsu-title{display:flex;align-items:center;gap:8px;font-weight:600}
|
|
187
|
+
.dshsu-logo{width:22px;height:22px;border-radius:50%;flex:none;display:flex;align-items:center;
|
|
188
|
+
justify-content:center;background:var(--dshsu-accent-soft);color:var(--dshsu-accent);
|
|
189
|
+
font-size:13px;font-weight:700}
|
|
190
|
+
/* SVG 更新图标:放在圆形底上,颜色继承强调色,随主题自动换肤 */
|
|
191
|
+
.dshsu-icon{flex:none;color:var(--dshsu-accent)}
|
|
192
|
+
.dshsu-chip{font-size:12px;padding:2px 9px;border-radius:999px;border:1px solid var(--dshsu-border);
|
|
193
|
+
color:var(--dshsu-muted);white-space:nowrap}
|
|
194
|
+
.dshsu-chip-new{color:var(--dshsu-warn);background:var(--dshsu-warn-soft);border-color:transparent;font-weight:600}
|
|
195
|
+
.dshsu-rows{display:grid;gap:8px;background:var(--dshsu-subtle);border-radius:8px;padding:10px 12px}
|
|
196
|
+
.dshsu-row{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:13px}
|
|
197
|
+
.dshsu-label{color:var(--dshsu-muted)}
|
|
198
|
+
.dshsu-value{font-weight:600;font-variant-numeric:tabular-nums}
|
|
199
|
+
.dshsu-value-new{color:var(--dshsu-warn)}
|
|
200
|
+
.dshsu-msg{font-size:13px;color:var(--dshsu-muted)}
|
|
201
|
+
.dshsu-msg-ok{color:var(--dshsu-success)}
|
|
202
|
+
.dshsu-msg-bad{color:var(--dshsu-danger)}
|
|
203
|
+
.dshsu-progress{display:flex;align-items:center;gap:10px;font-size:13px}
|
|
204
|
+
.dshsu-spin{width:15px;height:15px;border-radius:50%;flex:none;
|
|
205
|
+
border:2px solid var(--dshsu-accent-soft);border-top-color:var(--dshsu-accent);
|
|
206
|
+
animation:dshsu-rotate .8s linear infinite}
|
|
207
|
+
@keyframes dshsu-rotate{to{transform:rotate(360deg)}}
|
|
208
|
+
.dshsu-steps{display:flex;gap:5px}
|
|
209
|
+
.dshsu-step{height:4px;flex:1;border-radius:2px;background:var(--dshsu-border);transition:background .3s}
|
|
210
|
+
.dshsu-step-done{background:var(--dshsu-accent)}
|
|
211
|
+
.dshsu-step-active{background:var(--dshsu-accent);animation:dshsu-pulse 1.1s ease-in-out infinite}
|
|
212
|
+
@keyframes dshsu-pulse{50%{opacity:.35}}
|
|
213
|
+
.dshsu-actions{display:flex;align-items:center;gap:8px;margin-top:2px}
|
|
214
|
+
.dshsu-spacer{flex:1}
|
|
215
|
+
.dshsu-btn{appearance:none;border:1px solid var(--dshsu-border);border-radius:8px;cursor:pointer;
|
|
216
|
+
padding:6px 14px;font-size:13px;background:transparent;color:inherit;display:inline-flex;
|
|
217
|
+
align-items:center;gap:7px;transition:opacity .15s,transform .05s}
|
|
218
|
+
.dshsu-btn:hover:not(:disabled){background:var(--dshsu-subtle)}
|
|
219
|
+
.dshsu-btn:active:not(:disabled){transform:scale(.98)}
|
|
220
|
+
.dshsu-btn:disabled{cursor:not-allowed;opacity:.45}
|
|
221
|
+
.dshsu-btn-primary:not(:disabled){background:var(--dshsu-accent);border-color:transparent;
|
|
222
|
+
color:#fff;font-weight:600}
|
|
223
|
+
.dshsu-btn-primary:hover:not(:disabled){opacity:.88;background:var(--dshsu-accent)}
|
|
224
|
+
.dshsu-pill{font-size:12px;padding:2px 9px;border-radius:999px;white-space:nowrap}
|
|
225
|
+
.dshsu-pill-ok{color:var(--dshsu-success);background:var(--dshsu-success-soft)}
|
|
226
|
+
.dshsu-pill-bad{color:var(--dshsu-danger);background:var(--dshsu-danger-soft)}
|
|
227
|
+
/* ---- 插件更新卡片专用 ---- */
|
|
228
|
+
.dshsu-plist{display:grid;gap:8px;background:var(--dshsu-subtle);border-radius:8px;padding:10px 12px}
|
|
229
|
+
.dshsu-prow{display:flex;align-items:center;gap:10px;font-size:13px;min-width:0}
|
|
230
|
+
.dshsu-pmain{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}
|
|
231
|
+
.dshsu-pname{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
232
|
+
.dshsu-pver{color:var(--dshsu-muted);font-size:12px;font-variant-numeric:tabular-nums}
|
|
233
|
+
.dshsu-empty{font-size:13px;color:var(--dshsu-muted)}
|
|
234
|
+
.dshsu-pill-new{color:var(--dshsu-warn);background:var(--dshsu-warn-soft)}
|
|
235
|
+
/* ---- 单卡片内两个小节之间的分隔线(随主题换肤) ---- */
|
|
236
|
+
.dshsu-divider{height:1px;background:var(--dshsu-border);margin:4px 0}
|
|
237
|
+
/* 单卡片内的插件小节容器:只负责纵向排布,不画边框(边框属于整张卡片)。 */
|
|
238
|
+
.dshsu-sub{display:grid;gap:12px}
|
|
239
|
+
`;
|
|
240
|
+
|
|
241
|
+
/** 幂等注入 <style>;重复调用只保留第一份。 */
|
|
242
|
+
function injectStyles() {
|
|
243
|
+
if (typeof document === 'undefined') return;
|
|
244
|
+
if (document.getElementById(`${NS}-styles`) !== null) return;
|
|
245
|
+
const style = document.createElement('style');
|
|
246
|
+
style.id = `${NS}-styles`;
|
|
247
|
+
style.textContent = CARD_CSS;
|
|
248
|
+
document.head.appendChild(style);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 持续跟踪宿主主题:卡片祖先链 + body/html 属性 + 系统偏好三条路都监听。
|
|
253
|
+
* 只监听 subtree 上的 class/data-theme 变化(右侧容器单独切主题也能捕获);
|
|
254
|
+
* 自己写入的是 style 属性,不在监听范围内,不会造成死循环。
|
|
255
|
+
*/
|
|
256
|
+
function watchTheme() {
|
|
257
|
+
syncThemeVars();
|
|
258
|
+
if (typeof document === 'undefined') return () => {};
|
|
259
|
+
const observer = new MutationObserver(() => syncThemeVars());
|
|
260
|
+
for (const target of [document.documentElement, document.body]) {
|
|
261
|
+
observer.observe(target, {
|
|
262
|
+
attributes: true,
|
|
263
|
+
attributeFilter: ['class', 'data-theme'],
|
|
264
|
+
subtree: true,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
const media = window.matchMedia?.('(prefers-color-scheme: dark)');
|
|
268
|
+
const onChange = () => syncThemeVars();
|
|
269
|
+
media?.addEventListener?.('change', onChange);
|
|
270
|
+
return () => {
|
|
271
|
+
observer.disconnect();
|
|
272
|
+
media?.removeEventListener?.('change', onChange);
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* ------------------------------------------------------------------ *
|
|
277
|
+
* UI 组件
|
|
278
|
+
* ------------------------------------------------------------------ */
|
|
279
|
+
|
|
280
|
+
/** 阶段文案映射:把后端 state 翻译成用户能看懂的中文。 */
|
|
281
|
+
const STATE_LABELS = {
|
|
282
|
+
idle: '',
|
|
283
|
+
running: '升级进行中…',
|
|
284
|
+
downloading: '正在下载新版本…',
|
|
285
|
+
swapping: '正在替换程序文件…',
|
|
286
|
+
restarting: '正在重启服务…',
|
|
287
|
+
healthcheck: '正在等待服务就绪…',
|
|
288
|
+
rollback: '升级失败,正在回滚…',
|
|
289
|
+
done: '升级完成',
|
|
290
|
+
done_failed: '已回滚到旧版本',
|
|
291
|
+
error: '升级出错',
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/** 升级阶段对应的进度条刻度(0-3),用于渲染四段进度。 */
|
|
295
|
+
const STATE_STEP = { downloading: 0, swapping: 1, restarting: 2, healthcheck: 3 };
|
|
296
|
+
|
|
297
|
+
/** 四段式阶段进度条:已完成实心、进行中呼吸闪烁、未开始灰色。 */
|
|
298
|
+
function StepBar({ state }) {
|
|
299
|
+
const current = STATE_STEP[state];
|
|
300
|
+
if (current === undefined) return null;
|
|
301
|
+
return h('div', { className: 'dshsu-steps' },
|
|
302
|
+
[0, 1, 2, 3].map((i) => h('div', {
|
|
303
|
+
key: i,
|
|
304
|
+
className: i < current ? 'dshsu-step dshsu-step-done'
|
|
305
|
+
: i === current ? 'dshsu-step dshsu-step-active'
|
|
306
|
+
: 'dshsu-step',
|
|
307
|
+
})),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 更新图标:SVG 循环箭头(语义 = 刷新/更新),stroke 用 currentColor
|
|
313
|
+
* 自动跟随文字颜色,亮暗主题都无需额外处理。
|
|
314
|
+
*/
|
|
315
|
+
function UpdateIcon() {
|
|
316
|
+
return h('svg', {
|
|
317
|
+
className: 'dshsu-icon',
|
|
318
|
+
viewBox: '0 0 24 24',
|
|
319
|
+
width: 15,
|
|
320
|
+
height: 15,
|
|
321
|
+
'aria-hidden': true,
|
|
322
|
+
},
|
|
323
|
+
// 上半圈箭头 + 下半圈箭头组成循环,Material Design "autorenew" 造型。
|
|
324
|
+
h('path', {
|
|
325
|
+
d: 'M12 5V2L8 6l4 4V7a5 5 0 1 1-5 5H5a7 7 0 1 0 7-7z',
|
|
326
|
+
fill: 'currentColor',
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* ------------------------------------------------------------------ *
|
|
331
|
+
* 小节标题条:图标 + 名称 + 右侧徽章(单卡片内两个小节共用)
|
|
332
|
+
* ------------------------------------------------------------------ */
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 小节标题:左侧小圆点图标 + 标题文字,右侧可选徽章。
|
|
336
|
+
* 用于把"DSH 更新"和"插件更新"收纳在同一张卡片内分区展示。
|
|
337
|
+
*/
|
|
338
|
+
function SectionHead({ title, badge, badgeNew }) {
|
|
339
|
+
return h('div', { className: 'dshsu-head' },
|
|
340
|
+
h('div', { className: 'dshsu-title' }, h(UpdateIcon), h('span', null, title)),
|
|
341
|
+
badge ?? null,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** 顶部徽章的两种形态:可更新(琥珀色)/ 普通(灰色计数)。 */
|
|
346
|
+
function headBadge(text, isNew) {
|
|
347
|
+
if (text == null) return null;
|
|
348
|
+
return h('span', { className: `dshsu-chip${isNew ? ' dshsu-chip-new' : ''}` }, text);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* 版本更新主卡片(唯一注册到设置页的卡片):
|
|
353
|
+
* 上半部分 = DSH 更新小节;下半部分 = 插件更新小节。
|
|
354
|
+
* 两组状态相互独立、互不阻塞;样式共享同一套 CSS 类与主题变量。
|
|
355
|
+
*/
|
|
356
|
+
function UpdateCard({ t, status, busy, checking, onCheck, onUpgrade,
|
|
357
|
+
plugins, pluginBusy, pluginChecking, pluginMsg, onPluginCheck, onPluginUpgrade }) {
|
|
358
|
+
|
|
359
|
+
const updateAvailable = status?.latestVersion != null
|
|
360
|
+
&& status.latestVersion !== status.currentVersion;
|
|
361
|
+
const stage = STATE_LABELS[status?.state] ?? '';
|
|
362
|
+
const updateCount = plugins.filter((p) => p.updateAvailable).length;
|
|
363
|
+
|
|
364
|
+
// DSH 小节结果消息的语义着色:成功绿 / 失败红 / 其余灰。
|
|
365
|
+
const message = !busy && status?.message ? String(status.message) : '';
|
|
366
|
+
const msgClass = /已是最新|发现新版本|成功|完成/.test(message) ? ' dshsu-msg-ok'
|
|
367
|
+
: /失败|出错|错误/.test(message) || ['error', 'done_failed'].includes(status?.state) ? ' dshsu-msg-bad'
|
|
368
|
+
: '';
|
|
369
|
+
// 插件小节结果消息的语义着色,规则同上。
|
|
370
|
+
const pMsgClass = /已是最新|完成|成功/.test(pluginMsg) ? ' dshsu-msg-ok'
|
|
371
|
+
: /失败|出错|错误|超时/.test(pluginMsg) ? ' dshsu-msg-bad'
|
|
372
|
+
: '';
|
|
373
|
+
|
|
374
|
+
return h('div', { className: 'dshsu-card' },
|
|
375
|
+
/* ============ 小节一:DSH 更新 ============ */
|
|
376
|
+
h(SectionHead, {
|
|
377
|
+
title: t.nav,
|
|
378
|
+
badge: updateAvailable
|
|
379
|
+
? headBadge(`${t.updateAvailable} ${status.latestVersion}`, true)
|
|
380
|
+
: headBadge(status?.currentVersion ?? '—', false),
|
|
381
|
+
}),
|
|
382
|
+
h('div', { className: 'dshsu-rows' },
|
|
383
|
+
h('div', { className: 'dshsu-row' },
|
|
384
|
+
h('span', { className: 'dshsu-label' }, t.currentVersion),
|
|
385
|
+
h('span', { className: 'dshsu-value' }, status?.currentVersion ?? '—'),
|
|
386
|
+
),
|
|
387
|
+
h('div', { className: 'dshsu-row' },
|
|
388
|
+
h('span', { className: 'dshsu-label' }, t.latestVersion),
|
|
389
|
+
h('span', {
|
|
390
|
+
className: updateAvailable ? 'dshsu-value dshsu-value-new' : 'dshsu-value',
|
|
391
|
+
}, status?.latestVersion ?? '—'),
|
|
392
|
+
),
|
|
393
|
+
h('div', { className: 'dshsu-row' },
|
|
394
|
+
h('span', { className: 'dshsu-label' }, t.lastCheck),
|
|
395
|
+
h('span', { className: 'dshsu-value' }, formatTime(status?.lastCheck) ?? t.never),
|
|
396
|
+
),
|
|
397
|
+
),
|
|
398
|
+
busy ? h('div', { role: 'status', style: { display: 'grid', gap: 8 } },
|
|
399
|
+
h('div', { className: 'dshsu-progress' },
|
|
400
|
+
h('span', { className: 'dshsu-spin' }),
|
|
401
|
+
h('span', { style: status?.state === 'rollback' ? { color: 'var(--dshsu-danger)' } : undefined },
|
|
402
|
+
stage || t.processing),
|
|
403
|
+
),
|
|
404
|
+
h(StepBar, { state: status?.state }),
|
|
405
|
+
) : null,
|
|
406
|
+
message !== '' ? h('div', { className: `dshsu-msg${msgClass}` }, message) : null,
|
|
407
|
+
h('div', { className: 'dshsu-actions' },
|
|
408
|
+
h('button', {
|
|
409
|
+
type: 'button',
|
|
410
|
+
className: 'dshsu-btn',
|
|
411
|
+
disabled: busy || checking,
|
|
412
|
+
onClick: onCheck,
|
|
413
|
+
},
|
|
414
|
+
checking ? h('span', { className: 'dshsu-spin' }) : null,
|
|
415
|
+
t.checkUpdate,
|
|
416
|
+
),
|
|
417
|
+
h('button', {
|
|
418
|
+
type: 'button',
|
|
419
|
+
className: updateAvailable && !busy ? 'dshsu-btn dshsu-btn-primary' : 'dshsu-btn',
|
|
420
|
+
disabled: busy || !updateAvailable,
|
|
421
|
+
onClick: onUpgrade,
|
|
422
|
+
}, t.upgradeNow),
|
|
423
|
+
h('span', { className: 'dshsu-spacer' }),
|
|
424
|
+
status?.state === 'done' ? h('span', { className: 'dshsu-pill dshsu-pill-ok' }, t.upgraded)
|
|
425
|
+
: ['error', 'done_failed'].includes(status?.state) ? h('span', { className: 'dshsu-pill dshsu-pill-bad' }, t.failed)
|
|
426
|
+
: null,
|
|
427
|
+
),
|
|
428
|
+
|
|
429
|
+
/* ---- 分隔线:视觉上把 DSH 更新与插件更新分成两区 ---- */
|
|
430
|
+
h('div', { className: 'dshsu-divider' }),
|
|
431
|
+
|
|
432
|
+
/* ============ 小节二:插件更新(复用 PluginSection) ============ */
|
|
433
|
+
h(PluginSection, {
|
|
434
|
+
t,
|
|
435
|
+
plugins,
|
|
436
|
+
busy: pluginBusy,
|
|
437
|
+
checking: pluginChecking,
|
|
438
|
+
msg: pluginMsg,
|
|
439
|
+
onCheck: onPluginCheck,
|
|
440
|
+
onUpgrade: onPluginUpgrade,
|
|
441
|
+
}),
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** ISO 时间转本地短格式;无效输入返回 null。 */
|
|
446
|
+
function formatTime(iso) {
|
|
447
|
+
if (typeof iso !== 'string' || iso === '') return null;
|
|
448
|
+
const date = new Date(iso);
|
|
449
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
450
|
+
return date.toLocaleString(undefined, {
|
|
451
|
+
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* ------------------------------------------------------------------ *
|
|
456
|
+
* 插件更新小节(渲染在"版本更新"卡片下半部分):
|
|
457
|
+
* 列出已装插件 + 检查更新 + 一键全部升级
|
|
458
|
+
* ------------------------------------------------------------------ */
|
|
459
|
+
|
|
460
|
+
/** 插件更新进行中的状态集合(与后端锁文件/state 约定一致)。 */
|
|
461
|
+
const PLUGIN_BUSY_STATES = ['running', 'downloading', 'restarting', 'healthcheck', 'rollback'];
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* 插件列表行:包名 + 当前→最新版本 + 可更新徽章。
|
|
465
|
+
* @param p - 单个插件条目 { name, installedVersion, latestVersion, updateAvailable }
|
|
466
|
+
* @param t - 文案对象
|
|
467
|
+
*/
|
|
468
|
+
function PluginRow({ p, t }) {
|
|
469
|
+
return h('div', { className: 'dshsu-prow' },
|
|
470
|
+
h('div', { className: 'dshsu-pmain' },
|
|
471
|
+
h('span', {
|
|
472
|
+
className: 'dshsu-pname',
|
|
473
|
+
title: p.name,
|
|
474
|
+
}, p.name),
|
|
475
|
+
// 版本行:有新版时显示"当前 → 最新",一眼看出升级去向。
|
|
476
|
+
h('span', { className: 'dshsu-pver' },
|
|
477
|
+
p.updateAvailable && p.latestVersion != null
|
|
478
|
+
? `${p.installedVersion ?? '?'} → ${p.latestVersion}`
|
|
479
|
+
: (p.installedVersion ?? '?'),
|
|
480
|
+
),
|
|
481
|
+
),
|
|
482
|
+
p.updateAvailable
|
|
483
|
+
? h('span', { className: 'dshsu-pill dshsu-pill-new' }, t.updateAvailable)
|
|
484
|
+
: (p.latestVersion != null ? h('span', { className: 'dshsu-pill dshsu-pill-ok' }, t.upToDate) : null),
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* 插件更新小节(由 UpdateCard 内嵌渲染,不再单独注册卡片):
|
|
490
|
+
* 列出已装插件 + 检查更新 + 一键全部升级。
|
|
491
|
+
* 注意:外层容器用轻量 div 而非再套一层 dshsu-card,
|
|
492
|
+
* 避免卡片套卡片的嵌套边框;标题条复用 SectionHead 统一风格。
|
|
493
|
+
* @param props - plugins 插件数组;busy 是否有插件更新在跑;
|
|
494
|
+
* checking 是否正在检查;msg 结果消息;各事件回调
|
|
495
|
+
*/
|
|
496
|
+
function PluginSection({ t, plugins, busy, checking, msg, onCheck, onUpgrade }) {
|
|
497
|
+
const updateCount = plugins.filter((p) => p.updateAvailable).length;
|
|
498
|
+
// 结果消息的语义着色:成功绿 / 失败红 / 其余灰。
|
|
499
|
+
const msgClass = /已是最新|完成|成功/.test(msg) ? ' dshsu-msg-ok'
|
|
500
|
+
: /失败|出错|错误|超时/.test(msg) ? ' dshsu-msg-bad'
|
|
501
|
+
: '';
|
|
502
|
+
|
|
503
|
+
return h('div', { className: 'dshsu-sub' },
|
|
504
|
+
// 小节标题:与 DSH 部分同款 SectionHead,右侧显示计数徽章
|
|
505
|
+
h(SectionHead, {
|
|
506
|
+
title: t.pluginNav,
|
|
507
|
+
badge: updateCount > 0
|
|
508
|
+
? headBadge(`${updateCount} ${t.updatesSuffix}`, true)
|
|
509
|
+
: headBadge(`${plugins.length} ${t.pluginsSuffix}`, false),
|
|
510
|
+
}),
|
|
511
|
+
// 插件清单(空态给提示)
|
|
512
|
+
h('div', { className: 'dshsu-plist' },
|
|
513
|
+
plugins.length > 0
|
|
514
|
+
? plugins.map((p) => h(PluginRow, { key: p.name, p, t }))
|
|
515
|
+
: h('div', { className: 'dshsu-empty' }, t.noPlugins),
|
|
516
|
+
),
|
|
517
|
+
// 更新/检查进行中:spinner + 阶段文案
|
|
518
|
+
busy || checking ? h('div', { role: 'status', className: 'dshsu-progress' },
|
|
519
|
+
h('span', { className: 'dshsu-spin' }),
|
|
520
|
+
h('span', null, busy ? t.pluginUpdating : t.checkingLabel),
|
|
521
|
+
) : null,
|
|
522
|
+
// 空闲时的结果消息
|
|
523
|
+
!busy && !checking && msg !== '' ? h('div', { className: `dshsu-msg${msgClass}` }, msg) : null,
|
|
524
|
+
// 底部操作行:检查更新 + 一键全部升级
|
|
525
|
+
h('div', { className: 'dshsu-actions' },
|
|
526
|
+
h('button', {
|
|
527
|
+
type: 'button',
|
|
528
|
+
className: 'dshsu-btn',
|
|
529
|
+
disabled: busy || checking,
|
|
530
|
+
onClick: onCheck,
|
|
531
|
+
},
|
|
532
|
+
checking ? h('span', { className: 'dshsu-spin' }) : null,
|
|
533
|
+
t.checkUpdate,
|
|
534
|
+
),
|
|
535
|
+
h('button', {
|
|
536
|
+
type: 'button',
|
|
537
|
+
className: updateCount > 0 && !busy ? 'dshsu-btn dshsu-btn-primary' : 'dshsu-btn',
|
|
538
|
+
disabled: busy || checking || updateCount === 0,
|
|
539
|
+
onClick: onUpgrade,
|
|
540
|
+
}, t.upgradeAll),
|
|
541
|
+
h('span', { className: 'dshsu-spacer' }),
|
|
542
|
+
),
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/* ------------------------------------------------------------------ *
|
|
547
|
+
* 宿主挂载
|
|
548
|
+
* ------------------------------------------------------------------ */
|
|
549
|
+
|
|
550
|
+
/** 内置双语字典(宿主 locale 服务缺失时的兜底)。 */
|
|
551
|
+
const FALLBACK_DICT = {
|
|
552
|
+
zh: {
|
|
553
|
+
nav: 'DSH 更新', checkUpdate: '检查更新', upgradeNow: '一键升级',
|
|
554
|
+
currentVersion: '当前版本', latestVersion: '最新版本',
|
|
555
|
+
lastCheck: '上次检查', never: '从未', processing: '处理中…',
|
|
556
|
+
updateAvailable: '可更新', upgraded: '已升级', failed: '失败',
|
|
557
|
+
pluginNav: '插件更新', upgradeAll: '一键全部升级',
|
|
558
|
+
pluginsSuffix: '个插件', updatesSuffix: '个可更新',
|
|
559
|
+
noPlugins: '未发现已安装插件', upToDate: '最新',
|
|
560
|
+
pluginUpdating: '插件更新进行中…', checkingLabel: '正在检查更新…',
|
|
561
|
+
checkFailed: '检查更新失败',
|
|
562
|
+
// 侧边导航文案:现在只有一张"版本更新"卡片。
|
|
563
|
+
cardNav: '版本更新',
|
|
564
|
+
},
|
|
565
|
+
en: {
|
|
566
|
+
nav: 'DSH Update', checkUpdate: 'Check for updates', upgradeNow: 'Upgrade now',
|
|
567
|
+
currentVersion: 'Current', latestVersion: 'Latest',
|
|
568
|
+
lastCheck: 'Last check', never: 'never', processing: 'Working…',
|
|
569
|
+
updateAvailable: 'Update', upgraded: 'Upgraded', failed: 'Failed',
|
|
570
|
+
pluginNav: 'Plugin Updates', upgradeAll: 'Upgrade All',
|
|
571
|
+
pluginsSuffix: ' plugins', updatesSuffix: ' to update',
|
|
572
|
+
noPlugins: 'No installed plugins found', upToDate: 'Latest',
|
|
573
|
+
pluginUpdating: 'Plugin update in progress…', checkingLabel: 'Checking…',
|
|
574
|
+
checkFailed: 'Check failed',
|
|
575
|
+
// Side navigation label: there is now only one "Updates" card.
|
|
576
|
+
cardNav: 'Updates',
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* 浏览器端 apply 入口。
|
|
582
|
+
* @param ctx - 客户端上下文(slots 必需;locale/settingsScope 可选降级)
|
|
583
|
+
*/
|
|
584
|
+
function apply(ctx) {
|
|
585
|
+
const slots = ctx.slots;
|
|
586
|
+
if (slots === undefined || typeof slots.inject !== 'function') {
|
|
587
|
+
console.warn(`[${NS}] 宿主未提供 slots 服务,设置卡片不可用`);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// 样式与主题跟踪只需做一次(两张卡片共享同一套 CSS 类与变量)。
|
|
592
|
+
injectStyles();
|
|
593
|
+
watchTheme();
|
|
594
|
+
|
|
595
|
+
// 文案:宿主有 locale 服务就注册双语字典;没有则用内置兜底。
|
|
596
|
+
let dict = FALLBACK_DICT.zh;
|
|
597
|
+
try {
|
|
598
|
+
ctx.locale?.register?.(NS, FALLBACK_DICT);
|
|
599
|
+
if (typeof ctx.locale?.bind === 'function') {
|
|
600
|
+
const bound = ctx.locale.bind(NS);
|
|
601
|
+
dict = (key) => {
|
|
602
|
+
try {
|
|
603
|
+
const value = bound(key);
|
|
604
|
+
return typeof value === 'string' && value !== '' ? value : FALLBACK_DICT.zh[key];
|
|
605
|
+
} catch {
|
|
606
|
+
return FALLBACK_DICT.zh[key];
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
} catch { /* locale 服务缺失时用内置兜底文案 */ }
|
|
611
|
+
|
|
612
|
+
/** 卡片内部状态(轮询驱动),通过 useState 强制刷新。 */
|
|
613
|
+
let latestStatus = null;
|
|
614
|
+
let checking = false;
|
|
615
|
+
let refresh = () => {};
|
|
616
|
+
|
|
617
|
+
async function pollStatus() {
|
|
618
|
+
try {
|
|
619
|
+
latestStatus = await api('/status');
|
|
620
|
+
} catch { /* 服务重启期间拉不到状态属正常 */ }
|
|
621
|
+
refresh();
|
|
622
|
+
// 升级中高频轮询,空闲低频保活。
|
|
623
|
+
setTimeout(pollStatus, isBusyState(latestStatus) ? POLL_ACTIVE_MS : POLL_IDLE_MS);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function isBusyState(s) {
|
|
627
|
+
return s != null && ['running', 'downloading', 'swapping', 'restarting', 'healthcheck', 'rollback'].includes(s.state);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/* ---------- 插件更新卡片的状态与动作 ---------- */
|
|
631
|
+
|
|
632
|
+
let pluginList = [];
|
|
633
|
+
let pluginBusy = false;
|
|
634
|
+
let pluginMsg = '';
|
|
635
|
+
let pluginChecking = false;
|
|
636
|
+
let pluginRefresh = () => {};
|
|
637
|
+
|
|
638
|
+
/** 拉取插件清单(含上次检查缓存的可更新标记)。 */
|
|
639
|
+
async function pollPlugins() {
|
|
640
|
+
try {
|
|
641
|
+
const data = await api('/plugins');
|
|
642
|
+
pluginList = data.plugins ?? [];
|
|
643
|
+
pluginBusy = data.busy === true || PLUGIN_BUSY_STATES.includes(data.state);
|
|
644
|
+
if (!pluginBusy && typeof data.message === 'string' && data.message !== '') {
|
|
645
|
+
pluginMsg = data.message;
|
|
646
|
+
}
|
|
647
|
+
} catch { /* 服务重启期间拉不到属正常 */ }
|
|
648
|
+
pluginRefresh();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* 插件更新的轮询循环:独立于 DSH 主程序的状态轮询。
|
|
653
|
+
* 更新进行中走 2 秒高频;空闲时降为低频保活。
|
|
654
|
+
*/
|
|
655
|
+
async function pluginPollLoop() {
|
|
656
|
+
await pollPlugins();
|
|
657
|
+
setTimeout(pluginPollLoop, pluginBusy || pluginChecking ? POLL_ACTIVE_MS : PLUGIN_REFRESH_MS);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/** 检查插件更新:POST /plugins/check 成功后立刻重拉清单拿结果。 */
|
|
661
|
+
async function handlePluginCheck() {
|
|
662
|
+
pluginChecking = true;
|
|
663
|
+
pluginMsg = '';
|
|
664
|
+
pluginRefresh();
|
|
665
|
+
try {
|
|
666
|
+
const result = await api('/plugins/check', { method: 'POST', body: '{}' });
|
|
667
|
+
const n = result.updatedCount ?? 0;
|
|
668
|
+
pluginMsg = `检查完成:${n > 0 ? `${n} 个插件有新版本` : '所有插件均已是最新'}`;
|
|
669
|
+
await pollPlugins();
|
|
670
|
+
} catch (err) {
|
|
671
|
+
console.warn(`[${NS}] 插件检查更新失败:`, err);
|
|
672
|
+
pluginMsg = `${dict('checkFailed')}:${err.message}`;
|
|
673
|
+
} finally {
|
|
674
|
+
pluginChecking = false;
|
|
675
|
+
pluginRefresh();
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** 一键升级全部插件:受理成功后进入 2 秒高频轮询等结果。 */
|
|
680
|
+
async function handlePluginUpgrade() {
|
|
681
|
+
try {
|
|
682
|
+
await api('/plugins/update', { method: 'POST', body: '{}' });
|
|
683
|
+
pluginMsg = '';
|
|
684
|
+
// 服务即将退出;进入高频轮询等新进程起来后自动恢复进度展示。
|
|
685
|
+
setTimeout(pluginPollLoop, POLL_ACTIVE_MS);
|
|
686
|
+
} catch (err) {
|
|
687
|
+
console.warn(`[${NS}] 触发插件更新失败:`, err);
|
|
688
|
+
pluginMsg = `触发更新失败:${err.message}`;
|
|
689
|
+
pluginRefresh();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async function handleCheck() {
|
|
694
|
+
checking = true;
|
|
695
|
+
refresh();
|
|
696
|
+
try {
|
|
697
|
+
await api('/check', { method: 'POST', body: '{}' });
|
|
698
|
+
} catch (err) {
|
|
699
|
+
console.warn(`[${NS}] 检查更新失败:`, err);
|
|
700
|
+
// 失败信息立即显示到卡片消息区(随后轮询会用服务端落盘的同样文案覆盖),
|
|
701
|
+
// 避免"点击检测更新毫无反应"的体验。
|
|
702
|
+
latestStatus = { ...latestStatus, state: 'idle', message: `检查更新失败:${err.message}` };
|
|
703
|
+
} finally {
|
|
704
|
+
checking = false;
|
|
705
|
+
await pollStatus();
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async function handleUpgrade() {
|
|
710
|
+
try {
|
|
711
|
+
await api('/perform', { method: 'POST', body: '{}' });
|
|
712
|
+
// 服务即将退出;进入高频轮询等新进程起来后自动恢复。
|
|
713
|
+
setTimeout(pollStatus, POLL_ACTIVE_MS);
|
|
714
|
+
} catch (err) {
|
|
715
|
+
console.warn(`[${NS}] 触发升级失败:`, err);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* 唯一注册到设置页的"版本更新"卡片:
|
|
721
|
+
* 内部分 DSH 更新与插件更新两个小节,共用一个强制刷新开关。
|
|
722
|
+
* (useState 仅用来拿到 setState;两组轮询写状态后统一触发重渲染。)
|
|
723
|
+
*/
|
|
724
|
+
function Card() {
|
|
725
|
+
const [, tick] = useState(0);
|
|
726
|
+
refresh = () => tick((n) => n + 1);
|
|
727
|
+
pluginRefresh = refresh; // 两个小节在同一张卡片里,共享同一次重渲染
|
|
728
|
+
return h(UpdateCard, {
|
|
729
|
+
t: {
|
|
730
|
+
nav: dict('nav'), checkUpdate: dict('checkUpdate'), upgradeNow: dict('upgradeNow'),
|
|
731
|
+
currentVersion: dict('currentVersion'), latestVersion: dict('latestVersion'),
|
|
732
|
+
lastCheck: dict('lastCheck'), never: dict('never'), processing: dict('processing'),
|
|
733
|
+
updateAvailable: dict('updateAvailable'), upgraded: dict('upgraded'), failed: dict('failed'),
|
|
734
|
+
pluginNav: dict('pluginNav'), upgradeAll: dict('upgradeAll'),
|
|
735
|
+
upToDate: dict('upToDate'), noPlugins: dict('noPlugins'),
|
|
736
|
+
pluginsSuffix: dict('pluginsSuffix'), updatesSuffix: dict('updatesSuffix'),
|
|
737
|
+
pluginUpdating: dict('pluginUpdating'), checkingLabel: dict('checkingLabel'),
|
|
738
|
+
},
|
|
739
|
+
status: latestStatus,
|
|
740
|
+
busy: isBusyState(latestStatus),
|
|
741
|
+
checking,
|
|
742
|
+
onCheck: handleCheck,
|
|
743
|
+
onUpgrade: handleUpgrade,
|
|
744
|
+
plugins: pluginList,
|
|
745
|
+
pluginBusy,
|
|
746
|
+
pluginChecking,
|
|
747
|
+
pluginMsg,
|
|
748
|
+
onPluginCheck: handlePluginCheck,
|
|
749
|
+
onPluginUpgrade: handlePluginUpgrade,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// 注册主设置区(与 dshmarket 同款插槽):单张"版本更新"卡片,
|
|
754
|
+
// 内部分 DSH 更新与插件更新两个小节。
|
|
755
|
+
slots.inject('settings.section', () => slots.register({
|
|
756
|
+
name: 'settings.section',
|
|
757
|
+
id: NS,
|
|
758
|
+
order: 45,
|
|
759
|
+
label: () => dict('cardNav'),
|
|
760
|
+
locale: NS,
|
|
761
|
+
}, Card));
|
|
762
|
+
|
|
763
|
+
// 若宿主提供 settingsScope(rc.7+),把同一张卡片再挂到"插件"设置区。
|
|
764
|
+
try {
|
|
765
|
+
ctx.inject?.(['settingsScope'], (scoped) => {
|
|
766
|
+
scoped.slots?.inject?.('settings.plugin.item', () => scoped.slots.register({
|
|
767
|
+
name: 'settings.plugin.item',
|
|
768
|
+
key: NS,
|
|
769
|
+
locale: NS,
|
|
770
|
+
}, () => h(Card)));
|
|
771
|
+
});
|
|
772
|
+
} catch { /* settingsScope 缺失不影响主设置区 */ }
|
|
773
|
+
|
|
774
|
+
// 启动两条独立的轮询循环(DSH 状态 + 插件清单)。
|
|
775
|
+
void pollStatus();
|
|
776
|
+
void pluginPollLoop();
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// 按 dshmarket 编译产物的收尾格式:把三件套逐个挂到 exports 并返回命名空间对象。
|
|
780
|
+
exports.name = name;
|
|
781
|
+
exports.inject = inject;
|
|
782
|
+
exports.apply = apply;
|
|
783
|
+
return module.exports;
|
|
784
|
+
// 关闭 factory 函数体(对应顶部的 factory: (require) => {)。
|
|
785
|
+
}
|
|
786
|
+
});
|