dsh-remote-web 0.7.6-beta.5 → 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 +16 -1
- package/package.json +1 -1
- package/runtime/clients/dsh-remote/dsh-bridge.mjs +29 -1
- package/runtime/dsh-setup.mjs +33 -1
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
|
-
|
|
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.
|
|
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
|
-
|
|
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;
|
package/runtime/dsh-setup.mjs
CHANGED
|
@@ -1127,7 +1127,15 @@ const UPSTREAM_FILE = ".dsh-upstream";
|
|
|
1127
1127
|
* 本次安装要**钉住**的上游(null = 不钉,运行时按 env → .dsh-upstream → 探测 取值)。
|
|
1128
1128
|
* 见 desktopAppPort() 的注释:插件装进多个 profile 后,各宿主会互相覆盖 `.dsh-upstream`。
|
|
1129
1129
|
*/
|
|
1130
|
-
|
|
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
|
//
|
|
@@ -1915,6 +1936,17 @@ async function runBridge() {
|
|
|
1915
1936
|
// 另一个 bridge 把本连接顶掉)后会以"重复实例"退出码优雅退出(见 dsh-bridge.mjs)。
|
|
1916
1937
|
// 这种退出**绝不能**照常 10s 重拉:本机已经有另一个实例在跑,重拉只会继续乒乓互踢。
|
|
1917
1938
|
if (code === DUPLICATE_INSTANCE_EXIT_CODE) {
|
|
1939
|
+
// 🔴 2026-10-25:升级/重启后常见「上一代 bridge 变成孤儿继续占着单实例锁」,
|
|
1940
|
+
// 新 bridge 因此退出 code=4。旧逻辑一律暂停 5 分钟 → 设备在线要等 5 分钟(用户实测掉线)。
|
|
1941
|
+
// 现在先**清扫同运行时的孤儿 bridge**;扫到了说明冲突已解除,3 秒后立即重试;
|
|
1942
|
+
// 扫不到(多半真有另一个 watcher 的活实例)才按 5 分钟退避,避免乒乓互踢。
|
|
1943
|
+
let swept = [];
|
|
1944
|
+
try { swept = sweepStrayBridges(); } catch { /* 清扫失败不致命 */ }
|
|
1945
|
+
if (swept.length > 0) {
|
|
1946
|
+
duplicateBridgeBackoffUntil = Date.now() + 3000;
|
|
1947
|
+
console.log(`[dsh-remote] bridge 因同实例冲突退出(code=${code}),已清扫 ${swept.length} 个孤儿 bridge(pid ${swept.join(", ")})→ 3 秒后重试。`);
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1918
1950
|
duplicateBridgeBackoffUntil = Date.now() + DUPLICATE_BRIDGE_BACKOFF_MS;
|
|
1919
1951
|
console.log(`[dsh-remote] bridge 检测到同 deviceId 的另一个实例并退出(code=${code})——本守护暂停 ${Math.round(DUPLICATE_BRIDGE_BACKOFF_MS / 60000)} 分钟后再试,避免两个实例反复互踢。`);
|
|
1920
1952
|
return;
|