dsh-mobile-access 1.1.6 → 1.1.8

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.
Files changed (3) hide show
  1. package/README.md +231 -51
  2. package/client.js +98 -7
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -2,15 +2,19 @@
2
2
 
3
3
  [![verify](https://github.com/abbaababa/dsh-mobile-access/actions/workflows/verify.yml/badge.svg)](https://github.com/abbaababa/dsh-mobile-access/actions/workflows/verify.yml)
4
4
 
5
- > 用 iPhone / iPad 自带的 Safari,安全打开 Mac 上的 DeepSeek Harness Web。
5
+ > 用 iPhone / iPad 自带的 Safari,安全打开 **Mac 或 Windows 10/11** 上的 DeepSeek Harness Web。
6
6
  >
7
- > **不用装 App,不用改 DSH 源码,不影响 Mac 上原来怎么用。**
7
+ > **不用装 App,不用改 DSH 源码,不影响你原来在电脑上的使用。**
8
+ >
9
+ > **Windows 10/11 用户也能用**:同一份插件,不区分 Mac / Windows。
10
+ > Windows 只是多一个“防火墙允许 Node.js”的步骤;完整小白步骤见
11
+ > [Windows 10/11 快速上手](#windows-1011-快速上手小白版)。
8
12
 
9
- 这是一个独立的 TypeScript DSH 插件(bundle)。它只在 DSH 里增加三样东西:
13
+ 这是一个独立的 TypeScript DSH 插件(bundle)。它主要在 DSH 里增加三样东西:
10
14
 
11
15
  1. 左侧栏一个「手机访问」入口;
12
16
  2. 一个给手机用的短地址和 8 位配对码;
13
- 3. 一个只在 iPhone/iPad 上生效的「回车换行」小补丁。
17
+ 3. 一组只在 iPhone/iPad 上生效的输入与触摸小补丁(Return 换行、防自动放大、侧栏手势等)。
14
18
 
15
19
  其他东西都不动:不改 DSH 核心、不改登录方式、不替换页面、不装后台服务、不拦截现有请求。
16
20
 
@@ -18,7 +22,7 @@
18
22
 
19
23
  ## 先看结论:它到底帮你做了什么
20
24
 
21
- 假设你的 Mac 上正在运行 DSH Web,地址是:
25
+ 假设你的电脑(Mac 或 Windows 10/11)上正在运行 DSH Web,地址是:
22
26
 
23
27
  ```text
24
28
  http://192.168.1.23:3080
@@ -30,12 +34,12 @@ http://192.168.1.23:3080
30
34
  dsh web authentication required; reopen the URL printed by dsh web.
31
35
  ```
32
36
 
33
- 这不是网络坏了,而是 DSH 的安全保护:它只认 Mac 终端里打印出来的那条**带很长 token 的登录链接**。这条链接太长,手机不可能手输。
37
+ 这不是网络坏了,而是 DSH 的安全保护:它只认电脑终端(Mac 终端 / Windows PowerShell)里打印出来的那条**带很长 token 的登录链接**。这条链接太长,手机不可能手输。
34
38
 
35
39
  这个插件做的事,就是把这个过程变成:
36
40
 
37
41
  ```text
38
- Mac 面板显示 8 位配对码
42
+ 电脑面板显示 8 位配对码
39
43
  ↓
40
44
  iPhone 打开短地址 /m
41
45
  ↓
@@ -49,37 +53,198 @@ iPhone 打开短地址 /m
49
53
  - DSH 是一栋上锁的房子;
50
54
  - 原来的长 token 是一把很长的钥匙,手机没法输;
51
55
  - 插件在门口装了一个「临时门禁」;
52
- - Mac 上的人看到 8 位门禁码,告诉手机;
56
+ - 电脑前的人看到 8 位门禁码,告诉手机;
53
57
  - 手机输对后,屋里直接给它戴上 30 天有效的「手环」(HttpOnly 登录 Cookie)。
54
58
 
55
59
  **长钥匙不会出现在手机地址栏、浏览器历史或网页来源信息里。**
56
60
 
57
61
  ---
58
62
 
63
+ ## Windows 10/11 快速上手(小白版)
64
+
65
+ > 这一段是专门写给“不写代码、不懂网络”的 Windows 用户的。
66
+ > 如果你用的是 Mac,可以直接看下面的[安装](#安装mac-或-windows-操作复制即可)。
67
+
68
+ ### 先说结论
69
+
70
+ **这个插件不是 Mac 专用的。**
71
+
72
+ 它在 Windows 10 / Windows 11 上使用的是**同一个 npm 包、同一条安装命令、同一个「手机访问」面板**。
73
+ Windows 和 Mac 最需要额外注意的区别是:
74
+
75
+ > Windows 第一次运行时,系统防火墙会弹出一个“是否允许 Node.js 访问网络”的窗口,点一次“允许”即可。
76
+
77
+ 不需要安装 iTunes,不需要买 Mac,不需要改 DSH 源码,也不需要在 iPhone 上装 App。
78
+
79
+ ### 你需要准备什么
80
+
81
+ 只需要 4 样东西:
82
+
83
+ 1. 一台 **Windows 10 或 Windows 11** 电脑;
84
+ 2. 电脑上的 **DSH Web 已经能正常打开**(平时启动 DSH 的那个窗口不要关);
85
+ 3. iPhone 和 Windows 电脑连的是**同一个 Wi-Fi**;
86
+ 4. Windows 上已经装好 DSH 需要的 **Node.js**(DSH 能启动,就说明已经装好)。
87
+
88
+ ### 三步安装
89
+
90
+ #### 第 1 步:打开 PowerShell
91
+
92
+ 在 Windows 上:
93
+
94
+ 1. 按键盘上的 **`Win + X`**;
95
+ 2. 在弹出的菜单里选 **“终端”** 或 **“Windows PowerShell”**;
96
+ 3. 不需要“以管理员身份运行”;
97
+ 4. 看到类似 `PS C:\Users\你的名字>` 的提示符,就说明打开了。
98
+
99
+ #### 第 2 步:复制下面这一条命令,粘贴,回车
100
+
101
+ ```powershell
102
+ dsh plugin --profile web add dsh-mobile-access@1.1.8 --registry=https://registry.npmjs.org/
103
+ ```
104
+
105
+ 这条命令只做三件事:
106
+
107
+ 1. 从 npm 下载插件;
108
+ 2. 装进 DSH 的 `web` profile;
109
+ 3. 自动把插件加入 DSH 的插件列表。
110
+
111
+ > 如果你的 DSH 平时用的不是 `web` 这个 profile,把命令里的 `web` 换成你实际使用的名字。
112
+ > 不确定时先照抄;如果提示 profile 不存在,再检查你平时启动 DSH 用的名称。
113
+
114
+ 如果 PowerShell 提示“`dsh` 不是内部或外部命令”,说明 DSH 没有加入系统 PATH。
115
+ 这不代表插件坏了:请先按你平时启动 DSH 的方式打开它的终端,再执行同一条命令;或者把 DSH 的 `bin` 目录加入 PATH。不要因此重装 Node.js 或 DSH。
116
+
117
+ #### 第 3 步:回到 DSH 页面,按 `Ctrl + R` 刷新
118
+
119
+ 刷新后,DSH 左侧栏会出现一个**手机图标**,名字叫「手机访问」。
120
+
121
+ 如果插件管理器提示“需要重启”,请**手动重启 DSH**;不要连续反复重装。
122
+ 正常情况下不需要重启。
123
+
124
+ ### 第一次打开:Windows 防火墙会问你
125
+
126
+ 第一次让手机访问时,Windows 可能弹出:
127
+
128
+ ```text
129
+ Windows Defender 防火墙已阻止此应用的部分功能
130
+ 是否允许 Node.js 在专用网络上通信?
131
+ ```
132
+
133
+ 请这样选:
134
+
135
+ 1. 勾选 **“专用网络”**(英文是 **Private networks**);
136
+ 2. 点 **“允许访问”**(Allow access)。
137
+
138
+ 不要勾“公用网络”(Public),除非你明确知道自己在做什么。
139
+
140
+ 如果当时误点了“取消”,或者根本没弹窗,可以手动放行:
141
+
142
+ 1. 打开 **Windows 安全中心**;
143
+ 2. 点 **“防火墙和网络保护”**;
144
+ 3. 点 **“允许应用通过防火墙”**;
145
+ 4. 点 **“更改设置”**;
146
+ 5. 在列表里找到 **Node.js**;
147
+ 6. 勾选 **“专用”**;
148
+ 7. 点“确定”。
149
+
150
+ 还要确认当前 Wi-Fi 被 Windows 当作“专用网络”:
151
+
152
+ 1. 打开 **设置 → 网络和 Internet → Wi-Fi**;
153
+ 2. 点当前连接的 Wi-Fi 名称;
154
+ 3. 把“网络配置文件类型”改成 **“专用网络”**。
155
+
156
+ > 如果 Wi-Fi 被设成“公用网络”,Windows 防火墙通常会更严格,iPhone 可能打不开页面。
157
+
158
+ > **如果 DSH 安装在 WSL(Windows 的 Linux 子系统)里**:WSL 默认使用虚拟网卡,局域网里的 iPhone 通常无法直接访问 WSL 内部的端口。这不是插件本身的问题,而是 WSL 的网络隔离。建议把官方 DSH 直接安装在 Windows 本机;如果必须用 WSL,需要额外配置 WSL 端口转发和 Windows 防火墙,超出本插件“零配置”的范围。
159
+
160
+ ### 手机上输入哪个地址
161
+
162
+ 在 Windows 的 DSH 左侧栏点「手机访问」,面板会显示一个地址。
163
+ 直接把这个地址输入 iPhone 的 Safari 即可。
164
+
165
+ 两种常见情况:
166
+
167
+ | Windows 上 DSH 的启动方式 | 手机应该输入的地址 |
168
+ |---|---|
169
+ | 只监听 `127.0.0.1`(默认) | 面板显示的 `http://<Windows 局域网 IP>:3081/m` |
170
+ | 已经监听局域网地址 | 面板显示的 `http://<Windows 局域网 IP>:3080/m` |
171
+
172
+ **不要输入 `127.0.0.1` 或 `localhost`。**
173
+ 这两个地址在 iPhone 上代表“iPhone 自己”,不是你的 Windows 电脑。
174
+
175
+ 如果你不知道 Windows 的局域网 IP:
176
+
177
+ 1. 在 PowerShell 里输入:
178
+ ```powershell
179
+ ipconfig
180
+ ```
181
+ 2. 找到当前 Wi-Fi 网卡下的 **“IPv4 地址”**;
182
+ 3. 它通常长这样:`192.168.1.23`;
183
+ 4. 面板一般会自动显示这个地址,不需要你手动拼。
184
+
185
+ ### 配对(Windows + iPhone)
186
+
187
+ 1. 在 Windows 的 DSH 左侧栏点「手机访问」;
188
+ 2. 记下面板上的 **8 位配对码**;
189
+ 3. 在 iPhone Safari 打开面板显示的 `/m` 地址;
190
+ 4. 输入 8 位数字;
191
+ 5. 点「连接 DSH」;
192
+ 6. 进入后,建议点 Safari 的分享按钮 → **“添加到主屏幕”**。
193
+
194
+ ### Windows 常见问题
195
+
196
+ | 现象 | 最可能的原因 | 怎么解决 |
197
+ |---|---|---|
198
+ | iPhone 打不开 `/m` | 防火墙没放行 Node.js | 按上面的“Windows 防火墙”步骤,允许“专用网络” |
199
+ | iPhone 打不开 `/m` | Windows 和 iPhone 不在同一个 Wi-Fi | 检查两边 Wi-Fi 名称,关掉手机蜂窝网络再试 |
200
+ | iPhone 打不开 `/m` | 地址里写了 `127.0.0.1` | 换成面板显示的 Windows 局域网 IP |
201
+ | 页面能开,但提示 401 | 打开的是 `/` 而不是 `/m` | 先打开 `/m` 配对,再进入 |
202
+ | 提示“配对码已过期” | 8 位码超过 10 分钟 | 在 Windows 面板点“刷新配对码”,用新码重试 |
203
+ | 提示“已锁定” | 连续输错次数太多 | 在 Windows 面板刷新配对码,不会自动解锁 |
204
+ | `3081` 被占用 | 其他软件占用了端口 | 插件会自动尝试后面 10 个端口,以面板显示为准 |
205
+ | 电脑睡眠后手机打不开 | DSH 随电脑睡眠暂停 | 唤醒 Windows,确认 DSH 窗口仍在运行 |
206
+ | 公司/学校 Wi-Fi 打不开 | 路由器开了“客户端隔离” | 换家庭 Wi-Fi,或用手机热点让两边连同一个热点 |
207
+ | 装了 VPN 后打不开 | VPN 把局域网路由改走 | 临时关闭 VPN,或让 VPN 放行局域网 |
208
+
209
+ ### Windows 和 Mac 有什么区别
210
+
211
+ | 项目 | Mac | Windows 10/11 |
212
+ |---|---|---|
213
+ | 安装命令 | 同一条 | 同一条 |
214
+ | 终端 | “终端” App | PowerShell |
215
+ | 刷新 DSH | `Command + R` | `Ctrl + R` |
216
+ | 第一次防火墙 | 通常没有 | 允许 Node.js 访问“专用网络” |
217
+ | 回环网关 | 自动使用 `3081` | 自动使用 `3081` |
218
+ | iPhone 端操作 | 一样 | 一样 |
219
+ | Mac/Windows 原有使用 | 不变 | 不变 |
220
+
221
+ > Windows 的自动化构建和 33 项测试已在 GitHub 的 Windows CI 上通过。
222
+ > 真正挡住小白的通常不是代码,而是“防火墙点了拒绝”或“Wi-Fi 被设成公用网络”。
223
+
59
224
  ## 适合谁
60
225
 
61
- - Mac 和 iPhone / iPad 在**同一个 Wi-Fi / 局域网**;
62
- - Mac 上运行的是官方 DeepSeek Harness Web;
226
+ - **Mac 或 Windows 10/11** 电脑和 iPhone / iPad 在**同一个 Wi-Fi / 局域网**;
227
+ - 电脑上运行的是官方 DeepSeek Harness Web(Mac / Windows 均可);
63
228
  - 希望手机用 Safari 完整使用 DSH,而不是只看一个 401 错误页。
64
229
 
65
230
  ## 不适合谁
66
231
 
67
- - 想在**外网**(不在同一个 Wi-Fi)访问家里的 Mac:这不是 VPN / 内网穿透工具,请不要把 DSH 直接暴露到公网。
68
- - Mac 的 DSH 只监听 `127.0.0.1`:`1.1.0` 起插件会**自动**在 `3081` 起一个只做转发的局域网网关,Mac 仍用 `127.0.0.1`,不需要改 Host 配置,也不需要把 DSH 暴露到公网。
232
+ - 想在**外网**(不在同一个 Wi-Fi)访问家里的电脑:这不是 VPN / 内网穿透工具,请不要把 DSH 直接暴露到公网。
233
+ - 电脑上的 DSH 只监听 `127.0.0.1`:`1.1.0` 起插件会**自动**在 `3081` 起一个只做转发的局域网网关,电脑仍用 `127.0.0.1`,不需要改 Host 配置,也不需要把 DSH 暴露到公网。
69
234
  - 使用 HTTPS 但证书不被 iPhone 信任:Safari 会先拦截证书,这和插件无关。
70
235
 
71
236
  ---
72
237
 
73
- ## 安装(Mac 上操作,复制即可)
238
+ ## 安装(Mac 或 Windows 操作,复制即可)
74
239
 
75
- ### 第 1 步:确认 Mac 和 iPhone 连的是同一个 Wi-Fi
240
+ ### 第 1 步:确认电脑和 iPhone 连的是同一个 Wi-Fi
76
241
 
77
- iPhone 打开「设置 → 无线局域网」,确认和 Mac 连的是同一个网络。
242
+ iPhone 打开「设置 → 无线局域网」,确认和电脑连的是同一个网络。
78
243
 
79
- ### 第 2 步:在 Mac 终端执行一条命令
244
+ ### 第 2 步:在 Mac 终端或 Windows PowerShell 执行一条命令
80
245
 
81
246
  ```bash
82
- dsh plugin --profile web add dsh-mobile-access@1.1.6 --registry=https://registry.npmjs.org/
247
+ dsh plugin --profile web add dsh-mobile-access@1.1.8 --registry=https://registry.npmjs.org/
83
248
  ```
84
249
 
85
250
  这条命令做三件事:
@@ -90,15 +255,16 @@ dsh plugin --profile web add dsh-mobile-access@1.1.6 --registry=https://registry
90
255
 
91
256
  > `--profile web` 里的 `web` 是本项目使用的 profile 名称。
92
257
  > 如果你的 DSH 用的是其他 profile,把 `web` 换成实际名称即可。
93
- > 不确定时,照抄这条命令;如果提示 profile 不存在,再检查你平时启动 DSH 用的名称。
258
+ > 不确定时,照抄这条命令;如果提示 profile 不存在,再检查你平时启动 DSH 用的名称。
259
+ > Windows PowerShell 里也是这一条命令,不需要改引号。
94
260
 
95
261
  ### 如果 DSH 只监听 `127.0.0.1`
96
262
 
97
263
  从 `1.1.0` 开始不需要改 DSH 配置。插件检测到回环监听后,会自动在
98
264
  `3081` 起一个只做转发的局域网网关:
99
265
 
100
- - Mac 继续使用原来的 `http://127.0.0.1:3080`,行为不变;
101
- - iPhone 使用面板上显示的 `http://<Mac 局域网 IP>:3081/m`;
266
+ - 电脑继续使用原来的 `http://127.0.0.1:3080`,行为不变;
267
+ - iPhone 使用面板上显示的 `http://<电脑局域网 IP>:3081/m`;
102
268
  - 网关会同时转发 DSH 的 `/api/remote.mux` WebSocket,聊天流不受影响;
103
269
  - 如果 `3081` 被占用,会自动尝试后 10 个端口,面板会显示实际地址。
104
270
 
@@ -108,10 +274,11 @@ Node.js 访问网络,请勾选“专用网络”并允许;如果误点了拒
108
274
 
109
275
  ### 第 3 步:刷新 DSH 页面
110
276
 
111
- 在 Mac 的 DSH 页面按:
277
+ 在 DSH 页面按:
112
278
 
113
279
  ```text
114
- Command + R
280
+ Mac:Command + R
281
+ Windows:Ctrl + R
115
282
  ```
116
283
 
117
284
  刷新后,左侧栏应该出现一个**手机图标**,名字叫「手机访问」。
@@ -129,9 +296,9 @@ dsh plugin --profile web why dsh-mobile-access
129
296
 
130
297
  ---
131
298
 
132
- ## 配对(Mac + iPhone 一起操作)
299
+ ## 配对(电脑 + iPhone 一起操作)
133
300
 
134
- ### Mac 端
301
+ ### 电脑端(Mac 或 Windows)
135
302
 
136
303
  1. 打开 DSH Web;
137
304
  2. 点左侧栏的「手机访问」;
@@ -144,7 +311,7 @@ dsh plugin --profile web why dsh-mobile-access
144
311
  ### iPhone 端
145
312
 
146
313
  1. 打开 Safari;
147
- 2. 输入 Mac 面板上显示的 `/m` 地址(不要输成 `127.0.0.1`,也不要用 `localhost`);
314
+ 2. 输入电脑面板上显示的 `/m` 地址(不要输成 `127.0.0.1`,也不要用 `localhost`);
148
315
  3. 输入 8 位配对码;
149
316
  4. 点「连接 DSH」;
150
317
  5. 成功后会直接进入完整 DSH Web。
@@ -167,11 +334,11 @@ Safari 分享按钮 → 添加到主屏幕
167
334
  - 要发送时,点输入框右侧的**发送按钮**;
168
335
  - 这样可以正常输入多行提示词。
169
336
 
170
- ### Mac 上有没有变化
337
+ ### 电脑上有没有变化
171
338
 
172
339
  没有。
173
340
 
174
- - Mac 上仍然是:**Enter 发送,Shift+Enter 换行**;
341
+ - Mac 和 Windows 上仍然是:**Enter 发送,Shift+Enter 换行**;
175
342
  - 插件只在 iPhone / iPad 上安装「Return 换行」监听,Mac 和非 Apple 平台不会安装;
176
343
  - 左边栏只多一个「手机访问」入口,其他入口和面板保持原样。
177
344
 
@@ -190,18 +357,28 @@ iOS Safari 会把第一次触摸先用去聚焦,第二次才派发 `click`。
190
357
  如果左侧栏仍然是窄窄的图标条,第一下是展开侧栏——这是官方布局,不是会话行的问题;
191
358
  展开后每个会话只需单击一次。
192
359
 
193
- ### iPhone 上左右滑动展开 / 收起左侧栏
360
+ ### 切换会话后键盘不再自动弹出
361
+
362
+ 从 `1.1.8` 起,iPhone / iPad 上切换会话时,插件会先收起当前输入框的键盘:
363
+ 旧输入框如果还保持焦点,会立即失焦;会话切换后的 800 毫秒内,如果页面又尝试
364
+ 用程序方式把焦点塞回输入框,插件也会把它收起。只有你亲手点一下输入框,键盘才会
365
+ 再次出现。这样切换会话后不会莫名其妙弹出输入法,语音输入和正常打字都不受影响。
366
+ Mac 和 Windows 桌面不安装这条 iOS 专属逻辑,行为保持原样。
367
+
368
+ ### iPhone 上展开 / 收起左侧栏
194
369
 
195
- 从 `1.1.6` 起,iPhone / iPad 上还支持:
370
+ 从 `1.1.7` 起,iPhone / iPad 上支持:
196
371
 
197
372
  - 在对话内容区**向右滑**:展开左侧栏(仅在左侧栏已收起时生效);
198
- - 在对话内容区**向左滑**:收起左侧栏(仅在左侧栏已展开时生效);
199
- - 滑动需要水平距离至少 70px,且明显大于上下移动,避免和滚动、
200
- 横向代码块、输入框、模型菜单和右侧面板冲突。
373
+ - 在对话内容区**单击一下**:收起已经展开的左侧栏;
374
+ - 右滑需要水平距离至少 70px,且明显大于上下移动,避免和滚动、
375
+ 横向代码块、输入框、模型菜单和右侧面板冲突;
376
+ - 单击收起只对“内容区”生效,点按钮、输入框、链接、代码块、会话行、
377
+ 模型菜单或右侧面板不会误收起。
201
378
 
202
- 普通 Safari 标签页里,手势会避开屏幕左右各 24px 的边缘,以免和 Safari
379
+ 普通 Safari 标签页里,右滑会避开屏幕左右各 24px 的边缘,以免和 Safari
203
380
  自带的“后退 / 前进”边缘手势抢事件;添加到主屏幕的独立窗口里没有这个
204
- 系统手势,因此边缘也可以滑动。Mac 没有触摸事件,完全不受影响。
381
+ 系统手势,因此边缘也可以滑动。Mac 和 Windows 桌面没有安装这些 iOS 专属触摸处理,行为保持原样。
205
382
 
206
383
  `1.1.3` 的旧实现只移除 `tabindex`,在部分 iOS Safari 版本上仍可能被第一次触摸吃掉;
207
384
  如果更新到 `1.1.4` 后仍是两次,请彻底关闭这个 Safari 标签页再重新打开(不要只刷新),
@@ -217,7 +394,7 @@ iOS Safari 会把第一次触摸先用去聚焦,第二次才派发 `click`。
217
394
  - 打开右下角模型选择时,插件不会让模型搜索框自动抢焦点,所以键盘不会自动
218
395
  弹出;需要搜索时,点一下搜索框再输入即可;
219
396
  - 如果提示词键盘本来就是打开的,点模型入口时会先收起键盘;
220
- - Mac 和非 Apple 平台不安装这些处理,行为保持原样。
397
+ - 电脑端(Mac / Windows)和非 Apple 平台不安装这些 iOS 专属处理,行为保持原样。
221
398
 
222
399
  如果页面已经处于放大状态,先点 Safari 地址栏刷新一次,或双击页面空白处
223
400
  复位,再重新打开模型选择即可。
@@ -226,7 +403,7 @@ iOS Safari 会把第一次触摸先用去聚焦,第二次才派发 `click`。
226
403
 
227
404
  配对码默认 **10 分钟**有效。过期后:
228
405
 
229
- 1. 回到 Mac 的「手机访问」面板;
406
+ 1. 回到电脑的「手机访问」面板;
230
407
  2. 点「刷新配对码」;
231
408
  3. iPhone 重新打开 `/m`,输入新码。
232
409
 
@@ -267,14 +444,14 @@ iOS Safari 会把第一次触摸先用去聚焦,第二次才派发 `click`。
267
444
 
268
445
  按顺序检查:
269
446
 
270
- 1. iPhone 和 Mac 是不是同一个 Wi-Fi;
271
- 2. 地址是不是 Mac 面板上显示的**完整地址**(回环监听时通常是 `:3081`,局域网监听时可能是 `:3080`);
272
- 3. Mac 面板的「局域网网关」是否显示「已开启」;如果显示错误,按提示检查 Windows 防火墙或端口占用;
447
+ 1. iPhone 和电脑是不是同一个 Wi-Fi;
448
+ 2. 地址是不是电脑面板上显示的**完整地址**(回环监听时通常是 `:3081`,局域网监听时可能是 `:3080`);
449
+ 3. 电脑面板的「局域网网关」是否显示「已开启」;如果显示错误,按提示检查 Windows 防火墙或端口占用;
273
450
  4. 路由器是否开启了「客户端隔离 / AP 隔离」(开启后同一 Wi-Fi 的设备也不能互访)。
274
451
 
275
452
  ### 2. 左边栏没有「手机访问」
276
453
 
277
- - 先刷新 DSH 页面(`Command + R`);
454
+ - 先刷新 DSH 页面(Mac 按 `Command + R`,Windows 按 `Ctrl + R`);
278
455
  - 再确认安装命令没有报错;
279
456
  - 检查 `dsh plugin --profile web why dsh-mobile-access`;
280
457
  - 如果刚安装完仍没有,按插件管理器提示判断是否需要手动重启 DSH。
@@ -282,11 +459,11 @@ iOS Safari 会把第一次触摸先用去聚焦,第二次才派发 `click`。
282
459
  ### 3. 输入配对码后提示「已锁定」
283
460
 
284
461
  这是安全设计,不会自动解锁。
285
- 在 Mac 面板点「刷新配对码」,然后用新码重试。
462
+ 在电脑面板点「刷新配对码」,然后用新码重试。
286
463
 
287
464
  ### 4. 提示「请求未通过 Host 或来源校验」
288
465
 
289
- - 确认你访问的是 Mac 面板上显示的完整地址(包括实际端口,回环网关通常是 `:3081`);
466
+ - 确认你访问的是电脑面板上显示的完整地址(包括实际端口,回环网关通常是 `:3081`);
290
467
  - 不要用另一个域名 / IP 混着访问;
291
468
  - 升级到 `1.1.0` 及以上版本。`1.1.0` 同时修复早期 iOS Safari 表单提交被误判的问题,并加入回环网关。
292
469
 
@@ -300,13 +477,14 @@ iOS Safari 会把第一次触摸先用去聚焦,第二次才派发 `click`。
300
477
 
301
478
  - 插件在 iOS 的普通 HTTP 页面上提供剪贴板回退;
302
479
  - 如果仍然失败,长按文字手动复制;
303
- - Mac 不受这个回退影响。
480
+ - 电脑端不受这个回退影响。
304
481
 
305
- ### 7. 安卓 / Windows 浏览器能用吗
482
+ ### 7. 安卓手机 / Windows 电脑上的浏览器能用吗
306
483
 
307
- - 配对页对现代浏览器通用;
484
+ - 配对页 `/m` 对现代浏览器通用;
308
485
  - 左侧栏面板在所有 DSH Web 客户端显示;
309
- - 剪贴板和 Return 换行的全局回退只在 iOS / iPadOS 上安装。
486
+ - iOS 专属补丁(Return 换行、防自动放大、侧栏手势)只在 iPhone / iPad 上安装,安卓和 Windows 浏览器不会启用这些补丁;
487
+ - 在 Windows 电脑本机浏览器里打开 DSH,与在 Mac 本机浏览器里打开一样,原有行为不变。
310
488
 
311
489
  ---
312
490
 
@@ -314,13 +492,13 @@ iOS Safari 会把第一次触摸先用去聚焦,第二次才派发 `click`。
314
492
 
315
493
  ```text
316
494
  ┌─────────────────────────────┐
317
- │ Mac 上的 DSH Web │
495
+ │ 电脑上的 DSH Web │
318
496
  │ │
319
497
  │ host 插件 │
320
498
  │ ├─ GET /m │ 配对页:短地址 + 8 位码
321
499
  │ ├─ POST /m │ 校验 nonce/码,签发 Cookie
322
- │ ├─ GET /api/.../state │ 仅 Mac 面板可读
323
- │ └─ POST /api/.../rotate │ 仅 Mac 面板可刷新
500
+ │ ├─ GET /api/.../state │ 仅电脑面板可读
501
+ │ └─ POST /api/.../rotate │ 仅电脑面板可刷新
324
502
  │ │
325
503
  │ client 插件 │
326
504
  │ └─ 左侧栏「手机访问」 │
@@ -383,7 +561,7 @@ iPhone 软键盘没有可用的 Shift 键,因此插件在 **iOS / iPadOS** 上
383
561
  ### 推荐:npm 官方命令
384
562
 
385
563
  ```bash
386
- dsh plugin --profile web add dsh-mobile-access@1.1.6 --registry=https://registry.npmjs.org/
564
+ dsh plugin --profile web add dsh-mobile-access@1.1.8 --registry=https://registry.npmjs.org/
387
565
  ```
388
566
 
389
567
  ### 使用 `@latest`
@@ -513,7 +691,9 @@ dsh plugin --profile web remove dsh-mobile-access
513
691
  - `1.1.0` — 回环监听自动局域网网关(macOS / Windows,HTTP + `/api/remote.mux` WebSocket)。
514
692
  - `1.1.1` — iPhone / iPad 上模型选择页 16px 缩放守卫。
515
693
  - `1.1.2` — 打开模型选择时不自动弹键盘、不整体上移。
516
- - `1.1.6` — iPhone / iPad 上右滑展开、左滑收起左侧栏;当前 npm `latest`。
694
+ - `1.1.8` — iPhone / iPad 上切换会话后不再自动弹出输入法键盘;当前 npm `latest`。
695
+ - `1.1.7` — iPhone / iPad 上右滑展开左侧栏,点击内容区收起。
696
+ - `1.1.6` — iPhone / iPad 上右滑展开、左滑收起左侧栏。
517
697
  - `1.1.5` — Tailscale Serve 经 HTTPS 访问时,允许带 Tailscale 身份头的 `*.ts.net` Host(仅限本机回环转发)。
518
698
  - `1.1.4` — iPhone / iPad 上第一次点击直接切换会话,并在切换后自动收起左侧栏。
519
699
  - `1.1.3` — 首次尝试临时移除会话行 `tabindex`。
package/client.js CHANGED
@@ -202,7 +202,39 @@ function installIOSSidebarTapGuard(layout) {
202
202
  let pending = null;
203
203
  let syntheticClick = false;
204
204
  let suppressNativeClick = false;
205
+ let suppressComposerFocusUntil = 0;
206
+ let allowComposerFocus = false;
207
+ let composerFocusTimer;
205
208
  const isAction = (target) => target.closest('button, a, input, textarea, select, [role="button"], [role="menuitem"], [role="menuitemradio"], [role="switch"], [data-row-actions]') !== null;
209
+ const isComposer = (target) => {
210
+ const element = target;
211
+ return element !== null
212
+ && typeof element.closest === 'function'
213
+ && element.closest('[data-composer-input], [data-composer-card] [contenteditable="true"]') !== null;
214
+ };
215
+ const blurActive = () => {
216
+ const active = doc.activeElement;
217
+ try {
218
+ active?.blur?.();
219
+ }
220
+ catch {
221
+ }
222
+ };
223
+ const onFocusIn = (event) => {
224
+ if (Date.now() >= suppressComposerFocusUntil)
225
+ return;
226
+ if (!isComposer(event.target))
227
+ return;
228
+ if (allowComposerFocus) {
229
+ allowComposerFocus = false;
230
+ return;
231
+ }
232
+ try {
233
+ event.target.blur();
234
+ }
235
+ catch {
236
+ }
237
+ };
206
238
  const point = (event) => {
207
239
  const touches = event.changedTouches ?? event.touches;
208
240
  const touch = touches?.[0];
@@ -212,6 +244,15 @@ function installIOSSidebarTapGuard(layout) {
212
244
  const target = event.target;
213
245
  if (target === null || typeof target.closest !== 'function')
214
246
  return;
247
+ if (isComposer(target)) {
248
+ allowComposerFocus = true;
249
+ if (composerFocusTimer !== undefined)
250
+ window.clearTimeout(composerFocusTimer);
251
+ composerFocusTimer = window.setTimeout(() => {
252
+ composerFocusTimer = undefined;
253
+ allowComposerFocus = false;
254
+ }, 500);
255
+ }
215
256
  if (isAction(target)) {
216
257
  pending = null;
217
258
  return;
@@ -261,6 +302,9 @@ function installIOSSidebarTapGuard(layout) {
261
302
  return;
262
303
  if (Math.hypot(at.x - x, at.y - y) > 10 || Date.now() - startedAt > 600)
263
304
  return;
305
+ suppressComposerFocusUntil = Date.now() + 800;
306
+ allowComposerFocus = false;
307
+ blurActive();
264
308
  syntheticClick = true;
265
309
  row.click();
266
310
  const timer = window.setTimeout(() => {
@@ -282,22 +326,28 @@ function installIOSSidebarTapGuard(layout) {
282
326
  doc.addEventListener('touchstart', onTouchStart, true);
283
327
  doc.addEventListener('touchend', onTouchEnd, true);
284
328
  doc.addEventListener('click', onClickCapture, true);
329
+ doc.addEventListener('focusin', onFocusIn, true);
285
330
  return () => {
286
331
  doc.removeEventListener('touchstart', onTouchStart, true);
287
332
  doc.removeEventListener('touchend', onTouchEnd, true);
288
333
  doc.removeEventListener('click', onClickCapture, true);
334
+ doc.removeEventListener('focusin', onFocusIn, true);
335
+ if (composerFocusTimer !== undefined)
336
+ window.clearTimeout(composerFocusTimer);
289
337
  for (const timer of timers)
290
338
  window.clearTimeout(timer);
291
339
  timers.clear();
292
340
  };
293
341
  }
294
342
  /**
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.
343
+ * Right swipe on iPhone opens the left sidebar. The closing gesture is a tap
344
+ * on the content area (not a left swipe): a normal tap is easier to discover
345
+ * and does not compete with horizontal scrolling. The swipe is scoped to the
346
+ * content area and ignores vertical movement, editable controls, menus, code
347
+ * blocks and the right panel. On a normal Safari tab we stay 24px away from the
348
+ * screen edges so the system back/forward gesture keeps priority; an installed
349
+ * PWA has no such gesture, so edge starts are allowed there. Mac never installs
350
+ * this listener.
301
351
  */
302
352
  function installIOSSidebarSwipeGuard(layout) {
303
353
  if (!isAppleMobile() || layout === undefined)
@@ -341,7 +391,7 @@ function installIOSSidebarSwipeGuard(layout) {
341
391
  if (Math.abs(dx) < 70 || Math.abs(dx) <= Math.abs(dy) * 1.5)
342
392
  return;
343
393
  const collapsed = doc.querySelector('[data-sidebar-collapsed]') !== null;
344
- if (dx > 0 ? !collapsed : collapsed)
394
+ if (!collapsed)
345
395
  return;
346
396
  try {
347
397
  layout.toggleSidebar();
@@ -356,6 +406,41 @@ function installIOSSidebarSwipeGuard(layout) {
356
406
  doc.removeEventListener('touchend', onTouchEnd, true);
357
407
  };
358
408
  }
409
+ /**
410
+ * A tap on the conversation content closes an expanded narrow sidebar. It has
411
+ * to run in the bubble phase so message links and buttons still receive their
412
+ * click first, and it skips every interactive/row/right-panel target. The
413
+ * sidebar width comes from the frame's inline grid template, so a tap on the
414
+ * left column is not mistaken for content. Mac and wide layouts are untouched.
415
+ */
416
+ function installIOSContentTapCollapse(layout) {
417
+ if (!isAppleMobile() || layout === undefined)
418
+ return () => undefined;
419
+ const doc = document;
420
+ const interactive = 'button, a, input, textarea, select, pre, code, [contenteditable="true"], [data-composer-card], [data-row-key], [role="treeitem"], [role="tree"], nav, [role="menu"], [role="listbox"], [role="button"], [role="menuitem"], [data-rightbar]';
421
+ const onClick = (event) => {
422
+ if (window.innerWidth >= 1024)
423
+ return;
424
+ if (doc.querySelector('[data-sidebar-collapsed]') !== null)
425
+ return;
426
+ const target = event.target;
427
+ if (target === null || typeof target.closest !== 'function' || target.closest(interactive) !== null)
428
+ return;
429
+ const frame = doc.querySelector('[style*="grid-template-columns"]');
430
+ const first = frame?.style.gridTemplateColumns.split(' ')[0] ?? '';
431
+ const width = Number.parseFloat(first);
432
+ const x = event.clientX;
433
+ if (Number.isFinite(width) && x < width)
434
+ return;
435
+ try {
436
+ layout.toggleSidebar();
437
+ }
438
+ catch {
439
+ }
440
+ };
441
+ doc.addEventListener('click', onClick, true);
442
+ return () => doc.removeEventListener('click', onClick, true);
443
+ }
359
444
  /**
360
445
  * The official model menu can move focus into its search field while it opens.
361
446
  * On iPhone that opens the software keyboard and shifts the fixed-position menu
@@ -708,6 +793,12 @@ function factory(require) {
708
793
  catch (error) {
709
794
  ctx.logger?.warn?.(`[mobile-access] iOS sidebar swipe guard unavailable: ${String(error)}`);
710
795
  }
796
+ try {
797
+ cleanups.push(installIOSContentTapCollapse(ctx.layout));
798
+ }
799
+ catch (error) {
800
+ ctx.logger?.warn?.(`[mobile-access] iOS content tap collapse unavailable: ${String(error)}`);
801
+ }
711
802
  return () => {
712
803
  for (const cleanup of cleanups.reverse()) {
713
804
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mobile-access",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
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",
@@ -41,7 +41,7 @@
41
41
  "scripts": {
42
42
  "build": "node scripts/build.mjs",
43
43
  "typecheck": "node scripts/build.mjs --check",
44
- "test": "node scripts/build.mjs && node --test tests/*.test.mjs",
44
+ "test": "node scripts/build.mjs && node scripts/test.mjs",
45
45
  "verify": "bash scripts/verify.sh"
46
46
  },
47
47
  "dsh": {