dsh-remote-web 0.7.6-beta.6 → 0.7.6-beta.8

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 CHANGED
@@ -897,7 +897,10 @@ window.__ModuleLoader__.load({
897
897
  for (var i = 0; i < els.length; i++) {
898
898
  var el = els[i];
899
899
  if (!el || el.id === NAV_ENTRY_ID) continue; // 排除注入按钮自身(防递归)
900
- if (!navCellLike(el)) continue;
900
+ // 2026-10-25:新宿主把设置面板左栏的 navCell class 换掉了 → 只认 navCellLike 会漏掉
901
+ // 「远程访问」栏目,导致入口点击静默失败(用户实测:只能从更多→设置里手动点)。
902
+ // 在**设置弹层内**时放宽为「可见、文案精确匹配」,不再依赖 class 名。
903
+ if (!navCellLike(el) && !(insideDialog === true && navInDialog(el))) continue;
901
904
  if (NAV_BAD_RE.test(String(el.className || ""))) continue;
902
905
  var inDlg = navInDialog(el);
903
906
  if (insideDialog === true && !inDlg) continue;
@@ -928,6 +931,9 @@ window.__ModuleLoader__.load({
928
931
  if (!el.getBoundingClientRect || el.getBoundingClientRect().width === 0) continue;
929
932
  var t = String(el.textContent || "");
930
933
  if (t.indexOf("通用设置") !== -1 || t.indexOf("General") !== -1) return true;
934
+ // 2026-10-25:新版桌面端设置面板里是「远程访问 / 通用设置…」;若宿主把「通用设置」
935
+ // 改成了别的文案,只看它就会误判「设置没打开」→ 入口点击后一路走兜底/超时。
936
+ if (t.indexOf("远程访问") !== -1) return true;
931
937
  }
932
938
  } catch (e) { /* 忽略 */ }
933
939
  return false;
@@ -957,6 +963,15 @@ window.__ModuleLoader__.load({
957
963
  if (!el || el.id === NAV_ENTRY_ID) continue;
958
964
  var aria = String((el.getAttribute && el.getAttribute("aria-label")) || "");
959
965
  if (/账号菜单|Account menu/i.test(aria)) return el;
966
+ // 2026-10-25(用户反馈「只能从更多→设置里打开」):新宿主把设置收在**更多**菜单里,
967
+ // 这里补充「更多」形态的候选(aria-label / title / 文本 / ⋯ 字形)。
968
+ var title = String((el.getAttribute && (el.getAttribute("title") || "")) || "");
969
+ var txt = String((el.textContent || "")).replace(/\s+/g, "");
970
+ if (/更多|More|菜单|Menu/i.test(aria + " " + title)) {
971
+ var rMore = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
972
+ if (!rMore || (rMore.width > 0 && rMore.height > 0)) return el;
973
+ }
974
+ if (/^(更多|More|⋯|\.\.\.)$/i.test(txt) && el.getAttribute && el.getAttribute("aria-haspopup")) return el;
960
975
  var r = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
961
976
  if (!best && r && r.width > 0 && r.left < 360 && r.top > 300
962
977
  && String((el.getAttribute && el.getAttribute("aria-haspopup")) || "") === "menu") best = el;
@@ -1118,7 +1133,13 @@ window.__ModuleLoader__.load({
1118
1133
  try { e.preventDefault(); } catch (err) { /* 忽略 */ }
1119
1134
  setClicked(true);
1120
1135
  dotMarkSeen(); // 用过入口 = 已了解位置 → 收起首次红点
1121
- if (remoteNavOpen) remoteNavOpen();
1136
+ // 2026-10-25(用户实测「点不动、只能从更多→设置里打开」):
1137
+ // 不再依赖 remoteNavOpen 的赋值时序/是否被调用过,直接走内部打开链;
1138
+ // 打开链内部自带兜底:侧栏候选 → 官方快捷键 → 更多/账号菜单 → 栏目点击。
1139
+ try {
1140
+ if (typeof remoteNavOpen === "function") remoteNavOpen();
1141
+ else openRemoteSettings();
1142
+ } catch (errOpen) { /* 绝不因打开失败抛到宿主 */ }
1122
1143
  }
1123
1144
  }, [
1124
1145
  h("span", { className: "ico", key: "ico" }, "📱"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-web",
3
- "version": "0.7.6-beta.6",
3
+ "version": "0.7.6-beta.8",
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",
@@ -558,6 +558,19 @@ let upstreamRefreshInflight = null;
558
558
  * 重新解析上游地址(带身份校验;显式 DSH_BRIDGE_UPSTREAM 时是恒等操作)。
559
559
  * 上游端口变了(Desktop 换端口 / dsh web 重启到别的端口)时,靠它自愈而不必重启 bridge。
560
560
  */
561
+ /** 轻量探测上游是否活着(refreshUpstream 用;4xx/5xx 也算活着)。 */
562
+ async function upstreamReachableForRefresh(url, timeoutMs = 800) {
563
+ try {
564
+ const ac = new AbortController();
565
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
566
+ await fetch(String(url).replace(/\/+$/, "") + "/", { signal: ac.signal });
567
+ clearTimeout(timer);
568
+ return true;
569
+ } catch {
570
+ return false;
571
+ }
572
+ }
573
+
561
574
  async function refreshUpstream(reason = "") {
562
575
  if (upstreamRefreshInflight) return upstreamRefreshInflight;
563
576
  const now = Date.now();
@@ -565,7 +578,22 @@ async function refreshUpstream(reason = "") {
565
578
  upstreamRefreshedAt = now;
566
579
  upstreamRefreshInflight = (async () => {
567
580
  try {
568
- const found = await discoverUpstream({ relayDir: RELAY_DIR });
581
+ let found = null;
582
+ // 🔴 2026-10-25(设备掉线实测):升级前装的 launchd plist 可能带着安装时的旧端口,
583
+ // 旧版 discoverUpstream 对「显式 env」**不探测直接采信** → 桥永久连旧端口、每次请求
584
+ // upstream_unreachable。这里先验证 env:死了就临时忽略它,按插件写的 .dsh-upstream/探测
585
+ // 重新发现;发现成功后清掉失效 env,后续都不再被它挡。
586
+ const envUrl = String(process.env.DSH_BRIDGE_UPSTREAM || "").trim().replace(/\/+$/, "");
587
+ if (envUrl && !(await upstreamReachableForRefresh(envUrl))) {
588
+ const env2 = { ...process.env };
589
+ delete env2.DSH_BRIDGE_UPSTREAM;
590
+ found = await discoverUpstream({ relayDir: RELAY_DIR, env: env2 });
591
+ if (found && found.url && found.source !== "fallback") {
592
+ delete process.env.DSH_BRIDGE_UPSTREAM;
593
+ console.log(`[bridge] 旧上游 ${envUrl} 已不可达 → 改用 ${found.url}(来源 ${found.source})`);
594
+ }
595
+ }
596
+ if (!found) found = await discoverUpstream({ relayDir: RELAY_DIR });
569
597
  if (found.url && found.url !== UPSTREAM) {
570
598
  console.log(`[bridge] 上游地址切换: ${UPSTREAM} → ${found.url}(来源 ${found.source}${reason ? `,${reason}` : ""})`);
571
599
  UPSTREAM = found.url;
@@ -1127,7 +1127,15 @@ const UPSTREAM_FILE = ".dsh-upstream";
1127
1127
  * 本次安装要**钉住**的上游(null = 不钉,运行时按 env → .dsh-upstream → 探测 取值)。
1128
1128
  * 见 desktopAppPort() 的注释:插件装进多个 profile 后,各宿主会互相覆盖 `.dsh-upstream`。
1129
1129
  */
1130
- let PINNED_UPSTREAM = String(process.env.DSH_BRIDGE_UPSTREAM || "").trim().replace(/\/+$/, "") || null;
1130
+ // 🔴 2026-10-25 事故:以前把安装进程里的 DSH_BRIDGE_UPSTREAM(一个**当时的动态端口**)写进
1131
+ // launchd plist,之后 dsh 每次重启换端口,plist 里的旧端口就成了永久生效的"显式指定",
1132
+ // 守护/桥永远连不上 → 设备掉线。现在默认**不钉**(运行时按 插件写的 .dsh-upstream → 探测 取值);
1133
+ // 仅当部署方显式要求(DSH_BRIDGE_PIN_UPSTREAM=1)时才沿用旧行为。
1134
+ let PINNED_UPSTREAM = (() => {
1135
+ const pinFlag = String(process.env.DSH_BRIDGE_PIN_UPSTREAM || "").trim().toLowerCase();
1136
+ if (pinFlag !== "1" && pinFlag !== "true" && pinFlag !== "on") return null;
1137
+ return String(process.env.DSH_BRIDGE_UPSTREAM || "").trim().replace(/\/+$/, "") || null;
1138
+ })();
1131
1139
 
1132
1140
  /**
1133
1141
  * 探测桌面端(Electron 应用)当前监听的**回环端口**。
@@ -1671,7 +1679,22 @@ function releaseSpawnLock() {
1671
1679
  }
1672
1680
 
1673
1681
  // ---------- run:前台跑 bridge(带配置 + 自启动 watcher) ----------
1682
+ /** 以插件写入的 .dsh-upstream 为准校正进程内的旧上游环境变量(dsh 换端口后不再连旧端口)。 */
1683
+ function reconcileUpstreamEnvFromFile() {
1684
+ try {
1685
+ const fileUp = String(fs.readFileSync(path.join(CONFIG_DIR, UPSTREAM_FILE), "utf8")).trim().replace(/\/+$/, "");
1686
+ const envUp = String(process.env.DSH_BRIDGE_UPSTREAM || "").trim().replace(/\/+$/, "");
1687
+ if (envUp && /^https?:\/\/[^\s]+$/i.test(fileUp) && fileUp !== envUp) {
1688
+ console.log(`[dsh-remote] 忽略旧的上游地址 ${envUp} → 使用插件写入的最新地址 ${fileUp}(防端口漂移掉线)`);
1689
+ process.env.DSH_BRIDGE_UPSTREAM = fileUp;
1690
+ discoveredUpstream = fileUp;
1691
+ }
1692
+ } catch { /* 无文件:保持原行为 */ }
1693
+ }
1694
+
1674
1695
  async function runBridge() {
1696
+ // 启动/每次拉起前都按插件写入的端口校正继承来的旧 env(旧 plist 可能钉着安装时的端口)
1697
+ reconcileUpstreamEnvFromFile();
1675
1698
  // 去重(所有平台):自启动服务、登录任务、插件半的脱离进程兜底都可能拉起 watcher,
1676
1699
  // 两个 bridge 会抢同一个设备登记 → 以 pid 文件为准。
1677
1700
  //
@@ -1868,6 +1891,7 @@ async function runBridge() {
1868
1891
  }
1869
1892
  warnedDuplicateBridge = false;
1870
1893
 
1894
+ reconcileUpstreamEnvFromFile(); // dsh 可能刚重启并换了端口:拉起前再以最新端口文件为准
1871
1895
  const alive = await checkUpstream();
1872
1896
  if (alive && childStopped(bridgeProc)) {
1873
1897
  starting = true;
@@ -2052,9 +2076,11 @@ async function setup(argv) {
2052
2076
  if (!PINNED_UPSTREAM && pluginTargets.some((t) => t.name === "desktop")) {
2053
2077
  const p = desktopAppPort();
2054
2078
  if (p) {
2055
- PINNED_UPSTREAM = `http://127.0.0.1:${p}`;
2056
- try { fs.writeFileSync(path.join(CONFIG_DIR, UPSTREAM_FILE), PINNED_UPSTREAM + "\n"); } catch { /* 忽略 */ }
2057
- console.log(`ℹ 桌面端在 127.0.0.1:${p} 监听 → 桥的上游已钉到它(避免与 Web 版互抢)`);
2079
+ // 🔴 2026-10-25:这里只能写运行时端口文件;**不能**回填 PINNED_UPSTREAM ——
2080
+ // 它会被写进 launchd plist 变成永久显式指定,dsh 换端口后守护/桥永远连旧端口
2081
+ // (用户实测 upstream_unreachable)。
2082
+ try { fs.writeFileSync(path.join(CONFIG_DIR, UPSTREAM_FILE), `http://127.0.0.1:${p}` + "\n"); } catch { /* 忽略 */ }
2083
+ console.log(`ℹ 桌面端在 127.0.0.1:${p} 监听 → 已写入运行时上游端口(不再钉进自启动配置)`);
2058
2084
  }
2059
2085
  }
2060
2086
  } catch { /* 探测失败不阻断安装 */ }