dsh-mobile-access 1.0.2 → 1.1.0

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
@@ -1,206 +1,470 @@
1
1
  # 手机访问(dsh-mobile-access)
2
2
 
3
- 一个独立的 TypeScript DSH bundle:让同一局域网内的 iPhone / iPad 用自带 Safari 安全打开 Mac 上的官方 DeepSeek Harness Web,并在左侧栏提供一个「手机访问」入口。
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
- 它只增加三个精确路由和一个客户端面板,不修改 DSH 核心、不改认证、不替换页面根节点、不注册 Service Worker,也不拦截现有请求,因此 Mac 上原有的 DSH Web 使用方式保持不变。
5
+ > 用 iPhone / iPad 自带的 Safari,安全打开 Mac 上的 DeepSeek Harness Web。
6
+ >
7
+ > **不用装 App,不用改 DSH 源码,不影响 Mac 上原来怎么用。**
6
8
 
7
- ## 解决什么问题
9
+ 这是一个独立的 TypeScript DSH 插件(bundle)。它只在 DSH 里增加三样东西:
8
10
 
9
- DSH Web 在局域网上打开裸地址时会返回 401:
11
+ 1. 左侧栏一个「手机访问」入口;
12
+ 2. 一个给手机用的短地址和 8 位配对码;
13
+ 3. 一个只在 iPhone/iPad 上生效的「回车换行」小补丁。
14
+
15
+ 其他东西都不动:不改 DSH 核心、不改登录方式、不替换页面、不装后台服务、不拦截现有请求。
16
+
17
+ ---
18
+
19
+ ## 先看结论:它到底帮你做了什么
20
+
21
+ 假设你的 Mac 上正在运行 DSH Web,地址是:
22
+
23
+ ```text
24
+ http://192.168.1.23:3080
25
+ ```
26
+
27
+ 以前 iPhone 直接打开这个地址,会看到:
10
28
 
11
29
  ```text
12
30
  dsh web authentication required; reopen the URL printed by dsh web.
13
31
  ```
14
32
 
15
- 这是正常的认证保护,不是网络故障。手机无法输入终端里那条带 `?token=` 的长链接,本插件用「8 位配对码」完成一次安全配对:
33
+ 这不是网络坏了,而是 DSH 的安全保护:它只认 Mac 终端里打印出来的那条**带很长 token 的登录链接**。这条链接太长,手机不可能手输。
16
34
 
17
- 1. Mac 的 DSH 左侧栏打开「手机访问」。
18
- 2. iPhone Safari 打开面板显示的短地址,例如 `http://192.168.71.37:3080/m`。
19
- 3. 输入 Mac 面板上的 8 位数字。
20
- 4. 服务器在响应中直接签发 HttpOnly 登录 Cookie,然后跳转到 `/` 进入完整 DSH Web。
35
+ 这个插件做的事,就是把这个过程变成:
21
36
 
22
- **启动令牌不会出现在手机地址栏、浏览器历史或 Referer 中。** 配对成功后,手机获得与 Mac 浏览器相同的登录 Cookie 生命周期(默认 30 天)。
37
+ ```text
38
+ Mac 面板显示 8 位配对码
39
+ ↓
40
+ iPhone 打开短地址 /m
41
+ ↓
42
+ 输入 8 位数字
43
+ ↓
44
+ 自动登录,进入完整 DSH Web
45
+ ```
23
46
 
24
- ## 前置条件
47
+ 用生活里的话说:
25
48
 
26
- - Mac 与 iPhone 在同一局域网,且路由器未开启客户端隔离(AP isolation)。
27
- - DSH Web 监听的是具体局域网地址,而不是 `127.0.0.1`。
28
- - 本机当前配置在 `/Users/open/.dsh/profiles/web/cordis.patch.yml` 的 `webserver` 行:`host: 192.168.71.37`、`port: 3080`。
29
- - 手机访问 `http://<Mac局域网IP>:3080/m`;若使用 HTTPS,必须是手机信任的证书,否则 Safari 会先拦截。
49
+ - DSH 是一栋上锁的房子;
50
+ - 原来的长 token 是一把很长的钥匙,手机没法输;
51
+ - 插件在门口装了一个「临时门禁」;
52
+ - Mac 上的人看到 8 位门禁码,告诉手机;
53
+ - 手机输对后,屋里直接给它戴上 30 天有效的「手环」(HttpOnly 登录 Cookie)。
30
54
 
