@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
@@ -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,
@@ -52,7 +52,7 @@
52
52
  * - JS 用结构探测(frame → 子列 → 打 dsh-ma-* 稳定类)承载布局,不以 hashed 类为唯一钩子;
53
53
  * - 命中不到 frame 时静默跳过,页面保持官方默认,绝不报错/破坏。
54
54
  *
55
- * ⚠️ 官方 0.1.5-rc.2 起第三列改名(2026-09-19 线上事故,Windows 用户先撞到):
55
+ * ⚠️ 官方 0.1.5-rc.2 起第三列改名(2026-09-19 线上故障,Windows 用户先撞到):
56
56
  * 第三列 `pI_x6G_detailsCol` → `pI_x6G_rightbarCol`、`data-details-collapsed` →
57
57
  * `data-rightbar-collapsed`,且折叠时不再渲染拖拽把手、`overlayLayer`(data-shell-overlay,
58
58
  * inset:0 全屏)始终留在 frame 末尾。旧实现"详情抽屉兜底 = 取 frame 最后一个子元素"于是把
@@ -153,10 +153,10 @@ const STYLE = `
153
153
  ══════════════════════════════════════════════════════════════════════════════
154
154
  🚫 这里**故意什么都不写** —— 第三列(官方右栏)的显示/隐藏完全交还给官方。
155
155
  ══════════════════════════════════════════════════════════════════════════════
156
- 历史(2026-09-27 根治,业主拍板 B 方案):
156
+ 历史(2026-09-27 根治,用户拍板 B 方案):
157
157
  0.6.13 起我们把这列改造成自绘浮层 —— position:fixed; right:0; width:min(92vw,400px);
158
158
  transform:translateX(103%),再用 html.dsh-ma-details-open … { transform:none } 滑进来。
159
- 因为"自绘浮层会白框压屏"(2026-09-19 事故),又加了一道很严的闸门(必须"用户 8 秒内
159
+ 因为"自绘浮层会白框压屏"(2026-09-19 故障),又加了一道很严的闸门(必须"用户 8 秒内
160
160
  在正文里点过"才允许滑入);而 better-sidebar 这类插件只翻**它自己**的 store、官方
161
161
  cols.rightbar 永远是 0 → 闸门永远不开 → 于是又在悬浮菜单里补了颗「打开右侧栏」按钮。
162
162
 
@@ -188,7 +188,7 @@ const STYLE = `
188
188
  opacity: 0; pointer-events: none;
189
189
  transition: opacity .22s ease;
190
190
  }
