@mrrisega/dsh-remote 0.6.9-beta.2 → 0.6.9-beta.3
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.
|
@@ -109,7 +109,10 @@ const STYLE = `
|
|
|
109
109
|
注意:不能给浮层设 will-change:transform / 持久 transform —— 官方把设置面板等
|
|
110
110
|
fixed 弹层的 DOM 挂在侧栏子树里,transform 祖先会成为 fixed 的 containing block,
|
|
111
111
|
导致弹层被限制在 84vw 侧栏宽内(实测 328px“抽屉感”,fix2 根因之一)。 */
|
|
112
|
-
|
|
112
|
+
/* ⚠️ 全部加 :not(.dsh-ma-details):万一某列被同时标成抽屉与详情列(官方改版导致,2026-09-19 在
|
|
113
|
+
0.1.5-rc.2 上真的发生过:第三列里的 "Collapse right sidebar" 按钮被宽松探测命中),
|
|
114
|
+
详情列的定位/层级必须赢 —— 否则抽屉一展开,那一列会被拉到 left:0 盖住抽屉,用户点哪都没反应。 */
|
|
115
|
+
div.pI_x6G_sidebarCol:not(.dsh-ma-details), div.dsh-ma-sidebar:not(.dsh-ma-details) {
|
|
113
116
|
position: fixed; left: 0; top: 0; bottom: 0; margin: 0;
|
|
114
117
|
width: min(84vw, 340px); max-width: 92vw;
|
|
115
118
|
z-index: 300; overflow: hidden;
|
|
@@ -118,15 +121,15 @@ const STYLE = `
|
|
|
118
121
|
transform: translateX(-103%);
|
|
119
122
|
transition: transform .22s var(--ds-ease-in-out, ease);
|
|
120
123
|
}
|
|
121
|
-
html.dsh-ma-sidebar-open div.pI_x6G_sidebarCol,
|
|
122
|
-
html.dsh-ma-sidebar-open div.dsh-ma-sidebar { transform: none; }
|
|
124
|
+
html.dsh-ma-sidebar-open div.pI_x6G_sidebarCol:not(.dsh-ma-details),
|
|
125
|
+
html.dsh-ma-sidebar-open div.dsh-ma-sidebar:not(.dsh-ma-details) { transform: none; }
|
|
123
126
|
/* ⚠️ 抽屉展开时必须抬到详情列(第三列)之上 —— 两者同为 z-index:300 时按 DOM 顺序绘制,
|
|
124
127
|
而第三列在 frame 里排在侧栏**之后**,于是"详情列滑进来"会整块压住抽屉:
|
|
125
128
|
实测(2026-09-19 手机端)抽屉里每一行点下去命中的都是详情面板(_2ctAZa_empty),
|
|
126
129
|
用户表现为「展开左边抽屉,无法选择历史会话,点任何东西都没有反应」。
|
|
127
130
|
这里把抽屉抬到 310(>300 详情列,>290 遮罩):抽屉内的点击**永远**有效。 */
|
|
128
|
-
html.dsh-ma-sidebar-open div.pI_x6G_sidebarCol,
|
|
129
|
-
html.dsh-ma-sidebar-open div.dsh-ma-sidebar { z-index: 310; }
|
|
131
|
+
html.dsh-ma-sidebar-open div.pI_x6G_sidebarCol:not(.dsh-ma-details),
|
|
132
|
+
html.dsh-ma-sidebar-open div.dsh-ma-sidebar:not(.dsh-ma-details) { z-index: 310; }
|
|
130
133
|
|
|
131
134
|
div.pI_x6G_detailsCol, div.pI_x6G_rightbarCol, div.dsh-ma-details, div.dsh-ma-rightbar {
|
|
132
135
|
position: fixed; right: 0; top: 0; bottom: 0; margin: 0;
|
|
@@ -1128,22 +1131,61 @@ const SCRIPT = `(() => {
|
|
|
1128
1131
|
};
|
|
1129
1132
|
const overlayLayer = () => frame.querySelector('[data-shell-overlay]');
|
|
1130
1133
|
|
|
1131
|
-
/* 第三列在官方 0.1.2-rc.1 叫 details、0.1.5-rc.2 起叫 rightbar ——
|
|
1134
|
+
/* 第三列在官方 0.1.2-rc.1 叫 details、0.1.5-rc.2 起叫 rightbar —— 两个名字都认(真机两版都验过)。
|
|
1135
|
+
⚠️ 刻意**不**依赖本机第三方插件(dsh-better-sidebar)写的 data-rightbar-col / data-dsh-center-col 标记:
|
|
1136
|
+
那不是官方契约,插件换版本或被卸载就会失灵。只认官方类名 + 结构。 */
|
|
1132
1137
|
const THIRD_COL_RE = /details|rightbar/i;
|
|
1138
|
+
const isThirdCol = (col, cls) => THIRD_COL_RE.test(cls);
|
|
1139
|
+
/* ⚠️ 侧栏列的识别必须**排他 + 精确**,否则一列会被赋予两个身份(2026-09-19 在官方 0.1.5-rc.2 上实测到):
|
|
1140
|
+
第三列(rightbar)里有一颗按钮 aria-label="Collapse right sidebar",旧代码的宽松探测
|
|
1141
|
+
宽松探测 [aria-label*='sidebar' i] 把它当成了"这一列里有侧栏" → rightbar 同时挂上 dsh-ma-sidebar 与
|
|
1142
|
+
dsh-ma-details → 抽屉一展开,html.dsh-ma-sidebar-open div.dsh-ma-sidebar{transform:none;z-index:310}
|
|
1143
|
+
把 **rightbar** 也拉到 left:0 并盖在真抽屉之上(DOM 顺序在后) → 抽屉里点什么都命中详情面板,
|
|
1144
|
+
就是用户报的"白框压住抽屉"。而且它是**间歇性**的:那颗按钮只在右栏面板挂载时才渲染,
|
|
1145
|
+
实测 4 次启动里 3 次命中。现在的规则:
|
|
1146
|
+
① 已经判定为第三列(rightbar/details 类名)的列,**绝不是**抽屉列;
|
|
1147
|
+
② 类名含 sidebar 且不含 rightbar/details(0.1.2/0.1.5 的 pI_x6G_sidebarCol 都满足);
|
|
1148
|
+
③ 退到结构信号:列里有官方侧栏组件根 .hHd-Xa_root,或有**整串**匹配的官方侧栏开合按钮
|
|
1149
|
+
(精确匹配,绝不再用 *="sidebar" 这种包含匹配 —— 那正是事故来源);
|
|
1150
|
+
④ 同时给 CSS 加了 :not(.dsh-ma-details) 兜底:万一将来又标重,详情列的样式也必须赢。 */
|
|
1151
|
+
const SIDEBAR_RE = /sidebar/i;
|
|
1152
|
+
const NOT_SIDEBAR_RE = /rightbar|right-bar|details/i;
|
|
1153
|
+
const SIDEBAR_TOGGLE_SEL = '[aria-label="Open sidebar"], [aria-label="Close sidebar"], [aria-label="Collapse sidebar"], [aria-label="Expand sidebar"], [aria-label="打开侧边栏"], [aria-label="关闭侧边栏"], [aria-label="收起侧边栏"], [aria-label="展开侧边栏"]';
|
|
1154
|
+
const isSidebarCol = (col, cls) => {
|
|
1155
|
+
if (isThirdCol(col, cls)) return false; // ① 第三列绝不兼任抽屉列
|
|
1156
|
+
if (SIDEBAR_RE.test(cls) && !NOT_SIDEBAR_RE.test(cls)) return true; // ② 类名
|
|
1157
|
+
try { // ③ 结构兜底(类名前缀改名时)
|
|
1158
|
+
if (col.querySelector(".hHd-Xa_root")) return true;
|
|
1159
|
+
if (col.querySelector(SIDEBAR_TOGGLE_SEL)) return true;
|
|
1160
|
+
} catch (e) { /* 忽略 */ }
|
|
1161
|
+
return false;
|
|
1162
|
+
};
|
|
1133
1163
|
let sidebar = null, details = null, center = null;
|
|
1134
1164
|
const cols = [...frame.children].filter((c) => c instanceof HTMLElement && !isOverlayLayer(c) && !isHandle(c));
|
|
1165
|
+
/* 先把第三列认出来(它优先级最高、也最容易被误认),再从**剩下的**列里认抽屉与中央列。 */
|
|
1135
1166
|
for (const col of cols) {
|
|
1136
1167
|
const cls = typeof col.className === "string" ? col.className : "";
|
|
1137
|
-
if (
|
|
1168
|
+
if (isThirdCol(col, cls)) { col.classList.add("dsh-ma-details"); details = details || col; }
|
|
1169
|
+
}
|
|
1170
|
+
for (const col of cols) {
|
|
1171
|
+
if (col === details) continue;
|
|
1172
|
+
const cls = typeof col.className === "string" ? col.className : "";
|
|
1173
|
+
if (isSidebarCol(col, cls)) {
|
|
1138
1174
|
col.classList.add("dsh-ma-sidebar"); sidebar = sidebar || col;
|
|
1139
|
-
} else if (THIRD_COL_RE.test(cls)) {
|
|
1140
|
-
col.classList.add("dsh-ma-details"); details = details || col;
|
|
1141
1175
|
} else if (/center/i.test(cls)) {
|
|
1142
1176
|
col.classList.add("dsh-ma-center"); center = center || col;
|
|
1143
1177
|
}
|
|
1144
1178
|
}
|
|
1145
|
-
if (!sidebar
|
|
1146
|
-
if (!center
|
|
1179
|
+
if (!sidebar) { const c = cols.find((x) => x !== details); if (c) { c.classList.add("dsh-ma-sidebar"); sidebar = c; } }
|
|
1180
|
+
if (!center) { const c = cols.find((x) => x !== details && x !== sidebar); if (c) { c.classList.add("dsh-ma-center"); center = c; } }
|
|
1181
|
+
/* 🔒 不变量:同一列绝不允许既是抽屉又是详情列。真出现(将来官方再改名)时**以详情列为准** ——
|
|
1182
|
+
抽屉还能用汉堡按钮开合,而"自己盖住自己"的详情列会让抽屉彻底点不动。 */
|
|
1183
|
+
if (sidebar && details && sidebar === details) {
|
|
1184
|
+
sidebar.classList.remove("dsh-ma-sidebar");
|
|
1185
|
+
sidebar = null;
|
|
1186
|
+
const c = cols.find((x) => x !== details);
|
|
1187
|
+
if (c) { c.classList.add("dsh-ma-sidebar"); sidebar = c; }
|
|
1188
|
+
}
|
|
1147
1189
|
/* 兜底:只在**剩下的候选列**里取最后一个(overlay/把手/已认领的列都已排除)——
|
|
1148
1190
|
命中不到就老实放弃(details=null),绝不再把全屏层当抽屉。 */
|
|
1149
1191
|
if (!details) {
|
|
@@ -1165,15 +1207,20 @@ const SCRIPT = `(() => {
|
|
|
1165
1207
|
const s = String(v).trim().toLowerCase();
|
|
1166
1208
|
return !(s === "" || s === "false" || s === "0" || s === "off" || s === "no");
|
|
1167
1209
|
};
|
|
1210
|
+
/* 侧栏折叠态。官方 0.1.2-rc.1 只在**折叠时**写 data-sidebar-collapsed,展开时把属性**移除**
|
|
1211
|
+
(实测:窄屏抽屉展开后 frame 上完全没有该属性);也兼容带值写法(=false/=true)与反向的
|
|
1212
|
+
data-sidebar-open。所以"属性不在"有两种含义 ——"展开"或"官方根本不叫这个名字" ——必须区分:
|
|
1213
|
+
· 只要**见过**它出现过(sidebarAttrSeen),就说明这个名字是活的 → 之后的"不在" = 展开;
|
|
1214
|
+
· 从没见过 → 认不到 → **当作收起**(0.6.9-beta.2 加入的兜底):离屏抽屉盖在内容上、
|
|
1215
|
+
遮罩跟着下闸才是真正会困住用户的形态,收起态至少还能用汉堡按钮打开。
|
|
1216
|
+
⚠️ 注意 collapsed()/expanded() 只是"官方属性怎么表达"的**猜测**,只能用来**跟随官方**;
|
|
1217
|
+
"抽屉此刻是不是开着"适配层自己有确定答案(html 上的 dsh-ma-sidebar-open / 几何),
|
|
1218
|
+
凡是要据此做动作的地方(如自动收抽屉)都必须用后者 —— 见下面 autoClose 委托。 */
|
|
1219
|
+
let sidebarAttrSeen = false;
|
|
1168
1220
|
const collapsed = () => {
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
展开态会让离屏抽屉盖在内容上(scrim 也可能跟着下闸),而收起态至少还能用汉堡按钮打开 ——
|
|
1173
|
-
"拿不准就选不会困住用户的那一边"。 */
|
|
1174
|
-
if (frame.hasAttribute("data-sidebar-collapsed")) return truthyAttr(frame, "data-sidebar-collapsed");
|
|
1175
|
-
if (frame.hasAttribute("data-sidebar-open")) return !truthyAttr(frame, "data-sidebar-open");
|
|
1176
|
-
return true;
|
|
1221
|
+
if (frame.hasAttribute("data-sidebar-collapsed")) { sidebarAttrSeen = true; return truthyAttr(frame, "data-sidebar-collapsed"); }
|
|
1222
|
+
if (frame.hasAttribute("data-sidebar-open")) { sidebarAttrSeen = true; return !truthyAttr(frame, "data-sidebar-open"); }
|
|
1223
|
+
return !sidebarAttrSeen;
|
|
1177
1224
|
};
|
|
1178
1225
|
const expanded = () => !collapsed();
|
|
1179
1226
|
|
|
@@ -1382,7 +1429,11 @@ const SCRIPT = `(() => {
|
|
|
1382
1429
|
if (t.closest(".dsh-ma-sidebar, .dsh-ma-scrim, .dsh-ma-hamburger, .dsh-ma-fab, .dsh-ma-menu")) return;
|
|
1383
1430
|
if (!t.closest(".dsh-ma-center, .pI_x6G_centerCol")) return;
|
|
1384
1431
|
centerTouchedAt = Date.now();
|
|
1385
|
-
|
|
1432
|
+
/* 只有"官方此刻就说详情是展开的"才需要立刻重算 —— 否则这一下点击改变不了任何显示,
|
|
1433
|
+
白跑一次 sync()(它要读几次 getBoundingClientRect)。官方稍后把详情置为展开时,
|
|
1434
|
+
frame 属性观察器会再触发一次 sync,那时 centerTouchedAt 已经记下了用户意图。 */
|
|
1435
|
+
if (detailsCollapsed()) return;
|
|
1436
|
+
sync();
|
|
1386
1437
|
} catch (e2) { /* 忽略 */ }
|
|
1387
1438
|
}, true);
|
|
1388
1439
|
} catch (e2) { /* 忽略 */ }
|
|
@@ -1413,20 +1464,46 @@ const SCRIPT = `(() => {
|
|
|
1413
1464
|
sync();
|
|
1414
1465
|
/* 兜底复核:官方模块异步挂载、字体/主题切换都会改尺寸 —— 断点各复核一次遮罩闸门。 */
|
|
1415
1466
|
[400, 1200, 3000].forEach((ms) => setTimeout(applyScrim, ms));
|
|
1416
|
-
/*
|
|
1417
|
-
|
|
1467
|
+
/* 窄屏抽屉:点选会话/在工作区里新建会话/进 Task Board 后自动收起(官方不做;适配层点官方 toggle)。
|
|
1468
|
+
⚠️ 2026-09-19 回归(用户实测「选择工作区之后抽屉没有收回去」)根因就在这个闸门上:
|
|
1469
|
+
旧写法 if (!NARROW() || !expanded()) return; 用**官方属性**判断"抽屉是不是开着",
|
|
1470
|
+
而官方表达"展开"的方式恰恰是**把 data-sidebar-collapsed 移除** —— 于是抽屉一打开,
|
|
1471
|
+
expanded() 就变成 false(属性名认不到时更恒为 false),委托第一行直接 return,
|
|
1472
|
+
**所有**自动收起全部失效。现在改成用适配层自己的确定状态:
|
|
1473
|
+
· html.dsh-ma-sidebar-open —— 我们标记的展开态(汉堡按钮/官方切换都会走到 sync);
|
|
1474
|
+
· sidebarInView() —— 抽屉此刻**真的**在视口里(几何兜底,class 丢了也认)。
|
|
1475
|
+
两者任一成立就认为"抽屉开着,该收"。 */
|
|
1476
|
+
const drawerIsOpenNow = () =>
|
|
1477
|
+
document.documentElement.classList.contains("dsh-ma-sidebar-open") || sidebarInView();
|
|
1478
|
+
/* 点完就该收起抽屉的交互(基于 2026-09-19 真机 DOM 取证,只列**导航/切换上下文**的动作):
|
|
1479
|
+
· 会话行:div.YDXeBa_sessionRow[role="treeitem"][aria-selected] → 切会话
|
|
1480
|
+
· 新建会话:aria-label="New session" / "New session in <工作区>" → 新会话(后者=选择工作区并进入)
|
|
1481
|
+
· Task Board:aria-label="Task Board" / [data-dsh-taskboard-entry] → 切到任务板主视图
|
|
1482
|
+
明确**不**收起的(都是"就地操作",收起会把用户正在看的东西一起收掉):
|
|
1483
|
+
· 工作区行本身 div.YDXeBa_projectRow[role="treeitem"][aria-expanded](无 aria-selected)
|
|
1484
|
+
—— 官方源码里它是 disclosure:onClick: onToggle → setGroupExpanded(group.key, !expanded),
|
|
1485
|
+
真机实测点 wikiStore:aria-expanded false→true、会话行 6→11 条。点它=展开/折叠该工作区的
|
|
1486
|
+
会话列表(用户正要看里面的会话),收抽屉会让"展开→看不到"。
|
|
1487
|
+
(用户说的"选择工作区"在抽屉里的真实入口是行内那颗 ⊕「New session in <工作区>」,
|
|
1488
|
+
它属于"新建会话"那一类,已被上面的选择器覆盖并有用例。)
|
|
1489
|
+
· 行内「⋯」Workspace actions / 展开折叠 / 滚动 / 勾选:都不是导航。 */
|
|
1490
|
+
const AUTO_CLOSE_SEL = [
|
|
1491
|
+
'[role="treeitem"][aria-selected]',
|
|
1492
|
+
'[aria-label*="新建会话"]',
|
|
1493
|
+
'[aria-label*="New session" i]',
|
|
1494
|
+
'[aria-label*="Task Board" i]',
|
|
1495
|
+
'[data-dsh-taskboard-entry]',
|
|
1496
|
+
].join(",");
|
|
1418
1497
|
if (!drawerAutoCloseOn) {
|
|
1419
1498
|
drawerAutoCloseOn = true;
|
|
1420
1499
|
document.addEventListener("click", (e) => {
|
|
1421
1500
|
try {
|
|
1422
|
-
if (!NARROW() || !
|
|
1501
|
+
if (!NARROW() || !drawerIsOpenNow()) return;
|
|
1423
1502
|
const t = e.target;
|
|
1424
1503
|
if (!t || !t.closest) return;
|
|
1425
1504
|
if (t.closest(".dsh-ma-scrim, .dsh-ma-hamburger")) return;
|
|
1426
|
-
if (!t.closest(".dsh-ma-sidebar")) return;
|
|
1427
|
-
|
|
1428
|
-
const isNew = t.closest('[aria-label*="新建会话"], [aria-label*="New session" i]');
|
|
1429
|
-
if (!row && !isNew) return;
|
|
1505
|
+
if (!t.closest(".dsh-ma-sidebar")) return; // 只处理抽屉内的点选
|
|
1506
|
+
if (!t.closest(AUTO_CLOSE_SEL)) return; // 只认"导航/切换"类,就地操作不收起
|
|
1430
1507
|
userWantsOpen = false;
|
|
1431
1508
|
const togg = toggleOf();
|
|
1432
1509
|
if (togg) setTimeout(() => { try { togg.click(); } catch (e5) { /* ignore */ } }, 0);
|
|
@@ -213,10 +213,11 @@ function zIndexes(css) {
|
|
|
213
213
|
};
|
|
214
214
|
return {
|
|
215
215
|
scrim: num(/div\.dsh-ma-scrim\s*\{[\s\S]{0,200}?z-index:\s*(\d+)/),
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
216
|
+
// 抽屉规则都带 :not(.dsh-ma-details) 兜底(防止一列被同时标成抽屉与详情列)
|
|
217
|
+
sidebar: num(/div\.pI_x6G_sidebarCol(?::not\([^)]*\))?,\s*div\.dsh-ma-sidebar(?::not\([^)]*\))?\s*\{[\s\S]{0,500}?z-index:\s*(\d+)/),
|
|
218
|
+
sidebarOpen: num(/html\.dsh-ma-sidebar-open\s+div\.pI_x6G_sidebarCol(?::not\([^)]*\))?,[\s\S]{0,200}?\{\s*z-index:\s*(\d+)/),
|
|
219
|
+
details: num(/div\.pI_x6G_detailsCol,[\s\S]{0,300}?z-index:\s*(\d+)/),
|
|
220
|
+
hamburger: num(/button\.dsh-ma-hamburger\s*\{[\s\S]{0,700}?z-index:\s*(\d+)/),
|
|
220
221
|
};
|
|
221
222
|
}
|
|
222
223
|
|
|
@@ -258,10 +259,36 @@ function harness({
|
|
|
258
259
|
const center = new El("div", { class: "pI_x6G_centerCol" });
|
|
259
260
|
const third = new El("div", { class: variant === "rc2" ? "pI_x6G_rightbarCol" : "pI_x6G_detailsCol" });
|
|
260
261
|
const overlay = new El("div", { class: "pI_x6G_overlayLayer", "data-shell-overlay": "true" });
|
|
261
|
-
const row = new El("div", { role: "treeitem", "aria-selected": "false", class: "hHd-Xa_sessionRow" });
|
|
262
|
+
const row = new El("div", { role: "treeitem", "aria-selected": "false", class: "hHd-Xa_sessionRow YDXeBa_sessionRow" });
|
|
262
263
|
// 抽屉里一条历史会话行的实测位置(侧栏 0..328,避开左上角的汉堡按钮)
|
|
263
264
|
row._rect = { left: 16, top: 200, right: 216, bottom: 240, width: 200, height: 40 };
|
|
264
265
|
sidebar.appendChild(row);
|
|
266
|
+
/* 工作区行(官方真实结构,2026-09-19 真机 outerHTML 取证):
|
|
267
|
+
<div class="YDXeBa_projectRow" role="treeitem" aria-expanded="false" draggable="true">
|
|
268
|
+
<span class="YDXeBa_slot YDXeBa_folder">…<svg>
|
|
269
|
+
<span class="YDXeBa_slot YDXeBa_chevron">…<svg>
|
|
270
|
+
<span class="YDXeBa_projectText"><span class="YDXeBa_title">myFreeWork</span></span>
|
|
271
|
+
<span class="YDXeBa_rowActions">
|
|
272
|
+
<button class="YDXeBa_iconButton" aria-label="Workspace actions for myFreeWork">…</button>
|
|
273
|
+
<button class="YDXeBa_iconButton" aria-label="New session in myFreeWork">…</button>
|
|
274
|
+
官方源码里这一行的 onClick 是 onToggle(setGroupExpanded),点它 = 展开/折叠该工作区的会话列表。 */
|
|
275
|
+
const wsRow = new El("div", { class: "YDXeBa_projectRow", role: "treeitem", "aria-expanded": "false", draggable: "true" });
|
|
276
|
+
wsRow._rect = { left: 12, top: 100, right: 268, bottom: 134, width: 256, height: 34 };
|
|
277
|
+
const wsTitle = new El("span", { class: "YDXeBa_title" });
|
|
278
|
+
wsTitle.textContent = "myFreeWork";
|
|
279
|
+
const wsActions = new El("span", { class: "YDXeBa_rowActions" });
|
|
280
|
+
const wsMenuBtn = new El("button", { class: "YDXeBa_iconButton", "aria-label": "Workspace actions for myFreeWork" });
|
|
281
|
+
const wsNewBtn = new El("button", { class: "YDXeBa_iconButton", "aria-label": "New session in myFreeWork" });
|
|
282
|
+
wsActions.appendChild(wsMenuBtn); wsActions.appendChild(wsNewBtn);
|
|
283
|
+
wsRow.appendChild(wsTitle); wsRow.appendChild(wsActions);
|
|
284
|
+
sidebar.appendChild(wsRow);
|
|
285
|
+
// 侧栏里的其它入口:Task Board(导航)与非交互区(纯文本/滚动槽)
|
|
286
|
+
const taskBoard = new El("button", { class: "_7D6uKa_entry", "aria-label": "Task Board", "data-dsh-taskboard-entry": "" });
|
|
287
|
+
const gutter = new El("div", { class: "bhn1Oq_listArea" });
|
|
288
|
+
const gutterText = new El("span");
|
|
289
|
+
gutter.appendChild(gutterText);
|
|
290
|
+
sidebar.appendChild(taskBoard);
|
|
291
|
+
sidebar.appendChild(gutter);
|
|
265
292
|
// 官方输入区(Lexical):contenteditable 富文本宿主
|
|
266
293
|
const composer = new El("div", {
|
|
267
294
|
contenteditable: "true", "data-composer-input": "true", role: "textbox", "aria-multiline": "true",
|
|
@@ -286,7 +313,7 @@ function harness({
|
|
|
286
313
|
if (el === frame) return { left: 0, right: width, top: 0, bottom: height, width, height };
|
|
287
314
|
return el._rect;
|
|
288
315
|
};
|
|
289
|
-
for (const el of [sidebar, center, third, overlay, frame, row, composer, composerChild, toolRow]) {
|
|
316
|
+
for (const el of [sidebar, center, third, overlay, frame, row, composer, composerChild, toolRow, wsRow, wsTitle, wsNewBtn, wsMenuBtn, taskBoard, gutterText]) {
|
|
290
317
|
el.getBoundingClientRect = () => rectFor(el);
|
|
291
318
|
}
|
|
292
319
|
|
|
@@ -353,7 +380,8 @@ function harness({
|
|
|
353
380
|
ctx.self = win;
|
|
354
381
|
/** 手工 tick 一次已挂上的 setInterval(看门狗 800ms 那一支)。 */
|
|
355
382
|
const tickTimers = () => { for (const fn of intervals.slice()) { if (fn) fn(); } };
|
|
356
|
-
return { ctx, doc, html, body, frame, sidebar, center, third, overlay, row, toolRow, composer, composerChild, toggle,
|
|
383
|
+
return { ctx, doc, html, body, frame, sidebar, center, third, overlay, row, toolRow, composer, composerChild, toggle,
|
|
384
|
+
wsRow, wsTitle, wsNewBtn, wsMenuBtn, taskBoard, gutterText, win, scrimRef, tickTimers, intervals };
|
|
357
385
|
}
|
|
358
386
|
|
|
359
387
|
/** 跑注入脚本(真实 SCRIPT,一字不改)。 */
|
|
@@ -650,6 +678,30 @@ test("抽屉可点:抽屉展开时抽屉内点击命中抽屉自身(不被
|
|
|
650
678
|
assert.equal(hit2, env.row, "即便详情列同时展开,抽屉内的点击也必须有效(不能把用户困住)");
|
|
651
679
|
});
|
|
652
680
|
|
|
681
|
+
test("列识别:官方 0.1.5 第三列里的「Collapse right sidebar」按钮不得让它被当成抽屉列", () => {
|
|
682
|
+
const { js, css } = injected();
|
|
683
|
+
// 0.1.5-rc.2 真机取证:rightbar 列里有 <button class="P3OORG_iconButton" aria-label="Collapse right sidebar">
|
|
684
|
+
// 旧代码用 [aria-label*="sidebar" i] 宽松包含匹配 → rightbar 同时挂上 dsh-ma-sidebar + dsh-ma-details
|
|
685
|
+
// → 抽屉一展开,rightbar 也被 transform:none 拉到 left:0 盖住抽屉(实测 4 次启动 3 次命中)。
|
|
686
|
+
const env = harness({ collapsedAttr: "true", variant: "rc2", detailsAttr: null, rightbarAttr: "true" });
|
|
687
|
+
const rb = env.third; // harness variant rc2 → class="pI_x6G_rightbarCol"(官方 0.1.5-rc.2 的真实类名)
|
|
688
|
+
const collapseBtn = new El("button", { class: "P3OORG_iconButton", "aria-label": "Collapse right sidebar" });
|
|
689
|
+
rb.appendChild(collapseBtn);
|
|
690
|
+
run(env, js);
|
|
691
|
+
assert.equal(rb._cls.has("dsh-ma-details"), true, "第三列仍应被认成详情列");
|
|
692
|
+
assert.equal(rb._cls.has("dsh-ma-sidebar"), false,
|
|
693
|
+
"第三列绝不能同时被标成抽屉列(一列两个身份 → 抽屉被自己盖住,点哪都没反应)");
|
|
694
|
+
assert.equal(env.sidebar._cls.has("dsh-ma-sidebar"), true, "真正的侧栏列必须仍是抽屉列");
|
|
695
|
+
// CSS 兜底:抽屉的定位/层级规则都排除了"同时也是详情列"的元素
|
|
696
|
+
assert.match(css, /div\.dsh-ma-sidebar:not\(\.dsh-ma-details\)/,
|
|
697
|
+
"抽屉样式必须带 :not(.dsh-ma-details) 兜底(万一又标重,详情列的样式也要赢)");
|
|
698
|
+
// 抽屉仍然能开,且抽屉内点击可达
|
|
699
|
+
findHamburger(env).dispatchEvent(new Ev("click", {}));
|
|
700
|
+
env.doc.dispatchEvent(new Ev("resize", {}));
|
|
701
|
+
assert.equal(env.html.classList.contains("dsh-ma-sidebar-open"), true, "抽屉应能正常展开");
|
|
702
|
+
assert.equal(env.html.classList.contains("dsh-ma-scrim-on"), true, "抽屉在视口里 → 遮罩正常生效");
|
|
703
|
+
});
|
|
704
|
+
|
|
653
705
|
test("属性观察:官方换名字表达抽屉开合时,适配层仍能收到通知并同步", () => {
|
|
654
706
|
const { js } = injected();
|
|
655
707
|
const env = run(harness({ collapsedAttr: "true" }), js);
|
|
@@ -680,3 +732,101 @@ test("属性兜底:开合属性完全认不到时默认收起(拿不准就
|
|
|
680
732
|
ham.dispatchEvent(new Ev("click", {}));
|
|
681
733
|
assert.equal(env.html.classList.contains("dsh-ma-sidebar-open"), true, "认不到属性也要能用汉堡按钮打开抽屉(可自救)");
|
|
682
734
|
});
|
|
735
|
+
|
|
736
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
737
|
+
// 回归(0.6.9-beta.2 用户实测):「选择工作区之后,左边那个抽屉它自己没有收回去」
|
|
738
|
+
//
|
|
739
|
+
// 根因:自动收抽屉的委托用**官方属性**(expanded())当闸门,而官方表达"展开"的方式恰恰是
|
|
740
|
+
// **把 data-sidebar-collapsed 移除** —— 抽屉一打开,expanded() 就变成 false
|
|
741
|
+
// (属性名认不到时更恒为 false),委托第一行直接 return,所有自动收起全部失效。
|
|
742
|
+
// 真机取证(390×844,抽屉已展开):frame 上 "(无 data-sidebar-collapsed)"。
|
|
743
|
+
// 修法:闸门改用适配层自己的确定状态(dsh-ma-sidebar-open || sidebarInView())。
|
|
744
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
745
|
+
|
|
746
|
+
/** 模拟"官方两个开合属性都认不到"(比 0.1.5 改名更极端;也是回归用例最关键的现场)。 */
|
|
747
|
+
function openDrawerWithoutAttrs(env) {
|
|
748
|
+
env.frame.removeAttribute("data-sidebar-collapsed");
|
|
749
|
+
env.frame.removeAttribute("data-sidebar-open");
|
|
750
|
+
findHamburger(env).dispatchEvent(new Ev("click", {}));
|
|
751
|
+
}
|
|
752
|
+
const toggleClicks = (env) => { let n = 0; env.toggle.addEventListener("click", () => { n += 1; }); return () => n; };
|
|
753
|
+
const waitTick = () => new Promise((r) => setTimeout(r, 30));
|
|
754
|
+
|
|
755
|
+
test("自动收抽屉:官方开合属性认不到时,点会话行**仍然**收起(本次回归的主用例)", async () => {
|
|
756
|
+
const { js } = injected();
|
|
757
|
+
const env = run(harness({ collapsedAttr: "true", detailsAttr: "true" }), js);
|
|
758
|
+
const clicks = toggleClicks(env);
|
|
759
|
+
openDrawerWithoutAttrs(env);
|
|
760
|
+
assert.equal(env.html.classList.contains("dsh-ma-sidebar-open"), true, "前提:抽屉已展开");
|
|
761
|
+
assert.equal(env.frame.hasAttribute("data-sidebar-collapsed"), false, "前提:官方属性认不到(真机就是「展开时移除该属性」)");
|
|
762
|
+
env.row.dispatchEvent(new Ev("click", {}));
|
|
763
|
+
await waitTick();
|
|
764
|
+
assert.equal(env.html.classList.contains("dsh-ma-sidebar-open"), false,
|
|
765
|
+
"属性认不到也必须能自动收起(闸门要用适配层自己的状态,不能用官方属性的猜测)");
|
|
766
|
+
assert.ok(clicks() >= 1, "并应点一次官方 toggle(让官方状态与视觉一致)");
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
test("自动收抽屉:点工作区行里的「New session in <工作区>」也收起(= 抽屉里的「选择工作区」入口)", async () => {
|
|
770
|
+
const { js } = injected();
|
|
771
|
+
const env = run(harness({ collapsedAttr: "true", detailsAttr: "true" }), js);
|
|
772
|
+
openDrawerWithoutAttrs(env);
|
|
773
|
+
assert.equal(env.html.classList.contains("dsh-ma-sidebar-open"), true, "前提:抽屉已展开");
|
|
774
|
+
env.wsNewBtn.dispatchEvent(new Ev("click", {}));
|
|
775
|
+
await waitTick();
|
|
776
|
+
assert.equal(env.html.classList.contains("dsh-ma-sidebar-open"), false,
|
|
777
|
+
"在该工作区里新建会话 = 选定了工作区并进入 → 抽屉必须收起(官方 aria-label: New session in <工作区>)");
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("自动收抽屉:点工作区行本身(展开/折叠 disclosure)**不**收起", async () => {
|
|
781
|
+
const { js } = injected();
|
|
782
|
+
const env = run(harness({ collapsedAttr: "true", detailsAttr: "true" }), js);
|
|
783
|
+
openDrawerWithoutAttrs(env);
|
|
784
|
+
env.wsTitle.dispatchEvent(new Ev("click", {})); // 点在标题上 = 官方 onToggle(展开该工作区)
|
|
785
|
+
await waitTick();
|
|
786
|
+
assert.equal(env.html.classList.contains("dsh-ma-sidebar-open"), true,
|
|
787
|
+
"工作区行是 disclosure(官方源码 onClick: onToggle → setGroupExpanded;真机实测 aria-expanded false→true、会话行 6→11 条),"
|
|
788
|
+
+ "点它只是展开列表给用户看,收起抽屉会导致「展开完看不到」");
|
|
789
|
+
env.wsMenuBtn.dispatchEvent(new Ev("click", {})); // 行内「⋯」菜单
|
|
790
|
+
await waitTick();
|
|
791
|
+
assert.equal(env.html.classList.contains("dsh-ma-sidebar-open"), true, "行内「⋯」菜单也是就地操作,不得收起抽屉");
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
test("自动收抽屉:点侧栏里的非交互区域(滚动槽/纯文本)不收起", async () => {
|
|
795
|
+
const { js } = injected();
|
|
796
|
+
const env = run(harness({ collapsedAttr: "true", detailsAttr: "true" }), js);
|
|
797
|
+
openDrawerWithoutAttrs(env);
|
|
798
|
+
env.gutterText.dispatchEvent(new Ev("click", {}));
|
|
799
|
+
env.sidebar.dispatchEvent(new Ev("click", {}));
|
|
800
|
+
await waitTick();
|
|
801
|
+
assert.equal(env.html.classList.contains("dsh-ma-sidebar-open"), true,
|
|
802
|
+
"点侧栏空白/滚动槽不得收起(不能变成「点侧栏里任何东西都收」)");
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
test("自动收抽屉:点 Task Board 入口收起(导航类)", async () => {
|
|
806
|
+
const { js } = injected();
|
|
807
|
+
const env = run(harness({ collapsedAttr: "true", detailsAttr: "true" }), js);
|
|
808
|
+
openDrawerWithoutAttrs(env);
|
|
809
|
+
env.taskBoard.dispatchEvent(new Ev("click", {}));
|
|
810
|
+
await waitTick();
|
|
811
|
+
assert.equal(env.html.classList.contains("dsh-ma-sidebar-open"), false, "Task Board 是切换主视图的导航入口 → 应收起抽屉");
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
test("自动收抽屉:抽屉没开时点会话行不得产生任何副作用(不误点官方 toggle)", async () => {
|
|
815
|
+
const { js } = injected();
|
|
816
|
+
const env = run(harness({ collapsedAttr: "true", detailsAttr: "true" }), js);
|
|
817
|
+
const clicks = toggleClicks(env);
|
|
818
|
+
assert.equal(env.html.classList.contains("dsh-ma-sidebar-open"), false, "前提:抽屉是收起的");
|
|
819
|
+
env.row.dispatchEvent(new Ev("click", {}));
|
|
820
|
+
await waitTick();
|
|
821
|
+
assert.equal(clicks(), 0, "抽屉没开就不该去点官方 toggle(那会把抽屉打开/状态搞乱)");
|
|
822
|
+
assert.equal(env.html.classList.contains("dsh-ma-sidebar-open"), false, "也不得因此打开抽屉");
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
test("自动收抽屉:桌面宽屏(>820)不收起", async () => {
|
|
826
|
+
const { js } = injected();
|
|
827
|
+
const env = run(harness({ collapsedAttr: "true", detailsAttr: "true", width: 1280, height: 900 }), js);
|
|
828
|
+
const clicks = toggleClicks(env);
|
|
829
|
+
env.row.dispatchEvent(new Ev("click", {}));
|
|
830
|
+
await waitTick();
|
|
831
|
+
assert.equal(clicks(), 0, "宽屏没有离屏抽屉这回事 —— 窄屏门必须继续有效");
|
|
832
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.6.9-beta.
|
|
3
|
+
"version": "0.6.9-beta.3",
|
|
4
4
|
"description": "手机远程控制 DeepSeek Harness · Remote control DeepSeek Harness (dsh web) from any phone browser — 100% 全功能 App 级体验:发消息、看工具执行、审批权限、改设置、管凭据,含特权操作,免内网穿透。一条命令安装 npx @mrrisega/dsh-remote。Mobile remote control for DSH, self-host or SaaS, no server needed on LAN.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -2441,7 +2441,7 @@ const PLUGIN_ID = "dsh-remote-web";
|
|
|
2441
2441
|
const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
|
|
2442
2442
|
const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
|
|
2443
2443
|
/** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
|
|
2444
|
-
const PLUGIN_VERSION = "0.6.9-beta.
|
|
2444
|
+
const PLUGIN_VERSION = "0.6.9-beta.3";
|
|
2445
2445
|
const UPDATE_LOG = ".dsh-update.log";
|
|
2446
2446
|
const UPDATE_MARKER = ".dsh-update-running";
|
|
2447
2447
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-web",
|
|
3
|
-
"version": "0.6.9-beta.
|
|
3
|
+
"version": "0.6.9-beta.3",
|
|
4
4
|
"description": "公网远程控制 DeepSeek Harness(dsh web):安装即得专属加密地址,人在外面也能用手机访问电脑上的 dsh——无需同一局域网/WiFi、无需公网 IP 与内网穿透,全程加密;手机端 100% 还原电脑体验(对话/工具/审批/设置)。技术用户可选自建服务,流量走自己的服务器。Remote control DeepSeek Harness (dsh web) from anywhere over the public internet — install, get an encrypted URL, use it from your phone on any network (dual-half cordis plugin: Settings panel + same-origin /dsh-remote routes). (2026-09 由 dsh-remote-ui 更名 / renamed from dsh-remote-ui; dsh-remote 的 dsh web 插件半,不是纯 UI/皮肤插件)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|