dsh-selfupdater 0.4.15 → 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 +30 -3
- 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}` });
|
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",
|