dsh-selfupdater 0.4.16 → 0.4.18
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/lib/index.js +16 -5
- package/lib/plugin-update-task.mjs +14 -5
- package/lib/updater.mjs +27 -8
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - 升级动作通过锁文件防并发,双端校验。
|
|
15
15
|
*/
|
|
16
16
|
import { spawn } from 'node:child_process';
|
|
17
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
18
18
|
import { dirname, join, resolve } from 'node:path';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
20
|
import { fetchLatestVersion, installedSelfVersion, isNewer, runPluginUpdateTask } from './plugin-update-task.mjs';
|
|
@@ -258,7 +258,7 @@ export function apply(ctx) {
|
|
|
258
258
|
const updateAvailable = isNewer(latest, current);
|
|
259
259
|
// 检查结果落盘(内部自动建目录),UI 刷新后仍能看到上次检查时间。
|
|
260
260
|
writeStatus(dshStateDir, {
|
|
261
|
-
...
|
|
261
|
+
...readStatus(dshStateDir),
|
|
262
262
|
currentVersion: current,
|
|
263
263
|
latestVersion: latest,
|
|
264
264
|
state: isBusy() ? 'running' : 'idle',
|
|
@@ -286,11 +286,22 @@ export function apply(ctx) {
|
|
|
286
286
|
return;
|
|
287
287
|
}
|
|
288
288
|
// 并发防护:锁文件已存在说明有升级在跑(或上次异常残留未清理)。
|
|
289
|
+
// 残留判定:锁内容损坏,或 startedAt 超过 15 分钟(升级脚本总看门狗
|
|
290
|
+
// 仅 8 分钟,超过必然已死)—— 自动清理放行,避免强杀后永久 409。
|
|
289
291
|
if (isBusy()) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
+
let stale = false;
|
|
293
|
+
try {
|
|
294
|
+
const prev = JSON.parse(readFileSync(lockFile, 'utf8'));
|
|
295
|
+
stale = Date.now() - Number(prev.startedAt ?? 0) > 15 * 60 * 1000;
|
|
296
|
+
} catch { stale = true; }
|
|
297
|
+
if (!stale) {
|
|
298
|
+
sendJson(res, 409, { error: '已有一次升级在进行中' });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
try { rmSync(lockFile, { force: true }); } catch { /* 清不掉则下次再试 */ }
|
|
292
302
|
}
|
|
293
|
-
//
|
|
303
|
+
// 预置锁文件:作为并发防护;updater.mjs 见到 by=plugin 的新鲜锁会
|
|
304
|
+
// 视为合法握手接管(见 updater.mjs main 的锁文件握手注释)。
|
|
294
305
|
// 先确保 .dsh 目录存在(首次安装/手动清理后可能尚无该目录,教训来自插件写状态 ENOENT 问题)。
|
|
295
306
|
try {
|
|
296
307
|
mkdirSync(dshStateDir, { recursive: true });
|
|
@@ -88,13 +88,17 @@ function registryCandidates() {
|
|
|
88
88
|
return [...new Set(list.filter((v) => typeof v === 'string' && v !== ''))];
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/** 除 latest 外额外追踪的官方发布渠道 tag(官方习惯把新版先挂在 next/alpha 上)。 */
|
|
92
|
+
const EXTRA_DIST_TAGS = ['next', 'alpha'];
|
|
93
|
+
|
|
91
94
|
/**
|
|
92
|
-
* 从单个 registry
|
|
95
|
+
* 从单个 registry 的版本级端点(/<pkg>/<tag>)取文档(含 dist.tarball)。
|
|
93
96
|
* 用版本级端点而非整包 packument:后者会被 Fastly 等 CDN 长时间缓存,
|
|
94
97
|
* 出现"包已发布但查不到新版"的假象。
|
|
98
|
+
* tag 不存在时(如包从未打过 alpha tag)registry 返回 404,由调用方 allSettled 吞掉。
|
|
95
99
|
*/
|
|
96
|
-
async function fetchVersionDoc(base, pkg) {
|
|
97
|
-
const res = await fetch(`${base}/${encodeURIComponent(pkg)}
|
|
100
|
+
async function fetchVersionDoc(base, pkg, tag = 'latest') {
|
|
101
|
+
const res = await fetch(`${base}/${encodeURIComponent(pkg)}/${tag}`, {
|
|
98
102
|
headers: { accept: 'application/json', 'user-agent': 'dsh-selfupdater' },
|
|
99
103
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
100
104
|
});
|
|
@@ -117,14 +121,19 @@ async function fetchVersionDoc(base, pkg) {
|
|
|
117
121
|
*/
|
|
118
122
|
async function fetchLatestRelease(pkg) {
|
|
119
123
|
const candidates = registryCandidates();
|
|
120
|
-
|
|
124
|
+
// 每个 registry × 每个渠道 tag(latest/next/alpha)并行查询:
|
|
125
|
+
// 只查 /latest 会漏掉挂在 next/alpha tag 上的新版(0.1.2-alpha.2 实测踩坑),
|
|
126
|
+
// 与 build.sh 优先取 dist-tags.next 的官方发版习惯保持一致。
|
|
127
|
+
const tags = ['latest', ...EXTRA_DIST_TAGS];
|
|
128
|
+
const targets = candidates.flatMap((base) => tags.map((tag) => ({ base, tag })));
|
|
129
|
+
const results = await Promise.allSettled(targets.map((t) => fetchVersionDoc(t.base, pkg, t.tag)));
|
|
121
130
|
let best = null;
|
|
122
131
|
const errors = [];
|
|
123
132
|
results.forEach((r, i) => {
|
|
124
133
|
if (r.status === 'fulfilled') {
|
|
125
134
|
if (best === null || isNewer(r.value.version, best.version)) best = r.value;
|
|
126
135
|
} else {
|
|
127
|
-
errors.push(`${
|
|
136
|
+
errors.push(`${targets[i].base}/${targets[i].tag}: ${r.reason.message}`);
|
|
128
137
|
}
|
|
129
138
|
});
|
|
130
139
|
if (best === null) throw new Error(errors.join(';'));
|
package/lib/updater.mjs
CHANGED
|
@@ -241,13 +241,17 @@ function registryCandidates() {
|
|
|
241
241
|
return [...new Set(list.filter((v) => typeof v === 'string' && v !== ''))];
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
/** 除 latest 外额外追踪的官方发布渠道 tag(官方习惯把新版先挂在 next/alpha 上)。 */
|
|
245
|
+
const EXTRA_DIST_TAGS = ['next', 'alpha'];
|
|
246
|
+
|
|
244
247
|
/**
|
|
245
|
-
* 从单个 registry
|
|
248
|
+
* 从单个 registry 的版本级端点(/<pkg>/<tag>)取文档(含 dist.tarball)。
|
|
246
249
|
* 用版本级端点而非整包 packument:后者会被 Fastly 等 CDN 长时间缓存,
|
|
247
250
|
* 出现"包已发布但查不到新版"的假象(插件 0.4.5 踩坑根因)。
|
|
251
|
+
* tag 不存在时(如包从未打过 alpha tag)registry 返回 404,由调用方 allSettled 吞掉。
|
|
248
252
|
*/
|
|
249
|
-
async function fetchVersionDoc(base, pkg) {
|
|
250
|
-
const res = await fetch(`${base}/${encodeURIComponent(pkg)}
|
|
253
|
+
async function fetchVersionDoc(base, pkg, tag = 'latest') {
|
|
254
|
+
const res = await fetch(`${base}/${encodeURIComponent(pkg)}/${tag}`, {
|
|
251
255
|
headers: { accept: 'application/json', 'user-agent': 'dsh-selfupdater' },
|
|
252
256
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
253
257
|
});
|
|
@@ -266,14 +270,19 @@ async function fetchVersionDoc(base, pkg) {
|
|
|
266
270
|
*/
|
|
267
271
|
async function fetchLatestRelease(pkg) {
|
|
268
272
|
const candidates = registryCandidates();
|
|
269
|
-
|
|
273
|
+
// 每个 registry × 每个渠道 tag(latest/next/alpha)并行查询:
|
|
274
|
+
// 只查 /latest 会漏掉挂在 next/alpha tag 上的新版(0.1.2-alpha.2 实测踩坑),
|
|
275
|
+
// 与 build.sh 优先取 dist-tags.next 的官方发版习惯保持一致。
|
|
276
|
+
const tags = ['latest', ...EXTRA_DIST_TAGS];
|
|
277
|
+
const targets = candidates.flatMap((base) => tags.map((tag) => ({ base, tag })));
|
|
278
|
+
const results = await Promise.allSettled(targets.map((t) => fetchVersionDoc(t.base, pkg, t.tag)));
|
|
270
279
|
let best = null;
|
|
271
280
|
const errors = [];
|
|
272
281
|
results.forEach((r, i) => {
|
|
273
282
|
if (r.status === 'fulfilled') {
|
|
274
283
|
if (best === null || isNewer(r.value.version, best.version)) best = r.value;
|
|
275
284
|
} else {
|
|
276
|
-
errors.push(`${
|
|
285
|
+
errors.push(`${targets[i].base}/${targets[i].tag}: ${r.reason.message}`);
|
|
277
286
|
}
|
|
278
287
|
});
|
|
279
288
|
if (best === null) throw new Error(errors.join(';'));
|
|
@@ -394,9 +403,19 @@ async function main() {
|
|
|
394
403
|
mkdirSync(dshStateDir, { recursive: true });
|
|
395
404
|
const current = currentDshVersion();
|
|
396
405
|
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
406
|
+
// 锁文件握手:插件本体 POST /perform 会预写锁文件(by=plugin)作为并发
|
|
407
|
+
// 防护,本脚本见到它应视为合法交接直接接管 —— 旧实现"存在即拒绝"与
|
|
408
|
+
// 插件端的预写约定自相矛盾,导致升级必然秒败(0.4.17 实测踩坑)。
|
|
409
|
+
// 仅当锁是陈旧残留(超过 10 分钟)或来源不明时才判定为重复执行。
|
|
410
|
+
if (existsSync(lockFile)) {
|
|
411
|
+
let handover = false;
|
|
412
|
+
try {
|
|
413
|
+
const prev = JSON.parse(readFileSync(lockFile, 'utf8'));
|
|
414
|
+
handover = prev?.by === 'plugin' && Date.now() - Number(prev.startedAt ?? 0) < 10 * 60 * 1000;
|
|
415
|
+
} catch { handover = false; }
|
|
416
|
+
if (!handover) throw new Error('已有一次升级在进行中(锁文件存在)');
|
|
417
|
+
}
|
|
418
|
+
writeFileSync(lockFile, JSON.stringify({ pid: process.pid, startedAt: Date.now(), by: 'updater' }));
|
|
400
419
|
|
|
401
420
|
status = { currentVersion: current, startedAt: new Date().toISOString(), trigger: 'manual' };
|
|
402
421
|
setState('downloading', '正在查询 npm 最新版本 …');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-selfupdater",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.18",
|
|
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",
|