dsh-dock 0.12.0 → 0.13.0

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.
@@ -129,9 +129,10 @@ const LAN_COMPAT_MARKER = 'data-dsh-lan-compat'
129
129
  // 1) 设置弹窗官方只有"左导航+右内容"两栏,窄屏内容栏被压到不足百像素(一字一行);
130
130
  // 改纵向堆叠:导航横排在上、内容占满。
131
131
  // 2) 侧边栏展开是 grid 栅格挤占会话区(窄屏会话区只剩 ~110px);改为浮层覆盖。
132
- // 3) 侧栏栅格归零后,原生「打开侧边栏」按钮随窄轨一起被裁掉——补贴左边缘的抽屉把手
133
- // (.dsh-mobile-drawer-btn,行为脚本创建,点击转发原生 toggle,位置在趣味游戏
134
- // 浮标正上方、兜底左侧中部)与抽屉遮罩(.dsh-mobile-scrim,点击即收起,阻断滚动穿透)。
132
+ // 3) 侧栏栅格归零后,原生「打开侧边栏」按钮随窄轨一起被裁掉——会话入口由底部 Tab 栏
133
+ // 「会话」页签承担(点击转发原生 toggle);再补抽屉遮罩(.dsh-mobile-scrim,点击即
134
+ // 收起,阻断滚动穿透)。左缘浮动「抽屉把手」已移除:与 Tab 栏功能重复,还悬浮在
135
+ // 内容上压过输入区(用户实测截图)。
135
136
  // 类名用语义后缀匹配(哈希前缀随 DSH 版本会变,后缀稳定);DSH 升级改了结构时
136
137
  // 选择器自然失效,不影响其他功能。
