dsh-selfupdater 0.4.9 → 0.4.10

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.
@@ -46,6 +46,9 @@ const POLL_ACTIVE_MS = 2000;
46
46
  const POLL_IDLE_MS = 30000;
47
47
  /** 插件清单的空闲刷新间隔(比状态轮询慢一档,避免无谓请求)。 */
48
48
  const PLUGIN_REFRESH_MS = 60000;
49
+ /** 结果消息显示时效:同一条提示("发现新版本/已是最新"等)超过该时长自动隐藏,
50
+ * 解决"切走菜单回来提示还在"的问题;重要终态另有徽章兜底。 */
51
+ const MSG_TTL_MS = 12000;
49
52
 
50
53
  /* ------------------------------------------------------------------ *
51
54
  * 工具
@@ -64,6 +67,25 @@ async function api(path, options) {
64
67
  return data;
65
68
  }
66
69
 
70
+ /** 各小节消息的"首次出现"记录(dsh / plugin 两个通道独立计时)。 */
71
+ const msgSeen = { dsh: { text: '', at: 0 }, plugin: { text: '', at: 0 } };
72
+
73
+ /**
74
+ * 按时效过滤消息:新文本重置计时并显示;相同文本未超时继续显示;
75
+ * 超过 MSG_TTL_MS 返回空串(隐藏)。切换菜单回来时轮询仍在跑,
76
+ * 过期消息自然不再渲染。
77
+ */
78
+ function agedMessage(section, text) {
79
+ if (typeof text !== 'string' || text === '') return '';
80
+ const seen = msgSeen[section];
81
+ if (text !== seen.text) {
82
+ seen.text = text;
83
+ seen.at = Date.now();
84
+ return text;
85
+ }
86
+ return Date.now() - seen.at < MSG_TTL_MS ? text : '';
87
+ }
88
+
67
89
  /* ------------------------------------------------------------------ *
68
90
  * 主题系统:探测宿主亮暗模式并注入 --dshsu-* CSS 变量
69
91
  * ------------------------------------------------------------------ */
@@ -536,6 +558,8 @@ function PluginSection({ t, plugin, busy, checking, msg, onCheck, onUpgrade }) {
536
558
  h('span', { className: 'dshsu-spacer' }),
537
559
  // 待重启徽章:提醒用户重启 DeepSeek Harness 后新版本才会生效
538
560
  pendingRestart ? h('span', { className: 'dshsu-pill dshsu-pill-new' }, t.pendingRestart) : null,
561
+ // 失败徽章:错误消息 12 秒后自动隐藏,用徽章保底提示"上次更新失败"
562
+ plugin?.state === 'error' ? h('span', { className: 'dshsu-pill dshsu-pill-bad' }, t.failed) : null,
539
563
  ),
540
564
  );
541
565
  }
@@ -608,14 +632,19 @@ function apply(ctx) {
608
632
  let latestStatus = null;
609
633
  let checking = false;
610
634
  let refresh = () => {};
635
+ /** 升级请求已发出但服务端状态尚未接管的间隙标志(点击反馈/防重复点击)。 */
636
+ let upgradeStarting = false;
611
637
 
612
638
  async function pollStatus() {
613
639
  try {
614
- latestStatus = await api('/status');
615
- } catch { /* 服务重启期间拉不到状态属正常 */ }
640
+ const data = await api('/status');
641
+ // 消息按时效过滤后再生效:过期提示不再渲染(见 agedMessage 注释)。
642
+ latestStatus = { ...data, message: agedMessage('dsh', data.message) };
643
+ upgradeStarting = false; // 服务端状态已接管视觉
644
+ } catch { /* 服务重启期间拉不到状态属正常:保持 starting 视觉 */ }
616
645
  refresh();
617
646
  // 升级中高频轮询,空闲低频保活。
618
- setTimeout(pollStatus, isBusyState(latestStatus) ? POLL_ACTIVE_MS : POLL_IDLE_MS);
647
+ setTimeout(pollStatus, isBusyState(latestStatus) || upgradeStarting ? POLL_ACTIVE_MS : POLL_IDLE_MS);
619
648
  }
620
649
 
