@feiyang666/dsh-mobile-remote 1.7.1 → 1.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,128 @@
1
1
  # 更新日志 / Changelog
2
2
 
3
+ ## v1.9.0 (2026-10-24)
4
+
5
+ > 适配 **dsh 0.2.1-rc.3**。这一版重做了「对局域网开放」的实现方式——**不再改 webserver 绑定**。
6
+
7
+ ### 破坏性变更 / Breaking(面向 dsh 0.2.1+)
8
+
9
+ dsh 0.2.1-rc.3 起,`webserver.host` **只接受一块具体网卡的 IP 字面量**:
10
+
11
+ - `host: '0.0.0.0'`(旧版插件的做法)会在配置校验阶段抛错,进程 `code=1` 直接退出,所有下游插件都不激活;
12
+ - 即使改绑到 `192.168.x.x`,**本机 `127.0.0.1` 的监听也随之消失** —— 桌面端窗口固定连回环,
13
+ 会立刻白屏;官方桌面端还用 `--port 0`,patch 里重述 `port` 会让端口在重载后漂移,二维码/链接全部失效。
14
+
15
+ 因此 v1.9.0 **彻底放弃改绑定**,改为「旁挂 LAN 听众」。
16
+
17
+ ### 修复 / Fixed
18
+
19
+ - **开启远程后桌面端窗口打不开(本机 127.0.0.1 不再监听)**:不再写 `webserver` 覆盖。
20
+ - **官方桌面端开关点了没反应**:`patchFilePath()` 以前只认 `DSH_PROFILE` 环境变量,而官方桌面端
21
+ 启动 dsh 时**不设该变量**,于是插件把 patch 写进 `profiles/web`,实际运行的却是 `profiles/desktop`。
22
+ 现在优先取宿主 `profileContext.patchPath`(CLI / 官方桌面端 / 第三方壳全部适用),并依次回退
23
+ `ctx.baseUrl` → `DSH_PROFILE_DIR` → `DSH_HOME/profiles/<profile>`;`runtime.profile` 同步修正。
24
+ - **写入 profile patch 会让 dsh 启动失败**:`!!js` 在该 YAML schema 里只能作用于**标量**。
25
+ 旧写法 `trustedHosts: !!js [...]` 会被解析成「流式序列 + 标量标签」,报
26
+ `unknown tag !<tag:yaml.org,2002:js>`,直接让 dsh 起不来。现改为整体加引号的标量表达式
27
+ `trustedHosts: !!js "[...ctx.webStartup.trustedHosts, 'a', 'b']"`。
28
+ - **端口漂移**:不再重述 `port`,宿主端口(含 `--port 0` 系统分配)保持不变。
29
+ - **旧配置迁移**:启动时自动清理历史遗留的 `webserver` 绑定覆盖与
30
+ `ctx.webRuntime.trustedHosts` 表达式块(后者在 0.2.x 无此字段,求值即抛错),
31
+ 迁出的域名并入受管 `trustedHosts`;若旧绑定正在生效,会在 HMR watcher 就绪后再改写,
32
+ 确保监听被拉回回环。
33
+ - **重复扫码登录**:LAN 听众统一把 `Host` / `Origin` 改写成回环 authority,
34
+ 因此局域网访问**不再需要** `trustedHosts` 放行,也不会再出现「页面能开、`/api` 全 403」。
35
+
36
+ ### 新增 / New
37
+
38
+ - **`lib/lanproxy.js`**:局域网旁挂听众。在 `<局域网IP>:<当前端口>` 再监听一份,
39
+ 反向代理到 `127.0.0.1:<当前端口>`;支持 HTTP 与 WebSocket Upgrade,透传
40
+ `X-Forwarded-For`(设备列表仍显示手机真实 IP),长连接不因 keep-alive 超时被误杀。
41
+ 开关即时生效,**无需重启 dsh、无需 HMR、不写任何配置**。
42
+ - **移动端侧栏默认收起**:窄屏下侧栏轨道压到 `0px`,内容区吃满整屏;左下角新增悬浮按钮,
43
+ 点开是**覆盖式抽屉**(不再挤压中栏),带半透明遮罩;点遮罩 / Esc / 选中会话即收起。
44
+ 开合仍驱动官方 store(复用原生 toggle 按钮、aria 与快捷键),只是用 CSS 把「挤压」改写成「浮层」。
45
+ - **`remoteEnabled` 持久化**:写在插件自己的 `config.json`(与密码共用,未知字段保留),
46
+ 重启后自动恢复局域网听众;端口变化时 15s 内自适应重绑。
47
+ - 设置页文案与开关反馈更新:开启后立即刷新状态(不再等 HMR 的 1.5s)。
48
+
49
+ ### 测试 / Tests
50
+
51
+ - `tests/patch.test.mjs` 重写:覆盖「绝不写 webserver」「`!!js` 必须加引号」「旧配置迁移」
52
+ 「authority 无端口形式」「profileContext 路径优先」等 43 项断言,全部通过。
53
+
54
+ ## v1.8.1 (2026-10-07)
55
+
56
+ ### 修复 / Fixed
57
+
58
+ - **密钥(launch token)没被携带,手机 / 外网打开链接即 401**(`lib/index.js`):这是本版最关键的修复。
59
+ - **根因**:`withAuthUrl()` 只用严格模式 `ctx.get('connection')` 取宿主服务。cordis 的
60
+ `ctx.get(name)` 默认 `strict=true`,当插件 fiber 未把 `connection` 列入 `inject` 时可能取不到,
61
+ 函数便静默回退成「裸地址」——于是 `url` / `lanUrls` / `external.url` 全都不带 `?token=`,
62
+ 手机扫码后落到官方 401「dsh web authentication required」。
63
+ - 新增 `resolveConnection()`:严格模式 → 非严格 `ctx.get('connection', false)` → `ctx.connection`
64
+ 三级回退;新增 `resolveBrowserAuth()`:`conn.browserAuth` 不可见时直接复用
65
+ `HostConnectionService`(它本身就是 `BrowserAuth` 的委托层,具备 `authorizeIndex` /
66
+ `authenticatedUrl` / `isAuthenticated`)。
67
+ - **外网经反代时协议不一致导致 token 在跳转中丢失**(`lib/index.js`):裸地址自动登录原本生成
68
+ `http://<host>/?token=…` 绝对地址。经 nginx / frp 反代时本进程只看到明文 HTTP,发出的 http:// 会被
69
+ 反代 301 回 https,且 token 在跨协议跳转中被丢掉,握手永远完不成。现改为**相对地址跳转**
70
+ (`Location: /?token=…`),由浏览器按当前协议与 authority 自行解析,http / https / 反代子路径全部正确;
71
+ 同时不再解析重编码 token,直接透传原始 query,避免 base64url 的编码歧义。
72
+ - **外网隧道指向过期端口,导致 502 Bad Gateway**(`lib/external.js`):`autoRestoreExternal()`
73
+ 把持久化的 `localPort`(旧值如 3080)当显式端口传给 `startExternal()`,覆盖了当前真实端口,
74
+ frpc 于是转发到无人监听的端口(日志:`connect to local service 127.0.0.1:3080 ... refused`)。
75
+ 现在一律以**当前真实监听端口**为准,并在写 `frpc.toml` 前校验代理 `localPort` 是否与之一致,不一致直接中止并报错。
76
+ - **`connection.trustedHosts` 从未真正生效,手机 / 外网功能全 403**(`lib/patch.js`、`lib/external.js`):
77
+ 旧实现写的是 `trustedHosts: !!js "['域名', ...ctx.webRuntime.trustedHosts]"`,但 dsh 0.1.7 / 0.2.x 的
78
+ `webStartup` 服务**并不存在 `webRuntime` 字段**,表达式求值成对象而非数组,被 connection 的
79
+ `z.array(String)` 拒绝——表现为「页面能打开,但所有 API 403 forbidden」。
80
+ - **开关开启时若探测不到局域网 IP,会丢掉 connection 块**(`lib/patch.js`):`lanAddresses()`
81
+ 在 ESM 下残留了 `require('node:os')`,抛出的 `ReferenceError` 被外层 `catch {}` 静默吞掉,
82
+ 永远返回空列表。已改为顶层 `import`,并把 `npm run check` 补上 `lib/patch.js`
83
+ (此前该文件根本没被语法检查覆盖,才让上述错误长期潜伏)。
84
+ - **`302` 跳到自身造成无限重定向循环**(`lib/index.js`):取不到带 token 的地址时原本回退成
85
+ `location: '/'`,浏览器会反复请求自己。现改为明确返回 401,交回官方语义。
86
+
87
+ ### 新增 / New
88
+
89
+ - **`lib/patch.js`**:统一维护 profile `cordis.patch.yml` 的受管区域(幂等整体重写)。
90
+ 「开启远程控制」现在一次性写两块——`webserver.host = '0.0.0.0'`(手机能连上)与
91
+ `connection.trustedHosts = ['192.168.x.x:port', …]`(手机能调 API),并用
92
+ `normalizeAuthority()` 保证每个 authority 都是能通过 `assertTrustedAuthority` 的规范形式;
93
+ 外网域名由 `ensureTrustedAuthority()` 增量并入同一区域,不再产生互相冲突的重复块。
94
+ - **端口自适应**(`lib/index.js`):每 15s 跟随 `ctx.webServer.port`,端口变化(改 `--port`、
95
+ 被占用回退到 OS 分配、profile patch 变更)时自动把外网隧道重绑到新端口。
96
+ - **`tests/patch.test.mjs`**:针对 patch 读写的功能测试,覆盖 authority 规范化、
97
+ 受管块幂等重写、域名增量并入、关闭时整块移除等 32 项断言。
98
+
99
+ ### 变更 / Changed
100
+
101
+ - 未授权裸地址若既取不到 token 也无法安全自动登录,返回明确的 401,而不是把
102
+ 「dsh web is starting」这种误导性 503 给到手机端。
103
+ - 兼容性不变:老版 dsh(无 `connection` / 无浏览器会话认证)仍按原行为回退。
104
+
105
+ ## v1.8.0 (2026-09-06)
106
+
107
+ ### 新增 / New
108
+
109
+ - **裸地址直达(自动登录)**(`lib/index.js`):本插件现在接管 `GET /`。手机 / 远程设备**直接访问裸地址**(如 `http://192.168.3.119:3080`,不带 token)即可自动进入——
110
+ - 未授权访问 → 服务端 `302` 到带 token 的 URL(token 由服务端自动注入)→ 浏览器自动跟随 → 官方完成 `303 + Set-Cookie` → 自动回到裸 `/`;
111
+ - 已授权设备访问裸地址 → 直接返回官方完整 UI;
112
+ - 官方 token → cookie 的握手**一步未绕开**,只是把「手动重开带 token 链接」自动化,全程对用户无感,地址栏始终是干净裸地址。
113
+ - **新版 dsh 浏览器会话认证适配**(`lib/index.js` + `lib/client.js`):dsh `>=0.1.2-rc.1` 对 Web UI 强制「浏览器会话认证」(启动时打印一次性令牌,任何无 cookie 访问返回 401)。本插件自动兼容:
114
+ - `GET /` 由插件接管:首次裸地址访问自动完成授权,无需手动带 token;
115
+ - `/__dsh_remote/status` 返回的 `url` / `lanUrls` / `external.url` 同时携带官方认证 token(程序化调用仍可用),移动端 UI / 二维码 / 地址列表展示干净裸地址并提示「首次访问自动完成连接授权」;
116
+ - 老版 dsh(无认证 / 无 `connection` 服务)自动回退:`url` / `lanUrls` 为普通裸地址、`/` 直接返回官方 index,行为与旧版完全一致。
117
+
118
+ ### 修复 / Fixed
119
+ - 无(行为变更见下)。
120
+
121
+ ### 变更 / Changed
122
+ - **接管 `GET /`**:为避免与官方 index owner 冲突,插件在内部拉取官方 index(含全部官方 index taps 注入)时临时注销自身的 `/` 路由,取回后重新注册;插件卸载 / 服务停止时正确清理。
123
+ - 手机访问地址 / 二维码 / 局域网列表改展示**裸地址**(配合自动登录,首次访问即可用),不再显示一长串 `?token=`。
124
+ - 安全提示:裸地址自动登录等效于「局域网内能访问到该地址的设备都会自动获得授权」。若需要更强隔离,请在插件设置中开启**远程访问密码门禁**(对外网隧道生效)并仅在可信网络使用。
125
+
3
126
  ## v1.7.1 (2026-08-29)
