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

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;
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.7",
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 应用)当前监听的**回环端口**。
@@ -1672,6 +1680,19 @@ function releaseSpawnLock() {
1672
1680
 
1673
1681
  // ---------- run:前台跑 bridge(带配置 + 自启动 watcher) ----------
1674
1682
  async function runBridge() {
1683
+ // 🔴 2026-10-25:升级自旧版的机器,launchd plist 里可能还带着**安装时的旧端口**
1684
+ // (DSH_BRIDGE_UPSTREAM)。dsh 换端口后它会让守护/桥永远连旧端口 → 设备掉线。
1685
+ // 插件每次装载都会把当前端口写进 .dsh-upstream,这里以文件为准校正进程环境(仅当文件合法且不同)。
1686
+ try {
1687
+ const fileUp = String(fs.readFileSync(path.join(CONFIG_DIR, UPSTREAM_FILE), "utf8")).trim().replace(/\/+$/, "");
1688
+ const envUp = String(process.env.DSH_BRIDGE_UPSTREAM || "").trim().replace(/\/+$/, "");
1689
+ // 只在「继承到了旧的显式 env」时校正;env 为空时保持原有发现逻辑(文件可能过期,须走探测)。
1690
+ if (envUp && /^https?:\/\/[^\s]+$/i.test(fileUp) && fileUp !== envUp) {
1691
+ console.log(`[dsh-remote] 忽略旧的上游地址 ${envUp || "(空)"} → 使用插件写入的最新地址 ${fileUp}(防端口漂移掉线)`);
1692
+ process.env.DSH_BRIDGE_UPSTREAM = fileUp;
1693
+ discoveredUpstream = fileUp;
1694
+ }
1695
+ } catch { /* 没有端口文件:保持原行为 */ }
1675
1696
  // 去重(所有平台):自启动服务、登录任务、插件半的脱离进程兜底都可能拉起 watcher,
1676
1697
  // 两个 bridge 会抢同一个设备登记 → 以 pid 文件为准。
1677
1698
  //