@mrrisega/dsh-remote 0.6.3 → 0.6.4-beta.10

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.3",
3
+ "version": "0.6.4-beta.10",
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",
@@ -38,7 +38,8 @@
38
38
  "dsh-setup.mjs",
39
39
  "clients/dsh-remote",
40
40
  "packages/dsh-remote-web",
41
- "docs/self-hosting.md"
41
+ "docs/self-hosting.md",
42
+ "docs/telemetry.md"
42
43
  ],
43
44
  "workspaces": [
44
45
  "packages/*",
@@ -49,9 +50,9 @@
49
50
  },
50
51
  "scripts": {
51
52
  "test": "npm run test:router && npm run test:plugin && npm run test:bridge",
52
- "test:router": "node --test packages/relay-router/test/native-device-selection.test.mjs packages/relay-router/test/quotas.test.mjs",
53
+ "test:router": "node --test packages/relay-router/test/native-device-selection.test.mjs packages/relay-router/test/quotas.test.mjs packages/relay-router/test/presence-report.test.mjs",
53
54
  "_comment:test:plugin": "DSH_RELAY_SKIP_SERVICE=1 = 测试隔离:插件加载时不做真实系统操作(npx 补装 / launchctl / 重启 harness);需要真实分支的用例自行 unset(见 account-switch / runtime-provision / harness-restart 用例注释)",
54
- "test:plugin": "DSH_RELAY_SKIP_SERVICE=1 node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
55
+ "test:plugin": "DSH_REMOTE_TELEMETRY=0 DSH_RELAY_SKIP_SERVICE=1 node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
55
56
  "test:bridge": "node --test clients/dsh-remote/test/*.test.mjs",
56
57
  "check": "node --check clients/dsh-remote/dsh-bridge.mjs && node --check clients/dsh-remote/e2ee-shim.mjs && node --check dsh-setup.mjs && node --check packages/dsh-remote-web/lib/index.js && node --check packages/dsh-remote-web/lib/client.js && bash -n deploy/install-open.sh && npm run check:alias",
57
58
  "sync:alias": "node scripts/sync-legacy-alias.mjs",
@@ -142,6 +142,8 @@ window.__ModuleLoader__.load({
142
142
  ".dru-popup-foot button:hover{color:#0969da}",
143
143
  ".dru-popup .dru-msg{text-align:left}",
144
144
  ".dru-community-qr{display:block;width:220px;max-width:62vw;margin:0 auto;background:#ffffff;padding:10px;border-radius:10px;border:1px solid #d0d7de;box-sizing:content-box}",
145
+ ".dru-restart-auto{display:flex;align-items:center;gap:8px;margin-top:10px;font-size:12.5px;color:#1a7f37;background:#dafbe1;border-radius:8px;padding:8px 10px}",
146
+ ".dru-restart-auto.muted{color:#57606a;background:#f6f8fa}",
145
147
  ".dru-fb-community{margin-top:16px;padding-top:14px;border-top:1px dashed #d0d7de;text-align:center}",
146
148
  ".dru-fb-community .dru-community-qr{width:180px;max-width:56vw}",
147
149
  // ── 自管理:版本与更新(插件面板内提供在线更新/彻底卸载,市场无更新按钮) ──
@@ -408,6 +410,13 @@ window.__ModuleLoader__.load({
408
410
  // 导致 busy 护栏失效(重复请求)或误拦(该重试却跳过)。
409
411
  var akeyInFlight = { v: false };
410
412
  var devInFlight = { v: false };
413
+ /** 连接阶段轮询的在途标记(同上:必须放组件外,否则闭包过期会重复请求)。 */
414
+ var connInFlight = { v: false };
415
+ /** 最近一次连接阶段(供自调度定时器决定快/慢轮询节奏,避免依赖渲染闭包)。 */
416
+ var connPhaseRef = { v: "" };
417
+ /** 连接阶段轮询节奏:未连通时 2.5s(自动推进),已连接后退避到 15s;页面隐藏时完全停下。 */
418
+ var CONN_POLL_FAST_MS = 2500;
419
+ var CONN_POLL_SLOW_MS = 15000;
411
420
  function cancelAkeyRetry() { clearLater(akeyRetry.timer); akeyRetry.timer = null; }
412
421
  function cancelDevRetry() { clearLater(devRetry.timer); devRetry.timer = null; }
413
422
 
@@ -1122,6 +1131,8 @@ window.__ModuleLoader__.load({
1122
1131
  }
1123
1132
 
1124
1133
  // ── 面板主体(渲染于设置页 settings.section 栏目内) ─────────────────────
1134
+ // 匿名装机统计的「面板打开」只上报一次/每页(node 半还会每进程去重)。
1135
+ var panelOpenedSent = false;
1125
1136
  function RemoteControlSection(props) {
1126
1137
 
1127
1138
  var statusArr = useState(null); var st = statusArr[0]; var setSt = statusArr[1];
@@ -1182,6 +1193,16 @@ window.__ModuleLoader__.load({
1182
1193
  // ── 💬 交流群(「加入交流群」按钮 + 弹窗):同样放在全部既有字段之后 ──
1183
1194
  var communityArr = useState(null); var community = communityArr[0]; var setCommunity = communityArr[1]; // {qrcode,wechat};null=未加载
1184
1195
  var commOpenArr = useState(false); var commOpen = commOpenArr[0]; var setCommOpen = commOpenArr[1];
1196
+ // 【自动化】重启倒计时 / 是否已被用户取消(初值按本次 pending 事件的持久化标记,跨刷新生效)
1197
+ var restartCancelArr = useState(function () { return autoRestartCancelled((st && st.restart && st.restart.at) || 0); });
1198
+ var restartCancelled = restartCancelArr[0]; var setRestartCancelled = restartCancelArr[1];
1199
+ var restartCountArr = useState(null); var restartCountdown = restartCountArr[0]; var setRestartCountdown = restartCountArr[1];
1200
+ // ── 🔗 登录后自动闭环:连接阶段(运行环境 → bridge 进程 → 设备已在中继注册=online) ──
1201
+ // conn 来自 GET /dsh-remote/bridge-status(node 半在返回前会自动补装/拉起/退避重试),
1202
+ // 面板据它显示「正在准备运行环境…/正在启动 Bridge…/正在连接中继…/已连接 ✅」,
1203
+ // 并用短轮询自动推进到 online —— 用户不需要点按钮、也不需要刷新页面。
1204
+ var connArr = useState(null); var conn = connArr[0]; var setConn = connArr[1];
1205
+ var copiedDiagArr = useState(false); var copiedDiag = copiedDiagArr[0]; var setCopiedDiag = copiedDiagArr[1];
1185
1206
 
1186
1207
  var refresh = useCallback(function () {
1187
1208
  setBusy("status");
@@ -1222,6 +1243,16 @@ window.__ModuleLoader__.load({
1222
1243
  // 是否需要重启 DeepSeek harness(首次安装/在线更新后置顶提醒 + 底部常驻按钮)
1223
1244
  var restartInfo = (st && st.restart) || {};
1224
1245
  var restartPending = !!restartInfo.pending;
1246
+ // kind === "refresh" = 插件文件在运行中被改写(装完/更新完),只需刷新页面;
1247
+ // 其余情况(极为罕见)才提重启 dsh web。
1248
+ var isRefreshOnly = restartPending && restartInfo.kind === "refresh";
1249
+ // 连接阶段(node 半下发):轮询拿到的新鲜结果优先,其次用 /status 里带的那一份;
1250
+ // 旧版 host(响应里没有 connect 字段)→ 回退到原来的 service.running 文案(行为不变)。
1251
+ var connInfo = conn || (st && st.connect) || null;
1252
+ var connPhase = (connInfo && connInfo.phase) || "";
1253
+ var connOnline = connPhase === "online";
1254
+ var connText = (connInfo && connInfo.text) || "";
1255
+ var connError = connInfo && connInfo.error ? connInfo.error : null;
1225
1256
  // 账号标识(掩码手机号):登录成功/换账号时变化 → 触发上方的重取 effect(比布尔 loggedIn 更敏感)
1226
1257
  var phoneKey = (st && st.config && st.config.phone) || "";
1227
1258
  var launchdPid = st && st.service && st.service.launchd && st.service.launchd.pid;
@@ -1531,6 +1562,97 @@ window.__ModuleLoader__.load({
1531
1562
  };
1532
1563
  }, [view, mode, loggedIn, phoneKey, serviceRunning]);
1533
1564
 
1565
+ // ── 匿名装机统计(面板打开) ──────────────────────────────────────────────
1566
+ // 只上报「面板被打开过」这一个事件名(node 半每进程去重、DSH_REMOTE_TELEMETRY=0 直接丢弃);
1567
+ // 不含任何账号、手机号、设备指纹、会话内容或文件内容——隐私边界见 docs/telemetry.md。
1568
+ useEffect(function () {
1569
+ if (panelOpenedSent) return undefined;
1570
+ panelOpenedSent = true;
1571
+ post("/dsh-remote/telemetry/panel-opened", {}).catch(function () { /* 静默:统计失败绝不影响面板 */ });
1572
+ return undefined;
1573
+ }, []);
1574
+
1575
+ // ── 登录后自动闭环(0.6.4):连接阶段短轮询 ──────────────────────────────
1576
+ // 登录成功(含从别处已登录、被面板读到)后立刻开始推进连接,之后自调度:
1577
+ // 非 online → 2.5s 一次(阶段自动前进,用户零操作);online → 15s 一次(退避,只做保活/新设备发现)。
1578
+ // 页面隐藏时 pollConnect 直接返回(不空转),回前台由 visibilitychange 立即补一次。
1579
+ // 依赖里只有「视图/模式/登录态」:阶段变化不重建定时器,避免每次推进都多发一次请求。
1580
+ useEffect(function () {
1581
+ if (view !== "home" || mode !== "saas" || !loggedIn) return undefined;
1582
+ var stopped = false;
1583
+ var timer = null;
1584
+ var tick = function () {
1585
+ if (stopped) return;
1586
+ clearLater(timer);
1587
+ timer = later(function () {
1588
+ timer = null;
1589
+ tick();
1590
+ }, connPhaseRef.v === "online" ? CONN_POLL_SLOW_MS : CONN_POLL_FAST_MS);
1591
+ pollConnect();
1592
+ };
1593
+ var onVisible = function () { if (!document.hidden && !stopped) pollConnect(); };
1594
+ try { document.addEventListener("visibilitychange", onVisible); } catch (e) { /* 环境无 document */ }
1595
+ if (!document.hidden) { pollConnect(); tick(); }
1596
+ return function () {
1597
+ stopped = true;
1598
+ clearLater(timer);
1599
+ try { document.removeEventListener("visibilitychange", onVisible); } catch (e) { /* 忽略 */ }
1600
+ };
1601
+ }, [view, mode, loggedIn]);
1602
+
1603
+ /**
1604
+ * 连接阶段轮询:GET /dsh-remote/bridge-status(node 半在返回前自动补装运行环境 / 拉起 bridge /
1605
+ * 清理卡死标记,并自带退避),面板据此自动推进「准备运行环境 → 启动 Bridge → 连接中继 → 已连接」。
1606
+ * 关键点:
1607
+ * - document.hidden 时完全不发请求(后台标签页不空转);回前台由 visibilitychange 立即补一次;
1608
+ * - 阶段推进到 online 时自动重取二维码与已授权设备列表 —— 用户不用刷新页面就能扫码。
1609
+ */
1610
+ function pollConnect() {
1611
+ if (document.hidden) return;
1612
+ if (connInFlight.v) return;
1613
+ connInFlight.v = true;
1614
+ api("/dsh-remote/bridge-status").then(function (b) {
1615
+ if (!b || !b.ok || !b.connect) return;
1616
+ var prev = connPhaseRef.v;
1617
+ connPhaseRef.v = b.connect.phase || "";
1618
+ setConn(b.connect);
1619
+ if (b.connect.phase === "online") {
1620
+ // 已连接:立刻补一次二维码/设备列表(首次连上时 prev 不是 online → 无条件重取)
1621
+ if (prev !== "online") {
1622
+ akeyRetry.attempt = 0; devRetry.attempt = 0;
1623
+ loadAccessKey();
1624
+ loadDevices();
1625
+ } else if (devSessions !== null) {
1626
+ refreshDeviceList(); // 手机扫码后设备列表自动出现,无需刷新页面
1627
+ }
1628
+ }
1629
+ }).catch(function () { /* 轮询失败静默:下一轮自动重试 */ })
1630
+ .finally(function () { connInFlight.v = false; });
1631
+ }
1632
+
1633
+ /** 「重试」按钮:POST /dsh-remote/connect/retry(清退避 + 立刻再走一遍闭环),随后刷新阶段。 */
1634
+ var retryConnect = function () {
1635
+ setBusy("connect-retry");
1636
+ post("/dsh-remote/connect/retry").then(function (b) {
1637
+ if (b && b.connect) { connPhaseRef.v = b.connect.phase || ""; setConn(b.connect); }
1638
+ setMsg("ok", "已重新开始连接,正在自动重试…");
1639
+ }).catch(function (e) {
1640
+ setMsg("err", "重试失败:" + e.message);
1641
+ }).finally(function () { setBusy(""); });
1642
+ };
1643
+
1644
+ /** 「复制诊断信息」:版本 / 配置目录 / 阶段 / 最近错误 / 进程状态 / 日志路径(node 半已拼好)。 */
1645
+ var copyDiagnostics = function () {
1646
+ var text = (connInfo && connInfo.diagnostics) || "";
1647
+ if (!text) return;
1648
+ try {
1649
+ navigator.clipboard.writeText(text).then(function () {
1650
+ setCopiedDiag(true);
1651
+ later(function () { setCopiedDiag(false); }, 1500);
1652
+ });
1653
+ } catch (e) { /* 剪贴板不可用:文案里已给出日志路径,用户可自行查看 */ }
1654
+ };
1655
+
1534
1656
  function setMsg(kind, text) { setMessage({ kind: kind, text: text }); }
1535
1657
 
1536
1658
  // ---------- 登录(密码/短信) ----------
@@ -1588,7 +1710,10 @@ window.__ModuleLoader__.load({
1588
1710
  if (rpass.length < 8) { setMsg("err", "密码至少 8 位"); return; }
1589
1711
  if (rpass !== rpass2) { setMsg("err", "两次输入的密码不一致"); return; }
1590
1712
  setBusy("register");
1591
- var regPayload = { phone: rphone.trim(), sms_code: rsms.trim(), password: rpass };
1713
+ // reg_source:注册来源(增长口径)——面板内注册 = panel_register;
1714
+ // 手机端网页注册走 clients/dsh-web/native.html(= remoteweb_register)。
1715
+ // 节点半 /dsh-remote/register 全量透传 body,企业端白名单外的值会归一成 api_unknown。
1716
+ var regPayload = { phone: rphone.trim(), sms_code: rsms.trim(), password: rpass, reg_source: "panel_register" };
1592
1717
  var invite = rInvite.trim().toUpperCase();
1593
1718
  if (invite) regPayload.invite_code = invite;
1594
1719
  if (rcap) { regPayload.captcha_id = rcap.id; regPayload.captcha_answer = rcapTxt.trim(); }
@@ -1757,6 +1882,25 @@ window.__ModuleLoader__.load({
1757
1882
  });
1758
1883
  };
1759
1884
 
1885
+ /**
1886
+ * 【自动化】首次安装/在线更新后自动重启 DeepSeek harness —— 用户不需要点任何按钮。
1887
+ *
1888
+ * 为什么能自动:① 重启只是重启 dsh web(bridge 与手机端连接不受影响);② 重启后
1889
+ * waitHarnessBack 会轮询到它回来并**自动刷新页面**,面板与「连接中→已连接」流程继续跑完。
1890
+ * 安全阀:① 面板不可见(用户没在看)→ 不计时,回来再继续;② 15 秒倒计时内可一键取消,
1891
+ * 取消标记按本次 pending 事件(at 时间戳)持久化,同一事件不再自动重启(下次安装/更新会重新触发);
1892
+ * ③ 面板上有其他操作在跑(busy≠"")→ 暂停计时,避免打断用户正在做的事。
1893
+ */
1894
+ var AUTO_RESTART_DELAY_MS = 15000;
1895
+ var RESTART_CANCEL_KEY = "dsh-remote-auto-restart-cancelled";
1896
+
1897
+ function autoRestartCancelled(at) {
1898
+ try { return localStorage.getItem(RESTART_CANCEL_KEY) === String(at || ""); } catch (e) { return false; }
1899
+ }
1900
+ function markAutoRestartCancelled(at) {
1901
+ try { localStorage.setItem(RESTART_CANCEL_KEY, String(at || "")); } catch (e) { /* 忽略 */ }
1902
+ }
1903
+
1760
1904
  /** 轮询等待 harness 回来;观察到「断过又恢复」或等待足够久后自动刷新页面。 */
1761
1905
  var waitHarnessBack = function (tries, sawDown) {
1762
1906
  if (tries > 40) { setMsg("err", "重启后仍未就绪,请手动刷新页面或重启 DeepSeek harness。"); setBusy(""); return; }
@@ -1947,12 +2091,59 @@ window.__ModuleLoader__.load({
1947
2091
  var expMs = akey ? toMs(akey.expires_at) : 0;
1948
2092
  var remainMs = expMs ? expMs - nowTick : 0;
1949
2093
  var loggedInSaaS = !!(st && st.config && st.config.phone);
2094
+ // 状态行优先用连接阶段文案(node 半下发的 user-facing 文案):
2095
+ // 「正在准备运行环境(首次约 1~2 分钟)… / 正在启动 Bridge… / 正在连接中继… / 已连接 ✅ …」;
2096
+ // 旧版 host 没有 connect 字段 → 完全回退到原来的 running 文案(既有行为不变)。
1950
2097
  var statusTxt = st === null
1951
2098
  ? "查询中…"
1952
2099
  : !loggedInSaaS
1953
2100
  ? "请先登录(下方账号卡片)后启用远程访问"
1954
- : serviceRunning ? "已连接(可远程访问)" : "等待设备连接";
1955
- var dotCls = "dru-dot " + (loggedInSaaS && serviceRunning ? "dru-dot-on" : "dru-dot-off");
2101
+ : connInfo ? connText : serviceRunning ? "已连接(可远程访问)" : "等待设备连接";
2102
+ var dotOn = loggedInSaaS ? (connInfo ? connOnline : serviceRunning) : false;
2103
+ var dotCls = "dru-dot " + (dotOn ? "dru-dot-on" : "dru-dot-off");
2104
+
2105
+ /**
2106
+ * 连接阶段区块:把「环境 → bridge 进程 → 中继注册」的自动推进过程如实展示出来,
2107
+ * 面向非技术用户——非 online 阶段一律说明「正在自动进行,无需操作」;
2108
+ * 只有失败(error)才给可操作项:重试 / 复制诊断信息 / 日志路径,绝不出现死胡同。
2109
+ */
2110
+ function renderConnectBlock() {
2111
+ if (!loggedInSaaS || !connInfo) return null;
2112
+ var phase = connInfo.phase || "";
2113
+ if (phase === "online") {
2114
+ return h("div", { className: "dru-meta", style: { marginTop: 6 } },
2115
+ connInfo.registerSource === "account_api" || connInfo.registerSource === "account" || connInfo.registerSource === "state_account"
2116
+ ? "设备已登记到你的账号:手机端「设备列表」可以看到这台电脑。"
2117
+ : "设备已注册到中继:手机端扫码或打开链接即可进入这台电脑。");
2118
+ }
2119
+ var rows = [];
2120
+ if (phase === "error") {
2121
+ rows.push(h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 6 } },
2122
+ "⚠️ " + ((connError && connError.message) || connText)));
2123
+ if (connInfo.detail) rows.push(h("div", { className: "dru-hint", style: { marginTop: 4 } }, connInfo.detail));
2124
+ if (connInfo.nextRetryInMs > 0) {
2125
+ rows.push(h("div", { className: "dru-hint", style: { marginTop: 4 } },
2126
+ "已自动重试 " + (connInfo.attempts || 0) + " 次,约 " + Math.ceil(connInfo.nextRetryInMs / 1000) + " 秒后自动再试(也可以点下面按钮立刻重试)"));
2127
+ }
2128
+ rows.push(h("div", { className: "dru-actions", style: { marginTop: 8 } },
2129
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: retryConnect }, busy === "connect-retry" ? "重试中…" : "重试"),
2130
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: copyDiagnostics }, copiedDiag ? "已复制" : "复制诊断信息")));
2131
+ rows.push(h("div", { className: "dru-hint", style: { marginTop: 6 } },
2132
+ "查看日志:bridge " + (connInfo.logPath || "") + " ,安装 " + (connInfo.installLogPath || "") + "(把「复制诊断信息」的内容发给客服可加速定位)"));
2133
+ return h("div", null, rows);
2134
+ }
2135
+ // 非错误阶段:只解释「正在自动做什么」,并说明不需要任何操作
2136
+ if (connInfo.detail) rows.push(h("div", { className: "dru-hint", style: { marginTop: 6 } }, connInfo.detail));
2137
+ if (connInfo.installing || phase === "no_runtime") {
2138
+ rows.push(h("div", { className: "dru-hint", style: { marginTop: 4 } },
2139
+ "首次安装会自动下载并配置,期间请不要关闭 DeepSeek;装完会自动启动 bridge,无需任何操作。"));
2140
+ }
2141
+ if (connInfo.deviceId) rows.push(h("div", { className: "dru-meta" }, "设备 ID:" + connInfo.deviceId));
2142
+ rows.push(h("div", { className: "dru-actions", style: { marginTop: 8 } },
2143
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: retryConnect }, busy === "connect-retry" ? "立即重试中…" : "立即重试"),
2144
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: copyDiagnostics }, copiedDiag ? "已复制" : "复制诊断信息")));
2145
+ return h("div", null, rows);
2146
+ }
1956
2147
  // Phase-5:端到端加密(E2EE)状态行 —— 未登录/旧 host 未下发 e2ee 一律不渲染
1957
2148
  // (桌面宽屏与手机镜像共用同一面板:纯文字状态行、不弹层不打扰);启用=绿点绿字(🔒已启用),
1958
2149
  // 未启用=灰字 + 中性原因文案(回退普通 HTTPS 连接,不宣称“灰度等待”)。
@@ -1969,6 +2160,7 @@ window.__ModuleLoader__.load({
1969
2160
  h("span", { className: dotCls }),
1970
2161
  h("span", null, statusTxt)
1971
2162
  ),
2163
+ renderConnectBlock(),
1972
2164
  renderE2eeBadge(),
1973
2165
  akeyMsg
1974
2166
  ? h("div", { className: "dru-msg dru-msg-" + akeyMsg.kind, style: { marginTop: 8 } },
@@ -1999,7 +2191,9 @@ window.__ModuleLoader__.load({
1999
2191
  ),
2000
2192
  h("div", { className: "dru-hint", style: { marginTop: 4 } },
2001
2193
  !serviceRunning
2002
- ? "本机 bridge 未运行:先在下方「🖥 Bridge 服务」卡启动。"
2194
+ ? (connInfo && connPhase !== "online"
2195
+ ? "本机 bridge 正在自动准备中(" + connText + "):连上后这个链接/二维码即可使用,无需其他操作。"
2196
+ : "本机 bridge 未运行:先在下方「🖥 Bridge 服务」卡启动。")
2003
2197
  : "打开链接/扫码进入即登录态;同设备重复扫码只更新授权,不新增设备。")
2004
2198
  )
2005
2199
  )
@@ -2195,6 +2389,11 @@ window.__ModuleLoader__.load({
2195
2389
  h("span", null, st ? serviceStateText : "查询中…"),
2196
2390
  launchdPid ? h("span", { className: "dru-meta", style: { marginTop: 0 } }, "(pid=" + launchdPid + ")") : null
2197
2391
  ),
2392
+ // 自动连接进度(登录后由面板 2.5s 短轮询自动推进):让用户在 bridge 卡也能一眼看到
2393
+ // 「到底走到哪一步了」——进程在跑≠能用,注册到中继才算。
2394
+ connInfo && connPhase && connPhase !== "no_account"
2395
+ ? h("div", { className: "dru-hint", style: { marginTop: 6 } }, "自动连接进度:" + connText)
2396
+ : null,
2198
2397
  h("div", { className: "dru-actions", style: { marginTop: 10 } },
2199
2398
  !serviceRunning
2200
2399
  ? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: function () { toggleBridge(true); } }, busy === "start" ? "启动中…" : "启动 bridge")
@@ -2226,6 +2425,13 @@ window.__ModuleLoader__.load({
2226
2425
  h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🛠 电脑端一键安装:bridge 与「远程访问」面板一次到位——云端/自建切换、账号登录、bridge 启停、一次性扫码访问、已授权设备管理、意见反馈都在这里。"),
2227
2426
  h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表;服务端可配置流量配额。"),
2228
2427
  h("div", { className: "dru-hint" }, "🛡 端到端加密:手机↔电脑之间的消息内容用「你的账号密码派生密钥」端到端加密——密钥与密码不落服务端(仅存校验值),中继只可见路径/大小/时间(详见 README「安全与隐私」)。"),
2428
+ // 匿名装机统计(隐私披露):开源项目必须把「采什么 / 不采什么 / 怎么关」写在用户看得到的地方。
2429
+ // 只保留结论式的说明句:不再单列「隐私说明: README… · docs/telemetry.md」那一行文档链接
2430
+ // (面板里堆文档链接既占地方又不像产品文案;完整字段清单在仓库 docs/telemetry.md)。
2431
+ h("div", { className: "dru-hint", style: { marginTop: 6 } },
2432
+ "📊 匿名装机统计:只上报「装机/连接是否成功」这类事件(安装开始与失败原因、bridge 是否注册成功、是否首次远程打通)——"
2433
+ + "不含任何账号、手机号、会话或文件内容、主机名、路径、设备指纹与 IP;标识是本机随机 ID(重装即变)。"
2434
+ + "用环境变量 DSH_REMOTE_TELEMETRY=0 可完全关闭(不生成 ID、不发任何请求)。"),
2229
2435
  // 加入交流群(后台上传二维码后出现;点击弹出二维码大图便于扫码)
2230
2436
  community && community.qrcode
2231
2437
  ? h("div", { className: "dru-actions", style: { marginTop: 10 } },
@@ -2243,6 +2449,33 @@ window.__ModuleLoader__.load({
2243
2449
  );
2244
2450
  }
2245
2451
 
2452
+ // 【自动化】待重启时自动倒计时重启(见 autoRestartCancelled 说明);倒计时只在面板可见且无其他操作时推进。
2453
+ // ⚠️ isRefreshOnly(插件更新后只需刷新)时**不**自动重启:刷新是零风险动作,重启 dsh web 会打断
2454
+ // 用户正在进行的会话,不能替他做这个决定 —— 让他点「刷新页面」即可。
2455
+ useEffect(function () {
2456
+ if (!restartPending || isRefreshOnly || busy !== "" || restartCancelled) { setRestartCountdown(null); return undefined; }
2457
+ var at = restartInfo.at || 0;
2458
+ if (autoRestartCancelled(at)) { setRestartCountdown(null); return undefined; }
2459
+ var left = Math.round(AUTO_RESTART_DELAY_MS / 1000);
2460
+ setRestartCountdown(left);
2461
+ var iv = setInterval(function () {
2462
+ if (document.hidden) return; // 用户没在看 → 暂停计时
2463
+ left -= 1;
2464
+ if (left > 0) { setRestartCountdown(left); return; }
2465
+ clearInterval(iv);
2466
+ setRestartCountdown(0);
2467
+ restartHarness(); // 已有实现:重启 + 轮询等它回来 + 自动刷新页面
2468
+ }, 1000);
2469
+ return function () { clearInterval(iv); };
2470
+ }, [restartPending, isRefreshOnly, restartInfo.at, busy, restartCancelled]);
2471
+
2472
+ /** 取消本次自动重启(按事件持久化:同一 pending 事件不再自动重启)。 */
2473
+ var cancelAutoRestart = function () {
2474
+ markAutoRestartCancelled(restartInfo.at || 0);
2475
+ setRestartCancelled(true);
2476
+ setRestartCountdown(null);
2477
+ };
2478
+
2246
2479
  /** 交流群弹窗(复用满意度弹窗的 dru-popup 视觉;点遮罩/关闭即收起)。 */
2247
2480
  function renderCommunityModal() {
2248
2481
  if (!commOpen || !(community && community.qrcode)) return null;
@@ -2280,16 +2513,46 @@ window.__ModuleLoader__.load({
2280
2513
  h("span", { className: "dru-restart-alert-icon" }, "🔔"),
2281
2514
  h("div", { style: { flex: "1" } },
2282
2515
  h("div", { className: "dru-restart-alert-title" },
2283
- restartInfo.kind === "update" ? "在线更新已完成,需要重启 DeepSeek harness" : "首次安装需要重启 DeepSeek harness"),
2516
+ // 插件已改为 patch 热加载:装插件/在线更新都不再需要重启 harness,
2517
+ // 唯一要用户动一下的情形是"插件文件在运行中被改写"→ 刷新页面即可。
2518
+ isRefreshOnly ? "插件已更新,刷新页面即可生效" : "需要重启 dsh web 才能载入新插件"),
2284
2519
  h("div", { className: "dru-restart-alert-sub" },
2285
- "插件本体与「远程访问」面板是在 DeepSeek harness 启动时装载的,重启之后即可正常使用(账号、二维码、bridge 全部生效)。"),
2520
+ isRefreshOnly
2521
+ ? "刚装好/刚更新的插件已在磁盘上,浏览器里的面板还是旧版本。刷新一下这个页面即可(手机端连接与已登录状态都不受影响)。"
2522
+ : "当前运行中的 dsh web 里还没有载入这个插件。先刷新页面;如果刷新后仍然看不到面板,再重启一次 dsh web。"),
2523
+ // 【自动化】待重启时自动倒计时重启(用户零操作);倒计时期间可一键取消
2524
+ restartCountdown !== null && restartCountdown > 0
2525
+ ? h("div", { className: "dru-restart-auto", role: "status" },
2526
+ h("span", { className: "loading" }),
2527
+ h("span", null, "安装已完成,将在 " + restartCountdown + " 秒后自动重启 DeepSeek harness(页面会自动恢复,无需操作)"))
2528
+ : null,
2529
+ restartCountdown === 0
2530
+ ? h("div", { className: "dru-restart-auto", role: "status" }, h("span", { className: "loading" }), h("span", null, "正在自动重启…"))
2531
+ : null,
2532
+ restartCancelled && restartCountdown === null
2533
+ ? h("div", { className: "dru-restart-auto muted" }, "已取消自动重启 —— 你也可以随时点下面的按钮完成安装")
2534
+ : null,
2286
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,
2287
2543
  h("button", {
2288
2544
  type: "button",
2289
- className: "dru-btn dru-btn-primary",
2545
+ className: "dru-btn " + (isRefreshOnly ? "dru-btn-ghost" : "dru-btn-primary"),
2290
2546
  disabled: busy !== "",
2291
2547
  onClick: function () { restartHarness(); }
2292
- }, busy === "restart" ? "重启中…" : "重启 DeepSeek harness")
2548
+ }, busy === "restart" ? "重启中…" : (restartCountdown !== null && restartCountdown > 0 ? "立即重启(不用等)" : "重启 dsh web")),
2549
+ restartCountdown !== null && restartCountdown > 0
2550
+ ? h("button", {
2551
+ type: "button",
2552
+ className: "dru-btn dru-btn-ghost",
2553
+ onClick: cancelAutoRestart
2554
+ }, "取消自动重启")
2555
+ : null
2293
2556
  )
2294
2557
  )
2295
2558
  )
@@ -2298,13 +2561,13 @@ window.__ModuleLoader__.load({
2298
2561
  ),
2299
2562
  // 常驻入口最底部:「重启 DeepSeek harness」按钮始终可达(首次安装/更新/排查都用它)
2300
2563
  h("div", { className: "dru-restart-foot" },
2301
- h("div", { className: "dru-hint" }, "重启 DeepSeek harness:插件与面板在 harness 启动时装载,首次安装或在线更新后重启即可生效;仅重启 dsh web,不影响 bridge 与手机端连接。"),
2564
+ h("div", { className: "dru-hint" }, "重启 dsh web:仅在「刷新页面后仍看不到面板」时才需要。日常安装/更新插件已自动热加载,装完刷新页面即可;重启只影响 dsh web 本身,不影响 bridge 与手机端连接。"),
2302
2565
  h("button", {
2303
2566
  type: "button",
2304
2567
  className: "dru-btn " + (restartPending ? "dru-btn-primary" : "dru-btn-ghost"),
2305
2568
  disabled: busy !== "",
2306
2569
  onClick: function () { restartHarness(); }
2307
- }, busy === "restart" ? "重启中…" : "🔄 重启 DeepSeek harness")
2570
+ }, busy === "restart" ? "重启中…" : "🔄 重启 dsh web")
2308
2571
  ),
2309
2572
  renderCommunityModal()
2310
2573
  );