@wenbin_wb/dsh-bridge 2.8.5 → 2.8.7
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/CHANGELOG.md +34 -0
- package/client/client.js +396 -39
- package/client/index.js +399 -40
- package/docs/fix-tunnel-sse-and-session-list.md +134 -0
- package/lib/bridge-rpc-constants.js +1 -0
- package/lib/bridge-rpc.js +9 -0
- package/lib/cloudflared-manager.mjs +75 -12
- package/lib/index.js +147 -26
- package/lib/platform/conversation-bridge.js +153 -128
- package/lib/tunnel-client.mjs +402 -289
- package/package.json +2 -2
|
@@ -12,6 +12,7 @@ export const BRIDGE_ENDPOINTS = {
|
|
|
12
12
|
saveCloudflaredConfig: 'saveCloudflaredConfig',
|
|
13
13
|
setTunnelAutoStart: 'setTunnelAutoStart',
|
|
14
14
|
saveCustomTunnelConfig: 'saveCustomTunnelConfig',
|
|
15
|
+
setLanIp: 'setLanIp',
|
|
15
16
|
checkVersion: 'checkVersion',
|
|
16
17
|
upgradePlugin: 'upgradePlugin',
|
|
17
18
|
restartDsh: 'restartDsh',
|
package/lib/bridge-rpc.js
CHANGED
|
@@ -167,6 +167,15 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
167
167
|
return ok(status);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
if (endpoint === BRIDGE_ENDPOINTS.setLanIp) {
|
|
171
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
172
|
+
if (adminErr) return adminErr;
|
|
173
|
+
|
|
174
|
+
const { ip } = payload;
|
|
175
|
+
const status = await service.setLanIp({ ip });
|
|
176
|
+
return ok(status);
|
|
177
|
+
}
|
|
178
|
+
|
|
170
179
|
if (endpoint === BRIDGE_ENDPOINTS.startCustomTunnel) {
|
|
171
180
|
const adminErr = checkAdminAuth(authManager, payload);
|
|
172
181
|
if (adminErr) return adminErr;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
|
-
import { createWriteStream, existsSync, mkdirSync } from 'node:fs';
|
|
1
|
+
import { spawn, execSync } from 'node:child_process';
|
|
2
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
5
3
|
import { chmod, stat, unlink, rename } from 'node:fs/promises';
|
|
6
4
|
import { homedir, platform, arch } from 'node:os';
|
|
7
5
|
import { join } from 'node:path';
|
|
@@ -73,6 +71,27 @@ async function downloadFile(url, dest, onProgress) {
|
|
|
73
71
|
});
|
|
74
72
|
}
|
|
75
73
|
|
|
74
|
+
function findSystemCloudflared() {
|
|
75
|
+
const isWin = platform() === 'win32';
|
|
76
|
+
const candidates = [];
|
|
77
|
+
if (isWin) {
|
|
78
|
+
candidates.push('cloudflared.exe', 'cloudflared', 'C:\\Program Files (x86)\\cloudflared\\cloudflared.exe', 'C:\\Program Files\\cloudflared\\cloudflared.exe');
|
|
79
|
+
} else {
|
|
80
|
+
candidates.push('cloudflared', '/opt/homebrew/bin/cloudflared', '/usr/local/bin/cloudflared', '/usr/bin/cloudflared', '/bin/cloudflared');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const bin of candidates) {
|
|
84
|
+
try {
|
|
85
|
+
if (bin.includes('/') || bin.includes('\\')) {
|
|
86
|
+
if (!existsSync(bin)) continue;
|
|
87
|
+
}
|
|
88
|
+
execSync(`"${bin}" --version`, { stdio: 'ignore', timeout: 3000 });
|
|
89
|
+
return bin;
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
76
95
|
export class CloudflaredManager {
|
|
77
96
|
constructor({ port, home, token, hostname, onStateChange, logger }) {
|
|
78
97
|
this.port = port;
|
|
@@ -104,19 +123,47 @@ export class CloudflaredManager {
|
|
|
104
123
|
}
|
|
105
124
|
|
|
106
125
|
async _ensureBinary() {
|
|
126
|
+
// 1. 优先使用系统环境变量或 Homebrew / 包管理器已安装的全局二进制
|
|
127
|
+
const systemBin = findSystemCloudflared();
|
|
128
|
+
if (systemBin) {
|
|
129
|
+
this.binaryPath = systemBin;
|
|
130
|
+
this.logger?.info('优先使用系统全局 cloudflared: %s', systemBin);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
107
134
|
const { url, name } = getCloudflaredInfo();
|
|
108
135
|
const binDir = join(this.home, 'bin');
|
|
109
136
|
const binPath = join(binDir, name);
|
|
110
137
|
this.binaryPath = binPath;
|
|
111
138
|
|
|
139
|
+
// 2. 检查本地 ~/.dsh-bridge/bin/cloudflared 是否已存在且可用
|
|
112
140
|
if (existsSync(binPath)) {
|
|
113
141
|
try {
|
|
114
142
|
const s = await stat(binPath);
|
|
115
143
|
if (s.size > MIN_BINARY_SIZE) { // >5MB 才视为有效二进制
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
// 检查是否为历史残留未解压的 gzip 压缩包 (0x1f 0x8b)
|
|
145
|
+
const fd = readFileSync(binPath);
|
|
146
|
+
const isGzip = fd.length >= 2 && fd[0] === 0x1f && fd[1] === 0x8b;
|
|
147
|
+
if (isGzip) {
|
|
148
|
+
this.logger?.warn('检测到历史残留的未解压 cloudflared.tgz 压缩包,正在清理重新准备...');
|
|
149
|
+
await unlink(binPath).catch(() => {});
|
|
150
|
+
} else {
|
|
151
|
+
// macOS / Linux 赋予可执行权限并清除 Gatekeeper 隔离属性
|
|
152
|
+
if (platform() !== 'win32') {
|
|
153
|
+
await chmod(binPath, 0o755).catch(() => {});
|
|
154
|
+
if (platform() === 'darwin') {
|
|
155
|
+
try { execSync(`xattr -d com.apple.quarantine "${binPath}"`, { stdio: 'ignore' }); } catch {}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// 执行一次 --version 验证是否能正常 spawn
|
|
159
|
+
execSync(`"${binPath}" --version`, { stdio: 'ignore', timeout: 3000 });
|
|
160
|
+
this.logger?.info('cloudflared 已存在且验证通过: %s', binPath);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
118
163
|
}
|
|
119
|
-
} catch {
|
|
164
|
+
} catch (verifyErr) {
|
|
165
|
+
this.logger?.warn('现有 cloudflared 二进制验证失败 (%s),准备重新下载', verifyErr.message);
|
|
166
|
+
}
|
|
120
167
|
// 损坏文件,删掉重下
|
|
121
168
|
await unlink(binPath).catch(() => {});
|
|
122
169
|
}
|
|
@@ -135,16 +182,32 @@ export class CloudflaredManager {
|
|
|
135
182
|
this._setState('downloading', `下载 cloudflared: ${mb}/${totalMb} MB (${percent}%)`);
|
|
136
183
|
});
|
|
137
184
|
|
|
138
|
-
if (
|
|
139
|
-
|
|
185
|
+
if (url.endsWith('.tgz') || url.endsWith('.tar.gz')) {
|
|
186
|
+
try {
|
|
187
|
+
execSync(`tar -xzf "${tempPath}" -C "${binDir}"`);
|
|
188
|
+
await unlink(tempPath).catch(() => {});
|
|
189
|
+
} catch (tarErr) {
|
|
190
|
+
this.logger?.error('解压 cloudflared 压缩包失败: %s', tarErr.message);
|
|
191
|
+
throw new Error(`解压 cloudflared 失败: ${tarErr.message}`);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
if (existsSync(binPath)) await unlink(binPath).catch(() => {});
|
|
195
|
+
await rename(tempPath, binPath);
|
|
196
|
+
}
|
|
140
197
|
|
|
141
198
|
if (platform() !== 'win32') {
|
|
142
|
-
await chmod(binPath, 0o755);
|
|
199
|
+
await chmod(binPath, 0o755).catch(() => {});
|
|
200
|
+
if (platform() === 'darwin') {
|
|
201
|
+
try { execSync(`xattr -d com.apple.quarantine "${binPath}"`, { stdio: 'ignore' }); } catch {}
|
|
202
|
+
}
|
|
143
203
|
}
|
|
144
|
-
|
|
204
|
+
|
|
205
|
+
// 执行 --version 最终确认
|
|
206
|
+
execSync(`"${binPath}" --version`, { stdio: 'ignore', timeout: 3000 });
|
|
207
|
+
this.logger?.info('cloudflared 下载并准备完成');
|
|
145
208
|
} catch (err) {
|
|
146
209
|
await unlink(tempPath).catch(() => {});
|
|
147
|
-
throw new Error(
|
|
210
|
+
throw new Error(`准备 cloudflared 失败: ${err.message}`);
|
|
148
211
|
}
|
|
149
212
|
}
|
|
150
213
|
|
package/lib/index.js
CHANGED
|
@@ -10,9 +10,10 @@ import { get as httpsGet } from 'node:https';
|
|
|
10
10
|
import { networkInterfaces, homedir, totalmem, freemem, cpus, loadavg, platform, arch, release, hostname, uptime } from 'node:os';
|
|
11
11
|
import { join, dirname, basename, resolve, normalize } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
|
-
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
14
14
|
import { readFile, writeFile, mkdir, unlink, readdir, stat, access } from 'node:fs/promises';
|
|
15
15
|
import { spawn } from 'node:child_process';
|
|
16
|
+
import { createHash, createHmac } from 'node:crypto';
|
|
16
17
|
import QRCode from 'qrcode';
|
|
17
18
|
import { installBridgeRpc } from './bridge-rpc.js';
|
|
18
19
|
import { CustomTunnelClient } from './tunnel-client.mjs';
|
|
@@ -34,40 +35,67 @@ const inject = ['connection', 'webServer', 'sessions', 'agents', 'approval', 'wo
|
|
|
34
35
|
const PACKAGE_JSON = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'));
|
|
35
36
|
const VERSION = PACKAGE_JSON.version ?? '0.0.0';
|
|
36
37
|
|
|
38
|
+
const VIRTUAL_KEYWORDS = [
|
|
39
|
+
'vethernet', 'wsl', 'hyper-v', 'virtual', 'vmware', 'vbox', 'docker',
|
|
40
|
+
'tailscale', 'zerotier', 'tap', 'tun', 'utun', 'wireguard', 'loopback', 'bridge',
|
|
41
|
+
];
|
|
42
|
+
|
|
37
43
|
/**
|
|
38
|
-
*
|
|
44
|
+
* 列出所有可用的局域网 IPv4 网卡与 IP 地址(按推荐优先级排序)
|
|
39
45
|
*/
|
|
40
|
-
function
|
|
46
|
+
function listAllLanIPv4() {
|
|
41
47
|
const interfaces = networkInterfaces();
|
|
42
|
-
|
|
43
|
-
let bestScore = -1;
|
|
48
|
+
const list = [];
|
|
44
49
|
|
|
45
50
|
for (const [ifname, addrs] of Object.entries(interfaces)) {
|
|
46
51
|
if (!addrs) continue;
|
|
52
|
+
const lower = ifname.toLowerCase();
|
|
53
|
+
const isVirtual = VIRTUAL_KEYWORDS.some((kw) => lower.includes(kw));
|
|
47
54
|
|
|
48
55
|
for (const addr of addrs) {
|
|
49
56
|
if (addr.family !== 'IPv4' || addr.internal) continue;
|
|
50
57
|
|
|
51
58
|
let score = 0;
|
|
59
|
+
// 1. IP 网段优先(家庭/企业物理局域网最常用网段)
|
|
52
60
|
if (addr.address.startsWith('192.168.')) score += 100;
|
|
53
61
|
else if (addr.address.startsWith('10.')) score += 90;
|
|
54
|
-
else if (addr.address.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./)) score +=
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
bestScore = score;
|
|
65
|
-
best = addr.address;
|
|
62
|
+
else if (addr.address.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./)) score += 70;
|
|
63
|
+
else score += 10;
|
|
64
|
+
|
|
65
|
+
// 2. 物理网卡与名称特征优先
|
|
66
|
+
if (isVirtual) {
|
|
67
|
+
score -= 200; // 虚拟网卡大幅降权
|
|
68
|
+
} else {
|
|
69
|
+
score += 100;
|
|
70
|
+
if (lower.includes('wi-fi') || lower.includes('wlan') || lower.includes('wireless')) score += 50;
|
|
71
|
+
else if (lower.includes('ethernet') || lower.includes('以太网') || lower.includes('eth') || lower.includes('en')) score += 40;
|
|
66
72
|
}
|
|
73
|
+
|
|
74
|
+
let label = ifname;
|
|
75
|
+
if (lower.includes('wi-fi') || lower.includes('wlan') || lower.includes('wireless')) label += ' (Wi-Fi 无线网卡)';
|
|
76
|
+
else if (lower.includes('ethernet') || lower.includes('以太网') || lower.includes('eth') || lower.includes('en')) label += ' (有线网卡)';
|
|
77
|
+
else if (isVirtual) label += ' (虚拟网卡 / WSL / 虚拟机)';
|
|
78
|
+
|
|
79
|
+
list.push({
|
|
80
|
+
name: ifname,
|
|
81
|
+
label,
|
|
82
|
+
address: addr.address,
|
|
83
|
+
netmask: addr.netmask,
|
|
84
|
+
isVirtual,
|
|
85
|
+
score,
|
|
86
|
+
});
|
|
67
87
|
}
|
|
68
88
|
}
|
|
69
89
|
|
|
70
|
-
return
|
|
90
|
+
return list.sort((a, b) => b.score - a.score);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 选择最佳默认局域网 IP
|
|
95
|
+
*/
|
|
96
|
+
function selectLanIPv4() {
|
|
97
|
+
const list = listAllLanIPv4();
|
|
98
|
+
return list[0]?.address || null;
|
|
71
99
|
}
|
|
72
100
|
|
|
73
101
|
/**
|
|
@@ -161,6 +189,37 @@ function isCompressed(headers) {
|
|
|
161
189
|
return /(^|,\s*)(gzip|br|deflate)(\s*,|$)/i.test(String(headers['content-encoding'] ?? ''));
|
|
162
190
|
}
|
|
163
191
|
|
|
192
|
+
function encodeBase64Url(value) {
|
|
193
|
+
return Buffer.from(value).toString('base64url');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 读取 DSH 本地凭证并生成 loopback dsh-auth 认证签名 Cookie (适配 DSH 新版原生认证)
|
|
198
|
+
*/
|
|
199
|
+
function getDshLoopbackCookie(targetPort) {
|
|
200
|
+
try {
|
|
201
|
+
const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
202
|
+
const credPath = join(dshHome, '.credentials.yaml');
|
|
203
|
+
if (!existsSync(credPath)) return '';
|
|
204
|
+
const content = readFileSync(credPath, 'utf8');
|
|
205
|
+
const match = content.match(/secret:\s*([A-Za-z0-9_-]+)/);
|
|
206
|
+
if (!match) return '';
|
|
207
|
+
const secret = Buffer.from(match[1], 'base64url');
|
|
208
|
+
|
|
209
|
+
const authority = `127.0.0.1:${targetPort}`;
|
|
210
|
+
const name = 'dsh-auth-' + encodeBase64Url(createHash('sha256').update(authority).digest());
|
|
211
|
+
const issuedAt = Date.now() - 1000;
|
|
212
|
+
const expiresAt = issuedAt + 30 * 24 * 3600 * 1000;
|
|
213
|
+
const body = encodeBase64Url(Buffer.from(JSON.stringify({
|
|
214
|
+
version: 1, authority, issuedAt, expiresAt,
|
|
215
|
+
}), 'utf8'));
|
|
216
|
+
const sig = encodeBase64Url(createHmac('sha256', secret).update(body).digest());
|
|
217
|
+
return `${name}=v1.${body}.${sig}`;
|
|
218
|
+
} catch {
|
|
219
|
+
return '';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
164
223
|
/** 把请求头中的 Host 和 Origin 改写成 loopback,让 DSH 的安全栅栏放行 */
|
|
165
224
|
function loopbackHeaders(headers, targetPort) {
|
|
166
225
|
const authority = `127.0.0.1:${targetPort}`;
|
|
@@ -168,6 +227,19 @@ function loopbackHeaders(headers, targetPort) {
|
|
|
168
227
|
out['host'] = authority;
|
|
169
228
|
if (out['origin']) out['origin'] = `http://${authority}`;
|
|
170
229
|
if (out['Origin']) out['Origin'] = `http://${authority}`;
|
|
230
|
+
|
|
231
|
+
// 1. 注入 DSH 本地认证签名(若有)
|
|
232
|
+
const dshCookie = getDshLoopbackCookie(targetPort);
|
|
233
|
+
if (dshCookie) {
|
|
234
|
+
const existing = out['cookie'] || out['Cookie'] || '';
|
|
235
|
+
out['cookie'] = existing ? `${existing}; ${dshCookie}` : dshCookie;
|
|
236
|
+
delete out['Cookie'];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 2. 禁用内部代理流量压缩,确保代理层拿到未压缩 HTML 以稳定注入 ownsHost 和 Polyfill
|
|
240
|
+
delete out['accept-encoding'];
|
|
241
|
+
delete out['Accept-Encoding'];
|
|
242
|
+
|
|
171
243
|
return out;
|
|
172
244
|
}
|
|
173
245
|
|
|
@@ -453,12 +525,13 @@ class ProxyServer {
|
|
|
453
525
|
* Bridge Service
|
|
454
526
|
*/
|
|
455
527
|
class BridgeService {
|
|
456
|
-
constructor({ dshPort, proxyPort, home, cloudflaredConfig, customTunnelConfig, authManager, onPersist, logger }) {
|
|
528
|
+
constructor({ dshPort, proxyPort, home, cloudflaredConfig, customTunnelConfig, lanConfig, authManager, onPersist, logger }) {
|
|
457
529
|
this.dshPort = dshPort;
|
|
458
530
|
this.proxyPort = proxyPort;
|
|
459
531
|
this.home = home;
|
|
460
532
|
this.cloudflaredConfig = cloudflaredConfig ?? { token: '', hostname: '', autoStart: false };
|
|
461
533
|
this.customTunnelConfig = customTunnelConfig ?? null;
|
|
534
|
+
this.selectedLanIp = lanConfig?.selectedIp ?? null;
|
|
462
535
|
this.authManager = authManager ?? null;
|
|
463
536
|
this.onPersist = onPersist ?? null;
|
|
464
537
|
this.logger = logger;
|
|
@@ -473,6 +546,14 @@ class BridgeService {
|
|
|
473
546
|
this.cloudflaredState = { phase: 'idle', detail: '' };
|
|
474
547
|
}
|
|
475
548
|
|
|
549
|
+
async setLanIp({ ip } = {}) {
|
|
550
|
+
const trimmed = ip ? String(ip).trim() : null;
|
|
551
|
+
this.selectedLanIp = trimmed || null;
|
|
552
|
+
await this.onPersist?.({ lan: { selectedIp: this.selectedLanIp } });
|
|
553
|
+
this.logger?.info('局域网选定 IP 更新为: %s', this.selectedLanIp || '自动推荐');
|
|
554
|
+
return this.getStatus();
|
|
555
|
+
}
|
|
556
|
+
|
|
476
557
|
async startProxy() {
|
|
477
558
|
if (this.proxy) return this.proxy;
|
|
478
559
|
|
|
@@ -488,7 +569,9 @@ class BridgeService {
|
|
|
488
569
|
}
|
|
489
570
|
|
|
490
571
|
async getStatus({ adminAuthValid = false } = {}) {
|
|
491
|
-
const
|
|
572
|
+
const allInterfaces = listAllLanIPv4();
|
|
573
|
+
const isSelectedValid = Boolean(this.selectedLanIp && allInterfaces.some(i => i.address === this.selectedLanIp));
|
|
574
|
+
const lanIp = isSelectedValid ? this.selectedLanIp : selectLanIPv4();
|
|
492
575
|
const token = adminAuthValid ? this.authManager?.secretToken : null;
|
|
493
576
|
const isAuthEnabled = Boolean(this.authManager?.enabled && this.authManager?.mode !== 'password_only' && token);
|
|
494
577
|
|
|
@@ -529,6 +612,8 @@ class BridgeService {
|
|
|
529
612
|
|
|
530
613
|
lan: {
|
|
531
614
|
ip: lanIp,
|
|
615
|
+
selectedIp: this.selectedLanIp || '',
|
|
616
|
+
interfaces: allInterfaces,
|
|
532
617
|
url: lanUrl,
|
|
533
618
|
rawUrl: baseLanUrl,
|
|
534
619
|
qr: lanUrl ? await this.qrCache.get(lanUrl) : null,
|
|
@@ -742,10 +827,40 @@ class BridgeService {
|
|
|
742
827
|
const pkgSpec = `@wenbin_wb/dsh-bridge@${targetVersion}`;
|
|
743
828
|
const isWin = process.platform === 'win32';
|
|
744
829
|
|
|
830
|
+
// 自动构建包含 Homebrew / NVM / Node 兄弟目录的全量 PATH 环境变量
|
|
831
|
+
const nodeDir = dirname(process.execPath);
|
|
832
|
+
const home = homedir();
|
|
833
|
+
const extraPaths = isWin ? [
|
|
834
|
+
nodeDir,
|
|
835
|
+
] : [
|
|
836
|
+
nodeDir,
|
|
837
|
+
'/opt/homebrew/bin',
|
|
838
|
+
'/opt/homebrew/sbin',
|
|
839
|
+
'/usr/local/bin',
|
|
840
|
+
'/usr/bin',
|
|
841
|
+
'/bin',
|
|
842
|
+
join(home, '.nvm/current/bin'),
|
|
843
|
+
join(home, '.fnm/current/bin'),
|
|
844
|
+
join(home, '.local/bin'),
|
|
845
|
+
join(home, '.cargo/bin'),
|
|
846
|
+
];
|
|
847
|
+
|
|
848
|
+
const separator = isWin ? ';' : ':';
|
|
849
|
+
const existingPath = process.env.PATH || process.env.Path || '';
|
|
850
|
+
const augmentedEnv = {
|
|
851
|
+
...process.env,
|
|
852
|
+
PATH: [...extraPaths, existingPath].filter(Boolean).join(separator),
|
|
853
|
+
};
|
|
854
|
+
if (isWin) augmentedEnv.Path = augmentedEnv.PATH;
|
|
855
|
+
|
|
856
|
+
// 寻找与当前 node 配对的 npm/npx 绝对路径
|
|
857
|
+
const siblingNpm = join(nodeDir, isWin ? 'npm.cmd' : 'npm');
|
|
858
|
+
const siblingNpx = join(nodeDir, isWin ? 'npx.cmd' : 'npx');
|
|
859
|
+
|
|
745
860
|
const tasks = [
|
|
746
861
|
{ cmd: 'dsh', args: ['plugin', '--profile', 'web', 'add', pkgSpec] },
|
|
747
|
-
{ cmd: 'npx', args: ['--yes', '@deepseek-ai/dsh', 'plugin', '--profile', 'web', 'add', pkgSpec] },
|
|
748
|
-
{ cmd: 'npm', args: ['install', pkgSpec] },
|
|
862
|
+
{ cmd: existsSync(siblingNpx) ? siblingNpx : 'npx', args: ['--yes', '@deepseek-ai/dsh', 'plugin', '--profile', 'web', 'add', pkgSpec] },
|
|
863
|
+
{ cmd: existsSync(siblingNpm) ? siblingNpm : 'npm', args: ['install', pkgSpec] },
|
|
749
864
|
];
|
|
750
865
|
|
|
751
866
|
let lastError = null;
|
|
@@ -757,7 +872,8 @@ class BridgeService {
|
|
|
757
872
|
try {
|
|
758
873
|
cp = spawn(task.cmd, task.args, {
|
|
759
874
|
windowsHide: true,
|
|
760
|
-
shell:
|
|
875
|
+
shell: true,
|
|
876
|
+
env: augmentedEnv,
|
|
761
877
|
timeout: 120000,
|
|
762
878
|
});
|
|
763
879
|
} catch (spawnErr) {
|
|
@@ -1254,7 +1370,7 @@ class BridgeService {
|
|
|
1254
1370
|
*/
|
|
1255
1371
|
function apply(ctx, config = {}) {
|
|
1256
1372
|
const logger = ctx.logger(name);
|
|
1257
|
-
const dshPort = ctx.webServer.
|
|
1373
|
+
const dshPort = ctx.webServer?.port ?? config.targetPort ?? 3080;
|
|
1258
1374
|
|
|
1259
1375
|
if (!dshPort) {
|
|
1260
1376
|
logger.error('webServer port unavailable');
|
|
@@ -1344,6 +1460,7 @@ function apply(ctx, config = {}) {
|
|
|
1344
1460
|
home: config.home,
|
|
1345
1461
|
customTunnelConfig: config.customTunnel ?? null,
|
|
1346
1462
|
cloudflaredConfig: config.cloudflared ?? null,
|
|
1463
|
+
lanConfig: config.lan ?? null,
|
|
1347
1464
|
authManager,
|
|
1348
1465
|
onPersist: async (patch) => {
|
|
1349
1466
|
const stored = await loadConfig();
|
|
@@ -1353,8 +1470,12 @@ function apply(ctx, config = {}) {
|
|
|
1353
1470
|
logger,
|
|
1354
1471
|
});
|
|
1355
1472
|
|
|
1356
|
-
//
|
|
1473
|
+
// 启动时读取已保存的局域网网卡配置与公网隧道配置并按需自动拉起
|
|
1357
1474
|
loadConfig().then(async (stored) => {
|
|
1475
|
+
if (stored?.lan?.selectedIp) {
|
|
1476
|
+
service.selectedLanIp = stored.lan.selectedIp;
|
|
1477
|
+
logger.info('dsh-bridge: loaded saved lan config (selectedIp=%s)', service.selectedLanIp);
|
|
1478
|
+
}
|
|
1358
1479
|
if (stored?.cloudflared) {
|
|
1359
1480
|
service.cloudflaredConfig = stored.cloudflared;
|
|
1360
1481
|
logger.info('dsh-bridge: loaded saved cloudflared config (autoStart=%s, tokenConfigured=%s)', Boolean(service.cloudflaredConfig.autoStart), Boolean(service.cloudflaredConfig.token));
|
|
@@ -1680,4 +1801,4 @@ function apply(ctx, config = {}) {
|
|
|
1680
1801
|
}, 'dsh-bridge: stop wechat, qq, feishu, telegram, proxy, auth and tunnels');
|
|
1681
1802
|
}
|
|
1682
1803
|
|
|
1683
|
-
export { name, inject, apply, ProxyServer, BridgeService, selectLanIPv4 };
|
|
1804
|
+
export { name, inject, apply, ProxyServer, BridgeService, selectLanIPv4, listAllLanIPv4 };
|