4
127
 
5
128
  ### 新增 / New
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- # DeepSeek Harness Mobile Remote Control (dsh-mobile-remote) 📱
3
+ # DeepSeek Harness Mobile Remote Control (dsh-mobile-remote)
4
4
 
5
5
  **English** · [简体中文](./README.zh.md)
6
6
 
@@ -22,7 +22,9 @@
22
22
 
23
23
  - **Mobile image upload** (v1.5.0): a new **"Upload image" button next to the + in the composer toolbar** lets phone users pick photos (system gallery / file picker) for DeepSeek's vision models — format/count/size pre-checks, thumbnail rail, and send-time upload all reuse DSH's native pipeline; the desktop UI is untouched.
24
24
  - **Connection QR code**: auto-generates a QR for `http://<lan-ip>:<port>` — scan it with your phone's camera / browser to connect.
25
- - **On / Off toggle**: switches the webserver listen address (`0.0.0.0` ↔ `127.0.0.1`) with one click. Applied via dsh's official HMR hot-reload — **no service restart needed**.
25
+ - **Bare-URL direct access with auto sign-in** (v1.8.0): just open `http://192.168.x.x:3080` (no token) and it enters automatically — the server transparently performs the new dsh browser-session handshake (`dsh >= 0.1.2-rc.1`); older dsh versions keep their original behavior.
26
+ - **On / Off toggle** (rebuilt in v1.9.0): starts a **side-car listener** on the LAN interface (`<lan-ip>:<port>` → `127.0.0.1:<port>`). dsh's own listen address and port are **never touched** — your local window, the desktop app and sessions keep working, and the switch takes effect instantly with **no restart and no config writes**.
27
+ - **Mobile sidebar collapsed by default** (v1.9.0): on narrow screens the sidebar track is squeezed to `0px` so content gets the full width; a floating button opens an **overlay drawer** (no more squeezing the center column) with a scrim — closes on scrim tap / Esc / picking a session.
26
28
  - **Connected device count**: mobile heartbeats report online devices in real time.
