@mrrisega/dsh-remote 0.6.4-beta.8 → 0.6.4-beta.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrisega/dsh-remote",
3
- "version": "0.6.4-beta.8",
3
+ "version": "0.6.4-beta.9",
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",
@@ -1243,6 +1243,9 @@ window.__ModuleLoader__.load({
1243
1243
  // 是否需要重启 DeepSeek harness(首次安装/在线更新后置顶提醒 + 底部常驻按钮)
1244
1244
  var restartInfo = (st && st.restart) || {};
1245
1245
  var restartPending = !!restartInfo.pending;
1246
+ // kind === "refresh" = 插件文件在运行中被改写(装完/更新完),只需刷新页面;
1247
+ // 其余情况(极为罕见)才提重启 dsh web。
1248
+ var isRefreshOnly = restartPending && restartInfo.kind === "refresh";
1246
1249
  // 连接阶段(node 半下发):轮询拿到的新鲜结果优先,其次用 /status 里带的那一份;
1247
1250
  // 旧版 host(响应里没有 connect 字段)→ 回退到原来的 service.running 文案(行为不变)。
1248
1251
  var connInfo = conn || (st && st.connect) || null;
@@ -2446,9 +2449,11 @@ window.__ModuleLoader__.load({
2446
2449
  );
2447
2450
  }
2448
2451
 
2449
- // 【自动化】待重启时自动倒计时重启(见 autoRestartCancelled 说明);倒计时只在面板可见且无其他操作时推进
2452
+ // 【自动化】待重启时自动倒计时重启(见 autoRestartCancelled 说明);倒计时只在面板可见且无其他操作时推进。
2453
+ // ⚠️ isRefreshOnly(插件更新后只需刷新)时**不**自动重启:刷新是零风险动作,重启 dsh web 会打断
2454
+ // 用户正在进行的会话,不能替他做这个决定 —— 让他点「刷新页面」即可。
2450
2455
  useEffect(function () {
2451
- if (!restartPending || busy !== "" || restartCancelled) { setRestartCountdown(null); return undefined; }
2456
+ if (!restartPending || isRefreshOnly || busy !== "" || restartCancelled) { setRestartCountdown(null); return undefined; }
2452
2457
  var at = restartInfo.at || 0;
2453
2458
  if (autoRestartCancelled(at)) { setRestartCountdown(null); return undefined; }
2454
2459
  var left = Math.round(AUTO_RESTART_DELAY_MS / 1000);
@@ -2462,7 +2467,7 @@ window.__ModuleLoader__.load({
2462
2467
  restartHarness(); // 已有实现:重启 + 轮询等它回来 + 自动刷新页面
2463
2468
  }, 1000);
2464
2469
  return function () { clearInterval(iv); };
2465
- }, [restartPending, restartInfo.at, busy, restartCancelled]);
2470
+ }, [restartPending, isRefreshOnly, restartInfo.at, busy, restartCancelled]);
2466
2471
 
2467
2472
  /** 取消本次自动重启(按事件持久化:同一 pending 事件不再自动重启)。 */