31
- ## 安装
55
+ **长钥匙不会出现在手机地址栏、浏览器历史或网页来源信息里。**
32
56
 
33
- 推荐使用官方 DSH CLI 一次安装。已发布到 npm;下面是**可复现**的精确版本命令:
57
+ ---
34
58
 
35
- ```bash
36
- dsh plugin --profile web add dsh-mobile-access@1.0.2
37
- ```
59
+ ## 适合谁
60
+
61
+ - Mac 和 iPhone / iPad 在**同一个 Wi-Fi / 局域网**;
62
+ - Mac 上运行的是官方 DeepSeek Harness Web;
63
+ - 希望手机用 Safari 完整使用 DSH,而不是只看一个 401 错误页。
64
+
65
+ ## 不适合谁
66
+
67
+ - 想在**外网**(不在同一个 Wi-Fi)访问家里的 Mac:这不是 VPN / 内网穿透工具,请不要把 DSH 直接暴露到公网。
68
+ - Mac 的 DSH 只监听 `127.0.0.1`:`1.1.0` 起插件会**自动**在 `3081` 起一个只做转发的局域网网关,Mac 仍用 `127.0.0.1`,不需要改 Host 配置,也不需要把 DSH 暴露到公网。
69
+ - 使用 HTTPS 但证书不被 iPhone 信任:Safari 会先拦截证书,这和插件无关。
70
+
71
+ ---
38
72
 
39
- 也可以使用 `@latest`,但 pnpm 的元数据缓存/镜像可能暂时仍解析到旧版;需要最新版时优先使用上面的精确版本,或显式指定官方 registry:
73
+ ## 安装(Mac 上操作,复制即可)
74
+
75
+ ### 第 1 步:确认 Mac 和 iPhone 连的是同一个 Wi-Fi
76
+
77
+ iPhone 打开「设置 → 无线局域网」,确认和 Mac 连的是同一个网络。
78
+
79
+ ### 第 2 步:在 Mac 终端执行一条命令
40
80
 
41
81
  ```bash
42
- dsh plugin --profile web add dsh-mobile-access@latest --registry=https://registry.npmjs.org/
82
+ dsh plugin --profile web add dsh-mobile-access@1.1.0 --registry=https://registry.npmjs.org/
43
83
  ```
44
84
 
45
- 本机 2026-10-24 的实测结果:默认 registry 下 `@latest` 仍解析到 `1.0.0`,精确版本可正确安装;指定 npmjs 后 `@latest` 也解析到最新版。`1.0.0` 已包含 Safari 配对修复;`1.0.1` 只增加安装文档和发布元数据;`1.0.2` 增加 iPhone 自带键盘换行。
85
+ 这条命令做三件事:
46
86
 
47
- `dsh plugin` 会在 profile 目录执行 pnpm 安装,并自动把声明了 `dsh.bundle.patch` 的包追加到 `dsh.profile.bundles`;不需要手工编辑 `cordis.patch.yml` 或 bundle 列表。
87
+ 1. 从 npm 下载插件;
88
+ 2. 装进当前 DSH 的 `web` profile;
89
+ 3. 自动把它加入 DSH 的插件列表。
48
90
 
49
- 从本目录安装(开发机、未发布版本或离线环境):
91
+ > `--profile web` 里的 `web` 是本项目使用的 profile 名称。
92
+ > 如果你的 DSH 用的是其他 profile,把 `web` 换成实际名称即可。
93
+ > 不确定时,照抄这条命令;如果提示 profile 不存在,再检查你平时启动 DSH 用的名称。
50
94
 