27
29
  - **Remote access password gate** (v1.2.0+): set a password locally; external-tunnel visitors must enter it before the page loads — local/LAN access stays password-free.
28
30
  - **Faster remote loading** (v1.2.0+): larger frpc connection pool to stop the slow initial spinner over tunnels.
@@ -30,7 +32,7 @@
30
32
  - **Real external-tunnel status** (v1.4.1): no more fake "online" — the panel probes frpc's log to confirm the tunnel is really up, downgrades to "connecting" during reconnects, and shows a red **error card with the exact frpc reason** (e.g. `token in login doesn't match`) plus a one-click retry; FRP_TOKEN placeholder warnings from the relay show as a yellow banner; keep-alive heartbeats keep the relay's online state truthful.
31
33
  - LAN address list, phone access URLs, and config location at a glance.
32
34
 
33
- **Key selling point: pure CLI users don't need the desktop app.** The official CLI deliberately rejects `--host 0.0.0.0` for safety, but this plugin writes `webserver.host: 0.0.0.0` through the profile's `cordis.patch.yml` (or a `--patch <overlay>`) — exactly how the desktop app's "Mobile Remote Control" switch works. The phone and the computer share the same dsh backend process and the same `~/.dsh` data, so **sessions, history, workspace and settings are naturally two-way synced — no data copying involved**.
35
+ **Key selling point: pure CLI users don't need the desktop app.** The official CLI rejects `--host 0.0.0.0` for safety, and since **dsh 0.2.1-rc.3 the patch layer rejects wildcard binds too** (`host: '0.0.0.0'` aborts startup during config validation), while binding a concrete NIC IP makes the loopback listener disappear (desktop windows go blank). So v1.9.0 uses a **side-car LAN listener** instead: dsh keeps listening on loopback only, and the plugin listens on the LAN interface and forwards. The phone and the computer still share the same dsh backend process and the same `~/.dsh` data, so **sessions, history, workspace and settings are naturally two-way synced — no data copying involved**.
34
36
 
35
37
  ---
36
38
 
@@ -71,7 +73,11 @@ This plugin optimizes it by:
71
73
  ## Screenshots
72
74
 
73
75
  ### Settings → Remote Control
74
- ![Remote Control](./docs/assets/remote-control.png)
76
+
77
+ Open **Settings → Remote Control** to see the on/off switch and status badge, the connection
78
+ QR code, LAN address list, connected devices (expandable: system / browser / screen / network),
79
+ runtime info, DSH state, network interfaces, the remote-access password, and the external tunnel
80
+ (domain / frpc version / logs).
75
81
 
76
82
  ## Recommended Installation
77
83
 
@@ -90,6 +96,41 @@ dsh plugin --profile web add @feiyang666/dsh-mobile-remote
90
96
 
91
97
  Restart the dsh web service to activate.
92
98
 
99
+ ### Option 3: Install from a local tarball (unpublished / offline / self-testing)
100
+
101
+ When the plugin is not on npm yet (or the npm publish is blocked by 2FA / staged
102
+ approval), install straight from local source:
103
+
104
+ ```bash
105
+ # Run in the plugin repo root: packs and installs into the given profile(s)
106
+ node scripts/install-local.mjs # install into desktop
107
+ node scripts/install-local.mjs desktop web # install into several profiles
108
+ ```
109
+
110
+ The script packs, installs with the profile's own package manager (pnpm when a
111
+ `pnpm-workspace.yaml` exists, otherwise npm), registers the bundle name in
112
+ `dsh.profile.bundles`, then reads back the version and key files so a silent
113
+ "fake success" cannot happen.
114
+
115
+ > You can also pick a local `.tgz` from the desktop app's "Install plugin" dialog.
116
+ >
117
+ > After installing, the profile's `dependencies` records `file:...tgz`. That is
118
+ > intentional: until the plugin is published, a range such as `^1.7.1` would make a
119
+ > later `npm install` **downgrade** to an older registry version.
120
+
121
+ ### Fast hot-sync during development (skip packing)
122
+
123
+ For front-end-only changes (`lib/client.js`), `sync-to-profile.mjs` copies files
124
+ into the installed directory; a browser refresh is enough:
125
+
126
+ ```bash
127
+ node sync-to-profile.mjs # default profile: web
128
+ DSH_PROFILE=desktop node sync-to-profile.mjs
129
+ ```
130
+
131
+ Note: `lib/index.js`, `lib/external.js` and `lib/patch.js` are host-side code with a
132
+ module cache — **restart the dsh service** for those to take effect.
133
+
93
134
  ---
94
135
 
95
136
  ## Usage
@@ -111,14 +152,11 @@ dsh --profile web
111
152
  dsh --profile web --patch remote-control.patch.yml
112
153
  ```
