@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
@@ -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 最后一个子元素"于是把
@@ -85,6 +85,30 @@
85
85
  * ③ 遮罩不变量:只要遮罩在拦截点击而视口内没有任何可交互抽屉,立刻摘掉(800ms 看门狗 +
86
86
  * 300ms 复核),且**只摘遮罩不动展开 class**(不破坏抽屉的滑入动画与"点一下就打开")。
87
87
  *
88
+ * 🔴 2026-10-13 手机端设置面板「挤成一条竖线」根治(用户反馈,真机 390×844 + 本机 0.2 宿主实测):
89
+ * 现象:窄屏打开「设置」,左侧 nav(通用设置/模型/内置插件/…)是一条竖列,右侧内容被压成
90
+ * 四五字宽的竖条并向右溢出被裁。用户明确要求的目标形态:**tab 放顶部(横排)+ 下方内容满宽**。
91
+ * 根因(实测数字):官方设置弹层是**桌面两栏**布局 —— 本机 0.2.0-rc.2(dsh-web-frontend
92
+ * 0.2 + dsh-client-ui-settings-general 0.2)实测 panel 342×796、`nav` 188 宽(flex column)、
93
+ * `content` 可视宽只剩 **154px**。而适配层只挂了 `[class~="VOzbGW_*"]` 精确类,那是
94
+ * **0.1.2–0.1.5** 的混淆前缀;0.2 重新打包后前缀变成 **`Dws9Sa_*`**(结构、类名一一对应:
95
+ * Dws9Sa_overlay/panel/nav/navTitle/navList/navCell/content/header/close/options)
96
+ * → 整段设置区 CSS **静默失效**。这正是文件开头"类名变化退化策略"预言过的失效模式:
97
+ * 页面照常打开、只有真机看得出布局坏,而且没人会收到任何信号。
98
+ * 修法(三层识别,任一层命中即可成型,不再可能整段静默失效):
99
+ * ① 已知前缀:`[class~="VOzbGW_*"]` 与 `[class~="Dws9Sa_*"]` 并挂(CSS 与 JS 都改);
100
+ * ② **结构兜底**:JS 按 `[role=dialog]/[aria-modal] + 直接子 <nav> + 近似全屏` 认面板,
101
+ * 再按"子元素最多者为 tab 列表"认 navList,打 `dsh-ma-settings-*` 稳定类,由 CSS 成型
102
+ * —— 前缀再改也是"仍然基本可用",而不是彻底坏掉;
103
+ * ③ CSS `:has()` 兜底(`div[role="dialog"]:has(> nav)` 等):支持 :has 的浏览器连 JS 都不等。
104
+ * 降级诊断(**可见**,但不引入任何覆盖层 —— 护栏明令禁止):① 前缀认不出 → 报
105
+ * `console.warn("[dsh-mobile-adapter]", …注意 …混淆前缀已变化…)` 一次;② 有"大弹层"却认不出
106
+ * 设置面板结构 → 报一次"手机端可能仍是桌面两栏布局"。两条都只在 ≤820px 发生。
107
+ * 同批:窄屏**通用弹层**按手机尺寸收敛(`[role=dialog]/[role=alertdialog]/[aria-modal]/.dru-popup-card`
108
+ * 宽 ≤ 100vw-24px、高 ≤ 100dvh-24px 且内部可滚、圆角收敛 12px),修掉"内容裁在右边"。
109
+ * ⚠️ 顺序有讲究:通用弹层规则必须排在设置弹层规则**之前** —— 两者特异性相同,靠顺序让
110
+ * "设置面板铺满全屏"赢(否则设置面板会被 100vw-24px 钉住,又变成带边的浮层)。
111
+ *
88
112
  * 开关:环境变量 DSH_MOBILE_ADAPTER=0 整体关闭(默认开启)。
89
113
  */
90
114
 
@@ -99,6 +123,17 @@ const HOST_FEATURE_RE =
99
123
  const RUNTIME_HOST_RE =
100
124
  /(data-sidebar-collapsed|data-details-collapsed|data-rightbar-collapsed|data-shell-overlay)|(pI_x6G_frame|hHd-Xa_root|hHd-Xa_toggle)/;
101
125
 
126
+ /**
127
+ * 【可选】官方锚点清单 —— 供 scripts/check-ui-anchors.mjs 分类用(它直接读这个常量)。
128
+ * 这些名字**存在就用、不存在就按结构/几何退化**,所以它们缺失**不算故障**:
129
+ * · data-details-collapsed —— 官方 0.1.5-rc.2 已改名 data-rightbar-collapsed,这里只为兼容旧版保留;
130
+ * · data-sidebar-open —— 官方是否输出这个属性本身就不确定(见下面对 sidebarAttrSeen 的注释),
131
+ * 适配层刻意做了"认不到就只信几何"的分支;
132
+ * · data-rtt —— 适配层**自己**的延迟角标属性(不是官方锚点),列出只为让自检脚本不去它身上找。
133
+ * 约定:这里**只放**这种"缺了也无所谓"的锚点;凡是缺了会让布局坏掉的锚点,一律不要写进来。
134
+ */
135
+ const OPTIONAL_UI_ANCHORS = ["data-details-collapsed", "data-sidebar-open", "data-rtt"];
136
+
102
137
  const STYLE = `
103
138
 
104
139
  @media (max-width: 820px) {
@@ -153,10 +188,10 @@ const STYLE = `
153
188
  ══════════════════════════════════════════════════════════════════════════════
154
189
  🚫 这里**故意什么都不写** —— 第三列(官方右栏)的显示/隐藏完全交还给官方。
155
190
  ══════════════════════════════════════════════════════════════════════════════
