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.
@@ -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
@@ -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
- /** 解析工作区目录(与 runner.js 的优先级一致)。 */
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
- return resolve(process.env.TRIM_VAR ?? appDir);
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
- * 列出 profile 中已安装的插件及其版本。
237
- * 数据源是 profile/package.json 的 dependencies 字段 —— dsh plugin add
238
- * 安装时会把它登记进去,这是最权威的"已装清单"
258
+ * 读本插件自身的已装版本:优先 import 自己的 package.json(ESM 顶层 await
259
+ * 不适合这里,改用 createRequire 同步读取),失败时回退硬编码兜底值。
260
+ * 用途:让"检测更新"能覆盖 dsh-selfupdater 自己 —— 此前清单来自 profile
261
+ * 的 dependencies,但"检测自己"语义上不依赖那份清单。
239
262
  */
240
- function listInstalledPlugins(workspace) {
241
- const profilePkg = join(workspace, '.dsh', 'profiles', PLUGIN_PROFILE, 'package.json');
263
+ function readOwnVersion() {
242
264
  try {
243
- const deps = JSON.parse(readFileSync(profilePkg, 'utf8')).dependencies ?? {};
244
- return Object.entries(deps).map(([pkgName, range]) => ({
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
- ...readPluginStatus(dshStateDir),
386
- state: pluginBusy() ? (readPluginStatus(dshStateDir).state ?? 'running') : (readPluginStatus(dshStateDir).state ?? 'idle'),
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
- // 把上次检查得到的 latest 缓存合并进列表,UI 无需二次请求。
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
- 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,
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
- const installed = listInstalledPlugins(workspace);
417
- if (installed.length === 0) {
418
- sendJson(res, 200, { plugins: [], note: '未发现已安装插件' });
419
- return;
420
- }
421
- // 并发查询所有插件的 npm 最新版;单个失败不拖垮整体。
422
- const results = await Promise.allSettled(installed.map(async (p) => {
423
- const latestVersion = await fetchPluginLatest(p.name);
424
- return {
425
- name: p.name,
426
- installedVersion: p.installedVersion,
427
- latestVersion,
428
- updateAvailable: isNewer(latestVersion, p.installedVersion),
429
- checkedAt: new Date().toISOString(),
430
- };
431
- }));
432
- const updates = {};
433
- for (const r of results) {
434
- if (r.status !== 'fulfilled') continue;
435
- updates[r.value.name] = {
436
- latestVersion: r.value.latestVersion,
437
- updateAvailable: r.value.updateAvailable,
438
- checkedAt: r.value.checkedAt,
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 ---------- */
@@ -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
- /** 工作区目录(存放 .dsh 状态目录与插件 profile)。 */
126
- const workspace = resolve(String(args.workspace ?? process.env.TRIM_VAR ?? appDir));
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
- // 读已安装清单:profile/package.json 的 dependencies 是最权威的数据源。
320
- let deps;
331
+ /**
332
+ * 只更新本插件自身(dsh-selfupdater):其他插件已有各自的更新渠道,
333
+ * 不再扫描 profile 的 dependencies 清单。
334
+ * 已装版本直接读自己脚本身旁的 package.json,最可靠。
335
+ */
336
+ let selfVersion = '';
321
337
  try {
322
- deps = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8')).dependencies ?? {};
323
- } catch {
324
- setState('idle', '未发现已安装插件,无需更新');
325
- finish(0);
326
- }
327
- const installed = Object.entries(deps).map(([name, range]) => ({
328
- name,
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
- `插件更新完成:${updated.map((p) => p.name).join(', ')}`,
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
- /** 工作区目录(存放 .dsh 状态目录)。 */
134
- const workspace = resolve(String(args.workspace ?? process.env.TRIM_VAR ?? appDir));
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.0",
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",