@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.
- package/README.en.md +47 -14
- package/README.md +47 -14
- package/client/client.js +718 -42
- package/client/index.js +2891 -2193
- package/docs/feishu-usage.md +16 -0
- package/docs/screenshots/mobile-chat.jpg +0 -0
- package/docs/screenshots/mobile-drawer.jpg +0 -0
- package/docs/screenshots/mobile-settings-im.jpg +0 -0
- package/docs/screenshots/mobile-settings-lan.jpg +0 -0
- package/docs/screenshots/mobile-settings-security.jpg +0 -0
- package/docs/screenshots/mobile-settings-tunnel.jpg +0 -0
- package/docs/screenshots/remote-web-mobile.jpg +0 -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 +160 -19
- package/package.json +2 -1
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
|
+
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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 =
|
|
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
|
@@ -108,10 +108,52 @@ class QrCache {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
*
|
|
112
|
-
* DSH 连接层 mint RPC id 时会抛错,注入 polyfill 修复。
|
|
111
|
+
* PWA Web App Manifest 与 App 启动图标
|
|
113
112
|
*/
|
|
114
|
-
const
|
|
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}${
|
|
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
|
|
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('
|
|
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(
|
|
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
|
|
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 {
|
|
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?.
|
|
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.
|
|
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": {
|