156
- 历史(2026-09-27 根治,业主拍板 B 方案):
191
+ 历史(2026-09-27 根治,用户拍板 B 方案):
157
192
  0.6.13 起我们把这列改造成自绘浮层 —— position:fixed; right:0; width:min(92vw,400px);
158
193
  transform:translateX(103%),再用 html.dsh-ma-details-open … { transform:none } 滑进来。
159
- 因为"自绘浮层会白框压屏"(2026-09-19 事故),又加了一道很严的闸门(必须"用户 8 秒内
194
+ 因为"自绘浮层会白框压屏"(2026-09-19 故障),又加了一道很严的闸门(必须"用户 8 秒内
160
195
  在正文里点过"才允许滑入);而 better-sidebar 这类插件只翻**它自己**的 store、官方
161
196
  cols.rightbar 永远是 0 → 闸门永远不开 → 于是又在悬浮菜单里补了颗「打开右侧栏」按钮。
162
197
 
@@ -188,7 +223,7 @@ const STYLE = `
188
223
  opacity: 0; pointer-events: none;
189
224
  transition: opacity .22s ease;
190
225
  }
191
- /* ⚠️ 拦截点击**只认 dsh-ma-scrim-on 这一个闸**(2026-09-19 复盘):
226
+ /* ⚠️ 拦截点击**只认 dsh-ma-scrim-on 这一个闸**(2026-09-19 回顾):
192
227
  旧版还并列了一个「html.dsh-ma-details-open div.dsh-ma-scrim」规则,于是"详情展开判定出错"
193
228
  会直接变成"整屏被遮罩拦截"——官方 0.1.5 把第三列改名 rightbar 后就是这个死局。
194
229
  现在判定收口在 JS 里(必须有一个**真的在视口内、且不是全屏的**抽屉),
@@ -226,11 +261,42 @@ const STYLE = `
226
261
  div.wSkVaW_heroWorkspaceRow > * { max-width: calc(100vw - 32px); }
227
262
  div.wSkVaW_composerStack, div.wSkVaW_composerHero, div.uV2eYG_card { min-width: 0; }
228
263
 
229
- /* --- 设置面板(fix2 硬化版;实测 390×844:官方模块 CSS 运行期注入在适配层之后,
230
- 普通规则会被官方后加载样式覆盖——nav 是 <nav> 而非 div,圆1 的 div.VOzbGW_nav
231
- 从未命中;overlay 被官方宽度 min(800px,84vw) 钉在左侧 328px,呈“抽屉感”。
232
- 故本版全部用 [class~=...] 精确类 + 几何 !important,官方前缀变化则整段静默失效) --- */
233
- [class~="VOzbGW_overlay"] {
264
+ /* ===== 通用弹层(dialog/modal)按手机尺寸收敛(2026-10-13)=====
265
+ 官方弹层(设置 / 审批 / 确认 / 各类 modal)在桌面上写死 px 宽高,窄屏会右溢被裁;
266
+ 本项目自己的 .dru-popup 卡片同理。这里只加三条**尺寸**硬约束,不动配色与结构:
267
+ · 宽 ≤ 100vw - 24px(两侧各留 12px)—— 用户报的"内容裁在右边"根治;
268
+ · 高 ≤ 100dvh - 24px 且自身可滚(先写 100vh 给不认 dvh 的老浏览器兜底);
269
+ · 窄屏圆角收敛到 12px(官方 --dsw-radius-panel 在手机上偏大)。
270
+ ⚠️ 必须放在设置弹层规则**之前**:两者特异性相同,靠顺序让"设置面板铺满全屏"赢。 */
271
+ [role="dialog"], [role="alertdialog"], [aria-modal="true"], .dru-popup-card {
272
+ box-sizing: border-box;
273
+ max-width: calc(100vw - 24px) !important;
274
+ max-height: calc(100vh - 24px) !important;
275
+ max-height: calc(100dvh - 24px) !important;
276
+ border-radius: 12px !important;
277
+ overflow: auto !important;
278
+ overscroll-behavior: contain;
279
+ -webkit-overflow-scrolling: touch;
280
+ }
281
+ /* .dru-popup 是遮罩层,卡片 .dru-popup-card 才是"弹窗":窄屏把 24px 外边距收到 12px */
282
+ .dru-popup { padding: 12px !important; }
283
+ .dru-popup-card { width: 100% !important; max-width: calc(100vw - 24px) !important; }
284
+
285
+ /* ===== 设置弹层:窄屏「顶部横向 tab 行 + 下方内容区满宽」(2026-10-13 重写)=====
286
+ 🔴 根因(真机 390×844 实测,用户报「设置界面挤成一条竖线」):
287
+ 官方弹层是**桌面两栏** —— 0.2.0-rc.2 实测 panel 342×796、nav 188 宽竖排、
288
+ content(内容区)只剩 **154px** 可视宽 → 内容被压成竖条并向右溢出被裁。
289
+ 旧实现只有 [class~="VOzbGW_*"](0.1.2–0.1.5 的混淆前缀);0.2.0-rc.2 重新打包后
290
+ 前缀变成 Dws9Sa_*,**整段 CSS 静默失效** —— 页面照常打开,只有真机看得出坏。
291
+ 所以识别分三层,任一层命中即可成型(这是"不再静默失效"的核心):
292
+ ① 已知前缀 [class~="VOzbGW_*"] / [class~="Dws9Sa_*"](最短路径,随版本追加);
293
+ ② JS 结构识别后打的稳定类 .dsh-ma-settings-*(role=dialog + 直接子 <nav> + 几何,
294
+ 见脚本里的 maTagSettingsDialog)—— 前缀再改也照样生效;
295
+ ③ CSS :has() 兜底(div[role="dialog"]:has(> nav) 等),不必等 JS 就能成型。
296
+ ⚠️ 全部 !important:官方模块 CSS 是**运行期**注入 head 的(晚于本 <style>),
297
+ 同特异性下后加载者赢 —— fix2 之前 nav 选择器"从未命中"正是这个原因。 */
298
+ [class~="VOzbGW_overlay"], [class~="Dws9Sa_overlay"], .dsh-ma-settings-overlay,
299
+ div[role="presentation"]:has(> div[role="dialog"] > nav) {
234
300
  position: fixed !important;
235
301
  inset: 0 !important;
236
302
  width: auto !important; max-width: none !important;
@@ -240,31 +306,37 @@ const STYLE = `
240
306
  box-sizing: border-box;
241
307
  justify-content: center; align-items: stretch;
242
308
  }
243
- [class~="VOzbGW_panel"] {
309
+ /* 面板:nav 在上、content 在下的**竖排**,并铺满视口(圆角/外边距在窄屏收敛为 0) */
310
+ [class~="VOzbGW_panel"], [class~="Dws9Sa_panel"], .dsh-ma-settings-panel,
311
+ div[role="dialog"]:has(> nav) {
244
312
  box-sizing: border-box;
245
313
  width: 100% !important; max-width: 100% !important;
246
314
  height: 100% !important; max-height: 100% !important;
247
315
  margin: 0 !important; border-radius: 0 !important;
248
316
  flex-direction: column !important;
317
+ overflow: hidden !important; /* 内层 options 自己滚 */
249
318
  }
250
319
  /* 顶部 tab 行:整行铺满,字号调小(≤13px),横向可滚动 */
251
- [class~="VOzbGW_nav"] {
320
+ [class~="VOzbGW_nav"], [class~="Dws9Sa_nav"], .dsh-ma-settings-nav,
321
+ div[role="dialog"] > nav {
252
322
  box-sizing: border-box;
253
323
  flex: none;
254
324
  width: 100% !important;
255
325
  flex-direction: row !important;
256
326
  align-items: center;
257
327
  gap: 4px;
258
- padding: max(4px, env(safe-area-inset-top)) 4px 0;
328
+ padding: max(4px, env(safe-area-inset-top)) 8px 6px;
259
329
  overflow: visible;
260
330
  }
261
- [class~="VOzbGW_navTitle"] {
331
+ [class~="VOzbGW_navTitle"], [class~="Dws9Sa_navTitle"], .dsh-ma-settings-navtitle,
332
+ div[role="dialog"] > nav > :first-child {
262
333
  flex: none;
263
334
  font-size: 13px !important; line-height: 1.2;
264
335
  padding: 0 4px 0 8px;
265
336
  white-space: nowrap;
266
337
  }
267
- [class~="VOzbGW_navList"] {
338
+ [class~="VOzbGW_navList"], [class~="Dws9Sa_navList"], .dsh-ma-settings-navlist,
339
+ div[role="dialog"] > nav > :last-child {
268
340
  box-sizing: border-box;
269
341
  flex: 1 1 auto; min-width: 0;
270
342
  flex-direction: row !important;
@@ -274,33 +346,40 @@ const STYLE = `
274
346
  scrollbar-width: none;
275
347
  padding: 0 0 6px;
276
348
  }
277
- [class~="VOzbGW_navList"]::-webkit-scrollbar { display: none; }
278
- [class~="VOzbGW_navCell"] {
349
+ [class~="VOzbGW_navList"]::-webkit-scrollbar,
350
+ [class~="Dws9Sa_navList"]::-webkit-scrollbar,
351
+ .dsh-ma-settings-navlist::-webkit-scrollbar { display: none; }
352
+ [class~="VOzbGW_navCell"], [class~="Dws9Sa_navCell"], .dsh-ma-settings-cell,
353
+ div[role="dialog"] > nav > :last-child > * {
279
354
  flex: none; min-width: max-content;
280
355
  box-sizing: border-box;
281
356
  font-size: 13px !important;
282
357
  height: 32px !important;
283
358
  padding: 0 12px !important;
359
+ white-space: nowrap;
284
360
  }
285
361
  /* 内容区:全宽拉满,收敛内边距,最大化有效宽度 */
286
- [class~="VOzbGW_content"] {
362
+ [class~="VOzbGW_content"], [class~="Dws9Sa_content"], .dsh-ma-settings-content,
363
+ div[role="dialog"]:has(> nav) > div {
287
364
  box-sizing: border-box;
288
365
  width: 100% !important; max-width: none !important;
289
366
  flex: 1 1 auto; min-width: 0; min-height: 0;
290
367
  }
291
- [class~="VOzbGW_header"] {
368
+ [class~="VOzbGW_header"], [class~="Dws9Sa_header"], .dsh-ma-settings-header {
292
369
  box-sizing: border-box;
293
370
  flex: none;
294
- min-height: 40px;
371
+ min-height: 40px; height: auto !important;
295
372
  padding: 6px 8px 4px 14px !important;
296
373
  align-items: center;
297
374
  }
298
- [class~="VOzbGW_close"] { width: 32px; height: 32px; }
299
- [class~="VOzbGW_options"] {
375
+ [class~="VOzbGW_close"], [class~="Dws9Sa_close"], .dsh-ma-settings-close {
376
+ width: 32px !important; height: 32px !important;
377
+ }
378
+ [class~="VOzbGW_options"], [class~="Dws9Sa_options"], .dsh-ma-settings-options {
300
379
  box-sizing: border-box;
301
380
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
302
381
  -webkit-overflow-scrolling: touch;
303
- padding: 2px 14px calc(14px + env(safe-area-inset-bottom)) !important;
382
+ padding: 4px 14px calc(14px + env(safe-area-inset-bottom)) !important;
304
383
  }
305
384
  }
306
385
 
@@ -524,7 +603,7 @@ const SCRIPT = `(() => {
524
603
  "use strict";
525
604
 
526
605
  /* ===== 【0.6.16 已删除】首屏加载提示层(0.6.14 引入,0.6.15 仍在)=====
527
- 为什么删掉(业主实测,2026-09-25):「界面都已经加载完了,十几秒就把插件的接口加载好了,
606
+ 为什么删掉(用户实测,2026-09-25):「界面都已经加载完了,十几秒就把插件的接口加载好了,
528
607
  但这个遮罩一直不消失」—— 截图里官方 UI 就在下面透出来,秒表走到 187 秒。
529
608
 
530
609
  根因是**设计错误**,不是某个选择器写错:
@@ -535,7 +614,7 @@ const SCRIPT = `(() => {
535
614
  官方改版、会话尚未挂载输入框、或前缀变化时两组信号全不命中 → 层就永远留着。
536
615
  · 当时我给自己写的安慰是「pointer-events:none,万一移除失效也绝不挡住点击」——
537
616
  可对用户来说,**盖住视线和进不去没有区别**:刷新也一样,每次都要等 4 分钟兜底。
538
- 结论(业主要求):**直接删掉这个功能**。首屏反馈交给官方自己的转圈;
617
+ 结论(用户要求):**直接删掉这个功能**。首屏反馈交给官方自己的转圈;
539
618
  我们不再在用户页面上放任何会覆盖整屏的东西 —— 这类层一旦判断失误就是"永远进不去",
540
619
  代价远大于它带来的提示价值(何况限速放开后首屏本来就快了)。
541
620
 
@@ -579,7 +658,7 @@ const SCRIPT = `(() => {
579
658
 
580
659
  /* ===== 「加载疑似没完成」看门狗(2026-09-25,替代被整块删除的首屏提示层)=====
581
660
  那个提示层是全屏覆盖 + 启发式退出 —— 判断错一次就把**已经加载完**的界面永久盖住
582
- (业主实测:秒表走到 187 秒,刷新也一样)。所以这里换一条**安全的失败方向**:
661
+ (用户实测:秒表走到 187 秒,刷新也一样)。所以这里换一条**安全的失败方向**:
583
662
 
584
663
  · 解析期**不创建任何 DOM**(只有几个变量 + 一个定时器)—— 页面还没过主机门,
585
664
  更不该有任何覆盖层;
@@ -614,13 +693,13 @@ const SCRIPT = `(() => {
614
693
  return Date.now();
615
694
  }
616
695
  /**
617
- * 镜像页就绪耗时(毫秒)——**口径见 docs/TELEMETRY-CONTRACT.md / 迭代冻结文档 §2.3**:
696
+ * 镜像页就绪耗时(毫秒)——**约定见 docs/TELEMETRY-CONTRACT.md / 迭代冻结文档 §2.3**:
618
697
  * · 起点 = **导航开始**(performance.now() 的零点 = performance.timeOrigin ≈ 导航开始),
619
698
  * 所以一条值同时覆盖 DNS+TLS+HTML TTFB + 解析 + 界面就绪;
620
699
  * · **不能**用 Date.now() - maLoadWatch.startedAt:那个起点是"注入脚本解析时刻",会**漏掉
621
700
  * HTML TTFB**(HAR 实测那 79 秒里 TTFB 就有 8.7 秒),报出来的数会系统性小于用户体感;
622
- * 而且 bfcache 恢复(pageshow persisted)不会重置 startedAt,那口径还能报出几十分钟。
623
- * · 没有 performance 时退化回 startedAt 口径(老浏览器兜底,宁可粗糙也不能漏报)。
701
+ * 而且 bfcache 恢复(pageshow persisted)不会重置 startedAt,那约定还能报出几十分钟。
702
+ * · 没有 performance 时退化回 startedAt 约定(老浏览器兜底,宁可粗糙也不能漏报)。
624
703
  */
625
704
  function maLoadElapsedMs() {
626
705
  try {
@@ -995,13 +1074,177 @@ const SCRIPT = `(() => {
995
1074
  whaleNativeDragToBottomRight();
996
1075
  }
997
1076
 
1077
+ /* ===== 设置面板:结构识别 + 稳定类(2026-10-13;根因见 CSS 里那段长注释)=====
1078
+ 为什么不能只靠 [class~="VOzbGW_*"]:官方重新打包会换混淆前缀(0.1.5 = VOzbGW_,
1079
+ 0.2.0-rc.2 = Dws9Sa_),改名后整段 CSS **静默失效** —— 页面照常打开,
1080
+ 只是手机端布局坏掉,而且只有真机看得出。所以这里再按**结构 + 几何**认一遍:
1081
+ · 面板 = [role=dialog]/[aria-modal] 且**直接子元素里有 <nav>**、尺寸接近全屏
1082
+ (0.1.5 与 0.2 的面板都是 div[role=dialog] > nav + div 两栏,且都是固定像素宽);
1083
+ · 列表 = nav 里子元素最多的那个孩子(单元格 ≥3 个);
1084
+ · content = 面板里除 nav 之外的孩子;header/options = content 的首/末孩子。
1085
+ 命中 → 打 .dsh-ma-settings-* 稳定类,由 CSS 接管(前缀改名后**仍然基本可用**);
1086
+ 命中不到 → 原样放过(绝不破坏官方默认),只 warn 一次作为**可见的降级诊断**
1087
+ —— 护栏禁止适配层引入全屏覆盖层,所以诊断只能走 console / 既有上报通道。 */
1088
+ var MA_SETTINGS_PREFIX_RE = /(^|\s)(VOzbGW_|Dws9Sa_)/;
1089
+ var maSettingsPanelEl = null; // 已识别的面板(靠 isConnected 判存活,官方重渲染后自动重认)
1090
+ var maSettingsCells = []; // 已识别的 tab 单元格
1091
+ var maSettingsNextScan = 0; // 未命中时的扫描节流(见 maSettingsMaybeTag)
1092
+ var maSettingsWarned = "";
1093
+ function maSettingsWarn(msg) {
1094
+ if (maSettingsWarned === msg) return;
1095
+ maSettingsWarned = msg;
1096
+ /* window.console 优先(镜像页里它就是页面控制台),拿不到再退到全局 console ——
1097
+ 诊断**必须**能出得来,否则"静默失效"只是换了个地方静默。 */
1098
+ try {
1099
+ var c = (typeof window !== "undefined" && window && window.console) || (typeof console !== "undefined" ? console : null);
1100
+ if (c && c.warn) c.warn("[dsh-mobile-adapter]", msg);
1101
+ } catch (eW) {}
1102
+ }
1103
+ function maClsOf(el) { try { return (el && typeof el.className === "string") ? el.className : ""; } catch (eC) { return ""; } }
1104
+ function maKidsOf(el) { try { return (el && el.children) ? [].slice.call(el.children) : []; } catch (eK) { return []; } }
1105
+ /** 面板级几何门:排除小气泡/菜单/下拉,只认接近全屏的弹层。 */
1106
+ function maPanelSized(el) {
1107
+ try {
1108
+ var r = el.getBoundingClientRect();
1109
+ if (!r || !(r.width > 0) || !(r.height > 0)) return false;
1110
+ return r.width >= Math.max(240, window.innerWidth * 0.5) && r.height >= 160;
1111
+ } catch (eG) { return false; }
1112
+ }
1113
+ /** 结构识别:找到官方设置面板(flex 两栏 = 直接子 <nav> + 另一栏)。 */
1114
+ function maFindSettingsPanel() {
1115
+ var cands = [];
1116
+ try {
1117
+ cands = [].slice.call(document.querySelectorAll('[role="dialog"], [role="alertdialog"], [aria-modal="true"]'));
1118
+ } catch (eQ) { return null; }
1119
+ var best = null, bestArea = 0;
1120
+ for (var i = 0; i < cands.length; i++) {
1121
+ var el = cands[i];
1122
+ var kids = maKidsOf(el);
1123
+ if (kids.length < 2) continue; // 两栏才有 content 可谈
1124
+ var nav = null;
1125
+ for (var j = 0; j < kids.length; j++) { if (kids[j] && kids[j].tagName === "NAV") { nav = kids[j]; break; } }
1126
+ if (!nav || maKidsOf(nav).length < 2) continue; // 设置面板的 nav = 标题 + tab 列表
1127
+ if (!maPanelSized(el)) continue;
1128
+ var r = el.getBoundingClientRect();
1129
+ var area = r.width * r.height;
1130
+ if (area > bestArea) { bestArea = area; best = el; }
1131
+ }
1132
+ return best;
1133
+ }
1134
+ /** nav 里的 tab 列表 = 子元素最多的那个孩子(标题只有一个文本孩子)。 */
1135
+ function maNavListOf(nav) {
1136
+ var kids = maKidsOf(nav);
1137
+ if (!kids.length) return null;
1138
+ var best = null, bestN = 0;
1139
+ for (var i = 0; i < kids.length; i++) {
1140
+ var n = maKidsOf(kids[i]).length;
1141
+ if (n > bestN) { bestN = n; best = kids[i]; }
1142
+ }
1143
+ return bestN >= 2 ? best : (kids.length > 1 ? kids[kids.length - 1] : null);
1144
+ }
1145
+ /** 当前选中项:官方状态类(任意前缀 + _active)或 aria-current —— 两者都不依赖前缀。 */
1146
+ function maCellActive(cell) {
1147
+ try {
1148
+ if (cell.getAttribute && cell.getAttribute("aria-current") === "true") return true;
1149
+ /* 只认「整段 token 以 _active 结尾」或「token 就叫 active」——
1150
+ 不能用 *active 这种宽松匹配:inactive 也会命中,会把没选中的 tab 当成当前项。
1151
+ ⚠️ 本文件是模板字符串,注释里**不得出现反引号**(会提前闭合注入体,见 AGENTS.md)。 */
1152
+ return /(^|\s)(?:[A-Za-z0-9_-]+_active|active)(\s|$)/.test(maClsOf(cell));
1153
+ } catch (eA) { return false; }
1154
+ }
1155
+ function maMarkActiveCells() {
1156
+ for (var i = 0; i < maSettingsCells.length; i++) {
1157
+ var c = maSettingsCells[i];
1158
+ try { if (c && c.isConnected && maCellActive(c)) c.classList.add("dsh-ma-settings-active"); } catch (eM) {}
1159
+ }
1160
+ }
1161
+ function maTagSettingsDialog() {
1162
+ var panel = maFindSettingsPanel();
1163
+ if (!panel) {
1164
+ /* 认不到面板时给一条**可见的**降级诊断:现场是"大弹层 + 首栏比次栏窄"的桌面两栏形态,
1165
+ 说明官方又换了结构(或换成了我们不认识的前缀)—— 这条 warn 就是下次定位问题的起点。 */
1166
+ try {
1167
+ var any = document.querySelector('[role="dialog"], [aria-modal="true"]');
1168
+ if (any && maPanelSized(any)) {
1169
+ maSettingsWarn("检测到大弹层但未能识别为设置面板(官方结构/类名可能已变化):手机端可能仍是桌面两栏布局。");
1170
+ }
1171
+ } catch (eAny) {}
1172
+ maSettingsPanelEl = null; maSettingsCells = [];
1173
+ return;
1174
+ }
1175
+ var kids = maKidsOf(panel);
1176
+ var nav = null, content = null;
1177
+ for (var i = 0; i < kids.length; i++) {
1178
+ if (!nav && kids[i].tagName === "NAV") { nav = kids[i]; continue; }
1179
+ if (!content) content = kids[i];
1180
+ }
1181
+ var list = nav ? maNavListOf(nav) : null;
1182
+ var title = null;
1183
+ var navKids = maKidsOf(nav);
1184
+ for (var k = 0; k < navKids.length; k++) { if (navKids[k] !== list) { title = navKids[k]; break; } }
1185
+ var contentKids = maKidsOf(content);
1186
+ var header = contentKids[0] || null;
1187
+ var options = contentKids.length > 1 ? contentKids[contentKids.length - 1] : null;
1188
+ var overlay = null;
1189
+ try {
1190
+ var p = panel.parentElement || panel.parent || null; // parentElement 是标准属性;parent 兜底给极简 DOM
1191
+ if (p && p.getBoundingClientRect) {
1192
+ var pr = p.getBoundingClientRect();
1193
+ if (pr && pr.width >= window.innerWidth * 0.9 && pr.height >= window.innerHeight * 0.9) overlay = p;
1194
+ }
1195
+ } catch (eP) {}
1196
+ try {
1197
+ panel.classList.add("dsh-ma-settings-panel");
1198
+ if (nav) nav.classList.add("dsh-ma-settings-nav");
1199
+ if (title) title.classList.add("dsh-ma-settings-navtitle");
1200
+ if (list) list.classList.add("dsh-ma-settings-navlist");
1201
+ if (content) content.classList.add("dsh-ma-settings-content");
1202
+ if (header) header.classList.add("dsh-ma-settings-header");
1203
+ if (options) options.classList.add("dsh-ma-settings-options");
1204
+ if (overlay) overlay.classList.add("dsh-ma-settings-overlay");
1205
+ maSettingsCells = list ? maKidsOf(list) : [];
1206
+ for (var c = 0; c < maSettingsCells.length; c++) maSettingsCells[c].classList.add("dsh-ma-settings-cell");
1207
+ maMarkActiveCells();
1208
+ } catch (eT) { /* 打类失败也要继续:已知前缀那条 CSS 仍可能命中 */ }
1209
+ maSettingsPanelEl = panel;
1210
+ /* 前缀认不出 = 官方改名的**直接信号** —— 这时布局靠结构类仍然可用,所以只警告不降级。 */
1211
+ if (!MA_SETTINGS_PREFIX_RE.test(maClsOf(panel) + " " + maClsOf(nav))) {
1212
+ maSettingsWarn("设置面板已按**结构**识别(官方混淆前缀已变化:既不是 VOzbGW_ 也不是 Dws9Sa_);布局仍可用,但请把新前缀补进适配层。");
1213
+ }
1214
+ }
1215
+ /**
1216
+ * 每拍调用(节流):已识别且面板还在文档里 → 只刷新选中态;否则限频重扫。
1217
+ * 为什么要节流:未命中时每拍都要 querySelectorAll 一遍全文档,流式输出时每帧一拍。
1218
+ */
1219
+ function maSettingsMaybeTag() {
1220
+ if (!NARROW()) return;
1221
+ try {
1222
+ if (maSettingsPanelEl && maSettingsPanelEl.isConnected) { maMarkActiveCells(); return; }
1223
+ } catch (eLive) { /* 掉进重扫 */ }
1224
+ var now = Date.now();
1225
+ if (now < maSettingsNextScan) return;
1226
+ maSettingsNextScan = now + 400; // 未命中时最多 2.5 次/秒;命中后面板引用直接短路,不再全扫
1227
+ maTagSettingsDialog();
1228
+ }
1229
+
998
1230
  /* 设置面板打开时,把当前 tab 滚进横向可视区(顶部 tab 行手机横向滚动) */
999
1231
  let settingsWatchOn = false;
1000
1232
  let settingsTick = 0;
1001
1233
  function scrollActiveSettingsTab() {
1002
- const act = document.querySelector(
1003
- '[class~="VOzbGW_navCell"].VOzbGW_active, [class~="VOzbGW_navCell"][aria-current="true"]'
1004
- );
1234
+ /* 逐个尝试而不是写一条长选择器:结构类(前缀无关)优先,已知前缀退居兜底;
1235
+ 任何一条在旧浏览器上解析失败都不影响其余。 */
1236
+ const SEL = [
1237
+ ".dsh-ma-settings-cell.dsh-ma-settings-active",
1238
+ '[class~="Dws9Sa_navCell"].Dws9Sa_active',
1239
+ '[class~="VOzbGW_navCell"].VOzbGW_active',
1240
+ '[class~="Dws9Sa_navCell"][aria-current="true"]',
1241
+ '[class~="VOzbGW_navCell"][aria-current="true"]'
1242
+ ];
1243
+ let act = null;
1244
+ for (const sel of SEL) {
1245
+ try { act = document.querySelector(sel); } catch (e13a) { act = null; }
1246
+ if (act) break;
1247
+ }
1005
1248
  if (act && act.scrollIntoView) {
1006
1249
  try { act.scrollIntoView({ block: "nearest", inline: "nearest" }); } catch (e13) {
1007
1250
  try { act.scrollIntoView(); } catch (e14) {}
@@ -1014,7 +1257,12 @@ const SCRIPT = `(() => {
1014
1257
  try {
1015
1258
  const ob = new MutationObserver(function () {
1016
1259
  clearTimeout(settingsTick);
1017
- settingsTick = setTimeout(function () { if (NARROW()) scrollActiveSettingsTab(); }, 140);
1260
+ settingsTick = setTimeout(function () {
1261
+ if (!NARROW()) return;
1262
+ maSettingsNextScan = 0; // 官方刚挂上面板:别被节流挡在门外
1263
+ try { maSettingsMaybeTag(); } catch (e16) {}
1264
+ scrollActiveSettingsTab();
1265
+ }, 140);
1018
1266
  });
1019
1267
  ob.observe(document.body, { childList: true, subtree: true });
1020
1268
  } catch (e15) {}
@@ -1030,7 +1278,7 @@ const SCRIPT = `(() => {
1030
1278
  let fabDone = false;
1031
1279
 
1032
1280
  /* ===== 📶 网络延迟采样器(2026-10-10 二修,与"某一代菜单 DOM"彻底解耦)=================
1033
- 一修(0.7.0-beta.3)改掉了"亚毫秒样本被丢弃",但业主实测**仍然一直「测速中」**,
1281
+ 一修(0.7.0-beta.3)改掉了"亚毫秒样本被丢弃",但用户实测**仍然一直「测速中」**,
1034
1282
  而同一时段镜像页打了 69 次 /api/agentPresets/list 这类**完全合格**的请求 ——
1035
1283
  说明真根因不是样本被过滤,而是**采样根本没接上**(或渲染写进了已被替换的旧节点)。
1036
1284
 
@@ -1300,15 +1548,15 @@ const SCRIPT = `(() => {
1300
1548
  (dsh-bridge.mjs:763 → :770),两者都 splice 到 </head> 前 → adapter 的 fetch 包装在
1301
1549
  **内层** → 它量到的是"去掉加解密后的真实网络往返"(这正是用户想要的"网速")。
1302
1550
 
1303
- 统计口径:最近 RTT_KEEP(8) 次**成功**样本取中位数。
1551
+ 统计约定:最近 RTT_KEEP(8) 次**成功**样本取中位数。
1304
1552
  · 用中位数而不是均值/EWMA:偶发的一次 GC/系统调度卡顿会把均值永久带偏,
1305
1553
  中位数对单次离群点免疫,同时比"最小值"更贴近体感(最小值永远偏乐观);
1306
1554
  · 失败(reject)/超时(>20s)/亚毫秒(同一帧内的缓存命中)都不进样本:
1307
1555
  前者不是网络耗时,后者会算出 0ms 这种"谎言";
1308
1556
  · **无样本时显示「测速中…」而不是 0ms** —— 0ms 会让人以为网络飞快。
1309
- 分档(2026-09-27 业主口径调整):≤500ms 顺畅 / ≤1000ms 正常 / ≤2000ms 偏慢 / >2000ms 很慢。
1557
+ 分档(2026-09-27 用户约定调整):≤500ms 顺畅 / ≤1000ms 正常 / ≤2000ms 偏慢 / >2000ms 很慢。
1310
1558
  为什么放宽:这是个**文本窗口**的远程操作界面(看输出、敲字、点按钮),
1311
- 对往返延迟本来就宽容 —— 业主原话「500ms 以内的延迟都不叫事,1000ms 使用起来也是正常流畅的」。
1559
+ 对往返延迟本来就宽容 —— 用户原话「500ms 以内的延迟都不叫事,1000ms 使用起来也是正常流畅的」。
1312
1560
  旧的 120/300 分档是照"网页首屏"的体感定的,会把大量正常可用的移动网络误报成"较慢",
1313
1561
  反而让用户以为哪里坏了。 */
1314
1562
  const RTT_KEEP = 8, RTT_MAX_MS = 20000;
@@ -1323,7 +1571,7 @@ const SCRIPT = `(() => {
1323
1571
  const mid = s.length >> 1;
1324
1572
  return s.length % 2 ? s[mid] : Math.round((s[mid - 1] + s[mid]) / 2);
1325
1573
  };
1326
- /* 分档(2026-09-27 按业主口径放宽):文本窗口的远程操作,500ms 内都算顺畅、1000ms 内正常流畅。
1574
+ /* 分档(2026-09-27 按用户约定放宽):文本窗口的远程操作,500ms 内都算顺畅、1000ms 内正常流畅。
1327
1575
  ok=顺畅 / mid=正常 / slow=偏慢 / bad=很慢 —— 颜色只用于区分,**不把"正常"渲染成告警色**。 */
1328
1576
  const rttGrade = (ms) => (ms <= 500 ? "ok" : ms <= 1000 ? "mid" : ms <= 2000 ? "slow" : "bad");
1329
1577
  const rttLabel = (g) => (g === "ok" ? "顺畅" : g === "mid" ? "正常" : g === "slow" ? "偏慢" : "很慢");
@@ -1357,7 +1605,7 @@ const SCRIPT = `(() => {
1357
1605
  if (ico) ico.textContent = shown === null ? "📶" : (g === "bad" ? "📵" : "📶");
1358
1606
  if (txt) {
1359
1607
  /* 空态**说清原因**(2026-10-10):以前只有一句「等一次网络请求后显示」,
1360
- 于是"采样器压根没接上"和"接上了但还没有请求"长得一模一样,业主两次反馈
1608
+ 于是"采样器压根没接上"和"接上了但还没有请求"长得一模一样,用户两次反馈
1361
1609
  我们都只能猜。现在一眼可分。 */
1362
1610
  txt.textContent = shown !== null ? (shown + " ms · " + rttLabel(g))
1363
1611
  : (maNetPatch.ok ? "测速中…(已接上,等一次请求)" : "测速中…(采样器未接上)");
@@ -1585,7 +1833,7 @@ const SCRIPT = `(() => {
1585
1833
  frame.classList.add("dsh-ma-frame");
1586
1834
 
1587
1835
  /* 给 frame 子列打稳定标记(dsh-ma-*),规避官方类名前缀随构建变化。
1588
- ⚠️ 必须先把**不是列**的节点剔掉(2026-09-19 事故根因):
1836
+ ⚠️ 必须先把**不是列**的节点剔掉(2026-09-19 故障根因):
1589
1837
  · overlayLayer(data-shell-overlay / *overlayLayer*)= 官方 shell.overlay 宿主,inset:0 全屏,
1590
1838
  在 0.1.5-rc.2 里**始终**是 frame 的最后一个子元素;
1591
1839
  · 拖拽把手(*handle* / [data-side])= 8px 宽绝对定位条(折叠时官方根本不渲染)。
@@ -1622,7 +1870,7 @@ const SCRIPT = `(() => {
1622
1870
  ① 已经判定为第三列(rightbar/details 类名)的列,**绝不是**抽屉列;
1623
1871
  ② 类名含 sidebar 且不含 rightbar/details(0.1.2/0.1.5 的 pI_x6G_sidebarCol 都满足);
1624
1872
  ③ 退到结构信号:列里有官方侧栏组件根 .hHd-Xa_root,或有**整串**匹配的官方侧栏开合按钮
1625
- (精确匹配,绝不再用 *="sidebar" 这种包含匹配 —— 那正是事故来源);
1873
+ (精确匹配,绝不再用 *="sidebar" 这种包含匹配 —— 那正是故障来源);
1626
1874
  ④ 同时给 CSS 加了 :not(.dsh-ma-details) 兜底:万一将来又标重,详情列的样式也必须赢。 */
1627
1875
  const SIDEBAR_RE = /sidebar/i;
1628
1876
  const NOT_SIDEBAR_RE = /rightbar|right-bar|details/i;
@@ -1742,7 +1990,7 @@ const SCRIPT = `(() => {
1742
1990
  只要官方属性语义与我们理解的不一致,光看属性就可能误判;而遮罩一旦拦截整屏,
1743
1991
  用户看到的就是"整屏阴影 + 点哪都没反应"。以实测几何为准(isDrawer),误判也不会挡住用户。 */
1744
1992
  /* 抽屉必须"像抽屉":不是全屏层(overlay 层/主内容区都被排除在外)、不是拖拽把手、
1745
- **与视口真正相交**。任一不满足 → 它就不该让遮罩拦截点击(0.1.5 事故的结构性防线)。
1993
+ **与视口真正相交**。任一不满足 → 它就不该让遮罩拦截点击(0.1.5 故障的结构性防线)。
1746
1994
  ⚠️ 2026-09-19 手机实测第二波:旧实现只查 r.right > 8,这对"向右滑出屏外"的第三列是
1747
1995
  **恒真**的 —— 抽屉滑出时右边坐标是很大的正数(实测 left=400,right=759,width=358),
1748
1996
  于是"完全看不见、一个像素都没露"的第三列被判成"展开的详情列":
@@ -1809,7 +2057,7 @@ const SCRIPT = `(() => {
1809
2057
  let officialExpandedSeen = false;
1810
2058
  let lastOfficialCollapsed = null;
1811
2059
  let scrimTimer = 0, scrimWatchdog = 0;
1812
- /* ❌ 这里曾有「详情列(第三列)在手机端的显示条件」整段判据(白框事故后打的四层闸门)。
2060
+ /* ❌ 这里曾有「详情列(第三列)在手机端的显示条件」整段判据(白框故障后打的四层闸门)。
1813
2061
  2026-09-27 根治时删除:第三列的显隐**完全交还官方** —— 宽屏按 grid 列、窄屏全屏浮层。
1814
2062
  我们不再有"要不要显示它"的决定权,也就不需要任何判据。 */
1815
2063
 
@@ -2056,6 +2304,9 @@ const SCRIPT = `(() => {
2056
2304
  if (!whaleGuardsOn || !settingsWatchOn) {
2057
2305
  try { whaleHookInit(); watchSettings(); } catch (eHook) { /* 两者内部都自带一次性守卫 */ }
2058
2306
  }
2307
+ /* 设置面板结构识别(自带节流,幂等):官方每次重新打包都换混淆前缀,
2308
+ 这一步保证"改名后仍然基本可用",而不是整段静默失效(2026-10-13 根因)。 */
2309
+ try { maSettingsMaybeTag(); } catch (eSetTag) { /* 忽略 */ }
2059
2310
  };
2060
2311
  /* 节流:一次 DOM 变更往往产生几十条记录,逐条 boot() 就是忙轮询。
2061
2312
  统一合并成"本帧最多跑一次"(有 rAF 用 rAF,没有就退化成 0ms 宏任务) —— 天然去重。 */
@@ -2067,7 +2318,7 @@ const SCRIPT = `(() => {
2067
2318
  setTimeout(run, 0);
2068
2319
  };
2069
2320
  /* ===== 手机图片上传兼容(0.6.15)=====
2070
- 业主反馈:「手机端无法识别图片」——现象是选了照片发出去,模型看不到图 / 当成普通文件。
2321
+ 用户反馈:「手机端无法识别图片」——现象是选了照片发出去,模型看不到图 / 当成普通文件。
2071
2322
 
2072
2323
  根因(官方 dsh web 客户端逻辑,dsh-client-ui-conversation 的 createDrafts):
2073
2324
  · 只有 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 升级导致的兼容性故障」这个问题