dsh-mobile 0.1.0-alpha.10 → 0.1.0-alpha.12

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.md CHANGED
@@ -36,10 +36,10 @@ The mobile client is self-editable: ask DSH in an ordinary conversation to chang
36
36
  | Authenticated HTTPS LAN access | Yes | Yes |
37
37
  | One-time device pairing | Yes | Yes |
38
38
  | Extend mobile UI and Web functions through a DSH conversation with live preview | Yes | Yes |
39
- | File selection and downloads | Yes | Browser-dependent |
39
+ | Phone/computer image selection and downloads | Yes | Yes (the browser supplies the phone picker) |
40
40
  | iOS native app | Not yet | Safari can use the Web UI |
41
41
 
42
- Host-native actions such as opening desktop files remain subject to stock DSH and operating-system restrictions. Treat every paired device as a fully trusted operator of the computer, not as a guest account.
42
+ The plugin turns narrow screens into a single content column with a navigation drawer and pins Add workspace to DSH's in-page computer directory browser. The composer image action asks for Phone files or Computer files: the first opens Android's or the browser's picker, while the second browses the computer inside the phone UI. Stock DSH currently models image attachments only, so this surface does not pretend that arbitrary files can be sent. Treat every paired device as a fully trusted operator of the computer, not as a guest account.
43
43
 
44
44
  ## Quick start
45
45
 
@@ -68,7 +68,7 @@ Both paths install the public npm package into stock DSH without patching its so
68
68
  3. Open the Android app, select **Scan**, then select the discovered DSH. Paste the key and connect.
69
69
  4. Connect. The app fingerprint-pins this DSH inside its encrypted private storage; it does not install a CA into Android settings. Mobile browsers still require an HTTPS certificate they already trust.
70
70
 
71
- The key and browser link expire after two minutes and work once. After pairing, Android stores a revocable device credential encrypted by Android Keystore. The app can rediscover the same DSH installation on the default port after its LAN address changes and renew its short Web session without another key. Browser profiles still pair separately.
71
+ The key and browser link expire after two minutes and work once. After pairing, Android stores a revocable device credential encrypted by Android Keystore. Every later app launch silently renews a short Web session from that credential; no key is requested again unless the device is revoked, its trust expires, or app data is cleared. The app can rediscover the same DSH installation on the default port after its LAN address changes. Browser profiles still pair separately.
72
72
 
73
73
  ## Everyday control
74
74
 
@@ -79,6 +79,13 @@ The two access modes are equivalent:
79
79
  - Android app: a thin system WebView shell without browser address and tab bars.
80
80
  - Mobile browser: open the setup origin directly; no app installation is required.
81
81
 
82
+ On a narrow screen, only the active conversation is shown. Use the top-left navigation button for the session and workspace drawer; choosing a session closes it. The image button in the composer offers two sources:
83
+
84
+ - **Phone files** opens photos, camera, or device storage and uploads selected images into the current composer.
85
+ - **Computer files** browses computer folders inside the phone UI and uploads the selected image without opening a desktop dialog.
86
+
87
+ **Add workspace** always shows the computer directory browser on the phone, including folder creation and navigation. It does not browse phone storage because the workspace is owned by the DSH Host running on the computer.
88
+
82
89
  While Mobile Access is running, DSH publishes a small DNS-SD/mDNS service and a periodic UDP announcement. Each contains only the computer name, HTTPS origin, port, protocol version, and stable installation identifier. Android listens to both, also sends the original UDP query, and keeps HTTPS `/24` probing as a last compatibility fallback. Results are merged by installation identifier, so a new Wi-Fi or hotspot address updates the existing device instead of requiring another pairing. No pairing key, CA, device token, Cookie, or DSH data is discoverable. The first screen contains only Scan and the discovered DSH list; the pairing-key field appears after selecting one result.
83
90
 
84
91
  After the user selects a result and enters its fingerprint-bound pairing key, the app fetches the public CA from that selected HTTPS origin without sending the key or any credential. It keeps the CA only inside the app and accepts the DSH certificate only when its chain, hostname, validity, and SHA-256 fingerprint match the key and discovered installation identifier. Nothing is installed into Android's system trust settings. Mobile browsers cannot use this app-private trust and therefore still need an already trusted certificate.
package/README.zh.md CHANGED
@@ -36,10 +36,10 @@ DSH Mobile 不是另一套远程控制面板。插件直接复用 DSH 自己的
36
36
  | HTTPS 局域网入口 | 支持 | 支持 |
37
37
  | 一次性设备配对 | 支持 | 支持 |
38
38
  | 在 DSH 对话中扩展移动 UI与网页功能并实时预览 | 支持 | 支持 |
39
- | 文件选择与下载 | 支持 | 由浏览器决定 |
39
+ | 手机/电脑图片选择与下载 | 支持 | 支持(手机面板由浏览器提供) |
40
40
  | iOS | 暂不支持 | 可用 Safari 访问网页 |
41
41
 
42
- 宿主文件选择器、打开本机文档等动作仍受原生 DSH 和操作系统限制。配对设备应视为电脑上的完全可信操作者,而不是低权限访客。
42
+ 插件会把窄屏界面改成单栏内容与抽屉导航,并把“添加工作区”固定为手机页面内的电脑目录浏览器。消息输入栏的图片入口会先询问“手机文件”或“电脑文件”:前者打开 Android/浏览器文件面板,后者直接在手机页面中浏览电脑目录。当前原生 DSH 的附件协议只支持图片,因此这里不会把任意文件伪装成可发送附件。配对设备应视为电脑上的完全可信操作者,而不是低权限访客。
43
43
 
44
44
  ## 快速开始
45
45
 
@@ -68,7 +68,7 @@ pnpm dsh --profile web
68
68
  3. 打开 Android App,点击“扫描”并选择发现的 DSH,再粘贴密钥连接。
69
69
  4. 直接连接。App 会把这台 DSH 的证书指纹固定在自身加密存储中,不会向 Android 系统安装 CA。手机浏览器仍需要它本身已经信任的 HTTPS 证书。
70
70
 
71
- 密钥和浏览器链接两分钟内有效且只能使用一次。配对后,Android 会用 Android Keystore 加密保存可随时撤销的设备凭据。局域网 IP 变化后,App 能在默认端口重新找到同一个 DSH,并自动续期短期 Web 会话,不需要新密钥。浏览器仍需单独配对。
71
+ 密钥和浏览器链接两分钟内有效且只能使用一次。配对后,Android 会用 Android Keystore 加密保存可随时撤销的设备凭据;以后每次打开 App 都会静默换取新的短期 Web 会话,不再要求输入密钥。只有电脑端撤销该设备、信任到期或清除 App 数据后才需要重新配对。局域网 IP 变化后,App 也能在默认端口重新找到同一个 DSH。浏览器仍需单独配对。
72
72
 
73
73
  ## 日常使用
74
74
 
@@ -79,6 +79,13 @@ pnpm dsh --profile web
79
79
  - Android App:使用系统 WebView 打开同一 DSH 页面,没有浏览器地址栏和标签栏。
80
80
  - 手机浏览器:直接访问向导显示的 `https://局域网地址:端口`,无需安装 App。
81
81
 
82
+ 窄屏页面默认只显示当前会话。点左上角导航按钮可打开会话与工作区抽屉;选择会话后抽屉自动收起。输入框中的图片按钮提供两个来源:
83
+
84
+ - “手机文件”从相册、相机或手机存储选择图片并上传到当前输入框。
85
+ - “电脑文件”在手机端打开电脑文件列表,选择图片后上传到当前输入框,不会在电脑上弹系统窗口。
86
+
87
+ “添加工作区”始终在手机页面显示电脑目录,可逐层进入、新建文件夹并选择工作区。它不会读取手机目录,因为工作区实际运行在电脑上的 DSH Host 中。
88
+
82
89
  “移动访问”运行时,DSH 会同时发布小型 DNS-SD/mDNS 服务和周期性 UDP 公告。二者只包含电脑名称、HTTPS 地址、端口、协议版本和稳定安装标识。Android 同时监听两种公告,也保留原有的主动 UDP 查询,最后再以 HTTPS `/24` 网段探测兼容特殊热点。结果按安装标识合并,因此切换 Wi-Fi、热点或 DHCP 地址后只会更新同一设备的 IP,不需要重新配对。发现阶段不会传输配对密钥、CA、设备令牌、Cookie 或任何 DSH 数据。首页仍然只有扫描按钮和设备列表,点击设备后才显示密钥输入框。
83
90
 
84
91
  用户选中设备并输入带指纹的配对密钥后,App 才从所选 HTTPS 地址获取公开 CA;这个引导请求不会携带密钥或任何凭据。App 只把 CA 保存在自身加密存储中,并且仅在证书链、主机名、有效期和 SHA-256 指纹同时匹配密钥及安装标识时连接,不会向 Android 系统信任设置安装任何内容。手机浏览器无法使用这份 App 私有信任,因此仍需浏览器本身已信任的 HTTPS 证书。
package/SECURITY.md CHANGED
@@ -27,6 +27,6 @@ The maintainer will acknowledge a complete report within seven days. Publication
27
27
 
28
28
  ## Known alpha limitation
29
29
 
30
- The current DSH HTML boot process contains inline JavaScript and applies some styles dynamically. To keep the stock Web UI runnable, the gateway's Content Security Policy currently includes `script-src 'self' 'unsafe-inline'` and `style-src 'self' 'unsafe-inline'`. The remaining directives still restrict origins, connections, frames, objects, workers, images, and form targets, but this policy does not eliminate script-injection risk. Removing these allowances requires upstream DSH support for nonces, stable hashes, or external boot resources.
30
+ The current DSH HTML boot process contains inline JavaScript, revives Schemastery callbacks with `new Function`, and applies some styles dynamically. To keep the stock Web UI runnable, the gateway's Content Security Policy currently includes `script-src 'self' 'unsafe-inline' 'unsafe-eval'` and `style-src 'self' 'unsafe-inline'`. The remaining directives still restrict origins, connections, frames, objects, workers, images, and form targets, but this policy does not eliminate script-injection risk. Removing these allowances requires upstream DSH support for nonces, stable hashes, external boot resources, and a non-evaluating schema representation.
31
31
 
32
32
  This repository never accepts private keys, npm tokens, pairing values, device credentials, Cookies, or captured settings in issues, logs, fixtures, or example configuration.
package/cordis.patch.yml CHANGED
@@ -1,6 +1,16 @@
1
1
  # Community bundle for the stock DSH Web profile. Installation contributes a
2
2
  # Host gateway and a browser Client face; it does not patch DSH source files.
3
+ - id: directory-picker
4
+ name: '@deepseek-ai/dsh-host-directory-picker-auto'
5
+ disabled: true
6
+
3
7
  - insert:
8
+ - id: directory-picker-mobile-host
9
+ name: '@deepseek-ai/dsh-host-directory-picker-browse'
10
+
11
+ - id: directory-picker-mobile-surface
12
+ name: '@deepseek-ai/dsh-client-ui-directory-picker-browse'
13
+
4
14
  - id: mobile-access
5
15
  name: dsh-mobile
6
16
  inject: [webServer]
package/lib/client.js CHANGED
@@ -6,16 +6,16 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  //#region src/client.ts
9
+ const MOBILE_QUERY = "(max-width: 820px)";
9
10
  const BASE_STYLES = `
10
- @media (max-width: 720px) {
11
+ @media ${MOBILE_QUERY} {
11
12
  :root {
12
13
  --dsh-mobile-accent: #2563eb;
13
14
  --dsh-mobile-font-scale: 1;
14
15
  --dsh-mobile-radius: 14px;
15
16
  }
16
17
  html { font-size: calc(16px * var(--dsh-mobile-font-scale)); }
17
- body { overscroll-behavior: none; }
18
- button, [role='button'], input, textarea, select { min-height: 44px; }
18
+ body { overscroll-behavior: none; overflow: hidden; }
19
19
  input, textarea, select { font-size: 16px !important; }
20
20
  [data-sidebar-collapsed] {
21
21
  padding-top: env(safe-area-inset-top);
@@ -24,6 +24,154 @@ window.__ModuleLoader__.load({
24
24
  padding-left: env(safe-area-inset-left);
25
25
  }
26
26
  [data-shell-overlay] { inset: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); }
27
+ [data-dsh-mobile-frame] {
28
+ grid-template-columns: minmax(0, 1fr) !important;
29
+ padding: 0;
30
+ }
31
+ [data-dsh-mobile-frame] > [data-dsh-mobile-sidebar] {
32
+ position: fixed;
33
+ z-index: 121;
34
+ inset: 0 auto 0 0;
35
+ width: min(86vw, 336px);
36
+ max-width: calc(100vw - 48px);
37
+ overflow: hidden;
38
+ border-right: 1px solid var(--dsw-alias-border-l1, #e2e8f0);
39
+ box-shadow: 10px 0 30px rgb(15 23 42 / 16%);
40
+ transform: translateX(-105%);
41
+ transition: transform 180ms ease-out;
42
+ }
43
+ [data-dsh-mobile-frame][data-dsh-mobile-drawer-open='true'] > [data-dsh-mobile-sidebar] {
44
+ transform: translateX(0);
45
+ }
46
+ [data-dsh-mobile-frame] > [data-dsh-mobile-center] {
47
+ grid-column: 1;
48
+ width: 100vw;
49
+ min-width: 0;
50
+ }
51
+ [data-dsh-mobile-frame] > [data-dsh-mobile-details],
52
+ [data-dsh-mobile-frame] > [data-dsh-mobile-handle] { display: none !important; }
53
+ [data-dsh-mobile-frame] [role='tablist'] {
54
+ max-width: 100%;
55
+ overflow-x: auto;
56
+ overscroll-behavior-x: contain;
57
+ scrollbar-width: none;
58
+ }
59
+ [data-dsh-mobile-center] header,
60
+ [data-dsh-mobile-center] [role='banner'] {
61
+ min-height: 52px;
62
+ padding-left: 60px !important;
63
+ box-sizing: border-box;
64
+ }
65
+ [data-dsh-mobile-frame] [role='tablist']::-webkit-scrollbar { display: none; }
66
+ [data-conversation-scroll] { overscroll-behavior: contain; }
67
+ [data-dsh-mobile-center] { min-height: 0; }
68
+ [data-composer-card] {
69
+ width: calc(100% - 16px) !important;
70
+ max-width: calc(100vw - 16px) !important;
71
+ margin-inline: 8px !important;
72
+ border-radius: var(--dsh-mobile-radius) !important;
73
+ }
74
+ [data-composer-card] [data-dsh-mobile-composer-row] {
75
+ display: grid !important;
76
+ grid-template-columns: minmax(0, 1fr) minmax(0, auto);
77
+ align-items: center;
78
+ width: 100%;
79
+ gap: 6px !important;
80
+ padding-inline: 8px !important;
81
+ box-sizing: border-box;
82
+ min-width: 0;
83
+ overflow: hidden;
84
+ }
85
+ [data-composer-card] [data-dsh-mobile-composer-tools] {
86
+ gap: 4px !important;
87
+ min-width: 0;
88
+ overflow: hidden;
89
+ }
90
+ [data-composer-card] [data-dsh-mobile-composer-trailing] {
91
+ gap: 4px !important;
92
+ min-width: 0;
93
+ max-width: 58vw;
94
+ overflow: hidden;
95
+ }
96
+ [data-composer-card] [data-dsh-mobile-composer-trailing] > * { min-width: 0; }
97
+ .dsh-mobile-menu {
98
+ position: fixed;
99
+ z-index: 108;
100
+ top: max(8px, env(safe-area-inset-top));
101
+ left: max(8px, env(safe-area-inset-left));
102
+ width: 44px;
103
+ height: 44px;
104
+ padding: 0;
105
+ border: 1px solid rgb(148 163 184 / 55%);
106
+ border-radius: 14px;
107
+ background: rgb(255 255 255 / 94%);
108
+ color: #0f172a;
109
+ box-shadow: 0 4px 18px rgb(15 23 42 / 12%);
110
+ font: 700 22px/1 system-ui, sans-serif;
111
+ }
112
+ .dsh-mobile-scrim {
113
+ position: fixed;
114
+ z-index: 120;
115
+ inset: 0 0 0 min(86vw, 336px);
116
+ border: 0;
117
+ background: rgb(15 23 42 / 38%);
118
+ }
119
+ .dsh-mobile-attach {
120
+ flex: 0 0 36px;
121
+ width: 36px;
122
+ height: 36px;
123
+ min-height: 36px;
124
+ padding: 0;
125
+ border: 0;
126
+ border-radius: 12px;
127
+ background: transparent;
128
+ color: inherit;
129
+ font: 700 19px/1 system-ui, sans-serif;
130
+ }
131
+ .dsh-mobile-attach:active { background: rgb(148 163 184 / 18%); }
132
+ .dsh-mobile-sheet {
133
+ position: fixed;
134
+ z-index: 160;
135
+ inset: 0;
136
+ display: grid;
137
+ align-items: end;
138
+ background: rgb(15 23 42 / 42%);
139
+ padding: 12px max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
140
+ }
141
+ .dsh-mobile-sheet__panel {
142
+ max-height: min(78dvh, 680px);
143
+ overflow: auto;
144
+ overscroll-behavior: contain;
145
+ padding: 18px;
146
+ border-radius: 20px;
147
+ background: var(--dsw-alias-bg-base, #fff);
148
+ color: var(--dsw-alias-text-primary, #0f172a);
149
+ box-shadow: 0 18px 50px rgb(15 23 42 / 22%);
150
+ }
151
+ .dsh-mobile-sheet__header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
152
+ .dsh-mobile-sheet__title { margin: 0; font-size: 19px; line-height: 1.35; }
153
+ .dsh-mobile-sheet__close { width: 44px; padding: 0; border: 0; border-radius: 12px; background: transparent; font-size: 22px; }
154
+ .dsh-mobile-sheet__choices { display: grid; gap: 10px; }
155
+ .dsh-mobile-sheet__choice {
156
+ display: grid;
157
+ gap: 3px;
158
+ width: 100%;
159
+ min-height: 64px;
160
+ padding: 11px 14px;
161
+ text-align: left;
162
+ border: 1px solid var(--dsw-alias-border-l2, #dbe3ee);
163
+ border-radius: 14px;
164
+ background: transparent;
165
+ color: inherit;
166
+ }
167
+ .dsh-mobile-sheet__choice strong { font-size: 16px; }
168
+ .dsh-mobile-sheet__choice span { color: var(--dsw-alias-text-secondary, #64748b); font-size: 13px; }
169
+ .dsh-mobile-browser__path { margin: 0 0 10px; color: var(--dsw-alias-text-secondary, #64748b); font-size: 13px; overflow-wrap: anywhere; }
170
+ .dsh-mobile-browser__list { display: grid; gap: 6px; }
171
+ .dsh-mobile-browser__row { width: 100%; padding: 9px 12px; text-align: left; border: 0; border-radius: 10px; background: transparent; color: inherit; overflow-wrap: anywhere; }
172
+ .dsh-mobile-browser__row:active { background: rgb(37 99 235 / 10%); }
173
+ .dsh-mobile-browser__status { color: var(--dsw-alias-text-secondary, #64748b); }
174
+ [role='tooltip'] { display: none !important; }
27
175
  * { -webkit-tap-highlight-color: transparent; }
28
176
  }
29
177
  @media (prefers-reduced-motion: reduce) {
@@ -83,6 +231,244 @@ window.__ModuleLoader__.load({
83
231
  if (className !== void 0) node.className = className;
84
232
  return node;
85
233
  }
234
+ function mobileText(zh, en) {
235
+ return navigator.language.toLowerCase().startsWith("zh") ? zh : en;
236
+ }
237
+ function deliverFiles(files) {
238
+ if (files.length === 0) return;
239
+ const transfer = new DataTransfer();
240
+ for (const file of files) transfer.items.add(file);
241
+ document.dispatchEvent(new DragEvent("drop", {
242
+ bubbles: true,
243
+ cancelable: true,
244
+ dataTransfer: transfer
245
+ }));
246
+ }
247
+ function installMobileExperience() {
248
+ const query = window.matchMedia(MOBILE_QUERY);
249
+ const menu = element("button", "dsh-mobile-menu");
250
+ menu.type = "button";
251
+ menu.textContent = "☰";
252
+ menu.setAttribute("aria-label", mobileText("打开导航", "Open navigation"));
253
+ menu.setAttribute("aria-expanded", "false");
254
+ const scrim = element("button", "dsh-mobile-scrim");
255
+ scrim.type = "button";
256
+ scrim.hidden = true;
257
+ scrim.setAttribute("aria-label", mobileText("关闭导航", "Close navigation"));
258
+ const input = element("input");
259
+ input.type = "file";
260
+ input.accept = "image/*";
261
+ input.multiple = true;
262
+ input.hidden = true;
263
+ document.body.append(menu, scrim, input);
264
+ let drawerOpen = false;
265
+ let activeSheet;
266
+ const frame = () => document.querySelector("[data-sidebar-collapsed], [data-dsh-mobile-frame]");
267
+ const renderDrawer = () => {
268
+ const current = frame();
269
+ if (current !== null) current.dataset.dshMobileDrawerOpen = String(drawerOpen);
270
+ menu.setAttribute("aria-expanded", String(drawerOpen));
271
+ scrim.hidden = !drawerOpen;
272
+ };
273
+ const stockSidebarToggle = () => {
274
+ const sidebar = frame()?.querySelector("[data-dsh-mobile-sidebar]");
275
+ if (sidebar === void 0 || sidebar === null) return void 0;
276
+ return Array.from(sidebar.querySelectorAll("button")).find((button) => {
277
+ const label = button.getAttribute("aria-label")?.toLowerCase() ?? "";
278
+ return label.includes("侧边栏") || label.includes("sidebar");
279
+ }) ?? sidebar.querySelector("button") ?? void 0;
280
+ };
281
+ const setDrawer = (open) => {
282
+ const current = frame();
283
+ if (current === null) return;
284
+ const collapsed = current.hasAttribute("data-sidebar-collapsed");
285
+ if (open && collapsed) stockSidebarToggle()?.click();
286
+ drawerOpen = open;
287
+ renderDrawer();
288
+ if (open) window.setTimeout(() => stockSidebarToggle()?.focus(), 0);
289
+ else menu.focus();
290
+ };
291
+ menu.addEventListener("click", () => {
292
+ setDrawer(true);
293
+ });
294
+ scrim.addEventListener("click", () => {
295
+ setDrawer(false);
296
+ });
297
+ const navigationClick = (event) => {
298
+ const action = event.target instanceof Element ? event.target.closest("button, [role=\"treeitem\"]") : null;
299
+ if (action === null || action.closest("[data-dsh-mobile-sidebar]") === null) return;
300
+ const label = `${action.getAttribute("aria-label") ?? ""} ${action.textContent ?? ""}`.toLowerCase();
301
+ if (action.getAttribute("role") === "treeitem" || /收起侧边栏|close sidebar|新建会话|new session|添加工作区|add workspace|设置|settings/u.test(label)) window.setTimeout(() => {
302
+ drawerOpen = false;
303
+ renderDrawer();
304
+ }, 0);
305
+ };
306
+ document.addEventListener("click", navigationClick);
307
+ const closeSheet = () => {
308
+ activeSheet?.remove();
309
+ activeSheet = void 0;
310
+ };
311
+ const sheet = (titleText) => {
312
+ closeSheet();
313
+ const root = element("div", "dsh-mobile-sheet");
314
+ root.setAttribute("role", "presentation");
315
+ const panel = element("section", "dsh-mobile-sheet__panel");
316
+ panel.setAttribute("role", "dialog");
317
+ panel.setAttribute("aria-modal", "true");
318
+ const header = element("div", "dsh-mobile-sheet__header");
319
+ const title = element("h2", "dsh-mobile-sheet__title");
320
+ title.textContent = titleText;
321
+ const close = element("button", "dsh-mobile-sheet__close");
322
+ close.type = "button";
323
+ close.textContent = "×";
324
+ close.setAttribute("aria-label", mobileText("关闭", "Close"));
325
+ close.addEventListener("click", closeSheet);
326
+ header.append(title, close);
327
+ panel.append(header);
328
+ root.append(panel);
329
+ root.addEventListener("click", (event) => {
330
+ if (event.target === root) closeSheet();
331
+ });
332
+ document.body.append(root);
333
+ activeSheet = root;
334
+ window.setTimeout(() => close.focus(), 0);
335
+ return {
336
+ root,
337
+ panel
338
+ };
339
+ };
340
+ const openComputerBrowser = async (path) => {
341
+ const { panel } = sheet(mobileText("选择电脑图片", "Choose a computer image"));
342
+ const status = element("p", "dsh-mobile-browser__status");
343
+ status.textContent = mobileText("正在读取电脑文件…", "Loading computer files…");
344
+ panel.append(status);
345
+ try {
346
+ const response = await mobileExtensionRequest(`/mobile-access/computer-images${path === void 0 ? "" : `?path=${encodeURIComponent(path)}`}`);
347
+ if (!response.ok) throw new Error(`HTTP ${String(response.status)}`);
348
+ const listing = await response.json();
349
+ status.remove();
350
+ const current = element("p", "dsh-mobile-browser__path");
351
+ current.textContent = listing.path;
352
+ const list = element("div", "dsh-mobile-browser__list");
353
+ if (listing.parent !== void 0) {
354
+ const up = element("button", "dsh-mobile-browser__row");
355
+ up.type = "button";
356
+ up.textContent = mobileText("↑ 上一级", "↑ Parent folder");
357
+ up.addEventListener("click", () => {
358
+ openComputerBrowser(listing.parent);
359
+ });
360
+ list.append(up);
361
+ }
362
+ for (const entry of listing.entries) {
363
+ const row = element("button", "dsh-mobile-browser__row");
364
+ row.type = "button";
365
+ row.textContent = `${entry.kind === "directory" ? "▸" : "▧"} ${entry.name}`;
366
+ if (entry.kind === "directory") row.addEventListener("click", () => {
367
+ openComputerBrowser(entry.path);
368
+ });
369
+ else row.addEventListener("click", () => {
370
+ row.disabled = true;
371
+ mobileExtensionRequest(`/mobile-access/computer-image?path=${encodeURIComponent(entry.path)}`).then(async (image) => {
372
+ if (!image.ok) throw new Error(`HTTP ${String(image.status)}`);
373
+ const blob = await image.blob();
374
+ deliverFiles([new File([blob], entry.name, { type: blob.type })]);
375
+ closeSheet();
376
+ }).catch((error) => {
377
+ row.disabled = false;
378
+ row.textContent = error instanceof Error ? error.message : String(error);
379
+ });
380
+ });
381
+ list.append(row);
382
+ }
383
+ if (listing.entries.length === 0) {
384
+ const empty = element("p", "dsh-mobile-browser__status");
385
+ empty.textContent = mobileText("这里没有可上传的图片。", "No supported images here.");
386
+ list.append(empty);
387
+ }
388
+ if (listing.truncated) {
389
+ const truncated = element("p", "dsh-mobile-browser__status");
390
+ truncated.textContent = mobileText("项目过多,仅显示前 500 项。", "Only the first 500 items are shown.");
391
+ list.append(truncated);
392
+ }
393
+ panel.append(current, list);
394
+ } catch (error) {
395
+ status.textContent = error instanceof Error ? error.message : String(error);
396
+ }
397
+ };
398
+ const openSourceSheet = () => {
399
+ const { panel } = sheet(mobileText("添加图片", "Add images"));
400
+ const choices = element("div", "dsh-mobile-sheet__choices");
401
+ const phone = element("button", "dsh-mobile-sheet__choice");
402
+ phone.type = "button";
403
+ phone.innerHTML = `<strong>${mobileText("手机文件", "Phone files")}</strong><span>${mobileText("从相册、相机或手机存储选择", "Choose from photos, camera, or device storage")}</span>`;
404
+ phone.addEventListener("click", () => {
405
+ closeSheet();
406
+ input.click();
407
+ });
408
+ const computer = element("button", "dsh-mobile-sheet__choice");
409
+ computer.type = "button";
410
+ computer.innerHTML = `<strong>${mobileText("电脑文件", "Computer files")}</strong><span>${mobileText("在手机页面中浏览这台电脑的图片", "Browse images on this computer without opening a desktop dialog")}</span>`;
411
+ computer.addEventListener("click", () => {
412
+ openComputerBrowser();
413
+ });
414
+ choices.append(phone, computer);
415
+ panel.append(choices);
416
+ };
417
+ input.addEventListener("change", () => {
418
+ if (input.files !== null) deliverFiles(Array.from(input.files));
419
+ input.value = "";
420
+ });
421
+ const decorate = () => {
422
+ const current = frame();
423
+ if (current !== null) {
424
+ current.dataset.dshMobileFrame = "true";
425
+ const children = Array.from(current.children).filter((child) => child instanceof HTMLElement);
426
+ children[0]?.setAttribute("data-dsh-mobile-sidebar", "true");
427
+ children[1]?.setAttribute("data-dsh-mobile-center", "true");
428
+ children[2]?.setAttribute("data-dsh-mobile-details", "true");
429
+ for (const child of children.slice(4)) child.setAttribute("data-dsh-mobile-handle", "true");
430
+ renderDrawer();
431
+ }
432
+ for (const card of document.querySelectorAll("[data-composer-card]")) {
433
+ const row = card.lastElementChild;
434
+ const tools = row?.firstElementChild;
435
+ const trailing = row?.lastElementChild;
436
+ if (!(row instanceof HTMLElement) || !(tools instanceof HTMLElement)) continue;
437
+ row.dataset.dshMobileComposerRow = "true";
438
+ tools.dataset.dshMobileComposerTools = "true";
439
+ if (trailing instanceof HTMLElement) trailing.dataset.dshMobileComposerTrailing = "true";
440
+ if (tools.querySelector("[data-dsh-mobile-attach]") !== null) continue;
441
+ const attach = element("button", "dsh-mobile-attach");
442
+ attach.type = "button";
443
+ attach.dataset.dshMobileAttach = "true";
444
+ attach.textContent = "▧";
445
+ attach.setAttribute("aria-label", mobileText("从手机或电脑添加图片", "Add images from phone or computer"));
446
+ attach.addEventListener("click", openSourceSheet);
447
+ tools.insertBefore(attach, tools.children[1] ?? null);
448
+ }
449
+ menu.hidden = !query.matches;
450
+ };
451
+ const observer = new MutationObserver(decorate);
452
+ observer.observe(document.body, {
453
+ childList: true,
454
+ subtree: true,
455
+ attributes: true,
456
+ attributeFilter: ["data-sidebar-collapsed"]
457
+ });
458
+ query.addEventListener("change", decorate);
459
+ decorate();
460
+ return () => {
461
+ observer.disconnect();
462
+ query.removeEventListener("change", decorate);
463
+ document.removeEventListener("click", navigationClick);
464
+ closeSheet();
465
+ menu.remove();
466
+ scrim.remove();
467
+ input.remove();
468
+ for (const node of document.querySelectorAll("[data-dsh-mobile-frame], [data-dsh-mobile-sidebar], [data-dsh-mobile-center], [data-dsh-mobile-details], [data-dsh-mobile-handle], [data-dsh-mobile-composer-row], [data-dsh-mobile-composer-tools], [data-dsh-mobile-composer-trailing], [data-dsh-mobile-attach]")) if (node.hasAttribute("data-dsh-mobile-attach")) node.remove();
469
+ else for (const attribute of Array.from(node.attributes)) if (attribute.name.startsWith("data-dsh-mobile-")) node.removeAttribute(attribute.name);
470
+ };
471
+ }
86
472
  async function requestJson(path, init) {
87
473
  const response = await fetch(path, {
88
474
  ...init,
@@ -271,6 +657,7 @@ window.__ModuleLoader__.load({
271
657
  style.dataset.plugin = "dsh-mobile";
272
658
  style.textContent = BASE_STYLES;
273
659
  document.head.append(style);
660
+ const removeMobileExperience = installMobileExperience();
274
661
  const loopback = isLoopbackHost(location.hostname);
275
662
  const custom = loopback ? void 0 : document.createElement("style");
276
663
  let customRefresh;
@@ -315,6 +702,7 @@ window.__ModuleLoader__.load({
315
702
  disposeSlot?.();
316
703
  control?.remove();
317
704
  removeCustomExtension?.();
705
+ removeMobileExperience();
318
706
  custom?.remove();
319
707
  style.remove();
320
708
  };
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["createElement"],"sources":["../src/client.ts"],"sourcesContent":["import { createElement } from 'react'\n\ninterface ClientContext {\n effect(effect: () => void | (() => void), label?: string): void\n slots: {\n inject(key: string, callback: () => (() => void)): () => void\n register(options: { name: string; id: string }, component: (props: { wide: boolean }) => unknown): () => void\n }\n}\n\ninterface MobileExtensionContext {\n readonly document: Document\n readonly request: (path: string, init?: RequestInit) => Promise<Response>\n readonly root: HTMLElement\n readonly window: Window\n}\n\ntype MobileExtensionMount = (context: MobileExtensionContext) => void | (() => void)\n\ninterface MobileExtensionRegistry {\n register(mount: MobileExtensionMount): void\n}\n\ndeclare global {\n interface Window {\n dshMobile?: MobileExtensionRegistry\n }\n}\n\nconst MOBILE_QUERY = '(max-width: 720px)'\n\nconst BASE_STYLES = `\n@media ${MOBILE_QUERY} {\n :root {\n --dsh-mobile-accent: #2563eb;\n --dsh-mobile-font-scale: 1;\n --dsh-mobile-radius: 14px;\n }\n html { font-size: calc(16px * var(--dsh-mobile-font-scale)); }\n body { overscroll-behavior: none; }\n button, [role='button'], input, textarea, select { min-height: 44px; }\n input, textarea, select { font-size: 16px !important; }\n [data-sidebar-collapsed] {\n padding-top: env(safe-area-inset-top);\n padding-right: env(safe-area-inset-right);\n padding-bottom: env(safe-area-inset-bottom);\n padding-left: env(safe-area-inset-left);\n }\n [data-shell-overlay] { inset: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); }\n * { -webkit-tap-highlight-color: transparent; }\n}\n@media (prefers-reduced-motion: reduce) {\n *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }\n}\n.dsh-mobile-control {\n position: fixed;\n left: 68px;\n bottom: 12px;\n z-index: 90;\n color: #172554;\n font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n}\n.dsh-mobile-control button {\n min-width: 44px;\n min-height: 44px;\n border: 1px solid #bfdbfe;\n border-radius: 12px;\n background: #fff;\n color: #1e3a8a;\n cursor: pointer;\n}\n.dsh-mobile-control__trigger {\n width: 100%;\n min-width: 44px;\n min-height: 44px;\n padding: 0 12px;\n border: 0;\n border-radius: 10px;\n background: transparent;\n color: inherit;\n font: inherit;\n font-weight: 700;\n cursor: pointer;\n}\n.dsh-mobile-control__trigger:hover { background: rgb(37 99 235 / 8%); }\n.dsh-mobile-control__panel {\n width: min(320px, calc(100vw - 24px));\n margin-bottom: 8px;\n padding: 16px;\n border: 1px solid #dbeafe;\n border-radius: 16px;\n background: #fff;\n box-shadow: 0 10px 30px rgb(15 23 42 / 12%);\n}\n.dsh-mobile-control__title { margin: 0 0 4px; font-size: 16px; font-weight: 800; }\n.dsh-mobile-control__status { margin: 0 0 14px; color: #475569; overflow-wrap: anywhere; }\n.dsh-mobile-control__actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }\n.dsh-mobile-control__actions button[data-primary='true'] { background: #2563eb; border-color: #2563eb; color: #fff; }\n.dsh-mobile-control[hidden], .dsh-mobile-control__panel[hidden] { display: none; }\n`\n\nfunction isLoopbackHost(hostname: string): boolean {\n return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname === '::1'\n}\n\nfunction element<K extends keyof HTMLElementTagNameMap>(name: K, className?: string): HTMLElementTagNameMap[K] {\n const node = document.createElement(name)\n if (className !== undefined) node.className = className\n return node\n}\n\nasync function requestJson(path: string, init?: RequestInit): Promise<Record<string, unknown>> {\n const response = await fetch(path, {\n ...init,\n headers: { 'content-type': 'application/json', ...init?.headers },\n })\n const body = await response.json() as Record<string, unknown>\n if (!response.ok) {\n const code = typeof body.error === 'string' ? body.error : `HTTP ${String(response.status)}`\n throw new Error(controlErrorMessage(code))\n }\n return body\n}\n\nfunction controlErrorMessage(code: string): string {\n const zh = navigator.language.toLowerCase().startsWith('zh')\n const messages: Record<string, readonly [string, string]> = {\n network_address_changed: ['局域网地址已变化,请重启 DSH 后重试。', 'The LAN address changed. Restart DSH and try again.'],\n network_interface_unavailable: ['保存的局域网网卡未连接,请连接网络后重启 DSH。', 'The saved LAN interface is disconnected. Connect it and restart DSH.'],\n listen_port_in_use: ['移动访问端口已被占用。', 'The mobile-access port is already in use.'],\n }\n const message = messages[code]\n return message === undefined ? code : message[zh ? 0 : 1]\n}\n\nfunction mobileExtensionRequest(path: string, init: RequestInit = {}): Promise<Response> {\n const target = new URL(path, location.href)\n if (target.origin !== location.origin) throw new TypeError('mobile extension requests must stay on the DSH origin')\n const headers = new Headers(init.headers)\n const method = (init.method ?? 'GET').toUpperCase()\n if (method !== 'GET' && method !== 'HEAD' && !headers.has('x-dsh-mobile-csrf')) {\n const csrf = document.cookie.split(';').map(entry => entry.trim())\n .find(entry => entry.startsWith('dsh_ma_csrf='))?.slice('dsh_ma_csrf='.length)\n if (csrf !== undefined) headers.set('x-dsh-mobile-csrf', csrf)\n }\n return fetch(target, {\n ...init,\n cache: 'no-store',\n credentials: 'same-origin',\n headers,\n redirect: 'error',\n })\n}\n\nfunction installControl(): { remove: () => void; toggle: () => void } {\n const zh = navigator.language.toLowerCase().startsWith('zh')\n const root = element('div', 'dsh-mobile-control')\n const panel = element('section', 'dsh-mobile-control__panel')\n panel.hidden = true\n const title = element('h2', 'dsh-mobile-control__title')\n title.textContent = zh ? '移动访问' : 'Mobile access'\n const status = element('p', 'dsh-mobile-control__status')\n status.textContent = zh ? '正在读取状态…' : 'Reading status…'\n const actions = element('div', 'dsh-mobile-control__actions')\n const toggle = element('button')\n toggle.dataset.primary = 'true'\n const pair = element('button')\n pair.textContent = zh ? '生成配对密钥' : 'Create pairing key'\n actions.append(toggle, pair)\n panel.append(title, status, actions)\n root.append(panel)\n document.body.append(root)\n\n let running = false\n const render = (data: Record<string, unknown>): void => {\n running = data.running === true\n const origin = typeof data.origin === 'string' ? data.origin : undefined\n status.textContent = running\n ? (zh ? `已开启:${origin ?? ''}` : `Running: ${origin ?? ''}`)\n : (zh ? '已关闭。DSH 仍只在本机可用。' : 'Stopped. DSH remains local-only.')\n toggle.textContent = running ? (zh ? '关闭' : 'Stop') : (zh ? '开启' : 'Start')\n pair.disabled = !running\n }\n const refresh = async (): Promise<void> => {\n try { render(await requestJson('/api/mobile-access/control')) }\n catch (error) { status.textContent = error instanceof Error ? error.message : String(error) }\n }\n toggle.addEventListener('click', () => {\n toggle.disabled = true\n void requestJson('/api/mobile-access/control', {\n method: 'POST',\n body: JSON.stringify({ running: !running }),\n }).then(render, (error: unknown) => {\n status.textContent = error instanceof Error ? error.message : String(error)\n }).finally(() => { toggle.disabled = false })\n })\n pair.addEventListener('click', () => {\n pair.disabled = true\n void requestJson('/api/mobile-access/pairing/open', {\n method: 'POST',\n body: '{}',\n }).then(async (data) => {\n const url = typeof data.pairUrl === 'string' ? data.pairUrl : ''\n const token = typeof data.token === 'string' ? data.token : ''\n const appKey = typeof data.appKey === 'string' ? data.appKey : ''\n const copied = appKey !== '' ? appKey : token !== '' ? token : url\n if (copied !== '' && navigator.clipboard !== undefined) await navigator.clipboard.writeText(copied)\n status.textContent = copied === '' ? (zh ? '无法生成密钥' : 'Could not create key')\n : (zh\n ? `配对密钥已复制:${copied}${url === '' ? '' : `\\n手机浏览器链接:${url}`}`\n : `Pairing key copied: ${copied}${url === '' ? '' : `\\nMobile browser link: ${url}`}`)\n }, (error: unknown) => {\n status.textContent = error instanceof Error ? error.message : String(error)\n }).finally(() => { pair.disabled = !running })\n })\n void refresh()\n return {\n remove: () => { root.remove() },\n toggle: () => { panel.hidden = !panel.hidden },\n }\n}\n\nfunction installCustomExtension(): () => void {\n let activeRoot: HTMLElement | undefined\n let activeDispose: (() => void) | undefined\n let observedSource = ''\n let loading = false\n let pendingMount: MobileExtensionMount | undefined\n const previousRegistry = window.dshMobile\n const registry: MobileExtensionRegistry = Object.freeze({\n register: (mount: MobileExtensionMount): void => {\n if (typeof mount !== 'function') throw new TypeError('dshMobile.register requires a mount function')\n pendingMount = mount\n },\n })\n const registeredMount = (): MobileExtensionMount | undefined => pendingMount\n window.dshMobile = registry\n\n const refresh = async (): Promise<void> => {\n if (loading || document.visibilityState === 'hidden') return\n loading = true\n try {\n const response = await fetch('/mobile-access/custom.js', {\n cache: 'no-store',\n credentials: 'same-origin',\n redirect: 'error',\n })\n if (!response.ok || !response.headers.get('content-type')?.startsWith('text/javascript')) return\n const source = await response.text()\n if (source === observedSource) return\n observedSource = source\n pendingMount = undefined\n const script = document.createElement('script')\n script.textContent = `${source}\\n//# sourceURL=dsh-mobile-custom.js`\n document.head.append(script)\n script.remove()\n const mount = registeredMount()\n if (mount === undefined) return\n\n const nextRoot = document.createElement('div')\n nextRoot.dataset.dshMobileExtension = 'true'\n document.body.append(nextRoot)\n try {\n const nextDispose = mount(Object.freeze({\n document,\n request: mobileExtensionRequest,\n root: nextRoot,\n window,\n }))\n if (nextDispose !== undefined && typeof nextDispose !== 'function') {\n throw new TypeError('the mobile extension mount function must return a disposer or undefined')\n }\n activeDispose?.()\n activeRoot?.remove()\n activeRoot = nextRoot\n activeDispose = typeof nextDispose === 'function' ? nextDispose : undefined\n } catch (error) {\n nextRoot.remove()\n console.error('DSH Mobile custom extension failed to mount', error)\n }\n } catch {\n // A transient LAN disconnect keeps the last successfully mounted extension.\n } finally {\n loading = false\n }\n }\n void refresh()\n const timer = setInterval(() => { void refresh() }, 1_000)\n return () => {\n clearInterval(timer)\n activeDispose?.()\n activeRoot?.remove()\n if (previousRegistry === undefined) delete window.dshMobile\n else window.dshMobile = previousRegistry\n }\n}\n\n/** Install the responsive mobile layer and the loopback-only control card. */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => {\n const style = document.createElement('style')\n style.dataset.plugin = 'dsh-mobile'\n style.textContent = BASE_STYLES\n document.head.append(style)\n\n const loopback = isLoopbackHost(location.hostname)\n const custom = loopback ? undefined : document.createElement('style')\n let customRefresh: ReturnType<typeof setInterval> | undefined\n let customLoading = false\n if (custom !== undefined) {\n custom.dataset.plugin = 'dsh-mobile'\n document.head.append(custom)\n const refreshCustomCss = async (): Promise<void> => {\n if (customLoading || document.visibilityState === 'hidden') return\n customLoading = true\n try {\n const response = await fetch('/mobile-access/custom.css', {\n cache: 'no-store',\n credentials: 'same-origin',\n redirect: 'error',\n })\n if (!response.ok || !response.headers.get('content-type')?.startsWith('text/css')) return\n const css = await response.text()\n if (custom.textContent !== css) custom.textContent = css\n } catch {\n // A transient LAN disconnect keeps the last successfully applied stylesheet.\n } finally {\n customLoading = false\n }\n }\n void refreshCustomCss()\n customRefresh = setInterval(() => { void refreshCustomCss() }, 1_000)\n }\n const removeCustomExtension = loopback ? undefined : installCustomExtension()\n const control = loopback ? installControl() : undefined\n const disposeSlot = control === undefined ? undefined : ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({\n name: 'sidebar.footer.action',\n id: 'dsh-mobile',\n }, ({ wide }) => createElement('button', {\n className: 'dsh-mobile-control__trigger',\n type: 'button',\n title: controlLabel(),\n onClick: control.toggle,\n }, wide ? controlLabel() : 'M')))\n return () => {\n if (customRefresh !== undefined) clearInterval(customRefresh)\n disposeSlot?.()\n control?.remove()\n removeCustomExtension?.()\n custom?.remove()\n style.remove()\n }\n }, 'dsh-mobile: responsive UI and local control')\n}\n\n/** Client face has no service prerequisites. */\nexport const inject: readonly string[] = ['slots']\n\nfunction controlLabel(): string {\n return navigator.language.toLowerCase().startsWith('zh') ? '移动端' : 'Mobile access'\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["createElement"],"sources":["../src/client.ts"],"sourcesContent":["import { createElement } from 'react'\n\ninterface ClientContext {\n effect(effect: () => void | (() => void), label?: string): void\n slots: {\n inject(key: string, callback: () => (() => void)): () => void\n register(options: { name: string; id: string }, component: (props: { wide: boolean }) => unknown): () => void\n }\n}\n\ninterface MobileExtensionContext {\n readonly document: Document\n readonly request: (path: string, init?: RequestInit) => Promise<Response>\n readonly root: HTMLElement\n readonly window: Window\n}\n\ntype MobileExtensionMount = (context: MobileExtensionContext) => void | (() => void)\n\ninterface MobileExtensionRegistry {\n register(mount: MobileExtensionMount): void\n}\n\ndeclare global {\n interface Window {\n dshMobile?: MobileExtensionRegistry\n }\n}\n\nconst MOBILE_QUERY = '(max-width: 820px)'\n\nconst BASE_STYLES = `\n@media ${MOBILE_QUERY} {\n :root {\n --dsh-mobile-accent: #2563eb;\n --dsh-mobile-font-scale: 1;\n --dsh-mobile-radius: 14px;\n }\n html { font-size: calc(16px * var(--dsh-mobile-font-scale)); }\n body { overscroll-behavior: none; overflow: hidden; }\n input, textarea, select { font-size: 16px !important; }\n [data-sidebar-collapsed] {\n padding-top: env(safe-area-inset-top);\n padding-right: env(safe-area-inset-right);\n padding-bottom: env(safe-area-inset-bottom);\n padding-left: env(safe-area-inset-left);\n }\n [data-shell-overlay] { inset: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); }\n [data-dsh-mobile-frame] {\n grid-template-columns: minmax(0, 1fr) !important;\n padding: 0;\n }\n [data-dsh-mobile-frame] > [data-dsh-mobile-sidebar] {\n position: fixed;\n z-index: 121;\n inset: 0 auto 0 0;\n width: min(86vw, 336px);\n max-width: calc(100vw - 48px);\n overflow: hidden;\n border-right: 1px solid var(--dsw-alias-border-l1, #e2e8f0);\n box-shadow: 10px 0 30px rgb(15 23 42 / 16%);\n transform: translateX(-105%);\n transition: transform 180ms ease-out;\n }\n [data-dsh-mobile-frame][data-dsh-mobile-drawer-open='true'] > [data-dsh-mobile-sidebar] {\n transform: translateX(0);\n }\n [data-dsh-mobile-frame] > [data-dsh-mobile-center] {\n grid-column: 1;\n width: 100vw;\n min-width: 0;\n }\n [data-dsh-mobile-frame] > [data-dsh-mobile-details],\n [data-dsh-mobile-frame] > [data-dsh-mobile-handle] { display: none !important; }\n [data-dsh-mobile-frame] [role='tablist'] {\n max-width: 100%;\n overflow-x: auto;\n overscroll-behavior-x: contain;\n scrollbar-width: none;\n }\n [data-dsh-mobile-center] header,\n [data-dsh-mobile-center] [role='banner'] {\n min-height: 52px;\n padding-left: 60px !important;\n box-sizing: border-box;\n }\n [data-dsh-mobile-frame] [role='tablist']::-webkit-scrollbar { display: none; }\n [data-conversation-scroll] { overscroll-behavior: contain; }\n [data-dsh-mobile-center] { min-height: 0; }\n [data-composer-card] {\n width: calc(100% - 16px) !important;\n max-width: calc(100vw - 16px) !important;\n margin-inline: 8px !important;\n border-radius: var(--dsh-mobile-radius) !important;\n }\n [data-composer-card] [data-dsh-mobile-composer-row] {\n display: grid !important;\n grid-template-columns: minmax(0, 1fr) minmax(0, auto);\n align-items: center;\n width: 100%;\n gap: 6px !important;\n padding-inline: 8px !important;\n box-sizing: border-box;\n min-width: 0;\n overflow: hidden;\n }\n [data-composer-card] [data-dsh-mobile-composer-tools] {\n gap: 4px !important;\n min-width: 0;\n overflow: hidden;\n }\n [data-composer-card] [data-dsh-mobile-composer-trailing] {\n gap: 4px !important;\n min-width: 0;\n max-width: 58vw;\n overflow: hidden;\n }\n [data-composer-card] [data-dsh-mobile-composer-trailing] > * { min-width: 0; }\n .dsh-mobile-menu {\n position: fixed;\n z-index: 108;\n top: max(8px, env(safe-area-inset-top));\n left: max(8px, env(safe-area-inset-left));\n width: 44px;\n height: 44px;\n padding: 0;\n border: 1px solid rgb(148 163 184 / 55%);\n border-radius: 14px;\n background: rgb(255 255 255 / 94%);\n color: #0f172a;\n box-shadow: 0 4px 18px rgb(15 23 42 / 12%);\n font: 700 22px/1 system-ui, sans-serif;\n }\n .dsh-mobile-scrim {\n position: fixed;\n z-index: 120;\n inset: 0 0 0 min(86vw, 336px);\n border: 0;\n background: rgb(15 23 42 / 38%);\n }\n .dsh-mobile-attach {\n flex: 0 0 36px;\n width: 36px;\n height: 36px;\n min-height: 36px;\n padding: 0;\n border: 0;\n border-radius: 12px;\n background: transparent;\n color: inherit;\n font: 700 19px/1 system-ui, sans-serif;\n }\n .dsh-mobile-attach:active { background: rgb(148 163 184 / 18%); }\n .dsh-mobile-sheet {\n position: fixed;\n z-index: 160;\n inset: 0;\n display: grid;\n align-items: end;\n background: rgb(15 23 42 / 42%);\n padding: 12px max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));\n }\n .dsh-mobile-sheet__panel {\n max-height: min(78dvh, 680px);\n overflow: auto;\n overscroll-behavior: contain;\n padding: 18px;\n border-radius: 20px;\n background: var(--dsw-alias-bg-base, #fff);\n color: var(--dsw-alias-text-primary, #0f172a);\n box-shadow: 0 18px 50px rgb(15 23 42 / 22%);\n }\n .dsh-mobile-sheet__header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }\n .dsh-mobile-sheet__title { margin: 0; font-size: 19px; line-height: 1.35; }\n .dsh-mobile-sheet__close { width: 44px; padding: 0; border: 0; border-radius: 12px; background: transparent; font-size: 22px; }\n .dsh-mobile-sheet__choices { display: grid; gap: 10px; }\n .dsh-mobile-sheet__choice {\n display: grid;\n gap: 3px;\n width: 100%;\n min-height: 64px;\n padding: 11px 14px;\n text-align: left;\n border: 1px solid var(--dsw-alias-border-l2, #dbe3ee);\n border-radius: 14px;\n background: transparent;\n color: inherit;\n }\n .dsh-mobile-sheet__choice strong { font-size: 16px; }\n .dsh-mobile-sheet__choice span { color: var(--dsw-alias-text-secondary, #64748b); font-size: 13px; }\n .dsh-mobile-browser__path { margin: 0 0 10px; color: var(--dsw-alias-text-secondary, #64748b); font-size: 13px; overflow-wrap: anywhere; }\n .dsh-mobile-browser__list { display: grid; gap: 6px; }\n .dsh-mobile-browser__row { width: 100%; padding: 9px 12px; text-align: left; border: 0; border-radius: 10px; background: transparent; color: inherit; overflow-wrap: anywhere; }\n .dsh-mobile-browser__row:active { background: rgb(37 99 235 / 10%); }\n .dsh-mobile-browser__status { color: var(--dsw-alias-text-secondary, #64748b); }\n [role='tooltip'] { display: none !important; }\n * { -webkit-tap-highlight-color: transparent; }\n}\n@media (prefers-reduced-motion: reduce) {\n *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }\n}\n.dsh-mobile-control {\n position: fixed;\n left: 68px;\n bottom: 12px;\n z-index: 90;\n color: #172554;\n font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n}\n.dsh-mobile-control button {\n min-width: 44px;\n min-height: 44px;\n border: 1px solid #bfdbfe;\n border-radius: 12px;\n background: #fff;\n color: #1e3a8a;\n cursor: pointer;\n}\n.dsh-mobile-control__trigger {\n width: 100%;\n min-width: 44px;\n min-height: 44px;\n padding: 0 12px;\n border: 0;\n border-radius: 10px;\n background: transparent;\n color: inherit;\n font: inherit;\n font-weight: 700;\n cursor: pointer;\n}\n.dsh-mobile-control__trigger:hover { background: rgb(37 99 235 / 8%); }\n.dsh-mobile-control__panel {\n width: min(320px, calc(100vw - 24px));\n margin-bottom: 8px;\n padding: 16px;\n border: 1px solid #dbeafe;\n border-radius: 16px;\n background: #fff;\n box-shadow: 0 10px 30px rgb(15 23 42 / 12%);\n}\n.dsh-mobile-control__title { margin: 0 0 4px; font-size: 16px; font-weight: 800; }\n.dsh-mobile-control__status { margin: 0 0 14px; color: #475569; overflow-wrap: anywhere; }\n.dsh-mobile-control__actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }\n.dsh-mobile-control__actions button[data-primary='true'] { background: #2563eb; border-color: #2563eb; color: #fff; }\n.dsh-mobile-control[hidden], .dsh-mobile-control__panel[hidden] { display: none; }\n`\n\ninterface ComputerImageEntry {\n readonly kind: 'directory' | 'image'\n readonly name: string\n readonly path: string\n}\n\ninterface ComputerImageListing {\n readonly path: string\n readonly parent?: string\n readonly entries: readonly ComputerImageEntry[]\n readonly truncated: boolean\n}\n\nfunction isLoopbackHost(hostname: string): boolean {\n return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname === '::1'\n}\n\nfunction element<K extends keyof HTMLElementTagNameMap>(name: K, className?: string): HTMLElementTagNameMap[K] {\n const node = document.createElement(name)\n if (className !== undefined) node.className = className\n return node\n}\n\nfunction mobileText(zh: string, en: string): string {\n return navigator.language.toLowerCase().startsWith('zh') ? zh : en\n}\n\nfunction deliverFiles(files: readonly File[]): void {\n if (files.length === 0) return\n const transfer = new DataTransfer()\n for (const file of files) transfer.items.add(file)\n document.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer: transfer }))\n}\n\nfunction installMobileExperience(): () => void {\n const query = window.matchMedia(MOBILE_QUERY)\n const menu = element('button', 'dsh-mobile-menu')\n menu.type = 'button'\n menu.textContent = '☰'\n menu.setAttribute('aria-label', mobileText('打开导航', 'Open navigation'))\n menu.setAttribute('aria-expanded', 'false')\n const scrim = element('button', 'dsh-mobile-scrim')\n scrim.type = 'button'\n scrim.hidden = true\n scrim.setAttribute('aria-label', mobileText('关闭导航', 'Close navigation'))\n const input = element('input')\n input.type = 'file'\n input.accept = 'image/*'\n input.multiple = true\n input.hidden = true\n document.body.append(menu, scrim, input)\n\n let drawerOpen = false\n let activeSheet: HTMLElement | undefined\n const frame = (): HTMLElement | null => document.querySelector('[data-sidebar-collapsed], [data-dsh-mobile-frame]')\n const renderDrawer = (): void => {\n const current = frame()\n if (current !== null) current.dataset.dshMobileDrawerOpen = String(drawerOpen)\n menu.setAttribute('aria-expanded', String(drawerOpen))\n scrim.hidden = !drawerOpen\n }\n const stockSidebarToggle = (): HTMLButtonElement | undefined => {\n const sidebar = frame()?.querySelector<HTMLElement>('[data-dsh-mobile-sidebar]')\n if (sidebar === undefined || sidebar === null) return undefined\n return Array.from(sidebar.querySelectorAll<HTMLButtonElement>('button')).find(button => {\n const label = button.getAttribute('aria-label')?.toLowerCase() ?? ''\n return label.includes('侧边栏') || label.includes('sidebar')\n }) ?? sidebar.querySelector<HTMLButtonElement>('button') ?? undefined\n }\n const setDrawer = (open: boolean): void => {\n const current = frame()\n if (current === null) return\n const collapsed = current.hasAttribute('data-sidebar-collapsed')\n if (open && collapsed) stockSidebarToggle()?.click()\n drawerOpen = open\n renderDrawer()\n if (open) window.setTimeout(() => stockSidebarToggle()?.focus(), 0)\n else menu.focus()\n }\n menu.addEventListener('click', () => { setDrawer(true) })\n scrim.addEventListener('click', () => { setDrawer(false) })\n const navigationClick = (event: MouseEvent): void => {\n const action = event.target instanceof Element ? event.target.closest('button, [role=\"treeitem\"]') : null\n if (action === null || action.closest('[data-dsh-mobile-sidebar]') === null) return\n const label = `${action.getAttribute('aria-label') ?? ''} ${action.textContent ?? ''}`.toLowerCase()\n if (action.getAttribute('role') === 'treeitem'\n || /收起侧边栏|close sidebar|新建会话|new session|添加工作区|add workspace|设置|settings/u.test(label)) {\n window.setTimeout(() => { drawerOpen = false; renderDrawer() }, 0)\n }\n }\n document.addEventListener('click', navigationClick)\n\n const closeSheet = (): void => {\n activeSheet?.remove()\n activeSheet = undefined\n }\n const sheet = (titleText: string): { root: HTMLElement; panel: HTMLElement } => {\n closeSheet()\n const root = element('div', 'dsh-mobile-sheet')\n root.setAttribute('role', 'presentation')\n const panel = element('section', 'dsh-mobile-sheet__panel')\n panel.setAttribute('role', 'dialog')\n panel.setAttribute('aria-modal', 'true')\n const header = element('div', 'dsh-mobile-sheet__header')\n const title = element('h2', 'dsh-mobile-sheet__title')\n title.textContent = titleText\n const close = element('button', 'dsh-mobile-sheet__close')\n close.type = 'button'\n close.textContent = '×'\n close.setAttribute('aria-label', mobileText('关闭', 'Close'))\n close.addEventListener('click', closeSheet)\n header.append(title, close)\n panel.append(header)\n root.append(panel)\n root.addEventListener('click', event => { if (event.target === root) closeSheet() })\n document.body.append(root)\n activeSheet = root\n window.setTimeout(() => close.focus(), 0)\n return { root, panel }\n }\n const openComputerBrowser = async (path?: string): Promise<void> => {\n const { panel } = sheet(mobileText('选择电脑图片', 'Choose a computer image'))\n const status = element('p', 'dsh-mobile-browser__status')\n status.textContent = mobileText('正在读取电脑文件…', 'Loading computer files…')\n panel.append(status)\n try {\n const suffix = path === undefined ? '' : `?path=${encodeURIComponent(path)}`\n const response = await mobileExtensionRequest(`/mobile-access/computer-images${suffix}`)\n if (!response.ok) throw new Error(`HTTP ${String(response.status)}`)\n const listing = await response.json() as ComputerImageListing\n status.remove()\n const current = element('p', 'dsh-mobile-browser__path')\n current.textContent = listing.path\n const list = element('div', 'dsh-mobile-browser__list')\n if (listing.parent !== undefined) {\n const up = element('button', 'dsh-mobile-browser__row')\n up.type = 'button'\n up.textContent = mobileText('↑ 上一级', '↑ Parent folder')\n up.addEventListener('click', () => { void openComputerBrowser(listing.parent) })\n list.append(up)\n }\n for (const entry of listing.entries) {\n const row = element('button', 'dsh-mobile-browser__row')\n row.type = 'button'\n row.textContent = `${entry.kind === 'directory' ? '▸' : '▧'} ${entry.name}`\n if (entry.kind === 'directory') {\n row.addEventListener('click', () => { void openComputerBrowser(entry.path) })\n } else {\n row.addEventListener('click', () => {\n row.disabled = true\n void mobileExtensionRequest(`/mobile-access/computer-image?path=${encodeURIComponent(entry.path)}`)\n .then(async (image) => {\n if (!image.ok) throw new Error(`HTTP ${String(image.status)}`)\n const blob = await image.blob()\n deliverFiles([new File([blob], entry.name, { type: blob.type })])\n closeSheet()\n })\n .catch((error: unknown) => {\n row.disabled = false\n row.textContent = error instanceof Error ? error.message : String(error)\n })\n })\n }\n list.append(row)\n }\n if (listing.entries.length === 0) {\n const empty = element('p', 'dsh-mobile-browser__status')\n empty.textContent = mobileText('这里没有可上传的图片。', 'No supported images here.')\n list.append(empty)\n }\n if (listing.truncated) {\n const truncated = element('p', 'dsh-mobile-browser__status')\n truncated.textContent = mobileText('项目过多,仅显示前 500 项。', 'Only the first 500 items are shown.')\n list.append(truncated)\n }\n panel.append(current, list)\n } catch (error) {\n status.textContent = error instanceof Error ? error.message : String(error)\n }\n }\n const openSourceSheet = (): void => {\n const { panel } = sheet(mobileText('添加图片', 'Add images'))\n const choices = element('div', 'dsh-mobile-sheet__choices')\n const phone = element('button', 'dsh-mobile-sheet__choice')\n phone.type = 'button'\n phone.innerHTML = `<strong>${mobileText('手机文件', 'Phone files')}</strong><span>${mobileText('从相册、相机或手机存储选择', 'Choose from photos, camera, or device storage')}</span>`\n phone.addEventListener('click', () => { closeSheet(); input.click() })\n const computer = element('button', 'dsh-mobile-sheet__choice')\n computer.type = 'button'\n computer.innerHTML = `<strong>${mobileText('电脑文件', 'Computer files')}</strong><span>${mobileText('在手机页面中浏览这台电脑的图片', 'Browse images on this computer without opening a desktop dialog')}</span>`\n computer.addEventListener('click', () => { void openComputerBrowser() })\n choices.append(phone, computer)\n panel.append(choices)\n }\n input.addEventListener('change', () => {\n if (input.files !== null) deliverFiles(Array.from(input.files))\n input.value = ''\n })\n\n const decorate = (): void => {\n const current = frame()\n if (current !== null) {\n current.dataset.dshMobileFrame = 'true'\n const children = Array.from(current.children).filter((child): child is HTMLElement => child instanceof HTMLElement)\n children[0]?.setAttribute('data-dsh-mobile-sidebar', 'true')\n children[1]?.setAttribute('data-dsh-mobile-center', 'true')\n children[2]?.setAttribute('data-dsh-mobile-details', 'true')\n for (const child of children.slice(4)) child.setAttribute('data-dsh-mobile-handle', 'true')\n renderDrawer()\n }\n for (const card of document.querySelectorAll<HTMLElement>('[data-composer-card]')) {\n const row = card.lastElementChild\n const tools = row?.firstElementChild\n const trailing = row?.lastElementChild\n if (!(row instanceof HTMLElement) || !(tools instanceof HTMLElement)) continue\n row.dataset.dshMobileComposerRow = 'true'\n tools.dataset.dshMobileComposerTools = 'true'\n if (trailing instanceof HTMLElement) trailing.dataset.dshMobileComposerTrailing = 'true'\n if (tools.querySelector('[data-dsh-mobile-attach]') !== null) continue\n const attach = element('button', 'dsh-mobile-attach')\n attach.type = 'button'\n attach.dataset.dshMobileAttach = 'true'\n attach.textContent = '▧'\n attach.setAttribute('aria-label', mobileText('从手机或电脑添加图片', 'Add images from phone or computer'))\n attach.addEventListener('click', openSourceSheet)\n tools.insertBefore(attach, tools.children[1] ?? null)\n }\n menu.hidden = !query.matches\n }\n const observer = new MutationObserver(decorate)\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-sidebar-collapsed'] })\n query.addEventListener('change', decorate)\n decorate()\n return () => {\n observer.disconnect()\n query.removeEventListener('change', decorate)\n document.removeEventListener('click', navigationClick)\n closeSheet()\n menu.remove()\n scrim.remove()\n input.remove()\n for (const node of document.querySelectorAll('[data-dsh-mobile-frame], [data-dsh-mobile-sidebar], [data-dsh-mobile-center], [data-dsh-mobile-details], [data-dsh-mobile-handle], [data-dsh-mobile-composer-row], [data-dsh-mobile-composer-tools], [data-dsh-mobile-composer-trailing], [data-dsh-mobile-attach]')) {\n if (node.hasAttribute('data-dsh-mobile-attach')) node.remove()\n else for (const attribute of Array.from(node.attributes)) if (attribute.name.startsWith('data-dsh-mobile-')) node.removeAttribute(attribute.name)\n }\n }\n}\n\nasync function requestJson(path: string, init?: RequestInit): Promise<Record<string, unknown>> {\n const response = await fetch(path, {\n ...init,\n headers: { 'content-type': 'application/json', ...init?.headers },\n })\n const body = await response.json() as Record<string, unknown>\n if (!response.ok) {\n const code = typeof body.error === 'string' ? body.error : `HTTP ${String(response.status)}`\n throw new Error(controlErrorMessage(code))\n }\n return body\n}\n\nfunction controlErrorMessage(code: string): string {\n const zh = navigator.language.toLowerCase().startsWith('zh')\n const messages: Record<string, readonly [string, string]> = {\n network_address_changed: ['局域网地址已变化,请重启 DSH 后重试。', 'The LAN address changed. Restart DSH and try again.'],\n network_interface_unavailable: ['保存的局域网网卡未连接,请连接网络后重启 DSH。', 'The saved LAN interface is disconnected. Connect it and restart DSH.'],\n listen_port_in_use: ['移动访问端口已被占用。', 'The mobile-access port is already in use.'],\n }\n const message = messages[code]\n return message === undefined ? code : message[zh ? 0 : 1]\n}\n\nfunction mobileExtensionRequest(path: string, init: RequestInit = {}): Promise<Response> {\n const target = new URL(path, location.href)\n if (target.origin !== location.origin) throw new TypeError('mobile extension requests must stay on the DSH origin')\n const headers = new Headers(init.headers)\n const method = (init.method ?? 'GET').toUpperCase()\n if (method !== 'GET' && method !== 'HEAD' && !headers.has('x-dsh-mobile-csrf')) {\n const csrf = document.cookie.split(';').map(entry => entry.trim())\n .find(entry => entry.startsWith('dsh_ma_csrf='))?.slice('dsh_ma_csrf='.length)\n if (csrf !== undefined) headers.set('x-dsh-mobile-csrf', csrf)\n }\n return fetch(target, {\n ...init,\n cache: 'no-store',\n credentials: 'same-origin',\n headers,\n redirect: 'error',\n })\n}\n\nfunction installControl(): { remove: () => void; toggle: () => void } {\n const zh = navigator.language.toLowerCase().startsWith('zh')\n const root = element('div', 'dsh-mobile-control')\n const panel = element('section', 'dsh-mobile-control__panel')\n panel.hidden = true\n const title = element('h2', 'dsh-mobile-control__title')\n title.textContent = zh ? '移动访问' : 'Mobile access'\n const status = element('p', 'dsh-mobile-control__status')\n status.textContent = zh ? '正在读取状态…' : 'Reading status…'\n const actions = element('div', 'dsh-mobile-control__actions')\n const toggle = element('button')\n toggle.dataset.primary = 'true'\n const pair = element('button')\n pair.textContent = zh ? '生成配对密钥' : 'Create pairing key'\n actions.append(toggle, pair)\n panel.append(title, status, actions)\n root.append(panel)\n document.body.append(root)\n\n let running = false\n const render = (data: Record<string, unknown>): void => {\n running = data.running === true\n const origin = typeof data.origin === 'string' ? data.origin : undefined\n status.textContent = running\n ? (zh ? `已开启:${origin ?? ''}` : `Running: ${origin ?? ''}`)\n : (zh ? '已关闭。DSH 仍只在本机可用。' : 'Stopped. DSH remains local-only.')\n toggle.textContent = running ? (zh ? '关闭' : 'Stop') : (zh ? '开启' : 'Start')\n pair.disabled = !running\n }\n const refresh = async (): Promise<void> => {\n try { render(await requestJson('/api/mobile-access/control')) }\n catch (error) { status.textContent = error instanceof Error ? error.message : String(error) }\n }\n toggle.addEventListener('click', () => {\n toggle.disabled = true\n void requestJson('/api/mobile-access/control', {\n method: 'POST',\n body: JSON.stringify({ running: !running }),\n }).then(render, (error: unknown) => {\n status.textContent = error instanceof Error ? error.message : String(error)\n }).finally(() => { toggle.disabled = false })\n })\n pair.addEventListener('click', () => {\n pair.disabled = true\n void requestJson('/api/mobile-access/pairing/open', {\n method: 'POST',\n body: '{}',\n }).then(async (data) => {\n const url = typeof data.pairUrl === 'string' ? data.pairUrl : ''\n const token = typeof data.token === 'string' ? data.token : ''\n const appKey = typeof data.appKey === 'string' ? data.appKey : ''\n const copied = appKey !== '' ? appKey : token !== '' ? token : url\n if (copied !== '' && navigator.clipboard !== undefined) await navigator.clipboard.writeText(copied)\n status.textContent = copied === '' ? (zh ? '无法生成密钥' : 'Could not create key')\n : (zh\n ? `配对密钥已复制:${copied}${url === '' ? '' : `\\n手机浏览器链接:${url}`}`\n : `Pairing key copied: ${copied}${url === '' ? '' : `\\nMobile browser link: ${url}`}`)\n }, (error: unknown) => {\n status.textContent = error instanceof Error ? error.message : String(error)\n }).finally(() => { pair.disabled = !running })\n })\n void refresh()\n return {\n remove: () => { root.remove() },\n toggle: () => { panel.hidden = !panel.hidden },\n }\n}\n\nfunction installCustomExtension(): () => void {\n let activeRoot: HTMLElement | undefined\n let activeDispose: (() => void) | undefined\n let observedSource = ''\n let loading = false\n let pendingMount: MobileExtensionMount | undefined\n const previousRegistry = window.dshMobile\n const registry: MobileExtensionRegistry = Object.freeze({\n register: (mount: MobileExtensionMount): void => {\n if (typeof mount !== 'function') throw new TypeError('dshMobile.register requires a mount function')\n pendingMount = mount\n },\n })\n const registeredMount = (): MobileExtensionMount | undefined => pendingMount\n window.dshMobile = registry\n\n const refresh = async (): Promise<void> => {\n if (loading || document.visibilityState === 'hidden') return\n loading = true\n try {\n const response = await fetch('/mobile-access/custom.js', {\n cache: 'no-store',\n credentials: 'same-origin',\n redirect: 'error',\n })\n if (!response.ok || !response.headers.get('content-type')?.startsWith('text/javascript')) return\n const source = await response.text()\n if (source === observedSource) return\n observedSource = source\n pendingMount = undefined\n const script = document.createElement('script')\n script.textContent = `${source}\\n//# sourceURL=dsh-mobile-custom.js`\n document.head.append(script)\n script.remove()\n const mount = registeredMount()\n if (mount === undefined) return\n\n const nextRoot = document.createElement('div')\n nextRoot.dataset.dshMobileExtension = 'true'\n document.body.append(nextRoot)\n try {\n const nextDispose = mount(Object.freeze({\n document,\n request: mobileExtensionRequest,\n root: nextRoot,\n window,\n }))\n if (nextDispose !== undefined && typeof nextDispose !== 'function') {\n throw new TypeError('the mobile extension mount function must return a disposer or undefined')\n }\n activeDispose?.()\n activeRoot?.remove()\n activeRoot = nextRoot\n activeDispose = typeof nextDispose === 'function' ? nextDispose : undefined\n } catch (error) {\n nextRoot.remove()\n console.error('DSH Mobile custom extension failed to mount', error)\n }\n } catch {\n // A transient LAN disconnect keeps the last successfully mounted extension.\n } finally {\n loading = false\n }\n }\n void refresh()\n const timer = setInterval(() => { void refresh() }, 1_000)\n return () => {\n clearInterval(timer)\n activeDispose?.()\n activeRoot?.remove()\n if (previousRegistry === undefined) delete window.dshMobile\n else window.dshMobile = previousRegistry\n }\n}\n\n/** Install the responsive mobile layer and the loopback-only control card. */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => {\n const style = document.createElement('style')\n style.dataset.plugin = 'dsh-mobile'\n style.textContent = BASE_STYLES\n document.head.append(style)\n const removeMobileExperience = installMobileExperience()\n\n const loopback = isLoopbackHost(location.hostname)\n const custom = loopback ? undefined : document.createElement('style')\n let customRefresh: ReturnType<typeof setInterval> | undefined\n let customLoading = false\n if (custom !== undefined) {\n custom.dataset.plugin = 'dsh-mobile'\n document.head.append(custom)\n const refreshCustomCss = async (): Promise<void> => {\n if (customLoading || document.visibilityState === 'hidden') return\n customLoading = true\n try {\n const response = await fetch('/mobile-access/custom.css', {\n cache: 'no-store',\n credentials: 'same-origin',\n redirect: 'error',\n })\n if (!response.ok || !response.headers.get('content-type')?.startsWith('text/css')) return\n const css = await response.text()\n if (custom.textContent !== css) custom.textContent = css\n } catch {\n // A transient LAN disconnect keeps the last successfully applied stylesheet.\n } finally {\n customLoading = false\n }\n }\n void refreshCustomCss()\n customRefresh = setInterval(() => { void refreshCustomCss() }, 1_000)\n }\n const removeCustomExtension = loopback ? undefined : installCustomExtension()\n const control = loopback ? installControl() : undefined\n const disposeSlot = control === undefined ? undefined : ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({\n name: 'sidebar.footer.action',\n id: 'dsh-mobile',\n }, ({ wide }) => createElement('button', {\n className: 'dsh-mobile-control__trigger',\n type: 'button',\n title: controlLabel(),\n onClick: control.toggle,\n }, wide ? controlLabel() : 'M')))\n return () => {\n if (customRefresh !== undefined) clearInterval(customRefresh)\n disposeSlot?.()\n control?.remove()\n removeCustomExtension?.()\n removeMobileExperience()\n custom?.remove()\n style.remove()\n }\n }, 'dsh-mobile: responsive UI and local control')\n}\n\n/** Client face has no service prerequisites. */\nexport const inject: readonly string[] = ['slots']\n\nfunction controlLabel(): string {\n return navigator.language.toLowerCase().startsWith('zh') ? '移动端' : 'Mobile access'\n}\n"],"mappings":"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"}
package/lib/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import { X509Certificate, createHash, createPrivateKey, createPublicKey, randomBytes, timingSafeEqual } from "node:crypto";
2
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
2
+ import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
3
3
  import z from "@deepseek-ai/schemastery";
4
4
  import { connect, isIP } from "node:net";
5
- import { chmod, lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
5
+ import { chmod, lstat, mkdir, opendir, readFile, rename, rm, writeFile } from "node:fs/promises";
6
6
  import { createSocket } from "node:dgram";
7
- import { hostname, networkInterfaces } from "node:os";
7
+ import { homedir, hostname, networkInterfaces } from "node:os";
8
8
  import { createServer, request } from "node:http";
9
9
  import { createServer as createServer$1 } from "node:https";
10
10
  import { Transform } from "node:stream";
@@ -953,7 +953,7 @@ function setSecurityHeaders(response, tls) {
953
953
  "object-src 'none'",
954
954
  "frame-ancestors 'none'",
955
955
  "form-action 'self'",
956
- "script-src 'self' 'unsafe-inline'",
956
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
957
957
  "style-src 'self' 'unsafe-inline'",
958
958
  "img-src 'self' data: blob:",
959
959
  "font-src 'self' data:",
@@ -1074,6 +1074,82 @@ function assertLocalAdminTrust(request, requireBrowserOrigin) {
1074
1074
  if (requireBrowserOrigin && site !== void 0 && (origin === void 0 || site !== "same-origin")) throw new HttpError(403, "forbidden");
1075
1075
  }
1076
1076
  //#endregion
1077
+ //#region src/computer-images.ts
1078
+ const MAX_ENTRIES = 500;
1079
+ const MAX_IMAGE_BYTES = 20971520;
1080
+ const IMAGE_TYPES = Object.freeze({
1081
+ ".gif": "image/gif",
1082
+ ".jpeg": "image/jpeg",
1083
+ ".jpg": "image/jpeg",
1084
+ ".png": "image/png",
1085
+ ".webp": "image/webp"
1086
+ });
1087
+ /** Normalize an optional mobile-browser path without rebasing relative input. */
1088
+ function resolveComputerImagePath(path) {
1089
+ if (path === null || path === "") return homedir();
1090
+ if (!isAbsolute(path) || path.includes("\0")) throw new HttpError(400, "bad_path");
1091
+ return resolve(path);
1092
+ }
1093
+ /** List folders and supported image files without following symbolic links. */
1094
+ async function listComputerImages(path) {
1095
+ const target = resolveComputerImagePath(path);
1096
+ const rows = [];
1097
+ let truncated = false;
1098
+ let directory;
1099
+ try {
1100
+ directory = await opendir(target);
1101
+ for await (const entry of directory) {
1102
+ if (entry.isSymbolicLink()) continue;
1103
+ const kind = entry.isDirectory() ? "directory" : IMAGE_TYPES[extname(entry.name).toLowerCase()] === void 0 ? void 0 : "image";
1104
+ if (kind === void 0) continue;
1105
+ if (rows.length === MAX_ENTRIES) {
1106
+ truncated = true;
1107
+ break;
1108
+ }
1109
+ rows.push({
1110
+ kind,
1111
+ name: entry.name,
1112
+ path: resolve(target, entry.name)
1113
+ });
1114
+ }
1115
+ } catch {
1116
+ throw new HttpError(404, "directory_unavailable");
1117
+ } finally {
1118
+ await directory?.close().catch(() => void 0);
1119
+ }
1120
+ rows.sort((left, right) => left.kind === right.kind ? left.name.localeCompare(right.name) : left.kind === "directory" ? -1 : 1);
1121
+ const parent = dirname(target);
1122
+ return Object.freeze({
1123
+ path: target,
1124
+ ...parent === target ? {} : { parent },
1125
+ entries: Object.freeze(rows),
1126
+ truncated
1127
+ });
1128
+ }
1129
+ /** Read one bounded regular image file selected by an authenticated device. */
1130
+ async function readComputerImage(path) {
1131
+ const target = resolveComputerImagePath(path);
1132
+ const contentType = IMAGE_TYPES[extname(target).toLowerCase()];
1133
+ if (contentType === void 0) throw new HttpError(415, "unsupported_file_type");
1134
+ let info;
1135
+ try {
1136
+ info = await lstat(target);
1137
+ } catch {
1138
+ throw new HttpError(404, "file_unavailable");
1139
+ }
1140
+ if (!info.isFile() || info.isSymbolicLink()) throw new HttpError(404, "file_unavailable");
1141
+ if (info.size > MAX_IMAGE_BYTES) throw new HttpError(413, "file_too_large");
1142
+ try {
1143
+ return {
1144
+ body: await readFile(target),
1145
+ contentType,
1146
+ name: basename(target)
1147
+ };
1148
+ } catch {
1149
+ throw new HttpError(404, "file_unavailable");
1150
+ }
1151
+ }
1152
+ //#endregion
1077
1153
  //#region src/gateway.ts
1078
1154
  const MAX_CONTROL_BODY_BYTES = 16384;
1079
1155
  const MAX_HEADER_BYTES = 16384;
@@ -1754,6 +1830,8 @@ var MobileAccessGateway = class {
1754
1830
  await this.handleLogout(request, response);
1755
1831
  return;
1756
1832
  }
1833
+ const computerImages = request.method === "GET" && target.decodedPathname === `/mobile-access/computer-images`;
1834
+ const computerImage = request.method === "GET" && target.decodedPathname === `/mobile-access/computer-image`;
1757
1835
  const customAsset = request.method === "GET" ? target.decodedPathname === `/mobile-access/custom.css` ? {
1758
1836
  file: this.config.customCssFile,
1759
1837
  contentType: "text/css; charset=utf-8",
@@ -1763,7 +1841,7 @@ var MobileAccessGateway = class {
1763
1841
  contentType: "text/javascript; charset=utf-8",
1764
1842
  fallback: "window.dshMobile?.register(() => undefined)\n"
1765
1843
  } : void 0 : void 0;
1766
- if (customAsset === void 0 && (target.decodedPathname === "/mobile-access" || target.decodedPathname.startsWith(`/mobile-access/`))) throw new HttpError(404, "not_found");
1844
+ if (customAsset === void 0 && !computerImages && !computerImage && (target.decodedPathname === "/mobile-access" || target.decodedPathname.startsWith(`/mobile-access/`))) throw new HttpError(404, "not_found");
1767
1845
  if (request.method !== "GET" && request.method !== "HEAD" && request.method !== "POST") throw new HttpError(405, "method_not_allowed");
1768
1846
  if (request.method === "POST" && target.decodedPathname !== "/api" && !target.decodedPathname.startsWith("/api/")) throw new HttpError(405, "method_not_allowed");
1769
1847
  let authorization;
@@ -1803,6 +1881,23 @@ var MobileAccessGateway = class {
1803
1881
  response.end(body);
1804
1882
  return;
1805
1883
  }
1884
+ if (computerImages) {
1885
+ const query = new URL(target.raw, this.address().origin).searchParams;
1886
+ sendJson(response, 200, await listComputerImages(query.get("path")), this.tlsEnabled);
1887
+ return;
1888
+ }
1889
+ if (computerImage) {
1890
+ const query = new URL(target.raw, this.address().origin).searchParams;
1891
+ const image = await readComputerImage(query.get("path"));
1892
+ setSecurityHeaders(response, this.tlsEnabled);
1893
+ response.writeHead(200, {
1894
+ "Content-Type": image.contentType,
1895
+ "Content-Length": image.body.byteLength,
1896
+ "Content-Disposition": `inline; filename*=UTF-8''${encodeURIComponent(image.name)}`
1897
+ });
1898
+ response.end(image.body);
1899
+ return;
1900
+ }
1806
1901
  await this.proxyHttp(request, response, authorization);
1807
1902
  }
1808
1903
  allocateRequest(authorization, response, upstream) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mobile",
3
- "version": "0.1.0-alpha.10",
3
+ "version": "0.1.0-alpha.12",
4
4
  "private": false,
5
5
  "description": "Authenticated Android and mobile-browser access for the stock DeepSeek Harness Web UI",
6
6
  "type": "module",