dsh-selfupdater 0.3.0
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 +786 -0
- package/cordis.patch.yml +5 -0
- package/lib/index.js +494 -0
- package/lib/plugin-updater.mjs +424 -0
- package/lib/updater.mjs +391 -0
- package/package.json +40 -0
package/lib/updater.mjs
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dsh-selfupdater 分离升级脚本(独立进程执行,不依赖 DSH 存活)。
|
|
4
|
+
*
|
|
5
|
+
* 为什么必须分离进程:要替换的正是 DSH 脚下运行的 node_modules 目录,
|
|
6
|
+
* 目录被占用期间无法安全换入新版;所以插件本体只负责"触发后退出",
|
|
7
|
+
* 由本脚本完成:下载 → 原子替换 → 重启 → 健康检查 → 失败回滚。
|
|
8
|
+
*
|
|
9
|
+
* 用法:
|
|
10
|
+
* node updater.mjs --pid <DSH主进程PID> --app-dir <APP目录> \
|
|
11
|
+
* --workspace <工作区目录> --port <服务端口> [--pkg @deepseek-ai/dsh]
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
import net from 'node:net';
|
|
15
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { dirname, join, resolve } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
/** 插件自身安装位置(…/node_modules/dsh-selfupdater),用于定位状态文件所在目录。 */
|
|
20
|
+
const PLUGIN_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
|
|
22
|
+
/* ------------------------------------------------------------------ *
|
|
23
|
+
* 工具函数
|
|
24
|
+
* ------------------------------------------------------------------ */
|
|
25
|
+
|
|
26
|
+
/** 解析 --key value 形式的命令行参数。 */
|
|
27
|
+
function parseArgs(argv) {
|
|
28
|
+
const out = {};
|
|
29
|
+
for (let i = 2; i < argv.length; i++) {
|
|
30
|
+
if (!argv[i].startsWith('--')) continue;
|
|
31
|
+
const next = argv[i + 1];
|
|
32
|
+
out[argv[i].slice(2)] = next !== undefined && !next.startsWith('--') ? next : true;
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* semver 比较(零依赖实现,逻辑与 dshmarket updates.ts 一致)。
|
|
41
|
+
* 返回负数/0/正数;任一侧不是合法 semver 时返回 null。
|
|
42
|
+
* 规则:正式版 > 同版本号的任何预发布;预发布段数字按数值比较(rc.10 > rc.9)。
|
|
43
|
+
*/
|
|
44
|
+
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
45
|
+
|
|
46
|
+
function parseSemver(v) {
|
|
47
|
+
const m = SEMVER_RE.exec(String(v ?? '').trim());
|
|
48
|
+
if (m === null) return null;
|
|
49
|
+
return {
|
|
50
|
+
core: [Number(m[1]), Number(m[2]), Number(m[3])],
|
|
51
|
+
pre: m[4] === undefined ? [] : m[4].split('.'),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function compareVersions(a, b) {
|
|
56
|
+
const pa = parseSemver(a);
|
|
57
|
+
const pb = parseSemver(b);
|
|
58
|
+
if (pa === null || pb === null) return null;
|
|
59
|
+
for (let i = 0; i < 3; i++) {
|
|
60
|
+
if (pa.core[i] !== pb.core[i]) return pa.core[i] - pb.core[i];
|
|
61
|
+
}
|
|
62
|
+
if (pa.pre.length === 0 || pb.pre.length === 0) return pb.pre.length - pa.pre.length;
|
|
63
|
+
for (let i = 0; i < Math.max(pa.pre.length, pb.pre.length); i++) {
|
|
64
|
+
const x = pa.pre[i];
|
|
65
|
+
const y = pb.pre[i];
|
|
66
|
+
if (x === undefined) return -1;
|
|
67
|
+
if (y === undefined) return 1;
|
|
68
|
+
if (x === y) continue;
|
|
69
|
+
const nx = /^\d+$/.test(x);
|
|
70
|
+
const ny = /^\d+$/.test(y);
|
|
71
|
+
if (nx && ny) return Number(x) - Number(y);
|
|
72
|
+
if (nx !== ny) return nx ? -1 : 1;
|
|
73
|
+
return x < y ? -1 : 1;
|
|
74
|
+
}
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 仅当 latest 严格高于 installed 才算可升级,避免把已固定的新版降级回去。 */
|
|
79
|
+
function isNewer(latest, installed) {
|
|
80
|
+
const cmp = compareVersions(latest, installed);
|
|
81
|
+
return cmp !== null && cmp > 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 探测端口是否已被监听(占用=true)。 */
|
|
85
|
+
function probePort(port, host = '127.0.0.1') {
|
|
86
|
+
return new Promise((resolveProbe) => {
|
|
87
|
+
const socket = net.connect({ host, port });
|
|
88
|
+
socket.on('connect', () => { socket.destroy(); resolveProbe(true); });
|
|
89
|
+
socket.on('error', () => resolveProbe(false));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 等待端口释放(进程退出后 TIME_WAIT 期间仍可能短暂占用)。 */
|
|
94
|
+
async function waitPortFree(port, timeoutMs) {
|
|
95
|
+
const deadline = Date.now() + timeoutMs;
|
|
96
|
+
while (Date.now() < deadline) {
|
|
97
|
+
if (!(await probePort(port))) return true;
|
|
98
|
+
await sleep(500);
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 进程是否仍存活(kill(pid,0) 只做权限探测不真正发信号)。 */
|
|
104
|
+
function isAlive(pid) {
|
|
105
|
+
try {
|
|
106
|
+
process.kill(pid, 0);
|
|
107
|
+
return true;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 结束旧 DSH 主进程:SIGTERM 给优雅退出机会,超时后 SIGKILL 兜底。 */
|
|
114
|
+
async function killProcess(pid) {
|
|
115
|
+
if (!isAlive(pid)) return;
|
|
116
|
+
process.kill(pid, 'SIGTERM');
|
|
117
|
+
const deadline = Date.now() + 8000;
|
|
118
|
+
while (isAlive(pid) && Date.now() < deadline) await sleep(300);
|
|
119
|
+
if (isAlive(pid)) {
|
|
120
|
+
try { process.kill(pid, 'SIGKILL'); } catch { /* 已退出则忽略 */ }
|
|
121
|
+
await sleep(1000);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ------------------------------------------------------------------ *
|
|
126
|
+
* 全局上下文与状态落盘
|
|
127
|
+
* ------------------------------------------------------------------ */
|
|
128
|
+
|
|
129
|
+
const args = parseArgs(process.argv);
|
|
130
|
+
|
|
131
|
+
/** DSH 应用目录(含 node_modules / bin/runner.js),由插件本体解析后传入。 */
|
|
132
|
+
const appDir = resolve(String(args['app-dir'] ?? ''));
|
|
133
|
+
/** 工作区目录(存放 .dsh 状态目录)。 */
|
|
134
|
+
const workspace = resolve(String(args.workspace ?? process.env.TRIM_VAR ?? appDir));
|
|
135
|
+
/** Web 服务端口,健康检查用。 */
|
|
136
|
+
const port = parseInt(String(args.port ?? process.env.DSH_PORT ?? '3081'), 10);
|
|
137
|
+
/** 要升级的包名。 */
|
|
138
|
+
const PKG = String(args.pkg ?? '@deepseek-ai/dsh');
|
|
139
|
+
|
|
140
|
+
const dshStateDir = join(workspace, '.dsh');
|
|
141
|
+
const statusFile = join(dshStateDir, 'selfupdate-status.json');
|
|
142
|
+
const logFile = join(dshStateDir, 'selfupdate.log');
|
|
143
|
+
const lockFile = join(dshStateDir, 'selfupdate.lock');
|
|
144
|
+
const stagingDir = join(dshStateDir, 'selfupdate-staging');
|
|
145
|
+
const nmDir = join(appDir, 'node_modules');
|
|
146
|
+
const bakDir = join(appDir, 'node_modules.selfupdate-bak');
|
|
147
|
+
const failedDir = join(appDir, 'node_modules.selfupdate-failed');
|
|
148
|
+
|
|
149
|
+
let status = {};
|
|
150
|
+
|
|
151
|
+
/** 向日志文件追加一行(失败静默,日志不能阻断升级流程)。 */
|
|
152
|
+
function log(message) {
|
|
153
|
+
const line = `[${new Date().toISOString()}] ${message}`;
|
|
154
|
+
try { appendFileSync(logFile, line + '\n'); } catch { /* 日志写失败不影响主流程 */ }
|
|
155
|
+
console.log(line);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 把最新状态合并写入状态文件,供 UI 卡片轮询展示进度。 */
|
|
159
|
+
function setState(state, message, extra = {}) {
|
|
160
|
+
status = {
|
|
161
|
+
...status,
|
|
162
|
+
state,
|
|
163
|
+
message: message ?? status.message,
|
|
164
|
+
updatedAt: new Date().toISOString(),
|
|
165
|
+
...extra,
|
|
166
|
+
};
|
|
167
|
+
try { writeFileSync(statusFile, JSON.stringify({ ...status, pid: process.pid }, null, 2)); } catch { /* 同上 */ }
|
|
168
|
+
log(`state=${state}${message ? ` :: ${message}` : ''}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 终态收尾:释放锁文件并退出。 */
|
|
172
|
+
function finish(exitCode = 0) {
|
|
173
|
+
try { rmSync(lockFile, { force: true }); } catch { /* 忽略 */ }
|
|
174
|
+
process.exit(exitCode);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 读取当前安装的主程序版本。 */
|
|
178
|
+
function currentDshVersion() {
|
|
179
|
+
try {
|
|
180
|
+
return JSON.parse(readFileSync(join(nmDir, PKG, 'package.json'), 'utf8')).version ?? 'unknown';
|
|
181
|
+
} catch {
|
|
182
|
+
return 'unknown';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* ------------------------------------------------------------------ *
|
|
187
|
+
* 外部命令执行
|
|
188
|
+
* ------------------------------------------------------------------ */
|
|
189
|
+
|
|
190
|
+
/** 以 Promise 方式运行外部命令并收集输出。 */
|
|
191
|
+
function runCommand(file, cmdArgs, opts = {}) {
|
|
192
|
+
return new Promise((resolveRun) => {
|
|
193
|
+
const child = spawn(file, cmdArgs, { stdio: ['ignore', 'pipe', 'pipe'], ...opts });
|
|
194
|
+
let stdout = '';
|
|
195
|
+
let stderr = '';
|
|
196
|
+
child.stdout?.on('data', (d) => { stdout += d; });
|
|
197
|
+
child.stderr?.on('data', (d) => { stderr += d; });
|
|
198
|
+
child.on('error', (err) => resolveRun({ code: -1, stdout: '', stderr: String(err) }));
|
|
199
|
+
child.on('close', (code) => resolveRun({ code, stdout, stderr }));
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** 定位随应用打包的 pnpm(构建时固定安装于 APP_DIR,见 build.sh 步骤 3)。 */
|
|
204
|
+
function pnpmCommand() {
|
|
205
|
+
// 首选直接用自带 node 运行 pnpm.cjs,避免依赖 PATH 与 shebang。
|
|
206
|
+
const cjs = join(appDir, 'lib', 'node_modules', 'pnpm', 'bin', 'pnpm.cjs');
|
|
207
|
+
if (existsSync(cjs)) return { file: process.execPath, args: [cjs] };
|
|
208
|
+
const bin = join(appDir, 'bin', 'pnpm');
|
|
209
|
+
if (existsSync(bin)) return { file: bin, args: [] };
|
|
210
|
+
throw new Error('找不到随应用打包的 pnpm,无法在 staging 安装新版');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* ------------------------------------------------------------------ *
|
|
214
|
+
* 升级各阶段
|
|
215
|
+
* ------------------------------------------------------------------ */
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 从 npm registry 查询目标包的最新版本号。
|
|
219
|
+
* 直接读完整 packument 的 dist-tags.latest,网络异常抛错由上层处理。
|
|
220
|
+
*/
|
|
221
|
+
async function fetchLatestVersion(pkg) {
|
|
222
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}`, {
|
|
223
|
+
headers: { accept: 'application/json', 'user-agent': 'dsh-selfupdater' },
|
|
224
|
+
signal: AbortSignal.timeout(15000),
|
|
225
|
+
});
|
|
226
|
+
if (!res.ok) throw new Error(`registry 查询失败: HTTP ${res.status}`);
|
|
227
|
+
const doc = await res.json();
|
|
228
|
+
const latest = doc?.['dist-tags']?.latest;
|
|
229
|
+
if (typeof latest !== 'string' || latest === '') throw new Error('registry 未返回 latest 版本号');
|
|
230
|
+
return latest;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 阶段一:下载。把新版安装进独立 staging 目录。
|
|
235
|
+
* 关键点:pnpm 默认用符号链接布局,搬走 node_modules 后链接会全部失效;
|
|
236
|
+
* 通过 node-linker=hoisted 强制实体文件布局,保证 staging/node_modules
|
|
237
|
+
* 可以整体 rename 到 APP_DIR。
|
|
238
|
+
*/
|
|
239
|
+
async function downloadIntoStaging(target) {
|
|
240
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
241
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
242
|
+
writeFileSync(
|
|
243
|
+
join(stagingDir, 'package.json'),
|
|
244
|
+
JSON.stringify({ name: 'dsh-selfupdate-staging', private: true, dependencies: { [PKG]: `^${target}` } }, null, 2),
|
|
245
|
+
);
|
|
246
|
+
// 实体文件布局 + 关闭交互确认,保证无人值守可执行。
|
|
247
|
+
writeFileSync(join(stagingDir, '.npmrc'), 'node-linker=hoisted\n');
|
|
248
|
+
const pnpm = pnpmCommand();
|
|
249
|
+
setState('downloading', `正在下载 ${PKG}@${target} …`);
|
|
250
|
+
const result = await runCommand(pnpm.file, [...pnpm.args, 'install', '--omit=dev', '--no-audit', '--no-fund'], {
|
|
251
|
+
cwd: stagingDir,
|
|
252
|
+
env: { ...process.env, CI: 'true', npm_config_node_linker: 'hoisted' },
|
|
253
|
+
});
|
|
254
|
+
if (result.code !== 0) {
|
|
255
|
+
throw new Error(`staging 安装失败(pnpm exit ${result.code}): ${result.stderr.slice(-400)}`);
|
|
256
|
+
}
|
|
257
|
+
// 校验装到的确实是目标版本,防止镜像滞后悄悄装了旧版。
|
|
258
|
+
const staged = JSON.parse(readFileSync(join(stagingDir, 'node_modules', PKG, 'package.json'), 'utf8')).version;
|
|
259
|
+
if (staged !== target) throw new Error(`staging 版本不符:期望 ${target},实际 ${staged}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 阶段二:原子替换 node_modules。
|
|
264
|
+
* 同分区 rename 是原子操作;任一步失败立即反向回滚,不留中间态。
|
|
265
|
+
*/
|
|
266
|
+
function swapNodeModules() {
|
|
267
|
+
setState('swapping', '正在替换 node_modules …');
|
|
268
|
+
// 清掉上一次升级遗留的备份(保留到本次成功为止的策略:见 plan 第 8 节)。
|
|
269
|
+
rmSync(bakDir, { recursive: true, force: true });
|
|
270
|
+
renameSync(nmDir, bakDir); // 旧目录让位(此步几乎不会失败)
|
|
271
|
+
try {
|
|
272
|
+
renameSync(join(stagingDir, 'node_modules'), nmDir);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
// 新目录就位失败 → 还原旧目录,保持系统可用。
|
|
275
|
+
try { renameSync(bakDir, nmDir); } catch { /* 还原也失败只能报错 */ }
|
|
276
|
+
throw new Error(`新 node_modules 就位失败: ${err.message}(已回滚)`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 按 runner.js 的方式重新拉起完整启动链(runner 会再拉起 DSH)。
|
|
282
|
+
* 关键点:必须继承当前进程的全部环境变量 —— 飞牛的 TRIM_DATA_SHARE_PATHS
|
|
283
|
+
* 等变量决定工作区落点,丢失会导致重启后配置"失踪"。仅显式固定
|
|
284
|
+
* TRIM_APPDEST 指向本次换装后的应用目录。
|
|
285
|
+
*/
|
|
286
|
+
function launchRunnerChain() {
|
|
287
|
+
const runnerJs = join(appDir, 'bin', 'runner.js');
|
|
288
|
+
if (!existsSync(runnerJs)) throw new Error(`找不到启动器 ${runnerJs},无法重启`);
|
|
289
|
+
const child = spawn(process.execPath, [runnerJs], {
|
|
290
|
+
cwd: workspace,
|
|
291
|
+
detached: true,
|
|
292
|
+
stdio: 'ignore',
|
|
293
|
+
env: { ...process.env, TRIM_APPDEST: appDir },
|
|
294
|
+
});
|
|
295
|
+
child.unref();
|
|
296
|
+
log(`已拉起 runner 链 (pid=${child.pid})`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 健康检查:轮询本地端口直到有任意 HTTP 响应(服务能应答即算活着,
|
|
301
|
+
* 不苛求 200 —— 登录页/404 都证明 Web 服务已起来)。
|
|
302
|
+
*/
|
|
303
|
+
async function waitHealthy(timeoutMs) {
|
|
304
|
+
const deadline = Date.now() + timeoutMs;
|
|
305
|
+
while (Date.now() < deadline) {
|
|
306
|
+
try {
|
|
307
|
+
await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(3000) });
|
|
308
|
+
return true;
|
|
309
|
+
} catch { /* 未就绪继续等 */ }
|
|
310
|
+
await sleep(2000);
|
|
311
|
+
}
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** 失败回滚:把备份目录还原为 node_modules 并再次拉起服务。 */
|
|
316
|
+
async function rollback() {
|
|
317
|
+
setState('rollback', '健康检查失败,正在回滚到旧版本 …');
|
|
318
|
+
try {
|
|
319
|
+
rmSync(failedDir, { recursive: true, force: true });
|
|
320
|
+
renameSync(nmDir, failedDir); // 新目录先挪走(留作现场排查)
|
|
321
|
+
renameSync(bakDir, nmDir); // 还原旧目录
|
|
322
|
+
} catch (err) {
|
|
323
|
+
setState('error', `回滚失败:${err.message}。请参考 manifest 内置版本手动救砖。`);
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
launchRunnerChain();
|
|
327
|
+
const ok = await waitHealthy(60000);
|
|
328
|
+
setState(ok ? 'done_failed' : 'error',
|
|
329
|
+
ok ? `已回滚并恢复服务(${currentDshVersion()})。新版文件保留在 ${failedDir}` : '回滚后服务仍未恢复,请查看容器日志',
|
|
330
|
+
{ latestVersion: status.latestVersion });
|
|
331
|
+
return ok;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* ------------------------------------------------------------------ *
|
|
335
|
+
* 主流程
|
|
336
|
+
* ------------------------------------------------------------------ */
|
|
337
|
+
|
|
338
|
+
async function main() {
|
|
339
|
+
mkdirSync(dshStateDir, { recursive: true });
|
|
340
|
+
const current = currentDshVersion();
|
|
341
|
+
|
|
342
|
+
// 锁文件双端校验:插件本体触发前会检查;这里再补一道防手动重复执行。
|
|
343
|
+
if (existsSync(lockFile)) throw new Error('已有一次升级在进行中(锁文件存在)');
|
|
344
|
+
writeFileSync(lockFile, JSON.stringify({ pid: process.pid, startedAt: Date.now() }));
|
|
345
|
+
|
|
346
|
+
status = { currentVersion: current, startedAt: new Date().toISOString(), trigger: 'manual' };
|
|
347
|
+
setState('downloading', '正在查询 npm 最新版本 …');
|
|
348
|
+
|
|
349
|
+
const target = await fetchLatestVersion(PKG);
|
|
350
|
+
status.latestVersion = target;
|
|
351
|
+
if (!isNewer(target, current)) {
|
|
352
|
+
setState('idle', `当前已是最新版本(${current})`);
|
|
353
|
+
finish(0);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
log(`发现新版本:${current} -> ${target}`);
|
|
357
|
+
await downloadIntoStaging(target);
|
|
358
|
+
swapNodeModules();
|
|
359
|
+
|
|
360
|
+
setState('restarting', '正在重启 DeepSeek Harness …');
|
|
361
|
+
const pid = parseInt(String(args.pid ?? ''), 10);
|
|
362
|
+
if (Number.isFinite(pid) && pid > 0) await killProcess(pid);
|
|
363
|
+
await waitPortFree(port, 20000);
|
|
364
|
+
launchRunnerChain();
|
|
365
|
+
|
|
366
|
+
setState('healthcheck', '等待服务就绪 …');
|
|
367
|
+
if (await waitHealthy(90000)) {
|
|
368
|
+
// 成功:清理 staging;bak 目录保留作为手动救砖的最后手段。
|
|
369
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
370
|
+
setState('done', `升级完成:${current} -> ${target}`, { finishedAt: new Date().toISOString() });
|
|
371
|
+
} else {
|
|
372
|
+
await rollback();
|
|
373
|
+
}
|
|
374
|
+
finish(0);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 总看门狗:任何阶段卡死都强制终态,避免 UI 永远显示"升级中"。
|
|
378
|
+
setTimeout(() => {
|
|
379
|
+
setState('error', '升级超时(8 分钟看门狗触发),请查看 selfupdate.log');
|
|
380
|
+
finish(2);
|
|
381
|
+
}, 8 * 60 * 1000);
|
|
382
|
+
|
|
383
|
+
main().catch(async (err) => {
|
|
384
|
+
log(`升级失败: ${err.stack ?? err}`);
|
|
385
|
+
setState('error', String(err.message ?? err));
|
|
386
|
+
// 若已换目录但尚未回滚,尽力恢复一次。
|
|
387
|
+
if (existsSync(bakDir) && !existsSync(nmDir)) {
|
|
388
|
+
try { renameSync(bakDir, nmDir); } catch { /* 忽略 */ }
|
|
389
|
+
}
|
|
390
|
+
finish(1);
|
|
391
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-selfupdater",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Self-update plugin for DeepSeek Harness: check npm for newer @deepseek-ai/dsh, swap node_modules atomically, restart, health-check and roll back on failure; also supports one-click online update of installed DSH plugins. DSH 主程序与已装插件的一站式在线更新插件。",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"deepseek",
|
|
10
|
+
"harness",
|
|
11
|
+
"dsh",
|
|
12
|
+
"dsh-plugin",
|
|
13
|
+
"self-update",
|
|
14
|
+
"plugin-update",
|
|
15
|
+
"updater"
|
|
16
|
+
],
|
|
17
|
+
"files": [
|
|
18
|
+
"lib",
|
|
19
|
+
"client",
|
|
20
|
+
"cordis.patch.yml"
|
|
21
|
+
],
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./lib/index.js",
|
|
24
|
+
"./client": "./client/settings-card.js",
|
|
25
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"dsh": {
|
|
29
|
+
"bundle": {
|
|
30
|
+
"patch": "./cordis.patch.yml"
|
|
31
|
+
},
|
|
32
|
+
"client": {
|
|
33
|
+
"inject": [
|
|
34
|
+
"@deepseek-ai/dsh-client-connection",
|
|
35
|
+
"@deepseek-ai/dsh-client-runtime"
|
|
36
|
+
],
|
|
37
|
+
"platform": "web"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|