@linxin666/dsh-remote-web-ui 0.2.4 → 0.2.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.
Files changed (90) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +148 -77
  3. package/README.zh.md +32 -26
  4. package/lib/client.js +603 -68
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +667 -32
  7. package/lib/mobile.js +3 -1
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/FenceNotice.d.ts +21 -0
  10. package/lib/types/client/FenceNotice.d.ts.map +1 -0
  11. package/lib/types/client/FenceNotice.js +10 -0
  12. package/lib/types/client/RemoteEntry.d.ts.map +1 -1
  13. package/lib/types/client/RemoteEntry.js +18 -11
  14. package/lib/types/client/RemotePanel.d.ts +10 -4
  15. package/lib/types/client/RemotePanel.d.ts.map +1 -1
  16. package/lib/types/client/RemotePanel.js +7 -6
  17. package/lib/types/client/RemoteSettingsCard.d.ts +4 -0
  18. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  19. package/lib/types/client/RemoteSettingsCard.js +3 -1
  20. package/lib/types/client/device-name.d.ts +3 -0
  21. package/lib/types/client/device-name.d.ts.map +1 -0
  22. package/lib/types/client/device-name.js +30 -0
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/client/index.js +48 -0
  25. package/lib/types/client/locales.d.ts +51 -1
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/locales.js +79 -29
  28. package/lib/types/client/pair-api.d.ts +35 -0
  29. package/lib/types/client/pair-api.d.ts.map +1 -1
  30. package/lib/types/client/pair-api.js +39 -0
  31. package/lib/types/client/remote-channel.d.ts +93 -0
  32. package/lib/types/client/remote-channel.d.ts.map +1 -0
  33. package/lib/types/client/remote-channel.js +246 -0
  34. package/lib/types/dsh-home.d.ts +17 -0
  35. package/lib/types/dsh-home.d.ts.map +1 -0
  36. package/lib/types/dsh-home.js +34 -0
  37. package/lib/types/gate.d.ts.map +1 -1
  38. package/lib/types/gate.js +5 -2
  39. package/lib/types/index.d.ts +19 -12
  40. package/lib/types/index.d.ts.map +1 -1
  41. package/lib/types/index.js +73 -5
  42. package/lib/types/loopback-proxy.d.ts +28 -0
  43. package/lib/types/loopback-proxy.d.ts.map +1 -0
  44. package/lib/types/loopback-proxy.js +112 -0
  45. package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
  46. package/lib/types/mobile/views/SessionListView.js +6 -1
  47. package/lib/types/mobile-api.d.ts.map +1 -1
  48. package/lib/types/mobile-api.js +6 -2
  49. package/lib/types/mobile-routes.js +1 -1
  50. package/lib/types/pairing.d.ts +63 -11
  51. package/lib/types/pairing.d.ts.map +1 -1
  52. package/lib/types/pairing.js +137 -20
  53. package/lib/types/pairing.persist.test.js +60 -5
  54. package/lib/types/posture.d.ts +75 -0
  55. package/lib/types/posture.d.ts.map +1 -0
  56. package/lib/types/posture.js +116 -0
  57. package/lib/types/remote-api.d.ts +60 -0
  58. package/lib/types/remote-api.d.ts.map +1 -0
  59. package/lib/types/remote-api.js +181 -0
  60. package/lib/types/remote-methods.d.ts +33 -0
  61. package/lib/types/remote-methods.d.ts.map +1 -0
  62. package/lib/types/remote-methods.js +54 -0
  63. package/lib/types/routes.d.ts +4 -0
  64. package/lib/types/routes.d.ts.map +1 -1
  65. package/lib/types/routes.js +33 -4
  66. package/package.json +2 -2
  67. package/src/client/FenceNotice.tsx +43 -0
  68. package/src/client/RemoteEntry.tsx +19 -9
  69. package/src/client/RemotePanel.tsx +83 -10
  70. package/src/client/RemoteSettingsCard.tsx +16 -0
  71. package/src/client/device-name.ts +31 -0
  72. package/src/client/index.ts +46 -0
  73. package/src/client/locales.ts +79 -29
  74. package/src/client/pair-api.ts +60 -0
  75. package/src/client/remote-channel.ts +269 -0
  76. package/src/client/remote.module.css +240 -16
  77. package/src/client/settings-card.module.css +2 -0
  78. package/src/dsh-home.ts +36 -0
  79. package/src/gate.ts +5 -2
  80. package/src/index.ts +89 -17
  81. package/src/loopback-proxy.ts +126 -0
  82. package/src/mobile/views/SessionListView.tsx +6 -1
  83. package/src/mobile-api.ts +6 -2
  84. package/src/mobile-routes.ts +1 -1
  85. package/src/pairing.persist.test.ts +64 -5
  86. package/src/pairing.ts +167 -20
  87. package/src/posture.ts +154 -0
  88. package/src/remote-api.ts +217 -0
  89. package/src/remote-methods.ts +62 -0
  90. package/src/routes.ts +33 -4
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # node scripts/verify-docs.mjs --write <dir>
5
- README.md: f93e37eae9e0b0293d9f78b590dff7a237b72ad6
6
- README.zh.md: cfea9d0a66185db3af570255a349d01f82fd5efb
5
+ README.md: 290224ef41cca3f8d8820fc37b985edbfe2d6549
6
+ README.zh.md: 10b01e05a61885c53b48b2e9542a269119674f8a
package/README.md CHANGED
@@ -2,24 +2,23 @@
2
2
 
3
3
 
4
4
  English | [中文](README.zh.md)
5
- > Mobile remote control + one-click updates: pair a phone to use the current dsh web workspace remotely; the sidebar checks for a newer dsh-web-ui release after it loads and marks the update button when one is available; clicking it updates the family.
5
+ > Remote access for phones and computers + one-click updates: pair a phone to use the current dsh web workspace remotely, or pair a computer browser through the same token to run the full Web GUI from another device; the sidebar checks for a newer dsh-web-ui release after it loads and marks the update button when one is available; clicking it updates the family.
6
6
 
7
7
  This repository is an external plugin package for DeepSeek Harness (DSH):
8
- scan-to-pair mobile remote control for the dsh web GUI, plus a one-click
9
- self-update for the dsh-web-ui family. It is a single dual-face package — the
10
- host half owns pairing tokens, device sessions, the `/api/pair` route family,
11
- and the `/api/update` surface; the browser half renders the sidebar-foot
12
- entries (the download trigger and an icon-only mobile-remote control beside the settings button),
13
- the pairing panel with a QR code, live device status, and stop/refresh/copy
14
- actions, and the update panel that probes and runs the update.
8
+ pairing-based remote access for the dsh web GUI on phones and computers, plus
9
+ a one-click self-update for the dsh-web-ui family. It is a single
10
+ dual-face package — the host half owns pairing tokens, device sessions, the
11
+ `/api/pair` route family, the gated `/remote` desktop channel, and the
12
+ `/api/update` surface; the browser half renders the sidebar-foot entries (the
13
+ download trigger and an icon-only remote-access entry beside the settings
14
+ button), the pairing panel with a QR code, live device status, an
15
+ authorized-device list, and stop/refresh/copy actions, and the update panel
16
+ that probes and runs the update.
15
17
 
16
18
  ## What it does
17
19
 
