@linxin666/dsh-remote-web-ui 0.4.6 → 0.4.7
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 +13 -13
- package/README.zh.md +13 -13
- package/lib/client.js +21 -90
- package/lib/client.js.map +1 -1
- package/lib/index.js +398 -43
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +3 -15
- package/lib/types/client/RemotePanel.d.ts +1 -9
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +7 -4
- package/lib/types/client/locales.d.ts +4 -8
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +4 -8
- package/lib/types/client/settings-entry-form.d.ts.map +1 -1
- package/lib/types/client/settings-entry-form.js +0 -1
- package/lib/types/index.d.ts +14 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +67 -17
- package/lib/types/lan-source.d.ts +65 -0
- package/lib/types/lan-source.d.ts.map +1 -0
- package/lib/types/lan-source.js +113 -0
- package/lib/types/lan.d.ts +49 -3
- package/lib/types/lan.d.ts.map +1 -1
- package/lib/types/lan.js +120 -6
- package/lib/types/remote-api.d.ts +7 -2
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +17 -2
- package/lib/types/routes.d.ts +71 -9
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +157 -27
- package/lib/types/tunnel-plan.d.ts +8 -2
- package/lib/types/tunnel-plan.d.ts.map +1 -1
- package/lib/types/tunnel-plan.js +9 -1
- package/package.json +20 -25
- package/src/client/RemoteEntry.tsx +2 -18
- package/src/client/RemotePanel.tsx +9 -43
- package/src/client/locales.ts +4 -8
- package/src/client/remote.module.css +2 -48
- package/src/client/settings-entry-form.ts +0 -1
- package/src/index.ts +77 -17
- package/src/lan-source.ts +132 -0
- package/src/lan.ts +126 -6
- package/src/remote-api.ts +24 -4
- package/src/routes.ts +178 -27
- package/src/tunnel-plan.ts +9 -2
- package/lib/invariant.js +0 -21
- package/lib/types/invariant.d.ts +0 -16
- package/lib/types/invariant.d.ts.map +0 -1
- package/lib/types/invariant.js +0 -23
- package/src/invariant.ts +0 -32
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:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: db16ed3161ef4e3a9b2e7837e0d081c3a2ec7d95
|
|
6
|
+
README.zh.md: 8877617c3c511ad792d7525af0dd5593c6ca8216
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ English | [中文](README.zh.md)
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<img src="https://img.shields.io/npm/v/@linxin666/dsh-client-ui-remote-web-ui?style=flat-square" alt="Version">
|
|
7
7
|
|
|
8
|
-
<img src="https://img.shields.io/badge/DSH-%3E%3D0.2.
|
|
8
|
+
<img src="https://img.shields.io/badge/DSH-%3E%3D0.2.1--rc.2-4c6ef5?style=flat-square&labelColor=454a54" alt="DSH">
|
|
9
9
|
|
|
10
10
|
<img src="https://img.shields.io/badge/license-Apache--2.0-blue?style=flat-square" alt="License">
|
|
11
11
|
</p>
|
|
@@ -47,7 +47,7 @@ The paired remote desktop also runs in **host mode**: on this harness line the "
|
|
|
47
47
|
## Requirements
|
|
48
48
|
|
|
49
49
|
- A DSH installation whose `dsh` CLI supports profiles (`dsh --profile`, `dsh plugin`) — the profile/bundle mechanism this package rides on.
|
|
50
|
-
- 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.
|
|
50
|
+
- 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, after which a phone on the same network is moved off the tunnel onto the LAN origin by the landing page. The panel's mint/stop endpoints are loopback-only by design: a desktop browser opened at the LAN URL sees a "配对面板仅限本机使用" banner.
|
|
51
51
|
- 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.
|
|
52
52
|
- The same binary also powers the fixed-hostname named-tunnel mode (`tunnelToken`): the public hostname never changes, so a paired phone keeps its bookmark and its pairing cookie across restarts and never re-pairs.
|
|
53
53
|
|
|
@@ -74,10 +74,10 @@ Restart the profile (`dsh web`), then open the phone icon in the sidebar foot. T
|
|
|
74
74
|
|
|
75
75
|
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.
|
|
76
76
|
2. Start `dsh web`, click the phone icon, and the panel mints a fresh time-limited QR.
|
|
77
|
-
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).
|
|
77
|
+
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 `/`. When the link names the public base, the landing page also probes the advertised LAN origins (one short no-cors request to `/pair-ping` per candidate) and moves the phone to the first one that answers, remembering the decision for the session: the public page it was served from stays usable throughout, so the probe never blocks pairing, and a phone that is not on that network simply stays where it is. 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).
|
|
78
78
|
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.
|
|
79
79
|
5. The desktop badge flips to 已连接 in real time; the device roster lists the paired devices with per-device 取消配对, and 停止 revokes everything.
|
|
80
|
-
6. Configure pairing lifetime, device limits, the LAN fence policy (`requirePairingForLan`), the public base URL, the auto tunnel, the stable-hostname relay, and the named-tunnel token in the same settings card.
|
|
80
|
+
6. Configure pairing lifetime, device limits, the LAN fence policy (`requirePairingForLan`), the public base URL, the auto tunnel, the stable-hostname relay, and the named-tunnel token in the same settings card. Which network the pair traverses needs no choice: the QR link names the public base and the landing page moves a same-network phone to a LAN origin by itself. If that probe ever misjudges, pin it with the patch-only `pairingSource` key (`auto` | `public` | `lan`); the card carries no control for it, so a save from the card leaves it alone.
|
|
81
81
|
|
|
82
82
|
## Remote access over the internet (tunnels)
|
|
83
83
|
|
|
@@ -136,7 +136,7 @@ pnpm --filter @linxin666/dsh-remote-web-ui test
|
|
|
136
136
|
pnpm --filter @linxin666/dsh-remote-web-ui run typecheck
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
-
The peer APIs come from the official NPM SDK: every `@deepseek-ai/*` package used here is declared in devDependencies (0.2.
|
|
139
|
+
The peer APIs come from the official NPM SDK: every `@deepseek-ai/*` package used here is declared in devDependencies (0.2.1-rc.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.
|
|
140
140
|
|
|
141
141
|
## Checks
|
|
142
142
|
|
|
@@ -148,7 +148,7 @@ pnpm run build
|
|
|
148
148
|
|
|
149
149
|
## Harness contract dependencies
|
|
150
150
|
|
|
151
|
-
Pinned to the 0.2.
|
|
151
|
+
Pinned to the 0.2.1-rc.2 line; the seams this build relies on:
|
|
152
152
|
|
|
153
153
|
- **`sidebar.footer.action` foot seat** (the 0.1.7 shell composition): the sidebar declares and renders the seat the remote entry occupies.
|
|
154
154
|
- **`ctx.layout.toggleSidebar()`** (packages/client/ui-layout): the whale button expands the collapsed sidebar through the official panel-action face.
|
|
@@ -164,24 +164,24 @@ The fence helpers (`isTrustedApiRequest` / `isLoopbackHostname`) are reimplement
|
|
|
164
164
|
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:
|
|
165
165
|
|
|
166
166
|
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).
|
|
167
|
-
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>`.
|
|
168
|
-
3. In a second tab under mobile emulation (390x844, touch) open that link: the chain `/pair-accept → /pair-app?grant=<one-time> → /` sets the device cookie, spends the grant, 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.
|
|
167
|
+
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>`. With a public base configured the link names that origin instead — reachable from anywhere — and the landing does the network choice described under **Use**.
|
|
168
|
+
3. In a second tab under mobile emulation (390x844, touch) open that link: the chain `/pair-accept → /pair-app?grant=<one-time> → /` on an https origin, ending at `/pair-app` instead over plain-HTTP LAN, sets the device cookie, spends the grant, 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. Over plain-HTTP LAN the same tab reloads `/pair-app` and boots the app from the pairing cookie instead.
|
|
169
169
|
4. The desktop badge flips to 已连接 in real time; a LAN-origin desktop page instead shows the 配对面板仅限本机使用 banner and opens no status stream.
|
|
170
170
|
5. 停止 on the desktop cuts the device off: its next request 403s with `unpaired` (the fence page offers the manual pair-token input).
|
|
171
171
|
|
|
172
|
-
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`.
|
|
172
|
+
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, and when the advertised LAN origin actually answers (check with `curl -s -o /dev/null -w '%{http_code}' http://<lan-ip>:<port>/pair-ping`, expecting 204) the landing replaces itself with `http://<lan-ip>:<port>/pair-app?grant=<one-time>` and the UI boots from the LAN origin. Re-scanning the public QR within the same session lands on the public source again, because the decision records the failed switch. 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`.
|
|
173
173
|
|
|
174
174
|
## Security model
|
|
175
175
|
|
|
176
176
|
- **Pairing is the access control for the `/remote` channel, unconditionally**: 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. This gate does **not** follow `requirePairingForLan`: the channel re-issues every call with the process's own browser-auth credential, so admitting an unpaired caller would hand out that authority. The LAN policy governs the plain `/api` surface only and can never widen the channel (issue #1665).
|
|
177
177
|
- **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.
|
|
178
|
-
- **Runtime reality: pairing does not gate direct `/api`.** Nothing in the official runtime emits the `api/gate` seam (verified against the 0.2.
|
|
178
|
+
- **Runtime reality: pairing does not gate direct `/api`.** Nothing in the official runtime emits the `api/gate` seam (verified against the 0.2.1-rc.2 packages installed by this repository), 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.
|
|
179
179
|
- **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 three control planes (pairing, self-update, plugin install/remove) stay physically local. Only pair devices you control; 停止 or per-device 取消配对 revokes immediately.
|
|
180
180
|
- **Control endpoints stay loopback-only**: mint/stop/revoke, the device roster, and the lan-bind status answer only to loopback, and the gated channel keeps the self-update and plugin-install paths physically local. A LAN-origin browser sees the "配对面板仅限本机使用" banner.
|
|
181
181
|
- **Background file uploads ride the channel too.** The official upload service prefers a Web Worker carrier whose own globals no main-thread patch reaches, so the boot patch (and the browser patch as a fallback) publishes the official pre-Cordis hook `__DSH_FILE_UPLOAD__` and hands it the patched `fetch`: the raw `/api/session/uploadFileBinary` POST is rewritten onto `/remote` and carries the device credential exactly like every other gated call. Without the hook a paired browser's uploads bypass the channel and the harness browser-auth fence answers 401 (issue #1580). The hook is only published on non-loopback origins, only while the channel is installed, and never overwrites a hook the page already owns.
|
|
182
182
|
- **The landing URL carries a one-time grant, never the device id.** `/pair-accept` mints a 256-bit, single-consume grant with a 60 s TTL and redirects to `/pair-app?grant=<g>`; the landing spends it — delete-on-attempt, so a replayed or expired grant resolves to nothing — and hands the device id to the shell in its own response. A device id is a live session credential, so it never enters a URL, the address bar, or any log on the redirect path. The pairing cookie is `Secure` whenever the request arrived over TLS (`x-forwarded-proto: https`) and plain over LAN HTTP, where a Secure cookie would simply be dropped by the browser.
|
|
183
183
|
- **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.
|
|
184
|
-
- **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
|
|
184
|
+
- **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 never secure contexts and never register the worker; there the shell keeps `/pair-app` in the address bar instead, so a refresh, a bookmark, or a tab restore re-validates the device cookie through the same device-gated landing (and reaches the bilingual re-pair page once that pairing is gone).
|
|
185
185
|
- **Revocation is per-request**: a paired device whose request is already in flight when 停止 lands completes that request; the next one 403s.
|
|
186
186
|
- **Paired device sessions persist by default**: device sessions (not the time-limited 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). A revocation that cannot be written to disk (read-only DSH_HOME, a full disk) removes the stale store instead of leaving a revoked session on disk, so the next start cannot restore it.
|
|
187
187
|
- **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).
|
|
@@ -193,10 +193,10 @@ The public path is the same round trip through a tunnel (see "Remote access over
|
|
|
193
193
|
## Known Limitations and Deferred Work
|
|
194
194
|
|
|
195
195
|
- **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`).
|
|
196
|
-
- **
|
|
196
|
+
- **A refused worker registration falls back to the harness gate**: the reopen worker registers only where service workers exist. When an otherwise secure origin refuses it (deployment policy, a hardened browser profile), the address bar has already moved to `/` and a reopen needs a fresh QR; the plain-HTTP LAN path has no such gap, because its shell stays on `/pair-app`.
|
|
197
197
|
- **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).
|
|
198
198
|
- **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.
|
|
199
|
-
- **A mounted `@deepseek-ai/dsh-hmr` row blocks every settings save**: the official config editor serializes its writes through `hmr.runExclusive` whenever the `hmr` service is mounted, and on this cohort such a write is rejected as a nested transaction - the Host answers `HMR transactions cannot be nested` and each family settings card (and the official settings pages) reports a failed save. The row is user-added (no bundle ships it); a Desktop profile carries it to hot-reload a local plugin's host half. Remove the `- id: hmr` row from the profile patch to restore saves, and re-add it only while iterating on that plugin. Observed on the Desktop client 0.2.
|
|
199
|
+
- **A mounted `@deepseek-ai/dsh-hmr` row blocks every settings save**: the official config editor serializes its writes through `hmr.runExclusive` whenever the `hmr` service is mounted, and on this cohort such a write is rejected as a nested transaction - the Host answers `HMR transactions cannot be nested` and each family settings card (and the official settings pages) reports a failed save. The row is user-added (no bundle ships it); a Desktop profile carries it to hot-reload a local plugin's host half. Remove the `- id: hmr` row from the profile patch to restore saves, and re-add it only while iterating on that plugin. Observed on the Desktop client 0.2.1-rc.2 (2026-09-30).
|
|
200
200
|
|
|
201
201
|
## Dependency rationale
|
|
202
202
|
|
package/README.zh.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<img src="https://img.shields.io/npm/v/@linxin666/dsh-client-ui-remote-web-ui?style=flat-square" alt="Version">
|
|
7
7
|
|
|
8
|
-
<img src="https://img.shields.io/badge/DSH-%3E%3D0.2.
|
|
8
|
+
<img src="https://img.shields.io/badge/DSH-%3E%3D0.2.1--rc.2-4c6ef5?style=flat-square&labelColor=454a54" alt="DSH">
|
|
9
9
|
|
|
10
10
|
<img src="https://img.shields.io/badge/license-Apache--2.0-blue?style=flat-square" alt="License">
|
|
11
11
|
</p>
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
## 环境要求
|
|
48
48
|
|
|
49
49
|
- DSH 安装的 `dsh` CLI 需支持 profile(`dsh --profile`、`dsh plugin`)——本包依托的 profile/bundle 机制。
|
|
50
|
-
- 局域网使用:在设置卡片打开**局域网访问**(写入绑定块,自下一次 `dsh web` 启动生效),或以 `dsh web --host 0.0.0.0` 启动。默认 `127.0.0.1`
|
|
50
|
+
- 局域网使用:在设置卡片打开**局域网访问**(写入绑定块,自下一次 `dsh web` 启动生效),或以 `dsh web --host 0.0.0.0` 启动。默认 `127.0.0.1` 绑定时面板会显示明确说明而非失效二维码——除非已配置公网地址(见下文「通过互联网远程访问」),此时二维码随处可达而无需重绑,随后落地页会把同一网络里的手机从隧道移到局域网源。面板的铸造/停止端点设计上仅限回环:在局域网地址打开的桌面浏览器会看到「配对面板仅限本机使用」横幅。
|
|
51
51
|
- 一键公网隧道(`autoTunnel`)所需的 `cloudflared` 平台二进制随包分发(postinstall 下载;跳过 postinstall 的安装方式由运行时下载兜底)。无需用户侧工具、账号或域名。
|
|
52
52
|
- 同一二进制也支撑固定域名命名隧道模式(`tunnelToken`):公共主机名永久不变,手机配对一次后书签与配对 Cookie 跨重启持续有效,无需重新配对。
|
|
53
53
|
|
|
@@ -74,10 +74,10 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
|
|
|
74
74
|
|
|
75
75
|
1. 打开设置卡片(设置 → Web 插件 → 远程访问),若服务绑定回环则打开**局域网访问**;卡片展示运行中的绑定、防火墙状态与可达局域网地址。绑定变化自下一次 `dsh web` 启动生效。
|
|
76
76
|
2. 启动 `dsh web`,点手机图标,面板铸造一枚全新的限时二维码。
|
|
77
|
-
3. 手机扫码(或打开复制的链接):设备完成配对,进入由插件不依赖 cookie 交付的**官方 Web GUI**(`/pair-accept` → `/pair-app`),并重载到
|
|
77
|
+
3. 手机扫码(或打开复制的链接):设备完成配对,进入由插件不依赖 cookie 交付的**官方 Web GUI**(`/pair-accept` → `/pair-app`),并重载到 `/`。链接指向公网基址时,落地页还会探测对外广告的局域网源(每个候选向 `/pair-ping` 发一次短超时的 no-cors 请求),把手机切到第一个应答的源,并在会话内记住该决定:它所在的那张公网页全程可用,探测从不阻塞配对;不在同一网络的手机原地不动。手机上竖屏适配层已生效——与桌面同布局、同实时状态。此后的重开(历史、书签)直接回到应用(https 源;见安全模型)。
|
|
78
78
|
4. **改为配对电脑**:复制同一链接,在另一台电脑的浏览器打开。相同往返后完整 Web GUI 在彼处经门控 `/remote` 通道运行;未配对电脑看到引导式拦截页,其后没有数据。一枚令牌配一台设备;下一台设备请刷新二维码。
|
|
79
79
|
5. 桌面徽标实时切换为已连接;设备列表列出已配对设备并支持逐设备取消配对,停止则全部撤销。
|
|
80
|
-
6. 配对有效期、设备上限、局域网栅栏策略(`requirePairingForLan
|
|
80
|
+
6. 配对有效期、设备上限、局域网栅栏策略(`requirePairingForLan`)、公网地址、自动隧道、固定域名中继与命名隧道令牌都在同一设置卡片配置。走哪张网络无需选择:二维码链接固定指向公网基址,手机与本机同网时由落地页自行切到局域网源;若该探测误判,用仅补丁可见的 `pairingSource`(`auto` | `public` | `lan`)钉住选择——卡片没有它的控件,卡片保存不会改动它。
|
|
81
81
|
|
|
82
82
|
## 通过互联网远程访问(隧道)
|
|
83
83
|
|
|
@@ -136,7 +136,7 @@ pnpm --filter @linxin666/dsh-remote-web-ui test
|
|
|
136
136
|
pnpm --filter @linxin666/dsh-remote-web-ui run typecheck
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
-
对端 API 来自官方 NPM SDK:用到的每个 `@deepseek-ai/*` 包都声明在 devDependencies(0.2.
|
|
139
|
+
对端 API 来自官方 NPM SDK:用到的每个 `@deepseek-ai/*` 包都声明在 devDependencies(0.2.1-rc.2 cohort)中,TypeScript/Vitest 直接从 node_modules 解析类型——不需要 DSH 源码 checkout。消费侧 `prepare` 构建(`tsdown.prepare.config.ts`)不做类型检查地转译,git 安装同样无需 harness checkout。
|
|
140
140
|
|
|
141
141
|
## 检查
|
|
142
142
|
|
|
@@ -148,7 +148,7 @@ pnpm run build
|
|
|
148
148
|
|
|
149
149
|
## Harness 契约依赖
|
|
150
150
|
|
|
151
|
-
锚定 0.2.
|
|
151
|
+
锚定 0.2.1-rc.2 线;本构建依赖的接缝:
|
|
152
152
|
|
|
153
153
|
- **`sidebar.footer.action` 底部席位**(0.1.7 shell 组合):侧栏声明并渲染远程入口占据的席位。
|
|
154
154
|
- **`ctx.layout.toggleSidebar()`**(packages/client/ui-layout):鲸鱼按钮经官方面板动作面展开折叠侧栏。
|
|
@@ -164,24 +164,24 @@ pnpm run build
|
|
|
164
164
|
单测/组件 spec 覆盖路由族、栅栏、通道、lan-bind 块与适配层,但配对闭环涉及非回环源上的真实浏览器。凡改动 wire 契约或连接循环之后请重跑:
|
|
165
165
|
|
|
166
166
|
1. 启动隔离实例:`DSH_HOME=/tmp/dsh-qa dsh --profile web --no-open --port 3191`,并打开局域网绑定开关(或使用绑定块固定 0.0.0.0 的 profile)。
|
|
167
|
-
2. 浏览器打开**回环** URL(`http://127.0.0.1:3191`):手机图标位于侧栏底部;面板立即铸造二维码,链接形如 `<lan-url>/pair-accept?pair=<token
|
|
168
|
-
3. 在第二个标签(390x844
|
|
167
|
+
2. 浏览器打开**回环** URL(`http://127.0.0.1:3191`):手机图标位于侧栏底部;面板立即铸造二维码,链接形如 `<lan-url>/pair-accept?pair=<token>`。若配置了公网地址,链接改指该源(随处可达),网络选择由落地页完成,见「使用」一节。
|
|
168
|
+
3. 在第二个标签(390x844 触控模拟)打开该链接:链路在 https 源上是 `/pair-accept → /pair-app?grant=<一次性> → /`,在纯 HTTP 局域网上则停在 `/pair-app`;它设置设备 cookie、消费一次性授权、交付打过补丁的官方应用壳并启动 UI——`document.body.classList` 带 `dsh-remote-portrait`、适配样式表与小鲸鱼按钮存在、`__DSH_TRANSPORT__.ownsHost` 为 `true`。设置面渲染 host 数据(host 模式),而非 memory 镜像。https 部署下应用壳还会注册重开 service worker(`/pair-app.sw.js`);在该标签刷新 `/` 会直接进入应用而不是 harness 401。纯 HTTP 局域网下同一个标签刷新 `/pair-app`,同样凭配对 cookie 进入应用。
|
|
169
169
|
4. 桌面徽标实时切换为已连接;局域网源的桌面页面则显示配对面板仅限本机使用横幅且不开状态流。
|
|
170
170
|
5. 桌面点停止切断设备:下一次请求 403 且 `unpaired`(栅栏页提供手动配对令牌输入)。
|
|
171
171
|
|
|
172
|
-
公网路径是同一条经隧道的往返(见「通过互联网远程访问」):回环铸造 → 设备打开公网二维码 URL → 接受 → 官方 UI
|
|
172
|
+
公网路径是同一条经隧道的往返(见「通过互联网远程访问」):回环铸造 → 设备打开公网二维码 URL → 接受 → 官方 UI;当对外广告的局域网源确实应答时(`curl -s -o /dev/null -w '%{http_code}' http://<lan-ip>:<port>/pair-ping` 应为 204),落地页会把自身替换为 `http://<lan-ip>:<port>/pair-app?grant=<一次性>`,UI 从局域网源启动。同一会话内再次扫公网二维码会重新落在公网源,因为决定里记着这次没有生效的切换。只有 `publicBaseUrl`(插件配置)命名隧道主机;`--trusted-host` 不属于本配对流。桌面面板仍在 `http://127.0.0.1` 打开。
|
|
173
173
|
|
|
174
174
|
## 安全模型
|
|
175
175
|
|
|
176
176
|
- **配对是 `/remote` 通道的访问控制,且无条件生效**:每个请求必须携带有效配对设备 cookie,在任何字节转发之前强制。缺失或被撤销的会话收到 HTTP 403,JSON 拒绝携带 `error.code: "unpaired"`;浏览器 `EventSource` API 只暴露流失败,不暴露响应体。该门**不**跟随 `requirePairingForLan`:通道会用进程自身的浏览器认证凭据重新发起每一次调用,放行未配对调用者等于把该权限交出去。局域网策略只作用于普通 `/api` 面,永远不能放宽本通道(issue #1665)。
|
|
177
177
|
- **通道携带进程自己的内部凭据。** harness 浏览器认证 cookie 与 authority 绑定(为浏览器访问过的确切 `host:port` 签发)且没有回环豁免,因此转发到 `127.0.0.1` 的再发起请求无法复用设备的 cookie。插件因此自行兑换一次自己的启动令牌——与浏览器首次访问执行的是同一次交换——并把所得 cookie 附到再发起请求上。该凭据只在上面的配对门之后被使用;停止/取消配对会立即停止对它的使用。
|
|
178
|
-
- **运行时现实:配对不门控直连 `/api`。** 官方运行时没有任何组件发出 `api/gate` seam(对照本仓库安装的 0.2.
|
|
178
|
+
- **运行时现实:配对不门控直连 `/api`。** 官方运行时没有任何组件发出 `api/gate` seam(对照本仓库安装的 0.2.1-rc.2 包核实),因此来自局域网源头的直连 `/api` 仅由 harness 围栏(`0.0.0.0` 绑定下自动信任局域网字面量)加 harness 浏览器认证 cookie 约束。设备已经兑换过的浏览器凭据在停止/取消配对后仍然有效,直到其自然过期(30 天)——撤销约束的是 `/remote` 通道与配对 cookie,而不是那个凭据。插件会对 `/api` 姿态做探测并大声告警;请把局域网绑定当作深思熟虑的决定,在共享机器上优先回环加隧道。
|
|
179
179
|
- **配对设备是完全控制凭据。** host 模式下它可达完整 host API——聊天、会话、设置、凭据、Agent 预设、产出物——与 SDK 对回环桌面的信任一致。只有三个控制面(配对、自更新、插件安装/卸载)保持物理本地。只配对你控制的设备;停止或逐设备取消配对立即撤销。
|
|
180
180
|
- **控制端点仅限回环**:铸造/停止/撤销、设备列表与 lan-bind 状态只应答回环,门控通道同时让自更新与插件安装路径保持物理本地。局域网源浏览器看到「配对面板仅限本机使用」横幅。
|
|
181
181
|
- **后台文件上传同样走通道。** 官方上传服务优先使用 Web Worker 载体,其独立全局对象不受主线程补丁影响;因此引导补丁(以及作为兜底的浏览器补丁)会发布官方启动前钩子 `__DSH_FILE_UPLOAD__`,并把打过补丁的 `fetch` 交给它:原始 `/api/session/uploadFileBinary` POST 会被改写到 `/remote`,并像其他受门控调用一样携带设备凭据。没有该钩子时,配对浏览器的上传会绕过通道,被 harness 浏览器认证围栏拒为 401(issue #1580)。该钩子仅在非回环源、且通道安装期间发布,且永不覆盖页面已有的钩子。
|
|
182
182
|
- **落地页 URL 只携带一次性授权,绝不携带设备 ID。** `/pair-accept` 签发 256 位、一次性消费、TTL 60 秒的授权并 303 到 `/pair-app?grant=<g>`;落地页消费它——无论成功、过期还是重放都先删除记录,因此重放或过期的授权解析不出任何设备——再由自己的响应把设备 ID 交给应用壳。设备 ID 是活的会话凭据,因此永不进入 URL、地址栏或重定向路径上的任何日志。请求经 TLS 到达时(`x-forwarded-proto: https`)配对 cookie 带 `Secure`,局域网明文 HTTP 下不带——在那里 Secure cookie 会被浏览器直接丢弃。
|
|
183
183
|
- **应用落地页不依赖 cookie。** 配对后二维码把设备带到 `/pair-app`——由本插件直接交付官方应用壳,不经过 harness 索引认证门;设备凭据经 `x-dsh-remote-device` 请求头(fetch)与 `device` 查询参数(WebSocket 升级)由引导补丁从 sessionStorage 挂载。因此手机浏览器完全禁用 cookie 时链路依然成立;有 cookie 时配对 cookie 仍是主凭据,手机路径不再需要 harness 浏览器认证 cookie。
|
|
184
|
-
- **重开由 service worker 接管(仅 https 源)。** 配对过的手机从历史、书签或标签恢复回来时导航到裸 `/`——插件不拥有的路径,harness 兜底座会用浏览器认证 401 应答(不依赖 cookie 的流程永远拿不到那份凭据)。应用壳因此注册 `/pair-app.sw.js`(与 `/pair-app` 同一栅栏;脚本是不含任何秘密的惰性逻辑):只拦截对 `/` 的导航,经 `/pair-app` 网络优先地重发应用壳——同时校验设备 cookie 并刷新其活跃时间,每次重开也在为会话续期——离线时回退缓存的壳,插件不再应答时把导航原样放行(被撤销的设备随后看到 harness 应答或双语重扫页)。纯 HTTP
|
|
184
|
+
- **重开由 service worker 接管(仅 https 源)。** 配对过的手机从历史、书签或标签恢复回来时导航到裸 `/`——插件不拥有的路径,harness 兜底座会用浏览器认证 401 应答(不依赖 cookie 的流程永远拿不到那份凭据)。应用壳因此注册 `/pair-app.sw.js`(与 `/pair-app` 同一栅栏;脚本是不含任何秘密的惰性逻辑):只拦截对 `/` 的导航,经 `/pair-app` 网络优先地重发应用壳——同时校验设备 cookie 并刷新其活跃时间,每次重开也在为会话续期——离线时回退缓存的壳,插件不再应答时把导航原样放行(被撤销的设备随后看到 harness 应答或双语重扫页)。纯 HTTP 的局域网源永远不是安全上下文,因此也不会注册该 worker;那里的应用壳把 `/pair-app` 留在地址栏,刷新、书签或标签恢复都会经同一个设备门控落地页重新校验设备 cookie(配对失效后则落到双语重新配对页)。
|
|
185
185
|
- **撤销按请求生效**:停止落地时已在途的请求会完成;下一个请求 403。
|
|
186
186
|
- **配对设备会话默认持久化**:设备会话(非限时 QR 令牌)写入 `$DSH_HOME/remote-web-ui-devices.json`(0600,临时文件 + 原子改名)。`dsh web` 重启后配对 cookie 依然有效。刷新二维码铸造新令牌;重启不会恢复当前二维码。空闲超过 `idleExpireMs`(默认 30 天;重开 service worker 每次接管导航都会刷新该窗口)的会话被删除并须重新配对。设备 id 即会话凭据。需要时可用 `devicesFile` 指定其他绝对路径。更换 `cookieName` 会使现有设备失效(预期行为)。无法落盘的撤销(DSH_HOME 只读、磁盘写满)会删除过期设备库,而不是把被撤销的会话留在磁盘上,因此下次启动不会恢复它。
|
|
187
187
|
- **局域网绑定块拥有 webserver 行**:开关翻过后受管块固定绑定;插件每次启动重断言,显式 `--host`/`--port` 旗标通过重写块获胜。手工编辑该块会被检测并在卡片展示(`blockHost` 显示字面量)。
|
|
@@ -193,10 +193,10 @@ pnpm run build
|
|
|
193
193
|
## 已知限制与后续工作
|
|
194
194
|
|
|
195
195
|
- **绑定变化需要重启**:补丁热重载监视器无法重绑监听中的套接字,且热重载行为依赖 profile 形态。卡片以 `pendingRestart` 提示局域网访问将在下次 `dsh web` 启动时生效。
|
|
196
|
-
-
|
|
196
|
+
- **worker 注册被拒绝时会退回 harness 栅栏**:重开 service worker 只在存在 service worker 的源上注册。当一个本已安全的源拒绝注册(部署策略、浏览器加固配置)时,地址栏此时已经改到 `/`,重开需要重新扫码;纯 HTTP 局域网路径没有这个缺口——它的应用壳停在 `/pair-app`。
|
|
197
197
|
- **适配选择器跟随官方构建**:语义后缀策略可在 hash 变化中存活,但语义改名不行;每次官方 GUI 升级需要一轮视觉 QA(参考 dsh-LAN,这些后缀在多次官方发布中保持稳定)。
|
|
198
198
|
- **开发 HMR**:`dsh web --dev` 按路径轮询每个 roster bundle,重建本包(自身 `tsdown --watch`)即热重载客户端 bundle;host 半区需要重启。
|
|
199
|
-
- **profile 里挂了 `@deepseek-ai/dsh-hmr` 行会让所有设置保存失败**:只要 `hmr` 服务存在,官方配置编辑器就会把写入包进 `hmr.runExclusive`,而在本 cohort 上这种写入会被判为嵌套事务——Host 返回 `HMR transactions cannot be nested`,本插件的各设置卡片(以及官方设置页)都显示保存失败。该行由用户自行添加(没有任何 bundle 自带),桌面版 profile 用它为本地插件的 host 半区做热重载。从 profile 补丁里删掉 `- id: hmr` 这一行即可恢复保存;只在需要热重载该插件时再加回。观察于 2026-09-30,桌面版客户端 0.2.
|
|
199
|
+
- **profile 里挂了 `@deepseek-ai/dsh-hmr` 行会让所有设置保存失败**:只要 `hmr` 服务存在,官方配置编辑器就会把写入包进 `hmr.runExclusive`,而在本 cohort 上这种写入会被判为嵌套事务——Host 返回 `HMR transactions cannot be nested`,本插件的各设置卡片(以及官方设置页)都显示保存失败。该行由用户自行添加(没有任何 bundle 自带),桌面版 profile 用它为本地插件的 host 半区做热重载。从 profile 补丁里删掉 `- id: hmr` 这一行即可恢复保存;只在需要热重载该插件时再加回。观察于 2026-09-30,桌面版客户端 0.2.1-rc.2。
|
|
200
200
|
|
|
201
201
|
## 依赖说明
|
|
202
202
|
|
package/lib/client.js
CHANGED
|
@@ -32,6 +32,7 @@ window.__ModuleLoader__.load({
|
|
|
32
32
|
let react_dom = require("react-dom");
|
|
33
33
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
34
34
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
35
|
+
let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
|
|
35
36
|
//#region ../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
36
37
|
function r(e) {
|
|
37
38
|
var t, f, n = "";
|
|
@@ -1409,7 +1410,7 @@ window.__ModuleLoader__.load({
|
|
|
1409
1410
|
}
|
|
1410
1411
|
//#endregion
|
|
1411
1412
|
//#region \0dsh-css:packages/dsh-remote-web-ui/src/client/remote.module.css.mjs
|
|
1412
|
-
const css$1 = ".fThDlq_overlay{z-index:1000;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.fThDlq_mask{background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur);position:absolute;inset:0}.fThDlq_trigger{width:36px;height:36px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex;position:relative}.fThDlq_trigger[data-wide=wide]{border-radius:999px;flex:auto;justify-content:flex-start;gap:8px;width:auto;min-width:0;padding:0 10px}.fThDlq_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_trigger:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_trigger:disabled{opacity:.5;cursor:default}.fThDlq_panel{z-index:1;box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);width:560px;max-width:calc(100vw - 48px);max-height:calc(100vh - 48px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:24px;flex-direction:column;gap:14px;padding:24px;font-size:14px;line-height:22px;display:flex;position:relative;overflow:auto}.fThDlq_header{align-items:flex-start;gap:12px;display:flex}.fThDlq_heading{flex:1;min-width:0}.fThDlq_title{margin:0;font-size:18px;font-weight:600;line-height:26px}.fThDlq_subtitle{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}.fThDlq_close{width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex}.fThDlq_close:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_close:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_close:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_close:disabled{opacity:.5;cursor:default}.fThDlq_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;align-items:center;gap:12px;padding:16px;display:flex}.fThDlq_cardHeader{justify-content:space-between;align-items:center;gap:12px;width:100%;display:flex}.fThDlq_cardTitle{font-weight:500}.fThDlq_badge{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:6px;min-width:0;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex}.fThDlq_badge:before{content:\"\";background:currentColor;border-radius:50%;width:8px;height:8px}.fThDlq_badge-waiting{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-connected{color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-disconnected{color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-stopped{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badgePublic{color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badges{flex:none;align-items:center;gap:6px;display:inline-flex}.fThDlq_qrWrap{background:var(--dsw-alias-bg-base);border-radius:12px;justify-content:center;align-items:center;padding:12px;display:flex}.fThDlq_qr{display:block}.fThDlq_expired{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_expiry{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}.fThDlq_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_link{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-caption);font-family:var(--dsw-font-mono,ui-monospace, monospace);margin:0;font-size:12px;display:block;overflow:hidden}.fThDlq_pairLinks{flex-direction:column;gap:8px;display:flex}.fThDlq_pairLinkRow{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:10px;align-items:center;gap:10px;min-width:0;padding:10px 12px;display:flex}.fThDlq_pairLinkText{flex:1;min-width:0}.fThDlq_pairLinkLabel{color:var(--dsw-alias-label-secondary);margin-bottom:3px;font-size:12px;display:block}.fThDlq_copyLink{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);min-height:30px;color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:8px;flex:none;align-items:center;gap:5px;padding:0 10px;display:inline-flex}.fThDlq_oneTimeHint{color:var(--dsw-alias-label-caption);margin:0;font-size:12px}.fThDlq_stoppedHint{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_tunnelNote{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_tunnelFailed{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_actions{gap:8px;display:flex}.fThDlq_action{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);height:34px;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;border-radius:10px;justify-content:center;align-items:center;gap:6px;padding:0 14px;font-size:13px;transition:background-color .12s,border-color .12s,box-shadow .12s;display:inline-flex}.fThDlq_action:hover:not(:disabled){background:var(--dsw-alias-button-floating-hover)}.fThDlq_action:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_action:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_action:disabled{opacity:.5;cursor:default}.fThDlq_banner{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;padding:16px}.fThDlq_bannerTitle{color:var(--dsw-alias-state-warn-primary);margin:0;font-weight:500}.fThDlq_bannerHint{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}.fThDlq_fencePage{z-index:2000;box-sizing:border-box;background:var(--dsw-alias-bg-base);text-align:center;flex-direction:column;justify-content:center;align-items:center;padding:40px 24px;display:flex;position:fixed;inset:0;overflow:auto}.fThDlq_fenceCard{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:min(520px,100%);box-shadow:var(--dsw-shadow-lv3);text-align:center;border-radius:20px;margin-inline:auto;padding:36px 40px}.fThDlq_fenceMark{background:var(--dsw-alias-state-error-secondary);width:44px;height:44px;color:var(--dsw-alias-state-error-primary);border-radius:50%;place-items:center;margin-inline:auto;font-size:24px;line-height:1;display:grid}.fThDlq_fenceEyebrow{color:var(--dsw-alias-state-error-primary);margin:22px 0 8px;font-size:13px;font-weight:600}.fThDlq_fenceTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:24px;line-height:1.35}.fThDlq_fenceDetail{color:var(--dsw-alias-label-secondary);margin:12px 0 0;font-size:14px;line-height:1.65}.fThDlq_fenceSteps{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);text-align:left;border-radius:12px;margin:24px auto 0;padding:20px 20px 20px 42px;font-size:14px;line-height:1.65}.fThDlq_fenceSteps li+li{margin-top:8px}.fThDlq_fenceForm{width:100%;margin-top:20px}.fThDlq_fenceInputRow{gap:8px;width:100%;display:flex}.fThDlq_fenceInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-width:0;color:var(--dsw-alias-label-primary);font:inherit;border-radius:10px;flex:1;padding:10px 14px;font-size:13px;transition:border-color .12s,box-shadow .12s}.fThDlq_fenceInput:focus{border-color:var(--dsw-alias-brand-primary);outline:none;box-shadow:0 0 0 2px #0066ff26}.fThDlq_fencePairButton{border:1px solid var(--dsw-alias-button-primary-fill);background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;cursor:pointer;border-radius:10px;flex-shrink:0;padding:10px 18px;font-size:13px;font-weight:500;transition:filter .12s,opacity .12s}.fThDlq_fencePairButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill));border-color:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill))}.fThDlq_fencePairButton:disabled{opacity:.55;cursor:not-allowed}.fThDlq_fencePairButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.fThDlq_fenceError{color:var(--dsw-alias-state-error-primary);text-align:left;margin:10px 0 0;font-size:13px;line-height:1.4}.fThDlq_fenceRetry{border:1px solid var(--dsw-alias-border-l2);width:100%;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:10px;margin-top:12px;padding:10px 16px;font-weight:500;transition:background-color .12s,color .12s}.fThDlq_fenceRetry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_fenceRetry:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.fThDlq_fenceFootnote{color:var(--dsw-alias-label-tertiary);margin:14px 0 0;font-size:12px;line-height:1.55}.fThDlq_addresses{border:none;margin:12px 0 0;padding:0}.fThDlq_addresses legend{color:var(--dsw-alias-label-secondary);padding:0;font-size:13px}.fThDlq_address{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;cursor:pointer;border-radius:6px;align-items:center;gap:8px;margin-top:6px;padding:4px 6px;font-size:13px;transition:background-color .12s;display:flex}.fThDlq_address:hover{background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_address input:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);border-radius:50%;outline:none}.fThDlq_addressValue{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-size:12px;overflow:hidden}.fThDlq_addressHint{color:var(--dsw-alias-label-tertiary);margin:6px 0 0;font-size:12px}.fThDlq_devices{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;gap:8px;padding:12px 16px 14px;display:flex}.fThDlq_devicesTitle{margin:0;font-size:13px;font-weight:500;line-height:20px}.fThDlq_devicesEmpty{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_deviceList{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.fThDlq_deviceRow{justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.fThDlq_deviceMeta{flex-direction:column;gap:2px;min-width:0;display:flex}.fThDlq_deviceName{text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;overflow:hidden}.fThDlq_devicePresence{font-size:12px;line-height:18px}.fThDlq_deviceOnline{color:var(--dsw-alias-state-success-primary)}.fThDlq_deviceOffline{color:var(--dsw-alias-label-secondary)}.fThDlq_deviceSeen{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px}.fThDlq_deviceRevoke{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex:none;padding:6px 10px;font-size:12px;transition:background-color .12s,color .12s}.fThDlq_deviceRevoke:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_deviceRevoke:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}@media (prefers-reduced-motion:reduce){.fThDlq_trigger,.fThDlq_close,.fThDlq_action,.fThDlq_address,.fThDlq_deviceRevoke{transition:none}}.fThDlq_entryRow{flex:none;align-items:center;gap:6px;min-width:0;display:flex}.fThDlq_entryRow[data-rail=rail]{flex-direction:column-reverse;gap:4px}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footArea]{flex-flow:wrap;align-items:center}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footArea]>:not([class*=settingsArea]):not([class*=footerActions]){flex:100%;min-width:0}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footArea]>[class*=footerActions]{display:contents}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footerActions]>[data-slot=\"sidebar.footer.action\"]>:not([data-dsh-part=entry]):not([class*=entryRow]){flex:100%;order:1;min-width:0}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footerActions]>[data-slot=\"sidebar.footer.action\"]>[data-dsh-part=entry],[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footerActions]>[data-slot=\"sidebar.footer.action\"]>[class*=entryRow]{flex:none;order:4;min-width:0}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=settingsArea]{flex:auto;order:3;width:auto;min-width:0}[data-dsh-frame][data-sidebar-collapsed] [class*=footerActions]:has([data-rail=rail]){flex-direction:column;align-items:center;gap:4px}";
|
|
1413
|
+
const css$1 = ".fThDlq_overlay{z-index:1000;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.fThDlq_mask{background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur);position:absolute;inset:0}.fThDlq_trigger{width:36px;height:36px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex;position:relative}.fThDlq_trigger[data-wide=wide]{border-radius:999px;flex:auto;justify-content:flex-start;gap:8px;width:auto;min-width:0;padding:0 10px}.fThDlq_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_trigger:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_trigger:disabled{opacity:.5;cursor:default}.fThDlq_panel{z-index:1;box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);width:560px;max-width:calc(100vw - 48px);max-height:calc(100vh - 48px);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-primary);border-radius:24px;flex-direction:column;gap:14px;padding:24px;font-size:14px;line-height:22px;display:flex;position:relative;overflow:auto}.fThDlq_header{align-items:flex-start;gap:12px;display:flex}.fThDlq_heading{flex:1;min-width:0}.fThDlq_title{margin:0;font-size:18px;font-weight:600;line-height:26px}.fThDlq_subtitle{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}.fThDlq_close{width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;transition:background-color .12s,color .12s,box-shadow .12s;display:inline-flex}.fThDlq_close:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_close:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_close:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_close:disabled{opacity:.5;cursor:default}.fThDlq_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;align-items:center;gap:12px;padding:16px;display:flex}.fThDlq_cardHeader{justify-content:space-between;align-items:center;gap:12px;width:100%;display:flex}.fThDlq_cardTitle{font-weight:500}.fThDlq_badge{white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:6px;min-width:0;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex}.fThDlq_badge:before{content:\"\";background:currentColor;border-radius:50%;width:8px;height:8px}.fThDlq_badge-waiting{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-connected{color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-disconnected{color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badge-stopped{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badgePublic{color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-interactive-bg-hover)}.fThDlq_badges{flex:none;align-items:center;gap:6px;display:inline-flex}.fThDlq_qrWrap{background:var(--dsw-alias-bg-base);border-radius:12px;justify-content:center;align-items:center;padding:12px;display:flex}.fThDlq_qr{display:block}.fThDlq_expired{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_expiry{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}.fThDlq_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_link{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-caption);font-family:var(--dsw-font-mono,ui-monospace, monospace);margin:0;font-size:12px;display:block;overflow:hidden}.fThDlq_pairLinks{flex-direction:column;gap:8px;display:flex}.fThDlq_pairLinkRow{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:10px;align-items:center;gap:10px;min-width:0;padding:10px 12px;display:flex}.fThDlq_pairLinkText{flex:1;min-width:0}.fThDlq_pairLinkLabel{color:var(--dsw-alias-label-secondary);margin-bottom:3px;font-size:12px;display:block}.fThDlq_copyLink{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);min-height:30px;color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:8px;flex:none;align-items:center;gap:5px;padding:0 10px;display:inline-flex}.fThDlq_oneTimeHint{color:var(--dsw-alias-label-caption);margin:0;font-size:12px}.fThDlq_stoppedHint{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_tunnelNote{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_tunnelFailed{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px}.fThDlq_actions{gap:8px;display:flex}.fThDlq_action{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-elevated-fill);height:34px;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;border-radius:10px;justify-content:center;align-items:center;gap:6px;padding:0 14px;font-size:13px;transition:background-color .12s,border-color .12s,box-shadow .12s;display:inline-flex}.fThDlq_action:hover:not(:disabled){background:var(--dsw-alias-button-floating-hover)}.fThDlq_action:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.fThDlq_action:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}.fThDlq_action:disabled{opacity:.5;cursor:default}.fThDlq_banner{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;padding:16px}.fThDlq_bannerTitle{color:var(--dsw-alias-state-warn-primary);margin:0;font-weight:500}.fThDlq_bannerHint{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}.fThDlq_fencePage{z-index:2000;box-sizing:border-box;background:var(--dsw-alias-bg-base);text-align:center;flex-direction:column;justify-content:center;align-items:center;padding:40px 24px;display:flex;position:fixed;inset:0;overflow:auto}.fThDlq_fenceCard{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);width:min(520px,100%);box-shadow:var(--dsw-shadow-lv3);text-align:center;border-radius:20px;margin-inline:auto;padding:36px 40px}.fThDlq_fenceMark{background:var(--dsw-alias-state-error-secondary);width:44px;height:44px;color:var(--dsw-alias-state-error-primary);border-radius:50%;place-items:center;margin-inline:auto;font-size:24px;line-height:1;display:grid}.fThDlq_fenceEyebrow{color:var(--dsw-alias-state-error-primary);margin:22px 0 8px;font-size:13px;font-weight:600}.fThDlq_fenceTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:24px;line-height:1.35}.fThDlq_fenceDetail{color:var(--dsw-alias-label-secondary);margin:12px 0 0;font-size:14px;line-height:1.65}.fThDlq_fenceSteps{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);text-align:left;border-radius:12px;margin:24px auto 0;padding:20px 20px 20px 42px;font-size:14px;line-height:1.65}.fThDlq_fenceSteps li+li{margin-top:8px}.fThDlq_fenceForm{width:100%;margin-top:20px}.fThDlq_fenceInputRow{gap:8px;width:100%;display:flex}.fThDlq_fenceInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-width:0;color:var(--dsw-alias-label-primary);font:inherit;border-radius:10px;flex:1;padding:10px 14px;font-size:13px;transition:border-color .12s,box-shadow .12s}.fThDlq_fenceInput:focus{border-color:var(--dsw-alias-brand-primary);outline:none;box-shadow:0 0 0 2px #0066ff26}.fThDlq_fencePairButton{border:1px solid var(--dsw-alias-button-primary-fill);background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;cursor:pointer;border-radius:10px;flex-shrink:0;padding:10px 18px;font-size:13px;font-weight:500;transition:filter .12s,opacity .12s}.fThDlq_fencePairButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill));border-color:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill))}.fThDlq_fencePairButton:disabled{opacity:.55;cursor:not-allowed}.fThDlq_fencePairButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.fThDlq_fenceError{color:var(--dsw-alias-state-error-primary);text-align:left;margin:10px 0 0;font-size:13px;line-height:1.4}.fThDlq_fenceRetry{border:1px solid var(--dsw-alias-border-l2);width:100%;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:10px;margin-top:12px;padding:10px 16px;font-weight:500;transition:background-color .12s,color .12s}.fThDlq_fenceRetry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_fenceRetry:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.fThDlq_fenceFootnote{color:var(--dsw-alias-label-tertiary);margin:14px 0 0;font-size:12px;line-height:1.55}.fThDlq_addressHint{color:var(--dsw-alias-label-tertiary);margin:6px 0 0;font-size:12px}.fThDlq_devices{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:16px;flex-direction:column;gap:8px;padding:12px 16px 14px;display:flex}.fThDlq_devicesTitle{margin:0;font-size:13px;font-weight:500;line-height:20px}.fThDlq_devicesEmpty{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px}.fThDlq_deviceList{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.fThDlq_deviceRow{justify-content:space-between;align-items:flex-start;gap:12px;display:flex}.fThDlq_deviceMeta{flex-direction:column;gap:2px;min-width:0;display:flex}.fThDlq_deviceName{text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;overflow:hidden}.fThDlq_devicePresence{font-size:12px;line-height:18px}.fThDlq_deviceOnline{color:var(--dsw-alias-state-success-primary)}.fThDlq_deviceOffline{color:var(--dsw-alias-label-secondary)}.fThDlq_deviceSeen{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;font-size:12px}.fThDlq_deviceRevoke{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex:none;padding:6px 10px;font-size:12px;transition:background-color .12s,color .12s}.fThDlq_deviceRevoke:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.fThDlq_deviceRevoke:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2), 0 0 0 4px var(--dsw-alias-brand-primary);outline:none}@media (prefers-reduced-motion:reduce){.fThDlq_trigger,.fThDlq_close,.fThDlq_action,.fThDlq_deviceRevoke{transition:none}}.fThDlq_entryRow{flex:none;align-items:center;gap:6px;min-width:0;display:flex}.fThDlq_entryRow[data-rail=rail]{flex-direction:column-reverse;gap:4px}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footArea]{flex-flow:wrap;align-items:center}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footArea]>:not([class*=settingsArea]):not([class*=footerActions]){flex:100%;min-width:0}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footArea]>[class*=footerActions]{display:contents}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footerActions]>[data-slot=\"sidebar.footer.action\"]>:not([data-dsh-part=entry]):not([class*=entryRow]){flex:100%;order:1;min-width:0}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footerActions]>[data-slot=\"sidebar.footer.action\"]>[data-dsh-part=entry],[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=footerActions]>[data-slot=\"sidebar.footer.action\"]>[class*=entryRow]{flex:none;order:4;min-width:0}[data-dsh-frame]:not([data-sidebar-collapsed]) [class*=settingsArea]{flex:auto;order:3;width:auto;min-width:0}[data-dsh-frame][data-sidebar-collapsed] [class*=footerActions]:has([data-rail=rail]){flex-direction:column;align-items:center;gap:4px}";
|
|
1413
1414
|
const tagId$1 = "@linxin666/dsh-remote-web-ui/packages/dsh-remote-web-ui/src/client/remote.module.css";
|
|
1414
1415
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
1415
1416
|
const tag = document.createElement("style");
|
|
@@ -1421,10 +1422,7 @@ window.__ModuleLoader__.load({
|
|
|
1421
1422
|
var remote_module_css_default = {
|
|
1422
1423
|
"action": "fThDlq_action",
|
|
1423
1424
|
"actions": "fThDlq_actions",
|
|
1424
|
-
"address": "fThDlq_address",
|
|
1425
1425
|
"addressHint": "fThDlq_addressHint",
|
|
1426
|
-
"addressValue": "fThDlq_addressValue",
|
|
1427
|
-
"addresses": "fThDlq_addresses",
|
|
1428
1426
|
"badge": "fThDlq_badge",
|
|
1429
1427
|
"badge-connected": "fThDlq_badge-connected",
|
|
1430
1428
|
"badge-disconnected": "fThDlq_badge-disconnected",
|
|
@@ -1494,8 +1492,11 @@ window.__ModuleLoader__.load({
|
|
|
1494
1492
|
//#region src/client/RemotePanel.tsx
|
|
1495
1493
|
/**
|
|
1496
1494
|
* The mobile remote-control panel body: status card (state text + badge),
|
|
1497
|
-
* the QR code, the open-on-phone hint with the link text, and the
|
|
1498
|
-
* actions (stop / refresh / copy).
|
|
1495
|
+
* the QR code, the open-on-phone hint with the link text, and the two
|
|
1496
|
+
* actions (stop / refresh / copy). The advertised network is not a choice:
|
|
1497
|
+
* the QR names the public base and the landing page decides at connect time
|
|
1498
|
+
* whether to move to a LAN origin (lan-source.ts), so the panel only states
|
|
1499
|
+
* which source the current link carries. Pure presentation — all state and
|
|
1499
1500
|
* actions arrive through props from the entry's behavior component.
|
|
1500
1501
|
*/
|
|
1501
1502
|
/**
|
|
@@ -1542,7 +1543,7 @@ window.__ModuleLoader__.load({
|
|
|
1542
1543
|
* @param props - copy, state, and actions.
|
|
1543
1544
|
* @returns the panel element tree.
|
|
1544
1545
|
*/
|
|
1545
|
-
function RemotePanel({ t, state, copied, copiedToken, onClose, onStop, onRefresh, onCopy, onCopyToken,
|
|
1546
|
+
function RemotePanel({ t, state, copied, copiedToken, onClose, onStop, onRefresh, onCopy, onCopyToken, onRevoke }) {
|
|
1546
1547
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1547
1548
|
className: remote_module_css_default.panel,
|
|
1548
1549
|
role: "dialog",
|
|
@@ -1715,49 +1716,9 @@ window.__ModuleLoader__.load({
|
|
|
1715
1716
|
role: "status",
|
|
1716
1717
|
children: t("relay.failed", { error: state.relay.error ?? t("tunnel.unknownError") })
|
|
1717
1718
|
}),
|
|
1718
|
-
(state.publicBaseUrl !== void 0 || state.
|
|
1719
|
-
className: remote_module_css_default.
|
|
1720
|
-
children:
|
|
1721
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", { children: t("address.label") }),
|
|
1722
|
-
state.publicBaseUrl !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1723
|
-
className: remote_module_css_default.address,
|
|
1724
|
-
children: [
|
|
1725
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1726
|
-
type: "radio",
|
|
1727
|
-
name: "lan-address",
|
|
1728
|
-
"aria-label": t("address.public"),
|
|
1729
|
-
checked: state.public,
|
|
1730
|
-
onChange: onPickPublic
|
|
1731
|
-
}),
|
|
1732
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("address.public") }),
|
|
1733
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1734
|
-
className: remote_module_css_default.addressValue,
|
|
1735
|
-
children: state.publicBaseUrl
|
|
1736
|
-
})
|
|
1737
|
-
]
|
|
1738
|
-
}, "public"),
|
|
1739
|
-
state.lanAddresses.map((address) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1740
|
-
className: remote_module_css_default.address,
|
|
1741
|
-
children: [
|
|
1742
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1743
|
-
type: "radio",
|
|
1744
|
-
name: "lan-address",
|
|
1745
|
-
"aria-label": address,
|
|
1746
|
-
checked: !state.public && address === state.address,
|
|
1747
|
-
onChange: () => onPickAddress(address)
|
|
1748
|
-
}),
|
|
1749
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("address.lan") }),
|
|
1750
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1751
|
-
className: remote_module_css_default.addressValue,
|
|
1752
|
-
children: address
|
|
1753
|
-
})
|
|
1754
|
-
]
|
|
1755
|
-
}, address)),
|
|
1756
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1757
|
-
className: remote_module_css_default.addressHint,
|
|
1758
|
-
children: t("address.hint")
|
|
1759
|
-
})
|
|
1760
|
-
]
|
|
1719
|
+
(state.publicBaseUrl !== void 0 || !state.public) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1720
|
+
className: remote_module_css_default.addressHint,
|
|
1721
|
+
children: state.public ? t("address.auto") : t("address.pinnedLan")
|
|
1761
1722
|
}),
|
|
1762
1723
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1763
1724
|
className: remote_module_css_default.actions,
|
|
@@ -1894,10 +1855,10 @@ window.__ModuleLoader__.load({
|
|
|
1894
1855
|
eventSource.current?.close();
|
|
1895
1856
|
eventSource.current = void 0;
|
|
1896
1857
|
}, []);
|
|
1897
|
-
const mint = (0, react.useCallback)(async (
|
|
1858
|
+
const mint = (0, react.useCallback)(async () => {
|
|
1898
1859
|
let result;
|
|
1899
1860
|
try {
|
|
1900
|
-
result = await issuePair(
|
|
1861
|
+
result = await issuePair();
|
|
1901
1862
|
} catch {
|
|
1902
1863
|
return { kind: "unreachable" };
|
|
1903
1864
|
}
|
|
@@ -1918,9 +1879,7 @@ window.__ModuleLoader__.load({
|
|
|
1918
1879
|
onlineCount: 0,
|
|
1919
1880
|
devices: [],
|
|
1920
1881
|
public: publicBaseUrl !== void 0 && result.url.startsWith(publicBaseUrl),
|
|
1921
|
-
...publicBaseUrl !== void 0 ? { publicBaseUrl } : {}
|
|
1922
|
-
address: address ?? result.lanAddresses[0] ?? "",
|
|
1923
|
-
lanAddresses: result.lanAddresses
|
|
1882
|
+
...publicBaseUrl !== void 0 ? { publicBaseUrl } : {}
|
|
1924
1883
|
};
|
|
1925
1884
|
}, []);
|
|
1926
1885
|
const openPanel = (0, react.useCallback)(async () => {
|
|
@@ -1992,14 +1951,6 @@ window.__ModuleLoader__.load({
|
|
|
1992
1951
|
const handleRefresh = (0, react.useCallback)(() => {
|
|
1993
1952
|
mint().then(setState);
|
|
1994
1953
|
}, [mint]);
|
|
1995
|
-
/** Re-mint against another LAN literal (multi-homed machines). */
|
|
1996
|
-
const handlePickAddress = (0, react.useCallback)((address) => {
|
|
1997
|
-
mint(address).then(setState);
|
|
1998
|
-
}, [mint]);
|
|
1999
|
-
/** Re-mint against the configured public (tunneled) base. */
|
|
2000
|
-
const handlePickPublic = (0, react.useCallback)(() => {
|
|
2001
|
-
mint().then(setState);
|
|
2002
|
-
}, [mint]);
|
|
2003
1954
|
const handleCopy = (0, react.useCallback)((url) => {
|
|
2004
1955
|
copyText(url).then((ok) => {
|
|
2005
1956
|
if (!ok) return;
|
|
@@ -2044,8 +1995,6 @@ window.__ModuleLoader__.load({
|
|
|
2044
1995
|
onRefresh: handleRefresh,
|
|
2045
1996
|
onCopy: handleCopy,
|
|
2046
1997
|
onCopyToken: handleCopyToken,
|
|
2047
|
-
onPickAddress: handlePickAddress,
|
|
2048
|
-
onPickPublic: handlePickPublic,
|
|
2049
1998
|
onRevoke: handleRevoke
|
|
2050
1999
|
})]
|
|
2051
2000
|
}), document.body)] });
|
|
@@ -2581,19 +2530,6 @@ window.__ModuleLoader__.load({
|
|
|
2581
2530
|
});
|
|
2582
2531
|
}
|
|
2583
2532
|
//#endregion
|
|
2584
|
-
//#region \0dsh-store-engine
|
|
2585
|
-
const platform = ["@deepseek-ai/dsh-client", "-store"].join("");
|
|
2586
|
-
const legacy = ["@deepseek-ai/dsh-client-runtime", "/client"].join("");
|
|
2587
|
-
let engine;
|
|
2588
|
-
try {
|
|
2589
|
-
engine = require(platform);
|
|
2590
|
-
} catch {
|
|
2591
|
-
engine = require(legacy);
|
|
2592
|
-
}
|
|
2593
|
-
const createSnapshotStore = engine.createSnapshotStore;
|
|
2594
|
-
engine.defineStore;
|
|
2595
|
-
engine.shallowEqual;
|
|
2596
|
-
//#endregion
|
|
2597
2533
|
//#region src/client/settings-form.ts
|
|
2598
2534
|
/** A whole- or decimal-number field. An empty draft clears the field; any other draft that is not a finite number within the constraints blocks the save. */
|
|
2599
2535
|
function numberField(field, constraints = {}) {
|
|
@@ -2701,7 +2637,7 @@ window.__ModuleLoader__.load({
|
|
|
2701
2637
|
}
|
|
2702
2638
|
/** Publish a projection of this form, rebuilt whenever the form or a draft changes. */
|
|
2703
2639
|
bind(project) {
|
|
2704
|
-
const store = createSnapshotStore(project());
|
|
2640
|
+
const store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(project());
|
|
2705
2641
|
this.listeners.add(() => {
|
|
2706
2642
|
store.set(project());
|
|
2707
2643
|
});
|
|
@@ -3277,10 +3213,8 @@ window.__ModuleLoader__.load({
|
|
|
3277
3213
|
"status.unreachableHint": "请刷新页面后重试。",
|
|
3278
3214
|
"pair.hint": "无法扫码?可直接打开下方配对链接",
|
|
3279
3215
|
"pair.publicHint": "公网链接:设备无需与本机处于同一网络",
|
|
3280
|
-
"address.
|
|
3281
|
-
"address.
|
|
3282
|
-
"address.lan": "局域网",
|
|
3283
|
-
"address.hint": "远程设备不在同一网络时请使用公网地址;局域网地址仅限同一网络内使用。",
|
|
3216
|
+
"address.auto": "二维码固定指向公网地址;手机与本机处于同一网络时会自动切换到局域网地址。",
|
|
3217
|
+
"address.pinnedLan": "二维码固定指向局域网地址。",
|
|
3284
3218
|
"pair.expires": "二维码有效至 {time}",
|
|
3285
3219
|
"pair.expired": "二维码已过期,请刷新",
|
|
3286
3220
|
"pair.linkLabel": "配对链接",
|
|
@@ -3397,10 +3331,8 @@ window.__ModuleLoader__.load({
|
|
|
3397
3331
|
"status.unreachableHint": "Refresh the page and try again.",
|
|
3398
3332
|
"pair.hint": "Cannot scan? Open one of the pairing links below",
|
|
3399
3333
|
"pair.publicHint": "Public links work when the device is not on the same network",
|
|
3400
|
-
"address.
|
|
3401
|
-
"address.
|
|
3402
|
-
"address.lan": "LAN",
|
|
3403
|
-
"address.hint": "Use the public address when the remote device is on another network; LAN addresses only work on the same network.",
|
|
3334
|
+
"address.auto": "The QR code always points at the public address; a phone on the same network switches to the LAN address by itself.",
|
|
3335
|
+
"address.pinnedLan": "The QR code points at a LAN address.",
|
|
3404
3336
|
"pair.expires": "QR code valid until {time}",
|
|
3405
3337
|
"pair.expired": "QR code expired — refresh it",
|
|
3406
3338
|
"pair.linkLabel": "Pairing link",
|
|
@@ -4117,7 +4049,7 @@ window.__ModuleLoader__.load({
|
|
|
4117
4049
|
/** The building package's version, when the bundle carries it. */
|
|
4118
4050
|
function bakedVersion() {
|
|
4119
4051
|
try {
|
|
4120
|
-
return "0.4.
|
|
4052
|
+
return "0.4.7";
|
|
4121
4053
|
} catch {
|
|
4122
4054
|
return;
|
|
4123
4055
|
}
|
|
@@ -5270,8 +5202,7 @@ window.__ModuleLoader__.load({
|
|
|
5270
5202
|
base: void 0,
|
|
5271
5203
|
user: void 0,
|
|
5272
5204
|
revision: void 0,
|
|
5273
|
-
writable: false
|
|
5274
|
-
mode: "host"
|
|
5205
|
+
writable: false
|
|
5275
5206
|
};
|
|
5276
5207
|
}
|
|
5277
5208
|
/**
|