@linxin666/dsh-remote-web-ui 0.2.9 → 0.3.1
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 +46 -8
- package/README.zh.md +7 -2
- package/lib/client.js +103 -58
- package/lib/client.js.map +1 -1
- package/lib/index.js +704 -61
- package/lib/mobile.js +874 -64
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +71 -19
- package/lib/types/client/remote-channel.d.ts +4 -5
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +13 -28
- package/lib/types/http.d.ts +37 -8
- package/lib/types/http.d.ts.map +1 -1
- package/lib/types/http.js +76 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +25 -1
- package/lib/types/mobile/api.d.ts +57 -0
- package/lib/types/mobile/api.d.ts.map +1 -1
- package/lib/types/mobile/api.js +29 -0
- package/lib/types/mobile/markdown.d.ts +7 -1
- package/lib/types/mobile/markdown.d.ts.map +1 -1
- package/lib/types/mobile/markdown.js +16 -7
- package/lib/types/mobile/markdown.test.js +8 -0
- 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 +325 -6
- package/lib/types/mobile/views/ChatView.d.ts +10 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
- package/lib/types/mobile/views/ChatView.js +205 -8
- package/lib/types/mobile/views/ChatView.stream.test.d.ts +2 -0
- package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +1 -0
- package/lib/types/mobile/views/ChatView.stream.test.js +205 -0
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
- package/lib/types/mobile/views/WorkspaceView.js +46 -5
- package/lib/types/mobile/views/WorkspaceView.test.js +129 -2
- package/lib/types/mobile-api.d.ts +9 -3
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +61 -16
- package/lib/types/mobile-pending.d.ts +38 -0
- package/lib/types/mobile-pending.d.ts.map +1 -0
- package/lib/types/mobile-pending.js +59 -0
- package/lib/types/paired-model-catalog.d.ts +17 -0
- package/lib/types/paired-model-catalog.d.ts.map +1 -0
- package/lib/types/paired-model-catalog.js +311 -0
- package/lib/types/pairing.d.ts +13 -4
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +13 -5
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.d.ts +26 -0
- package/lib/types/remote-channel-boot.d.ts.map +1 -0
- package/lib/types/remote-channel-boot.js +128 -0
- package/lib/types/remote-channel-rules.d.ts +40 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -0
- package/lib/types/remote-channel-rules.js +32 -0
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +12 -23
- package/lib/types/uuid-polyfill.d.ts +10 -0
- package/lib/types/uuid-polyfill.d.ts.map +1 -0
- package/lib/types/uuid-polyfill.js +27 -0
- package/package.json +1 -1
- package/src/client/index.ts +72 -18
- package/src/client/remote-channel.ts +18 -28
- package/src/http.ts +87 -12
- package/src/index.ts +27 -2
- package/src/mobile/api.ts +91 -0
- package/src/mobile/markdown.test.ts +9 -0
- package/src/mobile/markdown.ts +17 -7
- package/src/mobile/mobile-styles.ts +326 -6
- package/src/mobile/views/ChatView.stream.test.tsx +231 -0
- package/src/mobile/views/ChatView.tsx +328 -10
- package/src/mobile/views/WorkspaceView.test.tsx +163 -2
- package/src/mobile/views/WorkspaceView.tsx +152 -26
- package/src/mobile-api.ts +59 -17
- package/src/mobile-pending.ts +88 -0
- package/src/paired-model-catalog.ts +272 -0
- package/src/pairing.ts +13 -5
- package/src/remote-api.ts +0 -1
- package/src/remote-channel-boot.ts +131 -0
- package/src/remote-channel-rules.ts +62 -0
- package/src/routes.ts +13 -24
- package/src/uuid-polyfill.ts +28 -0
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: edd272ef9b98700efc6ff465c5cb0dcc2e2d9648
|
|
6
|
+
README.zh.md: 53f1f72524ad3072ba2526108f7fc650403a5e0a
|
package/README.md
CHANGED
|
@@ -53,18 +53,45 @@ that probes and runs the update.
|
|
|
53
53
|
itself — pairing does not gate `/api` (no plugin can; the fence is the
|
|
54
54
|
SDK's own seam). The posture probe below reports that stance when `/api`
|
|
55
55
|
is reachable without pairing.
|
|
56
|
+
- **Paired model catalog**: authenticated paired devices can use `GET
|
|
57
|
+
/api/pair/model-catalog`, `POST /api/pair/model-catalog/discover`, and
|
|
58
|
+
`POST /api/pair/model-catalog/upsert` to inspect and adopt models for an
|
|
59
|
+
existing active `llm-pi-ai` provider only. The capability fixes the
|
|
60
|
+
provider settings address internally and cannot create providers or read or
|
|
61
|
+
change credentials, general settings, endpoints, headers, or arbitrary
|
|
62
|
+
configuration. The exact routes are served ahead of the connection
|
|
63
|
+
plugin's `/api` prefix, so a paired LAN or tunnel client may call them
|
|
64
|
+
directly, and they carry the same Host/Origin trust fence as the sibling
|
|
65
|
+
`/api/pair` endpoints: loopback, the advertised LAN literals, or the
|
|
66
|
+
configured public base URL. Through the `/remote` desktop channel all
|
|
67
|
+
`/api/pair/*` paths stay loopback-only. Adoption is refused while the
|
|
68
|
+
provider's live model catalog is unavailable or unknown. A provider whose
|
|
69
|
+
resolved `models` list is absent or empty keeps inheriting its installed
|
|
70
|
+
catalog until an unknown custom model must materialize that catalog;
|
|
71
|
+
existing model overrides are preserved and translated into the resulting
|
|
72
|
+
entries. Malformed or conflicting model profiles are refused instead of
|
|
73
|
+
being destructively rewritten. Stop or device
|
|
74
|
+
revocation disables it immediately; the
|
|
75
|
+
generic `settings.*`, `credentials.*`, and `llm.discoverModels` RPC methods
|
|
76
|
+
remain loopback-only.
|
|
56
77
|
- **Remote desktop channel**: with `requirePairingForLan` on (default), a
|
|
57
78
|
desktop Web GUI opened at the LAN URL or through the tunnel transparently
|
|
58
|
-
rides `/remote` — the same UI, gated by the same pairing cookie.
|
|
79
|
+
rides `/remote` — the same UI, gated by the same pairing cookie. The
|
|
80
|
+
rewrite installs twice-removed from the race it fixes: the host inlines a
|
|
81
|
+
small classic script right after the opening `<head>` tag
|
|
82
|
+
(`webserver/index-inject`), so fetch/WebSocket/EventSource and resource
|
|
83
|
+
`src` rewrites are active before any boot entry (the connection plugin
|
|
84
|
+
opens its event streams first); the browser half then adopts the installed
|
|
85
|
+
seat instead of patching twice. Loopback origins skip the patch entirely. Browser
|
|
59
86
|
requests under `/api`, `/sidebar`, `/git`, and `/pet`, including the known
|
|
60
87
|
event, terminal, and SSH WebSockets, are re-issued to the local web server
|
|
61
88
|
without forwarding the remote Origin or pairing cookie; the authenticated
|
|
62
89
|
proxy supplies its own same-origin browser marker for sibling route fences. The
|
|
63
90
|
SDK's loopback-only privileged methods (native dialogs, the settings and
|
|
64
|
-
credentials plane) stay unreachable from a paired remote desktop
|
|
65
|
-
`/api/pair
|
|
66
|
-
`/api/
|
|
67
|
-
endpoints stay loopback-only. Unpaired desktop browsers get a persistent
|
|
91
|
+
credentials plane) stay unreachable from a paired remote desktop;
|
|
92
|
+
`/api/pair/*` (including the paired model-catalog routes),
|
|
93
|
+
`/api/update/*`, `/api/plugin-manager/*`, `/api/dsh-desktop-launcher/*`
|
|
94
|
+
and `/api/dsh-web-ui-settings/*` control endpoints stay loopback-only. Unpaired desktop browsers get a persistent
|
|
68
95
|
full-page block instead of data (the page keys off the `unpaired` error
|
|
69
96
|
code, not every 403). The block retires once a gated call succeeds or the
|
|
70
97
|
channel itself is torn down — turning `requirePairingForLan` off (or the
|
|
@@ -223,9 +250,16 @@ need **no harness source changes** — the phone's RPC calls ride the
|
|
|
223
250
|
plugin's `/m/api` proxy (which delegates to the host ApiProxy service and
|
|
224
251
|
pages `session.list` itself), so the tunneled Host never has to enter the
|
|
225
252
|
connection plugin's trust fence. The phone is gated by its paired-device
|
|
226
|
-
cookie and an explicit method allowlist
|
|
227
|
-
|
|
228
|
-
|
|
253
|
+
cookie and an explicit method allowlist. The allowlist constrains the
|
|
254
|
+
`/m/api` proxy alone: the paired-device cookie itself also passes the global
|
|
255
|
+
api/gate, so a paired device is a full-control credential for the host `/api`
|
|
256
|
+
surface — settings/credentials/host-action domains stay unreachable only
|
|
257
|
+
because the SDK pins those privileged methods to loopback. Pairing is full
|
|
258
|
+
device trust; the `/m/api` proxy's model
|
|
259
|
+
reads/writes are limited to the advisory `session.models` /
|
|
260
|
+
`session.selectModel` pair. Separately, the exact paired-only model-catalog
|
|
261
|
+
routes may adopt models for an existing eligible `llm-pi-ai` provider; they
|
|
262
|
+
cannot create providers or access credentials or general settings. Agent preset
|
|
229
263
|
access to read-only `agentPreset.list`, creation to `session.create`
|
|
230
264
|
(workspace id plus an optional id from that roster — the phone never names a
|
|
231
265
|
working directory of its own), and the permission picker only ever sends the
|
|
@@ -458,6 +492,10 @@ accept → UI. Only `publicBaseUrl` (plugin config) names the tunneled host;
|
|
|
458
492
|
`--trusted-host` is not part of this pairing flow. The desktop panel still
|
|
459
493
|
opens at `http://127.0.0.1`.
|
|
460
494
|
|
|
495
|
+
## Security model
|
|
496
|
+
|
|
497
|
+
- Mobile unary routes and the mux event stream require a live paired-device session. 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.
|
|
498
|
+
|
|
461
499
|
## Known Limitations and Deferred Work
|
|
462
500
|
|
|
463
501
|
- **Revocation is per-request**: a paired phone whose request is already in
|
package/README.zh.md
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
- **手机侧**:扫码将手机与一次性、限时令牌绑定,并落地到 **`/m/` 独立移动端界面**——一款专为小屏设计的轻客户端(见[截图](#截图)),而不是把桌面 UI 塞进手机。该页面可安装为 PWA;每个已安装应用使用自己的已配对设备 cookie,存储隔离的移动端 Web App 需要在该上下文打开新的二维码链接,或粘贴桌面端新复制的链接完成配对。链接携带 `workspace` 参数,手机落地到桌面正在查看的同一工作区。
|
|
14
14
|
- **PC 侧(远程桌面配对)**:同一份二维码链接和配对令牌也能配对 PC 浏览器——把手机侧配对流扩展到桌面 Web GUI。从面板复制链接,在另一台电脑的浏览器打开(局域网 URL 或隧道 URL 均可);接受往返后,完整 Web GUI 在那台设备上运行——工作区、会话、聊天、切模型——受围栏保护的同源流量走门控的 `/remote` 通道,而不是直接调用仅 loopback 的 host 路由。手机进 `/m/`,PC 进桌面 UI:一枚令牌、一套配对流、两种界面。未配对的 PC 只看到不渲染底层数据的完整阻断页,其中提供电脑配对步骤与「重新检测」操作。
|
|
15
15
|
- **安全**:一个有效的一次性令牌(刷新会使旧链接失效;已接受的令牌不可复用;令牌会过期)。停止会撤销每一台已配对设备与当前令牌——已配对设备在下一次请求时被切断。配对是本插件对自己远程通道的访问控制:手机走 `/m/api`,非 loopback origin 打开的桌面浏览器走 `/remote`。未配对调用方在 `requirePairingForLan` 开启(默认)时会在读取请求体之前就被 `/remote` 拒绝。loopback(127.0.0.1)继续直接使用 `/api`。默认的远程桌面路径不使用 `--trusted-host`:连接插件的 `/api` 围栏对公网和局域网主机保持关闭,已配对 PC 改走 `/remote`。`--trusted-host` 是 SDK 的另一种用法,会让该主机被信任访问 `/api` 本身——配对不门控 `/api`(任何插件都无法做到;围栏是 SDK 自己的接缝)。当 `/api` 无需配对即可到达时,下方姿态探测会报告该姿态。
|
|
16
|
-
-
|
|
16
|
+
- **已配对模型目录**:已认证的已配对设备可使用 `GET /api/pair/model-catalog`、`POST /api/pair/model-catalog/discover` 和 `POST /api/pair/model-catalog/upsert`,仅查看并采纳一个现有、活跃的 `llm-pi-ai` provider 的模型。该能力在内部固定 provider 的 settings 地址,不能创建 provider,也不能读取或修改凭据、通用 settings、端点、header 或任意配置。这三条精确路由在 connection 插件的 `/api` 前缀之前匹配,已配对的局域网或隧道客户端可直接调用,并套用与 `/api/pair` 兄弟端点相同的 Host/Origin 信任围栏:仅接受 loopback、广告的局域网字面量或配置的公网 base URL。经 `/remote` 桌面通道访问时,所有 `/api/pair/*` 路径仍保持仅 loopback。当 provider 的实时模型目录不可用或未知时,采纳会被拒绝。解析后的 `models` 列表缺失或为空时,provider 会继续继承已安装目录;只有加入未知的自定义模型时才会实体化完整目录,现有模型 override 会保留并转换到对应条目。格式错误或相互冲突的模型配置会被拒绝,不会被破坏性重写。停止或撤销设备会立即禁用它;通用 `settings.*`、`credentials.*` 和 `llm.discoverModels` RPC 方法仍然仅限 loopback。
|
|
17
|
+
- **远程桌面通道**:`requirePairingForLan` 开启(默认)时,经局域网 URL 或隧道打开的桌面 Web GUI 透明地改走 `/remote`——同一套 UI,同一枚配对 cookie 门控。改写补丁在解析期即生效:宿主把一段内联 classic script 注入到 `<head>` 开标签之后(`webserver/index-inject`),使 fetch/WebSocket/EventSource 与资源 `src` 改写在任何启动条目执行前就绪(连接插件最先打开事件流);浏览器半边随后接管已安装的 seat 而非重复打补丁。loopback 来源完全跳过该补丁。浏览器中 `/api`、`/sidebar`、`/git`、`/pet` 下的请求,以及已知的事件、终端与 SSH WebSocket,会在不转发远程 Origin 或配对 cookie 的前提下重新发给本机 Web Server;认证反代会为兄弟路由围栏提供自己生成的同源浏览器标记。SDK 的 loopback 专属特权方法(原生对话框、settings 与 credentials 面)对已配对远程桌面保持不可达;`/api/pair/*`(包括三条模型目录路由)、`/api/update/*`、`/api/plugin-manager/*`、`/api/dsh-desktop-launcher/*` 与 `/api/dsh-web-ui-settings/*` 控制端点保持仅 loopback。未配对的桌面浏览器看到持久的全页阻断界面而不是数据(阻断页按错误码 `unpaired` 触发,不是任意 403)。阻断页在任一门控调用成功后,或通道本身被拆除时退出——把 `requirePairingForLan` 关掉(或关闭插件)也会清掉通道短暂激活期间弹出的提示。
|
|
17
18
|
- **姿态探测**:插件用伪造 Host 头(公网 base 与每个 LAN base)探测 SDK 的 `/api` 围栏。403 是默认姿态(围栏关闭,远程访问走配对)。任何非 403 结果——`--trusted-host`,或 `--host 0.0.0.0` 下 SDK 的局域网自动信任——都会以 CRITICAL 日志与配对面板红色横幅呈现,因此 SDK 的 `/api` 信任姿态是被看见的,而不是被假设的。探测失败会丢掉进行中的目标缓存,相同 origin 会在下次触发时重试。
|
|
18
19
|
- **实时状态**:桌面面板经 SSE 流实时镜像配对状态(等待 → 已连接 → 已断开),并展示已授权设备名单。
|
|
19
20
|
- **远程更新**:侧边栏加载后,底部的下载触发按钮(手机图标左侧)会静默探测 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 状态而不更新。
|
|
@@ -71,7 +72,7 @@ dsh plugin --profile web add link:$(pwd)/packages/dsh-remote-web-ui
|
|
|
71
72
|
6. 桌面徽标实时翻到 已连接;手机离开时回落到离线/断开。
|
|
72
73
|
7. 刷新二维码 使旧链接失效并铸一枚新的。停止 撤销移动端访问:已配对设备下一次请求 403,包括其实时流。
|
|
73
74
|
|
|
74
|
-
该移动端界面完全自包含在本插件内:`/m/` 页面及其数据通道(`/m/api`)由插件自己的路由伺服,**无需任何 harness 源码改动**——手机的 RPC 调用走插件的 `/m/api` 代理(它委托给 host 的 ApiProxy 服务并自己分页 `session.list`),因此被隧道化的 Host 永远不必进入连接插件的信任围栏。手机受其已配对设备 cookie
|
|
75
|
+
该移动端界面完全自包含在本插件内:`/m/` 页面及其数据通道(`/m/api`)由插件自己的路由伺服,**无需任何 harness 源码改动**——手机的 RPC 调用走插件的 `/m/api` 代理(它委托给 host 的 ApiProxy 服务并自己分页 `session.list`),因此被隧道化的 Host 永远不必进入连接插件的信任围栏。手机受其已配对设备 cookie 与 `/m/api` 显式方法白名单门控(白名单只约束 `/m/api` 代理本身:该 cookie 同时通过全局 api/gate,对宿主 `/api` 面是全量控制凭证——settings/credentials/host-action 域仅因 SDK 把这些特权方法钉为仅 loopback 才不可达。配对即对设备的完全信任;`/m/api` 代理的模型读写限制于建议性的 `session.models` / `session.selectModel` 对;另外,精确的已配对模型目录路由可为一个现有、合格的 `llm-pi-ai` provider 采纳模型,但不能创建 provider,也不能访问凭据或通用 settings;Agent 预设访问限制于只读 `agentPreset.list`,创建限制于 `session.create`(工作区 id 加清单中的可选 id——手机绝不自命名工作目录),权限选择器只通过已放行的 `session.prompt` 发送模式无关的 `/permission` 命令);实时流在 `/m/api/events.mux` 上经 Server-Sent Events 送达。规范的 `/m/` 页面拥有同 scope 的 manifest 与 Service Worker;其缓存仅包含静态壳和离线页,绝不包含移动端 API 响应、会话数据或命令。
|
|
75
76
|
|
|
76
77
|
### 行为说明
|
|
77
78
|
|
|
@@ -168,6 +169,10 @@ pnpm run build
|
|
|
168
169
|
|
|
169
170
|
公网路径是经隧道的同一往返(见「通过互联网远程访问」):loopback mint → 手机或 PC 打开公网二维码 URL → accept → UI。只有 `publicBaseUrl`(插件配置)命名隧道主机;`--trusted-host` 不属于这条配对流。桌面面板仍在 `http://127.0.0.1` 打开。
|
|
170
171
|
|
|
172
|
+
## 安全模型
|
|
173
|
+
|
|
174
|
+
- 移动端普通接口与 mux 事件流都要求有效的已配对设备会话。会话缺失或已撤销时返回 HTTP 403,并使用带 `error.code: "unpaired"` 的 JSON 拒绝信封;浏览器 `EventSource` API 只暴露事件流失败,不暴露该响应体。
|
|
175
|
+
|
|
171
176
|
## 已知限制与待办
|
|
172
177
|
|
|
173
178
|
- **撤销是逐请求的**:已配对手机请求已在 停止 落地时在途,完成该请求;下一个 403。
|
package/lib/client.js
CHANGED
|
@@ -3781,6 +3781,38 @@ window.__ModuleLoader__.load({
|
|
|
3781
3781
|
page.replaceState(`${url.pathname}${url.search}${url.hash}`);
|
|
3782
3782
|
}
|
|
3783
3783
|
//#endregion
|
|
3784
|
+
//#region src/remote-channel-rules.ts
|
|
3785
|
+
/**
|
|
3786
|
+
* The remote-channel rewrite contract as pure data (issue #987): both the
|
|
3787
|
+
* browser patch (client/remote-channel.ts) and the parse-time boot patch
|
|
3788
|
+
* (remote-channel-boot.ts, inlined into index.html by the host) decide from
|
|
3789
|
+
* these tables, so the two can never drift apart.
|
|
3790
|
+
* @module @linxin666/dsh-remote-web-ui/remote-channel-rules
|
|
3791
|
+
*/
|
|
3792
|
+
/** The gated mirror prefix (must match src/remote-methods.ts). */
|
|
3793
|
+
const REMOTE_PREFIX = "/remote";
|
|
3794
|
+
/** The live rule set. */
|
|
3795
|
+
const REMOTE_CHANNEL_RULES = {
|
|
3796
|
+
remotePrefix: REMOTE_PREFIX,
|
|
3797
|
+
apiPrefix: "/api/",
|
|
3798
|
+
pairPrefix: "/api/pair/",
|
|
3799
|
+
updatePrefix: "/api/update/",
|
|
3800
|
+
desktopLauncherPrefix: "/api/dsh-desktop-launcher",
|
|
3801
|
+
settingsBridgePrefix: "/api/dsh-web-ui-settings",
|
|
3802
|
+
sidebarPrefix: "/sidebar/",
|
|
3803
|
+
gitPrefix: "/git/",
|
|
3804
|
+
petPrefix: "/pet/",
|
|
3805
|
+
wsPaths: [
|
|
3806
|
+
"/api/events.mux",
|
|
3807
|
+
"/api/events.host",
|
|
3808
|
+
"/sidebar/ws/terminal",
|
|
3809
|
+
"/sidebar/ws/agent-terminals",
|
|
3810
|
+
"/api/dsh-ssh/terminal"
|
|
3811
|
+
]
|
|
3812
|
+
};
|
|
3813
|
+
/** The window global the boot patch publishes its seat under. */
|
|
3814
|
+
const REMOTE_CHANNEL_BOOT_GLOBAL = "__DSH_REMOTE_CHANNEL_BOOT__";
|
|
3815
|
+
//#endregion
|
|
3784
3816
|
//#region src/client/remote-channel.ts
|
|
3785
3817
|
/**
|
|
3786
3818
|
* The remote desktop channel — browser half. On a non-loopback origin (LAN
|
|
@@ -3807,23 +3839,7 @@ window.__ModuleLoader__.load({
|
|
|
3807
3839
|
* Pure helpers are exported for unit tests; `installRemoteChannel` patches
|
|
3808
3840
|
* the given window and returns their restore.
|
|
3809
3841
|
*/
|
|
3810
|
-
|
|
3811
|
-
const REMOTE_PREFIX = "/remote";
|
|
3812
|
-
const API_PREFIX = "/api/";
|
|
3813
|
-
const PAIR_PREFIX = "/api/pair/";
|
|
3814
|
-
const UPDATE_PREFIX = "/api/update/";
|
|
3815
|
-
const DESKTOP_LAUNCHER_PREFIX = "/api/dsh-desktop-launcher";
|
|
3816
|
-
const SETTINGS_BRIDGE_PREFIX = "/api/dsh-web-ui-settings";
|
|
3817
|
-
const SIDEBAR_PREFIX = "/sidebar/";
|
|
3818
|
-
const GIT_PREFIX = "/git/";
|
|
3819
|
-
const PET_PREFIX = "/pet/";
|
|
3820
|
-
const WS_PATHS = /* @__PURE__ */ new Set([
|
|
3821
|
-
"/api/events.mux",
|
|
3822
|
-
"/api/events.host",
|
|
3823
|
-
"/sidebar/ws/terminal",
|
|
3824
|
-
"/sidebar/ws/agent-terminals",
|
|
3825
|
-
"/api/dsh-ssh/terminal"
|
|
3826
|
-
]);
|
|
3842
|
+
const RULES = REMOTE_CHANNEL_RULES;
|
|
3827
3843
|
/** Decide whether a remote desktop channel is required from local or host policy. */
|
|
3828
3844
|
function remoteChannelRequired(hostname, snapshot, hostPairingPolicy) {
|
|
3829
3845
|
if (isLoopbackHostname(hostname)) return false;
|
|
@@ -3847,14 +3863,14 @@ window.__ModuleLoader__.load({
|
|
|
3847
3863
|
* @param pathname - the request URL pathname.
|
|
3848
3864
|
*/
|
|
3849
3865
|
function shouldRewriteFetchPath(pathname) {
|
|
3850
|
-
if (pathname.startsWith(
|
|
3851
|
-
if (pathname.startsWith(
|
|
3852
|
-
if (pathname ===
|
|
3853
|
-
if (pathname ===
|
|
3854
|
-
if (pathname.startsWith(
|
|
3855
|
-
if (pathname.startsWith(
|
|
3856
|
-
if (pathname.startsWith(
|
|
3857
|
-
if (pathname.startsWith(
|
|
3866
|
+
if (pathname.startsWith(RULES.pairPrefix)) return false;
|
|
3867
|
+
if (pathname.startsWith(RULES.updatePrefix)) return false;
|
|
3868
|
+
if (pathname === RULES.desktopLauncherPrefix || pathname.startsWith(`${RULES.desktopLauncherPrefix}/`)) return false;
|
|
3869
|
+
if (pathname === RULES.settingsBridgePrefix || pathname.startsWith(`${RULES.settingsBridgePrefix}/`)) return false;
|
|
3870
|
+
if (pathname.startsWith(RULES.apiPrefix)) return true;
|
|
3871
|
+
if (pathname.startsWith(RULES.sidebarPrefix) || pathname === "/sidebar") return true;
|
|
3872
|
+
if (pathname.startsWith(RULES.gitPrefix) || pathname === "/git") return true;
|
|
3873
|
+
if (pathname.startsWith(RULES.petPrefix) || pathname === "/pet") return true;
|
|
3858
3874
|
return false;
|
|
3859
3875
|
}
|
|
3860
3876
|
/**
|
|
@@ -3862,7 +3878,7 @@ window.__ModuleLoader__.load({
|
|
|
3862
3878
|
* @param pathname - the WebSocket URL pathname.
|
|
3863
3879
|
*/
|
|
3864
3880
|
function shouldRewriteWsPath(pathname) {
|
|
3865
|
-
return
|
|
3881
|
+
return RULES.wsPaths.includes(pathname);
|
|
3866
3882
|
}
|
|
3867
3883
|
/** The gated twin of one fenced path (`/remote` + original pathname). */
|
|
3868
3884
|
function rewritePath(pathname) {
|
|
@@ -4098,10 +4114,16 @@ window.__ModuleLoader__.load({
|
|
|
4098
4114
|
* @param ctx - client root context.
|
|
4099
4115
|
*/
|
|
4100
4116
|
function apply(ctx) {
|
|
4101
|
-
ctx.effect(() =>
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4117
|
+
ctx.effect(() => {
|
|
4118
|
+
try {
|
|
4119
|
+
return ctx.locale.register(NS, {
|
|
4120
|
+
zh,
|
|
4121
|
+
en
|
|
4122
|
+
});
|
|
4123
|
+
} catch {
|
|
4124
|
+
return () => {};
|
|
4125
|
+
}
|
|
4126
|
+
}, "remote-web-ui: dictionaries");
|
|
4105
4127
|
const t = ctx.locale.bind(NS);
|
|
4106
4128
|
const settingsScope = (ctx.get("webUiSettings") ?? ctx.settingsScope).bind({ namespace: REMOTE_WEB_UI_NS });
|
|
4107
4129
|
const enabled = () => {
|
|
@@ -4111,10 +4133,12 @@ window.__ModuleLoader__.load({
|
|
|
4111
4133
|
ctx.slots.inject("sidebar.remote", () => {
|
|
4112
4134
|
let disposeEntry;
|
|
4113
4135
|
const syncEntry = () => {
|
|
4114
|
-
if (enabled() && disposeEntry === void 0)
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4136
|
+
if (enabled() && disposeEntry === void 0) try {
|
|
4137
|
+
disposeEntry = ctx.slots.register({
|
|
4138
|
+
name: "sidebar.remote",
|
|
4139
|
+
locale: NS
|
|
4140
|
+
}, RemoteEntry);
|
|
4141
|
+
} catch {}
|
|
4118
4142
|
else if (!enabled() && disposeEntry !== void 0) {
|
|
4119
4143
|
disposeEntry();
|
|
4120
4144
|
disposeEntry = void 0;
|
|
@@ -4130,11 +4154,13 @@ window.__ModuleLoader__.load({
|
|
|
4130
4154
|
ctx.slots.inject("sidebar.footer.action", () => {
|
|
4131
4155
|
let disposeEntry;
|
|
4132
4156
|
const syncEntry = () => {
|
|
4133
|
-
if (enabled() && disposeEntry === void 0)
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4157
|
+
if (enabled() && disposeEntry === void 0) try {
|
|
4158
|
+
disposeEntry = ctx.slots.register({
|
|
4159
|
+
name: "sidebar.footer.action",
|
|
4160
|
+
id: "remote-web-ui",
|
|
4161
|
+
locale: NS
|
|
4162
|
+
}, FooterRemoteEntry);
|
|
4163
|
+
} catch {}
|
|
4138
4164
|
else if (!enabled() && disposeEntry !== void 0) {
|
|
4139
4165
|
disposeEntry();
|
|
4140
4166
|
disposeEntry = void 0;
|
|
@@ -4149,17 +4175,21 @@ window.__ModuleLoader__.load({
|
|
|
4149
4175
|
});
|
|
4150
4176
|
const remoteSettings = new RemoteSettingsCardController(settingsScope);
|
|
4151
4177
|
ctx.slots.inject("web-ui.plugin.item", () => {
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4178
|
+
try {
|
|
4179
|
+
const unregister = ctx.slots.register({
|
|
4180
|
+
name: "web-ui.plugin.item",
|
|
4181
|
+
id: "remote-web-ui",
|
|
4182
|
+
order: 90,
|
|
4183
|
+
locale: NS,
|
|
4184
|
+
inject: () => remoteSettings.inject()
|
|
4185
|
+
}, RemoteSettingsCard);
|
|
4186
|
+
return () => {
|
|
4187
|
+
remoteSettings.dispose();
|
|
4188
|
+
unregister();
|
|
4189
|
+
};
|
|
4190
|
+
} catch {
|
|
4191
|
+
return () => {};
|
|
4192
|
+
}
|
|
4163
4193
|
});
|
|
4164
4194
|
let disposeRuntime;
|
|
4165
4195
|
const syncRuntime = () => {
|
|
@@ -4216,19 +4246,34 @@ window.__ModuleLoader__.load({
|
|
|
4216
4246
|
showFenceNotice();
|
|
4217
4247
|
};
|
|
4218
4248
|
const channelActive = () => remoteChannelRequired(window.location.hostname, settingsScope.getSnapshot(), hostPairingPolicy);
|
|
4249
|
+
const bootSeat = () => window[REMOTE_CHANNEL_BOOT_GLOBAL];
|
|
4219
4250
|
const syncChannel = () => {
|
|
4220
4251
|
const transition = channelTransition(channelActive(), disposeChannel !== void 0);
|
|
4221
|
-
if (transition === "install")
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4252
|
+
if (transition === "install") {
|
|
4253
|
+
const seat = bootSeat();
|
|
4254
|
+
if (seat !== void 0) {
|
|
4255
|
+
seat.onUnpaired = handleUnpaired;
|
|
4256
|
+
seat.onPaired = hideFenceNotice;
|
|
4257
|
+
if (seat.pendingUnpaired) {
|
|
4258
|
+
seat.pendingUnpaired = false;
|
|
4259
|
+
handleUnpaired();
|
|
4260
|
+
}
|
|
4261
|
+
disposeChannel = ctx.effect(() => () => {
|
|
4262
|
+
seat.onUnpaired = null;
|
|
4263
|
+
seat.onPaired = null;
|
|
4264
|
+
}, "remote-web-ui: remote desktop channel (boot patch)");
|
|
4265
|
+
} else disposeChannel = ctx.effect(() => {
|
|
4266
|
+
return installRemoteChannel(window, {
|
|
4267
|
+
onUnpaired: handleUnpaired,
|
|
4268
|
+
onPaired: hideFenceNotice
|
|
4269
|
+
});
|
|
4270
|
+
}, "remote-web-ui: remote desktop channel");
|
|
4271
|
+
} else if (transition === "retire" && disposeChannel !== void 0) {
|
|
4228
4272
|
disposeChannel();
|
|
4229
4273
|
disposeChannel = void 0;
|
|
4274
|
+
bootSeat()?.restore();
|
|
4230
4275
|
hideFenceNotice();
|
|
4231
|
-
}
|
|
4276
|
+
} else if (transition === "none" && !channelActive()) bootSeat()?.restore();
|
|
4232
4277
|
};
|
|
4233
4278
|
settingsScope.subscribe(syncChannel);
|
|
4234
4279
|
syncChannel();
|