113
154
 
114
- `remote-control.patch.yml`:
115
-
116
- ```yaml
117
- - id: webserver
118
- config:
119
- host: '0.0.0.0'
120
- port: !!js ctx.webStartup.port ?? 3080
121
- ```
155
+ > **Note (dsh ≥ 0.2.1-rc.3)**: **do not** hand-write a `- id: webserver` +
156
+ > `host: '0.0.0.0'` overlay any more — the new schema aborts startup on wildcard binds,
157
+ > and a concrete NIC IP would take the loopback listener away (desktop windows stop loading).
158
+ > Since v1.9.0 the LAN exposure is provided by the plugin's own side-car listener,
159
+ > so **no overlay is required** — just use the settings toggle.
122
160
 
123
161
  > **Security note**: enabling remote control exposes the service to your LAN. Use it only on trusted home / office networks — **do not expose it to the public internet**.
124
162
 
@@ -201,6 +239,7 @@ Then restart the app.
201
239
  | Symptom | Cause / Fix |
202
240
  | --- | --- |
203
241
  | No "Remote Control" panel in Settings | Plugin not activated. Check the `cordis.patch.yml` row exists with the right `name`; restart and hard-refresh |
242
+ | Phone opening the bare URL shows "authentication required" | Running a pre-v1.8.0 plugin, or the server hasn't loaded this plugin's `/` takeover. Install v1.8.0+ and **restart the dsh web service** (on old dsh without auth this plugin falls back to its previous behavior) |
204
243
  | Phone still can't connect after toggling ON | Same Wi-Fi? Firewall allowing port 3080? Accessing the LAN IP (not `127.0.0.1`)? |
205
244
  | Phone connects but shows 403 | dsh's trust fence: make sure the plugin is installed and dsh is started the supported way (`--patch` / profile patch overriding `webserver.host`) |
206
245
  | Device count stays 0 | The phone page must be opened once to start heartbeat; wait a few seconds and refresh |
package/README.zh.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- # DeepSeek Harness 移动端远程控制插件(dsh-mobile-remote)📱
3
+ # DeepSeek Harness 移动端远程控制插件(dsh-mobile-remote)
4
4
 
5
5
  [English](./README.md) | **简体中文**
6
6
 
@@ -21,8 +21,10 @@
21
21
  `dsh-mobile-remote` 是 DeepSeek Harness 生态的**移动端远程控制插件**(DSH plugin,Host + Client 双面一体包)。装好后在 WebUI **设置 → 远程控制** 出现一个管理面板:
22
22
 
23
23
  - **移动端上传图片**(v1.5.0):composer 工具行 **+ 加号旁新增「上传图片」按钮**,手机直接选图(系统相册 / 文件选择器)即可让 DeepSeek 识图模型识别——格式 / 数量 / 大小预检查、缩略图预览、随消息上传全部复用 DSH 原生链路,桌面端不受影响;
24
+ - **移动端侧栏默认收起**(v1.9.0):窄屏下把侧栏轨道压到 0,内容区吃满整屏;左上角悬浮按钮点开为**覆盖式抽屉**(不挤压中栏),点遮罩 / Esc / 选中会话自动收起;
24
25
  - **连接二维码**:自动生成 `http://<局域网IP>:<端口>` 的二维码,手机相机 / 浏览器扫码即连;
25
- - **开启 / 关闭开关**:一键切换 webserver 监听地址(`0.0.0.0` ↔ `127.0.0.1`),经 dsh 官方 HMR 热重载**无需重启服务**;
26
+ - **裸地址直达,自动登录**(v1.8.0):直接访问裸地址 `http://192.168.x.x:3080`(不带任何 token)即可自动进入——服务端自动完成新版 dsh 的浏览器会话认证(`≥0.1.2-rc.1`),无需复制带 `?token=` 的长链接;老版 dsh 自动回退,行为不变;
27
+ - **开启 / 关闭开关**(v1.9.0 重做):在局域网网卡上**旁挂一个转发听众**(`<局域网IP>:<端口>` → `127.0.0.1:<端口>`),dsh 自身的监听地址与端口**完全不动**——本机窗口、桌面端、会话都不会被打断,开关**即时生效、无需重启、不写配置**;
26
28
  - **当前连接的设备数量**:移动端心跳上报,实时统计在线设备数;
27
29
  - **远程访问密码门禁**(v1.2.0+):管理员本机设置密码,外网隧道访问进入页面需先输密码,本机/局域网不受影响;
28
30
  - **远程访问提速**(v1.2.0+):frpc 连接池调大,缓解外网访问首屏转圈慢;
@@ -30,7 +32,7 @@
30
32
  - **外网隧道真实状态**(v1.4.1):不再"假在线"——面板探测 frpc 日志确认隧道真正建立,断线重连降级「连接中」,token 鉴权失败显示红色「出错」+ 精确原因(如 `token in login doesn't match`)+ 一键重试;中转端 `FRP_TOKEN` 占位符时显示黄色警告条;隧道持续心跳保活,让中转端在线状态真实可靠;
31
33
  - 局域网地址列表、手机访问地址、配置位置一目了然。
32
34
 
