@wenbin_wb/dsh-bridge 2.6.0 → 2.6.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 +25 -13
- package/README.md +25 -13
- package/client/client.js +180 -16
- package/client/index.js +149 -4
- package/docs/feishu-usage.md +16 -0
- package/lib/bridge-rpc-constants.js +2 -0
- package/lib/bridge-rpc.js +20 -0
- package/lib/cloudflared-manager.mjs +39 -7
- package/lib/index.js +91 -11
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -104,8 +104,8 @@ npm install -g @deepseek-ai/dsh
|
|
|
104
104
|
# Install the latest version
|
|
105
105
|
dsh plugin --profile web add @wenbin_wb/dsh-bridge
|
|
106
106
|
|
|
107
|
-
# Or install a specific version (e.g. 2.6.
|
|
108
|
-
dsh plugin --profile web add @wenbin_wb/dsh-bridge@2.6.
|
|
107
|
+
# Or install a specific version (e.g. 2.6.1)
|
|
108
|
+
dsh plugin --profile web add @wenbin_wb/dsh-bridge@2.6.1
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
> 💡 **No global install permission?** Use `npx`:
|
|
@@ -212,18 +212,23 @@ Deeply tailored for smartphone screens and touch gestures, providing a clean and
|
|
|
212
212
|
#### Chat & Session Experience
|
|
213
213
|
|
|
214
214
|
<p align="center">
|
|
215
|
-
<img src="docs/screenshots/remote-web-mobile.jpg" width="
|
|
216
|
-
|
|
217
|
-
<img src="docs/screenshots/mobile-
|
|
215
|
+
<img src="docs/screenshots/remote-web-mobile.jpg" width="29%" alt="Mobile New Session Home" />
|
|
216
|
+
|
|
217
|
+
<img src="docs/screenshots/mobile-chat.jpg" width="29%" alt="Mobile Active Chat View" />
|
|
218
|
+
|
|
219
|
+
<img src="docs/screenshots/mobile-drawer.jpg" width="29%" alt="Mobile Native Sidebar Drawer" />
|
|
218
220
|
</p>
|
|
219
221
|
|
|
220
222
|
#### Remote Access & Plugin Settings Console
|
|
221
223
|
|
|
222
224
|
<p align="center">
|
|
223
|
-
<img src="docs/screenshots/mobile-settings-lan.jpg" width="
|
|
224
|
-
|
|
225
|
-
<img src="docs/screenshots/mobile-settings-
|
|
226
|
-
|
|
225
|
+
<img src="docs/screenshots/mobile-settings-lan.jpg" width="22%" alt="LAN Access Console" />
|
|
226
|
+
|
|
227
|
+
<img src="docs/screenshots/mobile-settings-tunnel.jpg" width="22%" alt="Public Tunnel Settings" />
|
|
228
|
+
|
|
229
|
+
<img src="docs/screenshots/mobile-settings-im.jpg" width="22%" alt="IM Bot Platforms" />
|
|
230
|
+
|
|
231
|
+
<img src="docs/screenshots/mobile-settings-security.jpg" width="22%" alt="Global Access Security" />
|
|
227
232
|
</p>
|
|
228
233
|
|
|
229
234
|
---
|
|
@@ -236,10 +241,17 @@ Automatically active when the plugin starts, zero configuration needed. Open Set
|
|
|
236
241
|
|
|
237
242
|
### Cloudflare Tunnel
|
|
238
243
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
244
|
+
Supports both **Quick Tunnel (zero config)** and **Cloudflare Token Named Tunnel (fixed domain)**, with optional automatic start on DSH boot:
|
|
245
|
+
|
|
246
|
+
- **Mode 1: Instant Quick Tunnel (Default)**
|
|
247
|
+
1. Click the "Start" button in the "Cloudflare Tunnel" card;
|
|
248
|
+
2. On first use, cloudflared (~30MB) will be automatically downloaded from GitHub;
|
|
249
|
+
3. Displays a public URL and QR code within seconds (changes on restart or click "Reset URL").
|
|
250
|
+
|
|
251
|
+
- **Mode 2: Cloudflare Token Named Tunnel (Permanent Fixed Domain · 100% Free)**
|
|
252
|
+
1. Create a free tunnel on the [Cloudflare Zero Trust Console](https://one.dash.cloudflare.com/) and bind your custom domain (e.g. `dsh.yourdomain.com`);
|
|
253
|
+
2. Expand the **"⚙️ Advanced: Fixed Domain (Cloudflare Token)"** form at the bottom of the card, enter your domain and Tunnel Token, then save;
|
|
254
|
+
3. Check **"Auto-start with DSH"** — the tunnel will automatically recover on DSH restarts with a **permanently fixed URL**!
|
|
243
255
|
|
|
244
256
|

|
|
245
257
|
|
package/README.md
CHANGED
|
@@ -104,8 +104,8 @@ npm install -g @deepseek-ai/dsh
|
|
|
104
104
|
# 安装最新版
|
|
105
105
|
dsh plugin --profile web add @wenbin_wb/dsh-bridge
|
|
106
106
|
|
|
107
|
-
# 或指定版本(如 2.6.
|
|
108
|
-
dsh plugin --profile web add @wenbin_wb/dsh-bridge@2.6.
|
|
107
|
+
# 或指定版本(如 2.6.1)
|
|
108
|
+
dsh plugin --profile web add @wenbin_wb/dsh-bridge@2.6.1
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
> 💡 **没有全局安装权限?** 使用 `npx` 方式:
|
|
@@ -212,18 +212,23 @@ dsh plugin --profile web add @wenbin_wb/dsh-bridge@latest
|
|
|
212
212
|
#### 对话与会话管理体验
|
|
213
213
|
|
|
214
214
|
<p align="center">
|
|
215
|
-
<img src="docs/screenshots/remote-web-mobile.jpg" width="
|
|
216
|
-
|
|
217
|
-
<img src="docs/screenshots/mobile-
|
|
215
|
+
<img src="docs/screenshots/remote-web-mobile.jpg" width="29%" alt="移动端新会话主页" />
|
|
216
|
+
|
|
217
|
+
<img src="docs/screenshots/mobile-chat.jpg" width="29%" alt="移动端已有对话交互" />
|
|
218
|
+
|
|
219
|
+
<img src="docs/screenshots/mobile-drawer.jpg" width="29%" alt="移动端原生抽屉侧边栏" />
|
|
218
220
|
</p>
|
|
219
221
|
|
|
220
222
|
#### 远程访问与插件设置中心
|
|
221
223
|
|
|
222
224
|
<p align="center">
|
|
223
|
-
<img src="docs/screenshots/mobile-settings-lan.jpg" width="
|
|
224
|
-
|
|
225
|
-
<img src="docs/screenshots/mobile-settings-
|
|
226
|
-
|
|
225
|
+
<img src="docs/screenshots/mobile-settings-lan.jpg" width="22%" alt="局域网访问控制台" />
|
|
226
|
+
|
|
227
|
+
<img src="docs/screenshots/mobile-settings-tunnel.jpg" width="22%" alt="公网隧道配置" />
|
|
228
|
+
|
|
229
|
+
<img src="docs/screenshots/mobile-settings-im.jpg" width="22%" alt="IM 机器人矩阵" />
|
|
230
|
+
|
|
231
|
+
<img src="docs/screenshots/mobile-settings-security.jpg" width="22%" alt="全局访问安全认证" />
|
|
227
232
|
</p>
|
|
228
233
|
|
|
229
234
|
---
|
|
@@ -236,10 +241,17 @@ dsh plugin --profile web add @wenbin_wb/dsh-bridge@latest
|
|
|
236
241
|
|
|
237
242
|
### Cloudflare 隧道
|
|
238
243
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
244
|
+
支持**免登录临时隧道**与**专属 Token 固定域名隧道**双模式,支持随 DSH 启动自动开启:
|
|
245
|
+
|
|
246
|
+
- **模式 1:极速免登录临时隧道(默认)**
|
|
247
|
+
1. 直接点击「Cloudflare 隧道」卡片中的「开启」按钮;
|
|
248
|
+
2. 首次使用会自动从 GitHub 下载 cloudflared(约 30MB);
|
|
249
|
+
3. 几秒内自动生成公网 URL 和二维码,点「重置链接」可随时换新。
|
|
250
|
+
|
|
251
|
+
- **模式 2:Cloudflare Token 固定域名(永久不变,完全免费)**
|
|
252
|
+
1. 在 [Cloudflare Zero Trust 控制台](https://one.dash.cloudflare.com/) 免费创建 Tunnel 并绑定域名(如 `dsh.yourdomain.com`);
|
|
253
|
+
2. 展开卡片底部的 **「⚙️ 高级配置:固定域名 (Cloudflare Token)」**,填入自定义域名与 Tunnel Token 并保存;
|
|
254
|
+
3. 勾选 **「随 DSH 启动自动开启」**,每次 DSH 重启即可自动恢复隧道,**URL 永久固定不变**!
|
|
243
255
|
|
|
244
256
|

|
|
245
257
|
|
package/client/client.js
CHANGED
|
@@ -40,6 +40,8 @@ var BRIDGE_ENDPOINTS = {
|
|
|
40
40
|
startCloudflared: "startCloudflared",
|
|
41
41
|
stopCloudflared: "stopCloudflared",
|
|
42
42
|
resetCloudflared: "resetCloudflared",
|
|
43
|
+
saveCloudflaredConfig: "saveCloudflaredConfig",
|
|
44
|
+
setTunnelAutoStart: "setTunnelAutoStart",
|
|
43
45
|
saveCustomTunnelConfig: "saveCustomTunnelConfig",
|
|
44
46
|
checkVersion: "checkVersion",
|
|
45
47
|
upgradePlugin: "upgradePlugin",
|
|
@@ -447,7 +449,19 @@ var CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm2({ serve
|
|
|
447
449
|
)
|
|
448
450
|
);
|
|
449
451
|
});
|
|
450
|
-
var TunnelCard = React.memo(function TunnelCard2({
|
|
452
|
+
var TunnelCard = React.memo(function TunnelCard2({
|
|
453
|
+
title,
|
|
454
|
+
desc,
|
|
455
|
+
data,
|
|
456
|
+
autoStart,
|
|
457
|
+
onToggleAutoStart,
|
|
458
|
+
onStart,
|
|
459
|
+
onStop,
|
|
460
|
+
onReset,
|
|
461
|
+
auth,
|
|
462
|
+
onNavigateSecurity,
|
|
463
|
+
children
|
|
464
|
+
}) {
|
|
451
465
|
const { running, configured, url, qr, state } = data ?? {};
|
|
452
466
|
const phase = state?.phase ?? "idle";
|
|
453
467
|
return React.createElement(
|
|
@@ -462,7 +476,32 @@ var TunnelCard = React.memo(function TunnelCard2({ title, desc, data, onStart, o
|
|
|
462
476
|
React.createElement("div", { style: s.label }, title),
|
|
463
477
|
React.createElement("div", { style: { ...s.muted, marginTop: 2 } }, desc)
|
|
464
478
|
),
|
|
465
|
-
React.createElement(
|
|
479
|
+
React.createElement(
|
|
480
|
+
"div",
|
|
481
|
+
{ style: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 6 } },
|
|
482
|
+
React.createElement(StatusTag, { running }),
|
|
483
|
+
onToggleAutoStart && React.createElement(
|
|
484
|
+
"label",
|
|
485
|
+
{
|
|
486
|
+
style: {
|
|
487
|
+
display: "flex",
|
|
488
|
+
alignItems: "center",
|
|
489
|
+
gap: 4,
|
|
490
|
+
fontSize: 11,
|
|
491
|
+
color: "var(--dsw-alias-label-secondary,#6b7280)",
|
|
492
|
+
cursor: "pointer",
|
|
493
|
+
userSelect: "none"
|
|
494
|
+
},
|
|
495
|
+
title: "DSH \u542F\u52A8\u65F6\u81EA\u52A8\u6062\u590D\u8BE5\u96A7\u9053\u7684\u8FD0\u884C\u72B6\u6001"
|
|
496
|
+
},
|
|
497
|
+
React.createElement("input", {
|
|
498
|
+
type: "checkbox",
|
|
499
|
+
checked: Boolean(autoStart),
|
|
500
|
+
onChange: (e) => onToggleAutoStart(e.target.checked)
|
|
501
|
+
}),
|
|
502
|
+
"\u968F DSH \u542F\u52A8\u81EA\u52A8\u5F00\u542F"
|
|
503
|
+
)
|
|
504
|
+
)
|
|
466
505
|
),
|
|
467
506
|
children,
|
|
468
507
|
phase !== "idle" && phase !== "ready" && React.createElement("div", {
|
|
@@ -488,6 +527,107 @@ var TunnelCard = React.memo(function TunnelCard2({ title, desc, data, onStart, o
|
|
|
488
527
|
)
|
|
489
528
|
);
|
|
490
529
|
});
|
|
530
|
+
var CloudflareConfigForm = React.memo(function CloudflareConfigForm2({ token, hostname, onSave }) {
|
|
531
|
+
const [open, setOpen] = React.useState(Boolean(token || hostname));
|
|
532
|
+
const [tokenVal, setTokenVal] = React.useState(token || "");
|
|
533
|
+
const [hostnameVal, setHostnameVal] = React.useState(hostname || "");
|
|
534
|
+
const [saving, setSaving] = React.useState(false);
|
|
535
|
+
const [msg, setMsg] = React.useState(null);
|
|
536
|
+
React.useEffect(() => {
|
|
537
|
+
setTokenVal(token || "");
|
|
538
|
+
setHostnameVal(hostname || "");
|
|
539
|
+
}, [token, hostname]);
|
|
540
|
+
const handleSave = async (e) => {
|
|
541
|
+
e.preventDefault();
|
|
542
|
+
setSaving(true);
|
|
543
|
+
setMsg(null);
|
|
544
|
+
try {
|
|
545
|
+
await onSave({ token: tokenVal, hostname: hostnameVal });
|
|
546
|
+
setMsg({ ok: true, text: "\u2713 \u56FA\u5B9A\u57DF\u540D\u914D\u7F6E\u5DF2\u4FDD\u5B58" });
|
|
547
|
+
} catch (err) {
|
|
548
|
+
setMsg({ ok: false, text: err.message || "\u4FDD\u5B58\u5931\u8D25" });
|
|
549
|
+
} finally {
|
|
550
|
+
setSaving(false);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
return React.createElement(
|
|
554
|
+
"div",
|
|
555
|
+
{
|
|
556
|
+
style: {
|
|
557
|
+
...s.block,
|
|
558
|
+
borderTop: "1px solid var(--dsw-alias-border-secondary, #e5e7eb)",
|
|
559
|
+
paddingTop: 10,
|
|
560
|
+
marginTop: 10
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
React.createElement(
|
|
564
|
+
"div",
|
|
565
|
+
{
|
|
566
|
+
style: { display: "flex", alignItems: "center", justifyContent: "space-between", cursor: "pointer", userSelect: "none" },
|
|
567
|
+
onClick: () => setOpen((v) => !v)
|
|
568
|
+
},
|
|
569
|
+
React.createElement(
|
|
570
|
+
"div",
|
|
571
|
+
{ style: { fontSize: 12, fontWeight: 500, color: "var(--dsw-alias-brand-primary, #3b82f6)" } },
|
|
572
|
+
"\u2699\uFE0F \u9AD8\u7EA7\u914D\u7F6E\uFF1A\u56FA\u5B9A\u57DF\u540D (Cloudflare Token) ",
|
|
573
|
+
(token || hostname) && React.createElement("span", { style: { fontSize: 11, color: "var(--dsw-alias-state-success-primary, #059669)", fontWeight: 400 } }, "\u25CF \u5DF2\u914D\u7F6E\u56FA\u5B9A\u57DF\u540D")
|
|
574
|
+
),
|
|
575
|
+
React.createElement("span", { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary, #9ca3af)" } }, open ? "\u25B4 \u6298\u53E0" : "\u25BE \u5C55\u5F00")
|
|
576
|
+
),
|
|
577
|
+
open && React.createElement(
|
|
578
|
+
"form",
|
|
579
|
+
{ onSubmit: handleSave, style: { marginTop: 10 } },
|
|
580
|
+
React.createElement(
|
|
581
|
+
"div",
|
|
582
|
+
{ style: { fontSize: 12, color: "var(--dsw-alias-label-secondary, #6b7280)", marginBottom: 8, lineHeight: 1.5 } },
|
|
583
|
+
"\u5728 Cloudflare Zero Trust \u63A7\u5236\u53F0\u521B\u5EFA Tunnel \u5373\u53EF\u83B7\u53D6\u4E13\u5C5E Token \u5E76\u7ED1\u5B9A\u81EA\u5DF1\u7684\u57DF\u540D\uFF08\u5982 dsh.yourname.com\uFF09\uFF0C\u6BCF\u6B21\u91CD\u542F URL \u6C38\u4E0D\u53D8\u66F4\u3002\u4E0D\u586B\u5219\u4F7F\u7528\u9ED8\u8BA4\u514D\u767B\u5F55\u4E34\u65F6\u968F\u673A\u57DF\u540D\u3002"
|
|
584
|
+
),
|
|
585
|
+
React.createElement(
|
|
586
|
+
"div",
|
|
587
|
+
{ style: { marginBottom: 8 } },
|
|
588
|
+
React.createElement("input", {
|
|
589
|
+
style: s.input,
|
|
590
|
+
type: "text",
|
|
591
|
+
placeholder: "\u81EA\u5B9A\u4E49\u56FA\u5B9A\u57DF\u540D (\u4F8B\u5982: dsh.yourdomain.com)",
|
|
592
|
+
value: hostnameVal,
|
|
593
|
+
onChange: (e) => setHostnameVal(e.target.value)
|
|
594
|
+
})
|
|
595
|
+
),
|
|
596
|
+
React.createElement(
|
|
597
|
+
"div",
|
|
598
|
+
{ style: { marginBottom: 8 } },
|
|
599
|
+
React.createElement("input", {
|
|
600
|
+
style: s.input,
|
|
601
|
+
type: "password",
|
|
602
|
+
placeholder: "Tunnel Token (\u4F8B\u5982: eyJhIjoi...)",
|
|
603
|
+
value: tokenVal,
|
|
604
|
+
onChange: (e) => setTokenVal(e.target.value)
|
|
605
|
+
})
|
|
606
|
+
),
|
|
607
|
+
React.createElement(
|
|
608
|
+
"div",
|
|
609
|
+
{ style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" } },
|
|
610
|
+
React.createElement("button", {
|
|
611
|
+
type: "submit",
|
|
612
|
+
style: { ...s.btnPri, height: 28, fontSize: 12, padding: "0 12px" },
|
|
613
|
+
disabled: saving
|
|
614
|
+
}, saving ? "\u4FDD\u5B58\u4E2D\u2026" : "\u4FDD\u5B58\u56FA\u5B9A\u57DF\u540D\u914D\u7F6E"),
|
|
615
|
+
(tokenVal || hostnameVal) && React.createElement("button", {
|
|
616
|
+
type: "button",
|
|
617
|
+
style: { ...s.btnGhost, height: 28, fontSize: 12, padding: "0 10px" },
|
|
618
|
+
onClick: () => {
|
|
619
|
+
setTokenVal("");
|
|
620
|
+
setHostnameVal("");
|
|
621
|
+
onSave({ token: "", hostname: "" });
|
|
622
|
+
}
|
|
623
|
+
}, "\u6E05\u9664"),
|
|
624
|
+
msg && React.createElement("span", {
|
|
625
|
+
style: { fontSize: 12, color: msg.ok ? "var(--dsw-alias-state-success-primary, #059669)" : "var(--dsw-alias-state-error-primary, #dc2626)" }
|
|
626
|
+
}, msg.text)
|
|
627
|
+
)
|
|
628
|
+
)
|
|
629
|
+
);
|
|
630
|
+
});
|
|
491
631
|
var AccessAuthCard = React.memo(function AccessAuthCard2({ auth, rpcCall, onUpdate }) {
|
|
492
632
|
const [enabled, setEnabled] = React.useState(auth?.enabled ?? false);
|
|
493
633
|
const [mode, setMode] = React.useState(auth?.mode ?? "token_and_password");
|
|
@@ -2031,8 +2171,20 @@ function BridgePanel({ rpcCall }) {
|
|
|
2031
2171
|
() => act(BRIDGE_ENDPOINTS.stopCloudflared).then(() => act(BRIDGE_ENDPOINTS.startCloudflared)),
|
|
2032
2172
|
[act]
|
|
2033
2173
|
);
|
|
2174
|
+
const onToggleCloudflaredAutoStart = React.useCallback(
|
|
2175
|
+
(autoStart) => act(BRIDGE_ENDPOINTS.setTunnelAutoStart, { tunnel: "cloudflared", autoStart }),
|
|
2176
|
+
[act]
|
|
2177
|
+
);
|
|
2178
|
+
const saveCloudflaredConfig = React.useCallback(
|
|
2179
|
+
({ token, hostname }) => act(BRIDGE_ENDPOINTS.saveCloudflaredConfig, { token, hostname }),
|
|
2180
|
+
[act]
|
|
2181
|
+
);
|
|
2034
2182
|
const onStartCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.startCustomTunnel), [act]);
|
|
2035
2183
|
const onStopCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.stopCustomTunnel), [act]);
|
|
2184
|
+
const onToggleCustomAutoStart = React.useCallback(
|
|
2185
|
+
(autoStart) => act(BRIDGE_ENDPOINTS.setTunnelAutoStart, { tunnel: "customTunnel", autoStart }),
|
|
2186
|
+
[act]
|
|
2187
|
+
);
|
|
2036
2188
|
const saveConfig = React.useCallback(
|
|
2037
2189
|
(serverUrl, accessToken) => act(BRIDGE_ENDPOINTS.saveCustomTunnelConfig, { serverUrl, accessToken }),
|
|
2038
2190
|
[act]
|
|
@@ -2066,21 +2218,31 @@ function BridgePanel({ rpcCall }) {
|
|
|
2066
2218
|
tabContent = React.createElement(
|
|
2067
2219
|
React.Fragment,
|
|
2068
2220
|
null,
|
|
2069
|
-
React.createElement(
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2221
|
+
React.createElement(
|
|
2222
|
+
TunnelCard,
|
|
2223
|
+
{
|
|
2224
|
+
title: "Cloudflare \u96A7\u9053",
|
|
2225
|
+
desc: status?.cloudflared?.tokenConfigured ? "\u56FA\u5B9A\u57DF\u540D\u6A21\u5F0F\uFF08Token \u8FD0\u884C \xB7 \u91CD\u542F URL \u4FDD\u6301\u4E0D\u53D8\uFF09" : "\u4E00\u952E\u83B7\u53D6\u516C\u7F51\u5730\u5740\uFF08\u514D\u767B\u5F55\u4E34\u65F6\u968F\u673A\u57DF\u540D\uFF09",
|
|
2226
|
+
data: {
|
|
2227
|
+
running: status?.cloudflared?.running,
|
|
2228
|
+
url: status?.cloudflared?.url,
|
|
2229
|
+
qr: status?.cloudflared?.qr,
|
|
2230
|
+
state: status?.cloudflared?.state
|
|
2231
|
+
},
|
|
2232
|
+
autoStart: status?.cloudflared?.autoStart,
|
|
2233
|
+
onToggleAutoStart: onToggleCloudflaredAutoStart,
|
|
2234
|
+
auth: status?.auth,
|
|
2235
|
+
onNavigateSecurity: navSecurity,
|
|
2236
|
+
onStart: onStartCloudflared,
|
|
2237
|
+
onStop: onStopCloudflared,
|
|
2238
|
+
onReset: status?.cloudflared?.running ? onResetCloudflared : null
|
|
2077
2239
|
},
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2240
|
+
React.createElement(CloudflareConfigForm, {
|
|
2241
|
+
token: status?.cloudflared?.token ?? "",
|
|
2242
|
+
hostname: status?.cloudflared?.hostname ?? "",
|
|
2243
|
+
onSave: saveCloudflaredConfig
|
|
2244
|
+
})
|
|
2245
|
+
),
|
|
2084
2246
|
React.createElement(
|
|
2085
2247
|
TunnelCard,
|
|
2086
2248
|
{
|
|
@@ -2093,6 +2255,8 @@ function BridgePanel({ rpcCall }) {
|
|
|
2093
2255
|
qr: ct?.qr,
|
|
2094
2256
|
state: ct?.state
|
|
2095
2257
|
},
|
|
2258
|
+
autoStart: ct?.autoStart,
|
|
2259
|
+
onToggleAutoStart: onToggleCustomAutoStart,
|
|
2096
2260
|
auth: status?.auth,
|
|
2097
2261
|
onNavigateSecurity: navSecurity,
|
|
2098
2262
|
onStart: onStartCustom,
|
package/client/index.js
CHANGED
|
@@ -351,7 +351,19 @@ const CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm({ serv
|
|
|
351
351
|
);
|
|
352
352
|
});
|
|
353
353
|
|
|
354
|
-
const TunnelCard = React.memo(function TunnelCard({
|
|
354
|
+
const TunnelCard = React.memo(function TunnelCard({
|
|
355
|
+
title,
|
|
356
|
+
desc,
|
|
357
|
+
data,
|
|
358
|
+
autoStart,
|
|
359
|
+
onToggleAutoStart,
|
|
360
|
+
onStart,
|
|
361
|
+
onStop,
|
|
362
|
+
onReset,
|
|
363
|
+
auth,
|
|
364
|
+
onNavigateSecurity,
|
|
365
|
+
children
|
|
366
|
+
}) {
|
|
355
367
|
const { running, configured, url, qr, state } = data ?? {};
|
|
356
368
|
const phase = state?.phase ?? 'idle';
|
|
357
369
|
|
|
@@ -361,7 +373,28 @@ const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart,
|
|
|
361
373
|
React.createElement('div', { style: s.label }, title),
|
|
362
374
|
React.createElement('div', { style: { ...s.muted, marginTop: 2 } }, desc),
|
|
363
375
|
),
|
|
364
|
-
React.createElement(
|
|
376
|
+
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 6 } },
|
|
377
|
+
React.createElement(StatusTag, { running }),
|
|
378
|
+
onToggleAutoStart && React.createElement('label', {
|
|
379
|
+
style: {
|
|
380
|
+
display: 'flex',
|
|
381
|
+
alignItems: 'center',
|
|
382
|
+
gap: 4,
|
|
383
|
+
fontSize: 11,
|
|
384
|
+
color: 'var(--dsw-alias-label-secondary,#6b7280)',
|
|
385
|
+
cursor: 'pointer',
|
|
386
|
+
userSelect: 'none',
|
|
387
|
+
},
|
|
388
|
+
title: 'DSH 启动时自动恢复该隧道的运行状态',
|
|
389
|
+
},
|
|
390
|
+
React.createElement('input', {
|
|
391
|
+
type: 'checkbox',
|
|
392
|
+
checked: Boolean(autoStart),
|
|
393
|
+
onChange: (e) => onToggleAutoStart(e.target.checked),
|
|
394
|
+
}),
|
|
395
|
+
'随 DSH 启动自动开启',
|
|
396
|
+
),
|
|
397
|
+
),
|
|
365
398
|
),
|
|
366
399
|
children,
|
|
367
400
|
phase !== 'idle' && phase !== 'ready' && React.createElement('div', {
|
|
@@ -385,6 +418,96 @@ const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart,
|
|
|
385
418
|
);
|
|
386
419
|
});
|
|
387
420
|
|
|
421
|
+
// Cloudflare 命名隧道(固定域名 / Token)高级配置表单
|
|
422
|
+
const CloudflareConfigForm = React.memo(function CloudflareConfigForm({ token, hostname, onSave }) {
|
|
423
|
+
const [open, setOpen] = React.useState(Boolean(token || hostname));
|
|
424
|
+
const [tokenVal, setTokenVal] = React.useState(token || '');
|
|
425
|
+
const [hostnameVal, setHostnameVal] = React.useState(hostname || '');
|
|
426
|
+
const [saving, setSaving] = React.useState(false);
|
|
427
|
+
const [msg, setMsg] = React.useState(null);
|
|
428
|
+
|
|
429
|
+
React.useEffect(() => {
|
|
430
|
+
setTokenVal(token || '');
|
|
431
|
+
setHostnameVal(hostname || '');
|
|
432
|
+
}, [token, hostname]);
|
|
433
|
+
|
|
434
|
+
const handleSave = async (e) => {
|
|
435
|
+
e.preventDefault();
|
|
436
|
+
setSaving(true);
|
|
437
|
+
setMsg(null);
|
|
438
|
+
try {
|
|
439
|
+
await onSave({ token: tokenVal, hostname: hostnameVal });
|
|
440
|
+
setMsg({ ok: true, text: '✓ 固定域名配置已保存' });
|
|
441
|
+
} catch (err) {
|
|
442
|
+
setMsg({ ok: false, text: err.message || '保存失败' });
|
|
443
|
+
} finally {
|
|
444
|
+
setSaving(false);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
return React.createElement('div', {
|
|
449
|
+
style: {
|
|
450
|
+
...s.block,
|
|
451
|
+
borderTop: '1px solid var(--dsw-alias-border-secondary, #e5e7eb)',
|
|
452
|
+
paddingTop: 10,
|
|
453
|
+
marginTop: 10,
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
React.createElement('div', {
|
|
457
|
+
style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'pointer', userSelect: 'none' },
|
|
458
|
+
onClick: () => setOpen(v => !v),
|
|
459
|
+
},
|
|
460
|
+
React.createElement('div', { style: { fontSize: 12, fontWeight: 500, color: 'var(--dsw-alias-brand-primary, #3b82f6)' } },
|
|
461
|
+
'⚙️ 高级配置:固定域名 (Cloudflare Token) ',
|
|
462
|
+
(token || hostname) && React.createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-state-success-primary, #059669)', fontWeight: 400 } }, '● 已配置固定域名')
|
|
463
|
+
),
|
|
464
|
+
React.createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary, #9ca3af)' } }, open ? '▴ 折叠' : '▾ 展开'),
|
|
465
|
+
),
|
|
466
|
+
open && React.createElement('form', { onSubmit: handleSave, style: { marginTop: 10 } },
|
|
467
|
+
React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary, #6b7280)', marginBottom: 8, lineHeight: 1.5 } },
|
|
468
|
+
'在 Cloudflare Zero Trust 控制台创建 Tunnel 即可获取专属 Token 并绑定自己的域名(如 dsh.yourname.com),每次重启 URL 永不变更。不填则使用默认免登录临时随机域名。'
|
|
469
|
+
),
|
|
470
|
+
React.createElement('div', { style: { marginBottom: 8 } },
|
|
471
|
+
React.createElement('input', {
|
|
472
|
+
style: s.input,
|
|
473
|
+
type: 'text',
|
|
474
|
+
placeholder: '自定义固定域名 (例如: dsh.yourdomain.com)',
|
|
475
|
+
value: hostnameVal,
|
|
476
|
+
onChange: (e) => setHostnameVal(e.target.value),
|
|
477
|
+
}),
|
|
478
|
+
),
|
|
479
|
+
React.createElement('div', { style: { marginBottom: 8 } },
|
|
480
|
+
React.createElement('input', {
|
|
481
|
+
style: s.input,
|
|
482
|
+
type: 'password',
|
|
483
|
+
placeholder: 'Tunnel Token (例如: eyJhIjoi...)',
|
|
484
|
+
value: tokenVal,
|
|
485
|
+
onChange: (e) => setTokenVal(e.target.value),
|
|
486
|
+
}),
|
|
487
|
+
),
|
|
488
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
|
|
489
|
+
React.createElement('button', {
|
|
490
|
+
type: 'submit',
|
|
491
|
+
style: { ...s.btnPri, height: 28, fontSize: 12, padding: '0 12px' },
|
|
492
|
+
disabled: saving,
|
|
493
|
+
}, saving ? '保存中…' : '保存固定域名配置'),
|
|
494
|
+
(tokenVal || hostnameVal) && React.createElement('button', {
|
|
495
|
+
type: 'button',
|
|
496
|
+
style: { ...s.btnGhost, height: 28, fontSize: 12, padding: '0 10px' },
|
|
497
|
+
onClick: () => {
|
|
498
|
+
setTokenVal('');
|
|
499
|
+
setHostnameVal('');
|
|
500
|
+
onSave({ token: '', hostname: '' });
|
|
501
|
+
},
|
|
502
|
+
}, '清除'),
|
|
503
|
+
msg && React.createElement('span', {
|
|
504
|
+
style: { fontSize: 12, color: msg.ok ? 'var(--dsw-alias-state-success-primary, #059669)' : 'var(--dsw-alias-state-error-primary, #dc2626)' },
|
|
505
|
+
}, msg.text),
|
|
506
|
+
),
|
|
507
|
+
),
|
|
508
|
+
);
|
|
509
|
+
});
|
|
510
|
+
|
|
388
511
|
// ---- 访问安全认证卡片 ----
|
|
389
512
|
|
|
390
513
|
const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpdate }) {
|
|
@@ -1824,8 +1947,18 @@ function BridgePanel({ rpcCall }) {
|
|
|
1824
1947
|
const onResetCloudflared = React.useCallback(() =>
|
|
1825
1948
|
act(BRIDGE_ENDPOINTS.stopCloudflared).then(() => act(BRIDGE_ENDPOINTS.startCloudflared))
|
|
1826
1949
|
, [act]);
|
|
1950
|
+
const onToggleCloudflaredAutoStart = React.useCallback((autoStart) =>
|
|
1951
|
+
act(BRIDGE_ENDPOINTS.setTunnelAutoStart, { tunnel: 'cloudflared', autoStart })
|
|
1952
|
+
, [act]);
|
|
1953
|
+
const saveCloudflaredConfig = React.useCallback(({ token, hostname }) =>
|
|
1954
|
+
act(BRIDGE_ENDPOINTS.saveCloudflaredConfig, { token, hostname })
|
|
1955
|
+
, [act]);
|
|
1956
|
+
|
|
1827
1957
|
const onStartCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.startCustomTunnel), [act]);
|
|
1828
1958
|
const onStopCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.stopCustomTunnel), [act]);
|
|
1959
|
+
const onToggleCustomAutoStart = React.useCallback((autoStart) =>
|
|
1960
|
+
act(BRIDGE_ENDPOINTS.setTunnelAutoStart, { tunnel: 'customTunnel', autoStart })
|
|
1961
|
+
, [act]);
|
|
1829
1962
|
const saveConfig = React.useCallback((serverUrl, accessToken) =>
|
|
1830
1963
|
act(BRIDGE_ENDPOINTS.saveCustomTunnelConfig, { serverUrl, accessToken })
|
|
1831
1964
|
, [act]);
|
|
@@ -1865,19 +1998,29 @@ function BridgePanel({ rpcCall }) {
|
|
|
1865
1998
|
tabContent = React.createElement(React.Fragment, null,
|
|
1866
1999
|
React.createElement(TunnelCard, {
|
|
1867
2000
|
title: 'Cloudflare 隧道',
|
|
1868
|
-
desc:
|
|
2001
|
+
desc: status?.cloudflared?.tokenConfigured
|
|
2002
|
+
? '固定域名模式(Token 运行 · 重启 URL 保持不变)'
|
|
2003
|
+
: '一键获取公网地址(免登录临时随机域名)',
|
|
1869
2004
|
data: {
|
|
1870
2005
|
running: status?.cloudflared?.running,
|
|
1871
2006
|
url: status?.cloudflared?.url,
|
|
1872
2007
|
qr: status?.cloudflared?.qr,
|
|
1873
2008
|
state: status?.cloudflared?.state,
|
|
1874
2009
|
},
|
|
2010
|
+
autoStart: status?.cloudflared?.autoStart,
|
|
2011
|
+
onToggleAutoStart: onToggleCloudflaredAutoStart,
|
|
1875
2012
|
auth: status?.auth,
|
|
1876
2013
|
onNavigateSecurity: navSecurity,
|
|
1877
2014
|
onStart: onStartCloudflared,
|
|
1878
2015
|
onStop: onStopCloudflared,
|
|
1879
2016
|
onReset: status?.cloudflared?.running ? onResetCloudflared : null,
|
|
1880
|
-
}
|
|
2017
|
+
},
|
|
2018
|
+
React.createElement(CloudflareConfigForm, {
|
|
2019
|
+
token: status?.cloudflared?.token ?? '',
|
|
2020
|
+
hostname: status?.cloudflared?.hostname ?? '',
|
|
2021
|
+
onSave: saveCloudflaredConfig,
|
|
2022
|
+
}),
|
|
2023
|
+
),
|
|
1881
2024
|
React.createElement(TunnelCard, {
|
|
1882
2025
|
title: '自建隧道',
|
|
1883
2026
|
desc: '连接自己部署的隧道服务器,获得固定域名',
|
|
@@ -1888,6 +2031,8 @@ function BridgePanel({ rpcCall }) {
|
|
|
1888
2031
|
qr: ct?.qr,
|
|
1889
2032
|
state: ct?.state,
|
|
1890
2033
|
},
|
|
2034
|
+
autoStart: ct?.autoStart,
|
|
2035
|
+
onToggleAutoStart: onToggleCustomAutoStart,
|
|
1891
2036
|
auth: status?.auth,
|
|
1892
2037
|
onNavigateSecurity: navSecurity,
|
|
1893
2038
|
onStart: onStartCustom,
|
package/docs/feishu-usage.md
CHANGED
|
@@ -96,3 +96,19 @@
|
|
|
96
96
|
|
|
97
97
|

|
|
98
98
|
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## ❓ 常见问题 (FAQ)
|
|
102
|
+
|
|
103
|
+
### Q1: 点击卡片上的「批准」/「拒绝」按钮提示无权限或无反应?
|
|
104
|
+
请依次核对以下三项飞书开放平台配置:
|
|
105
|
+
1. **是否订阅了卡片交互事件**:在开放平台后台 **「事件与回调」➔「事件配置」** 中,必须添加 **`card.action.trigger`**(消息卡片回传交互)事件。若只添加了接收消息事件,卡片点击不会下发回调。
|
|
106
|
+
2. **是否发布了新版本生效**:飞书平台的所有权限与事件变更,**必须在「版本管理与发布」中「创建版本」并申请发布通过后才会生效**。
|
|
107
|
+
3. **应用可用范围**:在「版本管理与发布」中,确认应用可用范围包含了您当前的飞书账号(建议设为「所有员工」或将自己加入可用成员)。
|
|
108
|
+
4. **快速应急处理**:如果卡片按钮暂时受网络或配置影响,可直接在聊天中回复文字 **`/yes`**(或 `1`)批准,回复 **`/no`**(或 `2`)拒绝。
|
|
109
|
+
|
|
110
|
+
### Q2: 机器人无法在群聊中回复消息?
|
|
111
|
+
1. 确保在「权限管理」中开通了 **`im:message.group_msg`**(获取群组中所有消息)权限;
|
|
112
|
+
2. 确保在「版本管理与发布」中发布了包含该权限的新版本;
|
|
113
|
+
3. 将机器人拉入群聊后,需要 **`@机器人`** 唤醒并发送指令。
|
|
114
|
+
|
|
@@ -9,6 +9,8 @@ export const BRIDGE_ENDPOINTS = {
|
|
|
9
9
|
startCloudflared: 'startCloudflared',
|
|
10
10
|
stopCloudflared: 'stopCloudflared',
|
|
11
11
|
resetCloudflared: 'resetCloudflared',
|
|
12
|
+
saveCloudflaredConfig: 'saveCloudflaredConfig',
|
|
13
|
+
setTunnelAutoStart: 'setTunnelAutoStart',
|
|
12
14
|
saveCustomTunnelConfig: 'saveCustomTunnelConfig',
|
|
13
15
|
checkVersion: 'checkVersion',
|
|
14
16
|
upgradePlugin: 'upgradePlugin',
|
package/lib/bridge-rpc.js
CHANGED
|
@@ -144,6 +144,26 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
144
144
|
return ok(status);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
if (endpoint === BRIDGE_ENDPOINTS.saveCloudflaredConfig) {
|
|
148
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
149
|
+
if (adminErr) return adminErr;
|
|
150
|
+
|
|
151
|
+
const { token = '', hostname = '' } = payload;
|
|
152
|
+
await service.saveCloudflaredConfig({ token, hostname });
|
|
153
|
+
const status = await service.getStatus();
|
|
154
|
+
return ok(status);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (endpoint === BRIDGE_ENDPOINTS.setTunnelAutoStart) {
|
|
158
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
159
|
+
if (adminErr) return adminErr;
|
|
160
|
+
|
|
161
|
+
const { tunnel, autoStart } = payload;
|
|
162
|
+
await service.setTunnelAutoStart({ tunnel, autoStart });
|
|
163
|
+
const status = await service.getStatus();
|
|
164
|
+
return ok(status);
|
|
165
|
+
}
|
|
166
|
+
|
|
147
167
|
if (endpoint === BRIDGE_ENDPOINTS.startCustomTunnel) {
|
|
148
168
|
const adminErr = checkAdminAuth(authManager, payload);
|
|
149
169
|
if (adminErr) return adminErr;
|
|
@@ -74,9 +74,11 @@ async function downloadFile(url, dest, onProgress) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export class CloudflaredManager {
|
|
77
|
-
constructor({ port, home, onStateChange, logger }) {
|
|
77
|
+
constructor({ port, home, token, hostname, onStateChange, logger }) {
|
|
78
78
|
this.port = port;
|
|
79
79
|
this.home = home || join(homedir(), '.dsh-bridge');
|
|
80
|
+
this.token = token ? String(token).trim() : null;
|
|
81
|
+
this.hostname = hostname ? String(hostname).trim() : null;
|
|
80
82
|
this.onStateChange = onStateChange;
|
|
81
83
|
this.logger = logger;
|
|
82
84
|
|
|
@@ -152,8 +154,13 @@ export class CloudflaredManager {
|
|
|
152
154
|
|
|
153
155
|
this._setState('connecting', '正在连接 Cloudflare...');
|
|
154
156
|
|
|
155
|
-
const args =
|
|
156
|
-
|
|
157
|
+
const args = this.token
|
|
158
|
+
? ['tunnel', 'run', '--token', this.token]
|
|
159
|
+
: ['tunnel', '--url', `http://127.0.0.1:${this.port}`];
|
|
160
|
+
|
|
161
|
+
// 隐藏日志中的 token 敏感字段
|
|
162
|
+
const safeArgs = this.token ? ['tunnel', 'run', '--token', '***'] : args;
|
|
163
|
+
this.logger?.info('启动 cloudflared: %s %s', this.binaryPath, safeArgs.join(' '));
|
|
157
164
|
|
|
158
165
|
this.process = spawn(this.binaryPath, args, {
|
|
159
166
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -168,13 +175,38 @@ export class CloudflaredManager {
|
|
|
168
175
|
}
|
|
169
176
|
};
|
|
170
177
|
|
|
171
|
-
//
|
|
178
|
+
// 1. 命名/Token 隧道:通过握手日志判定就绪,使用预设固定域名
|
|
179
|
+
const parseNamedTunnel = (text) => {
|
|
180
|
+
if (!this.token) return;
|
|
181
|
+
if (
|
|
182
|
+
(text.includes('Registered tunnel') ||
|
|
183
|
+
text.includes('registered connIndex') ||
|
|
184
|
+
text.includes('Connection') && text.includes('registered') ||
|
|
185
|
+
text.includes('Updated to new configuration') ||
|
|
186
|
+
text.includes('Route propagated')) &&
|
|
187
|
+
!resolved
|
|
188
|
+
) {
|
|
189
|
+
let fixedUrl = this.hostname
|
|
190
|
+
? (this.hostname.startsWith('http') ? this.hostname : `https://${this.hostname}`)
|
|
191
|
+
: null;
|
|
192
|
+
this.url = fixedUrl;
|
|
193
|
+
this._setState('ready', fixedUrl ? `固定隧道已建立 (${fixedUrl})` : '固定隧道已建立');
|
|
194
|
+
this.logger?.info('cloudflared 固定隧道就绪: %s', this.url || 'Token 模式');
|
|
195
|
+
tryResolve();
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// 2. 免费临时隧道:从 stdout/stderr 解析随机分配的 trycloudflare.com 域名
|
|
172
200
|
const parseUrl = (text) => {
|
|
201
|
+
if (this.token) {
|
|
202
|
+
parseNamedTunnel(text);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
173
205
|
const match = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
174
206
|
if (match && !resolved) {
|
|
175
207
|
this.url = match[0];
|
|
176
|
-
this._setState('ready', '
|
|
177
|
-
this.logger?.info('cloudflared
|
|
208
|
+
this._setState('ready', '临时隧道已建立');
|
|
209
|
+
this.logger?.info('cloudflared 临时隧道就绪: %s', this.url);
|
|
178
210
|
tryResolve();
|
|
179
211
|
}
|
|
180
212
|
};
|
|
@@ -185,7 +217,7 @@ export class CloudflaredManager {
|
|
|
185
217
|
this.logger?.debug('cloudflared: %s', text.trim());
|
|
186
218
|
parseUrl(text);
|
|
187
219
|
if (text.includes('Registered tunnel') && !resolved) {
|
|
188
|
-
this._setState('connecting', '
|
|
220
|
+
this._setState('connecting', '隧道已注册,等待就绪...');
|
|
189
221
|
}
|
|
190
222
|
});
|
|
191
223
|
|
package/lib/index.js
CHANGED
|
@@ -452,12 +452,14 @@ class ProxyServer {
|
|
|
452
452
|
* Bridge Service
|
|
453
453
|
*/
|
|
454
454
|
class BridgeService {
|
|
455
|
-
constructor({ dshPort, proxyPort, home, customTunnelConfig, authManager, logger }) {
|
|
455
|
+
constructor({ dshPort, proxyPort, home, cloudflaredConfig, customTunnelConfig, authManager, onPersist, logger }) {
|
|
456
456
|
this.dshPort = dshPort;
|
|
457
457
|
this.proxyPort = proxyPort;
|
|
458
458
|
this.home = home;
|
|
459
|
+
this.cloudflaredConfig = cloudflaredConfig ?? { token: '', hostname: '', autoStart: false };
|
|
459
460
|
this.customTunnelConfig = customTunnelConfig ?? null;
|
|
460
461
|
this.authManager = authManager ?? null;
|
|
462
|
+
this.onPersist = onPersist ?? null;
|
|
461
463
|
this.logger = logger;
|
|
462
464
|
|
|
463
465
|
this.qrCache = new QrCache();
|
|
@@ -539,6 +541,10 @@ class BridgeService {
|
|
|
539
541
|
? await this.qrCache.get(cloudflaredUrl)
|
|
540
542
|
: null,
|
|
541
543
|
state: this.cloudflaredState,
|
|
544
|
+
tokenConfigured: !!this.cloudflaredConfig?.token,
|
|
545
|
+
token: adminAuthValid ? (this.cloudflaredConfig?.token || '') : (this.cloudflaredConfig?.token ? '******' : ''),
|
|
546
|
+
hostname: this.cloudflaredConfig?.hostname || '',
|
|
547
|
+
autoStart: Boolean(this.cloudflaredConfig?.autoStart),
|
|
542
548
|
},
|
|
543
549
|
|
|
544
550
|
customTunnel: {
|
|
@@ -551,6 +557,7 @@ class BridgeService {
|
|
|
551
557
|
? await this.qrCache.get(customUrl)
|
|
552
558
|
: null,
|
|
553
559
|
state: this.customTunnelState,
|
|
560
|
+
autoStart: Boolean(this.customTunnelConfig?.autoStart),
|
|
554
561
|
},
|
|
555
562
|
|
|
556
563
|
// 轻量摘要,供 UI Tab 状态点使用(完整状态由 wechatGetStatus 提供)
|
|
@@ -558,7 +565,33 @@ class BridgeService {
|
|
|
558
565
|
};
|
|
559
566
|
}
|
|
560
567
|
|
|
561
|
-
async
|
|
568
|
+
async saveCloudflaredConfig({ token, hostname }) {
|
|
569
|
+
this.cloudflaredConfig = {
|
|
570
|
+
...(this.cloudflaredConfig ?? {}),
|
|
571
|
+
token: token ? String(token).trim() : '',
|
|
572
|
+
hostname: hostname ? String(hostname).trim() : '',
|
|
573
|
+
};
|
|
574
|
+
await this.onPersist?.({ cloudflared: this.cloudflaredConfig });
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async setTunnelAutoStart({ tunnel, autoStart }) {
|
|
578
|
+
const isAuto = Boolean(autoStart);
|
|
579
|
+
if (tunnel === 'cloudflared') {
|
|
580
|
+
this.cloudflaredConfig = {
|
|
581
|
+
...(this.cloudflaredConfig ?? {}),
|
|
582
|
+
autoStart: isAuto,
|
|
583
|
+
};
|
|
584
|
+
await this.onPersist?.({ cloudflared: this.cloudflaredConfig });
|
|
585
|
+
} else if (tunnel === 'customTunnel' || tunnel === 'custom') {
|
|
586
|
+
this.customTunnelConfig = {
|
|
587
|
+
...(this.customTunnelConfig ?? {}),
|
|
588
|
+
autoStart: isAuto,
|
|
589
|
+
};
|
|
590
|
+
await this.onPersist?.({ customTunnel: this.customTunnelConfig });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async startCustomTunnel({ autoStart = true } = {}) {
|
|
562
595
|
if (this.customTunnel) {
|
|
563
596
|
throw new Error('自建隧道已在运行');
|
|
564
597
|
}
|
|
@@ -567,9 +600,15 @@ class BridgeService {
|
|
|
567
600
|
const accessToken = this.customTunnelConfig?.accessToken;
|
|
568
601
|
|
|
569
602
|
if (!serverUrl || !accessToken) {
|
|
570
|
-
throw new Error('
|
|
603
|
+
throw new Error('缺少配置:请在控制台配置 customTunnel.serverUrl 和 customTunnel.accessToken');
|
|
571
604
|
}
|
|
572
605
|
|
|
606
|
+
this.customTunnelConfig = {
|
|
607
|
+
...(this.customTunnelConfig ?? {}),
|
|
608
|
+
autoStart: Boolean(autoStart),
|
|
609
|
+
};
|
|
610
|
+
await this.onPersist?.({ customTunnel: this.customTunnelConfig });
|
|
611
|
+
|
|
573
612
|
this.customTunnel = new CustomTunnelClient({
|
|
574
613
|
serverUrl,
|
|
575
614
|
accessToken,
|
|
@@ -584,23 +623,36 @@ class BridgeService {
|
|
|
584
623
|
await this.customTunnel.connect();
|
|
585
624
|
}
|
|
586
625
|
|
|
587
|
-
stopCustomTunnel() {
|
|
626
|
+
async stopCustomTunnel() {
|
|
588
627
|
if (this.customTunnel) {
|
|
589
628
|
this.customTunnel.disconnect();
|
|
590
629
|
this.customTunnel = null;
|
|
591
630
|
this.customTunnelState = { phase: 'idle', detail: '' };
|
|
592
631
|
}
|
|
632
|
+
this.customTunnelConfig = {
|
|
633
|
+
...(this.customTunnelConfig ?? {}),
|
|
634
|
+
autoStart: false,
|
|
635
|
+
};
|
|
636
|
+
await this.onPersist?.({ customTunnel: this.customTunnelConfig });
|
|
593
637
|
}
|
|
594
638
|
|
|
595
|
-
async startCloudflared() {
|
|
639
|
+
async startCloudflared({ autoStart = true } = {}) {
|
|
596
640
|
if (this.cloudflared) {
|
|
597
641
|
throw new Error('Cloudflare 隧道已在运行');
|
|
598
642
|
}
|
|
599
643
|
|
|
644
|
+
this.cloudflaredConfig = {
|
|
645
|
+
...(this.cloudflaredConfig ?? {}),
|
|
646
|
+
autoStart: Boolean(autoStart),
|
|
647
|
+
};
|
|
648
|
+
await this.onPersist?.({ cloudflared: this.cloudflaredConfig });
|
|
649
|
+
|
|
600
650
|
this.cloudflaredState = { phase: 'connecting', detail: '正在初始化...' };
|
|
601
651
|
this.cloudflared = new CloudflaredManager({
|
|
602
652
|
port: this.proxyPort,
|
|
603
653
|
home: this.home,
|
|
654
|
+
token: this.cloudflaredConfig?.token,
|
|
655
|
+
hostname: this.cloudflaredConfig?.hostname,
|
|
604
656
|
onStateChange: (state) => {
|
|
605
657
|
this.cloudflaredState = state;
|
|
606
658
|
// 出错时自动清理,让用户可以重新开启
|
|
@@ -615,17 +667,22 @@ class BridgeService {
|
|
|
615
667
|
this.cloudflared.start();
|
|
616
668
|
}
|
|
617
669
|
|
|
618
|
-
stopCloudflared() {
|
|
670
|
+
async stopCloudflared() {
|
|
619
671
|
if (this.cloudflared) {
|
|
620
672
|
this.cloudflared.stop();
|
|
621
673
|
this.cloudflared = null;
|
|
622
674
|
this.cloudflaredState = { phase: 'idle', detail: '' };
|
|
623
675
|
}
|
|
676
|
+
this.cloudflaredConfig = {
|
|
677
|
+
...(this.cloudflaredConfig ?? {}),
|
|
678
|
+
autoStart: false,
|
|
679
|
+
};
|
|
680
|
+
await this.onPersist?.({ cloudflared: this.cloudflaredConfig });
|
|
624
681
|
}
|
|
625
682
|
|
|
626
683
|
// 重置 Cloudflare 隧道:关闭隧道 + 删除已下载的 cloudflared 二进制
|
|
627
684
|
async resetCloudflared() {
|
|
628
|
-
this.stopCloudflared();
|
|
685
|
+
await this.stopCloudflared();
|
|
629
686
|
const binDir = join(this.home ?? join(homedir(), '.dsh-bridge'), 'bin');
|
|
630
687
|
const candidates = ['cloudflared.exe', 'cloudflared'];
|
|
631
688
|
for (const name of candidates) {
|
|
@@ -835,15 +892,38 @@ function apply(ctx, config = {}) {
|
|
|
835
892
|
proxyPort,
|
|
836
893
|
home: config.home,
|
|
837
894
|
customTunnelConfig: config.customTunnel ?? null,
|
|
895
|
+
cloudflaredConfig: config.cloudflared ?? null,
|
|
838
896
|
authManager,
|
|
897
|
+
onPersist: async (patch) => {
|
|
898
|
+
const stored = await loadConfig();
|
|
899
|
+
Object.assign(stored, patch);
|
|
900
|
+
await saveConfig(stored);
|
|
901
|
+
},
|
|
839
902
|
logger,
|
|
840
903
|
});
|
|
841
904
|
|
|
842
|
-
//
|
|
843
|
-
loadConfig().then((stored) => {
|
|
844
|
-
if (stored?.
|
|
905
|
+
// 启动时读取已保存的公网隧道配置并按需自动拉起
|
|
906
|
+
loadConfig().then(async (stored) => {
|
|
907
|
+
if (stored?.cloudflared) {
|
|
908
|
+
service.cloudflaredConfig = stored.cloudflared;
|
|
909
|
+
logger.info('dsh-bridge: loaded saved cloudflared config (autoStart=%s, tokenConfigured=%s)', Boolean(service.cloudflaredConfig.autoStart), Boolean(service.cloudflaredConfig.token));
|
|
910
|
+
if (service.cloudflaredConfig.autoStart) {
|
|
911
|
+
logger.info('dsh-bridge: auto-starting cloudflared tunnel...');
|
|
912
|
+
service.startCloudflared({ autoStart: true }).catch((err) => {
|
|
913
|
+
logger.error('dsh-bridge: cloudflared auto-start failed: %s', err?.message ?? err);
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (stored?.customTunnel) {
|
|
845
919
|
service.customTunnelConfig = stored.customTunnel;
|
|
846
|
-
logger.info('dsh-bridge: loaded saved custom tunnel config');
|
|
920
|
+
logger.info('dsh-bridge: loaded saved custom tunnel config (autoStart=%s)', Boolean(service.customTunnelConfig.autoStart));
|
|
921
|
+
if (service.customTunnelConfig.autoStart && service.customTunnelConfig.serverUrl) {
|
|
922
|
+
logger.info('dsh-bridge: auto-starting custom tunnel...');
|
|
923
|
+
service.startCustomTunnel({ autoStart: true }).catch((err) => {
|
|
924
|
+
logger.error('dsh-bridge: custom tunnel auto-start failed: %s', err?.message ?? err);
|
|
925
|
+
});
|
|
926
|
+
}
|
|
847
927
|
}
|
|
848
928
|
}).catch(() => {});
|
|
849
929
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenbin_wb/dsh-bridge",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "手机扫码即可在移动端/公网继续用 DeepSeek Harness,人不在电脑前也能接着干。一键局域网二维码、Cloudflare 公网隧道、自建隧道与微信 / QQ / 飞书 / Telegram Bot(多工作区/会话持久化/媒体/卡片审批/流式输出),无需自己搭公网服务器。",
|
|
5
|
-
"releaseNotes": "【v2.6.
|
|
5
|
+
"releaseNotes": "【v2.6.1 公网隧道自启与固定域名】\n• 🔄 支持公网隧道(Cloudflare/自建)开机自启与重启状态记忆\n• 🌐 支持 Cloudflare 命名隧道(Token 模式),永久固定专属域名永不变更\n• ⚙️ 控制台新增「随 DSH 启动自动开启」开关与高级 Token 配置",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
|
8
8
|
"exports": {
|