621
650
  function isBusyState(s) {
@@ -629,6 +658,8 @@ function apply(ctx) {
629
658
  let pluginMsg = '';
630
659
  let pluginChecking = false;
631
660
  let pluginRefresh = () => {};
661
+ /** 更新请求已发出但服务端 busy 尚未接管的间隙标志(点击反馈/防重复点击)。 */
662
+ let pluginStarting = false;
632
663
 
633
664
  /** 拉取 dsh-selfupdater 自身的版本信息(含上次检查缓存的可更新标记)。 */
634
665
  async function pollPlugins() {
@@ -636,8 +667,13 @@ function apply(ctx) {
636
667
  const data = await api('/plugins');
637
668
  pluginData = data; // 响应本身就是单对象:{ currentVersion, latestVersion, ... }
638
669
  pluginBusy = data.busy === true || PLUGIN_BUSY_STATES.includes(data.state);
639
- if (!pluginBusy && typeof data.message === 'string' && data.message !== '') {
640
- pluginMsg = data.message;
670
+ if (!pluginBusy) {
671
+ // 空闲时才显示消息,且按时效过滤:过期提示(如"发现新版本")自动隐藏。
672
+ pluginMsg = agedMessage('plugin', data.message);
673
+ }
674
+ // 服务端 busy 或终态已可见:乐观标志功成身退。
675
+ if (pluginBusy || data.state === 'done_pending_restart' || data.state === 'error') {
676
+ pluginStarting = false;
641
677
  }
642
678
  } catch { /* 服务重启期间拉不到属正常 */ }
643
679
  pluginRefresh();
@@ -649,7 +685,7 @@ function apply(ctx) {
649
685
  */
650
686
  async function pluginPollLoop() {
651
687
  await pollPlugins();
652
- setTimeout(pluginPollLoop, pluginBusy || pluginChecking ? POLL_ACTIVE_MS : PLUGIN_REFRESH_MS);
688
+ setTimeout(pluginPollLoop, pluginBusy || pluginChecking || pluginStarting ? POLL_ACTIVE_MS : PLUGIN_REFRESH_MS);
653
689
  }
654
690
 
655
691
  /** 检查插件更新:POST /plugins/check 只查自己一个包,成功后立刻重拉结果。 */
@@ -670,16 +706,25 @@ function apply(ctx) {
670
706
  }
671
707
  }
672
708
 
673
- /** 一键升级自身:v0.4.6 起服务保持运行,更新完成后提示重启生效。 */
709
+ /** 一键升级自身:点击立即进入"进行中"视觉(不等轮询),按钮随之禁用防重复点击;
710
+ * v0.4.6 起服务保持运行,更新完成后提示重启生效。 */
674
711
  async function handlePluginUpgrade() {
712
+ if (pluginStarting || pluginBusy) return; // 防重复点击
713
+ pluginStarting = true;
714
+ pluginMsg = '';
715
+ pluginRefresh();
675
716
  try {
676
717
  await api('/plugins/update', { method: 'POST', body: '{}' });
677
- pluginMsg = '';
678
- // 后台任务运行期间高频轮询进度,直至出现 done_pending_restart / error 终态。
679
- setTimeout(pluginPollLoop, POLL_ACTIVE_MS);
718
+ // 202 返回时锁文件已写,立刻拉一次即可拿到服务端 busy 接管视觉。
719
+ await pollPlugins();
680
720
  } catch (err) {
681
721
  console.warn(`[${NS}] 触发插件更新失败:`, err);
682
722
  pluginMsg = `触发更新失败:${err.message}`;
723
+ pluginStarting = false;
724
+ } finally {
725
+ // 成功路径下 starting 已被 pollPlugins 清除(服务端 busy 接管);
726
+ // 这里只兜底失败/未接管的情况,避免视觉卡死。
727
+ if (!pluginBusy) pluginStarting = false;
683
728
  pluginRefresh();
684
729
  }
685
730
  }
@@ -700,13 +745,20 @@ function apply(ctx) {
700
745
  }
701
746
  }
702
747
 
748
+ /** 一键升级 DSH:点击立即进入"进行中"视觉(不等轮询),按钮随之禁用防重复点击。 */
703
749
  async function handleUpgrade() {
750
+ if (upgradeStarting) return; // 防重复点击
751
+ upgradeStarting = true;
752
+ refresh();
704
753
  try {
705
754
  await api('/perform', { method: 'POST', body: '{}' });
706
- // 服务即将退出;进入高频轮询等新进程起来后自动恢复。
707
- setTimeout(pollStatus, POLL_ACTIVE_MS);
755
+ // 锁文件已写,立刻拉一次状态拿 running;若已赶上服务退出,
756
+ // 轮询失败保持 starting 视觉,由循环重试直到新进程起来接管。
757
+ await pollStatus();
708
758
  } catch (err) {
709
759
  console.warn(`[${NS}] 触发升级失败:`, err);
760
+ upgradeStarting = false;
761
+ refresh();
710
762
  }
711
763
  }
712
764
 
@@ -730,12 +782,12 @@ function apply(ctx) {
730
782
  pendingRestart: dict('pendingRestart'),
731
783
  },
732
784
  status: latestStatus,
733
- busy: isBusyState(latestStatus),
785
+ busy: isBusyState(latestStatus) || upgradeStarting,
734
786
  checking,
735
787
  onCheck: handleCheck,
736
788
  onUpgrade: handleUpgrade,
737
789
  plugin: pluginData,
738
- pluginBusy,
790
+ pluginBusy: pluginBusy || pluginStarting,
739
791
  pluginChecking,
740
792
  pluginMsg,
741
793
  onPluginCheck: handlePluginCheck,
@@ -18,7 +18,7 @@
18
18
  import { spawn } from 'node:child_process';
19
19
  import { createRequire } from 'node:module';
20
20
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
21
- import { dirname, join } from 'node:path';
21
+ import { dirname, join, resolve } from 'node:path';
22
22
  import { fileURLToPath } from 'node:url';
23
23
 
24
24
  /** 本插件包名(更新的目标就是自己)。 */
@@ -193,16 +193,51 @@ export function installedSelfVersion(workspace) {
193
193
  return resolveInstalledVersion(profileDir);
194
194
  }
195
195
 
196
- /** 定位 dsh CLI 入口:打包布局优先,其次 node_modules 标准布局。 */
196
+ /**
197
+ * 定位 dsh CLI 入口(原地安装新版的执行器)。
198
+ * 0.4.10 修复:此前硬编码 bin/dsh.js,但 dsh 包实际 bin 是 lib/bin.js
199
+ * (由 package.json bin 字段声明),NAS 上因此报"无法定位 dsh 命令行工具"。
200
+ * 三级定位,任一命中即返回:
201
+ * 1. 读 dsh 包 package.json 的 bin 字段(权威,适配未来布局变化);
202
+ * 2. 常见候选路径(含新旧两种布局);
203
+ * 3. 从 DSH 进程自身启动入口(process.argv[1])向上推导包根再找 bin。
204
+ */
197
205
  function locateDshBin(appDir) {
198
- const candidates = [
206
+ const pkgRoot = join(appDir, 'node_modules', '@deepseek-ai', 'dsh');
207
+ // 1. bin 字段:字符串直接用;对象取 dsh 键,缺失则取第一个值。
208
+ const binField = readJson(join(pkgRoot, 'package.json')).bin;
209
+ const values = binField && typeof binField === 'object' ? Object.values(binField) : [];
210
+ const binRel = typeof binField === 'string' ? binField
211
+ : typeof binField?.dsh === 'string' ? binField.dsh
212
+ : typeof values[0] === 'string' ? values[0]
213
+ : null;
214
+ if (binRel !== null) {
215
+ const p = resolve(pkgRoot, binRel);
216
+ if (existsSync(p)) return p;
217
+ }
218
+ // 2. 常见候选:新布局 lib/bin.js 优先,兼容旧的 bin/dsh.js。
219
+ for (const p of [
220
+ join(pkgRoot, 'lib', 'bin.js'),
221
+ join(pkgRoot, 'bin', 'dsh.js'),
199
222
  join(appDir, 'bin', 'dsh.js'),
200
- join(appDir, 'node_modules', '@deepseek-ai', 'dsh', 'bin', 'dsh.js'),
201
- ];
202
- for (const p of candidates) {
223
+ ]) {
203
224
  if (existsSync(p)) return p;
204
225
  }
205
- throw new Error('无法定位 dsh 命令行工具(bin/dsh.js)');
226
+ // 3. DSH 进程自身入口向上找包根(如 node …/dsh/lib/xxx.js 启动时)。
227
+ if (typeof process.argv[1] === 'string') {
228
+ let dir = dirname(resolve(process.argv[1]));
229
+ for (let i = 0; i < 6; i++) {
230
+ if (readJson(join(dir, 'package.json')).name === '@deepseek-ai/dsh') {
231
+ const p = join(dir, 'lib', 'bin.js');
232
+ if (existsSync(p)) return p;
233
+ break;
234
+ }
235
+ const parent = dirname(dir);
236
+ if (parent === dir) break;
237
+ dir = parent;
238
+ }
239
+ }
240
+ throw new Error(`无法定位 dsh 命令行工具(bin 字段与常见路径均未命中,appDir=${appDir})`);
206
241
  }
207
242
 
208
243
  /** 下载 tgz 到本地暂存路径(体积小,直接缓冲写入)。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-selfupdater",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "description": "Self-update plugin for DeepSeek Harness: DSH core upgrades via detached swap script; plugin self-update installs in-place without killing the host and prompts for restart. DSH 主程序与已装插件的一站式在线更新插件。",
5
5
  "license": "MIT",
6
6
  "type": "module",