@mrrisega/dsh-remote 0.7.0-beta.8 → 0.7.1-beta.1

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 (71) 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 +294 -43
  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/mobile-adapter-settings.test.mjs +352 -0
  19. package/clients/dsh-remote/test/mobile-adapter.test.mjs +35 -0
  20. package/clients/dsh-remote/test/upstream-discovery.test.mjs +1 -1
  21. package/clients/dsh-remote/test/wechat-channel.test.mjs +5 -5
  22. package/clients/dsh-remote/test/wechat-runtime.test.mjs +26 -26
  23. package/clients/dsh-remote/test/wechat-tier-source.test.mjs +2 -2
  24. package/clients/dsh-remote/wechat-channel.mjs +9 -9
  25. package/clients/dsh-remote/wechat-runtime.mjs +38 -38
  26. package/docs/telemetry.md +3 -3
  27. package/dsh-setup.mjs +27 -27
  28. package/package.json +3 -3
  29. package/packages/dsh-remote-web/lib/client.js +56 -56
  30. package/packages/dsh-remote-web/lib/fix-prompt.js +1 -1
  31. package/packages/dsh-remote-web/lib/index.js +380 -59
  32. package/packages/dsh-remote-web/lib/tarball-update.js +1 -1
  33. package/packages/dsh-remote-web/package.json +1 -1
  34. package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +17 -17
  35. package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-events.mjs +6 -6
  36. package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-client.mjs +7 -7
  37. package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-shim-script.js +9 -9
  38. package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +294 -43
  39. package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-channel.mjs +9 -9
  40. package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-runtime.mjs +38 -38
  41. package/packages/dsh-remote-web/runtime/clients/dsh-web/native.html +5 -5
  42. package/packages/dsh-remote-web/runtime/dsh-setup.mjs +27 -27
  43. package/packages/dsh-remote-web/test/account-switch-device.test.mjs +2 -2
  44. package/packages/dsh-remote-web/test/activation-single-point.test.mjs +1 -1
  45. package/packages/dsh-remote-web/test/announcements-ui.test.mjs +1 -1
  46. package/packages/dsh-remote-web/test/connect-loop.test.mjs +1 -1
  47. package/packages/dsh-remote-web/test/connect-ui.test.mjs +2 -2
  48. package/packages/dsh-remote-web/test/desktop-compat.test.mjs +4 -4
  49. package/packages/dsh-remote-web/test/diag-report.test.mjs +1 -1
  50. package/packages/dsh-remote-web/test/fix-prompt.test.mjs +2 -2
  51. package/packages/dsh-remote-web/test/harness-restart.test.mjs +1 -1
  52. package/packages/dsh-remote-web/test/install-profile-targets.test.mjs +3 -3
  53. package/packages/dsh-remote-web/test/installer-cli.test.mjs +1 -1
  54. package/packages/dsh-remote-web/test/launchd-domain.test.mjs +2 -2
  55. package/packages/dsh-remote-web/test/patch-activation.test.mjs +2 -2
  56. package/packages/dsh-remote-web/test/picker-pin.test.mjs +1 -1
  57. package/packages/dsh-remote-web/test/plugin-manifest.test.mjs +2 -2
  58. package/packages/dsh-remote-web/test/quota-absent.test.mjs +4 -4
  59. package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +2 -2
  60. package/packages/dsh-remote-web/test/selfheal-restart-guard.test.mjs +420 -0
  61. package/packages/dsh-remote-web/test/settings-entry-placement.test.mjs +6 -6
  62. package/packages/dsh-remote-web/test/settings-entry.test.mjs +2 -2
  63. package/packages/dsh-remote-web/test/settings-open-path.test.mjs +4 -4
  64. package/packages/dsh-remote-web/test/setup-output.test.mjs +11 -11
  65. package/packages/dsh-remote-web/test/tarball-update.test.mjs +1 -1
  66. package/packages/dsh-remote-web/test/telemetry.test.mjs +4 -4
  67. package/packages/dsh-remote-web/test/uninstall-safety.test.mjs +2 -2
  68. package/packages/dsh-remote-web/test/watcher-upstream-port.test.mjs +1 -1
  69. package/packages/dsh-remote-web/test/wechat-bot-ui.test.mjs +7 -7
  70. package/packages/dsh-remote-web/test/windows-compat.test.mjs +5 -5
  71. package/scripts/test-net-guard.cjs +1 -1
