dsh-remote-mobile 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +4 -2
- package/README_EN.md +4 -2
- package/lib/bridge/compat.d.ts +22 -1
- package/lib/bridge/compat.js +149 -4
- package/lib/client.js +83 -5
- package/lib/styles/style-snippets.js +206 -60
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.4.2] - 2026-08-26
|
|
9
|
+
|
|
10
|
+
### Features (新功能)
|
|
11
|
+
- **新版本更新提醒**:设置面板打开时从 npm registry 拉取 `dsh-remote-mobile` 最新版本号,与当前安装版本做语义化版本比较;**仅当检测到新版本**时,在标题版本号旁显示一个小巧的红色提醒「🚀 发现新版本 vX.Y.Z」,点击直达 npm 对应发布页。拉取失败(离线 / 网络异常)或已是最新版本时完全不显示,不干扰界面,也无需前端轮询。
|
|
12
|
+
|
|
13
|
+
### Docs (文档)
|
|
14
|
+
- **README「这是什么」章节补充手机端样式适配说明**:说明插件针对 DSH 界面做的移动端样式适配(侧边栏抽屉折叠与可拖拽展开把手、设置弹窗居中微缩、对话正文高密度排版等)。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## [1.4.1] - 2026-08-26
|
|
19
|
+
|
|
20
|
+
### Features (新功能)
|
|
21
|
+
- **样式片段一键复制**:样式覆写页每个片段(内置预设与自定义小插件)在「查看 CSS」旁新增 **「📋 复制样式」** 按钮,点击即将该片段 CSS 完整复制到剪贴板并给出 toast 反馈,无需手动框选整段样式。
|
|
22
|
+
- **非安全上下文 Clipboard Polyfill**:HTTP + 外部 IP(Tailscale / 局域网)等非安全上下文下 `navigator.clipboard` 为 `undefined`,官方 bundle 直接调用 `writeText` 会抛 `Cannot read properties of undefined` 崩溃;现全端注入 polyfill(位于 `<head>` 最前、先于官方 bundle 生效),提供 `execCommand('copy')` 降级实现,原生可用时不替换,顺带保障上一条复制按钮与非安全环境下的复制功能可用。
|
|
23
|
+
|
|
24
|
+
### Fixed (缺陷修复)
|
|
25
|
+
- **修复:设置弹窗整窗缩放未覆盖导航按钮列**。`preset-settings` 等比微缩由覆盖单个卡片改为覆盖 `dialog` 全部直接子元素(`> *`),左侧 nav 按钮列表不再漏缩放;右侧内容保持 530px 最小宽度。
|
|
26
|
+
- **修复:插件市场弹窗窄屏隐藏左侧导航**。重置官方 `@media(max-width:560px)` 下隐藏左侧 nav 的行为(同选择器 + `!important` 稳压),导航保持可见。
|
|
27
|
+
- **修复:设置弹窗覆写误伤通用 Modal**。`preset-settings` 卡片/内部容器选择器改为限定在 overlay 上下文内(`:has` 限定),内测声明等直接挂 body、无 `data-slot` overlay 的通用 Modal 不再被 530px/zoom 覆写。
|
|
28
|
+
- **修复:composer 工具栏在窄屏过宽**。`preset-main` 工具栏紧凑化:gap/内边距缩小、发送按钮 30px、图标 12px、trigger 文字 12px,保持两行布局;命令按钮保持 26px 正方形、trigger 高度还原官方 28px。
|
|
29
|
+
- **修复:移动端侧边栏抽屉依赖生态插件选择器**。`preset-sidebar` 选择器全部改用官方稳定 class(`_frame` / `_sidebarCol` / `_centerCol`),不再依赖 web-ui 的 `data-pane` / `data-dsh-frame`,装不装 web-ui 均生效。
|
|
30
|
+
- **修复:窄屏详情列与顶部 header 重叠**。窄屏(≤900px)隐藏详情列,杜绝详情浮层与顶部 header 重叠;覆盖 web-ui 的 `conversation-header` 60px 让位与 `session-utilities` 按钮 `min-height:44px`(更高特异性 + 同断点,保留 min-width 触摸目标)。
|
|
31
|
+
- **修复:未装 web-ui 时窄屏点正文不收起抽屉**。新增 `dsh-mobile-dismiss` 守卫:未装 web-ui 时窄屏点正文自动收起抽屉(class 定位 + 让位守卫,不误伤抽屉内按钮)。
|
|
32
|
+
- **新增:设置弹窗顶部固定滑动浏览提示**。`overlay ::before`(absolute,不随内容滚动)提示用户可上下滑动浏览弹窗内容。
|
|
33
|
+
|
|
34
|
+
### Docs (文档)
|
|
35
|
+
- **新增 `screenshots.json`**:收录项目各界面截图图片 URL,便于 README 预览图管理与引用。
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
8
39
|
## [1.4.0] - 2026-08-24
|
|
9
40
|
|
|
10
41
|
### Compatibility (通用插件共存保护)
|
package/README.md
CHANGED
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
|
|
38
38
|
DSH 核心服务出于安全考虑默认仅监听本地回环地址(`127.0.0.1`),手机、平板或其他电脑无法从外部访问 Web 控制台。本项目通过**访问控制中间件**与**请求隔离技术**,安全地开放了 **Tailscale 虚拟私网** 以及 **本地局域网 (Wi-Fi/LAN)** 访问能力,并提供传输加密、扫码配对、长期密码认证与防暴力破解审计体系。
|
|
39
39
|
|
|
40
|
+
手机端访问时,插件还针对 DSH 界面做了**移动端样式适配**:侧边栏抽屉折叠与可拖拽展开把手、设置弹窗居中微缩、对话正文高密度排版等,让手机上的使用体验更贴近桌面端。
|
|
41
|
+
|
|
40
42
|
---
|
|
41
43
|
|
|
42
44
|
<span id="advantages"></span>
|
|
@@ -154,7 +156,7 @@ dsh web --no-open
|
|
|
154
156
|
### 4. 实时状态同步与国际化
|
|
155
157
|
- **SSE 实时事件推送**:基于 Server-Sent Events 实现新设备接入、设备重连、会话撤销及安全告警的实时通知,内置连接双向关闭监听与幂等清理。
|
|
156
158
|
- **全界面中英文双语自适应**:根据 DSH 全局语言偏好与浏览器环境,动态自适应中英文面板、提示及手机端界面。
|
|
157
|
-
- **非 HTTPS 兼容补丁**:自动注入 `crypto.randomUUID` Polyfill,解决移动端浏览器在 HTTP 非安全上下文下缺少原生 API
|
|
159
|
+
- **非 HTTPS 兼容补丁**:自动注入 `crypto.randomUUID` 与 `navigator.clipboard` Polyfill,解决移动端浏览器在 HTTP 非安全上下文下缺少原生 API 的报错(`clipboard` polyfill 保证非安全上下文下复制/样式复制功能正常)。
|
|
158
160
|
|
|
159
161
|
### 5. 移动端样式片段(样式小插件)
|
|
160
162
|
|
|
@@ -162,7 +164,7 @@ DSH Web 界面在手机上仍有不少沿袭桌面端的样式问题,插件内
|
|
|
162
164
|
|
|
163
165
|
- **按界面区域划分的三段内置预设**:`preset-sidebar` 侧边栏抽屉导航(折叠 0 宽度、可拖拽悬浮把手)、`preset-settings` 设置面板适配(弹窗居中微缩、遮罩锁滚动)、`preset-main` 对话正文**高密度排版**(小字号 12.5px + 紧凑行距 + 收紧边距 → 每行展示更多内容;基于稳定 HTML 元素与 localName 后缀,不做布局缩放,无右侧留白),默认移动端启用、PC 关闭;
|
|
164
166
|
- **PC / 移动端分别启停(按视口宽度判定,与设备无关)**:每段预设和每个自定义片段都有独立的「🖥️ PC」「📱 移动端」两个开关;「移动端」= 窄视口(≤900px)生效——**PC 浏览器拉小窗口也会生效**,「PC」= 宽视口(>900px)生效,两端都开 = 全宽度生效;
|
|
165
|
-
- **用户自定义(样式小插件)**:在 **设置 ⚙️ → 远程与移动端 → 🎨 移动端样式片段** 中粘贴自己的 CSS
|
|
167
|
+
- **用户自定义(样式小插件)**:在 **设置 ⚙️ → 远程与移动端 → 🎨 移动端样式片段** 中粘贴自己的 CSS 即可新增片段,支持编辑/启停/删除/**一键复制**(每个片段「查看 CSS」旁都有「📋 复制样式」按钮),持久化于 `~/.dsh/remote-mobile/style-snippets.json`,保存后下一次页面加载即生效,无需重启;
|
|
166
168
|
- **按 UA 打标记 + 按宽度生效**:样式按视口宽度档生效(见上),与设备 UA 无关;移动端 UA 请求额外给 `<html>` 打上 `data-dsh-mobile="1"` 标记作为作用域钩子,并注入可拖拽的展开把手脚本(全端注入,运行时仅在侧边栏折叠时生效)。
|
|
167
169
|
|
|
168
170
|
### 6. 插件通用共存保护
|
package/README_EN.md
CHANGED
|
@@ -37,6 +37,8 @@ English Documentation · [简体中文](./README.md)
|
|
|
37
37
|
|
|
38
38
|
By default, DSH strictly listens only to the local loopback address (`127.0.0.1`), preventing mobile devices, tablets, or external computers from accessing the Web console. This plugin uses **access control middleware** and **request isolation** to securely open up **Tailscale Private Network** and **Local Area Network (LAN / Wi-Fi)** access, complete with RSA transmission encryption, QR code pairing, persistent password auth, and automated brute-force defense.
|
|
39
39
|
|
|
40
|
+
When accessed from a phone, the plugin also adapts the DSH interface for **mobile styles**: a collapsible sidebar drawer with a draggable floating toggle, a centered/scaled settings dialog, and dense conversation typography — so the mobile experience feels closer to the desktop.
|
|
41
|
+
|
|
40
42
|
---
|
|
41
43
|
|
|
42
44
|
<span id="advantages"></span>
|
|
@@ -153,7 +155,7 @@ Open the DSH Web Console in your browser, navigate to **Settings ⚙️ -> Remot
|
|
|
153
155
|
### 4. Real-Time Sync & Internationalization
|
|
154
156
|
- **SSE Real-Time Push**: Push notifications for new device pairing, reconnections, revocations, and security alerts via Server-Sent Events, with bidirectional close listeners and idempotent cleanup.
|
|
155
157
|
- **Adaptive Bilingual UI**: Automatically switches between English and Simplified Chinese based on DSH settings and browser language preferences.
|
|
156
|
-
- **Non-HTTPS Compatibility Patch**: Injects `crypto.randomUUID`
|
|
158
|
+
- **Non-HTTPS Compatibility Patch**: Injects `crypto.randomUUID` and `navigator.clipboard` Polyfills into HTML templates to fix missing native Web Crypto / Clipboard APIs in HTTP non-secure mobile browser contexts (the clipboard polyfill keeps copy and snippet-copy working in non-secure contexts).
|
|
157
159
|
|
|
158
160
|
### 5. Mobile Style Snippets (style mini-plugins)
|
|
159
161
|
|
|
@@ -161,7 +163,7 @@ The DSH web UI still carries many desktop-oriented styles on phones. This plugin
|
|
|
161
163
|
|
|
162
164
|
- **Three area-based built-in presets**: `preset-sidebar` (collapsed sidebar becomes a 0-width drawer with a draggable floating toggle), `preset-settings` (centered/scaled settings dialog with locked overlay scroll), `preset-main` (dense conversation typography with scrollable code blocks). Mobile on / PC off by default;
|
|
163
165
|
- **Separate PC / Mobile toggles (viewport-width based, device-independent)**: every preset and custom snippet has independent PC and Mobile switches; "Mobile" = applies at narrow viewports (≤900px) — **a PC browser with a narrow window gets it too**; "PC" = applies at wide viewports (>900px); both on = applies at all widths;
|
|
164
|
-
- **Custom snippets (your own mini-plugins)**: paste your CSS in **Settings ⚙️ → Remote & Mobile → 🎨 Mobile Style Snippets** to add a snippet, with edit/toggle/delete support. Persisted to `~/.dsh/remote-mobile/style-snippets.json`; changes apply on the next page load without restart;
|
|
166
|
+
- **Custom snippets (your own mini-plugins)**: paste your CSS in **Settings ⚙️ → Remote & Mobile → 🎨 Mobile Style Snippets** to add a snippet, with edit/toggle/delete/**one-click copy** support (every snippet has a "📋 Copy CSS" button next to "View CSS"). Persisted to `~/.dsh/remote-mobile/style-snippets.json`; changes apply on the next page load without restart;
|
|
165
167
|
- **UA-based marker + width-based styling**: styles apply by viewport-width band (see above), independent of device UA; mobile UA requests additionally add `data-dsh-mobile="1"` to `<html>` as a scope hook, and the draggable toggle script is injected for all clients (it only activates while the sidebar is collapsed).
|
|
166
168
|
|
|
167
169
|
### 6. Generic Plugin Coexistence Protection
|
package/lib/bridge/compat.d.ts
CHANGED
|
@@ -38,11 +38,32 @@ import { type StyleSnippetStore } from '../styles/style-snippets.js';
|
|
|
38
38
|
* 针对非安全上下文(HTTP + 外部 IP)提供轻量高精度的 UUID v4 生成能力
|
|
39
39
|
*/
|
|
40
40
|
export declare const CRYPTO_POLYFILL_SNIPPET = "\n(function() {\n if (typeof window !== \"undefined\") {\n if (!window.crypto) window.crypto = {};\n if (typeof window.crypto.randomUUID !== \"function\") {\n window.crypto.randomUUID = function() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0;\n var v = c === 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n };\n }\n }\n})();\n";
|
|
41
|
+
/**
|
|
42
|
+
* 注入到 index.html <head> 最前列的 Clipboard API Polyfill 脚本
|
|
43
|
+
* 针对非安全上下文(HTTP + 外部 IP,如 Tailscale / 局域网)下
|
|
44
|
+
* navigator.clipboard 为 undefined 的场景:提供 execCommand('copy') 降级实现,
|
|
45
|
+
* 避免官方 bundle 直接调用 navigator.clipboard.writeText 时抛
|
|
46
|
+
* "Cannot read properties of undefined (reading 'writeText')" 崩溃。
|
|
47
|
+
* 原生可用时不做任何替换。
|
|
48
|
+
*/
|
|
49
|
+
export declare const CLIPBOARD_POLYFILL_SNIPPET = "\n(function() {\n if (typeof navigator === \"undefined\") return;\n try {\n if (navigator.clipboard && typeof navigator.clipboard.writeText === \"function\") return;\n var writeText = function(text) {\n try {\n var ta = document.createElement('textarea');\n ta.value = String(text == null ? '' : text);\n ta.setAttribute('readonly', '');\n ta.style.position = 'fixed';\n ta.style.top = '-9999px';\n ta.style.left = '-9999px';\n document.body.appendChild(ta);\n ta.select();\n var ok = document.execCommand('copy');\n document.body.removeChild(ta);\n return ok ? Promise.resolve() : Promise.reject(new Error('copy failed'));\n } catch (err) {\n return Promise.reject(err);\n }\n };\n var readText = function() {\n return Promise.reject(new Error('clipboard readText is not supported in insecure context'));\n };\n try {\n if (!navigator.clipboard) navigator.clipboard = {};\n if (typeof navigator.clipboard.writeText !== \"function\") navigator.clipboard.writeText = writeText;\n if (typeof navigator.clipboard.readText !== \"function\") navigator.clipboard.readText = readText;\n } catch (err) {\n try {\n Object.defineProperty(navigator, 'clipboard', {\n value: { writeText: writeText, readText: readText },\n configurable: true,\n });\n } catch (err2) {}\n }\n } catch (err) {}\n})();\n";
|
|
41
50
|
/**
|
|
42
51
|
* 注入到 index.html 的移动端悬浮入口拖拽与位置记忆脚本
|
|
43
52
|
* 允许用户在移动端上下随心拖动左上角入口把手,避免遮挡聊天内容,并自动保存位置
|
|
44
53
|
*/
|
|
45
|
-
export declare const DRAGGABLE_NAV_SNIPPET = "\n(function() {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n /* ===== \u9632\u8BEF\u6536\u8D77\u5B88\u536B =====\n * \u7B2C\u4E09\u65B9\u751F\u6001\u63D2\u4EF6\uFF08\u5982 @linxin666/dsh-web-ui-all \u7684\u79FB\u52A8\u7AEF\u62BD\u5C49\u589E\u5F3A\uFF09\u4F1A\u5728\u7528\u6237\u70B9\u51FB\n * \u4FA7\u8FB9\u680F\u6761\u76EE\uFF08role=treeitem\uFF09\u540E\uFF0C\u4E8E\u4E0B\u4E00\u5E27\u7A0B\u5E8F\u5316\u70B9\u51FB\u300C\u6536\u8D77\u4FA7\u8FB9\u680F\u300D\u6309\u94AE\u81EA\u52A8\u6536\u56DE\u62BD\u5C49\u3002\n * \u8BE5\u5224\u5B9A\u65E0\u6CD5\u533A\u5206\u300C\u9009\u4E2D\u6761\u76EE\u56DE\u4E3B\u533A\u300D\u4E0E\u300C\u70B9\u51FB\u6761\u76EE\u5185\u7684\u6B21\u7EA7\u64CD\u4F5C\u56FE\u6807\u300D\uFF08\u66F4\u591A\u64CD\u4F5C \u22EF /\n * \u5C55\u5F00\u7BAD\u5934\u7B49\uFF09\uFF0C\u5BFC\u81F4\u540E\u8005\u62BD\u5C49\u88AB\u8BEF\u5173\u3002\n * \u8FD9\u91CC\u8BB0\u5F55\u6700\u8FD1\u4E00\u6B21\u5BF9\u6761\u76EE\u5185\u6B21\u7EA7\u63A7\u4EF6\uFF08\u6309\u94AE / \u7BAD\u5934\u56FE\u6807\uFF09\u7684\u89E6\u6478\u65F6\u95F4\uFF0C\u5E76\u5305\u88C5\n * HTMLButtonElement.prototype.click\uFF1A\u77ED\u65F6\u95F4\u7A97\u53E3\u5185\u7684\u7A0B\u5E8F\u5316\u6536\u8D77\u8C03\u7528\u76F4\u63A5\u5FFD\u7565\uFF1B\n * \u7528\u6237\u771F\u5B9E\u70B9\u51FB\u6536\u8D77\u6309\u94AE\uFF08\u6D4F\u89C8\u5668\u5408\u6210\u4E8B\u4EF6\u4E0D\u8D70 prototype.click\uFF09\u4E0E\u70B9\u51FB\u6761\u76EE\u4E3B\u4F53\n * \uFF08\u975E\u6309\u94AE/\u7BAD\u5934\uFF09\u7684\u5B98\u65B9\u884C\u4E3A\u5747\u4E0D\u53D7\u5F71\u54CD\u3002 */\n if (!window.__dshRmAntiCollapseInstalled__) {\n window.__dshRmAntiCollapseInstalled__ = true;\n var lastSecondaryTapAt = 0;\n document.addEventListener('pointerdown', function (e) {\n try {\n var t = e.target;\n if (!t || !t.closest) return;\n if (!t.closest('[data-pane=\"sidebar\"]')) return;\n var entry = t.closest('[role=\"treeitem\"], [data-dsh-part=\"sidebar-entry\"]');\n if (!entry) return;\n var isSecondaryControl =\n !!t.closest('button, [role=\"button\"]') ||\n !!t.closest('[class*=\"_arrow\"]');\n if (isSecondaryControl) lastSecondaryTapAt = Date.now();\n } catch (err) {}\n }, { capture: true, passive: true });\n\n var protoClick = HTMLButtonElement.prototype.click;\n HTMLButtonElement.prototype.click = function () {\n try {\n var isSidebarToggle =\n this.matches('[data-dsh-responsive-part=\"sidebar-toggle\"]') ||\n /\u6536\u8D77\u4FA7\u8FB9\u680F|Collapse sidebar|Open sidebar/i.test(this.getAttribute('aria-label') || '');\n if (isSidebarToggle && Date.now() - lastSecondaryTapAt < 900) {\n return; // \u541E\u6389\u7D27\u968F\u6B21\u7EA7\u64CD\u4F5C\u56FE\u6807\u70B9\u51FB\u4E4B\u540E\u7684\u7A0B\u5E8F\u5316\u6536\u8D77\n }\n } catch (err) {}\n return protoClick.apply(this, arguments);\n };\n }\n\n function initDraggableNav() {\n var logoRow = document.querySelector('[data-pane=\"sidebar\"] [class*=\"_logoRow\"]');\n if (!logoRow || logoRow.__dsh_drag_init__) return;\n logoRow.__dsh_drag_init__ = true;\n\n var savedTop = localStorage.getItem('dsh_mobile_nav_top');\n if (savedTop && !isNaN(Number(savedTop))) {\n var topVal = Math.max(10, Math.min(window.innerHeight - 60, Number(savedTop)));\n logoRow.style.setProperty('top', topVal + 'px', 'important');\n }\n\n var startY = 0;\n var initialTop = 0;\n var isDragging = false;\n var hasMoved = false;\n\n function onTouchStart(e) {\n var sidebar = document.querySelector('[data-pane=\"sidebar\"]');\n if (sidebar && !sidebar.querySelector('[class*=\"_collapsed\"]')) return;\n\n var touch = e.touches ? e.touches[0] : e;\n startY = touch.clientY;\n var rect = logoRow.getBoundingClientRect();\n initialTop = rect.top;\n isDragging = true;\n hasMoved = false;\n }\n\n function onTouchMove(e) {\n if (!isDragging) return;\n var touch = e.touches ? e.touches[0] : e;\n var deltaY = touch.clientY - startY;\n\n if (!hasMoved && Math.abs(deltaY) > 5) {\n hasMoved = true;\n }\n\n if (hasMoved) {\n if (e.cancelable) e.preventDefault();\n var newTop = initialTop + deltaY;\n var minTop = 10;\n var maxTop = window.innerHeight - 56;\n newTop = Math.max(minTop, Math.min(maxTop, newTop));\n logoRow.style.setProperty('top', newTop + 'px', 'important');\n }\n }\n\n function onTouchEnd(e) {\n if (!isDragging) return;\n isDragging = false;\n if (hasMoved) {\n e.preventDefault();\n e.stopPropagation();\n var currentRect = logoRow.getBoundingClientRect();\n try {\n localStorage.setItem('dsh_mobile_nav_top', String(Math.round(currentRect.top)));\n } catch (err) {}\n }\n }\n\n logoRow.addEventListener('touchstart', onTouchStart, { passive: true });\n window.addEventListener('touchmove', onTouchMove, { passive: false });\n window.addEventListener('touchend', onTouchEnd, { capture: true });\n\n logoRow.addEventListener('mousedown', onTouchStart);\n window.addEventListener('mousemove', onTouchMove);\n window.addEventListener('mouseup', onTouchEnd, { capture: true });\n }\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', initDraggableNav);\n } else {\n initDraggableNav();\n }\n setInterval(initDraggableNav, 800);\n})();\n";
|
|
54
|
+
export declare const DRAGGABLE_NAV_SNIPPET = "\n(function() {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n /* ===== \u9632\u8BEF\u6536\u8D77\u5B88\u536B =====\n * \u7B2C\u4E09\u65B9\u751F\u6001\u63D2\u4EF6\uFF08\u5982 @linxin666/dsh-web-ui-all \u7684\u79FB\u52A8\u7AEF\u62BD\u5C49\u589E\u5F3A\uFF09\u4F1A\u5728\u7528\u6237\u70B9\u51FB\n * \u4FA7\u8FB9\u680F\u6761\u76EE\uFF08role=treeitem\uFF09\u540E\uFF0C\u4E8E\u4E0B\u4E00\u5E27\u7A0B\u5E8F\u5316\u70B9\u51FB\u300C\u6536\u8D77\u4FA7\u8FB9\u680F\u300D\u6309\u94AE\u81EA\u52A8\u6536\u56DE\u62BD\u5C49\u3002\n * \u8BE5\u5224\u5B9A\u65E0\u6CD5\u533A\u5206\u300C\u9009\u4E2D\u6761\u76EE\u56DE\u4E3B\u533A\u300D\u4E0E\u300C\u70B9\u51FB\u6761\u76EE\u5185\u7684\u6B21\u7EA7\u64CD\u4F5C\u56FE\u6807\u300D\uFF08\u66F4\u591A\u64CD\u4F5C \u22EF /\n * \u5C55\u5F00\u7BAD\u5934\u7B49\uFF09\uFF0C\u5BFC\u81F4\u540E\u8005\u62BD\u5C49\u88AB\u8BEF\u5173\u3002\n * \u8FD9\u91CC\u8BB0\u5F55\u6700\u8FD1\u4E00\u6B21\u5BF9\u6761\u76EE\u5185\u6B21\u7EA7\u63A7\u4EF6\uFF08\u6309\u94AE / \u7BAD\u5934\u56FE\u6807\uFF09\u7684\u89E6\u6478\u65F6\u95F4\uFF0C\u5E76\u5305\u88C5\n * HTMLButtonElement.prototype.click\uFF1A\u77ED\u65F6\u95F4\u7A97\u53E3\u5185\u7684\u7A0B\u5E8F\u5316\u6536\u8D77\u8C03\u7528\u76F4\u63A5\u5FFD\u7565\uFF1B\n * \u7528\u6237\u771F\u5B9E\u70B9\u51FB\u6536\u8D77\u6309\u94AE\uFF08\u6D4F\u89C8\u5668\u5408\u6210\u4E8B\u4EF6\u4E0D\u8D70 prototype.click\uFF09\u4E0E\u70B9\u51FB\u6761\u76EE\u4E3B\u4F53\n * \uFF08\u975E\u6309\u94AE/\u7BAD\u5934\uFF09\u7684\u5B98\u65B9\u884C\u4E3A\u5747\u4E0D\u53D7\u5F71\u54CD\u3002 */\n if (!window.__dshRmAntiCollapseInstalled__) {\n window.__dshRmAntiCollapseInstalled__ = true;\n var lastSecondaryTapAt = 0;\n document.addEventListener('pointerdown', function (e) {\n try {\n var t = e.target;\n if (!t || !t.closest) return;\n if (!t.closest('[data-pane=\"sidebar\"], [class*=\"_sidebarCol\"]')) return;\n var entry = t.closest('[role=\"treeitem\"], [data-dsh-part=\"sidebar-entry\"]');\n if (!entry) return;\n var isSecondaryControl =\n !!t.closest('button, [role=\"button\"]') ||\n !!t.closest('[class*=\"_arrow\"]');\n if (isSecondaryControl) lastSecondaryTapAt = Date.now();\n } catch (err) {}\n }, { capture: true, passive: true });\n\n var protoClick = HTMLButtonElement.prototype.click;\n HTMLButtonElement.prototype.click = function () {\n try {\n var isSidebarToggle =\n this.matches('[data-dsh-responsive-part=\"sidebar-toggle\"]') ||\n /\u6536\u8D77\u4FA7\u8FB9\u680F|Collapse sidebar|Open sidebar/i.test(this.getAttribute('aria-label') || '');\n if (isSidebarToggle && Date.now() - lastSecondaryTapAt < 900) {\n return; // \u541E\u6389\u7D27\u968F\u6B21\u7EA7\u64CD\u4F5C\u56FE\u6807\u70B9\u51FB\u4E4B\u540E\u7684\u7A0B\u5E8F\u5316\u6536\u8D77\n }\n } catch (err) {}\n return protoClick.apply(this, arguments);\n };\n }\n\n function initDraggableNav() {\n var logoRow = document.querySelector('[data-pane=\"sidebar\"] [class*=\"_logoRow\"], [class*=\"_sidebarCol\"] [class*=\"_logoRow\"]');\n if (!logoRow || logoRow.__dsh_drag_init__) return;\n logoRow.__dsh_drag_init__ = true;\n\n var savedTop = localStorage.getItem('dsh_mobile_nav_top');\n if (savedTop && !isNaN(Number(savedTop))) {\n var topVal = Math.max(10, Math.min(window.innerHeight - 60, Number(savedTop)));\n logoRow.style.setProperty('top', topVal + 'px', 'important');\n }\n\n var startY = 0;\n var initialTop = 0;\n var isDragging = false;\n var hasMoved = false;\n\n function onTouchStart(e) {\n var sidebar = document.querySelector('[data-pane=\"sidebar\"], [class*=\"_sidebarCol\"]');\n if (sidebar && !sidebar.querySelector('[class*=\"_collapsed\"]')) return;\n\n var touch = e.touches ? e.touches[0] : e;\n startY = touch.clientY;\n var rect = logoRow.getBoundingClientRect();\n initialTop = rect.top;\n isDragging = true;\n hasMoved = false;\n }\n\n function onTouchMove(e) {\n if (!isDragging) return;\n var touch = e.touches ? e.touches[0] : e;\n var deltaY = touch.clientY - startY;\n\n if (!hasMoved && Math.abs(deltaY) > 5) {\n hasMoved = true;\n }\n\n if (hasMoved) {\n if (e.cancelable) e.preventDefault();\n var newTop = initialTop + deltaY;\n var minTop = 10;\n var maxTop = window.innerHeight - 56;\n newTop = Math.max(minTop, Math.min(maxTop, newTop));\n logoRow.style.setProperty('top', newTop + 'px', 'important');\n }\n }\n\n function onTouchEnd(e) {\n if (!isDragging) return;\n isDragging = false;\n if (hasMoved) {\n e.preventDefault();\n e.stopPropagation();\n var currentRect = logoRow.getBoundingClientRect();\n try {\n localStorage.setItem('dsh_mobile_nav_top', String(Math.round(currentRect.top)));\n } catch (err) {}\n }\n }\n\n logoRow.addEventListener('touchstart', onTouchStart, { passive: true });\n window.addEventListener('touchmove', onTouchMove, { passive: false });\n window.addEventListener('touchend', onTouchEnd, { capture: true });\n\n logoRow.addEventListener('mousedown', onTouchStart);\n window.addEventListener('mousemove', onTouchMove);\n window.addEventListener('mouseup', onTouchEnd, { capture: true });\n }\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', initDraggableNav);\n } else {\n initDraggableNav();\n }\n setInterval(initDraggableNav, 800);\n})();\n";
|
|
55
|
+
/**
|
|
56
|
+
* 注入到 index.html 的移动端抽屉「点外部收起」脚本
|
|
57
|
+
*
|
|
58
|
+
* 【设计职责】
|
|
59
|
+
* 仅当 @linxin666/dsh-web-ui-all 未启用时生效——web-ui 已自带 installMobileSidebarDismiss,
|
|
60
|
+
* 本脚本通过检测 style[data-dsh-compat="responsive"] 动态让位,绝不与 web-ui 叠加双份收起逻辑。
|
|
61
|
+
*
|
|
62
|
+
* 定位全部基于官方 CSS Modules 稳定后缀(_frame / _sidebarCol / _toggle,前缀 hash 随机、
|
|
63
|
+
* 后缀稳定),不依赖 web-ui 注入的 data-pane / data-dsh-frame 属性,因此装/不装 web-ui
|
|
64
|
+
* 两种环境行为一致。折叠状态沿用官方写入的 data-sidebar-collapsed 属性。
|
|
65
|
+
*/
|
|
66
|
+
export declare const MOBILE_DISMISS_SNIPPET = "\n(function() {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n\n var frameSelector = '[class*=\"_frame\"]';\n var sidebarSelector = '[class*=\"_sidebarCol\"]';\n\n function webUiOwnsDrawer() {\n return !!document.querySelector('style[data-dsh-compat=\"responsive\"]');\n }\n\n function findToggle(frame) {\n return frame.querySelector('[class*=\"_toggle\"]') ||\n Array.prototype.filter.call(frame.querySelectorAll('button'), function (b) {\n return /\u6536\u8D77\u4FA7\u8FB9\u680F|Collapse sidebar|Open sidebar/i.test(b.getAttribute('aria-label') || '');\n })[0] || null;\n }\n\n function install(frame) {\n if (frame.__dshRmDismissInstalled__) return;\n frame.__dshRmDismissInstalled__ = true;\n var raf = 0;\n\n frame.addEventListener('click', function (event) {\n try {\n // web-ui \u53EF\u80FD\u8FD0\u884C\u4E2D\u624D\u6CE8\u5165 responsive style\uFF1A\u6BCF\u6B21\u4E8B\u4EF6\u90FD\u590D\u67E5\uFF0C\u52A8\u6001\u8BA9\u4F4D\n if (webUiOwnsDrawer()) return;\n var target = event.target;\n if (!target || !(target instanceof Element)) return;\n if (typeof window.matchMedia !== 'function' || !window.matchMedia('(max-width: 900px)').matches) return;\n var sidebar = frame.querySelector(sidebarSelector);\n var toggle = findToggle(frame);\n if (!sidebar || !toggle) return;\n\n // \u62BD\u5C49\u5C55\u5F00\u65F6\u70B9\u51FB\u6B63\u6587\uFF08sidebar \u5916\uFF09\u2192 \u89E6\u53D1\u5B98\u65B9\u6536\u8D77\u6309\u94AE\uFF0C\u5E76\u62E6\u622A\u88AB\u541E\u6389\u7684\u70B9\u51FB\uFF1B\n // \u627E\u4E0D\u5230\u6536\u8D77\u6309\u94AE\u65F6\u4E0A\u9762\u5DF2 return\uFF0C\u7EDD\u4E0D\u8BEF\u62E6\u6B63\u6587\u4EA4\u4E92\n if (!frame.hasAttribute('data-sidebar-collapsed') && !sidebar.contains(target)) {\n event.preventDefault();\n event.stopPropagation();\n toggle.click();\n return;\n }\n // \u70B9\u51FB\u4FA7\u8FB9\u680F\u6761\u76EE\uFF08\u9009\u4E2D\u540E\u56DE\u4E3B\u533A\uFF09\u2192 \u4E0B\u4E00\u5E27\u7A0B\u5E8F\u5316\u6536\u8D77\u3002\n // \u65E2\u6709\u9632\u8BEF\u6536\u8D77\u5B88\u536B\uFF08dsh-draggable-nav \u5305\u88C5 HTMLButtonElement.prototype.click\n // + 900ms \u7A97\u53E3\uFF09\u4F1A\u62E6\u622A\u300C\u70B9\u6761\u76EE\u5185\u6B21\u7EA7\u64CD\u4F5C\u56FE\u6807\u300D\u540E\u7684\u8BEF\u6536\u8D77\uFF0C\u884C\u4E3A\u4E0E web-ui \u4E00\u81F4\u3002\n if (target.closest('[class*=\"_toggle\"]')) return;\n if (!target.closest('[role=\"treeitem\"], [data-dsh-part=\"sidebar-entry\"]')) return;\n if (raf !== 0) cancelAnimationFrame(raf);\n raf = requestAnimationFrame(function () {\n raf = 0;\n if (!frame.hasAttribute('data-sidebar-collapsed')) toggle.click();\n });\n } catch (err) {}\n }, true);\n }\n\n function ensure() {\n if (webUiOwnsDrawer()) return;\n var frame = document.querySelector(frameSelector);\n if (frame) install(frame);\n }\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', ensure);\n } else {\n ensure();\n }\n setInterval(ensure, 800);\n})();\n";
|
|
46
67
|
/**
|
|
47
68
|
* 挂载底层 http.Server 门禁劫持与回环上下文虚拟化代理
|
|
48
69
|
*
|
package/lib/bridge/compat.js
CHANGED
|
@@ -52,6 +52,54 @@ export const CRYPTO_POLYFILL_SNIPPET = `
|
|
|
52
52
|
}
|
|
53
53
|
})();
|
|
54
54
|
`;
|
|
55
|
+
/**
|
|
56
|
+
* 注入到 index.html <head> 最前列的 Clipboard API Polyfill 脚本
|
|
57
|
+
* 针对非安全上下文(HTTP + 外部 IP,如 Tailscale / 局域网)下
|
|
58
|
+
* navigator.clipboard 为 undefined 的场景:提供 execCommand('copy') 降级实现,
|
|
59
|
+
* 避免官方 bundle 直接调用 navigator.clipboard.writeText 时抛
|
|
60
|
+
* "Cannot read properties of undefined (reading 'writeText')" 崩溃。
|
|
61
|
+
* 原生可用时不做任何替换。
|
|
62
|
+
*/
|
|
63
|
+
export const CLIPBOARD_POLYFILL_SNIPPET = `
|
|
64
|
+
(function() {
|
|
65
|
+
if (typeof navigator === "undefined") return;
|
|
66
|
+
try {
|
|
67
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") return;
|
|
68
|
+
var writeText = function(text) {
|
|
69
|
+
try {
|
|
70
|
+
var ta = document.createElement('textarea');
|
|
71
|
+
ta.value = String(text == null ? '' : text);
|
|
72
|
+
ta.setAttribute('readonly', '');
|
|
73
|
+
ta.style.position = 'fixed';
|
|
74
|
+
ta.style.top = '-9999px';
|
|
75
|
+
ta.style.left = '-9999px';
|
|
76
|
+
document.body.appendChild(ta);
|
|
77
|
+
ta.select();
|
|
78
|
+
var ok = document.execCommand('copy');
|
|
79
|
+
document.body.removeChild(ta);
|
|
80
|
+
return ok ? Promise.resolve() : Promise.reject(new Error('copy failed'));
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return Promise.reject(err);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var readText = function() {
|
|
86
|
+
return Promise.reject(new Error('clipboard readText is not supported in insecure context'));
|
|
87
|
+
};
|
|
88
|
+
try {
|
|
89
|
+
if (!navigator.clipboard) navigator.clipboard = {};
|
|
90
|
+
if (typeof navigator.clipboard.writeText !== "function") navigator.clipboard.writeText = writeText;
|
|
91
|
+
if (typeof navigator.clipboard.readText !== "function") navigator.clipboard.readText = readText;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
try {
|
|
94
|
+
Object.defineProperty(navigator, 'clipboard', {
|
|
95
|
+
value: { writeText: writeText, readText: readText },
|
|
96
|
+
configurable: true,
|
|
97
|
+
});
|
|
98
|
+
} catch (err2) {}
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {}
|
|
101
|
+
})();
|
|
102
|
+
`;
|
|
55
103
|
/**
|
|
56
104
|
* 注入到 index.html 的移动端悬浮入口拖拽与位置记忆脚本
|
|
57
105
|
* 允许用户在移动端上下随心拖动左上角入口把手,避免遮挡聊天内容,并自动保存位置
|
|
@@ -76,7 +124,7 @@ export const DRAGGABLE_NAV_SNIPPET = `
|
|
|
76
124
|
try {
|
|
77
125
|
var t = e.target;
|
|
78
126
|
if (!t || !t.closest) return;
|
|
79
|
-
if (!t.closest('[data-pane="sidebar"]')) return;
|
|
127
|
+
if (!t.closest('[data-pane="sidebar"], [class*="_sidebarCol"]')) return;
|
|
80
128
|
var entry = t.closest('[role="treeitem"], [data-dsh-part="sidebar-entry"]');
|
|
81
129
|
if (!entry) return;
|
|
82
130
|
var isSecondaryControl =
|
|
@@ -101,7 +149,7 @@ export const DRAGGABLE_NAV_SNIPPET = `
|
|
|
101
149
|
}
|
|
102
150
|
|
|
103
151
|
function initDraggableNav() {
|
|
104
|
-
var logoRow = document.querySelector('[data-pane="sidebar"] [class*="_logoRow"]');
|
|
152
|
+
var logoRow = document.querySelector('[data-pane="sidebar"] [class*="_logoRow"], [class*="_sidebarCol"] [class*="_logoRow"]');
|
|
105
153
|
if (!logoRow || logoRow.__dsh_drag_init__) return;
|
|
106
154
|
logoRow.__dsh_drag_init__ = true;
|
|
107
155
|
|
|
@@ -117,7 +165,7 @@ export const DRAGGABLE_NAV_SNIPPET = `
|
|
|
117
165
|
var hasMoved = false;
|
|
118
166
|
|
|
119
167
|
function onTouchStart(e) {
|
|
120
|
-
var sidebar = document.querySelector('[data-pane="sidebar"]');
|
|
168
|
+
var sidebar = document.querySelector('[data-pane="sidebar"], [class*="_sidebarCol"]');
|
|
121
169
|
if (sidebar && !sidebar.querySelector('[class*="_collapsed"]')) return;
|
|
122
170
|
|
|
123
171
|
var touch = e.touches ? e.touches[0] : e;
|
|
@@ -177,6 +225,87 @@ export const DRAGGABLE_NAV_SNIPPET = `
|
|
|
177
225
|
setInterval(initDraggableNav, 800);
|
|
178
226
|
})();
|
|
179
227
|
`;
|
|
228
|
+
/**
|
|
229
|
+
* 注入到 index.html 的移动端抽屉「点外部收起」脚本
|
|
230
|
+
*
|
|
231
|
+
* 【设计职责】
|
|
232
|
+
* 仅当 @linxin666/dsh-web-ui-all 未启用时生效——web-ui 已自带 installMobileSidebarDismiss,
|
|
233
|
+
* 本脚本通过检测 style[data-dsh-compat="responsive"] 动态让位,绝不与 web-ui 叠加双份收起逻辑。
|
|
234
|
+
*
|
|
235
|
+
* 定位全部基于官方 CSS Modules 稳定后缀(_frame / _sidebarCol / _toggle,前缀 hash 随机、
|
|
236
|
+
* 后缀稳定),不依赖 web-ui 注入的 data-pane / data-dsh-frame 属性,因此装/不装 web-ui
|
|
237
|
+
* 两种环境行为一致。折叠状态沿用官方写入的 data-sidebar-collapsed 属性。
|
|
238
|
+
*/
|
|
239
|
+
export const MOBILE_DISMISS_SNIPPET = `
|
|
240
|
+
(function() {
|
|
241
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
242
|
+
|
|
243
|
+
var frameSelector = '[class*="_frame"]';
|
|
244
|
+
var sidebarSelector = '[class*="_sidebarCol"]';
|
|
245
|
+
|
|
246
|
+
function webUiOwnsDrawer() {
|
|
247
|
+
return !!document.querySelector('style[data-dsh-compat="responsive"]');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function findToggle(frame) {
|
|
251
|
+
return frame.querySelector('[class*="_toggle"]') ||
|
|
252
|
+
Array.prototype.filter.call(frame.querySelectorAll('button'), function (b) {
|
|
253
|
+
return /收起侧边栏|Collapse sidebar|Open sidebar/i.test(b.getAttribute('aria-label') || '');
|
|
254
|
+
})[0] || null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function install(frame) {
|
|
258
|
+
if (frame.__dshRmDismissInstalled__) return;
|
|
259
|
+
frame.__dshRmDismissInstalled__ = true;
|
|
260
|
+
var raf = 0;
|
|
261
|
+
|
|
262
|
+
frame.addEventListener('click', function (event) {
|
|
263
|
+
try {
|
|
264
|
+
// web-ui 可能运行中才注入 responsive style:每次事件都复查,动态让位
|
|
265
|
+
if (webUiOwnsDrawer()) return;
|
|
266
|
+
var target = event.target;
|
|
267
|
+
if (!target || !(target instanceof Element)) return;
|
|
268
|
+
if (typeof window.matchMedia !== 'function' || !window.matchMedia('(max-width: 900px)').matches) return;
|
|
269
|
+
var sidebar = frame.querySelector(sidebarSelector);
|
|
270
|
+
var toggle = findToggle(frame);
|
|
271
|
+
if (!sidebar || !toggle) return;
|
|
272
|
+
|
|
273
|
+
// 抽屉展开时点击正文(sidebar 外)→ 触发官方收起按钮,并拦截被吞掉的点击;
|
|
274
|
+
// 找不到收起按钮时上面已 return,绝不误拦正文交互
|
|
275
|
+
if (!frame.hasAttribute('data-sidebar-collapsed') && !sidebar.contains(target)) {
|
|
276
|
+
event.preventDefault();
|
|
277
|
+
event.stopPropagation();
|
|
278
|
+
toggle.click();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// 点击侧边栏条目(选中后回主区)→ 下一帧程序化收起。
|
|
282
|
+
// 既有防误收起守卫(dsh-draggable-nav 包装 HTMLButtonElement.prototype.click
|
|
283
|
+
// + 900ms 窗口)会拦截「点条目内次级操作图标」后的误收起,行为与 web-ui 一致。
|
|
284
|
+
if (target.closest('[class*="_toggle"]')) return;
|
|
285
|
+
if (!target.closest('[role="treeitem"], [data-dsh-part="sidebar-entry"]')) return;
|
|
286
|
+
if (raf !== 0) cancelAnimationFrame(raf);
|
|
287
|
+
raf = requestAnimationFrame(function () {
|
|
288
|
+
raf = 0;
|
|
289
|
+
if (!frame.hasAttribute('data-sidebar-collapsed')) toggle.click();
|
|
290
|
+
});
|
|
291
|
+
} catch (err) {}
|
|
292
|
+
}, true);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function ensure() {
|
|
296
|
+
if (webUiOwnsDrawer()) return;
|
|
297
|
+
var frame = document.querySelector(frameSelector);
|
|
298
|
+
if (frame) install(frame);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (document.readyState === 'loading') {
|
|
302
|
+
document.addEventListener('DOMContentLoaded', ensure);
|
|
303
|
+
} else {
|
|
304
|
+
ensure();
|
|
305
|
+
}
|
|
306
|
+
setInterval(ensure, 800);
|
|
307
|
+
})();
|
|
308
|
+
`;
|
|
180
309
|
/**
|
|
181
310
|
* 挂载底层 http.Server 门禁劫持与回环上下文虚拟化代理
|
|
182
311
|
*
|
|
@@ -232,7 +361,12 @@ export function patchHttpServerWithVirtualizer(server, gateMiddleware, _store, s
|
|
|
232
361
|
// 悬浮把手拖拽脚本全端注入:脚本自带“仅折叠时工作”守卫,
|
|
233
362
|
// 预设按视口宽度生效后 PC 窄窗口同样需要可拖动的展开把手
|
|
234
363
|
html = html.replace('<head>', `<head><script id="dsh-draggable-nav">${DRAGGABLE_NAV_SNIPPET}</script>`);
|
|
364
|
+
// 移动端抽屉「点外部收起」全端注入:脚本自带 web-ui 让位守卫,
|
|
365
|
+
// web-ui 在场时完全交给它处理,未装时复刻等价行为
|
|
366
|
+
html = html.replace('<head>', `<head><script id="dsh-mobile-dismiss">${MOBILE_DISMISS_SNIPPET}</script>`);
|
|
235
367
|
html = html.replace('<head>', `<head><script id="dsh-crypto-polyfill">${CRYPTO_POLYFILL_SNIPPET}</script>`);
|
|
368
|
+
// Clipboard Polyfill 最后 replace → 位于 <head> 最前,先于官方 bundle 生效
|
|
369
|
+
html = html.replace('<head>', `<head><script id="dsh-clipboard-polyfill">${CLIPBOARD_POLYFILL_SNIPPET}</script>`);
|
|
236
370
|
chunk = html;
|
|
237
371
|
injected = true;
|
|
238
372
|
}
|
|
@@ -245,7 +379,10 @@ export function patchHttpServerWithVirtualizer(server, gateMiddleware, _store, s
|
|
|
245
379
|
str = str.replace('<head>', `<head>${buildMobileStyleTag(mobileStyleCss)}`);
|
|
246
380
|
}
|
|
247
381
|
str = str.replace('<head>', `<head><script id="dsh-draggable-nav">${DRAGGABLE_NAV_SNIPPET}</script>`);
|
|
382
|
+
str = str.replace('<head>', `<head><script id="dsh-mobile-dismiss">${MOBILE_DISMISS_SNIPPET}</script>`);
|
|
248
383
|
str = str.replace('<head>', `<head><script id="dsh-crypto-polyfill">${CRYPTO_POLYFILL_SNIPPET}</script>`);
|
|
384
|
+
// Clipboard Polyfill 最后 replace → 位于 <head> 最前,先于官方 bundle 生效
|
|
385
|
+
str = str.replace('<head>', `<head><script id="dsh-clipboard-polyfill">${CLIPBOARD_POLYFILL_SNIPPET}</script>`);
|
|
249
386
|
chunk = Buffer.from(str, 'utf8');
|
|
250
387
|
injected = true;
|
|
251
388
|
}
|
|
@@ -339,7 +476,10 @@ export function mountIndexInjections(ctx) {
|
|
|
339
476
|
if (ctx.webServer && typeof ctx.webServer.tapIndex === 'function') {
|
|
340
477
|
ctx.webServer.tapIndex((html) => {
|
|
341
478
|
if (html.includes('<head>')) {
|
|
342
|
-
return html
|
|
479
|
+
return html
|
|
480
|
+
.replace('<head>', `<head><script id="dsh-crypto-polyfill">${CRYPTO_POLYFILL_SNIPPET}</script>`)
|
|
481
|
+
// Clipboard Polyfill 后 replace → 位于 <head> 最前,先于官方 bundle 生效
|
|
482
|
+
.replace('<head>', `<head><script id="dsh-clipboard-polyfill">${CLIPBOARD_POLYFILL_SNIPPET}</script>`);
|
|
343
483
|
}
|
|
344
484
|
return html;
|
|
345
485
|
});
|
|
@@ -351,6 +491,11 @@ export function mountIndexInjections(ctx) {
|
|
|
351
491
|
placement: 'head',
|
|
352
492
|
text: CRYPTO_POLYFILL_SNIPPET,
|
|
353
493
|
});
|
|
494
|
+
table.push({
|
|
495
|
+
kind: 'script',
|
|
496
|
+
placement: 'head',
|
|
497
|
+
text: CLIPBOARD_POLYFILL_SNIPPET,
|
|
498
|
+
});
|
|
354
499
|
}
|
|
355
500
|
});
|
|
356
501
|
}
|
package/lib/client.js
CHANGED
|
@@ -815,6 +815,8 @@ var translations = {
|
|
|
815
815
|
stylesDisabled: "\u5DF2\u505C\u7528",
|
|
816
816
|
stylesViewCss: "\u67E5\u770B CSS",
|
|
817
817
|
stylesHideCss: "\u6536\u8D77 CSS",
|
|
818
|
+
stylesCopyCss: "\u{1F4CB} \u590D\u5236\u6837\u5F0F",
|
|
819
|
+
stylesCopyCssDone: "\u6837\u5F0F\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F\uFF01",
|
|
818
820
|
stylesNamePlaceholder: "\u7247\u6BB5\u540D\u79F0\uFF08\u5FC5\u586B\uFF09",
|
|
819
821
|
stylesCssPlaceholder: "\u7C98\u8D34\u6216\u7F16\u5199 CSS \u4EE3\u7801\u7247\u6BB5\u2026\uFF08\u79FB\u52A8\u7AEF\u53EF\u7528 html[data-dsh-mobile] \u4F5C\u4E3A\u4F5C\u7528\u57DF\u9009\u62E9\u5668\uFF09",
|
|
820
822
|
stylesAddNew: "\uFF0B \u65B0\u589E\u81EA\u5B9A\u4E49\u7247\u6BB5",
|
|
@@ -967,6 +969,8 @@ var translations = {
|
|
|
967
969
|
stylesDisabled: "Disabled",
|
|
968
970
|
stylesViewCss: "View CSS",
|
|
969
971
|
stylesHideCss: "Hide CSS",
|
|
972
|
+
stylesCopyCss: "\u{1F4CB} Copy CSS",
|
|
973
|
+
stylesCopyCssDone: "CSS copied to clipboard!",
|
|
970
974
|
stylesNamePlaceholder: "Snippet name (required)",
|
|
971
975
|
stylesCssPlaceholder: "Paste or write CSS here\u2026 (html[data-dsh-mobile] can be used as a mobile scope selector)",
|
|
972
976
|
stylesAddNew: "\uFF0B Add Custom Snippet",
|
|
@@ -3090,6 +3094,21 @@ function StylesCard({ lang, showToast }) {
|
|
|
3090
3094
|
const toggleExpand = (id) => {
|
|
3091
3095
|
setExpanded((prev) => ({ ...prev, [id]: !prev[id] }));
|
|
3092
3096
|
};
|
|
3097
|
+
const copyCss = (cssText) => {
|
|
3098
|
+
navigator.clipboard.writeText(cssText).then(() => showFeedback(t("stylesCopyCssDone", lang))).catch(() => {
|
|
3099
|
+
const ta = document.createElement("textarea");
|
|
3100
|
+
ta.value = cssText;
|
|
3101
|
+
document.body.appendChild(ta);
|
|
3102
|
+
ta.select();
|
|
3103
|
+
try {
|
|
3104
|
+
document.execCommand("copy");
|
|
3105
|
+
showFeedback(t("stylesCopyCssDone", lang));
|
|
3106
|
+
} catch {
|
|
3107
|
+
showFeedback(lang === "en" ? "Copy failed, please copy manually." : "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u590D\u5236\u3002", "danger");
|
|
3108
|
+
}
|
|
3109
|
+
document.body.removeChild(ta);
|
|
3110
|
+
});
|
|
3111
|
+
};
|
|
3093
3112
|
const scopeBadge = (s) => {
|
|
3094
3113
|
if (s.pcEnabled && s.mobileEnabled) return t("stylesBothDevices", lang);
|
|
3095
3114
|
if (s.mobileEnabled) return "\u{1F4F1} " + t("stylesMobileOnlyBadge", lang);
|
|
@@ -3222,7 +3241,8 @@ function StylesCard({ lang, showToast }) {
|
|
|
3222
3241
|
"\u{1F4F1} ",
|
|
3223
3242
|
t("stylesMobileShort", lang)
|
|
3224
3243
|
] }),
|
|
3225
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => toggleExpand(s.id), style: miniBtnStyle, children: expanded[s.id] ? t("stylesHideCss", lang) : t("stylesViewCss", lang) })
|
|
3244
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => toggleExpand(s.id), style: miniBtnStyle, children: expanded[s.id] ? t("stylesHideCss", lang) : t("stylesViewCss", lang) }),
|
|
3245
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => copyCss(s.css), style: miniBtnStyle, children: t("stylesCopyCss", lang) })
|
|
3226
3246
|
] })
|
|
3227
3247
|
] }),
|
|
3228
3248
|
expanded[s.id] ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
@@ -3410,7 +3430,8 @@ function StylesCard({ lang, showToast }) {
|
|
|
3410
3430
|
] }),
|
|
3411
3431
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => startEdit(s), style: miniBtnStyle, children: t("stylesEditBtn", lang) }),
|
|
3412
3432
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => remove(s), style: { ...miniBtnStyle, color: "#ef4444" }, children: t("stylesDeleteBtn", lang) }),
|
|
3413
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => toggleExpand(s.id), style: miniBtnStyle, children: expanded[s.id] ? t("stylesHideCss", lang) : t("stylesViewCss", lang) })
|
|
3433
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => toggleExpand(s.id), style: miniBtnStyle, children: expanded[s.id] ? t("stylesHideCss", lang) : t("stylesViewCss", lang) }),
|
|
3434
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: () => copyCss(s.css), style: miniBtnStyle, children: t("stylesCopyCss", lang) })
|
|
3414
3435
|
] })
|
|
3415
3436
|
] }),
|
|
3416
3437
|
expanded[s.id] ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
@@ -3443,8 +3464,8 @@ function StylesCard({ lang, showToast }) {
|
|
|
3443
3464
|
}
|
|
3444
3465
|
|
|
3445
3466
|
// src/client/version.js
|
|
3446
|
-
var PLUGIN_VERSION = "v1.4.
|
|
3447
|
-
var BUILD_TIME = "2026-08-
|
|
3467
|
+
var PLUGIN_VERSION = "v1.4.2";
|
|
3468
|
+
var BUILD_TIME = "2026-08-26 23:31:01";
|
|
3448
3469
|
|
|
3449
3470
|
// src/client/components/MainSection.jsx
|
|
3450
3471
|
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
@@ -3487,6 +3508,8 @@ function TailscaleMobileSection(props) {
|
|
|
3487
3508
|
const [isSaving, setIsSaving] = (0, import_react9.useState)(false);
|
|
3488
3509
|
const [showVersionTip, setShowVersionTip] = (0, import_react9.useState)(false);
|
|
3489
3510
|
const [currentLang, setCurrentLang] = (0, import_react9.useState)(resolveLocale(ctx, status));
|
|
3511
|
+
const [npmLatestVersion, setNpmLatestVersion] = (0, import_react9.useState)("");
|
|
3512
|
+
const [hasUpdate, setHasUpdate] = (0, import_react9.useState)(false);
|
|
3490
3513
|
const [conflictBannerDismissed, setConflictBannerDismissed] = (0, import_react9.useState)(false);
|
|
3491
3514
|
const qrTabTouchedRef = (0, import_react9.useRef)(false);
|
|
3492
3515
|
const updateQrTab = (0, import_react9.useCallback)((tab) => {
|
|
@@ -3560,6 +3583,34 @@ function TailscaleMobileSection(props) {
|
|
|
3560
3583
|
}).catch(() => {
|
|
3561
3584
|
});
|
|
3562
3585
|
}, [refreshStatusOnly]);
|
|
3586
|
+
(0, import_react9.useEffect)(() => {
|
|
3587
|
+
let cancelled = false;
|
|
3588
|
+
fetch("https://registry.npmjs.org/dsh-remote-mobile/latest", { cache: "no-store" }).then((res) => res.ok ? res.json() : Promise.reject(new Error("npm registry error"))).then((data) => {
|
|
3589
|
+
if (cancelled) return;
|
|
3590
|
+
const v = data && data.version ? String(data.version) : "";
|
|
3591
|
+
if (!v) return;
|
|
3592
|
+
setNpmLatestVersion(v);
|
|
3593
|
+
const installed = String(PLUGIN_VERSION).replace(/^v/i, "");
|
|
3594
|
+
setHasUpdate(compareVersions(v, installed) > 0);
|
|
3595
|
+
}).catch(() => {
|
|
3596
|
+
if (cancelled) return;
|
|
3597
|
+
setHasUpdate(false);
|
|
3598
|
+
});
|
|
3599
|
+
return () => {
|
|
3600
|
+
cancelled = true;
|
|
3601
|
+
};
|
|
3602
|
+
}, []);
|
|
3603
|
+
const compareVersions = (a, b) => {
|
|
3604
|
+
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
3605
|
+
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
|
|
3606
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
3607
|
+
const da = pa[i] || 0;
|
|
3608
|
+
const db = pb[i] || 0;
|
|
3609
|
+
if (da > db) return 1;
|
|
3610
|
+
if (da < db) return -1;
|
|
3611
|
+
}
|
|
3612
|
+
return 0;
|
|
3613
|
+
};
|
|
3563
3614
|
(0, import_react9.useEffect)(() => {
|
|
3564
3615
|
refreshStatusAndCode();
|
|
3565
3616
|
const localeUnsubs = [];
|
|
@@ -3994,7 +4045,34 @@ function TailscaleMobileSection(props) {
|
|
|
3994
4045
|
) : null
|
|
3995
4046
|
]
|
|
3996
4047
|
}
|
|
3997
|
-
)
|
|
4048
|
+
),
|
|
4049
|
+
hasUpdate && npmLatestVersion ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
|
|
4050
|
+
"a",
|
|
4051
|
+
{
|
|
4052
|
+
href: `https://www.npmjs.com/package/dsh-remote-mobile/v/${npmLatestVersion}`,
|
|
4053
|
+
target: "_blank",
|
|
4054
|
+
rel: "noopener noreferrer",
|
|
4055
|
+
style: {
|
|
4056
|
+
display: "inline-flex",
|
|
4057
|
+
alignItems: "center",
|
|
4058
|
+
gap: "4px",
|
|
4059
|
+
padding: "2px 8px",
|
|
4060
|
+
borderRadius: "10px",
|
|
4061
|
+
background: "rgba(203, 36, 49, 0.12)",
|
|
4062
|
+
border: "1px solid rgba(203, 36, 49, 0.3)",
|
|
4063
|
+
color: "var(--dsw-alias-brand-primary, #f87171)",
|
|
4064
|
+
fontSize: "11px",
|
|
4065
|
+
fontWeight: "600",
|
|
4066
|
+
textDecoration: "none",
|
|
4067
|
+
cursor: "pointer",
|
|
4068
|
+
whiteSpace: "nowrap"
|
|
4069
|
+
},
|
|
4070
|
+
children: [
|
|
4071
|
+
"\u{1F680} ",
|
|
4072
|
+
currentLang === "en" ? `Update available: v${npmLatestVersion}` : `\u53D1\u73B0\u65B0\u7248\u672C v${npmLatestVersion}`
|
|
4073
|
+
]
|
|
4074
|
+
}
|
|
4075
|
+
) : null
|
|
3998
4076
|
]
|
|
3999
4077
|
}
|
|
4000
4078
|
),
|
|
@@ -36,19 +36,21 @@ const PRESET_SIDEBAR_CSS = `
|
|
|
36
36
|
/* 0. 禁用浏览器原生橡皮筋下拉刷新与链式溢出滚动,彻底杜绝上下滑动误刷新页面 */
|
|
37
37
|
html,
|
|
38
38
|
body,
|
|
39
|
-
[
|
|
39
|
+
[class*="_frame"],
|
|
40
40
|
[role="dialog"],
|
|
41
41
|
[class*="_dialog"],
|
|
42
42
|
[class*="_modalContent"],
|
|
43
43
|
[class*="_overlay"],
|
|
44
44
|
[class*="_conversationArea"],
|
|
45
|
-
[
|
|
45
|
+
[class*="_centerCol"] {
|
|
46
46
|
overscroll-behavior-y: contain !important;
|
|
47
47
|
overscroll-behavior-x: none !important;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/* 1. 最外层三栏布局:在移动端主工作区占满 100%
|
|
51
|
-
|
|
50
|
+
/* 1. 最外层三栏布局:在移动端主工作区占满 100% 全宽,消除侧边栏强制占位。
|
|
51
|
+
选择器全部基于官方 CSS Modules 稳定后缀(_frame/_sidebarCol/_centerCol,
|
|
52
|
+
前缀 hash 随机、后缀稳定),不依赖 @linxin666/dsh-web-ui-all 注入的
|
|
53
|
+
data-pane / data-dsh-frame 属性,安装与否均生效。 */
|
|
52
54
|
[class*="_frame"] {
|
|
53
55
|
grid-template-columns: 0px minmax(0px, 1fr) 0px !important;
|
|
54
56
|
width: 100vw !important;
|
|
@@ -57,14 +59,12 @@ const PRESET_SIDEBAR_CSS = `
|
|
|
57
59
|
position: relative !important;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
[data-pane="conversation"],
|
|
61
62
|
[class*="_centerCol"] {
|
|
62
63
|
width: 100vw !important;
|
|
63
64
|
max-width: 100vw !important;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
/* 2. 侧边栏最外层容器:作为整体抽屉浮层(阴影仅加在最外层,严禁污染内部列表) */
|
|
67
|
-
[data-pane="sidebar"],
|
|
68
68
|
[class*="_sidebarCol"] {
|
|
69
69
|
position: fixed !important;
|
|
70
70
|
top: 0 !important;
|
|
@@ -75,8 +75,8 @@ const PRESET_SIDEBAR_CSS = `
|
|
|
75
75
|
/* 层级守卫:抽屉必须低于官方弹层体系(模态弹窗 z=1000、下拉/弹出菜单 z=1100),
|
|
76
76
|
否则设置面板的操作菜单(portal 到 body)会被抽屉盖住无法交互。
|
|
77
77
|
官方内容区元素均在 100 层级以下,900 足以保证抽屉覆盖正文。
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
展开态宽度:装了 web-ui 时由它接管(min(88vw,320px));未装时沿用官方
|
|
79
|
+
内联 width(280px 左右),宽度差由下方内层铺满规则消除。 */
|
|
80
80
|
min-width: 0 !important;
|
|
81
81
|
z-index: 900 !important;
|
|
82
82
|
background: var(--dsw-alias-bg-layer-1, #ffffff) !important;
|
|
@@ -93,26 +93,39 @@ const PRESET_SIDEBAR_CSS = `
|
|
|
93
93
|
will-change: auto !important;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/*
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
/* 遮罩:抽屉外全屏黑纱必须压在抽屉(900)之下,否则抽屉内容会被黑纱罩住并
|
|
97
|
+
拦截全部点击。压到 899:低于抽屉、仍高于正文(≤100),「点抽屉外暗处
|
|
98
|
+
自动收回」的能力保留。
|
|
99
|
+
- 装了 web-ui 时:它生成 [data-dsh-frame]:not([data-sidebar-collapsed])::after
|
|
100
|
+
(z-index:1050),由下方 [class*="_frame"]::after 的 !important 统一压到 899。
|
|
101
|
+
- 未装 web-ui 时:官方无遮罩,由下方 :not([data-sidebar-collapsed]) 规则补齐。 */
|
|
102
|
+
[class*="_frame"]::after {
|
|
101
103
|
z-index: 899 !important;
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
[class*="_frame"]:not([data-sidebar-collapsed])::after {
|
|
107
|
+
content: "";
|
|
108
|
+
position: fixed;
|
|
109
|
+
inset: 0;
|
|
110
|
+
z-index: 899;
|
|
111
|
+
background: rgb(0 0 0 / 24%);
|
|
112
|
+
}
|
|
113
|
+
|
|
104
114
|
/* 内层根节点铺满抽屉:覆盖官方响应式脚本写入的内联 width,
|
|
105
|
-
消除「外层 320 /
|
|
106
|
-
[
|
|
115
|
+
消除「外层 ~320 / 内层内联 280」的宽度差与右缘空白条 */
|
|
116
|
+
[class*="_sidebarCol"] > [data-slot="sidebar"] > div {
|
|
107
117
|
width: 100% !important;
|
|
108
118
|
min-width: 0 !important;
|
|
109
119
|
max-width: none !important;
|
|
110
120
|
height: 100% !important;
|
|
111
121
|
}
|
|
112
122
|
|
|
113
|
-
/* 3. 折叠状态:侧边栏收起为 0px
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
/* 3. 折叠状态:侧边栏收起为 0px 并隐藏整体阴影和多余按钮。
|
|
124
|
+
data-sidebar-collapsed 属性与 _collapsed class 均由官方 sidebar 插件写入,
|
|
125
|
+
两种环境(装/不装 web-ui)行为一致,这里用无值匹配 [data-sidebar-collapsed]
|
|
126
|
+
保证属性不带值也能命中。 */
|
|
127
|
+
[class*="_sidebarCol"]:has([class*="_collapsed"]),
|
|
128
|
+
[class*="_frame"][data-sidebar-collapsed] [class*="_sidebarCol"] {
|
|
116
129
|
width: 0px !important;
|
|
117
130
|
min-width: 0px !important;
|
|
118
131
|
border: none !important;
|
|
@@ -121,14 +134,14 @@ const PRESET_SIDEBAR_CSS = `
|
|
|
121
134
|
pointer-events: none !important;
|
|
122
135
|
}
|
|
123
136
|
|
|
124
|
-
[
|
|
125
|
-
[
|
|
137
|
+
[class*="_sidebarCol"] [class*="_collapsed"] > :not([class*="_logoRow"]),
|
|
138
|
+
[class*="_frame"][data-sidebar-collapsed] [class*="_sidebarCol"] > div > div > :not([class*="_logoRow"]) {
|
|
126
139
|
display: none !important;
|
|
127
140
|
}
|
|
128
141
|
|
|
129
142
|
/* 折叠状态下,Logo 按钮悬浮在屏幕左上角作为展开把手(与 dsh-draggable-nav 拖拽脚本联动) */
|
|
130
|
-
[
|
|
131
|
-
[
|
|
143
|
+
[class*="_sidebarCol"] [class*="_collapsed"] [class*="_logoRow"],
|
|
144
|
+
[class*="_frame"][data-sidebar-collapsed] [class*="_sidebarCol"] [class*="_logoRow"] {
|
|
132
145
|
position: fixed !important;
|
|
133
146
|
top: 10px !important;
|
|
134
147
|
left: 10px !important;
|
|
@@ -155,6 +168,30 @@ const PRESET_SIDEBAR_CSS = `
|
|
|
155
168
|
html[data-dsh-mobile="1"] [role="tooltip"] {
|
|
156
169
|
display: none !important;
|
|
157
170
|
}
|
|
171
|
+
|
|
172
|
+
/* 5. 窄屏隐藏官方详情(details)列:官方会把未折叠的详情面板做成全屏浮层
|
|
173
|
+
盖在 conversation 顶部(web-ui 窄屏折叠态也是 display:none),这里直接隐藏,
|
|
174
|
+
彻底杜绝缩小窗口时详情面板与顶部 header 重叠。 */
|
|
175
|
+
[class*="_detailsCol"] {
|
|
176
|
+
display: none !important;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* 6. 已装 web-ui 时,其移动端 conversation-header 为折叠 rail 预留 60px 左内边距;
|
|
180
|
+
本插件折叠为 0px + 悬浮把手,header 无需让位,覆盖为 8px。
|
|
181
|
+
用更高特异性((0,2,1) > web-ui 的 (0,2,0))保证无论加载顺序都压过 web-ui;
|
|
182
|
+
仅在与 web-ui 相同的 ≤768px 断点生效,避免干扰 769-900px 的官方 header。 */
|
|
183
|
+
@media (max-width: 768px) {
|
|
184
|
+
[class*="_frame"] header[data-dsh-responsive-part="conversation-header"] {
|
|
185
|
+
padding-left: 8px !important;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* 7. 覆盖 web-ui 给 session-utilities 按钮强制的 min-height: 44px(min-width 保留)。
|
|
189
|
+
该规则特异性与 web-ui 同为 (0,3,0),故补一个重复属性选择器抬到 (0,4,0)
|
|
190
|
+
确保无论加载顺序都压过 web-ui;仅在同断点 ≤768px 生效。 */
|
|
191
|
+
[class*="_frame"] [data-dsh-responsive-part="session-utilities"][data-dsh-responsive-part] :is(button, [role="button"]) {
|
|
192
|
+
min-height: 0 !important;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
158
195
|
`;
|
|
159
196
|
/** 设置面板预设:官方设置弹窗整体微缩 + 遮罩锁滚动 + 内容横向滑动 */
|
|
160
197
|
const PRESET_SETTINGS_CSS = `
|
|
@@ -175,10 +212,47 @@ const PRESET_SETTINGS_CSS = `
|
|
|
175
212
|
overscroll-behavior: contain !important;
|
|
176
213
|
}
|
|
177
214
|
|
|
178
|
-
/*
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
215
|
+
/* 移动端设置弹窗顶部固定提示:内容较宽(530px 等比微缩)需左右滑动浏览。
|
|
216
|
+
挂在 overlay 容器 ::before 上(absolute 定位相对 fixed overlay),
|
|
217
|
+
不随弹窗内容滚动,也不参与 flex 居中布局。内测声明等通用 Modal
|
|
218
|
+
(_root 容器,不在本选择器集合内)不会出现此提示。 */
|
|
219
|
+
[data-slot*="overlay"]:has([role="dialog"])::before,
|
|
220
|
+
[class*="_overlay"]:has([role="dialog"])::before,
|
|
221
|
+
[class*="_modalWrapper"]:has([role="dialog"])::before {
|
|
222
|
+
content: "↔ 左右滑动浏览设置";
|
|
223
|
+
position: absolute;
|
|
224
|
+
top: 16px;
|
|
225
|
+
left: 50%;
|
|
226
|
+
transform: translateX(-50%);
|
|
227
|
+
z-index: 100006 !important;
|
|
228
|
+
box-sizing: border-box;
|
|
229
|
+
white-space: nowrap;
|
|
230
|
+
max-width: calc(100vw - 32px);
|
|
231
|
+
overflow: hidden;
|
|
232
|
+
text-overflow: ellipsis;
|
|
233
|
+
background: rgba(0, 0, 0, 0.62);
|
|
234
|
+
color: #ffffff;
|
|
235
|
+
font-size: 12px;
|
|
236
|
+
line-height: 18px;
|
|
237
|
+
border-radius: 999px;
|
|
238
|
+
padding: 4px 14px;
|
|
239
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
|
|
240
|
+
pointer-events: none;
|
|
241
|
+
text-align: center;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* 弹窗卡片:仅命中设置弹窗——外层 overlay 容器内含 role=dialog(设置弹窗由
|
|
245
|
+
data-slot overlay / overlay 类包裹)。内测声明等通用 Modal 直接挂在 body、
|
|
246
|
+
无 data-slot overlay,天然排除,不会被 94vw / 530px 覆写。 */
|
|
247
|
+
[data-slot*="overlay"]:has([role="dialog"]) [role="dialog"],
|
|
248
|
+
[data-slot*="overlay"]:has([role="dialog"]) [class*="_dialog"],
|
|
249
|
+
[data-slot*="overlay"]:has([role="dialog"]) [class*="_modalContent"],
|
|
250
|
+
[class*="_overlay"]:has([role="dialog"]) [role="dialog"],
|
|
251
|
+
[class*="_overlay"]:has([role="dialog"]) [class*="_dialog"],
|
|
252
|
+
[class*="_overlay"]:has([role="dialog"]) [class*="_modalContent"],
|
|
253
|
+
[class*="_modalWrapper"]:has([role="dialog"]) [role="dialog"],
|
|
254
|
+
[class*="_modalWrapper"]:has([role="dialog"]) [class*="_dialog"],
|
|
255
|
+
[class*="_modalWrapper"]:has([role="dialog"]) [class*="_modalContent"],
|
|
182
256
|
[class*="_settingsModal"] {
|
|
183
257
|
position: relative !important;
|
|
184
258
|
width: 94vw !important;
|
|
@@ -198,14 +272,46 @@ const PRESET_SETTINGS_CSS = `
|
|
|
198
272
|
font-size: 13.5px !important;
|
|
199
273
|
}
|
|
200
274
|
|
|
201
|
-
/*
|
|
202
|
-
|
|
203
|
-
[
|
|
204
|
-
[class*="
|
|
275
|
+
/* 弹窗内部等比微缩:dialog 的全部直接子元素(如设置弹窗的 nav 左侧按钮列表 +
|
|
276
|
+
content 右侧内容)统一 zoom 0.88,保证整窗一致缩放。 */
|
|
277
|
+
[data-slot*="overlay"]:has([role="dialog"]) [role="dialog"] > *,
|
|
278
|
+
[data-slot*="overlay"]:has([role="dialog"]) [class*="_dialog"] > *,
|
|
279
|
+
[data-slot*="overlay"]:has([role="dialog"]) [class*="_modalContent"] > *,
|
|
280
|
+
[class*="_overlay"]:has([role="dialog"]) [role="dialog"] > *,
|
|
281
|
+
[class*="_overlay"]:has([role="dialog"]) [class*="_dialog"] > *,
|
|
282
|
+
[class*="_overlay"]:has([role="dialog"]) [class*="_modalContent"] > *,
|
|
283
|
+
[class*="_modalWrapper"]:has([role="dialog"]) [role="dialog"] > *,
|
|
284
|
+
[class*="_modalWrapper"]:has([role="dialog"]) [class*="_dialog"] > *,
|
|
285
|
+
[class*="_modalWrapper"]:has([role="dialog"]) [class*="_modalContent"] > *,
|
|
286
|
+
[class*="_settingsModal"] > * {
|
|
287
|
+
zoom: 0.88 !important;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* 弹窗右侧内容主体(直接 div 子元素):固定最小宽度并铺满高度,单屏显示更多
|
|
291
|
+
内容且字号更精致。左侧导航列表不强制宽度,保持官方窄宽。
|
|
292
|
+
同样限定在设置弹窗上下文内,避免波及通用 Modal。 */
|
|
293
|
+
[data-slot*="overlay"]:has([role="dialog"]) [role="dialog"] > div,
|
|
294
|
+
[data-slot*="overlay"]:has([role="dialog"]) [class*="_dialog"] > div,
|
|
295
|
+
[data-slot*="overlay"]:has([role="dialog"]) [class*="_modalContent"] > div,
|
|
296
|
+
[class*="_overlay"]:has([role="dialog"]) [role="dialog"] > div,
|
|
297
|
+
[class*="_overlay"]:has([role="dialog"]) [class*="_dialog"] > div,
|
|
298
|
+
[class*="_overlay"]:has([role="dialog"]) [class*="_modalContent"] > div,
|
|
299
|
+
[class*="_modalWrapper"]:has([role="dialog"]) [role="dialog"] > div,
|
|
300
|
+
[class*="_modalWrapper"]:has([role="dialog"]) [class*="_dialog"] > div,
|
|
301
|
+
[class*="_modalWrapper"]:has([role="dialog"]) [class*="_modalContent"] > div,
|
|
302
|
+
[class*="_settingsModal"] > div {
|
|
205
303
|
min-width: 530px !important;
|
|
206
304
|
width: 530px !important;
|
|
207
305
|
height: 100% !important;
|
|
208
|
-
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* 重置插件市场等弹窗在窄屏隐藏左侧 nav 的行为:保持导航列表可见。
|
|
309
|
+
该插件规则(@media max-width:560px 隐藏)无 !important,
|
|
310
|
+
我们同选择器加 !important 即可稳压。 */
|
|
311
|
+
@media (max-width: 560px) {
|
|
312
|
+
[role="dialog"]:has([data-dsh-market-root]) > nav {
|
|
313
|
+
display: block !important;
|
|
314
|
+
}
|
|
209
315
|
}
|
|
210
316
|
`;
|
|
211
317
|
/** 正文预设:对话区紧凑高密度排版(小字号/紧凑行距/等宽代码/防溢出) */
|
|
@@ -218,7 +324,7 @@ const PRESET_MAIN_CSS = `
|
|
|
218
324
|
内容容器用 CSS Modules localName 后缀(只有前缀 hash 随机,后缀稳定)。 */
|
|
219
325
|
|
|
220
326
|
/* 1. 消息内容容器:紧凑字号与行距 */
|
|
221
|
-
[
|
|
327
|
+
[class*="_centerCol"],
|
|
222
328
|
[data-slot^="conversation.chat"],
|
|
223
329
|
[class*="_markdown"],
|
|
224
330
|
[class*="_message"],
|
|
@@ -230,7 +336,7 @@ const PRESET_MAIN_CSS = `
|
|
|
230
336
|
}
|
|
231
337
|
|
|
232
338
|
/* 2. HTML 元素级(稳定):段落与标题层级 */
|
|
233
|
-
[
|
|
339
|
+
[class*="_centerCol"] p,
|
|
234
340
|
[data-slot^="conversation.chat"] p,
|
|
235
341
|
[class*="_markdown"] p,
|
|
236
342
|
[class*="_message"] p,
|
|
@@ -240,10 +346,10 @@ const PRESET_MAIN_CSS = `
|
|
|
240
346
|
margin: 0 0 0.4em 0 !important;
|
|
241
347
|
}
|
|
242
348
|
|
|
243
|
-
[
|
|
244
|
-
[
|
|
245
|
-
[
|
|
246
|
-
[
|
|
349
|
+
[class*="_centerCol"] h1,
|
|
350
|
+
[class*="_centerCol"] h2,
|
|
351
|
+
[class*="_centerCol"] h3,
|
|
352
|
+
[class*="_centerCol"] h4,
|
|
247
353
|
[data-slot^="conversation.chat"] h1,
|
|
248
354
|
[data-slot^="conversation.chat"] h2,
|
|
249
355
|
[data-slot^="conversation.chat"] h3,
|
|
@@ -256,14 +362,14 @@ const PRESET_MAIN_CSS = `
|
|
|
256
362
|
line-height: 1.3 !important;
|
|
257
363
|
}
|
|
258
364
|
|
|
259
|
-
[
|
|
260
|
-
[
|
|
365
|
+
[class*="_centerCol"] ul,
|
|
366
|
+
[class*="_centerCol"] ol,
|
|
261
367
|
[class*="_markdown"] ul,
|
|
262
368
|
[class*="_markdown"] ol {
|
|
263
369
|
margin: 0.2em 0 0.4em 0 !important;
|
|
264
370
|
padding-left: 1.2em !important;
|
|
265
371
|
}
|
|
266
|
-
[
|
|
372
|
+
[class*="_centerCol"] li,
|
|
267
373
|
[class*="_markdown"] li {
|
|
268
374
|
font-size: 12.5px !important;
|
|
269
375
|
line-height: 1.45 !important;
|
|
@@ -271,13 +377,13 @@ const PRESET_MAIN_CSS = `
|
|
|
271
377
|
}
|
|
272
378
|
|
|
273
379
|
/* 3. 行内代码与代码块:小等宽 + 防横向溢出 */
|
|
274
|
-
[
|
|
380
|
+
[class*="_centerCol"] code,
|
|
275
381
|
[data-slot^="conversation.chat"] code,
|
|
276
382
|
[class*="_markdown"] code {
|
|
277
383
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
|
|
278
384
|
font-size: 11.5px !important;
|
|
279
385
|
}
|
|
280
|
-
[
|
|
386
|
+
[class*="_centerCol"] pre,
|
|
281
387
|
[data-slot^="conversation.chat"] pre,
|
|
282
388
|
[class*="_markdown"] pre {
|
|
283
389
|
font-size: 11.5px !important;
|
|
@@ -288,7 +394,7 @@ const PRESET_MAIN_CSS = `
|
|
|
288
394
|
}
|
|
289
395
|
|
|
290
396
|
/* 表格:紧凑字号/内边距 + 窄屏横向滚动防撑破(display:block 让表格宽度受容器约束) */
|
|
291
|
-
[
|
|
397
|
+
[class*="_centerCol"] table,
|
|
292
398
|
[data-slot^="conversation.chat"] table,
|
|
293
399
|
[class*="_markdown"] table {
|
|
294
400
|
font-size: 11.5px !important;
|
|
@@ -298,8 +404,8 @@ const PRESET_MAIN_CSS = `
|
|
|
298
404
|
overflow-x: auto !important;
|
|
299
405
|
-webkit-overflow-scrolling: touch !important;
|
|
300
406
|
}
|
|
301
|
-
[
|
|
302
|
-
[
|
|
407
|
+
[class*="_centerCol"] table th,
|
|
408
|
+
[class*="_centerCol"] table td,
|
|
303
409
|
[data-slot^="conversation.chat"] table th,
|
|
304
410
|
[data-slot^="conversation.chat"] table td,
|
|
305
411
|
[class*="_markdown"] table th,
|
|
@@ -310,13 +416,13 @@ const PRESET_MAIN_CSS = `
|
|
|
310
416
|
}
|
|
311
417
|
|
|
312
418
|
/* 3.5 可折叠披露行(data-disclosure-row,如“Code/思考”等行):标题与摘要跟随紧凑字号 */
|
|
313
|
-
[
|
|
419
|
+
[class*="_centerCol"] [data-disclosure-row],
|
|
314
420
|
[data-slot^="conversation.chat"] [data-disclosure-row] {
|
|
315
421
|
font-size: 12.5px !important;
|
|
316
422
|
line-height: 1.45 !important;
|
|
317
423
|
}
|
|
318
|
-
[
|
|
319
|
-
[
|
|
424
|
+
[class*="_centerCol"] [data-disclosure-row] [class*="_title"],
|
|
425
|
+
[class*="_centerCol"] [data-disclosure-row] [class*="_summary"],
|
|
320
426
|
[data-slot^="conversation.chat"] [data-disclosure-row] [class*="_title"],
|
|
321
427
|
[data-slot^="conversation.chat"] [data-disclosure-row] [class*="_summary"] {
|
|
322
428
|
font-size: 12.5px !important;
|
|
@@ -327,32 +433,32 @@ const PRESET_MAIN_CSS = `
|
|
|
327
433
|
/* 3.7 消息列表双层滚动容器(wSkVaW_scrollBody + Md3f7G_scroll)padding 叠加:
|
|
328
434
|
官方各带 12~16px(合计每侧最多 28px),统一每层压到 8px(每侧合计 16px)。
|
|
329
435
|
均排除输入区滚动容器,避免误伤 composer。 */
|
|
330
|
-
[
|
|
331
|
-
[
|
|
436
|
+
[class*="_centerCol"] [class*="_scrollBody"],
|
|
437
|
+
[class*="_centerCol"] [class*="_scroll"]:not([data-composer-card] [class*="_scroll"]) {
|
|
332
438
|
padding: 8px 8px !important;
|
|
333
439
|
}
|
|
334
440
|
|
|
335
441
|
/* 4. 消息操作条(时间戳/复制等)与内行图标:显式收敛(SVG 不随字号缩放,需显式尺寸) */
|
|
336
|
-
[
|
|
442
|
+
[class*="_centerCol"] [class*="_actions"],
|
|
337
443
|
[data-slot^="conversation.chat"] [class*="_actions"] {
|
|
338
444
|
font-size: 11px !important;
|
|
339
445
|
line-height: 1.2 !important;
|
|
340
446
|
}
|
|
341
|
-
[
|
|
342
|
-
[
|
|
343
|
-
[
|
|
447
|
+
[class*="_centerCol"] [class*="_actions"] [class*="_timeStart"],
|
|
448
|
+
[class*="_centerCol"] [class*="_actions"] [class*="_timeEnd"],
|
|
449
|
+
[class*="_centerCol"] [class*="_actions"] [class*="_time"],
|
|
344
450
|
[data-slot^="conversation.chat"] [class*="_actions"] [class*="_timeStart"],
|
|
345
451
|
[data-slot^="conversation.chat"] [class*="_actions"] [class*="_timeEnd"],
|
|
346
452
|
[data-slot^="conversation.chat"] [class*="_actions"] [class*="_time"] {
|
|
347
453
|
font-size: 11px !important;
|
|
348
454
|
}
|
|
349
455
|
/* 用时统计内各指标/分隔点间距收紧(margin: 0) */
|
|
350
|
-
[
|
|
456
|
+
[class*="_centerCol"] [class*="_actions"] [class*="_timeEnd"] span,
|
|
351
457
|
[data-slot^="conversation.chat"] [class*="_actions"] [class*="_timeEnd"] span {
|
|
352
458
|
margin: 0 !important;
|
|
353
459
|
}
|
|
354
460
|
/* 统计行:单行优先,空间不足才折行(不再强制独占整行);折行后与消息左缘对齐(margin-left 清零) */
|
|
355
|
-
[
|
|
461
|
+
[class*="_centerCol"] [class*="_actions"] [class*="_timeEnd"],
|
|
356
462
|
[data-slot^="conversation.chat"] [class*="_actions"] [class*="_timeEnd"] {
|
|
357
463
|
flex: 0 1 auto !important;
|
|
358
464
|
min-width: 0 !important;
|
|
@@ -362,15 +468,15 @@ const PRESET_MAIN_CSS = `
|
|
|
362
468
|
text-overflow: clip !important;
|
|
363
469
|
padding-left: 6px !important; /* 折行时也保留 6px 缩进,与上方内容区分 */
|
|
364
470
|
}
|
|
365
|
-
[
|
|
471
|
+
[class*="_centerCol"] [class*="_actions"],
|
|
366
472
|
[data-slot^="conversation.chat"] [class*="_actions"] {
|
|
367
473
|
flex-wrap: wrap !important;
|
|
368
474
|
height: auto !important;
|
|
369
475
|
overflow: visible !important;
|
|
370
476
|
}
|
|
371
|
-
[
|
|
477
|
+
[class*="_centerCol"] [class*="_actions"] svg,
|
|
372
478
|
[data-slot^="conversation.chat"] [class*="_actions"] svg,
|
|
373
|
-
[
|
|
479
|
+
[class*="_centerCol"] [class*="_leading"] svg,
|
|
374
480
|
[data-slot^="conversation.chat"] [class*="_leading"] svg {
|
|
375
481
|
width: 13px !important;
|
|
376
482
|
height: 13px !important;
|
|
@@ -419,7 +525,47 @@ const PRESET_MAIN_CSS = `
|
|
|
419
525
|
max-height: 13px !important;
|
|
420
526
|
flex: none !important;
|
|
421
527
|
}
|
|
422
|
-
|
|
528
|
+
/* composer 工具栏紧凑:保持两行布局不变,仅缩小控件尺寸。
|
|
529
|
+
以工具栏行(_row,即 uV2eYG_row)为锚点,避免误伤输入文本区内的
|
|
530
|
+
文本引用 / chip 等行内元素。特异性 (0,2,0)~ (0,3,0) 均高于官方单类 (0,1,0)。 */
|
|
531
|
+
[data-composer-card="true"] [class*="_row"] {
|
|
532
|
+
gap: 6px !important;
|
|
533
|
+
padding: 2px 6px 4px !important;
|
|
534
|
+
}
|
|
535
|
+
[data-composer-card="true"] [class*="_row"] [class*="_tools"],
|
|
536
|
+
[data-composer-card="true"] [class*="_row"] [class*="_modes"],
|
|
537
|
+
[data-composer-card="true"] [class*="_row"] [class*="_trailing"] {
|
|
538
|
+
gap: 6px !important;
|
|
539
|
+
}
|
|
540
|
+
[data-composer-card="true"] [class*="_row"] [class*="_add"],
|
|
541
|
+
[data-composer-card="true"] [class*="_row"] [class*="_trigger"] {
|
|
542
|
+
padding: 0 4px !important;
|
|
543
|
+
gap: 2px !important;
|
|
544
|
+
}
|
|
545
|
+
[data-composer-card="true"] [class*="_row"] [class*="_add"] {
|
|
546
|
+
width: 26px !important;
|
|
547
|
+
min-width: 26px !important;
|
|
548
|
+
height: 26px !important;
|
|
549
|
+
min-height: 26px !important;
|
|
550
|
+
}
|
|
551
|
+
[data-composer-card="true"] [class*="_row"] [class*="_triggerLabel"],
|
|
552
|
+
[data-composer-card="true"] [class*="_row"] [class*="_triggerEffort"] {
|
|
553
|
+
font-size: 12px !important;
|
|
554
|
+
line-height: 16px !important;
|
|
555
|
+
}
|
|
556
|
+
[data-composer-card="true"] [class*="_row"] [class*="_primary"] {
|
|
557
|
+
width: 30px !important;
|
|
558
|
+
height: 30px !important;
|
|
559
|
+
min-width: 30px !important;
|
|
560
|
+
min-height: 30px !important;
|
|
561
|
+
}
|
|
562
|
+
[data-composer-card="true"] [class*="_row"] svg {
|
|
563
|
+
width: 12px !important;
|
|
564
|
+
height: 12px !important;
|
|
565
|
+
max-width: 12px !important;
|
|
566
|
+
max-height: 12px !important;
|
|
567
|
+
flex: none !important;
|
|
568
|
+
}
|
|
423
569
|
`;
|
|
424
570
|
/**
|
|
425
571
|
* 内置官方预设:按界面区域划分的三段预设。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-mobile",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "DeepSeek Harness 远程与移动端安全网关插件:零修改 DSH 底层代码安全开放局域网与 Tailscale 连接 | DeepSeek Harness (DSH) Remote & Mobile Security Guard: safely opens Tailscale/LAN with zero core modifications, QR scan auth, RSA encryption & brute-force defense.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|