33
- **核心卖点:纯命令行用户无需安装桌面端。** 官方 CLI 出于安全拒绝 `--host 0.0.0.0`,但本插件通过 profile 的 `cordis.patch.yml`(或 `--patch <overlay>`)写入 `webserver.host: 0.0.0.0`——这正是桌面端「移动端远程控制」开关的实现方式。手机与电脑共享同一个 dsh 后端进程与同一份 `~/.dsh` 数据,**会话列表、历史记录、工作区、设置全部天然双向同步,无需任何数据复制**。
35
+ **核心卖点:纯命令行用户无需安装桌面端。** 官方 CLI 出于安全拒绝 `--host 0.0.0.0`;而 dsh **0.2.1-rc.3 起连 patch 层也不再接受通配地址**(`host: '0.0.0.0'` 会在配置校验阶段直接让进程退出),改绑具体网卡又会让本机回环监听消失(桌面端窗口白屏)。所以 v1.9.0 改用**旁挂 LAN 听众**:dsh 保持只听回环,插件在局域网网卡上再监听一份并转发过去——手机与电脑仍共享同一个后端进程与同一份 `~/.dsh` 数据,**会话列表、历史记录、工作区、设置全部天然双向同步,无需任何数据复制**。
34
36
 
35
37
  ---
36
38
 
@@ -71,7 +73,10 @@
71
73
  ## 界面预览
72
74
 
73
75
  ### 设置 → 远程控制
74
- ![远程控制](./docs/assets/remote-control.png)
76
+
77
+ 打开「设置 → 远程控制」可以看到:开关与状态徽标、连接二维码、局域网地址列表、已连接设备
78
+ (可展开查看系统 / 浏览器 / 屏幕 / 网络等完整信息)、运行时信息、DSH 状态、网络接口、
79
+ 远程访问密码、外网隧道(域名 / frpc 版本 / 日志)。
75
80
 
76
81
  ## 推荐安装方式
77
82
 
@@ -90,6 +95,37 @@ dsh plugin --profile web add @feiyang666/dsh-mobile-remote
90
95
 
91
96
  装完重启 dsh web 服务即可。
92
97
 
98
+ ### 方式三:从本地 tarball 安装(未发布 / 离线 / 自测用)
99
+
100
+ 当插件尚未发布到 npm(或 npm 发布受 2FA、暂存审批阻塞),可直接从本地源码安装:
101
+
102
+ ```bash
103
+ # 在插件仓库根目录执行:自动 npm pack 并装进指定 profile(默认 desktop)
104
+ node scripts/install-local.mjs # 装到 desktop
105
+ node scripts/install-local.mjs desktop web # 装到多个 profile
106
+ ```
107
+
108
+ 脚本会:打包 → 按 profile 的包管理器安装(有 `pnpm-workspace.yaml` 用 pnpm,否则用 npm)
109
+ → 把包名注册进 `dsh.profile.bundles` → 回读校验版本与关键文件,避免「假成功」。
110
+
111
+ > 也可在桌面端「安装插件」里直接选择本地 `.tgz` 文件,效果等价。
112
+ >
113
+ > 安装后 profile 的 `dependencies` 会写成 `file:...tgz`。这是有意为之:
114
+ > 在插件发布到 npm 之前,依赖若写成 `^1.7.1` 之类的范围,后续 `npm install`
115
+ > 会从 registry **降级**到旧版本。
116
+
117
+ ### 开发期快速热同步(改代码后免打包)
118
+
119
+ 只改前端 `lib/client.js` 时,可用 `sync-to-profile.mjs` 直接覆盖已装目录,刷新浏览器即生效:
120
+
121
+ ```bash
122
+ node sync-to-profile.mjs # 默认 profile: web
123
+ DSH_PROFILE=desktop node sync-to-profile.mjs
124
+ ```
125
+
126
+ 注意:`lib/index.js` / `lib/external.js` / `lib/patch.js` 属 host 侧代码,有模块缓存,
127
+ **必须重启 dsh 服务**才生效。
128
+
93
129
  ---
94
130
 
95
131
  ## 使用步骤
@@ -111,14 +147,11 @@ dsh --profile web
111
147
  dsh --profile web --patch remote-control.patch.yml