51
- ```bash
52
- cd /Users/open/Documents/Stocks/plugins/dsh-mobile-access
53
- npm run verify
54
- dsh plugin --profile web add /Users/open/Documents/Stocks/plugins/dsh-mobile-access
95
+ ### 如果 DSH 只监听 `127.0.0.1`
96
+
97
+ 从 `1.1.0` 开始不需要改 DSH 配置。插件检测到回环监听后,会自动在
98
+ `3081` 起一个只做转发的局域网网关:
99
+
100
+ - Mac 继续使用原来的 `http://127.0.0.1:3080`,行为不变;
101
+ - iPhone 使用面板上显示的 `http://<Mac 局域网 IP>:3081/m`;
102
+ - 网关会同时转发 DSH 的 `/api/remote.mux` WebSocket,聊天流不受影响;
103
+ - 如果 `3081` 被占用,会自动尝试后 10 个端口,面板会显示实际地址。
104
+
105
+ **Windows 10 / 11 注意**:第一次启动时 Windows 防火墙可能询问是否允许
106
+ Node.js 访问网络,请勾选“专用网络”并允许;如果误点了拒绝,到“Windows
107
+ 安全中心 → 防火墙和网络保护 → 允许应用通过防火墙”里放行 Node.js。
108
+
109
+ ### 第 3 步:刷新 DSH 页面
110
+
111
+ 在 Mac 的 DSH 页面按:
112
+
113
+ ```text
114
+ Command + R
55
115
  ```
56
116
 
57
- 也可以使用 `npm pack` 生成 tgz 后安装:
117
+ 刷新后,左侧栏应该出现一个**手机图标**,名字叫「手机访问」。
118
+
119
+ 正常情况下**不需要重启**。
120
+ 如果插件管理器明确提示“需要重启”,请手动重启 DSH;不要在不清楚状态时反复重装。
121
+
122
+ ### 可选:确认装好了
58
123
 
59
124
  ```bash
60
- npm pack
61
- dsh plugin --profile web add /absolute/path/to/dsh-mobile-access-1.0.2.tgz
125
+ dsh plugin --profile web why dsh-mobile-access
62
126
  ```
63
127
 
64
- 如果 DSH 正在运行,安装后的 host bundle 可能需要重启才能加载;插件管理器提示 `restart-required` 时,**不要自行重启**,按项目约定通知用户手工重启 DSH,用户确认「已重启」后再继续。已有环境也可以继续使用 DSH 插件管理入口的 `install_bundle`,目标为本目录的绝对路径;两种方式等价。
128
+ 能看到 `dsh-mobile-access` 就说明依赖已经装上。
65
129
 
66
- ## 使用
130
+ ---
131
+
132
+ ## 配对(Mac + iPhone 一起操作)
67
133
 
68
134
  ### Mac 端
69
135
 
70
- 1. 打开 DSH Web,左侧栏应出现手机图标「手机访问」。
71
- 2. 面板会显示:
72
- - 配对地址,例如 `http://192.168.71.37:3080/m`
136
+ 1. 打开 DSH Web;
137
+ 2. 点左侧栏的「手机访问」;
138
+ 3. 面板会显示:
139
+ - 配对地址,例如 `http://192.168.1.23:3080/m`
73
140
  - 8 位配对码,例如 `1234 5678`
74
- - 剩余时间、最近配对时间、成功配对次数
141
+ - 剩余有效时间
75
142
  - 「复制链接」「复制配对码」「刷新配对码」
76
- - 手机兼容性自检
77
- 3. 如果面板提示「当前绑定在回环地址」,说明 DSH 只监听了 `127.0.0.1`,手机无法连接;需要把 `webserver.host` 改为 Mac 的局域网 IP,再由用户手工重启 DSH。
78
143
 
79
144
  ### iPhone 端
80
145
 
81
- 1. 确认 iPhone 与 Mac 在同一 Wi-Fi。
82
- 2. 打开 Safari,访问 Mac 面板上显示的 `/m` 地址。
83
- 3. 输入 8 位配对码,点「连接 DSH」。
84
- 4. 成功后进入完整 DSH Web。建议:
85
- - 点 Safari 分享按钮 → 「添加到主屏幕」。
86
- - 以后从主屏幕图标进入;如果登录过期,重新打开一次 `/m` 配对即可。
87
- 5. **iPhone 自带键盘换行**:在输入框内按 Return 是换行,不会直接发送;发送请点输入框右侧的发送按钮。Mac 上保持 Enter 发送、Shift+Enter 换行;外接键盘可用 Cmd+Enter 发送。
88
- 6. 若面板显示 `window.isSecureContext === false`,这是 HTTP 局域网的正常现象;本插件只对 iOS/iPadOS 安装一个窄范围的剪贴板回退,其他平台不会改动全局对象。
146
+ 1. 打开 Safari;
147
+ 2. 输入 Mac 面板上显示的 `/m` 地址(不要输成 `127.0.0.1`,也不要用 `localhost`);
148
+ 3. 输入 8 位配对码;
149
+ 4. 点「连接 DSH」;
150
+ 5. 成功后会直接进入完整 DSH Web。
89
151
 
