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.
@@ -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
- * 更新图标:SVG 循环箭头(语义 = 刷新/更新),stroke 用 currentColor
313
- * 自动跟随文字颜色,亮暗主题都无需额外处理。
304
+ * 小节标题图标(SVG,fill 用 currentColor 自动跟随主题文字色):
305
+ * - RocketIcon:火箭升空,语义 = 发版/升级,用于 DSH 更新小节;
306
+ * - PuzzleIcon:拼图块,语义 = 插件,用于插件更新小节。
307
+ * (旧版圆形箭头在小尺寸下形似齿轮、辨识度差,故换成语义更直白的图标。)
314
308
  */
315
- function UpdateIcon() {
316
- return h('svg', {
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, badge, badgeNew }) {
344
+ function SectionHead({ title, icon, badge }) {
339
345
  return h('div', { className: 'dshsu-head' },
340
- h('div', { className: 'dshsu-title' }, h(UpdateIcon), h('span', null, title)),
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
- plugins, pluginBusy, pluginChecking, pluginMsg, onPluginCheck, onPluginUpgrade }) {
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
- plugins,
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
- * @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 结果消息;各事件回调
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, plugins, busy, checking, msg, onCheck, onUpgrade }) {
497
- const updateCount = plugins.filter((p) => p.updateAvailable).length;
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
- // 小节标题:与 DSH 部分同款 SectionHead,右侧显示计数徽章
480
+ // 小节标题:拼图图标 + 右侧版本号徽章(有新版时变琥珀色提示)
505
481
  h(SectionHead, {
506
482
  title: t.pluginNav,
507
- badge: updateCount > 0
508
- ? headBadge(`${updateCount} ${t.updatesSuffix}`, true)
509
- : headBadge(`${plugins.length} ${t.pluginsSuffix}`, false),
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-plist' },
513
- plugins.length > 0
514
- ? plugins.map((p) => h(PluginRow, { key: p.name, p, t }))
515
- : h('div', { className: 'dshsu-empty' }, t.noPlugins),
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
- // 更新/检查进行中:spinner + 阶段文案
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: updateCount > 0 && !busy ? 'dshsu-btn dshsu-btn-primary' : 'dshsu-btn',
538
- disabled: busy || checking || updateCount === 0,
525
+ className: updateAvailable && !busy ? 'dshsu-btn dshsu-btn-primary' : 'dshsu-btn',
526
+ disabled: busy || checking || !updateAvailable,
539
527
  onClick: onUpgrade,
540
- }, t.upgradeAll),
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: '插件更新', upgradeAll: '一键全部升级',
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', upgradeAll: 'Upgrade All',
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 pluginList = [];
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
- pluginList = data.plugins ?? [];
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
- const result = await api('/plugins/check', { method: 'POST', body: '{}' });
667
- const n = result.updatedCount ?? 0;
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'), upgradeAll: dict('upgradeAll'),
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
- plugins: pluginList,
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
- ...readPluginStatus(dshStateDir),
426
- state: pluginBusy() ? (readPluginStatus(dshStateDir).state ?? 'running') : (readPluginStatus(dshStateDir).state ?? 'idle'),
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
- // 把上次检查得到的 latest 缓存合并进列表,UI 无需二次请求。
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
- profile: PLUGIN_PROFILE,
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
- // 清单 = 自己 + profile 已装插件(与 GET /plugins 保持一致)。
461
- const installed = [
462
- { name: SELF_NAME, installedVersion: SELF_INSTALLED },
463
- ...listInstalledPlugins(workspace).filter((p) => p.name !== SELF_NAME),
464
- ];
465
- // 并发查询所有插件(清单已保证至少包含自己,不可能为空)的最新版;
466
- // 单个失败不拖垮整体。
467
- const results = await Promise.allSettled(installed.map(async (p) => {
468
- const latestVersion = await fetchPluginLatest(p.name);
469
- return {
470
- name: p.name,
471
- installedVersion: p.installedVersion,
472
- latestVersion,
473
- updateAvailable: isNewer(latestVersion, p.installedVersion),
474
- checkedAt: new Date().toISOString(),
475
- };
476
- }));
477
- const updates = {};
478
- for (const r of results) {
479
- if (r.status !== 'fulfilled') continue;
480
- updates[r.value.name] = {
481
- latestVersion: r.value.latestVersion,
482
- updateAvailable: r.value.updateAvailable,
483
- checkedAt: r.value.checkedAt,
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 ---------- */
@@ -328,36 +328,25 @@ async function main() {
328
328
  // 锁文件双端校验:路由触发前会检查;这里再补一道防手动重复执行。
329
329
  if (existsSync(lockFile)) throw new Error('已有一次插件更新在进行中(锁文件存在)');
330
330
 
331
- // 读已安装清单:profile/package.json 的 dependencies 是最权威的数据源;
332
- // 再补上本插件自身(dsh-selfupdater),保证"自己也能被更新"。
333
- let deps;
334
- try {
335
- deps = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8')).dependencies ?? {};
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
- const installed = Object.entries(deps).map(([name, range]) => ({
347
- name,
348
- version: String(range).replace(/^[~^]\s*/, ''),
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
- `插件更新完成:${updated.map((p) => p.name).join(', ')}`,
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.1",
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",