@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 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.0)
108
- dsh plugin --profile web add @wenbin_wb/dsh-bridge@2.6.0
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="32%" alt="Mobile New Session Home" />
216
- <img src="docs/screenshots/mobile-chat.jpg" width="32%" alt="Mobile Active Chat View" />
217
- <img src="docs/screenshots/mobile-drawer.jpg" width="32%" alt="Mobile Native Sidebar Drawer" />
215
+ <img src="docs/screenshots/remote-web-mobile.jpg" width="29%" alt="Mobile New Session Home" />
216
+ &nbsp;&nbsp;&nbsp;
217
+ <img src="docs/screenshots/mobile-chat.jpg" width="29%" alt="Mobile Active Chat View" />
218
+ &nbsp;&nbsp;&nbsp;
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="24%" alt="LAN Access Console" />
224
- <img src="docs/screenshots/mobile-settings-tunnel.jpg" width="24%" alt="Public Tunnel Settings" />
225
- <img src="docs/screenshots/mobile-settings-im.jpg" width="24%" alt="IM Bot Platforms" />
226
- <img src="docs/screenshots/mobile-settings-security.jpg" width="24%" alt="Global Access Security" />
225
+ <img src="docs/screenshots/mobile-settings-lan.jpg" width="22%" alt="LAN Access Console" />
226
+ &nbsp;&nbsp;
227
+ <img src="docs/screenshots/mobile-settings-tunnel.jpg" width="22%" alt="Public Tunnel Settings" />
228
+ &nbsp;&nbsp;
229
+ <img src="docs/screenshots/mobile-settings-im.jpg" width="22%" alt="IM Bot Platforms" />
230
+ &nbsp;&nbsp;
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
- 1. Click the "Start" button in the "Cloudflare Tunnel" card
240
- 2. On first use, cloudflared (~30MB) will be automatically downloaded from GitHub
241
- 3. Once downloaded, it starts automatically and displays a public URL and QR code within seconds
242
- 4. The URL changes on each restart; click "Reset URL" to request a new URL
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
  ![Public Tunnel Settings](docs/screenshots/tunnel-access.jpg)
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.0
108
- dsh plugin --profile web add @wenbin_wb/dsh-bridge@2.6.0
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="32%" alt="移动端新会话主页" />
216
- <img src="docs/screenshots/mobile-chat.jpg" width="32%" alt="移动端已有对话交互" />
217
- <img src="docs/screenshots/mobile-drawer.jpg" width="32%" alt="移动端原生抽屉侧边栏" />
215
+ <img src="docs/screenshots/remote-web-mobile.jpg" width="29%" alt="移动端新会话主页" />
216
+ &nbsp;&nbsp;&nbsp;
217
+ <img src="docs/screenshots/mobile-chat.jpg" width="29%" alt="移动端已有对话交互" />
218
+ &nbsp;&nbsp;&nbsp;
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="24%" alt="局域网访问控制台" />
224
- <img src="docs/screenshots/mobile-settings-tunnel.jpg" width="24%" alt="公网隧道配置" />
225
- <img src="docs/screenshots/mobile-settings-im.jpg" width="24%" alt="IM 机器人矩阵" />
226
- <img src="docs/screenshots/mobile-settings-security.jpg" width="24%" alt="全局访问安全认证" />
225
+ <img src="docs/screenshots/mobile-settings-lan.jpg" width="22%" alt="局域网访问控制台" />
226
+ &nbsp;&nbsp;
227
+ <img src="docs/screenshots/mobile-settings-tunnel.jpg" width="22%" alt="公网隧道配置" />
228
+ &nbsp;&nbsp;
229
+ <img src="docs/screenshots/mobile-settings-im.jpg" width="22%" alt="IM 机器人矩阵" />
230
+ &nbsp;&nbsp;
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
- 1. 点击「Cloudflare 隧道」卡片中的「开启」按钮
240
- 2. 首次使用会自动从 GitHub 下载 cloudflared(约 30MB)
241
- 3. 下载完成后自动启动,几秒内显示公网 URL 和二维码
242
- 4. 每次重启后 URL 会变化;点「重置链接」可主动获取新 URL
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
  ![公网隧道配置](docs/screenshots/tunnel-access.jpg)
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({ title, desc, data, onStart, onStop, onReset, auth, onNavigateSecurity, children }) {
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(StatusTag, { running })
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(TunnelCard, {
2070
- title: "Cloudflare \u96A7\u9053",
2071
- desc: "\u4E00\u952E\u83B7\u53D6\u516C\u7F51\u5730\u5740\uFF08\u91CD\u542F\u540E URL \u4F1A\u53D8\u5316\uFF09",
2072
- data: {
2073
- running: status?.cloudflared?.running,
2074
- url: status?.cloudflared?.url,
2075
- qr: status?.cloudflared?.qr,
2076
- state: status?.cloudflared?.state
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
- auth: status?.auth,
2079
- onNavigateSecurity: navSecurity,
2080
- onStart: onStartCloudflared,
2081
- onStop: onStopCloudflared,
2082
- onReset: status?.cloudflared?.running ? onResetCloudflared : null
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({ title, desc, data, onStart, onStop, onReset, auth, onNavigateSecurity, children }) {
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(StatusTag, { running }),
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: '一键获取公网地址(重启后 URL 会变化)',
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,
@@ -96,3 +96,19 @@
96
96
 
97
97
  ![飞书对话与卡片审批](screenshots/feishu-chat.jpg)
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 = ['tunnel', '--url', `http://127.0.0.1:${this.port}`];
156
- this.logger?.info('启动 cloudflared: %s %s', this.binaryPath, args.join(' '));
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
- // URL 同时在 stdout 和 stderr 里找(不同版本行为不同)
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 隧道就绪: %s', this.url);
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', '隧道已注册,等待 URL...');
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 startCustomTunnel() {
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('缺少配置:请在 cordis.yml 中配置 customTunnel.serverUrl 和 customTunnel.accessToken');
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?.customTunnel?.serverUrl) {
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.0",
3
+ "version": "2.6.1",
4
4
  "description": "手机扫码即可在移动端/公网继续用 DeepSeek Harness,人不在电脑前也能接着干。一键局域网二维码、Cloudflare 公网隧道、自建隧道与微信 / QQ / 飞书 / Telegram Bot(多工作区/会话持久化/媒体/卡片审批/流式输出),无需自己搭公网服务器。",
5
- "releaseNotes": "【v2.6.0 移动端交互重构与长按菜单】\n• 📱 深度重构移动端触控交互,极简通透顶栏与大圆角输入胶囊\n• 🖐️ 侧边栏支持长按(Long Press)呼出操作菜单(重命名/分叉/归档),边缘滑动开闭抽屉\n• 🛠️ 彻底修复「运行中」等状态药丸徽标在手机屏幕下的换行问题\n• 优化版本检查与升级提示,直观展示新版本更新内容亮点",
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": {