dsh-pocket 2.1.1 → 2.1.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.
package/README.en.md CHANGED
@@ -191,9 +191,11 @@ Such tools take over all traffic and often cut cloudflared's tunnel-edge connect
191
191
  ```sh
192
192
  npm install
193
193
  node client/build.mjs # rebuild after editing client/
194
- npm test # proxy / auth / compression / tunnel / service / RPC (43 tests)
194
+ npm test # proxy / auth / compression / tunnel / service / RPC / settings (91 tests)
195
195
  ```
196
196
 
197
+ **Want to try your changes locally without publishing?** Point the installed plugin at your local checkout with a symlink and restart dsh web. Full steps (including switching back to the npm release) are in [LOCAL-DEV.md](./LOCAL-DEV.md).
198
+
197
199
  ## 🤝 Credits
198
200
 
199
201
  - Mobile adaptation ported from [mexiaosqwq/dsh-web-mobile](https://github.com/mexiaosqwq/dsh-web-mobile) (MIT)
package/README.md CHANGED
@@ -192,9 +192,11 @@ npx @deepseek-ai/dsh web
192
192
  ```sh
193
193
  npm install
194
194
  node client/build.mjs # 改 client/ 后重新打包
195
- npm test # 代理 / 认证 / 压缩 / 隧道 / 服务 / RPC(43 测试)
195
+ npm test # 代理 / 认证 / 压缩 / 隧道 / 服务 / RPC / 设置(91 测试)
196
196
  ```
197
197
 
198
+ **改完想在本机先试?** 不用发版:把插件换成指向本地仓库的软链,重启 dsh web 就是本地代码。完整步骤(含怎么换回 npm 官方版本)见 [LOCAL-DEV.md](./LOCAL-DEV.md)。
199
+
198
200
  ## 🤝 致谢
199
201
 
200
202
  - 移动端适配移植自 [mexiaosqwq/dsh-web-mobile](https://github.com/mexiaosqwq/dsh-web-mobile)(MIT)
package/client/client.js CHANGED
@@ -140,6 +140,41 @@ function MobileNavToggle({ toggleSidebar, t }) {
140
140
  // client/mobile/MobileNavOverlay.tsx
141
141
  var import_react = require("react");
142
142
  var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
143
+
144
+ // client/mobile/nav-targets.mjs
145
+ var DRAWER_SELECTOR = '[data-mobile-nav="frame"] > :first-child';
146
+ var TOGGLE_SELECTOR = '[data-mobile-nav="toggle"]';
147
+ var NAV_TARGETS = [
148
+ "button[data-dsh-taskboard-entry]",
149
+ "button[data-dsh-ssh-entry]",
150
+ // 抽屉底部的 "文件" 入口打开的是 dsh-web-ui 的 explorer 面板,它的 z-index
151
+ // (55) 低于展开的抽屉 (600),且在抽屉 DOM 之外:抽屉不关就会盖住面板,点
152
+ // 面板里的行又会被"点抽屉外就关"吃掉。所以按导航处理,一起关掉。
153
+ '[data-mobile-nav="files"]',
154
+ '[class*="sessionRow"]',
155
+ '[class*="newSession"]',
156
+ '[class*="searchResultWorkspace"]',
157
+ '[class*="searchResultRow"]'
158
+ ].join(", ");
159
+ var NAV_EXCLUDE = '[class*="sessionRow"] button';
160
+ var OVERLAY_SELECTOR = [
161
+ '[role="menu"]',
162
+ '[role="listbox"]',
163
+ '[role="dialog"]',
164
+ '[role="tooltip"]',
165
+ "[data-radix-popper-content-wrapper]"
166
+ ].join(", ");
167
+ function navTargetFor(target) {
168
+ if (target == null || typeof target.closest !== "function") return null;
169
+ if (target.closest(NAV_EXCLUDE) !== null) return null;
170
+ return target.closest(NAV_TARGETS);
171
+ }
172
+ function isOverlayTap(target) {
173
+ if (target == null || typeof target.closest !== "function") return false;
174
+ return target.closest(OVERLAY_SELECTOR) !== null;
175
+ }
176
+
177
+ // client/mobile/MobileNavOverlay.tsx
143
178
  var MOBILE_QUERY = "(max-width: 1023px)";
144
179
  function useMobile() {
145
180
  const [mobile, setMobile] = (0, import_react.useState)(() => window.matchMedia(MOBILE_QUERY).matches);
@@ -205,25 +240,47 @@ function MobileNavOverlay({ toggleSidebar, t }) {
205
240
  if (document.querySelector('[aria-modal="true"]') !== null) return;
206
241
  const target = event.target;
207
242
  if (target === null) return;
208
- const drawer = document.querySelector('[data-mobile-nav="frame"] > :first-child');
243
+ const drawer = document.querySelector(DRAWER_SELECTOR);
209
244
  if (drawer === null || !drawer.contains(target)) return;
210
- if (target.closest('[class*="sessionRow"] button') !== null) return;
211
- const navigates = target.closest(
212
- 'button[data-dsh-taskboard-entry], button[data-dsh-ssh-entry], [class*="newSession"], [class*="sessionRow"], [class*="searchResultRow"], [class*="searchResultWorkspace"], [data-mobile-nav="files"]'
213
- );
214
- if (navigates !== null) toggleSidebar();
245
+ if (navTargetFor(target) !== null) toggleSidebar();
215
246
  };
216
247
  document.addEventListener("click", onDrawerClick, true);
217
248
  return () => document.removeEventListener("click", onDrawerClick, true);
218
249
  }, [mobile, open, toggleSidebar]);
250
+ (0, import_react.useEffect)(() => {
251
+ if (!mobile || !open) return;
252
+ let timer = null;
253
+ const onDrawerPointerUp = (event) => {
254
+ if (event.pointerType !== "touch" && event.pointerType !== "pen") return;
255
+ const target = event.target;
256
+ if (!(target instanceof Element)) return;
257
+ const drawer = document.querySelector(DRAWER_SELECTOR);
258
+ if (drawer === null || !drawer.contains(target)) return;
259
+ if (navTargetFor(target) === null) return;
260
+ if (timer !== null) return;
261
+ timer = window.setTimeout(() => {
262
+ timer = null;
263
+ const frame = document.querySelector('[data-mobile-nav="frame"]');
264
+ if (frame === null || frame.hasAttribute("data-sidebar-collapsed")) return;
265
+ const row = navTargetFor(target);
266
+ row?.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
267
+ }, 0);
268
+ };
269
+ document.addEventListener("pointerup", onDrawerPointerUp, true);
270
+ return () => {
271
+ if (timer !== null) window.clearTimeout(timer);
272
+ document.removeEventListener("pointerup", onDrawerPointerUp, true);
273
+ };
274
+ }, [mobile, open]);
219
275
  (0, import_react.useEffect)(() => {
220
276
  if (!mobile || !open) return;
221
277
  const onOutsideClick = (event) => {
222
278
  if (document.querySelector('[aria-modal="true"]') !== null) return;
223
279
  const target = event.target;
224
280
  if (target === null) return;
225
- if (target.closest('[data-mobile-nav="toggle"]') !== null) return;
226
- const drawer = document.querySelector('[data-mobile-nav="frame"] > :first-child');
281
+ if (target.closest(TOGGLE_SELECTOR) !== null) return;
282
+ if (isOverlayTap(target)) return;
283
+ const drawer = document.querySelector(DRAWER_SELECTOR);
227
284
  if (drawer !== null && drawer.contains(target)) return;
228
285
  toggleSidebar();
229
286
  };
@@ -2,6 +2,12 @@ import { useEffect, useLayoutEffect, useState } from 'react'
2
2
  import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
3
3
  import { IconPanelLeftOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
4
4
  import { NS } from './locales.ts'
5
+ import {
6
+ DRAWER_SELECTOR,
7
+ TOGGLE_SELECTOR,
8
+ isOverlayTap,
9
+ navTargetFor,
10
+ } from './nav-targets.mjs'
5
11
 
6
12
  /** Full props for the shell overlay entry. */
7
13
  export interface MobileNavOverlayProps extends PropsRuntime<'shell.overlay'>, PropsLocale<typeof NS> {
@@ -104,7 +110,8 @@ export function MobileNavOverlay({ toggleSidebar, t }: MobileNavOverlayProps) {
104
110
  // - Settings / Session log: their dialogs render INSIDE the drawer DOM
105
111
  // (portaled into the sidebar); closing the drawer would slide the dialog
106
112
  // off-screen with it.
107
- // - Workspace folder chevrons, the logo: pure UI toggles, not navigation.
113
+ // - Workspace folder rows (YDXeBa_projectRow), the logo: pure UI toggles,
114
+ // not navigation — see NAV_TARGETS in nav-targets.mjs.
108
115
  // - Anything while a modal dialog is open: the dialog owns the screen.
109
116
  useEffect(() => {
110
117
  if (!mobile || !open) return
@@ -112,27 +119,64 @@ export function MobileNavOverlay({ toggleSidebar, t }: MobileNavOverlayProps) {
112
119
  if (document.querySelector('[aria-modal="true"]') !== null) return
113
120
  const target = event.target as HTMLElement | null
114
121
  if (target === null) return
115
- const drawer = document.querySelector<HTMLElement>('[data-mobile-nav="frame"] > :first-child')
122
+ const drawer = document.querySelector<HTMLElement>(DRAWER_SELECTOR)
116
123
  if (drawer === null || !drawer.contains(target)) return
117
124
  // A session row's own action buttons — the "Session actions" kebab
118
125
  // (delete / rename), revealed on hover / long-press — open an edit
119
126
  // menu. Tapping one must NOT count as tapping the row, or the drawer
120
127
  // would close and take the just-opened menu with it.
121
- if (target.closest('[class*="sessionRow"] button') !== null) return
122
- // The drawer footer "Files" action opens the dsh-web-ui explorer sheet,
123
- // which sits at a LOWER z-index (55) than the open drawer (600) and
124
- // outside the drawer DOM — leaving the drawer open would let it cover
125
- // the sheet, and tapping a sheet row would be eaten by the tap-outside
126
- // close handler. Close the drawer on Files, like navigation.
127
- const navigates = target.closest(
128
- 'button[data-dsh-taskboard-entry], button[data-dsh-ssh-entry], [class*="newSession"], [class*="sessionRow"], [class*="searchResultRow"], [class*="searchResultWorkspace"], [data-mobile-nav="files"]',
129
- )
130
- if (navigates !== null) toggleSidebar()
128
+ if (navTargetFor(target) !== null) toggleSidebar()
131
129
  }
132
130
  document.addEventListener('click', onDrawerClick, true)
133
131
  return () => document.removeEventListener('click', onDrawerClick, true)
134
132
  }, [mobile, open, toggleSidebar])
135
133
 
134
+ // iOS Safari touch self-heal (issue #72).
135
+ //
136
+ // A tap on a drawer row is delivered to the page as touchstart/touchend
137
+ // plus a browser-synthesized click. On iOS that click is routinely
138
+ // suppressed: a few px of finger drift is classified as a pan, and any DOM
139
+ // shift under the finger before dispatch cancels it outright. When it does
140
+ // not arrive, neither the row's own onClick nor the capture handler above
141
+ // runs — the row looks completely dead ("抽屉点了没反应").
142
+ //
143
+ // So: on touch/pen pointerup inside the drawer that hits a navigation row,
144
+ // arm a one-macrotask timer. When it fires:
145
+ // - the drawer is already closed → the real click did arrive and handled
146
+ // everything → do nothing (zero interference with the normal path);
147
+ // - otherwise the click never came → re-dispatch a bubbling click on the
148
+ // row ourselves. React's delegated listener runs the row's onClick (the
149
+ // session opens / the workspace switches) and the same click bubbles
150
+ // through the capture handler above, which closes the drawer.
151
+ //
152
+ // Mouse/pen-on-desktop keeps the plain click path: pointerType is 'mouse'.
153
+ useEffect(() => {
154
+ if (!mobile || !open) return
155
+ let timer: number | null = null
156
+ const onDrawerPointerUp = (event: PointerEvent): void => {
157
+ if (event.pointerType !== 'touch' && event.pointerType !== 'pen') return
158
+ const target = event.target
159
+ if (!(target instanceof Element)) return
160
+ const drawer = document.querySelector<HTMLElement>(DRAWER_SELECTOR)
161
+ if (drawer === null || !drawer.contains(target)) return
162
+ if (navTargetFor(target) === null) return
163
+ if (timer !== null) return
164
+ timer = window.setTimeout(() => {
165
+ timer = null
166
+ const frame = document.querySelector('[data-mobile-nav="frame"]')
167
+ // Real click already closed it — nothing to heal.
168
+ if (frame === null || frame.hasAttribute('data-sidebar-collapsed')) return
169
+ const row = navTargetFor(target)
170
+ row?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }))
171
+ }, 0)
172
+ }
173
+ document.addEventListener('pointerup', onDrawerPointerUp, true)
174
+ return () => {
175
+ if (timer !== null) window.clearTimeout(timer)
176
+ document.removeEventListener('pointerup', onDrawerPointerUp, true)
177
+ }
178
+ }, [mobile, open])
179
+
136
180
  // Tap-outside closes the drawer (issue #38). The backdrop is now
137
181
  // pointer-events: none (pure dimming layer that never steals clicks), so
138
182
  // "tap the dimmed area to close" moves here: any click outside the drawer
@@ -146,8 +190,15 @@ export function MobileNavOverlay({ toggleSidebar, t }: MobileNavOverlayProps) {
146
190
  if (document.querySelector('[aria-modal="true"]') !== null) return
147
191
  const target = event.target as HTMLElement | null
148
192
  if (target === null) return
149
- if (target.closest('[data-mobile-nav="toggle"]') !== null) return
150
- const drawer = document.querySelector<HTMLElement>('[data-mobile-nav="frame"] > :first-child')
193
+ if (target.closest(TOGGLE_SELECTOR) !== null) return
194
+ // Portaled overlays (issue #72): the workspace section's "视图选项" /
195
+ // "添加工作区" and the session row kebab all open menus that live on
196
+ // document.body — outside the drawer DOM. Without this exemption the
197
+ // first tap on a menu item is eaten: the drawer slides away, the menu
198
+ // unmounts with the sidebar, and the item's onClick never runs, so
199
+ // every workspace control reads as "点了没反应" on a phone.
200
+ if (isOverlayTap(target)) return
201
+ const drawer = document.querySelector<HTMLElement>(DRAWER_SELECTOR)
151
202
  if (drawer !== null && drawer.contains(target)) return
152
203
  toggleSidebar()
153
204
  }
@@ -0,0 +1,85 @@
1
+ // 抽屉点击规则的判定常量与纯函数 —— 抽成独立 ESM 模块,好让 node 测试直接
2
+ // 引入(client 侧是 esbuild 打进 bundle 的,测试里没有 DOM 可用)。
3
+
4
+ /**
5
+ * 抽屉本体:AppFrame 的第一个网格子元素,即被拉成抽屉的侧边栏列。
6
+ * @type {string}
7
+ */
8
+ export const DRAWER_SELECTOR = '[data-mobile-nav="frame"] > :first-child'
9
+
10
+ /** 抽屉开关(会话头部那颗按钮)——点它不该被"点抽屉外就关"二次触发。 */
11
+ export const TOGGLE_SELECTOR = '[data-mobile-nav="toggle"]'
12
+
13
+ /**
14
+ * 命中即视为导航的行:点它们要把屏幕让给刚打开的内容,所以顺带关抽屉。
15
+ *
16
+ * 类名现状(2026-08,侧边栏 qDHVXG_* / YDXeBa_* 这一代):
17
+ * - `YDXeBa_sessionRow` 会话行 —— 命中 [class*="sessionRow"]
18
+ * - `YDXeBa_projectRow` 工作区行 —— **故意不收**:实测它是折叠/展开开关
19
+ * (aria-expanded 来回翻,会话列表显隐),不是导航。收进来会让每次展开
20
+ * 工作区都把抽屉关掉(issue #72)。
21
+ * 旧一代侧边栏的 `_searchResultWorkspace_` / `_searchResultRow_` 保留,兼容
22
+ * 尚未升级的宿主。
23
+ *
24
+ * @type {string}
25
+ */
26
+ export const NAV_TARGETS = [
27
+ 'button[data-dsh-taskboard-entry]',
28
+ 'button[data-dsh-ssh-entry]',
29
+ // 抽屉底部的 "文件" 入口打开的是 dsh-web-ui 的 explorer 面板,它的 z-index
30
+ // (55) 低于展开的抽屉 (600),且在抽屉 DOM 之外:抽屉不关就会盖住面板,点
31
+ // 面板里的行又会被"点抽屉外就关"吃掉。所以按导航处理,一起关掉。
32
+ '[data-mobile-nav="files"]',
33
+ '[class*="sessionRow"]',
34
+ '[class*="newSession"]',
35
+ '[class*="searchResultWorkspace"]',
36
+ '[class*="searchResultRow"]',
37
+ ].join(', ')
38
+
39
+ /**
40
+ * 行内操作按钮(会话行的 kebab:重命名 / 删除)——点开的是菜单不是内容,
41
+ * 不能按导航处理,否则抽屉一关,刚弹出的菜单跟着没了。
42
+ * @type {string}
43
+ */
44
+ export const NAV_EXCLUDE = '[class*="sessionRow"] button'
45
+
46
+ /**
47
+ * 浮层(portal 到 body 的下拉菜单 / 弹窗)。
48
+ *
49
+ * 抽屉里的控件(工作区区块的"视图选项"、"添加工作区",会话行的 kebab…)
50
+ * 弹出的菜单在 DOM 上属于 body,不在抽屉里。"点抽屉外就关"因此会把菜单项
51
+ * 的第一下点击吃掉:抽屉先滑走 → 菜单随侧边栏卸载 → 菜单项的 onClick 永远
52
+ * 不执行。手机上表现就是工作区相关菜单"点了没反应"(issue #72)。
53
+ *
54
+ * 落在浮层内的点击一律豁免,交给浮层自己收尾。
55
+ *
56
+ * @type {string}
57
+ */
58
+ export const OVERLAY_SELECTOR = [
59
+ '[role="menu"]',
60
+ '[role="listbox"]',
61
+ '[role="dialog"]',
62
+ '[role="tooltip"]',
63
+ '[data-radix-popper-content-wrapper]',
64
+ ].join(', ')
65
+
66
+ /**
67
+ * 抽屉内一次点击是否算导航(应关抽屉)。命中 NAV_EXCLUDE 的行内按钮不算。
68
+ * @param {Element | null | undefined} target - 点击目标。
69
+ * @returns {Element | null} 命中的导航行,未命中返回 null。
70
+ */
71
+ export function navTargetFor(target) {
72
+ if (target == null || typeof target.closest !== 'function') return null
73
+ if (target.closest(NAV_EXCLUDE) !== null) return null
74
+ return target.closest(NAV_TARGETS)
75
+ }
76
+
77
+ /**
78
+ * 一次点击是否落在浮层内(浮层自己会关闭,不该由抽屉规则接管)。
79
+ * @param {Element | null | undefined} target - 点击目标。
80
+ * @returns {boolean}
81
+ */
82
+ export function isOverlayTap(target) {
83
+ if (target == null || typeof target.closest !== 'function') return false
84
+ return target.closest(OVERLAY_SELECTOR) !== null
85
+ }
package/lib/index.js CHANGED
@@ -20,7 +20,7 @@ import { randomBytes } from 'node:crypto';
20
20
  import { createPocketService } from './service.mjs';
21
21
  import { installPocketRpc } from './web-rpc.js';
22
22
  import { restartHost } from './restart.js';
23
- import { desktopEnvPatchScript, advancedNoticeScript, DEFAULT_INJECT, classifyHost } from './proxy.mjs';
23
+ import { advancedNoticeScript, DEFAULT_INJECT, classifyHost } from './proxy.mjs';
24
24
  import { lanEnabled, setLanEnabled, lanAuthEnabled, setLanAuthEnabled, lanIpOverride, setLanIpOverride, pinCustom, setPinCustom, tunnelMode, setTunnelMode, tunnelToken, setTunnelToken, tunnelHostname, setTunnelHostname, resetSettings } from './settings.mjs';
25
25
 
26
26
  const name = 'dsh-pocket';
@@ -251,12 +251,18 @@ export function apply(ctx, config = {}, internals = {}) {
251
251
  internals,
252
252
  getLanIpOverride: () => lanIpOverride(),
253
253
  getLanEnabled: () => lanEnabled(),
254
- // 桌面端:手机扫码访问的页面缺 dsh-desktop-mode/platform 参数会让 dsh-plugin-desktop
255
- // client 崩溃(issue #3/#4)。给代理注入「桌面参数补丁」(history.replaceState 补齐参数,
256
- // 无跳转;compatibility 模式不套桌面布局,避免移动端按钮叠加)。保留默认 polyfill。
257
- // advanced 模式再加警告覆盖层(issue #19)。
254
+ // 桌面端**不再**注入 dsh-desktop-* 标记(issue #76):
255
+ // - 旧版 dsh-plugin-desktop 缺 mode/platform 会抛错,当初正是为此加了补丁(issue #3/#4);
256
+ // - 2.0.3 起,mode 与 platform **同时缺失**时 parseDesktopClientEnvironment 直接返回
257
+ // undefined(视作非桌面外壳,跳过全部桌面逻辑),正是手机页需要的效果;
258
+ // - 反之只要 URL 上出现任一 dsh-desktop-* 标记,客户端就会强制校验整组
259
+ // (material + semver version + mica),只补两个必然抛 "invalid or missing
260
+ // dsh-desktop-material" → 插件树加载失败 → 页面变成「打开恢复模式」;
261
+ // - 更糟的是 decideDesktopBrowserAccess 见到 dsh-desktop-* 前缀就把没有渲染器 token 的
262
+ // 普通浏览器判为 denied(403),刷新后直接打不开。
263
+ // 结论:手机/浏览器访问一律不带这些标记,advanced 模式另加警告覆盖层(issue #19)。
258
264
  injectHtml: isDesktop
259
- ? DEFAULT_INJECT + desktopEnvPatchScript(process.platform) + (desktopAdvanced ? advancedNoticeScript() : '')
265
+ ? DEFAULT_INJECT + (desktopAdvanced ? advancedNoticeScript() : '')
260
266
  : undefined,
261
267
  // 访问密码(issue #13 + #18 + #24 + #33 + #66):公网永远要密码(默认每次开启变新,
262
268
  // 用户自定义后不再轮换);局域网按开关(默认开启;关闭后局域网扫码直连)。
package/lib/proxy.mjs CHANGED
@@ -38,15 +38,23 @@ export const RANDOM_UUID_POLYFILL = `<script data-dsh-pocket-polyfill="1">!funct
38
38
  const INJECT_MARK = 'data-dsh-pocket-polyfill="1"';
39
39
 
40
40
  /**
41
- * DSH Desktop(桌面版)渲染进程兼容补丁。
41
+ * DSH Desktop(桌面版)渲染进程兼容补丁(issue #3/#4,已于 issue #76 停用)。
42
42
  *
43
- * 桌面版 profile 里的 dsh-plugin-desktop client 会在页面加载时从 URL query 读
43
+ * 历史:旧版 dsh-plugin-desktop client 在页面加载时从 URL query 读
44
44
  * `dsh-desktop-mode` 与 `dsh-desktop-platform`,缺失即抛
45
- * "invalid or missing dsh-desktop-mode null" → 页面崩(手机扫码访问桌面版时正是如此,
46
- * issue #3/#4)。本脚本在页面加载前用 history.replaceState 把这两个参数补上
47
- * (无跳转、不重载),取最轻的 `compatibility` 模式——不激活桌面布局,避免与
48
- * 移动端适配叠加。
49
- * 仅宿主在桌面版(isDesktop)时由 lib/index.js 追加进 injectHtml。
45
+ * "invalid or missing dsh-desktop-mode null" → 页面崩(手机扫码访问桌面版时正是如此)。
46
+ * 本脚本用 history.replaceState 补上这两个参数(无跳转、不重载),取最轻的
47
+ * `compatibility` 模式——不激活桌面布局,避免与移动端适配叠加。
48
+ *
49
+ * @deprecated 不要再注入(issue #76,DSH Desktop 2.0.3 起):
50
+ * ① mode 与 platform **同时缺失**时,parseDesktopClientEnvironment 直接返回 undefined
51
+ * (视作非桌面外壳,跳过全部桌面逻辑),正是手机/浏览器页面需要的效果;
52
+ * ② 只要 URL 上出现任一 dsh-desktop-* 标记,客户端就强制校验整组(material +
53
+ * semver version + mica),只补两个必然抛 "invalid or missing
54
+ * dsh-desktop-material" → 插件树加载失败 → 页面变成「打开恢复模式」;
55
+ * ③ 更糟:decideDesktopBrowserAccess 见到 dsh-desktop-* 前缀就把没有渲染器 token 的
56
+ * 普通浏览器判为 denied(403),刷新后直接打不开。
57
+ * lib/index.js 已不再注入本脚本;保留导出仅为兼容旧版本桌面端与既有测试。
50
58
  */
51
59
  export function desktopEnvPatchScript(platform) {
52
60
  const p = ['darwin', 'win32', 'linux'].includes(platform) ? platform : 'linux';
package/package.json CHANGED
@@ -80,5 +80,5 @@
80
80
  "access": "public",
81
81
  "registry": "https://registry.npmjs.org/"
82
82
  },
83
- "version": "2.1.1"
83
+ "version": "2.1.3"
84
84
  }