@@ -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
  }
@@ -4,7 +4,7 @@
4
4
  * 运行时语义(docs/e2ee-protocol.md §4/§5/§6/§9;字节约定对齐 clients/dsh-remote/
5
5
  * e2ee-client.mjs「规范化决定 1–6」与 native.html Phase-3 手机端):
6
6
  * 0. document-start:读一次性交接单(sessionStorage dsh-e2ee-handover)→ 立即删除,
7
- * 用内存重建「已与桥端探针互认」的会话(仅 SHK+saltH+sessId;MK/密码不下放)。
7
+ * 用内存重建「已与桥端探测互认」的会话(仅 SHK+saltH+sessId;MK/密码不下放)。
8
8
  * 1. 有会话 → patch window.fetch / window.WebSocket:命中 §4.5 策略表(/api 排除
9
9
  * SSE、/sidebar、/git、/pet;channel 形态 /remote/{api|sidebar|git|pet}/* 与
10
10
  * device 形态 /remote/<dev>/… 归一后同样判定)→ HTTP 封 v2 信封发送、响应信封
@@ -132,7 +132,7 @@
132
132
  return out;
133
133
  }
134
134
 
135
- /** 一个解锁会话(由交接单重建;与桥端已探针互认的会话同 sessId)。密钥仅内存。 */
135
+ /** 一个解锁会话(由交接单重建;与桥端已探测互认的会话同 sessId)。密钥仅内存。 */
136
136
  function SeSession(o) {
137
137
  if (!o || typeof o.sessId !== "string" || !SE_SESS_RE.test(o.sessId)) throw seErr("bad_session", "e2ee: sessId 非法");
138
138
  if (!o.shk || o.shk.length !== SE_KEY_BYTES || !o.saltH || o.saltH.length !== SE_KEY_BYTES) throw seErr("bad_session", "e2ee: SHK/saltH 非法");
@@ -141,7 +141,7 @@
141
141
  this.saltH = o.saltH;
142
142
  this.profile = typeof o.profile === "string" ? o.profile : "";
143
143
  this.epoch = Number(o.epoch) || 0;
144
- this.verified = true; // 交接单来源 = 已通过探针的解锁会话
144
+ this.verified = true; // 交接单来源 = 已通过探测的解锁会话
145
145
  }
146
146
  /** 子密钥(node keyFor / wc keyBytesFor 同构)。 */
147
147
  SeSession.prototype.keyBytesFor = function (opt) {
@@ -292,7 +292,7 @@
292
292
  /**
293
293
  * 把**根路径**的受保护请求改写成"设备作用域"形态:`/api/x` → `/remote/<deviceId>/api/x`。
294
294
  *
295
- * 为什么必须(2026-09-26 线上事故,nginx access/error 日志为证):
295
+ * 为什么必须(2026-09-26 线上故障,nginx access/error 日志为证):
296
296
  * 官方客户端把 mux WS 的地址固定按 **`location.origin`** 拼成 `/api/remote.mux`
297
297
  * (@deepseek-ai/dsh-api-gateway/lib/client.js:544 的 remoteStreamUrl())—— 它**永远**不带设备前缀,
298
298
  * 于是中继只能靠 **`dsh_device` cookie** 来判"这是哪台设备"(relay-router/src/index.mjs:626-627
@@ -424,7 +424,7 @@
424
424
  if (typeof console !== "undefined" && console.warn) console.warn("[dsh-e2ee-shim]", message);
425
425
  }
426
426
 
427
- /* ---- 会话失效自愈(2026-09-26 线上事故:unknown_session 502 + 刷新也好不了) -------------
427
+ /* ---- 会话失效自愈(2026-09-26 线上故障:unknown_session 502 + 刷新也好不了) -------------
428
428
  链路:手机镜像页 → 加密信封 → 中继透明转发 → 桥端 e2ee-client 查会话表 → 查不到就回
429
429
  **明文** 502 + `x-dsh-e2ee-error: unknown_session`(桥端会话表是进程内存 + 24h TTL + CAP=32 LRU,
430
430
  桥重启/被挤掉之后就没了)。而手机侧靠 **localStorage 持久交接单**长期复用同一个 sessId
@@ -604,7 +604,7 @@
604
604
  inner.onopen = function (ev) { self._state = 1; self._opened = true; seNoteWsOpened(); if (typeof self._onopen === "function") self._onopen(ev || { type: "open", target: self }); };
605
605
  inner.onerror = function (ev) {
606
606
  /* 握手上就失败(error 早于任何 open)→ 让用户知道"加密数据通道没建立"。
607
- 这是 2026-09-26 事故里最缺的一条信号:mux 被 404 掐掉时页面**一声不响**,
607
+ 这是 2026-09-26 故障里最缺的一条信号:mux 被 404 掐掉时页面**一声不响**,
608
608
  用户只看到所有数据都停住。 */
609
609
  if (!self._opened) seNoteWsHandshakeFail(self._meta && self._meta.wsLabel, "error");
610
610
  if (typeof self._onerror === "function") self._onerror(ev || { type: "error", target: self });
@@ -715,7 +715,7 @@
715
715
  var base = ctx.origin || (typeof window !== "undefined" ? window.location.href : undefined);
716
716
  var uo = new URL(url, base);
717
717
  if (seSameWebContext(uo, ctx.origin)) {
718
- /* ★ 设备作用域改写(2026-09-26 事故):官方客户端把 mux WS 固定拼成**根路径**
718
+ /* ★ 设备作用域改写(2026-09-26 故障):官方客户端把 mux WS 固定拼成**根路径**
719
719
  `/api/remote.mux`(dsh-api-gateway/lib/client.js:544 用 location.origin 拼),
720
720
  于是中继只能靠 `dsh_device` cookie 判设备;cookie 一过期/缺失就是 404(upgrade 被
721
721
  rejectUpgrade 掐掉,nginx 只看到 009)→ E2EE 会话永远建不起来 → 之后每个受保护请求
@@ -730,7 +730,7 @@
730
730
  /* ★ 关键:改写结果要**留住**。原来不改写分支 return 的是原始 url,等于把
731
731
  `/remote/<dev>/api/remote.mux` 又丢回根路径 —— 那正是 404 的来源。 */
732
732
  passUrl = so.href;
733
- /* ⚠️ 必须同时要求"有会话":URL 改写在无会话时也要做(这是 2026-09-26 事故的正题),
733
+ /* ⚠️ 必须同时要求"有会话":URL 改写在无会话时也要做(这是 2026-09-26 故障的正题),
734
734
  但**加密**没有会话就做不了 —— 少了这个判断,未解锁的页面上任何
735
735
  `new WebSocket("/api/…")` 都会在 ctx.sess.sessId 上抛 TypeError,
736
736
  把整个页面打挂(比原来"不装补丁"还糟)。 */
@@ -948,7 +948,7 @@
948
948
  try { lsSess = seSessionOfHandover(lsRaw); } catch (e) { lsSess = null; }
949
949
  if (lsSess) {
950
950
  _install(lsSess);
951
- /* ⚠️ 0.6.18 起这句文案不再无条件说"加密好好的"(2026-09-26 事故的教训):
951
+ /* ⚠️ 0.6.18 起这句文案不再无条件说"加密好好的"(2026-09-26 故障的教训):
952
952
  从 localStorage 恢复的会话**没法在本机验证**桥端是否还认它(桥端会话表是进程内存 +
953
953
  24h TTL + CAP=32 LRU,桥重启/被挤掉之后这张交接单就是死票)。真实校验只能等
954
954
  第一个受保护请求 —— 那一步由 seNoteE2eeError 兜底:一旦桥端回 unknown_session,