dsh-mobile-access 1.1.2 → 1.1.4
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 +27 -4
- package/client.js +118 -2
- package/package.json +1 -1
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.
|
|
82
|
+
dsh plugin --profile web add dsh-mobile-access@1.1.4 --registry=https://registry.npmjs.org/
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
这条命令做三件事:
|
|
@@ -175,6 +175,25 @@ Safari 分享按钮 → 添加到主屏幕
|
|
|
175
175
|
- 插件只在 iPhone / iPad 上安装「Return 换行」监听,Mac 和非 Apple 平台不会安装;
|
|
176
176
|
- 左边栏只多一个「手机访问」入口,其他入口和面板保持原样。
|
|
177
177
|
|
|
178
|
+
### iPhone 上切换左侧栏会话为什么要两次点击
|
|
179
|
+
|
|
180
|
+
如果你在左侧栏已经展开、能看到会话标题的情况下,点第一下没有切换、点第二下才切换,
|
|
181
|
+
这是官方会话行的触摸行为:每一行是 `role="treeitem"`、`tabindex="0"` 的可聚焦元素,
|
|
182
|
+
iOS Safari 会把第一次触摸先用去聚焦,第二次才派发 `click`。
|
|
183
|
+
|
|
184
|
+
从 `1.1.4` 起,插件在 iPhone / iPad 上不再依赖“移除 `tabindex` 后等浏览器补发 click”
|
|
185
|
+
这一条不确定路径:第一次触摸结束时就主动触发该会话行的点击,并抑制随后可能重复的
|
|
186
|
+
原生点击,因此一次点击只会切换一次;切换完成后,窄屏左侧栏会自动收起,把空间还给
|
|
187
|
+
对话区。会话行同时加 `touch-action: manipulation`,避免旧式双击缩放延迟。Mac 和
|
|
188
|
+
键盘操作不受影响。
|
|
189
|
+
|
|
190
|
+
如果左侧栏仍然是窄窄的图标条,第一下是展开侧栏——这是官方布局,不是会话行的问题;
|
|
191
|
+
展开后每个会话只需单击一次。
|
|
192
|
+
|
|
193
|
+
`1.1.3` 的旧实现只移除 `tabindex`,在部分 iOS Safari 版本上仍可能被第一次触摸吃掉;
|
|
194
|
+
如果更新到 `1.1.4` 后仍是两次,请彻底关闭这个 Safari 标签页再重新打开(不要只刷新),
|
|
195
|
+
以排除旧的 `client.js` 缓存。
|
|
196
|
+
|
|
178
197
|
### iPhone 上切换大模型时页面放大、键盘自动弹出怎么办
|
|
179
198
|
|
|
180
199
|
从 `1.1.2` 起不需要改 DSH,也不需要关掉 Safari 的缩放:
|
|
@@ -351,7 +370,7 @@ iPhone 软键盘没有可用的 Shift 键,因此插件在 **iOS / iPadOS** 上
|
|
|
351
370
|
### 推荐:npm 官方命令
|
|
352
371
|
|
|
353
372
|
```bash
|
|
354
|
-
dsh plugin --profile web add dsh-mobile-access@1.1.
|
|
373
|
+
dsh plugin --profile web add dsh-mobile-access@1.1.4 --registry=https://registry.npmjs.org/
|
|
355
374
|
```
|
|
356
375
|
|
|
357
376
|
### 使用 `@latest`
|
|
@@ -466,7 +485,7 @@ dsh plugin --profile web remove dsh-mobile-access
|
|
|
466
485
|
- 左侧栏入口消失;
|
|
467
486
|
- `/m` 恢复为普通 SPA fallback(不再是配对页);
|
|
468
487
|
- `/api/mobile-access/*` 不再注册;
|
|
469
|
-
- iOS
|
|
488
|
+
- iOS 剪贴板回退、Return 换行监听、16px 缩放守卫和模型搜索焦点守卫被清理。
|
|
470
489
|
|
|
471
490
|
已签发的登录 Cookie 可能仍留在浏览器中,直到过期;这是 DSH 认证层的行为,不是插件额外写入的凭证。
|
|
472
491
|
|
|
@@ -478,7 +497,11 @@ dsh plugin --profile web remove dsh-mobile-access
|
|
|
478
497
|
- `1.0.1` — 安装文档与 npm 发布元数据。
|
|
479
498
|
- `1.0.2` — iPhone / iPad 自带键盘 Return 换行。
|
|
480
499
|
- `1.0.3` — Safari 不支持改写键盘事件时的换行回退。
|
|
481
|
-
- `1.1.0` — 回环监听自动局域网网关(macOS / Windows,HTTP + `/api/remote.mux` WebSocket
|
|
500
|
+
- `1.1.0` — 回环监听自动局域网网关(macOS / Windows,HTTP + `/api/remote.mux` WebSocket)。
|
|
501
|
+
- `1.1.1` — iPhone / iPad 上模型选择页 16px 缩放守卫。
|
|
502
|
+
- `1.1.2` — 打开模型选择时不自动弹键盘、不整体上移。
|
|
503
|
+
- `1.1.4` — iPhone / iPad 上第一次点击直接切换会话,并在切换后自动收起左侧栏;当前 npm `latest`。
|
|
504
|
+
- `1.1.3` — 首次尝试临时移除会话行 `tabindex`。
|
|
482
505
|
|
|
483
506
|
## 许可证
|
|
484
507
|
|
package/client.js
CHANGED
|
@@ -167,8 +167,11 @@ const IOS_INPUT_ZOOM_GUARD_CSS = `@media (hover: none) and (pointer: coarse) {
|
|
|
167
167
|
button,
|
|
168
168
|
[role="button"],
|
|
169
169
|
[role="menuitem"],
|
|
170
|
-
[role="menuitemradio"]
|
|
170
|
+
[role="menuitemradio"],
|
|
171
|
+
[data-row-key^="session:"],
|
|
172
|
+
[role="treeitem"] {
|
|
171
173
|
touch-action: manipulation;
|
|
174
|
+
-webkit-user-drag: none;
|
|
172
175
|
}
|
|
173
176
|
}`;
|
|
174
177
|
function installIOSInputZoomGuard() {
|
|
@@ -181,6 +184,113 @@ function installIOSInputZoomGuard() {
|
|
|
181
184
|
document.head.appendChild(style);
|
|
182
185
|
return () => style.remove();
|
|
183
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Safari treats a touch on a focusable, non-button row as a focus step: the
|
|
189
|
+
* first tap focuses the row and only the second tap reaches its click handler.
|
|
190
|
+
* Official session rows are `role="treeitem"` divs with `tabIndex={0}`, so
|
|
191
|
+
* switching sessions on iPhone takes two taps. Remove that one attribute for
|
|
192
|
+
* the duration of the tap, activate the row on the first tap ourselves, and
|
|
193
|
+
* collapse the narrow sidebar after the switch. Mac and keyboard users are
|
|
194
|
+
* untouched; `touch-action: manipulation` in the iOS stylesheet removes the
|
|
195
|
+
* legacy double-tap-zoom delay.
|
|
196
|
+
*/
|
|
197
|
+
function installIOSSidebarTapGuard(layout) {
|
|
198
|
+
if (!isAppleMobile())
|
|
199
|
+
return () => undefined;
|
|
200
|
+
const doc = document;
|
|
201
|
+
const timers = new Set();
|
|
202
|
+
let pending = null;
|
|
203
|
+
let syntheticClick = false;
|
|
204
|
+
let suppressNativeClick = false;
|
|
205
|
+
const isAction = (target) => target.closest('button, a, input, textarea, select, [role="button"], [role="menuitem"], [role="menuitemradio"], [role="switch"], [data-row-actions]') !== null;
|
|
206
|
+
const point = (event) => {
|
|
207
|
+
const touches = event.changedTouches ?? event.touches;
|
|
208
|
+
const touch = touches?.[0];
|
|
209
|
+
return touch === undefined ? null : { x: touch.clientX, y: touch.clientY };
|
|
210
|
+
};
|
|
211
|
+
const onTouchStart = (event) => {
|
|
212
|
+
const target = event.target;
|
|
213
|
+
if (target === null || typeof target.closest !== 'function')
|
|
214
|
+
return;
|
|
215
|
+
if (isAction(target)) {
|
|
216
|
+
pending = null;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const row = target.closest('[data-row-key^="session:"]');
|
|
220
|
+
if (row === null)
|
|
221
|
+
return;
|
|
222
|
+
const at = point(event);
|
|
223
|
+
if (at === null)
|
|
224
|
+
return;
|
|
225
|
+
pending = { row, x: at.x, y: at.y, startedAt: Date.now() };
|
|
226
|
+
const tabindex = row.getAttribute('tabindex');
|
|
227
|
+
if (tabindex === null)
|
|
228
|
+
return;
|
|
229
|
+
row.removeAttribute('tabindex');
|
|
230
|
+
const timer = window.setTimeout(() => {
|
|
231
|
+
timers.delete(timer);
|
|
232
|
+
if (row.isConnected)
|
|
233
|
+
row.setAttribute('tabindex', tabindex);
|
|
234
|
+
}, 400);
|
|
235
|
+
timers.add(timer);
|
|
236
|
+
};
|
|
237
|
+
const onClickCapture = (event) => {
|
|
238
|
+
const target = event.target;
|
|
239
|
+
if (target === null || typeof target.closest !== 'function' || target.closest('[data-row-key^="session:"]') === null)
|
|
240
|
+
return;
|
|
241
|
+
if (syntheticClick) {
|
|
242
|
+
syntheticClick = false;
|
|
243
|
+
suppressNativeClick = true;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (!suppressNativeClick)
|
|
247
|
+
return;
|
|
248
|
+
suppressNativeClick = false;
|
|
249
|
+
event.preventDefault();
|
|
250
|
+
event.stopImmediatePropagation();
|
|
251
|
+
};
|
|
252
|
+
const onTouchEnd = (event) => {
|
|
253
|
+
if (pending === null)
|
|
254
|
+
return;
|
|
255
|
+
const { row, x, y, startedAt } = pending;
|
|
256
|
+
pending = null;
|
|
257
|
+
if (!row.isConnected)
|
|
258
|
+
return;
|
|
259
|
+
const at = point(event);
|
|
260
|
+
if (at === null)
|
|
261
|
+
return;
|
|
262
|
+
if (Math.hypot(at.x - x, at.y - y) > 10 || Date.now() - startedAt > 600)
|
|
263
|
+
return;
|
|
264
|
+
syntheticClick = true;
|
|
265
|
+
row.click();
|
|
266
|
+
const timer = window.setTimeout(() => {
|
|
267
|
+
timers.delete(timer);
|
|
268
|
+
syntheticClick = false;
|
|
269
|
+
suppressNativeClick = false;
|
|
270
|
+
}, 500);
|
|
271
|
+
timers.add(timer);
|
|
272
|
+
if (window.innerWidth < 1024) {
|
|
273
|
+
window.setTimeout(() => {
|
|
274
|
+
try {
|
|
275
|
+
layout?.toggleSidebar();
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
}
|
|
279
|
+
}, 0);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
doc.addEventListener('touchstart', onTouchStart, true);
|
|
283
|
+
doc.addEventListener('touchend', onTouchEnd, true);
|
|
284
|
+
doc.addEventListener('click', onClickCapture, true);
|
|
285
|
+
return () => {
|
|
286
|
+
doc.removeEventListener('touchstart', onTouchStart, true);
|
|
287
|
+
doc.removeEventListener('touchend', onTouchEnd, true);
|
|
288
|
+
doc.removeEventListener('click', onClickCapture, true);
|
|
289
|
+
for (const timer of timers)
|
|
290
|
+
window.clearTimeout(timer);
|
|
291
|
+
timers.clear();
|
|
292
|
+
};
|
|
293
|
+
}
|
|
184
294
|
/**
|
|
185
295
|
* The official model menu can move focus into its search field while it opens.
|
|
186
296
|
* On iPhone that opens the software keyboard and shifts the fixed-position menu
|
|
@@ -461,7 +571,7 @@ const STYLES = ".dsh-ma{height:100%;min-height:100%;overflow:auto;background:var
|
|
|
461
571
|
function factory(require) {
|
|
462
572
|
React = require('react');
|
|
463
573
|
return {
|
|
464
|
-
inject: ['slots', 'locale'],
|
|
574
|
+
inject: ['slots', 'locale', 'layout'],
|
|
465
575
|
apply(ctx) {
|
|
466
576
|
ctx.effect(() => {
|
|
467
577
|
const cleanups = [];
|
|
@@ -521,6 +631,12 @@ function factory(require) {
|
|
|
521
631
|
catch (error) {
|
|
522
632
|
ctx.logger?.warn?.(`[mobile-access] iOS model keyboard guard unavailable: ${String(error)}`);
|
|
523
633
|
}
|
|
634
|
+
try {
|
|
635
|
+
cleanups.push(installIOSSidebarTapGuard(ctx.layout));
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
ctx.logger?.warn?.(`[mobile-access] iOS sidebar tap guard unavailable: ${String(error)}`);
|
|
639
|
+
}
|
|
524
640
|
return () => {
|
|
525
641
|
for (const cleanup of cleanups.reverse()) {
|
|
526
642
|
try {
|
package/package.json
CHANGED