112
148
  ```
113
149
 
114
- `remote-control.patch.yml`:
115
-
116
- ```yaml
117
- - id: webserver
118
- config:
119
- host: '0.0.0.0'
120
- port: !!js ctx.webStartup.port ?? 3080
121
- ```
150
+ > **注意(dsh ≥ 0.2.1-rc.3)**:**不要**再手写
151
+ > `- id: webserver` + `host: '0.0.0.0'` 的 overlay——新版会在配置校验阶段直接报错退出;
152
+ > 改成具体网卡 IP 又会顶掉本机 `127.0.0.1` 的监听(桌面端窗口随之打不开)。
153
+ > v1.9.0 起「对局域网开放」由插件自己的旁挂听众完成,**不需要任何 overlay**,
154
+ > 用方式一(设置页开关)即可。
122
155
 
123
156
  > **安全提示**:开启后服务暴露到局域网,请仅在可信的家庭 / 办公网络使用,**不要暴露到公网**。
124
157
 
@@ -143,14 +176,17 @@ dsh --profile web --patch remote-control.patch.yml
143
176
 
144
177
  | 层 | 文件 | 职责 |
145
178
  | --- | --- | --- |
146
- | host | `lib/index.js` | `/__dsh_remote/status` / `toggle` / `heartbeat` / `qr` API;patch 文件读写;心跳设备计数;index.html 注入 |
179
+ | host | `lib/index.js` | `/__dsh_remote/status` / `toggle` / `heartbeat` / `qr` API;心跳设备计数;index.html 注入 |
180
+ | lanproxy | `lib/lanproxy.js` | 局域网旁挂听众:`<LAN IP>:<端口>` → `127.0.0.1:<端口>`,HTTP + WebSocket 转发 |
181
+ | patch | `lib/patch.js` | profile `cordis.patch.yml` 受管区域(**只写 `connection.trustedHosts`**,绝不写 webserver)与旧配置迁移 |
147
182
  | client | `lib/client.js` | 注册 `settings.section`「远程控制」面板(开关 / 二维码 / 设备数) |
148
183
  | qrcode | `lib/qrcode.js` | 内联 MIT QR 生成器(`qrcode-generator`),零运行时依赖 |
149
184
 
150
- - **开关**:`POST /__dsh_remote/toggle` → 写入 / 移除 profile `cordis.patch.yml` 的 `webserver` 覆盖块 → dsh `watchUserPatches`(Cordis HMR)热重载 webserver 行重新监听。
185
+ - **开关**:`POST /__dsh_remote/toggle` → 在局域网网卡上开启 / 关闭旁挂听众。请求被转发到 `127.0.0.1:<端口>`,并把 `Host` / `Origin` 改写成回环 authority(dsh 的信任围栏对回环一律放行),因此**不需要改 dsh 任何配置、也不需要重启**。
151
186
  - **设备数**:移动端注入 JS 每 30s 心跳上报,host 维护活跃设备表(90s 过期)。每次心跳会上报完整设备元数据(屏幕 / 视口 / DPR / 网络 / 当前页面 / 语言 / 平台),设置面板的设备行可展开查看。
152
187
  - **二维码**:`GET /__dsh_remote/qr?url=...` 返回 SVG。
153
- - **移动端适配**:`tapIndex` 注入移动端 CSS/JS,composer 输入栏窄屏换行、选择器限宽、iOS 输入框 16px 防缩放、**加号旁注入「上传图片」按钮**(文件选择器选图 → 复用 DSH 原生 paste/drop intake 链路);**不破坏 DSH 原生 rail + 汉堡抽屉交互**;
188
+ - **移动端适配**:`tapIndex` 注入移动端 CSS/JS,composer 输入栏窄屏换行、选择器限宽、iOS 输入框 16px 防缩放、**加号旁注入「上传图片」按钮**(文件选择器选图 → 复用 DSH 原生 paste/drop intake 链路);
189
+ - **移动端侧栏**(v1.9.0):窄屏下侧栏轨道压到 `0px`,内容区吃满整屏;左上角悬浮按钮唤出**覆盖式抽屉**(半透明遮罩,点遮罩 / Esc / 选中会话自动收起)。仍复用原生 toggle 按钮驱动官方 store,只把「挤压中栏」改写成「浮层」,因此原生 aria / 快捷键 / 状态一致;框架节点用 `data-dsh-mobile-frame` 标记,不依赖 harness 哈希类名;
154
190
  - **移动端设置页适配**(v1.4.2+):原生设置弹窗在手机上自动变为全屏单栏——导航栏变顶部横向滚动标签、内容区占满剩余空间;同时抑制触屏上常驻的原生 Tooltip 气泡(如「停止 / 开始 / 关闭菜单栏」提示文字)。通过结构标记识别设置弹窗,**不依赖 harness 内部哈希类名**,harness 升级后无需修改插件即可直接适配。**v1.4.3** 修复该单栏布局——面板原本仍是横向 `flex`,内容列被压成 0 宽度(内容空白)且无法上下滚动;现强制 `flex-direction: column` + `min-height: 0`,设置选项区在手机上可正常滚动。并新增**内网(非安全上下文)`crypto.randomUUID` 兜底**:当前 DSH 客户端在连接时直接调用 `crypto.randomUUID()`,该 Web Crypto API 仅在 HTTPS / localhost 存在,内网 http 直连会报 `crypto.randomUUID is not a function` / `settings are unavailable in this browser`;插件注入的头部脚本用 `getRandomValues` 补一个 RFC 4122 v4 实现,内网访问 / 扫码连接恢复正常。
155
191
 
156
192
  ---
@@ -201,8 +237,11 @@ dsh plugin --profile web remove @feiyang666/dsh-mobile-remote
201
237
  | 现象 | 原因 / 处理 |
202
238
  | --- | --- |
203
239
  | 设置里没有「远程控制」面板 | 插件未激活。确认 `cordis.patch.yml` 里的行存在且 `name` 正确;重启后刷新(清缓存) |
204
- | 开启开关后手机还是连不上 | 确认手机与电脑同一 Wi-Fi;确认防火墙放行 3080 端口;确认访问的是局域网 IP 而非 `127.0.0.1` |
205
- | 手机能访问但显示 403 | dsh 的信任围栏:请确认本插件已安装、且以官方支持的方式启动(`--patch` / profile patch 覆盖 `webserver.host`) |
240
+ | 手机打开裸地址显示「authentication required / dsh web authentication required」 | 用的是 dsh `0.1.2-rc.1` 之前的老版插件,或服务端未加载本插件的 `/` 接管。确认已安装 v1.8.0+ 并**重启 dsh web 服务**(老版 dsh 无认证时按原行为返回 401 提示,可改用带 token 地址或升级 dsh) |
241
+ | 开启开关后手机还是连不上 | 确认手机与电脑同一 Wi-Fi;确认 Windows 防火墙放行 dsh 所用端口(`node.exe` 首次监听局域网时会弹窗);确认访问的是「设置页显示的那个地址」。开启后 `127.0.0.1` 之外也能连,但 dsh 自身仍只听回环——这是设计如此 |
242
+ | 手机能访问但显示 403 | v1.9.0 已不会出现:旁挂听众会把 `Host`/`Origin` 改写成回环,dsh 信任围栏直接放行。若仍 403,说明你用的是旧版插件,或前面还套了一层反代且改写了 Host |
243
+ | 开启开关后电脑上的窗口打不开 / 白屏 | 旧版(≤1.8.1)把 `webserver.host` 改成 `0.0.0.0` 或局域网 IP,导致本机回环监听消失。升级到 v1.9.0 后启动会自动清理旧配置并把监听拉回回环 |
244
+ | 手动写了 `host: '0.0.0.0'` 的 patch,dsh 起不来 | dsh 0.2.1-rc.3 起拒绝通配地址(配置校验阶段直接退出)。删掉那块覆盖,改用本插件的开关即可 |
206
245
  | 设备数为 0 | 手机端页面需打开过才会心跳上报;等待几秒刷新 |
207
246
  | 二维码扫不出来 | 确认「远程控制」已开启;相机应用需支持扫码 |
208
247
  | 外网访问被全屏密码页拦住 | 这是**远程访问密码门禁**(v1.2.0+)。管理员在本机「设置 → 远程控制 → 远程访问密码」输入正确密码即可进入;忘记密码需在本机重新设置 |
package/RELEASE_NOTES.md CHANGED
@@ -1,38 +1,43 @@
1
- # dsh-mobile-remote v1.7.0
1
+ # dsh-mobile-remote v1.9.0
2
2
 
3
- **DeepSeek Harness 移动端远程控制插件** —— 在 Web UI 设置页内置「远程控制」:连接二维码、一键开关、在线设备数,让手机通过局域网访问并操控电脑上的 DeepSeek Harness。移动端提供上传图片入口、底部抽屉式模型选择、发送状态提示等移动优先体验。
3
+ **DeepSeek Harness 移动端远程控制插件** —— 在 Web UI 设置页内置「远程控制」:连接二维码、一键开关、在线设备数,让手机通过局域网访问并操控电脑上的 DeepSeek Harness。
4
4
 
5
- ## 安装方式
5
+ ## 本版更新(v1.9.0)—— 让远程控制在 dsh 0.2.1+ 上重新可用
6
6
 
7
- ```bash
8
- dsh plugin --profile web add @feiyang666/dsh-mobile-remote
9
- ```
7
+ 一句话:**不再改 dsh 的监听绑定,改为旁挂一个局域网听众。** 桌面端窗口、端口、会话全都不受影响。
10
8
 
11
- 安装完成后**重启 dsh web 服务**,打开 `http://127.0.0.1:3080` → **设置 → 远程控制** 即可使用。
9
+ ### 为什么之前的版本用不了了
12
10
 
