@mrrisega/dsh-remote 0.7.6-beta.7 → 0.7.6-beta.9

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/dsh-setup.mjs CHANGED
@@ -1679,20 +1679,22 @@ function releaseSpawnLock() {
1679
1679
  }
1680
1680
 
1681
1681
  // ---------- run:前台跑 bridge(带配置 + 自启动 watcher) ----------
1682
- async function runBridge() {
1683
- // 🔴 2026-10-25:升级自旧版的机器,launchd plist 里可能还带着**安装时的旧端口**
1684
- // (DSH_BRIDGE_UPSTREAM)。dsh 换端口后它会让守护/桥永远连旧端口 → 设备掉线。
1685
- // 插件每次装载都会把当前端口写进 .dsh-upstream,这里以文件为准校正进程环境(仅当文件合法且不同)。
1682
+ /** 以插件写入的 .dsh-upstream 为准校正进程内的旧上游环境变量(dsh 换端口后不再连旧端口)。 */
1683
+ function reconcileUpstreamEnvFromFile() {
1686
1684
  try {
1687
1685
  const fileUp = String(fs.readFileSync(path.join(CONFIG_DIR, UPSTREAM_FILE), "utf8")).trim().replace(/\/+$/, "");
1688
1686
  const envUp = String(process.env.DSH_BRIDGE_UPSTREAM || "").trim().replace(/\/+$/, "");
1689
- // 只在「继承到了旧的显式 env」时校正;env 为空时保持原有发现逻辑(文件可能过期,须走探测)。
1690
1687
  if (envUp && /^https?:\/\/[^\s]+$/i.test(fileUp) && fileUp !== envUp) {
1691
- console.log(`[dsh-remote] 忽略旧的上游地址 ${envUp || "(空)"} → 使用插件写入的最新地址 ${fileUp}(防端口漂移掉线)`);
1688
+ console.log(`[dsh-remote] 忽略旧的上游地址 ${envUp} → 使用插件写入的最新地址 ${fileUp}(防端口漂移掉线)`);
1692
1689
  process.env.DSH_BRIDGE_UPSTREAM = fileUp;
1693
1690
  discoveredUpstream = fileUp;
1694
1691
  }
1695
- } catch { /* 没有端口文件:保持原行为 */ }
1692
+ } catch { /* 无文件:保持原行为 */ }
1693
+ }
1694
+
1695
+ async function runBridge() {
1696
+ // 启动/每次拉起前都按插件写入的端口校正继承来的旧 env(旧 plist 可能钉着安装时的端口)
1697
+ reconcileUpstreamEnvFromFile();
1696
1698
  // 去重(所有平台):自启动服务、登录任务、插件半的脱离进程兜底都可能拉起 watcher,
1697
1699
  // 两个 bridge 会抢同一个设备登记 → 以 pid 文件为准。
1698
1700
  //
@@ -1889,6 +1891,7 @@ async function runBridge() {
1889
1891
  }
1890
1892
  warnedDuplicateBridge = false;
1891
1893
 
1894
+ reconcileUpstreamEnvFromFile(); // dsh 可能刚重启并换了端口:拉起前再以最新端口文件为准
1892
1895
  const alive = await checkUpstream();
1893
1896
  if (alive && childStopped(bridgeProc)) {
1894
1897
  starting = true;
@@ -2073,9 +2076,11 @@ async function setup(argv) {
2073
2076
  if (!PINNED_UPSTREAM && pluginTargets.some((t) => t.name === "desktop")) {
2074
2077
  const p = desktopAppPort();
2075
2078
  if (p) {
2076
- PINNED_UPSTREAM = `http://127.0.0.1:${p}`;
2077
- try { fs.writeFileSync(path.join(CONFIG_DIR, UPSTREAM_FILE), PINNED_UPSTREAM + "\n"); } catch { /* 忽略 */ }
2078
- 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} 监听 → 已写入运行时上游端口(不再钉进自启动配置)`);
2079
2084
  }
2080
2085
  }
2081
2086
  } catch { /* 探测失败不阻断安装 */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrisega/dsh-remote",
3
- "version": "0.7.6-beta.7",
3
+ "version": "0.7.6-beta.9",
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",
@@ -98,7 +98,7 @@ window.__ModuleLoader__.load({
98
98
  // 绝不写 :root / body —— 否则会污染整个 GUI。JS 解析出主题后往根节点写
99
99
  // data-dru-theme="dark|light",下面按属性切换;末一条 `:not([data-dru-theme])`
100
100
  // 是 JS 尚未执行时的纯 CSS 兜底(一旦 JS 写过属性就不再参与,不会打架)。
101
- ".dru-settings-section,.dru-popup,.dru-nav-remote{",
101
+ ".dru-settings-section,.dru-popup,.dru-nav-remote,.dru-standalone-card{",
102
102
  " color-scheme:light;",
103
103
  " --dru-surface:#f6f8fa;", // 卡片底
104
104
  " --dru-surface-2:#ffffff;", // 抬升面(输入框/行/弹窗卡)
@@ -149,10 +149,11 @@ window.__ModuleLoader__.load({
149
149
  // 深色:JS 写的 data-dru-theme 优先;宿主属性作 JS 未执行时的兜底。
150
150
  // 取值跟着宿主的深色基色走(body 实测 #151517 / 层 1 #232324 / 层 2 #2c2c2e / 层 3 #353638),
151
151
  // 语义色**在深底上重新取值**,不是沿用亮色值(亮色的 #1a7f37 / #cf222e 在深底对比度不足)。
152
- ".dru-settings-section[data-dru-theme=\"dark\"],.dru-popup[data-dru-theme=\"dark\"],.dru-nav-remote[data-dru-theme=\"dark\"],",
152
+ ".dru-settings-section[data-dru-theme=\"dark\"],.dru-popup[data-dru-theme=\"dark\"],.dru-nav-remote[data-dru-theme=\"dark\"],.dru-standalone-card[data-dru-theme=\"dark\"],",
153
153
  "body[data-ds-dark-theme] .dru-settings-section:not([data-dru-theme]),",
154
154
  "body[data-ds-dark-theme] .dru-popup:not([data-dru-theme]),",
155
- "body[data-ds-dark-theme] .dru-nav-remote:not([data-dru-theme]){",
155
+ "body[data-ds-dark-theme] .dru-nav-remote:not([data-dru-theme]),",
156
+ "body[data-ds-dark-theme] .dru-standalone-card:not([data-dru-theme]){",
156
157
  " color-scheme:dark;",
157
158
  " --dru-surface:#232324;",
158
159
  " --dru-surface-2:#2c2c2e;",
@@ -202,6 +203,15 @@ window.__ModuleLoader__.load({
202
203
  "}",
203
204
  // 设置页栏目容器(nav 选中后渲染在 settings.section 内容区)
204
205
  ".dru-settings-section{max-width:720px;display:flex;flex-direction:column;gap:14px;padding-top:2px;color:var(--dru-fg)}",
206
+ // 📱 独立「远程访问」弹层(2026-10-25):入口点击不再去"猜"官方设置面板的 DOM,
207
+ // 直接弹我们自己的一份设置页 —— 宿主改版也影响不到它。
208
+ ".dru-standalone-overlay{position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;padding:24px;background:var(--dru-overlay)}",
209
+ ".dru-standalone-card{width:min(760px,100%);max-height:88vh;overflow:auto;box-sizing:border-box;padding:18px 20px 22px;border:1px solid var(--dru-border);border-radius:14px;background:var(--dru-surface-2);color:var(--dru-fg);box-shadow:0 18px 60px var(--dru-shadow)}",
210
+ ".dru-standalone-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:8px}",
211
+ ".dru-standalone-title{font-size:16px;font-weight:700}",
212
+ ".dru-standalone-close{appearance:none;border:1px solid var(--dru-border-ctl);background:var(--dru-surface-2);color:var(--dru-fg-muted);width:30px;height:30px;border-radius:8px;font-size:18px;line-height:1;cursor:pointer}",
213
+ ".dru-standalone-close:hover{background:var(--dru-hover);color:var(--dru-fg)}",
214
+ "@media (max-width:640px){.dru-standalone-overlay{padding:8px}.dru-standalone-card{max-height:94vh;border-radius:12px;padding:14px 14px 18px}}",
205
215
  ".dru-settings-head{display:flex;align-items:center;gap:10px;padding:6px 2px 2px}",
206
216
  ".dru-settings-icon{font-size:24px;line-height:1;flex:none}",
207
217
  // 标题/副标题优先沿用宿主语义令牌(自带深浅两套),取不到再退回本插件令牌
@@ -1117,6 +1127,61 @@ window.__ModuleLoader__.load({
1117
1127
  0.2 实测该槽就在侧栏底部、且**当前是空的**)—— 位置、宽度、折叠态全由宿主负责。
1118
1128
  所以这里不再注入任何 DOM,直接注册进槽位。id 仍保留 `dru-nav-remote`:
1119
1129
  公告徽标 / 首次红点那套按 id 取节点的既有逻辑因此不用改。 */
1130
+ /* 📱 独立「远程访问」弹层(2026-10-25 用户拍板):
1131
+ 桌面端点击侧栏入口时,不再去"点官方设置/找栏目"(宿主一改版就点错或没反应,
1132
+ 实测会点到左上角「更多→浏览插件」),而是弹出我们自己的一份设置页。
1133
+ 「设置 → 远程访问」栏目保留,正常从官方设置页进入的用户仍走那条路。
1134
+ 实现上用 shell.overlay 槽(宿主最上层,不受侧栏容器裁剪),组件常驻、按事件开关。 */
1135
+ var standaloneOverlayMounted = false;
1136
+ function openRemoteStandalonePanel() {
1137
+ try { window.dispatchEvent(new CustomEvent("dsh-remote:open-standalone")); } catch (e) { /* 忽略 */ }
1138
+ }
1139
+ function RemoteStandaloneOverlay() {
1140
+ var st = useState(false);
1141
+ var open = st[0];
1142
+ var setOpen = st[1];
1143
+ useEffect(function () {
1144
+ standaloneOverlayMounted = true;
1145
+ function onOpen() { try { setOpen(true); } catch (e) { /* 忽略 */ } }
1146
+ function onKey(e) {
1147
+ if (!e) return;
1148
+ if (e.key === "Escape" || e.keyCode === 27) { try { setOpen(false); } catch (e2) { /* 忽略 */ } }
1149
+ }
1150
+ try {
1151
+ window.addEventListener("dsh-remote:open-standalone", onOpen);
1152
+ window.addEventListener("keydown", onKey);
1153
+ } catch (e3) { /* 忽略 */ }
1154
+ return function () {
1155
+ standaloneOverlayMounted = false;
1156
+ try {
1157
+ window.removeEventListener("dsh-remote:open-standalone", onOpen);
1158
+ window.removeEventListener("keydown", onKey);
1159
+ } catch (e4) { /* 忽略 */ }
1160
+ };
1161
+ }, []);
1162
+ if (!open) return null;
1163
+ var dark = false;
1164
+ try { dark = !!document.body && document.body.hasAttribute("data-ds-dark-theme"); } catch (e5) { /* 忽略 */ }
1165
+ return h("div", {
1166
+ className: "dru-standalone-overlay",
1167
+ role: "dialog",
1168
+ "aria-modal": "true",
1169
+ "aria-label": "远程访问",
1170
+ onClick: function (e) { if (e && e.target === e.currentTarget) setOpen(false); }
1171
+ },
1172
+ h("div", { className: "dru-standalone-card", "data-dru-theme": dark ? "dark" : "light" },
1173
+ h("div", { className: "dru-standalone-head" },
1174
+ h("b", { className: "dru-standalone-title" }, "📱 远程访问"),
1175
+ h("button", {
1176
+ type: "button",
1177
+ className: "dru-standalone-close",
1178
+ "aria-label": "关闭",
1179
+ onClick: function () { setOpen(false); }
1180
+ }, "×")),
1181
+ h(RemoteControlSection, { standalone: true })
1182
+ ));
1183
+ }
1184
+
1120
1185
  function RemoteNavEntry(props) {
1121
1186
  var wide = !(props && props.wide === false);
1122
1187
  var seenArr = useState(false);
@@ -1133,7 +1198,16 @@ window.__ModuleLoader__.load({
1133
1198
  try { e.preventDefault(); } catch (err) { /* 忽略 */ }
1134
1199
  setClicked(true);
1135
1200
  dotMarkSeen(); // 用过入口 = 已了解位置 → 收起首次红点
1136
- if (remoteNavOpen) remoteNavOpen();
1201
+ // 2026-10-25(用户拍板):优先弹**我们自己的独立设置页**(不再依赖官方设置面板 DOM);
1202
+ // 只有宿主没有 shell.overlay 槽(极老版本)时才退回旧的"打开官方设置栏目"链路。
1203
+ try {
1204
+ openRemoteStandalonePanel();
1205
+ setTimeout(function () {
1206
+ if (!standaloneOverlayMounted) {
1207
+ try { openRemoteSettings(); } catch (e2) { /* 忽略 */ }
1208
+ }
1209
+ }, 150);
1210
+ } catch (errOpen) { /* 绝不因打开失败抛到宿主 */ }
1137
1211
  }
1138
1212
  }, [
1139
1213
  h("span", { className: "ico", key: "ico" }, "📱"),
@@ -6130,6 +6204,13 @@ window.__ModuleLoader__.load({
6130
6204
  // 公告是锦上添花,**没有任何资格**把门关上 —— 任何异常都只是"这次没有弹窗"。
6131
6205
  }
6132
6206
 
6207
+ // 📱 独立「远程访问」弹层(入口点击用;见 RemoteStandaloneOverlay 注释)
6208
+ try {
6209
+ ctx.slots.inject("shell.overlay", function () {
6210
+ return ctx.slots.register({ name: "shell.overlay", id: "dsh-remote-standalone", order: 95 }, RemoteStandaloneOverlay);
6211
+ });
6212
+ } catch (eStand) { /* 老宿主没有 shell.overlay:入口会在 150ms 后回退到官方设置栏目链路 */ }
6213
+
6133
6214
  // 📣 公告:每次页面加载**只拉一次**(sessionStorage 一次性标记,见 annPullOnce)。
6134
6215
  // 放在 apply() 里是刻意的:apply 跑在 app mount **之前**,所以这里**绝不能碰产品 DOM**
6135
6216
  // (那时还没有),只做数据拉取;UI 全部交给上面那个槽位与面板卡片。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-web",
3
- "version": "0.7.6-beta.7",
3
+ "version": "0.7.6-beta.9",
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",
@@ -1679,20 +1679,22 @@ function releaseSpawnLock() {
1679
1679
  }
1680
1680
 
1681
1681
  // ---------- run:前台跑 bridge(带配置 + 自启动 watcher) ----------
1682
- async function runBridge() {
1683
- // 🔴 2026-10-25:升级自旧版的机器,launchd plist 里可能还带着**安装时的旧端口**
1684
- // (DSH_BRIDGE_UPSTREAM)。dsh 换端口后它会让守护/桥永远连旧端口 → 设备掉线。
1685
- // 插件每次装载都会把当前端口写进 .dsh-upstream,这里以文件为准校正进程环境(仅当文件合法且不同)。
1682
+ /** 以插件写入的 .dsh-upstream 为准校正进程内的旧上游环境变量(dsh 换端口后不再连旧端口)。 */
1683
+ function reconcileUpstreamEnvFromFile() {
1686
1684
  try {
1687
1685
  const fileUp = String(fs.readFileSync(path.join(CONFIG_DIR, UPSTREAM_FILE), "utf8")).trim().replace(/\/+$/, "");
1688
1686
  const envUp = String(process.env.DSH_BRIDGE_UPSTREAM || "").trim().replace(/\/+$/, "");
1689
- // 只在「继承到了旧的显式 env」时校正;env 为空时保持原有发现逻辑(文件可能过期,须走探测)。
1690
1687
  if (envUp && /^https?:\/\/[^\s]+$/i.test(fileUp) && fileUp !== envUp) {
1691
- console.log(`[dsh-remote] 忽略旧的上游地址 ${envUp || "(空)"} → 使用插件写入的最新地址 ${fileUp}(防端口漂移掉线)`);
1688
+ console.log(`[dsh-remote] 忽略旧的上游地址 ${envUp} → 使用插件写入的最新地址 ${fileUp}(防端口漂移掉线)`);
1692
1689
  process.env.DSH_BRIDGE_UPSTREAM = fileUp;
1693
1690
  discoveredUpstream = fileUp;
1694
1691
  }
1695
- } catch { /* 没有端口文件:保持原行为 */ }
1692
+ } catch { /* 无文件:保持原行为 */ }
1693
+ }
1694
+
1695
+ async function runBridge() {
1696
+ // 启动/每次拉起前都按插件写入的端口校正继承来的旧 env(旧 plist 可能钉着安装时的端口)
1697
+ reconcileUpstreamEnvFromFile();
1696
1698
  // 去重(所有平台):自启动服务、登录任务、插件半的脱离进程兜底都可能拉起 watcher,
1697
1699
  // 两个 bridge 会抢同一个设备登记 → 以 pid 文件为准。
1698
1700
  //
@@ -1889,6 +1891,7 @@ async function runBridge() {
1889
1891
  }
1890
1892
  warnedDuplicateBridge = false;
1891
1893
 
1894
+ reconcileUpstreamEnvFromFile(); // dsh 可能刚重启并换了端口:拉起前再以最新端口文件为准
1892
1895
  const alive = await checkUpstream();
1893
1896
  if (alive && childStopped(bridgeProc)) {
1894
1897
  starting = true;
@@ -2073,9 +2076,11 @@ async function setup(argv) {
2073
2076
  if (!PINNED_UPSTREAM && pluginTargets.some((t) => t.name === "desktop")) {
2074
2077
  const p = desktopAppPort();
2075
2078
  if (p) {
2076
- PINNED_UPSTREAM = `http://127.0.0.1:${p}`;
2077
- try { fs.writeFileSync(path.join(CONFIG_DIR, UPSTREAM_FILE), PINNED_UPSTREAM + "\n"); } catch { /* 忽略 */ }
2078
- 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} 监听 → 已写入运行时上游端口(不再钉进自启动配置)`);
2079
2084
  }
2080
2085
  }
2081
2086
  } catch { /* 探测失败不阻断安装 */ }
@@ -193,11 +193,13 @@ test("入口:settings.section 栏目(id/order/label)+ 侧栏底部官方
193
193
  assert.ok(String(label).includes("📱"), "栏目名应带 📱 远程访问图标(手机访问语义)");
194
194
  assert.ok(!String(label).includes("远程控制"), "栏目名不应再残留旧称「远程控制」");
195
195
 
196
- // shell.overlay 仅保留满意度弹窗,浮动面板已移除
196
+ // shell.overlay:满意度弹窗 + 公告弹窗 + 「远程访问」独立弹层(2026-10-25 新增)
197
197
  assert.ok(plugin.injects.has("shell.overlay"));
198
198
  plugin.injects.get("shell.overlay")();
199
199
  assert.ok(plugin.registered.has("dsh-feedback-popup"), "满意度弹窗应保留在 shell.overlay");
200
- assert.equal(plugin.registered.has("dsh-remote-panel"), false, "浮动配置面板不应再注册");
200
+ assert.ok(plugin.registered.has("dsh-remote-standalone"),
201
+ "入口点击要弹**独立**的远程访问设置页(不再依赖官方设置面板 DOM)");
202
+ assert.equal(plugin.registered.has("dsh-remote-panel"), false, "旧的浮动配置面板不应复活");
201
203
  });
202
204
 
203
205
  test("入口迁移:settings.section 里**只有一个**栏目(id=dsh-remote)——微信机器人不再是第二个栏目", () => {
@@ -214,17 +214,23 @@ test("★ launchd bootout 后必须等 label 摘干净再 bootstrap(否则随
214
214
  assert.match(fn, /bootok|!boot\.ok[\s\S]{0,400}launchctl bootstrap/, "bootstrap 失败应重试一次再兜底");
215
215
  });
216
216
 
217
- test("★ 自启动配置里可以钉住上游(多 profile 时代,各宿主会互抢 .dsh-upstream)", () => {
217
+ test("★ 自启动配置**默认不钉**上游端口;仅在 DSH_BRIDGE_PIN_UPSTREAM=1 时显式 opt-in", () => {
218
+ // 2026-10-25 事故:把安装时的动态端口写进 launchd plist 后,dsh 每次重启换端口,
219
+ // 守护/桥就永远连旧端口(upstream_unreachable / 设备掉线)。所以默认改为不钉。
218
220
  const src = readFileSync(SETUP, "utf8");
219
221
  assert.match(src, /function desktopAppPort\(/, "要有桌面端端口探测(lsof -c DeepSeek)");
220
- assert.match(src, /PINNED_UPSTREAM/, "要有钉住上游的机制");
221
- // plist 模板里必须把它写进 EnvironmentVariables(环境变量优先级高于 .dsh-upstream 文件)
222
+ assert.match(src, /DSH_BRIDGE_PIN_UPSTREAM/, "要有显式 opt-in 开关(部署方需要固定上游时用)");
223
+ // plist 模板仍支持写 DSH_BRIDGE_UPSTREAM(opt-in 时),但默认 PINNED_UPSTREAM 必须为空
222
224
  assert.match(src, /DSH_BRIDGE_UPSTREAM<\/key><string>\$\{PINNED_UPSTREAM\}/,
223
- "plist 里要写入 DSH_BRIDGE_UPSTREAM,否则两端宿主会互相覆盖上游");
224
- // ⚠️ 顺序:必须在写自启动文件**之前**算出来,否则 plist 里带不上(实测踩到过)
225
- const pinAt = src.indexOf("PINNED_UPSTREAM = `http://127.0.0.1:${p}`");
225
+ "opt-in 时 plist 仍要能写入 DSH_BRIDGE_UPSTREAM");
226
+ // 关键回归锁:安装流程里不能再出现「把探测到的端口赋给 PINNED_UPSTREAM」
227
+ assert.doesNotMatch(src, /PINNED_UPSTREAM = `http:\/\/127\.0\.0\.1:\$\{p\}`/,
228
+ "不得再把动态端口回填进 PINNED_UPSTREAM(否则又会写进 plist,dsh 换端口即掉线)");
229
+ // 运行时端口仍必须写进 .dsh-upstream(守护/桥按它取最新端口)
230
+ assert.match(src, /UPSTREAM_FILE\)[^\n]*\n/);
231
+ const writeFileAt = src.indexOf("fs.writeFileSync(path.join(CONFIG_DIR, UPSTREAM_FILE)");
226
232
  const svcAt = src.indexOf("svc = installAutostart()");
227
- assert.ok(pinAt > 0 && svcAt > 0 && pinAt < svcAt, "钉定必须发生在 installAutostart() 之前");
233
+ assert.ok(writeFileAt > 0 && svcAt > 0 && writeFileAt < svcAt, "运行时端口文件要在 installAutostart() 之前写好");
228
234
  });
