@mrrisega/dsh-remote 0.6.0-beta.11 → 0.6.0-beta.2
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/README.md +0 -30
- package/clients/dsh-remote/dsh-bridge.mjs +5 -305
- package/package.json +2 -2
- package/packages/dsh-remote-web/lib/client.js +178 -467
- package/packages/dsh-remote-web/lib/index.js +4 -115
- package/packages/dsh-remote-web/test/access-key-sessions.test.mjs +11 -81
- package/packages/dsh-remote-web/test/account-switch.test.mjs +1 -1
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +2 -174
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +5 -55
- package/packages/dsh-remote-web/test/uninstall-runtime.test.mjs +2 -2
- package/clients/dsh-remote/e2ee-client.mjs +0 -711
- package/clients/dsh-remote/e2ee-shim-script.js +0 -697
- package/clients/dsh-remote/e2ee-shim.mjs +0 -158
- package/clients/dsh-remote/mobile-adapter.mjs +0 -541
- package/clients/dsh-remote/test/bridge-html-inject.test.mjs +0 -110
- package/clients/dsh-remote/test/e2ee-bridge.test.mjs +0 -630
- package/clients/dsh-remote/test/e2ee-client.test.mjs +0 -365
- package/clients/dsh-remote/test/e2ee-shim.test.mjs +0 -507
- package/clients/dsh-remote/test/e2ee-wecrypto-parity.test.mjs +0 -200
- package/clients/dsh-remote/test/lib-e2ee-shim.mjs +0 -59
- package/clients/dsh-remote/test/lib-native-e2ee.mjs +0 -92
- package/clients/dsh-remote/test/mobile-adapter.test.mjs +0 -124
- package/packages/dsh-remote-web/test/e2ee-state.test.mjs +0 -141
- package/packages/dsh-remote-web/test/feedback-account-mine.test.mjs +0 -70
- package/packages/dsh-remote-web/test/password-reset.test.mjs +0 -429
|
@@ -1,541 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* mobile-adapter.mjs — bridge 侧注入的“经隧道访问的官方 dsh web”移动端适配层。
|
|
3
|
-
*
|
|
4
|
-
* 注入目标(仅经隧道回传的手机页面;官方包 / 桌面宽屏不受影响):
|
|
5
|
-
* - 官方 dsh web(dsh-web-frontend 0.1.2-rc.1)index.html 的 </head> 前,由 dsh-bridge.mjs
|
|
6
|
-
* 对 content-type 含 text/html 的上游响应注入 <style id="dsh-mobile-adapter-css"> 与
|
|
7
|
-
* <script id="dsh-mobile-adapter-js">。
|
|
8
|
-
*
|
|
9
|
-
* 设计依据(实测 390×844 DOM,非猜测):
|
|
10
|
-
* - 布局外壳:官方 .pI_x6G_frame 为三列 grid(内联 gridTemplateColumns),侧栏/详情列通过
|
|
11
|
-
* data-sidebar-collapsed / data-details-collapsed 表达状态;官方在 <1024px 时把侧栏自动
|
|
12
|
-
* 收成 56px 图标 rail,再手动展开(narrowExpanded)时却把中心列挤到 ~110px(实测 grid
|
|
13
|
-
* "280px 110px 0px")——这就是“侧栏挤压/设置内容可视区不足”的根因。
|
|
14
|
-
* - 适配策略:
|
|
15
|
-
* 1) ≤820px 强制三列轨宽为 0 / 1fr / 0,内容区全宽;侧栏/详情改造成离屏浮层(off-canvas),
|
|
16
|
-
* 顶部放一个小鲸鱼/菜单按钮,点击 = 程序化点击官方 toggle(aria-label 打开/关闭侧边栏),
|
|
17
|
-
* 不复制官方状态,数据属性仍是唯一真源;
|
|
18
|
-
* 2) 设置面板(实测挂载在侧栏 .hHd-Xa_settingsArea 子树里的 fixed 弹层;panel 内
|
|
19
|
-
* nav 188px + content 154px 左右并排)→ 手机改为上下堆叠:顶部 tab 行(横向滚动)置顶,
|
|
20
|
-
* 下方内容区全高滚动。fix2:官方模块 CSS 运行期才注入 head(晚于本 <style>),普通规则
|
|
21
|
-
* 被官方同特异性后加载规则覆盖(nav 是 <nav>,圆1 的 div.VOzbGW_nav 从未命中;overlay
|
|
22
|
-
* 被官方 min(800px,84vw) 钉成左靠 328px 抽屉感)→ 本版设置区全部用 [class~=...] 精确类
|
|
23
|
-
* + 几何 !important;并把侧栏/详情浮层的 will-change:transform 去掉(transform 祖先会让
|
|
24
|
-
* 侧栏子树里的 fixed 设置弹层以侧栏为 containing block,被压到 84vw——fix2 设置窄的根因);
|
|
25
|
-
* 顶部 tab 字号 ≤13px、行宽拉满可横向滚动,当前项自动滚入可视;
|
|
26
|
-
* 3) 输入控件字号 ≥16px 防 iOS 聚焦缩放;hero 的工作区/模式座位行允许换行,防 212px 座位
|
|
27
|
-
* 右缘伸出视口(实测 cubgiG_seat right=409 > 390);
|
|
28
|
-
* 4) html/body overflow-x 防护、safe-area inset、触控目标友好;配色只用官方 CSS 变量;
|
|
29
|
-
* 5) 鲸鱼余额挂件(用户另装的第三方 dsh-whale-widget,浮动 .dshwv-root,z-index:9999):
|
|
30
|
-
* fix2 实测它的窄屏“卡左上角盖标题”与“拖不动”均为其自身缺陷的放大:
|
|
31
|
-
* - 拖不动:触屏上 pointer 拖拽被页面滚动抢占(pointercancel,坐标归 0 → 挂件被“瞬移”),
|
|
32
|
-
* 鼠标路径正常 → 适配层在鲸鱼实体像素的 touchstart 上 preventDefault(仅 ≤820),
|
|
33
|
-
* 让浏览器不抢滚动,挂件原生拖拽在手机上恢复;
|
|
34
|
-
* - 默认位置:它把锚点记忆存 localStorage(dshw-pos),一旦曾落在「左+上」就每次载入
|
|
35
|
-
* 盖住左上会话标题 → 适配层在页面解析期(挂件 widget.js 是 defer,晚于本脚本)清掉
|
|
36
|
-
* left+top 陈旧锚点,默认回到右下;再兜底一次“卡左上角”检测(合成鼠标事件走它自己
|
|
37
|
-
* 的拖拽收尾,state/localStorage 由它自洽)。
|
|
38
|
-
* 仅对 ≤820 且锚点命中 left+top 干预;透明区点击/滚动仍穿透,与桌面语义一致。
|
|
39
|
-
*
|
|
40
|
-
* 宿主特征门(防误注入其它 html):
|
|
41
|
-
* - 注入前:content-type 必须 text/html 且含 </head> 且原文含官方标记
|
|
42
|
-
* (__ModuleLoader__ / @deepseek-ai/dsh-client-* / <title>DeepSeek Harness</title>);
|
|
43
|
-
* - 运行时:注入脚本在 window.innerWidth ≤ 820 且 DOM 出现官方 frame/overlay 特征时才动作。
|
|
44
|
-
*
|
|
45
|
-
* 类名变化退化策略(官方升级重打包导致 pI_x6G_* / hHd-Xa_* / VOzbGW_* 等前缀改变时):
|
|
46
|
-
* - 结构优先用官方固定输出的 data-* 属性与 aria-label;
|
|
47
|
-
* - JS 用结构探测(frame → 子列 → 打 dsh-ma-* 稳定类)承载布局,不以 hashed 类为唯一钩子;
|
|
48
|
-
* - 命中不到 frame 时静默跳过,页面保持官方默认,绝不报错/破坏。
|
|
49
|
-
*
|
|
50
|
-
* 开关:环境变量 DSH_MOBILE_ADAPTER=0 整体关闭(默认开启)。
|
|
51
|
-
*/
|
|
52
|
-
|
|
53
|
-
const ADAPTER_ID = "dsh-mobile-adapter";
|
|
54
|
-
|
|
55
|
-
/* 注入前 raw HTML 层官方特征(0.1.2-rc.1 index.html 原文稳定存在)。 */
|
|
56
|
-
const HOST_FEATURE_RE =
|
|
57
|
-
/(__ModuleLoader__|@deepseek-ai\/dsh-client-modules|@deepseek-ai\/dsh-client-connection|<title>\s*DeepSeek Harness)/i;
|
|
58
|
-
|
|
59
|
-
/* 运行时 DOM 特征(注入脚本内再次校验;data-* 由官方组件固定输出,不随 CSS modules 改名)。 */
|
|
60
|
-
const RUNTIME_HOST_RE =
|
|
61
|
-
/(data-sidebar-collapsed|data-details-collapsed|data-shell-overlay)|(pI_x6G_frame|hHd-Xa_root|hHd-Xa_toggle)/;
|
|
62
|
-
|
|
63
|
-
const STYLE = `
|
|
64
|
-
/* ===== dsh-remote mobile adapter (bridge 注入;仅 ≤820px 生效,桌面宽屏无任何规则) ===== */
|
|
65
|
-
@media (max-width: 820px) {
|
|
66
|
-
html, body { max-width: 100%; overflow-x: hidden; }
|
|
67
|
-
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
|
|
68
|
-
|
|
69
|
-
/* --- 三列 frame → 内容区全宽(官方内联 gridTemplateColumns 用 !important 覆盖) --- */
|
|
70
|
-
div.pI_x6G_frame, div.dsh-ma-frame { grid-template-columns: 0 minmax(0, 1fr) 0 !important; min-width: 0; }
|
|
71
|
-
/* 侧栏/详情已移出 grid 流(fixed),唯一在流的中心列须显式落到 1fr 轨道,否则自动放置会被塞进 0px 轨 */
|
|
72
|
-
div.pI_x6G_centerCol, div.dsh-ma-center { grid-column: 2; min-width: 0; width: auto; }
|
|
73
|
-
|
|
74
|
-
/* --- 侧栏 / 详情:离屏浮层(off-canvas),展开态滑入 ---
|
|
75
|
-
注意:不能给浮层设 will-change:transform / 持久 transform —— 官方把设置面板等
|
|
76
|
-
fixed 弹层的 DOM 挂在侧栏子树里,transform 祖先会成为 fixed 的 containing block,
|
|
77
|
-
导致弹层被限制在 84vw 侧栏宽内(实测 328px“抽屉感”,fix2 根因之一)。 */
|
|
78
|
-
div.pI_x6G_sidebarCol, div.dsh-ma-sidebar {
|
|
79
|
-
position: fixed; left: 0; top: 0; bottom: 0; margin: 0;
|
|
80
|
-
width: min(84vw, 340px); max-width: 92vw;
|
|
81
|
-
z-index: 300; overflow: hidden;
|
|
82
|
-
border-right: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.25));
|
|
83
|
-
box-shadow: 0 10px 44px rgba(0,0,0,.26);
|
|
84
|
-
transform: translateX(-103%);
|
|
85
|
-
transition: transform .22s var(--ds-ease-in-out, ease);
|
|
86
|
-
}
|
|
87
|
-
html.dsh-ma-sidebar-open div.pI_x6G_sidebarCol,
|
|
88
|
-
html.dsh-ma-sidebar-open div.dsh-ma-sidebar { transform: none; }
|
|
89
|
-
|
|
90
|
-
div.pI_x6G_detailsCol, div.dsh-ma-details {
|
|
91
|
-
position: fixed; right: 0; top: 0; bottom: 0; margin: 0;
|
|
92
|
-
width: min(92vw, 400px); max-width: 96vw;
|
|
93
|
-
z-index: 300; overflow: hidden;
|
|
94
|
-
border-left: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.25));
|
|
95
|
-
box-shadow: 0 10px 44px rgba(0,0,0,.26);
|
|
96
|
-
transform: translateX(103%);
|
|
97
|
-
transition: transform .22s var(--ds-ease-in-out, ease);
|
|
98
|
-
}
|
|
99
|
-
html.dsh-ma-details-open div.pI_x6G_detailsCol,
|
|
100
|
-
html.dsh-ma-details-open div.dsh-ma-details { transform: none; }
|
|
101
|
-
|
|
102
|
-
/* --- 展开遮罩(在浮层下、在官方弹层下) --- */
|
|
103
|
-
div.dsh-ma-scrim {
|
|
104
|
-
position: fixed; inset: 0; z-index: 290;
|
|
105
|
-
background: rgba(0,0,0,.32);
|
|
106
|
-
opacity: 0; pointer-events: none;
|
|
107
|
-
transition: opacity .22s ease;
|
|
108
|
-
}
|
|
109
|
-
html.dsh-ma-sidebar-open div.dsh-ma-scrim,
|
|
110
|
-
html.dsh-ma-details-open div.dsh-ma-scrim { opacity: 1; pointer-events: auto; }
|
|
111
|
-
|
|
112
|
-
/* --- 顶部小鲸鱼/菜单按钮(侧栏收起时可见) --- */
|
|
113
|
-
button.dsh-ma-hamburger {
|
|
114
|
-
position: fixed;
|
|
115
|
-
top: max(10px, env(safe-area-inset-top));
|
|
116
|
-
left: max(10px, env(safe-area-inset-left));
|
|
117
|
-
z-index: 260;
|
|
118
|
-
width: 42px; height: 42px; padding: 0;
|
|
119
|
-
display: flex; align-items: center; justify-content: center;
|
|
120
|
-
border-radius: 12px;
|
|
121
|
-
background: var(--dsw-alias-button-elevated-fill, rgba(255,255,255,.96));
|
|
122
|
-
border: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.3));
|
|
123
|
-
box-shadow: 0 1px 6px rgba(0,0,0,.16);
|
|
124
|
-
color: var(--dsw-alias-label-primary, #111);
|
|
125
|
-
cursor: pointer; touch-action: manipulation;
|
|
126
|
-
}
|
|
127
|
-
button.dsh-ma-hamburger:active { transform: scale(.92); }
|
|
128
|
-
|
|
129
|
-
/* --- 输入字号 ≥16px(防 iOS 聚焦自动缩放) --- */
|
|
130
|
-
textarea,
|
|
131
|
-
input:not([type]), input[type="text"], input[type="search"], input[type="email"],
|
|
132
|
-
input[type="url"], input[type="tel"], input[type="number"], input[type="password"],
|
|
133
|
-
[contenteditable="true"] { font-size: 16px !important; }
|
|
134
|
-
select, button, a, [role="button"], [role="menuitem"], [role="option"], [role="tab"] { touch-action: manipulation; }
|
|
135
|
-
|
|
136
|
-
/* --- hero 工作区/模式座位行:可换行,防右缘被裁(实测 cubgiG_seat right=409>390) --- */
|
|
137
|
-
div.wSkVaW_heroWorkspaceRow { flex-wrap: wrap; row-gap: 8px; column-gap: 8px; justify-content: center; min-width: 0; }
|
|
138
|
-
div.wSkVaW_heroWorkspaceRow > * { max-width: calc(100vw - 32px); }
|
|
139
|
-
div.wSkVaW_composerStack, div.wSkVaW_composerHero, div.uV2eYG_card { min-width: 0; }
|
|
140
|
-
|
|
141
|
-
/* --- 设置面板(fix2 硬化版;实测 390×844:官方模块 CSS 运行期注入在适配层之后,
|
|
142
|
-
普通规则会被官方后加载样式覆盖——nav 是 <nav> 而非 div,圆1 的 div.VOzbGW_nav
|
|
143
|
-
从未命中;overlay 被官方宽度 min(800px,84vw) 钉在左侧 328px,呈“抽屉感”。
|
|
144
|
-
故本版全部用 [class~=...] 精确类 + 几何 !important,官方前缀变化则整段静默失效) --- */
|
|
145
|
-
[class~="VOzbGW_overlay"] {
|
|
146
|
-
position: fixed !important;
|
|
147
|
-
inset: 0 !important;
|
|
148
|
-
width: auto !important; max-width: none !important;
|
|
149
|
-
height: auto !important; max-height: none !important;
|
|
150
|
-
margin: 0 !important; padding: 0 !important;
|
|
151
|
-
border-radius: 0 !important;
|
|
152
|
-
box-sizing: border-box;
|
|
153
|
-
justify-content: center; align-items: stretch;
|
|
154
|
-
}
|
|
155
|
-
[class~="VOzbGW_panel"] {
|
|
156
|
-
box-sizing: border-box;
|
|
157
|
-
width: 100% !important; max-width: 100% !important;
|
|
158
|
-
height: 100% !important; max-height: 100% !important;
|
|
159
|
-
margin: 0 !important; border-radius: 0 !important;
|
|
160
|
-
flex-direction: column !important;
|
|
161
|
-
}
|
|
162
|
-
/* 顶部 tab 行:整行铺满,字号调小(≤13px),横向可滚动 */
|
|
163
|
-
[class~="VOzbGW_nav"] {
|
|
164
|
-
box-sizing: border-box;
|
|
165
|
-
flex: none;
|
|
166
|
-
width: 100% !important;
|
|
167
|
-
flex-direction: row !important;
|
|
168
|
-
align-items: center;
|
|
169
|
-
gap: 4px;
|
|
170
|
-
padding: max(4px, env(safe-area-inset-top)) 4px 0;
|
|
171
|
-
overflow: visible;
|
|
172
|
-
}
|
|
173
|
-
[class~="VOzbGW_navTitle"] {
|
|
174
|
-
flex: none;
|
|
175
|
-
font-size: 13px !important; line-height: 1.2;
|
|
176
|
-
padding: 0 4px 0 8px;
|
|
177
|
-
white-space: nowrap;
|
|
178
|
-
}
|
|
179
|
-
[class~="VOzbGW_navList"] {
|
|
180
|
-
box-sizing: border-box;
|
|
181
|
-
flex: 1 1 auto; min-width: 0;
|
|
182
|
-
flex-direction: row !important;
|
|
183
|
-
gap: 4px;
|
|
184
|
-
overflow-x: auto; overflow-y: hidden;
|
|
185
|
-
-webkit-overflow-scrolling: touch;
|
|
186
|
-
scrollbar-width: none;
|
|
187
|
-
padding: 0 0 6px;
|
|
188
|
-
}
|
|
189
|
-
[class~="VOzbGW_navList"]::-webkit-scrollbar { display: none; }
|
|
190
|
-
[class~="VOzbGW_navCell"] {
|
|
191
|
-
flex: none; min-width: max-content;
|
|
192
|
-
box-sizing: border-box;
|
|
193
|
-
font-size: 13px !important;
|
|
194
|
-
height: 32px !important;
|
|
195
|
-
padding: 0 12px !important;
|
|
196
|
-
}
|
|
197
|
-
/* 内容区:全宽拉满,收敛内边距,最大化有效宽度 */
|
|
198
|
-
[class~="VOzbGW_content"] {
|
|
199
|
-
box-sizing: border-box;
|
|
200
|
-
width: 100% !important; max-width: none !important;
|
|
201
|
-
flex: 1 1 auto; min-width: 0; min-height: 0;
|
|
202
|
-
}
|
|
203
|
-
[class~="VOzbGW_header"] {
|
|
204
|
-
box-sizing: border-box;
|
|
205
|
-
flex: none;
|
|
206
|
-
min-height: 40px;
|
|
207
|
-
padding: 6px 8px 4px 14px !important;
|
|
208
|
-
align-items: center;
|
|
209
|
-
}
|
|
210
|
-
[class~="VOzbGW_close"] { width: 32px; height: 32px; }
|
|
211
|
-
[class~="VOzbGW_options"] {
|
|
212
|
-
box-sizing: border-box;
|
|
213
|
-
flex: 1 1 auto; min-height: 0; overflow-y: auto;
|
|
214
|
-
-webkit-overflow-scrolling: touch;
|
|
215
|
-
padding: 2px 14px calc(14px + env(safe-area-inset-bottom)) !important;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/* 桌面宽屏(>820px):本文件无任何规则 → 对直连/桌面零影响。 */
|
|
220
|
-
`;
|
|
221
|
-
|
|
222
|
-
const SCRIPT = `(() => {
|
|
223
|
-
"use strict";
|
|
224
|
-
try {
|
|
225
|
-
const HOST_RE = new RegExp(${JSON.stringify(RUNTIME_HOST_RE.source)});
|
|
226
|
-
const NARROW = () => window.innerWidth <= 820;
|
|
227
|
-
const HOSTISH = () =>
|
|
228
|
-
HOST_RE.test(document.documentElement.outerHTML.slice(0, 200000)) ||
|
|
229
|
-
!!document.querySelector("[data-shell-overlay], [data-sidebar-collapsed], [data-details-collapsed]");
|
|
230
|
-
|
|
231
|
-
/* —— 鲸鱼余额挂件(dsh-whale-widget,第三方插件的浮动组件)窄屏协同 ——
|
|
232
|
-
若挂件记忆的锚点是「左+上」(实测会盖住左上角会话标题/左上菜单按钮),在页面解析期
|
|
233
|
-
(鲸鱼 widget.js 是 defer 脚本,一定晚于本脚本执行)清掉该陈旧锚点 → 挂件回到自身
|
|
234
|
-
默认右下角。仅窄屏 & 仅命中 left+top 组合,不影响其它自定义位置。 */
|
|
235
|
-
try {
|
|
236
|
-
if (window.innerWidth <= 820 && window.localStorage) {
|
|
237
|
-
const rawPos = window.localStorage.getItem("dshw-pos");
|
|
238
|
-
if (rawPos) {
|
|
239
|
-
const pos = JSON.parse(rawPos);
|
|
240
|
-
if (pos && pos.hAnchor === "left" && pos.vAnchor === "top") {
|
|
241
|
-
window.localStorage.removeItem("dshw-pos");
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
} catch (e3) { /* 私有模式/配额等:忽略 */ }
|
|
246
|
-
|
|
247
|
-
/* ================= 鲸鱼挂件手机辅助(仅 ≤820 生效;挂件类名 .dshwv-* 稳定) ========= */
|
|
248
|
-
let whaleGuardsOn = false;
|
|
249
|
-
let whaleHitMap = null;
|
|
250
|
-
let whaleUnstuckDone = false;
|
|
251
|
-
|
|
252
|
-
const WHALE_IMG = () => document.querySelector(".dshwv-img");
|
|
253
|
-
const WHALE_ROOT = () => document.querySelector(".dshwv-root");
|
|
254
|
-
|
|
255
|
-
/* 命中测试与挂件自身一致:鲸鱼图按 610×610 采样 alpha>10;贴左(镜像)时水平翻转。 */
|
|
256
|
-
function whaleBodyAt(x, y) {
|
|
257
|
-
const img = WHALE_IMG();
|
|
258
|
-
const root = WHALE_ROOT();
|
|
259
|
-
if (!img || !root) return false;
|
|
260
|
-
const flip = root.classList.contains("dshwv-left");
|
|
261
|
-
const r = img.getBoundingClientRect();
|
|
262
|
-
if (!r || r.width <= 0 || r.height <= 0) return false;
|
|
263
|
-
const lx0 = ((x - r.left) / r.width) * 610;
|
|
264
|
-
const ly0 = ((y - r.top) / r.height) * 610;
|
|
265
|
-
if (lx0 < 0 || ly0 < 0 || lx0 >= 610 || ly0 >= 610) return false;
|
|
266
|
-
const key = img.currentSrc || img.src || "";
|
|
267
|
-
if (!whaleHitMap || whaleHitMap.key !== key) {
|
|
268
|
-
if (!img.complete || !img.naturalWidth || img.naturalWidth === 0) return false; /* 图未就绪→不挡 */
|
|
269
|
-
try {
|
|
270
|
-
const cv = document.createElement("canvas");
|
|
271
|
-
cv.width = 610; cv.height = 610;
|
|
272
|
-
const ctx = cv.getContext("2d");
|
|
273
|
-
if (!ctx) return false;
|
|
274
|
-
ctx.drawImage(img, 0, 0, 610, 610);
|
|
275
|
-
whaleHitMap = { key: key, data: ctx.getImageData(0, 0, 610, 610).data };
|
|
276
|
-
} catch (e4) { whaleHitMap = null; return false; }
|
|
277
|
-
}
|
|
278
|
-
const lx = flip ? 610 - lx0 : lx0;
|
|
279
|
-
const ix = Math.min(609, Math.max(0, Math.floor(lx)));
|
|
280
|
-
const iy = Math.min(609, Math.max(0, Math.floor(ly0)));
|
|
281
|
-
return whaleHitMap.data[(iy * 610 + ix) * 4 + 3] > 10;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/* 手机上挂件自身拖拽被滚动抢占:触摸点在鲸鱼实体上时浏览器默认滚动 → pointercancel,
|
|
285
|
-
挂件 pointermove 收不到且把它“瞬移”到取消点(实测拖到左上角)。鼠标路径无此问题。
|
|
286
|
-
在 document capture 阶段对鲸鱼实体像素的 touchstart preventDefault → 浏览器不抢滚动,
|
|
287
|
-
挂件原生的 pointer 拖拽在触屏上随之恢复;透明区仍穿透(与桌面一致)。仅窄屏注册。 */
|
|
288
|
-
function onWhaleTouchStart(ev) {
|
|
289
|
-
try {
|
|
290
|
-
if (!NARROW()) return;
|
|
291
|
-
const t = ev.touches && ev.touches[0];
|
|
292
|
-
if (!t) return;
|
|
293
|
-
const tg = ev.target;
|
|
294
|
-
if (tg && tg.closest && tg.closest(".dshwv-menu, .dshwv-menu-btn, .dshwv-bubble")) return;
|
|
295
|
-
if (whaleBodyAt(t.clientX, t.clientY)) ev.preventDefault();
|
|
296
|
-
} catch (e5) { /* 忽略 */ }
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function whaleHookInit() {
|
|
300
|
-
if (whaleGuardsOn || !NARROW() || !WHALE_ROOT()) return;
|
|
301
|
-
whaleGuardsOn = true;
|
|
302
|
-
try { document.addEventListener("touchstart", onWhaleTouchStart, { capture: true, passive: false }); } catch (e6) {}
|
|
303
|
-
/* 兜底:极端时序下挂件若仍以 (0,0) 卡在左上(如解析期清锚点被竞态错过),settle 完再扶正一次 */
|
|
304
|
-
[1000, 3500].forEach(function (ms) {
|
|
305
|
-
setTimeout(function () { try { whaleUnstickIfStuck(); } catch (e7) {} }, ms);
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function safeInsetSides() {
|
|
310
|
-
const ins = { right: 0, bottom: 0 };
|
|
311
|
-
try {
|
|
312
|
-
const p = document.createElement("div");
|
|
313
|
-
p.style.cssText = "position:fixed;top:0;left:0;width:10px;height:10px;visibility:hidden;pointer-events:none";
|
|
314
|
-
document.body.appendChild(p);
|
|
315
|
-
p.style.bottom = "env(safe-area-inset-bottom, 0px)";
|
|
316
|
-
p.style.top = "auto"; p.style.left = "0px"; p.style.right = "auto";
|
|
317
|
-
let r = p.getBoundingClientRect();
|
|
318
|
-
ins.bottom = Math.max(0, Math.round(window.innerHeight - r.top - 10));
|
|
319
|
-
p.style.bottom = "auto"; p.style.top = "0px";
|
|
320
|
-
p.style.right = "env(safe-area-inset-right, 0px)"; p.style.left = "auto";
|
|
321
|
-
r = p.getBoundingClientRect();
|
|
322
|
-
ins.right = Math.max(0, Math.round(window.innerWidth - r.left - 10));
|
|
323
|
-
p.remove();
|
|
324
|
-
} catch (e8) { /* 忽略 */ }
|
|
325
|
-
return ins;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function whaleBodyPoint() {
|
|
329
|
-
const img = WHALE_IMG();
|
|
330
|
-
if (!img) return null;
|
|
331
|
-
const r = img.getBoundingClientRect();
|
|
332
|
-
if (!r || r.width <= 0 || r.height <= 0) return null;
|
|
333
|
-
const cands = [[0.5, 0.5], [0.62, 0.64], [0.5, 0.72], [0.72, 0.6], [0.35, 0.58], [0.8, 0.68]];
|
|
334
|
-
for (let i = 0; i < cands.length; i++) {
|
|
335
|
-
const x = r.left + cands[i][0] * r.width;
|
|
336
|
-
const y = r.top + cands[i][1] * r.height;
|
|
337
|
-
if (whaleBodyAt(x, y)) return { x: Math.round(x), y: Math.round(y) };
|
|
338
|
-
}
|
|
339
|
-
return { x: Math.round(r.left + r.width * 0.62), y: Math.round(r.top + r.height * 0.66) };
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function dispatchWhalePointer(type, x, y) {
|
|
343
|
-
try {
|
|
344
|
-
document.dispatchEvent(new PointerEvent(type, {
|
|
345
|
-
bubbles: true, cancelable: true, composed: true, pointerType: "mouse",
|
|
346
|
-
pointerId: 61001, isPrimary: true,
|
|
347
|
-
button: type === "pointerup" ? -1 : 0,
|
|
348
|
-
buttons: type === "pointerup" ? 0 : 1,
|
|
349
|
-
clientX: x, clientY: y
|
|
350
|
-
}));
|
|
351
|
-
} catch (e9) { /* 不支持 PointerEvent 的环境:跳过 */ }
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/* 走挂件自己(鼠标路径)的拖拽把它送回右下:state/localStorage 全部由挂件收尾,我们只发合成事件。 */
|
|
355
|
-
function whaleNativeDragToBottomRight() {
|
|
356
|
-
const root = WHALE_ROOT();
|
|
357
|
-
if (!root || root.classList.contains("dshwv-dragging")) return false;
|
|
358
|
-
const start = whaleBodyPoint();
|
|
359
|
-
if (!start) return false;
|
|
360
|
-
const rect = root.getBoundingClientRect();
|
|
361
|
-
const w = rect.width || 122;
|
|
362
|
-
const h = rect.height || 122;
|
|
363
|
-
const ins = safeInsetSides();
|
|
364
|
-
const cx = window.innerWidth - w / 2 - Math.max(ins.right, 10);
|
|
365
|
-
const cy = window.innerHeight - h / 2 - Math.max(ins.bottom, 10);
|
|
366
|
-
if (cx < 0 || cy < 0 || cx < window.innerWidth * 0.5 || cy < window.innerHeight * 0.5) return false;
|
|
367
|
-
dispatchWhalePointer("pointerdown", start.x, start.y);
|
|
368
|
-
let started = false;
|
|
369
|
-
try { started = root.classList.contains("dshwv-dragging"); } catch (e10) {}
|
|
370
|
-
if (!started) { try { dispatchWhalePointer("pointerup", start.x, start.y); } catch (e11) {} return false; }
|
|
371
|
-
const N = 14;
|
|
372
|
-
for (let i = 1; i <= N; i++) {
|
|
373
|
-
const x = Math.round(start.x + ((cx - start.x) * i) / N);
|
|
374
|
-
const y = Math.round(start.y + ((cy - start.y) * i) / N);
|
|
375
|
-
dispatchWhalePointer("pointermove", x, y);
|
|
376
|
-
}
|
|
377
|
-
dispatchWhalePointer("pointerup", cx, cy);
|
|
378
|
-
return true;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function whaleUnstickIfStuck() {
|
|
382
|
-
if (!NARROW() || whaleUnstuckDone) return;
|
|
383
|
-
const root = WHALE_ROOT();
|
|
384
|
-
if (!root || root.classList.contains("dshwv-dragging")) return;
|
|
385
|
-
const rect = root.getBoundingClientRect();
|
|
386
|
-
if (rect.top > 20 || rect.left > 20) return; /* 只扶“真卡左上角”的,用户自己放的位置不动 */
|
|
387
|
-
whaleUnstuckDone = true;
|
|
388
|
-
try { if (window.localStorage) window.localStorage.removeItem("dshw-pos"); } catch (e12) {}
|
|
389
|
-
whaleNativeDragToBottomRight();
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/* 设置面板打开时,把当前 tab 滚进横向可视区(顶部 tab 行手机横向滚动) */
|
|
393
|
-
let settingsWatchOn = false;
|
|
394
|
-
let settingsTick = 0;
|
|
395
|
-
function scrollActiveSettingsTab() {
|
|
396
|
-
const act = document.querySelector(
|
|
397
|
-
'[class~="VOzbGW_navCell"].VOzbGW_active, [class~="VOzbGW_navCell"][aria-current="true"]'
|
|
398
|
-
);
|
|
399
|
-
if (act && act.scrollIntoView) {
|
|
400
|
-
try { act.scrollIntoView({ block: "nearest", inline: "nearest" }); } catch (e13) {
|
|
401
|
-
try { act.scrollIntoView(); } catch (e14) {}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
function watchSettings() {
|
|
406
|
-
if (settingsWatchOn || !NARROW()) return;
|
|
407
|
-
settingsWatchOn = true;
|
|
408
|
-
try {
|
|
409
|
-
const ob = new MutationObserver(function () {
|
|
410
|
-
clearTimeout(settingsTick);
|
|
411
|
-
settingsTick = setTimeout(function () { if (NARROW()) scrollActiveSettingsTab(); }, 140);
|
|
412
|
-
});
|
|
413
|
-
ob.observe(document.body, { childList: true, subtree: true });
|
|
414
|
-
} catch (e15) {}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
let done = false;
|
|
418
|
-
const boot = () => {
|
|
419
|
-
if (done || !NARROW() || !HOSTISH()) return;
|
|
420
|
-
const frame = document.querySelector(".pI_x6G_frame, [data-sidebar-collapsed], [data-details-collapsed]");
|
|
421
|
-
if (!frame || !frame.querySelector) return;
|
|
422
|
-
done = true;
|
|
423
|
-
frame.classList.add("dsh-ma-frame");
|
|
424
|
-
|
|
425
|
-
/* 给 frame 子列打稳定标记(dsh-ma-*),规避官方类名前缀随构建变化 */
|
|
426
|
-
let sidebar = null, details = null, center = null;
|
|
427
|
-
const cols = [...frame.children].filter((c) => c instanceof HTMLElement);
|
|
428
|
-
for (const col of cols) {
|
|
429
|
-
const cls = typeof col.className === "string" ? col.className : "";
|
|
430
|
-
if (/sidebar/i.test(cls) || col.querySelector(".hHd-Xa_root, [aria-label*='侧边栏'], [aria-label*='sidebar' i]")) {
|
|
431
|
-
col.classList.add("dsh-ma-sidebar"); sidebar = sidebar || col;
|
|
432
|
-
} else if (/details/i.test(cls)) {
|
|
433
|
-
col.classList.add("dsh-ma-details"); details = details || col;
|
|
434
|
-
} else if (/center/i.test(cls)) {
|
|
435
|
-
col.classList.add("dsh-ma-center"); center = center || col;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
if (!sidebar && cols[0]) { cols[0].classList.add("dsh-ma-sidebar"); sidebar = sidebar || cols[0]; }
|
|
439
|
-
if (!center && cols[1]) { cols[1].classList.add("dsh-ma-center"); center = center || cols[1]; }
|
|
440
|
-
if (!details && cols[cols.length - 1] && cols[cols.length - 1] !== sidebar && cols[cols.length - 1] !== center) {
|
|
441
|
-
cols[cols.length - 1].classList.add("dsh-ma-details"); details = details || cols[cols.length - 1];
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
const toggleSel = '[aria-label="打开侧边栏"], [aria-label="关闭侧边栏"], [aria-label="Open sidebar"], [aria-label="Close sidebar"]';
|
|
445
|
-
const toggleOf = () => document.querySelector(toggleSel) || document.querySelector(".hHd-Xa_toggle");
|
|
446
|
-
const expanded = () => !frame.hasAttribute("data-sidebar-collapsed");
|
|
447
|
-
|
|
448
|
-
/* 遮罩:点击 = 点官方 toggle(走官方 store,无私有状态) */
|
|
449
|
-
const scrim = document.createElement("div");
|
|
450
|
-
scrim.className = "dsh-ma-scrim";
|
|
451
|
-
document.body.appendChild(scrim);
|
|
452
|
-
scrim.addEventListener("click", (e) => { e.stopPropagation(); const t = toggleOf(); if (t) t.click(); });
|
|
453
|
-
|
|
454
|
-
/* 顶部小鲸鱼/菜单按钮 */
|
|
455
|
-
const hamburger = document.createElement("button");
|
|
456
|
-
hamburger.type = "button";
|
|
457
|
-
hamburger.className = "dsh-ma-hamburger";
|
|
458
|
-
hamburger.setAttribute("aria-label", "打开菜单");
|
|
459
|
-
hamburger.innerHTML = '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h10"/></svg>';
|
|
460
|
-
document.body.appendChild(hamburger);
|
|
461
|
-
hamburger.addEventListener("click", (e) => {
|
|
462
|
-
e.stopPropagation(); e.preventDefault();
|
|
463
|
-
const t = toggleOf();
|
|
464
|
-
if (t) t.click();
|
|
465
|
-
else document.documentElement.classList.add("dsh-ma-sidebar-open"); /* 兜底 */
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
/* 状态唯一真源 = 官方 frame 的 data 属性 */
|
|
469
|
-
const sync = () => {
|
|
470
|
-
const open = expanded();
|
|
471
|
-
document.documentElement.classList.toggle("dsh-ma-sidebar-open", open);
|
|
472
|
-
if (hamburger) hamburger.style.display = open ? "none" : "";
|
|
473
|
-
const dOpen = !frame.hasAttribute("data-details-collapsed") && !!details;
|
|
474
|
-
document.documentElement.classList.toggle("dsh-ma-details-open", dOpen);
|
|
475
|
-
};
|
|
476
|
-
try {
|
|
477
|
-
new MutationObserver(sync).observe(frame, { attributes: true, attributeFilter: ["data-sidebar-collapsed", "data-details-collapsed"] });
|
|
478
|
-
} catch (e2) { /* 退化:仅在下次 boot 同步 */ }
|
|
479
|
-
sync();
|
|
480
|
-
whaleHookInit();
|
|
481
|
-
watchSettings();
|
|
482
|
-
};
|
|
483
|
-
|
|
484
|
-
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot); else boot();
|
|
485
|
-
/* 官方模块系统异步挂载 UI,晚一点再补几次(幂等,done 守卫);同时等鲸鱼/设置弹层就位 */
|
|
486
|
-
[700, 1600, 3200, 7000, 12000].forEach((ms) =>
|
|
487
|
-
setTimeout(() => { try { boot(); whaleHookInit(); watchSettings(); } catch (e) { /* 幂等重试 */ } }, ms)
|
|
488
|
-
);
|
|
489
|
-
} catch (e) { if (window.console) console.warn("[dsh-mobile-adapter]", e && e.message); }
|
|
490
|
-
})();`;
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* 纯函数:把适配层注入到上游 HTML 的 </head> 之前。
|
|
494
|
-
* @param {string} html
|
|
495
|
-
* @returns {{html:string, injected:boolean}}
|
|
496
|
-
*/
|
|
497
|
-
export function injectMobileAdapter(html) {
|
|
498
|
-
const src = String(html);
|
|
499
|
-
const headIdx = src.toLowerCase().lastIndexOf("</head>");
|
|
500
|
-
if (headIdx === -1) return { html: src, injected: false };
|
|
501
|
-
const block =
|
|
502
|
-
`\n<style id="${ADAPTER_ID}-css" data-dsh-mobile-adapter>${STYLE}</style>` +
|
|
503
|
-
`\n<script id="${ADAPTER_ID}-js" data-dsh-mobile-adapter>${SCRIPT}</script>\n`;
|
|
504
|
-
return { html: src.slice(0, headIdx) + block + src.slice(headIdx), injected: true };
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/** 环境开关:DSH_MOBILE_ADAPTER=0 关闭(默认开启)。 */
|
|
508
|
-
export function mobileAdapterEnabled() {
|
|
509
|
-
return process.env.DSH_MOBILE_ADAPTER !== "0";
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* 判断上游响应是否应注入(仅官方 dsh web 的 text/html)。
|
|
514
|
-
* @param {{contentType?:string, html?:string}} opts
|
|
515
|
-
*/
|
|
516
|
-
export function shouldInjectHtml({ contentType, html }) {
|
|
517
|
-
const ct = String(contentType || "");
|
|
518
|
-
if (!/text\/html/i.test(ct)) return false;
|
|
519
|
-
const src = String(html || "");
|
|
520
|
-
if (!/<\/head>/i.test(src)) return false;
|
|
521
|
-
if (/\uFFFD/.test(src)) return false; // 非 UTF-8,不冒险改写
|
|
522
|
-
return HOST_FEATURE_RE.test(src);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* bridge 接线主入口:开关 + 类型/特征 gate + 注入,一步完成。
|
|
527
|
-
* @param {{buf:Buffer, contentType?:string}} args
|
|
528
|
-
* @returns {{buf:Buffer, injected:boolean}}
|
|
529
|
-
*/
|
|
530
|
-
export function maybeInjectMobileAdapter({ buf, contentType }) {
|
|
531
|
-
if (!mobileAdapterEnabled() || !Buffer.isBuffer(buf) || buf.length === 0) {
|
|
532
|
-
return { buf, injected: false };
|
|
533
|
-
}
|
|
534
|
-
const text = buf.toString("utf8");
|
|
535
|
-
if (!shouldInjectHtml({ contentType, html: text })) return { buf, injected: false };
|
|
536
|
-
const out = injectMobileAdapter(text);
|
|
537
|
-
return { buf: Buffer.from(out.html, "utf8"), injected: out.injected };
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
export const ADAPTER_TAG = ADAPTER_ID;
|
|
541
|
-
export default { injectMobileAdapter, maybeInjectMobileAdapter, shouldInjectHtml, mobileAdapterEnabled };
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* bridge HTML 注入集成测试(经 doHttp/handleHttpFrame 全链路):
|
|
4
|
-
* - 官方特征 html 响应 → reply body 含注入的 <style data-dsh-mobile-adapter>;
|
|
5
|
-
* - 同时请求带 gzip Accept-Encoding → 仍是 gzip 且解压后含注入块(注入发生在 gzip 前);
|
|
6
|
-
* - DSH_MOBILE_ADAPTER=0 → 原样回传;
|
|
7
|
-
* - 非 html(SSE/json/二进制)不动。
|
|
8
|
-
*/
|
|
9
|
-
import assert from "node:assert/strict";
|
|
10
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
11
|
-
import http from "node:http";
|
|
12
|
-
import os from "node:os";
|
|
13
|
-
import path from "node:path";
|
|
14
|
-
import test, { after } from "node:test";
|
|
15
|
-
import { gunzipSync } from "node:zlib";
|
|
16
|
-
|
|
17
|
-
const OFFICIAL_HTML = `<!doctype html><html><head>
|
|
18
|
-
<script>window.__ModuleLoader__ = { mode: "queue" }</script>
|
|
19
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
20
|
-
<title>DeepSeek Harness</title>
|
|
21
|
-
</head><body><div id="root"></div></body></html>`;
|
|
22
|
-
|
|
23
|
-
const upstream = http.createServer((req, res) => {
|
|
24
|
-
if (req.url === "/page") {
|
|
25
|
-
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
26
|
-
res.end(OFFICIAL_HTML);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
if (req.url === "/events") {
|
|
30
|
-
res.writeHead(200, { "content-type": "text/event-stream" });
|
|
31
|
-
res.end("data: hi\n\n"); // 结束的 SSE 帧(便于缓冲测试)
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
if (req.url === "/app.js") {
|
|
35
|
-
res.writeHead(200, { "content-type": "application/javascript" });
|
|
36
|
-
res.end("console.log(1)");
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
res.writeHead(404);
|
|
40
|
-
res.end("nope");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "dsh-html-inject-"));
|
|
44
|
-
process.env.DSH_BRIDGE_DEVICE_ID = "dev-htmltest0001";
|
|
45
|
-
process.env.DSH_BRIDGE_CONFIG = path.join(tmpDir, "config.json");
|
|
46
|
-
delete process.env.DSH_MOBILE_ADAPTER; // 默认开启
|
|
47
|
-
|
|
48
|
-
const { handleHttpFrame } = await new Promise((resolve, reject) => {
|
|
49
|
-
upstream.listen(0, "127.0.0.1", () => {
|
|
50
|
-
process.env.DSH_BRIDGE_UPSTREAM = `http://127.0.0.1:${upstream.address().port}`;
|
|
51
|
-
import("../dsh-bridge.mjs").then(resolve, reject);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
after(() => {
|
|
56
|
-
try { upstream.close(); } catch {}
|
|
57
|
-
try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
async function request(frame) {
|
|
61
|
-
let reply;
|
|
62
|
-
const sender = (obj) => { reply = obj; return true; };
|
|
63
|
-
await handleHttpFrame(sender, { type: "http", ...frame });
|
|
64
|
-
return reply;
|
|
65
|
-
}
|
|
66
|
-
const decode = (reply) => {
|
|
67
|
-
const buf = Buffer.from(reply.body, "base64");
|
|
68
|
-
if (reply.headers["content-encoding"] === "gzip") return gunzipSync(buf).toString("utf8");
|
|
69
|
-
return buf.toString("utf8");
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
test("集成:官方特征 html 经 doHttp 回传时注入适配层", async () => {
|
|
73
|
-
const reply = await request({ id: "h1", method: "GET", path: "/page", headers: { "accept-encoding": "gzip", "user-agent": "phone" } });
|
|
74
|
-
assert.equal(reply.status, 200);
|
|
75
|
-
const text = decode(reply);
|
|
76
|
-
assert.ok(text.includes('data-dsh-mobile-adapter'), "应注入 style/script");
|
|
77
|
-
const headIdx = text.toLowerCase().lastIndexOf("</head>");
|
|
78
|
-
assert.ok(text.indexOf("dsh-mobile-adapter-css") < headIdx, "注入块应位于 </head> 前");
|
|
79
|
-
// 原文其余部分保持(仅 splice)
|
|
80
|
-
const oi = OFFICIAL_HTML.toLowerCase().lastIndexOf("</head>");
|
|
81
|
-
assert.ok(text.startsWith(OFFICIAL_HTML.slice(0, oi)));
|
|
82
|
-
assert.ok(text.endsWith(OFFICIAL_HTML.slice(oi)));
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("集成:注入在 gzip 前发生,回包仍可 gzip 解压且含注入块", async () => {
|
|
86
|
-
const reply = await request({ id: "h2", method: "GET", path: "/page", headers: { "accept-encoding": "gzip, deflate", "user-agent": "phone" } });
|
|
87
|
-
assert.equal(reply.headers["content-encoding"], "gzip", "压缩后应带 content-encoding");
|
|
88
|
-
const text = decode(reply);
|
|
89
|
-
assert.ok(text.includes("dsh-mobile-adapter-js"));
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("集成:DSH_MOBILE_ADAPTER=0 关闭注入,原样回传", async () => {
|
|
93
|
-
process.env.DSH_MOBILE_ADAPTER = "0";
|
|
94
|
-
try {
|
|
95
|
-
const reply = await request({ id: "h3", method: "GET", path: "/page", headers: { "user-agent": "phone" } });
|
|
96
|
-
const text = decode(reply);
|
|
97
|
-
assert.ok(!text.includes("data-dsh-mobile-adapter"), "关闭时应原样");
|
|
98
|
-
assert.equal(text, OFFICIAL_HTML);
|
|
99
|
-
} finally {
|
|
100
|
-
delete process.env.DSH_MOBILE_ADAPTER;
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("集成:非 html(SSE / js / json)不动", async () => {
|
|
105
|
-
const ev = await request({ id: "h4", method: "GET", path: "/events", headers: { "accept-encoding": "gzip", "user-agent": "phone" } });
|
|
106
|
-
assert.equal(decode(ev), "data: hi\n\n");
|
|
107
|
-
const js = await request({ id: "h5", method: "GET", path: "/app.js", headers: { "accept-encoding": "gzip", "user-agent": "phone" } });
|
|
108
|
-
assert.equal(decode(js), "console.log(1)");
|
|
109
|
-
assert.ok(!decode(js).includes("dsh-mobile-adapter"));
|
|
110
|
-
});
|