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

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
@@ -28,7 +28,13 @@ dsh-remote 是一个轻量的**隧道模式**远程控制方案:电脑端运
28
28
  └→ bridge (电脑端) → 127.0.0.1:3080 (dsh web)
29
29
  ```
30
30
 
31
- ## 0.6.2 速览(用户最关心的新能力)
31
+ ## 0.6.4 速览(首次安装不再卡)
32
+
33
+ - **登录后 bridge 自动连上,全程零刷新**:面板自动补运行环境 → 拉起 bridge → 显示「正在连接中继…」,连上后自动变成「已连接 ✅」并刷新二维码与设备列表;失败有可读原因、自动重试倒计时和「复制诊断信息」。
34
+ - **手机端自动等待电脑上线**:空设备列表不再让人干等或手动刷新,电脑一上线设备自己出现。
35
+ - **安装引导改版**:有插件市场入口就搜索 `dsh-remote` 安装(不用敲命令);没有就复制一条命令;两条路都收敛到「登录后稍等,电脑会自己出现」。
36
+
37
+ **0.6.2 起已有**
32
38
 
33
39
  - **插件市场安装即可用**:市场只装「面板插件」,桌面运行环境(bridge + 自启动)由插件在后台自动补齐;修掉了
34
40
  「装完 bridge 起不来且永远不自愈」的三处根因(运行环境缺失仍写自启动 → launchd 崩溃循环;崩溃循环被误判成
@@ -155,6 +155,45 @@ function clearBindError() {
155
155
  try { fs.rmSync(BIND_ERROR_FILE, { force: true }); } catch { /* 非关键 */ }
156
156
  }
157
157
 
158
+ /**
159
+ * bridge 运行状态文件:供 dsh web 插件面板判定「设备是否已在中继注册成功」(连接阶段 online)。
160
+ * 面板用它把「bridge 进程在跑」与「设备已注册、手机端真的能用」区分开——后者才是用户关心的状态,
161
+ * 只按进程存活判断会谎报可用。
162
+ * { device_id, started_at, account_bound_at, tunnel_registered_at, phase, last_error? }
163
+ * 每次进程启动都 reset 一次:上一轮进程的成功记录不能代表当前这一轮。
164
+ */
165
+ const BRIDGE_STATE_FILE = path.join(path.dirname(CONFIG_PATH), ".dsh-bridge-state.json");
166
+ function persistBridgeState(patch, opts = {}) {
167
+ try {
168
+ fs.mkdirSync(path.dirname(BRIDGE_STATE_FILE), { recursive: true });
169
+ let base = {};
170
+ if (!opts.reset) {
171
+ try { base = JSON.parse(fs.readFileSync(BRIDGE_STATE_FILE, "utf8")) || {}; } catch { base = {}; }
172
+ }
173
+ fs.writeFileSync(BRIDGE_STATE_FILE, JSON.stringify({ ...base, ...patch, at: Date.now() }, null, 2), { mode: 0o600 });
174
+ } catch { /* 非关键:面板侧还有日志/账号兜底判据 */ }
175
+ }
176
+
177
+ // ---------- 安装来源 / 版本(服务端设备行统计口径) ----------
178
+ // DSH_BRIDGE_INSTALL_SOURCE 由一键安装器(dsh-setup.mjs)注入 = npx;未设置表示这台电脑走的是
179
+ // 「插件市场装面板插件 + 插件自愈补装运行环境」那条路径 = plugin_market;白名单外一律 unknown。
180
+ const INSTALL_SOURCE_VALUES = new Set(["npx", "plugin_market"]);
181
+ function resolveInstallSource() {
182
+ const v = String(process.env.DSH_BRIDGE_INSTALL_SOURCE || "").trim();
183
+ if (INSTALL_SOURCE_VALUES.has(v)) return v;
184
+ return v ? "unknown" : "plugin_market";
185
+ }
186
+ /** 安装版本:安装器注入的包版本优先,其次本包 package.json 版本(读不到留空)。 */
187
+ function resolveInstallVersion() {
188
+ const v = String(process.env.DSH_BRIDGE_INSTALL_VERSION || "").trim();
189
+ if (v) return v;
190
+ try {
191
+ return JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")).version || "";
192
+ } catch { return ""; }
193
+ }
194
+ const INSTALL_SOURCE = resolveInstallSource();
195
+ const INSTALL_VERSION = resolveInstallVersion();
196
+
158
197
  // ---------- 稳定设备身份(.dsh-config.json) ----------
159
198
 
160
199
  function loadLocalConfig() {
@@ -921,12 +960,19 @@ async function registerDeviceInAccount(token) {
921
960
  device_id: DEVICE_ID,
922
961
  device_name: os.hostname() || "dsh-bridge",
923
962
  pub_key: pubKey,
924
- machine_fp: MACHINE_FP // v6 同机识别:服务端据此自动顶替旧设备(重装不再被设备数卡死)
963
+ machine_fp: MACHINE_FP, // v6 同机识别:服务端据此自动顶替旧设备(重装不再被设备数卡死)
964
+ // 安装口径(服务端存到设备行,供运营统计「市场安装/自愈补装」vs「安装器 npx」):
965
+ // install_source: npx(安装器注入) | plugin_market(插件拉起/自愈,默认) | unknown(白名单外)
966
+ install_source: INSTALL_SOURCE,
967
+ install_version: INSTALL_VERSION,
968
+ host_os: process.platform,
969
+ host_arch: process.arch
925
970
  })
926
971
  });
927
972
  const d = await r.json().catch(() => ({}));
928
973
  if (r.status === 201 || r.status === 200) {
929
974
  clearBindError();
975
+ persistBridgeState({ device_id: DEVICE_ID, account_bound_at: Date.now(), phase: "bound" });
930
976
  console.log(`[bridge] ✅ 设备已登记到账号: ${DEVICE_ID}`);
931
977
  return;
932
978
  }
@@ -961,6 +1007,7 @@ function connectTunnel(token) {
961
1007
  ws.on("open", () => {
962
1008
  tunnelRetry = 0;
963
1009
  console.log(`[bridge] 隧道已连 ${endpoint},注册 ${DEVICE_ID}...`);
1010
+ persistBridgeState({ device_id: DEVICE_ID, phase: "connecting" }); // 新一轮连接:上一轮的成功记录已过期
964
1011
  try {
965
1012
  // caps:bridge E2EE 能力上报(§6.2/§7.1;router 纯透传)。enabled=false → 空数组(明文回退)
966
1013
  ws.send(JSON.stringify({ type: "tunnel-register", deviceId: DEVICE_ID, token, name: os.hostname() || "dsh-bridge", caps: e2ee.caps() }));
@@ -981,10 +1028,13 @@ function connectTunnel(token) {
981
1028
  try {
982
1029
  receive(raw, (frame) => {
983
1030
  if (frame?.type === "tunnel-register-ok") {
1031
+ // 中继注册成功 = 手机端此刻真的能进这台电脑:面板据此把阶段推进到 online(已连接 ✅)
1032
+ persistBridgeState({ device_id: DEVICE_ID, tunnel_registered_at: Date.now(), phase: "online", last_error: null });
984
1033
  console.log(`[bridge] ✅ router 注册成功: ${DEVICE_ID},等待手机访问 /remote/${DEVICE_ID}/`);
985
1034
  return;
986
1035
  }
987
1036
  if (frame?.type === "tunnel-register-err") {
1037
+ persistBridgeState({ phase: "error", last_error: { code: frame.code || "register_err", message: frame.message || "router 拒绝注册" } });
988
1038
  console.error(`[bridge] router 拒绝注册: ${frame.code} ${frame.message || ""}`);
989
1039
  return;
990
1040
  }
@@ -1037,6 +1087,8 @@ async function runTunnel() {
1037
1087
  console.error("[bridge] 隧道模式需要账号认证:请设 DSH_BRIDGE_TOKEN,或 DSH_BRIDGE_PHONE+DSH_BRIDGE_PASSWORD");
1038
1088
  process.exit(1);
1039
1089
  }
1090
+ // 新进程开始:重置状态文件(上一轮的成功记录不能代表这一轮,面板据此判定 online)
1091
+ persistBridgeState({ device_id: DEVICE_ID, started_at: Date.now(), phase: "connecting", last_error: null }, { reset: true });
1040
1092
  await initE2ee(token);
1041
1093
  await registerDeviceInAccount(token);
1042
1094
  connectTunnel(token);
package/dsh-setup.mjs CHANGED
@@ -131,6 +131,16 @@ function preferredNode() {
131
131
  }
132
132
  const NODE_BIN = preferredNode();
133
133
 
134
+ // ---------- 安装口径(随 bridge 环境变量注入;服务端存到设备行,供运营统计) ----------
135
+ /**
136
+ * install_source:本文件即「一键安装器」→ 默认 npx(用户自己跑 `npx @mrrisega/dsh-remote` 的那条路径)。
137
+ * 插件市场那条路径由插件半自愈补装:插件 spawn 本安装器时显式传入
138
+ * DSH_BRIDGE_INSTALL_SOURCE=plugin_market,这里原样透传(外部注入 > 硬编码默认)。
139
+ * install_version:优先取外部注入(插件/在线更新器),否则取本包 package.json 版本。
140
+ */
141
+ const INSTALL_SOURCE = String(process.env.DSH_BRIDGE_INSTALL_SOURCE || "").trim() || "npx";
142
+ const INSTALL_VERSION = String(process.env.DSH_BRIDGE_INSTALL_VERSION || "").trim() || pkgVersion();
143
+
134
144
  // ---------- 配置读写 ----------
135
145
  function loadConfig() {
136
146
  try { return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8")); }
@@ -206,7 +216,7 @@ function writeAutostartFile() {
206
216
  <key>KeepAlive</key><true/>
207
217
  <key>StandardOutPath</key><string>${path.join(CONFIG_DIR, ".dsh-bridge.log")}</string>
208
218
  <key>StandardErrorPath</key><string>${path.join(CONFIG_DIR, ".dsh-bridge.log")}</string>
209
- <key>EnvironmentVariables</key><dict><key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string></dict>
219
+ <key>EnvironmentVariables</key><dict><key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string><key>DSH_BRIDGE_INSTALL_SOURCE</key><string>${INSTALL_SOURCE}</string><key>DSH_BRIDGE_INSTALL_VERSION</key><string>${INSTALL_VERSION}</string></dict>
210
220
  </dict></plist>`;