13
- ## 本版更新(v1.7.0)
11
+ | 症状 | 根因 |
12
+ | --- | --- |
13
+ | 开启开关后,**电脑上的窗口直接打不开/白屏** | 旧版把 `webserver.host` 改成 `0.0.0.0`。dsh 0.2.1-rc.3 起只接受具体网卡 IP,`0.0.0.0` 会让进程启动即退出;即使是具体 IP,改绑后 `127.0.0.1` 监听消失,桌面端固定连回环 → 白屏 |
14
+ | **官方桌面端**里开关点了完全没反应 | 旧版按 `DSH_PROFILE` 环境变量定位 profile,而官方桌面端**不设该变量**(只把 profile 目录作为 argv 传参)→ patch 写进了 `profiles/web`,实际运行的是 `profiles/desktop` |
15
+ | 写完 patch 后 **dsh 起不来**,报 `unknown tag !<tag:yaml.org,2002:js>` | `!!js` 在该 YAML schema 里只能作用于**标量**;`trustedHosts: !!js [...]` 会被当成「流式序列 + 标量标签」 |
16
+ | 端口莫名变了、二维码/链接失效 | 官方桌面端用 `--port 0` 由系统分配端口,旧 patch 里重述 `port` 会让重载后的端口漂移 |
14
17
 
15
- ### 🆕 中转服务器 WS 状态推送(长连接替代轮询)
18
+ ### 新版怎么做的
16
19
 
17
- 外网隧道开启后,插件端改为通过 **WebSocket 长连接**(`/api/tunnel/ws?bindCode=…`)接收中转服务器的隧道状态实时推送,**替代原来每 5s 的 HTTP 轮询** `/api/tunnel/stats`,中转服务器请求压力降为 0(服务器资源集中用于 frp 隧道中转)。
20
+ - **旁挂 LAN 听众(`lib/lanproxy.js`)**:在 `<局域网IP>:<当前端口>` 再监听一份,反向代理到
21
+ `127.0.0.1:<当前端口>`。HTTP 与 WebSocket 都通,`X-Forwarded-For` 透传真实手机 IP,
22
+ 长连接不会被 keep-alive 超时误杀。**开关即时生效,无需重启 dsh、不写任何配置文件。**
23
+ - **Host / Origin 改写成回环**:dsh 的 Host/Origin 信任围栏对回环一律放行,
24
+ 因此局域网访问**不再需要** `trustedHosts`,也不会再出现「页面能打开、`/api` 全 403」。
25
+ - **patch 只写 `connection.trustedHosts`**(外网隧道经 frp 直连真实监听时才需要),
26
+ 用 `!!js "[...ctx.webStartup.trustedHosts, '域名']"` 合并启动参数,绝不覆盖 `webserver`。
27
+ - **profile 路径校正**:优先 `profileContext.patchPath` —— CLI、官方桌面端、第三方壳全部适用。
28
+ - **旧配置自动迁移**:清理历史遗留的 `webserver` 覆盖与 `ctx.webRuntime.trustedHosts` 块,
29
+ 迁出的域名并入受管 trustedHosts;若旧绑定正在生效,会在 HMR watcher 就绪后再改写,把监听拉回回环。
18
30
 
19
- - **指数退避自动重连**:断线后 2s → 4s → … → 60s 封顶自动恢复;
20
- - **心跳优先走 WS**:WS 已连上时心跳经长连接送达(零 HTTP 请求),未连上才回退 HTTP 兜底;
21
- - **状态查询 60s 节流**:即使移动端每 5s 轮询本机 `/status`,也不会高频打中转服务器;前端优先读 WS 推送缓存;
22
- - **ws 依赖缺失自动降级**:`ws` 未安装时 WS 客户端 no-op,心跳与状态查询回退 HTTP 低频兜底,**功能不中断**。
31
+ ### 移动端体验
23
32
 
24
- > ⚠️ 本版新增运行时依赖 `ws`(`^8.18.0`)。通过 `dsh plugin --profile web add` 安装会自动带上;若使用离线 tgz 手动安装,请确保 `ws` 已安装(缺失时插件自动降级,不会崩溃)。
33
+ - **侧栏默认收起**:窄屏下侧栏轨道压到 `0px`,内容区吃满整屏(此前固定的 56px 图标 rail 在
34
+ 390px 手机上仍占 14% 宽)。
35
+ - **悬浮按钮唤出抽屉**:左上角悬浮汉堡按钮 → 覆盖式抽屉(不再挤压中栏)+ 半透明遮罩;
36
+ 点遮罩 / Esc / 选中会话自动收起。开合仍驱动官方 store(复用原生 toggle、aria、快捷键)。
37
+ - 端口变化时局域网听众 15s 内自适应重绑;`remoteEnabled` 持久化,重启自动恢复。
25
38
 
