dsh-selfupdater 0.4.19 → 0.4.22
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 +33 -2
- package/lib/index.js +63 -2
- package/lib/plugin-update-task.mjs +23 -10
- package/lib/updater.mjs +17 -6
- package/package.json +1 -1
package/client/settings-card.js
CHANGED
|
@@ -238,6 +238,8 @@ const CARD_CSS = `
|
|
|
238
238
|
.dshsu-step-active{background:var(--dshsu-accent);animation:dshsu-pulse 1.1s ease-in-out infinite}
|
|
239
239
|
@keyframes dshsu-pulse{50%{opacity:.35}}
|
|
240
240
|
.dshsu-actions{display:flex;align-items:center;gap:8px;margin-top:2px}
|
|
241
|
+
.dshsu-channel{display:flex;align-items:center;gap:6px;margin-top:8px;font-size:12px;opacity:.75;cursor:pointer;user-select:none}
|
|
242
|
+
.dshsu-channel input{accent-color:var(--dshsu-accent,#c9a227)}
|
|
241
243
|
.dshsu-spacer{flex:1}
|
|
242
244
|
.dshsu-btn{appearance:none;border:1px solid var(--dshsu-border);border-radius:8px;cursor:pointer;
|
|
243
245
|
padding:6px 14px;font-size:13px;background:transparent;color:inherit;display:inline-flex;
|
|
@@ -389,7 +391,7 @@ function headBadge(text, isNew) {
|
|
|
389
391
|
* 上半部分 = DSH 更新小节;下半部分 = 插件更新小节。
|
|
390
392
|
* 两组状态相互独立、互不阻塞;样式共享同一套 CSS 类与主题变量。
|
|
391
393
|
*/
|
|
392
|
-
function UpdateCard({ t, status, busy, checking, onCheck, onUpgrade,
|
|
394
|
+
function UpdateCard({ t, status, busy, checking, onCheck, onUpgrade, onChannelChange,
|
|
393
395
|
plugin, pluginBusy, pluginChecking, pluginMsg, onPluginCheck, onPluginUpgrade }) {
|
|
394
396
|
|
|
395
397
|
// 与插件小节保持一致:以后端 semver 权威判定为准,避免仅靠字符串 !== 误判预发布版本号
|
|
@@ -456,6 +458,17 @@ function UpdateCard({ t, status, busy, checking, onCheck, onUpgrade,
|
|
|
456
458
|
: ['error', 'done_failed'].includes(status?.state) ? h('span', { className: 'dshsu-pill dshsu-pill-bad' }, t.failed)
|
|
457
459
|
: null,
|
|
458
460
|
),
|
|
461
|
+
// alpha 预发布渠道开关:默认关闭(alpha 可能与旧插件 client bundle 不兼容,
|
|
462
|
+
// 0.1.2-alpha.3 实测升级后 web 端进不去),开启后检测/升级都会纳入 alpha 渠道。
|
|
463
|
+
h('label', { className: 'dshsu-channel' },
|
|
464
|
+
h('input', {
|
|
465
|
+
type: 'checkbox',
|
|
466
|
+
checked: status?.channel === 'alpha',
|
|
467
|
+
disabled: busy,
|
|
468
|
+
onChange: (e) => onChannelChange(e.target.checked === true),
|
|
469
|
+
}),
|
|
470
|
+
t.alphaChannel,
|
|
471
|
+
),
|
|
459
472
|
|
|
460
473
|
/* ---- 分隔线:视觉上把 DSH 更新与插件更新分成两区 ---- */
|
|
461
474
|
h('div', { className: 'dshsu-divider' }),
|
|
@@ -580,6 +593,7 @@ const FALLBACK_DICT = {
|
|
|
580
593
|
currentVersion: '当前版本', latestVersion: '最新版本',
|
|
581
594
|
lastCheck: '上次检查', never: '从未', processing: '处理中…',
|
|
582
595
|
updateAvailable: '可更新', upgraded: '已升级', failed: '失败',
|
|
596
|
+
alphaChannel: '追踪 alpha 预发布渠道(可能不稳定)',
|
|
583
597
|
pluginNav: '插件更新',
|
|
584
598
|
pluginUpdating: '插件更新进行中…', checkingLabel: '正在检查更新…',
|
|
585
599
|
checkFailed: '检查更新失败',
|
|
@@ -592,6 +606,7 @@ const FALLBACK_DICT = {
|
|
|
592
606
|
currentVersion: 'Current', latestVersion: 'Latest',
|
|
593
607
|
lastCheck: 'Last check', never: 'never', processing: 'Working…',
|
|
594
608
|
updateAvailable: 'Update', upgraded: 'Upgraded', failed: 'Failed',
|
|
609
|
+
alphaChannel: 'Track alpha prereleases (may be unstable)',
|
|
595
610
|
pluginNav: 'Plugin Updates',
|
|
596
611
|
pluginUpdating: 'Plugin update in progress…', checkingLabel: 'Checking…',
|
|
597
612
|
checkFailed: 'Check failed',
|
|
@@ -648,7 +663,10 @@ function apply(ctx) {
|
|
|
648
663
|
...data,
|
|
649
664
|
message: agedMessage('dsh', data.message, FINAL_MSG_STATES.includes(data.state)),
|
|
650
665
|
};
|
|
651
|
-
|
|
666
|
+
// 仅当服务端给出"进行中"或终态时才撤销乐观视觉:perform 受理初期
|
|
667
|
+
// 状态可能仍是旧的 idle,若此时撤销会退回"发现新版本"且按钮恢复
|
|
668
|
+
// 可点,造成重复点击(0.4.19 实测踩坑)。
|
|
669
|
+
if (isBusyState(latestStatus) || FINAL_MSG_STATES.includes(data.state)) upgradeStarting = false;
|
|
652
670
|
} catch { /* 服务重启期间拉不到状态属正常:保持 starting 视觉 */ }
|
|
653
671
|
refresh();
|
|
654
672
|
// 升级中高频轮询,空闲低频保活。
|
|
@@ -739,6 +757,17 @@ function apply(ctx) {
|
|
|
739
757
|
}
|
|
740
758
|
}
|
|
741
759
|
|
|
760
|
+
/** 切换 DSH 检测渠道(alpha 预发布开关):保存到后端并刷新回显。 */
|
|
761
|
+
async function handleChannelChange(enabled) {
|
|
762
|
+
try {
|
|
763
|
+
await api('/channel', { method: 'POST', body: JSON.stringify({ channel: enabled ? 'alpha' : 'stable' }) });
|
|
764
|
+
} catch (err) {
|
|
765
|
+
console.warn(`[${NS}] 保存渠道设置失败:`, err);
|
|
766
|
+
}
|
|
767
|
+
// 无论成败都刷新:开关以服务端落盘的真实渠道为准(保存失败会弹回)
|
|
768
|
+
await pollStatus();
|
|
769
|
+
}
|
|
770
|
+
|
|
742
771
|
async function handleCheck() {
|
|
743
772
|
checking = true;
|
|
744
773
|
refresh();
|
|
@@ -787,6 +816,7 @@ function apply(ctx) {
|
|
|
787
816
|
currentVersion: dict('currentVersion'), latestVersion: dict('latestVersion'),
|
|
788
817
|
lastCheck: dict('lastCheck'), never: dict('never'), processing: dict('processing'),
|
|
789
818
|
updateAvailable: dict('updateAvailable'), upgraded: dict('upgraded'), failed: dict('failed'),
|
|
819
|
+
alphaChannel: dict('alphaChannel'),
|
|
790
820
|
pluginNav: dict('pluginNav'),
|
|
791
821
|
pluginUpdating: dict('pluginUpdating'), checkingLabel: dict('checkingLabel'),
|
|
792
822
|
pendingRestart: dict('pendingRestart'),
|
|
@@ -796,6 +826,7 @@ function apply(ctx) {
|
|
|
796
826
|
checking,
|
|
797
827
|
onCheck: handleCheck,
|
|
798
828
|
onUpgrade: handleUpgrade,
|
|
829
|
+
onChannelChange: handleChannelChange,
|
|
799
830
|
plugin: pluginData,
|
|
800
831
|
pluginBusy: pluginBusy || pluginStarting,
|
|
801
832
|
pluginChecking,
|
package/lib/index.js
CHANGED
|
@@ -230,6 +230,20 @@ export function apply(ctx) {
|
|
|
230
230
|
return { ...raw, state: baseState };
|
|
231
231
|
};
|
|
232
232
|
|
|
233
|
+
/* ---------- DSH 检测渠道设置(alpha 预发布开关) ---------- */
|
|
234
|
+
// 开关持久化在 workspace/.dsh/selfupdate-config.json,插件更新/重启都不丢。
|
|
235
|
+
// alpha 是官方预发布通道,可能与旧插件 client bundle 不兼容(0.1.2-alpha.3
|
|
236
|
+
// 实测升级后 web 端 boot 报错),因此默认关闭,由用户在设置卡片显式开启。
|
|
237
|
+
const channelFile = join(dshStateDir, 'selfupdate-config.json');
|
|
238
|
+
const readChannel = () => {
|
|
239
|
+
try {
|
|
240
|
+
return JSON.parse(readFileSync(channelFile, 'utf8'))?.channel === 'alpha' ? 'alpha' : 'stable';
|
|
241
|
+
} catch { return 'stable'; }
|
|
242
|
+
};
|
|
243
|
+
// 检测用渠道 tag 列表:stable 只看 latest+next(与 build.sh 取版策略一致);
|
|
244
|
+
// alpha 开启后纳入 alpha 渠道(显式尝鲜)。
|
|
245
|
+
const dshTags = () => (readChannel() === 'alpha' ? ['latest', 'next', 'alpha'] : ['latest', 'next']);
|
|
246
|
+
|
|
233
247
|
/* ---------- GET /dsh-selfupdater/status ---------- */
|
|
234
248
|
registerRoute('GET', '/dsh-selfupdater/status', (_req, res) => {
|
|
235
249
|
const status = dshStatusView();
|
|
@@ -243,6 +257,8 @@ export function apply(ctx) {
|
|
|
243
257
|
state: status.state ?? 'idle',
|
|
244
258
|
message: status.message ?? null,
|
|
245
259
|
lastCheck: status.updatedAt ?? null,
|
|
260
|
+
// 当前检测渠道(前端 alpha 开关的回显依据)
|
|
261
|
+
channel: readChannel(),
|
|
246
262
|
});
|
|
247
263
|
});
|
|
248
264
|
|
|
@@ -253,7 +269,8 @@ export function apply(ctx) {
|
|
|
253
269
|
return;
|
|
254
270
|
}
|
|
255
271
|
try {
|
|
256
|
-
|
|
272
|
+
// 渠道 tag 按 UI 开关传入:stable 只看 latest+next,alpha 开启后多查 alpha
|
|
273
|
+
const latest = await fetchLatestVersion(PKG, { tags: dshTags() });
|
|
257
274
|
const current = currentDshVersion(appDir);
|
|
258
275
|
const updateAvailable = isNewer(latest, current);
|
|
259
276
|
// 检查结果落盘(内部自动建目录),UI 刷新后仍能看到上次检查时间。
|
|
@@ -310,6 +327,15 @@ export function apply(ctx) {
|
|
|
310
327
|
sendJson(res, 500, { error: `写入锁文件失败:${err.message}` });
|
|
311
328
|
return;
|
|
312
329
|
}
|
|
330
|
+
// 受理即落盘 running 状态:给前端乐观视觉一个服务端依据(转圈 + 按钮禁用),
|
|
331
|
+
// 避免宿主退出前轮询仍读到旧的 idle/"发现新版本" 而误判升级没发生、
|
|
332
|
+
// 允许重复点击。latestVersion 等历史字段原样保留,供归位判定使用。
|
|
333
|
+
writeStatus(dshStateDir, {
|
|
334
|
+
...readStatus(dshStateDir),
|
|
335
|
+
state: 'running',
|
|
336
|
+
message: '升级已受理,服务即将切换到升级脚本…',
|
|
337
|
+
updatedAt: new Date().toISOString(),
|
|
338
|
+
});
|
|
313
339
|
const child = spawn(process.execPath, [
|
|
314
340
|
join(PLUGIN_ROOT, 'lib', 'updater.mjs'),
|
|
315
341
|
'--pid', String(process.pid),
|
|
@@ -317,7 +343,12 @@ export function apply(ctx) {
|
|
|
317
343
|
'--workspace', workspace,
|
|
318
344
|
'--port', String(port),
|
|
319
345
|
'--pkg', PKG,
|
|
320
|
-
], {
|
|
346
|
+
], {
|
|
347
|
+
detached: true,
|
|
348
|
+
stdio: 'ignore',
|
|
349
|
+
// alpha 开关透传给升级脚本(updater.mjs 的 distTags 读该环境变量)
|
|
350
|
+
env: readChannel() === 'alpha' ? { ...process.env, DSHSU_DSH_CHANNEL: 'alpha' } : process.env,
|
|
351
|
+
});
|
|
321
352
|
child.unref();
|
|
322
353
|
|
|
323
354
|
host.logger?.info?.(`[dsh-selfupdater] 升级进程已启动 pid=${child.pid},主程序即将退出`);
|
|
@@ -327,6 +358,36 @@ export function apply(ctx) {
|
|
|
327
358
|
sendJson(res, 202, { accepted: true, note: '服务将自动退出并由升级脚本接管' });
|
|
328
359
|
});
|
|
329
360
|
|
|
361
|
+
/* ---------- POST /dsh-selfupdater/channel ---------- */
|
|
362
|
+
/** 读取并解析 JSON 请求体(仅一个开关字段,无需引入框架式解析)。 */
|
|
363
|
+
const readJsonBody = (req) => new Promise((resolve) => {
|
|
364
|
+
let raw = '';
|
|
365
|
+
req.on('data', (chunk) => {
|
|
366
|
+
raw += chunk;
|
|
367
|
+
if (raw.length > 4096) { resolve({}); req.destroy(); }
|
|
368
|
+
});
|
|
369
|
+
req.on('end', () => { try { resolve(JSON.parse(raw || '{}')); } catch { resolve({}); } });
|
|
370
|
+
req.on('error', () => resolve({}));
|
|
371
|
+
});
|
|
372
|
+
registerRoute('POST', '/dsh-selfupdater/channel', async (req, res) => {
|
|
373
|
+
if (!sameOrigin(req)) {
|
|
374
|
+
sendJson(res, 403, { error: 'untrusted request' });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const body = await readJsonBody(req);
|
|
378
|
+
// 只允许两个合法值,其余一律归为 stable(含 undefined/乱传)
|
|
379
|
+
const channel = body?.channel === 'alpha' ? 'alpha' : 'stable';
|
|
380
|
+
try {
|
|
381
|
+
mkdirSync(dshStateDir, { recursive: true });
|
|
382
|
+
writeFileSync(channelFile, JSON.stringify({ channel, updatedAt: new Date().toISOString() }, null, 2));
|
|
383
|
+
} catch (err) {
|
|
384
|
+
sendJson(res, 500, { error: `保存渠道设置失败:${err.message}` });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
host.logger?.info?.(`[dsh-selfupdater] DSH 检测渠道已切换为 ${channel}`);
|
|
388
|
+
sendJson(res, 200, { channel });
|
|
389
|
+
});
|
|
390
|
+
|
|
330
391
|
/* ---------- GET /dsh-selfupdater/plugins ---------- */
|
|
331
392
|
/**
|
|
332
393
|
* 插件更新小节只针对本插件自身(dsh-selfupdater):
|
|
@@ -88,8 +88,20 @@ function registryCandidates() {
|
|
|
88
88
|
return [...new Set(list.filter((v) => typeof v === 'string' && v !== ''))];
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
91
|
+
/**
|
|
92
|
+
* 追踪的发布渠道 tag 列表。
|
|
93
|
+
* 默认 latest + next,与 build.sh 的取版策略保持一致(构建 FPK 时优先
|
|
94
|
+
* dist-tags.next):官方把 alpha 当预发布通道,可能与旧插件 client bundle
|
|
95
|
+
* 不兼容(0.1.2-alpha.3 实测升级后 web 端报 boot manifest batches must be
|
|
96
|
+
* an array 进不去),因此默认不追;仅当环境变量 DSHSU_DSH_CHANNEL 含
|
|
97
|
+
* alpha(逗号分隔)时才纳入,显式尝鲜才开启。
|
|
98
|
+
*/
|
|
99
|
+
function distTags() {
|
|
100
|
+
const tags = ['latest', 'next'];
|
|
101
|
+
const channels = (process.env.DSHSU_DSH_CHANNEL ?? '').split(',').map((s) => s.trim());
|
|
102
|
+
if (channels.includes('alpha')) tags.push('alpha');
|
|
103
|
+
return tags;
|
|
104
|
+
}
|
|
93
105
|
|
|
94
106
|
/**
|
|
95
107
|
* 从单个 registry 的版本级端点(/<pkg>/<tag>)取文档(含 dist.tarball)。
|
|
@@ -119,12 +131,13 @@ async function fetchVersionDoc(base, pkg, tag = 'latest') {
|
|
|
119
131
|
*
|
|
120
132
|
* @returns {{version: string, tarball: string|null}} 全部失败时抛聚合错误。
|
|
121
133
|
*/
|
|
122
|
-
async function fetchLatestRelease(pkg) {
|
|
134
|
+
async function fetchLatestRelease(pkg, opts = {}) {
|
|
123
135
|
const candidates = registryCandidates();
|
|
124
|
-
// 每个 registry × 每个渠道 tag
|
|
125
|
-
// 只查 /latest 会漏掉挂在 next/alpha tag 上的新版(0.1.2-alpha.2
|
|
126
|
-
//
|
|
127
|
-
|
|
136
|
+
// 每个 registry × 每个渠道 tag 并行查询:
|
|
137
|
+
// 只查 /latest 会漏掉挂在 next/alpha tag 上的新版(0.1.2-alpha.2 实测踩坑);
|
|
138
|
+
// 渠道范围默认由 distTags() 决定(latest+next,alpha 需环境变量显式开启),
|
|
139
|
+
// 也允许调用方显式传入 tags(DSH 检测路由按 UI 开关传入)。
|
|
140
|
+
const tags = opts.tags ?? distTags();
|
|
128
141
|
const targets = candidates.flatMap((base) => tags.map((tag) => ({ base, tag })));
|
|
129
142
|
const results = await Promise.allSettled(targets.map((t) => fetchVersionDoc(t.base, pkg, t.tag)));
|
|
130
143
|
let best = null;
|
|
@@ -140,9 +153,9 @@ async function fetchLatestRelease(pkg) {
|
|
|
140
153
|
return best;
|
|
141
154
|
}
|
|
142
155
|
|
|
143
|
-
/**
|
|
144
|
-
export async function fetchLatestVersion(pkg) {
|
|
145
|
-
return (await fetchLatestRelease(pkg)).version;
|
|
156
|
+
/** 只取最新版本号(检查更新路由使用);opts.tags 可覆盖默认渠道。 */
|
|
157
|
+
export async function fetchLatestVersion(pkg, opts = {}) {
|
|
158
|
+
return (await fetchLatestRelease(pkg, opts)).version;
|
|
146
159
|
}
|
|
147
160
|
|
|
148
161
|
/* ------------------------------------------------------------------ *
|
package/lib/updater.mjs
CHANGED
|
@@ -241,8 +241,19 @@ function registryCandidates() {
|
|
|
241
241
|
return [...new Set(list.filter((v) => typeof v === 'string' && v !== ''))];
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
/**
|
|
245
|
-
|
|
244
|
+
/**
|
|
245
|
+
* 追踪的发布渠道 tag 列表(与 plugin-update-task.mjs 的 distTags 一致)。
|
|
246
|
+
* 默认 latest + next,与 build.sh 取版策略保持一致;alpha 为官方预发布
|
|
247
|
+
* 通道,可能与旧插件 client bundle 不兼容(0.1.2-alpha.3 实测升级后 web
|
|
248
|
+
* 端 boot manifest 报错进不去),默认不追,仅当 DSHSU_DSH_CHANNEL 含
|
|
249
|
+
* alpha 时显式纳入。
|
|
250
|
+
*/
|
|
251
|
+
function distTags() {
|
|
252
|
+
const tags = ['latest', 'next'];
|
|
253
|
+
const channels = (process.env.DSHSU_DSH_CHANNEL ?? '').split(',').map((s) => s.trim());
|
|
254
|
+
if (channels.includes('alpha')) tags.push('alpha');
|
|
255
|
+
return tags;
|
|
256
|
+
}
|
|
246
257
|
|
|
247
258
|
/**
|
|
248
259
|
* 从单个 registry 的版本级端点(/<pkg>/<tag>)取文档(含 dist.tarball)。
|
|
@@ -270,10 +281,10 @@ async function fetchVersionDoc(base, pkg, tag = 'latest') {
|
|
|
270
281
|
*/
|
|
271
282
|
async function fetchLatestRelease(pkg) {
|
|
272
283
|
const candidates = registryCandidates();
|
|
273
|
-
// 每个 registry × 每个渠道 tag
|
|
274
|
-
// 只查 /latest 会漏掉挂在 next/alpha tag 上的新版(0.1.2-alpha.2
|
|
275
|
-
//
|
|
276
|
-
const tags =
|
|
284
|
+
// 每个 registry × 每个渠道 tag 并行查询:
|
|
285
|
+
// 只查 /latest 会漏掉挂在 next/alpha tag 上的新版(0.1.2-alpha.2 实测踩坑);
|
|
286
|
+
// 渠道范围由 distTags() 决定(默认 latest+next,alpha 需环境变量显式开启)。
|
|
287
|
+
const tags = distTags();
|
|
277
288
|
const targets = candidates.flatMap((base) => tags.map((tag) => ({ base, tag })));
|
|
278
289
|
const results = await Promise.allSettled(targets.map((t) => fetchVersionDoc(t.base, pkg, t.tag)));
|
|
279
290
|
let best = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-selfupdater",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.22",
|
|
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",
|