@mrrisega/dsh-remote 0.7.0-beta.7 → 0.7.0

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.
Files changed (68) hide show
  1. package/clients/dsh-remote/dsh-bridge.mjs +17 -17
  2. package/clients/dsh-remote/dsh-events.mjs +6 -6
  3. package/clients/dsh-remote/e2ee-client.mjs +7 -7
  4. package/clients/dsh-remote/e2ee-shim-script.js +9 -9
  5. package/clients/dsh-remote/mobile-adapter.mjs +21 -21
  6. package/clients/dsh-remote/test/bridge-gzip.test.mjs +1 -1
  7. package/clients/dsh-remote/test/device-registration-report.test.mjs +1 -1
  8. package/clients/dsh-remote/test/dsh-events.test.mjs +10 -10
  9. package/clients/dsh-remote/test/e2ee-bridge.test.mjs +4 -4
  10. package/clients/dsh-remote/test/e2ee-client.test.mjs +7 -7
  11. package/clients/dsh-remote/test/e2ee-shim.test.mjs +1 -1
  12. package/clients/dsh-remote/test/e2ee-wecrypto-parity.test.mjs +1 -1
  13. package/clients/dsh-remote/test/harness-cookie-authority.test.mjs +4 -4
  14. package/clients/dsh-remote/test/machine-fingerprint.test.mjs +2 -2
  15. package/clients/dsh-remote/test/mobile-adapter-guards.test.mjs +6 -6
  16. package/clients/dsh-remote/test/mobile-adapter-image.test.mjs +1 -1
  17. package/clients/dsh-remote/test/mobile-adapter-runtime.test.mjs +5 -5
  18. package/clients/dsh-remote/test/upstream-discovery.test.mjs +1 -1
  19. package/clients/dsh-remote/test/wechat-channel.test.mjs +5 -5
  20. package/clients/dsh-remote/test/wechat-runtime.test.mjs +26 -26
  21. package/clients/dsh-remote/test/wechat-tier-source.test.mjs +2 -2
  22. package/clients/dsh-remote/wechat-channel.mjs +9 -9
  23. package/clients/dsh-remote/wechat-runtime.mjs +38 -38
  24. package/docs/telemetry.md +3 -3
  25. package/dsh-setup.mjs +63 -26
  26. package/package.json +2 -2
  27. package/packages/dsh-remote-web/lib/client.js +56 -56
  28. package/packages/dsh-remote-web/lib/fix-prompt.js +1 -1
  29. package/packages/dsh-remote-web/lib/index.js +43 -43
  30. package/packages/dsh-remote-web/lib/tarball-update.js +1 -1
  31. package/packages/dsh-remote-web/package.json +1 -1
  32. package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +17 -17
  33. package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-events.mjs +6 -6
  34. package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-client.mjs +7 -7
  35. package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-shim-script.js +9 -9
  36. package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +21 -21
  37. package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-channel.mjs +9 -9
  38. package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-runtime.mjs +38 -38
  39. package/packages/dsh-remote-web/runtime/clients/dsh-web/native.html +5 -5
  40. package/packages/dsh-remote-web/runtime/dsh-setup.mjs +63 -26
  41. package/packages/dsh-remote-web/test/account-switch-device.test.mjs +2 -2
  42. package/packages/dsh-remote-web/test/activation-single-point.test.mjs +1 -1
  43. package/packages/dsh-remote-web/test/announcements-ui.test.mjs +1 -1
  44. package/packages/dsh-remote-web/test/connect-loop.test.mjs +1 -1
  45. package/packages/dsh-remote-web/test/connect-ui.test.mjs +2 -2
  46. package/packages/dsh-remote-web/test/desktop-compat.test.mjs +4 -4
  47. package/packages/dsh-remote-web/test/diag-report.test.mjs +1 -1
  48. package/packages/dsh-remote-web/test/fix-prompt.test.mjs +2 -2
  49. package/packages/dsh-remote-web/test/harness-restart.test.mjs +1 -1
  50. package/packages/dsh-remote-web/test/install-profile-targets.test.mjs +6 -4
  51. package/packages/dsh-remote-web/test/installer-cli.test.mjs +1 -1
  52. package/packages/dsh-remote-web/test/launchd-domain.test.mjs +2 -2
  53. package/packages/dsh-remote-web/test/patch-activation.test.mjs +2 -2
  54. package/packages/dsh-remote-web/test/picker-pin.test.mjs +1 -1
  55. package/packages/dsh-remote-web/test/plugin-manifest.test.mjs +2 -2
  56. package/packages/dsh-remote-web/test/quota-absent.test.mjs +4 -4
  57. package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +2 -2
  58. package/packages/dsh-remote-web/test/settings-entry-placement.test.mjs +6 -6
  59. package/packages/dsh-remote-web/test/settings-entry.test.mjs +2 -2
  60. package/packages/dsh-remote-web/test/settings-open-path.test.mjs +4 -4
  61. package/packages/dsh-remote-web/test/setup-output.test.mjs +32 -9
  62. package/packages/dsh-remote-web/test/tarball-update.test.mjs +1 -1
  63. package/packages/dsh-remote-web/test/telemetry.test.mjs +4 -4
  64. package/packages/dsh-remote-web/test/uninstall-safety.test.mjs +2 -2
  65. package/packages/dsh-remote-web/test/watcher-upstream-port.test.mjs +1 -1
  66. package/packages/dsh-remote-web/test/wechat-bot-ui.test.mjs +7 -7
  67. package/packages/dsh-remote-web/test/windows-compat.test.mjs +5 -5
  68. package/scripts/test-net-guard.cjs +1 -1
@@ -15,7 +15,7 @@
15
15
  // (no_account/no_runtime/installing/starting/connecting/online/error),并在返回前自动补装运行环境、
16
16
  // 拉起 bridge、清理卡死标记(带退避);区分「bridge 进程在跑」与「设备已在中继注册成功(online)」。
17
17
  // 面板用 2~3s 短轮询自动推进到 online(无需点按钮/刷新页面),error 才给重试与诊断信息;
18
- // 另 POST /dsh-remote/connect/retry 手动重试、POST <api>/api/install-report 上报安装口径
18
+ // 另 POST /dsh-remote/connect/retry 手动重试、POST <api>/api/install-report 上报安装约定
19
19
  // (install_source/version + host_os/arch,详见 installSourceOf)。
20
20
  // - 0.1.2+ ?token 浏览器鉴权会话代持(0.4.1 起)
21
21
  //
