@wenbin_wb/dsh-bridge 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 wenbin-wb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # dsh-bridge
2
+
3
+ > DeepSeek Harness 多通道远程访问插件
4
+
5
+ 在 DSH 设置页新增「远程访问」面板,支持局域网二维码、Cloudflare 隧道、自建 WebSocket 隧道三种方式从任意设备访问你的 DSH 实例。
6
+
7
+ ![DSH Plugin](https://img.shields.io/badge/dsh--plugin-0.1.0--rc.6-blue)
8
+ ![License](https://img.shields.io/badge/license-MIT-green)
9
+ ![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)
10
+
11
+ ## 功能
12
+
13
+ - **局域网访问**:自动检测本机 IP,生成二维码,同一 Wi-Fi 下的设备直接扫码访问
14
+ - **Cloudflare 隧道**:一键获取公网地址,自动下载 cloudflared(无需手动安装),重启后 URL 会变化
15
+ - **自建隧道**:WebSocket 反向隧道,连接自己部署的隧道服务器,可获得固定域名
16
+ - **安全提示**:URL 和二维码带访问警告,防止误分享
17
+ - **版本检查**:进入面板自动检测是否有新版本
18
+
19
+ ## 兼容性
20
+
21
+ > 本插件基于 **DeepSeek Harness v0.1.0-rc.6** 开发测试。
22
+ > DSH 是开发者预览版,插件契约可能随版本变化,请关注 [CHANGELOG](./CHANGELOG.md)。
23
+
24
+ ## 安装
25
+
26
+ 从 npm 安装(发布后):
27
+
28
+ ```bash
29
+ dsh plugin --profile web add @wenbin_wb/dsh-bridge
30
+ ```
31
+
32
+ 从源码安装:
33
+
34
+ ```bash
35
+ git clone https://github.com/wenbin-wb/dsh-bridge.git
36
+ dsh plugin --profile web add ./dsh-bridge
37
+ ```
38
+
39
+ 安装完成后重启 DSH,在设置页找到「远程访问」即可使用。
40
+
41
+ ## 使用
42
+
43
+ ### 局域网访问
44
+
45
+ 插件启动后自动开启,无需任何配置。打开设置页「远程访问」,用手机扫描二维码即可访问。
46
+
47
+ ### Cloudflare 隧道
48
+
49
+ 点击「Cloudflare 隧道」卡片中的「开启」按钮:
50
+ 1. 首次使用会自动从 GitHub 下载 cloudflared(约 30MB),需要能访问 GitHub
51
+ 2. 下载完成后自动启动,几秒内显示公网 URL 和二维码
52
+ 3. 每次重启后 URL 会变化;点「重置链接」可主动获取新 URL
53
+
54
+ ### 自建隧道
55
+
56
+ 1. 展开「如何搭建自建隧道服务器?」查看搭建步骤
57
+ 2. 填写 WebSocket 服务器地址(`wss://...`)和访问令牌
58
+ 3. 点「保存配置」后点「开启」
59
+
60
+ 配置自动持久化到 `~/.dsh/dsh-bridge/config.json`,重启后无需重新填写。
61
+
62
+ ## 可选配置
63
+
64
+ 插件开箱即用,无需配置。如需修改代理端口,在 cordis.yml 中添加:
65
+
66
+ ```yaml
67
+ - name: dsh-bridge
68
+ config:
69
+ port: 3082 # 默认 3082
70
+ ```
71
+
72
+ ## 开发
73
+
74
+ ```bash
75
+ git clone https://github.com/wenbin-wb/dsh-bridge.git
76
+ cd dsh-bridge
77
+ npm install
78
+
79
+ # 修改 client/index.js 后重新构建
80
+ npm run build:client
81
+
82
+ # 安装到 web profile 并重启 DSH
83
+ dsh plugin --profile web add .
84
+ ```
85
+
86
+ ## License
87
+
88
+ MIT © [wenbin-wb](https://github.com/wenbin-wb)
@@ -0,0 +1,41 @@
1
+ // dsh-bridge 客户端打包:client/index.js → client/client.js
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { build } from 'esbuild';
6
+
7
+ const sourceDir = dirname(fileURLToPath(import.meta.url));
8
+ const packageRoot = resolve(sourceDir, '..');
9
+ const outputPath = resolve(packageRoot, 'client/client.js');
10
+ const loaderId = 'dsh-bridge';
11
+
12
+ const result = await build({
13
+ entryPoints: [resolve(sourceDir, 'index.js')],
14
+ bundle: true,
15
+ format: 'cjs',
16
+ platform: 'browser',
17
+ target: ['chrome100'],
18
+ external: ['react', 'react/jsx-runtime'],
19
+ write: false,
20
+ minify: process.env.NODE_ENV === 'production',
21
+ legalComments: 'none',
22
+ });
23
+
24
+ const bundled = result.outputFiles?.[0]?.text;
25
+ if (!bundled) throw new Error('esbuild did not produce a client bundle');
26
+
27
+ const wrapped = `window.__ModuleLoader__.load({
28
+ id: ${JSON.stringify(loaderId)},
29
+ factory: (require) => {
30
+ var module = { exports: {} };
31
+ var exports = module.exports;
32
+ var React = require("react");
33
+ ${bundled}
34
+ return module.exports;
35
+ }
36
+ });
37
+ `;
38
+
39
+ await mkdir(dirname(outputPath), { recursive: true });
40
+ await writeFile(outputPath, wrapped, 'utf8');
41
+ console.log(`Wrote ${outputPath}`);
@@ -0,0 +1,484 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-bridge",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ var React = require("react");
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __export = (target, all) => {
12
+ for (var name2 in all)
13
+ __defProp(target, name2, { get: all[name2], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
+
25
+ // client/index.js
26
+ var index_exports = {};
27
+ __export(index_exports, {
28
+ apply: () => apply,
29
+ inject: () => inject,
30
+ name: () => name
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+
34
+ // lib/bridge-rpc.js
35
+ var BRIDGE_RPC_CHANNEL = "/dsh-bridge";
36
+ var BRIDGE_ENDPOINTS = {
37
+ getStatus: "getStatus",
38
+ startCustomTunnel: "startCustomTunnel",
39
+ stopCustomTunnel: "stopCustomTunnel",
40
+ startCloudflared: "startCloudflared",
41
+ stopCloudflared: "stopCloudflared",
42
+ resetCloudflared: "resetCloudflared",
43
+ saveCustomTunnelConfig: "saveCustomTunnelConfig",
44
+ checkVersion: "checkVersion"
45
+ };
46
+
47
+ // client/index.js
48
+ var GITHUB_URL = "https://github.com/wenbin-wb/dsh-bridge";
49
+ var ISSUES_URL = "https://github.com/wenbin-wb/dsh-bridge/issues/new";
50
+ var name = "dsh-bridge";
51
+ var inject = ["slots", "connection"];
52
+ function semverGt(a, b) {
53
+ const parse = (v) => {
54
+ const [main = "", pre = ""] = String(v).split("-");
55
+ const [maj = 0, min = 0, pat = 0] = main.split(".").map(Number);
56
+ return { maj, min, pat, pre };
57
+ };
58
+ const av = parse(a), bv = parse(b);
59
+ if (av.maj !== bv.maj) return av.maj > bv.maj;
60
+ if (av.min !== bv.min) return av.min > bv.min;
61
+ if (av.pat !== bv.pat) return av.pat > bv.pat;
62
+ if (!av.pre && bv.pre) return true;
63
+ if (av.pre && !bv.pre) return false;
64
+ return av.pre > bv.pre;
65
+ }
66
+ var s = {
67
+ card: { background: "var(--dsw-alias-bg-layer-1,#fff)", border: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", borderRadius: 12, padding: "16px 20px", marginBottom: 16 },
68
+ block: { borderTop: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", marginTop: 12, paddingTop: 12 },
69
+ muted: { color: "var(--dsw-alias-label-tertiary,#8b93a1)", fontSize: 12, lineHeight: 1.5 },
70
+ label: { color: "var(--dsw-alias-label-primary,inherit)", fontSize: 13, fontWeight: 500 },
71
+ code: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 12, wordBreak: "break-all", color: "var(--dsw-alias-label-primary,inherit)" },
72
+ btnPri: { font: "inherit", cursor: "pointer", border: "none", background: "var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary,#4f6ef7))", color: "#fff", height: 32, padding: "0 14px", borderRadius: 999, fontSize: 13, fontWeight: 500, display: "inline-flex", alignItems: "center", gap: 4 },
73
+ btnGhost: { font: "inherit", cursor: "pointer", border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", background: "var(--dsw-alias-bg-layer-1,#fff)", color: "var(--dsw-alias-label-primary,inherit)", height: 32, padding: "0 14px", borderRadius: 999, fontSize: 13, display: "inline-flex", alignItems: "center", gap: 4 },
74
+ btnLink: { font: "inherit", cursor: "pointer", border: "none", background: "none", color: "var(--dsw-alias-brand-primary,#4f6ef7)", fontSize: 12, padding: 0, display: "inline-flex", alignItems: "center", gap: 3, textDecoration: "none" },
75
+ qr: { width: 200, height: 200, borderRadius: 10, border: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", margin: "8px 0", display: "block" },
76
+ tag: { display: "inline-flex", alignItems: "center", padding: "2px 8px", borderRadius: 999, fontSize: 12, fontWeight: 500 },
77
+ input: { width: "100%", font: "inherit", fontSize: 13, padding: "7px 10px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", background: "var(--dsw-alias-bg-layer-1,#fff)", color: "var(--dsw-alias-label-primary,inherit)", outline: "none", boxSizing: "border-box" },
78
+ warn: { background: "var(--dsw-alias-state-warn-bg,#fffbeb)", border: "1px solid var(--dsw-alias-state-warn-border,#fde68a)", borderRadius: 8, padding: "10px 14px", fontSize: 12, color: "var(--dsw-alias-state-warn-primary,#92400e)", lineHeight: 1.6 },
79
+ tip: { background: "var(--dsw-alias-bg-layer-2,#f9fafb)", borderRadius: 8, padding: "10px 14px", fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.6 }
80
+ };
81
+ function StatusTag({ running }) {
82
+ return React.createElement("span", {
83
+ style: {
84
+ ...s.tag,
85
+ background: running ? "var(--dsw-alias-state-success-bg,#ecfdf5)" : "var(--dsw-alias-bg-layer-2,#f3f4f6)",
86
+ color: running ? "var(--dsw-alias-state-success-primary,#059669)" : "var(--dsw-alias-label-secondary,#6b7280)"
87
+ }
88
+ }, running ? "\u8FD0\u884C\u4E2D" : "\u672A\u542F\u52A8");
89
+ }
90
+ function QrBlock({ url, qr, onReset }) {
91
+ const [copied, setCopied] = React.useState(false);
92
+ const [showQr, setShowQr] = React.useState(true);
93
+ const copy = React.useCallback(() => {
94
+ navigator.clipboard?.writeText(url).then(() => {
95
+ setCopied(true);
96
+ setTimeout(() => setCopied(false), 2e3);
97
+ }).catch(() => {
98
+ });
99
+ }, [url]);
100
+ const toggleQr = React.useCallback(() => setShowQr((v) => !v), []);
101
+ return React.createElement(
102
+ "div",
103
+ { style: { marginTop: 10 } },
104
+ React.createElement(
105
+ "div",
106
+ { style: s.warn },
107
+ "\u26A0\uFE0F \u8BF7\u52FF\u5C06\u6B64\u94FE\u63A5\u6216\u4E8C\u7EF4\u7801\u5206\u4EAB\u7ED9\u4ED6\u4EBA\uFF0C\u4EFB\u4F55\u4EBA\u626B\u7801\u540E\u90FD\u53EF\u76F4\u63A5\u8BBF\u95EE\u60A8\u7684 DSH\u3002"
108
+ ),
109
+ React.createElement(
110
+ "div",
111
+ { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 10 } },
112
+ React.createElement("code", { style: { ...s.code, flex: 1 } }, url),
113
+ React.createElement("button", {
114
+ style: { ...s.btnGhost, height: 26, padding: "0 10px", fontSize: 12, flexShrink: 0 },
115
+ onClick: copy
116
+ }, copied ? "\u2713 \u5DF2\u590D\u5236" : "\u590D\u5236"),
117
+ React.createElement("button", {
118
+ style: { ...s.btnGhost, height: 26, padding: "0 10px", fontSize: 12, flexShrink: 0 },
119
+ onClick: toggleQr
120
+ }, showQr ? "\u9690\u85CF\u4E8C\u7EF4\u7801" : "\u663E\u793A\u4E8C\u7EF4\u7801")
121
+ ),
122
+ showQr && qr && React.createElement(
123
+ "div",
124
+ { style: { marginTop: 8 } },
125
+ React.createElement("img", { src: qr, alt: "QR", style: s.qr }),
126
+ React.createElement("div", { style: { ...s.muted, marginTop: 4 } }, "\u8BF7\u5728\u79C1\u5BC6\u73AF\u5883\u4E0B\u4F7F\u7528")
127
+ ),
128
+ onReset && React.createElement(
129
+ "div",
130
+ { style: { marginTop: 8 } },
131
+ React.createElement("button", {
132
+ style: { ...s.btnGhost, fontSize: 12, height: 28 },
133
+ onClick: onReset,
134
+ title: "\u5173\u95ED\u96A7\u9053\u5E76\u91CD\u65B0\u5F00\u542F\uFF0C\u53EF\u83B7\u5F97\u65B0\u7684 URL"
135
+ }, "\u{1F504} \u91CD\u7F6E\u94FE\u63A5")
136
+ )
137
+ );
138
+ }
139
+ var CustomTunnelGuide = React.memo(function CustomTunnelGuide2() {
140
+ const [open, setOpen] = React.useState(false);
141
+ const toggle = React.useCallback(() => setOpen((v) => !v), []);
142
+ return React.createElement(
143
+ "div",
144
+ { style: s.block },
145
+ React.createElement("button", {
146
+ style: { ...s.btnGhost, fontSize: 12, height: 28, marginBottom: open ? 10 : 0 },
147
+ onClick: toggle
148
+ }, open ? "\u25B2 \u6536\u8D77\u642D\u5EFA\u6559\u7A0B" : "\u25B6 \u5982\u4F55\u642D\u5EFA\u81EA\u5EFA\u96A7\u9053\u670D\u52A1\u5668\uFF1F"),
149
+ open && React.createElement(
150
+ "div",
151
+ { style: s.tip },
152
+ React.createElement("div", { style: { fontWeight: 500, marginBottom: 6, color: "var(--dsw-alias-label-primary,inherit)" } }, "\u642D\u5EFA\u6B65\u9AA4"),
153
+ React.createElement(
154
+ "ol",
155
+ { style: { margin: 0, paddingLeft: 18, display: "flex", flexDirection: "column", gap: 4 } },
156
+ React.createElement("li", null, "\u5728\u516C\u7F51\u670D\u52A1\u5668\u4E0A\u5B89\u88C5 Node.js 18+"),
157
+ React.createElement("li", null, "\u90E8\u7F72\u96A7\u9053\u670D\u52A1\u7AEF\uFF0C\u63A8\u8350\u4F7F\u7528 frp \u6216\u517C\u5BB9 WebSocket \u7684\u53CD\u5411\u4EE3\u7406"),
158
+ React.createElement("li", null, "\u8BB0\u5F55\u670D\u52A1\u5668\u7684\u516C\u7F51\u57DF\u540D\uFF08\u5982 tunnel.example.com\uFF09\u548C\u8BBF\u95EE\u4EE4\u724C"),
159
+ React.createElement("li", null, "\u5728\u4E0B\u65B9\u586B\u5199 WebSocket \u5730\u5740\uFF08wss://...\uFF09\u548C\u4EE4\u724C\uFF0C\u4FDD\u5B58\u540E\u70B9\u5F00\u542F")
160
+ ),
161
+ React.createElement(
162
+ "div",
163
+ { style: { marginTop: 8, paddingTop: 8, borderTop: "1px solid var(--dsw-alias-border-l2,#e5e7eb)" } },
164
+ React.createElement("div", { style: { fontWeight: 500, marginBottom: 4, color: "var(--dsw-alias-label-primary,inherit)" } }, "\u5730\u5740\u683C\u5F0F"),
165
+ React.createElement(
166
+ "code",
167
+ { style: { ...s.code, display: "block", padding: "6px 8px", background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", borderRadius: 6 } },
168
+ "wss://tunnel.example.com/connect"
169
+ )
170
+ )
171
+ )
172
+ );
173
+ });
174
+ var CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm2({ serverUrl: initUrl, accessToken: initToken, onSave }) {
175
+ const [serverUrl, setServerUrl] = React.useState(initUrl ?? "");
176
+ const [accessToken, setAccessToken] = React.useState(initToken ?? "");
177
+ const [saving, setSaving] = React.useState(false);
178
+ const syncedRef = React.useRef(false);
179
+ React.useEffect(() => {
180
+ if (!syncedRef.current && (initUrl || initToken)) {
181
+ setServerUrl(initUrl ?? "");
182
+ setAccessToken(initToken ?? "");
183
+ syncedRef.current = true;
184
+ }
185
+ }, [initUrl, initToken]);
186
+ const dirty = serverUrl !== (initUrl ?? "") || accessToken !== (initToken ?? "");
187
+ const handleSave = React.useCallback(async () => {
188
+ setSaving(true);
189
+ try {
190
+ await onSave(serverUrl, accessToken);
191
+ } finally {
192
+ setSaving(false);
193
+ }
194
+ }, [onSave, serverUrl, accessToken]);
195
+ const handleUrlChange = React.useCallback((e) => setServerUrl(e.target.value), []);
196
+ const handleTokenChange = React.useCallback((e) => setAccessToken(e.target.value), []);
197
+ return React.createElement(
198
+ "div",
199
+ { style: s.block },
200
+ React.createElement("div", { style: { ...s.muted, marginBottom: 8 } }, "\u670D\u52A1\u5668\u914D\u7F6E"),
201
+ React.createElement(
202
+ "div",
203
+ { style: { display: "flex", flexDirection: "column", gap: 8 } },
204
+ React.createElement("input", {
205
+ style: s.input,
206
+ placeholder: "WebSocket \u5730\u5740\uFF0C\u4F8B\u5982 wss://tunnel.example.com/connect",
207
+ value: serverUrl,
208
+ onChange: handleUrlChange,
209
+ disabled: saving
210
+ }),
211
+ React.createElement("input", {
212
+ style: s.input,
213
+ type: "password",
214
+ placeholder: "\u8BBF\u95EE\u4EE4\u724C\uFF08Access Token\uFF09",
215
+ value: accessToken,
216
+ onChange: handleTokenChange,
217
+ disabled: saving
218
+ }),
219
+ React.createElement("button", {
220
+ style: { ...s.btnPri, alignSelf: "flex-start", opacity: !dirty || saving ? 0.5 : 1 },
221
+ disabled: !dirty || saving,
222
+ onClick: handleSave
223
+ }, saving ? "\u4FDD\u5B58\u4E2D\u2026" : "\u4FDD\u5B58\u914D\u7F6E")
224
+ )
225
+ );
226
+ });
227
+ var TunnelCard = React.memo(function TunnelCard2({ title, desc, data, onStart, onStop, onReset, children }) {
228
+ const { running, configured, url, qr, state } = data ?? {};
229
+ const phase = state?.phase ?? "idle";
230
+ return React.createElement(
231
+ "div",
232
+ { style: s.card },
233
+ React.createElement(
234
+ "div",
235
+ { style: { display: "flex", justifyContent: "space-between", alignItems: "center" } },
236
+ React.createElement(
237
+ "div",
238
+ null,
239
+ React.createElement("div", { style: s.label }, title),
240
+ React.createElement("div", { style: { ...s.muted, marginTop: 2 } }, desc)
241
+ ),
242
+ React.createElement(StatusTag, { running })
243
+ ),
244
+ children,
245
+ phase !== "idle" && phase !== "ready" && React.createElement("div", {
246
+ style: {
247
+ ...s.block,
248
+ fontSize: 12,
249
+ color: phase === "error" ? "var(--dsw-alias-state-error-primary,#dc2626)" : "var(--dsw-alias-label-secondary,#6b7280)"
250
+ }
251
+ }, state?.detail ?? phase),
252
+ url && React.createElement(QrBlock, { url, qr, onReset }),
253
+ (onStart || onStop) && React.createElement(
254
+ "div",
255
+ {
256
+ style: { ...s.block, display: "flex", gap: 8, flexWrap: "wrap" }
257
+ },
258
+ !running && onStart && React.createElement("button", {
259
+ style: { ...s.btnPri, opacity: configured === false ? 0.4 : 1 },
260
+ onClick: onStart,
261
+ disabled: configured === false || phase === "connecting" || phase === "downloading",
262
+ title: configured === false ? "\u8BF7\u5148\u4FDD\u5B58\u670D\u52A1\u5668\u914D\u7F6E" : ""
263
+ }, phase === "connecting" ? "\u8FDE\u63A5\u4E2D\u2026" : phase === "downloading" ? "\u4E0B\u8F7D\u4E2D\u2026" : "\u5F00\u542F"),
264
+ running && onStop && React.createElement("button", { style: s.btnGhost, onClick: onStop }, "\u5173\u95ED")
265
+ )
266
+ );
267
+ });
268
+ function VersionBanner({ rpcCall }) {
269
+ const [info, setInfo] = React.useState(null);
270
+ const [loading, setLoading] = React.useState(false);
271
+ const check = React.useCallback(async () => {
272
+ setLoading(true);
273
+ try {
274
+ const r = await rpcCall(BRIDGE_ENDPOINTS.checkVersion, {});
275
+ if (r?.ok) setInfo(r.value);
276
+ } finally {
277
+ setLoading(false);
278
+ }
279
+ }, [rpcCall]);
280
+ React.useEffect(() => {
281
+ check();
282
+ }, [check]);
283
+ const hasUpdate = info?.latest && info?.current && !info.error && semverGt(info.latest, info.current);
284
+ const links = React.createElement(
285
+ "div",
286
+ { style: { display: "flex", gap: 12, alignItems: "center" } },
287
+ React.createElement("a", {
288
+ href: GITHUB_URL,
289
+ target: "_blank",
290
+ rel: "noreferrer",
291
+ style: s.btnLink
292
+ }, "\u2B50 GitHub"),
293
+ React.createElement("a", {
294
+ href: ISSUES_URL,
295
+ target: "_blank",
296
+ rel: "noreferrer",
297
+ style: s.btnLink
298
+ }, "\u{1F41B} \u53CD\u9988 Bug")
299
+ );
300
+ if (hasUpdate) {
301
+ return React.createElement(
302
+ "div",
303
+ { style: { marginBottom: 16 } },
304
+ React.createElement(
305
+ "div",
306
+ {
307
+ style: {
308
+ display: "flex",
309
+ alignItems: "center",
310
+ gap: 12,
311
+ background: "linear-gradient(135deg, var(--dsw-alias-brand-primary,#4f6ef7) 0%, #6366f1 100%)",
312
+ borderRadius: 10,
313
+ padding: "12px 16px",
314
+ marginBottom: 8,
315
+ color: "#fff"
316
+ }
317
+ },
318
+ React.createElement("span", { style: { fontSize: 20 } }, "\u{1F389}"),
319
+ React.createElement(
320
+ "div",
321
+ { style: { flex: 1 } },
322
+ React.createElement(
323
+ "div",
324
+ { style: { fontSize: 13, fontWeight: 600 } },
325
+ `\u53D1\u73B0\u65B0\u7248\u672C v${info.latest}\uFF08\u5F53\u524D v${info.current}\uFF09`
326
+ ),
327
+ React.createElement("code", {
328
+ style: { fontSize: 11, opacity: 0.85, fontFamily: "ui-monospace,Menlo,monospace", wordBreak: "break-all" }
329
+ }, "dsh plugin --profile web update @wenbin_wb/dsh-bridge --latest")
330
+ ),
331
+ React.createElement("button", {
332
+ style: {
333
+ font: "inherit",
334
+ cursor: "pointer",
335
+ border: "1px solid rgba(255,255,255,0.4)",
336
+ background: "rgba(255,255,255,0.15)",
337
+ color: "#fff",
338
+ height: 30,
339
+ padding: "0 12px",
340
+ borderRadius: 999,
341
+ fontSize: 12,
342
+ display: "inline-flex",
343
+ alignItems: "center",
344
+ opacity: loading ? 0.5 : 1,
345
+ flexShrink: 0
346
+ },
347
+ onClick: check,
348
+ disabled: loading
349
+ }, loading ? "\u2026" : "\u5237\u65B0")
350
+ ),
351
+ links
352
+ );
353
+ }
354
+ return React.createElement(
355
+ "div",
356
+ { style: { marginBottom: 14, display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 8 } },
357
+ React.createElement(
358
+ "div",
359
+ { style: { ...s.muted, display: "flex", alignItems: "center", gap: 8 } },
360
+ loading && !info ? React.createElement("span", null, "\u68C0\u67E5\u66F4\u65B0\u4E2D\u2026") : info ? React.createElement("span", null, `v${info.current}${info.latest && !info.error ? " \xB7 \u5DF2\u662F\u6700\u65B0" : info.error ? " \xB7 \u68C0\u67E5\u5931\u8D25" : ""}`) : null,
361
+ info && React.createElement("button", {
362
+ style: { ...s.btnGhost, height: 22, padding: "0 8px", fontSize: 11, opacity: loading ? 0.5 : 1 },
363
+ onClick: check,
364
+ disabled: loading
365
+ }, loading ? "\u2026" : "\u91CD\u65B0\u68C0\u67E5")
366
+ ),
367
+ links
368
+ );
369
+ }
370
+ function BridgePanel({ rpcCall }) {
371
+ const [status, setStatus] = React.useState(null);
372
+ const [err, setErr] = React.useState(null);
373
+ const load = React.useCallback(async (quiet = false) => {
374
+ try {
375
+ const r = await rpcCall(BRIDGE_ENDPOINTS.getStatus, {});
376
+ if (!r?.ok) throw new Error(r?.error?.message ?? "RPC failed");
377
+ setStatus(r.value);
378
+ if (!quiet) setErr(null);
379
+ } catch (e) {
380
+ setErr(e.message);
381
+ }
382
+ }, [rpcCall]);
383
+ React.useEffect(() => {
384
+ load();
385
+ const t = setInterval(() => load(true), 3e3);
386
+ return () => clearInterval(t);
387
+ }, [load]);
388
+ const act = React.useCallback(async (endpoint, payload) => {
389
+ try {
390
+ const r = await rpcCall(endpoint, payload ?? {});
391
+ if (!r?.ok) throw new Error(r?.error?.message ?? "RPC failed");
392
+ setStatus(r.value);
393
+ setErr(null);
394
+ } catch (e) {
395
+ setErr(e.message);
396
+ }
397
+ }, [rpcCall]);
398
+ const onStartCloudflared = React.useCallback(() => act(BRIDGE_ENDPOINTS.startCloudflared), [act]);
399
+ const onStopCloudflared = React.useCallback(() => act(BRIDGE_ENDPOINTS.stopCloudflared), [act]);
400
+ const onResetCloudflared = React.useCallback(
401
+ () => act(BRIDGE_ENDPOINTS.stopCloudflared).then(() => act(BRIDGE_ENDPOINTS.startCloudflared)),
402
+ [act]
403
+ );
404
+ const onStartCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.startCustomTunnel), [act]);
405
+ const onStopCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.stopCustomTunnel), [act]);
406
+ const saveConfig = React.useCallback(
407
+ (serverUrl, accessToken) => act(BRIDGE_ENDPOINTS.saveCustomTunnelConfig, { serverUrl, accessToken }),
408
+ [act]
409
+ );
410
+ if (!status && !err) {
411
+ return React.createElement("div", {
412
+ style: { padding: 32, color: "var(--dsw-alias-label-tertiary,#9ca3af)", fontSize: 13 }
413
+ }, "\u52A0\u8F7D\u4E2D\u2026");
414
+ }
415
+ const ct = status?.customTunnel;
416
+ return React.createElement(
417
+ "div",
418
+ { style: { maxWidth: 560 } },
419
+ err && React.createElement("div", {
420
+ style: { ...s.card, background: "var(--dsw-alias-state-error-bg,#fef2f2)", color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 13, marginBottom: 16 }
421
+ }, err),
422
+ React.createElement(VersionBanner, { rpcCall }),
423
+ React.createElement(TunnelCard, {
424
+ title: "\u5C40\u57DF\u7F51\u8BBF\u95EE",
425
+ desc: "\u540C\u4E00 Wi-Fi \u4E0B\u7684\u8BBE\u5907\u53EF\u76F4\u63A5\u626B\u7801\u8BBF\u95EE",
426
+ data: { running: status?.proxy?.running, url: status?.lan?.url, qr: status?.lan?.qr }
427
+ }),
428
+ React.createElement(TunnelCard, {
429
+ title: "Cloudflare \u96A7\u9053",
430
+ desc: "\u4E00\u952E\u83B7\u53D6\u516C\u7F51\u5730\u5740\uFF08\u91CD\u542F\u540E URL \u4F1A\u53D8\u5316\uFF09",
431
+ data: {
432
+ running: status?.cloudflared?.running,
433
+ url: status?.cloudflared?.url,
434
+ qr: status?.cloudflared?.qr,
435
+ state: status?.cloudflared?.state
436
+ },
437
+ onStart: onStartCloudflared,
438
+ onStop: onStopCloudflared,
439
+ onReset: status?.cloudflared?.running ? onResetCloudflared : null
440
+ }),
441
+ React.createElement(
442
+ TunnelCard,
443
+ {
444
+ title: "\u81EA\u5EFA\u96A7\u9053",
445
+ desc: "\u8FDE\u63A5\u81EA\u5DF1\u90E8\u7F72\u7684\u96A7\u9053\u670D\u52A1\u5668\uFF0C\u83B7\u5F97\u56FA\u5B9A\u57DF\u540D",
446
+ data: {
447
+ configured: ct?.configured,
448
+ running: ct?.running,
449
+ url: ct?.url,
450
+ qr: ct?.qr,
451
+ state: ct?.state
452
+ },
453
+ onStart: onStartCustom,
454
+ onStop: onStopCustom
455
+ },
456
+ React.createElement(CustomTunnelGuide),
457
+ React.createElement(CustomTunnelConfigForm, {
458
+ serverUrl: ct?.serverUrl ?? "",
459
+ accessToken: ct?.accessToken ?? "",
460
+ onSave: saveConfig
461
+ })
462
+ )
463
+ );
464
+ }
465
+ function apply(ctx) {
466
+ const rpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(BRIDGE_RPC_CHANNEL, endpoint, payload, signal);
467
+ ctx.slots.inject(
468
+ "settings.section",
469
+ () => ctx.slots.register(
470
+ {
471
+ name: "settings.section",
472
+ id: "dsh-bridge",
473
+ order: 10,
474
+ label: () => "\u8FDC\u7A0B\u8BBF\u95EE",
475
+ inject: () => ({ rpcCall })
476
+ },
477
+ BridgePanel
478
+ )
479
+ );
480
+ }
481
+
482
+ return module.exports;
483
+ }
484
+ });