90
- ## 路由与权限
152
+ 建议再做一个动作:
91
153
 
92
- | 方法 | 路径 | 权限 | 用途 |
93
- |---|---|---|---|
94
- | GET | `/m` | 公开(Host 固定 + 一次性 nonce) | 显示配对页;已有有效 v1 页面 Cookie 时跳转 `/` |
95
- | POST | `/m` | 公开(Host 固定 + 一次性 nonce) | 校验 nonce 与配对码,签发 v1 HttpOnly Cookie |
96
- | GET | `/api/mobile-access/state` | 仅 owner | 面板读取状态、配对码、绑定地址诊断 |
97
- | POST | `/api/mobile-access/rotate` | 仅 owner | 刷新配对码并清空失败计数 |
154
+ ```text
155
+ Safari 分享按钮 → 添加到主屏幕
156
+ ```
98
157
 
99
- 所有路由都是 `kind: 'exact'`。没有前缀路由,没有 fallback 路由,不接管 SPA 的 index。
158
+ 以后从主屏幕图标进入,体验更接近 App。
100
159
 
101
- ## 安全设计
160
+ ---
102
161
 
103
- - 8 位数字配对码,使用 `crypto.getRandomValues` 生成,服务端使用常量时间比较。
104
- - 默认 10 分钟过期;每次刷新配对码都会清空失败计数。
105
- - 单 IP 5 次失败锁定;全局 20 次失败锁定;单 IP 每分钟最多 10 次尝试。
106
- - 配对成功由服务端调用 `authorizeIndex` 签发 Cookie,**不把启动令牌放进 303 的 Location**。
107
- - 配对 POST 校验 Host(固定为监听地址)、一次性 nonce、`Content-Type`,请求体上限 2048 字节。
108
- - 配对页带 CSP、`X-Frame-Options: DENY`、`Referrer-Policy: no-referrer`、`Cache-Control: no-store`。
109
- - 状态与刷新接口走 Connection 的 owner-only Fetch 路由,不是公开接口。
110
- - 配对码和失败计数只在内存中,不写磁盘、不写日志。
162
+ ## 日常使用
111
163
 
112
- 更完整的威胁模型见 `docs/security.md`。
164
+ ### iPhone 上怎么发消息
113
165
 
114
- ## 对 Mac 零影响的边界
166
+ - 在输入框里按 iPhone 自带键盘的 **Return**:**换行**,不会直接发送;
167
+ - 要发送时,点输入框右侧的**发送按钮**;
168
+ - 这样可以正常输入多行提示词。
115
169
 
116
- - 不修改 DSH 核心源码、profile 配置、认证密钥或 Cookie。
117
- - 不注册全局 CSS、字体、主题或布局补丁。
118
- - 不替换 `#root`,不挂载第二个应用。
119
- - 不注册 Service Worker,不缓存或改写现有请求。
120
- - 不启动后台轮询;面板只在挂载、窗口聚焦、页面可见时读取一次状态。
121
- - 全局剪贴板回退只在 **iOS/iPadOS 且非安全上下文** 时安装,并在插件销毁时恢复原 descriptor。
122
- - Return 换行监听只在 **iOS/iPadOS** 上安装,只作用于带有 `data-composer-input` 的官方输入框;Mac 和非 Apple 平台不会安装该监听。
123
- - 插件注册失败只影响本插件自身;所有路由、slot、监听器和定时器都有清理路径。
170
+ ### Mac 上有没有变化
124
171
 
125
- ## 开发与验证
172
+ 没有。
126
173
 
