dsh-selfupdater 0.4.14 → 0.4.16
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 +2 -2
- package/lib/index.js +56 -5
- package/lib/updater.mjs +53 -10
- package/package.json +1 -1
package/client/settings-card.js
CHANGED
|
@@ -392,8 +392,8 @@ function headBadge(text, isNew) {
|
|
|
392
392
|
function UpdateCard({ t, status, busy, checking, onCheck, onUpgrade,
|
|
393
393
|
plugin, pluginBusy, pluginChecking, pluginMsg, onPluginCheck, onPluginUpgrade }) {
|
|
394
394
|
|
|
395
|
-
|
|
396
|
-
|
|
395
|
+
// 与插件小节保持一致:以后端 semver 权威判定为准,避免仅靠字符串 !== 误判预发布版本号
|
|
396
|
+
const updateAvailable = status?.updateAvailable === true;
|
|
397
397
|
const stage = STATE_LABELS[status?.state] ?? '';
|
|
398
398
|
|
|
399
399
|
// DSH 小节结果消息的语义着色:成功绿 / 失败红 / 其余灰。
|
package/lib/index.js
CHANGED
|
@@ -208,14 +208,39 @@ export function apply(ctx) {
|
|
|
208
208
|
});
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/**
|
|
212
|
+
* DSH 状态归一化(对齐插件 done_pending_restart 归位经验):
|
|
213
|
+
* - 升级成功后 updater 置 state=done / done_failed 并写入 latestVersion;
|
|
214
|
+
* - 重启后当前版本已追平 latestVersion 时,"升级完成"已生效,不应一直挂着成功徽章;
|
|
215
|
+
* - 读取时若判定已生效则归位为 idle 并覆写磁盘,避免每次请求重复判定。
|
|
216
|
+
* - 最终态 done_pending_restart 理论上不会出现在 DSH 链路,但一并处理以防残留。
|
|
217
|
+
*/
|
|
218
|
+
const dshStatusView = () => {
|
|
219
|
+
const raw = readStatus(dshStateDir);
|
|
220
|
+
const baseState = isBusy() ? (raw.state ?? 'running') : (raw.state ?? 'idle');
|
|
221
|
+
const finalsToSettle = ['done', 'done_failed', 'done_pending_restart'];
|
|
222
|
+
if (finalsToSettle.includes(baseState) && typeof raw.latestVersion === 'string' && raw.latestVersion !== '') {
|
|
223
|
+
const currentNow = currentDshVersion(appDir);
|
|
224
|
+
if (currentNow !== 'unknown' && isNewer(raw.latestVersion, currentNow) === false) {
|
|
225
|
+
const settled = { ...raw, state: 'idle', message: null };
|
|
226
|
+
writeStatus(dshStateDir, settled);
|
|
227
|
+
return { ...settled, state: 'idle' };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { ...raw, state: baseState };
|
|
231
|
+
};
|
|
232
|
+
|
|
211
233
|
/* ---------- GET /dsh-selfupdater/status ---------- */
|
|
212
234
|
registerRoute('GET', '/dsh-selfupdater/status', (_req, res) => {
|
|
213
|
-
const status =
|
|
235
|
+
const status = dshStatusView();
|
|
214
236
|
const current = currentDshVersion(appDir);
|
|
237
|
+
const latest = status.latestVersion ?? null;
|
|
215
238
|
sendJson(res, 200, {
|
|
216
239
|
currentVersion: current,
|
|
217
|
-
latestVersion:
|
|
218
|
-
|
|
240
|
+
latestVersion: latest,
|
|
241
|
+
// 后端权威判定是否可更新,避免前端仅靠字符串 !== 误判预发布版本号
|
|
242
|
+
updateAvailable: typeof latest === 'string' && latest !== '' ? isNewer(latest, current) === true : false,
|
|
243
|
+
state: status.state ?? 'idle',
|
|
219
244
|
message: status.message ?? null,
|
|
220
245
|
lastCheck: status.updatedAt ?? null,
|
|
221
246
|
});
|
|
@@ -266,7 +291,9 @@ export function apply(ctx) {
|
|
|
266
291
|
return;
|
|
267
292
|
}
|
|
268
293
|
// 预置锁文件:updater.mjs 启动后会校验它存在才继续。
|
|
294
|
+
// 先确保 .dsh 目录存在(首次安装/手动清理后可能尚无该目录,教训来自插件写状态 ENOENT 问题)。
|
|
269
295
|
try {
|
|
296
|
+
mkdirSync(dshStateDir, { recursive: true });
|
|
270
297
|
writeFileSync(lockFile, JSON.stringify({ pid: process.pid, startedAt: Date.now(), by: 'plugin' }));
|
|
271
298
|
} catch (err) {
|
|
272
299
|
sendJson(res, 500, { error: `写入锁文件失败:${err.message}` });
|
|
@@ -296,10 +323,34 @@ export function apply(ctx) {
|
|
|
296
323
|
* 返回数据与 DSH 小节同款三行模板:当前版本 / 最新版本 / 上次检查。
|
|
297
324
|
*/
|
|
298
325
|
const pluginBusy = () => existsSync(pluginLockFile);
|
|
299
|
-
/**
|
|
326
|
+
/**
|
|
327
|
+
* 读插件状态并合并"服务端视角"的忙闲标记。
|
|
328
|
+
*
|
|
329
|
+
* 【为何要归位 done_pending_restart】更新成功后 setState 把
|
|
330
|
+
* done_pending_restart 写进磁盘,但"是否已经重启"只有通过比较
|
|
331
|
+
* 真实落盘版本才能判定:重启后 resolveInstalledVersion 读到的新版本
|
|
332
|
+
* 已经等于 targetVersion(更新已生效),此时再把"请重启生效"的徽章
|
|
333
|
+
* 继续挂在界面上就是误导(用户实测"重启后还一直显示请重启")。
|
|
334
|
+
* 这里在读取时归一化:若状态是 done_pending_restart 且当前真实
|
|
335
|
+
* 运行版本已 >= targetVersion,说明更新已生效,把 state 归位为 idle,
|
|
336
|
+
* 并顺手覆写磁盘,避免每次读都重复判定。
|
|
337
|
+
*/
|
|
300
338
|
const pluginStatusView = () => {
|
|
301
339
|
const status = readPluginStatus(dshStateDir);
|
|
302
|
-
|
|
340
|
+
const state = status.state ?? (pluginBusy() ? 'running' : 'idle');
|
|
341
|
+
// 仅处理"待重启"终态:targetVersion 存在才可能有归位判定。
|
|
342
|
+
if (state === 'done_pending_restart' && typeof status.targetVersion === 'string' && status.targetVersion !== '') {
|
|
343
|
+
const installedNow = selfInstalled();
|
|
344
|
+
// 重启后真实版本已到目标版本 => 更新已生效,徽章不再有意义。
|
|
345
|
+
// 用 isNewer(target, installed) 判"目标是否还新于已装":返回 false
|
|
346
|
+
// 即目标不再领先,说明已装版本已追平/超过目标(更新已生效)。
|
|
347
|
+
if (installedNow !== null && isNewer(status.targetVersion, installedNow) === false) {
|
|
348
|
+
const settled = { ...status, state: 'idle', message: null };
|
|
349
|
+
writePluginStatus(dshStateDir, settled);
|
|
350
|
+
return settled;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return { ...status, state };
|
|
303
354
|
};
|
|
304
355
|
|
|
305
356
|
registerRoute('GET', '/dsh-selfupdater/plugins', (_req, res) => {
|
package/lib/updater.mjs
CHANGED
|
@@ -176,7 +176,10 @@ function setState(state, message, extra = {}) {
|
|
|
176
176
|
updatedAt: new Date().toISOString(),
|
|
177
177
|
...extra,
|
|
178
178
|
};
|
|
179
|
-
try {
|
|
179
|
+
try {
|
|
180
|
+
mkdirSync(dshStateDir, { recursive: true });
|
|
181
|
+
writeFileSync(statusFile, JSON.stringify({ ...status, pid: process.pid }, null, 2));
|
|
182
|
+
} catch { /* 状态落盘失败不阻断主流程(首次运行时 .dsh 可能尚不存在) */ }
|
|
180
183
|
log(`state=${state}${message ? ` :: ${message}` : ''}`);
|
|
181
184
|
}
|
|
182
185
|
|
|
@@ -226,20 +229,60 @@ function pnpmCommand() {
|
|
|
226
229
|
* 升级各阶段
|
|
227
230
|
* ------------------------------------------------------------------ */
|
|
228
231
|
|
|
232
|
+
/** npm registry 查询超时。 */
|
|
233
|
+
const FETCH_TIMEOUT_MS = 15000;
|
|
234
|
+
/** 国内 npm 镜像(腾讯云),直连官方源超时时的第一回退。 */
|
|
235
|
+
const NPM_CHINA_MIRROR = 'https://mirrors.cloud.tencent.com/npm';
|
|
236
|
+
|
|
237
|
+
/** 本次要依次尝试的 registry 地址列表(去重)。 */
|
|
238
|
+
function registryCandidates() {
|
|
239
|
+
const custom = process.env.DSHSU_REGISTRY_URL?.replace(/\/+$/, '');
|
|
240
|
+
const list = [custom, NPM_CHINA_MIRROR, 'https://registry.npmjs.org'];
|
|
241
|
+
return [...new Set(list.filter((v) => typeof v === 'string' && v !== ''))];
|
|
242
|
+
}
|
|
243
|
+
|
|
229
244
|
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
245
|
+
* 从单个 registry 的 /latest 版本级端点取文档(含 dist.tarball)。
|
|
246
|
+
* 用版本级端点而非整包 packument:后者会被 Fastly 等 CDN 长时间缓存,
|
|
247
|
+
* 出现"包已发布但查不到新版"的假象(插件 0.4.5 踩坑根因)。
|
|
232
248
|
*/
|
|
233
|
-
async function
|
|
234
|
-
const res = await fetch(
|
|
249
|
+
async function fetchVersionDoc(base, pkg) {
|
|
250
|
+
const res = await fetch(`${base}/${encodeURIComponent(pkg)}/latest`, {
|
|
235
251
|
headers: { accept: 'application/json', 'user-agent': 'dsh-selfupdater' },
|
|
236
|
-
signal: AbortSignal.timeout(
|
|
252
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
237
253
|
});
|
|
238
|
-
if (!res.ok) throw new Error(`
|
|
254
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
239
255
|
const doc = await res.json();
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
256
|
+
if (typeof doc?.version !== 'string' || doc.version === '') throw new Error('响应缺少 version 字段');
|
|
257
|
+
return { version: doc.version, tarball: doc?.dist?.tarball ?? null };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 查询最新版本与下载地址(与 plugin-update-task.mjs 完全一致)。
|
|
262
|
+
*
|
|
263
|
+
* 0.4.14 经验复用:旧实现"第一个成功的 registry 就采用",腾讯镜像 /latest 端点
|
|
264
|
+
* CDN 缓存陈旧时仍返回旧版本,导致"检查拿到新版、执行却被旧版骗过"而误判
|
|
265
|
+
* "无需更新"。改为并行查询全部 registry、取 semver 最大的结果。
|
|
266
|
+
*/
|
|
267
|
+
async function fetchLatestRelease(pkg) {
|
|
268
|
+
const candidates = registryCandidates();
|
|
269
|
+
const results = await Promise.allSettled(candidates.map((base) => fetchVersionDoc(base, pkg)));
|
|
270
|
+
let best = null;
|
|
271
|
+
const errors = [];
|
|
272
|
+
results.forEach((r, i) => {
|
|
273
|
+
if (r.status === 'fulfilled') {
|
|
274
|
+
if (best === null || isNewer(r.value.version, best.version)) best = r.value;
|
|
275
|
+
} else {
|
|
276
|
+
errors.push(`${candidates[i]}: ${r.reason.message}`);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
if (best === null) throw new Error(errors.join(';'));
|
|
280
|
+
return best;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** 仅取最新版本号(主流程使用)。 */
|
|
284
|
+
async function fetchLatestVersion(pkg) {
|
|
285
|
+
return (await fetchLatestRelease(pkg)).version;
|
|
243
286
|
}
|
|
244
287
|
|
|
245
288
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-selfupdater",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.16",
|
|
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",
|