@mrrisega/dsh-remote 0.8.2-beta.2 → 0.8.2

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.
@@ -189,7 +189,7 @@ let UPSTREAM = (() => {
189
189
  // 微信机器人通道总开关:DSH_WECHAT=0 关闭(默认开启)。控制面只 bind 回环,且必须带 bridge_secret。
190
190
  const WECHAT_DISABLED = String(process.env.DSH_WECHAT || "") === "0";
191
191
  // 默认云端服务地址(dsh-remote setup 会显式传入;自建模式无需账号 API)
192
- const API_BASE = (process.env.DSH_BRIDGE_API || "https://n.risegao.cn:13443/relay-api").replace(/\/+$/, "");
192
+ const API_BASE = (process.env.DSH_BRIDGE_API || "https://dsh.ruichengai.com:13443/relay-api").replace(/\/+$/, "");
193
193
  const EMAIL = process.env.DSH_BRIDGE_EMAIL || "";
194
194
  // 手机号优先;兼容旧的 DSH_BRIDGE_EMAIL(过渡期)
195
195
  const PHONE = process.env.DSH_BRIDGE_PHONE || EMAIL;
@@ -908,7 +908,7 @@ const SCRIPT = `(() => {
908
908
  void 0;
909
909
 
910
910
 
911
- /* —— 主机门解除(2026-09):镜像页(如 n.risegao.cn)经设备流认证回连同一台 127.0.0.1:3080,
911
+ /* —— 主机门解除(2026-09):镜像页(如 n.risegao.cn / dsh.ruichengai.com)经设备流认证回连同一台 127.0.0.1:3080,
912
912
  桥已把 Host 回环化并剥 Origin(不扩大信任面)。官方客户端以 transport.ownsHost 判定
913
913
  「自有主机」,非回环域名下不置 true 会把设置页置为 memory(不可用)镜像 →
914
914
  模型/提供方设置无法加载(“Settings are unavailable in this browser”)。
package/dsh-setup.mjs CHANGED
@@ -44,9 +44,29 @@ const CONFIG_DIR = process.env.DSH_RELAY_DIR || path.join(os.homedir(), ".dsh-re
44
44
  const CONFIG_PATH = path.join(CONFIG_DIR, ".dsh-config.json");
45
45
  // 默认云端服务地址(服务商 SaaS 入口;自建用户用 --server/--key 指向自己的 router)
46
46
  // DSH_RELAY_DEFAULT_API 可覆盖(测试脚本指向本地死端口,避免兜底路径打到生产)。
47
+ // 【2026-11-14 企业域名迁移】官方 SaaS 入口 → dsh.ruichengai.com(旧域名仍在服役,双域名并存)
47
48
  const DEFAULT_API = String(process.env.DSH_RELAY_DEFAULT_API || "").trim()
48
- || "https://n.risegao.cn:13443/relay-api";
49
- const DEFAULT_APP_URL = "https://n.risegao.cn:13443/app/";
49
+ || "https://dsh.ruichengai.com:13443/relay-api";
50
+ const DEFAULT_APP_URL = "https://dsh.ruichengai.com:13443/app/";
51
+
52
+ /** 官方云端「旧域名 → 新域名」映射。只认 hostname 精确匹配,自建地址原样返回。 */
53
+ const OFFICIAL_HOST_MIGRATION = new Map([["n.risegao.cn", "dsh.ruichengai.com"]]);
54
+
55
+ /** 把官方旧域名改写成新域名(保留端口/路径);其它地址原样返回。与插件半同规则。 */
56
+ function migrateOfficialUrl(raw) {
57
+ const s = String(raw == null ? "" : raw).trim();
58
+ if (!s) return s;
59
+ let u;
60
+ try {
61
+ u = new URL(s);
62
+ } catch {
63
+ return s;
64
+ }
65
+ const to = OFFICIAL_HOST_MIGRATION.get(u.hostname);
66
+ if (!to) return s;
67
+ u.hostname = to;
68
+ return u.toString().replace(/\/+$/, "");
69
+ }
50
70
  const REPO_URL = "https://github.com/mrRisega/dsh-remote";
51
71
 
52
72
  // ---------- 平台(0.6.7 起支持 Windows) ----------
@@ -538,7 +558,9 @@ function deriveTunnelUrl(apiUrl) {
538
558
  function applySaaSMode(cfg) {
539
559
  delete cfg.local_key;
540
560
  delete cfg.server; // 旧字段兜底
541
- cfg.api_url = String(cfg.api_url || DEFAULT_API).replace(/\/+$/, "");
561
+ // 2026-11-14 企业域名迁移:存量配置里的官方旧域名顺手改写(幂等,自建地址不动)
562
+ const api = migrateOfficialUrl(String(cfg.api_url || DEFAULT_API).replace(/\/+$/, ""));
563
+ cfg.api_url = api || DEFAULT_API;
542
564
  cfg.tunnel_url = deriveTunnelUrl(cfg.api_url);
543
565
  return cfg;
544
566
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrisega/dsh-remote",
3
- "version": "0.8.2-beta.2",
3
+ "version": "0.8.2",
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",
@@ -2071,7 +2071,7 @@ window.__ModuleLoader__.load({
2071
2071
  // 注意:被邀请人**没有任何额外奖励**(trial_days = 0),所以禁止任何「双方都得」的写法。
2072
2072
  var INVITE_RULE_FALLBACK = { n: 1, days: 3 };
2073
2073
  // ── 套餐约定:**先读线上真形状** ─────────────────────────────────────────
2074
- // 线上实测(GET https://n.risegao.cn:13443/relay-api/api/public-config):
2074
+ // 线上实测(GET https://dsh.ruichengai.com:13443/relay-api/api/public-config):
2075
2075
  // plans.free = { max_mbps: 1, monthly_gb: 1 }
2076
2076
  // plans.pro = { max_mbps: 5, monthly_gb: 20, devices: 1, monthly_resets: 5 }
2077
2077
  // plans.pro_max = { max_mbps: 10, monthly_gb: 60, devices: 10, online: 3 }
@@ -2549,7 +2549,7 @@ window.__ModuleLoader__.load({
2549
2549
  var acct = (b && b.ok && b.account) ? b.account : null;
2550
2550
  if (!acct || !acct.invite_code) { setPopupInvite({ code: "" }); return; }
2551
2551
  api("/dsh-remote/remote-url").then(function (u) {
2552
- var base = (u && u.remoteUrl) || "https://n.risegao.cn:13443/app/";
2552
+ var base = (u && u.remoteUrl) || "https://dsh.ruichengai.com:13443/app/";
2553
2553
  // 特性检测:public-config 里带了 invite_rule 就用服务端数值;没带 → 0 = 不显示具体数字。
2554
2554
  // 🔒 用 firstNum 只判「缺没缺」:`n/days` 配成 0 是合法的「活动关闭」态,
2555
2555
  // 不能被 `> 0` 这样写吃掉再回落成 1/3(后端 `|| 3 / || 15` 的同款坑)。
@@ -5413,7 +5413,7 @@ window.__ModuleLoader__.load({
5413
5413
  var ruleDays = inv.days;
5414
5414
  var trialDays = Number(pub && pub.trial_days) > 0 ? Number(pub.trial_days) : 0; // 注册送礼(关闭时不下发/为 0)
5415
5415
  var code = (account && account.invite_code) || "";
5416
- var base = (st && st.remoteUrl) || (pub && pub.app_url) || "https://n.risegao.cn:13443/app/";
5416
+ var base = (st && st.remoteUrl) || (pub && pub.app_url) || "https://dsh.ruichengai.com:13443/app/";
5417
5417
  var link = code ? base.replace(/\/+$/, "") + "/?invite=" + encodeURIComponent(code) : "";
5418
5418
  var loggedInSaaS = !!(st && st.config && st.config.phone);
5419
5419
  var records = (inviteData && inviteData.records) || [];
@@ -92,9 +92,43 @@ const DEFAULT_RELAY_DIR = process.env.DSH_RELAY_DIR || join(homedir(), ".dsh-rem
92
92
  // DSH_RELAY_DEFAULT_API 只改「配置里根本没写 api_url」时的兜底:测试脚本把它指向本地死端口,
93
93
  // 这样任何**在临时目录被拆掉之后**才跑到的后台请求(如装机上报的重试)都不会打生产。
94
94
  // 真实故障形态见 test/wechat-bind-telemetry.test.mjs 的注释:用例拆目录 → 读到空配置 → 回落生产。
95
+ //
96
+ // 【2026-11-14 企业域名迁移】官方 SaaS 入口从 n.risegao.cn 迁到 dsh.ruichengai.com。
97
+ // 这里只改「新装默认值」;存量用户配置由 migrateOfficialUrl() 在装载/启动时一次性改写。
98
+ // 两个域名在 NAS 上是**并存**的(同一套后端、各自证书),所以迁移期间谁都不会断。
95
99
  const DEFAULT_API = String(process.env.DSH_RELAY_DEFAULT_API || "").trim()
96
- || "https://n.risegao.cn:13443/relay-api";
97
- const DEFAULT_APP_URL = "https://n.risegao.cn:13443/app/";
100
+ || "https://dsh.ruichengai.com:13443/relay-api";
101
+ const DEFAULT_APP_URL = "https://dsh.ruichengai.com:13443/app/";
102
+
103
+ /**
104
+ * 官方云端的「旧域名 → 新域名」映射(2026-11-14)。
105
+ *
106
+ * 为什么需要它:存量用户 `.dsh-config.json` 里写死的是
107
+ * `https://n.risegao.cn:13443/relay-api`。只改默认值的话,**老用户永远还在连旧域名**,
108
+ * 而产品预期是「升级上去就用新域名」。所以装载时做一次幂等改写。
109
+ *
110
+ * 边界(故意收得很窄,别扩大):
111
+ * · 只认 **hostname 精确等于**旧官方域名;自建/自定义地址一律原样返回;
112
+ * · 端口、路径、查询串全部保留(内网自建用户即便解析到同名 host 也不受影响);
113
+ * · 非法 URL 原样返回,绝不抛错。
114
+ */
115
+ const OFFICIAL_HOST_MIGRATION = new Map([["n.risegao.cn", "dsh.ruichengai.com"]]);
116
+
117
+ /** 把官方旧域名改写成新域名(保留端口/路径),其它地址原样返回。纯函数,便于单测。 */
118
+ export function migrateOfficialUrl(raw) {
119
+ const s = String(raw == null ? "" : raw).trim();
120
+ if (!s) return s;
121
+ let u;
122
+ try {
123
+ u = new URL(s);
124
+ } catch {
125
+ return s;
126
+ }
127
+ const to = OFFICIAL_HOST_MIGRATION.get(u.hostname);
128
+ if (!to) return s;
129
+ u.hostname = to;
130
+ return u.toString().replace(/\/+$/, "");
131
+ }
98
132
 
99
133
  // ---------- 上游 dsh web 地址(bridge/watcher 都要连它,绝不写死 3080) ----------
100
134
  //
@@ -772,11 +806,41 @@ function saveConfig(relayDir, cfg) {
772
806
  function applySaaSMode(cfg) {
773
807
  delete cfg.local_key;
774
808
  delete cfg.server;
775
- cfg.api_url = String(cfg.api_url || DEFAULT_API).replace(/\/+$/, "");
809
+ // 2026-11-14 企业域名迁移:存量配置里的官方旧域名顺手改写(幂等,自建地址不动)
810
+ const api = migrateOfficialUrl(String(cfg.api_url || DEFAULT_API).replace(/\/+$/, ""));
811
+ cfg.api_url = api || DEFAULT_API;
776
812
  cfg.tunnel_url = cfg.api_url.replace(/\/relay-api\/?$/, "").replace(/^https/, "wss");
777
813
  return cfg;
778
814
  }
779
815
 
816
+ /**
817
+ * 装载时的一次性域名迁移(2026-11-14 企业域名迁移)。
818
+ *
819
+ * 与 applySaaSMode 里的改写互补:那条只在「登录 / 启动桥」时跑,本条让**插件一装载**
820
+ * 就把配置摆正,避免刷新页面后仍有一段时间打旧域名。
821
+ * 幂等:已迁移过就什么都不做;自建模式(有 local_key)与自定义地址一律不动。
822
+ */
823
+ function migrateLegacyConfigAtBoot(relayDir, ctx) {
824
+ try {
825
+ const cfg = loadConfig(relayDir);
826
+ if (!cfg || !cfg.api_url || cfg.local_key) return false;
827
+ const next = migrateOfficialUrl(cfg.api_url);
828
+ if (!next || next === cfg.api_url) return false;
829
+ cfg.api_url = next;
830
+ // 隧道地址只在它**也指向官方旧域名**时才改写:自建用户可能留着自定义 tunnel_url,
831
+ // 没有 local_key 的老配置也不能被顺手改掉(那会让桥连到别人的服务器)。
832
+ if (cfg.tunnel_url) {
833
+ const tunnel = migrateOfficialUrl(cfg.tunnel_url);
834
+ if (tunnel) cfg.tunnel_url = tunnel;
835
+ }
836
+ saveConfig(relayDir, cfg);
837
+ ctx?.logger?.info?.("dsh-remote-web: 云端地址已迁移到企业域名 dsh.ruichengai.com");
838
+ return true;
839
+ } catch {
840
+ return false; // 迁移失败绝不影响装载
841
+ }
842
+ }
843
+
780
844
  // ---------- 安装/配对自检 + 配对信息备份(2026-10-17 事故) ----------
781
845
  //
782
846
  // 事故:用户本机 `~/.dsh-remote`(bridge 运行时 + **账号配对信息/设备密钥**)被误删 →
@@ -3826,7 +3890,7 @@ async function communityInfo(relayDir) {
3826
3890
  * 软着陆:public-config 不可达 / 非对象 / 字段缺失全部回兜底并 `ok:true`。
3827
3891
  * 协议链接**永远不该**成为购买流程的故障点(打不开也不影响旁边的「升级 / 续费」按钮)。
3828
3892
  */
3829
- export const DEFAULT_PURCHASE_AGREEMENT_URL = "https://n.risegao.cn:13443/relay-api/api/purchase-agreement";
3893
+ export const DEFAULT_PURCHASE_AGREEMENT_URL = "https://dsh.ruichengai.com:13443/relay-api/api/purchase-agreement";
3830
3894
 
3831
3895
  /** 从 public-config 解析协议 URL(纯函数,便于单测:优先级 + 安全过滤 + 降级一次看完)。 */
3832
3896
  export function purchaseAgreementUrlFrom(publicConfig) {
@@ -9662,6 +9726,9 @@ export function apply(ctx, config = {}) {
9662
9726
  // 记住宿主 ctx:上游地址要问 dsh web 自己的监听端口(见 upstreamUrl)。
9663
9727
  // 存 ctx 而不是当场取端口:apply 可能早于 webServer.listen 完成,端口那时还是 0/undefined。
9664
9728
  webServerCtx = ctx;
9729
+ // 2026-11-14 企业域名迁移:存量配置里的官方旧域名(n.risegao.cn)一次性改写到
9730
+ // dsh.ruichengai.com。幂等;自建模式(有 local_key)一律不动。
9731
+ migrateLegacyConfigAtBoot(relayDir, ctx);
9665
9732
  // 上游地址落盘(0.6.12):系统自启拉起的守护没有本进程的 env,只认这个文件。
9666
9733
  publishUpstream(relayDir);
9667
9734
  // 【0.6.15】问不到自己监听端口时(飞牛/NAS 实测就是这种情况)主动发现一次并固化:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-web",
3
- "version": "0.8.2-beta.2",
3
+ "version": "0.8.2",
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",
@@ -189,7 +189,7 @@ let UPSTREAM = (() => {
189
189
  // 微信机器人通道总开关:DSH_WECHAT=0 关闭(默认开启)。控制面只 bind 回环,且必须带 bridge_secret。
190
190
  const WECHAT_DISABLED = String(process.env.DSH_WECHAT || "") === "0";
191
191
  // 默认云端服务地址(dsh-remote setup 会显式传入;自建模式无需账号 API)
192
- const API_BASE = (process.env.DSH_BRIDGE_API || "https://n.risegao.cn:13443/relay-api").replace(/\/+$/, "");
192
+ const API_BASE = (process.env.DSH_BRIDGE_API || "https://dsh.ruichengai.com:13443/relay-api").replace(/\/+$/, "");
193
193
  const EMAIL = process.env.DSH_BRIDGE_EMAIL || "";
194
194
  // 手机号优先;兼容旧的 DSH_BRIDGE_EMAIL(过渡期)
195
195
  const PHONE = process.env.DSH_BRIDGE_PHONE || EMAIL;
@@ -908,7 +908,7 @@ const SCRIPT = `(() => {
908
908
  void 0;
909
909
 
910
910
 
911
- /* —— 主机门解除(2026-09):镜像页(如 n.risegao.cn)经设备流认证回连同一台 127.0.0.1:3080,
911
+ /* —— 主机门解除(2026-09):镜像页(如 n.risegao.cn / dsh.ruichengai.com)经设备流认证回连同一台 127.0.0.1:3080,
912
912
  桥已把 Host 回环化并剥 Origin(不扩大信任面)。官方客户端以 transport.ownsHost 判定
913
913
  「自有主机」,非回环域名下不置 true 会把设置页置为 memory(不可用)镜像 →
914
914
  模型/提供方设置无法加载(“Settings are unavailable in this browser”)。
@@ -328,7 +328,8 @@
328
328
  * 本页面同时被 SaaS 与自建部署使用;自建部署(其他域名)不加载、不采集任何数据。 */
329
329
  var _hmt = _hmt || [];
330
330
  (function() {
331
- if (location.hostname !== "n.risegao.cn") return; // 非官方 SaaS 域名:跳过采集
331
+ // 官方 SaaS 域名集合(2026-11-14 企业域名迁移:新旧域名并存,两个都算官方)
332
+ if (["n.risegao.cn", "dsh.ruichengai.com"].indexOf(location.hostname) === -1) return; // 非官方 SaaS 域名:跳过采集
332
333
  var hm = document.createElement("script");
333
334
  hm.src = "https://hm.baidu.com/hm.js?9ec1d21cfebe4404b427e8fb44289f42";
334
335
  var s = document.getElementsByTagName("script")[0];
@@ -346,7 +347,7 @@ var _hmt = _hmt || [];
346
347
  <button id="btn-logout" class="hidden">退出</button>
347
348
  <span id="dsh-e2ee-badge" class="hidden" title="端到端加密状态"></span>
348
349
  <div class="spacer"></div>
349
- <a class="nav-link" href="https://n.risegao.cn:8085/" target="_blank" rel="noopener">官网</a>
350
+ <a class="nav-link" href="https://dsh.ruichengai.com:8085/" target="_blank" rel="noopener">官网</a>
350
351
  </header>
351
352
 
352
353
  <main id="view-main">
@@ -44,9 +44,29 @@ const CONFIG_DIR = process.env.DSH_RELAY_DIR || path.join(os.homedir(), ".dsh-re
44
44
  const CONFIG_PATH = path.join(CONFIG_DIR, ".dsh-config.json");
45
45
  // 默认云端服务地址(服务商 SaaS 入口;自建用户用 --server/--key 指向自己的 router)
46
46
  // DSH_RELAY_DEFAULT_API 可覆盖(测试脚本指向本地死端口,避免兜底路径打到生产)。
47
+ // 【2026-11-14 企业域名迁移】官方 SaaS 入口 → dsh.ruichengai.com(旧域名仍在服役,双域名并存)
47
48
  const DEFAULT_API = String(process.env.DSH_RELAY_DEFAULT_API || "").trim()
48
- || "https://n.risegao.cn:13443/relay-api";
49
- const DEFAULT_APP_URL = "https://n.risegao.cn:13443/app/";
49
+ || "https://dsh.ruichengai.com:13443/relay-api";
50
+ const DEFAULT_APP_URL = "https://dsh.ruichengai.com:13443/app/";
51
+
52
+ /** 官方云端「旧域名 → 新域名」映射。只认 hostname 精确匹配,自建地址原样返回。 */
53
+ const OFFICIAL_HOST_MIGRATION = new Map([["n.risegao.cn", "dsh.ruichengai.com"]]);
54
+
55
+ /** 把官方旧域名改写成新域名(保留端口/路径);其它地址原样返回。与插件半同规则。 */
56
+ function migrateOfficialUrl(raw) {
57
+ const s = String(raw == null ? "" : raw).trim();
58
+ if (!s) return s;
59
+ let u;
60
+ try {
61
+ u = new URL(s);
62
+ } catch {
63
+ return s;
64
+ }
65
+ const to = OFFICIAL_HOST_MIGRATION.get(u.hostname);
66
+ if (!to) return s;
67
+ u.hostname = to;
68
+ return u.toString().replace(/\/+$/, "");
69
+ }
50
70
  const REPO_URL = "https://github.com/mrRisega/dsh-remote";
51
71
 
52
72
  // ---------- 平台(0.6.7 起支持 Windows) ----------
@@ -538,7 +558,9 @@ function deriveTunnelUrl(apiUrl) {
538
558
  function applySaaSMode(cfg) {
539
559
  delete cfg.local_key;
540
560
  delete cfg.server; // 旧字段兜底
541
- cfg.api_url = String(cfg.api_url || DEFAULT_API).replace(/\/+$/, "");
561
+ // 2026-11-14 企业域名迁移:存量配置里的官方旧域名顺手改写(幂等,自建地址不动)
562
+ const api = migrateOfficialUrl(String(cfg.api_url || DEFAULT_API).replace(/\/+$/, ""));
563
+ cfg.api_url = api || DEFAULT_API;
542
564
  cfg.tunnel_url = deriveTunnelUrl(cfg.api_url);
543
565
  return cfg;
544
566
  }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * 官方域名迁移契约(2026-11-14 企业域名迁移:n.risegao.cn → dsh.ruichengai.com)
3
+ *
4
+ * 为什么必须钉住:
5
+ * ① 默认值散在三处(插件半 lib/index.js、桥半 dsh-bridge.mjs、安装器 dsh-setup.mjs),
6
+ * 只改一处就会出现"新装用新域名、却把用户带去旧域名"的半吊子状态;
7
+ * ② 存量用户配置里写的是旧域名,只改默认值等于**老用户永远不迁移**;
8
+ * ③ 迁移只允许动「官方旧域名」,自建地址一旦被顺手改写,用户会直接连不上自己的服务器。
9
+ *
10
+ * 因此这里同时钉:纯函数行为、三处默认值一致、以及 apply() 里确实调用了装载期迁移。
11
+ */
12
+ import { test } from "node:test";
13
+ import assert from "node:assert/strict";
14
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
15
+ import os from "node:os";
16
+ import path from "node:path";
17
+ import { apply, migrateOfficialUrl } from "../lib/index.js";
18
+
19
+ const NEW = "dsh.ruichengai.com";
20
+
21
+ test("旧官方域名 → 新域名:端口/路径/查询串原样保留", () => {
22
+ assert.equal(
23
+ migrateOfficialUrl("https://n.risegao.cn:13443/relay-api"),
24
+ `https://${NEW}:13443/relay-api`
25
+ );
26
+ assert.equal(migrateOfficialUrl("https://n.risegao.cn:13443"), `https://${NEW}:13443`);
27
+ assert.equal(
28
+ migrateOfficialUrl("https://n.risegao.cn/relay-api?x=1"),
29
+ `https://${NEW}/relay-api?x=1`
30
+ );
31
+ });
32
+
33
+ test("幂等:已经是新域名时再迁移不变", () => {
34
+ const once = migrateOfficialUrl("https://n.risegao.cn:13443/relay-api");
35
+ assert.equal(migrateOfficialUrl(once), once);
36
+ });
37
+
38
+ test("自建/自定义地址一律原样返回(绝不能顺手改写用户自己的服务器)", () => {
39
+ for (const u of [
40
+ `https://${NEW}:13443/relay-api`,
41
+ "https://relay.example.com/relay-api",
42
+ "https://relay.example.com:8443/relay-api",
43
+ "http://127.0.0.1:1/relay-api",
44
+ // 相似但不同的 host:必须不被当成官方旧域名
45
+ "https://n.risegao.cn.evil.com/relay-api",
46
+ "https://evil-n.risegao.cn/relay-api",
47
+ ]) {
48
+ assert.equal(migrateOfficialUrl(u), u, `${u} 不该被改写`);
49
+ }
50
+ });
51
+
52
+ test("空值/非法值原样返回,不抛错(迁移失败绝不能拖垮装载)", () => {
53
+ for (const v of [null, undefined, "", " ", "not a url", "n.risegao.cn:13443"]) {
54
+ assert.doesNotThrow(() => migrateOfficialUrl(v));
55
+ }
56
+ assert.equal(migrateOfficialUrl(""), "");
57
+ assert.equal(migrateOfficialUrl("not a url"), "not a url");
58
+ });
59
+
60
+ test("跨文件不变量:插件半/桥半/安装器的默认云端地址必须一致且是新域名", async () => {
61
+ const root = new URL("../../../", import.meta.url);
62
+ const files = {
63
+ 插件半: new URL("packages/dsh-remote-web/lib/index.js", root),
64
+ 桥半: new URL("clients/dsh-remote/dsh-bridge.mjs", root),
65
+ 安装器: new URL("dsh-setup.mjs", root),
66
+ };
67
+ for (const [label, url] of Object.entries(files)) {
68
+ const src = await readFile(url, "utf8");
69
+ assert.ok(
70
+ src.includes(`https://${NEW}:13443/relay-api`),
71
+ `${label} 的默认 relay-api 必须指向 ${NEW}`
72
+ );
73
+ assert.ok(
74
+ !src.includes('|| "https://n.risegao.cn:13443/relay-api"'),
75
+ `${label} 不得再把旧域名当默认值`
76
+ );
77
+ }
78
+ });
79
+
80
+ test("装载期迁移确实接在 apply() 上(否则老用户永远不迁移)", async () => {
81
+ const root = new URL("../../../", import.meta.url);
82
+ const src = await readFile(new URL("packages/dsh-remote-web/lib/index.js", root), "utf8");
83
+ assert.match(src, /migrateLegacyConfigAtBoot\(relayDir, ctx\)/, "apply() 必须调用装载期迁移");
84
+ assert.match(src, /OFFICIAL_HOST_MIGRATION = new Map\(\[\s*\["n\.risegao\.cn", "dsh\.ruichengai\.com"\]/, "迁移映射必须存在");
85
+ // 自建模式不能被动:有 local_key 时必须直接返回
86
+ assert.match(src, /if \(!cfg \|\| !cfg\.api_url \|\| cfg\.local_key\) return false;/, "自建模式必须跳过迁移");
87
+ });
88
+
89
+ /** 最小宿主 ctx:只提供 apply() 会用到的东西(与 community-qr-ui.test.mjs 同一套做法)。 */
90
+ function fakeHost() {
91
+ return {
92
+ webServer: { register() { return () => {}; } },
93
+ effect(reg) { return typeof reg === "function" ? reg() : () => {}; },
94
+ logger: { info() {}, warn() {} },
95
+ };
96
+ }
97
+
98
+ /**
99
+ * 隔离器:`apply()` 会做系统操作与 profile 路径推导,必须同时压住
100
+ * · DSH_RELAY_SKIP_SERVICE=1(绝不碰 launchd/systemd/npx)
101
+ * · HOME 指向临时目录(绝不动真实 ~/.dsh / ~/Library/LaunchAgents)
102
+ * 仓库的 setup-output.test.mjs 对这条有静态护栏,别改成 delete 而不伪造 HOME。
103
+ */
104
+ function isolateHostEnv(home) {
105
+ const saved = { HOME: process.env.HOME, SKIP: process.env.DSH_RELAY_SKIP_SERVICE };
106
+ process.env.HOME = home;
107
+ process.env.DSH_RELAY_SKIP_SERVICE = "1";
108
+ return () => {
109
+ if (saved.HOME === undefined) delete process.env.HOME;
110
+ else process.env.HOME = saved.HOME;
111
+ if (saved.SKIP === undefined) process.env.DSH_RELAY_SKIP_SERVICE = "1"; // 保持隔离,不强求 unset
112
+ else process.env.DSH_RELAY_SKIP_SERVICE = saved.SKIP;
113
+ };
114
+ }
115
+
116
+ async function withConfig(cfg) {
117
+ const dir = await mkdtemp(path.join(os.tmpdir(), "dsh-domain-mig-"));
118
+ const file = path.join(dir, ".dsh-config.json");
119
+ await writeFile(file, JSON.stringify(cfg, null, 2));
120
+ return { dir, file, cleanup: () => rm(dir, { recursive: true, force: true }) };
121
+ }
122
+
123
+ test("装载期迁移(端到端):存量旧域名配置被改写并落盘", async () => {
124
+ const t = await withConfig({
125
+ phone: "13800000000", password: "pw", device_id: "dev-x",
126
+ api_url: "https://n.risegao.cn:13443/relay-api",
127
+ tunnel_url: "wss://n.risegao.cn",
128
+ });
129
+ const restore = isolateHostEnv(t.dir);
130
+ try {
131
+ apply(fakeHost(), { relayDir: t.dir });
132
+ const cfg = JSON.parse(await readFile(t.file, "utf8"));
133
+ assert.equal(cfg.api_url, "https://dsh.ruichengai.com:13443/relay-api");
134
+ assert.equal(cfg.tunnel_url, "wss://dsh.ruichengai.com");
135
+ // 其它字段必须原样保留(迁移只动地址)
136
+ assert.equal(cfg.phone, "13800000000");
137
+ assert.equal(cfg.device_id, "dev-x");
138
+ } finally {
139
+ restore();
140
+ t.cleanup();
141
+ }
142
+ });
143
+
144
+ test("装载期迁移(端到端):自建模式与自定义地址一律不动", async () => {
145
+ const t = await withConfig({
146
+ phone: "13800000000", password: "pw",
147
+ local_key: "lk", tunnel_url: "wss://relay.example.com",
148
+ api_url: "https://n.risegao.cn:13443/relay-api",
149
+ });
150
+ const restore = isolateHostEnv(t.dir);
151
+ try {
152
+ apply(fakeHost(), { relayDir: t.dir });
153
+ const cfg = JSON.parse(await readFile(t.file, "utf8"));
154
+ assert.equal(cfg.api_url, "https://n.risegao.cn:13443/relay-api", "自建模式不得被改写");
155
+ assert.equal(cfg.tunnel_url, "wss://relay.example.com");
156
+ } finally {
157
+ restore();
158
+ t.cleanup();
159
+ }
160
+ });
@@ -21,7 +21,9 @@ import { apply, DEFAULT_PURCHASE_AGREEMENT_URL, purchaseAgreementUrlFrom } from
21
21
  const FALLBACK = DEFAULT_PURCHASE_AGREEMENT_URL;
22
22
 
23
23
  test("兜底常量就是业主约定的正式兜底地址(企业端 /api/purchase-agreement 的 302/200 入口)", () => {
24
- assert.equal(FALLBACK, "https://n.risegao.cn:13443/relay-api/api/purchase-agreement");
24
+ // 2026-11-14 企业域名迁移:兜底地址随官方域名一起迁到 dsh.ruichengai.com
25
+ // (同一套企业端后端,路径不变;旧域名在过渡期仍在服役,但不再是"约定地址")
26
+ assert.equal(FALLBACK, "https://dsh.ruichengai.com:13443/relay-api/api/purchase-agreement");
25
27
  });
26
28
 
27
29
  test("纯函数:public-config.agreement_url 优先;非 http(s)/相对/缺失一律回兜底", () => {