229
235
 
230
236
  test("★ 桌面应用在运行时,绝不改写它的 profile(否则应用会自己退出/重启)", () => {
@@ -467,8 +467,8 @@ test("主题不变量:根节点带 .dru-settings-section + data-dru-theme;
467
467
  assert.doesNotMatch(block, /--dru-[a-z0-9-]+\s*:/, "新样式不得**声明**令牌(只许引用)");
468
468
  assert.doesNotMatch(block, /:root|\bbody\b/, "新样式不得挂在 :root/body 上(会污染整个 GUI)");
469
469
  assert.match(block, /var\(--dru-/, "新样式应复用既有令牌");
470
- // 令牌声明只允许出现在那三个选择器上(原有注释所述的不变量)
471
- assert.match(SOURCE, /"\.dru-settings-section,\.dru-popup,\.dru-nav-remote\{"/, "亮色令牌块的选择器不得扩大");
470
+ // 令牌声明只允许出现在那四个选择器上(2026-10-25 新增 .dru-standalone-card:独立远程访问弹层)
471
+ assert.match(SOURCE, /"\.dru-settings-section,\.dru-popup,\.dru-nav-remote,\.dru-standalone-card\{"/, "亮色令牌块的选择器不得再扩大");
472
472
  assert.doesNotMatch(SOURCE, /":root\{/);
473
473
  });
474
474