dsh-selfupdater 0.3.0 → 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 +78 -70
- package/lib/plugin-updater.mjs +31 -20
- package/lib/updater.mjs +14 -2
- 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
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - 升级动作通过锁文件防并发,双端校验。
|
|
11
11
|
*/
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
13
14
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
15
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
15
16
|
import { fileURLToPath } from 'node:url';
|
|
@@ -127,9 +128,30 @@ function resolveAppDir() {
|
|
|
127
128
|
throw new Error(`无法定位 ${PKG} 的应用目录`);
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* 解析工作区目录(插件清单 .dsh/profiles 的真正落盘处)。
|
|
133
|
+
* 与 runner.js 的推导链保持一致:
|
|
134
|
+
* 1. TRIM_DATA_SHARE_PATHS 第一个共享目录(飞牛声明的工作区,runner 会优先用它);
|
|
135
|
+
* 2. HOME 环境变量(runner 启动 DSH 时把 HOME 指到工作区,DSH 的 profile 就在 $HOME/.dsh 下);
|
|
136
|
+
* 3. TRIM_VAR / appDir/data 兜底(本地开发场景)。
|
|
137
|
+
* 注意:不能只看 TRIM_VAR —— 飞牛部署时它指向 app data 目录而非共享目录,
|
|
138
|
+
* 而 DSH 实际把 profiles 写在 HOME/.dsh 下;此前只读 TRIM_VAR 导致
|
|
139
|
+
* "版本更新"卡片始终显示"未发现已安装插件"。
|
|
140
|
+
*/
|
|
131
141
|
function resolveWorkspace(appDir) {
|
|
132
|
-
|
|
142
|
+
const shares = (process.env.TRIM_DATA_SHARE_PATHS ?? '').split(':').map((s) => s.trim()).filter(Boolean);
|
|
143
|
+
const candidates = [
|
|
144
|
+
...shares,
|
|
145
|
+
process.env.HOME ?? '',
|
|
146
|
+
process.env.TRIM_VAR ?? '',
|
|
147
|
+
join(appDir, 'data'),
|
|
148
|
+
].filter((v) => v !== '');
|
|
149
|
+
// 候选目录下若已存在 .dsh 则视为工作区,立即采用;
|
|
150
|
+
// 否则按 runner.js 同样的优先级取第一个候选(与实际落盘位置保持一致)。
|
|
151
|
+
for (const dir of candidates) {
|
|
152
|
+
if (existsSync(join(dir, '.dsh'))) return resolve(dir);
|
|
153
|
+
}
|
|
154
|
+
return resolve(candidates[0] ?? appDir);
|
|
133
155
|
}
|
|
134
156
|
|
|
135
157
|
/** 服务端口:与 runner.js 保持一致的环境变量与默认值。 */
|
|
@@ -233,28 +255,20 @@ function writePluginStatus(dshStateDir, payload) {
|
|
|
233
255
|
}
|
|
234
256
|
|
|
235
257
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
258
|
+
* 读本插件自身的已装版本:优先 import 自己的 package.json(ESM 顶层 await
|
|
259
|
+
* 不适合这里,改用 createRequire 同步读取),失败时回退硬编码兜底值。
|
|
260
|
+
* 用途:让"检测更新"能覆盖 dsh-selfupdater 自己 —— 此前清单来自 profile
|
|
261
|
+
* 的 dependencies,但"检测自己"语义上不依赖那份清单。
|
|
239
262
|
*/
|
|
240
|
-
function
|
|
241
|
-
const profilePkg = join(workspace, '.dsh', 'profiles', PLUGIN_PROFILE, 'package.json');
|
|
263
|
+
function readOwnVersion() {
|
|
242
264
|
try {
|
|
243
|
-
const
|
|
244
|
-
return
|
|
245
|
-
name: pkgName,
|
|
246
|
-
installedVersion: String(range).replace(/^[~^]\s*/, '') || String(range),
|
|
247
|
-
}));
|
|
265
|
+
const require = createRequire(import.meta.url);
|
|
266
|
+
return require('../package.json').version;
|
|
248
267
|
} catch {
|
|
249
|
-
return
|
|
268
|
+
return '0.0.0';
|
|
250
269
|
}
|
|
251
270
|
}
|
|
252
271
|
|
|
253
|
-
/** 查询 npm 上目标插件的 latest 版本号(复用带镜像回退的实现)。 */
|
|
254
|
-
async function fetchPluginLatest(pkgName) {
|
|
255
|
-
return fetchLatestVersion(pkgName);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
272
|
/* ------------------------------------------------------------------ *
|
|
259
273
|
* apply 入口
|
|
260
274
|
* ------------------------------------------------------------------ */
|
|
@@ -272,6 +286,9 @@ export function apply(ctx) {
|
|
|
272
286
|
const lockFile = join(dshStateDir, 'selfupdate.lock');
|
|
273
287
|
const pluginLockFile = join(dshStateDir, 'pluginupdate.lock');
|
|
274
288
|
const port = servicePort();
|
|
289
|
+
/** 本插件的包名与已装版本(用于"检测自己"的更新能力)。 */
|
|
290
|
+
const SELF_NAME = name;
|
|
291
|
+
const SELF_INSTALLED = readOwnVersion();
|
|
275
292
|
|
|
276
293
|
/** 升级是否正在进行(锁文件存在)。 */
|
|
277
294
|
const isBusy = () => existsSync(lockFile);
|
|
@@ -378,32 +395,32 @@ export function apply(ctx) {
|
|
|
378
395
|
});
|
|
379
396
|
|
|
380
397
|
/* ---------- GET /dsh-selfupdater/plugins ---------- */
|
|
381
|
-
/**
|
|
398
|
+
/**
|
|
399
|
+
* 插件更新小节只针对本插件自身(dsh-selfupdater):
|
|
400
|
+
* 其他插件已有各自渠道做更新检测,这里不再扫描 profile 清单。
|
|
401
|
+
* 返回数据与 DSH 小节同款三行模板:当前版本 / 最新版本 / 上次检查。
|
|
402
|
+
*/
|
|
382
403
|
const pluginBusy = () => existsSync(pluginLockFile);
|
|
383
404
|
/** 读插件状态并合并"服务端视角"的忙闲标记。 */
|
|
384
|
-
const pluginStatusView = () =>
|
|
385
|
-
|
|
386
|
-
state:
|
|
387
|
-
}
|
|
405
|
+
const pluginStatusView = () => {
|
|
406
|
+
const status = readPluginStatus(dshStateDir);
|
|
407
|
+
return { ...status, state: status.state ?? (pluginBusy() ? 'running' : 'idle') };
|
|
408
|
+
};
|
|
388
409
|
|
|
389
410
|
registerRoute('GET', '/dsh-selfupdater/plugins', (_req, res) => {
|
|
390
|
-
const installed = listInstalledPlugins(workspace);
|
|
391
411
|
const status = pluginStatusView();
|
|
392
|
-
//
|
|
393
|
-
const cache = status.updates ?? {};
|
|
394
|
-
const items = installed.map((p) => ({
|
|
395
|
-
...p,
|
|
396
|
-
latestVersion: cache[p.name]?.latestVersion ?? null,
|
|
397
|
-
updateAvailable: cache[p.name]?.updateAvailable === true,
|
|
398
|
-
checkedAt: cache[p.name]?.checkedAt ?? null,
|
|
399
|
-
}));
|
|
412
|
+
// 上次检查的缓存结果(latest / updateAvailable / checkedAt)存在 updates[SELF_NAME]。
|
|
413
|
+
const cache = status.updates?.[SELF_NAME] ?? {};
|
|
400
414
|
sendJson(res, 200, {
|
|
401
|
-
|
|
415
|
+
name: SELF_NAME,
|
|
416
|
+
currentVersion: SELF_INSTALLED,
|
|
417
|
+
latestVersion: cache.latestVersion ?? null,
|
|
418
|
+
updateAvailable: cache.updateAvailable === true,
|
|
419
|
+
lastCheck: cache.checkedAt ?? null,
|
|
402
420
|
busy: pluginBusy(),
|
|
403
421
|
state: status.state ?? 'idle',
|
|
404
422
|
message: status.message ?? null,
|
|
405
423
|
updatedAt: status.updatedAt ?? null,
|
|
406
|
-
plugins: items,
|
|
407
424
|
});
|
|
408
425
|
});
|
|
409
426
|
|
|
@@ -413,43 +430,34 @@ export function apply(ctx) {
|
|
|
413
430
|
sendJson(res, 403, { error: 'untrusted request' });
|
|
414
431
|
return;
|
|
415
432
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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}` });
|
|
440
460
|
}
|
|
441
|
-
const failed = results.filter((r) => r.status === 'rejected').length;
|
|
442
|
-
writePluginStatus(dshStateDir, {
|
|
443
|
-
...readPluginStatus(dshStateDir),
|
|
444
|
-
state: 'idle',
|
|
445
|
-
message: failed > 0 ? `检查完成,${failed} 个插件查询失败(网络原因可重试)` : '检查完成',
|
|
446
|
-
updates,
|
|
447
|
-
updatedAt: new Date().toISOString(),
|
|
448
|
-
});
|
|
449
|
-
sendJson(res, 200, {
|
|
450
|
-
updatedCount: Object.values(updates).filter((u) => u.updateAvailable).length,
|
|
451
|
-
failedCount: failed,
|
|
452
|
-
});
|
|
453
461
|
});
|
|
454
462
|
|
|
455
463
|
/* ---------- POST /dsh-selfupdater/plugins/update ---------- */
|
package/lib/plugin-updater.mjs
CHANGED
|
@@ -122,8 +122,20 @@ const args = parseArgs(process.argv);
|
|
|
122
122
|
|
|
123
123
|
/** DSH 应用目录(含 bin/node 与 node_modules/@deepseek-ai/dsh)。 */
|
|
124
124
|
const appDir = resolve(String(args['app-dir'] ?? ''));
|
|
125
|
-
/**
|
|
126
|
-
|
|
125
|
+
/**
|
|
126
|
+
* 工作区目录(存放 .dsh 状态目录与插件 profile)。
|
|
127
|
+
* 与 index.js 的 resolveWorkspace 推导链一致:共享目录 > HOME > TRIM_VAR > appDir。
|
|
128
|
+
* index.js 已解析出正确 workspace 并通过 --workspace 传入,这里仅在参数缺失时兜底;
|
|
129
|
+
* 不能只信 TRIM_VAR —— 飞牛部署时 profiles 实际写在 $HOME/.dsh 下。
|
|
130
|
+
*/
|
|
131
|
+
const workspace = (() => {
|
|
132
|
+
if (args.workspace) return resolve(String(args.workspace));
|
|
133
|
+
const shares = (process.env.TRIM_DATA_SHARE_PATHS ?? '').split(':').map((s) => s.trim()).filter(Boolean);
|
|
134
|
+
for (const dir of [...shares, process.env.HOME ?? '', process.env.TRIM_VAR ?? '', appDir]) {
|
|
135
|
+
if (dir !== '' && existsSync(join(dir, '.dsh'))) return resolve(dir);
|
|
136
|
+
}
|
|
137
|
+
return resolve(shares[0] ?? process.env.HOME ?? process.env.TRIM_VAR ?? appDir);
|
|
138
|
+
})();
|
|
127
139
|
/** Web 服务端口,健康检查用。 */
|
|
128
140
|
const port = parseInt(String(args.port ?? process.env.DSH_PORT ?? '3081'), 10);
|
|
129
141
|
|
|
@@ -316,26 +328,25 @@ async function main() {
|
|
|
316
328
|
// 锁文件双端校验:路由触发前会检查;这里再补一道防手动重复执行。
|
|
317
329
|
if (existsSync(lockFile)) throw new Error('已有一次插件更新在进行中(锁文件存在)');
|
|
318
330
|
|
|
319
|
-
|
|
320
|
-
|
|
331
|
+
/**
|
|
332
|
+
* 只更新本插件自身(dsh-selfupdater):其他插件已有各自的更新渠道,
|
|
333
|
+
* 不再扫描 profile 的 dependencies 清单。
|
|
334
|
+
* 已装版本直接读自己脚本身旁的 package.json,最可靠。
|
|
335
|
+
*/
|
|
336
|
+
let selfVersion = '';
|
|
321
337
|
try {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
version: String(range).replace(/^[~^]\s*/, ''),
|
|
330
|
-
})).filter((p) => p.version !== '');
|
|
331
|
-
|
|
332
|
-
if (installed.length === 0) {
|
|
333
|
-
setState('idle', '未发现已安装插件,无需更新');
|
|
334
|
-
finish(0);
|
|
338
|
+
selfVersion = JSON.parse(
|
|
339
|
+
readFileSync(new URL('./../package.json', import.meta.url), 'utf8'),
|
|
340
|
+
).version ?? '';
|
|
341
|
+
} catch { /* 读不到版本号则无事可做 */ }
|
|
342
|
+
if (selfVersion === '') {
|
|
343
|
+
setState('error', '无法读取 dsh-selfupdater 自身版本号,更新中止');
|
|
344
|
+
finish(1);
|
|
335
345
|
}
|
|
346
|
+
const installed = [{ name: 'dsh-selfupdater', version: selfVersion }];
|
|
336
347
|
|
|
337
348
|
status = { startedAt: new Date().toISOString(), trigger: 'manual' };
|
|
338
|
-
setState('downloading', '
|
|
349
|
+
setState('downloading', '正在查询 dsh-selfupdater 的最新版本 …');
|
|
339
350
|
|
|
340
351
|
// 阶段一:并发查询每个插件的 latest,筛出真正有新版可升的子集。
|
|
341
352
|
const checks = await Promise.allSettled(installed.map(async (p) => ({ ...p, info: await fetchLatest(p.name) })));
|
|
@@ -349,7 +360,7 @@ async function main() {
|
|
|
349
360
|
if (isNewer(info.latest, version)) pending.push({ name, currentVersion: version, targetVersion: info.latest, tgzUrl: info.tgzUrl });
|
|
350
361
|
}
|
|
351
362
|
if (pending.length === 0) {
|
|
352
|
-
setState('idle', '
|
|
363
|
+
setState('idle', 'dsh-selfupdater 已是最新版本');
|
|
353
364
|
finish(0);
|
|
354
365
|
}
|
|
355
366
|
log(`待更新插件:${pending.map((p) => `${p.name}@${p.currentVersion}->${p.targetVersion}`).join(', ')}`);
|
|
@@ -392,7 +403,7 @@ async function main() {
|
|
|
392
403
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
393
404
|
// bak 目录保留一次作为手动救砖手段,下次升级前会自动清理。
|
|
394
405
|
setState('done',
|
|
395
|
-
|
|
406
|
+
`插件更新完成:dsh-selfupdater@${updated[0]?.targetVersion ?? ''}`,
|
|
396
407
|
{ finishedAt: new Date().toISOString() });
|
|
397
408
|
} else {
|
|
398
409
|
await rollback();
|
package/lib/updater.mjs
CHANGED
|
@@ -130,8 +130,20 @@ const args = parseArgs(process.argv);
|
|
|
130
130
|
|
|
131
131
|
/** DSH 应用目录(含 node_modules / bin/runner.js),由插件本体解析后传入。 */
|
|
132
132
|
const appDir = resolve(String(args['app-dir'] ?? ''));
|
|
133
|
-
/**
|
|
134
|
-
|
|
133
|
+
/**
|
|
134
|
+
* 工作区目录(存放 .dsh 状态目录)。
|
|
135
|
+
* 与 index.js / plugin-updater.mjs 的推导链一致:共享目录 > HOME > TRIM_VAR > appDir。
|
|
136
|
+
* 正常情况下 index.js 已通过 --workspace 传入解析结果,这里仅兜底;
|
|
137
|
+
* 不能只信 TRIM_VAR —— 飞牛部署时 .dsh 实际写在 $HOME(工作区)下。
|
|
138
|
+
*/
|
|
139
|
+
const workspace = (() => {
|
|
140
|
+
if (args.workspace) return resolve(String(args.workspace));
|
|
141
|
+
const shares = (process.env.TRIM_DATA_SHARE_PATHS ?? '').split(':').map((s) => s.trim()).filter(Boolean);
|
|
142
|
+
for (const dir of [...shares, process.env.HOME ?? '', process.env.TRIM_VAR ?? '', appDir]) {
|
|
143
|
+
if (dir !== '' && existsSync(join(dir, '.dsh'))) return resolve(dir);
|
|
144
|
+
}
|
|
145
|
+
return resolve(shares[0] ?? process.env.HOME ?? process.env.TRIM_VAR ?? appDir);
|
|
146
|
+
})();
|
|
135
147
|
/** Web 服务端口,健康检查用。 */
|
|
136
148
|
const port = parseInt(String(args.port ?? process.env.DSH_PORT ?? '3081'), 10);
|
|
137
149
|
/** 要升级的包名。 */
|
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",
|