@@ -90,14 +90,14 @@ const DEFAULT_RELAY_DIR = process.env.DSH_RELAY_DIR || join(homedir(), ".dsh-rem
90
90
  // 默认云端服务地址(SaaS 入口;自建用户在设置页/面板切换)。
91
91
  // DSH_RELAY_DEFAULT_API 只改「配置里根本没写 api_url」时的兜底:测试脚本把它指向本地死端口,
92
92
  // 这样任何**在临时目录被拆掉之后**才跑到的后台请求(如装机上报的重试)都不会打生产。
93
- // 真实事故形态见 test/wechat-bind-telemetry.test.mjs 的注释:用例拆目录 → 读到空配置 → 回落生产。
93
+ // 真实故障形态见 test/wechat-bind-telemetry.test.mjs 的注释:用例拆目录 → 读到空配置 → 回落生产。
94
94
  const DEFAULT_API = String(process.env.DSH_RELAY_DEFAULT_API || "").trim()
95
95
  || "https://n.risegao.cn:13443/relay-api";
96
96
  const DEFAULT_APP_URL = "https://n.risegao.cn:13443/app/";
97
97
 
98
98
  // ---------- 上游 dsh web 地址(bridge/watcher 都要连它,绝不写死 3080) ----------
99
99
  //
100
- // 【2026-09-23 用户实测事故】这里(以及运行时的 checkUpstream)原本把上游**写死**
100
+ // 【2026-09-23 用户实测故障】这里(以及运行时的 checkUpstream)原本把上游**写死**
101
101
  // `http://127.0.0.1:3080`:只要用户的 dsh web 不在默认端口(`dsh web --port 8090`、
102
102
  // profile 里配了 port、或 `--port 0` 让系统分配空闲端口),watcher 的「dsh web 在线吗」
103
103
  // 探测就永远失败 → **bridge 永远不启动**,而面板只会说「正在启动 Bridge…」(错误为空)
@@ -491,7 +491,7 @@ function npxInvocation() {
491
491
  /** 子进程环境:把 node 目录补进 PATH(npx 及其 shebang 需要),可附加额外变量。 */
492
492
  function spawnEnv(extra) {
493
493
  // ⚠️ 这里刻意用 **process.execPath**,不是 NODE_BIN:PATH 首位的用途是让 npx 的
494
- // `#!/usr/bin/env node` 解析到**正在跑本插件的那个 node**(与 npxInvocation 同口径)。
494
+ // `#!/usr/bin/env node` 解析到**正在跑本插件的那个 node**(与 npxInvocation 同约定)。
495
495
  // NODE_BIN 回答的是另一个问题——"自启动服务里该登记哪个解释器"(真实 node 优先)。
496
496
  // 两者语义不同,混用会悄悄改掉 web 版上 npx 用哪个 node。
497
497
  const nodeDir = dirname(process.execPath);
@@ -759,9 +759,9 @@ function fetchFailReason(e) {
759
759
  }
760
760
 
761
761
  /**
762
- * Cookie 按 **authority 分文件**(0.7.0-beta.2 起,2026-10-09 事故根治)。
762
+ * Cookie 按 **authority 分文件**(0.7.0-beta.2 起,2026-10-09 故障根治)。
763
763
  *
764
- * 【事故】`~/.dsh-remote/` 只有一份 `.harness-cookie.json`,而 Web 端(3080)与桌面端(每次启动
764
+ * 【故障】`~/.dsh-remote/` 只有一份 `.harness-cookie.json`,而 Web 端(3080)与桌面端(每次启动
765
765
  * 随机端口)是**两个宿主进程**,各自的插件都往里写"自己 authority 的 Cookie" → 互相覆盖。
766
766
  * 桥撞 401 后重读磁盘,拿到的是另一个宿主刚写的 Cookie → 重试 → 还是 401 → 再读 → 又被
767
767
  * 覆盖回去……**乒乓,永不自愈**,手机端持续白页(现象与"自愈坏了"一模一样,极易误判)。
@@ -782,7 +782,7 @@ function harnessCookieFileFor(authority) {
782
782
  function harnessCookieRevokedFileFor(authority) {
783
783
  return `${HARNESS_COOKIE_REVOKED_FILE}.${authoritySlug(authority)}`;
784
784
  }
785
- /** 由本进程 webServer 端口推出 authority(与 bridge 的 Host 口径一致)。 */
785
+ /** 由本进程 webServer 端口推出 authority(与 bridge 的 Host 约定一致)。 */
786
786
  function harnessAuthorityOfPort(port) {
787
787
  return port ? `127.0.0.1:${port}` : "";
788
788
  }
@@ -832,7 +832,7 @@ async function mintHarnessCookie(ctx, relayDir) {
832
832
  if (UNINSTALLED_DIRS.has(relayDir)) return false;
833
833
  const out = { authority: `127.0.0.1:${port}`, cookie, mintedAt: Date.now() };
834
834
  mkdirSync(dirname(configPathOf(relayDir)), { recursive: true });
835
- // ① 本宿主专属文件(权威,两个宿主互不覆盖 —— 事故根治点)
835
+ // ① 本宿主专属文件(权威,两个宿主互不覆盖 —— 故障根治点)
836
836
  const ownPath = join(relayDir, harnessCookieFileFor(out.authority));
837
837
  writeFileSync(ownPath, JSON.stringify(out, null, 2), { mode: 0o600 });
838
838
  hardenFile(ownPath); // 会话 Cookie ≈ dsh web 的完整访问权,同样要收紧
@@ -1003,7 +1003,7 @@ function runtimeManifest(relayDir) {
1003
1003
  /**
1004
1004
  * 桌面运行环境缺失/不完整的文件清单(空 = 就绪)。
1005
1005
  *
1006
- * 【2026-09-23 用户实测事故】判据原本是「`dsh-setup.mjs` 存在即就绪」——**存在 ≠ 能加载**。
1006
+ * 【2026-09-23 用户实测故障】判据原本是「`dsh-setup.mjs` 存在即就绪」——**存在 ≠ 能加载**。
1007
1007
  * npx 补装是「先拷入口脚本、再递归拷 clients/」,中途被打断(用户关窗口 / 杀软锁文件 /
1008
1008
  * 机器休眠 / 看门狗判超时结束进程)就会留下一个**只有入口脚本的半装运行时**。此后:
1009
1009
  * · runtimeReady() 恒为 true → 插件再也不补装(自愈彻底停摆);
@@ -1049,7 +1049,7 @@ function launchAgentPath() {
1049
1049
  /**
1050
1050
  * ★ 这份自启动 plist 是否**确实是为本 relayDir 装的**(内容里写着 `<relayDir>/dsh-setup.mjs`)。
1051
1051
  *
1052
- * 🔴 2026-10-10 事故(两次,同一根因):`reapBrokenAutostart()` 用**当前 relayDir** 判断
1052
+ * 🔴 2026-10-10 故障(两次,同一根因):`reapBrokenAutostart()` 用**当前 relayDir** 判断
1053
1053
  * "入口脚本缺失"(测试的临时 relayDir 当然没有 dsh-setup.mjs → 判 true),
1054
1054
  * 但删除的却是 `launchAgentPath()` = **真实 HOME 下那份 plist** → 把开发者本机的自启动
1055
1055
  * 连 bootout 带删文件一起干掉了(远程访问中断,且日志里看不出异常)。
@@ -1136,7 +1136,7 @@ WantedBy=default.target
1136
1136
  /**
1137
1137
  * Linux:拉起 bridge 的阶梯(**与 macOS 同构:可自愈优先,可用兜底**)。
1138
1138
  *
1139
- * 【0.6.15 事故根因】0.6.14 及更早,`startBridge` 只认 macOS 的 launchd,Linux 直接返回
1139
+ * 【0.6.15 故障根因】0.6.14 及更早,`startBridge` 只认 macOS 的 launchd,Linux 直接返回
1140
1140
  * `unsupported: 当前平台(linux)暂不支持自启动服务`。于是:
1141
1141
  * · 自愈轮次每轮都调用它、每轮都拿到 unsupported、**bridge 永远起不来**;
1142
1142
  * · 用户面板上的表现是永远停在「正在启动 Bridge…」+「bridge 进程: 未运行」;
@@ -1188,7 +1188,7 @@ function startBridgeLinux(relayDir) {
1188
1188
  /**
1189
1189
  * launchd 域名阶梯(**必须与 dsh-setup.mjs 一致**)。
1190
1190
  *
1191
- * 事故(2026-09-19 实测):安装器按 macOS 26 的修复把作业装在 **`user/<uid>`** 域
1191
+ * 故障(2026-09-19 实测):安装器按 macOS 26 的修复把作业装在 **`user/<uid>`** 域
1192
1192
  * (gui 域会被系统置为 on-demand-only,RunAtLoad/KeepAlive 失效),
1193
1193
  * 但插件这边只认 `gui/<uid>` → 切换账号时那次"重启 bridge"实际执行的是
1194
1194
  * launchctl bootout gui/501/com.dshremote.bridge → 找不到
@@ -1449,7 +1449,7 @@ function appendLogLine(relayDir, name, line) {
1449
1449
  * 注意:默认优先官方源——镜像(npmmirror)可能滞后于刚发布的版本,装到旧版会把
1450
1450
  * 已被 0.4.5 移除的“用户 include”重新写回 profile(历史上造成 dsh web 重复 ID 崩溃)。 */
1451
1451
  /**
1452
- * 补装失败退避(2026-09-15 生产事故)。
1452
+ * 补装失败退避(2026-09-15 生产故障)。
1453
1453
  *
1454
1454
  * 背景:Windows 用户补装 `npx @mrrisega/dsh-remote` 持续失败时,自愈调度每轮都重试,
1455
1455
  * 生产遥测里看到单机 12 秒一次、累计 606 次的失败风暴(1659 次 install_failed 全在 Windows)。
@@ -1673,7 +1673,7 @@ function ensureRuntime(relayDir) {
1673
1673
  /**
1674
1674
  * 生成 plist(与 dsh-setup.mjs 的 writeAutostartFile **必须同构**),返回路径。
1675
1675
  *
1676
- * ⚠️ 两份模板历史上漂移过、并因此各造成一个真机事故(插件每次自愈/启动都会重写 plist,
1676
+ * ⚠️ 两份模板历史上漂移过、并因此各造成一个真机故障(插件每次自愈/启动都会重写 plist,
1677
1677
  * 所以**插件这份谁写谁说了算**,漂移等于把安装器那份正确的覆盖掉):
1678
1678
  * · 漏 `/usr/sbin`、`/sbin`(SERVICE_PATH)→ bridge 找不到 `ioreg` → 机器指纹静默退化成
1679
1679
  * hostname 哈希(改一次主机名就被当成新设备);
@@ -2007,7 +2007,7 @@ function stopBridge(relayDir) {
2007
2007
  }
2008
2008
  const targets = launchTargets();
2009
2009
  if (!targets.length) return { ok: false, status: "unsupported", detail: `当前平台(${osPlatform()})没有 launchd 服务可停,请手动结束 \`dsh-remote run\` 进程` };
2010
- // 两个域都要停:作业可能由安装器装在 user/<uid>,而旧版插件只会去 gui/<uid> 找不到(实测事故)
2010
+ // 两个域都要停:作业可能由安装器装在 user/<uid>,而旧版插件只会去 gui/<uid> 找不到(实测故障)
2011
2011
  let lastErr = "";
2012
2012
  for (const target of targets) {
2013
2013
  const r = sh(`launchctl bootout ${target}`);
@@ -2065,7 +2065,7 @@ function uninstallRuntime(relayDir, protectedPath) {
2065
2065
  // a) 停服务 + 移除自启动文件
2066
2066
  try {
2067
2067
  if (isDarwin()) {
2068
- /* 🔴 2026-10-10 事故护栏:**relayDir 不在当前 HOME 之下时,绝不碰真实自启动**。
2068
+ /* 🔴 2026-10-10 故障护栏:**relayDir 不在当前 HOME 之下时,绝不碰真实自启动**。
2069
2069
  当天某个用例用「真 HOME + 临时 relayDir」调到了这里:plist 路径解析到真实的
2070
2070
  ~/Library/LaunchAgents/com.dshremote.bridge.plist → 先 bootout 真桥、再把 plist 删了
2071
2071
  → **开发者本机的远程访问被测试整个卸掉**(job 消失、桥停摆)。
@@ -2085,7 +2085,7 @@ function uninstallRuntime(relayDir, protectedPath) {
2085
2085
  // 只处理「plist 位于当前 HOME」的服务:本插件/dsh-setup.mjs 安装的服务一定在此
2086
2086
  const plistPath = launchAgentPath();
2087
2087
  if (plistPath && existsSync(plistPath)) {
2088
- /* ★★ 归属校验(2026-10-10 事故的最终护栏):
2088
+ /* ★★ 归属校验(2026-10-10 故障的最终护栏):
2089
2089
  这份 plist 必须**确实是为本 relayDir 装的**(内容里写着 <relayDir>/dsh-setup.mjs)。
2090
2090
  不校验的话,只要有用例拿「真 HOME + 临时 relayDir」走到这里,就会把开发者本机
2091
2091
  真实的服务**先 bootout 再删文件**(当天实测发生:桥停摆、远程访问中断)。
@@ -3240,7 +3240,7 @@ async function proxyFeedback(relayDir, req, res, pathname) {
3240
3240
 
3241
3241
  /**
3242
3242
  * 把 `/dsh-remote/announcements*` 代理到云端公告 API(契约冻结见
3243
- * dsh-relay-enterprise/docs/announcements-contract.md §3):
3243
+ * 服务端公告契约文档(冻结 v1) §3):
3244
3244
  * · GET /dsh-remote/announcements → 上游 GET {api}/api/announcements?client=desktop[&install_id=…]
3245
3245
  * · POST /dsh-remote/announcements/click → 上游 POST {api}/api/announcements/click {id[, install_id]}
3246
3246
  *
@@ -3251,7 +3251,7 @@ async function proxyFeedback(relayDir, req, res, pathname) {
3251
3251
  * 两者**同时**带上会让唯一索引 (announcement_id, user_id, install_id) 把"同一人两台机器"
3252
3252
  * 算成两行 —— UV 直接翻倍。所以下面是有 Bearer 就不带 install_id,反之亦然。
3253
3253
  * ② **install_id 复用本机既有的 device_id,绝不新造 ID**:服务端那个维度就是"这台电脑",
3254
- * 再生成一个 ID 只会让同一台机器在统计里变成两台(且新 ID 与反馈/设备列表的口径对不上)。
3254
+ * 再生成一个 ID 只会让同一台机器在统计里变成两台(且新 ID 与反馈/设备列表的约定对不上)。
3255
3255
  * 注意 `cfg.device_id` 是**本机设备标识**,与遥测的 `.telemetry-install-id` 不是一回事:
3256
3256
  * 后者在 DSH_REMOTE_TELEMETRY=0 时根本不生成,公告不该被遥测开关牵连。
3257
3257
  * ③ **失败一律软着陆**:上游 404(老服务端)/ 不可达 / 超时都只回一个 JSON,**绝不抛**。
@@ -3271,7 +3271,7 @@ async function proxyAnnouncements(relayDir, req, res, pathname) {
3271
3271
  }
3272
3272
 
3273
3273
  const headers = { "x-dsh-client": `dsh-remote-web/${PLUGIN_VERSION}` };
3274
- // 浏览器已带的 Bearer 优先透传(与 proxyFeedback 同口径:那是请求方自己的身份);
3274
+ // 浏览器已带的 Bearer 优先透传(与 proxyFeedback 同约定:那是请求方自己的身份);
3275
3275
  // 否则由 node 半附当前账号 JWT —— 只有"确实配了账号"才值得为此跑一次 device-login。
3276
3276
  const auth = req.headers.authorization;
3277
3277
  let bearer = "";
@@ -3497,7 +3497,7 @@ async function fetchDistTags() {
3497
3497
  return {};
3498
3498
  }
3499
3499
 
3500
- /** 在「当前通道」与 latest 之间取更高者(**更新与"检查更新"必须用同一口径**,否则面板会说"已是最新"而更新却装新版)。 */
3500
+ /** 在「当前通道」与 latest 之间取更高者(**更新与"检查更新"必须用同一约定**,否则面板会说"已是最新"而更新却装新版)。 */
3501
3501
  function pickBestTag(tags) {
3502
3502
  const t = tags || {};
3503
3503
  const channelVer = typeof t[UPDATE_TAG] === "string" ? t[UPDATE_TAG] : "";
@@ -3511,7 +3511,7 @@ function pickBestTag(tags) {
3511
3511
  return { tag: UPDATE_TAG, version: channelVer, why: `当前通道 ${UPDATE_TAG}(${channelVer}) 不低于 latest(${latestVer})` };
3512
3512
  }
3513
3513
 
3514
- /** 查询"对我而言"的最新版本号(口径同 pickBestTag)。 */
3514
+ /** 查询"对我而言"的最新版本号(约定同 pickBestTag)。 */
3515
3515
  async function npmLatestVersion() {
3516
3516
  return pickBestTag(await fetchDistTags()).version;
3517
3517
  }
@@ -3607,7 +3607,7 @@ function spawnUpdater(relayDir, extraEnv, onError, spec = UPDATE_SPEC) {
3607
3607
  // Windows 上跑 npx.cmd 必须经 shell(Node ≥20.12 起否则 EINVAL);走 node+npx-cli.js 时不需要。
3608
3608
  shell: npx.shell,
3609
3609
  windowsHide: true, // Windows:不弹控制台窗口(POSIX 上该选项被忽略)
3610
- // PATH 补 node 目录:App 最小 PATH 下也能跑 npx;同时保持本机既有的安装来源口径
3610
+ // PATH 补 node 目录:App 最小 PATH 下也能跑 npx;同时保持本机既有的安装来源约定
3611
3611
  // (在线更新会重跑安装器并重写 plist,若不带来源就会把 plugin_market 误记成 npx)。
3612
3612
  env: spawnEnv({
3613
3613
  DSH_BRIDGE_INSTALL_SOURCE: installSourceOf(relayDir),
@@ -3877,7 +3877,7 @@ function rotateUpdateLog(relayDir) {
3877
3877
  /**
3878
3878
  * 判断 patch 文本是否是「合法的顶层 YAML 数组文档」。
3879
3879
  *
3880
- * 为什么必须判(2026-09-18 Windows 用户实测的致命事故):dsh 解析 cordis.patch.yml 时
3880
+ * 为什么必须判(2026-09-18 Windows 用户实测的致命故障):dsh 解析 cordis.patch.yml 时
3881
3881
  * **要求顶层是数组**,而**纯注释文档的 YAML 解析结果是 `null` 而不是空数组**,于是 dsh 直接:
3882
3882
  * `Error: dsh: overlay …/cordis.patch.yml must be a top-level YAML array of loader patch entries`
3883
3883
  * 官方空 profile 模板是「注释 + `[]`」——`[]` 这个占位符不能省。
@@ -3907,7 +3907,7 @@ function stripOwnEntriesFromPatch(patch) {
3907
3907
  // ① 管理标记块(dsh-setup 写入的形式)
3908
3908
  // 结尾标记后面可能还有残留文字(历史变体),用 [^\n]* 吃掉整行,避免留下半截内容
3909
3909
  const out = String(patch ?? "").replace(/\n?# >>> dsh-remote-(?:web|ui)[^\n]*\n[\s\S]*?# <<< dsh-remote-(?:web|ui)[^\n]*\n?/g, "\n");
3910
- // ② 无标记的 insert 块(旧版 / 其它工具写入的形式,历史事故里出现过)。
3910
+ // ② 无标记的 insert 块(旧版 / 其它工具写入的形式,历史故障里出现过)。
3911
3911
  // 只摘标记块是不够的:目录已删而 patch 仍引用 → dsh 插件树加载失败 → 整个 dsh web 起不来。
3912
3912
  const lines = out.split("\n");
3913
3913
  const kept = [];
@@ -3949,7 +3949,7 @@ function writePatchDocument(patchFile, text) {
3949
3949
  /**
3950
3950
  * 「运行时清理助手」源码(卸载用;由插件生成、detached 执行)。
3951
3951
  *
3952
- * 为什么必须是独立进程(2026-09-18 用户实测的僵死事故):
3952
+ * 为什么必须是独立进程(2026-09-18 用户实测的僵死故障):
3953
3953
  * uninstallRuntime() 里有 spawnSync("schtasks"/"powershell"/"taskkill"),同步子进程调用会把
3954
3954
  * **dsh web 的事件循环整个钉死** —— 不只是这一个请求不返回,手机端隧道、其它页面、所有 API
3955
3955
  * 一起停摆,表现为「端口在听、连接建立、但永不响应」。而 spawnSync 的 timeout 在 Windows 上
@@ -3959,7 +3959,7 @@ function writePatchDocument(patchFile, text) {
3959
3959
  * 助手做的是「宁可少做也不卡住」的有界清理:杀残留进程 → 删自启动 → 清配置目录,每步独立 try/catch。
3960
3960
  */
3961
3961
  function buildRuntimeCleanupHelper(relayDir, protectedPath) {
3962
- /* ★ 归属校验(2026-10-10 事故第三条路径):计划里的 plistPath 用的是**当前 homedir**,
3962
+ /* ★ 归属校验(2026-10-10 故障第三条路径):计划里的 plistPath 用的是**当前 homedir**,
3963
3963
  而 relayDir 来自调用方 —— 两者不必是同一个目录。曾有测试用「真 HOME + 临时 relayDir」
3964
3964
  走到这里,于是这个 detached 助手把**开发者本机真实的自启动 plist 删了**(异步删,
3965
3965
  所以在进程内插桩根本抓不到)。现在:plist 里没写着本 relayDir 的入口脚本 → **不给路径**,
@@ -3986,7 +3986,7 @@ import { dirname, join, sep } from "node:path";
3986
3986
 
3987
3987
  const PLAN = ${plan};
3988
3988
  // 与主进程一致的测试隔离开关:置位时**绝不做任何真实系统操作**(不碰 launchctl/systemctl/schtasks/taskkill),
3989
- // 但仍清空配置目录 —— 否则用例会去动开发者本机真实的自启动服务(历史事故,见 setup-output 用例的护栏)。
3989
+ // 但仍清空配置目录 —— 否则用例会去动开发者本机真实的自启动服务(历史故障,见 setup-output 用例的护栏)。
3990
3990
  const SKIP_SYSTEM_OPS = process.env.DSH_RELAY_SKIP_SERVICE === "1";
3991
3991
  function log(m) { try { appendFileSync(PLAN.log, "\\n[" + new Date().toISOString() + "] " + m + "\\n"); } catch (e) { /* 日志失败不阻断清理 */ } }
3992
3992
  function run(cmd, args, timeout) {
@@ -4086,7 +4086,7 @@ function accountFingerprintOf(cfg) {
4086
4086
  /**
4087
4087
  * 账号(或模式/设备身份)变化后,**必须让 bridge 用新凭据重新登记**。
4088
4088
  *
4089
- * 事故(2026-09-19 实测):从 A 账号切到 B 账号后,配置已经改了、device_id 也清了,
4089
+ * 故障(2026-09-19 实测):从 A 账号切到 B 账号后,配置已经改了、device_id 也清了,
4090
4090
  * 但**正在运行的 bridge 是启动时把账号/密码固化在环境变量里的**,它不会自己换账号;
4091
4091
  * 而那次"重启 bridge"又因 launchd 域名不一致(插件只认 gui/<uid>,安装器装在 user/<uid>)静默失败
4092
4092
  * → 设备继续留在 A 账号,B 账号的设备列表里永远看不到这台机器。
@@ -4477,7 +4477,7 @@ async function composeConnect(relayDir, opts = {}) {
4477
4477
  // ── 中继注册证据(只有 bridge 进程在跑时才算「现在能用」) ──
4478
4478
  // ⚠️ 先判「这个 bridge 是不是当前账号的」:账号切换后进程不会自己换凭据,
4479
4479
  // 旧账号留下的状态文件(phase=online + 旧 device_id)会让面板谎报「已连接」,
4480
- // 自愈也就永远不会去重启它(2026-09-19 实测事故)。
4480
+ // 自愈也就永远不会去重启它(2026-09-19 实测故障)。
4481
4481
  const accountCurrent = bridgeMatchesCurrentAccount(relayDir, cfg);
4482
4482
  let registered = false;
4483
4483
  let registerSource = "";
@@ -4815,7 +4815,7 @@ async function submitFeedback(relayDir, payload) {
4815
4815
  "content-type": "application/json",
4816
4816
  "x-dsh-device": cfg.device_id || "",
4817
4817
  "x-dsh-client": `dsh-remote-web/${PLUGIN_VERSION}`,
4818
- // 与 proxyFeedback 同一口径:结构化平台字段 + 真实 UA(此处没有浏览器请求可转发,
4818
+ // 与 proxyFeedback 同一约定:结构化平台字段 + 真实 UA(此处没有浏览器请求可转发,
4819
4819
  // 就如实写本机平台,绝不冒充浏览器 UA)。
4820
4820
  "user-agent": `dsh-remote-web/${PLUGIN_VERSION} (${process.platform}; ${process.arch})`,
4821
4821
  "x-dsh-platform": process.platform,
@@ -4890,7 +4890,7 @@ function ensureConnection(relayDir, opts = {}) {
4890
4890
  const watcherRunning = Boolean(launchd.running || watcherProcs.length);
4891
4891
  if (bridgeProcs.length || watcherRunning) {
4892
4892
  // 进程在跑 ≠ 跑对了账号:切换账号后旧 bridge 会一直用旧凭据,
4893
- // 只看"有没有进程"会让自愈永久停摆(实测事故)。这里多判一次账号归属。
4893
+ // 只看"有没有进程"会让自愈永久停摆(实测故障)。这里多判一次账号归属。
4894
4894
  if (bridgeProcs.length && !bridgeMatchesCurrentAccount(relayDir, cfg)) {
4895
4895
  book.lastAttemptAt = now;
4896
4896
  book.attempts += 1;
@@ -5473,7 +5473,7 @@ function telemetryRuntimeReady(relayDir) {
5473
5473
  }
5474
5474
 
5475
5475
  /**
5476
- * 运行环境探针(在补装 watcher 与面板轮询里顺手调用,不额外起定时器):
5476
+ * 运行环境探测(在补装 watcher 与面板轮询里顺手调用,不额外起定时器):
5477
5477
  * - 现在缺运行环境 → 记下「本进程见过缺失」,并在超过 INSTALL_STALE_MS 仍缺时归因为
5478
5478
  * install_failed(runtime_install_timeout)(每进程一次);
5479
5479
  * - 之前见过缺失、现在已就绪 → runtime_ready(每进程一次)。
@@ -5729,7 +5729,7 @@ export const __telemetryInternals = {
5729
5729
 
5730
5730
  /**
5731
5731
  * 安装信息上报(插件侧通道):登录后把本机安装信息发给企业端,供「已登录用户升级插件后刷新版本号」
5732
- * 之类的口径统计。无账号凭据不上报;失败静默(绝不阻塞面板、不弹错误)。
5732
+ * 之类的约定统计。无账号凭据不上报;失败静默(绝不阻塞面板、不弹错误)。
5733
5733
  * 契约:POST <api_url>/api/install-report,头 `authorization: Bearer <device-login JWT>`,
5734
5734
  * body { device_id?, install_source, install_version, harness_version, host_os, host_arch },成功 200 {ok:true};
5735
5735
  * 404/400 一律忽略(老服务端没有该接口时不能报错)。
@@ -6218,7 +6218,7 @@ function registerRoutes(ctx, relayDir) {
6218
6218
  handler: async (_req, res) => {
6219
6219
  // 彻底卸载 = ① 插件 profile 清理(快、纯 fs、可预期)② 运行时/bridge 清理(慢、要调外部命令)
6220
6220
  //
6221
- // ⚠️ 顺序是刻意的,别改回去(2026-09-18 用户实测的僵死事故):
6221
+ // ⚠️ 顺序是刻意的,别改回去(2026-09-18 用户实测的僵死故障):
6222
6222
  // 旧实现把**慢的** uninstallRuntime 排在最前、uninstallSelf 排在其后,于是
6223
6223
  // uninstallRuntime 里任何一次 spawnSync 卡住(Windows 沙箱拦 schtasks,而 spawnSync 的
6224
6224
  // timeout 在 Windows 上并不可靠,实测 18s+ 未返回),uninstallSelf 就**永远不执行**
@@ -6596,7 +6596,7 @@ function registerRoutes(ctx, relayDir) {
6596
6596
  invalidateRelayToken(relayDir); // 账号可能变了:丢弃旧 token 缓存,立即用新账号认证
6597
6597
  if (accountFingerprintOf(cfg) !== accountFpBefore) resetBridgeForAccountChange(relayDir, "账号/模式已变更");
6598
6598
  const bridgeRestart = startBridge(relayDir);
6599
- // 登录成功即上报本机安装信息(企业端用于「升级插件后刷新版本号」口径);失败静默。
6599
+ // 登录成功即上报本机安装信息(企业端用于「升级插件后刷新版本号」约定);失败静默。
6600
6600
  maybeReportInstall(relayDir, { force: true });
6601
6601
  sendJson(res, 200, { ok: true, bridgeRestart, ...(await composeStatus(relayDir)) });
6602
6602
  },
@@ -6627,7 +6627,7 @@ function registerRoutes(ctx, relayDir) {
6627
6627
  // 首次安装/在线更新后重启 DeepSeek harness:插件本体与浏览器半在进程启动时装载,
6628
6628
  // 不重启则面板入口与 /dsh-remote/* 路由都不会出现。只重启承载本插件的进程。
6629
6629
  //
6630
- // 🔴 2026-10-11 业主口径:**桌面端绝不代为重启**。
6630
+ // 🔴 2026-10-11 用户约定:**桌面端绝不代为重启**。
6631
6631
  // "自动重启会很突兀地把窗口给关掉,关没了……我们应该提示用户自己重启,不要由我们来重启。"
6632
6632
  // 桌面端 Host 由 Electron 托管,重启 = 强杀承载窗口的进程(还会弹原生致命错误)。
6633
6633
  // 这里做成**硬拒绝**(不只是前端藏按钮):任何调用方都拿不到"被重启"的结果,
@@ -6754,7 +6754,7 @@ function registerRoutes(ctx, relayDir) {
6754
6754
  const password = String(body.password ?? "");
6755
6755
  if (!phone || !smsCode || !password) return sendJson(res, 400, { ok: false, error: "手机号、短信验证码与密码必填" });
6756
6756
  // 全量透传 body(不挑字段):注册来源 reg_source(面板=panel_register/手机端网页=remoteweb_register)
6757
- // 等扩展字段必须原样进企业端,否则「用户从哪注册的」这类增长口径会全部丢失。
6757
+ // 等扩展字段必须原样进企业端,否则「用户从哪注册的」这类增长约定会全部丢失。
6758
6758
  const payload = { ...body, phone, sms_code: smsCode, password };
6759
6759
  delete payload.__parseError;
6760
6760
  const captchaId = body.captcha_id ?? body.captchaId;
@@ -6904,7 +6904,7 @@ function registerRoutes(ctx, relayDir) {
6904
6904
  // 自己持有 webserver 配置(实测监听 43120 / 回环),我们的 LAN 绑定补丁并不生效,于是它一直
6905
6905
  // 用的是原生那条路。**不能把"手机能不能选工作区"押在宿主的绑定地址上。**
6906
6906
  //
6907
- // 【0.6.15 事故修正 —— 0.6.14 的"先建 browse、后摘 auto"在 DSH Desktop 上必然失败】
6907
+ // 【0.6.15 故障修正 —— 0.6.14 的"先建 browse、后摘 auto"在 DSH Desktop 上必然失败】
6908
6908
  //
6909
6909
  // 现场(用户诊断日志):
6910
6910
  // error: service "directoryPicker" has been registered at .dLateDirectoryPicker3
@@ -7064,9 +7064,9 @@ const PICKER_PIN_PROBE_WAIT_MS = 30_000; // 等服务挂出来的上限(只
7064
7064
  const PICKER_PIN_PROBE_GAP_MS = 3_000; // 上面那段轮询间隔
7065
7065
 
7066
7066
  /**
7067
- * 目录选择器的**延后**接线 —— 这是 2026-09-26 启动事故的直接修复。
7067
+ * 目录选择器的**延后**接线 —— 这是 2026-09-26 启动故障的直接修复。
7068
7068
  *
7069
- * 🔴 事故:旧实现在 `apply()` 里**立刻**调 `pinBrowseDirectoryPicker(ctx)`(最多 6 次 × 700ms 轮询)。
7069
+ * 🔴 故障:旧实现在 `apply()` 里**立刻**调 `pinBrowseDirectoryPicker(ctx)`(最多 6 次 × 700ms 轮询)。
7070
7070
  * `loader.remove()` / `loader.create()` 会触发 dsh 的 `internal/plugin` 事件,而
7071
7071
  * `@deepseek-ai/dsh-client-modules` 收到事件就**重新 compose 整张前端插件图**
7072
7072
  * (`ClientModuleRegistry.flush() → buildCombo() → newlineCount()/identitySectionMap()`)。
@@ -7184,7 +7184,7 @@ export function apply(ctx, config = {}) {
7184
7184
  // 0.1.2-rc.1+ 浏览器会话代持:换取 Harness 会话 Cookie 供 bridge 上游携带(手机点设备不再 401 白页)
7185
7185
  ctx.effect(() => scheduleHarnessMint(ctx, relayDir), "dsh-remote-web: harness browser-session mint");
7186
7186
  // 目录选择器:远程控制下必须用浏览器内实现(否则手机点「添加工作区」像没反应) —— 见上方长注释
7187
- // ⚠️ **必须延后**:绝不能在 apply() 里直接切 Loader —— 会把宿主启动拖成假死(2026-09-26 事故,
7187
+ // ⚠️ **必须延后**:绝不能在 apply() 里直接切 Loader —— 会把宿主启动拖成假死(2026-09-26 故障,
7188
7188
  // 见 schedulePickerPin 上方长注释)。schedulePickerPin 自己等"宿主已 listen + 静置"再动手。
7189
7189
  ctx.effect(() => schedulePickerPin(ctx), "dsh-remote-web: pin browser directory picker");
7190
7190
  // 插件市场一键全功能:缺桌面运行环境则自动安装,登录后自动拉起 bridge(不依赖用户跑 npx)
@@ -7200,7 +7200,7 @@ export function apply(ctx, config = {}) {
7200
7200
  telemetryRuntimeProbe(relayDir); // 匿名遥测:记下「本进程见过运行环境缺失」(装好后发 runtime_ready)
7201
7201
  }
7202
7202
  // 插件启动即上报本机安装信息(install_source/version + 主机 OS/架构;仅有账号凭据时上报,失败静默)。
7203
- // 面板登录成功(POST /dsh-remote/config)时会再上报一次,覆盖「已登录用户升级插件」的口径。
7203
+ // 面板登录成功(POST /dsh-remote/config)时会再上报一次,覆盖「已登录用户升级插件」的约定。
7204
7204
  maybeReportInstall(relayDir);
7205
7205
  ctx.logger?.info?.(`dsh-remote-web: /dsh-remote routes ready (relayDir=${relayDir})`);
7206
7206
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 零依赖更新通道:直接从 npm registry 取 tarball,用 Node 内置 zlib + 自解析 tar 落到 staging 目录。
3
3
  *
4
- * 为什么必须有这条通道(真实事故,生产遥测近 14 天):
4
+ * 为什么必须有这条通道(真实故障,生产遥测近 14 天):
5
5
  * 插件面板的「一键更新」过去一律走 `npx @mrrisega/dsh-remote@<tag>`。在 Windows 上,
6
6
  * Node ≥20.12(CVE-2024-27980 的修复)起 `spawn("npx.cmd")` 不带 shell 会**同步抛 EINVAL**,
7
7
  * 0.6.6 及更早正是这么写的 —— 于是**升级通道本身坏掉**:427 台 Windows 里 157 台装机失败
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-web",
3
- "version": "0.7.0-beta.7",
3
+ "version": "0.7.0",
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",
@@ -55,8 +55,8 @@
55
55
  * - 帧体带信封标记(content-type: application/vnd.dsh.e2ee-v2 或 x-dsh-e2ee)= 已加密:
56
56
  * HTTP 解封→转发上游→响应封回;WS 逐消息封/解。任何解封失败显式
57
57
  * 502 + x-dsh-e2ee-error / ws-close 1008,绝不静默降级。
58
- * - 控制通道 /_e2ee/ctrl(device/channel 形态)不连上游,跑 §5.2 握手(hello/ack/探针);
59
- * 数据流 ws-open 携带 &e2ee=<sessId>&w=<8B hex>,探针通过前不转发任何数据。
58
+ * - 控制通道 /_e2ee/ctrl(device/channel 形态)不连上游,跑 §5.2 握手(hello/ack/探测);
59
+ * 数据流 ws-open 携带 &e2ee=<sessId>&w=<8B hex>,探测通过前不转发任何数据。
60
60
  *
61
61
  * E2EE Phase-4(镜像页 shim,见 e2ee-shim.mjs / e2ee-shim-script.js):
62
62
  * - doHttp 在 text/html 注入 mobile-adapter 之后、压缩/封包之前,对「e2ee.enabled ∧
@@ -237,7 +237,7 @@ function persistBridgeState(patch, opts = {}) {
237
237
  } catch { /* 非关键:面板侧还有日志/账号兜底判据 */ }
238
238
  }
239
239
 
240
- // ---------- 安装来源 / 版本(服务端设备行统计口径) ----------
240
+ // ---------- 安装来源 / 版本(服务端设备行统计约定) ----------
241
241
  // DSH_BRIDGE_INSTALL_SOURCE 由一键安装器(dsh-setup.mjs)注入 = npx;未设置表示这台电脑走的是
242
242
  // 「插件市场装面板插件 + 插件自愈补装运行环境」那条路径 = plugin_market;白名单外一律 unknown。
243
243
  const INSTALL_SOURCE_VALUES = new Set(["npx", "plugin_market"]);
@@ -339,9 +339,9 @@ const HARNESS_UNAUTHORIZED_TEXT = "dsh web authentication required";
339
339
  const HARNESS_COOKIE_REVOKED_FILE = ".harness-cookie-revoked";
340
340
 
341
341
  /**
342
- * Cookie 按 **authority 分文件**(0.7.0-beta.2 起,2026-10-09 事故根治)。
342
+ * Cookie 按 **authority 分文件**(0.7.0-beta.2 起,2026-10-09 故障根治)。
343
343
  *
344
- * 【事故】`~/.dsh-remote/` 只有一份 `.harness-cookie.json`,而 Web 端(3080)与桌面端(每次启动
344
+ * 【故障】`~/.dsh-remote/` 只有一份 `.harness-cookie.json`,而 Web 端(3080)与桌面端(每次启动
345
345
  * 随机端口)是**两个宿主、两个进程**,各自的插件都会往里写"自己 authority 的 Cookie" →
346
346
  * 互相覆盖。桥撞 401 后重读磁盘,拿到的是**另一个宿主刚写的** Cookie(与失败的不是同一个)
347
347
  * → 重试 → 还是 401 → 再读 → 又被覆盖回去……**乒乓,永不自愈**。手机端持续白页。
@@ -905,7 +905,7 @@ async function doHttp(method, path, reqHeaders, body, isB64) {
905
905
  * 【0.6.15】这里曾经是 `body: Buffer.from(JSON.stringify(respEnv)).toString("base64")`
906
906
  * + `bodyBase64: true` —— 也就是把整个信封 JSON 再 base64 一次。中继拿到它做的第一件事
907
907
  * 就是解回 UTF-8 再写给手机,所以这一层**纯属白花 33% 流量**,而且正好落在按流量计量的
908
- * bridge→中继 那条 WS 上(业主实测那条聚合包:12.69 MiB vs 直接发 UTF-8 的 9.5 MiB)。
908
+ * bridge→中继 那条 WS 上(用户实测那条聚合包:12.69 MiB vs 直接发 UTF-8 的 9.5 MiB)。
909
909
  * 中继对两种形态都支持(`frame.bodyBase64` 真假之分),所以直接发 UTF-8 明文即可。
910
910
  *
911
911
  * @param {string|number} id 帧 id
@@ -931,7 +931,7 @@ export function e2eeHttpResponseFrame(id, sessId, respEnv) {
931
931
  * 「镜像页交付了、但页面再也没回来」的看门狗(2026-09-25)。
932
932
  *
933
933
  * 【为什么需要】首屏提示层被整块删除后,页面上不再有任何东西能告诉用户"没加载完"
934
- * (那正是业主的要求:不许再有会盖住界面的层)。但**服务端能看见一个更硬的信号**:
934
+ * (那正是用户的要求:不许再有会盖住界面的层)。但**服务端能看见一个更硬的信号**:
935
935
  * 我们把镜像页 HTML 交付给手机后,官方前端一定会在几秒内发一串请求(模块、会话、mux…)。
936
936
  * 若 60 秒内一个都没有,那这个页面几乎必然没起来 —— 这时在**日志**里留一条带时间戳的结论,
937
937
  * 用户「复制诊断信息」或 `dsh-remote doctor` 时就能看到,不用再靠猜。
@@ -1109,7 +1109,7 @@ function sendCtrlError(send, id, code, extra = "") {
1109
1109
  sendCtrlMsg(send, id, { v: 2, type: "e2ee-error", code, ...(extra ? { message: extra } : {}) });
1110
1110
  }
1111
1111
 
1112
- /** 控制通道 ws-open:不连上游,应答 ok 后按 §5.2 跑握手(hello/ack → 探针)。 */
1112
+ /** 控制通道 ws-open:不连上游,应答 ok 后按 §5.2 跑握手(hello/ack → 探测)。 */
1113
1113
  function openControlWs(send, id) {
1114
1114
  if (!e2ee.enabled) {
1115
1115
  console.log(`[bridge] e2ee ctrl 被拒(桥端未启用): id=${id}`);
@@ -1122,7 +1122,7 @@ function openControlWs(send, id) {
1122
1122
  console.log(`[bridge] e2ee ctrl 通道已开 (id=${id}),等待 hello`);
1123
1123
  }
1124
1124
 
1125
- /** 控制通道消息:明文 hello / AEAD 探针信封(§5.2)。 */
1125
+ /** 控制通道消息:明文 hello / AEAD 探测信封(§5.2)。 */
1126
1126
  function handleCtrlMessage(send, id, rawText) {
1127
1127
  const conn = ctrlConns.get(id);
1128
1128
  if (!conn) return;
@@ -1145,9 +1145,9 @@ function handleCtrlMessage(send, id, rawText) {
1145
1145
  return;
1146
1146
  }
1147
1147
  if (obj.v === 2 && obj.k === "ctrl") {
1148
- // AEAD 探针(密文信封);失败 → bad_key 提示手机“密码/密钥不一致”,绝不静默
1148
+ // AEAD 探测(密文信封);失败 → bad_key 提示手机“密码/密钥不一致”,绝不静默
1149
1149
  if (!conn.sessId) {
1150
- sendCtrlError(send, id, "unknown_session", "探针先于 hello");
1150
+ sendCtrlError(send, id, "unknown_session", "探测先于 hello");
1151
1151
  return;
1152
1152
  }
1153
1153
  try {
@@ -1155,7 +1155,7 @@ function handleCtrlMessage(send, id, rawText) {
1155
1155
  sendCtrlMsg(send, id, reply);
1156
1156
  } catch (e) {
1157
1157
  const code = e2eeCodeOf(e);
1158
- // 探针解密失败对手机语义统一为 bad_key(§5.2;密码不一致/改密未更新)
1158
+ // 探测解密失败对手机语义统一为 bad_key(§5.2;密码不一致/改密未更新)
1159
1159
  sendCtrlError(send, id, code === "auth_failed" || code === "bad_key" ? "bad_key" : code, e?.message || "");
1160
1160
  }
1161
1161
  return;
@@ -1183,7 +1183,7 @@ export async function handleWsOpen(dchOrSend, frame) {
1183
1183
  send({ id, type: "ws-open", ok: false, code: 1008, reason: "e2ee_session_locked:请先解锁完成握手(检查密码)" });
1184
1184
  return;
1185
1185
  }
1186
- // 探测前不转发任何数据(§4.6/§5.2:探针通过后数据流才可建);此时已验证 → 安全
1186
+ // 探测前不转发任何数据(§4.6/§5.2:探测通过后数据流才可建);此时已验证 → 安全
1187
1187
  }
1188
1188
  if (wsSessions.has(id)) { try { wsSessions.get(id).ws.terminate(); } catch {} wsSessions.delete(id); }
1189
1189
  const safe = safePath(tagged ? tagged.upstreamPath : path);
@@ -1412,7 +1412,7 @@ async function registerDeviceInAccount(token) {
1412
1412
  device_name: os.hostname() || "dsh-bridge",
1413
1413
  pub_key: pubKey,
1414
1414
  machine_fp: MACHINE_FP, // v6 同机识别:服务端据此自动顶替旧设备(重装不再被设备数卡死)
1415
- // 安装口径(服务端存到设备行,用于区分「市场安装/自愈补装」与「安装器 npx」):
1415
+ // 安装约定(服务端存到设备行,用于区分「市场安装/自愈补装」与「安装器 npx」):
1416
1416
  // install_source: npx(安装器注入) | plugin_market(插件拉起/自愈,默认) | unknown(白名单外)
1417
1417
  install_source: INSTALL_SOURCE,
1418
1418
  install_version: INSTALL_VERSION,
@@ -1659,7 +1659,7 @@ async function resolveWechatTier() {
1659
1659
 
1660
1660
  /**
1661
1661
  * App 入口(免费用户越界时给的转化链接)。
1662
- * 由账号 API 地址推导:去掉 `/relay-api` 再加 `/app/` —— 与插件半的 DEFAULT_APP_URL 同一口径
1662
+ * 由账号 API 地址推导:去掉 `/relay-api` 再加 `/app/` —— 与插件半的 DEFAULT_APP_URL 同一约定
1663
1663
  * (https://host:port/relay-api → https://host:port/app/)。
1664
1664
  */
1665
1665
  function wechatAppUrl() {
@@ -1691,7 +1691,7 @@ function startWeChat() {
1691
1691
  relayDir,
1692
1692
  // 传**取值器**而不是当次的值(0.7.0-beta.2 修复)。
1693
1693
  //
1694
- // 【2026-10-09 真机事故】这里以前传的是 `UPSTREAM` 这个**当次快照**:bridge 起来时宿主还在
1694
+ // 【2026-10-09 真机故障】这里以前传的是 `UPSTREAM` 这个**当次快照**:bridge 起来时宿主还在
1695
1695
  // 3080,随后宿主换了端口(Desktop 每次启动随机),隧道转发侧因为每次读最新 UPSTREAM 而
1696
1696
  // 一切正常,**唯独微信事件通道抱着 3080 死循环重连** —— 现象极具迷惑性:手机能开页面、
1697
1697
  // bridge-status 显示 online,但任务跑完**一条推送都没有**,日志里只有刷屏的
@@ -1770,7 +1770,7 @@ async function runTunnel() {
1770
1770
  /**
1771
1771
  * 给 stdout/stderr 的每一行加 ISO 时间戳。
1772
1772
  *
1773
- * 【2026-09-25 事故】bridge 日志**没有任何时间戳**,于是排查
1773
+ * 【2026-09-25 故障】bridge 日志**没有任何时间戳**,于是排查
1774
1774
  * 「任务跑完没收到微信推送」时完全无法把日志和事件对上时间:
1775
1775
  * 不知道最后一条 `sendmessage 失败` 发生在用户的回话之前还是之后,
1776
1776
  * 最后只能靠"数行数 ÷ 每分钟行数"去估时间(估出来的结论还差点搞反方向)。
@@ -45,7 +45,7 @@
45
45
  * (裸 id → `session/agent-busy`,说明找到了;`session-<uuid>` → `session/not-found`,说明不存在)。
46
46
  * 规格 §4 那句「必须用持久 session-<uuid>,裸 uuid 会 not-found」只在根会话上成立。
47
47
  * 本模块主形式原样发送,另一种形式只在 `session/not-found` 时兜底各试一次(见 `sessionIdCandidates`)。
48
- * - `api-session/*` 的口径(真机逐帧核对):`status` = `[sessionId, running]`(**边沿**事件)、
48
+ * - `api-session/*` 的约定(真机逐帧核对):`status` = `[sessionId, running]`(**边沿**事件)、
49
49
  * `error` = `[id, message]`、`activity` = `[sessionId, epochMs]`、`removed` = `[sessionId]`、
50
50
  * `added` = `[summary]`,summary 字段 `sessionId/updatedAt/running/blank/parentSessionId/origin/cwd/projections`。
51
51
  * - `turn/end.reason.kind` 闭集:completed|aborted|blocked|error|max-tokens|interrupted
@@ -88,7 +88,7 @@
88
88
  * 缓存只用于「list 失败」时兜底);两处都拿不到就**显式失败** `through-seq-unavailable`,
89
89
  * 绝不猜一个魔法数字。
90
90
  * ⚠️ 不用描述符允许的 `throughSeq: -1`:history.js `paginate()` 会把它算成
91
- * `end = min(-1+1, …) = 0` → 返回**空页**(不是「最新一页」)。口径为负,故不采用。
91
+ * `end = min(-1+1, …) = 0` → 返回**空页**(不是「最新一页」)。约定为负,故不采用。
92
92
  *
93
93
  * 助手结论文本的真实形状(`session/page` → `value.records[]`)
94
94
  * `record = {type:'event', event:{type,seq,time,data}}`;助手消息是 `event.type === 'assistant/message'`,
@@ -1012,7 +1012,7 @@ class EventSubscriber extends EventEmitter {
1012
1012
  // 旧行为是失败即 `#followBlocked.add()`,此后**本代**(直到 mux 重连)再也不订阅它 ——
1013
1013
  // 而「这一轮又跑起来了」(status:true)恰恰是重新订阅的正当理由。
1014
1014
  // 不清掉的话:一次 `session/agent-busy`/`session/not-found` 抖动 = 该会话这一整轮的
1015
- // turn/end 全部丢失 = 完成推送静默消失(与上面定时任务那个自我关停是同一类事故)。
1015
+ // turn/end 全部丢失 = 完成推送静默消失(与上面定时任务那个自我关停是同一类故障)。
1016
1016
  // 重试天然有界:一次 running 边沿最多触发一次重新订阅,不会打转。
1017
1017
  this.#followBlocked.delete(sessionId);
1018
1018
  this.#ensureFollow(sessionId);
@@ -1156,14 +1156,14 @@ class EventSubscriber extends EventEmitter {
1156
1156
  const timer = setTimeout(() => {
1157
1157
  this.#discoverTimer = null;
1158
1158
  if (this.#closed || this.#state !== "ready") return;
1159
- // ★ 2026-09-25 事故修复:这里原本写成 `if (this.#follows.size > 0)`,注释是
1159
+ // ★ 2026-09-25 故障修复:这里原本写成 `if (this.#follows.size > 0)`,注释是
1160
1160
  // 「没有 follow 流就没有可对账的东西(每代至少发现过一次)」。
1161
1161
  // 它漏掉了这条定时任务的**另一半职责:发现**。丢一次边沿事件(mux 重连、status:true
1162
1162
  // 早于我们订阅、帧被丢)就会让 `#follows` 长期为 0,而这条任务又因为「0 个流」把自己关掉
1163
1163
  // → **从此再也不发现任何会话** → 该会话的 turn/end 永远收不到 → 完成推送**静默消失**。
1164
1164
  //
1165
- // 真机现场(业主本人,2026-09-25):任务跑完一条微信都没收到,bridge 日志里也
1166
- // **没有任何报错**(因为确实什么都没发生)。活体探针(以微信通道同款方式订阅本机)显示:
1165
+ // 真机现场(用户本人,2026-09-25):任务跑完一条微信都没收到,bridge 日志里也
1166
+ // **没有任何报错**(因为确实什么都没发生)。活体探测(以微信通道同款方式订阅本机)显示:
1167
1167
  // · 新起的订阅器 **163ms** 就发现并 follow 了正在跑的会话 —— 机制本身是好的;
1168
1168
  // · 而当时在跑的 bridge 进程到 3080 只有 **2 条** WS(mux + control)、**没有 follow 流**,
1169
1169
  // 可它明明有一个正在跑长任务的会话 —— 正是这里的自我关停把发现能力锁死了。
@@ -22,7 +22,7 @@
22
22
  * http 请求 "dsh-e2ee/v1\0http\0<reqNonceB64>\0p2b"(reqNonce=请求信封 n)
23
23
  * http 响应 "…\0http\0<reqNonceB64>\0b2p"(响应信封 n 回显请求 n)
24
24
  * ws 流 "…\0ws\0<wsLabel>\0p2b|b2p"(wsLabel 见 parseWsE2eeParams)
25
- * ctrl 探针 "…\0ctrl\0p2b|b2p"
25
+ * ctrl 探测 "…\0ctrl\0p2b|b2p"
26
26
  * 4. AAD = UTF8("dsh-e2ee/v1\0<kind>\0<sessId>\0<dir>\0<counter>")。
27
27
  * [偏差] 评审稿 §4.3 建议把路径摘要也放进 AAD,但因解密方在打开前并不知道
28
28
  * 明文里的路径(循环依赖),v1 规范化 AAD 不含路径;模块保留 extra 参数供后续绑定。
@@ -247,7 +247,7 @@ export class E2eeSession {
247
247
  this.saltH = saltH;
248
248
  this.profile = profile;
249
249
  this.epoch = epoch;
250
- this.verified = false; // 探针通过前不接受任何数据流/请求
250
+ this.verified = false; // 探测通过前不接受任何数据流/请求
251
251
  this.createdAt = Date.now();
252
252
  this.usedAt = this.createdAt;
253
253
  }
@@ -681,7 +681,7 @@ export class E2eeService {
681
681
  * dsh_token cookie 完成「同账号」授权(device/channel 形态都只到本设备)——
682
682
  * 信任模型见 docs/e2ee-protocol.md §5.4 [DECISION]:引导瞬间以「持有效扫码会话=账号本人」
683
683
  * 为准,存在被中继主动冒充的理论窗口(与扫码登录产品一致);引导后数据面照常走
684
- * hello/probe 探针(双方已同持 MK)与 AEAD 信封。
684
+ * hello/probe 探测(双方已同持 MK)与 AEAD 信封。
685
685
  * @returns {{mk:string, profile:string, epoch:number}}
686
686
  */
687
687
  introGrant() {
@@ -765,15 +765,15 @@ export class E2eeService {
765
765
  handleProbe(sessId, env) {
766
766
  const session = this.sessionOf(sessId);
767
767
  if (!session) throw new E2eeError("unknown_session", "e2ee: 会话不存在或已过期(请重新解锁)");
768
- // 先解密成功再推进计数:失败的尝试(密钥不符)不得消耗合法探针的计数
768
+ // 先解密成功再推进计数:失败的尝试(密钥不符)不得消耗合法探测的计数
769
769
  const opened = session.open({ kind: "ctrl", dir: DIR_P2B, env, counter: env?.c ?? 0, reqNonceB64: env?.n });
770
770
  let pt;
771
771
  try {
772
772
  pt = JSON.parse(opened.data.toString("utf8"));
773
773
  } catch {
774
- throw new E2eeError("bad_key", "e2ee: 探针明文非法(密钥不一致?)");
774
+ throw new E2eeError("bad_key", "e2ee: 探测明文非法(密钥不一致?)");
775
775
  }
776
- if (pt?.p !== "dsh-e2ee-probe-v1") throw new E2eeError("bad_key", "e2ee: 探针载荷不符");
776
+ if (pt?.p !== "dsh-e2ee-probe-v1") throw new E2eeError("bad_key", "e2ee: 探测载荷不符");
777
777
  const guards = this.ctrlCounters.get(sessId) || { p2b: new SequenceGuard(), b2p: new SequenceGuard() };
778
778
  guards.p2b.check(Number(env?.c) || 0); // 成功后推进,后续重放被拒
779
779
  session.verified = true;
@@ -791,7 +791,7 @@ export class E2eeService {
791
791
  guardHttpRequest(sessId, env) {
792
792
  const session = this.sessionOf(sessId);
793
793
  if (!session) throw new E2eeError("unknown_session", "e2ee: 会话不存在或已过期(请重新解锁)");
794
- if (!session.verified) throw new E2eeError("not_verified", "e2ee: 会话未完成探针校验");
794
+ if (!session.verified) throw new E2eeError("not_verified", "e2ee: 会话未完成探测校验");
795
795
  if (!this.httpNonces.check(sessId, env.n)) throw new E2eeError("replay", "e2ee: HTTP 信封 nonce 重放");
796
796
  return session;
797
797
  }