dsh-selfupdater 0.3.1 → 0.3.2
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 +76 -95
- package/lib/index.js +43 -80
- package/lib/plugin-updater.mjs +13 -24
- package/package.json +1 -1
package/client/settings-card.js
CHANGED
|
@@ -224,14 +224,6 @@ const CARD_CSS = `
|
|
|
224
224
|
.dshsu-pill{font-size:12px;padding:2px 9px;border-radius:999px;white-space:nowrap}
|
|
225
225
|
.dshsu-pill-ok{color:var(--dshsu-success);background:var(--dshsu-success-soft)}
|
|
226
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
227
|
/* ---- 单卡片内两个小节之间的分隔线(随主题换肤) ---- */
|
|
236
228
|
.dshsu-divider{height:1px;background:var(--dshsu-border);margin:4px 0}
|
|
237
229
|
/* 单卡片内的插件小节容器:只负责纵向排布,不画边框(边框属于整张卡片)。 */
|
|
@@ -309,40 +301,54 @@ function StepBar({ state }) {
|
|
|
309
301
|
}
|
|
310
302
|
|
|
311
303
|
/**
|
|
312
|
-
*
|
|
313
|
-
*
|
|
304
|
+
* 小节标题图标(SVG,fill 用 currentColor 自动跟随主题文字色):
|
|
305
|
+
* - RocketIcon:火箭升空,语义 = 发版/升级,用于 DSH 更新小节;
|
|
306
|
+
* - PuzzleIcon:拼图块,语义 = 插件,用于插件更新小节。
|
|
307
|
+
* (旧版圆形箭头在小尺寸下形似齿轮、辨识度差,故换成语义更直白的图标。)
|
|
314
308
|
*/
|
|
315
|
-
|
|
316
|
-
|
|
309
|
+
const ICON_ROCKET_D = 'M9.19 6.35c-2.04 2.29-3.44 5.58-3.57 5.89L2 10.69l4.05-4.05'
|
|
310
|
+
+ 'c.47-.47 1.15-.68 1.81-.55l1.33.26zM11.17 17s3.74-1.55 5.89-3.7'
|
|
311
|
+
+ 'c5.4-5.4 4.5-9.62 4.21-10.57-.95-.3-5.17-1.19-10.57 4.21C8.55 9.09 7 12.83 7 12.83L11.17 17zm6.48-2.19'
|
|
312
|
+
+ 'c-2.29 2.04-5.58 3.44-5.89 3.57L13.31 22l4.05-4.05c.47-.47.68-1.15.55-1.81l-.26-1.33zM9 18'
|
|
313
|
+
+ 'c0 .83-.34 1.58-.88 2.12C6.94 21.3 2 22 2 22s.7-4.94 1.88-6.12A2.996 2.996 0 0 1 9 18zm3-6'
|
|
314
|
+
+ 'c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z';
|
|
315
|
+
const ICON_PUZZLE_D = 'M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4'
|
|
316
|
+
+ 'c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V19c0 1.1.9 2 2 2h3.8v-1.5'
|
|
317
|
+
+ 'c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V21H17c1.1 0 2-.9 2-2v-4h1.5'
|
|
318
|
+
+ 'c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z';
|
|
319
|
+
|
|
320
|
+
/** 图标组件工厂:同一份 SVG 外壳,只换路径数据,避免重复样板代码。 */
|
|
321
|
+
function svgIcon(pathD) {
|
|
322
|
+
return () => h('svg', {
|
|
317
323
|
className: 'dshsu-icon',
|
|
318
324
|
viewBox: '0 0 24 24',
|
|
319
325
|
width: 15,
|
|
320
326
|
height: 15,
|
|
321
327
|
'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
|
+
}, h('path', { d: pathD, fill: 'currentColor' }));
|
|
328
329
|
}
|
|
329
330
|
|
|
331
|
+
/** DSH 更新小节标题图标(火箭)。 */
|
|
332
|
+
const RocketIcon = svgIcon(ICON_ROCKET_D);
|
|
333
|
+
/** 插件更新小节标题图标(拼图块)。 */
|
|
334
|
+
const PuzzleIcon = svgIcon(ICON_PUZZLE_D);
|
|
335
|
+
|
|
330
336
|
/* ------------------------------------------------------------------ *
|
|
331
337
|
* 小节标题条:图标 + 名称 + 右侧徽章(单卡片内两个小节共用)
|
|
332
338
|
* ------------------------------------------------------------------ */
|
|
333
339
|
|
|
334
340
|
/**
|
|
335
|
-
*
|
|
341
|
+
* 小节标题:左侧图标(默认火箭,可传入其他图标)+ 标题文字,右侧可选徽章。
|
|
336
342
|
* 用于把"DSH 更新"和"插件更新"收纳在同一张卡片内分区展示。
|
|
337
343
|
*/
|
|
338
|
-
function SectionHead({ title,
|
|
344
|
+
function SectionHead({ title, icon, badge }) {
|
|
339
345
|
return h('div', { className: 'dshsu-head' },
|
|
340
|
-
h('div', { className: 'dshsu-title' }, h(
|
|
346
|
+
h('div', { className: 'dshsu-title' }, icon ?? h(RocketIcon), h('span', null, title)),
|
|
341
347
|
badge ?? null,
|
|
342
348
|
);
|
|
343
349
|
}
|
|
344
350
|
|
|
345
|
-
/**
|
|
351
|
+
/** 小节标题右侧徽章的两种形态:可更新(琥珀色)/ 普通(灰色版本号)。 */
|
|
346
352
|
function headBadge(text, isNew) {
|
|
347
353
|
if (text == null) return null;
|
|
348
354
|
return h('span', { className: `dshsu-chip${isNew ? ' dshsu-chip-new' : ''}` }, text);
|
|
@@ -354,22 +360,17 @@ function headBadge(text, isNew) {
|
|
|
354
360
|
* 两组状态相互独立、互不阻塞;样式共享同一套 CSS 类与主题变量。
|
|
355
361
|
*/
|
|
356
362
|
function UpdateCard({ t, status, busy, checking, onCheck, onUpgrade,
|
|
357
|
-
|
|
363
|
+
plugin, pluginBusy, pluginChecking, pluginMsg, onPluginCheck, onPluginUpgrade }) {
|
|
358
364
|
|
|
359
365
|
const updateAvailable = status?.latestVersion != null
|
|
360
366
|
&& status.latestVersion !== status.currentVersion;
|
|
361
367
|
const stage = STATE_LABELS[status?.state] ?? '';
|
|
362
|
-
const updateCount = plugins.filter((p) => p.updateAvailable).length;
|
|
363
368
|
|
|
364
369
|
// DSH 小节结果消息的语义着色:成功绿 / 失败红 / 其余灰。
|
|
365
370
|
const message = !busy && status?.message ? String(status.message) : '';
|
|
366
371
|
const msgClass = /已是最新|发现新版本|成功|完成/.test(message) ? ' dshsu-msg-ok'
|
|
367
372
|
: /失败|出错|错误/.test(message) || ['error', 'done_failed'].includes(status?.state) ? ' dshsu-msg-bad'
|
|
368
373
|
: '';
|
|
369
|
-
// 插件小节结果消息的语义着色,规则同上。
|
|
370
|
-
const pMsgClass = /已是最新|完成|成功/.test(pluginMsg) ? ' dshsu-msg-ok'
|
|
371
|
-
: /失败|出错|错误|超时/.test(pluginMsg) ? ' dshsu-msg-bad'
|
|
372
|
-
: '';
|
|
373
374
|
|
|
374
375
|
return h('div', { className: 'dshsu-card' },
|
|
375
376
|
/* ============ 小节一:DSH 更新 ============ */
|
|
@@ -432,7 +433,7 @@ function UpdateCard({ t, status, busy, checking, onCheck, onUpgrade,
|
|
|
432
433
|
/* ============ 小节二:插件更新(复用 PluginSection) ============ */
|
|
433
434
|
h(PluginSection, {
|
|
434
435
|
t,
|
|
435
|
-
|
|
436
|
+
plugin,
|
|
436
437
|
busy: pluginBusy,
|
|
437
438
|
checking: pluginChecking,
|
|
438
439
|
msg: pluginMsg,
|
|
@@ -454,74 +455,61 @@ function formatTime(iso) {
|
|
|
454
455
|
|
|
455
456
|
/* ------------------------------------------------------------------ *
|
|
456
457
|
* 插件更新小节(渲染在"版本更新"卡片下半部分):
|
|
457
|
-
*
|
|
458
|
+
* 只针对本插件(dsh-selfupdater)自身的检测与升级,
|
|
459
|
+
* 展示模板与 DSH 小节同款三行:当前版本 / 最新版本 / 上次检查。
|
|
458
460
|
* ------------------------------------------------------------------ */
|
|
459
461
|
|
|
460
462
|
/** 插件更新进行中的状态集合(与后端锁文件/state 约定一致)。 */
|
|
461
463
|
const PLUGIN_BUSY_STATES = ['running', 'downloading', 'restarting', 'healthcheck', 'rollback'];
|
|
462
464
|
|
|
463
465
|
/**
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
|
|
468
|
-
|
|
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 结果消息;各事件回调
|
|
466
|
+
* 插件更新小节:数据契约与 DSH 小节一致
|
|
467
|
+
* (currentVersion / latestVersion / lastCheck / updateAvailable),
|
|
468
|
+
* 标题右侧徽章显示当前 dsh-selfupdater 版本号,有新版时换成琥珀色可更新徽章。
|
|
469
|
+
* @param props - plugin 单条自身数据;busy 更新进行中;checking 检查中;
|
|
470
|
+
* msg 结果消息;onCheck/onUpgrade 事件回调
|
|
495
471
|
*/
|
|
496
|
-
function PluginSection({ t,
|
|
497
|
-
const
|
|
472
|
+
function PluginSection({ t, plugin, busy, checking, msg, onCheck, onUpgrade }) {
|
|
473
|
+
const updateAvailable = plugin?.updateAvailable === true;
|
|
498
474
|
// 结果消息的语义着色:成功绿 / 失败红 / 其余灰。
|
|
499
475
|
const msgClass = /已是最新|完成|成功/.test(msg) ? ' dshsu-msg-ok'
|
|
500
476
|
: /失败|出错|错误|超时/.test(msg) ? ' dshsu-msg-bad'
|
|
501
477
|
: '';
|
|
502
478
|
|
|
503
479
|
return h('div', { className: 'dshsu-sub' },
|
|
504
|
-
//
|
|
480
|
+
// 小节标题:拼图图标 + 右侧版本号徽章(有新版时变琥珀色提示)
|
|
505
481
|
h(SectionHead, {
|
|
506
482
|
title: t.pluginNav,
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
483
|
+
icon: h(PuzzleIcon),
|
|
484
|
+
badge: updateAvailable && plugin.latestVersion != null
|
|
485
|
+
? headBadge(`${t.updateAvailable} ${plugin.latestVersion}`, true)
|
|
486
|
+
: headBadge(plugin?.currentVersion ?? '—', false),
|
|
510
487
|
}),
|
|
511
|
-
//
|
|
512
|
-
h('div', { className: 'dshsu-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
488
|
+
// 三行模板与 DSH 小节完全同款:当前版本 / 最新版本 / 上次检查
|
|
489
|
+
h('div', { className: 'dshsu-rows' },
|
|
490
|
+
h('div', { className: 'dshsu-row' },
|
|
491
|
+
h('span', { className: 'dshsu-label' }, t.currentVersion),
|
|
492
|
+
h('span', { className: 'dshsu-value' }, plugin?.currentVersion ?? '—'),
|
|
493
|
+
),
|
|
494
|
+
h('div', { className: 'dshsu-row' },
|
|
495
|
+
h('span', { className: 'dshsu-label' }, t.latestVersion),
|
|
496
|
+
h('span', {
|
|
497
|
+
className: updateAvailable ? 'dshsu-value dshsu-value-new' : 'dshsu-value',
|
|
498
|
+
}, plugin?.latestVersion ?? '—'),
|
|
499
|
+
),
|
|
500
|
+
h('div', { className: 'dshsu-row' },
|
|
501
|
+
h('span', { className: 'dshsu-label' }, t.lastCheck),
|
|
502
|
+
h('span', { className: 'dshsu-value' }, formatTime(plugin?.lastCheck) ?? t.never),
|
|
503
|
+
),
|
|
516
504
|
),
|
|
517
|
-
//
|
|
505
|
+
// 检查/更新进行中:spinner + 阶段文案
|
|
518
506
|
busy || checking ? h('div', { role: 'status', className: 'dshsu-progress' },
|
|
519
507
|
h('span', { className: 'dshsu-spin' }),
|
|
520
508
|
h('span', null, busy ? t.pluginUpdating : t.checkingLabel),
|
|
521
509
|
) : null,
|
|
522
510
|
// 空闲时的结果消息
|
|
523
511
|
!busy && !checking && msg !== '' ? h('div', { className: `dshsu-msg${msgClass}` }, msg) : null,
|
|
524
|
-
// 底部操作行:检查更新 +
|
|
512
|
+
// 底部操作行:检查更新 + 一键升级(仅自身)
|
|
525
513
|
h('div', { className: 'dshsu-actions' },
|
|
526
514
|
h('button', {
|
|
527
515
|
type: 'button',
|
|
@@ -534,10 +522,10 @@ function PluginSection({ t, plugins, busy, checking, msg, onCheck, onUpgrade })
|
|
|
534
522
|
),
|
|
535
523
|
h('button', {
|
|
536
524
|
type: 'button',
|
|
537
|
-
className:
|
|
538
|
-
disabled: busy || checking ||
|
|
525
|
+
className: updateAvailable && !busy ? 'dshsu-btn dshsu-btn-primary' : 'dshsu-btn',
|
|
526
|
+
disabled: busy || checking || !updateAvailable,
|
|
539
527
|
onClick: onUpgrade,
|
|
540
|
-
}, t.
|
|
528
|
+
}, t.upgradeNow),
|
|
541
529
|
h('span', { className: 'dshsu-spacer' }),
|
|
542
530
|
),
|
|
543
531
|
);
|
|
@@ -554,9 +542,7 @@ const FALLBACK_DICT = {
|
|
|
554
542
|
currentVersion: '当前版本', latestVersion: '最新版本',
|
|
555
543
|
lastCheck: '上次检查', never: '从未', processing: '处理中…',
|
|
556
544
|
updateAvailable: '可更新', upgraded: '已升级', failed: '失败',
|
|
557
|
-
pluginNav: '插件更新',
|
|
558
|
-
pluginsSuffix: '个插件', updatesSuffix: '个可更新',
|
|
559
|
-
noPlugins: '未发现已安装插件', upToDate: '最新',
|
|
545
|
+
pluginNav: '插件更新',
|
|
560
546
|
pluginUpdating: '插件更新进行中…', checkingLabel: '正在检查更新…',
|
|
561
547
|
checkFailed: '检查更新失败',
|
|
562
548
|
// 侧边导航文案:现在只有一张"版本更新"卡片。
|
|
@@ -567,9 +553,7 @@ const FALLBACK_DICT = {
|
|
|
567
553
|
currentVersion: 'Current', latestVersion: 'Latest',
|
|
568
554
|
lastCheck: 'Last check', never: 'never', processing: 'Working…',
|
|
569
555
|
updateAvailable: 'Update', upgraded: 'Upgraded', failed: 'Failed',
|
|
570
|
-
pluginNav: 'Plugin Updates',
|
|
571
|
-
pluginsSuffix: ' plugins', updatesSuffix: ' to update',
|
|
572
|
-
noPlugins: 'No installed plugins found', upToDate: 'Latest',
|
|
556
|
+
pluginNav: 'Plugin Updates',
|
|
573
557
|
pluginUpdating: 'Plugin update in progress…', checkingLabel: 'Checking…',
|
|
574
558
|
checkFailed: 'Check failed',
|
|
575
559
|
// Side navigation label: there is now only one "Updates" card.
|
|
@@ -627,19 +611,19 @@ function apply(ctx) {
|
|
|
627
611
|
return s != null && ['running', 'downloading', 'swapping', 'restarting', 'healthcheck', 'rollback'].includes(s.state);
|
|
628
612
|
}
|
|
629
613
|
|
|
630
|
-
/* ----------
|
|
614
|
+
/* ---------- 插件更新小节的状态与动作(只针对自身) ---------- */
|
|
631
615
|
|
|
632
|
-
let
|
|
616
|
+
let pluginData = null;
|
|
633
617
|
let pluginBusy = false;
|
|
634
618
|
let pluginMsg = '';
|
|
635
619
|
let pluginChecking = false;
|
|
636
620
|
let pluginRefresh = () => {};
|
|
637
621
|
|
|
638
|
-
/**
|
|
622
|
+
/** 拉取 dsh-selfupdater 自身的版本信息(含上次检查缓存的可更新标记)。 */
|
|
639
623
|
async function pollPlugins() {
|
|
640
624
|
try {
|
|
641
625
|
const data = await api('/plugins');
|
|
642
|
-
|
|
626
|
+
pluginData = data; // 响应本身就是单对象:{ currentVersion, latestVersion, ... }
|
|
643
627
|
pluginBusy = data.busy === true || PLUGIN_BUSY_STATES.includes(data.state);
|
|
644
628
|
if (!pluginBusy && typeof data.message === 'string' && data.message !== '') {
|
|
645
629
|
pluginMsg = data.message;
|
|
@@ -657,15 +641,14 @@ function apply(ctx) {
|
|
|
657
641
|
setTimeout(pluginPollLoop, pluginBusy || pluginChecking ? POLL_ACTIVE_MS : PLUGIN_REFRESH_MS);
|
|
658
642
|
}
|
|
659
643
|
|
|
660
|
-
/** 检查插件更新:POST /plugins/check
|
|
644
|
+
/** 检查插件更新:POST /plugins/check 只查自己一个包,成功后立刻重拉结果。 */
|
|
661
645
|
async function handlePluginCheck() {
|
|
662
646
|
pluginChecking = true;
|
|
663
647
|
pluginMsg = '';
|
|
664
648
|
pluginRefresh();
|
|
665
649
|
try {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
pluginMsg = `检查完成:${n > 0 ? `${n} 个插件有新版本` : '所有插件均已是最新'}`;
|
|
650
|
+
await api('/plugins/check', { method: 'POST', body: '{}' });
|
|
651
|
+
// 结果文案由轮询从服务端落盘的 message 读取,这里无需自行拼装。
|
|
669
652
|
await pollPlugins();
|
|
670
653
|
} catch (err) {
|
|
671
654
|
console.warn(`[${NS}] 插件检查更新失败:`, err);
|
|
@@ -731,9 +714,7 @@ function apply(ctx) {
|
|
|
731
714
|
currentVersion: dict('currentVersion'), latestVersion: dict('latestVersion'),
|
|
732
715
|
lastCheck: dict('lastCheck'), never: dict('never'), processing: dict('processing'),
|
|
733
716
|
updateAvailable: dict('updateAvailable'), upgraded: dict('upgraded'), failed: dict('failed'),
|
|
734
|
-
pluginNav: dict('pluginNav'),
|
|
735
|
-
upToDate: dict('upToDate'), noPlugins: dict('noPlugins'),
|
|
736
|
-
pluginsSuffix: dict('pluginsSuffix'), updatesSuffix: dict('updatesSuffix'),
|
|
717
|
+
pluginNav: dict('pluginNav'),
|
|
737
718
|
pluginUpdating: dict('pluginUpdating'), checkingLabel: dict('checkingLabel'),
|
|
738
719
|
},
|
|
739
720
|
status: latestStatus,
|
|
@@ -741,7 +722,7 @@ function apply(ctx) {
|
|
|
741
722
|
checking,
|
|
742
723
|
onCheck: handleCheck,
|
|
743
724
|
onUpgrade: handleUpgrade,
|
|
744
|
-
|
|
725
|
+
plugin: pluginData,
|
|
745
726
|
pluginBusy,
|
|
746
727
|
pluginChecking,
|
|
747
728
|
pluginMsg,
|
package/lib/index.js
CHANGED
|
@@ -254,29 +254,6 @@ function writePluginStatus(dshStateDir, payload) {
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
/**
|
|
258
|
-
* 列出 profile 中已安装的插件及其版本。
|
|
259
|
-
* 数据源是 profile/package.json 的 dependencies 字段 —— dsh plugin add
|
|
260
|
-
* 安装时会把它登记进去,这是最权威的"已装清单"。
|
|
261
|
-
*/
|
|
262
|
-
function listInstalledPlugins(workspace) {
|
|
263
|
-
const profilePkg = join(workspace, '.dsh', 'profiles', PLUGIN_PROFILE, 'package.json');
|
|
264
|
-
try {
|
|
265
|
-
const deps = JSON.parse(readFileSync(profilePkg, 'utf8')).dependencies ?? {};
|
|
266
|
-
return Object.entries(deps).map(([pkgName, range]) => ({
|
|
267
|
-
name: pkgName,
|
|
268
|
-
installedVersion: String(range).replace(/^[~^]\s*/, '') || String(range),
|
|
269
|
-
}));
|
|
270
|
-
} catch {
|
|
271
|
-
return [];
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/** 查询 npm 上目标插件的 latest 版本号(复用带镜像回退的实现)。 */
|
|
276
|
-
async function fetchPluginLatest(pkgName) {
|
|
277
|
-
return fetchLatestVersion(pkgName);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
257
|
/**
|
|
281
258
|
* 读本插件自身的已装版本:优先 import 自己的 package.json(ESM 顶层 await
|
|
282
259
|
* 不适合这里,改用 createRequire 同步读取),失败时回退硬编码兜底值。
|
|
@@ -418,36 +395,32 @@ export function apply(ctx) {
|
|
|
418
395
|
});
|
|
419
396
|
|
|
420
397
|
/* ---------- GET /dsh-selfupdater/plugins ---------- */
|
|
421
|
-
/**
|
|
398
|
+
/**
|
|
399
|
+
* 插件更新小节只针对本插件自身(dsh-selfupdater):
|
|
400
|
+
* 其他插件已有各自渠道做更新检测,这里不再扫描 profile 清单。
|
|
401
|
+
* 返回数据与 DSH 小节同款三行模板:当前版本 / 最新版本 / 上次检查。
|
|
402
|
+
*/
|
|
422
403
|
const pluginBusy = () => existsSync(pluginLockFile);
|
|
423
404
|
/** 读插件状态并合并"服务端视角"的忙闲标记。 */
|
|
424
|
-
const pluginStatusView = () =>
|
|
425
|
-
|
|
426
|
-
state:
|
|
427
|
-
}
|
|
405
|
+
const pluginStatusView = () => {
|
|
406
|
+
const status = readPluginStatus(dshStateDir);
|
|
407
|
+
return { ...status, state: status.state ?? (pluginBusy() ? 'running' : 'idle') };
|
|
408
|
+
};
|
|
428
409
|
|
|
429
410
|
registerRoute('GET', '/dsh-selfupdater/plugins', (_req, res) => {
|
|
430
|
-
// 清单 = 自己 + profile 已装插件(自己排最前,保证 UI 始终展示自身)。
|
|
431
|
-
const installed = [
|
|
432
|
-
{ name: SELF_NAME, installedVersion: SELF_INSTALLED },
|
|
433
|
-
...listInstalledPlugins(workspace).filter((p) => p.name !== SELF_NAME),
|
|
434
|
-
];
|
|
435
411
|
const status = pluginStatusView();
|
|
436
|
-
//
|
|
437
|
-
const cache = status.updates ?? {};
|
|
438
|
-
const items = installed.map((p) => ({
|
|
439
|
-
...p,
|
|
440
|
-
latestVersion: cache[p.name]?.latestVersion ?? null,
|
|
441
|
-
updateAvailable: cache[p.name]?.updateAvailable === true,
|
|
442
|
-
checkedAt: cache[p.name]?.checkedAt ?? null,
|
|
443
|
-
}));
|
|
412
|
+
// 上次检查的缓存结果(latest / updateAvailable / checkedAt)存在 updates[SELF_NAME]。
|
|
413
|
+
const cache = status.updates?.[SELF_NAME] ?? {};
|
|
444
414
|
sendJson(res, 200, {
|
|
445
|
-
|
|
415
|
+
name: SELF_NAME,
|
|
416
|
+
currentVersion: SELF_INSTALLED,
|
|
417
|
+
latestVersion: cache.latestVersion ?? null,
|
|
418
|
+
updateAvailable: cache.updateAvailable === true,
|
|
419
|
+
lastCheck: cache.checkedAt ?? null,
|
|
446
420
|
busy: pluginBusy(),
|
|
447
421
|
state: status.state ?? 'idle',
|
|
448
422
|
message: status.message ?? null,
|
|
449
423
|
updatedAt: status.updatedAt ?? null,
|
|
450
|
-
plugins: items,
|
|
451
424
|
});
|
|
452
425
|
});
|
|
453
426
|
|
|
@@ -457,44 +430,34 @@ export function apply(ctx) {
|
|
|
457
430
|
sendJson(res, 403, { error: 'untrusted request' });
|
|
458
431
|
return;
|
|
459
432
|
}
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
433
|
+
// 只查自己这一个包,成功后把结果写入状态文件缓存。
|
|
434
|
+
try {
|
|
435
|
+
const latestVersion = await fetchLatestVersion(SELF_NAME);
|
|
436
|
+
writePluginStatus(dshStateDir, {
|
|
437
|
+
...readPluginStatus(dshStateDir),
|
|
438
|
+
state: 'idle',
|
|
439
|
+
message: isNewer(latestVersion, SELF_INSTALLED)
|
|
440
|
+
? `发现新版本 ${latestVersion}` : '当前已是最新',
|
|
441
|
+
updates: {
|
|
442
|
+
[SELF_NAME]: {
|
|
443
|
+
latestVersion,
|
|
444
|
+
updateAvailable: isNewer(latestVersion, SELF_INSTALLED),
|
|
445
|
+
checkedAt: new Date().toISOString(),
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
updatedAt: new Date().toISOString(),
|
|
449
|
+
});
|
|
450
|
+
sendJson(res, 200, { updatedCount: isNewer(latestVersion, SELF_INSTALLED) ? 1 : 0 });
|
|
451
|
+
} catch (err) {
|
|
452
|
+
host.logger?.warn?.(`[dsh-selfupdater] 插件检查更新失败: ${err.message}`);
|
|
453
|
+
writePluginStatus(dshStateDir, {
|
|
454
|
+
...readPluginStatus(dshStateDir),
|
|
455
|
+
state: 'idle',
|
|
456
|
+
message: `插件检查更新失败:${err.message}`,
|
|
457
|
+
updatedAt: new Date().toISOString(),
|
|
458
|
+
});
|
|
459
|
+
sendJson(res, 502, { error: `插件检查更新失败:${err.message}` });
|
|
485
460
|
}
|
|
486
|
-
const failed = results.filter((r) => r.status === 'rejected').length;
|
|
487
|
-
writePluginStatus(dshStateDir, {
|
|
488
|
-
...readPluginStatus(dshStateDir),
|
|
489
|
-
state: 'idle',
|
|
490
|
-
message: failed > 0 ? `检查完成,${failed} 个插件查询失败(网络原因可重试)` : '检查完成',
|
|
491
|
-
updates,
|
|
492
|
-
updatedAt: new Date().toISOString(),
|
|
493
|
-
});
|
|
494
|
-
sendJson(res, 200, {
|
|
495
|
-
updatedCount: Object.values(updates).filter((u) => u.updateAvailable).length,
|
|
496
|
-
failedCount: failed,
|
|
497
|
-
});
|
|
498
461
|
});
|
|
499
462
|
|
|
500
463
|
/* ---------- POST /dsh-selfupdater/plugins/update ---------- */
|
package/lib/plugin-updater.mjs
CHANGED
|
@@ -328,36 +328,25 @@ async function main() {
|
|
|
328
328
|
// 锁文件双端校验:路由触发前会检查;这里再补一道防手动重复执行。
|
|
329
329
|
if (existsSync(lockFile)) throw new Error('已有一次插件更新在进行中(锁文件存在)');
|
|
330
330
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
} catch {
|
|
337
|
-
deps = {};
|
|
338
|
-
}
|
|
339
|
-
/** 本插件自身的已装版本:直接读自己脚本身旁的 package.json,最可靠。 */
|
|
331
|
+
/**
|
|
332
|
+
* 只更新本插件自身(dsh-selfupdater):其他插件已有各自的更新渠道,
|
|
333
|
+
* 不再扫描 profile 的 dependencies 清单。
|
|
334
|
+
* 已装版本直接读自己脚本身旁的 package.json,最可靠。
|
|
335
|
+
*/
|
|
340
336
|
let selfVersion = '';
|
|
341
337
|
try {
|
|
342
338
|
selfVersion = JSON.parse(
|
|
343
339
|
readFileSync(new URL('./../package.json', import.meta.url), 'utf8'),
|
|
344
340
|
).version ?? '';
|
|
345
|
-
} catch { /*
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
})).filter((p) => p.version !== '' && p.name !== 'dsh-selfupdater');
|
|
350
|
-
if (selfVersion !== '') {
|
|
351
|
-
installed.unshift({ name: 'dsh-selfupdater', version: selfVersion });
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (installed.length === 0) {
|
|
355
|
-
setState('idle', '未发现已安装插件,无需更新');
|
|
356
|
-
finish(0);
|
|
341
|
+
} catch { /* 读不到版本号则无事可做 */ }
|
|
342
|
+
if (selfVersion === '') {
|
|
343
|
+
setState('error', '无法读取 dsh-selfupdater 自身版本号,更新中止');
|
|
344
|
+
finish(1);
|
|
357
345
|
}
|
|
346
|
+
const installed = [{ name: 'dsh-selfupdater', version: selfVersion }];
|
|
358
347
|
|
|
359
348
|
status = { startedAt: new Date().toISOString(), trigger: 'manual' };
|
|
360
|
-
setState('downloading', '
|
|
349
|
+
setState('downloading', '正在查询 dsh-selfupdater 的最新版本 …');
|
|
361
350
|
|
|
362
351
|
// 阶段一:并发查询每个插件的 latest,筛出真正有新版可升的子集。
|
|
363
352
|
const checks = await Promise.allSettled(installed.map(async (p) => ({ ...p, info: await fetchLatest(p.name) })));
|
|
@@ -371,7 +360,7 @@ async function main() {
|
|
|
371
360
|
if (isNewer(info.latest, version)) pending.push({ name, currentVersion: version, targetVersion: info.latest, tgzUrl: info.tgzUrl });
|
|
372
361
|
}
|
|
373
362
|
if (pending.length === 0) {
|
|
374
|
-
setState('idle', '
|
|
363
|
+
setState('idle', 'dsh-selfupdater 已是最新版本');
|
|
375
364
|
finish(0);
|
|
376
365
|
}
|
|
377
366
|
log(`待更新插件:${pending.map((p) => `${p.name}@${p.currentVersion}->${p.targetVersion}`).join(', ')}`);
|
|
@@ -414,7 +403,7 @@ async function main() {
|
|
|
414
403
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
415
404
|
// bak 目录保留一次作为手动救砖手段,下次升级前会自动清理。
|
|
416
405
|
setState('done',
|
|
417
|
-
|
|
406
|
+
`插件更新完成:dsh-selfupdater@${updated[0]?.targetVersion ?? ''}`,
|
|
418
407
|
{ finishedAt: new Date().toISOString() });
|
|
419
408
|
} else {
|
|
420
409
|
await rollback();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-selfupdater",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Self-update plugin for DeepSeek Harness: check npm for newer @deepseek-ai/dsh, swap node_modules atomically, restart, health-check and roll back on failure; also supports one-click online update of installed DSH plugins. DSH 主程序与已装插件的一站式在线更新插件。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|