@wenbin_wb/dsh-bridge 2.5.5 → 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.
@@ -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
+
Binary file
@@ -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
@@ -108,10 +108,52 @@ class QrCache {
108
108
  }
109
109
 
110
110
  /**
111
- * 非安全上下文(http://LAN-IP:端口)里浏览器没有 crypto.randomUUID,
112
- * DSH 连接层 mint RPC id 时会抛错,注入 polyfill 修复。
111
+ * PWA Web App Manifest 与 App 启动图标
113
112
  */
114
- const RANDOM_UUID_POLYFILL = `<script data-dsh-bridge-polyfill="1">!function(){try{if(self.crypto&&!self.crypto.randomUUID){self.crypto.randomUUID=function(){var b=new Uint8Array(16);self.crypto.getRandomValues(b);b[6]=b[6]&15|64;b[8]=b[8]&63|128;var h="";for(var i=0;i<16;i++){var x=b[i].toString(16);h+=(x.length<2?"0":"")+x;if(i===3||i===5||i===7||i===9)h+="-";}return h;}}}catch(e){}}();</script>`;
113
+ const PWA_MANIFEST = JSON.stringify({
114
+ name: 'DeepSeek Harness',
115
+ short_name: 'DSH',
116
+ description: 'DeepSeek Harness Remote & Mobile Workspace',
117
+ start_url: '/',
118
+ display: 'standalone',
119
+ background_color: '#181825',
120
+ theme_color: '#1e1e2e',
121
+ orientation: 'any',
122
+ icons: [
123
+ {
124
+ src: '/__dsh_bridge__/pwa-icon.svg',
125
+ sizes: 'any',
126
+ type: 'image/svg+xml',
127
+ purpose: 'any maskable'
128
+ }
129
+ ]
130
+ }, null, 2);
131
+
132
+ const PWA_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
133
+ <defs>
134
+ <linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
135
+ <stop offset="0%" stop-color="#4f6ef7"/>
136
+ <stop offset="100%" stop-color="#24388a"/>
137
+ </linearGradient>
138
+ </defs>
139
+ <rect width="512" height="512" rx="128" fill="url(#g)"/>
140
+ <path d="M150 170 C150 140, 362 140, 362 170 L362 330 C362 360, 150 360, 150 330 Z" fill="#ffffff" fill-opacity="0.12"/>
141
+ <circle cx="206" cy="220" r="28" fill="#ffffff"/>
142
+ <circle cx="306" cy="220" r="28" fill="#ffffff"/>
143
+ <path d="M200 290 Q256 340 312 290" stroke="#ffffff" stroke-width="24" stroke-linecap="round" fill="none"/>
144
+ <rect x="236" y="90" width="40" height="60" rx="10" fill="#ffffff"/>
145
+ <circle cx="256" cy="80" r="16" fill="#4f6ef7"/>
146
+ </svg>`;
147
+
148
+ const HTML_HEAD_INJECTIONS = `<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
149
+ <meta name="apple-mobile-web-app-capable" content="yes">
150
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
151
+ <meta name="apple-mobile-web-app-title" content="DSH">
152
+ <meta name="theme-color" content="#1e1e2e">
153
+ <link rel="manifest" href="/manifest.webmanifest">
154
+ <link rel="icon" type="image/svg+xml" href="/__dsh_bridge__/pwa-icon.svg">
155
+ <link rel="apple-touch-icon" href="/__dsh_bridge__/pwa-icon.svg">
156
+ <script data-dsh-bridge-polyfill="1">!function(){try{if(self.crypto&&!self.crypto.randomUUID){self.crypto.randomUUID=function(){var b=new Uint8Array(16);self.crypto.getRandomValues(b);b[6]=b[6]&15|64;b[8]=b[8]&63|128;var h="";for(var i=0;i<16;i++){var x=b[i].toString(16);h+=(x.length<2?"0":"")+x;if(i===3||i===5||i===7||i===9)h+="-";}return h;}}}catch(e){}}();</script>`;
115
157
  const INJECT_MARK = 'data-dsh-bridge-polyfill="1"';
116
158
 
117
159
  function isCompressed(headers) {
@@ -150,6 +192,18 @@ class ProxyServer {
150
192
  this.server = createServer((req, res) => {
151
193
  const pathname = (req.url || '/').split('?')[0].replace(/\/+$/, '') || '/';
152
194
 
195
+ // 0. PWA Web App Manifest 与 App 图标支持
196
+ if (pathname === '/manifest.webmanifest' || pathname === '/manifest.json') {
197
+ res.writeHead(200, { 'Content-Type': 'application/manifest+json; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
198
+ res.end(PWA_MANIFEST);
199
+ return;
200
+ }
201
+ if (pathname === '/__dsh_bridge__/pwa-icon.svg' || pathname === '/apple-touch-icon.png') {
202
+ res.writeHead(200, { 'Content-Type': 'image/svg+xml; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
203
+ res.end(PWA_ICON_SVG);
204
+ return;
205
+ }
206
+
153
207
  // 1. 处理登录 API: POST /__dsh_bridge__/login
154
208
  if (pathname === '/__dsh_bridge__/login' && req.method === 'POST') {
155
209
  const chunks = [];
@@ -289,7 +343,7 @@ class ProxyServer {
289
343
  proxyRes.on('end', () => {
290
344
  let html = Buffer.concat(chunks).toString('utf8');
291
345
  if (!html.includes(INJECT_MARK)) {
292
- html = html.replace(/<head[^>]*>/i, (m) => `${m}${RANDOM_UUID_POLYFILL}`);
346
+ html = html.replace(/<head[^>]*>/i, (m) => `${m}${HTML_HEAD_INJECTIONS}`);
293
347
  }
294
348
  const out = Buffer.from(html, 'utf8');
295
349
  const outHeaders = { ...proxyRes.headers };
@@ -398,12 +452,14 @@ class ProxyServer {
398
452
  * Bridge Service
399
453
  */
400
454
  class BridgeService {
401
- constructor({ dshPort, proxyPort, home, customTunnelConfig, authManager, logger }) {
455
+ constructor({ dshPort, proxyPort, home, cloudflaredConfig, customTunnelConfig, authManager, onPersist, logger }) {
402
456
  this.dshPort = dshPort;
403
457
  this.proxyPort = proxyPort;
404
458
  this.home = home;
459
+ this.cloudflaredConfig = cloudflaredConfig ?? { token: '', hostname: '', autoStart: false };
405
460
  this.customTunnelConfig = customTunnelConfig ?? null;
406
461
  this.authManager = authManager ?? null;
462
+ this.onPersist = onPersist ?? null;
407
463
  this.logger = logger;
408
464
 
409
465
  this.qrCache = new QrCache();
@@ -485,6 +541,10 @@ class BridgeService {
485
541
  ? await this.qrCache.get(cloudflaredUrl)
486
542
  : null,
487
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),
488
548
  },
489
549
 
490
550
  customTunnel: {
@@ -497,6 +557,7 @@ class BridgeService {
497
557
  ? await this.qrCache.get(customUrl)
498
558
  : null,
499
559
  state: this.customTunnelState,
560
+ autoStart: Boolean(this.customTunnelConfig?.autoStart),
500
561
  },
501
562
 
502
563
  // 轻量摘要,供 UI Tab 状态点使用(完整状态由 wechatGetStatus 提供)
@@ -504,7 +565,33 @@ class BridgeService {
504
565
  };
505
566
  }
506
567
 
507
- 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 } = {}) {
508
595
  if (this.customTunnel) {
509
596
  throw new Error('自建隧道已在运行');
510
597
  }
@@ -513,9 +600,15 @@ class BridgeService {
513
600
  const accessToken = this.customTunnelConfig?.accessToken;
514
601
 
515
602
  if (!serverUrl || !accessToken) {
516
- throw new Error('缺少配置:请在 cordis.yml 中配置 customTunnel.serverUrl 和 customTunnel.accessToken');
603
+ throw new Error('缺少配置:请在控制台配置 customTunnel.serverUrl 和 customTunnel.accessToken');
517
604
  }
518
605
 
606
+ this.customTunnelConfig = {
607
+ ...(this.customTunnelConfig ?? {}),
608
+ autoStart: Boolean(autoStart),
609
+ };
610
+ await this.onPersist?.({ customTunnel: this.customTunnelConfig });
611
+
519
612
  this.customTunnel = new CustomTunnelClient({
520
613
  serverUrl,
521
614
  accessToken,
@@ -530,23 +623,36 @@ class BridgeService {
530
623
  await this.customTunnel.connect();
531
624
  }
532
625
 
533
- stopCustomTunnel() {
626
+ async stopCustomTunnel() {
534
627
  if (this.customTunnel) {
535
628
  this.customTunnel.disconnect();
536
629
  this.customTunnel = null;
537
630
  this.customTunnelState = { phase: 'idle', detail: '' };
538
631
  }
632
+ this.customTunnelConfig = {
633
+ ...(this.customTunnelConfig ?? {}),
634
+ autoStart: false,
635
+ };
636
+ await this.onPersist?.({ customTunnel: this.customTunnelConfig });
539
637
  }
540
638
 
541
- async startCloudflared() {
639
+ async startCloudflared({ autoStart = true } = {}) {
542
640
  if (this.cloudflared) {
543
641
  throw new Error('Cloudflare 隧道已在运行');
544
642
  }
545
643
 
644
+ this.cloudflaredConfig = {
645
+ ...(this.cloudflaredConfig ?? {}),
646
+ autoStart: Boolean(autoStart),
647
+ };
648
+ await this.onPersist?.({ cloudflared: this.cloudflaredConfig });
649
+
546
650
  this.cloudflaredState = { phase: 'connecting', detail: '正在初始化...' };
547
651
  this.cloudflared = new CloudflaredManager({
548
652
  port: this.proxyPort,
549
653
  home: this.home,
654
+ token: this.cloudflaredConfig?.token,
655
+ hostname: this.cloudflaredConfig?.hostname,
550
656
  onStateChange: (state) => {
551
657
  this.cloudflaredState = state;
552
658
  // 出错时自动清理,让用户可以重新开启
@@ -561,17 +667,22 @@ class BridgeService {
561
667
  this.cloudflared.start();
562
668
  }
563
669
 
564
- stopCloudflared() {
670
+ async stopCloudflared() {
565
671
  if (this.cloudflared) {
566
672
  this.cloudflared.stop();
567
673
  this.cloudflared = null;
568
674
  this.cloudflaredState = { phase: 'idle', detail: '' };
569
675
  }
676
+ this.cloudflaredConfig = {
677
+ ...(this.cloudflaredConfig ?? {}),
678
+ autoStart: false,
679
+ };
680
+ await this.onPersist?.({ cloudflared: this.cloudflaredConfig });
570
681
  }
571
682
 
572
683
  // 重置 Cloudflare 隧道:关闭隧道 + 删除已下载的 cloudflared 二进制
573
684
  async resetCloudflared() {
574
- this.stopCloudflared();
685
+ await this.stopCloudflared();
575
686
  const binDir = join(this.home ?? join(homedir(), '.dsh-bridge'), 'bin');
576
687
  const candidates = ['cloudflared.exe', 'cloudflared'];
577
688
  for (const name of candidates) {
@@ -584,14 +695,17 @@ class BridgeService {
584
695
  // 检查 npm 上是否有新版本(优先国内高速镜像 npmmirror,降级 npmjs 官方源)
585
696
  async checkVersion() {
586
697
  const fetchRegistry = (url, timeoutMs = 4000) => new Promise((resolve, reject) => {
587
- const req = httpsGet(url, { timeout: timeoutMs }, (res) => {
698
+ const req = httpsGet(url, { timeout: timeoutMs, headers: { 'User-Agent': 'dsh-bridge' } }, (res) => {
588
699
  if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`));