211
221
  fs.writeFileSync(plistPath, plist);
212
222
  console.log(`✅ 已创建自启动服务: ${plistPath}`);
@@ -215,7 +225,7 @@ function writeAutostartFile() {
215
225
  if (process.platform === "linux") {
216
226
  const dir = path.join(os.homedir(), ".config/systemd/user");
217
227
  fs.mkdirSync(dir, { recursive: true });
218
- const unit = `[Unit]\nDescription=dsh-remote bridge (auto-starts with dsh web)\n\n[Service]\nExecStart=${runCmd}\nRestart=on-failure\nRestartSec=5\nEnvironment=PATH=/usr/local/bin:/usr/bin:/bin\n\n[Install]\nWantedBy=default.target\n`;
228
+ const unit = `[Unit]\nDescription=dsh-remote bridge (auto-starts with dsh web)\n\n[Service]\nExecStart=${runCmd}\nRestart=on-failure\nRestartSec=5\nEnvironment=PATH=/usr/local/bin:/usr/bin:/bin\nEnvironment=DSH_BRIDGE_INSTALL_SOURCE=${INSTALL_SOURCE}\nEnvironment=DSH_BRIDGE_INSTALL_VERSION=${INSTALL_VERSION}\n\n[Install]\nWantedBy=default.target\n`;
219
229
  const unitPath = autostartFilePath();
220
230
  fs.writeFileSync(unitPath, unit);
221
231
  console.log(`✅ 已创建自启动服务: ${unitPath}`);
@@ -350,6 +360,9 @@ async function runBridge() {
350
360
  ...process.env,
351
361
  DSH_BRIDGE_CONFIG: CONFIG_PATH,
352
362
  DSH_BRIDGE_TUNNEL_URL: cfg.tunnel_url,
363
+ // 安装口径透传给 bridge(bridge 把它随设备登记请求一起上报给账号 API)
364
+ DSH_BRIDGE_INSTALL_SOURCE: INSTALL_SOURCE,
365
+ DSH_BRIDGE_INSTALL_VERSION: INSTALL_VERSION,
353
366
  ...(saas ? { DSH_BRIDGE_PHONE: (cfg.phone || cfg.email || ""), DSH_BRIDGE_PASSWORD: cfg.password, DSH_BRIDGE_API: apiUrl, DSH_BRIDGE_SECRET: cfg.bridge_secret || "" } : {}),
354
367
  ...(local ? { DSH_BRIDGE_LOCAL_KEY: cfg.local_key } : {})
355
368
  };
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.1",
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",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "scripts": {
51
51
  "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",
52
+ "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
53
  "_comment:test:plugin": "DSH_RELAY_SKIP_SERVICE=1 = 测试隔离:插件加载时不做真实系统操作(npx 补装 / launchctl / 重启 harness);需要真实分支的用例自行 unset(见 account-switch / runtime-provision / harness-restart 用例注释)",
54
54
  "test:plugin": "DSH_RELAY_SKIP_SERVICE=1 node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
55
55
  "test:bridge": "node --test clients/dsh-remote/test/*.test.mjs",
@@ -408,6 +408,13 @@ window.__ModuleLoader__.load({
408
408
  // 导致 busy 护栏失效(重复请求)或误拦(该重试却跳过)。
409
409
  var akeyInFlight = { v: false };
410
410
  var devInFlight = { v: false };
411
+ /** 连接阶段轮询的在途标记(同上:必须放组件外,否则闭包过期会重复请求)。 */
412
+ var connInFlight = { v: false };
413
+ /** 最近一次连接阶段(供自调度定时器决定快/慢轮询节奏,避免依赖渲染闭包)。 */
414
+ var connPhaseRef = { v: "" };
415
+ /** 连接阶段轮询节奏:未连通时 2.5s(自动推进),已连接后退避到 15s;页面隐藏时完全停下。 */
416
+ var CONN_POLL_FAST_MS = 2500;
417
+ var CONN_POLL_SLOW_MS = 15000;
411
418
  function cancelAkeyRetry() { clearLater(akeyRetry.timer); akeyRetry.timer = null; }
412
419
  function cancelDevRetry() { clearLater(devRetry.timer); devRetry.timer = null; }
413
420
 
@@ -1182,6 +1189,12 @@ window.__ModuleLoader__.load({
1182
1189
  // ── 💬 交流群(「加入交流群」按钮 + 弹窗):同样放在全部既有字段之后 ──
1183
1190
  var communityArr = useState(null); var community = communityArr[0]; var setCommunity = communityArr[1]; // {qrcode,wechat};null=未加载
1184
1191
  var commOpenArr = useState(false); var commOpen = commOpenArr[0]; var setCommOpen = commOpenArr[1];
1192
+ // ── 🔗 登录后自动闭环:连接阶段(运行环境 → bridge 进程 → 设备已在中继注册=online) ──
1193
+ // conn 来自 GET /dsh-remote/bridge-status(node 半在返回前会自动补装/拉起/退避重试),
1194
+ // 面板据它显示「正在准备运行环境…/正在启动 Bridge…/正在连接中继…/已连接 ✅」,
1195
+ // 并用短轮询自动推进到 online —— 用户不需要点按钮、也不需要刷新页面。
1196
+ var connArr = useState(null); var conn = connArr[0]; var setConn = connArr[1];
1197
+ var copiedDiagArr = useState(false); var copiedDiag = copiedDiagArr[0]; var setCopiedDiag = copiedDiagArr[1];
1185
1198
 
1186
1199
  var refresh = useCallback(function () {
1187
1200
  setBusy("status");
@@ -1222,6 +1235,13 @@ window.__ModuleLoader__.load({
1222
1235
  // 是否需要重启 DeepSeek harness(首次安装/在线更新后置顶提醒 + 底部常驻按钮)
1223
1236
  var restartInfo = (st && st.restart) || {};
1224
1237
  var restartPending = !!restartInfo.pending;
1238
+ // 连接阶段(node 半下发):轮询拿到的新鲜结果优先,其次用 /status 里带的那一份;
1239
+ // 旧版 host(响应里没有 connect 字段)→ 回退到原来的 service.running 文案(行为不变)。
1240
+ var connInfo = conn || (st && st.connect) || null;
1241
+ var connPhase = (connInfo && connInfo.phase) || "";
1242
+ var connOnline = connPhase === "online";
1243
+ var connText = (connInfo && connInfo.text) || "";
1244
+ var connError = connInfo && connInfo.error ? connInfo.error : null;
1225
1245
  // 账号标识(掩码手机号):登录成功/换账号时变化 → 触发上方的重取 effect(比布尔 loggedIn 更敏感)
1226
1246
  var phoneKey = (st && st.config && st.config.phone) || "";
1227
1247
  var launchdPid = st && st.service && st.service.launchd && st.service.launchd.pid;
@@ -1531,6 +1551,87 @@ window.__ModuleLoader__.load({
1531
1551
  };
1532
1552
  }, [view, mode, loggedIn, phoneKey, serviceRunning]);
1533
1553
 
1554
+ // ── 登录后自动闭环(0.6.4):连接阶段短轮询 ──────────────────────────────
1555
+ // 登录成功(含从别处已登录、被面板读到)后立刻开始推进连接,之后自调度:
1556
+ // 非 online → 2.5s 一次(阶段自动前进,用户零操作);online → 15s 一次(退避,只做保活/新设备发现)。
1557
+ // 页面隐藏时 pollConnect 直接返回(不空转),回前台由 visibilitychange 立即补一次。
1558
+ // 依赖里只有「视图/模式/登录态」:阶段变化不重建定时器,避免每次推进都多发一次请求。
1559
+ useEffect(function () {
1560
+ if (view !== "home" || mode !== "saas" || !loggedIn) return undefined;
1561
+ var stopped = false;
1562
+ var timer = null;
1563
+ var tick = function () {
1564
+ if (stopped) return;
1565
+ clearLater(timer);
1566
+ timer = later(function () {
1567
+ timer = null;
1568
+ tick();
1569
+ }, connPhaseRef.v === "online" ? CONN_POLL_SLOW_MS : CONN_POLL_FAST_MS);
1570
+ pollConnect();
1571
+ };
1572
+ var onVisible = function () { if (!document.hidden && !stopped) pollConnect(); };
1573
+ try { document.addEventListener("visibilitychange", onVisible); } catch (e) { /* 环境无 document */ }
1574
+ if (!document.hidden) { pollConnect(); tick(); }
1575
+ return function () {
1576
+ stopped = true;
1577
+ clearLater(timer);
1578
+ try { document.removeEventListener("visibilitychange", onVisible); } catch (e) { /* 忽略 */ }
1579
+ };
1580
+ }, [view, mode, loggedIn]);
1581
+
1582
+ /**
1583
+ * 连接阶段轮询:GET /dsh-remote/bridge-status(node 半在返回前自动补装运行环境 / 拉起 bridge /
1584
+ * 清理卡死标记,并自带退避),面板据此自动推进「准备运行环境 → 启动 Bridge → 连接中继 → 已连接」。
1585
+ * 关键点:
1586
+ * - document.hidden 时完全不发请求(后台标签页不空转);回前台由 visibilitychange 立即补一次;
1587
+ * - 阶段推进到 online 时自动重取二维码与已授权设备列表 —— 用户不用刷新页面就能扫码。
1588
+ */
1589
+ function pollConnect() {
1590
+ if (document.hidden) return;
1591
+ if (connInFlight.v) return;
1592
+ connInFlight.v = true;
1593
+ api("/dsh-remote/bridge-status").then(function (b) {
1594
+ if (!b || !b.ok || !b.connect) return;
1595
+ var prev = connPhaseRef.v;
1596
+ connPhaseRef.v = b.connect.phase || "";
1597
+ setConn(b.connect);
1598
+ if (b.connect.phase === "online") {
1599
+ // 已连接:立刻补一次二维码/设备列表(首次连上时 prev 不是 online → 无条件重取)
1600
+ if (prev !== "online") {
1601
+ akeyRetry.attempt = 0; devRetry.attempt = 0;
1602
+ loadAccessKey();
1603
+ loadDevices();
1604
+ } else if (devSessions !== null) {
1605
+ refreshDeviceList(); // 手机扫码后设备列表自动出现,无需刷新页面
1606
+ }
1607
+ }
1608
+ }).catch(function () { /* 轮询失败静默:下一轮自动重试 */ })
1609
+ .finally(function () { connInFlight.v = false; });
1610
+ }
1611
+
1612
+ /** 「重试」按钮:POST /dsh-remote/connect/retry(清退避 + 立刻再走一遍闭环),随后刷新阶段。 */
1613
+ var retryConnect = function () {
1614
+ setBusy("connect-retry");
1615
+ post("/dsh-remote/connect/retry").then(function (b) {
1616
+ if (b && b.connect) { connPhaseRef.v = b.connect.phase || ""; setConn(b.connect); }
1617
+ setMsg("ok", "已重新开始连接,正在自动重试…");
1618
+ }).catch(function (e) {
1619
+ setMsg("err", "重试失败:" + e.message);
1620
+ }).finally(function () { setBusy(""); });
1621
+ };
1622
+
1623
+ /** 「复制诊断信息」:版本 / 配置目录 / 阶段 / 最近错误 / 进程状态 / 日志路径(node 半已拼好)。 */
1624
+ var copyDiagnostics = function () {
1625
+ var text = (connInfo && connInfo.diagnostics) || "";
1626
+ if (!text) return;
1627
+ try {
1628
+ navigator.clipboard.writeText(text).then(function () {
1629
+ setCopiedDiag(true);
1630
+ later(function () { setCopiedDiag(false); }, 1500);
1631
+ });
1632
+ } catch (e) { /* 剪贴板不可用:文案里已给出日志路径,用户可自行查看 */ }
1633
+ };
1634
+
1534
1635
  function setMsg(kind, text) { setMessage({ kind: kind, text: text }); }
1535
1636
 
1536
1637
  // ---------- 登录(密码/短信) ----------
@@ -1588,7 +1689,10 @@ window.__ModuleLoader__.load({
1588
1689
  if (rpass.length < 8) { setMsg("err", "密码至少 8 位"); return; }
1589
1690
  if (rpass !== rpass2) { setMsg("err", "两次输入的密码不一致"); return; }
1590
1691
  setBusy("register");
1591
- var regPayload = { phone: rphone.trim(), sms_code: rsms.trim(), password: rpass };
1692
+ // reg_source:注册来源(增长口径)——面板内注册 = panel_register;
1693
+ // 手机端网页注册走 clients/dsh-web/native.html(= remoteweb_register)。
1694
+ // 节点半 /dsh-remote/register 全量透传 body,企业端白名单外的值会归一成 api_unknown。
1695
+ var regPayload = { phone: rphone.trim(), sms_code: rsms.trim(), password: rpass, reg_source: "panel_register" };
1592
1696
  var invite = rInvite.trim().toUpperCase();
1593
1697
  if (invite) regPayload.invite_code = invite;
1594
1698
  if (rcap) { regPayload.captcha_id = rcap.id; regPayload.captcha_answer = rcapTxt.trim(); }
@@ -1947,12 +2051,59 @@ window.__ModuleLoader__.load({
1947
2051
  var expMs = akey ? toMs(akey.expires_at) : 0;
1948
2052
  var remainMs = expMs ? expMs - nowTick : 0;
1949
2053
  var loggedInSaaS = !!(st && st.config && st.config.phone);
2054
+ // 状态行优先用连接阶段文案(node 半下发的 user-facing 文案):
2055
+ // 「正在准备运行环境(首次约 1~2 分钟)… / 正在启动 Bridge… / 正在连接中继… / 已连接 ✅ …」;
2056
+ // 旧版 host 没有 connect 字段 → 完全回退到原来的 running 文案(既有行为不变)。
1950
2057
  var statusTxt = st === null
1951
2058
  ? "查询中…"
1952
2059
  : !loggedInSaaS
1953
2060
  ? "请先登录(下方账号卡片)后启用远程访问"
1954
- : serviceRunning ? "已连接(可远程访问)" : "等待设备连接";
1955
- var dotCls = "dru-dot " + (loggedInSaaS && serviceRunning ? "dru-dot-on" : "dru-dot-off");
2061
+ : connInfo ? connText : serviceRunning ? "已连接(可远程访问)" : "等待设备连接";
2062
+ var dotOn = loggedInSaaS ? (connInfo ? connOnline : serviceRunning) : false;
2063
+ var dotCls = "dru-dot " + (dotOn ? "dru-dot-on" : "dru-dot-off");
2064
+
2065
+ /**
2066
+ * 连接阶段区块:把「环境 → bridge 进程 → 中继注册」的自动推进过程如实展示出来,
2067
+ * 面向非技术用户——非 online 阶段一律说明「正在自动进行,无需操作」;
2068
+ * 只有失败(error)才给可操作项:重试 / 复制诊断信息 / 日志路径,绝不出现死胡同。
2069
+ */
2070
+ function renderConnectBlock() {
2071
+ if (!loggedInSaaS || !connInfo) return null;
2072
+ var phase = connInfo.phase || "";
2073
+ if (phase === "online") {
2074
+ return h("div", { className: "dru-meta", style: { marginTop: 6 } },
2075
+ connInfo.registerSource === "account_api" || connInfo.registerSource === "account" || connInfo.registerSource === "state_account"
2076
+ ? "设备已登记到你的账号:手机端「设备列表」可以看到这台电脑。"
2077
+ : "设备已注册到中继:手机端扫码或打开链接即可进入这台电脑。");
2078
+ }
2079
+ var rows = [];
2080
+ if (phase === "error") {
2081
+ rows.push(h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 6 } },
2082
+ "⚠️ " + ((connError && connError.message) || connText)));
2083
+ if (connInfo.detail) rows.push(h("div", { className: "dru-hint", style: { marginTop: 4 } }, connInfo.detail));
2084
+ if (connInfo.nextRetryInMs > 0) {
2085
+ rows.push(h("div", { className: "dru-hint", style: { marginTop: 4 } },
2086
+ "已自动重试 " + (connInfo.attempts || 0) + " 次,约 " + Math.ceil(connInfo.nextRetryInMs / 1000) + " 秒后自动再试(也可以点下面按钮立刻重试)"));
2087
+ }
2088
+ rows.push(h("div", { className: "dru-actions", style: { marginTop: 8 } },
2089
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: retryConnect }, busy === "connect-retry" ? "重试中…" : "重试"),
2090
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: copyDiagnostics }, copiedDiag ? "已复制" : "复制诊断信息")));
2091
+ rows.push(h("div", { className: "dru-hint", style: { marginTop: 6 } },
2092
+ "查看日志:bridge " + (connInfo.logPath || "") + " ,安装 " + (connInfo.installLogPath || "") + "(把「复制诊断信息」的内容发给客服可加速定位)"));
2093
+ return h("div", null, rows);
2094
+ }
2095
+ // 非错误阶段:只解释「正在自动做什么」,并说明不需要任何操作
2096
+ if (connInfo.detail) rows.push(h("div", { className: "dru-hint", style: { marginTop: 6 } }, connInfo.detail));
2097
+ if (connInfo.installing || phase === "no_runtime") {
2098
+ rows.push(h("div", { className: "dru-hint", style: { marginTop: 4 } },
2099
+ "首次安装会自动下载并配置,期间请不要关闭 DeepSeek;装完会自动启动 bridge,无需任何操作。"));
2100
+ }
2101
+ if (connInfo.deviceId) rows.push(h("div", { className: "dru-meta" }, "设备 ID:" + connInfo.deviceId));
2102
+ rows.push(h("div", { className: "dru-actions", style: { marginTop: 8 } },
2103
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: retryConnect }, busy === "connect-retry" ? "立即重试中…" : "立即重试"),
2104
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: copyDiagnostics }, copiedDiag ? "已复制" : "复制诊断信息")));
2105
+ return h("div", null, rows);
2106
+ }
1956
2107
  // Phase-5:端到端加密(E2EE)状态行 —— 未登录/旧 host 未下发 e2ee 一律不渲染
1957
2108
  // (桌面宽屏与手机镜像共用同一面板:纯文字状态行、不弹层不打扰);启用=绿点绿字(🔒已启用),
1958
2109
  // 未启用=灰字 + 中性原因文案(回退普通 HTTPS 连接,不宣称“灰度等待”)。
@@ -1969,6 +2120,7 @@ window.__ModuleLoader__.load({
1969
2120
  h("span", { className: dotCls }),
1970
2121
  h("span", null, statusTxt)
1971
2122
  ),
2123
+ renderConnectBlock(),
1972
2124
  renderE2eeBadge(),
1973
2125
  akeyMsg
1974
2126
  ? h("div", { className: "dru-msg dru-msg-" + akeyMsg.kind, style: { marginTop: 8 } },
@@ -1999,7 +2151,9 @@ window.__ModuleLoader__.load({
1999
2151
  ),
2000
2152
  h("div", { className: "dru-hint", style: { marginTop: 4 } },
2001
2153
  !serviceRunning
2002
- ? "本机 bridge 未运行:先在下方「🖥 Bridge 服务」卡启动。"
2154
+ ? (connInfo && connPhase !== "online"
2155
+ ? "本机 bridge 正在自动准备中(" + connText + "):连上后这个链接/二维码即可使用,无需其他操作。"
2156
+ : "本机 bridge 未运行:先在下方「🖥 Bridge 服务」卡启动。")
2003
2157
  : "打开链接/扫码进入即登录态;同设备重复扫码只更新授权,不新增设备。")
2004
2158
  )
2005
2159
  )
@@ -2195,6 +2349,11 @@ window.__ModuleLoader__.load({
2195
2349
  h("span", null, st ? serviceStateText : "查询中…"),
2196
2350
  launchdPid ? h("span", { className: "dru-meta", style: { marginTop: 0 } }, "(pid=" + launchdPid + ")") : null
2197
2351
  ),
2352
+ // 自动连接进度(登录后由面板 2.5s 短轮询自动推进):让用户在 bridge 卡也能一眼看到
2353
+ // 「到底走到哪一步了」——进程在跑≠能用,注册到中继才算。
2354
+ connInfo && connPhase && connPhase !== "no_account"
2355
+ ? h("div", { className: "dru-hint", style: { marginTop: 6 } }, "自动连接进度:" + connText)
2356
+ : null,
2198
2357
  h("div", { className: "dru-actions", style: { marginTop: 10 } },
2199
2358
  !serviceRunning
2200
2359
  ? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: function () { toggleBridge(true); } }, busy === "start" ? "启动中…" : "启动 bridge")