dsh-remote-web 0.6.1 → 0.6.2
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/lib/client.js +91 -1
- package/lib/index.js +371 -23
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -91,6 +91,13 @@ window.__ModuleLoader__.load({
|
|
|
91
91
|
".dru-msg-err{color:#cf222e}",
|
|
92
92
|
".dru-msg-warn{color:#9a6700}",
|
|
93
93
|
".dru-hint{font-size:12px;color:#57606a;line-height:1.6}",
|
|
94
|
+
// ── 重启 DeepSeek harness(首次安装/更新后置顶提醒 + 底部常驻按钮) ──
|
|
95
|
+
".dru-restart-alert{display:flex;gap:10px;align-items:flex-start;background:#fff8c5;border:1px solid #d4a72c;border-radius:10px;padding:12px 14px;margin-bottom:12px}",
|
|
96
|
+
".dru-restart-alert-icon{font-size:18px;line-height:1.2}",
|
|
97
|
+
".dru-restart-alert-title{font-size:13px;font-weight:700;color:#7d4e00}",
|
|
98
|
+
".dru-restart-alert-sub{font-size:12px;color:#6b5900;margin-top:4px;line-height:1.6}",
|
|
99
|
+
".dru-restart-foot{margin-top:14px;padding-top:12px;border-top:1px solid #eaeef2;display:flex;gap:10px;align-items:center;flex-wrap:wrap}",
|
|
100
|
+
".dru-restart-foot .dru-hint{flex:1;min-width:180px;margin:0}",
|
|
94
101
|
// ── 用户反馈模块 ──
|
|
95
102
|
".dru-fb-tabs{display:flex;gap:8px;margin-bottom:10px}",
|
|
96
103
|
".dru-fb-tab{flex:1;padding:6px 0;text-align:center;border-radius:8px;cursor:pointer;font-size:12.5px;font-weight:600;color:#57606a;background:#eaeef2;border:1px solid #d0d7de;user-select:none}",
|
|
@@ -1202,6 +1209,19 @@ window.__ModuleLoader__.load({
|
|
|
1202
1209
|
|
|
1203
1210
|
var loggedIn = !!(st && st.config && (st.config.phone || st.config.hasLocalKey));
|
|
1204
1211
|
var serviceRunning = !!(st && st.service && st.service.running);
|
|
1212
|
+
// 0.6.2:运行环境是否就绪 + launchd 是否处于崩溃循环(入口脚本缺失导致的 KeepAlive 重拉)。
|
|
1213
|
+
// 旧版把「作业在 launchd 里但进程秒退」显示成「运行中」,用户完全看不出问题在哪。
|
|
1214
|
+
var serviceRuntimeReady = !(st && st.service && st.service.runtimeReady === false);
|
|
1215
|
+
var serviceLaunchd = (st && st.service && st.service.launchd) || {};
|
|
1216
|
+
var serviceCrashing = !!serviceLaunchd.crashing;
|
|
1217
|
+
var serviceStateText = !st ? "查询中…"
|
|
1218
|
+
: serviceRunning ? "运行中"
|
|
1219
|
+
: serviceCrashing ? "启动失败(已自动转入修复)"
|
|
1220
|
+
: !serviceRuntimeReady ? "运行环境安装中…"
|
|
1221
|
+
: "已停止";
|
|
1222
|
+
// 是否需要重启 DeepSeek harness(首次安装/在线更新后置顶提醒 + 底部常驻按钮)
|
|
1223
|
+
var restartInfo = (st && st.restart) || {};
|
|
1224
|
+
var restartPending = !!restartInfo.pending;
|
|
1205
1225
|
// 账号标识(掩码手机号):登录成功/换账号时变化 → 触发上方的重取 effect(比布尔 loggedIn 更敏感)
|
|
1206
1226
|
var phoneKey = (st && st.config && st.config.phone) || "";
|
|
1207
1227
|
var launchdPid = st && st.service && st.service.launchd && st.service.launchd.pid;
|
|
@@ -1718,6 +1738,39 @@ window.__ModuleLoader__.load({
|
|
|
1718
1738
|
.finally(function () { setBusy(""); });
|
|
1719
1739
|
};
|
|
1720
1740
|
|
|
1741
|
+
/**
|
|
1742
|
+
* 重启 DeepSeek harness:插件本体与浏览器半在进程启动时装载,首次安装/在线更新后
|
|
1743
|
+
* 必须重启才生效。重启会短暂断开本页面 —— 这里轮询等它回来,然后自动刷新。
|
|
1744
|
+
*/
|
|
1745
|
+
var restartHarness = function () {
|
|
1746
|
+
setBusy("restart");
|
|
1747
|
+
post("/dsh-remote/harness/restart")
|
|
1748
|
+
.then(function (body) {
|
|
1749
|
+
setSt(body);
|
|
1750
|
+
setMsg("ok", "🔄 正在重启 DeepSeek harness" + (body && body.mode ? "(" + body.mode + ")" : "") +
|
|
1751
|
+
"…约 2~10 秒,页面会自动刷新,无需手动操作。");
|
|
1752
|
+
waitHarnessBack(0, false);
|
|
1753
|
+
})
|
|
1754
|
+
.catch(function (e) {
|
|
1755
|
+
setMsg("err", "重启失败: " + e.message + " —— 请手动重启 DeepSeek harness(退出 dsh web 后重新启动)。");
|
|
1756
|
+
setBusy("");
|
|
1757
|
+
});
|
|
1758
|
+
};
|
|
1759
|
+
|
|
1760
|
+
/** 轮询等待 harness 回来;观察到「断过又恢复」或等待足够久后自动刷新页面。 */
|
|
1761
|
+
var waitHarnessBack = function (tries, sawDown) {
|
|
1762
|
+
if (tries > 40) { setMsg("err", "重启后仍未就绪,请手动刷新页面或重启 DeepSeek harness。"); setBusy(""); return; }
|
|
1763
|
+
setTimeout(function () {
|
|
1764
|
+
fetch("/dsh-remote/status", { cache: "no-store" })
|
|
1765
|
+
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
|
|
1766
|
+
.then(function () {
|
|
1767
|
+
if (sawDown || tries >= 8) { location.reload(); return; }
|
|
1768
|
+
waitHarnessBack(tries + 1, sawDown);
|
|
1769
|
+
})
|
|
1770
|
+
.catch(function () { waitHarnessBack(tries + 1, true); });
|
|
1771
|
+
}, 1500);
|
|
1772
|
+
};
|
|
1773
|
+
|
|
1721
1774
|
var saveLocal = function () {
|
|
1722
1775
|
if (!selfHost.trim() || !localKey.trim()) { setMsg("err", "请填写服务器地址与访问密钥"); return; }
|
|
1723
1776
|
setBusy("local");
|
|
@@ -2139,7 +2192,7 @@ window.__ModuleLoader__.load({
|
|
|
2139
2192
|
card("🖥 Bridge 服务",
|
|
2140
2193
|
h("div", { className: "dru-status-line" },
|
|
2141
2194
|
h("span", { className: "dru-dot " + (serviceRunning ? "dru-dot-on" : "dru-dot-off") }),
|
|
2142
|
-
h("span", null, st ?
|
|
2195
|
+
h("span", null, st ? serviceStateText : "查询中…"),
|
|
2143
2196
|
launchdPid ? h("span", { className: "dru-meta", style: { marginTop: 0 } }, "(pid=" + launchdPid + ")") : null
|
|
2144
2197
|
),
|
|
2145
2198
|
h("div", { className: "dru-actions", style: { marginTop: 10 } },
|
|
@@ -2149,6 +2202,14 @@ window.__ModuleLoader__.load({
|
|
|
2149
2202
|
),
|
|
2150
2203
|
h("div", { className: "dru-meta" }, st && st.config && st.config.deviceId ? "设备 ID:" + st.config.deviceId : "设备 ID:生成中"),
|
|
2151
2204
|
h("div", { className: "dru-meta" }, st ? (st.service && st.service.plistExists ? "自启动服务已安装" : "自启动服务未安装(启动时自动创建)") : ""),
|
|
2205
|
+
// 0.6.2:把「运行环境缺失 / launchd 崩溃循环」如实告诉用户,而不是显示「运行中」
|
|
2206
|
+
st && !serviceRuntimeReady
|
|
2207
|
+
? h("div", { className: "dru-hint", style: { marginTop: 8 } },
|
|
2208
|
+
"🛠 插件市场只装「面板插件」,桌面运行环境(bridge)需要额外补装——已自动在后台安装,装完会自动启动 bridge,不用做任何操作。若超过几分钟仍未变成「运行中」,点上方版本卡的「一键更新」手动补全。")
|
|
2209
|
+
: serviceCrashing
|
|
2210
|
+
? h("div", { className: "dru-hint", style: { marginTop: 8 } },
|
|
2211
|
+
"⚠️ 上一次启动失败(自启动入口失效),已自动清理失效自启动项并重新补装运行环境。稍候会自动恢复;仍失败请点「一键更新」。")
|
|
2212
|
+
: null,
|
|
2152
2213
|
st && st.service && st.service.bindError
|
|
2153
2214
|
? h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 8 } },
|
|
2154
2215
|
"⚠️ 设备注册失败:" + (st.service.bindError.message || "未说明原因"),
|
|
@@ -2214,8 +2275,37 @@ window.__ModuleLoader__.load({
|
|
|
2214
2275
|
)
|
|
2215
2276
|
),
|
|
2216
2277
|
h("div", { className: "dru-settings-body" },
|
|
2278
|
+
restartPending
|
|
2279
|
+
? h("div", { className: "dru-restart-alert", role: "status" },
|
|
2280
|
+
h("span", { className: "dru-restart-alert-icon" }, "🔔"),
|
|
2281
|
+
h("div", { style: { flex: "1" } },
|
|
2282
|
+
h("div", { className: "dru-restart-alert-title" },
|
|
2283
|
+
restartInfo.kind === "update" ? "在线更新已完成,需要重启 DeepSeek harness" : "首次安装需要重启 DeepSeek harness"),
|
|
2284
|
+
h("div", { className: "dru-restart-alert-sub" },
|
|
2285
|
+
"插件本体与「远程访问」面板是在 DeepSeek harness 启动时装载的,重启之后即可正常使用(账号、二维码、bridge 全部生效)。"),
|
|
2286
|
+
h("div", { className: "dru-actions", style: { marginTop: 10 } },
|
|
2287
|
+
h("button", {
|
|
2288
|
+
type: "button",
|
|
2289
|
+
className: "dru-btn dru-btn-primary",
|
|
2290
|
+
disabled: busy !== "",
|
|
2291
|
+
onClick: function () { restartHarness(); }
|
|
2292
|
+
}, busy === "restart" ? "重启中…" : "重启 DeepSeek harness")
|
|
2293
|
+
)
|
|
2294
|
+
)
|
|
2295
|
+
)
|
|
2296
|
+
: null,
|
|
2217
2297
|
view === "feedback" ? renderFeedback() : view === "invite" ? renderInvite() : renderHome()
|
|
2218
2298
|
),
|
|
2299
|
+
// 常驻入口最底部:「重启 DeepSeek harness」按钮始终可达(首次安装/更新/排查都用它)
|
|
2300
|
+
h("div", { className: "dru-restart-foot" },
|
|
2301
|
+
h("div", { className: "dru-hint" }, "重启 DeepSeek harness:插件与面板在 harness 启动时装载,首次安装或在线更新后重启即可生效;仅重启 dsh web,不影响 bridge 与手机端连接。"),
|
|
2302
|
+
h("button", {
|
|
2303
|
+
type: "button",
|
|
2304
|
+
className: "dru-btn " + (restartPending ? "dru-btn-primary" : "dru-btn-ghost"),
|
|
2305
|
+
disabled: busy !== "",
|
|
2306
|
+
onClick: function () { restartHarness(); }
|
|
2307
|
+
}, busy === "restart" ? "重启中…" : "🔄 重启 DeepSeek harness")
|
|
2308
|
+
),
|
|
2219
2309
|
renderCommunityModal()
|
|
2220
2310
|
);
|
|
2221
2311
|
}
|
package/lib/index.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// - 0.1.2+ ?token 浏览器鉴权会话代持(0.4.1 起)
|
|
15
15
|
//
|
|
16
16
|
// 不依赖任何第三方包:只使用 node 内置模块与 cordis 注入的 webServer 服务。
|
|
17
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync, accessSync, chmodSync, openSync, closeSync, rmSync, constants as fsConstants } from "node:fs";
|
|
17
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync, accessSync, chmodSync, openSync, closeSync, rmSync, statSync, constants as fsConstants } from "node:fs";
|
|
18
18
|
import { join, dirname, sep } from "node:path";
|
|
19
19
|
import { execSync, spawn } from "node:child_process";
|
|
20
20
|
import { homedir, platform } from "node:os";
|
|
@@ -76,8 +76,8 @@ const NODE_BIN = preferredNode();
|
|
|
76
76
|
function npxCommand() {
|
|
77
77
|
const name = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
78
78
|
const dirs = [
|
|
79
|
+
process.env.DSH_SETUP_NPX_DIR || "", // 显式覆盖优先(测试注入假 npx;生产通常不设)
|
|
79
80
|
dirname(process.execPath), // 与当前 node 同目录(homebrew/usr/local 均可覆盖)
|
|
80
|
-
process.env.DSH_SETUP_NPX_DIR || "",
|
|
81
81
|
"/opt/homebrew/bin",
|
|
82
82
|
"/usr/local/bin",
|
|
83
83
|
"/opt/homebrew/opt/node@20/bin",
|
|
@@ -290,6 +290,25 @@ function scheduleHarnessMint(ctx, relayDir) {
|
|
|
290
290
|
};
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
// ---------- 桌面运行环境就绪判定 / 系统级操作开关 ----------
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 桌面运行环境是否就绪:固化运行时 `<relayDir>/dsh-setup.mjs` 存在即视为就绪。
|
|
297
|
+
* 这是「bridge 能不能跑」的唯一权威判据,也是 launchd 自启动指向的入口脚本
|
|
298
|
+
* (见 writeAutostartFile)——插件市场只装「插件半」时该文件不存在,必须先补装。
|
|
299
|
+
*/
|
|
300
|
+
function runtimeReady(relayDir) {
|
|
301
|
+
return existsSync(join(relayDir, "dsh-setup.mjs"));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 系统级操作总开关(测试隔离):置位后不做 launchctl / npx 安装 / 杀进程等一切真实系统操作。
|
|
306
|
+
* 生产环境绝不设置;测试与 CI 必须设置,否则用例会去碰本机真实的 bridge 自启动服务。
|
|
307
|
+
*/
|
|
308
|
+
function skipsSystemOps() {
|
|
309
|
+
return process.env.DSH_RELAY_SKIP_SERVICE === "1";
|
|
310
|
+
}
|
|
311
|
+
|
|
293
312
|
// ---------- bridge 服务状态 / 启停(launchctl,macOS) ----------
|
|
294
313
|
|
|
295
314
|
function launchAgentPath() {
|
|
@@ -301,20 +320,46 @@ function launchTarget() {
|
|
|
301
320
|
return `gui/${process.getuid()}/com.dshremote.bridge`;
|
|
302
321
|
}
|
|
303
322
|
|
|
304
|
-
/**
|
|
323
|
+
/**
|
|
324
|
+
* 检查 launchd 服务状态。
|
|
325
|
+
*
|
|
326
|
+
* 【0.6.2 关键修复】`launchctl print` 只要成功,它的 `state` 就是权威,**不得**再回退
|
|
327
|
+
* `launchctl list`。原因:plist 指向不存在的入口脚本时,KeepAlive 会让作业陷入
|
|
328
|
+
* 「秒退→立刻重拉」的崩溃循环,而在重拉的瞬间 `launchctl list` 的 PID 列会闪现一个
|
|
329
|
+
* **已经死掉**的 pid(本机实测 `launchctl list` = `40213 1 com.dshremote.bridge`,
|
|
330
|
+
* 同时 `ps -p 40213` 为空、`launchctl print` = `state = spawn scheduled`)。
|
|
331
|
+
* 旧实现据此谎报 running=true,后果有二:
|
|
332
|
+
* 1) 面板显示「运行中」,用户以为 bridge 在跑(实际从未注册成功);
|
|
333
|
+
* 2) scheduleRuntime 判定「已在运行」→ 自愈 watcher 永久停摆,再也不补运行环境。
|
|
334
|
+
* 现在:print 的 state 为准 + 附带 runs/lastExitCode/crashing 供面板与自愈解释原因;
|
|
335
|
+
* 仅在 print 本身不可用时才回退 list,且回退路径要求 pid 真实存活。
|
|
336
|
+
*/
|
|
305
337
|
function launchdStatus() {
|
|
306
338
|
const target = launchTarget();
|
|
307
339
|
const pr = sh(`launchctl print ${target}`);
|
|
308
|
-
if (pr.ok
|
|
309
|
-
const
|
|
310
|
-
|
|
340
|
+
if (pr.ok) {
|
|
341
|
+
const stateMatch = pr.stdout.match(/state\s*=\s*([^\n]+)/);
|
|
342
|
+
const state = stateMatch ? stateMatch[1].trim() : "";
|
|
343
|
+
const pidMatch = pr.stdout.match(/(?:^|\n)\s*pid\s*=\s*(\d+)/);
|
|
344
|
+
const runsMatch = pr.stdout.match(/runs\s*=\s*(\d+)/);
|
|
345
|
+
const exitMatch = pr.stdout.match(/last exit code\s*=\s*(-?\d+)/);
|
|
346
|
+
const runs = runsMatch ? Number(runsMatch[1]) : null;
|
|
347
|
+
const lastExitCode = exitMatch ? Number(exitMatch[1]) : null;
|
|
348
|
+
const running = state === "running";
|
|
349
|
+
// 崩溃循环:作业在 launchd 里可见但没在跑,且上次退出码非 0(KeepAlive 会无限重拉)
|
|
350
|
+
const crashing = !running && runs !== null && runs > 0 && lastExitCode !== null && lastExitCode !== 0;
|
|
351
|
+
return { running, pid: running && pidMatch ? Number(pidMatch[1]) : null, state, runs, lastExitCode, crashing };
|
|
311
352
|
}
|
|
312
|
-
const ls = sh(
|
|
353
|
+
const ls = sh("launchctl list | grep com.dshremote.bridge");
|
|
313
354
|
if (ls.ok) {
|
|
314
355
|
const pidStr = ls.stdout.trim().split(/\s+/)[0];
|
|
315
|
-
if (pidStr && pidStr !== "-" && /^\d+$/.test(pidStr))
|
|
356
|
+
if (pidStr && pidStr !== "-" && /^\d+$/.test(pidStr)) {
|
|
357
|
+
const pid = Number(pidStr);
|
|
358
|
+
// list 的 PID 列在崩溃循环中会出现已死进程 → 必须校验存活才敢报运行
|
|
359
|
+
if (pidAlive(pid) !== false) return { running: true, pid, state: "running", runs: null, lastExitCode: null, crashing: false };
|
|
360
|
+
}
|
|
316
361
|
}
|
|
317
|
-
return { running: false, pid: null };
|
|
362
|
+
return { running: false, pid: null, state: "", runs: null, lastExitCode: null, crashing: false };
|
|
318
363
|
}
|
|
319
364
|
|
|
320
365
|
/** 检查手动运行的 watcher(dsh-setup.mjs run)与 bridge 子进程(排除 launchd 托管链)。 */
|
|
@@ -350,6 +395,14 @@ function manualStatus() {
|
|
|
350
395
|
const PROVISION_MARKER = ".dsh-setup-installing";
|
|
351
396
|
const AUTO_INSTALL_LOG = ".dsh-setup-install.log";
|
|
352
397
|
const STALE_MARKER_MS = 30 * 60 * 1000; // 超过该时长视为上次进程残留,插件启动时清理
|
|
398
|
+
/**
|
|
399
|
+
* 自愈 watcher 轮询间隔(默认 12s)。DSH_RELAY_SELFHEAL_MS 可覆盖——仅供测试把
|
|
400
|
+
* 崩溃循环自愈链路压到亚秒级验证,生产不必设置。调用时读取(而非模块加载时固定),
|
|
401
|
+
* 测试才能在 import 之后再改。
|
|
402
|
+
*/
|
|
403
|
+
function selfhealIntervalMs() {
|
|
404
|
+
return Math.max(200, Number(process.env.DSH_RELAY_SELFHEAL_MS) || 12_000);
|
|
405
|
+
}
|
|
353
406
|
|
|
354
407
|
/** 向日志追加一行(多个子进程写同一日志用 append 模式,互不覆盖)。 */
|
|
355
408
|
function appendLogLine(relayDir, name, line) {
|
|
@@ -374,7 +427,8 @@ function appendLogLine(relayDir, name, line) {
|
|
|
374
427
|
* 已被 0.4.5 移除的“用户 include”重新写回 profile(历史上造成 dsh web 重复 ID 崩溃)。 */
|
|
375
428
|
function ensureRuntime(relayDir) {
|
|
376
429
|
if (UNINSTALLED_DIRS.has(relayDir)) return false; // 已彻底卸载:不再自动安装运行环境
|
|
377
|
-
if (
|
|
430
|
+
if (runtimeReady(relayDir)) return true;
|
|
431
|
+
if (skipsSystemOps()) return false; // 测试隔离:绝不在用例里 spawn 真实 npx 安装
|
|
378
432
|
const marker = join(relayDir, PROVISION_MARKER);
|
|
379
433
|
if (existsSync(marker)) return false; // 正在安装中
|
|
380
434
|
try {
|
|
@@ -419,6 +473,7 @@ function writeAutostartFile(relayDir) {
|
|
|
419
473
|
<array><string>${NODE_BIN}</string><string>${setupUrl}</string><string>run</string></array>
|
|
420
474
|
<key>RunAtLoad</key><true/>
|
|
421
475
|
<key>KeepAlive</key><true/>
|
|
476
|
+
<key>ThrottleInterval</key><integer>10</integer>
|
|
422
477
|
<key>StandardOutPath</key><string>${join(relayDir, ".dsh-bridge.log")}</string>
|
|
423
478
|
<key>StandardErrorPath</key><string>${join(relayDir, ".dsh-bridge.log")}</string>
|
|
424
479
|
<key>EnvironmentVariables</key><dict><key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string></dict>
|
|
@@ -428,7 +483,44 @@ function writeAutostartFile(relayDir) {
|
|
|
428
483
|
return plistPath;
|
|
429
484
|
}
|
|
430
485
|
|
|
431
|
-
/**
|
|
486
|
+
/**
|
|
487
|
+
* 自启动 plist 里的入口脚本是否已不存在(不存在 = launchd 必然陷入崩溃循环)。
|
|
488
|
+
* 只解析 ProgramArguments 里的第一个 .mjs 参数,不引入 plist 解析依赖。
|
|
489
|
+
*/
|
|
490
|
+
function plistEntryMissing() {
|
|
491
|
+
const plistPath = launchAgentPath();
|
|
492
|
+
if (!plistPath || !existsSync(plistPath)) return false;
|
|
493
|
+
let raw = "";
|
|
494
|
+
try { raw = readFileSync(plistPath, "utf8"); } catch { return false; }
|
|
495
|
+
const m = raw.match(/<string>([^<]*\.mjs)<\/string>/);
|
|
496
|
+
if (!m) return false;
|
|
497
|
+
return !existsSync(m[1]);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* 摘掉「指向不存在入口脚本」的自启动项,止住 launchd 崩溃循环。
|
|
502
|
+
*
|
|
503
|
+
* 现场(0.6.1 及更早):只装插件半的用户在面板点「启动 bridge」或首次登录时,
|
|
504
|
+
* startBridge 会无条件生成指向 `<relayDir>/dsh-setup.mjs` 的 plist 并 bootstrap,
|
|
505
|
+
* 而该文件并不存在 → launchd KeepAlive 无限重拉(日志 20+ 次 MODULE_NOT_FOUND、
|
|
506
|
+
* `runs = 23 / last exit code = 1`)。此时仅删 plist 文件没用:作业已加载在 launchd 里,
|
|
507
|
+
* 必须 bootout 才会停。判据严格限定为「入口脚本确实缺失」,绝不碰正常停止的服务。
|
|
508
|
+
* @returns {boolean} 是否执行了清理
|
|
509
|
+
*/
|
|
510
|
+
function reapBrokenAutostart(relayDir) {
|
|
511
|
+
if (skipsSystemOps()) return false;
|
|
512
|
+
if (!plistEntryMissing()) return false;
|
|
513
|
+
const st = launchdStatus();
|
|
514
|
+
if (!st.running && !st.crashing && !st.state) return false; // 作业未被 launchd 加载、无可摘的东西
|
|
515
|
+
sh(`launchctl bootout ${launchTarget()}`);
|
|
516
|
+
try { rmSync(launchAgentPath(), { force: true }); } catch { /* 非关键 */ }
|
|
517
|
+
appendLogLine(relayDir, AUTO_INSTALL_LOG,
|
|
518
|
+
`[dsh-remote-web] 已摘除失效自启动:plist 入口 ${join(relayDir, "dsh-setup.mjs")} 不存在`
|
|
519
|
+
+ `(launchd state=${st.state || "?"}, runs=${st.runs ?? "?"}, lastExit=${st.lastExitCode ?? "?"})→ 转入后台补装运行环境`);
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** 启动 bridge:确保运行环境就绪 + plist 存在 → launchctl bootstrap(回退 load -w)。 */
|
|
432
524
|
function startBridge(relayDir) {
|
|
433
525
|
if (UNINSTALLED_DIRS.has(relayDir)) {
|
|
434
526
|
// 已彻底卸载:面板/自愈在重启前可能仍在内存中,禁止再把自启动与 plist 拉回来
|
|
@@ -436,6 +528,23 @@ function startBridge(relayDir) {
|
|
|
436
528
|
}
|
|
437
529
|
const plistPath = launchAgentPath();
|
|
438
530
|
if (!plistPath) return { ok: false, status: "unsupported", detail: "仅支持 macOS" };
|
|
531
|
+
if (skipsSystemOps()) return { ok: false, status: "skipped", detail: "测试隔离(DSH_RELAY_SKIP_SERVICE=1):跳过真实服务操作" };
|
|
532
|
+
// 【0.6.2 关键修复】运行环境缺失时绝不 bootstrap:写一个指向不存在脚本的 plist
|
|
533
|
+
// 只会让 launchd 进入 KeepAlive 崩溃循环——面板还可能因瞬时 pid 谎报「已启动」。
|
|
534
|
+
// 正确做法:先把运行环境补起来(后台 npx),就绪后由 scheduleRuntime / 面板再次启动。
|
|
535
|
+
if (!runtimeReady(relayDir)) {
|
|
536
|
+
ensureRuntime(relayDir);
|
|
537
|
+
return {
|
|
538
|
+
ok: false,
|
|
539
|
+
status: "provisioning",
|
|
540
|
+
runtimeMissing: true,
|
|
541
|
+
detail: "桌面运行环境(bridge)尚未安装,已在后台自动安装,完成后会自动启动 bridge——请稍候刷新面板(也可点下方「一键更新」查看进度)",
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
// plist 存在但入口不是当前运行环境(relayDir 变更/旧安装)→ 重写,避免又指到失效路径
|
|
545
|
+
if (existsSync(plistPath) && !readFileSync(plistPath, "utf8").includes(join(relayDir, "dsh-setup.mjs"))) {
|
|
546
|
+
writeAutostartFile(relayDir);
|
|
547
|
+
}
|
|
439
548
|
if (!existsSync(plistPath)) writeAutostartFile(relayDir);
|
|
440
549
|
if (!existsSync(plistPath)) return { ok: false, status: "not-installed", detail: "plist 生成失败" };
|
|
441
550
|
const target = launchTarget();
|
|
@@ -451,27 +560,42 @@ function startBridge(relayDir) {
|
|
|
451
560
|
return { ok: st.running, status: st.running ? "running" : "failed", pid: st.pid, detail: st.running ? void 0 : "服务未进入运行态" };
|
|
452
561
|
}
|
|
453
562
|
|
|
454
|
-
/**
|
|
563
|
+
/**
|
|
564
|
+
* 运行时自愈 watcher:补齐桌面运行环境 + 环境就绪后自动拉起 bridge。
|
|
565
|
+
*
|
|
566
|
+
* 【0.6.2 顺序修复】运行环境检查提到最前,且必须先于「账号就绪」与「服务是否在跑」:
|
|
567
|
+
* - 旧版先判 `st.running` 才判运行环境,而崩溃循环中的作业会被误判为 running
|
|
568
|
+
* → watcher `done = true` 永久停摆,自动补装(ensureRuntime)再也不会执行,
|
|
569
|
+
* 这正是「市场装完插件、bridge 却起不来」且永不恢复的直接原因;
|
|
570
|
+
* - 补装运行环境不该等用户先登录:市场安装只给到插件半,登录那一刻就会 startBridge。
|
|
571
|
+
*/
|
|
455
572
|
function scheduleRuntime(relayDir) {
|
|
456
573
|
let done = false;
|
|
574
|
+
// 本进程启动时是否缺运行环境:缺 → 补齐完成后需要重启 DeepSeek harness 才算「安装完成」。
|
|
575
|
+
let awaitingRestartHint = !runtimeReady(relayDir);
|
|
457
576
|
const iv = setInterval(() => {
|
|
458
577
|
if (done) { clearInterval(iv); return; }
|
|
459
578
|
if (UNINSTALLED_DIRS.has(relayDir)) { done = true; clearInterval(iv); return; } // 已彻底卸载:自愈 watcher 停摆
|
|
460
579
|
try {
|
|
580
|
+
if (!runtimeReady(relayDir)) {
|
|
581
|
+
reapBrokenAutostart(relayDir); // 先摘掉指向不存在脚本的自启动,止住崩溃循环
|
|
582
|
+
ensureRuntime(relayDir); // 后台 npx 补齐(marker 防重入);就绪后下一轮自动拉起
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (awaitingRestartHint) {
|
|
586
|
+
awaitingRestartHint = false;
|
|
587
|
+
// 首次安装:运行环境(bridge + 自启动)刚在本进程内补齐 → 提示重启,重启后即完全可用
|
|
588
|
+
markRestartPending(relayDir, "first-install", "首次安装需要重启 DeepSeek harness");
|
|
589
|
+
}
|
|
461
590
|
const cfg = loadConfig(relayDir);
|
|
462
591
|
const hasAcct = Boolean((cfg.phone || cfg.email) && cfg.password) || Boolean(cfg.local_key);
|
|
463
592
|
if (!hasAcct) return;
|
|
464
|
-
//
|
|
593
|
+
// 运行环境已在,看服务是否已在运行(runtime 可能位于 npx 缓存/固化目录,不必重复安装)
|
|
465
594
|
const st = launchdStatus();
|
|
466
595
|
if (st.running) { done = true; clearInterval(iv); return; }
|
|
467
|
-
const setupUrl = join(relayDir, "dsh-setup.mjs");
|
|
468
|
-
if (!existsSync(setupUrl)) {
|
|
469
|
-
ensureRuntime(relayDir); // 什么环境都没有 → 后台 npx 安装一次
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
596
|
startBridge(relayDir); // 环境在但服务没起 → 拉起
|
|
473
597
|
} catch { /* 下一轮再试 */ }
|
|
474
|
-
},
|
|
598
|
+
}, selfhealIntervalMs());
|
|
475
599
|
iv.unref?.();
|
|
476
600
|
return () => clearInterval(iv);
|
|
477
601
|
}
|
|
@@ -533,7 +657,10 @@ function uninstallRuntime(relayDir, protectedPath) {
|
|
|
533
657
|
// 只处理「plist 位于当前 HOME」的服务:本插件/dsh-setup.mjs 安装的服务一定在此
|
|
534
658
|
const plistPath = launchAgentPath();
|
|
535
659
|
if (plistPath && existsSync(plistPath)) {
|
|
536
|
-
|
|
660
|
+
// crashing 也要 bootout:崩溃循环中的作业在 launchd 里仍是「已加载」,
|
|
661
|
+
// 只删 plist 文件它照样被 KeepAlive 反复重拉(见 launchdStatus 注释)。
|
|
662
|
+
const st = launchdStatus();
|
|
663
|
+
if (st.running || st.crashing) {
|
|
537
664
|
const r = stopBridge(); // launchctl bootout → KeepAlive 一并失效
|
|
538
665
|
out.stoppedService = r.ok;
|
|
539
666
|
}
|
|
@@ -590,6 +717,181 @@ function uninstallRuntime(relayDir, protectedPath) {
|
|
|
590
717
|
return out;
|
|
591
718
|
}
|
|
592
719
|
|
|
720
|
+
// ---------- DeepSeek harness 重启(0.6.2:首次安装/更新后必须重启才能加载插件本体) ----------
|
|
721
|
+
//
|
|
722
|
+
// 为什么需要:dsh web 的插件(宿主半 + 浏览器半)都是在**进程启动时**装载的。
|
|
723
|
+
// 市场点击安装只把文件写进 profile,当前进程里既没有 /dsh-remote/* 路由、也没有面板入口,
|
|
724
|
+
// 必须重启 DeepSeek harness 才生效(面板与 bridge 才能真正可用)。
|
|
725
|
+
// 本段提供:① 持久化「待重启」状态(跨进程判定重启是否真的发生过);
|
|
726
|
+
// ② 一条可靠的重启实现(优先交回监管者,其次自拉起);③ 面板按钮用的路由。
|
|
727
|
+
|
|
728
|
+
const RESTART_STATE_FILE = ".dsh-restart-state.json";
|
|
729
|
+
const RESTART_SCRIPT_FILE = ".dsh-restart-harness.sh";
|
|
730
|
+
const RESTART_LOG_FILE = ".dsh-restart.log";
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* 当前 dsh web 进程身份(pid + 启动时刻)。模块加载时算一次并固定:
|
|
734
|
+
* 若每次调用重算,`Date.now() - uptime*1000` 的毫秒漂移会让同一个进程算出不同的 id,
|
|
735
|
+
* 结清逻辑就会误判「已经重启过」而错误撤下提示。
|
|
736
|
+
*/
|
|
737
|
+
const BOOT_ID = `${process.pid}-${Math.round(Date.now() - process.uptime() * 1000)}`;
|
|
738
|
+
|
|
739
|
+
function bootId() {
|
|
740
|
+
return BOOT_ID;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** 读取「待重启」状态;文件缺失/损坏一律视为无需重启。 */
|
|
744
|
+
function readRestartState(relayDir) {
|
|
745
|
+
try {
|
|
746
|
+
const raw = JSON.parse(readFileSync(join(relayDir, RESTART_STATE_FILE), "utf8"));
|
|
747
|
+
if (raw && typeof raw === "object") return raw;
|
|
748
|
+
} catch { /* 无文件 / 损坏 */ }
|
|
749
|
+
return { pending: false };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function writeRestartState(relayDir, state) {
|
|
753
|
+
try {
|
|
754
|
+
mkdirSync(relayDir, { recursive: true });
|
|
755
|
+
writeFileSync(join(relayDir, RESTART_STATE_FILE), JSON.stringify({ ...state, updatedAt: Date.now() }, null, 2), { mode: 0o600 });
|
|
756
|
+
} catch { /* 写盘失败不致命:面板退化为不提示 */ }
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** 标记「需要重启 DeepSeek harness」。kind: first-install(首次安装) | update(在线更新)。 */
|
|
760
|
+
function markRestartPending(relayDir, kind, reason) {
|
|
761
|
+
const cur = readRestartState(relayDir);
|
|
762
|
+
if (cur.pending && cur.kind === kind) return cur; // 幂等:同一原因不反复重写
|
|
763
|
+
const next = { pending: true, kind, reason, at: Date.now(), bootId: bootId(), installedBy: cur.installedBy || "" };
|
|
764
|
+
writeRestartState(relayDir, next);
|
|
765
|
+
return next;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* 插件装载时调用:bootId 与记录中的不同 = 中间确实重启过一次 → 结清「待重启」状态。
|
|
770
|
+
* @returns {{cleared:boolean, state:object}}
|
|
771
|
+
*/
|
|
772
|
+
function settleRestartState(relayDir) {
|
|
773
|
+
const cur = readRestartState(relayDir);
|
|
774
|
+
if (!cur.pending) return { cleared: false, state: cur };
|
|
775
|
+
if (cur.bootId && cur.bootId === bootId()) return { cleared: false, state: cur }; // 同一进程:仍待重启
|
|
776
|
+
const state = { pending: false, kind: "", reason: "", at: cur.at || 0, bootId: bootId(), lastRestartedAt: Date.now() };
|
|
777
|
+
writeRestartState(relayDir, state);
|
|
778
|
+
return { cleared: true, state };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* 插件文件是否**晚于本进程启动**才落盘(= 运行中被市场安装/在线更新改写过)。
|
|
783
|
+
* 依据:本插件自身 package.json 的 mtime 对比 dsh web 进程启动时刻(2s 容差)。
|
|
784
|
+
*/
|
|
785
|
+
function pluginInstalledAfterBoot() {
|
|
786
|
+
try {
|
|
787
|
+
const here = fileURLToPath(import.meta.url); // <profile>/node_modules/<pkg>/lib/index.js
|
|
788
|
+
const pkgFile = join(dirname(dirname(here)), "package.json");
|
|
789
|
+
const mtimeMs = statSync(pkgFile).mtimeMs;
|
|
790
|
+
const bootMs = Date.now() - process.uptime() * 1000;
|
|
791
|
+
return mtimeMs > bootMs + 2000;
|
|
792
|
+
} catch { return false; }
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** 本进程的启动命令(重启时原样复用:node 路径 + dsh 参数 + 工作目录)。 */
|
|
796
|
+
function selfCommand() {
|
|
797
|
+
return { node: process.execPath, args: process.argv.slice(1), cwd: process.cwd() };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** macOS:从 `launchctl list` 里找出托管本进程(pid 精确匹配)的作业标签。 */
|
|
801
|
+
function launchdLabelForPid(pid) {
|
|
802
|
+
const r = sh("launchctl list");
|
|
803
|
+
if (!r.ok) return "";
|
|
804
|
+
for (const line of r.stdout.split("\n")) {
|
|
805
|
+
const m = line.trim().match(/^(\d+)\s+(-?\d+)\s+(\S+)$/);
|
|
806
|
+
if (m && Number(m[1]) === pid) return m[3];
|
|
807
|
+
}
|
|
808
|
+
return "";
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/** Linux:从 /proc/self/cgroup 里找出托管本进程的 systemd 单元。 */
|
|
812
|
+
function systemdUnitForSelf() {
|
|
813
|
+
try {
|
|
814
|
+
const cg = readFileSync("/proc/self/cgroup", "utf8");
|
|
815
|
+
const m = cg.match(/\/([A-Za-z0-9_.@-]+\.service)/);
|
|
816
|
+
return m ? m[1] : "";
|
|
817
|
+
} catch { return ""; }
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* 生成重启脚本(延迟执行,先把 HTTP 响应让浏览器收完)。
|
|
822
|
+
* mode=launchd/systemd:交给监管者重启最干净(KeepAlive/Restart 会拉起新进程);
|
|
823
|
+
* mode=relaunch:没有监管者时自行拉起同一条命令行(detached 新会话,脱离将被 kill 的旧进程)。
|
|
824
|
+
*/
|
|
825
|
+
function buildRestartScript(mode, target, cmd, relayDir) {
|
|
826
|
+
const log = join(relayDir, RESTART_LOG_FILE);
|
|
827
|
+
const head = `#!/bin/sh\n# dsh-remote 自动生成:重启 DeepSeek harness(mode=${mode})\necho "[$(date '+%F %T')] restart mode=${mode} target=${target || "-"} pid=${process.pid}" >> '${log}'\nsleep 1\n`;
|
|
828
|
+
if (mode === "launchd") {
|
|
829
|
+
return `${head}exec launchctl kickstart -k '${target}' >> '${log}' 2>&1\n`;
|
|
830
|
+
}
|
|
831
|
+
if (mode === "systemd") {
|
|
832
|
+
return `${head}exec systemctl --user restart '${target}' >> '${log}' 2>&1\n`;
|
|
833
|
+
}
|
|
834
|
+
const quote = (s) => "'" + String(s).replace(/'/g, `'\\''`) + "'";
|
|
835
|
+
const argv = [cmd.node, ...cmd.args].map(quote).join(" ");
|
|
836
|
+
// 先优雅退出旧进程,超时再强杀;随后在新会话里拉起同一条命令
|
|
837
|
+
return `${head}kill -TERM ${process.pid} 2>/dev/null\n`
|
|
838
|
+
+ `i=0\nwhile [ $i -lt 60 ]; do kill -0 ${process.pid} 2>/dev/null || break; i=$((i+1)); sleep 0.5; done\n`
|
|
839
|
+
+ `kill -KILL ${process.pid} 2>/dev/null\n`
|
|
840
|
+
+ `cd ${quote(cmd.cwd)} || exit 1\n`
|
|
841
|
+
+ `exec ${argv} >> '${log}' 2>&1\n`;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* 重启 DeepSeek harness(dsh web)。
|
|
846
|
+
* 只重启「承载本插件的那个进程」(pid 匹配到的监管者或自身),绝不触碰 bridge。
|
|
847
|
+
* @returns {{ok:boolean, status:string, mode?:string, detail?:string, script?:string, log?:string}}
|
|
848
|
+
*/
|
|
849
|
+
function restartHarness(relayDir, opts = {}) {
|
|
850
|
+
if (skipsSystemOps()) return { ok: false, status: "skipped", detail: "测试隔离(DSH_RELAY_SKIP_SERVICE=1):跳过真实重启" };
|
|
851
|
+
const cmd = selfCommand();
|
|
852
|
+
let mode = "relaunch";
|
|
853
|
+
let target = "";
|
|
854
|
+
if (platform() === "darwin") {
|
|
855
|
+
const label = launchdLabelForPid(process.pid);
|
|
856
|
+
if (label) { mode = "launchd"; target = `gui/${process.getuid()}/${label}`; }
|
|
857
|
+
} else if (platform() === "linux") {
|
|
858
|
+
const unit = systemdUnitForSelf();
|
|
859
|
+
if (unit) { mode = "systemd"; target = unit; }
|
|
860
|
+
}
|
|
861
|
+
const script = buildRestartScript(mode, target, cmd, relayDir);
|
|
862
|
+
if (opts.dryRun || process.env.DSH_RELAY_RESTART_DRYRUN === "1") {
|
|
863
|
+
// 测试/诊断用:只返回将要执行的脚本,不做任何真实动作
|
|
864
|
+
return { ok: true, status: "dry-run", mode, target, script, log: join(relayDir, RESTART_LOG_FILE) };
|
|
865
|
+
}
|
|
866
|
+
const scriptPath = join(relayDir, RESTART_SCRIPT_FILE);
|
|
867
|
+
try {
|
|
868
|
+
mkdirSync(relayDir, { recursive: true });
|
|
869
|
+
writeFileSync(scriptPath, script, { mode: 0o700 });
|
|
870
|
+
const log = join(relayDir, RESTART_LOG_FILE);
|
|
871
|
+
const child = spawn("/bin/sh", [scriptPath], {
|
|
872
|
+
detached: true,
|
|
873
|
+
cwd: homedir(),
|
|
874
|
+
env: spawnEnv(),
|
|
875
|
+
stdio: ["ignore", openSync(log, "a"), openSync(log, "a")],
|
|
876
|
+
});
|
|
877
|
+
child.unref();
|
|
878
|
+
appendLogLine(relayDir, RESTART_LOG_FILE, `[restart] 已调度重启:mode=${mode}${target ? " target=" + target : ""} helper=${child.pid}`);
|
|
879
|
+
return {
|
|
880
|
+
ok: true,
|
|
881
|
+
status: "restarting",
|
|
882
|
+
mode,
|
|
883
|
+
target,
|
|
884
|
+
pid: child.pid,
|
|
885
|
+
log,
|
|
886
|
+
detail: mode === "launchd" ? "已交由 launchd 重启(约 2~5 秒)"
|
|
887
|
+
: mode === "systemd" ? "已交由 systemd 重启(约 2~5 秒)"
|
|
888
|
+
: "无监管者,已用同一条命令行自拉起(约 2~5 秒)",
|
|
889
|
+
};
|
|
890
|
+
} catch (e) {
|
|
891
|
+
return { ok: false, status: "failed", detail: `调度重启失败: ${e.message}` };
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
593
895
|
// ---------- relay API 代理(直连,不走系统代理;undici 默认忽略代理环境变量) ----------
|
|
594
896
|
|
|
595
897
|
async function relayFetch(relayDir, pathname, init) {
|
|
@@ -1157,11 +1459,17 @@ async function composeStatus(relayDir) {
|
|
|
1157
1459
|
launchd,
|
|
1158
1460
|
manual,
|
|
1159
1461
|
running: launchd.running || manual.bridge.length > 0,
|
|
1462
|
+
// 桌面运行环境(固化运行时 dsh-setup.mjs)是否就绪:面板据此区分
|
|
1463
|
+
// 「运行环境没装(正在自动补装)」与「装了但没跑」,不再把崩溃循环显示成「运行中」。
|
|
1464
|
+
runtimeReady: runtimeReady(relayDir),
|
|
1160
1465
|
bindError,
|
|
1161
1466
|
// E2EE 开关状态(bridge 写 .e2ee-state.json;文件缺失 = 未启用明文):
|
|
1162
1467
|
// {enabled, reason, profile, epoch, caps},供面板「📱 远程访问」卡展示加密状态。
|
|
1163
1468
|
e2ee: readE2eeStateFile(relayDir),
|
|
1164
1469
|
},
|
|
1470
|
+
// 是否需要重启 DeepSeek harness(首次安装 / 在线更新后必须重启才加载插件本体):
|
|
1471
|
+
// {pending, kind: first-install|update, reason, at},供面板顶部醒目提示 + 重启按钮。
|
|
1472
|
+
restart: readRestartState(relayDir),
|
|
1165
1473
|
// 隐私审计(2026-09):不再下发真实 hostname(移除 host 字段)——设备标识统一走 deviceId/服务端登记名
|
|
1166
1474
|
};
|
|
1167
1475
|
}
|
|
@@ -1269,7 +1577,7 @@ const PLUGIN_ID = "dsh-remote-web";
|
|
|
1269
1577
|
const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
|
|
1270
1578
|
const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
|
|
1271
1579
|
/** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
|
|
1272
|
-
const PLUGIN_VERSION = "0.6.
|
|
1580
|
+
const PLUGIN_VERSION = "0.6.2";
|
|
1273
1581
|
const UPDATE_LOG = ".dsh-update.log";
|
|
1274
1582
|
const UPDATE_MARKER = ".dsh-update-running";
|
|
1275
1583
|
|
|
@@ -1336,6 +1644,8 @@ function runOnlineUpdate(relayDir) {
|
|
|
1336
1644
|
}
|
|
1337
1645
|
appendLogLine(relayDir, UPDATE_LOG, `[update] npx 退出 code=${code ?? "?"}(${retried ? "默认源" : "官方源"})`);
|
|
1338
1646
|
clear();
|
|
1647
|
+
// 在线更新改写了插件/运行环境文件 → 必须重启 DeepSeek harness 才装载新版本
|
|
1648
|
+
if (code === 0) markRestartPending(relayDir, "update", "已在线更新,需要重启 DeepSeek harness 生效");
|
|
1339
1649
|
});
|
|
1340
1650
|
child.on("error", (e) => {
|
|
1341
1651
|
appendLogLine(relayDir, UPDATE_LOG, `[update] 子进程启动失败: ${e.message}`);
|
|
@@ -1625,12 +1935,29 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1625
1935
|
sendJson(res, 200, { ok: true, ...(await composeStatus(relayDir)) });
|
|
1626
1936
|
},
|
|
1627
1937
|
},
|
|
1938
|
+
{
|
|
1939
|
+
method: "POST",
|
|
1940
|
+
path: "/dsh-remote/harness/restart",
|
|
1941
|
+
handler: async (_req, res) => {
|
|
1942
|
+
// 首次安装/在线更新后重启 DeepSeek harness:插件本体与浏览器半在进程启动时装载,
|
|
1943
|
+
// 不重启则面板入口与 /dsh-remote/* 路由都不会出现。只重启承载本插件的进程。
|
|
1944
|
+
const r = restartHarness(relayDir);
|
|
1945
|
+
const payload = { ...(await composeStatus(relayDir)), ...r };
|
|
1946
|
+
if (!r.ok) payload.error = r.detail || r.status || "重启失败";
|
|
1947
|
+
sendJson(res, r.ok ? 200 : 500, payload);
|
|
1948
|
+
},
|
|
1949
|
+
},
|
|
1628
1950
|
{
|
|
1629
1951
|
method: "POST",
|
|
1630
1952
|
path: "/dsh-remote/start",
|
|
1631
1953
|
handler: async (_req, res) => {
|
|
1632
1954
|
const r = startBridge(relayDir);
|
|
1633
|
-
|
|
1955
|
+
// 【0.6.2】composeStatus 自带 ok:true(那是「状态查询成功」),必须先展开再覆盖,
|
|
1956
|
+
// 否则不管启动成没成功都回 ok:true → 面板一律弹「✅ bridge 已启动」。
|
|
1957
|
+
// 失败时额外带上 error:浏览器半的 api() 优先用它做提示文案,避免只看到「HTTP 500」。
|
|
1958
|
+
const payload = { ...(await composeStatus(relayDir)), ...r };
|
|
1959
|
+
if (!r.ok) payload.error = r.detail || r.status || "启动失败";
|
|
1960
|
+
sendJson(res, r.ok ? 200 : 500, payload);
|
|
1634
1961
|
},
|
|
1635
1962
|
},
|
|
1636
1963
|
{
|
|
@@ -1638,7 +1965,9 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1638
1965
|
path: "/dsh-remote/stop",
|
|
1639
1966
|
handler: async (_req, res) => {
|
|
1640
1967
|
const r = stopBridge();
|
|
1641
|
-
|
|
1968
|
+
const payload = { ...(await composeStatus(relayDir)), ...r };
|
|
1969
|
+
if (!r.ok) payload.error = r.detail || r.status || "停止失败";
|
|
1970
|
+
sendJson(res, r.ok ? 200 : 500, payload);
|
|
1642
1971
|
},
|
|
1643
1972
|
},
|
|
1644
1973
|
{
|
|
@@ -1836,10 +2165,29 @@ export function apply(ctx, config = {}) {
|
|
|
1836
2165
|
UNINSTALLED_DIRS.delete(relayDir);
|
|
1837
2166
|
// 清理上次进程残留的安装/更新 marker(宿主被重启/强杀时子进程清理回调会丢失)
|
|
1838
2167
|
sweepStaleMarkers(relayDir);
|
|
2168
|
+
// 「待重启」状态结清 + 运行时安装检测:
|
|
2169
|
+
// - bootId 变了 = 这次装载发生在新进程里 → 重启已完成,撤下提示;
|
|
2170
|
+
// - 插件文件晚于本进程启动才落盘(市场安装/在线更新在运行中改写)→ 当前进程里
|
|
2171
|
+
// 根本没有本插件的路由与面板,必须重启 DeepSeek harness 才能生效。
|
|
2172
|
+
const settled = settleRestartState(relayDir);
|
|
2173
|
+
if (settled.cleared) {
|
|
2174
|
+
ctx.logger?.info?.("dsh-remote-web: 检测到 DeepSeek harness 已重启,撤下「需要重启」提示");
|
|
2175
|
+
}
|
|
2176
|
+
if (!readRestartState(relayDir).pending && pluginInstalledAfterBoot()) {
|
|
2177
|
+
markRestartPending(relayDir, "first-install", "首次安装需要重启 DeepSeek harness");
|
|
2178
|
+
ctx.logger?.info?.("dsh-remote-web: 插件文件晚于本次进程启动 → 需要重启 DeepSeek harness");
|
|
2179
|
+
}
|
|
1839
2180
|
ctx.effect(() => registerRoutes(ctx, relayDir), "dsh-remote-web: /dsh-remote routes");
|
|
1840
2181
|
// 0.1.2-rc.1+ 浏览器会话代持:换取 Harness 会话 Cookie 供 bridge 上游携带(手机点设备不再 401 白页)
|
|
1841
2182
|
ctx.effect(() => scheduleHarnessMint(ctx, relayDir), "dsh-remote-web: harness browser-session mint");
|
|
1842
2183
|
// 插件市场一键全功能:缺桌面运行环境则自动安装,登录后自动拉起 bridge(不依赖用户跑 npx)
|
|
1843
2184
|
ctx.effect(() => scheduleRuntime(relayDir), "dsh-remote-web: runtime self-provision");
|
|
2185
|
+
// 【0.6.2】市场安装路径的「装到用户电脑上」第一步:插件加载即后台补装桌面运行环境
|
|
2186
|
+
// (bridge + 自启动),不再等用户先登录、也不再依赖 launchd 状态判断是否「已在运行」。
|
|
2187
|
+
// 已就绪时是零副作用 no-op;正在安装中/已卸载时 ensureRuntime 自身幂等挡下。
|
|
2188
|
+
const provisioned = ensureRuntime(relayDir);
|
|
2189
|
+
if (!provisioned && !runtimeReady(relayDir)) {
|
|
2190
|
+
ctx.logger?.info?.(`dsh-remote-web: 桌面运行环境缺失,已在后台自动安装(relayDir=${relayDir})`);
|
|
2191
|
+
}
|
|
1844
2192
|
ctx.logger?.info?.(`dsh-remote-web: /dsh-remote routes ready (relayDir=${relayDir})`);
|
|
1845
2193
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-web",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "公网远程控制 DeepSeek Harness(dsh web):安装即得专属加密地址,人在外面也能用手机访问电脑上的 dsh——无需同一局域网/WiFi、无需公网 IP 与内网穿透,全程加密;手机端 100% 还原电脑体验(对话/工具/审批/设置)。技术用户可选自建服务,流量走自己的服务器。Remote control DeepSeek Harness (dsh web) from anywhere over the public internet — install, get an encrypted URL, use it from your phone on any network (dual-half cordis plugin: Settings panel + same-origin /dsh-remote routes). (2026-09 由 dsh-remote-ui 更名 / renamed from dsh-remote-ui; dsh-remote 的 dsh web 插件半,不是纯 UI/皮肤插件)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|