@mrrisega/dsh-remote 0.6.7-beta.3 → 0.6.7
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.
- package/clients/dsh-remote/dsh-bridge.mjs +16 -0
- package/clients/dsh-remote/e2ee-shim-script.js +18 -1
- package/clients/dsh-remote/mobile-adapter.mjs +487 -170
- package/clients/dsh-remote/test/mobile-adapter-runtime.test.mjs +91 -34
- package/dsh-setup.mjs +59 -6
- package/package.json +6 -5
- package/packages/dsh-remote-web/lib/client.js +22 -4
- package/packages/dsh-remote-web/lib/index.js +161 -40
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/test/account-switch-device.test.mjs +307 -0
- package/packages/dsh-remote-web/test/uninstall-safety.test.mjs +68 -1
- package/packages/dsh-remote-web/test/windows-compat.test.mjs +18 -0
- package/scripts/test-net-guard.cjs +49 -0
|
@@ -47,6 +47,16 @@
|
|
|
47
47
|
* - JS 用结构探测(frame → 子列 → 打 dsh-ma-* 稳定类)承载布局,不以 hashed 类为唯一钩子;
|
|
48
48
|
* - 命中不到 frame 时静默跳过,页面保持官方默认,绝不报错/破坏。
|
|
49
49
|
*
|
|
50
|
+
* ⚠️ 官方 0.1.5-rc.2 起第三列改名(2026-09-19 线上事故,Windows 用户先撞到):
|
|
51
|
+
* 第三列 `pI_x6G_detailsCol` → `pI_x6G_rightbarCol`、`data-details-collapsed` →
|
|
52
|
+
* `data-rightbar-collapsed`,且折叠时不再渲染拖拽把手、`overlayLayer`(data-shell-overlay,
|
|
53
|
+
* inset:0 全屏)始终留在 frame 末尾。旧实现"详情抽屉兜底 = 取 frame 最后一个子元素"于是把
|
|
54
|
+
* **官方 overlay 层**当成详情抽屉:属性不存在 → 判成"未折叠",overlay 全屏 → 几何可见
|
|
55
|
+
* → `dsh-ma-details-open` 常亮 → CSS 让全屏 scrim 拦截整屏点击 → 手机端一打开就是遮罩死局。
|
|
56
|
+
* 现在三处一起兜底:① 第三列按 /details|rightbar/ 识别,且**排除** overlay 层与拖拽把手;
|
|
57
|
+
* ② 折叠态同时认两个属性名,认不到就只信几何;③ 抽屉必须是"不是全屏的、真的在视口里的"元素,
|
|
58
|
+
* 并加常驻看门狗:遮罩拦截时若没有任何抽屉在视口内,立刻摘掉(任何未来改名都不会再变成死局)。
|
|
59
|
+
*
|
|
50
60
|
* 开关:环境变量 DSH_MOBILE_ADAPTER=0 整体关闭(默认开启)。
|
|
51
61
|
*/
|
|
52
62
|
|
|
@@ -56,9 +66,10 @@ const ADAPTER_ID = "dsh-mobile-adapter";
|
|
|
56
66
|
const HOST_FEATURE_RE =
|
|
57
67
|
/(__ModuleLoader__|@deepseek-ai\/dsh-client-modules|@deepseek-ai\/dsh-client-connection|<title>\s*DeepSeek Harness)/i;
|
|
58
68
|
|
|
59
|
-
/* 运行时 DOM 特征(注入脚本内再次校验;data-* 由官方组件固定输出,不随 CSS modules 改名)。
|
|
69
|
+
/* 运行时 DOM 特征(注入脚本内再次校验;data-* 由官方组件固定输出,不随 CSS modules 改名)。
|
|
70
|
+
data-rightbar-collapsed = 官方 0.1.5-rc.2 起第三列(原 details)的折叠属性。 */
|
|
60
71
|
const RUNTIME_HOST_RE =
|
|
61
|
-
/(data-sidebar-collapsed|data-details-collapsed|data-shell-overlay)|(pI_x6G_frame|hHd-Xa_root|hHd-Xa_toggle)/;
|
|
72
|
+
/(data-sidebar-collapsed|data-details-collapsed|data-rightbar-collapsed|data-shell-overlay)|(pI_x6G_frame|hHd-Xa_root|hHd-Xa_toggle)/;
|
|
62
73
|
|
|
63
74
|
const STYLE = `
|
|
64
75
|
/* ===== dsh-remote mobile adapter (bridge 注入;仅 ≤820px 生效,桌面宽屏无任何规则) ===== */
|
|
@@ -71,7 +82,7 @@ const STYLE = `
|
|
|
71
82
|
/* 侧栏/详情已移出 grid 流(fixed),唯一在流的中心列须显式落到 1fr 轨道,否则自动放置会被塞进 0px 轨 */
|
|
72
83
|
div.pI_x6G_centerCol, div.dsh-ma-center { grid-column: 2; min-width: 0; width: auto; }
|
|
73
84
|
|
|
74
|
-
/* --- 侧栏 /
|
|
85
|
+
/* --- 侧栏 / 详情(官方 0.1.5-rc.2 起叫 rightbar):离屏浮层(off-canvas),展开态滑入 ---
|
|
75
86
|
注意:不能给浮层设 will-change:transform / 持久 transform —— 官方把设置面板等
|
|
76
87
|
fixed 弹层的 DOM 挂在侧栏子树里,transform 祖先会成为 fixed 的 containing block,
|
|
77
88
|
导致弹层被限制在 84vw 侧栏宽内(实测 328px“抽屉感”,fix2 根因之一)。 */
|
|
@@ -87,7 +98,7 @@ const STYLE = `
|
|
|
87
98
|
html.dsh-ma-sidebar-open div.pI_x6G_sidebarCol,
|
|
88
99
|
html.dsh-ma-sidebar-open div.dsh-ma-sidebar { transform: none; }
|
|
89
100
|
|
|
90
|
-
div.pI_x6G_detailsCol, div.dsh-ma-details {
|
|
101
|
+
div.pI_x6G_detailsCol, div.pI_x6G_rightbarCol, div.dsh-ma-details, div.dsh-ma-rightbar {
|
|
91
102
|
position: fixed; right: 0; top: 0; bottom: 0; margin: 0;
|
|
92
103
|
width: min(92vw, 400px); max-width: 96vw;
|
|
93
104
|
z-index: 300; overflow: hidden;
|
|
@@ -97,7 +108,9 @@ const STYLE = `
|
|
|
97
108
|
transition: transform .22s var(--ds-ease-in-out, ease);
|
|
98
109
|
}
|
|
99
110
|
html.dsh-ma-details-open div.pI_x6G_detailsCol,
|
|
100
|
-
html.dsh-ma-details-open div.
|
|
111
|
+
html.dsh-ma-details-open div.pI_x6G_rightbarCol,
|
|
112
|
+
html.dsh-ma-details-open div.dsh-ma-details,
|
|
113
|
+
html.dsh-ma-details-open div.dsh-ma-rightbar { transform: none; }
|
|
101
114
|
|
|
102
115
|
/* --- 展开遮罩(在浮层下、在官方弹层下) --- */
|
|
103
116
|
div.dsh-ma-scrim {
|
|
@@ -106,10 +119,12 @@ const STYLE = `
|
|
|
106
119
|
opacity: 0; pointer-events: none;
|
|
107
120
|
transition: opacity .22s ease;
|
|
108
121
|
}
|
|
109
|
-
/*
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
/* ⚠️ 拦截点击**只认 dsh-ma-scrim-on 这一个闸**(2026-09-19 复盘):
|
|
123
|
+
旧版还并列了一个「html.dsh-ma-details-open div.dsh-ma-scrim」规则,于是"详情展开判定出错"
|
|
124
|
+
会直接变成"整屏被遮罩拦截"——官方 0.1.5 把第三列改名 rightbar 后就是这个死局。
|
|
125
|
+
现在判定收口在 JS 里(必须有一个**真的在视口内、且不是全屏的**抽屉),
|
|
126
|
+
CSS 不再提供第二条能让遮罩生效的路径。 */
|
|
127
|
+
html.dsh-ma-scrim-on div.dsh-ma-scrim { opacity: 1; pointer-events: auto; }
|
|
113
128
|
|
|
114
129
|
/* --- 顶部小鲸鱼/菜单按钮(侧栏收起时可见) --- */
|
|
115
130
|
button.dsh-ma-hamburger {
|
|
@@ -220,77 +235,155 @@ const STYLE = `
|
|
|
220
235
|
}
|
|
221
236
|
}
|
|
222
237
|
|
|
223
|
-
/* =====
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
238
|
+
/* ===== 字号档位 + 远程控制悬浮按钮(手机端与电脑端通用) =====
|
|
239
|
+
需求(2026-09-19 用户要求,替代原先"右下角字号按钮 + 右上角加密小锁"两处分散入口):
|
|
240
|
+
合并成**一个**悬浮按钮 → 弹出菜单里同时提供 ①字号档位 ②加密状态 ③返回设备列表。
|
|
241
|
+
"返回设备列表"是用户在镜像页里**唯一的主动退路**,所以它必须在任何宽度都可用。
|
|
242
|
+
|
|
243
|
+
字号实现(官方组件大量写死的 px 字号,只改 :root font-size 对它们无效,"尽力而为"叠加):
|
|
228
244
|
① html { font-size: 计算后的 px } —— 对用 rem 的地方生效;
|
|
229
|
-
② html { -webkit-text-size-adjust: 百分比 } —— 移动端 Safari/Chrome
|
|
230
|
-
(
|
|
245
|
+
② html { -webkit-text-size-adjust: 百分比 } —— 移动端 Safari/Chrome 对文本整体缩放
|
|
246
|
+
(只影响文字、不影响布局,比 zoom/transform 安全得多);
|
|
231
247
|
③ 正文气泡走官方自己的变量 --dsh-content-font-size / --dsh-content-font-delta
|
|
232
|
-
(官方聊天正文就是这么取值的)
|
|
233
|
-
适配层自己注入的 UI(.dsh-ma-*)写成固定 px
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
/* 字号设置面板:右下角浮动按钮 + 展开的小卡片(自适应、可点外部关闭) */
|
|
243
|
-
button.dsh-ma-fsbtn {
|
|
244
|
-
position: fixed;
|
|
245
|
-
right: max(10px, env(safe-area-inset-right));
|
|
246
|
-
bottom: calc(84px + env(safe-area-inset-bottom));
|
|
247
|
-
z-index: 310;
|
|
248
|
-
width: 44px; height: 44px; padding: 0;
|
|
249
|
-
display: flex; align-items: center; justify-content: center;
|
|
250
|
-
border-radius: 50%;
|
|
251
|
-
font: 600 15px/1 -apple-system, system-ui, sans-serif;
|
|
252
|
-
background: var(--dsw-alias-button-elevated-fill, rgba(255,255,255,.96));
|
|
253
|
-
border: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.3));
|
|
254
|
-
box-shadow: 0 2px 10px rgba(0,0,0,.18);
|
|
255
|
-
color: var(--dsw-alias-label-primary, #111);
|
|
256
|
-
cursor: pointer; touch-action: manipulation;
|
|
257
|
-
-webkit-tap-highlight-color: transparent;
|
|
258
|
-
}
|
|
259
|
-
button.dsh-ma-fsbtn:active { transform: scale(.94); }
|
|
260
|
-
div.dsh-ma-fspanel {
|
|
261
|
-
position: fixed;
|
|
262
|
-
right: max(10px, env(safe-area-inset-right));
|
|
263
|
-
bottom: calc(136px + env(safe-area-inset-bottom));
|
|
264
|
-
z-index: 311;
|
|
265
|
-
width: min(78vw, 300px);
|
|
266
|
-
box-sizing: border-box;
|
|
267
|
-
padding: 12px 14px 14px;
|
|
268
|
-
border-radius: 14px;
|
|
269
|
-
background: var(--dsw-alias-bg-elevated, #fff);
|
|
270
|
-
border: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.28));
|
|
271
|
-
box-shadow: 0 8px 28px rgba(0,0,0,.24);
|
|
272
|
-
color: var(--dsw-alias-label-primary, #111);
|
|
273
|
-
font: 400 14px/1.45 -apple-system, system-ui, sans-serif;
|
|
274
|
-
}
|
|
275
|
-
div.dsh-ma-fspanel[hidden] { display: none; }
|
|
276
|
-
div.dsh-ma-fspanel .dsh-ma-fs-title { font-weight: 600; font-size: 14px; margin-bottom: 2px; }
|
|
277
|
-
div.dsh-ma-fspanel .dsh-ma-fs-sub { color: var(--dsw-alias-label-tertiary, #6b7280); font-size: 12px; }
|
|
278
|
-
div.dsh-ma-fspanel .dsh-ma-fs-val { font-variant-numeric: tabular-nums; font-weight: 600; }
|
|
279
|
-
div.dsh-ma-fspanel input[type="range"] {
|
|
280
|
-
width: 100%; margin: 10px 0 2px; height: 28px;
|
|
281
|
-
accent-color: var(--dsw-alias-brand-primary, #4d6bfe);
|
|
282
|
-
}
|
|
283
|
-
div.dsh-ma-fspanel .dsh-ma-fs-ticks {
|
|
284
|
-
display: flex; justify-content: space-between;
|
|
285
|
-
color: var(--dsw-alias-label-tertiary, #6b7280); font-size: 11px;
|
|
286
|
-
}
|
|
287
|
-
div.dsh-ma-fspanel .dsh-ma-fs-preview {
|
|
288
|
-
margin-top: 10px; padding-top: 9px;
|
|
289
|
-
border-top: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.22));
|
|
290
|
-
font-size: 14px;
|
|
291
|
-
}
|
|
248
|
+
(官方聊天正文就是这么取值的),换算后同步设置,保证正文一定变大。
|
|
249
|
+
适配层自己注入的 UI(.dsh-ma-*)写成固定 px,不参与缩放。 */
|
|
250
|
+
html { --dsh-ma-fs-scale: 1; font-size: calc(16px * var(--dsh-ma-fs-scale)); }
|
|
251
|
+
html[data-dsh-ma-fs="1"] { --dsh-ma-fs-scale: 1.08; -webkit-text-size-adjust: 108%; }
|
|
252
|
+
html[data-dsh-ma-fs="2"] { --dsh-ma-fs-scale: 1.18; -webkit-text-size-adjust: 118%; }
|
|
253
|
+
html[data-dsh-ma-fs="3"] { --dsh-ma-fs-scale: 1.32; -webkit-text-size-adjust: 132%; }
|
|
254
|
+
/* 注意:不要再给 body 设 font-size —— html 的字号已经缩放过了,
|
|
255
|
+
1rem 此时等于"缩放后的字号",乘上 scale 会把放大平方(1.32 → 1.74)并连累所有子元素。
|
|
256
|
+
子元素走 -webkit-text-size-adjust(文本整体缩放) + inherit 即可。 */
|
|
292
257
|
|
|
293
|
-
/*
|
|
258
|
+
/* 悬浮按钮:z-index 必须高于遮罩(290)与抽屉(300) —— 遮罩万一因官方改版常亮,
|
|
259
|
+
用户至少还能从这里"返回设备列表"自救(这是 2026-09-19 死局的兜底出口)。 */
|
|
260
|
+
button.dsh-ma-fab {
|
|
261
|
+
position: fixed;
|
|
262
|
+
right: max(10px, env(safe-area-inset-right));
|
|
263
|
+
bottom: calc(16px + env(safe-area-inset-bottom));
|
|
264
|
+
z-index: 400;
|
|
265
|
+
width: 46px; height: 46px; padding: 0;
|
|
266
|
+
display: flex; align-items: center; justify-content: center;
|
|
267
|
+
border-radius: 50%;
|
|
268
|
+
font: 600 15px/1 -apple-system, system-ui, sans-serif;
|
|
269
|
+
background: var(--dsw-alias-button-elevated-fill, rgba(255,255,255,.96));
|
|
270
|
+
border: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.3));
|
|
271
|
+
box-shadow: 0 2px 12px rgba(0,0,0,.2);
|
|
272
|
+
color: var(--dsw-alias-label-primary, #111);
|
|
273
|
+
cursor: pointer; touch-action: manipulation;
|
|
274
|
+
-webkit-tap-highlight-color: transparent;
|
|
275
|
+
}
|
|
276
|
+
button.dsh-ma-fab:active { transform: scale(.94); }
|
|
277
|
+
button.dsh-ma-fab .dsh-ma-fab-lock {
|
|
278
|
+
position: absolute; top: -3px; right: -3px;
|
|
279
|
+
font-size: 12px; line-height: 1; padding: 2px 3px; border-radius: 999px;
|
|
280
|
+
background: var(--dsw-alias-bg-elevated, #fff);
|
|
281
|
+
box-shadow: 0 0 0 .5px var(--dsw-alias-border-l3, rgba(127,127,127,.3));
|
|
282
|
+
}
|
|
283
|
+
/* 菜单卡片:fixed 贴右下,最大高度受限可滚动 */
|
|
284
|
+
div.dsh-ma-menu {
|
|
285
|
+
position: fixed;
|
|
286
|
+
right: max(10px, env(safe-area-inset-right));
|
|
287
|
+
bottom: calc(70px + env(safe-area-inset-bottom));
|
|
288
|
+
z-index: 401;
|
|
289
|
+
width: min(86vw, 320px);
|
|
290
|
+
max-height: min(70vh, 520px);
|
|
291
|
+
overflow-y: auto;
|
|
292
|
+
box-sizing: border-box;
|
|
293
|
+
padding: 12px 14px 14px;
|
|
294
|
+
border-radius: 14px;
|
|
295
|
+
background: var(--dsw-alias-bg-elevated, #fff);
|
|
296
|
+
border: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.28));
|
|
297
|
+
box-shadow: 0 10px 32px rgba(0,0,0,.26);
|
|
298
|
+
color: var(--dsw-alias-label-primary, #111);
|
|
299
|
+
font: 400 14px/1.45 -apple-system, system-ui, sans-serif;
|
|
300
|
+
-webkit-overflow-scrolling: touch;
|
|
301
|
+
}
|
|
302
|
+
div.dsh-ma-menu[hidden] { display: none; }
|
|
303
|
+
div.dsh-ma-menu .dsh-ma-menu-head {
|
|
304
|
+
display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
|
|
305
|
+
margin-bottom: 10px;
|
|
306
|
+
}
|
|
307
|
+
div.dsh-ma-menu .dsh-ma-menu-head b { font-size: 14px; font-weight: 700; }
|
|
308
|
+
div.dsh-ma-menu .dsh-ma-menu-dev {
|
|
309
|
+
font-size: 11.5px; color: var(--dsw-alias-label-tertiary, #6b7280);
|
|
310
|
+
max-width: 52%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
311
|
+
}
|
|
312
|
+
div.dsh-ma-menu .dsh-ma-menu-sec {
|
|
313
|
+
font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, #57606a);
|
|
314
|
+
margin: 10px 0 4px;
|
|
315
|
+
}
|
|
316
|
+
div.dsh-ma-menu .dsh-ma-menu-sub { font-size: 12px; color: var(--dsw-alias-label-tertiary, #6b7280); }
|
|
317
|
+
div.dsh-ma-menu .dsh-ma-menu-sub b { font-weight: 600; color: var(--dsw-alias-label-primary, #111); }
|
|
318
|
+
div.dsh-ma-menu input[type="range"] {
|
|
319
|
+
width: 100%; margin: 8px 0 2px; height: 28px;
|
|
320
|
+
accent-color: var(--dsw-alias-brand-primary, #4d6bfe);
|
|
321
|
+
}
|
|
322
|
+
div.dsh-ma-menu .dsh-ma-menu-ticks {
|
|
323
|
+
display: flex; justify-content: space-between;
|
|
324
|
+
color: var(--dsw-alias-label-tertiary, #6b7280); font-size: 11px;
|
|
325
|
+
}
|
|
326
|
+
div.dsh-ma-menu .dsh-ma-menu-preview {
|
|
327
|
+
margin-top: 8px; padding-top: 8px; font-size: 14px;
|
|
328
|
+
border-top: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.22));
|
|
329
|
+
}
|
|
330
|
+
div.dsh-ma-menu .dsh-ma-menu-e2ee {
|
|
331
|
+
display: flex; align-items: flex-start; gap: 8px;
|
|
332
|
+
font-size: 12.5px; line-height: 1.6;
|
|
333
|
+
background: var(--dsw-alias-bg-base, rgba(127,127,127,.08));
|
|
334
|
+
border-radius: 10px; padding: 9px 11px;
|
|
335
|
+
}
|
|
336
|
+
div.dsh-ma-menu .dsh-ma-menu-e2ee .ico { flex: none; }
|
|
337
|
+
div.dsh-ma-menu .dsh-ma-menu-e2ee .txt { min-width: 0; overflow-wrap: anywhere; }
|
|
338
|
+
/* ⚠️ 必须显式写 [hidden]:UA 的 [hidden]{display:none} 会被上面的 display:flex 盖过(实测过)。 */
|
|
339
|
+
div.dsh-ma-menu .dsh-ma-e2ee-sec[hidden],
|
|
340
|
+
div.dsh-ma-menu .dsh-ma-menu-e2ee[hidden] { display: none !important; }
|
|
341
|
+
button.dsh-ma-fab .dsh-ma-fab-lock[hidden] { display: none !important; }
|
|
342
|
+
div.dsh-ma-menu .dsh-ma-menu-e2ee.is-warn .txt { color: var(--dsw-alias-label-warning, #9a6700); }
|
|
343
|
+
div.dsh-ma-menu .dsh-ma-menu-e2ee.is-err .txt { color: var(--dsw-alias-label-danger, #cf222e); }
|
|
344
|
+
div.dsh-ma-menu .dsh-ma-menu-e2ee a { color: var(--dsw-alias-brand-primary, #4d6bfe); }
|
|
345
|
+
button.dsh-ma-menu-back {
|
|
346
|
+
display: block; width: 100%; box-sizing: border-box;
|
|
347
|
+
margin-top: 12px; padding: 11px 12px;
|
|
348
|
+
border-radius: 10px; border: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.3));
|
|
349
|
+
background: var(--dsw-alias-button-elevated-fill, rgba(127,127,127,.08));
|
|
350
|
+
color: var(--dsw-alias-label-primary, #111);
|
|
351
|
+
font: 600 14px/1 -apple-system, system-ui, sans-serif;
|
|
352
|
+
cursor: pointer; touch-action: manipulation;
|
|
353
|
+
-webkit-tap-highlight-color: transparent;
|
|
354
|
+
}
|
|
355
|
+
button.dsh-ma-menu-back:active { transform: scale(.98); }
|
|
356
|
+
div.dsh-ma-menu .dsh-ma-menu-note {
|
|
357
|
+
margin-top: 8px; font-size: 11.5px; line-height: 1.7;
|
|
358
|
+
color: var(--dsw-alias-label-tertiary, #6b7280);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* 会话过期引导条:镜像页在会话过期后只会"连不上",这里给一句能照做的话 + 一个出口。
|
|
362
|
+
非阻塞(顶部细条),点关闭即消失;不拦截任何请求。 */
|
|
363
|
+
div.dsh-ma-sessionbar {
|
|
364
|
+
position: fixed; left: 0; right: 0; top: 0;
|
|
365
|
+
z-index: 402;
|
|
366
|
+
display: flex; align-items: center; gap: 10px;
|
|
367
|
+
padding: max(8px, env(safe-area-inset-top)) 12px 8px;
|
|
368
|
+
background: #fff8e6; color: #7a4b00;
|
|
369
|
+
border-bottom: .5px solid rgba(154,103,0,.35);
|
|
370
|
+
font: 500 13px/1.5 -apple-system, system-ui, sans-serif;
|
|
371
|
+
box-shadow: 0 2px 10px rgba(0,0,0,.12);
|
|
372
|
+
}
|
|
373
|
+
div.dsh-ma-sessionbar[hidden] { display: none; }
|
|
374
|
+
div.dsh-ma-sessionbar .msg { flex: 1 1 auto; min-width: 0; }
|
|
375
|
+
div.dsh-ma-sessionbar button {
|
|
376
|
+
flex: none; padding: 6px 12px; border-radius: 8px; cursor: pointer;
|
|
377
|
+
border: .5px solid rgba(154,103,0,.5); background: #fff; color: #7a4b00;
|
|
378
|
+
font: 600 12.5px/1 -apple-system, system-ui, sans-serif;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* 官方 E2EE 徽标(#dsh-e2ee-badge,shim 注入的右上角小药丸)已并入本菜单:
|
|
382
|
+
隐藏独立徽标,状态显示在悬浮按钮的锁图标与菜单的「加密状态」行里。 */
|
|
383
|
+
html[data-dsh-ma-merged-e2ee] #dsh-e2ee-badge { display: none !important; }
|
|
384
|
+
|
|
385
|
+
/* 桌面宽屏(>820px):只有上面这些 .dsh-ma-* 元素可见(悬浮按钮/菜单/引导条),
|
|
386
|
+
官方界面本身不受任何影响。 */
|
|
294
387
|
`;
|
|
295
388
|
|
|
296
389
|
const SCRIPT = `(() => {
|
|
@@ -335,7 +428,7 @@ const SCRIPT = `(() => {
|
|
|
335
428
|
const NARROW = () => window.innerWidth <= 820;
|
|
336
429
|
const HOSTISH = () =>
|
|
337
430
|
HOST_RE.test(document.documentElement.outerHTML.slice(0, 200000)) ||
|
|
338
|
-
!!document.querySelector("[data-shell-overlay], [data-sidebar-collapsed], [data-details-collapsed]");
|
|
431
|
+
!!document.querySelector("[data-shell-overlay], [data-sidebar-collapsed], [data-details-collapsed], [data-rightbar-collapsed]");
|
|
339
432
|
|
|
340
433
|
/* —— 鲸鱼余额挂件(dsh-whale-widget,第三方插件的浮动组件)窄屏协同 ——
|
|
341
434
|
若挂件记忆的锚点是「左+上」(实测会盖住左上角会话标题/左上菜单按钮),在页面解析期
|
|
@@ -523,22 +616,261 @@ const SCRIPT = `(() => {
|
|
|
523
616
|
} catch (e15) {}
|
|
524
617
|
}
|
|
525
618
|
|
|
619
|
+
/* 悬浮按钮/菜单:任何宽度都要有(电脑端访问镜像页同样需要「返回设备列表」的出口),
|
|
620
|
+
与手机端布局改造分开守门;失败也不影响布局适配。 */
|
|
621
|
+
let fabDone = false;
|
|
622
|
+
const buildRemoteMenu = () => {
|
|
623
|
+
/* ===== 统一「远程控制」悬浮按钮:字号 + 加密状态 + 返回设备列表 =====
|
|
624
|
+
2026-09-19 用户要求原话:「把字体的菜单和加密小锁的菜单合并成一个小悬浮按钮;点击后可以弹菜单
|
|
625
|
+
调节字体,也可以返回设备拉取列表,让用户主动返回回去,这一点很重要」「手机端要有,电脑端也可以有」。
|
|
626
|
+
要点:
|
|
627
|
+
· 与「手机端布局改造」分开守门:本段只要求 HOSTISH(确实是官方 dsh web 页),不看 NARROW;
|
|
628
|
+
· z-index 400/401 高于遮罩(290)与抽屉(300):万一日后官方再改版把遮罩判错,
|
|
629
|
+
用户仍然点得到这颗按钮 → 能主动返回设备列表(唯一自救出口);
|
|
630
|
+
· 加密状态来自 E2EE shim 的 #dsh-e2ee-badge:本层只**读**状态并隐藏那颗独立药丸,
|
|
631
|
+
不碰加解密逻辑(耦合面最小:window.__dshE2eeBadge + dsh-e2ee-badge 事件)。 */
|
|
632
|
+
const FS_KEY = "dsh-ma-font-scale";
|
|
633
|
+
const FS_LEVELS = [
|
|
634
|
+
{ name: "最小", scale: 1.00, content: 14 },
|
|
635
|
+
{ name: "中", scale: 1.08, content: 15 },
|
|
636
|
+
{ name: "大", scale: 1.18, content: 16.5 },
|
|
637
|
+
{ name: "超大", scale: 1.32, content: 18.5 }
|
|
638
|
+
];
|
|
639
|
+
const readFsLevel = () => {
|
|
640
|
+
try {
|
|
641
|
+
const raw = window.localStorage && window.localStorage.getItem(FS_KEY);
|
|
642
|
+
const i = Number(raw);
|
|
643
|
+
return Number.isInteger(i) && i >= 0 && i < FS_LEVELS.length ? i : 0;
|
|
644
|
+
} catch (e) { return 0; }
|
|
645
|
+
};
|
|
646
|
+
const applyFsLevel = (level) => {
|
|
647
|
+
const L = FS_LEVELS[level] || FS_LEVELS[0];
|
|
648
|
+
const root = document.documentElement;
|
|
649
|
+
if (level === 0) root.removeAttribute("data-dsh-ma-fs");
|
|
650
|
+
else root.setAttribute("data-dsh-ma-fs", String(level));
|
|
651
|
+
// 官方聊天正文:直接喂它自己的变量(它就是这样取值的)
|
|
652
|
+
try {
|
|
653
|
+
root.style.setProperty("--dsh-content-font-size", L.content + "px");
|
|
654
|
+
root.style.setProperty("--dsh-content-font-delta", (L.content - 14) + "px");
|
|
655
|
+
} catch (e2) { /* 忽略 */ }
|
|
656
|
+
return L;
|
|
657
|
+
};
|
|
658
|
+
/* 当前设备 id:镜像页路径形如 /remote/<deviceId>/…(旧入口是根路径 → 留空不显示) */
|
|
659
|
+
const deviceIdOfPath = () => {
|
|
660
|
+
try {
|
|
661
|
+
const parts = String(window.location.pathname || "").split("/").filter(Boolean);
|
|
662
|
+
if (parts[0] !== "remote" || !parts[1]) return "";
|
|
663
|
+
return decodeURIComponent(parts[1]);
|
|
664
|
+
} catch (e) { return ""; }
|
|
665
|
+
};
|
|
666
|
+
const gotoApp = (to) => {
|
|
667
|
+
try { window.location.assign(to); } catch (e) { try { window.location.href = to; } catch (e2) { /* 忽略 */ } }
|
|
668
|
+
};
|
|
669
|
+
const esc = (s) => String(s == null ? "" : s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
670
|
+
|
|
671
|
+
const menu = document.createElement("div");
|
|
672
|
+
menu.className = "dsh-ma-menu";
|
|
673
|
+
menu.hidden = true;
|
|
674
|
+
menu.setAttribute("role", "dialog");
|
|
675
|
+
menu.setAttribute("aria-label", "远程控制菜单");
|
|
676
|
+
menu.innerHTML =
|
|
677
|
+
'<div class="dsh-ma-menu-head"><b>远程控制</b><span class="dsh-ma-menu-dev"></span></div>' +
|
|
678
|
+
'<div class="dsh-ma-menu-sec">显示字号</div>' +
|
|
679
|
+
'<div class="dsh-ma-menu-sub">当前 <b class="dsh-ma-menu-fsval">最小</b> · 只影响这台设备的远程页面</div>' +
|
|
680
|
+
'<input type="range" min="0" max="3" step="1" value="0" aria-label="显示字号">' +
|
|
681
|
+
'<div class="dsh-ma-menu-ticks"><span>最小</span><span>中</span><span>大</span><span>超大</span></div>' +
|
|
682
|
+
'<div class="dsh-ma-menu-preview">预览:手机远程控制,随手可用。</div>' +
|
|
683
|
+
'<div class="dsh-ma-e2ee-sec" data-role="e2ee-sec">' +
|
|
684
|
+
'<div class="dsh-ma-menu-sec">加密状态</div>' +
|
|
685
|
+
'<div class="dsh-ma-menu-e2ee" data-role="e2ee"><span class="ico">🔒</span><span class="txt">正在读取…</span></div>' +
|
|
686
|
+
'</div>' +
|
|
687
|
+
'<button type="button" class="dsh-ma-menu-back">← 返回设备列表</button>' +
|
|
688
|
+
'<div class="dsh-ma-menu-note">返回后可重新选择设备;端到端加密需在设备列表页重新解锁(会话密钥只存在本机内存里,刷新即失效)。</div>';
|
|
689
|
+
document.body.appendChild(menu);
|
|
690
|
+
|
|
691
|
+
const fab = document.createElement("button");
|
|
692
|
+
fab.type = "button";
|
|
693
|
+
fab.className = "dsh-ma-fab";
|
|
694
|
+
fab.setAttribute("aria-label", "远程控制菜单");
|
|
695
|
+
fab.innerHTML = '<span class="dsh-ma-fab-ico">字</span><span class="dsh-ma-fab-lock" data-role="lock">🔒</span>';
|
|
696
|
+
document.body.appendChild(fab);
|
|
697
|
+
|
|
698
|
+
const fsRange = menu.querySelector('input[type="range"]');
|
|
699
|
+
const fsVal = menu.querySelector(".dsh-ma-menu-fsval");
|
|
700
|
+
const fsPreview = menu.querySelector(".dsh-ma-menu-preview");
|
|
701
|
+
const setLevel = (level, persist) => {
|
|
702
|
+
const L = applyFsLevel(level);
|
|
703
|
+
if (fsVal) fsVal.textContent = L.name;
|
|
704
|
+
if (fsRange && Number(fsRange.value) !== level) fsRange.value = String(level);
|
|
705
|
+
if (fsPreview) fsPreview.style.fontSize = L.content + "px";
|
|
706
|
+
if (persist) { try { window.localStorage && window.localStorage.setItem(FS_KEY, String(level)); } catch (e3) { /* 忽略 */ } }
|
|
707
|
+
};
|
|
708
|
+
setLevel(readFsLevel(), false);
|
|
709
|
+
|
|
710
|
+
/* ---- 加密状态:只读 E2EE shim 的徽标状态,并把它合并进本菜单 ---- */
|
|
711
|
+
const e2eeSec = menu.querySelector('[data-role="e2ee-sec"]');
|
|
712
|
+
const e2eeRow = menu.querySelector('[data-role="e2ee"]');
|
|
713
|
+
const lockIcon = fab.querySelector('[data-role="lock"]');
|
|
714
|
+
const renderE2ee = () => {
|
|
715
|
+
let st = null;
|
|
716
|
+
try { st = window.__dshE2eeBadge && window.__dshE2eeBadge.state ? window.__dshE2eeBadge.state() : null; } catch (e4) { st = null; }
|
|
717
|
+
if (!st) {
|
|
718
|
+
// shim 未注入(旧版 bridge / 自建关闭 E2EE)→ 整段隐藏(含标题),不留"正在读取…"这种误导文案
|
|
719
|
+
if (e2eeSec) e2eeSec.hidden = true;
|
|
720
|
+
if (e2eeRow) e2eeRow.hidden = true;
|
|
721
|
+
if (lockIcon) lockIcon.hidden = true;
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (e2eeSec) e2eeSec.hidden = false;
|
|
725
|
+
const mode = st.mode === "ok" ? "ok" : (st.mode === "err" ? "err" : "warn");
|
|
726
|
+
if (e2eeRow) {
|
|
727
|
+
e2eeRow.hidden = false;
|
|
728
|
+
e2eeRow.className = "dsh-ma-menu-e2ee" + (mode === "ok" ? "" : mode === "err" ? " is-err" : " is-warn");
|
|
729
|
+
const ico = e2eeRow.querySelector(".ico");
|
|
730
|
+
const txt = e2eeRow.querySelector(".txt");
|
|
731
|
+
if (ico) ico.textContent = mode === "ok" ? "🔒" : "⚠";
|
|
732
|
+
if (txt) txt.textContent = st.text || (mode === "ok" ? "已加密" : "未加密");
|
|
733
|
+
}
|
|
734
|
+
if (lockIcon) { lockIcon.hidden = false; lockIcon.textContent = mode === "ok" ? "🔒" : "⚠"; }
|
|
735
|
+
};
|
|
736
|
+
try {
|
|
737
|
+
// 合并成功后隐藏独立药丸(CSS 认 html[data-dsh-ma-merged-e2ee])
|
|
738
|
+
if (window.__dshE2eeBadge) document.documentElement.setAttribute("data-dsh-ma-merged-e2ee", "1");
|
|
739
|
+
document.addEventListener("dsh-e2ee-badge", renderE2ee);
|
|
740
|
+
window.addEventListener("dsh-e2ee-badge", renderE2ee);
|
|
741
|
+
} catch (e4b) { /* 忽略 */ }
|
|
742
|
+
renderE2ee();
|
|
743
|
+
[600, 2000, 5000].forEach((ms) => setTimeout(renderE2ee, ms)); // shim 可能晚于本层挂牌
|
|
744
|
+
|
|
745
|
+
/* ---- 交互 ---- */
|
|
746
|
+
const closeMenu = () => { menu.hidden = true; };
|
|
747
|
+
const openMenu = () => { renderE2ee(); menu.hidden = false; };
|
|
748
|
+
fab.addEventListener("click", (e) => {
|
|
749
|
+
e.stopPropagation(); e.preventDefault();
|
|
750
|
+
if (menu.hidden) openMenu(); else closeMenu();
|
|
751
|
+
});
|
|
752
|
+
menu.addEventListener("click", (e) => e.stopPropagation());
|
|
753
|
+
if (fsRange) {
|
|
754
|
+
fsRange.addEventListener("input", (e) => { e.stopPropagation(); setLevel(Number(fsRange.value), true); });
|
|
755
|
+
fsRange.addEventListener("click", (e) => e.stopPropagation());
|
|
756
|
+
fsRange.addEventListener("touchstart", (e) => e.stopPropagation(), { passive: true });
|
|
757
|
+
}
|
|
758
|
+
const backBtn = menu.querySelector(".dsh-ma-menu-back");
|
|
759
|
+
if (backBtn) {
|
|
760
|
+
backBtn.addEventListener("click", (e) => {
|
|
761
|
+
e.stopPropagation(); e.preventDefault();
|
|
762
|
+
// 绝对路径:镜像页当前路径可能是 /remote/<deviceId>/…,必须回到 APP 外壳的根
|
|
763
|
+
gotoApp("/app/");
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
// 点菜单外部关闭(菜单内的事件都 stopPropagation)
|
|
767
|
+
document.addEventListener("click", (e) => {
|
|
768
|
+
if (menu.hidden) return;
|
|
769
|
+
const t = e.target;
|
|
770
|
+
if (t === fab || (t && t.closest && t.closest(".dsh-ma-menu, .dsh-ma-fab"))) return;
|
|
771
|
+
closeMenu();
|
|
772
|
+
});
|
|
773
|
+
const devEl = menu.querySelector(".dsh-ma-menu-dev");
|
|
774
|
+
if (devEl) {
|
|
775
|
+
const d = deviceIdOfPath();
|
|
776
|
+
if (d) devEl.textContent = d; else devEl.hidden = true;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/* ===== 会话过期引导(非阻塞) =====
|
|
780
|
+
镜像页的会话(dsh_token)过期后,官方客户端只会表现为"连不上/一直重试",用户看不到任何解释。
|
|
781
|
+
这里**只观察**同源 API 的 401/403 响应(不拦截、不改写、不读 body),连续两次就给一条可点的
|
|
782
|
+
提示条:「登录状态已过期 → 去重新登录」(跳到 /app/ 由外壳决定是回设备列表还是回登录页)。 */
|
|
783
|
+
try {
|
|
784
|
+
if (!window.__dshMaSessionWatch) {
|
|
785
|
+
window.__dshMaSessionWatch = true;
|
|
786
|
+
const bar = document.createElement("div");
|
|
787
|
+
bar.className = "dsh-ma-sessionbar";
|
|
788
|
+
bar.hidden = true;
|
|
789
|
+
bar.innerHTML = '<span class="msg">登录状态已过期,请返回设备列表重新登录。</span>' +
|
|
790
|
+
'<button type="button" data-role="go">去重新登录</button>' +
|
|
791
|
+
'<button type="button" data-role="close" aria-label="关闭">✕</button>';
|
|
792
|
+
document.body.appendChild(bar);
|
|
793
|
+
bar.addEventListener("click", (e) => {
|
|
794
|
+
const t = e.target;
|
|
795
|
+
if (!t || !t.getAttribute) return;
|
|
796
|
+
const role = t.getAttribute("data-role");
|
|
797
|
+
if (role === "go") gotoApp("/app/?reason=expired");
|
|
798
|
+
if (role === "close") bar.hidden = true;
|
|
799
|
+
});
|
|
800
|
+
let strikes = 0;
|
|
801
|
+
const noteStatus = (status) => {
|
|
802
|
+
// 只认 401:中继对"会话失效"回 401,对"设备不属于本账号"回 403 —— 后者不是会话过期,
|
|
803
|
+
// 拿它提示"请重新登录"会把人引到错误的动作上。
|
|
804
|
+
if (status !== 401) { strikes = 0; return; }
|
|
805
|
+
strikes += 1;
|
|
806
|
+
if (strikes >= 2) bar.hidden = false;
|
|
807
|
+
};
|
|
808
|
+
const origFetch = window.fetch;
|
|
809
|
+
if (typeof origFetch === "function") {
|
|
810
|
+
window.fetch = function (input, init) {
|
|
811
|
+
let url = "";
|
|
812
|
+
try { url = typeof input === "string" ? input : (input && input.url) || ""; } catch (e5) { url = ""; }
|
|
813
|
+
const p = origFetch.call(this, input, init);
|
|
814
|
+
try {
|
|
815
|
+
const abs = url.slice(0, 8).toLowerCase();
|
|
816
|
+
const sameOrigin = !(abs.indexOf("http://") === 0 || abs.indexOf("https://") === 0) || url.indexOf(window.location.origin) === 0;
|
|
817
|
+
const apiish = url.indexOf("/api/") >= 0 || url.indexOf("/remote/") >= 0 || url.indexOf("_devices") >= 0 || url.indexOf("_quota") >= 0;
|
|
818
|
+
if (sameOrigin && apiish && p && typeof p.then === "function") {
|
|
819
|
+
p.then((res) => { try { noteStatus(res && res.status); } catch (e6) { /* 忽略 */ } },
|
|
820
|
+
() => { /* 网络错误不算会话过期 */ });
|
|
821
|
+
}
|
|
822
|
+
} catch (e7) { /* 忽略 */ }
|
|
823
|
+
return p;
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
} catch (e8) { /* 引导条失败绝不能影响主功能 */ }
|
|
828
|
+
};
|
|
829
|
+
|
|
526
830
|
let done = false;
|
|
527
831
|
const boot = () => {
|
|
528
|
-
if (
|
|
529
|
-
|
|
832
|
+
if (!HOSTISH()) return;
|
|
833
|
+
/* 悬浮按钮/菜单:任何宽度都要有(电脑端访问镜像页时同样需要返回设备列表的出口)。
|
|
834
|
+
失败也不影响手机端布局适配 —— 两条链路各自独立。 */
|
|
835
|
+
if (!fabDone) { fabDone = true; try { buildRemoteMenu(); } catch (eFabDone) { /* 忽略 */ } }
|
|
836
|
+
if (done || !NARROW()) return;
|
|
837
|
+
const frame = document.querySelector(".pI_x6G_frame, [data-sidebar-collapsed], [data-details-collapsed], [data-rightbar-collapsed]");
|
|
530
838
|
if (!frame || !frame.querySelector) return;
|
|
531
839
|
done = true;
|
|
532
840
|
frame.classList.add("dsh-ma-frame");
|
|
533
841
|
|
|
534
|
-
/* 给 frame 子列打稳定标记(dsh-ma-*)
|
|
842
|
+
/* 给 frame 子列打稳定标记(dsh-ma-*),规避官方类名前缀随构建变化。
|
|
843
|
+
⚠️ 必须先把**不是列**的节点剔掉(2026-09-19 事故根因):
|
|
844
|
+
· overlayLayer(data-shell-overlay / *overlayLayer*)= 官方 shell.overlay 宿主,inset:0 全屏,
|
|
845
|
+
在 0.1.5-rc.2 里**始终**是 frame 的最后一个子元素;
|
|
846
|
+
· 拖拽把手(*handle* / [data-side])= 8px 宽绝对定位条(折叠时官方根本不渲染)。
|
|
847
|
+
旧实现的"详情抽屉兜底 = 取 frame 最后一个子元素"会把 overlay 层当成抽屉:
|
|
848
|
+
属性认不到 → 判成未折叠,overlay 全屏 → 几何可见 → 遮罩常亮拦住整屏。 */
|
|
849
|
+
const isOverlayLayer = (el) => {
|
|
850
|
+
try {
|
|
851
|
+
if (el.hasAttribute && el.hasAttribute("data-shell-overlay")) return true;
|
|
852
|
+
const c = typeof el.className === "string" ? el.className : "";
|
|
853
|
+
return /overlaylayer/i.test(c);
|
|
854
|
+
} catch (e) { return false; }
|
|
855
|
+
};
|
|
856
|
+
const isHandle = (el) => {
|
|
857
|
+
try {
|
|
858
|
+
if (el.hasAttribute && el.hasAttribute("data-side")) return true;
|
|
859
|
+
const c = typeof el.className === "string" ? el.className : "";
|
|
860
|
+
return /handle|resizer/i.test(c);
|
|
861
|
+
} catch (e) { return false; }
|
|
862
|
+
};
|
|
863
|
+
const overlayLayer = () => frame.querySelector('[data-shell-overlay]');
|
|
864
|
+
|
|
865
|
+
/* 第三列在官方 0.1.2-rc.1 叫 details、0.1.5-rc.2 起叫 rightbar —— 两个名字都认。 */
|
|
866
|
+
const THIRD_COL_RE = /details|rightbar/i;
|
|
535
867
|
let sidebar = null, details = null, center = null;
|
|
536
|
-
const cols = [...frame.children].filter((c) => c instanceof HTMLElement);
|
|
868
|
+
const cols = [...frame.children].filter((c) => c instanceof HTMLElement && !isOverlayLayer(c) && !isHandle(c));
|
|
537
869
|
for (const col of cols) {
|
|
538
870
|
const cls = typeof col.className === "string" ? col.className : "";
|
|
539
871
|
if (/sidebar/i.test(cls) || col.querySelector(".hHd-Xa_root, [aria-label*='侧边栏'], [aria-label*='sidebar' i]")) {
|
|
540
872
|
col.classList.add("dsh-ma-sidebar"); sidebar = sidebar || col;
|
|
541
|
-
} else if (
|
|
873
|
+
} else if (THIRD_COL_RE.test(cls)) {
|
|
542
874
|
col.classList.add("dsh-ma-details"); details = details || col;
|
|
543
875
|
} else if (/center/i.test(cls)) {
|
|
544
876
|
col.classList.add("dsh-ma-center"); center = center || col;
|
|
@@ -546,8 +878,12 @@ const SCRIPT = `(() => {
|
|
|
546
878
|
}
|
|
547
879
|
if (!sidebar && cols[0]) { cols[0].classList.add("dsh-ma-sidebar"); sidebar = sidebar || cols[0]; }
|
|
548
880
|
if (!center && cols[1]) { cols[1].classList.add("dsh-ma-center"); center = center || cols[1]; }
|
|
549
|
-
|
|
550
|
-
|
|
881
|
+
/* 兜底:只在**剩下的候选列**里取最后一个(overlay/把手/已认领的列都已排除)——
|
|
882
|
+
命中不到就老实放弃(details=null),绝不再把全屏层当抽屉。 */
|
|
883
|
+
if (!details) {
|
|
884
|
+
const rest = cols.filter((c) => c !== sidebar && c !== center);
|
|
885
|
+
const cand = rest.length ? rest[rest.length - 1] : null;
|
|
886
|
+
if (cand) { cand.classList.add("dsh-ma-details"); details = cand; }
|
|
551
887
|
}
|
|
552
888
|
|
|
553
889
|
const toggleSel = '[aria-label="打开侧边栏"], [aria-label="关闭侧边栏"], [aria-label="Open sidebar"], [aria-label="Close sidebar"]';
|
|
@@ -597,84 +933,6 @@ const SCRIPT = `(() => {
|
|
|
597
933
|
sync(); // 立刻按意图生效(不依赖官方属性回调)
|
|
598
934
|
});
|
|
599
935
|
|
|
600
|
-
/* ===== 手机端字号档位(2026-09-16 新增) =====
|
|
601
|
-
4 档:0=最小(当前) / 1=中 / 2=大 / 3=超大;滑块调;localStorage 记住;只作用于手机端
|
|
602
|
-
(样式在 @media (max-width:820px) 内,PC 不受影响)。
|
|
603
|
-
实现见 STYLE 里 "手机端字号档位" 段:改 :root 字号 + -webkit-text-size-adjust,
|
|
604
|
-
并同步官方正文变量(--dsh-content-font-size),保证聊天正文一定跟着变。 */
|
|
605
|
-
const FS_KEY = "dsh-ma-font-scale";
|
|
606
|
-
const FS_LEVELS = [
|
|
607
|
-
{ name: "最小", scale: 1.00, content: 14 },
|
|
608
|
-
{ name: "中", scale: 1.08, content: 15 },
|
|
609
|
-
{ name: "大", scale: 1.18, content: 16.5 },
|
|
610
|
-
{ name: "超大", scale: 1.32, content: 18.5 }
|
|
611
|
-
];
|
|
612
|
-
const readFsLevel = () => {
|
|
613
|
-
try {
|
|
614
|
-
const raw = window.localStorage && window.localStorage.getItem(FS_KEY);
|
|
615
|
-
const i = Number(raw);
|
|
616
|
-
return Number.isInteger(i) && i >= 0 && i < FS_LEVELS.length ? i : 0;
|
|
617
|
-
} catch (e) { return 0; }
|
|
618
|
-
};
|
|
619
|
-
const applyFsLevel = (level) => {
|
|
620
|
-
const L = FS_LEVELS[level] || FS_LEVELS[0];
|
|
621
|
-
const root = document.documentElement;
|
|
622
|
-
if (level === 0) root.removeAttribute("data-dsh-ma-fs");
|
|
623
|
-
else root.setAttribute("data-dsh-ma-fs", String(level));
|
|
624
|
-
// 官方聊天正文:直接喂它自己的变量(它就是这样取值的)
|
|
625
|
-
try {
|
|
626
|
-
root.style.setProperty("--dsh-content-font-size", L.content + "px");
|
|
627
|
-
root.style.setProperty("--dsh-content-font-delta", (L.content - 14) + "px");
|
|
628
|
-
} catch (e2) { /* 忽略 */ }
|
|
629
|
-
return L;
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
const fsPanel = document.createElement("div");
|
|
633
|
-
fsPanel.className = "dsh-ma-fspanel";
|
|
634
|
-
fsPanel.hidden = true;
|
|
635
|
-
fsPanel.innerHTML =
|
|
636
|
-
'<div class="dsh-ma-fs-title">显示字号</div>' +
|
|
637
|
-
'<div class="dsh-ma-fs-sub">仅本机手机端生效,电脑端不受影响 · 当前 <span class="dsh-ma-fs-val">最小</span></div>' +
|
|
638
|
-
'<input type="range" min="0" max="3" step="1" value="0" aria-label="显示字号">' +
|
|
639
|
-
'<div class="dsh-ma-fs-ticks"><span>最小</span><span>中</span><span>大</span><span>超大</span></div>' +
|
|
640
|
-
'<div class="dsh-ma-fs-preview">预览:手机远程控制,随手可用。</div>';
|
|
641
|
-
document.body.appendChild(fsPanel);
|
|
642
|
-
const fsBtn = document.createElement("button");
|
|
643
|
-
fsBtn.type = "button";
|
|
644
|
-
fsBtn.className = "dsh-ma-fsbtn";
|
|
645
|
-
fsBtn.setAttribute("aria-label", "显示字号");
|
|
646
|
-
fsBtn.textContent = "字";
|
|
647
|
-
document.body.appendChild(fsBtn);
|
|
648
|
-
|
|
649
|
-
const fsRange = fsPanel.querySelector('input[type="range"]');
|
|
650
|
-
const fsVal = fsPanel.querySelector(".dsh-ma-fs-val");
|
|
651
|
-
const fsPreview = fsPanel.querySelector(".dsh-ma-fs-preview");
|
|
652
|
-
const setLevel = (level, persist) => {
|
|
653
|
-
const L = applyFsLevel(level);
|
|
654
|
-
if (fsVal) fsVal.textContent = L.name;
|
|
655
|
-
if (fsRange && Number(fsRange.value) !== level) fsRange.value = String(level);
|
|
656
|
-
if (fsPreview) fsPreview.style.fontSize = L.content + "px";
|
|
657
|
-
if (persist) { try { window.localStorage && window.localStorage.setItem(FS_KEY, String(level)); } catch (e3) { /* 忽略 */ } }
|
|
658
|
-
};
|
|
659
|
-
setLevel(readFsLevel(), false);
|
|
660
|
-
|
|
661
|
-
if (fsRange) {
|
|
662
|
-
fsRange.addEventListener("input", (e) => { e.stopPropagation(); setLevel(Number(fsRange.value), true); });
|
|
663
|
-
fsRange.addEventListener("click", (e) => e.stopPropagation());
|
|
664
|
-
fsRange.addEventListener("touchstart", (e) => e.stopPropagation(), { passive: true });
|
|
665
|
-
}
|
|
666
|
-
fsBtn.addEventListener("click", (e) => {
|
|
667
|
-
e.stopPropagation(); e.preventDefault();
|
|
668
|
-
fsPanel.hidden = !fsPanel.hidden;
|
|
669
|
-
});
|
|
670
|
-
// 点面板外部关闭(面板内的事件都 stopPropagation)
|
|
671
|
-
document.addEventListener("click", (e) => {
|
|
672
|
-
if (fsPanel.hidden) return;
|
|
673
|
-
const t = e.target;
|
|
674
|
-
if (t === fsBtn || (t && t.closest && t.closest(".dsh-ma-fspanel, .dsh-ma-fsbtn"))) return;
|
|
675
|
-
fsPanel.hidden = true;
|
|
676
|
-
});
|
|
677
|
-
|
|
678
936
|
/* 侧栏是否**真的**滑进了视口。这是"遮罩能不能拦截点击"的最后一道保险:
|
|
679
937
|
只要官方属性语义与我们理解的不一致,光看属性就可能误判;而遮罩一旦拦截整屏,
|
|
680
938
|
用户看到的就是"整屏阴影 + 点哪都没反应"。以实测几何为准,误判也不会挡住用户。 */
|
|
@@ -683,7 +941,24 @@ const SCRIPT = `(() => {
|
|
|
683
941
|
try { const r = el.getBoundingClientRect(); return !!r && r.right > 8 && r.width > 40; }
|
|
684
942
|
catch (e2) { return false; }
|
|
685
943
|
};
|
|
686
|
-
|
|
944
|
+
/* 抽屉必须"像抽屉":不是全屏层(overlay 层/主内容区都被排除在外)、不是拖拽把手、
|
|
945
|
+
此刻真的在视口里。任一不满足 → 它就不该让遮罩拦截点击(0.1.5 事故的结构性防线)。 */
|
|
946
|
+
const isDrawer = (el) => {
|
|
947
|
+
if (!el || isOverlayLayer(el) || isHandle(el)) return false;
|
|
948
|
+
try {
|
|
949
|
+
const r = el.getBoundingClientRect();
|
|
950
|
+
if (!r || r.width <= 40 || r.right <= 8) return false;
|
|
951
|
+
return r.width < window.innerWidth * 0.98; // 全屏宽 = 主界面,不是抽屉
|
|
952
|
+
} catch (e2) { return false; }
|
|
953
|
+
};
|
|
954
|
+
const sidebarInView = () => isDrawer(sidebar || document.querySelector(".dsh-ma-sidebar"));
|
|
955
|
+
const detailsInView = () => isDrawer(details || document.querySelector(".dsh-ma-details, .dsh-ma-rightbar"));
|
|
956
|
+
/* 第三列折叠态:官方 0.1.2-rc.1 是 data-details-collapsed,0.1.5-rc.2 起是 data-rightbar-collapsed;
|
|
957
|
+
两个都认;都不存在(未来再改名)时**不当作展开**,只由几何判定决定(见 detailsInView)。 */
|
|
958
|
+
const detailsCollapsed = () =>
|
|
959
|
+
truthyAttr(frame, "data-details-collapsed") || truthyAttr(frame, "data-rightbar-collapsed");
|
|
960
|
+
const detailsAttrKnown = () =>
|
|
961
|
+
frame.hasAttribute("data-details-collapsed") || frame.hasAttribute("data-rightbar-collapsed");
|
|
687
962
|
/* 状态来源(按优先级):
|
|
688
963
|
① 用户意图 userWantsOpen —— 点菜单按钮打开 / 点遮罩关闭,期间**不被几何判定推翻**;
|
|
689
964
|
② 官方 frame 的 data 属性 —— 用户没表达意图时(初始、官方自己切换)以它为准。
|
|
@@ -691,22 +966,62 @@ const SCRIPT = `(() => {
|
|
|
691
966
|
点菜单按钮加 class → 动画未开始、rect 仍在屏外 → sync 立刻把 class 摘掉 → 抽屉永远打不开。
|
|
692
967
|
现在只让几何判定决定**遮罩能否拦截点击**(最后一道保险),不再否决用户的展开意图。 */
|
|
693
968
|
let userWantsOpen = null; // null=未表达意图(跟随官方); true/false=用户已明确开/关
|
|
969
|
+
let scrimTimer = 0, scrimWatchdog = 0;
|
|
970
|
+
/* 遮罩闸门:**只有此刻真的看得见抽屉**才允许拦截点击。
|
|
971
|
+
抽屉是滑入动画(.22s),所以同步判定必然"还看不见" —— 那一瞬间不下闸即可,
|
|
972
|
+
真正的下闸交给下一帧与动画结束后的复核(applyScrim),既不误拦也不影响手感。
|
|
973
|
+
另外 CSS 侧已收口成"只认 dsh-ma-scrim-on 一个闸",不会再出现"某个 class 常亮 → 整屏被拦"。 */
|
|
974
|
+
const scrimShouldBeOn = () => {
|
|
975
|
+
const html = document.documentElement;
|
|
976
|
+
if (html.classList.contains("dsh-ma-sidebar-open") && sidebarInView()) return true;
|
|
977
|
+
if (html.classList.contains("dsh-ma-details-open") && detailsInView()) return true;
|
|
978
|
+
return false;
|
|
979
|
+
};
|
|
980
|
+
const applyScrim = () => {
|
|
981
|
+
try { document.documentElement.classList.toggle("dsh-ma-scrim-on", scrimShouldBeOn()); }
|
|
982
|
+
catch (e2) { /* 忽略 */ }
|
|
983
|
+
};
|
|
984
|
+
/* 看门狗:遮罩一旦在拦截点击,就必须**始终**有抽屉在视口里;否则立刻摘掉。
|
|
985
|
+
它防的是"未来官方再改名/再改结构"导致的同类死局 —— 用户被遮罩困住的代价太高,
|
|
986
|
+
宁可多这一个每 800ms 的轻量校验(没有遮罩时自会停表)。 */
|
|
987
|
+
const armScrimWatchdog = () => {
|
|
988
|
+
if (scrimWatchdog) return;
|
|
989
|
+
try {
|
|
990
|
+
scrimWatchdog = setInterval(() => {
|
|
991
|
+
applyScrim();
|
|
992
|
+
const html = document.documentElement;
|
|
993
|
+
if (!html.classList.contains("dsh-ma-scrim-on") && !html.classList.contains("dsh-ma-sidebar-open") && !html.classList.contains("dsh-ma-details-open")) {
|
|
994
|
+
clearInterval(scrimWatchdog); scrimWatchdog = 0;
|
|
995
|
+
}
|
|
996
|
+
}, 800);
|
|
997
|
+
} catch (e2) { scrimWatchdog = 0; }
|
|
998
|
+
};
|
|
694
999
|
const sync = () => {
|
|
695
1000
|
const open = userWantsOpen !== null ? userWantsOpen : expanded();
|
|
696
1001
|
document.documentElement.classList.toggle("dsh-ma-sidebar-open", open);
|
|
697
|
-
/* 只给遮罩加闸:即使判定/几何有偏差,也绝不让"看不见的遮罩"拦住整屏点击 */
|
|
698
|
-
const scrimOn = open && (userWantsOpen === true || sidebarInView());
|
|
699
|
-
document.documentElement.classList.toggle("dsh-ma-scrim-on", scrimOn);
|
|
700
1002
|
/* 汉堡按钮**始终可见**:以前判定为"展开"时会把它 display:none 隐藏,
|
|
701
1003
|
于是判定一旦出错(或遮罩因异常常亮),用户既点不动内容、也没有任何入口 —— 死局。
|
|
702
1004
|
保持可见 + 点它可开可关(见其 click 处理),任何异常状态下都留一条出路。 */
|
|
703
1005
|
if (hamburger) hamburger.style.display = "";
|
|
704
|
-
|
|
705
|
-
|
|
1006
|
+
/* 第三列(详情/rightbar):属性说展开 **且** 真的是个在视口里的抽屉,才算展开。
|
|
1007
|
+
⚠️ 这里就是 2026-09-19 的根因点:旧版只判属性 + inView,而官方 0.1.5 把属性改名后,
|
|
1008
|
+
被误标成 details 的 overlay 层(全屏)满足 inView → 遮罩常亮。现在多了 isDrawer 的结构约束。 */
|
|
1009
|
+
const dOpen = detailsAttrKnown() ? (!detailsCollapsed() && detailsInView()) : detailsInView();
|
|
706
1010
|
document.documentElement.classList.toggle("dsh-ma-details-open", dOpen);
|
|
1011
|
+
// 遮罩:同步先按当前几何下闸(抽屉已在视口内时无延迟),再在动画开始/结束后复核两次
|
|
1012
|
+
applyScrim();
|
|
1013
|
+
try {
|
|
1014
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(applyScrim);
|
|
1015
|
+
clearTimeout(scrimTimer);
|
|
1016
|
+
scrimTimer = setTimeout(applyScrim, 300);
|
|
1017
|
+
} catch (e2) { /* 忽略 */ }
|
|
1018
|
+
armScrimWatchdog();
|
|
707
1019
|
};
|
|
708
1020
|
try {
|
|
709
|
-
new MutationObserver(sync).observe(frame, {
|
|
1021
|
+
new MutationObserver(sync).observe(frame, {
|
|
1022
|
+
attributes: true,
|
|
1023
|
+
attributeFilter: ["data-sidebar-collapsed", "data-details-collapsed", "data-rightbar-collapsed", "data-rightbar-fullscreen"],
|
|
1024
|
+
});
|
|
710
1025
|
} catch (e2) { /* 退化:仅在下次 boot 同步 */ }
|
|
711
1026
|
// 几何变化(旋转屏幕 / 窗口缩放 / 侧栏动画结束)也要重算,否则会把 boot 时的判定一直沿用
|
|
712
1027
|
try {
|
|
@@ -715,6 +1030,8 @@ const SCRIPT = `(() => {
|
|
|
715
1030
|
if (sidebar && typeof ResizeObserver === "function") new ResizeObserver(sync).observe(sidebar);
|
|
716
1031
|
} catch (e2) { /* 非关键 */ }
|
|
717
1032
|
sync();
|
|
1033
|
+
/* 兜底复核:官方模块异步挂载、字体/主题切换都会改尺寸 —— 断点各复核一次遮罩闸门。 */
|
|
1034
|
+
[400, 1200, 3000].forEach((ms) => setTimeout(applyScrim, ms));
|
|
718
1035
|
/* 窄屏抽屉:点选会话(激活行)/点侧栏内「新建会话」后自动收起(官方不做;适配层点官方 toggle)。
|
|
719
1036
|
仅当:窄屏 && 抽屉展开 && 点击发生在侧栏抽屉内;行内「⋯」按钮 stopPropagation 不会误触。 */
|
|
720
1037
|
if (!drawerAutoCloseOn) {
|