2468
2473
  var cancelAutoRestart = function () {
@@ -2508,9 +2513,13 @@ window.__ModuleLoader__.load({
2508
2513
  h("span", { className: "dru-restart-alert-icon" }, "🔔"),
2509
2514
  h("div", { style: { flex: "1" } },
2510
2515
  h("div", { className: "dru-restart-alert-title" },
2511
- restartInfo.kind === "update" ? "在线更新已完成,需要重启 DeepSeek harness" : "首次安装需要重启 DeepSeek harness"),
2516
+ // 插件已改为 patch 热加载:装插件/在线更新都不再需要重启 harness,
2517
+ // 唯一要用户动一下的情形是"插件文件在运行中被改写"→ 刷新页面即可。
2518
+ isRefreshOnly ? "插件已更新,刷新页面即可生效" : "需要重启 dsh web 才能载入新插件"),
2512
2519
  h("div", { className: "dru-restart-alert-sub" },
2513
- "插件本体与「远程访问」面板是在 DeepSeek harness 启动时装载的,重启之后即可正常使用(账号、二维码、bridge 全部生效)。"),
2520
+ isRefreshOnly
2521
+ ? "刚装好/刚更新的插件已在磁盘上,浏览器里的面板还是旧版本。刷新一下这个页面即可(手机端连接与已登录状态都不受影响)。"
2522
+ : "当前运行中的 dsh web 里还没有载入这个插件。先刷新页面;如果刷新后仍然看不到面板,再重启一次 dsh web。"),
2514
2523
  // 【自动化】待重启时自动倒计时重启(用户零操作);倒计时期间可一键取消
2515
2524
  restartCountdown !== null && restartCountdown > 0
2516
2525
  ? h("div", { className: "dru-restart-auto", role: "status" },
@@ -2524,12 +2533,19 @@ window.__ModuleLoader__.load({
2524
2533
  ? h("div", { className: "dru-restart-auto muted" }, "已取消自动重启 —— 你也可以随时点下面的按钮完成安装")
2525
2534
  : null,
2526
2535
  h("div", { className: "dru-actions", style: { marginTop: 10 } },
2536
+ isRefreshOnly
2537
+ ? h("button", {
2538
+ type: "button",
2539
+ className: "dru-btn dru-btn-primary",
2540
+ onClick: function () { try { location.reload(); } catch (e) { /* 忽略 */ } }
2541
+ }, "刷新页面")
2542
+ : null,
2527
2543
  h("button", {
2528
2544
  type: "button",
2529
- className: "dru-btn dru-btn-primary",
2545
+ className: "dru-btn " + (isRefreshOnly ? "dru-btn-ghost" : "dru-btn-primary"),
2530
2546
  disabled: busy !== "",
2531
2547
  onClick: function () { restartHarness(); }
2532
- }, busy === "restart" ? "重启中…" : (restartCountdown !== null && restartCountdown > 0 ? "立即重启(不用等)" : "重启 DeepSeek harness")),
2548
+ }, busy === "restart" ? "重启中…" : (restartCountdown !== null && restartCountdown > 0 ? "立即重启(不用等)" : "重启 dsh web")),
2533
2549
  restartCountdown !== null && restartCountdown > 0
2534
2550
  ? h("button", {
2535
2551
  type: "button",
@@ -2545,13 +2561,13 @@ window.__ModuleLoader__.load({
2545
2561
  ),
2546
2562
  // 常驻入口最底部:「重启 DeepSeek harness」按钮始终可达(首次安装/更新/排查都用它)
2547
2563
  h("div", { className: "dru-restart-foot" },
2548
- h("div", { className: "dru-hint" }, "重启 DeepSeek harness:插件与面板在 harness 启动时装载,首次安装或在线更新后重启即可生效;仅重启 dsh web,不影响 bridge 与手机端连接。"),
2564
+ h("div", { className: "dru-hint" }, "重启 dsh web:仅在「刷新页面后仍看不到面板」时才需要。日常安装/更新插件已自动热加载,装完刷新页面即可;重启只影响 dsh web 本身,不影响 bridge 与手机端连接。"),
2549
2565
  h("button", {
2550
2566
  type: "button",
2551
2567
  className: "dru-btn " + (restartPending ? "dru-btn-primary" : "dru-btn-ghost"),
2552
2568
  disabled: busy !== "",
2553
2569
  onClick: function () { restartHarness(); }
2554
- }, busy === "restart" ? "重启中…" : "🔄 重启 DeepSeek harness")
2570
+ }, busy === "restart" ? "重启中…" : "🔄 重启 dsh web")
2555
2571
  ),
2556
2572
  renderCommunityModal()
2557
2573
  );
@@ -677,8 +677,11 @@ function scheduleRuntime(relayDir) {
677
677
  telemetryRuntimeProbe(relayDir); // 匿名遥测:环境刚补齐 → runtime_ready(每进程一次)
678
678
  if (awaitingRestartHint) {
679
679
  awaitingRestartHint = false;
680
- // 首次安装:运行环境(bridge + 自启动)刚在本进程内补齐 → 提示重启,重启后即完全可用
681
- markRestartPending(relayDir, "first-install", "首次安装需要重启 DeepSeek harness");
680
+ // 运行环境刚在本进程内补齐 —— **不需要重启 harness**:
681
+ // · bridge 是独立 launchd 进程,补齐后自愈调度会直接把它拉起;
682
+ // · 插件本体走 patch 热加载,装完即生效。
683
+ // 以前这里会 markRestartPending("first-install") 弹出"需要重启 DeepSeek harness"横幅,
684
+ // 在热加载落地后已经变成纯粹的误导(用户实测反馈),故移除。
682
685
  }
683
686
  const cfg = loadConfig(relayDir);
684
687
  const hasAcct = Boolean((cfg.phone || cfg.email) && cfg.password) || Boolean(cfg.local_key);
@@ -849,7 +852,10 @@ function writeRestartState(relayDir, state) {
849
852
  } catch { /* 写盘失败不致命:面板退化为不提示 */ }
850
853
  }
851
854
 
852
- /** 标记「需要重启 DeepSeek harness」。kind: first-install(首次安装) | update(在线更新)。 */
855
+ /**
856
+ * 标记「需要用户动一下」的状态。kind = refresh(插件被运行时改写,刷新页面即可)。
857
+ * 历史 kind(first-install / update)已废弃:热加载落地后,装插件与在线更新都不再需要重启 harness。
858
+ */
853
859
  function markRestartPending(relayDir, kind, reason) {
854
860
  const cur = readRestartState(relayDir);
855
861
  if (cur.pending && cur.kind === kind) return cur; // 幂等:同一原因不反复重写
@@ -1739,7 +1745,7 @@ const PLUGIN_ID = "dsh-remote-web";
1739
1745
  const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
1740
1746
  const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
1741
1747
  /** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
1742
- const PLUGIN_VERSION = "0.6.4-beta.8";
1748
+ const PLUGIN_VERSION = "0.6.4-beta.9";
1743
1749
  const UPDATE_LOG = ".dsh-update.log";
1744
1750
  const UPDATE_MARKER = ".dsh-update-running";
1745
1751
 
@@ -1809,9 +1815,12 @@ function runOnlineUpdate(relayDir) {
1809
1815
  }
1810
1816
  appendLogLine(relayDir, UPDATE_LOG, `[update] npx 退出 code=${code ?? "?"}(${retried ? "默认源" : "官方源"})`);
1811
1817
  clear();
1812
- // 在线更新改写了插件/运行环境文件 → 必须重启 DeepSeek harness 才装载新版本
1813
- if (code === 0) markRestartPending(relayDir, "update", "已在线更新,需要重启 DeepSeek harness 生效");
1814
- else telemetryRecord(relayDir, "update_failed", { fail_code: telemetryFailCodeFromText(readTail(join(relayDir, UPDATE_LOG))) });
1818
+ // 在线更新改写了插件/运行环境文件 → 版本对不上时由 pluginInstalledAfterBoot() 自动识别,
1819
+ // 面板会提示"刷新页面"。这里**不再**写"需要重启 DeepSeek harness"的待重启标记:
1820
+ // 插件走 patch 热加载、bridge 是独立进程,重启 harness 已无必要(用户实测反馈)。
1821
+ // 成功不发事件(update_started 已发过,失败才发 update_failed;白名单里没有 update_done,
1822
+ // 硬发只会被服务端静默丢弃)。
1823
+ if (code !== 0) telemetryRecord(relayDir, "update_failed", { fail_code: telemetryFailCodeFromText(readTail(join(relayDir, UPDATE_LOG))) });
1815
1824
  });
1816
1825
  child.on("error", (e) => {
1817
1826
  appendLogLine(relayDir, UPDATE_LOG, `[update] 子进程启动失败: ${e.message}`);
@@ -3325,8 +3334,12 @@ export function apply(ctx, config = {}) {
3325
3334
  ctx.logger?.info?.("dsh-remote-web: 检测到 DeepSeek harness 已重启,撤下「需要重启」提示");
3326
3335
  }
3327
3336
  if (!readRestartState(relayDir).pending && pluginInstalledAfterBoot()) {
3328
- markRestartPending(relayDir, "first-install", "首次安装需要重启 DeepSeek harness");
3329
- ctx.logger?.info?.("dsh-remote-web: 插件文件晚于本次进程启动 → 需要重启 DeepSeek harness");
3337
+ // 唯一还需要用户动一下的情形:插件文件晚于本进程启动才落盘(= 运行中被市场安装/在线更新改写),
3338
+ // 此时本进程里没有本插件的路由与面板。**先让用户刷新页面**(客户端半随之更新);
3339
+ // 只有刷新后仍无面板才需要重启(那时多半是 profile patch 行没被 HMR 读到)。
3340
+ // 不再对"首次安装/在线更新完成"提示重启:插件走 patch 热加载、bridge 是独立进程,重启 harness 无必要。
3341
+ markRestartPending(relayDir, "refresh", "插件已更新,刷新页面即可生效");
3342
+ ctx.logger?.info?.("dsh-remote-web: 插件文件晚于本次进程启动 → 提示刷新页面");
3330
3343
  }
3331
3344
  ctx.effect(() => registerRoutes(ctx, relayDir), "dsh-remote-web: /dsh-remote routes");
3332
3345
  // 0.1.2-rc.1+ 浏览器会话代持:换取 Harness 会话 Cookie 供 bridge 上游携带(手机点设备不再 401 白页)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-web",
3
- "version": "0.6.4-beta.8",
3
+ "version": "0.6.4-beta.9",
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",
@@ -1,7 +1,11 @@
1
- // 【自动化】首次安装/更新后「自动重启 DeepSeek harness」的安全回归(浏览器半,0.6.4)。
1
+ // 【自动化】「自动重启 dsh web」的安全回归(浏览器半,0.6.4)。
2
2
  //
3
- // 目标:用户装完不需要点任何按钮 —— 面板发现「待重启」后自己倒计时重启,重启后自动恢复页面
3
+ // 目标:**确实需要重启时**,用户不必自己点按钮 —— 面板发现待重启后自己倒计时重启,重启后自动恢复页面
4
4
  // (复用既有 restartHarness + waitHarnessBack:轮询到 harness 回来即自动刷新)。
5
+ //
6
+ // ⚠️ 0.6.4-beta.9 起:kind="refresh"(插件被运行时改写 → 只需刷新页面)**不再自动重启**,
7
+ // 因为刷新是零风险动作、重启会打断用户正在进行的会话。因此本用例用 kind="update"
8
+ // (非 refresh)来构造"确实需要重启"的场景,以覆盖倒计时链路。
5
9
  // 但「自动重启进程」是危险动作,本用例锁死四条安全阀:
6
10
  // ① 只在面板可见时计时(用户没在看就不动);
7
11
  // ② 15 秒倒计时内可一键取消,取消标记按本次事件(restart.at)持久化,同一事件不再自动重启;
@@ -40,7 +44,9 @@ const LOGGED = { ok: true, config: { phone: "138****0000", hasPhone: true, mode:
40
44
  const KEY_URL = "https://app.test/a/K1";
41
45
  const SESSIONS = [{ id: "ms_1", label: "iPhone 15", os: "iOS", browser: "Safari", created_at: 1700000000000, last_seen_at: 1700000600000, revoked_at: null }];
42
46
  function statusWithRestart(pending, at) {
43
- return Object.assign({}, LOGGED, { restart: { pending: !!pending, kind: "first-install", reason: "首次安装需要重启 DeepSeek harness", at: at || 0 } });
47
+ // kind 用 "update"(非 refresh):只有这一路才走自动重启倒计时;
48
+ // kind="refresh" 的面板只提示刷新页面(见 harness-restart.test.mjs)。
49
+ return Object.assign({}, LOGGED, { restart: { pending: !!pending, kind: "update", reason: "需要重启 dsh web 才能载入新插件", at: at || 0 } });
44
50
  }
45
51
 
46
52
  function loadPlugin(opts = {}) {
@@ -238,7 +244,7 @@ function loadPlugin(opts = {}) {
238
244
  test("待重启 + 面板可见 → 15 秒后自动重启(用户零操作),重启后 pending 结清即停止", async () => {
239
245
  const plugin = loadPlugin({ status: statusWithRestart(true, 1111) });
240
246
  let tree = await plugin.settle();
241
- assert.ok(textHas(tree, "首次安装需要重启 DeepSeek harness"), "应显示待重启横幅");
247
+ assert.ok(textHas(tree, "需要重启 dsh web 才能载入新插件"), "应显示待重启横幅");
242
248
  assert.ok(textHas(tree, "秒后自动重启"), "应显示自动重启倒计时文案");
243
249
  assert.equal(plugin.counts().restartCalls, 0, "倒计时期间不得提前重启");
244
250
 
@@ -1,9 +1,16 @@
1
- // 「首次安装需要重启 DeepSeek harness」回归(0.6.2 新增)。
1
+ // 「插件装载 / 刷新 / 重启」回归(0.6.2 新增,0.6.4-beta.9 语义修订)。
2
2
  //
3
3
  // 产品诉求(用户原话):
4
- // 1. 安装完成后桌面端 UI 直接可用,并提示「首次安装需要重启 DeepSeek harness」,旁边给「重启」按钮;
5
- // 2. 常驻入口最底下要有「重启 DeepSeek harness」按钮;
6
- // 3. 首次打开(尚未重启)时,该按钮要出现在上方显眼位置。
4
+ // 1. 安装完成后桌面端 UI 直接可用;
5
+ // 2. 常驻入口最底下要有「重启 dsh web」按钮(排查用,始终可达);
6
+ // 3. 需要用户动一下时,该提示要出现在上方显眼位置。
7
+ //
8
+ // ⚠️ 0.6.4-beta.9 语义修订(用户反馈「能热加载了,这个设置窗口就不需要了」):
9
+ // 插件已改为 profile patch **热加载**装载(HMR 监听 cordis.patch.yml,存盘约 1 秒生效),
10
+ // 运行环境(bridge)又是独立 launchd 进程 —— 所以「装插件 / 在线更新完成」都**不再需要重启
11
+ // DeepSeek harness**,那条横幅确实是误导。现在只在一种情形提示用户:
12
+ // **磁盘上的插件文件比当前进程新**(= 运行中被市场安装/在线更新改写),文案是
13
+ // 「插件已更新,刷新页面即可生效」,并且**不自动重启**(刷新零风险,重启会打断用户会话)。
7
14
  //
8
15
  // 机制:dsh web 的插件(宿主半 + 浏览器半)都在**进程启动时**装载,市场安装/在线更新只是把文件
9
16
  // 写进 profile,当前进程里既没有 /dsh-remote/* 路由也没有面板入口 → 必须重启才生效。
@@ -12,7 +19,7 @@
12
19
  import assert from "node:assert/strict";
13
20
  import http from "node:http";
14
21
  import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
15
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
22
+ import { mkdir, mkdtemp, readFile, rm, utimes, writeFile } from "node:fs/promises";
16
23
  import os from "node:os";
17
24
  import path from "node:path";
18
25
  import test from "node:test";
@@ -107,7 +114,7 @@ async function serve(routes) {
107
114
 
108
115
  // ─────────────────────────── 宿主半:状态机 ───────────────────────────
109
116
 
110
- test("首次安装(运行环境在本进程内补齐)→ 自动标记「需要重启」并在 /status 下发", async () => {
117
+ test("首次安装(运行环境在本进程内补齐)→ **不再**提示重启 harness(热加载后已无必要)", async () => {
111
118
  const env = await setup();
112
119
  try {
113
120
  await writeConfig(env.relayDir);
@@ -118,16 +125,41 @@ test("首次安装(运行环境在本进程内补齐)→ 自动标记「需
118
125
  const { host, base } = await serve(routes);
119
126
  try {
120
127
  const before = await (await fetch(`${base}/dsh-remote/status`)).json();
121
- assert.equal(before.restart.pending, false, "运行环境补齐前不该提示重启");
128
+ assert.equal(before.restart.pending, false, "运行环境补齐前不该有提示");
122
129
 
123
130
  // 模拟后台 npx 安装完成:固化运行时落盘
124
131
  await writeFile(path.join(env.relayDir, "dsh-setup.mjs"), "// runtime stub");
125
- await waitFor(async () => readFile(env.stateFile, "utf8").then(() => true).catch(() => false));
132
+ await sleep(400); // 给自愈调度一轮时间(以前这一轮会 markRestartPending)
126
133
 
127
134
  const after = await (await fetch(`${base}/dsh-remote/status`)).json();
128
- assert.equal(after.restart.pending, true, "运行环境刚补齐 → 应提示需要重启 DeepSeek harness");
129
- assert.equal(after.restart.kind, "first-install");
130
- assert.match(String(after.restart.reason), /首次安装需要重启 DeepSeek harness/);
135
+ // bridge 是独立 launchd 进程、插件走 patch 热加载 → 补齐运行环境不需要重启 harness。
136
+ // 旧行为(提示"首次安装需要重启 DeepSeek harness")已被用户实测判定为误导,故断言必须为 false。
137
+ assert.equal(after.restart.pending, false,
138
+ "运行环境补齐**不该**再提示重启 harness(热加载 + 独立 bridge 进程)");
139
+ } finally { host.close(); routes.dispose(); }
140
+ } finally { await env.restore(); }
141
+ });
142
+
143
+ test("插件文件晚于本进程启动 → 提示「刷新页面」,且**不**自动重启", async () => {
144
+ const env = await setup();
145
+ try {
146
+ await writeConfig(env.relayDir);
147
+ await writeFile(path.join(env.relayDir, "dsh-setup.mjs"), "// runtime stub");
148
+ const routes = boot(env.relayDir);
149
+ const { host, base } = await serve(routes);
150
+ try {
151
+ const cur = await (await fetch(`${base}/dsh-remote/status`)).json();
152
+ // 用**真实的 bootId** 写回状态:否则 settleRestartState 会判定"这是上个进程写的 = 已重启过"
153
+ // 而把提示结清(那样就测不到"同进程内保留提示"这条语义了)。
154
+ await writeFile(env.stateFile, JSON.stringify({
155
+ pending: true, kind: "refresh", reason: "插件已更新,刷新页面即可生效",
156
+ at: Date.now() - 3000, bootId: cur.restart.bootId
157
+ }));
158
+
159
+ const st = await (await fetch(`${base}/dsh-remote/status`)).json();
160
+ assert.equal(st.restart.pending, true, "插件被运行时改写 → 应提示");
161
+ assert.equal(st.restart.kind, "refresh", "kind 必须是 refresh(刷新语义,不是重启)");
162
+ assert.match(String(st.restart.reason), /刷新页面/);
131
163
  } finally { host.close(); routes.dispose(); }
132
164
  } finally { await env.restore(); }
133
165
  });
@@ -148,18 +180,31 @@ test("重启已完成(bootId 变化)→ 自动撤下提示;同一进程内
148
180
  } finally { host.close(); routes.dispose(); }
149
181
  } finally { await a.restore(); }
150
182
 
151
- // 场景 B:状态由**当前进程**写下(用户还没点重启,只是刷新了页面 / 插件重新装载)→ 必须保留。
152
- // 走真实链路:插件自己写 pending(运行环境在本进程内补齐),再在同一进程内重新装载一次。
183
+ // 场景 B:状态由**当前进程**写下(用户还没处理,只是刷新了页面 / 插件重新装载)→ 必须保留提示。
184
+ // 注:0.6.4-beta.9 起"运行环境在本进程内补齐"不再写待重启(热加载后无必要),
185
+ // 所以这里直接按**真实 bootId** 写一份 kind=refresh 状态来构造该场景。
153
186
  const b = await setup();
154
187
  try {
155
188
  await writeConfig(b.relayDir);
189
+ await writeFile(path.join(b.relayDir, "dsh-setup.mjs"), "// runtime stub");
156
190
  process.env.DSH_RELAY_SELFHEAL_MS = "200";
191
+ // 本进程的 bootId:必须与 lib/index.js 的 BOOT_ID 完全一致,否则 settleRestartState
192
+ // 会判定"状态来自上一个进程 = 重启已完成"而把提示结清。
193
+ // 构造方式与 lib 相同;lib 对外不导出它,状态文件只在有 pending 时才写,故这里自行推出。
194
+ const sameProcessBootId = `${process.pid}-${Math.round(Date.now() - process.uptime() * 1000)}`;
157
195
  const routes1 = boot(b.relayDir);
158
196
  const { host: host1, base: base1 } = await serve(routes1);
159
- await writeFile(path.join(b.relayDir, "dsh-setup.mjs"), "// runtime stub");
160
- await waitFor(async () => readFile(b.stateFile, "utf8").then(() => true).catch(() => false));
161
- const st1 = await (await fetch(`${base1}/dsh-remote/status`)).json();
162
- assert.equal(st1.restart.pending, true, "运行环境补齐后应标记待重启");
197
+ await writeFile(b.stateFile, JSON.stringify({
198
+ pending: true, kind: "refresh", reason: "插件已更新,刷新页面即可生效",
199
+ at: Date.now() - 3000, bootId: sameProcessBootId
200
+ }));
201
+ // 把状态文件的 mtime 置为"刚写过":pluginInstalledAfterBoot() 判的是"插件文件晚于本进程启动"。
202
+ // 不这么做,第二次 apply() 的条件 `!pending && pluginInstalledAfterBoot()` 会为真,
203
+ // 于是 markRestartPending 又写一次(新 at/bootId),测的就不是"settle 不清"这条语义了。
204
+ const nowMs = Date.now() / 1000;
205
+ await utimes(b.stateFile, nowMs, nowMs);
206
+ const st1b = await (await fetch(`${base1}/dsh-remote/status`)).json();
207
+ assert.equal(st1b.restart.pending, true, "同进程写下的待处理状态应保留");
163
208
  host1.close();
164
209
  routes1.dispose();
165
210
 
@@ -169,7 +214,7 @@ test("重启已完成(bootId 变化)→ 自动撤下提示;同一进程内
169
214
  try {
170
215
  const st2 = await (await fetch(`${base2}/dsh-remote/status`)).json();
171
216
  assert.equal(st2.restart.pending, true, "同一进程内(还没真重启)不得误撤提示");
172
- assert.equal(st2.restart.bootId, st1.restart.bootId, "bootId 应在同一进程内稳定不变");
217
+ assert.equal(st2.restart.bootId, sameProcessBootId, "bootId 应在同一进程内稳定不变");
173
218
  } finally { host2.close(); routes2.dispose(); }
174
219
  } finally { await b.restore(); }
175
220
  });
@@ -299,15 +344,11 @@ function loadPlugin(statusBody) {
299
344
  return { requests, states, render() { hook = 0; return registered.get("dsh-remote")({ close() {} }); } };
300
345
  }
301
346
 
302
- const STATUS_FIRST_INSTALL = {
347
+ // 现在唯一会提示用户的状态是 kind=refresh(插件文件被运行时改写 → 刷新页面即可)
348
+ const STATUS_REFRESH = {
303
349
  ok: true, config: { phone: "13800000000", deviceId: "dev-x" },
304
350
  service: { running: true, runtimeReady: true, launchd: {} },
305
- restart: { pending: true, kind: "first-install", reason: "首次安装需要重启 DeepSeek harness" },
306
- };
307
- const STATUS_UPDATE = {
308
- ok: true, config: { phone: "13800000000", deviceId: "dev-x" },
309
- service: { running: true, runtimeReady: true, launchd: {} },
310
- restart: { pending: true, kind: "update", reason: "已在线更新,需要重启 DeepSeek harness 生效" },
351
+ restart: { pending: true, kind: "refresh", reason: "插件已更新,刷新页面即可生效", at: Date.now() },
311
352
  };
312
353
  const STATUS_IDLE = {
313
354
  ok: true, config: { phone: "13800000000", deviceId: "dev-x" },
@@ -315,21 +356,26 @@ const STATUS_IDLE = {
315
356
  restart: { pending: false },
316
357
  };
317
358
 
318
- test("UI:待重启 → 顶部醒目提示「首次安装需要重启 DeepSeek harness」+ 重启按钮", () => {
359
+ test("UI:插件更新后 → 顶部提示「刷新页面即可生效」+ 刷新按钮(不再喊重启 harness)", () => {
319
360
  const plugin = loadPlugin({
320
361
  ok: true,
321
362
  config: { phone: "13800000000", deviceId: "dev-x" },
322
363
  service: { running: true, runtimeReady: true, launchd: {} },
323
- restart: { pending: true, kind: "first-install", reason: "首次安装需要重启 DeepSeek harness" },
364
+ restart: { pending: true, kind: "refresh", reason: "插件已更新,刷新页面即可生效" },
324
365
  });
325
- plugin.states[0] = STATUS_FIRST_INSTALL; // mock 的 useEffect 不执行 → 直接注入面板状态
366
+ plugin.states[0] = STATUS_REFRESH; // mock 的 useEffect 不执行 → 直接注入面板状态
326
367
  const tree = plugin.render();
327
- assert.ok(textHas(tree, "首次安装需要重启 DeepSeek harness"), "顶部应出现首次安装重启提示");
368
+ assert.ok(textHas(tree, "插件已更新,刷新页面即可生效"), "顶部应出现刷新提示");
369
+ // 不能再说"需要重启 DeepSeek harness" —— 热加载后那是误导(用户实测反馈)
370
+ assert.ok(!textHas(tree, "需要重启 DeepSeek harness"), "不应再出现「需要重启 DeepSeek harness」");
328
371
  const alert = find(tree, (n) => n.props && n.props.className === "dru-restart-alert");
329
372
  assert.ok(alert, "应有醒目提示块(dru-restart-alert)");
330
- const btns = findAll(tree, (n) => n.props && String(n.props.className || "").includes("dru-btn") &&
331
- (n.children || []).some((c) => typeof c === "string" && c.includes("重启 DeepSeek harness")));
332
- assert.ok(btns.length >= 2, "提示块内 + 底部常驻各一个重启按钮,实际 " + btns.length);
373
+ // 主按钮应是「刷新页面」
374
+ const refreshBtn = find(tree, (n) => n.props && String(n.props.className || "").includes("dru-btn-primary") &&
375
+ (n.children || []).some((c) => typeof c === "string" && c.includes("刷新页面")));
376
+ assert.ok(refreshBtn, "提示块内主按钮应是「刷新页面」");
377
+ // 重启按钮仍可达(兜底),但不再是主按钮
378
+ assert.ok(textHas(tree, "重启 dsh web"), "应保留「重启 dsh web」作为兜底入口");
333
379
  });
334
380
 
335
381
  test("UI:常驻入口最底部始终有「重启 DeepSeek harness」按钮;不需要重启时也不消失", () => {
@@ -341,28 +387,29 @@ test("UI:常驻入口最底部始终有「重启 DeepSeek harness」按钮;
341
387
  });
342
388
  plugin.states[0] = STATUS_IDLE;
343
389
  const tree = plugin.render();
344
- assert.ok(!textHas(tree, "首次安装需要重启 DeepSeek harness"), "不需要重启时不该出现顶部提示");
390
+ assert.ok(!textHas(tree, "刷新页面即可生效"), "不需要动作时不该出现顶部提示");
345
391
  const foot = find(tree, (n) => n.props && n.props.className === "dru-restart-foot");
346
392
  assert.ok(foot, "底部应常驻重启按钮区(dru-restart-foot)");
347
- assert.ok(textHas(foot, "重启 DeepSeek harness"), "常驻区应含重启按钮文案");
393
+ assert.ok(textHas(foot, "重启 dsh web"), "常驻区应含重启按钮文案(排查兜底,始终可达)");
348
394
  });
349
395
 
350
- test("UI:点「重启」→ 调用 /dsh-remote/harness/restart", async () => {
396
+ test("UI:点「重启 dsh web」→ 调用 /dsh-remote/harness/restart", async () => {
351
397
  const plugin = loadPlugin({
352
398
  ok: true,
353
399
  config: { phone: "13800000000", deviceId: "dev-x" },
354
400
  service: { running: true, runtimeReady: true, launchd: {} },
355
- restart: { pending: true, kind: "update", reason: "已在线更新,需要重启 DeepSeek harness 生效" },
401
+ restart: { pending: true, kind: "refresh", reason: "插件已更新,刷新页面即可生效" },
356
402
  });
357
- plugin.states[0] = STATUS_UPDATE;
403
+ plugin.states[0] = STATUS_REFRESH;
358
404
  const tree = plugin.render();
359
- // 顶部提示块内的那个按钮(常驻脚注里也有一个)
405
+ // 顶部提示块内的兜底重启按钮(常驻脚注里也有一个)
360
406
  const alert = find(tree, (n) => n.props && n.props.className === "dru-restart-alert");
361
407
  const btn = find(alert, (n) => n.props && String(n.props.className || "").includes("dru-btn") &&
362
- (n.children || []).some((c) => typeof c === "string" && c.includes("重启 DeepSeek harness")));
408
+ (n.children || []).some((c) => typeof c === "string" && c.includes("重启 dsh web")));
363
409
  assert.ok(btn, "顶部提示块内应能定位到重启按钮");
364
410
  btn.props.onClick();
365
411
  await new Promise((r) => setImmediate(r));
366
412
  assert.ok(plugin.requests.some((r) => r.path === "/dsh-remote/harness/restart" && r.method === "POST"),
367
413
  "点击后应 POST /dsh-remote/harness/restart,实际 " + JSON.stringify(plugin.requests));
368
414
  });
415
+