18
- - **Entry**: a phone icon beside the settings button in both the expanded sidebar and narrow rail; its tooltip and accessible label say "Mobile remote control".
19
- - **Panel**: "移动端远程控制" title, "扫码或在手机上打开链接,即可远程控制当前工作区"
20
- subtitle, a "手机扫码连接" card with the status area ("等待手机连接" + status
21
- badge), a large QR code, the "无法扫码?可以在手机上打开链接" hint, and three
22
- buttons: 停止 / 刷新二维码 / 复制链接.
20
+ - **Entry**: a phone icon beside the settings button in both the expanded sidebar and narrow rail; its tooltip and accessible label say "Remote access".
21
+ - **Panel**: "Remote access" title, "Pair a phone or another computer to access this workspace remotely" subtitle, a "Pair a device" card with the status area ("Waiting for a device" + status badge), a large QR code, separate phone (`/m/?pair=...`) and computer (`/?pair=...`) links with their own copy buttons, plus Stop / Refresh QR actions and an authorized-device list (a device name inferred from User-Agent, online/offline, last active time, per-device unpair). Credential-bearing device ids and raw User-Agent values are never rendered. Both links share one single-use token, so pairing either device invalidates the other link.
23
22
  - **Phone side**: scanning the QR binds the phone with a one-time,
24
23
  time-limited token and lands it on the **standalone mobile surface at
