dsh-remote-bridge 2.3.7 → 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">
@@ -77,10 +77,10 @@ dsh plugin --profile web add dsh-remote-bridge
77
77
  dsh plugin --profile web add dsh-remote-bridge --registry https://registry.npmmirror.com
78
78
 
79
79
  # 2. tarball from the GitHub release
80
- 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
81
81
 
82
82
  # 3. tarball from the Gitee release (faster from mainland China)
83
- 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
84
84
 
85
85
  npx @deepseek-ai/dsh web
86
86
  ```
@@ -97,7 +97,7 @@ Restart DSH after installation, then open settings — The section is titled **
97
97
  To install a downloaded release archive:
98
98
 
99
99
  ```sh
100
- 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
101
101
  ```
102
102
 
103
103
  ## First connection
@@ -331,11 +331,11 @@ pnpm pack
331
331
 
332
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.
333
333
 
334
- 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.
335
335
 
336
336
  ## Project
337
337
 
338
- - Version: `v2.3.6`
338
+ - Version: `v2.4.0`
339
339
  - Author: upstream [Hakunm](https://github.com/Hakunm); this fork maintained by [westanke](https://github.com/westanke)
340
340
  - Repository: [GitHub](https://github.com/westanke/dsh-remote-bridge) · [Gitee](https://gitee.com/westanke/dsh-remote-bridge)
341
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">
@@ -76,10 +76,10 @@ dsh plugin --profile web add dsh-remote-bridge
76
76
  dsh plugin --profile web add dsh-remote-bridge --registry https://registry.npmmirror.com
77
77
 
78
78
  # ② GitHub Release 的 tarball
79
- 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
80
80
 
81
81
  # ③ Gitee Release 的 tarball(国内访问更快)
82
- 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
83
83
 
84
84
  npx @deepseek-ai/dsh web
85
85
  ```
@@ -95,7 +95,7 @@ npx @deepseek-ai/dsh web
95
95
  从 GitHub Release 下载离线安装包时,可直接安装 tarball:
96
96
 
97
97
  ```sh
98
- 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
99
99
  ```
100
100
 
101
101
  ## 第一次使用
@@ -325,11 +325,11 @@ pnpm pack
325
325
 
326
326
  `npm pack` 的产物约 **0.29 MB**:2.2.0 删除独立页及其编辑器依赖、并关闭 source map 之后,从 1.72 MB 降下来。
327
327
 
328
- v2.3.6 基于 DSH `master@47f943859bef60e4160492346772ded9b24f765a` 开发,CI 覆盖 Windows、Ubuntu 和 macOS。
328
+ v2.4.0 基于 DSH `master@47f943859bef60e4160492346772ded9b24f765a` 开发,CI 覆盖 Windows、Ubuntu 和 macOS。
329
329
 
330
330
  ## 项目信息
331
331
 
332
- - 当前版本:`v2.3.6`
332
+ - 当前版本:`v2.4.0`
333
333
  - 作者:上游 [Hakunm](https://github.com/Hakunm),本 fork 维护 [westanke](https://github.com/westanke)
334
334
  - 仓库:[GitHub](https://github.com/westanke/dsh-remote-bridge) · [Gitee](https://gitee.com/westanke/dsh-remote-bridge)
335
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.7
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.7
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.7 }
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.7"
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")