589
700
  const chunks = [];
590
701
  res.on('data', (c) => chunks.push(c));
591
702
  res.on('end', () => {
592
703
  try {
593
704
  const data = JSON.parse(Buffer.concat(chunks).toString());
594
- resolve(data.version ?? null);
705
+ resolve({
706
+ version: data.version ?? null,
707
+ releaseNotes: data.releaseNotes ?? data.description ?? null,
708
+ });
595
709
  } catch (e) {
596
710
  reject(e);
597
711
  }
@@ -602,9 +716,13 @@ class BridgeService {
602
716
  });
603
717
 
604
718
  try {
605
- const latest = await fetchRegistry('https://registry.npmmirror.com/@wenbin_wb/dsh-bridge/latest', 3500)
719
+ const latestData = await fetchRegistry('https://registry.npmmirror.com/@wenbin_wb/dsh-bridge/latest', 3500)
606
720
  .catch(() => fetchRegistry('https://registry.npmjs.org/@wenbin_wb/dsh-bridge/latest', 5000));
607
- return { current: VERSION, latest: latest ?? null };
721
+ return {
722
+ current: VERSION,
723
+ latest: latestData?.version ?? null,
724
+ releaseNotes: latestData?.releaseNotes ?? null,
725
+ };
608
726
  } catch (e) {
609
727
  return { current: VERSION, latest: null, error: e.message ?? '检查失败' };
610
728
  }
@@ -774,15 +892,38 @@ function apply(ctx, config = {}) {
774
892
  proxyPort,
775
893
  home: config.home,
776
894
  customTunnelConfig: config.customTunnel ?? null,
895
+ cloudflaredConfig: config.cloudflared ?? null,
777
896
  authManager,
897
+ onPersist: async (patch) => {
898
+ const stored = await loadConfig();
899
+ Object.assign(stored, patch);
900
+ await saveConfig(stored);
901
+ },
778
902
  logger,
779
903
  });
780
904
 
781
- // 启动时读取已保存的自建隧道配置
782
- loadConfig().then((stored) => {
783
- 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) {
784
919
  service.customTunnelConfig = stored.customTunnel;
785
- 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
+ }
786
927
  }
787
928
  }).catch(() => {});
788
929
 
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@wenbin_wb/dsh-bridge",
3
- "version": "2.5.5",
3
+ "version": "2.6.1",
4
4
  "description": "手机扫码即可在移动端/公网继续用 DeepSeek Harness,人不在电脑前也能接着干。一键局域网二维码、Cloudflare 公网隧道、自建隧道与微信 / QQ / 飞书 / Telegram Bot(多工作区/会话持久化/媒体/卡片审批/流式输出),无需自己搭公网服务器。",
5
+ "releaseNotes": "【v2.6.1 公网隧道自启与固定域名】\n• 🔄 支持公网隧道(Cloudflare/自建)开机自启与重启状态记忆\n• 🌐 支持 Cloudflare 命名隧道(Token 模式),永久固定专属域名永不变更\n• ⚙️ 控制台新增「随 DSH 启动自动开启」开关与高级 Token 配置",
5
6
  "type": "module",
6
7
  "main": "lib/index.js",
7
8
  "exports": {