@linxin666/dsh-remote-web-ui 0.1.20 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +41 -19
- package/README.zh.md +17 -12
- package/assets/mobile-icon-192.png +0 -0
- package/assets/mobile-icon-512.png +0 -0
- package/assets/mobile-service-worker.js +64 -0
- package/lib/client.js +37 -10
- package/lib/client.js.map +1 -1
- package/lib/index.js +290 -53
- package/lib/mobile.js +278 -17
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +6 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +5 -3
- package/lib/types/client/RemoteEntry.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +1 -1
- package/lib/types/client/UpdateEntry.d.ts.map +1 -1
- package/lib/types/client/UpdateEntry.js +36 -3
- package/lib/types/client/deep-link.js +1 -1
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +2 -0
- package/lib/types/gate.d.ts +9 -0
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +15 -4
- package/lib/types/index.d.ts +25 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +30 -13
- package/lib/types/mobile/PairRequiredView.d.ts +7 -0
- package/lib/types/mobile/PairRequiredView.d.ts.map +1 -0
- package/lib/types/mobile/PairRequiredView.js +28 -0
- package/lib/types/mobile/PairRequiredView.test.d.ts +2 -0
- package/lib/types/mobile/PairRequiredView.test.d.ts.map +1 -0
- package/lib/types/mobile/PairRequiredView.test.js +24 -0
- package/lib/types/mobile/index.js +14 -1
- package/lib/types/mobile/mobile-styles.d.ts +1 -1
- package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
- package/lib/types/mobile/mobile-styles.js +48 -0
- package/lib/types/mobile/pairing.d.ts +31 -0
- package/lib/types/mobile/pairing.d.ts.map +1 -0
- package/lib/types/mobile/pairing.js +62 -0
- package/lib/types/mobile/views/App.d.ts +12 -5
- package/lib/types/mobile/views/App.d.ts.map +1 -1
- package/lib/types/mobile/views/App.js +34 -7
- package/lib/types/mobile/views/App.test.d.ts +2 -0
- package/lib/types/mobile/views/App.test.d.ts.map +1 -0
- package/lib/types/mobile/views/App.test.js +52 -0
- package/lib/types/mobile-pwa.d.ts +6 -0
- package/lib/types/mobile-pwa.d.ts.map +1 -0
- package/lib/types/mobile-pwa.js +17 -0
- package/lib/types/mobile-routes.d.ts +5 -8
- package/lib/types/mobile-routes.d.ts.map +1 -1
- package/lib/types/mobile-routes.js +115 -30
- package/lib/types/pairing-access.d.ts +28 -0
- package/lib/types/pairing-access.d.ts.map +1 -0
- package/lib/types/pairing-access.js +26 -0
- package/lib/types/pairing.d.ts +27 -0
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +68 -0
- package/lib/types/pairing.persist.test.d.ts +2 -0
- package/lib/types/pairing.persist.test.d.ts.map +1 -0
- package/lib/types/pairing.persist.test.js +128 -0
- package/lib/types/routes.js +1 -1
- package/lib/types/update.d.ts +3 -3
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +34 -11
- package/package.json +12 -12
- package/src/client/PluginSettingsCard.tsx +10 -0
- package/src/client/RemoteEntry.tsx +2 -2
- package/src/client/RemoteSettingsCard.tsx +1 -0
- package/src/client/UpdateEntry.tsx +36 -4
- package/src/client/deep-link.ts +1 -1
- package/src/client/locales.ts +2 -0
- package/src/client/remote.module.css +26 -0
- package/src/gate.ts +15 -3
- package/src/index.ts +46 -15
- package/src/mobile/PairRequiredView.test.tsx +28 -0
- package/src/mobile/PairRequiredView.tsx +58 -0
- package/src/mobile/index.tsx +15 -1
- package/src/mobile/mobile-styles.ts +48 -0
- package/src/mobile/pairing.ts +77 -0
- package/src/mobile/views/App.test.tsx +62 -0
- package/src/mobile/views/App.tsx +53 -6
- package/src/mobile-pwa.ts +19 -0
- package/src/mobile-routes.ts +123 -30
- package/src/pairing-access.ts +36 -0
- package/src/pairing.persist.test.ts +145 -0
- package/src/pairing.ts +70 -1
- package/src/routes.ts +1 -1
- package/src/update.ts +40 -12
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: ed09a3bb6b09641147fecd3a465a6766f22fb298
|
|
6
|
+
README.zh.md: d95d6fefe6b583c90169cd201464a62ac52381e0
|
package/README.md
CHANGED
|
@@ -2,29 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
English | [中文](README.zh.md)
|
|
5
|
-
>
|
|
6
|
-
> 点击侧边栏更新按钮自动检查并更新 dsh-web-ui 全家桶。
|
|
5
|
+
> Mobile remote control + one-click updates: pair a phone to use the current dsh web workspace remotely; the sidebar checks for a newer dsh-web-ui release after it loads and marks the update button when one is available; clicking it updates the family.
|
|
7
6
|
|
|
8
7
|
This repository is an external plugin package for DeepSeek Harness (DSH):
|
|
9
8
|
scan-to-pair mobile remote control for the dsh web GUI, plus a one-click
|
|
10
9
|
self-update for the dsh-web-ui family. It is a single dual-face package — the
|
|
11
10
|
host half owns pairing tokens, device sessions, the `/api/pair` route family,
|
|
12
11
|
and the `/api/update` surface; the browser half renders the sidebar-foot
|
|
13
|
-
entries (the download trigger and
|
|
12
|
+
entries (the download trigger and an icon-only mobile-remote control beside the settings button),
|
|
14
13
|
the pairing panel with a QR code, live device status, and stop/refresh/copy
|
|
15
14
|
actions, and the update panel that probes and runs the update.
|
|
16
15
|
|
|
17
16
|
## What it does
|
|
18
17
|
|
|
19
|
-
- **Entry**: a phone icon in the sidebar
|
|
18
|
+
- **Entry**: a phone icon beside the settings button in both the expanded sidebar and narrow rail; its tooltip and accessible label say "Mobile remote control".
|
|
20
19
|
- **Panel**: "移动端远程控制" title, "扫码或在手机上打开链接,即可远程控制当前工作区"
|
|
21
20
|
subtitle, a "手机扫码连接" card with the status area ("等待手机连接" + status
|
|
22
21
|
badge), a large QR code, the "无法扫码?可以在手机上打开链接" hint, and three
|
|
23
22
|
buttons: 停止 / 刷新二维码 / 复制链接.
|
|
24
23
|
- **Phone side**: scanning the QR binds the phone with a one-time,
|
|
25
24
|
time-limited token and lands it on the **standalone mobile surface at
|
|
26
|
-
`/m
|
|
25
|
+
`/m/`** — a thin client purpose-built for a small screen (see
|
|
27
26
|
[Screenshots](#screenshots)), not the desktop UI squeezed into a phone.
|
|
27
|
+
The page is installable as a PWA. Each installed app uses its own
|
|
28
|
+
paired-device cookie; a storage-isolated mobile web-app context pairs by
|
|
29
|
+
opening a fresh QR link there or pasting a fresh desktop-issued link.
|
|
28
30
|
The link carries a `workspace` parameter so the phone lands in the same
|
|
29
31
|
workspace the desktop was looking at.
|
|
30
32
|
- **Security**: one active one-time token (a refresh invalidates the old
|
|
@@ -36,9 +38,14 @@ actions, and the update panel that probes and runs the update.
|
|
|
36
38
|
- **Live status**: the desktop panel mirrors the pairing state in real time
|
|
37
39
|
(waiting → connected → disconnected) over an SSE stream.
|
|
38
40
|
- **Remote update**: the download trigger in the sidebar foot (left of the
|
|
39
|
-
phone icon)
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
phone icon) performs a silent status probe after the sidebar loads. When a
|
|
42
|
+
newer registry release is available, the trigger shows a dot and the "New
|
|
43
|
+
version available. Check for updates" tooltip. Clicking the trigger opens the
|
|
44
|
+
update panel, which probes the npm registry for the installed
|
|
45
|
+
`@linxin666/dsh-*` family releases. Without the aggregate package, checks and
|
|
46
|
+
updates cover every registry-managed direct `@linxin666/*` dependency in the
|
|
47
|
+
profile; local link/file development dependencies are skipped. When a newer
|
|
48
|
+
release exists, the panel runs the update automatically (`pnpm update --latest` inside the
|
|
42
49
|
owning dsh profile; when pnpm is missing it falls back to `corepack pnpm`
|
|
43
50
|
and then `npx --yes pnpm`, and on Windows the command runs through
|
|
44
51
|
`cmd.exe` so npm-installed `.cmd` shims resolve; the loopback-only
|
|
@@ -47,9 +54,8 @@ actions, and the update panel that probes and runs the update.
|
|
|
47
54
|
versions are re-checked against the registry: a green exit that left every
|
|
48
55
|
version in place (e.g. the pnpm `minimumReleaseAge` gate silently skipping
|
|
49
56
|
same-day releases) is reported as a stale update with configuration
|
|
50
|
-
guidance instead of a false success.
|
|
51
|
-
|
|
52
|
-
without updating.
|
|
57
|
+
guidance instead of a false success. When the anchor itself is a local link
|
|
58
|
+
install (development mode), it reports the npm state without updating.
|
|
53
59
|
|
|
54
60
|
## Screenshots
|
|
55
61
|
|
|
@@ -89,6 +95,7 @@ sun/moon toggle in every header flips to the dark palette at any time.
|
|
|
89
95
|
runtime download covers installers that skip postinstall scripts). No
|
|
90
96
|
user-side tooling, account, or domain is needed — a Cloudflare quick
|
|
91
97
|
tunnel is free and anonymous.
|
|
98
|
+
- 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.
|
|
92
99
|
|
|
93
100
|
## Install
|
|
94
101
|
|
|
@@ -121,7 +128,7 @@ mounts both halves.
|
|
|
121
128
|
1. `dsh web --host 0.0.0.0` (the printed LAN URL confirms reachability).
|
|
122
129
|
2. Click the phone icon → the panel mints a fresh one-time QR.
|
|
123
130
|
3. Scan with the phone (or open the copied link): the phone binds and
|
|
124
|
-
lands on the **standalone mobile surface at `/m
|
|
131
|
+
lands on the **standalone mobile surface at `/m/`** — no desktop UI on a
|
|
125
132
|
small screen. The surface is deliberately thin:
|
|
126
133
|
- workspaces straight away (a 新建会话 button lives on each workspace's
|
|
127
134
|
session list: it creates a blank session attached to that workspace via
|
|
@@ -150,9 +157,10 @@ mounts both halves.
|
|
|
150
157
|
sheet with persistent toggles for 工具调用 (tool-call disclosures) and
|
|
151
158
|
系统提示词 (injected system messages), and a 上下文 usage chip that
|
|
152
159
|
shows the latest assistant answer's context-fill percentage.
|
|
153
|
-
4. The
|
|
160
|
+
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.
|
|
161
|
+
5. The desktop badge flips to 已连接 in real time; it falls back to
|
|
154
162
|
offline/断开 when the phone leaves.
|
|
155
|
-
|
|
163
|
+
6. 刷新二维码 invalidates the old link and issues a new one. 停止 revokes
|
|
156
164
|
mobile access: paired devices 403 on their next request, including their
|
|
157
165
|
live stream.
|
|
158
166
|
|
|
@@ -169,7 +177,7 @@ to the advisory `session.models` / `session.selectModel` pair, creation to
|
|
|
169
177
|
directory of its own), and the permission picker only ever sends the
|
|
170
178
|
mode-agnostic `/permission` command
|
|
171
179
|
through the already-allowlisted `session.prompt`); the live stream arrives
|
|
172
|
-
over Server-Sent Events on `/m/api/events.mux`.
|
|
180
|
+
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.
|
|
173
181
|
|
|
174
182
|
### Behavior notes
|
|
175
183
|
|
|
@@ -180,11 +188,16 @@ over Server-Sent Events on `/m/api/events.mux`.
|
|
|
180
188
|
through its own `/m/api` preferences method when a chat opens. On
|
|
181
189
|
browsers that support `field-sizing: content`, the input grows with the
|
|
182
190
|
draft up to its 120px cap in either mode.
|
|
191
|
+
- 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.
|
|
183
192
|
- Installing this plugin gates non-loopback `/api` access behind pairing
|
|
184
193
|
(see `requirePairingForLan` in `src/index.ts`). A desktop browser opened
|
|
185
194
|
via the LAN URL must pair like any remote device; loopback (127.0.0.1)
|
|
186
195
|
is unaffected. Set `requirePairingForLan: false` in the profile patch to
|
|
187
196
|
restore the open-LAN behavior while keeping tokens/status/revocation.
|
|
197
|
+
- Sibling host routes outside `/api` (the right-panel `/aionui-panel/*`
|
|
198
|
+
family) can consult this plugin's `remoteWebUiPairing` service: a live
|
|
199
|
+
paired-device cookie is an allow path, `stop()` still cuts them off, and
|
|
200
|
+
the service is absent when this plugin is not installed.
|
|
188
201
|
- The QR link is built from the machine's non-internal IPv4 literals; a
|
|
189
202
|
multi-homed host (Wi-Fi + wired, or a proxy/VPN virtual adapter) shows a
|
|
190
203
|
radio picker so you can advertise the network the phone can actually
|
|
@@ -367,15 +380,24 @@ panel still opens at `http://127.0.0.1`.
|
|
|
367
380
|
|
|
368
381
|
- **Revocation is per-request**: a paired phone whose request is already in
|
|
369
382
|
flight when 停止 lands completes that request; the next one 403s.
|
|
370
|
-
- **Device sessions are in-memory**: pairing state (token + devices)
|
|
371
|
-
with the `dsh web` process.
|
|
383
|
+
- **Device sessions are in-memory by default**: pairing state (token + devices)
|
|
384
|
+
resets with the `dsh web` process. Setting the `devicesFile` config option
|
|
385
|
+
(an absolute JSON path) persists sessions across restarts — the phone-side
|
|
386
|
+
cookie already lives 365 days, so with persistence a `dsh web` restart no
|
|
387
|
+
longer requires re-scanning the QR. An explicit 停止 (stop) still revokes
|
|
388
|
+
immediately and the revocation is persisted. Device ids are session
|
|
389
|
+
credentials (the gate authorizes requests by the device id in the cookie),
|
|
390
|
+
so the file is written 0600 via a temp file + atomic rename; keep it inside
|
|
391
|
+
a 0700 directory such as `$DSH_HOME`. Changing `cookieName` invalidates
|
|
392
|
+
existing devices (expected).
|
|
372
393
|
- **No per-device management UI**: the panel shows aggregate status
|
|
373
394
|
(waiting / connected N / offline); individual device revocation is
|
|
374
395
|
deferred.
|
|
375
396
|
- **Quick-tunnel hostnames change per run**: a `trycloudflare.com` URL is
|
|
376
397
|
random on every `cloudflared` start, so `--trusted-host` and
|
|
377
398
|
`publicBaseUrl` must be updated together whenever the tunnel restarts.
|
|
378
|
-
A named tunnel (fixed hostname) avoids the churn.
|
|
399
|
+
A named tunnel (fixed hostname) avoids the churn and is required for a durable installed-PWA address.
|
|
400
|
+
- **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.
|
|
379
401
|
- **Dev HMR**: `dsh web --dev` polls every roster bundle by path, so
|
|
380
402
|
rebuilding this package (its own `tsdown --watch`) hot-reloads the client
|
|
381
403
|
bundle; no harness-side watcher is involved.
|
|
@@ -387,4 +409,4 @@ QR as a dependency-free SVG component — no canvas, no server-side image
|
|
|
387
409
|
generation. It is inlined into the client bundle at build time (like the
|
|
388
410
|
official skin/turtle-ui plugins inline their non-shared deps), so profile
|
|
389
411
|
installations need no extra runtime dependency beyond the dsh peer closure.
|
|
390
|
-
`schemastery` is the DSH-standard config schema validator.
|
|
412
|
+
`schemastery` is the DSH-standard config schema validator.
|
package/README.zh.md
CHANGED
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 中文
|
|
4
4
|
|
|
5
|
-
> 移动端远程控制 + 一键远程更新:扫码配对后用手机远程使用当前 dsh web
|
|
5
|
+
> 移动端远程控制 + 一键远程更新:扫码配对后用手机远程使用当前 dsh web 工作区;侧边栏加载后静默检查 dsh-web-ui 全家桶新版本,发现时标记更新按钮;点击按钮后自动更新全家桶。
|
|
6
6
|
|
|
7
|
-
本仓库是 DeepSeek Harness(DSH)的外部插件包:为 dsh web GUI 提供扫码配对式移动端远程控制,外加 dsh-web-ui 全家桶的一键自更新。它是单一双半区包——host 半区持有配对令牌、设备会话、`/api/pair` 路由族与 `/api/update`
|
|
7
|
+
本仓库是 DeepSeek Harness(DSH)的外部插件包:为 dsh web GUI 提供扫码配对式移动端远程控制,外加 dsh-web-ui 全家桶的一键自更新。它是单一双半区包——host 半区持有配对令牌、设备会话、`/api/pair` 路由族与 `/api/update` 面板;浏览器半区渲染侧边栏底部入口(下载触发按钮与设置按钮旁的手机图标入口)、带二维码的配对面板、实时设备状态,以及停止/刷新/复制操作,还渲染探测并执行更新的更新面板。
|
|
8
8
|
|
|
9
9
|
## 功能
|
|
10
10
|
|
|
11
|
-
-
|
|
11
|
+
- **入口**:设置按钮旁始终显示手机图标;悬停时显示“移动端远程控制”提示。
|
|
12
12
|
- **面板**:「移动端远程控制」标题、「扫码或在手机上打开链接,即可远程控制当前工作区」副标题、「手机扫码连接」卡片(含状态区「等待手机连接」+ 状态徽标)、大号二维码、「无法扫码?可以在手机上打开链接」提示,以及三个按钮:停止 / 刷新二维码 / 复制链接。
|
|
13
|
-
- **手机侧**:扫码将手机与一次性、限时令牌绑定,并落地到 **`/m
|
|
13
|
+
- **手机侧**:扫码将手机与一次性、限时令牌绑定,并落地到 **`/m/` 独立移动端界面**——一款专为小屏设计的轻客户端(见[截图](#截图)),而不是把桌面 UI 塞进手机。该页面可安装为 PWA;每个已安装应用使用自己的已配对设备 cookie,存储隔离的移动端 Web App 需要在该上下文打开新的二维码链接,或粘贴桌面端新复制的链接完成配对。链接携带 `workspace` 参数,手机落地到桌面正在查看的同一工作区。
|
|
14
14
|
- **安全**:一个有效的一次性令牌(刷新会使旧链接失效;已接受的令牌不可复用;令牌会过期)。停止会撤销每一台已配对设备与当前令牌——已配对设备在下一次请求时被切断。当插件 `requirePairingForLan` 门开启(默认)时,每个非 loopback 的 `/api` 请求必须携带有效的已配对设备 cookie,因此二维码是进入暴露在局域网上的 dsh web 的唯一途径。
|
|
15
15
|
- **实时状态**:桌面面板经 SSE 流实时镜像配对状态(等待 → 已连接 → 已断开)。
|
|
16
|
-
-
|
|
16
|
+
- **远程更新**:侧边栏加载后,底部的下载触发按钮(手机图标左侧)会静默探测 npm registry 上已安装的 `@linxin666/dsh-*` 全家桶版本;发现可自动更新的新版本时,按钮显示圆点并提供“发现新版本,检查更新”提示。点击按钮打开更新面板;未安装聚合包时,检查和更新覆盖 profile 中 registry 管理的全部 `@linxin666/*` 直接依赖,本地 link / file 开发依赖会被跳过。当存在较新版本时,面板自动执行更新(在所属 dsh profile 内 `pnpm update --latest`;pnpm 缺失时依次回退 `corepack pnpm`、`npx --yes pnpm`,Windows 上经 `cmd.exe` 执行以解析 npm 安装的 `.cmd` shim;由仅 loopback 的 `/api/update/status` + `/api/update/run` 端点驱动)并请求重启 dsh web 以生效。pnpm 绿色退出后还会对照 registry 复核已装版本:绿色退出但版本纹丝不动(例如 pnpm 的 `minimumReleaseAge` 门禁静默跳过同日发布的新版本)会报告为「未更新成功」并附配置指引,而不是误报成功。锚点自身是本地 link 安装(开发模式)时,只报告 npm 状态而不更新。
|
|
17
17
|
|
|
18
18
|
## 截图
|
|
19
19
|
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
- 其 `dsh` CLI 支持 profile(`dsh --profile`、`dsh plugin`)的 DSH 安装——本包所依托的 profile/bundle 机制。
|
|
30
30
|
- 局域网使用必须手机可到达服务器:用 `dsh web --host 0.0.0.0` 启动。默认 `127.0.0.1` 绑定时,面板会显示明确说明而不是死二维码——除非配置了公网 base URL(见下文「通过互联网远程访问」),那会让二维码在无需重新绑定即可从任意位置访问。面板的 mint/stop 端点设计上仅限 loopback:在局域网 URL 打开的桌面浏览器只会看到「配对面板仅限本机使用」横幅——请在 `http://127.0.0.1` 打开面板,让手机使用配对链接。
|
|
31
31
|
- 一键公网隧道(`autoTunnel`)需要 `cloudflared` 平台二进制随包分发(其 postinstall 会下载它;运行时下载覆盖跳过 postinstall 脚本的安装器)。无需用户侧工具、账号或域名——Cloudflare quick tunnel 免费且匿名。
|
|
32
|
+
- 把 `/m/` 安装为 PWA 需要安全上下文:`localhost` 和 `127.0.0.1` 可用于本机,手机安装需要 HTTPS。普通局域网 HTTP 仍可在线使用移动端远程控制,但无法注册其 Service Worker。
|
|
32
33
|
|
|
33
34
|
## 安装
|
|
34
35
|
|
|
@@ -54,7 +55,7 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
|
|
|
54
55
|
|
|
55
56
|
1. `dsh web --host 0.0.0.0`(打印的局域网 URL 确认可达性)。
|
|
56
57
|
2. 点击手机图标 → 面板铸一枚新的二维码。
|
|
57
|
-
3. 用手机扫码(或打开复制的链接):手机绑定并落到 **`/m
|
|
58
|
+
3. 用手机扫码(或打开复制的链接):手机绑定并落到 **`/m/` 独立移动端界面**——不在小屏显示桌面 UI。该界面刻意精简:
|
|
58
59
|
- 直接进入工作区(每个工作区的会话列表上有 新建会话 按钮:它经 host 的 `session.create` 创建附加到该工作区的空白会话,并立即打开新聊天),
|
|
59
60
|
- 一个工作区的会话**增量**加载(每页 20 行,"加载更多会话"继续;绝不同时加载整份列表),
|
|
60
61
|
- 打开会话**按需**抓取聊天内容(历史分页,"加载更早的消息"继续往回翻),
|
|
@@ -62,15 +63,18 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
|
|
|
62
63
|
- **亮色优先主题**:界面默认亮色调色板;每个页头内的日/月切换翻到暗色调色板,选择跨访问持久(localStorage),
|
|
63
64
|
- 消息按桌面折叠纪律渲染:推理隐藏在被折叠的 深度思考 揭示下面,工具调用隐藏在被折叠的 工具 行下面(点击查看每个调用的参数),超长回答藏在显式 展开全文 切换下面,每行带时间,并且 assistant 回复按 GFM Markdown 渲染(标题 / 加粗 / 斜体 / 行内码 / 代码块 / 列表 / 表格 / 引用 / 链接 / 图片;零依赖自写渲染器,先转义再白名单协议,移动端 bundle 体积几乎不变;KaTeX 公式暂不支持,后续单独评估),用户消息保持纯文本,
|
|
64
65
|
- 输入栏工具条带 **模型** 选择器(provider 分组目录 + 每模型 思考强度 effort 区)与 **权限** 选择器(权限预设;完全权限 需要显式确认步骤)。两者都走 host 自己的 `session.models` / `session.selectModel` RPC 与 `/permission` 命令——手机改的与桌面改的是同一个会话设置——外加 **显示** 弹层(含 工具调用 与 系统提示词 两个持久开关)和一个 上下文 用量 chip(显示最近一次助手回答的上下文占用百分比)。
|
|
65
|
-
4.
|
|
66
|
-
5.
|
|
66
|
+
4. 在安全 origin 上从浏览器安装 `/m/` 页面。已安装应用保留相同的移动端远程控制能力;其浏览器上下文没有已配对设备 cookie 时,把桌面端新复制的配对链接粘贴进配对页面。
|
|
67
|
+
5. 桌面徽标实时翻到 已连接;手机离开时回落到离线/断开。
|
|
68
|
+
6. 刷新二维码 使旧链接失效并铸一枚新的。停止 撤销移动端访问:已配对设备下一次请求 403,包括其实时流。
|
|
67
69
|
|
|
68
|
-
该移动端界面完全自包含在本插件内:`/m
|
|
70
|
+
该移动端界面完全自包含在本插件内:`/m/` 页面及其数据通道(`/m/api`)由插件自己的路由伺服,**无需任何 harness 源码改动**——手机的 RPC 调用走插件的 `/m/api` 代理(它委托给 host 的 ApiProxy 服务并自己分页 `session.list`),因此被隧道化的 Host 永远不必进入连接插件的信任围栏。手机受其已配对设备 cookie 与显式方法白名单门控(settings/credentials/host-action 域手机永远不可达;模型读写限制于建议性的 `session.models` / `session.selectModel` 对,创建限制于 `session.create`(仅工作区 id——手机绝不自命名工作目录),权限选择器只通过已放行的 `session.prompt` 发送模式无关的 `/permission` 命令);实时流在 `/m/api/events.mux` 上经 Server-Sent Events 送达。规范的 `/m/` 页面拥有同 scope 的 manifest 与 Service Worker;其缓存仅包含静态壳和离线页,绝不包含移动端 API 响应、会话数据或命令。
|
|
69
71
|
|
|
70
72
|
### 行为说明
|
|
71
73
|
|
|
72
74
|
- 移动端输入框默认 Enter 发送(Shift+Enter 换行)。在插件设置卡片(或 profile patch)把 `mobileEnterToSend` 设为 false 后,普通 Enter 改为插入换行,只有「发送」按钮会发送;手机打开聊天时经自己的 `/m/api` 偏好方法读取该开关。在支持 `field-sizing: content` 的浏览器上,输入框随草稿自动增高,最高 120px 封顶(两种模式一致)。
|
|
75
|
+
- `/m/` Worker 对静态壳使用 network-first 回退,并等待当前页面关闭后才激活更新。它旁路 `/m/api`、`/api`、SSE 与所有写请求。
|
|
73
76
|
- 安装本插件会门控非 loopback 的 `/api` 访问于配对之后(见 `src/index.ts` 的 `requirePairingForLan`)。经局域网 URL 打开的桌面浏览器必须像任何远程设备一样配对;loopback(127.0.0.1)不受影响。把 profile patch 里 `requirePairingForLan` 设为 false 可恢复开放局域网行为,同时保留令牌/状态/撤销。
|
|
77
|
+
- `/api` 之外的兄弟 host 路由(右侧面板的 `/aionui-panel/*`)可查询本插件的 `remoteWebUiPairing` 服务:有效的已配对设备 cookie 是放行路径,`stop()` 仍会切断它们;未安装本插件时该服务不存在。
|
|
74
78
|
- 二维码链接基于机器的非内部 IPv4 字面量构建;多宿主主机(Wi-Fi + 有线,或代理/VPN 虚拟适配器)会显示单选器供你发布手机实际可达的网络。第一个字面量是默认值。设 `publicBaseUrl` 后,单选器在顶部额外加一项 公网地址——默认二维码改用公网 base,选中局域网字面量会重新铸一枚网内链接。
|
|
75
79
|
- 配置的 `publicBaseUrl` 本身满足可达绑定需求:`dsh web` 绑定 `127.0.0.1`(不带 `--host 0.0.0.0`)仍能经隧道铸出可用的公网二维码链接。
|
|
76
80
|
|
|
@@ -161,11 +165,12 @@ pnpm run build
|
|
|
161
165
|
## 已知限制与待办
|
|
162
166
|
|
|
163
167
|
- **撤销是逐请求的**:已配对手机请求已在 停止 落地时在途,完成该请求;下一个 403。
|
|
164
|
-
-
|
|
168
|
+
- **设备会话默认在内存中**:配对状态(token + devices)随 `dsh web` 进程重置;设置 `devicesFile` 配置项(绝对路径 JSON)后可跨重启持久化——手机端 cookie 本有 365 天有效期,持久化后重启 `dsh web` 无需重新扫码(点「停止」主动撤销仍会立即生效并同步落盘)。设备 id 即会话凭证(网关凭 cookie 中的设备 id 放行请求),文件以 0600 权限经临时文件原子写入,建议把文件放置在 0700 目录(如 `$DSH_HOME`)内;变更 `cookieName` 会使旧设备失效(预期行为)。
|
|
165
169
|
- **无逐设备管理 UI**:面板显示聚合状态(waiting / connected N / offline);单设备撤销延后。
|
|
166
|
-
- **Quick-tunnel hostname 每次运行变化**:`trycloudflare.com` URL 每次 `cloudflared` 启动随机,所以隧道重启时 `--trusted-host` 与 `publicBaseUrl` 必须一起更新。named tunnel(固定 hostname
|
|
170
|
+
- **Quick-tunnel hostname 每次运行变化**:`trycloudflare.com` URL 每次 `cloudflared` 启动随机,所以隧道重启时 `--trusted-host` 与 `publicBaseUrl` 必须一起更新。named tunnel(固定 hostname)避免这种抖动,也是持久安装 PWA 地址所必需的。
|
|
171
|
+
- **PWA 为在线优先**:只缓存静态壳和离线页。全部移动端远程控制能力仍要求运行中的 DSH host;离线时不提供会话、API 响应或命令。
|
|
167
172
|
- **开发 HMR**:`dsh web --dev` 按路径轮询每个 roster bundle,因此重建本包(其自己的 `tsdown --watch`)会热重载 client bundle;无 harness 侧 watcher。
|
|
168
173
|
|
|
169
174
|
## 依赖理由
|
|
170
175
|
|
|
171
|
-
`qrcode.react`(MIT,活跃维护,React 16–19 支持)将二维码渲染为无依赖的 SVG 组件——无 canvas、无服务端图片生成。它在构建时内联进 client bundle(与官方 skin/turtle-ui 插件内联其非共享依赖相同),profile 安装无需超出 dsh peer closure 之外的额外运行时依赖。`schemastery` 是 DSH 标准配置 schema 校验器。
|
|
176
|
+
`qrcode.react`(MIT,活跃维护,React 16–19 支持)将二维码渲染为无依赖的 SVG 组件——无 canvas、无服务端图片生成。它在构建时内联进 client bundle(与官方 skin/turtle-ui 插件内联其非共享依赖相同),profile 安装无需超出 dsh peer closure 之外的额外运行时依赖。`schemastery` 是 DSH 标准配置 schema 校验器。
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/* PWA worker for the standalone /m mobile shell. */
|
|
2
|
+
const CACHE_NAME = 'dsh-remote-mobile-shell-v1'
|
|
3
|
+
const OFFLINE_URL = '/m/offline.html'
|
|
4
|
+
const SHELL_PATHS = new Set([
|
|
5
|
+
'/m/',
|
|
6
|
+
'/m/mobile.js',
|
|
7
|
+
'/m/manifest.webmanifest',
|
|
8
|
+
'/m/apple-touch-icon.png',
|
|
9
|
+
'/m/icon-192.png',
|
|
10
|
+
'/m/icon-512.png',
|
|
11
|
+
OFFLINE_URL,
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
self.addEventListener('install', event => {
|
|
15
|
+
event.waitUntil(caches.open(CACHE_NAME).then(cache => cache.add(OFFLINE_URL)))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
self.addEventListener('activate', event => {
|
|
19
|
+
event.waitUntil(caches.keys().then(keys => Promise.all(
|
|
20
|
+
keys
|
|
21
|
+
.filter(key => key.startsWith('dsh-remote-mobile-shell-') && key !== CACHE_NAME)
|
|
22
|
+
.map(key => caches.delete(key)),
|
|
23
|
+
)))
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
self.addEventListener('fetch', event => {
|
|
27
|
+
const request = event.request
|
|
28
|
+
if (request.method !== 'GET') return
|
|
29
|
+
|
|
30
|
+
const url = new URL(request.url)
|
|
31
|
+
if (url.origin !== self.location.origin) return
|
|
32
|
+
if (url.pathname === '/api' || url.pathname.startsWith('/api/') || url.pathname === '/m/api' || url.pathname.startsWith('/m/api/')) return
|
|
33
|
+
|
|
34
|
+
const isMobileNavigation = request.mode === 'navigate' && (url.pathname === '/m/' || url.pathname === '/m')
|
|
35
|
+
if (isMobileNavigation) {
|
|
36
|
+
event.respondWith(networkFirst(request, OFFLINE_URL, false))
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (SHELL_PATHS.has(url.pathname)) event.respondWith(networkFirst(request, url.pathname))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
async function networkFirst(request, fallbackPath, allowCachedResponse = true) {
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetch(request)
|
|
46
|
+
if (response.status >= 500) throw new Error('mobile shell unavailable')
|
|
47
|
+
if (response.ok && new URL(request.url).search === '') {
|
|
48
|
+
const cache = await caches.open(CACHE_NAME)
|
|
49
|
+
await cache.put(request, response.clone())
|
|
50
|
+
}
|
|
51
|
+
return response
|
|
52
|
+
} catch {
|
|
53
|
+
const cache = await caches.open(CACHE_NAME)
|
|
54
|
+
if (allowCachedResponse) {
|
|
55
|
+
const cached = await cache.match(request)
|
|
56
|
+
if (cached !== undefined) return cached
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const fallback = await cache.match(fallbackPath)
|
|
60
|
+
if (fallback !== undefined) return fallback
|
|
61
|
+
|
|
62
|
+
return new Response('', { status: 503, statusText: 'Service Unavailable' })
|
|
63
|
+
}
|
|
64
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -1343,7 +1343,7 @@ window.__ModuleLoader__.load({
|
|
|
1343
1343
|
}
|
|
1344
1344
|
//#endregion
|
|
1345
1345
|
//#region \0dsh-css:packages/dsh-remote-web-ui/src/client/remote.module.css.mjs
|
|
1346
|
-
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}.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;overflow:hidden}.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_notice{z-index:2000;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);max-width:min(420px,100vw - 32px);box-shadow:var(--dsw-shadow-lv3);border-radius:12px;padding:12px 16px;position:fixed;top:16px;left:50%;transform:translate(-50%)}.fThDlq_noticeTitle{color:var(--dsw-alias-state-error-primary);margin:0;font-weight:500}.fThDlq_noticeDetail{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}.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}@media (prefers-reduced-motion:reduce){.fThDlq_trigger,.fThDlq_close,.fThDlq_action,.fThDlq_address{transition:none}}.fThDlq_entryRow{flex:none;align-items:center;gap:6px;display:flex}.fThDlq_entryRow[data-rail=rail]{flex-direction:column-reverse;gap:4px}.fThDlq_updateStatus{margin:0;font-weight:600}.fThDlq_updateDetail{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}.fThDlq_updateError{color:var(--dsw-alias-text-danger,var(--dsw-alias-label-primary));margin:0;font-weight:600}.fThDlq_updateOutput{background:var(--dsw-alias-bg-layer-1);max-height:180px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-all;border-radius:10px;margin:10px 0 0;padding:10px 12px;font-size:12px;line-height:18px;overflow:auto}.fThDlq_updateList{flex-direction:column;gap:6px;margin:12px 0 0;padding:0;list-style:none;display:flex}.fThDlq_updateListItem{justify-content:space-between;align-items:baseline;gap:12px;font-size:13px;display:flex}.fThDlq_updateListName{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);overflow:hidden}.fThDlq_updateListVersions{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none}.fThDlq_updateActions{justify-content:flex-end;margin-top:16px;display:flex}.fThDlq_updateRetry{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:10px;align-items:center;gap:6px;padding:8px 14px;font-size:13px;transition:background-color .12s,color .12s;display:inline-flex}.fThDlq_updateRetry:hover{background:var(--dsw-alias-interactive-bg-active)}@media (prefers-reduced-motion:reduce){.fThDlq_updateRetry{transition:none}}";
|
|
1346
|
+
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-update-available]:after{box-sizing:border-box;border:2px solid var(--dsw-alias-bg-layer-1);background:var(--dsw-alias-brand-primary);content:\"\";pointer-events:none;border-radius:50%;width:8px;height:8px;position:absolute;top:4px;right:4px}.fThDlq_trigger[data-wide=wide]{border-radius:8px;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;overflow:hidden}.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_notice{z-index:2000;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);max-width:min(420px,100vw - 32px);box-shadow:var(--dsw-shadow-lv3);border-radius:12px;padding:12px 16px;position:fixed;top:16px;left:50%;transform:translate(-50%)}.fThDlq_noticeTitle{color:var(--dsw-alias-state-error-primary);margin:0;font-weight:500}.fThDlq_noticeDetail{color:var(--dsw-alias-label-secondary);margin:4px 0 0;font-size:13px}.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}@media (prefers-reduced-motion:reduce){.fThDlq_trigger,.fThDlq_close,.fThDlq_action,.fThDlq_address{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}.fThDlq_updateStatus{margin:0;font-weight:600}.fThDlq_updateDetail{color:var(--dsw-alias-label-secondary);margin:6px 0 0;font-size:13px}.fThDlq_updateError{color:var(--dsw-alias-text-danger,var(--dsw-alias-label-primary));margin:0;font-weight:600}.fThDlq_updateOutput{background:var(--dsw-alias-bg-layer-1);max-height:180px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-all;border-radius:10px;margin:10px 0 0;padding:10px 12px;font-size:12px;line-height:18px;overflow:auto}.fThDlq_updateList{flex-direction:column;gap:6px;margin:12px 0 0;padding:0;list-style:none;display:flex}.fThDlq_updateListItem{justify-content:space-between;align-items:baseline;gap:12px;font-size:13px;display:flex}.fThDlq_updateListName{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);overflow:hidden}.fThDlq_updateListVersions{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none}.fThDlq_updateActions{justify-content:flex-end;margin-top:16px;display:flex}.fThDlq_updateRetry{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:10px;align-items:center;gap:6px;padding:8px 14px;font-size:13px;transition:background-color .12s,color .12s;display:inline-flex}.fThDlq_updateRetry:hover{background:var(--dsw-alias-interactive-bg-active)}@media (prefers-reduced-motion:reduce){.fThDlq_updateRetry{transition:none}}";
|
|
1347
1347
|
const tagId$1 = "@linxin666/dsh-remote-web-ui/remote.module.css";
|
|
1348
1348
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
1349
1349
|
const tag = document.createElement("style");
|
|
@@ -1899,13 +1899,27 @@ window.__ModuleLoader__.load({
|
|
|
1899
1899
|
function UpdateEntry({ wide, t }) {
|
|
1900
1900
|
const [open, setOpen] = (0, react.useState)(false);
|
|
1901
1901
|
const [view, setView] = (0, react.useState)({ kind: "checking" });
|
|
1902
|
+
const [updateAvailable, setUpdateAvailable] = (0, react.useState)(false);
|
|
1902
1903
|
const runToken = (0, react.useRef)(0);
|
|
1904
|
+
const availabilityToken = (0, react.useRef)(0);
|
|
1905
|
+
const mounted = (0, react.useRef)(false);
|
|
1906
|
+
const probeAvailability = (0, react.useCallback)(async () => {
|
|
1907
|
+
const token = ++availabilityToken.current;
|
|
1908
|
+
try {
|
|
1909
|
+
const status = await fetchUpdateStatus();
|
|
1910
|
+
if (token === availabilityToken.current) setUpdateAvailable(status.mode === "npm" && status.outdated);
|
|
1911
|
+
} catch {
|
|
1912
|
+
if (token === availabilityToken.current) setUpdateAvailable(false);
|
|
1913
|
+
}
|
|
1914
|
+
}, []);
|
|
1903
1915
|
const check = (0, react.useCallback)(async () => {
|
|
1916
|
+
const availabilityCheck = ++availabilityToken.current;
|
|
1904
1917
|
setView({ kind: "checking" });
|
|
1905
1918
|
let status;
|
|
1906
1919
|
try {
|
|
1907
1920
|
status = await fetchUpdateStatus();
|
|
1908
1921
|
} catch (error) {
|
|
1922
|
+
if (availabilityCheck === availabilityToken.current) setUpdateAvailable(false);
|
|
1909
1923
|
if (error instanceof UpdateStatusError && error.status === 404) {
|
|
1910
1924
|
setView({
|
|
1911
1925
|
kind: "error",
|
|
@@ -1921,6 +1935,7 @@ window.__ModuleLoader__.load({
|
|
|
1921
1935
|
});
|
|
1922
1936
|
return;
|
|
1923
1937
|
}
|
|
1938
|
+
if (availabilityCheck === availabilityToken.current) setUpdateAvailable(status.mode === "npm" && status.outdated);
|
|
1924
1939
|
if (status.error === "registry-unreachable") {
|
|
1925
1940
|
setView({
|
|
1926
1941
|
kind: "result",
|
|
@@ -1940,6 +1955,7 @@ window.__ModuleLoader__.load({
|
|
|
1940
1955
|
const token = ++runToken.current;
|
|
1941
1956
|
try {
|
|
1942
1957
|
const result = await runUpdate();
|
|
1958
|
+
if (result.ok && mounted.current) setUpdateAvailable(false);
|
|
1943
1959
|
if (token !== runToken.current) return;
|
|
1944
1960
|
setView({
|
|
1945
1961
|
kind: "done",
|
|
@@ -1962,15 +1978,23 @@ window.__ModuleLoader__.load({
|
|
|
1962
1978
|
runToken.current++;
|
|
1963
1979
|
setOpen(false);
|
|
1964
1980
|
}, []);
|
|
1965
|
-
(0, react.useEffect)(() =>
|
|
1966
|
-
|
|
1967
|
-
|
|
1981
|
+
(0, react.useEffect)(() => {
|
|
1982
|
+
mounted.current = true;
|
|
1983
|
+
probeAvailability();
|
|
1984
|
+
return () => {
|
|
1985
|
+
mounted.current = false;
|
|
1986
|
+
runToken.current++;
|
|
1987
|
+
availabilityToken.current++;
|
|
1988
|
+
};
|
|
1989
|
+
}, [probeAvailability]);
|
|
1990
|
+
const updateLabel = updateAvailable ? t("update.availableLabel") : t("update.label");
|
|
1968
1991
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1969
1992
|
type: "button",
|
|
1970
1993
|
className: remote_module_css_default.trigger,
|
|
1971
1994
|
"data-wide": wide ? void 0 : "rail",
|
|
1972
|
-
"
|
|
1973
|
-
|
|
1995
|
+
"data-update-available": updateAvailable ? "true" : void 0,
|
|
1996
|
+
"aria-label": updateLabel,
|
|
1997
|
+
title: updateLabel,
|
|
1974
1998
|
onClick: openPanel,
|
|
1975
1999
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutline16, { size: wide ? 16 : 18 })
|
|
1976
2000
|
}), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -2171,12 +2195,12 @@ window.__ModuleLoader__.load({
|
|
|
2171
2195
|
})]
|
|
2172
2196
|
}), document.body)] });
|
|
2173
2197
|
}
|
|
2174
|
-
/** The trigger: an icon
|
|
2198
|
+
/** The trigger: an icon-only control with a persistent accessible label. */
|
|
2175
2199
|
function TooltipAnchor({ wide, label, onClick }) {
|
|
2176
2200
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2177
2201
|
type: "button",
|
|
2178
2202
|
className: remote_module_css_default.trigger,
|
|
2179
|
-
"data-wide": wide ?
|
|
2203
|
+
"data-wide": wide ? "wide" : "rail",
|
|
2180
2204
|
"aria-label": label,
|
|
2181
2205
|
title: label,
|
|
2182
2206
|
onClick,
|
|
@@ -2379,7 +2403,7 @@ window.__ModuleLoader__.load({
|
|
|
2379
2403
|
children: props.t("settings.readOnly")
|
|
2380
2404
|
}) : null,
|
|
2381
2405
|
props.children,
|
|
2382
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2406
|
+
props.hideFooter === true ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2383
2407
|
className: settings_card_module_css_default.footer,
|
|
2384
2408
|
children: [
|
|
2385
2409
|
state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
@@ -2868,6 +2892,7 @@ window.__ModuleLoader__.load({
|
|
|
2868
2892
|
t,
|
|
2869
2893
|
titleKey: "settings.title",
|
|
2870
2894
|
descriptionKey: "settings.description",
|
|
2895
|
+
defaultOpen: false,
|
|
2871
2896
|
state,
|
|
2872
2897
|
onSave: props.save,
|
|
2873
2898
|
onDiscard: props.discard,
|
|
@@ -3083,6 +3108,7 @@ window.__ModuleLoader__.load({
|
|
|
3083
3108
|
"settings.saveFailed": "部署未接受这些值,已保留供你修改。",
|
|
3084
3109
|
"settings.invalidNumber": "请输入数字,留空则使用默认值。",
|
|
3085
3110
|
"update.label": "检查更新",
|
|
3111
|
+
"update.availableLabel": "发现新版本,检查更新",
|
|
3086
3112
|
"update.title": "检查更新",
|
|
3087
3113
|
"update.checking": "正在检查远程仓库中的新版本…",
|
|
3088
3114
|
"update.upToDate": "当前已是最新版本",
|
|
@@ -3187,6 +3213,7 @@ window.__ModuleLoader__.load({
|
|
|
3187
3213
|
"settings.saveFailed": "The deployment did not accept these values; they were left for you to correct.",
|
|
3188
3214
|
"settings.invalidNumber": "Enter a number, or leave blank to use the default.",
|
|
3189
3215
|
"update.label": "Check for updates",
|
|
3216
|
+
"update.availableLabel": "New version available. Check for updates",
|
|
3190
3217
|
"update.title": "Check for updates",
|
|
3191
3218
|
"update.checking": "Checking the remote repository for newer releases…",
|
|
3192
3219
|
"update.upToDate": "Everything is up to date",
|
|
@@ -3272,7 +3299,7 @@ window.__ModuleLoader__.load({
|
|
|
3272
3299
|
url.searchParams.delete("pair");
|
|
3273
3300
|
page.replaceState(`${url.pathname}${url.search}${url.hash}`);
|
|
3274
3301
|
if (ok) if (isMobileSurface()) {
|
|
3275
|
-
url.pathname = "/m";
|
|
3302
|
+
url.pathname = "/m/";
|
|
3276
3303
|
page.navigate(`${url.pathname}${url.search}${url.hash}`);
|
|
3277
3304
|
} else page.reload();
|
|
3278
3305
|
}
|