191
- /* ⚠️ 拦截点击**只认 dsh-ma-scrim-on 这一个闸**(2026-09-19 复盘):
191
+ /* ⚠️ 拦截点击**只认 dsh-ma-scrim-on 这一个闸**(2026-09-19 回顾):
192
192
  旧版还并列了一个「html.dsh-ma-details-open div.dsh-ma-scrim」规则,于是"详情展开判定出错"
193
193
  会直接变成"整屏被遮罩拦截"——官方 0.1.5 把第三列改名 rightbar 后就是这个死局。
194
194
  现在判定收口在 JS 里(必须有一个**真的在视口内、且不是全屏的**抽屉),
@@ -524,7 +524,7 @@ const SCRIPT = `(() => {
524
524
  "use strict";
525
525
 
526
526
  /* ===== 【0.6.16 已删除】首屏加载提示层(0.6.14 引入,0.6.15 仍在)=====
527
- 为什么删掉(业主实测,2026-09-25):「界面都已经加载完了,十几秒就把插件的接口加载好了,
527
+ 为什么删掉(用户实测,2026-09-25):「界面都已经加载完了,十几秒就把插件的接口加载好了,
528
528
  但这个遮罩一直不消失」—— 截图里官方 UI 就在下面透出来,秒表走到 187 秒。
529
529
 
530
530
  根因是**设计错误**,不是某个选择器写错:
@@ -535,7 +535,7 @@ const SCRIPT = `(() => {
535
535
  官方改版、会话尚未挂载输入框、或前缀变化时两组信号全不命中 → 层就永远留着。
536
536
  · 当时我给自己写的安慰是「pointer-events:none,万一移除失效也绝不挡住点击」——
537
537
  可对用户来说,**盖住视线和进不去没有区别**:刷新也一样,每次都要等 4 分钟兜底。
538
- 结论(业主要求):**直接删掉这个功能**。首屏反馈交给官方自己的转圈;
538
+ 结论(用户要求):**直接删掉这个功能**。首屏反馈交给官方自己的转圈;
539
539
  我们不再在用户页面上放任何会覆盖整屏的东西 —— 这类层一旦判断失误就是"永远进不去",
540
540
  代价远大于它带来的提示价值(何况限速放开后首屏本来就快了)。
541
541
 
@@ -579,7 +579,7 @@ const SCRIPT = `(() => {
579
579
 
580
580
  /* ===== 「加载疑似没完成」看门狗(2026-09-25,替代被整块删除的首屏提示层)=====
581
581
  那个提示层是全屏覆盖 + 启发式退出 —— 判断错一次就把**已经加载完**的界面永久盖住
582
- (业主实测:秒表走到 187 秒,刷新也一样)。所以这里换一条**安全的失败方向**:
582
+ (用户实测:秒表走到 187 秒,刷新也一样)。所以这里换一条**安全的失败方向**:
583
583
 
584
584
  · 解析期**不创建任何 DOM**(只有几个变量 + 一个定时器)—— 页面还没过主机门,
585
585
  更不该有任何覆盖层;
@@ -614,13 +614,13 @@ const SCRIPT = `(() => {
614
614
  return Date.now();
615
615
  }
616
616
  /**
617
- * 镜像页就绪耗时(毫秒)——**口径见 docs/TELEMETRY-CONTRACT.md / 迭代冻结文档 §2.3**:
617
+ * 镜像页就绪耗时(毫秒)——**约定见 docs/TELEMETRY-CONTRACT.md / 迭代冻结文档 §2.3**:
618
618
  * · 起点 = **导航开始**(performance.now() 的零点 = performance.timeOrigin ≈ 导航开始),
619
619
  * 所以一条值同时覆盖 DNS+TLS+HTML TTFB + 解析 + 界面就绪;
620
620
  * · **不能**用 Date.now() - maLoadWatch.startedAt:那个起点是"注入脚本解析时刻",会**漏掉
621
621
  * HTML TTFB**(HAR 实测那 79 秒里 TTFB 就有 8.7 秒),报出来的数会系统性小于用户体感;
622
- * 而且 bfcache 恢复(pageshow persisted)不会重置 startedAt,那口径还能报出几十分钟。
623
- * · 没有 performance 时退化回 startedAt 口径(老浏览器兜底,宁可粗糙也不能漏报)。
622
+ * 而且 bfcache 恢复(pageshow persisted)不会重置 startedAt,那约定还能报出几十分钟。
623
+ * · 没有 performance 时退化回 startedAt 约定(老浏览器兜底,宁可粗糙也不能漏报)。
624
624
  */