26
- ## 历史更新(v1.6.x)
39
+ ### 验证
27
40
 
28
- - **v1.6.1**:用量插件等宽表移动端兜底(强制 720px + ≤540px 折叠中间列)
29
- - **v1.6.0**:新增 `GET /__dsh_remote/events` SSE 实时推送端点(设备心跳 / 远程开关 / 外网隧道状态变化即时推送)
30
-
31
- ## 已知事项
32
-
33
- - 开启后服务暴露到公网,**请务必设置远程访问密码**,并仅在可信网络上使用
34
- - 外网隧道需确保服务器 `.env` 的 `FRP_TOKEN` 与 `/www/server/frps/frps.toml` 的 `auth.token` 完全一致,否则面板会明确提示鉴权失败
35
-
36
- ## 变更日志
37
-
38
- 详见 [CHANGELOG.md](./CHANGELOG.md)。
41
+ - `tests/patch.test.mjs`:43 项断言全通过(含「绝不写 webserver」「`!!js` 必须加引号」「旧配置迁移」)。
42
+ - 实测环境:dsh 0.2.1-rc.3 + Windows;验证了「本机端口不变 + 局域网同端口可访问 +
43
+ 扫码自动登录 + `/api` 不再 403 + WebSocket 升级透传 + 开关即时生效(无需重启)」。
package/lib/client.js CHANGED
@@ -321,7 +321,8 @@ window.__ModuleLoader__.load({
321
321
  .then(function (data) {
322
322
  if (!data.ok) throw new Error(data.error || 'toggle failed');
323
323
  setBusy(false);
324
- setTimeout(refresh, 1500); // 等待 HMR 重载后再刷新
324
+ // v1.9:旁挂 LAN 听众即时生效(不再依赖 HMR 重载),立即刷新即可
325
+ refresh();
325
326
  })
326
327
  .catch(function (e) {
327
328
  setBusy(false);
@@ -345,10 +346,13 @@ window.__ModuleLoader__.load({
345
346
  style: Object.assign({}, st.switch, on ? st.switchOn : null),
346
347
  }, el('span', { style: Object.assign({}, st.knob, on ? st.knobOn : null) }));
347
348
 
348
- // 局域网二维码
349
+ // 局域网二维码(裸地址:插件已在服务端接管 / 自动完成认证,无需带 token)
350
+ var bareLanUrl = (status && status.lanAddresses && status.lanAddresses.length && status.port)
351
+ ? ('http://' + status.lanAddresses[0] + ':' + status.port)
352
+ : (status && status.url || null);
349
353
  var qrEl;
350
- if (on && status && status.url) {
351
- var qrSrc = '/__dsh_remote/qr?url=' + encodeURIComponent(status.url);
354
+ if (on && bareLanUrl) {
355
+ var qrSrc = '/__dsh_remote/qr?url=' + encodeURIComponent(bareLanUrl);
352
356
  qrEl = el('div', { style: st.qrBox },
353
357
  el('img', { src: qrSrc, alt: '连接二维码', style: st.qrImg }));
354
358
  } else if (status) {
@@ -515,21 +519,18 @@ window.__ModuleLoader__.load({
515
519
  : (extError ? '外网隧道异常,请查看上方错误信息' : '开启外网访问后,这里显示外网二维码'));
516
520
  }
517
521
 
518
- // 命令行提示(未开启时展示 CLI 启动方式)
522
+ // 说明卡片(未开启时展示新版工作原理)
519
523
  var cliHint = null;
520
524
  if (status && !status.remoteEnabled) {
521
525
  cliHint = el('div', { style: st.card },
522
- el('p', { style: st.title }, '命令行启动方式'),
526
+ el('p', { style: st.title }, '开启后会发生什么'),
523
527
  el('p', { style: st.sub },
524
- '开启开关后,本插件会把 webserver.host 写入当前 profile 的 cordis.patch.yml,' +
525
- '由 dsh 的 HMR 热重载生效。如果你希望手动启动(无需桌面端),可预先创建 overlay:'),
526
- el('pre', { style: st.code },
527
- '# remote-control.patch.yml\n' +
528
- '- id: webserver\n' +
529
- ' config:\n' +
530
- " host: '0.0.0.0'\n"),
528
+ '本插件会在局域网网卡上旁挂一个转发听众,把手机的访问转发到本机回环监听。' +
529
+ 'dsh 自身的监听地址与端口不发生变化,因此本机窗口、桌面端、会话与端口都不会被打断,' +
530
+ '开关即时生效、无需重启,也不会修改任何配置文件。'),
531
531
  el('p', { style: st.hint },
532
- '然后以 dsh --profile web --patch remote-control.patch.yml 启动。'),
532
+ '提示:dsh 0.2.1 起不再允许 webserver 绑定 0.0.0.0(会直接启动失败),' +
533
+ '手写 patch 改绑局域网 IP 也会让本机窗口连不上。'),
533
534
  );
534
535
  }
535
536
 
@@ -698,8 +699,8 @@ window.__ModuleLoader__.load({
698
699
  el('p', { style: st.title }, '手机连接'),
699
700
  el('p', { style: st.sub }, '手机与电脑连接同一 Wi-Fi,用相机或浏览器扫码访问')),
700
701
  qrEl,
701
- status && status.url
702
- ? el('a', { href: status.url, style: st.link, target: '_blank', rel: 'noreferrer' }, status.url)
702
+ bareLanUrl
703
+ ? el('a', { href: bareLanUrl, style: st.link, target: '_blank', rel: 'noreferrer' }, bareLanUrl)
703
704
  : el('p', { style: st.hint }, fmtUrl(status && status.url))),
704
705
 
705
706
  // 统计
@@ -779,6 +780,7 @@ window.__ModuleLoader__.load({
779
780
  status && status.lanAddresses.length
780
781
  ? el('div', { style: st.card },
781
782
  el('p', { style: st.title }, '手机访问地址'),
783
+ el('p', { style: st.hint }, '直接打开即可,首次访问会自动完成连接授权'),
782
784
  el('pre', { style: st.code },
783
785
  status.lanAddresses.map(function (ip) { return 'http://' + ip + ':' + status.port; }).join('\n')))
784
786
  : null,