dsh-mobile-ui 0.2.8 → 0.2.9
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 +90 -6
- package/package.json +1 -1
- package/tests/dump-header.py +120 -0
- package/tests/dump-menu.py +96 -0
- package/tests/dump-rightbar.py +95 -0
- package/tests/live-probe.py +345 -164
- package/tests/probe-actions.py +176 -0
- package/tests/probe-pc-rightbar.py +105 -0
- package/tests/probe-rightbar.py +164 -0
- package/tests/try-menu-fix.py +125 -0
- package/tests/try-rightbar.py +132 -0
- package/tests/width-scan.py +179 -0
package/client.js
CHANGED
|
@@ -271,19 +271,25 @@ window.__ModuleLoader__.load({
|
|
|
271
271
|
而容器在 header 里的横坐标随「有无子代理/后台任务」变化 —— 候选方案 right:0 在容器靠右时
|
|
272
272
|
可用、靠左时左溢出;fixed 与容器位置无关(实测两种场景都在视口内)。
|
|
273
273
|
⚠️ hash 前缀(QsffPG_ / _1nxmc_)会随 dsh 升级漂移、失效是静默的(退回溢出),
|
|
274
|
-
升级后跑 tests/live-probe.py 复核(已覆盖后台任务菜单)。
|
|
274
|
+
升级后跑 tests/live-probe.py 复核(已覆盖后台任务菜单)。
|
|
275
|
+
🔴 2026-09-28 实证(dsh 0.1.7):_list_1nxmc_ → _list_gzo7u_,且上游改用
|
|
276
|
+
`--dsh-menu-anchor` CSS 变量定位 ⇒ 旧选择器整条失配、菜单左溢 61px
|
|
277
|
+
(用户报「三个点显示不全」)。已改用**语义锚点**
|
|
278
|
+
`[class*=headerUtilities] [role=menu]` —— 只依赖 headerUtilities 这个语义后缀、
|
|
279
|
+
不含 hash 前缀;0.1.5 与 0.1.7 双版本实测均生效
|
|
280
|
+
(对比脚本 tests/try-menu-fix.py:注入前后 rect 从 [-61,…] 回到 [8,…])。 */
|
|
275
281
|
/* ⚠️ popover 的 top 必须落在**触发按钮下方**:菜单弹出后若盖住按钮本身,
|
|
276
282
|
用户再点一下(想关掉/重开)时点击落在菜单上 —— 体感就是「点了没反应」
|
|
277
283
|
(2026-09-14 用户报「三个点按钮点击没反应」:实测命中率只有 2/6,
|
|
278
284
|
菜单 [8,76,374,42] 正好压在 tabs 行的按钮 80..106 上)。
|
|
279
285
|
· QsffPG:触发按钮在 header 第二行 42..70 ⇒ top 74
|
|
280
286
|
· _1nxmc_(「更多操作」):触发按钮在 tabs 行 80..106 ⇒ top 122 */
|
|
281
|
-
" body.dsh-mobile-ui [class*=QsffPG_menu],body.dsh-mobile-ui [role=menu]
|
|
282
|
-
" body.dsh-mobile-ui [role=menu]
|
|
287
|
+
" body.dsh-mobile-ui [class*=QsffPG_menu],body.dsh-mobile-ui [class*=headerUtilities] [role=menu]{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}",
|
|
288
|
+
" body.dsh-mobile-ui [class*=headerUtilities] [role=menu]{--dsh-mobile-popover-top:122px}",
|
|
283
289
|
/* ── 手机端 header 瘦身 + 正文占比(2026-09-14,用户反馈「标题栏三行有点乱 / 正文显得窄」)──
|
|
284
290
|
实测 390px:header 138px(titleRow 三行)+ 输入区 272px ⇒ 正文只剩 434px(占屏 51%)。
|
|
285
291
|
三行里第三行是「终端打开 / 选择打开方式 / 更多操作 / 打开右侧边栏」——前两个点了是在
|
|
286
|
-
|
|
292
|
+
**服务器上**开终端(手机上什么也看不到)、右侧边栏自 2026-09-28 起改由工具组 ▥ 以浮层打开(此前被本插件隐藏、点了空白),
|
|
287
293
|
只有「更多操作」(下载 Session 日志)有用。处理:
|
|
288
294
|
① 整行隐藏(行空掉后 header 少 32px);
|
|
289
295
|
② 「更多操作」改由**插件注入到 tabs 行空白处的按钮代理**(点击转发给上游按钮,
|
|
@@ -342,6 +348,12 @@ window.__ModuleLoader__.load({
|
|
|
342
348
|
JS 会强制展开(见 sync 里的 qaOpen 分支),否则用户看不到问题。 */
|
|
343
349
|
" body.dsh-mobile-ui.dsh-mobile-composer-hidden [class*=composerSeat],body.dsh-mobile-ui.dsh-mobile-composer-hidden [class*=composerStack],body.dsh-mobile-ui.dsh-mobile-composer-hidden [class*=uV2eYG_root]{display:none !important}",
|
|
344
350
|
" body.dsh-mobile-ui #dsh-mobile-tab-tools button.dsh-mobile-on{background:var(--dsw-specific-selector,rgba(255,255,255,.12)) !important;color:var(--dsw-alias-label-primary,rgba(255,255,255,.9)) !important}",
|
|
351
|
+
/* ── 右侧边栏(工作区文件/终端)以浮层呈现(2026-09-28 新增,用户要「打开工作区文件的按钮」)──
|
|
352
|
+
上游的「打开右侧边栏」按钮与 rightbarCol 都在,但本插件为拿正文宽度把 frame 改成单列
|
|
353
|
+
(minmax(0,1fr))⇒ rightbarCol 落到第二行且 height:0(实测 rect=[0,839,412,0]),
|
|
354
|
+
点了也是空白。故给入口按钮 + 这条规则:打开时把 rightbarCol 提成覆盖整屏的浮层
|
|
355
|
+
(不挤压正文),关闭仍走上游自带的「收起右侧边栏」(面板右上角,实测 rect=[378,10,28,28])。 */
|
|
356
|
+
" body.dsh-mobile-ui.dsh-mobile-rightbar [class*=rightbarCol]{position:fixed !important;inset:0 !important;width:auto !important;height:auto !important;z-index:2147482950 !important;display:block !important;background:var(--dsw-alias-bg-layer-1,#16171b) !important}",
|
|
345
357
|
/* 输入框聚焦反馈 */
|
|
346
358
|
" body.dsh-mobile-ui textarea:focus,body.dsh-mobile-ui [class*=input]:focus-within{outline:none;box-shadow:0 0 0 2px var(--dsw-alias-brand-primary,rgba(79,124,255,.45))}",
|
|
347
359
|
/* 消息流操作按钮(复制/反馈/分享)触摸目标提升。
|
|
@@ -592,7 +604,7 @@ window.__ModuleLoader__.load({
|
|
|
592
604
|
开着就什么都不做 —— 交给上游关掉,真正的 toggle。 */
|
|
593
605
|
more.addEventListener("pointerdown", function () {
|
|
594
606
|
try {
|
|
595
|
-
more.__menuWasOpen = !!document.querySelector("[role=menu]
|
|
607
|
+
more.__menuWasOpen = !!document.querySelector("[class*=headerUtilities] [role=menu]");
|
|
596
608
|
} catch (err) { more.__menuWasOpen = false; }
|
|
597
609
|
}, true);
|
|
598
610
|
more.addEventListener("click", function (e) {
|
|
@@ -630,8 +642,22 @@ window.__ModuleLoader__.load({
|
|
|
630
642
|
openDrawer();
|
|
631
643
|
} catch (err) { /* 静默 */ }
|
|
632
644
|
});
|
|
645
|
+
/* 右侧边栏(工作区文件/终端)入口 —— 见 CSS 段说明 */
|
|
646
|
+
var panelBtn = document.createElement("button");
|
|
647
|
+
panelBtn.type = "button";
|
|
648
|
+
panelBtn.id = "dsh-mobile-rightbar-toggle";
|
|
649
|
+
panelBtn.setAttribute("aria-label", "工作区文件");
|
|
650
|
+
panelBtn.innerHTML = "<svg viewBox='0 0 24 24'><rect x='3' y='4' width='18' height='16' rx='2'/><path d='M14 4v16'/></svg>";
|
|
651
|
+
panelBtn.addEventListener("click", function (e) {
|
|
652
|
+
try {
|
|
653
|
+
e.preventDefault();
|
|
654
|
+
e.stopPropagation();
|
|
655
|
+
toggleRightbar();
|
|
656
|
+
} catch (err) { /* 静默 */ }
|
|
657
|
+
});
|
|
633
658
|
box.appendChild(more);
|
|
634
659
|
box.appendChild(fold);
|
|
660
|
+
box.appendChild(panelBtn);
|
|
635
661
|
box.appendChild(menuBtn);
|
|
636
662
|
tabs.appendChild(box);
|
|
637
663
|
}
|
|
@@ -675,6 +701,62 @@ window.__ModuleLoader__.load({
|
|
|
675
701
|
if (tabbar) tabbar.style.display = "";
|
|
676
702
|
}
|
|
677
703
|
|
|
704
|
+
/* ── 右侧边栏(工作区文件/终端)开关(2026-09-28 新增)──────────────────────────
|
|
705
|
+
用户要「打开工作区文件的按钮」。上游按钮与面板都在,但本插件为拿正文宽度把 frame 改成单列
|
|
706
|
+
⇒ rightbarCol 落到第二行且 height:0(实测 rect=[0,839,412,0]),点了也是空白。
|
|
707
|
+
做法:代理上游「打开右侧边栏」→ 给 body 加 class → CSS 把 rightbarCol 提成覆盖整屏的浮层
|
|
708
|
+
(不挤压正文);关闭走上游自带的「收起右侧边栏」(面板右上角)。
|
|
709
|
+
⚠️ 上游按钮 label 随状态变(打开 ⇄ 收起),且 hash 类名会漂移 ⇒ 一律按 aria-label 精确选。 */
|
|
710
|
+
var rightbarOpenedAt = 0; /* 打开时刻,用于避开 React 尚未更新 label 的窗口 */
|
|
711
|
+
function rightbarOpen() {
|
|
712
|
+
try { return document.body.classList.contains("dsh-mobile-rightbar"); } catch (e) { return false; }
|
|
713
|
+
}
|
|
714
|
+
function syncRightbarBtn() {
|
|
715
|
+
try {
|
|
716
|
+
/* 双向同步(2026-09-28 实测补):用户可能直接点面板内上游自带的
|
|
717
|
+
「收起右侧边栏」关闭 —— 那时我们的浮层 class 必须跟着清掉,否则
|
|
718
|
+
class 残留会让面板继续以 fixed 全屏显示(表现为「关不掉」)。
|
|
719
|
+
判据用上游按钮 label:面板开着时是「收起右侧边栏」、收起后回到
|
|
720
|
+
「打开右侧边栏」。⚠️ 刚打开 1.2s 内不判定(React 还没换 label,会误清)。 */
|
|
721
|
+
if (mq.matches && rightbarOpen() && Date.now() - rightbarOpenedAt > 1200
|
|
722
|
+
&& document.querySelector('button[aria-label="打开右侧边栏"]')) {
|
|
723
|
+
document.body.classList.remove("dsh-mobile-rightbar");
|
|
724
|
+
}
|
|
725
|
+
var b = document.getElementById("dsh-mobile-rightbar-toggle");
|
|
726
|
+
if (b) b.classList.toggle("dsh-mobile-on", rightbarOpen());
|
|
727
|
+
} catch (e) { /* 静默 */ }
|
|
728
|
+
}
|
|
729
|
+
function openRightbar() {
|
|
730
|
+
/* 浮层只是**移动端**的替代方案:PC 用上游原生三列布局,插件完全不介入 */
|
|
731
|
+
if (!mq.matches) return;
|
|
732
|
+
try {
|
|
733
|
+
var up = document.querySelector('button[aria-label="打开右侧边栏"]');
|
|
734
|
+
if (up) up.click();
|
|
735
|
+
document.body.classList.add("dsh-mobile-rightbar");
|
|
736
|
+
rightbarOpenedAt = Date.now();
|
|
737
|
+
/* 面板在被 display 期间没算过尺寸 ⇒ 触发一次 resize 让它重新布局 */
|
|
738
|
+
setTimeout(function () { try { window.dispatchEvent(new Event("resize")); } catch (e) {} }, 350);
|
|
739
|
+
} catch (e) { /* 静默 */ }
|
|
740
|
+
syncRightbarBtn();
|
|
741
|
+
}
|
|
742
|
+
function closeRightbar() {
|
|
743
|
+
/* ⚠️ 非移动端绝不能点上游按钮(PC 的右侧栏是上游默认可见状态)——只清自己的 class */
|
|
744
|
+
if (!mq.matches) { resetRightbarOverlay(); return; }
|
|
745
|
+
try {
|
|
746
|
+
var up = document.querySelector('button[aria-label="收起右侧边栏"]');
|
|
747
|
+
if (up) up.click();
|
|
748
|
+
document.body.classList.remove("dsh-mobile-rightbar");
|
|
749
|
+
} catch (e) { /* 静默 */ }
|
|
750
|
+
syncRightbarBtn();
|
|
751
|
+
}
|
|
752
|
+
/* 只清插件自己的浮层状态,**绝不触碰上游按钮** —— 用于「非移动端」与「离开移动端」。
|
|
753
|
+
⚠️ 2026-09-28 事故:非移动端分支原先误调 closeRightbar(),而它会在 PC 上点上游
|
|
754
|
+
「收起右侧边栏」⇒ 每次 sync(MutationObserver 触发)都把用户本来开着的右侧栏收起,
|
|
755
|
+
表现为「PC 端不停打开/关闭右侧栏」。 */
|
|
756
|
+
function resetRightbarOverlay() {
|
|
757
|
+
try { document.body.classList.remove("dsh-mobile-rightbar"); } catch (e) { /* 静默 */ }
|
|
758
|
+
}
|
|
759
|
+
function toggleRightbar() { if (rightbarOpen()) closeRightbar(); else openRightbar(); }
|
|
678
760
|
function sync() {
|
|
679
761
|
try {
|
|
680
762
|
if (mq.matches) {
|
|
@@ -734,13 +816,14 @@ window.__ModuleLoader__.load({
|
|
|
734
816
|
top:42 全宽后,右上角的汉堡按钮正好压在菜单首行右侧 —— vision 复核截图时发现
|
|
735
817
|
「第一行的耗时被按钮遮住」,故菜单开着时也隐藏它) */
|
|
736
818
|
var popOpen = !!(document.querySelector(
|
|
737
|
-
"[class*=QsffPG_menu],[role=menu]
|
|
819
|
+
"[class*=QsffPG_menu],[class*=headerUtilities] [role=menu]"));
|
|
738
820
|
if (tabbar) {
|
|
739
821
|
tabbar.style.display = (drawerOpen || settingsOpen || qaOpen || popOpen)
|
|
740
822
|
? "none" : "";
|
|
741
823
|
}
|
|
742
824
|
/* tabs 行工具按钮(更多操作代理 + 折叠输入区)—— React 重渲染后重建 */
|
|
743
825
|
ensureTabTools();
|
|
826
|
+
syncRightbarBtn();
|
|
744
827
|
} else {
|
|
745
828
|
document.body.classList.remove("dsh-mobile-ui");
|
|
746
829
|
qaOpen = false;
|
|
@@ -752,6 +835,7 @@ window.__ModuleLoader__.load({
|
|
|
752
835
|
var side2 = getSidebar();
|
|
753
836
|
if (side2) side2.style.display = "";
|
|
754
837
|
closeDrawer();
|
|
838
|
+
resetRightbarOverlay(); /* PC 端只清插件状态,绝不碰上游右侧栏 */
|
|
755
839
|
if (tabbar) tabbar.style.display = "none";
|
|
756
840
|
}
|
|
757
841
|
} catch (e) { /* 同步失败静默 */ }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mobile-ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
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",
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""打开一个真实会话,dump 会话头部(header)里所有按钮的几何/可见性,定位 ⋯ 与 ☰ 的问题。
|
|
3
|
+
|
|
4
|
+
用法: python3 dump-header.py <base-url> <token-file> [--title 片段] [--width 390]
|
|
5
|
+
"""
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from playwright.sync_api import sync_playwright
|
|
9
|
+
|
|
10
|
+
BASE = sys.argv[1]
|
|
11
|
+
TOKF = sys.argv[2]
|
|
12
|
+
TITLE = sys.argv[sys.argv.index("--title") + 1] if "--title" in sys.argv else None
|
|
13
|
+
WIDTH = int(sys.argv[sys.argv.index("--width") + 1]) if "--width" in sys.argv else 390
|
|
14
|
+
TAG = sys.argv[sys.argv.index("--tag") + 1] if "--tag" in sys.argv else "dump"
|
|
15
|
+
TOKEN = open(TOKF, encoding="utf-8").read().strip()
|
|
16
|
+
|
|
17
|
+
DUMP_JS = """
|
|
18
|
+
() => {
|
|
19
|
+
const out = [];
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
document.querySelectorAll("button, [role=button], a[class*=btn]").forEach((el) => {
|
|
22
|
+
const r = el.getBoundingClientRect();
|
|
23
|
+
if (r.width === 0 && r.height === 0) return;
|
|
24
|
+
if (r.top < -5 || r.top > 260) return; // 只看贴顶的头部区域(排除滚出屏的内容)
|
|
25
|
+
if (r.left < -5 || r.left > window.innerWidth + 5) return;
|
|
26
|
+
const cs = getComputedStyle(el);
|
|
27
|
+
let op = 1, vis = "visible", pe = "auto", node = el;
|
|
28
|
+
while (node && node !== document.documentElement) {
|
|
29
|
+
const s = getComputedStyle(node);
|
|
30
|
+
op *= parseFloat(s.opacity || "1");
|
|
31
|
+
if (s.visibility === "hidden") vis = "hidden";
|
|
32
|
+
if (s.pointerEvents === "none") pe = "none";
|
|
33
|
+
node = node.parentElement;
|
|
34
|
+
}
|
|
35
|
+
const key = (el.id || "") + "|" + (el.className || "").toString().slice(0, 40) + "|" + Math.round(r.left) + "," + Math.round(r.top);
|
|
36
|
+
if (seen.has(key)) return;
|
|
37
|
+
seen.add(key);
|
|
38
|
+
out.push({
|
|
39
|
+
id: el.id || null,
|
|
40
|
+
cls: (el.className || "").toString().slice(0, 70),
|
|
41
|
+
label: el.getAttribute("aria-label") || (el.innerText || "").trim().slice(0, 14) || null,
|
|
42
|
+
rect: [Math.round(r.left), Math.round(r.top), Math.round(r.width), Math.round(r.height)],
|
|
43
|
+
op: +op.toFixed(2), vis, pe,
|
|
44
|
+
clipped: el.scrollWidth > el.clientWidth + 1 || el.scrollHeight > el.clientHeight + 1,
|
|
45
|
+
scrollW: el.scrollWidth, clientW: el.clientWidth,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
const tabs = document.querySelector('[class*=tabs]');
|
|
49
|
+
const header = document.querySelector('[class*=titleRow]') || document.querySelector('[class*=wSkVaW_header]');
|
|
50
|
+
return {
|
|
51
|
+
buttons: out,
|
|
52
|
+
tabsFound: !!tabs,
|
|
53
|
+
tabsCls: tabs ? tabs.className.toString().slice(0, 90) : null,
|
|
54
|
+
headerFound: !!header,
|
|
55
|
+
headerCls: header ? header.className.toString().slice(0, 90) : null,
|
|
56
|
+
headerRect: header ? (() => { const r = header.getBoundingClientRect(); return [Math.round(r.left), Math.round(r.top), Math.round(r.width), Math.round(r.height)]; })() : null,
|
|
57
|
+
toolGroup: !!document.querySelector("#dsh-mobile-tab-tools"),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
with sync_playwright() as p:
|
|
63
|
+
b = p.chromium.launch()
|
|
64
|
+
ctx = b.new_context(viewport={"width": WIDTH, "height": 900}, is_mobile=True, has_touch=True, device_scale_factor=2)
|
|
65
|
+
page = ctx.new_page()
|
|
66
|
+
page.goto(f"{BASE}/?token={TOKEN}", wait_until="domcontentloaded", timeout=60000)
|
|
67
|
+
page.wait_for_timeout(6000)
|
|
68
|
+
print(f"base={BASE} width={WIDTH}")
|
|
69
|
+
|
|
70
|
+
# 打开抽屉 → 选一个真实会话(跳过空的新会话)
|
|
71
|
+
page.click("#dsh-mobile-menu-btn", timeout=8000)
|
|
72
|
+
page.wait_for_timeout(1500)
|
|
73
|
+
rows = page.query_selector_all("[role=treeitem][class*=sessionRow]") or page.query_selector_all("[role=treeitem]")
|
|
74
|
+
picked, title = None, None
|
|
75
|
+
for row in rows[:12]:
|
|
76
|
+
txt = (row.inner_text() or "").strip().replace("\n", " ")
|
|
77
|
+
if TITLE:
|
|
78
|
+
if TITLE in txt:
|
|
79
|
+
picked, title = row, txt[:34]
|
|
80
|
+
break
|
|
81
|
+
elif "新会话" not in txt and txt:
|
|
82
|
+
picked, title = row, txt[:34]
|
|
83
|
+
break
|
|
84
|
+
if picked is None:
|
|
85
|
+
print("找不到目标会话"); sys.exit(2)
|
|
86
|
+
picked.click()
|
|
87
|
+
page.wait_for_timeout(1500)
|
|
88
|
+
scrim = page.query_selector("[class*=scrim]")
|
|
89
|
+
if scrim:
|
|
90
|
+
try:
|
|
91
|
+
scrim.click(timeout=3000)
|
|
92
|
+
except Exception:
|
|
93
|
+
page.keyboard.press("Escape")
|
|
94
|
+
# 等会话内容 + 插件 sync(文档:历史可能很大,改用轮询)
|
|
95
|
+
for i in range(30):
|
|
96
|
+
page.wait_for_timeout(1000)
|
|
97
|
+
if page.evaluate("() => !!document.querySelector('#dsh-mobile-tab-tools') || !!document.querySelector('[class*=tabs]')"):
|
|
98
|
+
break
|
|
99
|
+
page.wait_for_timeout(2500)
|
|
100
|
+
print(f"会话:{title}\n")
|
|
101
|
+
d = page.evaluate(DUMP_JS)
|
|
102
|
+
print(f"tabs 行: {d['tabsFound']} cls={d['tabsCls']}")
|
|
103
|
+
print(f"header : {d['headerFound']} cls={d['headerCls']} rect={d['headerRect']}")
|
|
104
|
+
print(f"插件工具组 #dsh-mobile-tab-tools: {d['toolGroup']}")
|
|
105
|
+
print("\n--- 头部区域按钮 ---")
|
|
106
|
+
for x in d["buttons"]:
|
|
107
|
+
flags = []
|
|
108
|
+
if x["op"] < 0.9:
|
|
109
|
+
flags.append(f"op={x['op']}")
|
|
110
|
+
if x["vis"] != "visible":
|
|
111
|
+
flags.append("visibility:hidden")
|
|
112
|
+
if x["pe"] == "none":
|
|
113
|
+
flags.append("pointer-events:none")
|
|
114
|
+
if x["clipped"]:
|
|
115
|
+
flags.append(f"内容溢出 scrollW={x['scrollW']}>clientW={x['clientW']}")
|
|
116
|
+
print(f" id={x['id']} cls={x['cls'][:46]} label={x['label']} rect={x['rect']} " + ("; ".join(flags) if flags else ""))
|
|
117
|
+
page.screenshot(path=f"/tmp/mob-{TAG}-{WIDTH}.png")
|
|
118
|
+
print(f"\n截图: /tmp/mob-{TAG}-{WIDTH}.png")
|
|
119
|
+
ctx.close()
|
|
120
|
+
b.close()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""点开 ⋯ 后 dump 菜单的真实结构/类名/定位,用于对比插件选择器为何失配。"""
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from playwright.sync_api import sync_playwright
|
|
6
|
+
|
|
7
|
+
BASE, TOKF = sys.argv[1], sys.argv[2]
|
|
8
|
+
TITLE = sys.argv[sys.argv.index("--title") + 1] if "--title" in sys.argv else None
|
|
9
|
+
TOKEN = open(TOKF, encoding="utf-8").read().strip()
|
|
10
|
+
|
|
11
|
+
DUMP = """
|
|
12
|
+
() => {
|
|
13
|
+
const menus = [...document.querySelectorAll('[role=menu], [role=listbox]')].filter((m) => {
|
|
14
|
+
const r = m.getBoundingClientRect();
|
|
15
|
+
return r.width > 40 && r.height > 20;
|
|
16
|
+
});
|
|
17
|
+
if (!menus.length) return { none: true };
|
|
18
|
+
const m = menus[menus.length - 1];
|
|
19
|
+
const r = m.getBoundingClientRect();
|
|
20
|
+
const cs = getComputedStyle(m);
|
|
21
|
+
const chain = [];
|
|
22
|
+
let node = m.parentElement;
|
|
23
|
+
while (node && node !== document.body && chain.length < 6) {
|
|
24
|
+
const c = getComputedStyle(node);
|
|
25
|
+
const rr = node.getBoundingClientRect();
|
|
26
|
+
chain.push({
|
|
27
|
+
tag: node.tagName, cls: (node.className || "").toString().slice(0, 80),
|
|
28
|
+
pos: c.position, left: c.left, right: c.right, width: c.width,
|
|
29
|
+
transform: c.transform === "none" ? null : c.transform,
|
|
30
|
+
rect: [rr.left, rr.top, rr.width, rr.height].map(Math.round),
|
|
31
|
+
});
|
|
32
|
+
node = node.parentElement;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
cls: (m.className || "").toString(),
|
|
36
|
+
role: m.getAttribute("role"),
|
|
37
|
+
rect: [r.left, r.top, r.width, r.height].map(Math.round),
|
|
38
|
+
computed: { position: cs.position, left: cs.left, right: cs.right, top: cs.top,
|
|
39
|
+
width: cs.width, minWidth: cs.minWidth, maxWidth: cs.maxWidth,
|
|
40
|
+
transform: cs.transform, inset: cs.inset },
|
|
41
|
+
inlineStyle: m.getAttribute("style"),
|
|
42
|
+
firstItemCls: m.querySelector("[role=menuitem], button, a")?.className?.toString()?.slice(0, 60) || null,
|
|
43
|
+
htmlHead: m.outerHTML.slice(0, 260),
|
|
44
|
+
chain,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
with sync_playwright() as p:
|
|
50
|
+
b = p.chromium.launch()
|
|
51
|
+
ctx = b.new_context(**p.devices.get("Pixel 7", p.devices["Pixel 5"]))
|
|
52
|
+
page = ctx.new_page()
|
|
53
|
+
page.goto(f"{BASE}/?token={TOKEN}", wait_until="domcontentloaded", timeout=60000)
|
|
54
|
+
page.wait_for_timeout(6000)
|
|
55
|
+
page.touchscreen.tap(*page.evaluate("() => { const r = document.querySelector('#dsh-mobile-menu-btn').getBoundingClientRect(); return [r.left+r.width/2, r.top+r.height/2]; }"))
|
|
56
|
+
page.wait_for_timeout(1500)
|
|
57
|
+
rows = page.query_selector_all("[role=treeitem][class*=sessionRow]") or page.query_selector_all("[role=treeitem]")
|
|
58
|
+
picked = t = None
|
|
59
|
+
for row in rows[:12]:
|
|
60
|
+
s = (row.inner_text() or "").strip().replace("\n", " ")
|
|
61
|
+
if TITLE and TITLE in s:
|
|
62
|
+
picked, t = row, s[:26]; break
|
|
63
|
+
if not TITLE and s and "新会话" not in s:
|
|
64
|
+
picked, t = row, s[:26]; break
|
|
65
|
+
picked.click()
|
|
66
|
+
page.wait_for_timeout(1500)
|
|
67
|
+
vw = page.viewport_size["width"]
|
|
68
|
+
for _ in range(6):
|
|
69
|
+
st = page.evaluate("() => ({s: !!document.querySelector('.dsh-mobile-scrim'), d: !!document.querySelector('.dsh-mobile-drawer')})")
|
|
70
|
+
if not st["s"] and not st["d"]:
|
|
71
|
+
break
|
|
72
|
+
page.touchscreen.tap(vw - 30, 500)
|
|
73
|
+
page.wait_for_timeout(800)
|
|
74
|
+
for _ in range(20):
|
|
75
|
+
page.wait_for_timeout(800)
|
|
76
|
+
if page.evaluate("() => !!document.querySelector('#dsh-mobile-more-proxy')"):
|
|
77
|
+
break
|
|
78
|
+
print(f"base={BASE} 会话={t} 视口宽={vw}\n")
|
|
79
|
+
|
|
80
|
+
c = page.evaluate("""() => { const el = document.querySelector('#dsh-mobile-more-proxy');
|
|
81
|
+
const r = el.getBoundingClientRect(); return [r.left+r.width/2, r.top+r.height/2]; }""")
|
|
82
|
+
page.touchscreen.tap(*c)
|
|
83
|
+
page.wait_for_timeout(1600)
|
|
84
|
+
d = page.evaluate(DUMP)
|
|
85
|
+
if d.get("none"):
|
|
86
|
+
print("没有菜单"); sys.exit(2)
|
|
87
|
+
print(f"菜单 cls = {d['cls']}")
|
|
88
|
+
print(f"菜单 role= {d['role']} rect={d['rect']}")
|
|
89
|
+
print(f"computed = {d['computed']}")
|
|
90
|
+
print(f"inline = {d['inlineStyle']}")
|
|
91
|
+
print(f"首条目 cls= {d['firstItemCls']}")
|
|
92
|
+
print(f"htmlHead = {d['htmlHead']}")
|
|
93
|
+
print("\n祖先链(自内向外):")
|
|
94
|
+
for x in d["chain"]:
|
|
95
|
+
print(f" {x['tag']:6s} cls={x['cls'][:56]:56s} pos={x['pos']:9s} L={x['left']:>8s} R={x['right']:>8s} w={x['width']:>8s} tf={x['transform']} rect={x['rect']}")
|
|
96
|
+
ctx.close(); b.close()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""诊断右侧边栏(工作区文件面板):frame grid / detailsCol / 相关按钮。
|
|
3
|
+
|
|
4
|
+
用法: python3 dump-rightbar.py <base-url> <token-file> [--title 片段]
|
|
5
|
+
"""
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from playwright.sync_api import sync_playwright
|
|
9
|
+
|
|
10
|
+
BASE, TOKF = sys.argv[1], sys.argv[2]
|
|
11
|
+
TITLE = sys.argv[sys.argv.index("--title") + 1] if "--title" in sys.argv else None
|
|
12
|
+
TOKEN = open(TOKF, encoding="utf-8").read().strip()
|
|
13
|
+
|
|
14
|
+
JS = """
|
|
15
|
+
() => {
|
|
16
|
+
const rect = (el) => { const r = el.getBoundingClientRect(); return [Math.round(r.left), Math.round(r.top), Math.round(r.width), Math.round(r.height)]; };
|
|
17
|
+
const out = { grid: null, cols: [], buttons: [] };
|
|
18
|
+
|
|
19
|
+
// 主布局 frame(display:grid 且子元素 ≥3)
|
|
20
|
+
const frames = [...document.querySelectorAll("div")].filter((e) => {
|
|
21
|
+
const cs = getComputedStyle(e);
|
|
22
|
+
return cs.display === "grid" && e.children.length >= 3;
|
|
23
|
+
});
|
|
24
|
+
if (frames.length) {
|
|
25
|
+
const f = frames[0], cs = getComputedStyle(f);
|
|
26
|
+
out.grid = { cls: (f.className || "").toString().slice(0, 70), cols: cs.gridTemplateColumns,
|
|
27
|
+
rect: rect(f), inlineCols: f.style.gridTemplateColumns || null, vw: window.innerWidth };
|
|
28
|
+
for (const ch of f.children) {
|
|
29
|
+
const cs = getComputedStyle(ch);
|
|
30
|
+
out.cols.push({ cls: (ch.className || "").toString().slice(0, 70), display: cs.display,
|
|
31
|
+
pos: cs.position, vis: cs.visibility, rect: rect(ch),
|
|
32
|
+
overflow: cs.overflow, text: (ch.innerText || "").trim().slice(0, 40) });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 含关键词的按钮(不论可见性)
|
|
37
|
+
document.querySelectorAll("button, [role=button]").forEach((el) => {
|
|
38
|
+
const label = ((el.getAttribute("aria-label") || "") + " " + (el.title || "") + " " + (el.innerText || "").slice(0, 14)).trim();
|
|
39
|
+
if (/侧边栏|右侧|文件|预览|面板|详情|工作区|sidebar|panel|detail|file/i.test(label)) {
|
|
40
|
+
const cs = getComputedStyle(el);
|
|
41
|
+
out.buttons.push({ label: label.slice(0, 44), cls: (el.className || "").toString().slice(0, 54),
|
|
42
|
+
display: cs.display, vis: cs.visibility, rect: rect(el) });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
with sync_playwright() as p:
|
|
50
|
+
b = p.chromium.launch()
|
|
51
|
+
ctx = b.new_context(**p.devices.get("Pixel 7", p.devices["Pixel 5"]))
|
|
52
|
+
page = ctx.new_page()
|
|
53
|
+
page.goto(f"{BASE}/?token={TOKEN}", wait_until="domcontentloaded", timeout=60000)
|
|
54
|
+
page.wait_for_timeout(6000)
|
|
55
|
+
page.touchscreen.tap(*page.evaluate("() => { const r = document.querySelector('#dsh-mobile-menu-btn').getBoundingClientRect(); return [r.left+r.width/2, r.top+r.height/2]; }"))
|
|
56
|
+
page.wait_for_timeout(1500)
|
|
57
|
+
rows = page.query_selector_all("[role=treeitem][class*=sessionRow]") or page.query_selector_all("[role=treeitem]")
|
|
58
|
+
picked = t = None
|
|
59
|
+
for row in rows[:12]:
|
|
60
|
+
s = (row.inner_text() or "").strip().replace("\n", " ")
|
|
61
|
+
if TITLE and TITLE in s:
|
|
62
|
+
picked, t = row, s[:26]; break
|
|
63
|
+
if not TITLE and s and "新会话" not in s:
|
|
64
|
+
picked, t = row, s[:26]; break
|
|
65
|
+
picked.click()
|
|
66
|
+
page.wait_for_timeout(1500)
|
|
67
|
+
vw = page.viewport_size["width"]
|
|
68
|
+
for _ in range(6):
|
|
69
|
+
st = page.evaluate("() => ({s: !!document.querySelector('.dsh-mobile-scrim'), d: !!document.querySelector('.dsh-mobile-drawer')})")
|
|
70
|
+
if not st["s"] and not st["d"]:
|
|
71
|
+
break
|
|
72
|
+
page.touchscreen.tap(vw - 30, 500)
|
|
73
|
+
page.wait_for_timeout(800)
|
|
74
|
+
for _ in range(20):
|
|
75
|
+
page.wait_for_timeout(800)
|
|
76
|
+
if page.evaluate("() => !!document.querySelector('#dsh-mobile-tab-tools')"):
|
|
77
|
+
break
|
|
78
|
+
print(f"base={BASE} 会话={t} 视口={page.viewport_size}\n")
|
|
79
|
+
d = page.evaluate(JS)
|
|
80
|
+
|
|
81
|
+
if d["grid"]:
|
|
82
|
+
g = d["grid"]
|
|
83
|
+
print(f"frame grid: cls={g['cls']}")
|
|
84
|
+
print(f" cols(computed) = {g['cols']}")
|
|
85
|
+
print(f" cols(inline) = {g['inlineCols']} ← 插件若改单列会写在这里")
|
|
86
|
+
print(f" rect={g['rect']}\n ── 三列 ──")
|
|
87
|
+
for c in d["cols"]:
|
|
88
|
+
print(f" cls={c['cls'][:52]:52s} display={c['display']:12s} pos={c['pos']:9s} vis={c['vis']:8s} rect={c['rect']} text={c['text']!r}")
|
|
89
|
+
else:
|
|
90
|
+
print("(没找到 display:grid 且 ≥3 子元素的 frame)")
|
|
91
|
+
|
|
92
|
+
print(f"\n── 含侧栏/文件关键词的按钮({len(d['buttons'])} 个)──")
|
|
93
|
+
for x in d["buttons"]:
|
|
94
|
+
print(f" label={x['label']!r:46s} display={x['display']:10s} vis={x['vis']:8s} rect={x['rect']} cls={x['cls'][:40]}")
|
|
95
|
+
ctx.close(); b.close()
|