127
- ```bash
128
- cd /Users/open/Documents/Stocks/plugins/dsh-mobile-access
174
+ - Mac 上仍然是:**Enter 发送,Shift+Enter 换行**;
175
+ - 插件只在 iPhone / iPad 上安装「Return 换行」监听,Mac 和非 Apple 平台不会安装;
176
+ - 左边栏只多一个「手机访问」入口,其他入口和面板保持原样。
129
177
 
130
- # 只做类型检查,不写文件
131
- node scripts/build.mjs --check
178
+ ### 配对码过期了怎么办
132
179
 
133
- # 构建根目录运行产物
134
- node scripts/build.mjs
180
+ 配对码默认 **10 分钟**有效。过期后:
135
181
 
136
- # 单元测试:配对服务 + HTTP 路由
137
- node --test tests/*.test.mjs
182
+ 1. 回到 Mac 的「手机访问」面板;
183
+ 2. 点「刷新配对码」;
184
+ 3. iPhone 重新打开 `/m`,输入新码。
138
185
 
139
- # 完整门禁:类型检查、构建、测试、静态检查、npm pack 检查
140
- npm run verify
186
+ ### 登录过期了怎么办
187
+
188
+ 配对成功后会签发一个默认 **30 天**有效的 HttpOnly Cookie。
189
+ 如果过期,重新走一次 `/m` 配对即可,不需要重装插件。
190
+
191
+ ---
192
+
193
+ ## 安全吗
194
+
195
+ 这个插件把安全边界做在“局域网 + 短时配对码”上:
196
+
197
+ - 8 位数字配对码,使用系统安全随机数生成;
198
+ - 默认 10 分钟过期;
199
+ - 单个 IP 连续失败 5 次锁定;
200
+ - 全局失败 20 次锁定;
201
+ - 单个 IP 每分钟最多尝试 10 次;
202
+ - 配对页每次刷新都会换一个一次性 nonce,并校验 Host;
203
+ - 配对码和失败次数只放在内存里,不写磁盘、不写日志;
204
+ - 登录 Cookie 是 HttpOnly,手机网页脚本读不到;
205
+ - 启动 token **不会**出现在手机的地址栏、历史记录或 Referer 里。
206
+
207
+ 需要注意:
208
+
209
+ - 配对码就相当于临时门禁码,**不要截图发到公开群**;
210
+ - 它只适合可信的局域网;
211
+ - 不要把 DSH 的端口直接映射到公网。
212
+
213
+ 更完整的安全说明见 [`docs/security.md`](docs/security.md)。
214
+
215
+ ---
216
+
217
+ ## 常见问题(小白版)
218
+
219
+ ### 1. 打开 `/m` 还是 401 或打不开
220
+
221
+ 按顺序检查:
222
+
223
+ 1. iPhone 和 Mac 是不是同一个 Wi-Fi;
224
+ 2. 地址是不是 Mac 面板上显示的**完整地址**(回环监听时通常是 `:3081`,局域网监听时可能是 `:3080`);
225
+ 3. Mac 面板的「局域网网关」是否显示「已开启」;如果显示错误,按提示检查 Windows 防火墙或端口占用;
226
+ 4. 路由器是否开启了「客户端隔离 / AP 隔离」(开启后同一 Wi-Fi 的设备也不能互访)。
227
+
228
+ ### 2. 左边栏没有「手机访问」
229
+
230
+ - 先刷新 DSH 页面(`Command + R`);
231
+ - 再确认安装命令没有报错;
232
+ - 检查 `dsh plugin --profile web why dsh-mobile-access`;
233
+ - 如果刚安装完仍没有,按插件管理器提示判断是否需要手动重启 DSH。
234
+
235
+ ### 3. 输入配对码后提示「已锁定」
236
+
237
+ 这是安全设计,不会自动解锁。
238
+ 在 Mac 面板点「刷新配对码」,然后用新码重试。
239
+
240
+ ### 4. 提示「请求未通过 Host 或来源校验」
241
+
242
+ - 确认你访问的是 Mac 面板上显示的完整地址(包括实际端口,回环网关通常是 `:3081`);
243
+ - 不要用另一个域名 / IP 混着访问;
244
+ - 升级到 `1.1.0` 及以上版本。`1.1.0` 同时修复早期 iOS Safari 表单提交被误判的问题,并加入回环网关。
245
+
246
+ ### 5. iPhone 按 Return 还是直接发送
247
+
248
+ - 确认版本是 `1.1.0` 或更高;
249
+ - 安装/升级后刷新一次页面,让新的 `client.js` 生效;
250
+ - 如果仍然直接发送,请记录 iPhone 型号、iOS 版本和页面地址,提交 issue。
251
+
252
+ ### 6. 复制按钮没反应
253
+
254
+ - 插件在 iOS 的普通 HTTP 页面上提供剪贴板回退;
255
+ - 如果仍然失败,长按文字手动复制;
256
+ - Mac 不受这个回退影响。
257
+
258
+ ### 7. 安卓 / Windows 浏览器能用吗
259
+
260
+ - 配对页对现代浏览器通用;
261
+ - 左侧栏面板在所有 DSH Web 客户端显示;
262
+ - 剪贴板和 Return 换行的全局回退只在 iOS / iPadOS 上安装。
263
+
264
+ ---
265
+
266
+ ## 它是怎么工作的(技术小白可跳过)
267
+
268
+ ```text
269
+ ┌─────────────────────────────┐
270
+ │ Mac 上的 DSH Web │
271
+ │ │
272
+ │ host 插件 │
273
+ │ ├─ GET /m │ 配对页:短地址 + 8 位码
274
+ │ ├─ POST /m │ 校验 nonce/码,签发 Cookie
275
+ │ ├─ GET /api/.../state │ 仅 Mac 面板可读
276
+ │ └─ POST /api/.../rotate │ 仅 Mac 面板可刷新
277
+ │ │
278
+ │ client 插件 │
279
+ │ └─ 左侧栏「手机访问」 │
280
+ └─────────────────────────────┘
281
+ │
282
+ │ 同一局域网
283
+ ▼
284
+ ┌─────────────────────────────┐
285
+ │ iPhone Safari │
286
+ │ 打开 /m → 输入 8 位码 │
287
+ │ → 收到 HttpOnly Cookie │
288
+ │ → 进入完整 DSH Web │
289
+ └─────────────────────────────┘
141
290
  ```
142
291
 
143
- TypeScript 编译器默认使用当前环境的
144
- `/Users/open/deepseek-harness/node_modules/.bin/tsc`;也可通过 `DSH_TSC` 指定其他 `tsc`。
292
+ 技术边界:
145
293
 
146
- ### 产物结构
294
+ - 三个路由都是 `kind: 'exact'`,没有前缀路由,没有 fallback 路由;
295
+ - 不接管 DSH 的 SPA index;
296
+ - 不注册 Service Worker;
297
+ - 不修改 DSH 核心源码、认证密钥或 profile 配置;
298
+ - 所有路由、slot、监听器都有清理路径;
299
+ - 插件注册失败只影响插件自身,不会替换 DSH 页面根节点。
300
+
301
+ ### 认证细节
302
+
303
+ 配对成功后,host 调用 DSH 自己的 `authenticatedUrl()` 取得启动 token,再在**服务端**用合成的 `GET /` 请求调用 `authorizeIndex()`,把返回的 303 和 `Set-Cookie` 重放给手机浏览器。
304
+ 浏览器只会看到 `Location: /`,不会看到 `?token=`。
305
+
306
+ 这条路径避开了早期 iOS 27 Safari 表单 POST 被 Connection Host/Origin 校验误判为 403 的问题;`/m` 改用:
307
+
308
+ - Host 固定;
309
+ - 一次性 nonce;
310
+ - `Content-Type` 校验;
311
+ - 2048 字节请求体上限。
312
+
313
+ 同时保留 DSH 自己的 v1 页面 Cookie 签发逻辑,不复制、不重写认证算法。
314
+
315
+ ### iPhone Return 换行细节
316
+
317
+ 官方输入框的规则是:
147
318
 
148
319
  ```text