625
625
  function maLoadElapsedMs() {
626
626
  try {
@@ -1030,7 +1030,7 @@ const SCRIPT = `(() => {
1030
1030
  let fabDone = false;
1031
1031
 
1032
1032
  /* ===== 📶 网络延迟采样器(2026-10-10 二修,与"某一代菜单 DOM"彻底解耦)=================
1033
- 一修(0.7.0-beta.3)改掉了"亚毫秒样本被丢弃",但业主实测**仍然一直「测速中」**,
1033
+ 一修(0.7.0-beta.3)改掉了"亚毫秒样本被丢弃",但用户实测**仍然一直「测速中」**,
1034
1034
  而同一时段镜像页打了 69 次 /api/agentPresets/list 这类**完全合格**的请求 ——
1035
1035
  说明真根因不是样本被过滤,而是**采样根本没接上**(或渲染写进了已被替换的旧节点)。
1036
1036
 
@@ -1300,15 +1300,15 @@ const SCRIPT = `(() => {
1300
1300
  (dsh-bridge.mjs:763 → :770),两者都 splice 到 </head> 前 → adapter 的 fetch 包装在
1301
1301
  **内层** → 它量到的是"去掉加解密后的真实网络往返"(这正是用户想要的"网速")。
1302
1302
 
1303
- 统计口径:最近 RTT_KEEP(8) 次**成功**样本取中位数。
1303
+ 统计约定:最近 RTT_KEEP(8) 次**成功**样本取中位数。
1304
1304
  · 用中位数而不是均值/EWMA:偶发的一次 GC/系统调度卡顿会把均值永久带偏,
1305
1305
  中位数对单次离群点免疫,同时比"最小值"更贴近体感(最小值永远偏乐观);
1306
1306
  · 失败(reject)/超时(>20s)/亚毫秒(同一帧内的缓存命中)都不进样本:
1307
1307
  前者不是网络耗时,后者会算出 0ms 这种"谎言";
1308
1308
  · **无样本时显示「测速中…」而不是 0ms** —— 0ms 会让人以为网络飞快。
1309
- 分档(2026-09-27 业主口径调整):≤500ms 顺畅 / ≤1000ms 正常 / ≤2000ms 偏慢 / >2000ms 很慢。
1309
+ 分档(2026-09-27 用户约定调整):≤500ms 顺畅 / ≤1000ms 正常 / ≤2000ms 偏慢 / >2000ms 很慢。
1310
1310
  为什么放宽:这是个**文本窗口**的远程操作界面(看输出、敲字、点按钮),
1311
- 对往返延迟本来就宽容 —— 业主原话「500ms 以内的延迟都不叫事,1000ms 使用起来也是正常流畅的」。
1311
+ 对往返延迟本来就宽容 —— 用户原话「500ms 以内的延迟都不叫事,1000ms 使用起来也是正常流畅的」。
1312
1312
  旧的 120/300 分档是照"网页首屏"的体感定的,会把大量正常可用的移动网络误报成"较慢",
1313
1313
  反而让用户以为哪里坏了。 */
1314
1314
  const RTT_KEEP = 8, RTT_MAX_MS = 20000;
@@ -1323,7 +1323,7 @@ const SCRIPT = `(() => {
1323
1323
  const mid = s.length >> 1;
1324
1324
  return s.length % 2 ? s[mid] : Math.round((s[mid - 1] + s[mid]) / 2);
1325
1325
  };
1326
- /* 分档(2026-09-27 按业主口径放宽):文本窗口的远程操作,500ms 内都算顺畅、1000ms 内正常流畅。
1326
+ /* 分档(2026-09-27 按用户约定放宽):文本窗口的远程操作,500ms 内都算顺畅、1000ms 内正常流畅。
1327
1327
  ok=顺畅 / mid=正常 / slow=偏慢 / bad=很慢 —— 颜色只用于区分,**不把"正常"渲染成告警色**。 */
1328
1328
  const rttGrade = (ms) => (ms <= 500 ? "ok" : ms <= 1000 ? "mid" : ms <= 2000 ? "slow" : "bad");
1329
1329
  const rttLabel = (g) => (g === "ok" ? "顺畅" : g === "mid" ? "正常" : g === "slow" ? "偏慢" : "很慢");
@@ -1357,7 +1357,7 @@ const SCRIPT = `(() => {
1357
1357
  if (ico) ico.textContent = shown === null ? "📶" : (g === "bad" ? "📵" : "📶");
1358
1358
  if (txt) {
1359
1359
  /* 空态**说清原因**(2026-10-10):以前只有一句「等一次网络请求后显示」,
1360
- 于是"采样器压根没接上"和"接上了但还没有请求"长得一模一样,业主两次反馈
1360
+ 于是"采样器压根没接上"和"接上了但还没有请求"长得一模一样,用户两次反馈
1361
1361
  我们都只能猜。现在一眼可分。 */
1362
1362
  txt.textContent = shown !== null ? (shown + " ms · " + rttLabel(g))
1363
1363
  : (maNetPatch.ok ? "测速中…(已接上,等一次请求)" : "测速中…(采样器未接上)");
@@ -1585,7 +1585,7 @@ const SCRIPT = `(() => {
1585
1585
  frame.classList.add("dsh-ma-frame");
1586
1586
 
1587
1587
  /* 给 frame 子列打稳定标记(dsh-ma-*),规避官方类名前缀随构建变化。
1588
- ⚠️ 必须先把**不是列**的节点剔掉(2026-09-19 事故根因):
1588
+ ⚠️ 必须先把**不是列**的节点剔掉(2026-09-19 故障根因):
1589
1589
  · overlayLayer(data-shell-overlay / *overlayLayer*)= 官方 shell.overlay 宿主,inset:0 全屏,
1590
1590
  在 0.1.5-rc.2 里**始终**是 frame 的最后一个子元素;
1591
1591
  · 拖拽把手(*handle* / [data-side])= 8px 宽绝对定位条(折叠时官方根本不渲染)。
@@ -1622,7 +1622,7 @@ const SCRIPT = `(() => {
1622
1622
  ① 已经判定为第三列(rightbar/details 类名)的列,**绝不是**抽屉列;
1623
1623
  ② 类名含 sidebar 且不含 rightbar/details(0.1.2/0.1.5 的 pI_x6G_sidebarCol 都满足);
1624
1624
  ③ 退到结构信号:列里有官方侧栏组件根 .hHd-Xa_root,或有**整串**匹配的官方侧栏开合按钮
1625
- (精确匹配,绝不再用 *="sidebar" 这种包含匹配 —— 那正是事故来源);
1625
+ (精确匹配,绝不再用 *="sidebar" 这种包含匹配 —— 那正是故障来源);
1626
1626
  ④ 同时给 CSS 加了 :not(.dsh-ma-details) 兜底:万一将来又标重,详情列的样式也必须赢。 */
1627
1627
  const SIDEBAR_RE = /sidebar/i;
1628
1628
  const NOT_SIDEBAR_RE = /rightbar|right-bar|details/i;
@@ -1742,7 +1742,7 @@ const SCRIPT = `(() => {
1742
1742
  只要官方属性语义与我们理解的不一致,光看属性就可能误判;而遮罩一旦拦截整屏,
1743
1743
  用户看到的就是"整屏阴影 + 点哪都没反应"。以实测几何为准(isDrawer),误判也不会挡住用户。 */
1744
1744
  /* 抽屉必须"像抽屉":不是全屏层(overlay 层/主内容区都被排除在外)、不是拖拽把手、
1745
- **与视口真正相交**。任一不满足 → 它就不该让遮罩拦截点击(0.1.5 事故的结构性防线)。
1745
+ **与视口真正相交**。任一不满足 → 它就不该让遮罩拦截点击(0.1.5 故障的结构性防线)。
1746
1746
  ⚠️ 2026-09-19 手机实测第二波:旧实现只查 r.right > 8,这对"向右滑出屏外"的第三列是
1747
1747
  **恒真**的 —— 抽屉滑出时右边坐标是很大的正数(实测 left=400,right=759,width=358),
1748
1748
  于是"完全看不见、一个像素都没露"的第三列被判成"展开的详情列":
@@ -1809,7 +1809,7 @@ const SCRIPT = `(() => {
1809
1809
  let officialExpandedSeen = false;
1810
1810
  let lastOfficialCollapsed = null;
1811
1811
  let scrimTimer = 0, scrimWatchdog = 0;
1812
- /* ❌ 这里曾有「详情列(第三列)在手机端的显示条件」整段判据(白框事故后打的四层闸门)。
1812
+ /* ❌ 这里曾有「详情列(第三列)在手机端的显示条件」整段判据(白框故障后打的四层闸门)。
1813
1813
  2026-09-27 根治时删除:第三列的显隐**完全交还官方** —— 宽屏按 grid 列、窄屏全屏浮层。
1814
1814
  我们不再有"要不要显示它"的决定权,也就不需要任何判据。 */
1815
1815
 
@@ -2067,7 +2067,7 @@ const SCRIPT = `(() => {
2067
2067
  setTimeout(run, 0);
2068
2068
  };
2069
2069
  /* ===== 手机图片上传兼容(0.6.15)=====
2070
- 业主反馈:「手机端无法识别图片」——现象是选了照片发出去,模型看不到图 / 当成普通文件。
2070
+ 用户反馈:「手机端无法识别图片」——现象是选了照片发出去,模型看不到图 / 当成普通文件。
2071
2071
 
2072
2072
  根因(官方 dsh web 客户端逻辑,dsh-client-ui-conversation 的 createDrafts):
2073
2073
  · 只有 file.type ∈ {image/png, image/jpeg, image/webp, image/gif} 才走**图片**通道
@@ -234,7 +234,7 @@ test("集成:204 响应不压缩", async () => {
234
234
  });
235
235
 
236
236
  test("★ 回归:上游自己 gzip 过(content-encoding: gzip),bridge 仍必须重压回传", async () => {
237
- /* 事故原貌(2026-09-23):
237
+ /* 故障原貌(2026-09-23):
238
238
  * undici 的 fetch 会**自动解压** body,但**保留** `content-encoding: gzip` 响应头。
239
239
  * 旧代码把该头当作"buf 已编码"传给 maybeCompressResponse → 函数保守 return null →
240
240
  * **永不重压**。于是线上 dsh web 的 /plugins/?? 聚合包(上游 gzip 后 5.12MB)
@@ -1,4 +1,4 @@
1
- // 设备登记上报口径回归(2026-10 版本分析需求)。
1
+ // 设备登记上报约定回归(2026-10 版本分析需求)。
2
2
  //
3
3
  // 【为什么必须有】生产反馈只能看到「插件版本」(install_version),从来拿不到
4
4
  // 「宿主 DeepSeek Harness 版本」—— 于是「是不是 DSH 升级导致的兼容性故障」这个问题
@@ -147,7 +147,7 @@ function unaryArgsProblem(method, args) {
147
147
  }
148
148
 
149
149
  /**
150
- * 一元方法的默认应答(真值口径),让 happy-path 用例不必逐个配桩。
150
+ * 一元方法的默认应答(真值约定),让 happy-path 用例不必逐个配桩。
151
151
  * 需要精确控制的用例用 `api.setRpcFor()` 覆盖。
152
152
  */
153
153
  function defaultRpcFor(method, body) {
@@ -331,7 +331,7 @@ async function startFakeDsh(options = {}) {
331
331
  const stream = { streamId: msg.streamId, endpoint: msg.endpoint, payload: msg.payload, closed: false };
332
332
  // 镜像真 gateway 的 payload 校验(dsh-api-gateway/lib/index.js:929):
333
333
  // 非 $events 的流必须恰好带一个 plain-object 的 args 字段 —— 本地假服务端若放松这条,
334
- // 就会漏掉真实线上会拒的形状(这个坑正是靠真机探针发现的)。
334
+ // 就会漏掉真实线上会拒的形状(这个坑正是靠真机探测发现的)。
335
335
  if (!isPlainArgsPayload(msg.payload)) {
336
336
  api.fail(conn, msg.streamId, {
337
337
  code: "gateway/internal",
@@ -815,7 +815,7 @@ test("follow 的会话 id:原样为主、alternative 兜底一次;agent-busy
815
815
  assert.equal(toDurableSessionId("session-9b4f"), "session-9b4f");
816
816
 
817
817
  const faults = [];
818
- // 真机口径:子代理会话的真实 id 是裸 uuid,`session-<uuid>` 不存在(实测)。
818
+ // 真机约定:子代理会话的真实 id 是裸 uuid,`session-<uuid>` 不存在(实测)。
819
819
  // 这个假服务端只认「原样 id」,对另一种形式回 session/not-found。
820
820
  const api = await startFakeDsh({
821
821
  onOpen(server, conn, stream) {
@@ -847,7 +847,7 @@ test("follow 的会话 id:原样为主、alternative 兜底一次;agent-busy
847
847
  sub.on("fault", (fault) => faults.push(fault));
848
848
  await start(sub);
849
849
 
850
- // 事件里报裸 uuid(= 真机 api-session/status 的口径)→ 主形式必须**原样**,不加前缀
850
+ // 事件里报裸 uuid(= 真机 api-session/status 的约定)→ 主形式必须**原样**,不加前缀
851
851
  const reportedId = "9b4f9e73-8912-49ca-98ba-23da9c34ab47";
852
852
  globalThis.__acceptedId = `session-${reportedId}`; // 服务端只认带前缀的那种
853
853
  api.pushEmit("api-session/status", [reportedId, true]);
@@ -1997,17 +1997,17 @@ test("一元:本地校验(空 text / 缺 sessionId / 坏 mode…)直接类
1997
1997
  }
1998
1998
  });
1999
1999
 
2000
- // ═══════════════════ 2026-09-25 事故:完成推送静默消失(业主实测)═══════════════════
2000
+ // ═══════════════════ 2026-09-25 故障:完成推送静默消失(用户实测)═══════════════════
2001
2001
  //
2002
2002
  // 现场:任务跑完了,微信**一条推送都没有**,bridge 日志里也**没有任何报错**。
2003
- // 活体探针(以微信通道同款方式订阅本机 dsh web)显示:
2003
+ // 活体探测(以微信通道同款方式订阅本机 dsh web)显示:
2004
2004
  // · 新起的订阅器 163ms 就发现并 follow 了正在跑的会话 —— 机制本身是好的;
2005
2005
  // · 而当时在跑的 bridge 进程到 3080 只有 2 条 WS(mux + control)、**没有 follow 流**,
2006
2006
  // 可它明明有一个正在跑长任务的会话。
2007
2007
  // 根因就在下面这条:周期性发现被"当前一条 follow 都没有"这个条件**关掉了自己**——
2008
2008
  // 丢一次 status:true 边沿就永久失去发现能力,而 turn/end 只在 follow 流上才有。
2009
2009
 
2010
- test("★ 事故锁:一条 follow 都没有时,周期性发现**仍然要跑**(丢一次边沿不能永久失聪)", async () => {
2010
+ test("★ 回归锁:一条 follow 都没有时,周期性发现**仍然要跑**(丢一次边沿不能永久失聪)", async () => {
2011
2011
  const api = await startFakeDsh({ listItems: [] }); // 连上时没有任何会话在跑
2012
2012
  const sub = createEventSubscriber({
2013
2013
  upstream: api.base,
@@ -2087,18 +2087,18 @@ test("★ 一次 follow 失败不该把这条会话**永久**钉死:下一轮
2087
2087
  });
2088
2088
 
2089
2089
  // ============================================================
2090
- // 上游是**函数**:宿主换端口要能自己跟上(2026-10-09 真机事故回归)
2090
+ // 上游是**函数**:宿主换端口要能自己跟上(2026-10-09 真机故障回归)
2091
2091
  // ============================================================
2092
2092
 
2093
2093
  /**
2094
- * 【事故原貌】bridge 起来时宿主在 3080,随后宿主换端口(DSH Desktop 每次启动都是随机端口)。
2094
+ * 【故障原貌】bridge 起来时宿主在 3080,随后宿主换端口(DSH Desktop 每次启动都是随机端口)。
2095
2095
  * 隧道转发侧每次都读最新 UPSTREAM,所以一切正常;**唯独事件订阅器抱着启动那一刻的字符串**,
2096
2096
  * 于是永久 `connect ECONNREFUSED 127.0.0.1:3080` —— 手机能开页面、面板显示 online,
2097
2097
  * 但任务跑完**一条微信推送都没有**。迷惑性极强,最后靠 bridge 日志里刷屏的那一行才定位。
2098
2098
  *
2099
2099
  * 修法:`upstream` 允许传函数,每次连接/RPC 重新求值。这条用例就是那件事的锁。
2100
2100
  */
2101
- test("★ 事故锁:upstream 传函数时,宿主换端口后订阅器必须自己跟过去(不得永久打旧端口)", async () => {
2101
+ test("★ 回归锁:upstream 传函数时,宿主换端口后订阅器必须自己跟过去(不得永久打旧端口)", async () => {
2102
2102
  const a = await startFakeDsh();
2103
2103
  const b = await startFakeDsh();
2104
2104
  let current = a.base;
@@ -310,7 +310,7 @@ test("开关与回退:enabled bridge 上报 caps + 状态文件;off/401 明文
310
310
  assert.match(await r.text(), /⚠/);
311
311
  });
312
312
 
313
- test("E2EE 解锁握手(device 形态)+ channel 形态可开,探针通过后会话可用", async () => {
313
+ test("E2EE 解锁握手(device 形态)+ channel 形态可开,探测通过后会话可用", async () => {
314
314
  const { mk, params } = await phonePrepare();
315
315
  const { ctrl, client, sessId } = await unlock(`${routerBase}/remote/${DEV_ON}/_e2ee/ctrl`, { mk, params });
316
316
  assert.equal(sessId.length, 32);
@@ -379,7 +379,7 @@ test("E2EE HTTP:信封请求解封→上游→封回往返一致;篡改密文
379
379
  });
380
380
 
381
381
  test("★ 0.6.15 三层 base64 降为两层:声明过的客户端收到二进制明文,协商头不转发上游", async () => {
382
- // 业主实测(dev-1014f947f8ab,Ubuntu 上的 dsh + Windows 访问,首屏要等几分钟):
382
+ // 用户实测(dev-1014f947f8ab,Ubuntu 上的 dsh + Windows 访问,首屏要等几分钟):
383
383
  // 同一条响应被 base64 编了三层(1.333³≈2.37×)。第一层是"gzip 正文进 JSON",
384
384
  // 现在由客户端声明 x-dsh-e2ee-want: bin 换成二进制框架;老客户端不声明 → 形态不变。
385
385
  const u = globalThis.__unlocked;
@@ -492,7 +492,7 @@ test("E2EE WS 数据流:&e2ee=&w= 标记 → 桥剥除标记连上游,逐消息
492
492
  // (而非测试内复制)跑真实解锁 + HTTP 信封 + WS 数据流,验证手机端与桥端互通。
493
493
  // ============================================================
494
494
 
495
- /** 手机端(浏览器 WebCrypto)解锁:hello→ack→deriveShk→探针;密码错 → bad_key。 */
495
+ /** 手机端(浏览器 WebCrypto)解锁:hello→ack→deriveShk→探测;密码错 → bad_key。 */
496
496
  function wcUnlock(ctrlWsUrl, { mkBytes, params }) {
497
497
  return new Promise((resolve, reject) => {
498
498
  const sessId = wc.wcNewSessId();
@@ -649,7 +649,7 @@ test("Phase-4 镜像页 shim:注入+交接单重建+HTTP 信封/WS 真实互通;
649
649
  const params = await fetchE2eeParams(apiBase, signJwt({ sub: "42", phone: "13811110001", plan: "free" }));
650
650
  assert.equal(params.enabled, true);
651
651
  const mk = new Uint8Array(deriveMasterKey(PASSWORD, params.salt, params.kdf));
652
- // 手机端(浏览器 WebCrypto)解锁 → 已与桥端探针互认的会话
652
+ // 手机端(浏览器 WebCrypto)解锁 → 已与桥端探测互认的会话
653
653
  const { client, sessId } = await wcUnlock(`${routerBase}/remote/${DEV_ON}/_e2ee/ctrl`, { mkBytes: mk, params });
654
654
  assert.equal(sessId.length, 32);
655
655