@linxin666/dsh-remote-web-ui 0.3.6 → 0.3.9
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.i18n.yaml +2 -2
- package/README.md +77 -468
- package/README.zh.md +83 -113
- package/lib/client.js +955 -203
- package/lib/client.js.map +1 -1
- package/lib/index.js +1136 -1466
- package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
- package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
- package/lib/types/client/FooterRemoteEntry.js +1 -1
- package/lib/types/client/RemoteEntry.d.ts +7 -4
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +7 -10
- package/lib/types/client/RemotePanel.d.ts +2 -2
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts +9 -5
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +56 -3
- package/lib/types/client/deep-link.d.ts +10 -13
- package/lib/types/client/deep-link.d.ts.map +1 -1
- package/lib/types/client/deep-link.js +14 -74
- package/lib/types/client/index.d.ts +2 -6
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +73 -39
- package/lib/types/client/locales.d.ts +26 -12
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +36 -22
- package/lib/types/client/mobile-adapt.d.ts +40 -0
- package/lib/types/client/mobile-adapt.d.ts.map +1 -0
- package/lib/types/client/mobile-adapt.js +967 -0
- package/lib/types/client/pair-api.d.ts +24 -6
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +9 -14
- package/lib/types/client/remote-channel.d.ts +3 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +35 -3
- package/lib/types/client/settings-form.d.ts +35 -43
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +60 -53
- package/lib/types/firewall.d.ts +75 -0
- package/lib/types/firewall.d.ts.map +1 -0
- package/lib/types/firewall.js +184 -0
- package/lib/types/gate.d.ts +11 -5
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +11 -5
- package/lib/types/index.d.ts +39 -10
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +206 -58
- package/lib/types/inner-auth.d.ts +32 -0
- package/lib/types/inner-auth.d.ts.map +1 -0
- package/lib/types/inner-auth.js +72 -0
- package/lib/types/lan-bind-plan.d.ts +55 -0
- package/lib/types/lan-bind-plan.d.ts.map +1 -0
- package/lib/types/lan-bind-plan.js +60 -0
- package/lib/types/lan-bind.d.ts +69 -0
- package/lib/types/lan-bind.d.ts.map +1 -0
- package/lib/types/lan-bind.js +128 -0
- package/lib/types/loopback-proxy.d.ts +9 -2
- package/lib/types/loopback-proxy.d.ts.map +1 -1
- package/lib/types/loopback-proxy.js +24 -2
- package/lib/types/pairing.d.ts +15 -3
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +20 -6
- package/lib/types/remote-api.d.ts +30 -13
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +55 -42
- package/lib/types/remote-channel-boot.d.ts +21 -2
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +65 -3
- package/lib/types/remote-channel-rules.d.ts +8 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +9 -2
- package/lib/types/remote-methods.d.ts +45 -11
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +56 -27
- package/lib/types/remote-presence-pet.d.ts +51 -0
- package/lib/types/remote-presence-pet.d.ts.map +1 -0
- package/lib/types/remote-presence-pet.js +101 -0
- package/lib/types/routes.d.ts +76 -4
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +336 -19
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +7 -1
- package/package.json +20 -18
- package/src/client/FooterRemoteEntry.tsx +9 -10
- package/src/client/RemoteEntry.tsx +13 -14
- package/src/client/RemotePanel.tsx +5 -16
- package/src/client/RemoteSettingsCard.tsx +72 -13
- package/src/client/deep-link.ts +14 -75
- package/src/client/index.ts +77 -39
- package/src/client/locales.ts +36 -22
- package/src/client/mobile-adapt.ts +913 -0
- package/src/client/pair-api.ts +26 -15
- package/src/client/remote-channel.ts +31 -3
- package/src/client/settings-form.ts +78 -92
- package/src/dsh-home.ts +1 -1
- package/src/firewall.ts +230 -0
- package/src/gate.ts +11 -5
- package/src/index.ts +235 -65
- package/src/inner-auth.ts +85 -0
- package/src/lan-bind-plan.ts +81 -0
- package/src/lan-bind.ts +145 -0
- package/src/loopback-proxy.ts +34 -0
- package/src/pairing.persist.test.ts +1 -1
- package/src/pairing.ts +20 -6
- package/src/remote-api.ts +67 -45
- package/src/remote-channel-boot.ts +70 -3
- package/src/remote-channel-rules.ts +17 -2
- package/src/remote-methods.ts +58 -27
- package/src/remote-presence-pet.ts +129 -0
- package/src/routes.ts +358 -19
- package/src/tunnel.ts +7 -1
- package/assets/apple-touch-icon.png +0 -0
- package/assets/mobile-icon-192.png +0 -0
- package/assets/mobile-icon-512.png +0 -0
- package/assets/mobile-service-worker.js +0 -63
- package/lib/mobile.js +0 -17658
- package/lib/mobile.js.map +0 -1
- package/lib/types/mobile/PairRequiredView.d.ts +0 -7
- package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.js +0 -28
- package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
- package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.test.js +0 -24
- package/lib/types/mobile/api.d.ts +0 -145
- package/lib/types/mobile/api.d.ts.map +0 -1
- package/lib/types/mobile/api.js +0 -103
- package/lib/types/mobile/index.d.ts +0 -10
- package/lib/types/mobile/index.d.ts.map +0 -1
- package/lib/types/mobile/index.js +0 -38
- package/lib/types/mobile/markdown.d.ts +0 -34
- package/lib/types/mobile/markdown.d.ts.map +0 -1
- package/lib/types/mobile/markdown.js +0 -285
- package/lib/types/mobile/markdown.test.d.ts +0 -2
- package/lib/types/mobile/markdown.test.d.ts.map +0 -1
- package/lib/types/mobile/markdown.test.js +0 -79
- package/lib/types/mobile/messages.d.ts +0 -149
- package/lib/types/mobile/messages.d.ts.map +0 -1
- package/lib/types/mobile/messages.js +0 -561
- package/lib/types/mobile/messages.test.d.ts +0 -2
- package/lib/types/mobile/messages.test.d.ts.map +0 -1
- package/lib/types/mobile/messages.test.js +0 -356
- package/lib/types/mobile/mobile-styles.d.ts +0 -6
- package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
- package/lib/types/mobile/mobile-styles.js +0 -1438
- package/lib/types/mobile/mobile-theme.d.ts +0 -22
- package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.js +0 -70
- package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
- package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.test.js +0 -71
- package/lib/types/mobile/mux.d.ts +0 -128
- package/lib/types/mobile/mux.d.ts.map +0 -1
- package/lib/types/mobile/mux.js +0 -305
- package/lib/types/mobile/mux.test.d.ts +0 -2
- package/lib/types/mobile/mux.test.d.ts.map +0 -1
- package/lib/types/mobile/mux.test.js +0 -267
- package/lib/types/mobile/pairing.d.ts +0 -31
- package/lib/types/mobile/pairing.d.ts.map +0 -1
- package/lib/types/mobile/pairing.js +0 -62
- package/lib/types/mobile/rpc.d.ts +0 -38
- package/lib/types/mobile/rpc.d.ts.map +0 -1
- package/lib/types/mobile/rpc.js +0 -83
- package/lib/types/mobile/theme-toggle.d.ts +0 -12
- package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
- package/lib/types/mobile/theme-toggle.js +0 -26
- package/lib/types/mobile/views/App.d.ts +0 -57
- package/lib/types/mobile/views/App.d.ts.map +0 -1
- package/lib/types/mobile/views/App.js +0 -150
- package/lib/types/mobile/views/App.test.d.ts +0 -2
- package/lib/types/mobile/views/App.test.d.ts.map +0 -1
- package/lib/types/mobile/views/App.test.js +0 -52
- package/lib/types/mobile/views/ChatView.d.ts +0 -63
- package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.js +0 -821
- package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.stream.test.js +0 -236
- package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.test.js +0 -718
- package/lib/types/mobile/views/SessionListView.d.ts +0 -27
- package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.js +0 -139
- package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
- package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.test.js +0 -167
- package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.js +0 -93
- package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
- package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
- package/lib/types/mobile-api.d.ts +0 -65
- package/lib/types/mobile-api.d.ts.map +0 -1
- package/lib/types/mobile-api.js +0 -404
- package/lib/types/mobile-pending.d.ts +0 -38
- package/lib/types/mobile-pending.d.ts.map +0 -1
- package/lib/types/mobile-pending.js +0 -59
- package/lib/types/mobile-pwa.d.ts +0 -6
- package/lib/types/mobile-pwa.d.ts.map +0 -1
- package/lib/types/mobile-pwa.js +0 -17
- package/lib/types/mobile-routes.d.ts +0 -13
- package/lib/types/mobile-routes.d.ts.map +0 -1
- package/lib/types/mobile-routes.js +0 -217
- package/lib/types/paired-model-catalog.d.ts +0 -17
- package/lib/types/paired-model-catalog.d.ts.map +0 -1
- package/lib/types/paired-model-catalog.js +0 -311
- package/src/mobile/PairRequiredView.test.tsx +0 -28
- package/src/mobile/PairRequiredView.tsx +0 -58
- package/src/mobile/api.ts +0 -229
- package/src/mobile/index.tsx +0 -41
- package/src/mobile/markdown.test.ts +0 -91
- package/src/mobile/markdown.ts +0 -291
- package/src/mobile/messages.test.ts +0 -402
- package/src/mobile/messages.ts +0 -685
- package/src/mobile/mobile-styles.ts +0 -1439
- package/src/mobile/mobile-theme.test.ts +0 -84
- package/src/mobile/mobile-theme.ts +0 -76
- package/src/mobile/mux.test.ts +0 -314
- package/src/mobile/mux.ts +0 -336
- package/src/mobile/pairing.ts +0 -77
- package/src/mobile/rpc.ts +0 -90
- package/src/mobile/theme-toggle.tsx +0 -47
- package/src/mobile/views/App.test.tsx +0 -62
- package/src/mobile/views/App.tsx +0 -213
- package/src/mobile/views/ChatView.stream.test.tsx +0 -268
- package/src/mobile/views/ChatView.test.tsx +0 -816
- package/src/mobile/views/ChatView.tsx +0 -1342
- package/src/mobile/views/SessionListView.test.tsx +0 -207
- package/src/mobile/views/SessionListView.tsx +0 -244
- package/src/mobile/views/WorkspaceView.test.tsx +0 -227
- package/src/mobile/views/WorkspaceView.tsx +0 -245
- package/src/mobile-api.ts +0 -418
- package/src/mobile-pending.ts +0 -88
- package/src/mobile-pwa.ts +0 -19
- package/src/mobile-routes.ts +0 -231
- package/src/paired-model-catalog.ts +0 -272
package/README.md
CHANGED
|
@@ -1,175 +1,39 @@
|
|
|
1
1
|
# DSH Remote Web UI
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
English | [中文](README.zh.md)
|
|
5
|
-
> Remote access for
|
|
6
|
-
|
|
7
|
-
This repository is an external plugin package for DeepSeek Harness (DSH):
|
|
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 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.
|
|
4
|
+
> Remote access for the dsh web GUI that shares **one interface**: pair a phone or another computer from a QR beside the settings button, and both run the same official Web GUI this machine runs — phones get an injected portrait-touch adaptation, PCs get the full desktop — through one-time pairing tokens and revocable device sessions. A settings toggle binds the server to the LAN, an optional Cloudflare quick tunnel reaches the internet, and the sidebar checks for a newer dsh-web release with one-click self-update.
|
|
5
|
+
|
|
6
|
+
This repository is an external plugin package for DeepSeek Harness (DSH). It is a single dual-face package: the host half owns pairing tokens, device sessions, the `/api/pair` route family, the gated `/remote` channel, the LAN bind toggle, and the `/api/update` surface; the browser half renders the sidebar-foot entries (the download trigger and the remote-access entry beside the settings button), the pairing panel with a QR code, live device status, the authorized-device roster, the settings card, the portrait-touch adaptation layer over the official UI, and the update panel.
|
|
17
7
|
|
|
18
8
|
## What it does
|
|
19
9
|
|
|
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,
|
|
22
|
-
- **Phone side**: scanning the QR binds the phone with a one-time,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- **Security**: one active one-time token (a refresh invalidates the old
|
|
42
|
-
link; an accepted token cannot be reused; tokens expire). 停止 revokes
|
|
43
|
-
every paired device and the current token — paired devices are cut off on
|
|
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 while `requirePairingForLan` is
|
|
48
|
-
on (default). Loopback (127.0.0.1) keeps using
|
|
49
|
-
`/api` directly. The default remote-desktop path does not use
|
|
50
|
-
`--trusted-host`: the connection plugin's `/api` fence stays closed for
|
|
51
|
-
public and LAN hosts, and the paired PC rides `/remote` instead.
|
|
52
|
-
`--trusted-host` is a different SDK usage that trusts that host for `/api`
|
|
53
|
-
itself — pairing does not gate `/api` (no plugin can; the fence is the
|
|
54
|
-
SDK's own seam). The posture probe below reports that stance when `/api`
|
|
55
|
-
is reachable without pairing.
|
|
56
|
-
- **Paired model catalog**: authenticated paired devices can use `GET
|
|
57
|
-
/api/pair/model-catalog`, `POST /api/pair/model-catalog/discover`, and
|
|
58
|
-
`POST /api/pair/model-catalog/upsert` to inspect and adopt models for an
|
|
59
|
-
existing active `llm-pi-ai` provider only. The capability fixes the
|
|
60
|
-
provider settings address internally and cannot create providers or read or
|
|
61
|
-
change credentials, general settings, endpoints, headers, or arbitrary
|
|
62
|
-
configuration. The exact routes are served ahead of the connection
|
|
63
|
-
plugin's `/api` prefix, so a paired LAN or tunnel client may call them
|
|
64
|
-
directly, and they carry the same Host/Origin trust fence as the sibling
|
|
65
|
-
`/api/pair` endpoints: loopback, the advertised LAN literals, or the
|
|
66
|
-
configured public base URL. Through the `/remote` desktop channel all
|
|
67
|
-
`/api/pair/*` paths stay loopback-only. Adoption is refused while the
|
|
68
|
-
provider's live model catalog is unavailable or unknown. A provider whose
|
|
69
|
-
resolved `models` list is absent or empty keeps inheriting its installed
|
|
70
|
-
catalog until an unknown custom model must materialize that catalog;
|
|
71
|
-
existing model overrides are preserved and translated into the resulting
|
|
72
|
-
entries. Malformed or conflicting model profiles are refused instead of
|
|
73
|
-
being destructively rewritten. Stop or device
|
|
74
|
-
revocation disables it immediately; the
|
|
75
|
-
generic `settings.*`, `credentials.*`, and `llm.discoverModels` RPC methods
|
|
76
|
-
remain loopback-only.
|
|
77
|
-
- **Remote desktop channel**: with `requirePairingForLan` on (default), a
|
|
78
|
-
desktop Web GUI opened at the LAN URL or through the tunnel transparently
|
|
79
|
-
rides `/remote` — the same UI, gated by the same pairing cookie. The
|
|
80
|
-
rewrite installs twice-removed from the race it fixes: the host inlines a
|
|
81
|
-
small classic script right after the opening `<head>` tag
|
|
82
|
-
(`webserver/index-inject`), so fetch/WebSocket/EventSource and resource
|
|
83
|
-
`src` rewrites are active before any boot entry (the connection plugin
|
|
84
|
-
opens its event streams first); the browser half then adopts the installed
|
|
85
|
-
seat instead of patching twice. Loopback origins skip the patch entirely. Browser
|
|
86
|
-
requests under `/api`, `/sidebar`, `/git`, and `/pet`, including the known
|
|
87
|
-
event, terminal, and SSH WebSockets, are re-issued to the local web server
|
|
88
|
-
without forwarding the remote Origin or pairing cookie; the authenticated
|
|
89
|
-
proxy supplies its own same-origin browser marker for sibling route fences. The
|
|
90
|
-
SDK's loopback-only privileged methods (native dialogs, the settings and
|
|
91
|
-
credentials plane) stay unreachable from a paired remote desktop;
|
|
92
|
-
`/api/pair/*` (including the paired model-catalog routes),
|
|
93
|
-
`/api/update/*`, `/api/plugin-manager/*`, `/api/dsh-desktop-launcher/*`
|
|
94
|
-
and `/api/dsh-web-ui-settings/*` control endpoints stay loopback-only. Unpaired desktop browsers get a persistent
|
|
95
|
-
full-page block instead of data (the page keys off the `unpaired` error
|
|
96
|
-
code, not every 403). The block retires once a gated call succeeds or the
|
|
97
|
-
channel itself is torn down — turning `requirePairingForLan` off (or the
|
|
98
|
-
plugin off) also clears a notice raised while the channel was briefly
|
|
99
|
-
active.
|
|
100
|
-
- **Posture probe**: the plugin probes the SDK `/api` fence with forged
|
|
101
|
-
Host headers (the public base and every LAN base). A 403 is the default
|
|
102
|
-
stance (fence closed; remote access goes through pairing). Anything other
|
|
103
|
-
than a 403 — `--trusted-host`, or the SDK's LAN auto-trust under
|
|
104
|
-
`--host 0.0.0.0` — is surfaced as a CRITICAL log line and a red banner on
|
|
105
|
-
the pairing panel, so the SDK `/api` trust stance is visible rather than
|
|
106
|
-
assumed. A failed probe round drops the in-flight target key so the same
|
|
107
|
-
origins are retried.
|
|
108
|
-
- **Live status**: the desktop panel mirrors the pairing state in real time
|
|
109
|
-
(waiting → connected → disconnected) over an SSE stream, including the
|
|
110
|
-
authorized-device list.
|
|
111
|
-
- **Remote update**: the download trigger in the sidebar foot (left of the
|
|
112
|
-
phone icon) performs a silent status probe after the sidebar loads. When a
|
|
113
|
-
newer registry release is available, the trigger shows a dot and the "New
|
|
114
|
-
version available. Check for updates" tooltip. Clicking the trigger opens the
|
|
115
|
-
update panel, which probes the npm registry for the installed
|
|
116
|
-
`@linxin666/dsh-*` family releases. Without the aggregate package, checks and
|
|
117
|
-
updates cover every registry-managed direct `@linxin666/*` dependency in the
|
|
118
|
-
profile; local link/file development dependencies are skipped. When a newer
|
|
119
|
-
release exists, the panel shows the GitHub release notes grouped into New
|
|
120
|
-
Features / Bug Fixes / Other Changes and waits for confirmation; the exact
|
|
121
|
-
component-version list remains available in a collapsed section. Clicking
|
|
122
|
-
"Update now" runs the update (`pnpm update --latest` inside the
|
|
123
|
-
owning dsh profile; when pnpm is missing it falls back to `corepack pnpm`
|
|
124
|
-
and then `npx --yes pnpm`, and on Windows the command runs through
|
|
125
|
-
`cmd.exe` so npm-installed `.cmd` shims resolve; the loopback-only
|
|
126
|
-
`/api/update/status` + `/api/update/run` endpoints drive it) and asks for
|
|
127
|
-
a dsh web restart to pick it up. After a green pnpm exit the installed
|
|
128
|
-
versions are re-checked against the registry: a green exit that left every
|
|
129
|
-
version in place (e.g. the pnpm `minimumReleaseAge` gate silently skipping
|
|
130
|
-
same-day releases) is reported as a stale update with configuration
|
|
131
|
-
guidance instead of a false success. When the anchor itself is a local link
|
|
132
|
-
install (development mode), it reports the npm state without updating.
|
|
133
|
-
|
|
134
|
-
## Screenshots
|
|
135
|
-
|
|
136
|
-
The phone surface on a 390pt viewport. Light is the default theme; a
|
|
137
|
-
sun/moon toggle in every header flips to the dark palette at any time.
|
|
138
|
-
|
|
139
|
-
- **Workspaces** — the roster, each row a workspace with its own sessions:
|
|
140
|
-

