dsh-remote-bridge 2.3.6 → 2.4.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.en.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <img alt="Version" src="https://img.shields.io/badge/version-v2.3.6-087f8c">
12
+ <img alt="Version" src="https://img.shields.io/badge/version-v2.4.0-087f8c">
13
13
  <img alt="DSH plugin" src="https://img.shields.io/badge/DeepSeek_Harness-plugin-1f2328">
14
14
  <img alt="Platforms" src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-586069">
15
15
  <img alt="License" src="https://img.shields.io/badge/license-AGPL--3.0-2da44e">
@@ -34,7 +34,11 @@ The desktop entry lives **inside your DSH settings**: look for the section title
34
34
 
35
35
  Remote access remains disabled until a local administrator chooses a bind address, port, and initial device permissions.
36
36
 
37
- > This section used to carry two UI screenshots, but both showed the pre-rename interface (titled "DSH 文件工作区"; the remote-access one was still labelled v1.0.0 and showed the file tree and settings as two stacked dialogs). They no longer match the shape described above, so they were removed rather than left to mislead. Re-recording needs a browser environment that can drive the plugin panel, which is not available here.
37
+ ![Phone settings — the Roots card, open by default](assets/screenshots/panel-roots.png)
38
+
39
+ > **About these screenshots.** The four images under `assets/screenshots/` are generated by `scripts/generate-screenshots.ts`, which renders the panel from the same sources the tests use — `ADMIN_TABS`, `WORKSPACE_CSS` and `APP_QR_CODES` out of `src/client/` — so what you see is the shipped markup and stylesheet, not a redrawn mock-up. They show a machine that has just installed the plugin and not yet enabled remote access, which is the honest default state. An earlier revision of this README carried two UI screenshots; both predated the rename and showed the file tree and settings as two stacked dialogs, so they were removed rather than left to mislead.
40
+ >
41
+ > Storefronts pick these up automatically from [`screenshots.json`](./screenshots.json) — no pull request, no waiting on a maintainer. Update the images and push; the next build takes them.
38
42
 
39
43
  ## About the rename (2.0.4)
40
44
 
@@ -73,10 +77,10 @@ dsh plugin --profile web add dsh-remote-bridge
73
77
  dsh plugin --profile web add dsh-remote-bridge --registry https://registry.npmmirror.com
74
78
 
75
79
  # 2. tarball from the GitHub release
76
- dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.3.6/dsh-remote-bridge-v2.3.6.tgz
80
+ dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.4.0/dsh-remote-bridge-v2.4.0.tgz
77
81
 
78
82
  # 3. tarball from the Gitee release (faster from mainland China)
79
- dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.3.6/dsh-remote-bridge-v2.3.6.tgz
83
+ dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.4.0/dsh-remote-bridge-v2.4.0.tgz
80
84
 
81
85
  npx @deepseek-ai/dsh web
82
86
  ```
@@ -93,7 +97,7 @@ Restart DSH after installation, then open settings — The section is titled **
93
97
  To install a downloaded release archive:
94
98
 
95
99
  ```sh
