@mrrisega/dsh-remote 0.6.18-beta.2 → 0.6.18-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.
@@ -149,19 +149,37 @@ const STYLE = `
149
149
  html.dsh-ma-official-collapsed:not(.dsh-ma-user-wants-open) div.pI_x6G_sidebarCol:not(.dsh-ma-details),
150
150
  html.dsh-ma-official-collapsed:not(.dsh-ma-user-wants-open) div.dsh-ma-sidebar:not(.dsh-ma-details) { transform: translateX(-103%) !important; }
151
151
 
152
- div.pI_x6G_detailsCol, div.pI_x6G_rightbarCol, div.dsh-ma-details, div.dsh-ma-rightbar {
153
- position: fixed; right: 0; top: 0; bottom: 0; margin: 0;
154
- width: min(92vw, 400px); max-width: 96vw;
155
- z-index: 300; overflow: hidden;
156
- border-left: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.25));
157
- box-shadow: 0 10px 44px rgba(0,0,0,.26);
158
- transform: translateX(103%);
159
- transition: transform .22s var(--ds-ease-in-out, ease);
160
- }
161
- html.dsh-ma-details-open div.pI_x6G_detailsCol,
162
- html.dsh-ma-details-open div.pI_x6G_rightbarCol,
163
- html.dsh-ma-details-open div.dsh-ma-details,
164
- html.dsh-ma-details-open div.dsh-ma-rightbar { transform: none; }
152
+ /*
153
+ ══════════════════════════════════════════════════════════════════════════════
154
+ 🚫 这里**故意什么都不写** —— 第三列(官方右栏)的显示/隐藏完全交还给官方。
155
+ ══════════════════════════════════════════════════════════════════════════════
156
+ 历史(2026-09-27 根治,业主拍板 B 方案):
157
+ 0.6.13 起我们把这列改造成自绘浮层 —— position:fixed; right:0; width:min(92vw,400px);
158
+ transform:translateX(103%),再用 html.dsh-ma-details-open … { transform:none } 滑进来。
159
+ 因为"自绘浮层会白框压屏"(2026-09-19 事故),又加了一道很严的闸门(必须"用户 8 秒内
160
+ 在正文里点过"才允许滑入);而 better-sidebar 这类插件只翻**它自己**的 store、官方
161
+ cols.rightbar 永远是 0 → 闸门永远不开 → 于是又在悬浮菜单里补了颗「打开右侧栏」按钮。
162
+
163
+ 🔴 为什么这条路是错的(真机症状:点开之后整屏一层黑遮罩、看不到右栏内容):
164
+ 官方右栏在窄屏**本来就是全屏浮层**,由官方自己做:
165
+ · .P3OORG_panel[data-sidebar-right-panel=fullscreen]{ z-index:40; position:fixed; inset:0 }
166
+ (见 @deepseek-ai/dsh-client-ui-sidebar-right)
167
+ · 官方 ctx.layout.openRightbar(track, fullscreen) —— 窄屏打开时还会顺手收掉左栏
168
+ (viewportWidth < 1024 → narrowExpanded = false)。
169
+ 而 CSS 里**只要祖先有 transform,就会给 position:fixed 的后代创建新的包含块** ——
170
+ 我们那行 translateX(103%) 于是把官方那个 inset:0 的全屏面板**一起推到屏幕外**;
171
+ 与此同时我们的闸门又把 dsh-ma-scrim-on(rgba(0,0,0,.32) 全屏遮罩)打开
172
+ ⇒ 用户看到的正是"整屏变黑、什么也没有"。
173
+ **官方本来就做对了,是我们把它挡住的。**
174
+
175
+ 现在的契约(护栏用例 mobile-adapter-guards 守着):
176
+ ① 适配层**不得**给第三列设 position / transform / width / z-index / 背景;
177
+ ② 不得用类名(如 dsh-ma-details-open)驱动第三列的显隐;
178
+ ③ 不得为第三列开遮罩;
179
+ ④ 不再需要「打开右侧栏」按钮 —— 官方右栏自己的 tab / 会话头按钮就会调 openRightbar。
180
+ 宽屏时官方把它当普通 grid 列(computeColumns),窄屏时当全屏浮层 —— 两种都不需要我们插手。
181
+ 我们只保留 (.dsh-ma-details) 这个**标记**用于"别把第三列误认成左抽屉"(见上面的 :not 守卫)。
182
+ */
165
183
 
166
184
  /* --- 展开遮罩(在浮层下、在官方弹层下) --- */
167
185
  div.dsh-ma-scrim {
@@ -1004,34 +1022,11 @@ const SCRIPT = `(() => {
1004
1022
 
1005
1023
  /* 悬浮按钮/菜单:任何宽度都要有(电脑端访问镜像页同样需要「返回设备列表」的出口),
1006
1024
  与手机端布局改造分开守门;失败也不影响布局适配。 */
1007
- /* —— 「打开右侧栏」的接线(落点见 buildRemoteMenu 里的按钮注释)——
1008
- buildRemoteMenu 在**任何宽度**都会建(电脑端也要有返回设备列表的出口),而驱动第三列滑入的
1009
- 状态机(detailsSticky / centerTouchedAt / sync)只在 boot() 的窄屏段里存在 ——
1010
- 两者作用域不同、执行顺序也不保证(fab 先建)。所以用**一个模块级回调**做唯一接口:
1011
- 菜单按钮只调 maRightPaneRequest(),状态机自己决定怎么开,两边永不互相引用私有变量。 */
1012
- let maOpenRightPane = null; // 由 boot() 的窄屏段注入;null = 还没建好或当前是宽屏
1013
- /** 官方第三列(右栏)的「展开」按钮探测 —— 只用官方 aria-label / 结构,不认第三方标记。
1014
- 官方 0.1.5-rc.2:收起态下会话头有一颗 ExpandButton,展开后它就消失 ——
1015
- 所以"找不到"是正常情况(宽屏本来就不需要),不是错误。 */
1016
- const maRightPaneExpandBtn = () => {
1017
- try {
1018
- const sels = [
1019
- '[aria-label="Expand right sidebar"]', '[aria-label="Open right sidebar"]',
1020
- '[aria-label="Expand right pane"]',
1021
- '[aria-label="展开右侧栏"]', '[aria-label="打开右侧栏"]', '[aria-label="展开右栏"]'
1022
- ];
1023
- for (let i = 0; i < sels.length; i++) {
1024
- const el = document.querySelector(sels[i]);
1025
- if (el) return el;
1026
- }
1027
- } catch (e) { /* 选择器异常/老浏览器:当作找不到 */ }
1028
- return null;
1029
- };
1030
- const maRightPaneRequest = () => {
1031
- if (typeof maOpenRightPane === "function") { try { maOpenRightPane(); return; } catch (e) { /* 落到下面兜底 */ } }
1032
- // 宽屏 / 状态机还没建好:至少替用户点一下官方那颗展开按钮(没有就什么都不做,绝不报错)
1033
- try { const b = maRightPaneExpandBtn(); if (b && b.click) b.click(); } catch (e2) { /* 忽略 */ }
1034
- };
1025
+ /* ❌ 这里曾有 maOpenRightPane / maRightPaneExpandBtn / maRightPaneRequest 三件套 ——
1026
+ 它们是「从 FAB 手动打开官方右栏」的入口。2026-09-27 根治时整条删除:
1027
+ 官方右栏的 tab / 会话头按钮本来就会调 ctx.layout.openRightbar(track, fullscreen),
1028
+ 窄屏还会自动收起左栏(viewportWidth < 1024 → narrowExpanded = false)。
1029
+ 我们插一脚只会把官方推到相反方向(这正是「点开变黑遮罩」那条链的起点)。 */
1035
1030
  let fabDone = false;
1036
1031
  const buildRemoteMenu = () => {
1037
1032
  /* ===== 统一「远程控制」悬浮按钮:字号 + 加密状态 + 返回设备列表 =====
@@ -1105,7 +1100,9 @@ const SCRIPT = `(() => {
1105
1100
  // 「打开右侧栏」(0.6.18):手机端第三列是离屏浮层,滑入闸门要求"用户 8 秒内在正文点过",
1106
1101
  // 而把按钮注册进官方右栏的第三方插件(如 dsh-better-sidebar)只翻它自己的 store,
1107
1102
  // 官方 cols.rightbar 永远是 0 → 面板永远不滑进来。这里给用户一条明路(详见下方 click 处理)。
1108
- '<button type="button" class="dsh-ma-menu-right" data-role="right-pane">📋 打开右侧栏</button>' +
1103
+ // ❌ 这里曾有颗「📋 打开右侧栏」按钮(0.6.18-beta.1/2)。已删:
1104
+ // 它是「我们自绘浮层 → 浮层会挡屏 → 加闸门 → 闸门挡住第三方插件 → 再补个手动入口」
1105
+ // 这条错误链的最后一环。官方右栏自己就能开(窄屏还会自动收左栏),不需要这颗按钮。
1109
1106
  '<button type="button" class="dsh-ma-menu-back">← 返回设备列表</button>' +
1110
1107
  '<div class="dsh-ma-load-warn" data-role="load-warn" hidden>' +
1111
1108
  '<span class="ico">⚠</span>' +
@@ -1288,26 +1285,10 @@ const SCRIPT = `(() => {
1288
1285
  };
1289
1286
  renderRtt();
1290
1287
 
1291
- /* 「打开右侧栏」:手机端第三列(官方右栏)是**离屏浮层**,唯一滑入闸门在 sync() 里:
1292
- detailsAttrLive() && !detailsCollapsed() && (用户 8s 内在正文点过 || detailsSticky)
1293
- 而 better-sidebar 这类"把按钮注册进官方右栏"的插件只翻它**自己**的 store,官方 cols.rightbar
1294
- 永远是 0 → 面板永远不会滑进来(用户报的「右侧按钮区看不见」)。
1295
- 这里给用户一条**明路**:把"我要看右侧栏"登记成和"在正文里点了一下"等价的意图(不受 8s 窗口
1296
- 限制),并在官方确实有展开按钮时替用户点一下。
1297
- ⚠️ 探测只用**官方类名/aria-label/结构**,绝不依赖 better-sidebar 的私有标记
1298
- (data-rightbar-col / data-dsh-center-col)—— 那正是 0.6.15/0.6.16 反复踩的坑:
1299
- 插件换版本或被卸载,标记就没了,适配层会跟着失灵。
1300
- ⚠️ 状态机在 boot() 的窄屏段里(buildRemoteMenu 宽窄都建、且可能更早执行),
1301
- 所以通过模块级回调 maOpenRightPane 单向调用;拿不到回调(宽屏/还没建好)时
1302
- 退化成"只点官方那颗展开按钮"。 */
1303
- const rightBtn = menu.querySelector('[data-role="right-pane"]');
1304
- if (rightBtn) {
1305
- rightBtn.addEventListener("click", (e) => {
1306
- e.stopPropagation(); e.preventDefault();
1307
- closeMenu();
1308
- maRightPaneRequest();
1309
- });
1310
- }
1288
+ /* ❌ 这里曾有「打开右侧栏」这一段与 maRightPane* 三件套 —— 2026-09-27 根治时整条删除。
1289
+ 原因见 STYLE 段顶部:官方右栏在窄屏本来就是全屏浮层,我们把它劫持成自绘浮层
1290
+ (祖先 transform 会给 fixed 后代创建包含块)才导致「点开变整屏黑遮罩」。
1291
+ 官方自己的 tab / 会话头按钮就会调 ctx.layout.openRightbar(track, fullscreen)。 */
1311
1292
 
1312
1293
  /* ---- 交互 ---- */
1313
1294
  /* 菜单跟随按钮:按钮在上半屏 → 菜单挂在它下方;按钮靠左 → 菜单左对齐(都不超出视口)。 */
@@ -1643,12 +1624,10 @@ const SCRIPT = `(() => {
1643
1624
  scrim.addEventListener("click", (e) => {
1644
1625
  e.stopPropagation();
1645
1626
  userWantsOpen = false; // 用户明确要关(遮罩的作用就是关抽屉)
1646
- centerTouchedAt = 0; detailsSticky = false; // 顺带收回"要看详情"的意图(下次在正文里点一下即可恢复)
1647
- rightPaneWanted = false; // 也收回「打开右侧栏」的显式请求
1648
1627
  const t = toggleOf();
1649
1628
  if (t) { t.click(); }
1650
1629
  /* 兜底 + 保险:无论官方 toggle 在不在,都确保"点空白处"能关掉/不再拦截 */
1651
- document.documentElement.classList.remove("dsh-ma-sidebar-open", "dsh-ma-details-open", "dsh-ma-scrim-on");
1630
+ document.documentElement.classList.remove("dsh-ma-sidebar-open", "dsh-ma-scrim-on");
1652
1631
  });
1653
1632
 
1654
1633
  /* 顶部小鲸鱼/菜单按钮 */
@@ -1704,7 +1683,7 @@ const SCRIPT = `(() => {
1704
1683
  } catch (e2) { return false; }
1705
1684
  };
1706
1685
  const sidebarInView = () => isDrawer(sidebar || document.querySelector(".dsh-ma-sidebar"));
1707
- const detailsInView = () => isDrawer(details || document.querySelector(".dsh-ma-details, .dsh-ma-rightbar"));
1686
+ // ❌ 已随 2026-09-27 根治删除:detailsInView(第三列不再由我们判显隐)。
1708
1687
  /**
1709
1688
  * 抽屉列是不是**空壳**:官方已经把内容收起来了,只剩一个宽度还在的固定柱。
1710
1689
  *
@@ -1730,19 +1709,8 @@ const SCRIPT = `(() => {
1730
1709
  return true; // 子节点为空 → 空壳(官方折叠后内容被卸载的情况)
1731
1710
  } catch (e) { return false; }
1732
1711
  };
1733
- /* 第三列折叠态:官方 0.1.2-rc.1 是 data-details-collapsed,0.1.5-rc.2 起是 data-rightbar-collapsed;
1734
- 两个都认;都不存在(未来再改名)时**不当作展开**(见下面 detailsAttrLive)。 */
1735
- const detailsCollapsed = () =>
1736
- truthyAttr(frame, "data-details-collapsed") || truthyAttr(frame, "data-rightbar-collapsed");
1737
- const detailsAttrKnown = () =>
1738
- frame.hasAttribute("data-details-collapsed") || frame.hasAttribute("data-rightbar-collapsed");
1739
- /* 官方这两个属性都是"折叠时才写,展开时**移除**"(实测 0.1.2-rc.1:展开态下属性消失)。
1740
- 所以"属性不在"既可能是"展开",也可能是"官方改名了,我们根本不认识这个属性"——必须区分:
1741
- · 只要**见过**它出现过(detailsSeenCollapsed),就说明这个名字是活的 → 之后的"不在"= 展开;
1742
- · 从没见过 → 不认识 → 一律不认(绝不把未知结构当成"展开的详情列",这是白框事故的根因)。
1743
- data-rightbar-fullscreen 一并算作"这个名字是活的"的旁证(0.1.5 起第三列的另一种开合表达)。 */
1744
- const detailsAttrLive = () =>
1745
- detailsAttrKnown() || detailsSeenCollapsed || frame.hasAttribute("data-rightbar-fullscreen");
1712
+ /* ❌ 已随 2026-09-27 根治删除:detailsCollapsed / detailsAttrKnown / detailsAttrLive 三件套 ——
1713
+ 它们的前提是「由我们决定第三列显隐」,而决定权已交还官方。 */
1746
1714
  /* 状态来源(按优先级):
1747
1715
  ① 用户意图 userWantsOpen —— 点菜单按钮打开 / 点遮罩关闭,期间**不被几何判定推翻**;
1748
1716
  ② 官方 frame 的 data 属性 —— 用户没表达意图时(初始、官方自己切换)以它为准。
@@ -1757,36 +1725,15 @@ const SCRIPT = `(() => {
1757
1725
  let officialExpandedSeen = false;
1758
1726
  let lastOfficialCollapsed = null;
1759
1727
  let scrimTimer = 0, scrimWatchdog = 0;
1760
- /* ---- 详情列(第三列)在手机端的显示条件 ----------------------------------------
1761
- ⚠️ 为什么不能"官方属性说展开就滑进来"(2026-09-19 白框事故的根因之一):
1762
- 详情列在手机端是**离屏浮层**,我们自己的 CSS 用 translateX(103%) 把它推到屏幕右侧外面,
1763
- 到底显不显示**完全由 dsh-ma-details-open 决定**。所以几何永远无法充当"要不要打开"的依据
1764
- (它永远在屏外);反过来,旧实现用带缺陷的几何判定去决定是否滑入,就把
1765
- "官方在宽屏/上次会话遗留的展开态(attr 缺失或 =false)"读成"现在就该显示",
1766
- 于是手机一载入就滑出一个**空的白色 Details 面板**盖住界面。
1767
- 现在的规则(唯一一条,很保守):
1768
- 官方说得清"此刻是展开的" **且** 用户在**主内容列**里点过至少一次 → 才滑入。
1769
- 为什么要求"用户点过主内容列":官方表达展开的方式有两种 —— 属性被移除,或写成 =false
1770
- (实测 0.1.2-rc.1 是"收起才写属性";0.1.5 起第三列改名 rightbar,写法可能与旧版不同)。
1771
- "载入时官方就是展开的"多半是宽屏/上次会话留下的状态,手机端一进来就弹一个空白白面板
1772
- 正是用户报的那个白框;而"用户在正文里点了一下"是**可靠且唯一的**用户意图信号
1773
- (点工具行必然落在主内容列里)。这样两种写法都能正确处理,也不需要猜官方的时序。
1774
- 注:用户点过之后,官方若把属性收回(=收起),下面的 !detailsCollapsed() 立刻让它退出 ——
1775
- 关掉详情面板也是即时的。 */
1776
- let detailsSeenCollapsed = false; // 是否见过官方写出折叠态(证明这个属性名是活的)
1777
- /* 手机端显示详情列的**唯一**依据:用户在主内容列里的点击(见下面 click 委托)。
1778
- · centerTouchedAt:最近一次点击时间 —— 点工具行后官方通常在同一帧内把详情置为展开,
1779
- 给一个几秒窗口足够覆盖"点击 → 官方改属性 → 我们收到通知"的时序;
1780
- · detailsSticky:一经用户动作显示过就粘住(官方没说收起之前不自己缩回去),
1781
- 否则那些 700/1600/…/12000ms 的兜底 re-sync 会在窗口过期后把面板又收起来。 */
1782
- const CENTER_TOUCH_MS = 8000;
1783
- let centerTouchedAt = 0;
1784
- let detailsSticky = false;
1785
- /* 「打开右侧栏」的**显式**请求(0.6.18,来自悬浮菜单那颗按钮)。
1786
- 与 centerTouchedAt/detailsSticky 的区别:那两个是"用户大概想看点细节"的弱信号,
1787
- 这个是"我就是要看官方右栏"的强信号 —— 强信号才允许跳过"官方说未收起"这一条
1788
- (better-sidebar 这类只翻自己 store 的插件永远不会让官方 cols.rightbar 变正)。 */
1789
- let rightPaneWanted = false;
1728
+ /* ❌ 这里曾有「详情列(第三列)在手机端的显示条件」整段判据(白框事故后打的四层闸门)。
1729
+ 2026-09-27 根治时删除:第三列的显隐**完全交还官方** —— 宽屏按 grid 列、窄屏全屏浮层。
1730
+ 我们不再有"要不要显示它"的决定权,也就不需要任何判据。 */
1731
+
1732
+ // ❌ 已随 2026-09-27 根治删除:detailsSeenCollapsed。
1733
+ // ❌ 同一批删除:centerTouchedAt / detailsSticky 的说明(自绘浮层时代的启发式)。
1734
+
1735
+ /* ❌ 已随 2026-09-27 根治删除:CENTER_TOUCH_MS / centerTouchedAt / detailsSticky / rightPaneWanted ——
1736
+ 这套「用户 8 秒内在正文点过 / 从 FAB 明确要求」的启发式闸门,是为自绘浮层打的补丁。 */
1790
1737
  /**
1791
1738
  * 官方右栏里到底有没有"宿主注册的内容"?—— 有内容才值得滑进来。
1792
1739
  * 判据只用**官方结构 + 几何**,绝不认 better-sidebar 的私有标记
@@ -1795,19 +1742,7 @@ const SCRIPT = `(() => {
1795
1742
  * 为什么重要:官方右栏收起时**面板仍然挂载**(只是平移出 frame 右缘),所以"有子节点"
1796
1743
  * 就足以说明"这颗按钮后面真有东西可看",而不是一片空白。
1797
1744
  */
1798
- const rightPaneHasContent = () => {
1799
- try {
1800
- const el = details || document.querySelector(".dsh-ma-details, .dsh-ma-rightbar");
1801
- if (!el) return false;
1802
- const kids = el.children;
1803
- if (!kids || typeof kids.length !== "number") return false;
1804
- if (kids.length === 0) return false;
1805
- /* 官方折叠时宽度会被压到 0;但我们自己在窄屏把它改成 fixed + translateX(103%),
1806
- 所以宽度不再是"官方有没有内容"的判据,只用来排除"被压成 0 宽的列"。 */
1807
- if (typeof el.clientWidth === "number" && el.clientWidth === 0) return false;
1808
- return true;
1809
- } catch (e2) { return false; }
1810
- };
1745
+ // ❌ 已随 2026-09-27 根治删除:rightPaneHasContent(「右栏里有没有内容」也交给官方判断)。
1811
1746
  /* scrim 的"到底有没有在拦"以浏览器算出来的 pointer-events 为准(class 只是我们的意图) */
1812
1747
  const scrimBlocks = () => {
1813
1748
  const html = document.documentElement;
@@ -1819,16 +1754,19 @@ const SCRIPT = `(() => {
1819
1754
  } catch (e2) { /* 忽略 */ }
1820
1755
  return true; // 认不出实际样式 → 以 class 为准(保守:当成在拦)
1821
1756
  };
1757
+ /* 遮罩的「视口里确实有一层可交互抽屉」判据 —— 现在**只认左抽屉**。
1758
+ 第三列(官方右栏)不再是我们的浮层,也就绝不应该让我们的遮罩下闸。 */
1822
1759
  const anyDrawerInView = () =>
1823
- (sidebarInView() && !isShellColumn(sidebar || document.querySelector(".dsh-ma-sidebar"))) || detailsInView();
1760
+ (sidebarInView() && !isShellColumn(sidebar || document.querySelector(".dsh-ma-sidebar")));
1824
1761
  /* 遮罩闸门:**只有此刻真的看得见抽屉**才允许拦截点击。
1825
1762
  抽屉是滑入动画(.22s),所以同步判定必然"还看不见" —— 那一瞬间不下闸即可,
1826
1763
  真正的下闸交给下一帧与动画结束后的复核(applyScrim),既不误拦也不影响手感。
1827
1764
  另外 CSS 侧已收口成"只认 dsh-ma-scrim-on 一个闸",不会再出现"某个 class 常亮 → 整屏被拦"。 */
1828
1765
  const scrimShouldBeOn = () => {
1766
+ // 只认**左抽屉**:第三列(官方右栏)交还官方之后,窄屏时官方自己做全屏浮层,
1767
+ // 不需要(也不该)由我们再盖一层遮罩 —— 那正是「点开变整屏黑」的直接成因。
1829
1768
  const html = document.documentElement;
1830
1769
  if (html.classList.contains("dsh-ma-sidebar-open") && sidebarInView()) return true;
1831
- if (html.classList.contains("dsh-ma-details-open") && detailsInView()) return true;
1832
1770
  return false;
1833
1771
  };
1834
1772
  const applyScrim = () => {
@@ -1858,7 +1796,7 @@ const SCRIPT = `(() => {
1858
1796
  if (enforceScrimInvariant()) { /* 不变量被触发:已摘掉遮罩 */ }
1859
1797
  applyScrim();
1860
1798
  const html = document.documentElement;
1861
- if (!html.classList.contains("dsh-ma-scrim-on") && !html.classList.contains("dsh-ma-sidebar-open") && !html.classList.contains("dsh-ma-details-open")) {
1799
+ if (!html.classList.contains("dsh-ma-scrim-on") && !html.classList.contains("dsh-ma-sidebar-open")) {
1862
1800
  clearInterval(scrimWatchdog); scrimWatchdog = 0;
1863
1801
  }
1864
1802
  }, 800);
@@ -1893,23 +1831,14 @@ const SCRIPT = `(() => {
1893
1831
  保持可见 + 点它可开可关(见其 click 处理),任何异常状态下都留一条出路。
1894
1832
  它的 z-index(320) 高于抽屉(310)/详情列(300)/遮罩(290) —— 判定怎么错都点得到。 */
1895
1833
  if (hamburger) hamburger.style.display = "";
1896
- /* 第三列(详情/rightbar)在手机端要不要滑进来(0.6.18 四层判据):
1897
- ① 官方说得清(detailsAttrLive)+ 官方未收起 + 用户此刻想看 → 滑入(原判据);
1898
- ② **用户从 FAB 明确要求**(rightPaneWanted)+ 右栏确实有内容 → 也滑入。为什么必须绕过
1899
- "官方未收起":better-sidebar 这类把按钮注册进官方右栏的插件只翻它**自己**的 store,
1900
- 官方 cols.rightbar 永远是 0 → 等它松口 = 面板永远打不开(用户报的「右侧按钮区看不见」)。
1901
- 而"有没有内容"用官方结构/几何判,**绝不**认任何第三方私有标记;
1902
- ③ 官方属性改名了(detailsAttrLive 为假)但右栏里确实有内容 → 在用户已表达"要看"的前提下
1903
- 也允许滑入(未来官方再改名的兜底;"载入即滑出白框"由 wantsRightPane 挡掉 —— 它要求
1904
- 先有用户动作,那正是 2026-09-19 白框事故的根因);
1905
- ④ 官方收起 → 立刻退出,并复位所有粘性意图(下次点正文 / 点 FAB 即可恢复)。 */
1906
- if (detailsCollapsed()) { detailsSeenCollapsed = true; detailsSticky = false; rightPaneWanted = false; }
1907
- const touchedRecently = centerTouchedAt !== 0 && (Date.now() - centerTouchedAt) <= CENTER_TOUCH_MS;
1908
- const wantsRightPane = touchedRecently || detailsSticky || rightPaneWanted;
1909
- const officialOpen = detailsAttrLive() && !detailsCollapsed();
1910
- const dOpen = wantsRightPane && (officialOpen || rightPaneHasContent());
1911
- if (dOpen) detailsSticky = true;
1912
- document.documentElement.classList.toggle("dsh-ma-details-open", dOpen);
1834
+ /* ❌ 2026-09-27 根治:这里曾用四层判据决定"第三列(官方右栏)要不要滑进来",
1835
+ 并 toggle dsh-ma-details-open。整段删除 —— 第三列的显隐**完全交还给官方**:
1836
+ · 宽屏:官方按 grid 列摆(computeColumns);
1837
+ · 窄屏:官方把它做成全屏浮层(.P3OORG_panel[data-sidebar-right-panel=fullscreen]
1838
+ { position:fixed; inset:0 }),并且 openRightbar() 时自动收起左栏。
1839
+ 我们插手的两个后果都已真机验证:① 祖先 transform 会给 fixed 后代创建包含块 →
1840
+ 官方全屏面板被推到屏幕外;② 我们再开遮罩 → 只剩"整屏一层黑"。
1841
+ 现在这里**刻意什么都不做**,护栏用例守着「不得出现 dsh-ma-details-open」。 */
1913
1842
  // 遮罩:同步先按当前几何下闸(抽屉已在视口内时无延迟),再在动画开始/结束后复核两次
1914
1843
  applyScrim();
1915
1844
  try {
@@ -1919,46 +1848,10 @@ const SCRIPT = `(() => {
1919
1848
  } catch (e2) { /* 忽略 */ }
1920
1849
  armScrimWatchdog();
1921
1850
  };
1922
- /* 「打开右侧栏」的真正实现(0.6.18;悬浮菜单那颗按钮通过模块级 maOpenRightPane 回调进来)——
1923
- 状态机在这里,菜单那边看不到 detailsSticky/sync(两者作用域与创建时机都不同:
1924
- 菜单宽窄都建、可能更早;状态机只在 boot() 的窄屏段)。
1925
- ① 登记"我就是要看官方右栏"的强意图(不受 8s 窗口限制);
1926
- ② 官方若真有展开按钮就替用户点一下(探测失败是正常的:宽屏/已展开时它本来就不在);
1927
- ③ sync() 立刻按意图生效;官方展开是异步的,动画结束(≈260ms)后再对齐一次。 */
1928
- maOpenRightPane = () => {
1929
- rightPaneWanted = true;
1930
- detailsSticky = true; // 双保险:即便官方 toggle 点不动,判据②也放行
1931
- centerTouchedAt = Date.now();
1932
- try {
1933
- const b = maRightPaneExpandBtn();
1934
- if (b && b.click && detailsCollapsed()) b.click();
1935
- } catch (eR) { /* 探测/点击失败不影响下面的 sync */ }
1936
- sync();
1937
- try { setTimeout(() => { try { sync(); } catch (eS) { /* 忽略 */ } }, 260); } catch (eS2) { /* 忽略 */ }
1938
- };
1939
- /* 用户在主内容列里点一下 → 视为"他要看详情"(手机端显示详情列的唯一开关)。
1940
- 为什么必须由用户动作来解锁:官方表达"详情展开"的方式有两种 —— 属性被移除,或写成 =false,
1941
- 而"载入时官方就是展开的"多半是宽屏/上次会话留下的状态;手机端一进来就滑出一个空白白面板
1942
- 正是用户报的那个白框。点了正文之后才显示,两种写法都能正确处理,也不用去猜官方时序;
1943
- 用户点空白(遮罩)即收回这个意图(见 scrim 的 click 处理)。
1944
- 只认主内容列(.dsh-ma-center),不认抽屉/菜单/适配层自己的 UI —— 不会误触发。 */
1945
- try {
1946
- document.addEventListener("click", (e) => {
1947
- try {
1948
- if (!NARROW()) return;
1949
- const t = e.target;
1950
- if (!t || !t.closest) return;
1951
- if (t.closest(".dsh-ma-sidebar, .dsh-ma-scrim, .dsh-ma-hamburger, .dsh-ma-fab, .dsh-ma-menu")) return;
1952
- if (!t.closest(".dsh-ma-center, .pI_x6G_centerCol")) return;
1953
- centerTouchedAt = Date.now();
1954
- /* 只有"官方此刻就说详情是展开的"才需要立刻重算 —— 否则这一下点击改变不了任何显示,
1955
- 白跑一次 sync()(它要读几次 getBoundingClientRect)。官方稍后把详情置为展开时,
1956
- frame 属性观察器会再触发一次 sync,那时 centerTouchedAt 已经记下了用户意图。 */
1957
- if (detailsCollapsed()) return;
1958
- sync();
1959
- } catch (e2) { /* 忽略 */ }
1960
- }, true);
1961
- } catch (e2) { /* 忽略 */ }
1851
+ /* ❌ 「打开右侧栏」的实现已整条删除(2026-09-27 根治,原因见 STYLE 段顶部的长注释)。 */
1852
+ /* ❌ 「在正文里点一下才允许详情列滑入」的 click 委托已删除(2026-09-27 根治)。
1853
+ 那道启发式闸门存在的唯一理由,是我们把官方右栏改成了自绘浮层、怕它白框压屏;
1854
+ 现在右栏完全交还给官方(官方自己 openRightbar / fullscreen),闸门没有存在意义了)。 */
1962
1855
  try {
1963
1856
  /* 观察 frame 的**全部属性**,再在回调里按名字筛(2026-09-19 手机实测后的加固):
1964
1857
  官方 0.1.5-rc.2 起表达抽屉开合的属性改过名(details→rightbar),将来还可能再改;
@@ -37,6 +37,9 @@ import test from "node:test";
37
37
  import vm from "node:vm";
38
38
  import { injectMobileAdapter } from "../mobile-adapter.mjs";
39
39
 
40
+ /** 适配层源码路径(源码契约类断言直接读它)。 */
41
+ const ADAPTER = new URL("../mobile-adapter.mjs", import.meta.url);
42
+
40
43
  /** 取注入结果里的真实 <script> / <style> 内容(不加壳,原样执行)。 */
41
44
  function injected() {
42
45
  const { html } = injectMobileAdapter("<!doctype html><html><head></head><body></body></html>");
@@ -218,8 +221,8 @@ function zIndexes(css) {
218
221
  // 抽屉规则都带 :not(.dsh-ma-details) 兜底(防止一列被同时标成抽屉与详情列)
219
222
  sidebar: num(/div\.pI_x6G_sidebarCol(?::not\([^)]*\))?,\s*div\.dsh-ma-sidebar(?::not\([^)]*\))?\s*\{[\s\S]{0,500}?z-index:\s*(\d+)/),
220
223
  sidebarOpen: num(/html\.dsh-ma-sidebar-open\s+div\.pI_x6G_sidebarCol(?::not\([^)]*\))?,[\s\S]{0,200}?\{\s*z-index:\s*(\d+)/),
221
- details: num(/div\.pI_x6G_detailsCol,[\s\S]{0,300}?z-index:\s*(\d+)/),
222
224
  hamburger: num(/button\.dsh-ma-hamburger\s*\{[\s\S]{0,700}?z-index:\s*(\d+)/),
225
+ // ❌ 2026-09-27 根治:不再解析 details 的 z-index —— 适配层已不给第三列写任何 CSS。
223
226
  };
224
227
  }
225
228
 
@@ -568,21 +571,47 @@ test("白框:官方属性说「展开」但页面刚载入时不得自动滑
568
571
  assert.equal(env.html.classList.contains("dsh-ma-scrim-on"), false, "没有抽屉 → 遮罩不得拦点击");
569
572
  });
570
573
 
571
- test("白框:官方 收起→展开 的实际切换(用户点了工具行)仍然要能滑入详情", () => {
572
- const { js } = injected();
574
+ test("★ 第三列(官方右栏)的显隐**完全交还官方**:适配层一个字节都不许干预", () => {
575
+ const { js, css } = injected();
573
576
  const env = run(harness({ detailsAttr: "true" }), js);
574
577
  const obs = MutationObserverStub.instances.find((o) => o.targets.some((t) => t.t === env.frame));
575
578
  assert.ok(obs, "应挂了 frame 属性观察器");
576
- assert.equal(env.html.classList.contains("dsh-ma-details-open"), false, "前提:初始为收起");
577
- env.toolRow.dispatchEvent(new Ev("click", {})); // 用户点正文里的工具行
578
- assert.equal(env.html.classList.contains("dsh-ma-details-open"), false, "官方此刻还说收起 → 不得弹面板");
579
- env.frame.removeAttribute("data-details-collapsed"); // 官方展开详情(属性被移除)
579
+
580
+ // 反例一:用户点了正文里的工具行(旧实现在这里"解锁"详情列的滑入)
581
+ env.toolRow.dispatchEvent(new Ev("click", {}));
582
+ assert.equal(env.html.classList.contains("dsh-ma-details-open"), false, "适配层不得再驱动第三列显隐");
583
+
584
+ // 反例二:官方把属性移除 = 官方自己展开了右栏 —— 我们**既不滑入也不拦截**,只是什么都不做
585
+ env.frame.removeAttribute("data-details-collapsed");
580
586
  obs.fire("data-details-collapsed");
581
- assert.equal(env.html.classList.contains("dsh-ma-details-open"), true,
582
- "用户点过正文 + 官方展开详情 → 必须能滑入(不能为了防白框把功能废掉)");
583
- // 详情列真的滑进视口后,遮罩才允许拦住它背后 —— 正常行为不能被改坏
584
587
  env.doc.dispatchEvent(new Ev("resize", {}));
585
- assert.equal(env.html.classList.contains("dsh-ma-scrim-on"), true, "详情列真的在视口里时,遮罩应正常生效");
588
+ assert.equal(env.html.classList.contains("dsh-ma-details-open"), false,
589
+ "官方展开右栏时,适配层不得再插一脚(官方的全屏浮层由官方自己摆)");
590
+ assert.equal(env.html.classList.contains("dsh-ma-scrim-on"), false,
591
+ "★ 也**绝对不得**为第三列开遮罩 —— 那正是「点开变整屏黑」的直接成因");
592
+
593
+ // 样式侧:不得给第三列设定位/位移/层级(祖先 transform 会给 fixed 后代创建包含块 → 把官方全屏面板推出屏外)
594
+ // ⚠️ 必须**只看生效的 CSS**:STYLE 是一个模板字符串,注释也是文本,直接扫会命中「历史说明」里的字样。
595
+ const cssCode = String(css).replace(/\/\*[\s\S]*?\*\//g, "");
596
+ assert.ok(!/translateX\(103%\)/.test(cssCode), "★ 不得再把第三列推到屏外(官方窄屏是全屏浮层,会被一起推走)");
597
+ assert.ok(!/dsh-ma-details-open/.test(cssCode), "★ 不得再有 dsh-ma-details-open 驱动的样式");
598
+ assert.equal(/div\.pI_x6G_detailsCol[^{]*\{[^}]*\}/.exec(cssCode), null,
599
+ "★ 适配层不得给第三列写任何 CSS 规则(position/transform/width/z-index 一律不许)");
600
+ assert.equal(/div\.dsh-ma-details[^{]*\{[^}]*\}/.exec(cssCode), null,
601
+ "★ 也不得给 dsh-ma-details 标记写规则");
602
+ });
603
+
604
+ test("★ 遮罩只跟左抽屉走:第三列(右栏)在视口里也绝不让遮罩下闸", () => {
605
+ const { js } = injected();
606
+ const env = run(harness({ collapsedAttr: "true", detailsAttr: "true" }), js);
607
+ const obs = MutationObserverStub.instances.find((o) => o.targets.some((t) => t.t === env.frame));
608
+ // 人为把第三列摆进视口(模拟官方自己展开了右栏)
609
+ env.third.getBoundingClientRect = () => ({ left: 40, top: 0, right: 380, bottom: 800, width: 340, height: 800 });
610
+ env.frame.removeAttribute("data-details-collapsed");
611
+ obs.fire("data-details-collapsed");
612
+ env.doc.dispatchEvent(new Ev("resize", {}));
613
+ assert.equal(env.html.classList.contains("dsh-ma-scrim-on"), false,
614
+ "★ 第三列在视口里 ≠ 我们要开遮罩:官方右栏(尤其窄屏全屏浮层)不需要我们的遮罩");
586
615
  });
587
616
 
588
617
  test("白框:没有用户动作时官方自己翻转成展开(改名/重渲染)不得弹白框", () => {
@@ -597,38 +626,49 @@ test("白框:没有用户动作时官方自己翻转成展开(改名/重渲
597
626
  assert.equal(env.html.classList.contains("dsh-ma-scrim-on"), false, "同理:遮罩不得跟着拦点击");
598
627
  });
599
628
 
600
- test("白框:详情面板显示后点空白(遮罩)能关掉,且不会自己弹回来", () => {
629
+ test("遮罩点击:只关左抽屉,不碰第三列", () => {
601
630
  const { js } = injected();
602
- const env = run(harness({ detailsAttr: "true" }), js);
603
- const obs = MutationObserverStub.instances.find((o) => o.targets.some((t) => t.t === env.frame));
604
- env.toolRow.dispatchEvent(new Ev("click", {}));
605
- env.frame.removeAttribute("data-details-collapsed");
606
- obs.fire("data-details-collapsed");
631
+ const env = run(harness({ collapsedAttr: "true", detailsAttr: "true" }), js);
632
+ const ham = findHamburger(env);
633
+ ham.dispatchEvent(new Ev("click", {}));
607
634
  env.doc.dispatchEvent(new Ev("resize", {}));
608
- assert.equal(env.html.classList.contains("dsh-ma-details-open"), true, "前提:用户点过正文后详情已显示");
635
+ env.html.classList.add("dsh-ma-details-open"); // 人为制造"第三列也开着"的遗留状态
636
+ assert.equal(env.html.classList.contains("dsh-ma-scrim-on"), true, "前提:抽屉展开、遮罩在拦");
609
637
  findScrim(env).dispatchEvent(new Ev("click", {}));
610
- assert.equal(env.html.classList.contains("dsh-ma-details-open"), false, "点空白应关掉详情面板");
611
- // 后续任何一次兜底 re-sync 都不得把用户明确关掉的面板又弹回来
612
- env.doc.dispatchEvent(new Ev("resize", {}));
613
- obs.fire("data-rightbar-collapsed");
614
- assert.equal(env.html.classList.contains("dsh-ma-details-open"), false,
615
- "用户点空白关掉之后,「要看详情」的意图必须被收回,不得自己弹回");
638
+ assert.equal(env.html.classList.contains("dsh-ma-sidebar-open"), false, "点空白应关掉左抽屉");
639
+ assert.equal(env.html.classList.contains("dsh-ma-scrim-on"), false, "遮罩也要撤掉");
640
+ // 我们**根本不设置** dsh-ma-details-open,所以这里断言的是"源码里不许出现它"(见下面的源码契约)。
641
+ // (不要求"清掉" —— 我们连碰都不该碰第三列。)
616
642
  });
617
643
 
618
- test("白框:详情列在视口里时遮罩生效;官方收起来后遮罩立刻撤掉", () => {
644
+ test("★ 源码契约:适配层代码里不许再出现任何「驱动第三列」的符号(去注释后扫)", () => {
645
+ const src = readFileSync(ADAPTER, "utf8");
646
+ // 只看**代码**:STYLE 是模板字符串、注释也是文本,直接扫会命中"历史说明"
647
+ const code = src
648
+ .replace(/\/\*[\s\S]*?\*\//g, "") // 块注释
649
+ .split("\n").map((l) => l.replace(/\/\/(?![^"'\n]*["']).*$/, "")) // 行注释(粗略:不动 http:// 之类)
650
+ .join("\n");
651
+ for (const sym of ["dsh-ma-details-open", "detailsSticky", "centerTouchedAt", "rightPaneWanted",
652
+ "rightPaneHasContent", "detailsAttrLive", "detailsCollapsed", "detailsSeenCollapsed",
653
+ "maRightPaneRequest", "maOpenRightPane", "maRightPaneExpandBtn"]) {
654
+ assert.ok(!code.includes(sym),
655
+ `★ 代码里不得再出现 ${sym} —— 第三列的显隐已完全交还官方(2026-09-27 根治)`);
656
+ }
657
+ });
658
+
659
+ test("★ 回归锁:第三列在视口里 + 官方收起,遮罩都不为它动一下", () => {
619
660
  const { js } = injected();
620
661
  const env = run(harness({ detailsAttr: "true" }), js);
621
662
  const obs = MutationObserverStub.instances.find((o) => o.targets.some((t) => t.t === env.frame));
622
- env.toolRow.dispatchEvent(new Ev("click", {}));
663
+ env.third.getBoundingClientRect = () => ({ left: 40, top: 0, right: 380, bottom: 800, width: 340, height: 800 });
623
664
  env.frame.removeAttribute("data-details-collapsed");
624
665
  obs.fire("data-details-collapsed");
625
666
  env.doc.dispatchEvent(new Ev("resize", {}));
626
- assert.equal(env.html.classList.contains("dsh-ma-scrim-on"), true, "前提:详情列展开且遮罩生效");
667
+ assert.equal(env.html.classList.contains("dsh-ma-scrim-on"), false, "第三列展开 → 遮罩不为它下闸");
627
668
  env.frame.setAttribute("data-details-collapsed", "true");
628
669
  obs.fire("data-details-collapsed");
629
670
  env.doc.dispatchEvent(new Ev("resize", {}));
630
- assert.equal(env.html.classList.contains("dsh-ma-details-open"), false, "官方收起 → 详情 class 撤掉");
631
- assert.equal(env.html.classList.contains("dsh-ma-scrim-on"), false, "官方收起 → 遮罩必须一起撤掉");
671
+ assert.equal(env.html.classList.contains("dsh-ma-scrim-on"), false, "第三列收起 → 遮罩也不为它变化");
632
672
  });
633
673
 
634
674
  test("遮罩不变量:遮罩在拦点击但视口内没有任何可交互抽屉 → 看门狗立刻摘掉", () => {
@@ -664,9 +704,9 @@ test("抽屉可点:抽屉展开时抽屉内点击命中抽屉自身(不被
664
704
  const z = zIndexes(css);
665
705
  assert.ok(z.sidebarOpen > z.scrim,
666
706
  `抽屉展开时的层级(${z.sidebarOpen})必须高于遮罩(${z.scrim}) —— 否则抽屉里所有点击都被遮罩吃掉`);
667
- assert.ok(z.sidebarOpen > z.details,
668
- `抽屉展开时的层级(${z.sidebarOpen})必须高于详情列(${z.details}) —— 详情列在 frame 里排在抽屉之后,同级会压住抽屉`);
669
- assert.ok(z.hamburger > z.details && z.hamburger > z.sidebarOpen,
707
+ // ❌ 2026-09-27 根治:不再断言「抽屉必须高于详情列」 —— 适配层已不给第三列设 z-index,
708
+ // 它回到官方布局里,不再是我们自绘的浮层,也就不可能压住抽屉。
709
+ assert.ok(z.hamburger > z.sidebarOpen,
670
710
  "汉堡按钮必须始终在最上层(判定怎么错都点得到 = 唯一自救出口)");
671
711
 
672
712
  const env = run(harness({ collapsedAttr: "true", detailsAttr: "true" }), js);
@@ -677,7 +717,7 @@ test("抽屉可点:抽屉展开时抽屉内点击命中抽屉自身(不被
677
717
  const r = env.row.getBoundingClientRect();
678
718
  const hit = hitTest(env, z, r.left + r.width / 2, r.top + r.height / 2);
679
719
  assert.equal(hit, env.row, "抽屉展开时,抽屉内的点击必须落在抽屉内容上");
680
- // 最坏情况:详情列也同时被判成展开 —— 抽屉仍然必须压得住(能在抽屉里点选)
720
+ // 最坏情况:即便有人(旧版本/官方)把 dsh-ma-details-open 挂上,抽屉内的点击也必须有效
681
721
  env.html.classList.add("dsh-ma-details-open");
682
722
  const hit2 = hitTest(env, z, r.left + r.width / 2, r.top + r.height / 2);
683
723
  assert.equal(hit2, env.row, "即便详情列同时展开,抽屉内的点击也必须有效(不能把用户困住)");
@@ -976,31 +1016,40 @@ test("白柱:汉堡按钮不再无条件点官方 toggle(两态不得互相
976
1016
  assert.equal(env.html.classList.contains("dsh-ma-sidebar-open"), true, "第二次点击应把抽屉打开(意图生效)");
977
1017
  });
978
1018
 
979
- test("打开右侧栏:官方右栏里有宿主注册的内容时,FAB 菜单那颗按钮必须能把它滑进来", () => {
1019
+ test("★ 官方右栏里有内容时:适配层**不插手**(交还官方 openRightbar),菜单里也不再有「打开右侧栏」按钮", () => {
980
1020
  const { js } = injected();
981
- // 现场(用户报的「右侧按钮区看不见」):better-sidebar 这类插件把 tab 注册进官方右栏,
982
- // 但它只翻自己的 store → 官方 cols.rightbar 永远是 0(data-rightbar-collapsed 恒为 true)。
1021
+ // 现场(用户 2026-09-27 反馈):better-sidebar 把 tab 注册进官方右栏,官方 cols.rightbar 仍为 0。
1022
+ // 旧做法是我们自绘浮层 + 手动按钮 → 祖先 transform 把官方全屏面板推出屏外 + 我们再开遮罩
1023
+ // → 用户看到「整屏一层黑」。
983
1024
  const env = harness({ collapsedAttr: "true", variant: "rc2", detailsAttr: null, rightbarAttr: "true" });
984
- env.third.appendChild(env.doc.createElement("div")); // 官方右栏里确实有内容(插件注册的 tab 面板)
1025
+ env.third.appendChild(env.doc.createElement("div"));
985
1026
  run(env, js);
986
- assert.equal(env.html.classList.contains("dsh-ma-details-open"), false, "前提:默认不显示右栏");
987
1027
  const menu = env.body.children.find((c) => c._cls.has("dsh-ma-menu"));
988
1028
  assert.ok(menu, "应有悬浮菜单");
989
- const btn = menu.querySelector('[data-role="right-pane"]');
990
- assert.ok(btn, "菜单里必须有「打开右侧栏」按钮(用户唯一的明路)");
991
- btn.dispatchEvent(new Ev("click", {}));
992
- assert.equal(env.html.classList.contains("dsh-ma-details-open"), true,
993
- "点「打开右侧栏」后,即便官方说收起(第三方插件不会让官方展开),也必须把有内容的右栏滑进来");
994
- assert.equal(menu.hidden, true, "点完应关闭菜单(让用户看见滑入的面板)");
1029
+ assert.equal(menu.querySelector('[data-role="right-pane"]'), null,
1030
+ "★ 菜单里不该再有「打开右侧栏」按钮 —— 官方右栏自己的 tab / 会话头按钮就能开");
1031
+ assert.equal(env.html.classList.contains("dsh-ma-details-open"), false,
1032
+ "★ 适配层不得替官方决定右栏显隐");
1033
+ assert.equal(env.html.classList.contains("dsh-ma-scrim-on"), false, "★ 更不得为它开遮罩");
995
1034
  });
996
1035
 
997
- test("打开右侧栏:右栏里没有任何内容时不得滑出一个空白面板(2026-09-19 白框事故的回归)", () => {
1036
+ test("★ 2026-09-19 白框事故的**结构性**回归锁:适配层任何情况下都不滑出第三列", () => {
998
1037
  const { js } = injected();
999
- const env = run(harness({ collapsedAttr: "true", variant: "rc2", detailsAttr: null, rightbarAttr: "true" }), js);
1000
- const menu = env.body.children.find((c) => c._cls.has("dsh-ma-menu"));
1001
- menu.querySelector('[data-role="right-pane"]').dispatchEvent(new Ev("click", {}));
1002
- assert.equal(env.html.classList.contains("dsh-ma-details-open"), false,
1003
- "官方右栏里什么都没有 → 宁可什么都不发生,也不弹一个空白白面板(那正是白框事故)");
1038
+ // 当年白框的三种触发现场各来一遍,断言"我们什么都不做" ⇒ 白框在结构上不可能再出现
1039
+ for (const opts of [
1040
+ { collapsedAttr: "true", variant: "rc2", detailsAttr: null, rightbarAttr: "true" }, // 官方说收起 + 右栏有内容
1041
+ { collapsedAttr: "true", variant: "rc2", detailsAttr: null, rightbarAttr: null }, // 官方属性说不清 + 有内容
1042
+ { collapsedAttr: "true", detailsAttr: "false" } // 官方说第三列展开(左抽屉仍收起)
1043
+ ]) {
1044
+ const env = harness(opts);
1045
+ env.third.appendChild(env.doc.createElement("div"));
1046
+ run(env, js);
1047
+ env.doc.dispatchEvent(new Ev("resize", {}));
1048
+ assert.equal(env.html.classList.contains("dsh-ma-details-open"), false,
1049
+ "适配层不得滑出第三列(白框的结构性杜绝): " + JSON.stringify(opts));
1050
+ assert.equal(env.html.classList.contains("dsh-ma-scrim-on"), false,
1051
+ "也不得为它开遮罩: " + JSON.stringify(opts));
1052
+ }
1004
1053
  });
1005
1054
 
1006
1055
  test("打开右侧栏:绝不允许「载入即滑出白框」——必须由用户动作解锁", () => {
@@ -481,13 +481,16 @@ test("悬浮菜单:预览行已彻底删除(DOM 里不得再有 .dsh-ma-menu
481
481
  assert.match(menu.innerHTML, /只影响这台设备的远程页面/, "字号说明文案应保留");
482
482
  });
483
483
 
484
- test("悬浮菜单:新增「网络延迟」一行与「打开右侧栏」按钮(只读文案 + 显式出口)", () => {
484
+ test("悬浮菜单:有「网络延迟」一行(只读文案);**没有**「打开右侧栏」按钮", () => {
485
485
  const env = runAdapter(harness({ width: 390 }));
486
486
  const menu = findMenu(env);
487
487
  assert.ok(menu.querySelector(".dsh-ma-menu-net"), "菜单里应有网络延迟块");
488
488
  assert.match(menu.innerHTML, /网络延迟/, "应有「网络延迟」小标题");
489
489
  assert.ok(menu.querySelector('[data-role="net"]'), "延迟块要带 data-role=net(供渲染函数更新)");
490
- assert.ok(menu.querySelector('[data-role="right-pane"]'), "菜单里应有「打开右侧栏」按钮");
490
+ // ❌ 2026-09-27 根治:那颗按钮已删 —— 官方右栏自己的 tab / 会话头按钮就会调 ctx.layout.openRightbar,
491
+ // 我们再造一个手动入口只会把官方推到相反方向(真机症状:点开变整屏黑遮罩)。
492
+ assert.equal(menu.querySelector('[data-role="right-pane"]'), null,
493
+ "菜单里不该再有「打开右侧栏」按钮(官方右栏自己能开)");
491
494
  });
492
495
 
493
496
  test("网络延迟:只在**已有**的 fetch 包装里采样,成功样本显示成角标(绝不主动测速)", async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrisega/dsh-remote",
3
- "version": "0.6.18-beta.2",
3
+ "version": "0.6.18-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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-web",
3
- "version": "0.6.18-beta.2",
3
+ "version": "0.6.18-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",
@@ -149,19 +149,37 @@ const STYLE = `
149
149
  html.dsh-ma-official-collapsed:not(.dsh-ma-user-wants-open) div.pI_x6G_sidebarCol:not(.dsh-ma-details),
150
150
  html.dsh-ma-official-collapsed:not(.dsh-ma-user-wants-open) div.dsh-ma-sidebar:not(.dsh-ma-details) { transform: translateX(-103%) !important; }
151
151
 
152
- div.pI_x6G_detailsCol, div.pI_x6G_rightbarCol, div.dsh-ma-details, div.dsh-ma-rightbar {
153
- position: fixed; right: 0; top: 0; bottom: 0; margin: 0;
154
- width: min(92vw, 400px); max-width: 96vw;
155
- z-index: 300; overflow: hidden;
156
- border-left: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.25));
157
- box-shadow: 0 10px 44px rgba(0,0,0,.26);
158
- transform: translateX(103%);
159
- transition: transform .22s var(--ds-ease-in-out, ease);
160
- }
161
- html.dsh-ma-details-open div.pI_x6G_detailsCol,
162
- html.dsh-ma-details-open div.pI_x6G_rightbarCol,
163
- html.dsh-ma-details-open div.dsh-ma-details,
164
- html.dsh-ma-details-open div.dsh-ma-rightbar { transform: none; }
152
+ /*
153
+ ══════════════════════════════════════════════════════════════════════════════
154
+ 🚫 这里**故意什么都不写** —— 第三列(官方右栏)的显示/隐藏完全交还给官方。
155
+ ══════════════════════════════════════════════════════════════════════════════
156
+ 历史(2026-09-27 根治,业主拍板 B 方案):
157
+ 0.6.13 起我们把这列改造成自绘浮层 —— position:fixed; right:0; width:min(92vw,400px);
158
+ transform:translateX(103%),再用 html.dsh-ma-details-open … { transform:none } 滑进来。
159
+ 因为"自绘浮层会白框压屏"(2026-09-19 事故),又加了一道很严的闸门(必须"用户 8 秒内
160
+ 在正文里点过"才允许滑入);而 better-sidebar 这类插件只翻**它自己**的 store、官方
161
+ cols.rightbar 永远是 0 → 闸门永远不开 → 于是又在悬浮菜单里补了颗「打开右侧栏」按钮。
162
+
163
+ 🔴 为什么这条路是错的(真机症状:点开之后整屏一层黑遮罩、看不到右栏内容):
164
+ 官方右栏在窄屏**本来就是全屏浮层**,由官方自己做:
165
+ · .P3OORG_panel[data-sidebar-right-panel=fullscreen]{ z-index:40; position:fixed; inset:0 }
166
+ (见 @deepseek-ai/dsh-client-ui-sidebar-right)
167
+ · 官方 ctx.layout.openRightbar(track, fullscreen) —— 窄屏打开时还会顺手收掉左栏
168
+ (viewportWidth < 1024 → narrowExpanded = false)。
169
+ 而 CSS 里**只要祖先有 transform,就会给 position:fixed 的后代创建新的包含块** ——
170
+ 我们那行 translateX(103%) 于是把官方那个 inset:0 的全屏面板**一起推到屏幕外**;
171
+ 与此同时我们的闸门又把 dsh-ma-scrim-on(rgba(0,0,0,.32) 全屏遮罩)打开
172
+ ⇒ 用户看到的正是"整屏变黑、什么也没有"。
173
+ **官方本来就做对了,是我们把它挡住的。**
174
+
175
+ 现在的契约(护栏用例 mobile-adapter-guards 守着):
176
+ ① 适配层**不得**给第三列设 position / transform / width / z-index / 背景;
177
+ ② 不得用类名(如 dsh-ma-details-open)驱动第三列的显隐;
178
+ ③ 不得为第三列开遮罩;
179
+ ④ 不再需要「打开右侧栏」按钮 —— 官方右栏自己的 tab / 会话头按钮就会调 openRightbar。
180
+ 宽屏时官方把它当普通 grid 列(computeColumns),窄屏时当全屏浮层 —— 两种都不需要我们插手。
181
+ 我们只保留 (.dsh-ma-details) 这个**标记**用于"别把第三列误认成左抽屉"(见上面的 :not 守卫)。
182
+ */
165
183
 
166
184
  /* --- 展开遮罩(在浮层下、在官方弹层下) --- */
167
185
  div.dsh-ma-scrim {
@@ -1004,34 +1022,11 @@ const SCRIPT = `(() => {
1004
1022
 
1005
1023
  /* 悬浮按钮/菜单:任何宽度都要有(电脑端访问镜像页同样需要「返回设备列表」的出口),
1006
1024
  与手机端布局改造分开守门;失败也不影响布局适配。 */
1007
- /* —— 「打开右侧栏」的接线(落点见 buildRemoteMenu 里的按钮注释)——
1008
- buildRemoteMenu 在**任何宽度**都会建(电脑端也要有返回设备列表的出口),而驱动第三列滑入的
1009
- 状态机(detailsSticky / centerTouchedAt / sync)只在 boot() 的窄屏段里存在 ——
1010
- 两者作用域不同、执行顺序也不保证(fab 先建)。所以用**一个模块级回调**做唯一接口:
1011
- 菜单按钮只调 maRightPaneRequest(),状态机自己决定怎么开,两边永不互相引用私有变量。 */
1012
- let maOpenRightPane = null; // 由 boot() 的窄屏段注入;null = 还没建好或当前是宽屏
1013
- /** 官方第三列(右栏)的「展开」按钮探测 —— 只用官方 aria-label / 结构,不认第三方标记。
1014
- 官方 0.1.5-rc.2:收起态下会话头有一颗 ExpandButton,展开后它就消失 ——
1015
- 所以"找不到"是正常情况(宽屏本来就不需要),不是错误。 */
1016
- const maRightPaneExpandBtn = () => {
1017
- try {
1018
- const sels = [
1019
- '[aria-label="Expand right sidebar"]', '[aria-label="Open right sidebar"]',
1020
- '[aria-label="Expand right pane"]',
1021
- '[aria-label="展开右侧栏"]', '[aria-label="打开右侧栏"]', '[aria-label="展开右栏"]'
1022
- ];
1023
- for (let i = 0; i < sels.length; i++) {
1024
- const el = document.querySelector(sels[i]);
1025
- if (el) return el;
1026
- }
1027
- } catch (e) { /* 选择器异常/老浏览器:当作找不到 */ }
1028
- return null;
1029
- };
1030
- const maRightPaneRequest = () => {
1031
- if (typeof maOpenRightPane === "function") { try { maOpenRightPane(); return; } catch (e) { /* 落到下面兜底 */ } }
1032
- // 宽屏 / 状态机还没建好:至少替用户点一下官方那颗展开按钮(没有就什么都不做,绝不报错)
1033
- try { const b = maRightPaneExpandBtn(); if (b && b.click) b.click(); } catch (e2) { /* 忽略 */ }
1034
- };
1025
+ /* ❌ 这里曾有 maOpenRightPane / maRightPaneExpandBtn / maRightPaneRequest 三件套 ——
1026
+ 它们是「从 FAB 手动打开官方右栏」的入口。2026-09-27 根治时整条删除:
1027
+ 官方右栏的 tab / 会话头按钮本来就会调 ctx.layout.openRightbar(track, fullscreen),
1028
+ 窄屏还会自动收起左栏(viewportWidth < 1024 → narrowExpanded = false)。
1029
+ 我们插一脚只会把官方推到相反方向(这正是「点开变黑遮罩」那条链的起点)。 */
1035
1030
  let fabDone = false;
1036
1031
  const buildRemoteMenu = () => {
1037
1032
  /* ===== 统一「远程控制」悬浮按钮:字号 + 加密状态 + 返回设备列表 =====
@@ -1105,7 +1100,9 @@ const SCRIPT = `(() => {
1105
1100
  // 「打开右侧栏」(0.6.18):手机端第三列是离屏浮层,滑入闸门要求"用户 8 秒内在正文点过",
1106
1101
  // 而把按钮注册进官方右栏的第三方插件(如 dsh-better-sidebar)只翻它自己的 store,
1107
1102
  // 官方 cols.rightbar 永远是 0 → 面板永远不滑进来。这里给用户一条明路(详见下方 click 处理)。
1108
- '<button type="button" class="dsh-ma-menu-right" data-role="right-pane">📋 打开右侧栏</button>' +
1103
+ // ❌ 这里曾有颗「📋 打开右侧栏」按钮(0.6.18-beta.1/2)。已删:
1104
+ // 它是「我们自绘浮层 → 浮层会挡屏 → 加闸门 → 闸门挡住第三方插件 → 再补个手动入口」
1105
+ // 这条错误链的最后一环。官方右栏自己就能开(窄屏还会自动收左栏),不需要这颗按钮。
1109
1106
  '<button type="button" class="dsh-ma-menu-back">← 返回设备列表</button>' +
1110
1107
  '<div class="dsh-ma-load-warn" data-role="load-warn" hidden>' +
1111
1108
  '<span class="ico">⚠</span>' +
@@ -1288,26 +1285,10 @@ const SCRIPT = `(() => {
1288
1285
  };
1289
1286
  renderRtt();
1290
1287
 
1291
- /* 「打开右侧栏」:手机端第三列(官方右栏)是**离屏浮层**,唯一滑入闸门在 sync() 里:
1292
- detailsAttrLive() && !detailsCollapsed() && (用户 8s 内在正文点过 || detailsSticky)
1293
- 而 better-sidebar 这类"把按钮注册进官方右栏"的插件只翻它**自己**的 store,官方 cols.rightbar
1294
- 永远是 0 → 面板永远不会滑进来(用户报的「右侧按钮区看不见」)。
1295
- 这里给用户一条**明路**:把"我要看右侧栏"登记成和"在正文里点了一下"等价的意图(不受 8s 窗口
1296
- 限制),并在官方确实有展开按钮时替用户点一下。
1297
- ⚠️ 探测只用**官方类名/aria-label/结构**,绝不依赖 better-sidebar 的私有标记
1298
- (data-rightbar-col / data-dsh-center-col)—— 那正是 0.6.15/0.6.16 反复踩的坑:
1299
- 插件换版本或被卸载,标记就没了,适配层会跟着失灵。
1300
- ⚠️ 状态机在 boot() 的窄屏段里(buildRemoteMenu 宽窄都建、且可能更早执行),
1301
- 所以通过模块级回调 maOpenRightPane 单向调用;拿不到回调(宽屏/还没建好)时
1302
- 退化成"只点官方那颗展开按钮"。 */
1303
- const rightBtn = menu.querySelector('[data-role="right-pane"]');
1304
- if (rightBtn) {
1305
- rightBtn.addEventListener("click", (e) => {
1306
- e.stopPropagation(); e.preventDefault();
1307
- closeMenu();
1308
- maRightPaneRequest();
1309
- });
1310
- }
1288
+ /* ❌ 这里曾有「打开右侧栏」这一段与 maRightPane* 三件套 —— 2026-09-27 根治时整条删除。
1289
+ 原因见 STYLE 段顶部:官方右栏在窄屏本来就是全屏浮层,我们把它劫持成自绘浮层
1290
+ (祖先 transform 会给 fixed 后代创建包含块)才导致「点开变整屏黑遮罩」。
1291
+ 官方自己的 tab / 会话头按钮就会调 ctx.layout.openRightbar(track, fullscreen)。 */
1311
1292
 
1312
1293
  /* ---- 交互 ---- */
1313
1294
  /* 菜单跟随按钮:按钮在上半屏 → 菜单挂在它下方;按钮靠左 → 菜单左对齐(都不超出视口)。 */
@@ -1643,12 +1624,10 @@ const SCRIPT = `(() => {
1643
1624
  scrim.addEventListener("click", (e) => {
1644
1625
  e.stopPropagation();
1645
1626
  userWantsOpen = false; // 用户明确要关(遮罩的作用就是关抽屉)
1646
- centerTouchedAt = 0; detailsSticky = false; // 顺带收回"要看详情"的意图(下次在正文里点一下即可恢复)
1647
- rightPaneWanted = false; // 也收回「打开右侧栏」的显式请求
1648
1627
  const t = toggleOf();
1649
1628
  if (t) { t.click(); }
1650
1629
  /* 兜底 + 保险:无论官方 toggle 在不在,都确保"点空白处"能关掉/不再拦截 */
1651
- document.documentElement.classList.remove("dsh-ma-sidebar-open", "dsh-ma-details-open", "dsh-ma-scrim-on");
1630
+ document.documentElement.classList.remove("dsh-ma-sidebar-open", "dsh-ma-scrim-on");
1652
1631
  });
1653
1632
 
1654
1633
  /* 顶部小鲸鱼/菜单按钮 */
@@ -1704,7 +1683,7 @@ const SCRIPT = `(() => {
1704
1683
  } catch (e2) { return false; }
1705
1684
  };
1706
1685
  const sidebarInView = () => isDrawer(sidebar || document.querySelector(".dsh-ma-sidebar"));
1707
- const detailsInView = () => isDrawer(details || document.querySelector(".dsh-ma-details, .dsh-ma-rightbar"));
1686
+ // ❌ 已随 2026-09-27 根治删除:detailsInView(第三列不再由我们判显隐)。
1708
1687
  /**
1709
1688
  * 抽屉列是不是**空壳**:官方已经把内容收起来了,只剩一个宽度还在的固定柱。
1710
1689
  *
@@ -1730,19 +1709,8 @@ const SCRIPT = `(() => {
1730
1709
  return true; // 子节点为空 → 空壳(官方折叠后内容被卸载的情况)
1731
1710
  } catch (e) { return false; }
1732
1711
  };
1733
- /* 第三列折叠态:官方 0.1.2-rc.1 是 data-details-collapsed,0.1.5-rc.2 起是 data-rightbar-collapsed;
1734
- 两个都认;都不存在(未来再改名)时**不当作展开**(见下面 detailsAttrLive)。 */
1735
- const detailsCollapsed = () =>
1736
- truthyAttr(frame, "data-details-collapsed") || truthyAttr(frame, "data-rightbar-collapsed");
1737
- const detailsAttrKnown = () =>
1738
- frame.hasAttribute("data-details-collapsed") || frame.hasAttribute("data-rightbar-collapsed");
1739
- /* 官方这两个属性都是"折叠时才写,展开时**移除**"(实测 0.1.2-rc.1:展开态下属性消失)。
1740
- 所以"属性不在"既可能是"展开",也可能是"官方改名了,我们根本不认识这个属性"——必须区分:
1741
- · 只要**见过**它出现过(detailsSeenCollapsed),就说明这个名字是活的 → 之后的"不在"= 展开;
1742
- · 从没见过 → 不认识 → 一律不认(绝不把未知结构当成"展开的详情列",这是白框事故的根因)。
1743
- data-rightbar-fullscreen 一并算作"这个名字是活的"的旁证(0.1.5 起第三列的另一种开合表达)。 */
1744
- const detailsAttrLive = () =>
1745
- detailsAttrKnown() || detailsSeenCollapsed || frame.hasAttribute("data-rightbar-fullscreen");
1712
+ /* ❌ 已随 2026-09-27 根治删除:detailsCollapsed / detailsAttrKnown / detailsAttrLive 三件套 ——
1713
+ 它们的前提是「由我们决定第三列显隐」,而决定权已交还官方。 */
1746
1714
  /* 状态来源(按优先级):
1747
1715
  ① 用户意图 userWantsOpen —— 点菜单按钮打开 / 点遮罩关闭,期间**不被几何判定推翻**;
1748
1716
  ② 官方 frame 的 data 属性 —— 用户没表达意图时(初始、官方自己切换)以它为准。
@@ -1757,36 +1725,15 @@ const SCRIPT = `(() => {
1757
1725
  let officialExpandedSeen = false;
1758
1726
  let lastOfficialCollapsed = null;
1759
1727
  let scrimTimer = 0, scrimWatchdog = 0;
1760
- /* ---- 详情列(第三列)在手机端的显示条件 ----------------------------------------
1761
- ⚠️ 为什么不能"官方属性说展开就滑进来"(2026-09-19 白框事故的根因之一):
1762
- 详情列在手机端是**离屏浮层**,我们自己的 CSS 用 translateX(103%) 把它推到屏幕右侧外面,
1763
- 到底显不显示**完全由 dsh-ma-details-open 决定**。所以几何永远无法充当"要不要打开"的依据
1764
- (它永远在屏外);反过来,旧实现用带缺陷的几何判定去决定是否滑入,就把
1765
- "官方在宽屏/上次会话遗留的展开态(attr 缺失或 =false)"读成"现在就该显示",
1766
- 于是手机一载入就滑出一个**空的白色 Details 面板**盖住界面。
1767
- 现在的规则(唯一一条,很保守):
1768
- 官方说得清"此刻是展开的" **且** 用户在**主内容列**里点过至少一次 → 才滑入。
1769
- 为什么要求"用户点过主内容列":官方表达展开的方式有两种 —— 属性被移除,或写成 =false
1770
- (实测 0.1.2-rc.1 是"收起才写属性";0.1.5 起第三列改名 rightbar,写法可能与旧版不同)。
1771
- "载入时官方就是展开的"多半是宽屏/上次会话留下的状态,手机端一进来就弹一个空白白面板
1772
- 正是用户报的那个白框;而"用户在正文里点了一下"是**可靠且唯一的**用户意图信号
1773
- (点工具行必然落在主内容列里)。这样两种写法都能正确处理,也不需要猜官方的时序。
1774
- 注:用户点过之后,官方若把属性收回(=收起),下面的 !detailsCollapsed() 立刻让它退出 ——
1775
- 关掉详情面板也是即时的。 */
1776
- let detailsSeenCollapsed = false; // 是否见过官方写出折叠态(证明这个属性名是活的)
1777
- /* 手机端显示详情列的**唯一**依据:用户在主内容列里的点击(见下面 click 委托)。
1778
- · centerTouchedAt:最近一次点击时间 —— 点工具行后官方通常在同一帧内把详情置为展开,
1779
- 给一个几秒窗口足够覆盖"点击 → 官方改属性 → 我们收到通知"的时序;
1780
- · detailsSticky:一经用户动作显示过就粘住(官方没说收起之前不自己缩回去),
1781
- 否则那些 700/1600/…/12000ms 的兜底 re-sync 会在窗口过期后把面板又收起来。 */
1782
- const CENTER_TOUCH_MS = 8000;
1783
- let centerTouchedAt = 0;
1784
- let detailsSticky = false;
1785
- /* 「打开右侧栏」的**显式**请求(0.6.18,来自悬浮菜单那颗按钮)。
1786
- 与 centerTouchedAt/detailsSticky 的区别:那两个是"用户大概想看点细节"的弱信号,
1787
- 这个是"我就是要看官方右栏"的强信号 —— 强信号才允许跳过"官方说未收起"这一条
1788
- (better-sidebar 这类只翻自己 store 的插件永远不会让官方 cols.rightbar 变正)。 */
1789
- let rightPaneWanted = false;
1728
+ /* ❌ 这里曾有「详情列(第三列)在手机端的显示条件」整段判据(白框事故后打的四层闸门)。
1729
+ 2026-09-27 根治时删除:第三列的显隐**完全交还官方** —— 宽屏按 grid 列、窄屏全屏浮层。
1730
+ 我们不再有"要不要显示它"的决定权,也就不需要任何判据。 */
1731
+
1732
+ // ❌ 已随 2026-09-27 根治删除:detailsSeenCollapsed。
1733
+ // ❌ 同一批删除:centerTouchedAt / detailsSticky 的说明(自绘浮层时代的启发式)。
1734
+
1735
+ /* ❌ 已随 2026-09-27 根治删除:CENTER_TOUCH_MS / centerTouchedAt / detailsSticky / rightPaneWanted ——
1736
+ 这套「用户 8 秒内在正文点过 / 从 FAB 明确要求」的启发式闸门,是为自绘浮层打的补丁。 */
1790
1737
  /**
1791
1738
  * 官方右栏里到底有没有"宿主注册的内容"?—— 有内容才值得滑进来。
1792
1739
  * 判据只用**官方结构 + 几何**,绝不认 better-sidebar 的私有标记
@@ -1795,19 +1742,7 @@ const SCRIPT = `(() => {
1795
1742
  * 为什么重要:官方右栏收起时**面板仍然挂载**(只是平移出 frame 右缘),所以"有子节点"
1796
1743
  * 就足以说明"这颗按钮后面真有东西可看",而不是一片空白。
1797
1744
  */
1798
- const rightPaneHasContent = () => {
1799
- try {
1800
- const el = details || document.querySelector(".dsh-ma-details, .dsh-ma-rightbar");
1801
- if (!el) return false;
1802
- const kids = el.children;
1803
- if (!kids || typeof kids.length !== "number") return false;
1804
- if (kids.length === 0) return false;
1805
- /* 官方折叠时宽度会被压到 0;但我们自己在窄屏把它改成 fixed + translateX(103%),
1806
- 所以宽度不再是"官方有没有内容"的判据,只用来排除"被压成 0 宽的列"。 */
1807
- if (typeof el.clientWidth === "number" && el.clientWidth === 0) return false;
1808
- return true;
1809
- } catch (e2) { return false; }
1810
- };
1745
+ // ❌ 已随 2026-09-27 根治删除:rightPaneHasContent(「右栏里有没有内容」也交给官方判断)。
1811
1746
  /* scrim 的"到底有没有在拦"以浏览器算出来的 pointer-events 为准(class 只是我们的意图) */
1812
1747
  const scrimBlocks = () => {
1813
1748
  const html = document.documentElement;
@@ -1819,16 +1754,19 @@ const SCRIPT = `(() => {
1819
1754
  } catch (e2) { /* 忽略 */ }
1820
1755
  return true; // 认不出实际样式 → 以 class 为准(保守:当成在拦)
1821
1756
  };
1757
+ /* 遮罩的「视口里确实有一层可交互抽屉」判据 —— 现在**只认左抽屉**。
1758
+ 第三列(官方右栏)不再是我们的浮层,也就绝不应该让我们的遮罩下闸。 */
1822
1759
  const anyDrawerInView = () =>
1823
- (sidebarInView() && !isShellColumn(sidebar || document.querySelector(".dsh-ma-sidebar"))) || detailsInView();
1760
+ (sidebarInView() && !isShellColumn(sidebar || document.querySelector(".dsh-ma-sidebar")));
1824
1761
  /* 遮罩闸门:**只有此刻真的看得见抽屉**才允许拦截点击。
1825
1762
  抽屉是滑入动画(.22s),所以同步判定必然"还看不见" —— 那一瞬间不下闸即可,
1826
1763
  真正的下闸交给下一帧与动画结束后的复核(applyScrim),既不误拦也不影响手感。
1827
1764
  另外 CSS 侧已收口成"只认 dsh-ma-scrim-on 一个闸",不会再出现"某个 class 常亮 → 整屏被拦"。 */
1828
1765
  const scrimShouldBeOn = () => {
1766
+ // 只认**左抽屉**:第三列(官方右栏)交还官方之后,窄屏时官方自己做全屏浮层,
1767
+ // 不需要(也不该)由我们再盖一层遮罩 —— 那正是「点开变整屏黑」的直接成因。
1829
1768
  const html = document.documentElement;
1830
1769
  if (html.classList.contains("dsh-ma-sidebar-open") && sidebarInView()) return true;
1831
- if (html.classList.contains("dsh-ma-details-open") && detailsInView()) return true;
1832
1770
  return false;
1833
1771
  };
1834
1772
  const applyScrim = () => {
@@ -1858,7 +1796,7 @@ const SCRIPT = `(() => {
1858
1796
  if (enforceScrimInvariant()) { /* 不变量被触发:已摘掉遮罩 */ }
1859
1797
  applyScrim();
1860
1798
  const html = document.documentElement;
1861
- if (!html.classList.contains("dsh-ma-scrim-on") && !html.classList.contains("dsh-ma-sidebar-open") && !html.classList.contains("dsh-ma-details-open")) {
1799
+ if (!html.classList.contains("dsh-ma-scrim-on") && !html.classList.contains("dsh-ma-sidebar-open")) {
1862
1800
  clearInterval(scrimWatchdog); scrimWatchdog = 0;
1863
1801
  }
1864
1802
  }, 800);
@@ -1893,23 +1831,14 @@ const SCRIPT = `(() => {
1893
1831
  保持可见 + 点它可开可关(见其 click 处理),任何异常状态下都留一条出路。
1894
1832
  它的 z-index(320) 高于抽屉(310)/详情列(300)/遮罩(290) —— 判定怎么错都点得到。 */
1895
1833
  if (hamburger) hamburger.style.display = "";
1896
- /* 第三列(详情/rightbar)在手机端要不要滑进来(0.6.18 四层判据):
1897
- ① 官方说得清(detailsAttrLive)+ 官方未收起 + 用户此刻想看 → 滑入(原判据);
1898
- ② **用户从 FAB 明确要求**(rightPaneWanted)+ 右栏确实有内容 → 也滑入。为什么必须绕过
1899
- "官方未收起":better-sidebar 这类把按钮注册进官方右栏的插件只翻它**自己**的 store,
1900
- 官方 cols.rightbar 永远是 0 → 等它松口 = 面板永远打不开(用户报的「右侧按钮区看不见」)。
1901
- 而"有没有内容"用官方结构/几何判,**绝不**认任何第三方私有标记;
1902
- ③ 官方属性改名了(detailsAttrLive 为假)但右栏里确实有内容 → 在用户已表达"要看"的前提下
1903
- 也允许滑入(未来官方再改名的兜底;"载入即滑出白框"由 wantsRightPane 挡掉 —— 它要求
1904
- 先有用户动作,那正是 2026-09-19 白框事故的根因);
1905
- ④ 官方收起 → 立刻退出,并复位所有粘性意图(下次点正文 / 点 FAB 即可恢复)。 */
1906
- if (detailsCollapsed()) { detailsSeenCollapsed = true; detailsSticky = false; rightPaneWanted = false; }
1907
- const touchedRecently = centerTouchedAt !== 0 && (Date.now() - centerTouchedAt) <= CENTER_TOUCH_MS;
1908
- const wantsRightPane = touchedRecently || detailsSticky || rightPaneWanted;
1909
- const officialOpen = detailsAttrLive() && !detailsCollapsed();
1910
- const dOpen = wantsRightPane && (officialOpen || rightPaneHasContent());
1911
- if (dOpen) detailsSticky = true;
1912
- document.documentElement.classList.toggle("dsh-ma-details-open", dOpen);
1834
+ /* ❌ 2026-09-27 根治:这里曾用四层判据决定"第三列(官方右栏)要不要滑进来",
1835
+ 并 toggle dsh-ma-details-open。整段删除 —— 第三列的显隐**完全交还给官方**:
1836
+ · 宽屏:官方按 grid 列摆(computeColumns);
1837
+ · 窄屏:官方把它做成全屏浮层(.P3OORG_panel[data-sidebar-right-panel=fullscreen]
1838
+ { position:fixed; inset:0 }),并且 openRightbar() 时自动收起左栏。
1839
+ 我们插手的两个后果都已真机验证:① 祖先 transform 会给 fixed 后代创建包含块 →
1840
+ 官方全屏面板被推到屏幕外;② 我们再开遮罩 → 只剩"整屏一层黑"。
1841
+ 现在这里**刻意什么都不做**,护栏用例守着「不得出现 dsh-ma-details-open」。 */
1913
1842
  // 遮罩:同步先按当前几何下闸(抽屉已在视口内时无延迟),再在动画开始/结束后复核两次
1914
1843
  applyScrim();
1915
1844
  try {
@@ -1919,46 +1848,10 @@ const SCRIPT = `(() => {
1919
1848
  } catch (e2) { /* 忽略 */ }
1920
1849
  armScrimWatchdog();
1921
1850
  };
1922
- /* 「打开右侧栏」的真正实现(0.6.18;悬浮菜单那颗按钮通过模块级 maOpenRightPane 回调进来)——
1923
- 状态机在这里,菜单那边看不到 detailsSticky/sync(两者作用域与创建时机都不同:
1924
- 菜单宽窄都建、可能更早;状态机只在 boot() 的窄屏段)。
1925
- ① 登记"我就是要看官方右栏"的强意图(不受 8s 窗口限制);
1926
- ② 官方若真有展开按钮就替用户点一下(探测失败是正常的:宽屏/已展开时它本来就不在);
1927
- ③ sync() 立刻按意图生效;官方展开是异步的,动画结束(≈260ms)后再对齐一次。 */
1928
- maOpenRightPane = () => {
1929
- rightPaneWanted = true;
1930
- detailsSticky = true; // 双保险:即便官方 toggle 点不动,判据②也放行
1931
- centerTouchedAt = Date.now();
1932
- try {
1933
- const b = maRightPaneExpandBtn();
1934
- if (b && b.click && detailsCollapsed()) b.click();
1935
- } catch (eR) { /* 探测/点击失败不影响下面的 sync */ }
1936
- sync();
1937
- try { setTimeout(() => { try { sync(); } catch (eS) { /* 忽略 */ } }, 260); } catch (eS2) { /* 忽略 */ }
1938
- };
1939
- /* 用户在主内容列里点一下 → 视为"他要看详情"(手机端显示详情列的唯一开关)。
1940
- 为什么必须由用户动作来解锁:官方表达"详情展开"的方式有两种 —— 属性被移除,或写成 =false,
1941
- 而"载入时官方就是展开的"多半是宽屏/上次会话留下的状态;手机端一进来就滑出一个空白白面板
1942
- 正是用户报的那个白框。点了正文之后才显示,两种写法都能正确处理,也不用去猜官方时序;
1943
- 用户点空白(遮罩)即收回这个意图(见 scrim 的 click 处理)。
1944
- 只认主内容列(.dsh-ma-center),不认抽屉/菜单/适配层自己的 UI —— 不会误触发。 */
1945
- try {
1946
- document.addEventListener("click", (e) => {
1947
- try {
1948
- if (!NARROW()) return;
1949
- const t = e.target;
1950
- if (!t || !t.closest) return;
1951
- if (t.closest(".dsh-ma-sidebar, .dsh-ma-scrim, .dsh-ma-hamburger, .dsh-ma-fab, .dsh-ma-menu")) return;
1952
- if (!t.closest(".dsh-ma-center, .pI_x6G_centerCol")) return;
1953
- centerTouchedAt = Date.now();
1954
- /* 只有"官方此刻就说详情是展开的"才需要立刻重算 —— 否则这一下点击改变不了任何显示,
1955
- 白跑一次 sync()(它要读几次 getBoundingClientRect)。官方稍后把详情置为展开时,
1956
- frame 属性观察器会再触发一次 sync,那时 centerTouchedAt 已经记下了用户意图。 */
1957
- if (detailsCollapsed()) return;
1958
- sync();
1959
- } catch (e2) { /* 忽略 */ }
1960
- }, true);
1961
- } catch (e2) { /* 忽略 */ }
1851
+ /* ❌ 「打开右侧栏」的实现已整条删除(2026-09-27 根治,原因见 STYLE 段顶部的长注释)。 */
1852
+ /* ❌ 「在正文里点一下才允许详情列滑入」的 click 委托已删除(2026-09-27 根治)。
1853
+ 那道启发式闸门存在的唯一理由,是我们把官方右栏改成了自绘浮层、怕它白框压屏;
1854
+ 现在右栏完全交还给官方(官方自己 openRightbar / fullscreen),闸门没有存在意义了)。 */
1962
1855
  try {
1963
1856
  /* 观察 frame 的**全部属性**,再在回调里按名字筛(2026-09-19 手机实测后的加固):
1964
1857
  官方 0.1.5-rc.2 起表达抽屉开合的属性改过名(details→rightbar),将来还可能再改;