@mrrisega/dsh-remote 0.4.3 → 0.4.4
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/README.md
CHANGED
|
@@ -79,6 +79,12 @@ npx @mrrisega/dsh-remote setup --server wss://<你的域名>:端口 --key <访
|
|
|
79
79
|
其他命令:`settings`(设置页)、`status`(查看状态)、`run`(前台调试)、
|
|
80
80
|
`plugin`(重装/卸载 dsh web 插件)。运行 `npx @mrrisega/dsh-remote --help` 查看完整说明。
|
|
81
81
|
|
|
82
|
+
> **版本与更新 / 卸载**:dsh 官方插件市场目前不提供更新按钮,也不会改写用户补丁(因此市场
|
|
83
|
+
> 卸载会提示「仍通过 insert 引用 dsh-remote-ui」而拒绝)。插件设置面板里已内置管理入口
|
|
84
|
+
> (dsh web → 设置 → 「远程控制」→「🔄 版本与更新」卡片):显示当前版本、自动检测 npm 新版、
|
|
85
|
+
> **一键在线更新**(后台补运行环境并重启 bridge,完成后重启 dsh web 生效)、以及**彻底卸载**
|
|
86
|
+
> (移除补丁 include / 依赖 / bundle 与本地文件,之后市场卸载或直接重启均可完成卸载)。
|
|
87
|
+
|
|
82
88
|
源码安装(开发 / 自建服务器):`git clone https://github.com/mrRisega/dsh-remote.git`
|
|
83
89
|
并 `npm install`,见下文各组件说明。
|
|
84
90
|
|
package/dsh-setup.mjs
CHANGED
|
@@ -41,6 +41,63 @@ const DEFAULT_API = "https://n.risegao.cn:13443/relay-api";
|
|
|
41
41
|
const DEFAULT_APP_URL = "https://n.risegao.cn:13443/app/";
|
|
42
42
|
const REPO_URL = "https://github.com/mrRisega/dsh-remote";
|
|
43
43
|
|
|
44
|
+
// ---------- 运行时自物化(npm/npx 安装 → 固化到配置目录,脱离 npx 缓存) ----------
|
|
45
|
+
// npx 每次安装的缓存目录(~/.npm/_npx/<hash>)不固定:缓存一旦清理,指向它的自启动服务
|
|
46
|
+
// 就会像“找不到模块”一样崩溃。因此 npm 形态安装时把 dsh-setup.mjs + clients + 依赖(ws)
|
|
47
|
+
// 固化到 CONFIG_DIR(~/.dsh-remote),自启动服务只指向这个稳定路径。
|
|
48
|
+
// 插件(dsh-remote-ui)的“运行环境已就绪”判断同样以 CONFIG_DIR/dsh-setup.mjs 为准。
|
|
49
|
+
|
|
50
|
+
function pkgVersion() {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(fs.readFileSync(path.join(THIS_DIR, "package.json"), "utf8")).version || "";
|
|
53
|
+
} catch { return ""; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 向上查找依赖树里的 ws(npx 布局通常提升到缓存根 node_modules,npm 布局则内嵌)。 */
|
|
57
|
+
function findDepWs(startDir) {
|
|
58
|
+
let d = startDir;
|
|
59
|
+
while (true) {
|
|
60
|
+
const cand = path.join(d, "node_modules", "ws");
|
|
61
|
+
if (fs.existsSync(cand)) return cand;
|
|
62
|
+
const parent = path.dirname(d);
|
|
63
|
+
if (parent === d) break;
|
|
64
|
+
d = parent;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ensureRuntimeCopy() {
|
|
70
|
+
if (!IS_NPM_INSTALL) return; // 仓库开发形态:原地使用
|
|
71
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
72
|
+
const ver = pkgVersion();
|
|
73
|
+
const setupTarget = path.join(CONFIG_DIR, "dsh-setup.mjs");
|
|
74
|
+
if (setupTarget === path.join(THIS_DIR, "dsh-setup.mjs")) return; // 已在配置目录内执行
|
|
75
|
+
const verFile = path.join(CONFIG_DIR, ".dsh-setup-version");
|
|
76
|
+
let cur = "";
|
|
77
|
+
try { cur = fs.readFileSync(verFile, "utf8").trim(); } catch { /* 首次 */ }
|
|
78
|
+
if (fs.existsSync(setupTarget) && cur === ver) return; // 同版本幂等跳过
|
|
79
|
+
fs.cpSync(path.join(THIS_DIR, "dsh-setup.mjs"), setupTarget);
|
|
80
|
+
fs.cpSync(path.join(THIS_DIR, "clients"), path.join(CONFIG_DIR, "clients"), { recursive: true, force: true });
|
|
81
|
+
const wsSrc = findDepWs(THIS_DIR);
|
|
82
|
+
if (wsSrc) {
|
|
83
|
+
fs.mkdirSync(path.join(CONFIG_DIR, "node_modules"), { recursive: true });
|
|
84
|
+
fs.cpSync(wsSrc, path.join(CONFIG_DIR, "node_modules", "ws"), { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
fs.writeFileSync(verFile, ver);
|
|
87
|
+
console.log(`✅ 运行时已固化到 ${CONFIG_DIR}(自启动指向稳定路径,不再依赖 npx 缓存)`);
|
|
88
|
+
}
|
|
89
|
+
try { ensureRuntimeCopy(); } catch (e) { console.warn(`⚠️ 运行时固化跳过: ${e.message}`); }
|
|
90
|
+
|
|
91
|
+
/** 自启动服务应指向的 dsh-setup.mjs:优先配置目录内的固化副本,否则当前执行文件。 */
|
|
92
|
+
function runtimeSetupPath() {
|
|
93
|
+
const local = path.join(CONFIG_DIR, "dsh-setup.mjs");
|
|
94
|
+
try {
|
|
95
|
+
fs.accessSync(local, fs.constants.R_OK);
|
|
96
|
+
return local;
|
|
97
|
+
} catch { /* 未固化(如仓库开发)→ 用当前文件 */ }
|
|
98
|
+
return fileURLToPath(import.meta.url);
|
|
99
|
+
}
|
|
100
|
+
|
|
44
101
|
// ---------- 工具 ----------
|
|
45
102
|
|
|
46
103
|
function sh(cmd, timeoutMs = 15000, cwd = undefined) {
|
|
@@ -137,7 +194,7 @@ function autostartFilePath() {
|
|
|
137
194
|
}
|
|
138
195
|
|
|
139
196
|
function writeAutostartFile() {
|
|
140
|
-
const runCmd = `"${NODE_BIN}" "${
|
|
197
|
+
const runCmd = `"${NODE_BIN}" "${runtimeSetupPath()}" run`;
|
|
141
198
|
if (process.platform === "darwin") {
|
|
142
199
|
const plistPath = autostartFilePath();
|
|
143
200
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -145,7 +202,7 @@ function writeAutostartFile() {
|
|
|
145
202
|
<plist version="1.0"><dict>
|
|
146
203
|
<key>Label</key><string>com.dshremote.bridge</string>
|
|
147
204
|
<key>ProgramArguments</key>
|
|
148
|
-
<array><string>${NODE_BIN}</string><string>${
|
|
205
|
+
<array><string>${NODE_BIN}</string><string>${runtimeSetupPath()}</string><string>run</string></array>
|
|
149
206
|
<key>RunAtLoad</key><true/>
|
|
150
207
|
<key>KeepAlive</key><true/>
|
|
151
208
|
<key>StandardOutPath</key><string>${path.join(CONFIG_DIR, ".dsh-bridge.log")}</string>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "手机远程控制 DeepSeek Harness · Remote control DeepSeek Harness (dsh web) from any phone browser — 100% 全功能 App 级体验:发消息、看工具执行、审批权限、改设置、管凭据,含特权操作,免内网穿透。一条命令安装 npx @mrrisega/dsh-remote。Mobile remote control for DSH, self-host or SaaS, no server needed on LAN.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -64,6 +64,89 @@ function preferredNode() {
|
|
|
64
64
|
|
|
65
65
|
const NODE_BIN = preferredNode();
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* 解析 npx 绝对路径。DeepSeek App 拉起 dsh web 时 PATH 只有 /usr/bin:/bin:/usr/sbin:/sbin
|
|
69
|
+
* (没有 /opt/homebrew/bin 等),裸 `npx` 会 spawn ENOENT 而静默失败——必须按绝对路径找,
|
|
70
|
+
* 且子进程 env 的 PATH 要把当前 node 所在目录补在最前(npx 的 #!/usr/bin/env node 依赖它)。
|
|
71
|
+
*/
|
|
72
|
+
function npxCommand() {
|
|
73
|
+
const name = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
74
|
+
const dirs = [
|
|
75
|
+
dirname(process.execPath), // 与当前 node 同目录(homebrew/usr/local 均可覆盖)
|
|
76
|
+
process.env.DSH_SETUP_NPX_DIR || "",
|
|
77
|
+
"/opt/homebrew/bin",
|
|
78
|
+
"/usr/local/bin",
|
|
79
|
+
"/opt/homebrew/opt/node@20/bin",
|
|
80
|
+
"/usr/local/opt/node@20/bin",
|
|
81
|
+
"/usr/bin",
|
|
82
|
+
].filter(Boolean);
|
|
83
|
+
for (const d of dirs) {
|
|
84
|
+
const real = resolveExecutable(join(d, name));
|
|
85
|
+
if (real) return real;
|
|
86
|
+
}
|
|
87
|
+
return name; // 全找不到 → 退回裸名(普通 shell 场景仍可用)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 子进程环境:把 node 目录补进 PATH(npx 及其 shebang 需要),可附加额外变量。 */
|
|
91
|
+
function spawnEnv(extra) {
|
|
92
|
+
const nodeDir = dirname(process.execPath);
|
|
93
|
+
const base = process.env.PATH || "";
|
|
94
|
+
const PATH = [nodeDir, base, "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"].filter(Boolean).join(":");
|
|
95
|
+
return { ...process.env, PATH, ...(extra || {}) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------- 后台子进程标记(防重入 + 宿主重启自愈) ----------
|
|
99
|
+
// marker 内容 = JSON {pid, at}:pid 供“宿主重启后立即清理死进程残留”判断;
|
|
100
|
+
// 兼容旧格式(纯时间戳数字 → 只按超时清理)。
|
|
101
|
+
|
|
102
|
+
function readMarkerInfo(filePath) {
|
|
103
|
+
try {
|
|
104
|
+
const raw = readFileSync(filePath, "utf8").trim();
|
|
105
|
+
const j = JSON.parse(raw);
|
|
106
|
+
if (Number.isInteger(j?.pid) || Number.isInteger(j?.at)) return j;
|
|
107
|
+
} catch { /* 非 JSON → 数字时间戳或空 */ }
|
|
108
|
+
const t = Number(raw || "0");
|
|
109
|
+
return Number.isFinite(t) && t > 0 ? { pid: null, at: t } : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function writeMarker(filePath, pid) {
|
|
113
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
114
|
+
writeFileSync(filePath, JSON.stringify({ pid: pid ?? null, at: Date.now() }), { mode: 0o600 });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** pid 是否存活(ESRCH=已死)。 */
|
|
118
|
+
function pidAlive(pid) {
|
|
119
|
+
if (!Number.isInteger(pid) || pid <= 0) return null; // 未知 → 由超时规则兜底
|
|
120
|
+
try {
|
|
121
|
+
process.kill(pid, 0);
|
|
122
|
+
return true;
|
|
123
|
+
} catch (e) {
|
|
124
|
+
return e.code === "EPERM" ? true : false; // EPERM=存在但无权限
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 清理残留标记:宿主 dsh web 在后台安装/更新期间被重启/强杀时,子进程清理回调随之丢失,
|
|
130
|
+
* 若只按“30 分钟超时”清理,用户会在这半小时内反复遇到“已有更新进行中/正在安装”。
|
|
131
|
+
* 现在:记录 pid → 重启后立刻清掉已死进程的标记;pid 不可读的旧标记仍按超时兜底。
|
|
132
|
+
*/
|
|
133
|
+
function sweepStaleMarkers(relayDir) {
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
for (const name of [PROVISION_MARKER, UPDATE_MARKER]) {
|
|
136
|
+
const p = join(relayDir, name);
|
|
137
|
+
let info;
|
|
138
|
+
try { info = readMarkerInfo(p); } catch { continue; }
|
|
139
|
+
if (!info) continue;
|
|
140
|
+
const dead = pidAlive(info.pid);
|
|
141
|
+
const expired = now - info.at > STALE_MARKER_MS;
|
|
142
|
+
if (dead === false || (dead === null && expired)) {
|
|
143
|
+
try { rmSync(p, { force: true }); } catch { /* ignore */ }
|
|
144
|
+
appendLogLine(relayDir, AUTO_INSTALL_LOG,
|
|
145
|
+
`[dsh-remote-ui] 清理残留标记 ${name}(pid=${info.pid ?? "?"}, at=${new Date(info.at).toISOString()}${dead === false ? ", 进程已死" : ", 已超时"})`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
67
150
|
/** 读取 JSON body。 */
|
|
68
151
|
async function readJsonBody(req) {
|
|
69
152
|
let raw = "";
|
|
@@ -274,23 +357,8 @@ function appendLogLine(relayDir, name, line) {
|
|
|
274
357
|
}
|
|
275
358
|
|
|
276
359
|
/**
|
|
277
|
-
*
|
|
278
|
-
* 子进程的清理回调会随宿主进程一起丢失,marker 会永久卡住后续安装/更新。
|
|
279
|
-
* 插件每次启动时把超时的 marker 清掉(内容=创建时间戳)。
|
|
360
|
+
* 清理“进程残留”标记的实现见上方小工具区 sweepStaleMarkers(pid 存活 + 超时双保险)。
|
|
280
361
|
*/
|
|
281
|
-
function sweepStaleMarkers(relayDir) {
|
|
282
|
-
const now = Date.now();
|
|
283
|
-
for (const name of [PROVISION_MARKER, UPDATE_MARKER]) {
|
|
284
|
-
const p = join(relayDir, name);
|
|
285
|
-
try {
|
|
286
|
-
const t = Number((readFileSync(p, "utf8") || "0").trim());
|
|
287
|
-
if (Number.isFinite(t) && t > 0 && now - t > STALE_MARKER_MS) {
|
|
288
|
-
rmSync(p, { force: true });
|
|
289
|
-
appendLogLine(relayDir, AUTO_INSTALL_LOG, `[dsh-remote-ui] 清理残留标记 ${name}(${new Date(t).toISOString()} 创建,已超时)`);
|
|
290
|
-
}
|
|
291
|
-
} catch { /* 无文件等 → 忽略 */ }
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
362
|
|
|
295
363
|
/** 插件市场只装了 UI 插件;若桌面缺 dsh-remote 运行环境(dsh-setup.mjs=bridge/自启动),
|
|
296
364
|
* 由插件在后台自动执行一次 `npx @mrrisega/dsh-remote` 补齐,用户无需手动跑命令。
|
|
@@ -301,18 +369,23 @@ function ensureRuntime(relayDir) {
|
|
|
301
369
|
if (existsSync(marker)) return false; // 正在安装中
|
|
302
370
|
try {
|
|
303
371
|
mkdirSync(relayDir, { recursive: true });
|
|
304
|
-
writeFileSync(marker, String(Date.now()), { mode: 0o600 });
|
|
305
372
|
const log = join(relayDir, AUTO_INSTALL_LOG);
|
|
306
|
-
const
|
|
307
|
-
const child = spawn(npx, ["--yes", "@mrrisega/dsh-remote"], {
|
|
373
|
+
const child = spawn(npxCommand(), ["--yes", "@mrrisega/dsh-remote"], {
|
|
308
374
|
detached: true,
|
|
375
|
+
env: spawnEnv(),
|
|
309
376
|
stdio: ["ignore", openSync(log, "a"), openSync(log, "a")]
|
|
310
377
|
});
|
|
378
|
+
writeMarker(marker, child.pid); // 记 pid:宿主重启后可立即清理死进程残留
|
|
379
|
+
const clear = () => { try { rmSync(marker, { force: true }); } catch { /* ignore */ } };
|
|
311
380
|
child.on("exit", (code) => {
|
|
312
|
-
|
|
313
|
-
try { rmSync(marker, { force: true }); } catch { /* ignore */ }
|
|
381
|
+
clear();
|
|
314
382
|
appendLogLine(relayDir, AUTO_INSTALL_LOG, `[auto-install] npx 退出 code=${code ?? "?"}`);
|
|
315
383
|
});
|
|
384
|
+
child.on("error", (e) => {
|
|
385
|
+
clear();
|
|
386
|
+
appendLogLine(relayDir, AUTO_INSTALL_LOG, `[auto-install] 启动失败: ${e.message}`);
|
|
387
|
+
console.warn(`[dsh-remote-ui] 自动安装子进程启动失败: ${e.message}`);
|
|
388
|
+
});
|
|
316
389
|
child.unref();
|
|
317
390
|
console.log(`[dsh-remote-ui] 检测到缺少桌面运行环境,已在后台自动安装(日志: ${log}),完成后将自动启动 bridge`);
|
|
318
391
|
return false;
|
|
@@ -373,14 +446,15 @@ function scheduleRuntime(relayDir) {
|
|
|
373
446
|
const cfg = loadConfig(relayDir);
|
|
374
447
|
const hasAcct = Boolean((cfg.phone || cfg.email) && cfg.password) || Boolean(cfg.local_key);
|
|
375
448
|
if (!hasAcct) return;
|
|
449
|
+
// 先看服务是否已在运行(runtime 可能位于 npx 缓存/固化目录,不必重复安装)
|
|
450
|
+
const st = launchdStatus();
|
|
451
|
+
if (st.running) { done = true; clearInterval(iv); return; }
|
|
376
452
|
const setupUrl = join(relayDir, "dsh-setup.mjs");
|
|
377
453
|
if (!existsSync(setupUrl)) {
|
|
378
|
-
ensureRuntime(relayDir);
|
|
454
|
+
ensureRuntime(relayDir); // 什么环境都没有 → 后台 npx 安装一次
|
|
379
455
|
return;
|
|
380
456
|
}
|
|
381
|
-
|
|
382
|
-
if (st.running) { done = true; clearInterval(iv); return; }
|
|
383
|
-
startBridge(relayDir);
|
|
457
|
+
startBridge(relayDir); // 环境在但服务没起 → 拉起
|
|
384
458
|
} catch { /* 下一轮再试 */ }
|
|
385
459
|
}, 12_000);
|
|
386
460
|
iv.unref?.();
|
|
@@ -637,7 +711,7 @@ async function proxyFeedback(relayDir, req, res, pathname) {
|
|
|
637
711
|
// ---------- 自管理:版本 / 在线更新 / 彻底卸载(面板内“版本与更新”卡片) ----------
|
|
638
712
|
|
|
639
713
|
/** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
|
|
640
|
-
const PLUGIN_VERSION = "0.4.
|
|
714
|
+
const PLUGIN_VERSION = "0.4.4";
|
|
641
715
|
const UPDATE_LOG = ".dsh-update.log";
|
|
642
716
|
const UPDATE_MARKER = ".dsh-update-running";
|
|
643
717
|
|
|
@@ -657,11 +731,10 @@ async function npmLatestVersion() {
|
|
|
657
731
|
/** 以 detached 子进程执行 `npx --yes @mrrisega/dsh-remote@latest`(env 可覆盖 npm 源)。 */
|
|
658
732
|
function spawnUpdater(relayDir, extraEnv) {
|
|
659
733
|
const log = join(relayDir, UPDATE_LOG);
|
|
660
|
-
|
|
661
|
-
return spawn(npx, ["--yes", "@mrrisega/dsh-remote@latest"], {
|
|
734
|
+
return spawn(npxCommand(), ["--yes", "@mrrisega/dsh-remote@latest"], {
|
|
662
735
|
detached: true,
|
|
663
736
|
cwd: homedir(),
|
|
664
|
-
env:
|
|
737
|
+
env: spawnEnv(extraEnv), // PATH 补 node 目录:App 最小 PATH 下也能跑 npx
|
|
665
738
|
stdio: ["ignore", openSync(log, "a"), openSync(log, "a")]
|
|
666
739
|
});
|
|
667
740
|
}
|
|
@@ -669,20 +742,22 @@ function spawnUpdater(relayDir, extraEnv) {
|
|
|
669
742
|
/**
|
|
670
743
|
* 后台执行在线一键更新:npx @mrrisega/dsh-remote@latest(幂等自愈:补运行环境/更新 bridge/重写 include)。
|
|
671
744
|
* 稳健性:
|
|
745
|
+
* - npx 用绝对路径 + PATH 补全解析(App 拉起的 dsh web PATH 最小化时不再 ENOENT 静默失败);
|
|
672
746
|
* - 默认 npx 源(国内常为 npmmirror)未同步到最新版导致失败时,自动用官方 npm 源重试一次;
|
|
673
|
-
* - marker
|
|
747
|
+
* - marker 记录 pid,子进程退出/出错即清理;宿主重启后由 sweepStaleMarkers 立即清掉死进程残留。
|
|
674
748
|
*/
|
|
675
749
|
function runOnlineUpdate(relayDir) {
|
|
676
750
|
try {
|
|
677
751
|
mkdirSync(relayDir, { recursive: true });
|
|
678
752
|
const marker = join(relayDir, UPDATE_MARKER);
|
|
679
753
|
if (existsSync(marker)) return { ok: false, detail: "已有更新在进行中,请稍候" };
|
|
680
|
-
writeFileSync(marker, String(Date.now()), { mode: 0o600 });
|
|
681
754
|
appendLogLine(relayDir, UPDATE_LOG, `[update] 开始在线更新 @mrrisega/dsh-remote@latest (${new Date().toISOString()})`);
|
|
682
755
|
|
|
683
756
|
let retried = false;
|
|
757
|
+
const clear = () => { try { rmSync(marker, { force: true }); } catch { /* ignore */ } };
|
|
684
758
|
const run = () => {
|
|
685
759
|
const child = spawnUpdater(relayDir, retried ? { npm_config_registry: "https://registry.npmjs.org" } : {});
|
|
760
|
+
writeMarker(marker, child.pid);
|
|
686
761
|
child.on("exit", (code) => {
|
|
687
762
|
if (!retried && code !== 0) {
|
|
688
763
|
retried = true;
|
|
@@ -691,7 +766,11 @@ function runOnlineUpdate(relayDir) {
|
|
|
691
766
|
return;
|
|
692
767
|
}
|
|
693
768
|
appendLogLine(relayDir, UPDATE_LOG, `[update] npx 退出 code=${code ?? "?"}(默认源${retried ? "/官方源" : ""})`);
|
|
694
|
-
|
|
769
|
+
clear();
|
|
770
|
+
});
|
|
771
|
+
child.on("error", (e) => {
|
|
772
|
+
appendLogLine(relayDir, UPDATE_LOG, `[update] 子进程启动失败: ${e.message}`);
|
|
773
|
+
clear();
|
|
695
774
|
});
|
|
696
775
|
child.unref();
|
|
697
776
|
return child;
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// 3) 版本可见 + 新版本检测 + 稳健更新(含残留 marker 清理兜底逻辑在 apply 时执行)。
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
7
|
import http from "node:http";
|
|
8
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
9
|
import { mkdtemp, mkdir, rm, writeFile, readFile } from "node:fs/promises";
|
|
9
|
-
import { readFileSync } from "node:fs";
|
|
10
10
|
import os from "node:os";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import test from "node:test";
|
|
@@ -66,24 +66,24 @@ test("self 路由:版本可见 + 运行环境状态(无 npx 环境时不谎
|
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
test("update-check 路由:从 npm 检测新版本(dist-tags.latest
|
|
69
|
+
test("update-check 路由:从 npm 检测新版本(dist-tags.latest 9.9.9 > 当前)", async () => {
|
|
70
70
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-upchk-"));
|
|
71
71
|
const origFetch = globalThis.fetch;
|
|
72
72
|
try {
|
|
73
73
|
const routes = boot(null, tempDir);
|
|
74
74
|
const { host, base } = await serve(routes);
|
|
75
|
-
// 假 npm 源:registry 请求回
|
|
75
|
+
// 假 npm 源:registry 请求回 9.9.9,其余请求(本测试自身的 HTTP 调用)走真实 fetch
|
|
76
76
|
globalThis.fetch = async (url, init) => {
|
|
77
77
|
if (String(url).startsWith("https://registry.")) {
|
|
78
|
-
return { ok: true, status: 200, json: async () => ({ "dist-tags": { latest: "
|
|
78
|
+
return { ok: true, status: 200, json: async () => ({ "dist-tags": { latest: "9.9.9" } }) };
|
|
79
79
|
}
|
|
80
80
|
return origFetch(url, init);
|
|
81
81
|
};
|
|
82
82
|
try {
|
|
83
83
|
const r = await (await fetch(`${base}/dsh-remote/self/update-check`)).json();
|
|
84
84
|
assert.equal(r.ok, true);
|
|
85
|
-
assert.equal(r.latest, "
|
|
86
|
-
assert.equal(r.outdated, true, "
|
|
85
|
+
assert.equal(r.latest, "9.9.9");
|
|
86
|
+
assert.equal(r.outdated, true, "9.9.9 > 当前版本 → outdated 应为 true");
|
|
87
87
|
assert.notEqual(r.current, r.latest);
|
|
88
88
|
} finally {
|
|
89
89
|
host.close();
|
|
@@ -139,6 +139,22 @@ test("update 路由:已有更新进行中时拒绝重复触发(防重入)"
|
|
|
139
139
|
}
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
test("残留 marker 自愈:记录进程已死的 marker 在插件启动时立即清理(不等 30 分钟超时)", () => {
|
|
143
|
+
const tempDir = mkdtempSync(path.join(os.tmpdir(), "dsh-ui-sweep-"));
|
|
144
|
+
try {
|
|
145
|
+
// 模拟“宿主在更新途中被重启”:更新子进程已死,但清理回调随旧宿主丢失
|
|
146
|
+
const deadPid = 2147483647; // 不可能存在的 pid
|
|
147
|
+
writeFileSync(path.join(tempDir, ".dsh-setup-installing"), JSON.stringify({ pid: deadPid, at: Date.now() }));
|
|
148
|
+
writeFileSync(path.join(tempDir, ".dsh-update-running"), JSON.stringify({ pid: deadPid, at: Date.now() }));
|
|
149
|
+
writeFileSync(path.join(tempDir, ".dsh-config.json"), "{}");
|
|
150
|
+
boot(null, tempDir); // apply() → sweepStaleMarkers
|
|
151
|
+
assert.equal(existsSync(path.join(tempDir, ".dsh-setup-installing")), false, "死进程的安装 marker 应被立即清理");
|
|
152
|
+
assert.equal(existsSync(path.join(tempDir, ".dsh-update-running")), false, "死进程的更新 marker 应被立即清理");
|
|
153
|
+
} finally {
|
|
154
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
142
158
|
test("uninstall 路由:移除 include 块 + package.json 依赖/bundle + 本地目录(解锁市场卸载)", async () => {
|
|
143
159
|
const tempHome = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-home-"));
|
|
144
160
|
const relayDir = path.join(tempHome, "relay");
|