137
138
  const MOBILE_LAYOUT_CSS = [
@@ -145,14 +146,18 @@ const MOBILE_LAYOUT_CSS = [
145
146
  ' [class$="frame"]{grid-template-columns:0px minmax(0,1fr) 0px!important}',
146
147
  ' [class$="_handle"]{display:none!important}',
147
148
  ' [class$="frame"]:not([data-sidebar-collapsed]) [class*="sidebarCol"]{position:fixed!important;top:0!important;bottom:0!important;left:0!important;width:min(85vw,320px)!important;z-index:80!important;box-shadow:0 12px 48px rgba(0,0,0,.45)}',
148
- ' [class$="frame"]:not([data-details-collapsed="true"]) [class*="detailsCol"]{position:fixed!important;top:0!important;bottom:0!important;right:0!important;left:auto!important;width:min(85vw,320px)!important;z-index:85!important;box-shadow:-12px 0 48px rgba(0,0,0,.45)}',
149
+ // 右侧详情列:dsh 0.1.7 起 detailsCol 改名 rightbarCol(data-rightbar-col),且窄屏
150
+ // (<768px)原生就把面板切成全屏(data-sidebar-right-panel="fullscreen",自身 100vw
151
+ // 盖满视口),不再需要旧版「固定到右缘抽屉」的兜底规则。
152
+ // ⚠️ 实测原生全屏态两个缺陷(手机上双重曝光):① 面板背景透明——会话页 Hero 从缝隙里
153
+ // 整屏透出来,两层内容叠花;② 面板/panelBody 是 pointer-events:none,空隙处的点击
154
+ // 穿透到下层(能误点到 Hero 卡片和底部 Tab)。这里补不透明背景 + 拦截穿透;z 88 把
155
+ // 底部 Tab(65)/抽屉把手(70) 压在面板下,配合行为脚本 rightOpen 时主动隐藏(双保险)。
156
+ ' [class*="rightbarCol"]{min-width:0}',
157
+ ' [data-sidebar-right-panel="fullscreen"][data-sidebar-right-open]{z-index:88!important;pointer-events:auto!important;background:var(--dsw-alias-bg-layer-2,rgb(22,24,28))!important}',
149
158
  ' [class*="overlayLayer"]{z-index:90!important}',
150
- // 抽屉把手:贴左边缘(同趣味游戏浮标的边缘吸附语言),top 由 place() 动态设定
151
- // (趣味游戏浮标正上方,兜底左侧中部);z 序 70 < 遮罩 75 < 侧栏 80 < overlayLayer 90,
152
- // 面板/弹窗打开时自然被盖,无需额外隐藏逻辑。
153
- ' .dsh-mobile-drawer-btn{position:fixed;left:0;top:38vh;z-index:70;box-sizing:border-box;width:30px;height:48px;padding:0;display:flex;align-items:center;justify-content:center;border:1px solid var(--dsw-alias-border-l1,rgba(127,139,161,.35));border-left:none;border-radius:0 12px 12px 0;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2,#1c2230) 88%,transparent);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);color:var(--dsw-alias-label-primary,#e6eaf2);box-shadow:4px 0 18px rgba(0,0,0,.28);cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent}',
154
- ' .dsh-mobile-drawer-btn:active{transform:scale(.94)}',
155
- ' .dsh-mobile-drawer-btn svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}',
159
+ // 抽屉遮罩:侧栏浮层展开时盖住页面。左缘浮动「抽屉把手」已移除——与底部 Tab 栏
160
+ // 「会话」页签功能重复,还悬浮在内容上压过输入区(用户实测截图)。
156
161
  ' .dsh-mobile-scrim{position:fixed;inset:0;z-index:75;background:rgba(8,10,14,.45);backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);touch-action:none}',
157
162
  // ---- 仿 ZCode 手机端:底部 Tab 栏 + 输入区吸附底部 ----
158
163
  // Tab 栏 z 序 65:低于遮罩 75 / 抽屉 80 / overlayLayer 90 / 功能坞面板 200,
@@ -163,24 +168,54 @@ const MOBILE_LAYOUT_CSS = [
163
168
  ' .dsh-mobile-tab.on{color:var(--dsw-alias-label-primary,#e6eaf2)}',
164
169
  ' .dsh-mobile-tab.on svg{color:#4d9fff}',
165
170
  ' .dsh-mobile-tab:active{transform:scale(.94)}',
171
+ // 「任务」页签角标:进行中=蓝(数量),等待确认=红(数量 + 脉冲)——接力场景里
172
+ // 等待确认是最需要人介入的状态,一眼可见;数据来自 runstate 宿主路由的低频轮询。
173
+ ' .dsh-mobile-tab{position:relative}',
174
+ ' .dsh-mobile-tab-badge{position:absolute;top:0;right:calc(50% - 26px);box-sizing:border-box;min-width:16px;height:16px;padding:0 4px;border-radius:9px;display:flex;align-items:center;justify-content:center;background:#4d9fff;color:#fff;font-size:10px;font-weight:700;line-height:1;font-variant-numeric:tabular-nums;box-shadow:0 2px 8px rgba(0,0,0,.35);pointer-events:none}',
175
+ ' .dsh-mobile-tab-badge.wait{background:#ef4444;animation:dsh-badge-pulse 1.2s ease-in-out infinite}',
176
+ ' @keyframes dsh-badge-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.18)}}',
177
+ ' @media (prefers-reduced-motion:reduce){.dsh-mobile-tab-badge.wait{animation:none}}',
166
178
  // 输入区吸附到 Tab 栏上方:宿主把欢迎内容 justify-content:center 垂直居中,
167
- // 手机视口下输入卡片悬在中部、下方留出大片空白。改为 flex-end 贴底,并给
168
- // 滚动体留出 Tab 栏高度的内边距,避免内容被 Tab 栏遮挡。
169
- ' [class*="scrollBody"]{justify-content:flex-end!important;padding-bottom:calc(72px + env(safe-area-inset-bottom,0px))!important}',
170
- ' [class*="scrollBody"]>[class*="composerSeat"]{padding-bottom:6px}',
179
+ // 手机视口下输入卡片悬在中部、下方留出大片空白。改用「输入区 margin-top:auto」
180
+ // 贴底,并给滚动体留出 Tab 栏高度的内边距,避免内容被 Tab 栏遮挡。
181
+ //
182
+ // ⚠️ 绝不能再给滚动体加 justify-content:flex-end/center 来贴底:那会把内容挤到
183
+ // 滚动体起点之外,而 Chrome 不把起点方向的溢出算进 scrollHeight(手机视口实测
184
+ // scrollHeight==clientHeight、maxScroll==0),结果是会话记录完全滚不动、首条消息
185
+ // 永远停在视口上方 2000px 处。auto 外边距只在有剩余空间时生效、不产生起点溢出,
186
+ // 欢迎页照样贴底——见 scripts/test-mobile-relay-host.mjs 的回归断言。
187
+ //
188
+ // 软键盘弹出(visualViewport 判定,行为脚本挂 html.dsh-dock-kbd):Tab 栏
189
+ // 整体让位(打字时它只会挤占键盘上方的一线空间),滚动体留白收窄。
190
+ ' html:not(.dsh-dock-kbd) [class*="scrollBody"]{padding-bottom:calc(72px + env(safe-area-inset-bottom,0px))!important}',
191
+ ' html.dsh-dock-kbd [class*="scrollBody"]{padding-bottom:12px!important}',
192
+ ' html.dsh-dock-kbd .dsh-mobile-tabbar{display:none!important}',
193
+ ' [class*="scrollBody"]>[class*="composerSeat"]{margin-top:auto;padding-bottom:6px}',
194
+ // 输入区工具行上的 dsh-dock chip(用量/余额):窄屏下 28cqw 的宽度上限把它们挤成
195
+ // 「…」「余..」(手机实测),既读不出数值又白占输入行空间。手机端整组收起,数值在
196
+ // 底部「功能坞」里看(用量/余额页信息更全);桌面端不动。
197
+ ' .dockchip-row{display:none}',
171
198
  // 收起的侧栏轨道(collapsed rail)在窄屏只剩 1px 占位,但轨道里的按钮
172
199
  // 仍会溢出贴在左边缘(功能坞/进化/设置三枚露出半边的圆钮)。隐藏整列,
173
200
  // 这些入口已由底部 Tab 栏接管;展开抽屉(z 80)不受影响。
174
201
  ' [class*="sidebarCol"]:has([class*="collapsed"]){visibility:hidden}',
202
+ // ⚠️ 设置/插件管理弹层挂在侧栏列内部(footArea → settingsArea → overlay → panel),
203
+ // 上面隐藏整列会把它连坐隐藏——点「设置」得到一个 390×844 的空气弹层(属性齐全但不绘制)。
204
+ // visibility 可被后代覆盖,弹层关闭态会整体卸载(DOM 里没有 [role=dialog]),所以强制
205
+ // 显示只在弹层真正打开时命中,是安全的;列本身继续隐藏,轨道按钮照旧被藏住。
206
+ ' [class*="sidebarCol"]:has([class*="collapsed"]) [role="dialog"]{visibility:visible}',
207
+ // 功能坞面板在手机端抬到原生弹层之上:从侧栏进的官方插件管理页等原生弹层 z 序高于
208
+ // 面板默认的 200,功能坞开在下面会被盖住——底部「插件」页签点了像没反应/露出原页面。
209
+ ' [class*="dockm-backdrop"]{z-index:2000!important}',
175
210
  '}',
176
211
  ].join('\n')
177
212
  const MOBILE_LAYOUT_MARKER = 'data-dsh-mobile-layout'
178
213
  // 窄屏行为补丁:侧边栏浮层化后,原生的"保持展开"会一直挡住半屏——
179
214
  // 点会话行/新会话后自动收起;点侧栏外的页面区域也收起(浮层语义)。
180
215
  // 侧栏内的其他点击(工作区折叠、搜索、功能坞入口)保持原生行为不收。
181
- // 另创建抽屉按钮(窄屏 + 侧栏收起时才显示,点击转发原生「打开侧边栏」toggle;
182
- // 侧栏展开或设置弹窗打开时自动隐藏)与抽屉遮罩(点击经全局捕获 handler 收起,
183
- // 单一路径防止同一事件两次 toggle)。注入点在 <head> 后,body 尚未解析,
216
+ // 会话入口在底部 Tab 栏「会话」页签(点击转发原生「打开侧边栏」toggle);另建抽屉
217
+ // 遮罩(点击经全局捕获 handler 收起,单一路径防止同一事件两次 toggle)。左缘浮动
218
+ // 「抽屉把手」已移除(与 Tab 栏功能重复)。注入点在 <head> 后,body 尚未解析,
184
219
  // DOM 创建一律推迟到 DOMContentLoaded;显隐同步走 MutationObserver(属性翻转即时)
185
220
  // + 低速轮询(覆盖路由重渲等一切边角)。
186
221
  const MOBILE_BEHAVIOR_JS = [
@@ -197,11 +232,27 @@ const MOBILE_BEHAVIOR_JS = [
197
232
  'function dialogOpen(){return !!document.querySelector(\'[role="dialog"][class*="panel"]\')}',
198
233
  'function collapse(){var b=document.querySelector(\'button[aria-label="收起侧边栏"],button[aria-label="Collapse sidebar"]\');if(b)b.click()}',
199
234
  'function expand(){var b=document.querySelector(\'button[aria-label="打开侧边栏"],button[aria-label="Open sidebar"]\');if(b)b.click()}',
200
- 'function closeDetails(){var d=document.querySelector(\'[class*="detailsCol"]\');if(!d||!d.getBoundingClientRect().width)return;var c=d.querySelector(\'button[aria-label="关闭详情"]\');if(c)c.click()}',
201
- 'var fab=null,scrim=null,tabbar=null;',
235
+ 'function closeDetails(){var d=document.querySelector(\'[data-sidebar-right-panel][data-sidebar-right-open]\');if(!d)return;var c=d.querySelector(\'[data-sidebar-right-toggle]\');if(c)c.click()}',
236
+ 'var scrim=null,tabbar=null,badge=null,runstateGone=false,tasksActive=0,tasksWaiting=0;',
237
+ // 打开功能坞(可再点进指定页面):宿主版没有客户端的 openPanel 总线,先点侧栏
238
+ // 轨道上的功能坞按钮开面板,再在面板里找文字匹配的导航按钮点进去(找不到就停在
239
+ // 面板首页——宿主版「插件」页签的降级导航路径)。
240
+ 'function openDockPage(page){',
241
+ ' var b=document.querySelector("button.docke2-rail,button[class*=\'docke2-btn\'][class*=\'docke2-rail\']");',
242
+ ' if(b){var col=b.closest(\'[class*="sidebarCol"]\');if(col)col.style.visibility="";b.click();if(col)setTimeout(function(){col.style.visibility=""},0)}',
243
+ ' if(!page)return;',
244
+ ' setTimeout(function(){',
245
+ ' var panel=document.querySelector(".dockm-backdrop");if(!panel)return;',
246
+ ' var bs=panel.querySelectorAll("button");',
247
+ ' for(var i=0;i<bs.length;i++){var tx=bs[i].textContent||"";if(tx.indexOf(page)>=0&&bs[i].offsetParent){bs[i].click();return}}',
248
+ ' },120);',
249
+ '}',
250
+ // 「任务」「功能坞」合并为「插件」页签(与客户端自建版同款):有任务在跑/等确认时
251
+ // 直接落到运行状态页,否则进面板首页。
252
+ 'function openDockSmart(){openDockPage((tasksActive>0||tasksWaiting>0)?"运行状态":null)}',
202
253
  'var TAB_DEFS=[',
203
254
  ' {id:"sessions",label:"会话",icon:\'<svg viewBox="0 0 24 24"><path d="M21 12a8 8 0 0 1-8 8H5l-2 2V12a8 8 0 0 1 8-8h2a8 8 0 0 1 8 8z"/></svg>\',act:function(){expand()}},',
204
- ' {id:"dock",label:"功能坞",icon:\'<svg viewBox="0 0 24 24"><rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/></svg>\',act:function(){var b=document.querySelector("button.docke2-rail,button[class*=\'docke2-btn\'][class*=\'docke2-rail\']");if(b){var col=b.closest(\'[class*="sidebarCol"]\');if(col)col.style.visibility="";b.click();if(col)setTimeout(function(){col.style.visibility=""},0)}}},',
255
+ ' {id:"dock",label:"插件",icon:\'<svg viewBox="0 0 24 24"><rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/></svg>\',act:function(){openDockSmart()}},',
205
256
  ' {id:"settings",label:"设置",icon:\'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 1.55V21a2 2 0 1 1-4 0v-.09A1.7 1.7 0 0 0 9 19.4a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.55-1H3a2 2 0 1 1 0-4h.09A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-1.55V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1 1.51 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9c.23.6.86 1 1.51 1H21a2 2 0 1 1 0 4h-.09a1.7 1.7 0 0 0-1.51 1z"/></svg>\',act:function(){var bs=document.querySelectorAll("button");for(var i=0;i<bs.length;i++){if(bs[i].getAttribute("aria-label")==="设置"){bs[i].click();return}}}}',
206
257
  '];',
207
258
  'function syncTabs(){',
@@ -217,7 +268,6 @@ const MOBILE_BEHAVIOR_JS = [
217
268
  ' }',
218
269
  '}',
219
270
  'function ensureChrome(){',
220
- ' if(!fab){fab=document.createElement("button");fab.type="button";fab.className="dsh-mobile-drawer-btn";fab.setAttribute("aria-label","打开会话列表");fab.innerHTML=\'<svg viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h10"/></svg>\';fab.addEventListener("click",function(){expand()});document.body.appendChild(fab)}',
221
271
  ' if(!scrim){scrim=document.createElement("div");scrim.className="dsh-mobile-scrim";document.body.appendChild(scrim)}',
222
272
  ' if(!tabbar){',
223
273
  ' tabbar=document.createElement("nav");tabbar.className="dsh-mobile-tabbar";tabbar.setAttribute("aria-label","底部导航");',
@@ -226,69 +276,92 @@ const MOBILE_BEHAVIOR_JS = [
226
276
  ' var b=document.createElement("button");b.type="button";b.className="dsh-mobile-tab";b.dataset.tab=def.id;',
227
277
  ' b.innerHTML=def.icon+"<span>"+def.label+"</span>";',
228
278
  ' b.addEventListener("click",function(){def.act()});',
279
+ ' if(def.id==="dock"){badge=document.createElement("span");badge.className="dsh-mobile-tab-badge";badge.style.display="none";b.appendChild(badge)}',
229
280
  ' tabbar.appendChild(b);',
230
281
  ' })(TAB_DEFS[i]);',
231
282
  ' }',
232
283
  ' document.body.appendChild(tabbar);',
233
284
  ' }',
234
285
  '}',
235
- // 把手定位:贴着趣味游戏浮标(.dgfab,可拖拽)正上方;浮标太靠上时改放它下面
236
- // (避开顶栏标题),被拖走/隐藏/不存在时兜底左侧中部(38% 视高)。
237
- 'function place(){',
238
- ' if(!fab)return;',
239
- ' var vh=window.innerHeight,top=null;',
240
- ' var g=document.querySelector(".dgfab");',
241
- ' if(g&&!g.classList.contains("dgfab-hide")&&g.getBoundingClientRect){',
242
- ' var r=g.getBoundingClientRect();',
243
- ' if(r&&r.height>0&&r.top>0){',
244
- ' top=r.top-fab.offsetHeight-8;',
245
- ' if(top<64)top=r.bottom+8;',
246
- ' if(top+fab.offsetHeight>vh-16)top=null;',
247
- ' }',
248
- ' }',
249
- ' if(top==null)top=Math.round(vh*.38);',
250
- ' fab.style.top=top+"px";',
286
+ // 「插件」页签上的任务角标轮询(低频、页面可见且窄屏才发):进行中=蓝、等待确认=红+脉冲。
287
+ // runstate 路由不存在(功能未启用/宿主旧版)时静默停轮询。
288
+ 'function pollTasks(){',
289
+ ' if(runstateGone||!badge||!narrow()||document.visibilityState==="hidden"||!tabbar||!tabbar.isConnected)return;',
290
+ ' fetch("/dsh-dock/runstate/status",{method:"POST",headers:{"content-type":"application/json"},body:"{}"}).then(function(r){',
291
+ ' if(r.status===404||r.status===405){runstateGone=true;tasksActive=0;tasksWaiting=0;badge.style.display="none";return null}',
292
+ ' return r.json()',
293
+ ' }).then(function(body){',
294
+ ' if(!body||!badge)return;',
295
+ ' var active=(body.data&&body.data.active)||[],wait=0;',
296
+ ' for(var i=0;i<active.length;i++){var ap=active[i].approvals;if(ap&&ap.length)wait++}',
297
+ ' tasksActive=active.length;tasksWaiting=wait;',
298
+ ' if(wait>0){badge.textContent=String(wait);badge.className="dsh-mobile-tab-badge wait";badge.style.display=""}',
299
+ ' else if(active.length>0){badge.textContent=String(active.length);badge.className="dsh-mobile-tab-badge";badge.style.display=""}',
300
+ ' else{badge.style.display="none"}',
301
+ ' }).catch(function(){})',
302
+ '}',
303
+ // 软键盘判定:visualViewport 与布局视口高度差超阈值视为键盘弹出(挂 html.dsh-dock-kbd,
304
+ // 样式表隐藏 Tab 栏并收窄滚动体留白)。
305
+ 'var vv=window.visualViewport||null;',
306
+ 'function kbdSync(){',
307
+ ' var kbd=false;',
308
+ ' if(vv&&vv.height>0)kbd=(window.innerHeight-vv.height)>120;',
309
+ ' var de=document.documentElement;',
310
+ ' if(de.classList.toggle)de.classList.toggle("dsh-dock-kbd",kbd);',
311
+ ' else if(kbd)de.classList.add("dsh-dock-kbd");else de.classList.remove("dsh-dock-kbd");',
251
312
  '}',
252
313
  'function syncChrome(){',
253
- ' if(!fab||!scrim)return;',
314
+ ' kbdSync();',
315
+ ' if(!scrim)return;',
254
316
  ' var on=narrow()&&!!frameEl()&&!dialogOpen();',
255
317
  ' var col=sidebarCollapsed();',
256
318
  ' var dockOpen=!!document.querySelector(".dockm-backdrop");',
257
- ' var showFab=on&&col&&!dockOpen;',
258
- ' fab.style.display=showFab?"":"none";',
259
- ' if(showFab)place();',
319
+ // 右侧详情面板在窄屏是原生全屏(z 40),而 Tab 栏 65 比它高会压边——
320
+ // 面板打开时整体让位,收起(面板自带的折叠按钮)后再回来。
321
+ ' var rightOpen=!!document.querySelector(\'[data-sidebar-right-panel][data-sidebar-right-open]\');',
260
322
  ' scrim.style.display=on&&!col?"":"none";',
261
- // Tab 栏:窄屏且页面骨架就绪后常驻;功能坞面板打开时整体隐藏(面板全屏
262
- // 模态,Tab 栏 z 序低于它仍可能从边缘露出);宿主设置弹窗 z 序更高,自然盖住。
263
- ' if(tabbar)tabbar.style.display=narrow()&&!!frameEl()&&!dockOpen?"":"none";',
323
+ // Tab 栏:窄屏且页面骨架就绪后常驻;功能坞面板/右侧详情面板打开时隐藏(它们
324
+ // 或是全屏模态、或是原生全屏面板,Tab 栏 z 序更高会盖住内容边缘);宿主设置弹窗 z 序更高,自然盖住。
325
+ ' if(tabbar)tabbar.style.display=narrow()&&!!frameEl()&&!dockOpen&&!rightOpen?"":"none";',
264
326
  ' syncTabs();',
265
327
  '}',
266
328
  'function boot(){',
267
329
  ' ensureChrome();syncChrome();',
268
330
  ' if(typeof MutationObserver!=="undefined"){new MutationObserver(function(){syncChrome()}).observe(document.documentElement,{attributes:true,attributeFilter:["data-sidebar-collapsed"],subtree:true})}',
269
331
  ' setInterval(syncChrome,1500);',
332
+ ' setInterval(pollTasks,3000);pollTasks();',
270
333
  ' var onMq=function(){syncChrome()};',
271
334
  ' if(mq.addEventListener)mq.addEventListener("change",onMq);else if(mq.addListener)mq.addListener(onMq);',
272
- ' window.addEventListener("resize",place);',
335
+ ' if(vv&&vv.addEventListener)vv.addEventListener("resize",kbdSync);',
336
+ ' window.addEventListener("resize",kbdSync);',
273
337
  '}',
274
338
  'if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",boot);else boot();',
275
339
  'document.addEventListener("click",function(e){',
276
340
  ' if(!narrow())return;',
277
341
  ' var t=e.target;',
278
342
  ' if(!t||!t.closest)return;',
279
- ' if(t.closest(".dsh-mobile-drawer-btn")||t.closest(".dsh-mobile-tabbar"))return;',
343
+ ' if(t.closest(".dsh-mobile-tabbar"))return;',
280
344
  ' var expanded=!!document.querySelector(\'button[aria-label="收起侧边栏"],button[aria-label="Collapse sidebar"]\');',
281
345
  ' var col=document.querySelector(\'[class*="sidebarCol"]\');',
282
346
  ' var inSidebar=col&&col.contains(t);',
283
347
  ' if(!expanded&&!inSidebar)return;',
284
348
  ' if(inSidebar){',
349
+ // 只有点「会话」才收抽屉:会话行 data-row-key 以 session: 开头(0.1.7 结构,ui-workspace
350
+ // Rows.tsx);工作区文件夹行是 workspace: 开头——点它只是展开/收拢分组,不能收抽屉
351
+ // (旧「无子 treeitem 即叶子」判定会误伤折叠状态的文件夹:折叠时子节点不在 DOM 里)。
352
+ // 搜索结果行无 data-row-key,回退按无子叶判定。
285
353
  ' var row=t.closest(\'[role="treeitem"]\');',
286
- ' var leaf=row&&!row.querySelector(\'[role="treeitem"]\')&&row.closest(\'[role="tree"]\');',
354
+ ' var rowKey=(row&&row.getAttribute("data-row-key"))||"";',
355
+ ' var sessionRow=(rowKey&&rowKey.indexOf("session:")===0)||(row&&!rowKey&&!row.querySelector(\'[role="treeitem"]\')&&row.closest(\'[role="tree"]\'));',
287
356
  ' var fresh=t.closest(\'[class*="newSession"]\');',
288
- ' if(leaf||fresh)setTimeout(function(){collapse();closeDetails()},300);',
357
+ ' if(sessionRow||fresh){setTimeout(function(){collapse();closeDetails()},300);return}',
358
+ // 侧栏入口(插件/自动化任务/设置/记忆/自动进化…)点击后会弹出面板,而面板 z 序
359
+ // 常低于展开的抽屉——被盖住等于「点了没反应」(用户实测截图)。稍候探测:有面板
360
+ // 弹出就收起抽屉让它露出来;目录折叠/搜索等不弹面板的点击不受影响。
361
+ ' setTimeout(function(){if(document.querySelector(\'[role="dialog"]\'))collapse()},260);',
289
362
  ' return',
290
363
  ' }',
291
- ' if(t.closest(\'[role="dialog"],[class*="dockm"],[class*="dgfab"],[class*="dgwin"],[class*="dgame"],[class*="detailsCol"]\'))return;',
364
+ ' if(t.closest(\'[role="dialog"],[class*="dockm"],[class*="dgfab"],[class*="dgwin"],[class*="dgame"],[data-sidebar-right-panel]\'))return;',
292
365
  ' collapse()',
293
366
  '},true)',
294
367
  '})();',
@@ -457,11 +530,19 @@ export const feature = {
457
530
  webserverActive = legacyWebserverRowPresent(list)
458
531
  } catch { applied = false }
459
532
  const auth = getAuth()
533
+ let saved = null
534
+ try {
535
+ const root = readDockRoot(ctx)
536
+ saved = root && root.remoteGateway ? root.remoteGateway : null
537
+ } catch { saved = null }
460
538
  return {
461
539
  gatewayActive: Boolean(gateway),
462
540
  gatewayPort: gateway ? gateway.port : null,
541
+ gatewayEnabled: Boolean(saved && saved.enabled),
463
542
  mainPort: webServer.port,
464
543
  addresses: lanAddresses(),
544
+ /** 经网关登录的会话数(手机/平板各占一条;面板据此显示在线设备数)。 */
545
+ devices: gateway && typeof gateway.sessionCount === 'function' ? gateway.sessionCount() : 0,
465
546
  patchApplied: applied,
466
547
  webserverActive,
467
548
  accountSet: Boolean(auth),
@@ -470,6 +551,18 @@ export const feature = {
470
551
  }
471
552
  }
472
553
 
554
+ /** 持久化网关开关(remoteGateway 段):dsh web 重启后据此自动拉起。 */
555
+ async function persistGatewayState(enabled, port) {
556
+ try {
557
+ await mutateDockSection(ctx, ['remoteGateway'], {
558
+ enabled: Boolean(enabled),
559
+ port: Number.isInteger(port) && port > 0 ? port : 0,
560
+ })
561
+ } catch (error) {
562
+ console.warn('[dsh-dock] 远程访问开关状态保存失败:', (error && error.message) || String(error))
563
+ }
564
+ }
565
+
473
566
  async function stopGateway() {
474
567
  const current = gateway
475
568
  gateway = null
@@ -586,13 +679,17 @@ export const feature = {
586
679
  const list = readPatchList(file) || []
587
680
  const changed = !serverPatchApplied(list) || legacyWebserverRowPresent(list)
588
681
  writePatchList(file, upsertRemotePatches(list))
589
- await ensureGateway(wsCtx.webServer, payload && payload.port, () => mintUpstreamSession(wsCtx.webServer.port))
682
+ const started = await ensureGateway(wsCtx.webServer, payload && payload.port, () => mintUpstreamSession(wsCtx.webServer.port))
683
+ // 记住「开过」:重启 dsh web 后凭此自动拉起(账号存在才拉)。
684
+ await persistGatewayState(true, started.port)
590
685
  return sendJson(res, 200, { ok: true, data: { ...lanStatus(wsCtx.webServer), needsRestart: changed } })
591
686
  }
592
687
  if (method === 'lan/stop') {
593
688
  const file = serverPatchFile()
594
689
  const list = readPatchList(file)
690
+ const wasPort = gateway ? gateway.port : 0
595
691
  await stopGateway()
692
+ await persistGatewayState(false, wasPort)
596
693
  if (list === null) {
597
694
  return sendJson(res, 200, { ok: true, data: { ...lanStatus(wsCtx.webServer), needsRestart: false } })
598
695
  }
@@ -608,6 +705,34 @@ export const feature = {
608
705
  }
609
706
  },
610
707
  }), 'dsh-dock mobile relay: /dsh-dock/mobile-relay HTTP route')
708
+
709
+ // ── 网关自动拉起 ──
710
+ // 面板开过远程访问(remoteGateway.enabled=true)就持久记住:dsh web 重启后
711
+ // 只要账号还在,直接把网关重新监听起来,局域网设备不用等进面板再点一次。
712
+ // 门禁语义不变:未登录一律 302 到登录页;账号被清掉则只日志提醒不拉起。
713
+ void (async () => {
714
+ try {
715
+ const root = readDockRoot(ctx)
716
+ const saved = root && root.remoteGateway
717
+ if (!saved || !saved.enabled) return
718
+ if (!getAuth()) {
719
+ console.warn('[dsh-dock] 远程访问曾开启但账号不存在,网关未自动拉起(请在面板重新设置账号)')
720
+ return
721
+ }
722
+ // 目录选择器浏览模式补丁若被清掉则补回(幂等;重启生效,网关本身不受影响)。
723
+ try {
724
+ const file = serverPatchFile()
725
+ const list = readPatchList(file) || []
726
+ if (!serverPatchApplied(list) || legacyWebserverRowPresent(list)) {
727
+ writePatchList(file, upsertRemotePatches(list))
728
+ }
729
+ } catch { /* 补丁层异常不阻断网关 */ }
730
+ const started = await ensureGateway(wsCtx.webServer, saved.port || undefined, () => mintUpstreamSession(wsCtx.webServer.port))
731
+ console.log(`[dsh-dock] 远程访问网关已自动拉起: http://<本机IP>:${started.port}`)
732
+ } catch (error) {
733
+ console.warn('[dsh-dock] 远程访问网关自动拉起失败:', (error && error.message) || String(error))
734
+ }
735
+ })()
611
736
  }))
612
737
 
613
738
  return () => {