dsh-mobile-access 1.1.4 → 1.1.6

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
@@ -79,7 +79,7 @@ iPhone 打开「设置 → 无线局域网」,确认和 Mac 连的是同一个
79
79
  ### 第 2 步:在 Mac 终端执行一条命令
80
80
 
81
81
  ```bash
82
- dsh plugin --profile web add dsh-mobile-access@1.1.4 --registry=https://registry.npmjs.org/
82
+ dsh plugin --profile web add dsh-mobile-access@1.1.6 --registry=https://registry.npmjs.org/
83
83
  ```
84
84
 
85
85
  这条命令做三件事:
@@ -190,6 +190,19 @@ iOS Safari 会把第一次触摸先用去聚焦,第二次才派发 `click`。
190
190
  如果左侧栏仍然是窄窄的图标条,第一下是展开侧栏——这是官方布局,不是会话行的问题;
191
191
  展开后每个会话只需单击一次。
192
192
 
193
+ ### iPhone 上左右滑动展开 / 收起左侧栏
194
+
195
+ 从 `1.1.6` 起,iPhone / iPad 上还支持:
196
+
197
+ - 在对话内容区**向右滑**:展开左侧栏(仅在左侧栏已收起时生效);
198
+ - 在对话内容区**向左滑**:收起左侧栏(仅在左侧栏已展开时生效);
199
+ - 滑动需要水平距离至少 70px,且明显大于上下移动,避免和滚动、
200
+ 横向代码块、输入框、模型菜单和右侧面板冲突。
201
+
202
+ 普通 Safari 标签页里,手势会避开屏幕左右各 24px 的边缘,以免和 Safari
203
+ 自带的“后退 / 前进”边缘手势抢事件;添加到主屏幕的独立窗口里没有这个
204
+ 系统手势,因此边缘也可以滑动。Mac 没有触摸事件,完全不受影响。
205
+
193
206
  `1.1.3` 的旧实现只移除 `tabindex`,在部分 iOS Safari 版本上仍可能被第一次触摸吃掉;
194
207
  如果更新到 `1.1.4` 后仍是两次,请彻底关闭这个 Safari 标签页再重新打开(不要只刷新),
195
208
  以排除旧的 `client.js` 缓存。
@@ -370,7 +383,7 @@ iPhone 软键盘没有可用的 Shift 键,因此插件在 **iOS / iPadOS** 上
370
383
  ### 推荐:npm 官方命令
371
384
 
372
385
  ```bash
373
- dsh plugin --profile web add dsh-mobile-access@1.1.4 --registry=https://registry.npmjs.org/
386
+ dsh plugin --profile web add dsh-mobile-access@1.1.6 --registry=https://registry.npmjs.org/
374
387
  ```
375
388
 
376
389
  ### 使用 `@latest`
@@ -500,7 +513,9 @@ dsh plugin --profile web remove dsh-mobile-access
500
513
  - `1.1.0` — 回环监听自动局域网网关(macOS / Windows,HTTP + `/api/remote.mux` WebSocket)。
501
514
  - `1.1.1` — iPhone / iPad 上模型选择页 16px 缩放守卫。
502
515
  - `1.1.2` — 打开模型选择时不自动弹键盘、不整体上移。
503
- - `1.1.4` — iPhone / iPad 上第一次点击直接切换会话,并在切换后自动收起左侧栏;当前 npm `latest`。
516
+ - `1.1.6` — iPhone / iPad 上右滑展开、左滑收起左侧栏;当前 npm `latest`。
517
+ - `1.1.5` — Tailscale Serve 经 HTTPS 访问时,允许带 Tailscale 身份头的 `*.ts.net` Host(仅限本机回环转发)。
518
+ - `1.1.4` — iPhone / iPad 上第一次点击直接切换会话,并在切换后自动收起左侧栏。
504
519
  - `1.1.3` — 首次尝试临时移除会话行 `tabindex`。
505
520
 
506
521
  ## 许可证
package/client.js CHANGED
@@ -291,6 +291,71 @@ function installIOSSidebarTapGuard(layout) {
291
291
  timers.clear();
292
292
  };
293
293
  }
