dsh-remote-bridge 2.3.7 → 2.4.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.en.md +6 -6
- package/README.md +6 -6
- package/docs/api/asyncapi.yaml +117 -5
- package/docs/api/openapi.yaml +36 -3
- package/kotlin-sdk/build.gradle.kts +1 -1
- package/lib/client.cjs +166 -1
- package/lib/index.d.ts +183 -1
- package/lib/index.js +912 -19
- package/package.json +1 -1
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.
|
|
12
|
+
<img alt="Version" src="https://img.shields.io/badge/version-v2.4.1-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.
|
|
80
|
+
dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.4.1/dsh-remote-bridge-v2.4.1.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.
|
|
83
|
+
dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.4.1/dsh-remote-bridge-v2.4.1.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.
|
|
100
|
+
dsh plugin --profile web add ./dsh-remote-bridge-v2.4.1.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.
|
|
334
|
+
v2.4.1 was developed against DSH `master@47f943859bef60e4160492346772ded9b24f765a`. CI covers Windows, Ubuntu, and macOS.
|
|
335
335
|
|
|
336
336
|
## Project
|
|
337
337
|
|
|
338
|
-
- Version: `v2.
|
|
338
|
+
- Version: `v2.4.1`
|
|
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.
|
|
12
|
+
<img alt="Version" src="https://img.shields.io/badge/version-v2.4.1-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.
|
|
79
|
+
dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.4.1/dsh-remote-bridge-v2.4.1.tgz
|
|
80
80
|
|
|
81
81
|
# ③ Gitee Release 的 tarball(国内访问更快)
|
|
82
|
-
dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.
|
|
82
|
+
dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.4.1/dsh-remote-bridge-v2.4.1.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.
|
|
98
|
+
dsh plugin --profile web add ./dsh-remote-bridge-v2.4.1.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.
|
|
328
|
+
v2.4.1 基于 DSH `master@47f943859bef60e4160492346772ded9b24f765a` 开发,CI 覆盖 Windows、Ubuntu 和 macOS。
|
|
329
329
|
|
|
330
330
|
## 项目信息
|
|
331
331
|
|
|
332
|
-
- 当前版本:`v2.
|
|
332
|
+
- 当前版本:`v2.4.1`
|
|
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))
|
package/docs/api/asyncapi.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
asyncapi: 3.0.0
|
|
2
2
|
info:
|
|
3
3
|
title: DeepSeek Harness Mobile Events
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.4.1
|
|
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:
|
|
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:
|
|
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:
|
|
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,92 @@ components:
|
|
|
119
142
|
message: { type: string }
|
|
120
143
|
code: { type: string }
|
|
121
144
|
stopped: { type: boolean, const: true }
|
|
145
|
+
|
|
146
|
+
ChatTurnEnd:
|
|
147
|
+
type: object
|
|
148
|
+
description: |
|
|
149
|
+
A turn ended. Emitted for every `turn/end` the host reports, including the
|
|
150
|
+
ordinary `reason.kind === 'ok'` case. When the host stops a turn at its
|
|
151
|
+
output-token cap (`reason.kind === 'max-tokens'`) the reply is **truncated,
|
|
152
|
+
not failed** — this frame is the only place a client can learn that, because
|
|
153
|
+
no separate failure frame is emitted for it. Clients that ignore `reason`
|
|
154
|
+
keep working unchanged (the field is additive).
|
|
155
|
+
required: [sessionId, eventType]
|
|
156
|
+
properties:
|
|
157
|
+
sessionId: { type: string }
|
|
158
|
+
eventType: { type: string, const: turn/end }
|
|
159
|
+
seq: { type: integer }
|
|
160
|
+
reason:
|
|
161
|
+
type: object
|
|
162
|
+
description: |
|
|
163
|
+
Forwarded **verbatim** from the host; this repository does not interpret
|
|
164
|
+
it. Known `kind` values (kernel `turn-error.js` / `turn-max-tokens.js` /
|
|
165
|
+
`retry.js`): `ok` (normal), `error` (terminal failure — also delivered as
|
|
166
|
+
`chat.turn.failed`), `max-tokens` (output-token cap: reply truncated,
|
|
167
|
+
earlier output preserved — render as a **warning**, not an error),
|
|
168
|
+
`aborted` (cancelled by the user or a hook, e.g. signing out).
|
|
169
|
+
Unknown kinds must be ignored rather than treated as failure.
|
|
170
|
+
properties:
|
|
171
|
+
kind: { type: string }
|
|
172
|
+
error:
|
|
173
|
+
type: object
|
|
174
|
+
properties:
|
|
175
|
+
message: { type: string }
|
|
176
|
+
code: { type: string }
|
|
177
|
+
ChatDeliverablesPresented:
|
|
178
|
+
type: object
|
|
179
|
+
description: |
|
|
180
|
+
A file the agent explicitly **presented** (`chat.deliverables.presented`, produced by the
|
|
181
|
+
kernel's `present` tool). The durable event `deliverables/presented` has always been
|
|
182
|
+
forwarded verbatim inside history; this frame is what makes it visible **live**, so a
|
|
183
|
+
client does not have to wait for the next message to discover a new deliverable.
|
|
184
|
+
`files` is forwarded as-is (clients read `path` and `description`), so fields the kernel
|
|
185
|
+
adds later need no change here.
|
|
186
|
+
required: [sessionId, files]
|
|
187
|
+
properties:
|
|
188
|
+
sessionId: { type: string }
|
|
189
|
+
eventType: { type: string, const: deliverables/presented }
|
|
190
|
+
seq: { type: integer }
|
|
191
|
+
files:
|
|
192
|
+
type: array
|
|
193
|
+
items:
|
|
194
|
+
type: object
|
|
195
|
+
properties:
|
|
196
|
+
path: { type: string }
|
|
197
|
+
description: { type: string }
|
|
198
|
+
|
|
199
|
+
ChatSessionSnapshot:
|
|
200
|
+
type: object
|
|
201
|
+
description: |
|
|
202
|
+
The **subscription baseline** (`chat.session.snapshot`): the durable event sequence the
|
|
203
|
+
server starts streaming from. Sent once when a session's live relay attaches (the
|
|
204
|
+
upstream relay's first frame is always a snapshot). `cursor` is the durable `seq` at that cut, so a client can tell
|
|
205
|
+
whether its own history is behind (`max(local seq) < cursor` = events were missed
|
|
206
|
+
between the REST read and the subscription).
|
|
207
|
+
The baseline's **records are deliberately not shipped**: this bridge is REST + WebSocket
|
|
208
|
+
dual-channel, and pushing history bodies here would create two baselines whose relative
|
|
209
|
+
order cannot be guaranteed. History bodies stay on REST as the single source.
|
|
210
|
+
required: [sessionId]
|
|
211
|
+
properties:
|
|
212
|
+
sessionId: { type: string }
|
|
213
|
+
cursor: { type: integer }
|
|
214
|
+
hasMore: { type: boolean }
|
|
215
|
+
recordCount: { type: integer }
|
|
216
|
+
|
|
217
|
+
ConnectionReadyData:
|
|
218
|
+
type: object
|
|
219
|
+
description: |
|
|
220
|
+
Payload of the very first frame on every accepted WebSocket
|
|
221
|
+
(`{"type": "connection.ready"}`). Clients should use `capabilities` for
|
|
222
|
+
feature detection instead of comparing `bridgeVersion` against a
|
|
223
|
+
hard-coded minimum: a capability says what this server can do right now,
|
|
224
|
+
while a version number is only its historical label. Capability names are
|
|
225
|
+
`<feature>-v<major>` and are never silently removed. Send
|
|
226
|
+
`Authorization: Bearer <token>` (or the `bearer.<token>` subprotocol) as
|
|
227
|
+
usual; this frame carries no credentials.
|
|
228
|
+
required: [bridgeVersion, capabilities]
|
|
229
|
+
properties:
|
|
230
|
+
bridgeVersion: { type: string }
|
|
231
|
+
capabilities:
|
|
232
|
+
type: array
|
|
233
|
+
items: { type: string }
|
package/docs/api/openapi.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
openapi: 3.1.0
|
|
2
2
|
info:
|
|
3
3
|
title: DeepSeek Harness Mobile API
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.4.1
|
|
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.
|
|
27
|
+
pluginVersion: { type: string, const: 2.4.1 }
|
|
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':
|
|
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
|
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")
|