96
- dsh plugin --profile web add ./dsh-remote-bridge-v2.3.6.tgz
100
+ dsh plugin --profile web add ./dsh-remote-bridge-v2.4.0.tgz
97
101
  ```
98
102
 
99
103
  ## First connection
@@ -327,11 +331,11 @@ pnpm pack
327
331
 
328
332
  `npm pack` produces roughly a **0.29 MB** archive: 2.2.0 dropped the standalone page and its editor dependencies and turned source maps off, down from 1.72 MB.
329
333
 
330
- v2.3.6 was developed against DSH `master@47f943859bef60e4160492346772ded9b24f765a`. CI covers Windows, Ubuntu, and macOS.
334
+ v2.4.0 was developed against DSH `master@47f943859bef60e4160492346772ded9b24f765a`. CI covers Windows, Ubuntu, and macOS.
331
335
 
332
336
  ## Project
333
337
 
334
- - Version: `v2.3.6`
338
+ - Version: `v2.4.0`
335
339
  - Author: upstream [Hakunm](https://github.com/Hakunm); this fork maintained by [westanke](https://github.com/westanke)
336
340
  - Repository: [GitHub](https://github.com/westanke/dsh-remote-bridge) · [Gitee](https://gitee.com/westanke/dsh-remote-bridge)
337
341
  - npm: [`dsh-remote-bridge`](https://www.npmjs.com/package/dsh-remote-bridge) ([mainland-China mirror](https://registry.npmmirror.com/dsh-remote-bridge))
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <img alt="Version" src="https://img.shields.io/badge/version-v2.3.6-087f8c">
12
+ <img alt="Version" src="https://img.shields.io/badge/version-v2.4.0-087f8c">
13
13
  <img alt="DSH plugin" src="https://img.shields.io/badge/DeepSeek_Harness-plugin-1f2328">
14
14
  <img alt="Platforms" src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-586069">
15
15
  <img alt="License" src="https://img.shields.io/badge/license-AGPL--3.0-2da44e">
@@ -32,6 +32,12 @@
32
32
 
33
33
  远程访问默认关闭。管理员可在本机 WebUI 中设置绑定 IP、端口和新设备的初始权限。
34
34
 
35
+ ![手机设置 —— 默认展开的「根目录」卡片](assets/screenshots/panel-roots.png)
36
+
37
+ > **关于这几张截图。** `assets/screenshots/` 下的四张图由 `scripts/generate-screenshots.ts` 生成,它用与测试同一份产出来源渲染面板 —— `ADMIN_TABS`、`WORKSPACE_CSS`、`APP_QR_CODES` 全部取自 `src/client/`,所以你看到的是**发布包里的标记与样式表**,不是另画的示意图。图中呈现的是一台刚装好插件、尚未启用远程访问的机器,这是最诚实的默认状态。本 README 早前曾放过两张界面截图,但都摄于改名之前(标题还是「DSH 文件工作区」,远程访问那张还标着 v1.0.0,且把文件树与设置显示成上下两个弹窗),与上文描述的形态已不符,于是撤掉而不是留着误导。
38
+ >
39
+ > 市场会从 [`screenshots.json`](./screenshots.json) 自动取用这些图 —— 不必再来提 PR、不必等维护者。换了图直接推自己的仓库即可,下一次构建自动生效。
40
+
35
41
 
36
42
  ## 关于改名(2.0.4)
37
43
 
@@ -70,10 +76,10 @@ dsh plugin --profile web add dsh-remote-bridge
70
76
  dsh plugin --profile web add dsh-remote-bridge --registry https://registry.npmmirror.com
71
77
 
72
78
  # ② GitHub Release 的 tarball
73
- dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.3.6/dsh-remote-bridge-v2.3.6.tgz
79
+ dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.4.0/dsh-remote-bridge-v2.4.0.tgz
74
80
 
75
81
  # ③ Gitee Release 的 tarball(国内访问更快)
76
- dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.3.6/dsh-remote-bridge-v2.3.6.tgz
82
+ dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.4.0/dsh-remote-bridge-v2.4.0.tgz
77
83
 
78
84
  npx @deepseek-ai/dsh web
79
85
  ```
@@ -89,7 +95,7 @@ npx @deepseek-ai/dsh web
89
95
  从 GitHub Release 下载离线安装包时,可直接安装 tarball:
90
96
 
91
97
  ```sh
92
- dsh plugin --profile web add ./dsh-remote-bridge-v2.3.6.tgz
98
+ dsh plugin --profile web add ./dsh-remote-bridge-v2.4.0.tgz
93
99
  ```
94
100
 
95
101
  ## 第一次使用
@@ -319,11 +325,11 @@ pnpm pack
319
325
 
320
326
  `npm pack` 的产物约 **0.29 MB**:2.2.0 删除独立页及其编辑器依赖、并关闭 source map 之后,从 1.72 MB 降下来。
321
327
 
322
- v2.3.6 基于 DSH `master@47f943859bef60e4160492346772ded9b24f765a` 开发,CI 覆盖 Windows、Ubuntu 和 macOS。
328
+ v2.4.0 基于 DSH `master@47f943859bef60e4160492346772ded9b24f765a` 开发,CI 覆盖 Windows、Ubuntu 和 macOS。
323
329
 
324
330
  ## 项目信息
325
331
 