294
+ /**
295
+ * Horizontal swipes on iPhone: right opens the left sidebar, left closes it.
296
+ * The gesture is limited to the content area and ignores vertical scrolling,
297
+ * editable controls, menus, code blocks and the right panel. On a normal Safari
298
+ * tab we stay 24px away from the screen edges so the system back/forward gesture
299
+ * keeps priority; an installed PWA has no such gesture, so edge starts are
300
+ * allowed there. Mac never installs this listener.
301
+ */
302
+ function installIOSSidebarSwipeGuard(layout) {
303
+ if (!isAppleMobile() || layout === undefined)
304
+ return () => undefined;
305
+ const doc = document;
306
+ let pending = null;
307
+ const isBlocked = (target) => target.closest('input, textarea, select, [contenteditable="true"], [data-composer-card], pre, code, [role="slider"], [data-rightbar], [role="menu"], [role="listbox"], button, a, [role="button"]') !== null;
308
+ const point = (event) => {
309
+ const touches = event.changedTouches ?? event.touches;
310
+ const touch = touches?.[0];
311
+ return touch === undefined ? null : { x: touch.clientX, y: touch.clientY };
312
+ };
313
+ const onTouchStart = (event) => {
314
+ pending = null;
315
+ const touches = event.touches;
316
+ if (touches !== undefined && touches.length !== 1)
317
+ return;
318
+ const target = event.target;
319
+ if (target === null || typeof target.closest !== 'function' || isBlocked(target))
320
+ return;
321
+ const at = point(event);
322
+ if (at === null)
323
+ return;
324
+ const standalone = navigator.standalone === true;
325
+ if (!standalone && (at.x < 24 || at.x > window.innerWidth - 24))
326
+ return;
327
+ pending = { x: at.x, y: at.y, startedAt: Date.now() };
328
+ };
329
+ const onTouchEnd = (event) => {
330
+ if (pending === null)
331
+ return;
332
+ const { x, y, startedAt } = pending;
333
+ pending = null;
334
+ if (Date.now() - startedAt > 800)
335
+ return;
336
+ const at = point(event);
337
+ if (at === null)
338
+ return;
339
+ const dx = at.x - x;
340
+ const dy = at.y - y;
341
+ if (Math.abs(dx) < 70 || Math.abs(dx) <= Math.abs(dy) * 1.5)
342
+ return;
343
+ const collapsed = doc.querySelector('[data-sidebar-collapsed]') !== null;
344
+ if (dx > 0 ? !collapsed : collapsed)
345
+ return;
346
+ try {
347
+ layout.toggleSidebar();
348
+ }
349
+ catch {
350
+ }
351
+ };
352
+ doc.addEventListener('touchstart', onTouchStart, true);
353
+ doc.addEventListener('touchend', onTouchEnd, true);
354
+ return () => {
355
+ doc.removeEventListener('touchstart', onTouchStart, true);
356
+ doc.removeEventListener('touchend', onTouchEnd, true);
357
+ };
358
+ }
294
359
  /**
295
360
  * The official model menu can move focus into its search field while it opens.
296
361
  * On iPhone that opens the software keyboard and shifts the fixed-position menu
@@ -637,6 +702,12 @@ function factory(require) {
637
702
  catch (error) {
638
703
  ctx.logger?.warn?.(`[mobile-access] iOS sidebar tap guard unavailable: ${String(error)}`);
639
704
  }
705
+ try {
706
+ cleanups.push(installIOSSidebarSwipeGuard(ctx.layout));
707
+ }
708
+ catch (error) {
709
+ ctx.logger?.warn?.(`[mobile-access] iOS sidebar swipe guard unavailable: ${String(error)}`);
710
+ }
640
711
  return () => {
641
712
  for (const cleanup of cleanups.reverse()) {
642
713
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mobile-access",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "Independent DSH bundle that pairs iPhones and iPads with the official DeepSeek Harness Web GUI over the local network.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/routes.js CHANGED
@@ -112,7 +112,28 @@ function pairingHostAllowed(ctx, request) {
112
112
  catch {
113
113
  return false;
114
114
  }
115
- return isLoopbackHost(bare) || bare === expected;
115
+ if (isLoopbackHost(bare) || bare === expected)
116
+ return true;
117
+ return isTailscaleServeRequest(request, bare);
118
+ }
119
+ /**
120
+ * Tailscale Serve terminates TLS and forwards to the loopback listener while
121
+ * preserving the public `*.ts.net` Host. Serve adds identity headers that a
122
+ * cross-site browser form cannot forge; requiring one plus a loopback peer
123
+ * keeps DNS-rebinding protection intact for direct loopback requests.
124
+ */
125
+ function isTailscaleServeRequest(request, hostname) {
126
+ if (!hostname.endsWith('.ts.net'))
127
+ return false;
128
+ const identity = firstHeader(request.headers, 'tailscale-user-login')
129
+ ?? firstHeader(request.headers, 'tailscale-user-name');
130
+ if (identity === undefined || identity.length === 0)
131
+ return false;
132
+ const remote = request.socket?.remoteAddress;
133
+ if (remote === undefined)
134
+ return false;
135
+ const normalized = remote.startsWith('::ffff:') ? remote.slice(7) : remote;
136
+ return isLoopbackHost(normalized);
116
137
  }
117
138
  function pairingNonce() {
118
139
  const bytes = new Uint8Array(18);