|
|
141
|
-
- **Sessions** — one workspace's sessions, headed by the 新建会话 button
|
|
142
|
-
(creates a blank session attached to the workspace and opens it
|
|
143
|
-
immediately):
|
|
144
|
-

|
|
145
|
-
- **Chat** — messages with the desktop fold discipline (collapsed
|
|
146
|
-
深度思考 reasoning and 工具 tool-call rows), a pinned composer with
|
|
147
|
-
模型 / 权限 chips, and a live stream while the agent works:
|
|
148
|
-

|
|
149
|
-
- **Model picker** — the bottom sheet with a provider-grouped catalog and a
|
|
150
|
-
思考强度 section per model (the same `session.models` directory the
|
|
151
|
-
desktop uses):
|
|
152
|
-

|
|
10
|
+
- **Entry**: a phone icon beside the settings button in both the expanded sidebar and the narrow rail; its tooltip and accessible label say "Remote access".
|
|
11
|
+
- **Panel**: "Remote access" title, a "Pair a device" card with the status area ("Waiting for a device" + status badge), a large QR code, the pairing link with its copy button, Stop / Refresh QR actions, and the authorized-device roster (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 the phone and the PC use the same single-use link.
|
|
12
|
+
- **Phone side**: scanning the QR binds the phone with a one-time, time-limited token and reloads into the **official Web GUI** — there is no second interface to drift from. While the phone is in portrait, the plugin injects a touch-adaptation layer over the running UI (see "The mobile adaptation layer" below). The accept chain is cookieless (`/pair-accept` → `/pair-app`): the plugin itself serves the official shell, so the phone needs no harness browser-auth cookie at all. A reopen service worker (https origins) then owns later navigations to `/` from history, bookmarks, or tab restore, so the phone reopens straight into the app instead of a 401 dead end (see Security model).
|
|
13
|
+
- **PC side**: the same link opens the full desktop Web GUI on another computer over the gated `/remote` channel; unpaired PCs see a guided blocking page (with a manual pair-token input) and no workspace data behind it.
|
|
14
|
+
- **Security**: one active token at a time (a refresh invalidates the old link; the link stays re-usable within its expiry window so a scan handed between browsers can complete pairing; it expires). 停止 revokes every paired device and the current token — the `/remote` channel cuts them off on their next request. Pairing is this plugin's access control for the `/remote` channel; direct `/api` on a LAN-exposed bind is governed by the harness fence + browser auth (see Security model). Loopback (127.0.0.1) keeps using `/api` directly. A paired device is a **full-control credential** (see Security model).
|
|
15
|
+
- **LAN bind toggle**: the settings card writes a managed block into the profile `cordis.patch.yml` that pins the webserver bind to `0.0.0.0` (on) or `127.0.0.1` (off) — no `--host` command-line dance; an explicit `--host`/`--port` flag still wins. It maintains the matching host firewall rule (Windows Defender via netsh; Linux firewalld/ufw/iptables; other platforms report the firewall as unmanaged) and shows the live bind, the reachable LAN URLs, and the firewall state.
|
|
16
|
+
- **Live status**: the desktop badge flips to 已连接 in real time; an `/api` posture probe reports any host whose `/api` fence the SDK leaves open, and the auto-tunnel state appears on the panel while the tunnel is starting.
|
|
17
|
+
- **One-click self-update**: the sidebar download trigger checks for a newer dsh-web release after load, marks the button when one exists, and runs the verified update (release notes shown in the panel).
|
|
18
|
+
|
|
19
|
+
## The mobile adaptation layer
|
|
20
|
+
|
|
21
|
+
The official desktop layout already auto-collapses the sidebar below 1024px. On top of that, while the viewport is portrait + coarse pointer + narrower than 1100px the plugin injects:
|
|
22
|
+
|
|
23
|
+
- a CSS sheet keyed on CSS-Modules **semantic suffixes** (`[class$="_composerSeat"]`) so the selectors survive official rebuilds that only change hashes: 44px touch targets on the collapsed rail, 16px inputs (prevents iOS focus zoom), safe-area padding for the composer, compact type for the message list and sidebar, a column layout for the settings modal, and a full-width PlanReview card;
|
|
24
|
+
- a **draggable whale button** as the entry to the collapsed sidebar (position persisted; wired to the official `ctx.layout.toggleSidebar()`), hidden while the sidebar is expanded;
|
|
25
|
+
- **gestures**: left-swipe collapses the sidebar, right-swipe in the conversation opens it; long-pressing a session row opens the same action menu as the desktop ellipsis; tapping a session row or anywhere outside the sidebar collapses it again;
|
|
26
|
+
- **input behavior for touch**: Enter only inserts a newline (send goes through the send button), programmatic composer focus is suppressed (no spurious keyboards), and official tooltip bubbles are hidden because taps leave them stuck;
|
|
27
|
+
- **mobile plugin scope**: while the layer is active, the right-hand details column and the desktop-oriented tool surfaces (SSH terminal, skill explorer, task board, git graph, pet, perf, usage) are hidden - keyed on the L2 semantic roots (`data-dsh-plugin`), so ownership stays with the declaring plugin and class churn cannot resurrect them. These are render suppressions; the client bundles still load. Activation also closes the details panel through the official `ctx.layout.closeDetails()`.
|
|
28
|
+
- a **manual opt-out**: `sessionStorage.dsh-remote-force-desktop = 1` disables the whole layer; landscape, desktop, and wide viewports are never touched.
|
|
29
|
+
|
|
30
|
+
The paired remote desktop also runs in **host mode**: on this harness line the "configuration plane is local" behavior is a client-side branch on `connection.isLoopback`, and the channel boot script installs the transport hook (`__DSH_TRANSPORT__.ownsHost = true`) on non-loopback origins before any boot entry. Settings, credentials, agent presets, and deliverables therefore work on the phone exactly as on the desktop — every call still rides the gated `/remote` channel. Four control planes stay physically local: `/api/pair/*`, `/api/update/*`, `/api/plugin-manager/*`, and `/api/dsh-desktop-launcher/*`.
|
|
153
31
|
|
|
154
32
|
## Requirements
|
|
155
33
|
|
|
156
|
-
- A DSH installation whose `dsh` CLI supports profiles (`dsh --profile`,
|
|
157
|
-
|
|
158
|
-
- For
|
|
159
|
-
`dsh web --host 0.0.0.0`. With the default `127.0.0.1` bind the panel
|
|
160
|
-
shows an explicit explanation instead of a dead QR code — unless a public
|
|
161
|
-
base URL is configured (see "Remote access over the internet" below),
|
|
162
|
-
which makes the QR reachable from anywhere without rebinding. The panel's
|
|
163
|
-
mint/stop endpoints are loopback-only by design: a desktop browser
|
|
164
|
-
opened at the LAN URL sees a "配对面板仅限本机使用" banner instead —
|
|
165
|
-
open the panel at `http://127.0.0.1` and let the phone use the paired
|
|
166
|
-
link.
|
|
167
|
-
- For the one-click public tunnel (`autoTunnel`), the `cloudflared`
|
|
168
|
-
platform binary ships with the package (its postinstall downloads it; a
|
|
169
|
-
runtime download covers installers that skip postinstall scripts). No
|
|
170
|
-
user-side tooling, account, or domain is needed — a Cloudflare quick
|
|
171
|
-
tunnel is free and anonymous.
|
|
172
|
-
- Installing `/m/` as a PWA requires a secure context: `localhost` and `127.0.0.1` work for local use, while phone installation requires HTTPS. Plain LAN HTTP keeps the online mobile remote usable but cannot register its Service Worker.
|
|
34
|
+
- A DSH installation whose `dsh` CLI supports profiles (`dsh --profile`, `dsh plugin`) — the profile/bundle mechanism this package rides on.
|
|
35
|
+
- For LAN use, either flip the **局域网访问** toggle in the settings card (writes the bind block; effective from the next `dsh web` start) or start with `dsh web --host 0.0.0.0`. With the default `127.0.0.1` bind the panel shows an explicit explanation instead of a dead QR code — unless a public base URL is configured (see "Remote access over the internet" below), which makes the QR reachable from anywhere without rebinding. The panel's mint/stop endpoints are loopback-only by design: a desktop browser opened at the LAN URL sees a "配对面板仅限本机使用" banner.
|
|
36
|
+
- For the one-click public tunnel (`autoTunnel`), the `cloudflared` platform binary ships with the package (its postinstall downloads it; a runtime download covers installers that skip postinstall scripts). No user-side tooling, account, or domain is needed.
|
|
173
37
|
|
|
174
38
|
## Install
|
|
175
39
|
|
|
@@ -184,191 +48,30 @@ git clone https://github.com/zhu1090093659/dsh-web.git
|
|
|
184
48
|
cd dsh-web
|
|
185
49
|
pnpm install && pnpm -r build
|
|
186
50
|
dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
|
|
187
|
-
|
|
188
51
|
```
|
|
189
52
|
|
|
190
|
-
Restart the profile (`dsh web`), then open the phone icon in the sidebar
|
|
191
|
-
foot. The plugin's `cordis.patch.yml` inserts the single plugin row that
|
|
192
|
-
mounts both halves.
|
|
53
|
+
Restart the profile (`dsh web`), then open the phone icon in the sidebar foot. The plugin's `cordis.patch.yml` inserts the single plugin row that mounts both halves.
|
|
193
54
|
|
|
194
|
-
> `github:<org>/<repo>` installs work for a standalone repo whose package
|
|
195
|
-
> sits at the root (the `prepare` script builds `lib/` during install;
|
|
196
|
-
> pnpm ≥10 blocks that until you copy the printed key into the profile's
|
|
197
|
-
> `pnpm-workspace.yaml` `allowBuilds` and re-run). Monorepo subpackages
|
|
198
|
-
> use the `link:` form above.
|
|
55
|
+
> `github:<org>/<repo>` installs work for a standalone repo whose package sits at the root (the `prepare` script builds `lib/` during install; pnpm ≥10 blocks that until you copy the printed key into the profile's `pnpm-workspace.yaml` `allowBuilds` and re-run). Monorepo subpackages use the `link:` form above.
|
|
199
56
|
|
|
200
57
|
## Use
|
|
201
58
|
|
|
202
|
-
1.
|
|
203
|
-
2.
|
|
204
|
-
3. Scan with the phone (or open the copied link): the phone
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
mode picker before 新建会话: it selects the usable default preset, or the
|
|
209
|
-
first usable preset, and passes that roster id with the workspace id to
|
|
210
|
-
the host's `session.create`; an empty or unavailable roster keeps the
|
|
211
|
-
host-default creation flow),
|
|
212
|
-
- one workspace's sessions load **incrementally** (20 rows per page,
|
|
213
|
-
"加载更多会话" continues; never the whole list at once),
|
|
214
|
-
- opening a session fetches its chat content **on demand** (history
|
|
215
|
-
pages, "加载更早的消息" goes further back),
|
|
216
|
-
- a live stream shows new messages as they arrive, with a prompt box
|
|
217
|
-
for sending your own (**Enter sends and Shift+Enter inserts a newline
|
|
218
|
-
by default**; set `mobileEnterToSend: false` to make Enter insert a
|
|
219
|
-
newline and reserve sending for the 发送 button),
|
|
220
|
-
- a **light-first theme**: the surface ships a light palette by default;
|
|
221
|
-
a sun/moon toggle in every header flips to the dark palette and the
|
|
222
|
-
choice persists across visits (localStorage),
|
|
223
|
-
- messages render with the desktop fold discipline: reasoning hides
|
|
224
|
-
behind a collapsed 深度思考 disclosure, tool calls behind a collapsed
|
|
225
|
-
工具 row (tap to see each call's arguments), very long answers behind
|
|
226
|
-
an explicit 展开全文 toggle, each row carries its time, and assistant replies render as GFM Markdown (headings / bold / italic / inline code / code blocks / lists / tables / quotes / links / images; a zero-dependency renderer escapes first and allow-lists protocols, so the mobile bundle size barely moves; KaTeX is not supported yet and will be evaluated separately), while user messages stay plain text — and
|
|
227
|
-
- a composer toolbar carries the **model** picker (provider-grouped
|
|
228
|
-
catalog with a 思考强度 effort section per model) and the **权限**
|
|
229
|
-
picker (permission presets; 完全权限 requires an explicit confirm
|
|
230
|
-
step). Both ride the host's own `session.models` /
|
|
231
|
-
`session.selectModel` RPCs and the `/permission` command — the phone
|
|
232
|
-
changes the same session settings the desktop would — plus a **显示**
|
|
233
|
-
sheet with persistent toggles for 工具调用 (tool-call disclosures) and
|
|
234
|
-
系统提示词 (injected system messages), and a 上下文 usage chip that
|
|
235
|
-
shows the latest assistant answer's context-fill percentage.
|
|
236
|
-
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.
|
|
237
|
-
5. **To pair a PC instead**: copy the same link and open it in a browser on
|
|
238
|
-
the other computer — the desktop URL form (`/?pair=<token>`, not `/m/`).
|
|
239
|
-
After the accept round trip the full Web GUI runs there over the gated
|
|
240
|
-
`/remote/api` channel; unpaired PCs see the guided blocking page and no
|
|
241
|
-
data. One active token pairs one device; mint a fresh QR for the
|
|
242
|
-
next device.
|
|
243
|
-
6. The desktop badge flips to 已连接 in real time; it falls back to
|
|
244
|
-
offline/断开 when the phone leaves.
|
|
245
|
-
7. 刷新二维码 invalidates the old link and issues a new one. 停止 revokes
|
|
246
|
-
mobile access: paired devices 403 on their next request, including their
|
|
247
|
-
live stream.
|
|
248
|
-
|
|
249
|
-
The mobile surface is fully self-contained in this plugin: the `/m` page
|
|
250
|
-
and its data channel (`/m/api`) are served by the plugin's own routes and
|
|
251
|
-
need **no harness source changes** — the phone's RPC calls ride the
|
|
252
|
-
plugin's `/m/api` proxy (which delegates to the host ApiProxy service and
|
|
253
|
-
pages `session.list` itself), so the tunneled Host never has to enter the
|
|
254
|
-
connection plugin's trust fence. The phone is gated by its paired-device
|
|
255
|
-
cookie and an explicit method allowlist. The allowlist constrains the
|
|
256
|
-
`/m/api` proxy alone: the paired-device cookie itself also passes the global
|
|
257
|
-
api/gate, so a paired device is a full-control credential for the host `/api`
|
|
258
|
-
surface — settings/credentials/host-action domains stay unreachable only
|
|
259
|
-
because the SDK pins those privileged methods to loopback. Pairing is full
|
|
260
|
-
device trust; the `/m/api` proxy's model
|
|
261
|
-
reads/writes are limited to the advisory `session.models` /
|
|
262
|
-
`session.selectModel` pair, and session control is limited to
|
|
263
|
-
`session.cancel` (stops the active turn while preserving pending queued
|
|
264
|
-
work — the phone has no queue management). Separately, the exact paired-only model-catalog
|
|
265
|
-
routes may adopt models for an existing eligible `llm-pi-ai` provider; they
|
|
266
|
-
cannot create providers or access credentials or general settings. Agent preset
|
|
267
|
-
access to read-only `agentPreset.list`, creation to `session.create`
|
|
268
|
-
(workspace id plus an optional id from that roster — the phone never names a
|
|
269
|
-
working directory of its own), and the permission picker only ever sends the
|
|
270
|
-
mode-agnostic `/permission` command
|
|
271
|
-
through the already-allowlisted `session.prompt`); the live stream arrives
|
|
272
|
-
over Server-Sent Events on `/m/api/events.mux`. The canonical `/m/` page owns a same-scope manifest and Service Worker; its cache is limited to the static shell and an offline page, never mobile API responses, session data, or commands.
|
|
273
|
-
|
|
274
|
-
### Behavior notes
|
|
275
|
-
|
|
276
|
-
- While a turn runs (between its turn/start and turn/end frames), the
|
|
277
|
-
composer's primary button switches from 发送 to a stop button — the same
|
|
278
|
-
Send/Stop switch as the desktop input bar. Tapping it calls
|
|
279
|
-
`session.cancel` to stop the active turn (pending queued prompts resume
|
|
280
|
-
afterwards); the button is disabled while the stop request is in flight
|
|
281
|
-
and flips back to 发送 when the turn ends.
|
|
282
|
-
- The mobile composer sends on Enter by default (Shift+Enter inserts a
|
|
283
|
-
newline). Set `mobileEnterToSend: false` in the plugin settings card (or
|
|
284
|
-
the profile patch) to make plain Enter insert a newline instead; sending
|
|
285
|
-
then happens only through the 发送 button. The phone reads the flag
|
|
286
|
-
through its own `/m/api` preferences method when a chat opens. On
|
|
287
|
-
browsers that support `field-sizing: content`, the input grows with the
|
|
288
|
-
draft up to its 120px cap in either mode.
|
|
289
|
-
- 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.
|
|
290
|
-
- Installing this plugin routes fenced non-loopback desktop traffic onto the
|
|
291
|
-
gated `/remote` channel (see `requirePairingForLan` in `src/index.ts`).
|
|
292
|
-
While the flag is on (default), a desktop browser opened via the LAN URL or
|
|
293
|
-
the tunnel must pair like any remote device — the unpaired state shows a
|
|
294
|
-
persistent blocking page instead of data; loopback (127.0.0.1) is unaffected
|
|
295
|
-
and keeps `/api`. Set `requirePairingForLan: false` in the profile patch to
|
|
296
|
-
keep the desktop on plain `/api` and have a leftover `/remote` rewrite
|
|
297
|
-
proxy unpaired calls (only useful with the open-LAN stance; loopback-only
|
|
298
|
-
paths stay denied) while keeping tokens/status/revocation. Note the
|
|
299
|
-
underlying `/api` route itself is the
|
|
300
|
-
SDK's: on a `--host 0.0.0.0` bind the SDK auto-trusts LAN literals, so a
|
|
301
|
-
LAN client bypassing the UI can still reach `/api` directly — the posture
|
|
302
|
-
probe reports that stance on the panel.
|
|
303
|
-
- Sibling host routes outside `/api` (`/pet/*`, `/git/*`, the right-panel `/aionui-panel/*`
|
|
304
|
-
family) can consult this plugin's `remoteWebUiPairing` service: a live
|
|
305
|
-
paired-device cookie is an allow path, `stop()` still cuts them off, and
|
|
306
|
-
the service is absent when this plugin is not installed.
|
|
307
|
-
- The QR link is built from the machine's non-internal IPv4 literals; a
|
|
308
|
-
multi-homed host (Wi-Fi + wired, or a proxy/VPN virtual adapter) shows a
|
|
309
|
-
radio picker so you can advertise the network the phone can actually
|
|
310
|
-
reach. The first literal is the default. When `publicBaseUrl` is set, the
|
|
311
|
-
picker adds a 公网地址 option on top — the default QR then uses the
|
|
312
|
-
public base, and picking a LAN literal re-mints an in-network link.
|
|
313
|
-
- A configured `publicBaseUrl` satisfies the reachable-bind requirement on
|
|
314
|
-
its own: `dsh web` bound to `127.0.0.1` (no `--host 0.0.0.0`) still mints
|
|
315
|
-
working public QR links through the tunnel.
|
|
59
|
+
1. Open the settings card (设置 → Web 插件 → 远程访问设置) and flip **局域网访问** on if the server binds loopback; the card shows the live bind, the firewall state, and the reachable LAN URLs. The bind change takes effect from the next `dsh web` start.
|
|
60
|
+
2. Start `dsh web`, click the phone icon, and the panel mints a fresh one-time QR.
|
|
61
|
+
3. Scan with the phone (or open the copied link): the device pairs and boots the **official Web GUI** served cookieless by the plugin (`/pair-accept` → `/pair-app`), which reloads into `/`. On a phone, the portrait adaptation layer is already active — same layout as the desktop, same live state. Later reopens from history or a bookmark go straight back into the app (https origins; see Security model).
|
|
62
|
+
4. **To pair a PC instead**: copy the same link and open it in a browser on the other computer. After the same round trip the full Web GUI runs there over the gated `/remote` channel; unpaired PCs see the guided blocking page and no data. One active token pairs one device; mint a fresh QR for the next device.
|
|
63
|
+
5. The desktop badge flips to 已连接 in real time; the device roster lists the paired devices with per-device 取消配对, and 停止 revokes everything.
|
|
64
|
+
6. Configure pairing lifetime, device limits, the LAN fence policy (`requirePairingForLan`), the public base URL, and the auto tunnel in the same settings card.
|
|
316
65
|
|
|
317
66
|
## Remote access over the internet (tunnels)
|
|
318
67
|
|
|
319
68
|
### One-click public tunnel (recommended)
|
|
320
69
|
|
|
321
|
-
|
|
322
|
-
`autoTunnel: true` in the profile patch). The plugin then runs its own
|
|
323
|
-
Cloudflare quick tunnel — the `cloudflared` binary ships with the package,
|
|
324
|
-
no install, account, or domain needed — and wires everything itself:
|
|
325
|
-
|
|
326
|
-
- the minted `https://xxx.trycloudflare.com` URL becomes the QR base, so a
|
|
327
|
-
phone anywhere can pair. The panel shows the tunnel status (starting /
|
|
328
|
-
running / failed with the reason), and a crash is restarted
|
|
329
|
-
automatically with backoff.
|
|
330
|
-
|
|
331
|
-
The QR stays LAN-only until the tunnel reports its URL, and a tunnel
|
|
332
|
-
restart mints a NEW hostname — the plugin clears the old link and mints a
|
|
333
|
-
fresh one, so users never touch configuration. Note that a quick tunnel is
|
|
334
|
-
public: anyone with the URL can load the static page; the paired-device
|
|
335
|
-
cookie gates are the real fence (the phone's `/m/api` with its method
|
|
336
|
-
allowlist, the remote desktop's `/remote/api`). The connection plugin's
|
|
337
|
-
`/api` fence refuses the tunneled host outright — the tunneled Host never
|
|
338
|
-
enters the connection trust fence — so **no profile or harness
|
|
339
|
-
customization is required for the auto tunnel to work**. `--trusted-host`
|
|
340
|
-
is not part of this path: a paired PC uses `/remote/api`.
|
|
70
|
+
Set **自动公网隧道** (autoTunnel) in the settings card. The plugin runs its own Cloudflare quick tunnel (`cloudflared` ships with the package), feeds the minted public URL into the QR base and the pairing fence dynamically, and keeps the posture probe informed — a phone anywhere can pair at any time. The manual public address below is ignored while this is on.
|
|
341
71
|
|
|
342
72
|
### Manual tunnels (bring your own)
|
|
343
73
|
|
|
344
|
-
|
|
345
|
-
it. Point a tunnel at the dsh web port and tell the plugin its public
|
|
346
|
-
address — the QR is then built from the tunnel URL. One knob is involved;
|
|
347
|
-
`--trusted-host` is a separate SDK flag, not part of this pairing path:
|
|
348
|
-
|
|
349
|
-
- **`publicBaseUrl`** (plugin config, in the profile patch or the settings
|
|
350
|
-
card): the public origin, e.g. `https://foo.trycloudflare.com`. The QR
|
|
351
|
-
link is built from it, and `accept`/`heartbeat`/`status` accept its host.
|
|
352
|
-
A paired desktop browser rides the gated `/remote/api` channel through
|
|
353
|
-
the same origin, so the desktop Web GUI works from anywhere too.
|
|
354
|
-
Malformed values are ignored with a warning (LAN-only behavior kept).
|
|
355
|
-
Unpaired callers of `status` see only the pairing-relevant fields (phase /
|
|
356
|
-
LAN bases); token expiry, device roster, and the tunnel URL require a live
|
|
357
|
-
device cookie. The accept rate limit partitions its buckets by the
|
|
358
|
-
client-visible `X-Forwarded-For` hop so one internet client cannot
|
|
359
|
-
exhaust the shared bucket behind the tunnel.
|
|
360
|
-
- **`--trusted-host <tunnel-domain>`** (optional dsh web flag, not recommended
|
|
361
|
-
for this plugin on security grounds): prefer device pairing so phones use
|
|
362
|
-
`/m/api` and PCs use `/remote/api`. This flag makes the SDK trust the host
|
|
363
|
-
for `/api` itself, so unpaired callers can reach the ungated host API;
|
|
364
|
-
pairing still gates `/m/api` and `/remote/api`, but not `/api`. If it is
|
|
365
|
-
set, the posture probe reports the open `/api` fence.
|
|
366
|
-
|
|
367
|
-
### Cloudflare Tunnel (quick tunnel — no account, no domain)
|
|
368
|
-
|
|
369
|
-
Install the client once (macOS: `brew install cloudflared`; other systems:
|
|
370
|
-
grab the `cloudflared-darwin-{arm64,amd64}` binary from the official GitHub
|
|
371
|
-
releases). Then:
|
|
74
|
+
Expose the local port with any tunnel and set **公网地址** (publicBaseUrl), e.g.:
|
|
372
75
|
|
|
373
76
|
```sh
|
|
374
77
|
# 1. Expose the local port (whatever dsh web is listening on):
|
|
@@ -377,58 +80,17 @@ cloudflared tunnel --url http://127.0.0.1:3080
|
|
|
377
80
|
|
|
378
81
|
# 2. Start dsh web as usual. Do not add --trusted-host for the tunnel
|
|
379
82
|
# domain unless you intentionally want the SDK to trust that host for
|
|
380
|
-
# /api
|
|
381
|
-
# Use --host 0.0.0.0 only when LAN access should stay available:
|
|
382
|
-
dsh web
|
|
83
|
+
# /api. Keep the LAN bind off if only tunnel access is wanted.
|
|
383
84
|
```
|
|
384
85
|
|
|
385
|
-
Then set
|
|
386
|
-
profile patch (or the plugin settings card — it hot-reloads). Open the
|
|
387
|
-
phone icon at `http://127.0.0.1`, scan the QR from anywhere: the phone
|
|
388
|
-
binds, reloads into the mobile surface, and heartbeats keep it online. A
|
|
389
|
-
desktop browser opened at the same tunnel URL pairs the same way and then
|
|
390
|
-
runs the full Web GUI over the gated `/remote/api` channel.
|
|
391
|
-
|
|
392
|
-
Notes:
|
|
393
|
-
|
|
394
|
-
- Quick tunnels are free and need no login, but the hostname is random per
|
|
395
|
-
run: every `cloudflared` restart changes it, so update `publicBaseUrl`
|
|
396
|
-
with it. Cloudflare documents no uptime guarantees;
|
|
397
|
-
in-flight-request concurrency is capped (HTTP 429 past it), and **Quick
|
|
398
|
-
Tunnels do not forward Server-Sent Events**. `Tailscale Serve` (and
|
|
399
|
-
`tailscale serve` on a single port) behaves the same way. SSE is how the
|
|
400
|
-
phone receives **live messages** in real time, so over a quick tunnel or
|
|
401
|
-
Tailscale Serve the mobile chat falls back to polling: the phone still
|
|
402
|
-
sends and receives messages (everything else rides plain HTTP, which does
|
|
403
|
-
forward), only a new message may arrive a few seconds late instead of
|
|
404
|
-
instantly. The plugin polls `session.history` on a short interval once the
|
|
405
|
-
SSE channel goes silent, and resumes streaming as soon as SSE works again.
|
|
406
|
-
For true real-time push, point the QR at a tunnel that forwards SSE — a
|
|
407
|
-
Cloudflare **named tunnel** (domain hosted on Cloudflare, see below), or a
|
|
408
|
-
plain TCP port forward (LAN address, the `tailscale up` virtual-interface
|
|
409
|
-
address, or a manual `ssh -L` / cloudflared TCP tunnel to the port). A
|
|
410
|
-
remote desktop browser is unaffected: its event streams ride WebSocket
|
|
411
|
-
upgrades, which quick tunnels forward.
|
|
412
|
-
- A quick tunnel is public: anyone with the URL can load the static page.
|
|
413
|
-
The paired-device cookie gates are the real fence (unpaired devices get
|
|
414
|
-
403 on every `/m/api` and `/remote/api` call) — keep `requirePairingForLan`
|
|
415
|
-
on. `--trusted-host` is not needed for this path and opens SDK `/api`
|
|
416
|
-
outside pairing.
|
|
417
|
-
- For a stable hostname, create a named tunnel from the Cloudflare
|
|
418
|
-
dashboard (Networking → Tunnels; the domain must be hosted on Cloudflare)
|
|
419
|
-
and use its hostname as `publicBaseUrl` (only). Reachability from
|
|
420
|
-
mainland China is not guaranteed by Cloudflare; verify locally.
|
|
421
|
-
- Tailscale is an alternative for personal use that needs no plugin
|
|
422
|
-
changes at all: its virtual-interface address (`100.x.y.z`) shows up in
|
|
423
|
-
the QR's address picker automatically, and a phone on the same tailnet
|
|
424
|
-
reaches it like a LAN host.
|
|
86
|
+
Then set the printed URL as 公网地址. The QR link is built from it, and the plugin's pairing fence accepts the tunneled authority. The posture probe keeps auditing which `/api` authorities the SDK fence leaves open.
|
|
425
87
|
|
|
426
88
|
## Development
|
|
427
89
|
|
|
428
90
|
Work from this repository (no sibling checkout needed):
|
|
429
91
|
|
|
430
92
|
```sh
|
|
431
|
-
cd
|
|
93
|
+
cd dsh-web
|
|
432
94
|
export NPM_TOKEN='<token>' # only if private @deepseek-ai auth is still required
|
|
433
95
|
pnpm install
|
|
434
96
|
pnpm --filter @linxin666/dsh-remote-web-ui run build
|
|
@@ -436,11 +98,7 @@ pnpm --filter @linxin666/dsh-remote-web-ui test
|
|
|
436
98
|
pnpm --filter @linxin666/dsh-remote-web-ui run typecheck
|
|
437
99
|
```
|
|
438
100
|
|
|
439
|
-
The peer APIs come from the official NPM SDK: every `@deepseek-ai/*` package
|
|
440
|
-
used here is declared in devDependencies (rc.6), and TypeScript/Vitest resolve
|
|
441
|
-
types straight from node_modules — no DSH source checkout is required. The
|
|
442
|
-
consumer-side `prepare` build (`tsdown.prepare.config.ts`) transpiles without
|
|
443
|
-
type checking, so git installs work without any harness checkout either.
|
|
101
|
+
The peer APIs come from the official NPM SDK: every `@deepseek-ai/*` package used here is declared in devDependencies (0.1.2-alpha.2 cohort), and TypeScript/Vitest resolve types straight from node_modules — no DSH source checkout is required. The consumer-side `prepare` build (`tsdown.prepare.config.ts`) transpiles without type checking, so git installs work without any harness checkout either.
|
|
444
102
|
|
|
445
103
|
## Checks
|
|
446
104
|
|
|
@@ -452,103 +110,54 @@ pnpm run build
|
|
|
452
110
|
|
|
453
111
|
## Harness contract dependencies
|
|
454
112
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
- **`
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
- **`sidebar.remote` foot seat** (packages/client/ui-sidebar): the sidebar
|
|
465
|
-
declares and renders the seat the phone entry occupies.
|
|
466
|
-
- **LAN runtime connection fixes** (host-apiproxy `mintRpcId` fallback for
|
|
467
|
-
insecure-context origins; the 20260808-branch connection loop opening the
|
|
468
|
-
host stream after the mux stream): without them the browser runtime cannot
|
|
469
|
-
run on a plain-HTTP LAN page at all (the mobile side of this feature).
|
|
470
|
-
|
|
471
|
-
The fence helpers (`isTrustedApiRequest` / `isLoopbackHostname`) are
|
|
472
|
-
reimplemented locally in `src/gate.ts` / `src/routes.ts`: the 20260810
|
|
473
|
-
upstream moved the trust fence inside the connection plugin and stopped
|
|
474
|
-
exporting them, so the pairing routes carry their own copy scoped to the
|
|
475
|
-
literals the QR links advertise.
|
|
476
|
-
See the Agent Notes `api-gate-and-sidebar-remote-seat` and
|
|
477
|
-
`lan-runtime-connection-fixes` in the harness checkout.
|
|
113
|
+
Pinned to the 0.1.2-alpha.2 line; the seams this build relies on:
|
|
114
|
+
|
|
115
|
+
- **`sidebar.footer.action` foot seat** (the 0.1.2 shell composition): the sidebar declares and renders the seat the remote entry occupies.
|
|
116
|
+
- **`ctx.layout.toggleSidebar()`** (packages/client/ui-layout): the whale button expands the collapsed sidebar through the official panel-action face.
|
|
117
|
+
- **`ctx.connection.authenticatedUrl()`** (packages/client/connection): the sanctioned launch-token seam the proxy redeems once for its inner credential (`src/inner-auth.ts`), so re-issued `/api` calls satisfy the harness browser-auth check.
|
|
118
|
+
- **`__DSH_TRANSPORT__.ownsHost`** (client-connection transport hooks): the host-mode flip for paired remote desktops. There is no host-side per-method privilege pin on this line — the configuration plane branches on `connection.isLoopback` in the client — and no `api/gate` waterfall either (the gate listener stays mounted for deployments that gain the seam; pairing enforcement lives on this plugin's own `/remote` channel).
|
|
119
|
+
- **User-patch bind semantics**: same-id patch rows replace the row config wholesale, and the user patch layer cannot evaluate `webStartup`-dependent `!!js` expressions reliably — the LAN bind block therefore materializes static values and the plugin re-asserts it every boot.
|
|
120
|
+
|
|
121
|
+
The fence helpers (`isTrustedApiRequest` / `isLoopbackHostname`) are reimplemented locally in `src/gate.ts` / `src/routes.ts`: the connection plugin stopped exporting them, so the pairing routes carry their own copy scoped to the literals the QR links advertise.
|
|
478
122
|
|
|
479
123
|
## Manual E2E: LAN pairing round trip
|
|
480
124
|
|
|
481
|
-
The unit/component specs cover the route family, the gate, and the
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
(e.g. `http://192.168.1.7:3190/?pair=<token>`): the page accepts, sets
|
|
491
|
-
the HttpOnly `dsh_pair` cookie, reloads, and boots the full UI — no
|
|
492
|
-
console errors, and a generation round trip completes.
|
|
493
|
-
4. The desktop badge flips to 已连接 in real time; a LAN-origin desktop
|
|
494
|
-
page instead shows the 配对面板仅限本机使用 banner and opens no status
|
|
495
|
-
stream.
|
|
496
|
-
5. 停止 on the desktop cuts the phone off: its next `/api` request 403s
|
|
497
|
-
(reconnect loops retry until a fresh QR re-pairs).
|
|
498
|
-
|
|
499
|
-
The public path is the same round trip through a tunnel (see "Remote access
|
|
500
|
-
over the internet"): loopback mint → phone or PC opens the public QR URL →
|
|
501
|
-
accept → UI. Only `publicBaseUrl` (plugin config) names the tunneled host;
|
|
502
|
-
`--trusted-host` is not part of this pairing flow. The desktop panel still
|
|
503
|
-
opens at `http://127.0.0.1`.
|
|
125
|
+
The unit/component specs cover the route family, the gate, the channel, the lan-bind block, and the adaptation layer, but the pairing loop involves a real browser on a non-loopback origin. Repeat this after any change to the wire contract or the connection loop:
|
|
126
|
+
|
|
127
|
+
1. Start an isolated instance: `DSH_HOME=/tmp/dsh-qa dsh --profile web --no-open --port 3191` with the LAN bind toggle on (or a profile whose bind block pins 0.0.0.0).
|
|
128
|
+
2. Open the **loopback** URL (`http://127.0.0.1:3191`) in a browser: the phone icon sits in the sidebar foot; the panel mints a QR whose link is `<lan-url>/pair-accept?pair=<token>`.
|
|
129
|
+
3. In a second tab under mobile emulation (390x844, touch) open that link: the chain `/pair-accept → /pair-app?device=<id> → /` sets the device cookie, serves the patched official shell, and boots the UI — `document.body.classList` carries `dsh-remote-portrait`, the adaptation stylesheet and the whale button exist, and `__DSH_TRANSPORT__.ownsHost` is `true`. The settings surface renders host data (host mode), not a memory mirror. In an https deployment the shell also registers the reopen service worker (`/pair-app.sw.js`); reloading `/` in that tab boots the app instead of the harness 401.
|
|
130
|
+
4. The desktop badge flips to 已连接 in real time; a LAN-origin desktop page instead shows the 配对面板仅限本机使用 banner and opens no status stream.
|
|
131
|
+
5. 停止 on the desktop cuts the device off: its next request 403s with `unpaired` (the fence page offers the manual pair-token input).
|
|
132
|
+
|
|
133
|
+
The public path is the same round trip through a tunnel (see "Remote access over the internet"): loopback mint → device opens the public QR URL → accept → official UI. Only `publicBaseUrl` (plugin config) names the tunneled host; `--trusted-host` is not part of this pairing flow. The desktop panel still opens at `http://127.0.0.1`.
|
|
504
134
|
|
|
505
135
|
## Security model
|
|
506
136
|
|
|
507
|
-
-
|
|
137
|
+
- **Pairing is the access control for the `/remote` channel**: while `requirePairingForLan` is on (default), every request must carry a live paired-device cookie, enforced before any bytes are forwarded. A missing or revoked session receives HTTP 403 with a JSON rejection carrying `error.code: "unpaired"`; the browser's `EventSource` API exposes only the stream failure, not that response body.
|
|
138
|
+
- **The channel carries the process's own inner credential.** The harness browser-auth cookie is authority-bound (minted for the exact `host:port` a browser visited) and has no loopback exemption, so a proxied re-issue to `127.0.0.1` cannot reuse a device's cookie. The plugin therefore redeems its own launch token once — the same exchange a first browser visit performs — and attaches that cookie to re-issued requests. The credential is only ever exercised behind the pairing gate above; 停止/取消配对 immediately stop exercising it.
|
|
139
|
+
- **Cohort reality: pairing does not gate direct `/api`.** On the pinned 0.1.2-alpha.2 line nothing emits the `api/gate` seam, so a direct `/api` call from a LAN origin is governed solely by the harness fence (which auto-trusts LAN literals under a `0.0.0.0` bind) plus the harness browser-auth cookie. A browser credential a device has already redeemed therefore survives 停止/取消配对 until its natural expiry (30 days) — revocation binds the `/remote` channel and the pairing cookie, not that credential. The plugin probes the `/api` posture and warns loudly; treat a LAN-exposed bind as a deliberate decision, and prefer loopback plus tunnels when the machine is shared.
|
|
140
|
+
- **A paired device is a full-control credential.** With host mode active it reaches the complete host API — chat, sessions, settings, credentials, agent presets, deliverables — mirroring the SDK's own stance that a loopback desktop is trusted. Only the four control planes (pairing, self-update, plugin install/remove, desktop launcher) stay physically local. Only pair devices you control; 停止 or per-device 取消配对 revokes immediately.
|
|
141
|
+
- **Control endpoints stay loopback-only**: mint/stop/revoke, the device roster, the lan-bind status, and the update endpoints answer only to loopback. A LAN-origin browser sees the "配对面板仅限本机使用" banner.
|
|
142
|
+
- **The app landing is cookieless-optional.** After pairing, the QR lands the device on `/pair-app`, served by this plugin: the official shell is delivered without passing the harness index gate, and the device credential rides a `x-dsh-remote-device` header (fetch) / `device` query (WebSocket upgrades) that the boot patch attaches from sessionStorage. The flow therefore works when the device browser blocks all cookies; the pairing cookie remains the primary credential where the browser stores it, and the harness browser-auth cookie is no longer required on the phone path at all.
|
|
143
|
+
- **Reopens are owned by a service worker (https origins).** A paired phone returning from history, a bookmark, or tab restore navigates to bare `/` — a path the plugin does not own, where the harness fallback seat answers with its browser-auth 401 (the cookieless flow never holds that credential). The app shell therefore registers `/pair-app.sw.js` (fenced like `/pair-app`; the script is inert logic with no secrets): it intercepts navigations to `/` only, re-serves the shell network-first through `/pair-app` — which re-validates the device cookie and refreshes its presence, so every reopen also keeps the session alive — falls back to the cached shell offline, and passes the navigation through when the plugin no longer answers (a revoked device then sees the harness response or the bilingual re-scan page). Plain-HTTP LAN origins are not secure contexts and never register the worker; there a reopen means scanning a fresh QR.
|
|
144
|
+
- **Revocation is per-request**: a paired device whose request is already in flight when 停止 lands completes that request; the next one 403s.
|
|
145
|
+
- **Paired device sessions persist by default**: device sessions (not the one-time QR token) are written to `$DSH_HOME/remote-web-ui-devices.json` (0600, temp file + atomic rename). A paired cookie still works after a `dsh web` restart. Refreshing the QR still mints a new token; restarting does not restore the current QR. Sessions idle for `idleExpireMs` (default 30 days; the reopen service worker refreshes the window on every navigation it serves) are deleted and must pair again. Device ids are session credentials. Override `devicesFile` with another absolute path when needed. Changing `cookieName` invalidates existing devices (expected).
|
|
146
|
+
- **The LAN bind block owns the webserver row**: while the toggle has been flipped, the managed block pins the bind; the plugin re-asserts it each boot so explicit `--host`/`--port` flags win by rewriting the block. Hand-editing the block is detected and surfaced in the card (`blockHost` shows the literal).
|
|
147
|
+
- **The desktop gate policy is public**: `/api/pair/status` exposes only the boolean `requirePairingForLan` policy so a remote desktop can choose the correct transport before its settings scope is available. This field is not a credential and does not expose tokens, devices, counts, or tunnel URLs.
|
|
148
|
+
- **Quick-tunnel hostnames change per run**: a `trycloudflare.com` URL is random on every `cloudflared` start, so `publicBaseUrl` (or the auto tunnel) must be refreshed with it. A named tunnel avoids the churn.
|
|
508
149
|
|
|
509
150
|
## Known Limitations and Deferred Work
|
|
510
151
|
|
|
511
|
-
- **
|
|
512
|
-
|
|
513
|
-
- **
|
|
514
|
-
|
|
515
|
-
(0600, temp file + atomic rename). A paired cookie still works after a
|
|
516
|
-
`dsh web` restart. Refreshing the QR still mints a new token; restarting
|
|
517
|
-
does not restore the current QR. An explicit 停止 (stop) or per-device
|
|
518
|
-
取消配对 still revokes immediately and the revocation is persisted.
|
|
519
|
-
Sessions idle for `idleExpireMs` (default 7 days, no heartbeat or gated
|
|
520
|
-
request) are deleted and must pair again. Device ids are session
|
|
521
|
-
credentials (the gate authorizes requests by the device id in the cookie).
|
|
522
|
-
Override `devicesFile` with another absolute path when needed. Changing
|
|
523
|
-
`cookieName` invalidates existing devices (expected).
|
|
524
|
-
- **Device roster is loopback-only**: the pairing panel lists authorized
|
|
525
|
-
devices using a short name inferred from User-Agent (for example,
|
|
526
|
-
`Windows · Chrome`), online/offline state, and last active time, and can
|
|
527
|
-
unpair one at a time. Credential-bearing device ids and raw User-Agent
|
|
528
|
-
values are not rendered. `/api/pair/status` never returns the device id
|
|
529
|
-
list, even to a paired phone.
|
|
530
|
-
- **The desktop gate policy is public**: `/api/pair/status` exposes only the
|
|
531
|
-
boolean `requirePairingForLan` policy so a remote desktop can choose the
|
|
532
|
-
correct transport before its settings scope is available. This field is
|
|
533
|
-
not a credential and does not expose tokens, devices, counts, or tunnel URLs.
|
|
534
|
-
- **Quick-tunnel hostnames change per run**: a `trycloudflare.com` URL is
|
|
535
|
-
random on every `cloudflared` start, so `publicBaseUrl` must be updated
|
|
536
|
-
whenever the tunnel restarts. A named tunnel (fixed hostname) avoids the
|
|
537
|
-
churn and is required for a durable installed-PWA address.
|
|
538
|
-
- **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.
|
|
539
|
-
- **Dev HMR**: `dsh web --dev` polls every roster bundle by path, so
|
|
540
|
-
rebuilding this package (its own `tsdown --watch`) hot-reloads the client
|
|
541
|
-
bundle; no harness-side watcher is involved.
|
|
152
|
+
- **The bind change needs a restart**: the live patch watcher cannot rebind a listening socket, and hot reload behavior is profile-shape dependent. The card shows 局域网访问 will apply after the next `dsh web` start (`pendingRestart`).
|
|
153
|
+
- **Plain-HTTP LAN reopens need a re-scan**: the reopen service worker registers only on secure contexts (https tunnels, localhost); a phone paired over a plain-HTTP LAN URL that navigates back to `/` hits the harness 401 and must scan a fresh QR. Pairing again is cheap in-network and restores everything.
|
|
154
|
+
- **The adaptation selectors track the official build**: the semantic-suffix strategy survives hash churn but not semantic renames; each official GUI update needs a visual QA pass (per the dsh-LAN reference, these suffixes have been stable across many official releases).
|
|
155
|
+
- **Dev HMR**: `dsh web --dev` polls every roster bundle by path, so rebuilding this package (its own `tsdown --watch`) hot-reloads the client bundle; the host half needs a restart.
|
|
542
156
|
|
|
543
157
|
## Dependency rationale
|
|
544
158
|
|
|
545
|
-
`qrcode.react` (MIT, actively maintained, React 16–19 support) renders the
|
|
546
|
-
QR as a dependency-free SVG component — no canvas, no server-side image
|
|
547
|
-
generation. It is inlined into the client bundle at build time (like the
|
|
548
|
-
official skin/turtle-ui plugins inline their non-shared deps), so profile
|
|
549
|
-
installations need no extra runtime dependency beyond the dsh peer closure.
|
|
550
|
-
`schemastery` is the DSH-standard config schema validator.
|
|
159
|
+
`qrcode.react` (MIT, actively maintained, React 16–19 support) renders the QR as a dependency-free SVG component — no canvas, no server-side image generation. It is inlined into the client bundle at build time (like the official skin/turtle-ui plugins inline their non-shared deps), so profile installations need no extra runtime dependency beyond the dsh peer closure. `schemastery` is the DSH-standard config schema validator.
|
|
551
160
|
|
|
552
161
|
## Telemetry
|
|
553
162
|
|
|
554
|
-
The browser half sends one anonymous install heartbeat per UTC day to dsh-market.com: a random localStorage id plus this package's name, nothing else. The server stores only a salted hash of that id, never IP addresses, and exposes aggregate counts only. See [docs/telemetry.md](../../docs/telemetry.md) for the full contract.
|
|
163
|
+
The browser half sends one anonymous install heartbeat per UTC day to dsh-market.com: a random localStorage id plus this package's name, nothing else. The server stores only a salted hash of that id, never IP addresses, and exposes aggregate counts only. See [docs/telemetry.md](../../docs/telemetry.md) for the full contract.
|