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 +23 -2
- package/package.json +1 -1
- package/runtime/clients/dsh-remote/dsh-bridge.mjs +29 -1
- package/runtime/dsh-setup.mjs +30 -4
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;
|
|
@@ -1118,7 +1133,13 @@ window.__ModuleLoader__.load({
|
|
|
1118
1133
|
try { e.preventDefault(); } catch (err) { /* 忽略 */ }
|
|
1119
1134
|
setClicked(true);
|
|
1120
1135
|
dotMarkSeen(); // 用过入口 = 已了解位置 → 收起首次红点
|
|
1121
|
-
|
|
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.
|
|
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
|
-
|
|
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 应用)当前监听的**回环端口**。
|
|
@@ -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
|
|
2056
|
-
|
|
2057
|
-
|
|
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 { /* 探测失败不阻断安装 */ }
|