dsh-mobile-ui 0.2.5 → 0.2.8
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/client.js +48 -10
- package/package.json +1 -1
- package/tests/live-probe.py +94 -0
package/client.js
CHANGED
|
@@ -272,8 +272,14 @@ window.__ModuleLoader__.load({
|
|
|
272
272
|
可用、靠左时左溢出;fixed 与容器位置无关(实测两种场景都在视口内)。
|
|
273
273
|
⚠️ hash 前缀(QsffPG_ / _1nxmc_)会随 dsh 升级漂移、失效是静默的(退回溢出),
|
|
274
274
|
升级后跑 tests/live-probe.py 复核(已覆盖后台任务菜单)。 */
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
/* ⚠️ popover 的 top 必须落在**触发按钮下方**:菜单弹出后若盖住按钮本身,
|
|
276
|
+
用户再点一下(想关掉/重开)时点击落在菜单上 —— 体感就是「点了没反应」
|
|
277
|
+
(2026-09-14 用户报「三个点按钮点击没反应」:实测命中率只有 2/6,
|
|
278
|
+
菜单 [8,76,374,42] 正好压在 tabs 行的按钮 80..106 上)。
|
|
279
|
+
· QsffPG:触发按钮在 header 第二行 42..70 ⇒ top 74
|
|
280
|
+
· _1nxmc_(「更多操作」):触发按钮在 tabs 行 80..106 ⇒ top 122 */
|
|
281
|
+
" body.dsh-mobile-ui [class*=QsffPG_menu],body.dsh-mobile-ui [role=menu][class*=_list_1nxmc_]{position:fixed !important;left:8px !important;right:8px !important;width:auto !important;min-width:0 !important;max-width:none !important;max-height:calc(100vh - 150px) !important;overflow:auto !important;top:calc(var(--dsh-mobile-popover-top,74px) + env(safe-area-inset-top,0px)) !important}",
|
|
282
|
+
" body.dsh-mobile-ui [role=menu][class*=_list_1nxmc_]{--dsh-mobile-popover-top:122px}",
|
|
277
283
|
/* ── 手机端 header 瘦身 + 正文占比(2026-09-14,用户反馈「标题栏三行有点乱 / 正文显得窄」)──
|
|
278
284
|
实测 390px:header 138px(titleRow 三行)+ 输入区 272px ⇒ 正文只剩 434px(占屏 51%)。
|
|
279
285
|
三行里第三行是「终端打开 / 选择打开方式 / 更多操作 / 打开右侧边栏」——前两个点了是在
|
|
@@ -284,12 +290,19 @@ window.__ModuleLoader__.load({
|
|
|
284
290
|
上游按钮仍留在 DOM 里只是不可见,程序化 click 照常触发 React 逻辑);
|
|
285
291
|
③ tabs 行右侧本来空着 246px,顺带放「折叠输入框」开关。
|
|
286
292
|
隐藏后 titleRow 只剩 titleCluster 一行组(crumbs 行 + headerActions 行),header ≈ 105px。 */
|
|
287
|
-
/*
|
|
293
|
+
/* ⚠️⚠️ headerUtilities 用「移出文档流但保留坐标」而不是 display:none:
|
|
288
294
|
上游「更多操作」按钮的菜单位置由它自己的 getBoundingClientRect 算出,display:none 时
|
|
289
295
|
该矩形全 0 ⇒ 菜单渲染成 0×0 根本看不见(2026-09-14 实测踩到)。
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
296
|
+
⚠️⚠️ 更不能再用 opacity:0(0.2.5–0.2.7 的写法,错):**opacity 是「组不透明度」,祖先
|
|
297
|
+
为 0 会让整棵子树(含弹层)一起透明** —— 实测「更多操作」菜单就挂在这棵子树里
|
|
298
|
+
(祖先链:menu → span._root_1nxmc_ → div → headerUtilities),于是菜单**弹出来了、几何也正确**
|
|
299
|
+
(实测 rect=[8,122,396,42]、含「下载 Session 日志」),却**完全看不见**,还因继承
|
|
300
|
+
pointer-events:none 而**点不动** ⇒ 用户第三次反馈「点三个点还是没反应」的真根因
|
|
301
|
+
(2026-09-15 定案;前两轮修的把手死区/竞态都是真问题,但不是这一条)。
|
|
302
|
+
✅ 正解 = visibility:hidden:同样不占布局、getBoundingClientRect 照常可测,
|
|
303
|
+
且 **visibility 可被后代用 visible 覆盖**(opacity 不行)⇒ 弹层照常显示、照常可点。 */
|
|
304
|
+
" body.dsh-mobile-ui [class*=wSkVaW_headerUtilities]{position:absolute !important;top:74px !important;left:40px !important;visibility:hidden !important}",
|
|
305
|
+
" body.dsh-mobile-ui [class*=wSkVaW_headerUtilities] [role=menu],body.dsh-mobile-ui [class*=wSkVaW_headerUtilities] [class*=_list_1nxmc_],body.dsh-mobile-ui [class*=wSkVaW_headerUtilities] [class*=_portal_],body.dsh-mobile-ui [class*=wSkVaW_headerUtilities] [class*=QsffPG_menu]{visibility:visible !important;pointer-events:auto !important}",
|
|
293
306
|
" body.dsh-mobile-ui [class*=wSkVaW_headerCorner]{display:none !important}",
|
|
294
307
|
/* 抽屉入口(原右上角悬浮汉堡 top:48/right:12,42px)在 header 从 138 降到 107 后
|
|
295
308
|
正好压在第二行(后台任务)与 tabs 行上 —— vision 复核截图时抓到。
|
|
@@ -305,11 +318,24 @@ window.__ModuleLoader__.load({
|
|
|
305
318
|
用户可能不知道从哪恢复;提示跟随内容,长会话滚到底才看到,不打扰阅读) */
|
|
306
319
|
" body.dsh-mobile-ui.dsh-mobile-composer-hidden [class*=scrollBody]::after{content:'输入框已折叠 · 点标签行右侧 ▤ 图标展开';display:block;text-align:center;padding:14px 0 4px;font-size:11px;line-height:1.4;color:var(--dsw-alias-label-caption,rgba(255,255,255,.35))}",
|
|
307
320
|
/* tabs 行做成「标签靠左 / 工具靠右」 */
|
|
308
|
-
" body.dsh-mobile-ui [class*=wSkVaW_tabs]{align-items:center !important}",
|
|
309
|
-
" body.dsh-mobile-ui #dsh-mobile-tab-tools{margin-left:auto !important;display:inline-flex !important;align-items:center !important;gap:
|
|
310
|
-
" body.dsh-mobile-ui #dsh-mobile-tab-tools button{width:
|
|
321
|
+
" body.dsh-mobile-ui [class*=wSkVaW_tabs]{align-items:center !important;min-height:34px !important}",
|
|
322
|
+
" body.dsh-mobile-ui #dsh-mobile-tab-tools{margin-left:auto !important;display:inline-flex !important;align-items:center !important;gap:8px !important;flex:0 0 auto !important}",
|
|
323
|
+
" body.dsh-mobile-ui #dsh-mobile-tab-tools button{width:36px !important;height:34px !important;min-width:36px !important;display:inline-flex !important;align-items:center !important;justify-content:center !important;padding:0 !important;border:none !important;border-radius:8px !important;background:transparent !important;color:var(--dsw-alias-label-secondary,rgba(255,255,255,.7)) !important;cursor:pointer !important}",
|
|
311
324
|
" body.dsh-mobile-ui #dsh-mobile-tab-tools button:active{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.08)) !important}",
|
|
312
|
-
" body.dsh-mobile-ui #dsh-mobile-tab-tools svg{width:
|
|
325
|
+
" body.dsh-mobile-ui #dsh-mobile-tab-tools svg{width:18px !important;height:18px !important;fill:none !important;stroke:currentColor !important;stroke-width:1.8 !important;stroke-linecap:round !important;stroke-linejoin:round !important}",
|
|
326
|
+
/* ⚠️⚠️ 隐形死区:右侧栏 resize 把手的残留(2026-09-14 定案,用户报「三个点没反应」的真根因)
|
|
327
|
+
上游 pI_x6G_handle 是 rightbarCol 的拖拽热区:position:absolute;top:0;bottom:0;
|
|
328
|
+
width:8px;margin-left:-4px;z-index:11;pointer-events:auto,left 是**固定像素**(实测 276px)。
|
|
329
|
+
折叠右侧栏时 rightbarCol 收缩成 height:0(实测 rect=[0,915,412,0]),
|
|
330
|
+
**但把手不跟着消失**,仍以 8px 宽、**贯穿整个视口高度**(0→915)钉在 x=276..284。
|
|
331
|
+
后果:那一整列 8px 宽的 tap 全被吃掉(手机上没有 col-resize,用户只会觉得「点了没反应」),
|
|
332
|
+
正文区的按钮/输入框也一并被挡。实测(同一份 client.js,仅视口不同):
|
|
333
|
+
412px:⋯ 按钮 260..296 的中心 278 落在 276..284 内 ⇒ elementFromPoint=handle,tap 开合命中 0/5
|
|
334
|
+
390/435/460px:按钮恰好错开这条带 ⇒ 5/5(所以这个 bug「挑宽度」,极难靠换机型复现)
|
|
335
|
+
移动端不需要 col-resize ⇒ 直接移除;同时给工具组抬 z-index 做第二道保险
|
|
336
|
+
(把手 z-index:11,工具组 z-index:30,即便将来又冒出别的覆盖层也压不住按钮)。 */
|
|
337
|
+
" body.dsh-mobile-ui [class*=pI_x6G_handle]{display:none !important}",
|
|
338
|
+
" body.dsh-mobile-ui #dsh-mobile-tab-tools{position:relative !important;z-index:30 !important}",
|
|
313
339
|
/* ── 折叠输入区(阅读模式):省下 composerSeat 的 ~272px,全部让给正文 ──
|
|
314
340
|
触发方式:tabs 行的键盘按钮(状态存 localStorage,刷新后保持)。
|
|
315
341
|
⚠️ 提问卡片(ask_user_question)就渲染在 composerSeat 里 ⇒ 折叠状态下若检测到卡片,
|
|
@@ -558,10 +584,22 @@ window.__ModuleLoader__.load({
|
|
|
558
584
|
more.id = "dsh-mobile-more-proxy";
|
|
559
585
|
more.setAttribute("aria-label", "更多操作");
|
|
560
586
|
more.innerHTML = "<svg viewBox='0 0 24 24'><circle cx='5' cy='12' r='1.7'/><circle cx='12' cy='12' r='1.7'/><circle cx='19' cy='12' r='1.7'/></svg>";
|
|
587
|
+
/* ⚠️ 竞态:上游的「点外面就关」监听的是 **pointerdown**(document 冒泡阶段),
|
|
588
|
+
而我们的转发在 **click**(按钮上)。如果菜单本来开着,一次 tap 会:
|
|
589
|
+
pointerdown → 上游关掉菜单 → click → 我们又叫开
|
|
590
|
+
净效果「点了没反应」(2026-09-14 用户报「三个点按钮点击没反应」,
|
|
591
|
+
实测命中率 2/6)。修法:在按钮上的 pointerdown(比 document 早)记下当前是否开着,
|
|
592
|
+
开着就什么都不做 —— 交给上游关掉,真正的 toggle。 */
|
|
593
|
+
more.addEventListener("pointerdown", function () {
|
|
594
|
+
try {
|
|
595
|
+
more.__menuWasOpen = !!document.querySelector("[role=menu][class*=_list_1nxmc_]");
|
|
596
|
+
} catch (err) { more.__menuWasOpen = false; }
|
|
597
|
+
}, true);
|
|
561
598
|
more.addEventListener("click", function (e) {
|
|
562
599
|
try {
|
|
563
600
|
e.preventDefault();
|
|
564
601
|
e.stopPropagation();
|
|
602
|
+
if (more.__menuWasOpen) { more.__menuWasOpen = false; return; }
|
|
565
603
|
var up = document.querySelector("button[class*=nL4_yW_moreButton]");
|
|
566
604
|
if (up) up.click();
|
|
567
605
|
} catch (err) { /* 静默 */ }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mobile-ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Mobile UI for the DeepSeek Harness Web GUI: responsive layout, overlay session drawer, 44px touch targets, safe-area support, reading enhancements (≤768px, zero desktop impact)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/tests/live-probe.py
CHANGED
|
@@ -47,9 +47,20 @@ POPOVER = """(sel) => { const m = document.querySelector(sel); if (!m) return nu
|
|
|
47
47
|
const r = m.getBoundingClientRect();
|
|
48
48
|
const items = m.querySelectorAll('[role=menuitem],[class*=row],[class*=item_]').length;
|
|
49
49
|
const mid = document.elementFromPoint(r.right - 40, r.top + 16);
|
|
50
|
+
// ⚠️ 几何正常 ≠ 人能看见/能点:必须同时查「中心命中自身」+「祖先链上的有效可见性」
|
|
51
|
+
// (2026-09-15 教训:菜单 rect 完全正确、条目也在,但祖先 opacity:0 把它整棵子树变透明,
|
|
52
|
+
// 只断言 left/right/items 的旧版探针一路放行 ⇒ 用户连报三轮「没反应」)
|
|
53
|
+
const hit = document.elementFromPoint(r.left + r.width / 2, r.top + Math.min(20, r.height / 2));
|
|
54
|
+
// ⚠️ visibility 只看**自身计算值**:祖先 visibility:hidden 会被后代的 visible 覆盖(这正是本插件的修法),
|
|
55
|
+
// 扫祖先链会把「已经修好」判成 hidden(2026-09-15 自测踩到)。opacity 相反 —— 组不透明度**必须逐级相乘**。
|
|
56
|
+
const vis = getComputedStyle(m).visibility; let op = 1, n = m;
|
|
57
|
+
while (n && n !== document.body) { op *= parseFloat(getComputedStyle(n).opacity || '1'); n = n.parentElement; }
|
|
50
58
|
return { left: Math.round(r.left), right: Math.round(r.right), h: Math.round(r.height), items: items,
|
|
51
59
|
ok: r.left >= -0.5 && r.right <= window.innerWidth + 0.5,
|
|
52
60
|
topRightHit: mid ? String(mid.className || '').split(' ').pop().slice(0, 22) : null,
|
|
61
|
+
visibility: vis, effectiveOpacity: +op.toFixed(2), pointerEvents: getComputedStyle(m).pointerEvents,
|
|
62
|
+
hitSelf: !!(hit && (hit === m || m.contains(hit))),
|
|
63
|
+
hitTop: hit ? String(hit.id || hit.className || hit.tagName).split(' ').filter(Boolean).pop().slice(0, 22) : null,
|
|
53
64
|
hamburger: (document.getElementById('dsh-mobile-menu-btn') || {}).style ? document.getElementById('dsh-mobile-menu-btn').style.display : '?' }; }"""
|
|
54
65
|
|
|
55
66
|
DRAWER = """() => { const s = document.querySelector('[class*=sidebarCol]');
|
|
@@ -171,6 +182,16 @@ def run(args) -> int:
|
|
|
171
182
|
fails.append(f"{name}跑出视口({g})—— 上游对齐按桌面宽度算,插件的浮层约束失效?")
|
|
172
183
|
elif g.get("items", 0) < 1:
|
|
173
184
|
fails.append(f"{name}打开后没有任何条目({g})")
|
|
185
|
+
# ⚠️⚠️ 光有几何和条目不算数:必须真能看见、真能点到
|
|
186
|
+
# (2026-09-15 血泪:菜单 rect 全对、条目也在,但祖先 wSkVaW_headerUtilities 的
|
|
187
|
+
# opacity:0 把整棵子树变透明 + pointer-events:none 被继承 ⇒ 用户「点了没反应」,
|
|
188
|
+
# 而只断言 left/right/items 的旧版探针一路绿灯)
|
|
189
|
+
elif g.get("visibility") == "hidden" or g.get("effectiveOpacity", 1) < 0.9 \
|
|
190
|
+
or g.get("pointerEvents") == "none" or not g.get("hitSelf"):
|
|
191
|
+
fails.append(
|
|
192
|
+
f"{name}看得见却点不到(visibility={g.get('visibility')} "
|
|
193
|
+
f"有效opacity={g.get('effectiveOpacity')} pointer-events={g.get('pointerEvents')} "
|
|
194
|
+
f"中心命中={g.get('hitTop')})—— 祖先 opacity:0 的组透明 / pointer-events 继承泄漏?")
|
|
174
195
|
page.keyboard.press("Escape")
|
|
175
196
|
page.wait_for_timeout(800)
|
|
176
197
|
|
|
@@ -214,6 +235,79 @@ def run(args) -> int:
|
|
|
214
235
|
if back:
|
|
215
236
|
fails.append("再次点击没能恢复输入区")
|
|
216
237
|
|
|
238
|
+
# ③d 工具按钮必须真的能用:⋯ 代理按钮开→关一次就翻状态
|
|
239
|
+
# (曾因「上游 pointerdown 先关 / 我们的 click 又开」的竞态,命中率只有 2/6 ——
|
|
240
|
+
# 用户报「三个点按钮点击没反应」;折叠按钮也曾因 30×26 目标过小被报「不灵敏」)
|
|
241
|
+
menus_js = "() => document.querySelectorAll('[role=menu][class*=_list_1nxmc_]').length"
|
|
242
|
+
mp = page.evaluate("""() => { const b = document.getElementById('dsh-mobile-more-proxy'); if (!b) return null;
|
|
243
|
+
const r = b.getBoundingClientRect(); return [r.x + r.width / 2, r.y + r.height / 2]; }""")
|
|
244
|
+
if mp:
|
|
245
|
+
seq = []
|
|
246
|
+
for _ in range(2):
|
|
247
|
+
page.touchscreen.tap(mp[0], mp[1])
|
|
248
|
+
page.wait_for_timeout(1000)
|
|
249
|
+
seq.append(page.evaluate(menus_js))
|
|
250
|
+
print(f" [⋯ 代理按钮] 开合序列={seq}(期望 [1,0])")
|
|
251
|
+
if seq != [1, 0]:
|
|
252
|
+
fails.append(f"「更多操作」代理按钮不能开合:{seq}(上游 pointerdown 与转发 click 的竞态又回来了?)")
|
|
253
|
+
# 按钮尺寸:触摸目标不得小于 32px(插件规范 44,实测 30×26 时用户报"不灵敏")
|
|
254
|
+
size = page.evaluate("""() => { const b = document.getElementById('dsh-mobile-more-proxy');
|
|
255
|
+
if (!b) return null; const r = b.getBoundingClientRect(); return [Math.round(r.width), Math.round(r.height)]; }""")
|
|
256
|
+
if size and (size[0] < 32 or size[1] < 30):
|
|
257
|
+
fails.append(f"工具按钮触摸目标过小:{size}")
|
|
258
|
+
|
|
259
|
+
# ③e ⚠️⚠️ 宽度相关的「隐形死区」回归(2026-09-14 用户第三次报「三个点没反应」的真根因)
|
|
260
|
+
# 上游右侧栏 resize 把手 pI_x6G_handle 是 absolute;top:0;bottom:0;width:8px;
|
|
261
|
+
# z-index:11;pointer-events:auto —— 右侧栏折叠后 rightbarCol 缩成 height:0,
|
|
262
|
+
# 但把手**不跟着消失**,仍以 8px 宽、贯穿整个视口高度钉在固定 x(实测 276..284)。
|
|
263
|
+
# 工具组是右对齐的(⋯ 中心 = 视口宽 - 134)⇒ 它只在 **410–417px** 这段视口里
|
|
264
|
+
# 正好压住 ⋯ 的中心:「390px 测着全好、用户手机却点不动」就是这么来的。
|
|
265
|
+
# ⇒ 断言**必须扫一批宽度**,单宽度探针天然漏检;同时确认把手已不可命中
|
|
266
|
+
# (插件 0.2.7 起 display:none,另给工具组 position:relative;z-index:30 做第二道保险)。
|
|
267
|
+
HITTEST = """() => {
|
|
268
|
+
const h = document.querySelector('[class*=pI_x6G_handle]');
|
|
269
|
+
const hs = h ? getComputedStyle(h) : null;
|
|
270
|
+
return { handle: h ? { display: hs.display, pe: hs.pointerEvents } : null,
|
|
271
|
+
buttons: [...document.querySelectorAll('#dsh-mobile-tab-tools button')].map(b => {
|
|
272
|
+
const r = b.getBoundingClientRect();
|
|
273
|
+
const t = document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2);
|
|
274
|
+
return { id: b.id, self: !!(t && (t === b || b.contains(t))),
|
|
275
|
+
top: t ? (t.id || String(t.className || '').split(' ').filter(Boolean).pop() || t.tagName) : null }; }) }; }"""
|
|
276
|
+
dead, covered, probe412 = [], [], None
|
|
277
|
+
for vw in (320, 360, 375, 390, 400, 410, 412, 415, 418, 428, 460, 768):
|
|
278
|
+
page.set_viewport_size({"width": vw, "height": h})
|
|
279
|
+
page.wait_for_timeout(320)
|
|
280
|
+
r = page.evaluate(HITTEST)
|
|
281
|
+
if r["handle"] and r["handle"]["display"] != "none":
|
|
282
|
+
dead.append(vw)
|
|
283
|
+
miss = [f"{x['id']}←{x['top']}" for x in r["buttons"] if not x["self"]]
|
|
284
|
+
if miss:
|
|
285
|
+
covered.append(f"{vw}px:{','.join(miss)}")
|
|
286
|
+
if vw == 412: # 用户实测踩中的那一段宽度,做端到端点击验证
|
|
287
|
+
c412 = page.evaluate("""() => { const b = document.getElementById('dsh-mobile-more-proxy');
|
|
288
|
+
if (!b) return null; const r = b.getBoundingClientRect();
|
|
289
|
+
return [r.x + r.width / 2, r.y + r.height / 2]; }""")
|
|
290
|
+
seq2 = []
|
|
291
|
+
if c412:
|
|
292
|
+
for _ in range(2):
|
|
293
|
+
page.touchscreen.tap(c412[0], c412[1])
|
|
294
|
+
page.wait_for_timeout(900)
|
|
295
|
+
seq2.append(page.evaluate(menus_js))
|
|
296
|
+
if seq2[1]: # 别把菜单留着干扰后续断言
|
|
297
|
+
page.touchscreen.tap(c412[0], c412[1])
|
|
298
|
+
page.wait_for_timeout(700)
|
|
299
|
+
probe412 = seq2
|
|
300
|
+
page.set_viewport_size({"width": w, "height": h})
|
|
301
|
+
page.wait_for_timeout(500)
|
|
302
|
+
print(f" [宽度扫描 12 档] 把手残留={dead or '无'} 按钮被覆盖={covered or '无'} 412px ⋯开合={probe412}(期望 [1,0])")
|
|
303
|
+
if dead:
|
|
304
|
+
fails.append(f"右侧栏拖拽把手又在这些宽度残留成隐形死区:{dead}px"
|
|
305
|
+
f"(pI_x6G_handle 覆盖整列 ⇒ 该列 tap 全被吃掉)")
|
|
306
|
+
if covered:
|
|
307
|
+
fails.append(f"tabs 行工具按钮被别的层盖住(点不到):{covered}")
|
|
308
|
+
if probe412 is not None and probe412 != [1, 0]:
|
|
309
|
+
fails.append(f"412px(用户手机宽度)下 ⋯ 按钮仍不能开合:{probe412}")
|
|
310
|
+
|
|
217
311
|
# ④ 菜单行可进入子代理(面包屑应变成两段)
|
|
218
312
|
page.touchscreen.tap(sw["x"], sw["y"])
|
|
219
313
|
page.wait_for_timeout(800)
|