326
- - 当前版本:`v2.3.6`
332
+ - 当前版本:`v2.4.0`
327
333
  - 作者:上游 [Hakunm](https://github.com/Hakunm),本 fork 维护 [westanke](https://github.com/westanke)
328
334
  - 仓库:[GitHub](https://github.com/westanke/dsh-remote-bridge) · [Gitee](https://gitee.com/westanke/dsh-remote-bridge)
329
335
  - npm:[`dsh-remote-bridge`](https://www.npmjs.com/package/dsh-remote-bridge)([国内镜像](https://registry.npmmirror.com/dsh-remote-bridge))
@@ -1,7 +1,7 @@
1
1
  asyncapi: 3.0.0
2
2
  info:
3
3
  title: DeepSeek Harness Mobile Events
4
- version: 2.3.6
4
+ version: 2.4.0
5
5
  description: |
6
6
  Live file and authorized DSH chat events. Reconnect clients must refresh
7
7
  REST state; unbounded replay is not provided.
@@ -39,7 +39,7 @@ components:
39
39
  id: { type: string }
40
40
  type:
41
41
  type: string
42
- examples: [file.changed, file.trashed, chat.message.delta, chat.message.committed, chat.session.status, chat.approval.requested, chat.approval.resolved, chat.turn.failed, connection.ready]
42
+ examples: [file.changed, file.trashed, chat.message.delta, chat.message.committed, chat.session.status, chat.session.snapshot, chat.deliverables.presented, chat.approval.requested, chat.approval.resolved, chat.turn.failed, connection.ready]
43
43
  time: { type: integer }
44
44
  data:
45
45
  oneOf:
@@ -48,17 +48,31 @@ components:
48
48
  - $ref: '#/components/schemas/ChatApprovalRequested'
49
49
  - $ref: '#/components/schemas/ChatApprovalResolved'
50
50
  - $ref: '#/components/schemas/ChatTurnFailed'
51
+ - $ref: '#/components/schemas/ConnectionReadyData'
52
+ - $ref: '#/components/schemas/ChatSessionSnapshot'
53
+ - $ref: '#/components/schemas/ChatDeliverablesPresented'
51
54
  - type: object
52
55
  additionalProperties: true
53
56
  schemas:
54
57
  ChatMessageDelta:
55
58
  type: object
56
- description: A stable token-level assistant update normalized from DSH assistant/chunk events.
59
+ description: |
60
+ A stable token-level assistant update normalized from DSH assistant/chunk events.
61
+ Streaming deltas have **no durable position** -- `index` is the chunk index within the
62
+ current stream (it restarts at 0 every turn), so this message carries no `seq`. See
63
+ ADR-0008: `seq` means one thing only, the durable session event sequence.
57
64
  required: [sessionId, eventType, turn, step, index, kind, text]
58
65
  properties:
59
66
  sessionId: { type: string }
60
67
  eventType: { type: string, const: assistant/chunk }
61
- seq: { type: integer }
68
+ seq:
69
+ type: integer
70
+ deprecated: true
71
+ description: |
72
+ Deprecated and **no longer emitted** on this message (ADR-0008). It previously
73
+ carried the chunk index, which made `seq` mean two different things depending on
74
+ which path produced the frame. Kept in the schema rather than removed so existing
75
+ third-party implementations are not judged invalid; it was optional either way.
62
76
  turn: { type: integer }
63
77
  step: { type: integer }
64
78
  index: { type: integer }
@@ -77,13 +91,22 @@ components:
77
91
  step: { type: integer }
78
92
  ChatApprovalRequested:
79
93
  type: object
80
- description: Signals that clients should refresh the session pending-approval REST resource.
94
+ description: |
95
+ Signals that clients should refresh the session pending-approval REST resource.
96
+ On WebSocket connect the plugin also replays every approval that is still pending to
97
+ **that connection only** (it is not broadcast). Re-querying REST after connecting is a
98
+ client's own choice, so the protocol does not rely on it: this makes "a fresh connection
99
+ learns everything still pending" a server-side guarantee rather than a client
100
+ obligation. Clients that already refresh on connect just see one idempotent extra
101
+ refresh. Those frames carry `replayed: true`; deduplicate by `approvalId` or ignore the
102
+ flag entirely.
81
103
  required: [sessionId, approvalId, toolName, risk]
82
104
  properties:
83
105
  sessionId: { type: string }
84
106
  approvalId: { type: string }
85
107
  toolName: { type: string }
86
108
  risk: { type: string, enum: [standard, full-access] }
109
+ replayed: { type: boolean }
87
110
  ChatApprovalResolved:
88
111
  type: object
89
112
  description: Signals that an approval was settled and clients should refresh session state.
@@ -119,3 +142,61 @@ components:
119
142
  message: { type: string }
120
143
  code: { type: string }
121
144
  stopped: { type: boolean, const: true }
145
+
146
+ ChatDeliverablesPresented:
147
+ type: object
148
+ description: |
149
+ A file the agent explicitly **presented** (`chat.deliverables.presented`, produced by the
150
+ kernel's `present` tool). The durable event `deliverables/presented` has always been
151
+ forwarded verbatim inside history; this frame is what makes it visible **live**, so a
152
+ client does not have to wait for the next message to discover a new deliverable.
153
+ `files` is forwarded as-is (clients read `path` and `description`), so fields the kernel
154
+ adds later need no change here.
155
+ required: [sessionId, files]
156
+ properties:
157
+ sessionId: { type: string }
158
+ eventType: { type: string, const: deliverables/presented }
159
+ seq: { type: integer }
160
+ files:
161
+ type: array
162
+ items:
163
+ type: object
164
+ properties:
165
+ path: { type: string }
166
+ description: { type: string }
167
+
168
+ ChatSessionSnapshot:
169
+ type: object
170
+ description: |
171
+ The **subscription baseline** (`chat.session.snapshot`): the durable event sequence the
172
+ server starts streaming from. Sent once when a session's live relay attaches (the
173
+ upstream relay's first frame is always a snapshot). `cursor` is the durable `seq` at that cut, so a client can tell
174
+ whether its own history is behind (`max(local seq) < cursor` = events were missed
175
+ between the REST read and the subscription).
176
+ The baseline's **records are deliberately not shipped**: this bridge is REST + WebSocket
177
+ dual-channel, and pushing history bodies here would create two baselines whose relative
178
+ order cannot be guaranteed. History bodies stay on REST as the single source.
179
+ required: [sessionId]
180
+ properties:
181
+ sessionId: { type: string }
182
+ cursor: { type: integer }
183
+ hasMore: { type: boolean }
184
+ recordCount: { type: integer }
185
+
186
+ ConnectionReadyData:
187
+ type: object
188
+ description: |
189
+ Payload of the very first frame on every accepted WebSocket
190
+ (`{"type": "connection.ready"}`). Clients should use `capabilities` for
191
+ feature detection instead of comparing `bridgeVersion` against a
192
+ hard-coded minimum: a capability says what this server can do right now,
193
+ while a version number is only its historical label. Capability names are
194
+ `<feature>-v<major>` and are never silently removed. Send
195
+ `Authorization: Bearer <token>` (or the `bearer.<token>` subprotocol) as
196
+ usual; this frame carries no credentials.
197
+ required: [bridgeVersion, capabilities]
198
+ properties:
199
+ bridgeVersion: { type: string }
200
+ capabilities:
201
+ type: array
202
+ items: { type: string }
@@ -1,7 +1,7 @@
1
1
  openapi: 3.1.0
2
2
  info:
3
3
  title: DeepSeek Harness Mobile API
4
- version: 2.3.6
4
+ version: 2.4.0
5
5
  description: Versioned file and chat BFF for mobile and third-party DSH clients. HTTP and HTTPS are both supported.
6
6
  servers:
7
7
  - url: http://127.0.0.1:3090/api/v1
@@ -24,7 +24,7 @@ paths:
24
24
  properties:
25
25
  ok: { type: boolean, const: true }
26
26
  version: { type: string, const: v1 }
27
- pluginVersion: { type: string, const: 2.3.6 }
27
+ pluginVersion: { type: string, const: 2.4.0 }
28
28
  /pairings/exchange:
29
29
  post:
30
30
  security: []
@@ -457,6 +457,17 @@ paths:
457
457
  /chat/sessions:
458
458
  get:
459
459
  operationId: listChatSessions
460
+ parameters:
461
+ - name: archived
462
+ in: query
463
+ schema: { type: boolean, default: false }
464
+ description: |
465
+ When true, return the **archived** sessions instead of the active ones.
466
+ Archiving hides a session from every grouped surface (kernel semantics), so
467
+ without this parameter a mobile client that archives a session could never see it
468
+ again. Un-archiving is NOT part of this bridge: the adapter only wraps the kernel's
469
+ `archiveSession`, and whether the kernel exposes an unarchive call is not verifiable
470
+ from this repository.
460
471
  responses:
461
472
  '200':
462
473
  description: Sessions whose cwd is within a granted root.
@@ -566,7 +577,29 @@ paths:
566
577
  in: query
567
578
  schema: { type: integer, minimum: 1 }
568
579
  responses:
569
- '200': { description: DSH history events and continuation state. }
580
+ '200':
581
+ description: |
582
+ DSH history events and continuation state. Subject to a byte budget
583
+ (2 MiB per response, 512 KiB per event): the oldest events may be dropped
584
+ (with `hasMore` forced to true) and oversized strings inside one event may be
585
+ head/tail clamped (that element then carries `bridgeTruncated`). `truncated`
586
+ and `bytes` appear only when something was actually clamped -- otherwise the
587
+ payload is byte-for-byte what it was before the budget existed.
588
+ `historyFormatVersion` is always present and describes the shape of `events`.
589
+ content:
590
+ application/json:
591
+ schema:
592
+ type: object
593
+ required: [events, hasMore]
594
+ properties:
595
+ events:
596
+ type: array
597
+ items: { type: object, additionalProperties: true }
598
+ hasMore: { type: boolean }
599
+ projections: { type: object, additionalProperties: true }
600
+ bytes: { type: integer, minimum: 0 }
601
+ truncated: { type: boolean }
602
+ historyFormatVersion: { type: integer, minimum: 1 }
570
603
  '404': { $ref: '#/components/responses/Error' }
571
604
  post:
572
605
  operationId: sendMessage
@@ -4,7 +4,7 @@ plugins {
4
4
  }
5
5
 
6
6
  group = "ai.deepseek.dsh"
7
- version = "2.3.6"
7
+ version = "2.4.0"
8
8
 
9
9
  repositories { mavenCentral() }
10
10
 
package/lib/client.cjs CHANGED
@@ -164,6 +164,22 @@ window.__ModuleLoader__.load({
164
164
  async audit() {
165
165
  return this.manageJson("/audit?limit=200");
166
166
  }
167
+ /** 当前 Cloudflare 隧道状态(未启动时 running:false)。 */
168
+ async tunnelStatus() {
169
+ return this.manageJson("/cloudflare/status");
170
+ }
171
+ /** 创建云隧道进行中任务的阶段进度(无任务时 stage 为 idle)。 */
172
+ async tunnelActivity() {
173
+ return this.manageJson("/cloudflare/activity");
174
+ }
175
+ /** 创建(或复用)cloudflared 并启动 Quick Tunnel,返回对外地址。 */
176
+ async tunnelCreate() {
177
+ return this.manageJson("/cloudflare/create", { method: "POST" });
178
+ }
179
+ /** 停止当前隧道。 */
180
+ async tunnelStop() {
181
+ return this.manageJson("/cloudflare/stop", { method: "POST" });
182
+ }
167
183
  /** 识图路径:当前值 + 出厂默认(只读)+ 兜底目录(只读)。 */
168
184
  async imageSettings() {
169
185
  return this.manageJson("/image");
@@ -30159,6 +30175,17 @@ window.__ModuleLoader__.load({
30159
30175
  enablePairing: "启用并创建配对",
30160
30176
  newPairingCode: "新建配对码",
30161
30177
  newConfigText: "生成配置文本",
30178
+ tunnelTitle: "Cloudflare 隧道(公网)",
30179
+ tunnelHint: "在外网想连回来时用:一键创建快速隧道,把本机端口经 Cloudflare 边缘暴露成临时公网地址。地址是临时、会变的,重建即换新。",
30180
+ tunnelRunning: "隧道运行中",
30181
+ tunnelStopped: "隧道已停止",
30182
+ tunnelCreate: "开启云隧道",
30183
+ tunnelStop: "停止云隧道",
30184
+ tunnelEndpointLabel: "云隧道",
30185
+ tunnelGenConfigReady: "生成隧道配置文件",
30186
+ tunnelGenConfigOff: "隧道未开启",
30187
+ tunnelSecurityWarning: "开启后将本机远程端口经 Cloudflare 边缘暴露到公网,任何人拿到下面的临时地址即可尝试连接。请仅在需要外网连回来时开启,用完及时停止隧道。",
30188
+ tunnelStartConfirm: "开启云隧道会把你设置页里的远程端口经 Cloudflare 暴露到公网,强密码(token)才能挡住乱连。确认开启吗?",
30162
30189
  configTextSecurity: "这段文本内含设备令牌,等价于一把钥匙:只发给自己,不要发到群聊、公开仓库或截图里。",
30163
30190
  configTextHint: "把它发给自己(微信、邮件都可以),到外面在手机 APP 里选择「导入配置文本」粘贴即可,全程不需要电脑在旁。",
30164
30191
  configTextManual: "手工指定地址(可选)",
@@ -30320,6 +30347,17 @@ window.__ModuleLoader__.load({
30320
30347
  enablePairing: "Enable and create pairing",
30321
30348
  newPairingCode: "New pairing code",
30322
30349
  newConfigText: "Generate config text",
30350
+ tunnelTitle: "Cloudflare tunnel (public)",
30351
+ tunnelHint: "To reach this machine from public networks: create a quick tunnel that exposes the local port through Cloudflare edge as a temporary public address. The address is temporary and changes on recreation.",
30352
+ tunnelRunning: "Tunnel running",
30353
+ tunnelStopped: "Tunnel stopped",
30354
+ tunnelCreate: "Start tunnel",
30355
+ tunnelStop: "Stop tunnel",
30356
+ tunnelEndpointLabel: "Cloudflare tunnel",
30357
+ tunnelGenConfigReady: "Generate config file from tunnel",
30358
+ tunnelGenConfigOff: "Tunnel not started",
30359
+ tunnelSecurityWarning: "Starting exposes your remote port to the public internet through Cloudflare edge; anyone with the temporary address below can try to connect. Only start it when you need to reach the machine from outside, and stop the tunnel when done.",
30360
+ tunnelStartConfirm: "Starting the tunnel exposes your remote port through Cloudflare to the public internet; only the device token (a strong password) keeps strangers out. Start anyway?",
30323
30361
  configTextSecurity: "This text embeds a device token — treat it like a key: send it only to yourself, never to a group chat, a public repo, or a screenshot.",
30324
30362
  configTextHint: "Send it to yourself (WeChat, email, anything), then paste it into the phone app under \"Import config text\". The computer does not need to be nearby.",
30325
30363
  configTextManual: "Manual address (optional)",
@@ -30483,7 +30521,7 @@ window.__ModuleLoader__.load({
30483
30521
  .daw-card-body{border-top:1px solid var(--dsw-alias-border-l2,var(--daw-line));margin:0 16px;padding-bottom:8px}.daw-settings-section{display:grid;grid-template-rows:auto minmax(0,1fr);gap:10px;min-width:0;min-height:0}.daw-settings-section>.daw-list-meta{margin:0}.daw-settings-section .daw-admin{padding:14px 2px}.daw-qr-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-top:12px}.daw-qr-card{margin:0;display:grid;justify-items:center;gap:8px;padding:16px;border:1px solid var(--daw-line);border-radius:6px;background:var(--daw-panel)}.daw-qr-image{width:180px;height:180px;background:var(--dsw-alias-bg-layer-3,#fff);border-radius:4px}.daw-qr-site{font-weight:600}.daw-qr-url{font:12px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--daw-accent);text-decoration:none;overflow-wrap:anywhere;text-align:center}.daw-qr-url:hover{text-decoration:underline}.daw-form-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(120px,.45fr) auto;gap:8px;margin-bottom:14px}.daw-listener-form{display:grid;grid-template-columns:minmax(180px,1fr) 130px auto;gap:8px;align-items:end}.daw-field{display:grid;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:12px}.daw-listener-status{display:flex;flex-wrap:wrap;gap:5px 18px;padding:9px 0;color:var(--daw-muted);font:12px ui-monospace,SFMono-Regular,Consolas,monospace}.daw-listener-hint{margin:0 0 8px}.daw-list{border-top:1px solid var(--daw-line)}.daw-list-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:center;min-height:48px;border-bottom:1px solid var(--daw-line);padding:7px 2px}.daw-list-title{font-weight:600;overflow-wrap:anywhere}
30484
30522
  .daw-list-title .daw-pill{margin-left:8px;vertical-align:middle}
30485
30523
  .daw-builtin-hint{text-align:right;align-self:center}
30486
- .daw-builtin-notice{margin-top:10px;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}.daw-list-meta{font-size:12px;color:var(--daw-muted);overflow-wrap:anywhere}.daw-scope-grid{display:flex;flex-wrap:wrap;gap:6px 12px;padding:8px 0}.daw-check{display:inline-flex;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted))}.daw-code{font:600 22px ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:2px!important;padding:14px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px;text-align:center;margin:12px 0}.daw-warning{flex:none;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--daw-muted);margin-bottom:0}.daw-pill{display:inline-flex;align-items:center;gap:5px;border-radius:999px;padding:1px 8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:11px;font-weight:500;line-height:17px}.daw-pill.on{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-state-success-primary,var(--daw-good))}.daw-inline-command{margin-left:8px}@mediaa(max-width:720px){.daw-form-row,.daw-listener-form{grid-template-columns:1fr}.daw-admin{padding:12px}.daw-qr-grid{grid-template-columns:1fr}.daw-qr-image{width:150px;height:150px}.daw-inline-command{margin:8px 0 0}}
30524
+ .daw-builtin-notice{margin-top:10px;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft))}.daw-list-meta{font-size:12px;color:var(--daw-muted);overflow-wrap:anywhere}.daw-scope-grid{display:flex;flex-wrap:wrap;gap:6px 12px;padding:8px 0}.daw-check{display:inline-flex;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary,var(--daw-muted))}.daw-code{font:600 22px ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:2px!important;padding:14px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px;text-align:center;margin:12px 0}.daw-warning{flex:none;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));border-radius:8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));padding:9px 11px;color:var(--daw-muted);margin-bottom:0}.daw-pill{display:inline-flex;align-items:center;gap:5px;border-radius:999px;padding:1px 8px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-label-secondary,var(--daw-muted));font-size:11px;font-weight:500;line-height:17px}.daw-pill.on{background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));color:var(--dsw-alias-state-success-primary,var(--daw-good))}.daw-tunnel{margin-top:14px;border-top:1px dashed var(--dsw-alias-border-l2,var(--daw-line));padding-top:12px}.daw-tunnel-head{display:flex;align-items:center;gap:8px;justify-content:space-between}.daw-tunnel-url{font:500 13px ui-monospace,SFMono-Regular,Consolas,monospace;overflow-wrap:anywhere;padding:9px 11px;border:1px solid var(--dsw-alias-border-l2,var(--daw-line));background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));border-radius:8px}.daw-tunnel-progress{max-width:360px}.daw-progress-track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-module-platform,var(--daw-accent-soft));overflow:hidden}.daw-progress-fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-info-primary,var(--daw-accent,var(--daw-line)));transition:width .25s ease}.daw-inline-row{display:flex;flex-wrap:wrap;gap:8px}.daw-inline-command{margin-left:8px}@mediaa(max-width:720px){.daw-form-row,.daw-listener-form{grid-template-columns:1fr}.daw-admin{padding:12px}.daw-qr-grid{grid-template-columns:1fr}.daw-qr-image{width:150px;height:150px}.daw-inline-command{margin:8px 0 0}}
30487
30525
  `;
30488
30526
  function installWorkspaceStyles() {
30489
30527
  if (document.getElementById(STYLE_ID) !== null) return;
@@ -30533,6 +30571,10 @@ window.__ModuleLoader__.load({
30533
30571
  function tabLabelKey(tab) {
30534
30572
  return tab === "app-download" ? "appDownloadTab" : `${tab}Tab`;
30535
30573
  }
30574
+ /** 云隧道进度条宽度:有确定百分比则用之,否则给 30% 作「流动中」占位。 */
30575
+ function pct(a) {
30576
+ return `${a.percent ?? 30}%`;
30577
+ }
30536
30578
  /**
30537
30579
  * 卡片头部的描述行。与 `tabLabelKey` 同理:唯一含连字符的分区显式映射。
30538
30580
  *
@@ -30671,6 +30713,9 @@ window.__ModuleLoader__.load({
30671
30713
  const [openCards, setOpenCards] = (0, react.useState)(() => /* @__PURE__ */ new Set([props.initialOpen]));
30672
30714
  const [imageSaved, setImageSaved] = (0, react.useState)(false);
30673
30715
  const [error, setError] = (0, react.useState)();
30716
+ const [tunnel, setTunnel] = (0, react.useState)();
30717
+ const [tunnelBusy, setTunnelBusy] = (0, react.useState)(false);
30718
+ const [activity, setActivity] = (0, react.useState)();
30674
30719
  const isOpen = (0, react.useCallback)((card) => openCards.has(card), [openCards]);
30675
30720
  const toggleCard = (0, react.useCallback)((card) => {
30676
30721
  setOpenCards((previous) => {
@@ -30691,11 +30736,49 @@ window.__ModuleLoader__.load({
30691
30736
  setImageDraft(settings.subdir);
30692
30737
  setImageSaved(false);
30693
30738
  }
30739
+ if (openCards.has("remote")) setTunnel(await props.api.tunnelStatus());
30694
30740
  setError(void 0);
30695
30741
  } catch (cause) {
30696
30742
  setError(cause);
30697
30743
  }
30698
30744
  }, [props.api, openCards]);
30745
+ const createTunnel = (0, react.useCallback)(() => {
30746
+ if (!window.confirm(t("tunnelStartConfirm"))) return;
30747
+ setTunnelBusy(true);
30748
+ setError(void 0);
30749
+ setActivity(void 0);
30750
+ setActivity({
30751
+ stage: "locating",
30752
+ label: "正在准备 cloudflared…",
30753
+ percent: null
30754
+ });
30755
+ const poll = window.setInterval(() => {
30756
+ props.api.tunnelActivity().then((a) => {
30757
+ if (a.stage !== "idle") setActivity(a);
30758
+ }).catch(() => {});
30759
+ }, 350);
30760
+ props.api.tunnelCreate().then(async (next) => {
30761
+ setTunnel(next);
30762
+ setActivity(void 0);
30763
+ await refresh();
30764
+ }).catch(setError).finally(() => {
30765
+ window.clearInterval(poll);
30766
+ setActivity(void 0);
30767
+ setTunnelBusy(false);
30768
+ });
30769
+ }, [
30770
+ props.api,
30771
+ refresh,
30772
+ t
30773
+ ]);
30774
+ const stopTunnel = (0, react.useCallback)(() => {
30775
+ setTunnelBusy(true);
30776
+ setError(void 0);
30777
+ props.api.tunnelStop().then(async (next) => {
30778
+ setTunnel(next);
30779
+ await refresh();
30780
+ }).catch(setError).finally(() => setTunnelBusy(false));
30781
+ }, [props.api, refresh]);
30699
30782
  (0, react.useEffect)(() => {
30700
30783
  refresh();
30701
30784
  }, [refresh]);
@@ -30705,6 +30788,25 @@ window.__ModuleLoader__.load({
30705
30788
  setListenerPort(String(status.configuredPort));
30706
30789
  }, [status?.configuredHost, status?.configuredPort]);
30707
30790
  const roots = status?.roots ?? [];
30791
+ const genTunnelConfig = (0, react.useCallback)((baseUrl) => {
30792
+ setConfigTextBusy(true);
30793
+ setError(void 0);
30794
+ props.api.createConfigText({
30795
+ ...roots.length === 0 ? {} : { rootIds: roots.map((root) => root.id) },
30796
+ endpoints: [{
30797
+ label: t("tunnelEndpointLabel"),
30798
+ baseUrl
30799
+ }]
30800
+ }).then((value) => {
30801
+ setConfigText(value);
30802
+ return refresh();
30803
+ }).catch(setError).finally(() => setConfigTextBusy(false));
30804
+ }, [
30805
+ props.api,
30806
+ refresh,
30807
+ roots,
30808
+ t
30809
+ ]);
30708
30810
  const [grantedScopes, setGrantedScopes] = (0, react.useState)([...DEVICE_SCOPES]);
30709
30811
  const toggleScope = (0, react.useCallback)((scope) => {
30710
30812
  setGrantedScopes((current) => current.includes(scope) ? current.filter((item) => item !== scope) : [...current, scope]);
@@ -30880,6 +30982,60 @@ window.__ModuleLoader__.load({
30880
30982
  className: `daw-pill${status?.remoteEnabled ? " on" : ""}`,
30881
30983
  children: status?.remoteEnabled ? t("remoteEnabled") : t("loopbackOnly")
30882
30984
  }) }),
30985
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
30986
+ className: "daw-tunnel",
30987
+ children: [
30988
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
30989
+ className: "daw-tunnel-head",
30990
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("tunnelTitle") }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
30991
+ className: `daw-pill${tunnel?.running ? " on" : ""}`,
30992
+ children: tunnel?.running ? t("tunnelRunning") : t("tunnelStopped")
30993
+ })]
30994
+ }),
30995
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
30996
+ className: "daw-list-meta",
30997
+ children: t("tunnelHint")
30998
+ }),
30999
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
31000
+ className: "daw-warning",
31001
+ children: t("tunnelSecurityWarning")
31002
+ }),
31003
+ tunnel?.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
31004
+ className: "daw-warning",
31005
+ children: tunnel.error
31006
+ }),
31007
+ tunnel?.url !== void 0 && tunnel.url !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
31008
+ className: "daw-tunnel-url",
31009
+ style: { marginTop: 8 },
31010
+ children: tunnel.url
31011
+ }),
31012
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
31013
+ className: "daw-command primary",
31014
+ style: { marginTop: 10 },
31015
+ disabled: tunnelBusy || roots.length === 0,
31016
+ onClick: () => {
31017
+ if (tunnel?.running === true) stopTunnel();
31018
+ else createTunnel();
31019
+ },
31020
+ children: tunnel?.running === true ? t("tunnelStop") : t("tunnelCreate")
31021
+ }),
31022
+ activity !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
31023
+ className: "daw-tunnel-progress",
31024
+ style: { marginTop: 10 },
31025
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
31026
+ className: "daw-progress-track",
31027
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
31028
+ className: "daw-progress-fill",
31029
+ style: { width: pct(activity) }
31030
+ })
31031
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
31032
+ className: "daw-list-meta",
31033
+ style: { marginTop: 4 },
31034
+ children: activity.label
31035
+ })]
31036
+ })
31037
+ ]
31038
+ }),
30883
31039
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
30884
31040
  className: "daw-scope-grid",
30885
31041
  children: DEVICE_SCOPES.map((scope) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
@@ -30942,6 +31098,15 @@ window.__ModuleLoader__.load({
30942
31098
  },
30943
31099
  children: t("newConfigText")
30944
31100
  }),
31101
+ tunnel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
31102
+ className: "daw-command daw-inline-command",
31103
+ disabled: configTextBusy || tunnelBusy,
31104
+ onClick: () => {
31105
+ if (tunnel.running === true && tunnel.url !== null && tunnel.url !== void 0) genTunnelConfig(tunnel.url);
31106
+ else createTunnel();
31107
+ },
31108
+ children: tunnel.running === true && tunnel.url !== null && tunnel.url !== void 0 ? t("tunnelGenConfigReady") : t("tunnelGenConfigOff")
31109
+ }),
30945
31110
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
30946
31111
  className: "daw-list-meta daw-listener-hint",
30947
31112
  children: t("configTextScopeNote")