25
24
  `/m/`** — a thin client purpose-built for a small screen (see
@@ -29,14 +28,55 @@ actions, and the update panel that probes and runs the update.
29
28
  opening a fresh QR link there or pasting a fresh desktop-issued link.
30
29
  The link carries a `workspace` parameter so the phone lands in the same
31
30
  workspace the desktop was looking at.
31
+ - **PC side (remote desktop pairing)**: the same QR link and pairing token
32
+ also pair a PC browser — the phone flow extended to the desktop Web GUI.
33
+ Copy the link from the panel and open it in a browser on another
34
+ computer (LAN URL or tunnel URL); after the accept round trip the full
35
+ Web GUI runs there — workspaces, sessions, chat, model switching — with
36
+ its fenced same-origin traffic on the gated `/remote` channel instead of
37
+ calling loopback-only host routes directly. The
38
+ phone gets `/m/`, the PC gets the desktop UI: one token, one pairing
39
+ flow, two surfaces. An unpaired PC sees a full blocking page with computer
40
+ pairing steps and a retry action, with no workspace data rendered behind it.
32
41
  - **Security**: one active one-time token (a refresh invalidates the old
33
42
  link; an accepted token cannot be reused; tokens expire). 停止 revokes
34
43
  every paired device and the current token — paired devices are cut off on
35
- their next request. When the plugin's `requirePairingForLan` gate is on
36
- (default), every non-loopback `/api` request must carry a live paired
37
- device cookie, so the QR is the only way into a LAN-exposed dsh web.
44
+ their next request. Pairing is this plugin's access control for its own
45
+ remote channels: `/m/api` for the phone and `/remote` for a desktop
46
+ browser opened at a non-loopback origin. Unpaired `/remote` callers
47
+ are refused before the request body is read. Loopback (127.0.0.1) keeps using
48
+ `/api` directly. The default remote-desktop path does not use
49
+ `--trusted-host`: the connection plugin's `/api` fence stays closed for
50
+ public and LAN hosts, and the paired PC rides `/remote` instead.
51
+ `--trusted-host` is a different SDK usage that trusts that host for `/api`
52
+ itself — pairing does not gate `/api` (no plugin can; the fence is the
53
+ SDK's own seam). The posture probe below reports that stance when `/api`
54
+ is reachable without pairing.
55
+ - **Remote desktop channel**: with `requirePairingForLan` on (default), a
56
+ desktop Web GUI opened at the LAN URL or through the tunnel transparently
57
+ rides `/remote` — the same UI, gated by the same pairing cookie. Browser
58
+ requests under `/api`, `/sidebar`, `/git`, and `/pet`, including the known
59
+ event, terminal, and SSH WebSockets, are re-issued to the local web server
60
+ without forwarding the remote Origin or pairing cookie; the authenticated
61
+ proxy supplies its own same-origin browser marker for sibling route fences. The
62
+ SDK's loopback-only privileged methods (native dialogs, the settings and
63
+ credentials plane) stay unreachable from a paired remote desktop, and the
64
+ `/api/pair/*`, `/api/update/*`, `/api/plugin-manager/*`,
65
+ `/api/dsh-desktop-launcher/*` and `/api/dsh-web-ui-settings/*` control
66
+ endpoints stay loopback-only. Unpaired desktop browsers get a persistent
67
+ full-page block instead of data (the page keys off the `unpaired` error
68
+ code, not every 403).
69
+ - **Posture probe**: the plugin probes the SDK `/api` fence with forged
70
+ Host headers (the public base and every LAN base). A 403 is the default
71
+ stance (fence closed; remote access goes through pairing). Anything other
72
+ than a 403 — `--trusted-host`, or the SDK's LAN auto-trust under
73
+ `--host 0.0.0.0` — is surfaced as a CRITICAL log line and a red banner on
74
+ the pairing panel, so the SDK `/api` trust stance is visible rather than
75
+ assumed. A failed probe round drops the in-flight target key so the same
76
+ origins are retried.
38
77
  - **Live status**: the desktop panel mirrors the pairing state in real time
39
- (waiting → connected → disconnected) over an SSE stream.
78
+ (waiting → connected → disconnected) over an SSE stream, including the
79
+ authorized-device list.
40
80
  - **Remote update**: the download trigger in the sidebar foot (left of the
41
81
  phone icon) performs a silent status probe after the sidebar loads. When a
42
82
  newer registry release is available, the trigger shows a dot and the "New
@@ -160,12 +200,18 @@ mounts both halves.
160
200
  sheet with persistent toggles for 工具调用 (tool-call disclosures) and
161
201
  系统提示词 (injected system messages), and a 上下文 usage chip that
162
202
  shows the latest assistant answer's context-fill percentage.
163
- 4. On a secure origin, install the `/m/` page from the browser. The installed app retains the same mobile remote capabilities; when its browser context has no paired-device cookie, paste a fresh desktop-issued pairing link into its pairing screen.
164
- 5. The desktop badge flips to 已连接 in real time; it falls back to
165
- offline/断开 when the phone leaves.
166
- 6. 刷新二维码 invalidates the old link and issues a new one. 停止 revokes
167
- mobile access: paired devices 403 on their next request, including their
168
- live stream.
203
+ 4. On a secure origin, install the `/m/` page from the browser. The installed app retains the same mobile remote capabilities; when its browser context has no paired-device cookie, paste a fresh desktop-issued pairing link into its pairing screen.
204
+ 5. **To pair a PC instead**: copy the same link and open it in a browser on
205
+ the other computer — the desktop URL form (`/?pair=<token>`, not `/m/`).
206
+ After the accept round trip the full Web GUI runs there over the gated
207
+ `/remote/api` channel; unpaired PCs see the guided blocking page and no
208
+ data. One active token pairs one device; mint a fresh QR for the
209
+ next device.
210
+ 6. The desktop badge flips to 已连接 in real time; it falls back to
211
+ offline/断开 when the phone leaves.
212
+ 7. 刷新二维码 invalidates the old link and issues a new one. 停止 revokes
213
+ mobile access: paired devices 403 on their next request, including their
214
+ live stream.
169
215
 
170
216
  The mobile surface is fully self-contained in this plugin: the `/m` page
171
217
  and its data channel (`/m/api`) are served by the plugin's own routes and
@@ -193,12 +239,18 @@ over Server-Sent Events on `/m/api/events.mux`. The canonical `/m/` page owns a
193
239
  browsers that support `field-sizing: content`, the input grows with the
194
240
  draft up to its 120px cap in either mode.
195
241
  - The `/m/` worker uses network-first static-shell fallback and waits for current pages to close before an updated worker activates. It bypasses `/m/api`, `/api`, SSE, and every write request.
196
- - Installing this plugin gates non-loopback `/api` access behind pairing
197
- (see `requirePairingForLan` in `src/index.ts`). A desktop browser opened
198
- via the LAN URL must pair like any remote device; loopback (127.0.0.1)
199
- is unaffected. Set `requirePairingForLan: false` in the profile patch to
200
- restore the open-LAN behavior while keeping tokens/status/revocation.
201
- - Sibling host routes outside `/api` (the right-panel `/aionui-panel/*`
242
+ - Installing this plugin routes fenced non-loopback desktop traffic onto the
243
+ gated `/remote` channel (see `requirePairingForLan` in `src/index.ts`).
244
+ A desktop browser opened via the LAN URL or the tunnel must pair like any
245
+ remote device — the unpaired state shows a persistent blocking page instead of
246
+ data; loopback (127.0.0.1) is unaffected and keeps `/api`. Set
247
+ `requirePairingForLan: false` in the profile patch to keep the desktop on
248
+ plain `/api` (only useful with the open-LAN stance) while keeping
249
+ tokens/status/revocation. Note the underlying `/api` route itself is the
250
+ SDK's: on a `--host 0.0.0.0` bind the SDK auto-trusts LAN literals, so a
251
+ LAN client bypassing the UI can still reach `/api` directly — the posture
252
+ probe reports that stance on the panel.
253
+ - Sibling host routes outside `/api` (`/pet/*`, `/git/*`, the right-panel `/aionui-panel/*`
202
254
  family) can consult this plugin's `remoteWebUiPairing` service: a live
203
255
  paired-device cookie is an allow path, `stop()` still cuts them off, and
204
256
  the service is absent when this plugin is not installed.
@@ -229,34 +281,38 @@ no install, account, or domain needed — and wires everything itself:
229
281
  The QR stays LAN-only until the tunnel reports its URL, and a tunnel
230
282
  restart mints a NEW hostname — the plugin clears the old link and mints a
231
283
  fresh one, so users never touch configuration. Note that a quick tunnel is
232
- public: anyone with the URL can load the static page; the pairing gate is
233
- the real fence, and the phone's data channel (`/m/api`) is protected by
234
- its own paired-device gate plus a method allowlist — the tunneled Host
235
- never needs to enter the connection plugin's trust fence, so **no profile
236
- or harness customization is required for the auto tunnel to work**.
284
+ public: anyone with the URL can load the static page; the paired-device
285
+ cookie gates are the real fence (the phone's `/m/api` with its method
286
+ allowlist, the remote desktop's `/remote/api`). The connection plugin's
287
+ `/api` fence refuses the tunneled host outright — the tunneled Host never
288
+ enters the connection trust fence — so **no profile or harness
289
+ customization is required for the auto tunnel to work**. `--trusted-host`
290
+ is not part of this path: a paired PC uses `/remote/api`.
237
291
 
238
292
  ### Manual tunnels (bring your own)
239
293
 
240
294
  The QR link is normally a LAN URL, so a phone outside the house cannot use
241
295
  it. Point a tunnel at the dsh web port and tell the plugin its public
242
- address — the QR is then built from the tunnel URL and the phone-facing
243
- pairing fence trusts the tunneled host. Two knobs are involved:
296
+ address — the QR is then built from the tunnel URL. One knob is involved;
297
+ `--trusted-host` is a separate SDK flag, not part of this pairing path:
244
298
 
245
299
  - **`publicBaseUrl`** (plugin config, in the profile patch or the settings
246
300
  card): the public origin, e.g. `https://foo.trycloudflare.com`. The QR
247
301
  link is built from it, and `accept`/`heartbeat`/`status` accept its host.
302
+ A paired desktop browser rides the gated `/remote/api` channel through
303
+ the same origin, so the desktop Web GUI works from anywhere too.
248
304
  Malformed values are ignored with a warning (LAN-only behavior kept).
249
305
  Unpaired callers of `status` see only the pairing-relevant fields (phase /
250
306
  LAN bases); token expiry, device roster, and the tunnel URL require a live
251
307
  device cookie. The accept rate limit partitions its buckets by the
252
308
  client-visible `X-Forwarded-For` hop so one internet client cannot
253
309
  exhaust the shared bucket behind the tunnel.
254
- - **`--trusted-host <authority>`** (dsh web flag): the transport-level
255
- `/api` fence of the connection plugin must accept the public host too —
256
- without it every `/api` request through the tunnel 403s *before* the
257
- pairing layer (the plugin's own fence only covers the `/api/pair`
258
- routes). Pass the public host (or `host:port`) exactly as the tunnel
259
- forwards it.
310
+ - **`--trusted-host <tunnel-domain>`** (optional dsh web flag, not recommended
311
+ for this plugin on security grounds): prefer device pairing so phones use
312
+ `/m/api` and PCs use `/remote/api`. This flag makes the SDK trust the host
313
+ for `/api` itself, so unpaired callers can reach the ungated host API;
314
+ pairing still gates `/m/api` and `/remote/api`, but not `/api`. If it is
315
+ set, the posture probe reports the open `/api` fence.
260
316
 
261
317
  ### Cloudflare Tunnel (quick tunnel — no account, no domain)
262
318
 
@@ -269,21 +325,25 @@ releases). Then:
269
325
  cloudflared tunnel --url http://127.0.0.1:3080
270
326
  # prints something like: https://xxxx-xxxx-xxxx.trycloudflare.com
271
327
 
272
- # 2. Start dsh web with that host trusted (use --host 0.0.0.0 too when LAN
273
- # access should stay available):
274
- dsh web --trusted-host xxxx-xxxx-xxxx.trycloudflare.com
328
+ # 2. Start dsh web as usual. Do not add --trusted-host for the tunnel
329
+ # domain unless you intentionally want the SDK to trust that host for
330
+ # /api (pairing does not cover /api; remote desktop uses /remote/api).
331
+ # Use --host 0.0.0.0 only when LAN access should stay available:
332
+ dsh web
275
333
  ```
276
334
 
277
335
  Then set `publicBaseUrl: https://xxxx-xxxx-xxxx.trycloudflare.com` in the
278
336
  profile patch (or the plugin settings card — it hot-reloads). Open the
279
337
  phone icon at `http://127.0.0.1`, scan the QR from anywhere: the phone
280
- binds, reloads into the mobile surface, and heartbeats keep it online.
338
+ binds, reloads into the mobile surface, and heartbeats keep it online. A
339
+ desktop browser opened at the same tunnel URL pairs the same way and then
340
+ runs the full Web GUI over the gated `/remote/api` channel.
281
341
 
282
342
  Notes:
283
343
 
284
344
  - Quick tunnels are free and need no login, but the hostname is random per
285
- run: every `cloudflared` restart changes it, so update `--trusted-host`
286
- and `publicBaseUrl` together. Cloudflare documents no uptime guarantees;
345
+ run: every `cloudflared` restart changes it, so update `publicBaseUrl`
346
+ with it. Cloudflare documents no uptime guarantees;
287
347
  in-flight-request concurrency is capped (HTTP 429 past it), and **Quick
288
348
  Tunnels do not forward Server-Sent Events**. `Tailscale Serve` (and
289
349
  `tailscale serve` on a single port) behaves the same way. SSE is how the
@@ -296,14 +356,18 @@ Notes:
296
356
  For true real-time push, point the QR at a tunnel that forwards SSE — a
297
357
  Cloudflare **named tunnel** (domain hosted on Cloudflare, see below), or a
298
358
  plain TCP port forward (LAN address, the `tailscale up` virtual-interface
299
- address, or a manual `ssh -L` / cloudflared TCP tunnel to the port).
359
+ address, or a manual `ssh -L` / cloudflared TCP tunnel to the port). A
360
+ remote desktop browser is unaffected: its event streams ride WebSocket
361
+ upgrades, which quick tunnels forward.
300
362
  - A quick tunnel is public: anyone with the URL can load the static page.
301
- The pairing gate is the real fence — unpaired devices get 403 on every
302
- `/api` call — so keep `requirePairingForLan` on.
363
+ The paired-device cookie gates are the real fence (unpaired devices get
364
+ 403 on every `/m/api` and `/remote/api` call) — keep `requirePairingForLan`
365
+ on. `--trusted-host` is not needed for this path and opens SDK `/api`
366
+ outside pairing.
303
367
  - For a stable hostname, create a named tunnel from the Cloudflare
304
- dashboard (Networking → Tunnels; the domain must be hosted on
305
- Cloudflare) and use its hostname in the same two places. Reachability
306
- from mainland China is not guaranteed by Cloudflare; verify locally.
368
+ dashboard (Networking → Tunnels; the domain must be hosted on Cloudflare)
369
+ and use its hostname as `publicBaseUrl` (only). Reachability from
370
+ mainland China is not guaranteed by Cloudflare; verify locally.
307
371
  - Tailscale is an alternative for personal use that needs no plugin
308
372
  changes at all: its virtual-interface address (`100.x.y.z`) shows up in
309
373
  the QR's address picker automatically, and a phone on the same tailnet
@@ -341,9 +405,12 @@ pnpm run build
341
405
  This plugin rides three harness seams that may not exist in older checkouts:
342
406
 
343
407
  - **`api/gate` waterfall** (packages/client/connection): the /api route and
344
- event WebSocket upgrades emit this event after the trust fence so plugins
345
- can enforce application-level access control. Without it, revocation has
346
- no server-side teeth.
408
+ event WebSocket upgrades are *designed* to emit this event after the trust
409
+ fence so plugins can enforce application-level access control. Current
410
+ published SDK lines do NOT emit it (the listener stays mounted for
411
+ deployments that gain the seam). Pairing for the phone and the PC therefore
412
+ lives on this plugin's own routes (`/m/api`, `/remote/api`); the posture
413
+ probe reports the SDK `/api` fence stance rather than assuming it.
347
414
  - **`sidebar.remote` foot seat** (packages/client/ui-sidebar): the sidebar
348
415
  declares and renders the seat the phone entry occupies.
349
416
  - **LAN runtime connection fixes** (host-apiproxy `mintRpcId` fallback for
@@ -380,32 +447,36 @@ Repeat this after any change to the wire contract or the connection loop:
380
447
  (reconnect loops retry until a fresh QR re-pairs).
381
448
 
382
449
  The public path is the same round trip through a tunnel (see "Remote access
383
- over the internet"): loopback mint → phone opens the public QR URL →
384
- accept → full UI. Both `publicBaseUrl` (plugin config) and
385
- `--trusted-host` (dsh web flag) must name the tunneled host; the desktop
386
- panel still opens at `http://127.0.0.1`.
450
+ over the internet"): loopback mint → phone or PC opens the public QR URL →
451
+ accept → UI. Only `publicBaseUrl` (plugin config) names the tunneled host;
452
+ `--trusted-host` is not part of this pairing flow. The desktop panel still
453
+ opens at `http://127.0.0.1`.
387
454
 
388
455
  ## Known Limitations and Deferred Work
389
456
 
390
457
  - **Revocation is per-request**: a paired phone whose request is already in
391
458
  flight when 停止 lands completes that request; the next one 403s.
392
- - **Device sessions are in-memory by default**: pairing state (token + devices)
393
- resets with the `dsh web` process. Setting the `devicesFile` config option
394
- (an absolute JSON path) persists sessions across restarts — the phone-side
395
- cookie already lives 365 days, so with persistence a `dsh web` restart no
396
- longer requires re-scanning the QR. An explicit 停止 (stop) still revokes
397
- immediately and the revocation is persisted. Device ids are session
398
- credentials (the gate authorizes requests by the device id in the cookie),
399
- so the file is written 0600 via a temp file + atomic rename; keep it inside
400
- a 0700 directory such as `$DSH_HOME`. Changing `cookieName` invalidates
401
- existing devices (expected).
402
- - **No per-device management UI**: the panel shows aggregate status
403
- (waiting / connected N / offline); individual device revocation is
404
- deferred.
459
+ - **Paired device sessions persist by default**: device sessions (not the
460
+ one-time QR token) are written to `$DSH_HOME/remote-web-ui-devices.json`
461
+ (0600, temp file + atomic rename). A paired cookie still works after a
462
+ `dsh web` restart. Refreshing the QR still mints a new token; restarting
463
+ does not restore the current QR. An explicit 停止 (stop) or per-device
464
+ 取消配对 still revokes immediately and the revocation is persisted.
465
+ Sessions idle for `idleExpireMs` (default 7 days, no heartbeat or gated
466
+ request) are deleted and must pair again. Device ids are session
467
+ credentials (the gate authorizes requests by the device id in the cookie).
468
+ Override `devicesFile` with another absolute path when needed. Changing
469
+ `cookieName` invalidates existing devices (expected).
470
+ - **Device roster is loopback-only**: the pairing panel lists authorized
471
+ devices using a short name inferred from User-Agent (for example,
472
+ `Windows · Chrome`), online/offline state, and last active time, and can
473
+ unpair one at a time. Credential-bearing device ids and raw User-Agent
474
+ values are not rendered. `/api/pair/status` never returns the device id
475
+ list, even to a paired phone.
405
476
  - **Quick-tunnel hostnames change per run**: a `trycloudflare.com` URL is
406
- random on every `cloudflared` start, so `--trusted-host` and
407
- `publicBaseUrl` must be updated together whenever the tunnel restarts.
408
- A named tunnel (fixed hostname) avoids the churn and is required for a durable installed-PWA address.
477
+ random on every `cloudflared` start, so `publicBaseUrl` must be updated
478
+ whenever the tunnel restarts. A named tunnel (fixed hostname) avoids the
479
+ churn and is required for a durable installed-PWA address.
409
480
  - **PWA is online-first**: only the static shell and offline page are cached. The running DSH host remains required for all mobile remote capabilities; no session, API response, or command is available offline.
410
481
  - **Dev HMR**: `dsh web --dev` polls every roster bundle by path, so
411
482
  rebuilding this package (its own `tsdown --watch`) hot-reloads the client
package/README.zh.md CHANGED
@@ -2,17 +2,20 @@
2
2
 
3
3
  [English](README.md) | 中文
4
4
 
5
- > 移动端远程控制 + 一键远程更新:扫码配对后用手机远程使用当前 dsh web 工作区;侧边栏加载后静默检查 dsh-web-ui 全家桶新版本,发现时标记更新按钮;点击按钮后自动更新全家桶。
5
+ > 手机与电脑远程访问 + 一键远程更新:扫码配对后用手机远程使用当前 dsh web 工作区,同一枚令牌也可配对电脑浏览器、在其他设备上运行完整 Web GUI;侧边栏加载后静默检查 dsh-web-ui 全家桶新版本,发现时标记更新按钮;点击按钮后自动更新全家桶。
6
6
 
7
- 本仓库是 DeepSeek Harness(DSH)的外部插件包:为 dsh web GUI 提供扫码配对式移动端远程控制,外加 dsh-web-ui 全家桶的一键自更新。它是单一双半区包——host 半区持有配对令牌、设备会话、`/api/pair` 路由族与 `/api/update` 面板;浏览器半区渲染侧边栏底部入口(下载触发按钮与设置按钮旁的手机图标入口)、带二维码的配对面板、实时设备状态,以及停止/刷新/复制操作,还渲染探测并执行更新的更新面板。
7
+ 本仓库是 DeepSeek Harness(DSH)的外部插件包:为 dsh web GUI 提供基于配对的手机与电脑远程访问,外加 dsh-web-ui 全家桶的一键自更新。它是单一双半区包——host 半区持有配对令牌、设备会话、`/api/pair` 路由族、门控的 `/remote` 桌面通道与 `/api/update` 面板;浏览器半区渲染侧边栏底部入口(下载触发按钮与设置按钮旁的手机图标入口)、带二维码的配对面板、实时设备状态、已授权设备列表,以及停止/刷新/复制操作,还渲染探测并执行更新的更新面板。
8
8
 
9
9
  ## 功能
10
10
 
11
- - **入口**:设置按钮旁始终显示手机图标;悬停时显示“移动端远程控制”提示。
12
- - **面板**:「移动端远程控制」标题、「扫码或在手机上打开链接,即可远程控制当前工作区」副标题、「手机扫码连接」卡片(含状态区「等待手机连接」+ 状态徽标)、大号二维码、「无法扫码?可以在手机上打开链接」提示,以及三个按钮:停止 / 刷新二维码 / 复制链接。
11
+ - **入口**:设置按钮旁始终显示手机图标;悬停时显示“远程访问”提示。
12
+ - **面板**:「远程访问」标题、「通过手机或另一台电脑配对,远程访问当前工作区」副标题、「设备配对」卡片(含状态区「等待设备连接」+ 状态徽标)、大号二维码、各带复制按钮的手机链接(`/m/?pair=...`)与电脑链接(`/?pair=...`)、停止 / 刷新二维码操作,以及已授权设备列表(根据 User-Agent 推断的设备名称、在线/离线、最近活动时间、单台取消配对)。界面不渲染作为会话凭据的设备 id 或原始 User-Agent。两条链接共用一枚一次性令牌,任意设备配对成功后另一条立即失效。
13
13
  - **手机侧**:扫码将手机与一次性、限时令牌绑定,并落地到 **`/m/` 独立移动端界面**——一款专为小屏设计的轻客户端(见[截图](#截图)),而不是把桌面 UI 塞进手机。该页面可安装为 PWA;每个已安装应用使用自己的已配对设备 cookie,存储隔离的移动端 Web App 需要在该上下文打开新的二维码链接,或粘贴桌面端新复制的链接完成配对。链接携带 `workspace` 参数,手机落地到桌面正在查看的同一工作区。
14
- - **安全**:一个有效的一次性令牌(刷新会使旧链接失效;已接受的令牌不可复用;令牌会过期)。停止会撤销每一台已配对设备与当前令牌——已配对设备在下一次请求时被切断。当插件 `requirePairingForLan` 门开启(默认)时,每个非 loopback 的 `/api` 请求必须携带有效的已配对设备 cookie,因此二维码是进入暴露在局域网上的 dsh web 的唯一途径。
15
- - **实时状态**:桌面面板经 SSE 流实时镜像配对状态(等待 → 已连接 → 已断开)。
14
+ - **PC 侧(远程桌面配对)**:同一份二维码链接和配对令牌也能配对 PC 浏览器——把手机侧配对流扩展到桌面 Web GUI。从面板复制链接,在另一台电脑的浏览器打开(局域网 URL 或隧道 URL 均可);接受往返后,完整 Web GUI 在那台设备上运行——工作区、会话、聊天、切模型——受围栏保护的同源流量走门控的 `/remote` 通道,而不是直接调用仅 loopback 的 host 路由。手机进 `/m/`,PC 进桌面 UI:一枚令牌、一套配对流、两种界面。未配对的 PC 只看到不渲染底层数据的完整阻断页,其中提供电脑配对步骤与「重新检测」操作。
15
+ - **安全**:一个有效的一次性令牌(刷新会使旧链接失效;已接受的令牌不可复用;令牌会过期)。停止会撤销每一台已配对设备与当前令牌——已配对设备在下一次请求时被切断。配对是本插件对自己远程通道的访问控制:手机走 `/m/api`,非 loopback origin 打开的桌面浏览器走 `/remote`。未配对调用方在读取请求体之前就被 `/remote` 拒绝。loopback(127.0.0.1)继续直接使用 `/api`。默认的远程桌面路径不使用 `--trusted-host`:连接插件的 `/api` 围栏对公网和局域网主机保持关闭,已配对 PC 改走 `/remote`。`--trusted-host` 是 SDK 的另一种用法,会让该主机被信任访问 `/api` 本身——配对不门控 `/api`(任何插件都无法做到;围栏是 SDK 自己的接缝)。当 `/api` 无需配对即可到达时,下方姿态探测会报告该姿态。
16
+ - **远程桌面通道**:`requirePairingForLan` 开启(默认)时,经局域网 URL 或隧道打开的桌面 Web GUI 透明地改走 `/remote`——同一套 UI,同一枚配对 cookie 门控。浏览器中 `/api`、`/sidebar`、`/git`、`/pet` 下的请求,以及已知的事件、终端与 SSH WebSocket,会在不转发远程 Origin 或配对 cookie 的前提下重新发给本机 Web Server;认证反代会为兄弟路由围栏提供自己生成的同源浏览器标记。SDK 的 loopback 专属特权方法(原生对话框、settings 与 credentials 面)对已配对远程桌面保持不可达,`/api/pair/*`、`/api/update/*`、`/api/plugin-manager/*`、`/api/dsh-desktop-launcher/*` 与 `/api/dsh-web-ui-settings/*` 控制端点保持仅 loopback。未配对的桌面浏览器看到持久的全页阻断界面而不是数据(阻断页按错误码 `unpaired` 触发,不是任意 403)。
17
+ - **姿态探测**:插件用伪造 Host 头(公网 base 与每个 LAN base)探测 SDK 的 `/api` 围栏。403 是默认姿态(围栏关闭,远程访问走配对)。任何非 403 结果——`--trusted-host`,或 `--host 0.0.0.0` 下 SDK 的局域网自动信任——都会以 CRITICAL 日志与配对面板红色横幅呈现,因此 SDK 的 `/api` 信任姿态是被看见的,而不是被假设的。探测失败会丢掉进行中的目标缓存,相同 origin 会在下次触发时重试。
18
+ - **实时状态**:桌面面板经 SSE 流实时镜像配对状态(等待 → 已连接 → 已断开),并展示已授权设备名单。
16
19
  - **远程更新**:侧边栏加载后,底部的下载触发按钮(手机图标左侧)会静默探测 npm registry 上已安装的 `@linxin666/dsh-*` 全家桶版本;发现可自动更新的新版本时,按钮显示圆点并提供“发现新版本,检查更新”提示。点击按钮打开更新面板;未安装聚合包时,检查和更新覆盖 profile 中 registry 管理的全部 `@linxin666/*` 直接依赖,本地 link / file 开发依赖会被跳过。当存在较新版本时,面板展示版本对比并等待确认——点击「开始更新」后执行更新(在所属 dsh profile 内 `pnpm update --latest`;pnpm 缺失时依次回退 `corepack pnpm`、`npx --yes pnpm`,Windows 上经 `cmd.exe` 执行以解析 npm 安装的 `.cmd` shim;由仅 loopback 的 `/api/update/status` + `/api/update/run` 端点驱动)并请求重启 dsh web 以生效。pnpm 绿色退出后还会对照 registry 复核已装版本:绿色退出但版本纹丝不动(例如 pnpm 的 `minimumReleaseAge` 门禁静默跳过同日发布的新版本)会报告为「未更新成功」并附配置指引,而不是误报成功。锚点自身是本地 link 安装(开发模式)时,只报告 npm 状态而不更新。
17
20
 
18
21
  ## 截图
@@ -63,9 +66,10 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
63
66
  - **亮色优先主题**:界面默认亮色调色板;每个页头内的日/月切换翻到暗色调色板,选择跨访问持久(localStorage),
64
67
  - 消息按桌面折叠纪律渲染:推理隐藏在被折叠的 深度思考 揭示下面,工具调用隐藏在被折叠的 工具 行下面(点击查看每个调用的参数),超长回答藏在显式 展开全文 切换下面,每行带时间,并且 assistant 回复按 GFM Markdown 渲染(标题 / 加粗 / 斜体 / 行内码 / 代码块 / 列表 / 表格 / 引用 / 链接 / 图片;零依赖自写渲染器,先转义再白名单协议,移动端 bundle 体积几乎不变;KaTeX 公式暂不支持,后续单独评估),用户消息保持纯文本,
65
68
  - 输入栏工具条带 **模型** 选择器(provider 分组目录 + 每模型 思考强度 effort 区)与 **权限** 选择器(权限预设;完全权限 需要显式确认步骤)。两者都走 host 自己的 `session.models` / `session.selectModel` RPC 与 `/permission` 命令——手机改的与桌面改的是同一个会话设置——外加 **显示** 弹层(含 工具调用 与 系统提示词 两个持久开关)和一个 上下文 用量 chip(显示最近一次助手回答的上下文占用百分比)。
66
- 4. 在安全 origin 上从浏览器安装 `/m/` 页面。已安装应用保留相同的移动端远程控制能力;其浏览器上下文没有已配对设备 cookie 时,把桌面端新复制的配对链接粘贴进配对页面。
67
- 5. 桌面徽标实时翻到 已连接;手机离开时回落到离线/断开。
68
- 6. 刷新二维码 使旧链接失效并铸一枚新的。停止 撤销移动端访问:已配对设备下一次请求 403,包括其实时流。
69
+ 4. 在安全 origin 上从浏览器安装 `/m/` 页面。已安装应用保留相同的移动端远程控制能力;其浏览器上下文没有已配对设备 cookie 时,把桌面端新复制的配对链接粘贴进配对页面。
70
+ 5. **改为配对 PC**:复制同一份链接,在另一台电脑的浏览器打开——用桌面 URL 形态(`/?pair=<token>`,不是 `/m/`)。接受往返后,完整 Web GUI 在那台设备上经门控的 `/remote/api` 通道运行;未配对的 PC 只看到带操作指引的阻断页且无数据。一枚有效令牌配对一台设备;给下一台设备配对请刷新出新的二维码。
71
+ 6. 桌面徽标实时翻到 已连接;手机离开时回落到离线/断开。
72
+ 7. 刷新二维码 使旧链接失效并铸一枚新的。停止 撤销移动端访问:已配对设备下一次请求 403,包括其实时流。
69
73
 
70
74
  该移动端界面完全自包含在本插件内:`/m/` 页面及其数据通道(`/m/api`)由插件自己的路由伺服,**无需任何 harness 源码改动**——手机的 RPC 调用走插件的 `/m/api` 代理(它委托给 host 的 ApiProxy 服务并自己分页 `session.list`),因此被隧道化的 Host 永远不必进入连接插件的信任围栏。手机受其已配对设备 cookie 与显式方法白名单门控(settings/credentials/host-action 域手机永远不可达;模型读写限制于建议性的 `session.models` / `session.selectModel` 对,Agent 预设访问限制于只读 `agentPreset.list`,创建限制于 `session.create`(工作区 id 加清单中的可选 id——手机绝不自命名工作目录),权限选择器只通过已放行的 `session.prompt` 发送模式无关的 `/permission` 命令);实时流在 `/m/api/events.mux` 上经 Server-Sent Events 送达。规范的 `/m/` 页面拥有同 scope 的 manifest 与 Service Worker;其缓存仅包含静态壳和离线页,绝不包含移动端 API 响应、会话数据或命令。
71
75
 
@@ -73,8 +77,8 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
73
77
 
74
78
  - 移动端输入框默认 Enter 发送(Shift+Enter 换行)。在插件设置卡片(或 profile patch)把 `mobileEnterToSend` 设为 false 后,普通 Enter 改为插入换行,只有「发送」按钮会发送;手机打开聊天时经自己的 `/m/api` 偏好方法读取该开关。在支持 `field-sizing: content` 的浏览器上,输入框随草稿自动增高,最高 120px 封顶(两种模式一致)。
75
79
  - `/m/` Worker 对静态壳使用 network-first 回退,并等待当前页面关闭后才激活更新。它旁路 `/m/api`、`/api`、SSE 与所有写请求。
76
- - 安装本插件会门控非 loopback 的 `/api` 访问于配对之后(见 `src/index.ts` 的 `requirePairingForLan`)。经局域网 URL 打开的桌面浏览器必须像任何远程设备一样配对;loopback(127.0.0.1)不受影响。把 profile patch 里 `requirePairingForLan` 设为 false 可恢复开放局域网行为,同时保留令牌/状态/撤销。
77
- - `/api` 之外的兄弟 host 路由(右侧面板的 `/aionui-panel/*`)可查询本插件的 `remoteWebUiPairing` 服务:有效的已配对设备 cookie 是放行路径,`stop()` 仍会切断它们;未安装本插件时该服务不存在。
80
+ - 安装本插件后,非 loopback 桌面中受围栏保护的流量会改走门控的 `/remote` 通道(见 `src/index.ts` 的 `requirePairingForLan`)。经局域网 URL 或隧道打开的桌面浏览器必须像任何远程设备一样配对——未配对状态显示完整阻断页而不是数据;loopback(127.0.0.1)不受影响,继续使用原始路径。把 profile patch 里 `requirePairingForLan` 设为 false 可让桌面继续走普通 `/api`(仅在开放局域网姿态下有意义),同时保留令牌/状态/撤销。注意 `/api` 路由本体属于 SDK:`--host 0.0.0.0` 绑定时 SDK 自动信任局域网字面量,绕过 UI 的局域网客户端仍能直接访问 `/api`——姿态探测会在面板上报告该姿态。
81
+ - `/api` 之外的兄弟 host 路由(`/pet/*`、`/git/*`、右侧面板的 `/aionui-panel/*`)可查询本插件的 `remoteWebUiPairing` 服务:有效的已配对设备 cookie 是放行路径,`stop()` 仍会切断它们;未安装本插件时该服务不存在。
78
82
  - 二维码链接基于机器的非内部 IPv4 字面量构建;多宿主主机(Wi-Fi + 有线,或代理/VPN 虚拟适配器)会显示单选器供你发布手机实际可达的网络。第一个字面量是默认值。设 `publicBaseUrl` 后,单选器在顶部额外加一项 公网地址——默认二维码改用公网 base,选中局域网字面量会重新铸一枚网内链接。
79
83
  - 配置的 `publicBaseUrl` 本身满足可达绑定需求:`dsh web` 绑定 `127.0.0.1`(不带 `--host 0.0.0.0`)仍能经隧道铸出可用的公网二维码链接。
80
84
 
@@ -86,14 +90,14 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
86
90
 
87
91
  - 铸出的 `https://xxx.trycloudflare.com` URL 成为二维码 base,因此任意地点的手机都能配对。面板显示隧道状态(starting / running / failed 带原因),崩溃按退避自动重启。
88
92
 
89
- 二维码在隧道报告其 URL 前保持仅局域网,且隧道重启会铸一枚**新的** hostname——插件清除旧链接并铸一枚新的,用户永远不必触碰配置。注意 quick tunnel 是公网的:任何拿到 URL 的人都能加载静态页;配对门才是真正的围栏,手机的数据通道(`/m/api`)由自己的已配对设备门加方法白名单保护——被隧道化的 Host 永远不必进入连接插件的信任围栏,因此 **auto tunnel 工作无需任何 profile 或 harness 定制**。
93
+ 二维码在隧道报告其 URL 前保持仅局域网,且隧道重启会铸一枚**新的** hostname——插件清除旧链接并铸一枚新的,用户永远不必触碰配置。注意 quick tunnel 是公网的:任何拿到 URL 的人都能加载静态页;已配对设备 cookie 门才是真正的围栏(手机的 `/m/api` 带方法白名单、远程桌面的 `/remote/api`)。连接插件的 `/api` 围栏则直接拒绝隧道化主机——被隧道化的 Host 永远不进入连接信任围栏——因此 **auto tunnel 工作无需任何 profile 或 harness 定制**。`--trusted-host` 不属于这条路径:已配对 PC 走 `/remote/api`。
90
94
 
91
95
  ### 手动隧道(自带)
92
96
 
93
- 二维码链接通常是局域网 URL,所以家外的手机无法使用。把隧道指向 dsh web 端口,并告知插件其公网地址——二维码随后由隧道 URL 构建,面向手机的配对围栏信任隧道化的主机。涉及两个钮:
97
+ 二维码链接通常是局域网 URL,所以家外的手机无法使用。把隧道指向 dsh web 端口,并告知插件其公网地址——二维码随后由隧道 URL 构建。涉及一个钮;`--trusted-host` 是独立的 SDK flag,不属于这条配对流:
94
98
 
95
- - **`publicBaseUrl`**(插件配置,在 profile patch 或设置卡片里):公网 origin,如 `https://foo.trycloudflare.com`。二维码链接由它构建,`accept`/`heartbeat`/`status` 接受它的主机。畸形值被忽略并告警(保持仅局域网行为)。未配对调用方的 `status` 只看到配对相关字段(phase / 局域网地址);token 过期时间、设备列表与隧道 URL 需要有效设备 cookie。accept 限速按客户端可见的 `X-Forwarded-For` 跳点分桶,避免隧道背后的单个来源耗尽共享桶。
96
- - **`--trusted-host <authority>`**(dsh web flag):连接插件传输层 `/api` 围栏也必须接受公网主机——否则经隧道的每个 `/api` 请求在**配对层之前**就 403(插件自己的围栏只覆盖 `/api/pair` 路由)。请像隧道转发那样精确传入公网主机(或 `host:port`)。
99
+ - **`publicBaseUrl`**(插件配置,在 profile patch 或设置卡片里):公网 origin,如 `https://foo.trycloudflare.com`。二维码链接由它构建,`accept`/`heartbeat`/`status` 接受它的主机。已配对的桌面浏览器经同一 origin 走门控的 `/remote/api` 通道,因此桌面 Web GUI 也能从任意地点使用。畸形值被忽略并告警(保持仅局域网行为)。未配对调用方的 `status` 只看到配对相关字段(phase / 局域网地址);token 过期时间、设备列表与隧道 URL 需要有效设备 cookie。accept 限速按客户端可见的 `X-Forwarded-For` 跳点分桶,避免隧道背后的单个来源耗尽共享桶。
100
+ - **`--trusted-host <隧道域名>`**(可选的 dsh web flag,安全上不建议用于本插件):优先使用设备配对,让手机走 `/m/api`、PC 走 `/remote/api`。该 flag 会让主机被 SDK 信任访问 `/api` 本身,因此未配对调用方可以到达无门控的 host API;配对仍门控 `/m/api` 与 `/remote/api`,但不门控 `/api`。若仍设置,姿态探测会报告敞开的 `/api` 围栏。
97
101
 
98
102
  ### Cloudflare 隧道(quick tunnel——无账号、无域名)
99
103
 
@@ -104,17 +108,19 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
104
108
  cloudflared tunnel --url http://127.0.0.1:3080
105
109
  # 打印类似:https://xxxx-xxxx-xxxx.trycloudflare.com
106
110
 
107
- # 2. 以该主机为信任启动 dsh web(需要保留局域网访问时也用 --host 0.0.0.0):
108
- dsh web --trusted-host xxxx-xxxx-xxxx.trycloudflare.com
111
+ # 2. 照常启动 dsh web。不必为隧道域名加 --trusted-host,除非你有意让 SDK
112
+ # 信任该主机访问 /api(配对不管 /api;远程桌面走 /remote/api)。
113
+ # 仅需保留局域网访问时才加 --host 0.0.0.0:
114
+ dsh web
109
115
  ```
110
116
 
111
- 然后在 profile patch(或插件设置卡片——它会热重载)里设 `publicBaseUrl: https://xxxx-xxxx-xxxx.trycloudflare.com`。在 `http://127.0.0.1` 打开手机图标,从任意处扫码:手机绑定、重载进移动端界面,心跳保持其在线。
117
+ 然后在 profile patch(或插件设置卡片——它会热重载)里设 `publicBaseUrl: https://xxxx-xxxx-xxxx.trycloudflare.com`。在 `http://127.0.0.1` 打开手机图标,从任意处扫码:手机绑定、重载进移动端界面,心跳保持其在线。在同一隧道 URL 打开的桌面浏览器以同样方式配对,随后完整 Web GUI 经门控的 `/remote/api` 通道运行。
112
118
 
113
119
  说明:
114
120
 
115
- - Quick tunnel 免费无需登录,但 hostname 每次运行随机:每次 `cloudflared` 重启都变,所以 `--trusted-host` 与 `publicBaseUrl` 要一起更新。Cloudflare 不保证 uptime;在途请求并发受限(超过返回 HTTP 429),且 **Quick Tunnels 不转发 Server-Sent Events**。`Tailscale Serve`(以及单端口的 `tailscale serve`)行为相同。SSE 是手机**实时接收消息**的方式,所以在 quick tunnel 或 Tailscale Serve 上移动端聊天回退到轮询:手机仍收发消息(其余都走普通 HTTP,可转发),只是新消息可能晚几秒而非即时。SSE 通道一旦静默,插件按短间隔轮询 `session.history`,SSE 恢复后立即恢复流式。要真正实时推送,把二维码指向能转发 SSE 的隧道——Cloudflare **named tunnel**(域名托管在 Cloudflare,见下),或普通 TCP 端口转发(局域网地址、`tailscale up` 虚拟接口地址,或手动 `ssh -L` / 指向端口的 cloudflared TCP 隧道)。
116
- - Quick tunnel 是公网的:任何拿到 URL 的人都能加载静态页。配对门才是真正的围栏——未配对设备每个 `/api` 调用都 403——所以请保持 `requirePairingForLan` 开启。
117
- - 稳定 hostname 可从 Cloudflare 控制台创建 named tunnel(Networking → Tunnels;域名必须托管在 Cloudflare),并在同样两处使用其 hostname。Cloudflare 不保证中国大陆可达性;请本地验证。
121
+ - Quick tunnel 免费无需登录,但 hostname 每次运行随机:每次 `cloudflared` 重启都变,所以 `publicBaseUrl` 要随之更新。Cloudflare 不保证 uptime;在途请求并发受限(超过返回 HTTP 429),且 **Quick Tunnels 不转发 Server-Sent Events**。`Tailscale Serve`(以及单端口的 `tailscale serve`)行为相同。SSE 是手机**实时接收消息**的方式,所以在 quick tunnel 或 Tailscale Serve 上移动端聊天回退到轮询:手机仍收发消息(其余都走普通 HTTP,可转发),只是新消息可能晚几秒而非即时。SSE 通道一旦静默,插件按短间隔轮询 `session.history`,SSE 恢复后立即恢复流式。要真正实时推送,把二维码指向能转发 SSE 的隧道——Cloudflare **named tunnel**(域名托管在 Cloudflare,见下),或普通 TCP 端口转发(局域网地址、`tailscale up` 虚拟接口地址,或手动 `ssh -L` / 指向端口的 cloudflared TCP 隧道)。远程桌面浏览器不受影响:其事件流走 WebSocket upgrade,quick tunnel 可转发。
122
+ - Quick tunnel 是公网的:任何拿到 URL 的人都能加载静态页。已配对设备 cookie 门才是真正的围栏——未配对设备每个 `/m/api` 与 `/remote/api` 调用都 403——所以请保持 `requirePairingForLan` 开启。`--trusted-host` 这条路径用不到,且会在配对之外打开 SDK 的 `/api`。
123
+ - 稳定 hostname 可从 Cloudflare 控制台创建 named tunnel(Networking → Tunnels;域名必须托管在 Cloudflare),其 hostname 只用作 `publicBaseUrl`。Cloudflare 不保证中国大陆可达性;请本地验证。
118
124
  - Tailscale 是无需任何插件改动的个人替代:其虚拟接口地址(`100.x.y.z`)自动出现在二维码的地址选择器中,同一 tailnet 的手机像局域网主机一样到达它。
119
125
 
120
126
  ## 开发
@@ -144,7 +150,7 @@ pnpm run build
144
150
 
145
151
  本插件依托三个在较老 checkout 里可能不存在的 harness seam:
146
152
 
147
- - **`api/gate` 瀑布**(packages/client/connection):/api 路由与事件 WebSocket 升级在信任围栏后发出该事件,插件可据此实施应用层访问控制。没有它,撤销就没有服务端执行力。
153
+ - **`api/gate` 瀑布**(packages/client/connection):/api 路由与事件 WebSocket 升级被*设计*为在信任围栏后发出该事件,插件可据此实施应用层访问控制。当前已发布的 SDK 线**并不**发出它(监听器为将来具备该 seam 的部署保持挂载)。因此手机与 PC 的配对都落在本插件自己的路由上(`/m/api`、`/remote/api`);姿态探测报告 SDK 的 `/api` 围栏姿态,而不是假设它。
148
154
  - **`sidebar.remote` 底部座位**(packages/client/ui-sidebar):侧边栏声明并渲染手机入口占据的座位。
149
155
  - **局域网运行时连接修复**(host-apiproxy 为不安全上下文 origin 的 `mintRpcId` 回退;20260808 分支在 mux 流之后打开 host 流的连接循环):没有它们,浏览器 runtime 根本无法在纯 HTTP 局域网页上运行(本特性的移动端侧)。
150
156
 
@@ -160,14 +166,14 @@ pnpm run build
160
166
  4. 桌面徽标实时翻到 已连接;局域网 origin 的桌面页则显示 配对面板仅限本机使用 横幅且不打开状态流。
161
167
  5. 桌面 停止 切断手机:其下一个 `/api` 请求 403(重连循环重试直到新二维码重新配对)。
162
168
 
163
- 公网路径是经隧道的同一往返(见「通过互联网远程访问」):loopback mint → 手机打开公网二维码 URL → accept → 完整 UI。`publicBaseUrl`(插件配置)与 `--trusted-host`(dsh web flag)都必须命名隧道主机;桌面面板仍在 `http://127.0.0.1` 打开。
169
+ 公网路径是经隧道的同一往返(见「通过互联网远程访问」):loopback mint → 手机或 PC 打开公网二维码 URL → accept → UI。只有 `publicBaseUrl`(插件配置)命名隧道主机;`--trusted-host` 不属于这条配对流。桌面面板仍在 `http://127.0.0.1` 打开。
164
170
 
165
171
  ## 已知限制与待办
166
172
 
167
173
  - **撤销是逐请求的**:已配对手机请求已在 停止 落地时在途,完成该请求;下一个 403。
168
- - **设备会话默认在内存中**:配对状态(token + devices)随 `dsh web` 进程重置;设置 `devicesFile` 配置项(绝对路径 JSON)后可跨重启持久化——手机端 cookie 本有 365 天有效期,持久化后重启 `dsh web` 无需重新扫码(点「停止」主动撤销仍会立即生效并同步落盘)。设备 id 即会话凭证(网关凭 cookie 中的设备 id 放行请求),文件以 0600 权限经临时文件原子写入,建议把文件放置在 0700 目录(如 `$DSH_HOME`)内;变更 `cookieName` 会使旧设备失效(预期行为)。
169
- - **无逐设备管理 UI**:面板显示聚合状态(waiting / connected N / offline);单设备撤销延后。
170
- - **Quick-tunnel hostname 每次运行变化**:`trycloudflare.com` URL 每次 `cloudflared` 启动随机,所以隧道重启时 `--trusted-host` 与 `publicBaseUrl` 必须一起更新。named tunnel(固定 hostname)避免这种抖动,也是持久安装 PWA 地址所必需的。
174
+ - **已配对设备会话默认落盘**:设备会话(不含一次性二维码 token)写入 `$DSH_HOME/remote-web-ui-devices.json`(0600,临时文件加原子 rename)。已配对 cookie 在重启 `dsh web` 后仍然有效。刷新二维码仍会签发新 token;重启不会恢复当前二维码。点「停止」或单台「取消配对」会立即撤销并同步落盘。超过 `idleExpireMs`(默认 7 天,无心跳或门控请求)的会话会被删除,必须重新扫码。设备 id 即会话凭证(网关凭 cookie 中的设备 id 放行请求)。需要时可用 `devicesFile` 覆盖为其他绝对路径。变更 `cookieName` 会使旧设备失效(预期行为)。
175
+ - **设备名单仅本机可见**:配对面板根据 User-Agent 显示精简设备名称(例如 `Windows · Chrome`)、在线/离线状态与最近活动时间,并可单台取消配对。界面不渲染作为会话凭据的设备 id 或原始 User-Agent。`/api/pair/status` 即使对已配对手机也不返回设备 id 名单。
176
+ - **Quick-tunnel hostname 每次运行变化**:`trycloudflare.com` URL 每次 `cloudflared` 启动随机,所以隧道重启时 `publicBaseUrl` 必须随之更新。named tunnel(固定 hostname)避免这种抖动,也是持久安装 PWA 地址所必需的。
171
177
  - **PWA 为在线优先**:只缓存静态壳和离线页。全部移动端远程控制能力仍要求运行中的 DSH host;离线时不提供会话、API 响应或命令。
172
178
  - **开发 HMR**:`dsh web --dev` 按路径轮询每个 roster bundle,因此重建本包(其自己的 `tsdown --watch`)会热重载 client bundle;无 harness 侧 watcher。
173
179