@aaayeaaa-p1ws/dsh-plugin-auto-update 0.0.0-stage → 1.1.1

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.js ADDED
@@ -0,0 +1,713 @@
1
+ 'use strict';
2
+ // Client half of @local/dsh-auto-update:
3
+ // 1) shell.overlay 弹窗 —— 每天第一次打开 DSH 时报告这次更新装了什么/哪里失败,
4
+ // 失败原因是「插件正在使用中」时给一键「关闭 → 更新 → 重启 DSH」。
5
+ // 2) settings.section 设置页 —— 检查时机(打开时 / 每天定时 / 每 N 小时)、
6
+ // 自动检查与自动安装开关、更新日志、立即检查。
7
+ // 所有数据都来自 Host 的 /dsh-auto-update/*(见 index.js 的路由表)。
8
+ window.__ModuleLoader__.load({
9
+ id: '@local/dsh-auto-update',
10
+ factory(require) {
11
+ const React = require('react');
12
+ const h = React.createElement;
13
+ const { useState, useEffect, useRef, useCallback } = React;
14
+
15
+ const API = '/dsh-auto-update';
16
+
17
+ async function api(path, init) {
18
+ const res = await fetch(API + path, {
19
+ credentials: 'same-origin',
20
+ cache: 'no-store',
21
+ headers: init && init.body ? { 'content-type': 'application/json' } : undefined,
22
+ ...init,
23
+ });
24
+ let body = null;
25
+ try {
26
+ body = await res.json();
27
+ } catch {
28
+ body = null;
29
+ }
30
+ if (!res.ok) {
31
+ const message = (body && body.error) || `HTTP ${res.status}`;
32
+ throw new Error(message);
33
+ }
34
+ return body;
35
+ }
36
+
37
+ const fmtTime = (iso) => {
38
+ if (!iso) return '';
39
+ const d = new Date(iso);
40
+ if (Number.isNaN(d.getTime())) return String(iso);
41
+ const pad = (n) => String(n).padStart(2, '0');
42
+ return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
43
+ };
44
+
45
+ const S = {
46
+ backdrop: {
47
+ position: 'fixed',
48
+ inset: 0,
49
+ background: 'rgba(0, 0, 0, 0.45)',
50
+ display: 'flex',
51
+ alignItems: 'center',
52
+ justifyContent: 'center',
53
+ zIndex: 1200,
54
+ padding: 24,
55
+ },
56
+ card: {
57
+ width: 'min(560px, 100%)',
58
+ maxHeight: '80vh',
59
+ overflow: 'auto',
60
+ background: 'var(--dsw-alias-bg-layer-2, #1f2329)',
61
+ color: 'var(--dsw-alias-label-primary, #e6e8eb)',
62
+ border: '1px solid var(--dsw-alias-border-l1, rgba(255,255,255,0.12))',
63
+ borderRadius: 12,
64
+ padding: '18px 20px',
65
+ boxShadow: '0 18px 48px rgba(0, 0, 0, 0.35)',
66
+ fontSize: 13,
67
+ lineHeight: 1.6,
68
+ },
69
+ title: { fontSize: 15, fontWeight: 600, marginBottom: 4 },
70
+ sub: { fontSize: 12, color: 'var(--dsw-alias-label-secondary, #9aa4b6)', marginBottom: 12 },
71
+ row: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' },
72
+ ok: { color: 'var(--dsw-alias-state-success-primary, #3fb950)' },
73
+ bad: { color: 'var(--dsw-alias-state-error-primary, #f85149)' },
74
+ warn: { color: 'var(--dsw-alias-state-warn-primary, #d29922)' },
75
+ list: { margin: '6px 0 12px', padding: 0, listStyle: 'none' },
76
+ item: { padding: '4px 0', borderBottom: '1px solid var(--dsw-alias-border-l1, rgba(255,255,255,0.08))' },
77
+ // 按钮统一走「显式前景色 + 可见背景」,不要用 color: inherit / transparent:
78
+ // 设置页/弹窗里按钮继承到的前景色在深色主题下会和背景糊成一片。
79
+ btn: {
80
+ appearance: 'none',
81
+ WebkitAppearance: 'none',
82
+ border: '1px solid var(--dsw-alias-border-l2, rgba(255,255,255,0.28))',
83
+ background: 'var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.12))',
84
+ color: 'var(--dsw-alias-label-primary, #e8eaed)',
85
+ borderRadius: 8,
86
+ padding: '6px 14px',
87
+ fontSize: 13,
88
+ fontFamily: 'inherit',
89
+ lineHeight: 1.45,
90
+ fontWeight: 500,
91
+ textShadow: 'none',
92
+ cursor: 'pointer',
93
+ opacity: 1,
94
+ },
95
+ primary: {
96
+ appearance: 'none',
97
+ WebkitAppearance: 'none',
98
+ border: '1px solid var(--dsw-alias-brand-primary, #4c8dff)',
99
+ background: 'var(--dsw-alias-brand-primary, #4c8dff)',
100
+ color: 'var(--dsw-alias-bg-base, #0f1115)',
101
+ borderRadius: 8,
102
+ padding: '6px 14px',
103
+ fontSize: 13,
104
+ fontFamily: 'inherit',
105
+ lineHeight: 1.45,
106
+ fontWeight: 600,
107
+ textShadow: 'none',
108
+ cursor: 'pointer',
109
+ opacity: 1,
110
+ },
111
+ section: { marginBottom: 18 },
112
+ h3: { fontSize: 13, fontWeight: 600, margin: '0 0 6px' },
113
+ hint: { fontSize: 12, color: 'var(--dsw-alias-label-secondary, #9aa4b6)', marginBottom: 8 },
114
+ field: { display: 'flex', alignItems: 'center', gap: 10, margin: '6px 0', flexWrap: 'wrap' },
115
+ input: {
116
+ background: 'var(--dsw-alias-bg-layer-1, rgba(255,255,255,0.06))',
117
+ color: 'var(--dsw-alias-label-primary, #e8eaed)',
118
+ border: '1px solid var(--dsw-alias-border-l1, rgba(255,255,255,0.22))',
119
+ borderRadius: 6,
120
+ padding: '4px 8px',
121
+ fontSize: 13,
122
+ fontFamily: 'inherit',
123
+ },
124
+ log: {
125
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
126
+ fontSize: 11.5,
127
+ color: 'var(--dsw-alias-label-primary, #e8eaed)',
128
+ background: 'var(--dsw-alias-bg-layer-1, rgba(255,255,255,0.06))',
129
+ border: '1px solid var(--dsw-alias-border-l1, rgba(255,255,255,0.1))',
130
+ borderRadius: 8,
131
+ padding: '8px 10px',
132
+ maxHeight: 260,
133
+ overflow: 'auto',
134
+ whiteSpace: 'pre-wrap',
135
+ },
136
+ };
137
+
138
+ const describeFailure = (item) => {
139
+ const reason = item.reason || item.message || item.code || '未知原因';
140
+ return item.inUse ? `插件正在使用中 —— ${reason}` : String(reason);
141
+ };
142
+
143
+ // ------------------------------------------------------------------ 弹窗
144
+ function UpdatePopup() {
145
+ const [data, setData] = useState(null);
146
+ const [error, setError] = useState(null);
147
+ const [busy, setBusy] = useState(false);
148
+ const [fix, setFix] = useState(null);
149
+ const [hidden, setHidden] = useState(false);
150
+ const pollRef = useRef(null);
151
+
152
+ const load = useCallback(async () => {
153
+ try {
154
+ const payload = await api('/status');
155
+ setData(payload);
156
+ if (payload && payload.fix) setFix(payload.fix);
157
+ setError(null);
158
+ return payload;
159
+ } catch (err) {
160
+ setError(String(err && err.message ? err.message : err));
161
+ return null;
162
+ }
163
+ }, []);
164
+
165
+ useEffect(() => {
166
+ void load();
167
+ // 开机那轮检查在宿主启动约 12 秒后才跑,所以这里轮询得勤一点,装完就能弹出来。
168
+ const timer = setInterval(() => {
169
+ if (document.visibilityState === 'visible') void load();
170
+ }, 20 * 1000);
171
+ return () => clearInterval(timer);
172
+ }, [load]);
173
+
174
+ useEffect(() => () => {
175
+ if (pollRef.current) clearInterval(pollRef.current);
176
+ }, []);
177
+
178
+ const ack = useCallback(async () => {
179
+ setHidden(true);
180
+ try {
181
+ await api('/popup/ack', { method: 'POST', body: '{}' });
182
+ } catch {
183
+ /* 记录失败不影响关闭 */
184
+ }
185
+ }, []);
186
+
187
+ const startFix = useCallback(async () => {
188
+ setBusy(true);
189
+ setError(null);
190
+ try {
191
+ await api('/fix', { method: 'POST', body: '{}' });
192
+ } catch (err) {
193
+ setError(String(err && err.message ? err.message : err));
194
+ }
195
+ // 修复过程在 Host 侧跑(关插件 → 更新 → 重启),这边只负责把步骤刷出来。
196
+ const started = Date.now();
197
+ if (pollRef.current) clearInterval(pollRef.current);
198
+ pollRef.current = setInterval(async () => {
199
+ const payload = await load();
200
+ const job = payload && payload.fix;
201
+ if ((job && job.done) || (job && job.error) || Date.now() - started > 5 * 60 * 1000) {
202
+ clearInterval(pollRef.current);
203
+ pollRef.current = null;
204
+ setBusy(false);
205
+ }
206
+ }, 1500);
207
+ }, [load]);
208
+
209
+ if (hidden) return null;
210
+ const popup = data && data.popup;
211
+ if (!popup || popup.show !== true) return null;
212
+
213
+ const result = popup.result || {};
214
+ const applied = Array.isArray(result.applied) ? result.applied : [];
215
+ const failed = Array.isArray(result.failed) ? result.failed : [];
216
+ const deferred = Array.isArray(result.deferred) ? result.deferred : [];
217
+ const blocked = failed.filter((item) => item && item.inUse === true);
218
+ const steps = (fix && Array.isArray(fix.steps) ? fix.steps : []).filter(Boolean);
219
+
220
+ return h('div', { style: S.backdrop, role: 'dialog', 'aria-modal': 'true' },
221
+ h('div', { style: S.card },
222
+ h('div', { style: { ...S.row, justifyContent: 'space-between' } },
223
+ h('div', { style: S.title }, '插件自动更新'),
224
+ h('button', { style: S.btn, onClick: () => void ack() }, '×'),
225
+ ),
226
+ h('div', { style: S.sub }, `${fmtTime(popup.at) || ''} 检查了 ${result.checked || 0} 个插件,${applied.length} 个已更新`),
227
+
228
+ applied.length
229
+ ? h('div', null,
230
+ h('div', { style: { ...S.h3, ...S.ok } }, `已更新 ${applied.length} 个`),
231
+ h('ul', { style: S.list }, applied.map((item, index) =>
232
+ h('li', { key: `${item.name}-${index}`, style: S.item },
233
+ h('span', null, item.name),
234
+ h('span', { style: { color: 'var(--dsw-alias-label-secondary, #9aa4b6)' } },
235
+ ` ${item.from || '旧版本'} → ${item.to || '新版本'}`),
236
+ ),
237
+ )),
238
+ )
239
+ : null,
240
+
241
+ failed.length
242
+ ? h('div', null,
243
+ h('div', { style: { ...S.h3, ...S.bad } }, `更新失败 ${failed.length} 个`),
244
+ h('ul', { style: S.list }, failed.map((item, index) =>
245
+ h('li', { key: `${item.name}-${index}`, style: S.item },
246
+ h('span', null, item.name),
247
+ h('span', { style: item.inUse ? S.warn : S.bad }, ` ${describeFailure(item)}`),
248
+ ),
249
+ )),
250
+ )
251
+ : null,
252
+
253
+ deferred.length
254
+ ? h('div', { style: { ...S.hint, marginTop: 4 } },
255
+ `延后重试:${deferred.map((item) => item.name).filter(Boolean).join('、')}(市场正忙或 agent 在跑)`)
256
+ : null,
257
+
258
+ steps.length
259
+ ? h('div', { style: { marginTop: 10 } },
260
+ h('div', { style: S.h3 }, '正在处理'),
261
+ h('ul', { style: S.list }, steps.map((step, index) =>
262
+ h('li', { key: index, style: S.item },
263
+ step.name
264
+ ? `${step.name}:${step.step === 'disable' ? (step.ok === false ? '关闭失败' : '已关闭') : step.step}`
265
+ : step.step === 'update'
266
+ ? `重新更新:装上 ${(step.applied || []).length} 个,失败 ${(step.failed || []).length} 个`
267
+ : step.step === 'restart'
268
+ ? step.ok === false
269
+ ? `未重启:${step.reason || '没有需要重启的变化'}`
270
+ : '已安排重启 DeepSeek Harness'
271
+ : String(step.step || ''),
272
+ ),
273
+ )),
274
+ )
275
+ : null,
276
+
277
+ error ? h('div', { style: { ...S.bad, marginTop: 8 } }, error) : null,
278
+ fix && fix.error ? h('div', { style: { ...S.bad, marginTop: 8 } }, fix.error) : null,
279
+
280
+ h('div', { style: { ...S.row, justifyContent: 'flex-end', marginTop: 14 } },
281
+ blocked.length && !busy
282
+ ? h('button', { style: S.primary, onClick: () => void startFix() },
283
+ '关闭插件并更新,然后重启 DSH')
284
+ : null,
285
+ busy ? h('span', { style: S.hint }, '处理中…') : null,
286
+ h('button', { style: S.btn, onClick: () => void ack(), disabled: busy }, '稍后'),
287
+ ),
288
+ ),
289
+ );
290
+ }
291
+
292
+ // ------------------------------------------------------------------ 设置页
293
+ // 版式照 DSH 原生设置页:卡片 + 行(左边标签/说明,右边控件),行间一条细分隔线。
294
+ const T = {
295
+ card: {
296
+ background: 'var(--dsw-alias-bg-layer-1, rgba(255,255,255,0.04))',
297
+ border: '1px solid var(--dsw-alias-border-l1, rgba(255,255,255,0.1))',
298
+ borderRadius: 10,
299
+ padding: '0 16px',
300
+ margin: '0 0 14px',
301
+ },
302
+ cardTitle: {
303
+ fontSize: 14,
304
+ fontWeight: 600,
305
+ color: 'var(--dsw-alias-label-primary, #e8eaed)',
306
+ margin: '14px 0 2px',
307
+ lineHeight: 1.5,
308
+ },
309
+ cardHint: {
310
+ fontSize: 12.5,
311
+ color: 'var(--dsw-alias-label-secondary, #9aa4b6)',
312
+ margin: '0 0 12px',
313
+ lineHeight: 1.6,
314
+ },
315
+ row: {
316
+ display: 'flex',
317
+ alignItems: 'center',
318
+ gap: 16,
319
+ padding: '13px 0',
320
+ borderTop: '1px solid var(--dsw-alias-border-l1, rgba(255,255,255,0.08))',
321
+ },
322
+ label: {
323
+ fontSize: 13.5,
324
+ fontWeight: 500,
325
+ color: 'var(--dsw-alias-label-primary, #e8eaed)',
326
+ lineHeight: 1.5,
327
+ },
328
+ hint: {
329
+ fontSize: 12.5,
330
+ color: 'var(--dsw-alias-label-secondary, #9aa4b6)',
331
+ marginTop: 3,
332
+ lineHeight: 1.55,
333
+ },
334
+ control: { marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 },
335
+ select: {
336
+ appearance: 'none',
337
+ WebkitAppearance: 'none',
338
+ minWidth: 128,
339
+ padding: '7px 30px 7px 12px',
340
+ borderRadius: 8,
341
+ border: '1px solid var(--dsw-alias-border-l1, rgba(255,255,255,0.16))',
342
+ background:
343
+ 'var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.08)) url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'10\' height=\'6\'><path d=\'M1 1l4 4 4-4\' fill=\'none\' stroke=\'%238b93a1\' stroke-width=\'1.5\'/></svg>") no-repeat right 11px center',
344
+ color: 'var(--dsw-alias-label-primary, #e8eaed)',
345
+ fontSize: 13,
346
+ fontFamily: 'inherit',
347
+ lineHeight: 1.45,
348
+ cursor: 'pointer',
349
+ },
350
+ input: {
351
+ width: 96,
352
+ padding: '7px 10px',
353
+ borderRadius: 8,
354
+ border: '1px solid var(--dsw-alias-border-l1, rgba(255,255,255,0.16))',
355
+ background: 'var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.08))',
356
+ color: 'var(--dsw-alias-label-primary, #e8eaed)',
357
+ fontSize: 13,
358
+ fontFamily: 'inherit',
359
+ lineHeight: 1.45,
360
+ textAlign: 'center',
361
+ },
362
+ unit: { fontSize: 13, color: 'var(--dsw-alias-label-secondary, #9aa4b6)' },
363
+ linkBtn: {
364
+ background: 'none',
365
+ border: 'none',
366
+ padding: 0,
367
+ color: 'var(--dsw-alias-brand-primary, #4c8dff)',
368
+ fontSize: 13,
369
+ fontFamily: 'inherit',
370
+ cursor: 'pointer',
371
+ },
372
+ log: {
373
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
374
+ fontSize: 12,
375
+ lineHeight: 1.65,
376
+ color: 'var(--dsw-alias-label-primary, #e8eaed)',
377
+ background: 'var(--dsw-alias-bg-base, rgba(0,0,0,0.2))',
378
+ border: '1px solid var(--dsw-alias-border-l1, rgba(255,255,255,0.08))',
379
+ borderRadius: 8,
380
+ padding: '10px 12px',
381
+ margin: '0 0 14px',
382
+ maxHeight: 280,
383
+ overflow: 'auto',
384
+ whiteSpace: 'pre-wrap',
385
+ },
386
+ };
387
+
388
+ const Card = ({ title, hint, children }) =>
389
+ h(
390
+ 'section',
391
+ { style: T.card },
392
+ title ? h('div', { style: T.cardTitle }, title) : null,
393
+ hint ? h('div', { style: T.cardHint }, hint) : null,
394
+ children,
395
+ );
396
+
397
+ const Row = ({ label, hint, control, first }) =>
398
+ h(
399
+ 'div',
400
+ { style: first ? { ...T.row, borderTop: 'none' } : T.row },
401
+ h(
402
+ 'div',
403
+ { style: { minWidth: 0, flex: '1 1 auto' } },
404
+ h('div', { style: T.label }, label),
405
+ hint ? h('div', { style: T.hint }, hint) : null,
406
+ ),
407
+ h('div', { style: T.control }, control),
408
+ );
409
+
410
+ // 原生风格开关:44×24 圆角胶囊 + 18px 圆点。
411
+ const Toggle = ({ checked, onChange }) =>
412
+ h(
413
+ 'button',
414
+ {
415
+ type: 'button',
416
+ role: 'switch',
417
+ 'aria-checked': checked ? 'true' : 'false',
418
+ onClick: () => onChange(!checked),
419
+ style: {
420
+ position: 'relative',
421
+ width: 44,
422
+ height: 24,
423
+ borderRadius: 999,
424
+ border: 'none',
425
+ padding: 0,
426
+ cursor: 'pointer',
427
+ background: checked
428
+ ? 'var(--dsw-alias-brand-primary, #4c8dff)'
429
+ : 'var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.16))',
430
+ boxShadow: checked ? 'none' : 'inset 0 0 0 1px var(--dsw-alias-border-l1, rgba(255,255,255,0.22))',
431
+ transition: 'background 120ms ease',
432
+ },
433
+ },
434
+ h('span', {
435
+ style: {
436
+ position: 'absolute',
437
+ top: 3,
438
+ left: checked ? 23 : 3,
439
+ width: 18,
440
+ height: 18,
441
+ borderRadius: '50%',
442
+ background: checked
443
+ ? 'var(--dsw-alias-bg-base, #0f1115)'
444
+ : 'var(--dsw-alias-label-secondary, #c3ccdb)',
445
+ transition: 'left 120ms ease, background 120ms ease',
446
+ },
447
+ }),
448
+ );
449
+
450
+ function SettingsPage() {
451
+ const [data, setData] = useState(null);
452
+ const [form, setForm] = useState(null);
453
+ const [log, setLog] = useState([]);
454
+ const [busy, setBusy] = useState(false);
455
+ const [note, setNote] = useState(null);
456
+ const [error, setError] = useState(null);
457
+
458
+ const loadLog = useCallback(async () => {
459
+ try {
460
+ const payload = await api('/log?limit=40');
461
+ setLog(Array.isArray(payload && payload.entries) ? payload.entries : []);
462
+ } catch (err) {
463
+ setError(String(err && err.message ? err.message : err));
464
+ }
465
+ }, []);
466
+
467
+ const load = useCallback(async () => {
468
+ try {
469
+ const payload = await api('/status');
470
+ setData(payload);
471
+ setForm((prev) => prev || { ...payload.config });
472
+ setError(null);
473
+ } catch (err) {
474
+ setError(String(err && err.message ? err.message : err));
475
+ }
476
+ }, []);
477
+
478
+ useEffect(() => {
479
+ void load();
480
+ void loadLog();
481
+ }, [load, loadLog]);
482
+
483
+ const patch = (next) => setForm((prev) => ({ ...(prev || {}), ...next }));
484
+
485
+ const save = useCallback(async () => {
486
+ if (!form) return;
487
+ setBusy(true);
488
+ setNote(null);
489
+ try {
490
+ const payload = await api('/config', { method: 'POST', body: JSON.stringify(form) });
491
+ setForm({ ...payload.config });
492
+ setNote('已保存');
493
+ await load();
494
+ } catch (err) {
495
+ setError(String(err && err.message ? err.message : err));
496
+ } finally {
497
+ setBusy(false);
498
+ }
499
+ }, [form, load]);
500
+
501
+ const runNow = useCallback(async () => {
502
+ setBusy(true);
503
+ setNote('正在检查…');
504
+ try {
505
+ const payload = await api('/run', { method: 'POST', body: '{}' });
506
+ const result = payload && payload.result;
507
+ setNote(
508
+ payload && payload.skipped
509
+ ? `跳过:${payload.skipped}`
510
+ : result
511
+ ? `检查 ${result.checked || 0} 个,更新 ${(result.applied || []).length} 个,失败 ${(result.failed || []).length} 个`
512
+ : '已完成',
513
+ );
514
+ await load();
515
+ await loadLog();
516
+ } catch (err) {
517
+ setError(String(err && err.message ? err.message : err));
518
+ } finally {
519
+ setBusy(false);
520
+ }
521
+ }, [load, loadLog]);
522
+
523
+ const cfg = form || {};
524
+ const mode = cfg.mode || 'startup';
525
+ const modeHint =
526
+ mode === 'startup'
527
+ ? '每次启动 DSH 时检查一轮(20 分钟内不重复检查)。'
528
+ : mode === 'daily'
529
+ ? '每天到点后的第一轮检查;当天已经检查过就不再重复。'
530
+ : '距上次检查满 N 小时后,下个复查周期自动跑一轮。';
531
+
532
+ return h(
533
+ 'div',
534
+ { style: { color: 'var(--dsw-alias-label-primary, #e8eaed)', paddingBottom: 8 } },
535
+
536
+ h(
537
+ Card,
538
+ {
539
+ title: '插件自动更新',
540
+ hint:
541
+ '自动检查插件市场里已安装插件的更新并在后台安装。市场对发布时间有冷静期(默认 6 小时),' +
542
+ '刚发布的版本会晚一点才装得上;安装时插件正在使用(bundle-in-use)会失败,可在开机弹窗里一键处理。',
543
+ },
544
+ null,
545
+ ),
546
+
547
+ h(
548
+ Card,
549
+ { title: '检查时机' },
550
+ h(Row, {
551
+ first: true,
552
+ label: '何时检查',
553
+ hint: modeHint,
554
+ control: h(
555
+ 'select',
556
+ {
557
+ style: T.select,
558
+ value: mode,
559
+ onChange: (event) => patch({ mode: event.target.value }),
560
+ },
561
+ h('option', { value: 'startup' }, '每次打开 DSH'),
562
+ h('option', { value: 'daily' }, '每天定时'),
563
+ h('option', { value: 'interval' }, '每隔一段时间'),
564
+ ),
565
+ }),
566
+ mode === 'daily'
567
+ ? h(Row, {
568
+ label: '每天几点',
569
+ hint: '当地时间,到点后自动跑一轮。',
570
+ control: h('input', {
571
+ type: 'time',
572
+ style: T.input,
573
+ value: cfg.dailyAt || '09:30',
574
+ onChange: (event) => patch({ dailyAt: event.target.value }),
575
+ }),
576
+ })
577
+ : null,
578
+ mode === 'interval'
579
+ ? h(Row, {
580
+ label: '检查间隔',
581
+ hint: '1–48 小时,跨重启保留。',
582
+ control: [
583
+ h('input', {
584
+ key: 'hours',
585
+ type: 'number',
586
+ min: 1,
587
+ max: 48,
588
+ style: T.input,
589
+ value: cfg.intervalHours || 6,
590
+ onChange: (event) => patch({ intervalHours: Number(event.target.value) || 6 }),
591
+ }),
592
+ h('span', { key: 'unit', style: T.unit }, '小时'),
593
+ ],
594
+ })
595
+ : null,
596
+ ),
597
+
598
+ h(
599
+ Card,
600
+ { title: '行为' },
601
+ h(Row, {
602
+ first: true,
603
+ label: '启用自动检查',
604
+ hint: '关掉后不再自动检查更新(手动「立即检查」仍然可用)。',
605
+ control: h(Toggle, {
606
+ checked: cfg.enabled !== false,
607
+ onChange: (value) => patch({ enabled: value }),
608
+ }),
609
+ }),
610
+ h(Row, {
611
+ label: '发现更新就自动安装',
612
+ hint: '关掉则只检查、不安装。',
613
+ control: h(Toggle, {
614
+ checked: cfg.autoApply !== false,
615
+ onChange: (value) => patch({ autoApply: value }),
616
+ }),
617
+ }),
618
+ h(Row, {
619
+ label: '开机弹窗汇报结果',
620
+ hint: '当天第一次打开 DSH 时,弹窗列出已更新与失败的插件;点「稍后」当天不再弹。',
621
+ control: h(Toggle, {
622
+ checked: cfg.popupEnabled !== false,
623
+ onChange: (value) => patch({ popupEnabled: value }),
624
+ }),
625
+ }),
626
+ ),
627
+
628
+ h(
629
+ Card,
630
+ { title: '操作' },
631
+ h(Row, {
632
+ first: true,
633
+ label: '保存设置',
634
+ hint: note || '改动会写进 config.json,重载后仍然保留。',
635
+ control: h(
636
+ 'div',
637
+ { style: { display: 'flex', gap: 8 } },
638
+ h('button', { style: S.btn, disabled: busy, onClick: () => void runNow() }, '立即检查'),
639
+ h('button', { style: S.primary, disabled: busy, onClick: () => void save() }, '保存'),
640
+ ),
641
+ }),
642
+ error
643
+ ? h(Row, {
644
+ label: '出错了',
645
+ hint: String(error),
646
+ control: null,
647
+ })
648
+ : null,
649
+ ),
650
+
651
+ h(
652
+ 'div',
653
+ { style: { display: 'flex', alignItems: 'baseline', gap: 12, margin: '18px 2px 8px' } },
654
+ h('div', { style: { ...T.cardTitle, margin: 0 } }, '更新日志'),
655
+ h(
656
+ 'button',
657
+ { style: T.linkBtn, onClick: () => void loadLog() },
658
+ '刷新',
659
+ ),
660
+ data && data.state
661
+ ? h(
662
+ 'div',
663
+ { style: { ...T.hint, marginTop: 0, marginLeft: 'auto' } },
664
+ `上次检查 ${fmtTime(data.state.lastCheckAt) || '从未'} · 今天 ${data.state.lastDay || '-'}` +
665
+ (data.desktop && data.desktop.isDesktop ? '' : ' · 当前不是桌面端,一键重启不可用'),
666
+ )
667
+ : null,
668
+ ),
669
+ h(
670
+ 'div',
671
+ { style: T.log },
672
+ log.length
673
+ ? log
674
+ .map(
675
+ (entry) =>
676
+ `${fmtTime(entry.at)} ${entry.level === 'error' ? '✗' : entry.level === 'warn' ? '!' : '·'} ${entry.message || entry.event}`,
677
+ )
678
+ .join('\n')
679
+ : '(还没有记录)',
680
+ ),
681
+ );
682
+ }
683
+
684
+ return {
685
+ inject: ['slots'],
686
+ apply(ctx) {
687
+ ctx.slots.inject('shell.overlay', () =>
688
+ ctx.slots.register({ name: 'shell.overlay', id: 'dsh-auto-update-popup', order: 62 }, UpdatePopup),
689
+ );
690
+ ctx.slots.inject('settings.section', () =>
691
+ ctx.slots.register(
692
+ {
693
+ name: 'settings.section',
694
+ id: 'dsh-auto-update-settings',
695
+ order: 63,
696
+ label: () => {
697
+ const locale = (() => {
698
+ try {
699
+ return ctx.get('locale')?.getLocale()?.active || 'zh-CN';
700
+ } catch {
701
+ return 'zh-CN';
702
+ }
703
+ })();
704
+ return String(locale).toLowerCase().startsWith('en') ? 'Plugin auto update' : '插件自动更新';
705
+ },
706
+ },
707
+ SettingsPage,
708
+ ),
709
+ );
710
+ },
711
+ };
712
+ },
713
+ });