dsh-selfupdater 0.4.8 → 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.
- package/client/settings-card.js +66 -14
- package/lib/plugin-update-task.mjs +42 -7
- package/package.json +1 -1
package/client/settings-card.js
CHANGED
|
@@ -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
|
-
|
|
615
|
-
|
|
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
|
|
640
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
678
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
201
|
-
];
|
|
202
|
-
for (const p of candidates) {
|
|
223
|
+
]) {
|
|
203
224
|
if (existsSync(p)) return p;
|
|
204
225
|
}
|
|
205
|
-
|
|
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.
|
|
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",
|