149
- index.js host 入口(ESM)
150
- service.js 配对状态机
151
- routes.js /m、/api/mobile-access/state、/api/mobile-access/rotate
152
- html.js 无依赖配对页
153
- types.js 结构性类型(编译后为空导出)
154
- client.js 浏览器端 bundle(单文件、无 import/export)
155
- cordis.patch.yml
156
- locale/*.json meta 与 ui 文案
157
- icon.svg
320
+ Enter → 发送
321
+ Shift+Enter → 换行
158
322
  ```
159
323
 
160
- `client.js` 由 `scripts/build.mjs` 把 `client-runtime.js` 与 `client.js` 两段拼接,并注入 `locale/*.json` 的 `ui` 字典和 `src/panel.css`,没有运行时依赖。
324
+ iPhone 软键盘没有可用的 Shift 键,因此插件在 **iOS / iPadOS** 上监听官方输入框的 `keydown`:
325
+
326
+ - 只处理带 `data-composer-input` 的输入框;
327
+ - 跳过 Shift/Alt/Ctrl/Meta、输入法组合、打开的菜单;
328
+ - 把普通 Return 当作 Shift+Enter 处理,让 Lexical 走浏览器原生换行;
329
+ - 如果浏览器不允许改写键盘事件,则回退到 `execCommand('insertLineBreak')`;
330
+ - Mac 和非 Apple 平台完全不安装这个监听。
331
+
332
+ ---
333
+
334
+ ## 安装方式汇总(给不同场景)
161
335
 
162
- ## 常见问题
336
+ ### 推荐:npm 官方命令
163
337
 
164
- ### 手机打开 `/m` 一直是配对页,配对成功后仍回配对页
338
+ ```bash
339
+ dsh plugin --profile web add dsh-mobile-access@1.1.0 --registry=https://registry.npmjs.org/
340
+ ```
341
+
342
+ ### 使用 `@latest`
343
+
344
+ ```bash
345
+ dsh plugin --profile web add dsh-mobile-access@latest --registry=https://registry.npmjs.org/
346
+ ```
165
347
 
166
- - 检查 iPhone 是否允许 Cookie(Safari 设置 → 隐私与安全性 → 阻止所有 Cookie 必须关闭)。
167
- - 检查访问地址的 `Host` 是否与 Mac 面板上的地址完全一致(`192.168.71.37:3080` 与 `openmacbook-pro...` 是不同 authority,Cookie 不通用)。
168
- - 检查 DSH 是否在配对后被重启;启动令牌会随进程轮换,但已签发的 Cookie 仍然有效,除非签名密钥变化。
348
+ 说明:pnpm 的默认镜像可能比 npm 官方源晚同步。
349
+ 如果 `@latest` 装到旧版,请使用上面的精确版本,或加 `--registry=https://registry.npmjs.org/`。
169
350
 
170
- ### 配对码输入后提示「已锁定」
351
+ ### 从本机目录安装(开发 / 离线)
171
352
 
172
- 在 Mac 面板点「刷新配对码」。锁定是安全设计,不会自动解锁。
353
+ ```bash
354
+ git clone https://github.com/abbaababa/dsh-mobile-access.git
355
+ cd dsh-mobile-access
356
+ npm install
357
+ npm run verify
358
+ dsh plugin --profile web add /absolute/path/to/dsh-mobile-access
359
+ ```
173
360
 
174
- ### 面板提示回环地址
361
+ ### 从 tarball 安装
175
362
 
176
- DSH 只绑定了 `127.0.0.1`,iPhone 无法访问。把 `webserver.host` 改为局域网 IP 后,**由用户手工重启 DSH**,不要让 agent 自行重启。
363
+ ```bash
364
+ npm pack
365
+ dsh plugin --profile web add /absolute/path/to/dsh-mobile-access-1.1.0.tgz
366
+ ```
177
367
 
178
- ### 复制按钮在 iPhone 上无反应
368
+ > GitHub 仓库是源码与文档的家;日常安装请优先用 npm 一条命令,不要求用户自己编译。
179
369
 
180
- 本插件在 iOS 非安全上下文会安装剪贴板回退,并优先尝试 `navigator.clipboard`;若仍失败,请长按文本手动复制。Mac 不受此回退影响。
370
+ ---
181
371
 
182
- ### 是否支持 Android / 桌面浏览器
372
+ ## 开发与验证(技术读者)
183
373
 
184
- 配对页对现代浏览器通用;左侧栏面板在所有 DSH Web 客户端显示。剪贴板全局回退只在 iOS/iPadOS 生效。
374
+ 要求:
185
375
 
186
- ## 已知平台限制(不是插件缺陷)
376
+ - Node.js >= 22
377
+ - npm
378
+ - 不需要 DSH 源码 checkout
379
+
380
+ ```bash
381
+ git clone https://github.com/abbaababa/dsh-mobile-access.git
382
+ cd dsh-mobile-access
383
+ npm install
384
+ npm run verify
385
+ ```
386
+
387
+ `npm run verify` 会依次执行:
388
+
389
+ 1. TypeScript 类型检查;
390
+ 2. 构建 host / client 产物;
391
+ 3. Node 语法检查;
392
+ 4. 单元测试、路由测试、静态清单测试;
393
+ 5. 占位符残留扫描;
394
+ 6. `npm pack --dry-run` 检查发布内容。
395
+
396
+ 单独命令:
397
+
398
+ ```bash
399
+ npm run typecheck # 只检查,不写文件
400
+ npm run build # 构建根目录运行产物
401
+ npm test # 构建 + 单元测试
402
+ ```
403
+
404
+ TypeScript 编译器优先使用本目录的 `node_modules/.bin/tsc`;
405
+ 没有安装时,可用 `DSH_TSC` 指定:
406
+
407
+ ```bash
408
+ DSH_TSC=/path/to/tsc npm run verify
409
+ ```
410
+
411
+ CI 在 GitHub Actions 上使用 Node 22 执行同一套 `npm run verify`。
412
+
413
+ ### 产物结构
414
+
415
+ ```text
416
+ index.js host 入口(ESM)
417
+ service.js 配对状态机
418
+ routes.js /m、/api/mobile-access/state、/api/mobile-access/rotate
419
+ html.js 无依赖配对页
420
+ types.js 结构性类型(编译后为空导出)
421
+ client.js 浏览器端 bundle(单文件、无 import/export)
422
+ cordis.patch.yml
423
+ locale/*.json meta 与 ui 文案
424
+ icon.svg
425
+ ```
426
+
427
+ `client.js` 由 `scripts/build.mjs` 把 `client-runtime.js` 与 `client.js` 两段拼接,并注入 `locale/*.json` 的 `ui` 字典和 `src/panel.css`,没有运行时依赖。
428
+
429
+ ---
430
+
431
+ ## 平台限制(不是插件缺陷)
187
432
 
188
433
  - iOS Safari 不支持桌面式拖放(Drag and Drop);依赖该交互的桌面功能在 iPhone 上不可用。
189
434
  - iOS 会在后台挂起页面;长任务请保持 DSH 页面在前台。
190
435
  - Safari 的下载行为由 iOS 的「文件」App 接管,不能像 macOS 一样直接保存到任意目录。
191
436
  - 局域网 HTTP 不是安全上下文;依赖 `crypto.subtle`、Service Worker、Web Push 等安全上下文 API 的功能不可用。DSH 已对 UUID 和剪贴板做了回退,本插件再补一层 iOS 剪贴板回退。
437
+ - 本插件不提供公网访问、内网穿透、VPN 或端口映射。
438
+
439
+ ---
192
440
 
193
441
  ## 卸载
194
442
 
195
- 通过 DSH 插件管理器移除 `dsh-mobile-access`。移除后:
443
+ 通过 DSH 插件管理器移除 `dsh-mobile-access`:
444
+
445
+ ```bash
446
+ dsh plugin --profile web remove dsh-mobile-access
447
+ ```
448
+
449
+ 移除后:
196
450
 
197
451
  - 左侧栏入口消失;
198
- - `/m` 恢复为 SPA fallback(不再是配对页);
452
+ - `/m` 恢复为普通 SPA fallback(不再是配对页);
199
453
  - `/api/mobile-access/*` 不再注册;
200
- - iOS 剪贴板回退被恢复。
454
+ - iOS 剪贴板回退和 Return 换行监听被清理。
201
455
 
202
- 已签发的登录 Cookie 仍可能留在浏览器中,直到过期;这是 DSH 认证层的行为,不是插件写入的额外凭证。
456
+ 已签发的登录 Cookie 可能仍留在浏览器中,直到过期;这是 DSH 认证层的行为,不是插件额外写入的凭证。
457
+
458
+ ---
203
459
 
204
460
  ## 版本
205
461
 
206
462
  - `1.0.0` — 首个工程化版本:配对页、owner-only 状态/刷新、左侧栏面板、零 Mac 影响边界、单元/路由/静态门禁。
463
+ - `1.0.1` — 安装文档与 npm 发布元数据。
464
+ - `1.0.2` — iPhone / iPad 自带键盘 Return 换行。
465
+ - `1.0.3` — Safari 不支持改写键盘事件时的换行回退。
466
+ - `1.1.0` — 回环监听自动局域网网关(macOS / Windows,HTTP + `/api/remote.mux` WebSocket);当前 npm `latest`。
467
+
468
+ ## 许可证
469
+
470
+ [MIT](LICENSE)