@wenbin_wb/dsh-bridge 2.8.6 → 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 +15 -0
- package/client/client.js +94 -7
- package/client/index.js +80 -1
- package/lib/bridge-rpc-constants.js +1 -0
- package/lib/bridge-rpc.js +9 -0
- package/lib/index.js +86 -21
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [v2.8.7] - 2026-08-30
|
|
8
|
+
|
|
9
|
+
### 🛜 多网卡智能切换与 macOS 升级与隧道自愈加固
|
|
10
|
+
- **🛜 支持局域网多网卡可视化切换与持久化 (Issue #5)**:
|
|
11
|
+
- 自动探测并列出宿主机所有可用物理网卡与虚拟网卡(Wi-Fi、以太网、WSL、VMware、Docker 等),支持识别网卡类型与优先级推荐;
|
|
12
|
+
- 在局域网控制台提供网卡下拉切换器,切换后即时重新生成局域网链接与二维码,并自动记忆持久化保存,彻底解决多网卡/WSL 环境下移动端扫码 IP 无法互通的问题;
|
|
13
|
+
- **🍏 加固 macOS 一键平滑升级**:
|
|
14
|
+
- `upgradePlugin` 自动补全 macOS GUI / 后台环境下缺失的系统 `PATH` 环境变量(自动兼容 `/opt/homebrew/bin`、`/usr/local/bin`、`~/.nvm/...` 等);
|
|
15
|
+
- 自动优先绑定当前运行中 Node.js 同级目录下的 `npm`/`npx` 二进制绝对路径,杜绝 `spawn ENOENT` 升级失败;
|
|
16
|
+
- **🛡️ 优化 macOS Cloudflared 二进制探测与隔离自愈**:
|
|
17
|
+
- 启动时优先扫描系统 `PATH` 与 Homebrew 安装的 `cloudflared` 全局二进制;
|
|
18
|
+
- 网络下载解压后自动通过 `xattr -d com.apple.quarantine` 移除 Gatekeeper 隔离标记,并加入 `--version` 运行期校验与自愈。
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
7
22
|
## [v2.8.6] - 2026-08-30
|
|
8
23
|
|
|
9
24
|
### 📱 移动端交互适配重构 & 隧道保活与传输优化
|
package/client/client.js
CHANGED
|
@@ -43,6 +43,7 @@ var BRIDGE_ENDPOINTS = {
|
|
|
43
43
|
saveCloudflaredConfig: "saveCloudflaredConfig",
|
|
44
44
|
setTunnelAutoStart: "setTunnelAutoStart",
|
|
45
45
|
saveCustomTunnelConfig: "saveCustomTunnelConfig",
|
|
46
|
+
setLanIp: "setLanIp",
|
|
46
47
|
checkVersion: "checkVersion",
|
|
47
48
|
upgradePlugin: "upgradePlugin",
|
|
48
49
|
restartDsh: "restartDsh",
|
|
@@ -376,6 +377,84 @@ function QrBlock({ url, qr, onReset, auth, onNavigateSecurity }) {
|
|
|
376
377
|
)
|
|
377
378
|
);
|
|
378
379
|
}
|
|
380
|
+
var LanNetworkSelector = React.memo(function LanNetworkSelector2({ lan, onSelectIp }) {
|
|
381
|
+
const interfaces = lan?.interfaces || [];
|
|
382
|
+
const selectedIp = lan?.selectedIp || "";
|
|
383
|
+
const currentIp = lan?.ip || "";
|
|
384
|
+
const [switching, setSwitching] = React.useState(false);
|
|
385
|
+
if (!interfaces || interfaces.length <= 1) return null;
|
|
386
|
+
const handleChange = async (e) => {
|
|
387
|
+
const val = e.target.value;
|
|
388
|
+
setSwitching(true);
|
|
389
|
+
try {
|
|
390
|
+
await onSelectIp(val || null);
|
|
391
|
+
} finally {
|
|
392
|
+
setSwitching(false);
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
return React.createElement(
|
|
396
|
+
"div",
|
|
397
|
+
{
|
|
398
|
+
style: {
|
|
399
|
+
...s.block,
|
|
400
|
+
background: "var(--dsw-alias-bg-layer-2, rgba(243, 244, 246, 0.6))",
|
|
401
|
+
padding: "10px 12px",
|
|
402
|
+
borderRadius: 8,
|
|
403
|
+
border: "1px solid var(--dsw-alias-border-l2, #e5e7eb)",
|
|
404
|
+
marginTop: 8,
|
|
405
|
+
marginBottom: 6
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
React.createElement(
|
|
409
|
+
"div",
|
|
410
|
+
{
|
|
411
|
+
style: {
|
|
412
|
+
display: "flex",
|
|
413
|
+
alignItems: "center",
|
|
414
|
+
justifyContent: "space-between",
|
|
415
|
+
marginBottom: 6,
|
|
416
|
+
fontSize: 12,
|
|
417
|
+
fontWeight: 500,
|
|
418
|
+
color: "var(--dsw-alias-label-primary, currentColor)"
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
React.createElement(
|
|
422
|
+
"span",
|
|
423
|
+
{ style: { display: "inline-flex", alignItems: "center", gap: 5 } },
|
|
424
|
+
"\u{1F6DC} \u5C40\u57DF\u7F51\u7F51\u5361 / IP \u9009\u62E9"
|
|
425
|
+
),
|
|
426
|
+
switching && React.createElement("span", {
|
|
427
|
+
style: { fontSize: 11, color: "var(--dsw-alias-brand-primary, #4f6ef7)" }
|
|
428
|
+
}, "\u5207\u6362\u4E2D\u2026")
|
|
429
|
+
),
|
|
430
|
+
React.createElement(
|
|
431
|
+
"div",
|
|
432
|
+
{ style: { ...s.muted, fontSize: 11, marginBottom: 6 } },
|
|
433
|
+
"\u68C0\u6D4B\u5230\u4E3B\u673A\u5B58\u5728\u591A\u5F20\u7F51\u5361\uFF08\u5982\u7269\u7406 Wi-Fi\u3001\u4EE5\u592A\u7F51\u3001WSL \u6216\u865A\u62DF\u673A\uFF09\u3002\u82E5\u9ED8\u8BA4 IP \u65E0\u6CD5\u88AB\u79FB\u52A8\u7AEF\u8BBF\u95EE\uFF0C\u53EF\u624B\u52A8\u5207\u6362\uFF1A"
|
|
434
|
+
),
|
|
435
|
+
React.createElement(
|
|
436
|
+
"select",
|
|
437
|
+
{
|
|
438
|
+
style: {
|
|
439
|
+
...s.input,
|
|
440
|
+
height: 32,
|
|
441
|
+
fontSize: 12,
|
|
442
|
+
padding: "0 8px",
|
|
443
|
+
background: "var(--dsw-alias-bg-layer-1, #ffffff)",
|
|
444
|
+
cursor: "pointer"
|
|
445
|
+
},
|
|
446
|
+
value: selectedIp,
|
|
447
|
+
onChange: handleChange,
|
|
448
|
+
disabled: switching
|
|
449
|
+
},
|
|
450
|
+
React.createElement("option", { value: "" }, `\u26A1 \u81EA\u52A8\u63A8\u8350 (${interfaces[0]?.address || currentIp} \xB7 ${interfaces[0]?.label || interfaces[0]?.name || ""})`),
|
|
451
|
+
interfaces.map((iface) => React.createElement("option", {
|
|
452
|
+
key: `${iface.name}-${iface.address}`,
|
|
453
|
+
value: iface.address
|
|
454
|
+
}, `${iface.address} \xB7 ${iface.label || iface.name}${iface.isVirtual ? " [\u865A\u62DF/WSL]" : ""}`))
|
|
455
|
+
)
|
|
456
|
+
);
|
|
457
|
+
});
|
|
379
458
|
var CustomTunnelGuide = React.memo(function CustomTunnelGuide2() {
|
|
380
459
|
return React.createElement(
|
|
381
460
|
"div",
|
|
@@ -2783,6 +2862,7 @@ function BridgePanel({ rpcCall }) {
|
|
|
2783
2862
|
({ token, hostname }) => act(BRIDGE_ENDPOINTS.saveCloudflaredConfig, { token, hostname }),
|
|
2784
2863
|
[act]
|
|
2785
2864
|
);
|
|
2865
|
+
const onSelectLanIp = React.useCallback((ip) => act(BRIDGE_ENDPOINTS.setLanIp, { ip }), [act]);
|
|
2786
2866
|
const onStartCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.startCustomTunnel), [act]);
|
|
2787
2867
|
const onStopCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.stopCustomTunnel), [act]);
|
|
2788
2868
|
const onToggleCustomAutoStart = React.useCallback(
|
|
@@ -2811,13 +2891,20 @@ function BridgePanel({ rpcCall }) {
|
|
|
2811
2891
|
};
|
|
2812
2892
|
let tabContent;
|
|
2813
2893
|
if (activeTab === "lan") {
|
|
2814
|
-
tabContent = React.createElement(
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2894
|
+
tabContent = React.createElement(
|
|
2895
|
+
TunnelCard,
|
|
2896
|
+
{
|
|
2897
|
+
title: "\u5C40\u57DF\u7F51\u8BBF\u95EE",
|
|
2898
|
+
desc: "\u540C\u4E00 Wi-Fi \u4E0B\u7684\u8BBE\u5907\u53EF\u76F4\u63A5\u626B\u7801\u8BBF\u95EE",
|
|
2899
|
+
data: { running: status?.proxy?.running, url: status?.lan?.url, qr: status?.lan?.qr },
|
|
2900
|
+
auth: status?.auth,
|
|
2901
|
+
onNavigateSecurity: navSecurity
|
|
2902
|
+
},
|
|
2903
|
+
React.createElement(LanNetworkSelector, {
|
|
2904
|
+
lan: status?.lan,
|
|
2905
|
+
onSelectIp: onSelectLanIp
|
|
2906
|
+
})
|
|
2907
|
+
);
|
|
2821
2908
|
} else if (activeTab === "tunnel") {
|
|
2822
2909
|
tabContent = React.createElement(
|
|
2823
2910
|
React.Fragment,
|
package/client/index.js
CHANGED
|
@@ -317,6 +317,78 @@ function QrBlock({ url, qr, onReset, auth, onNavigateSecurity }) {
|
|
|
317
317
|
);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
const LanNetworkSelector = React.memo(function LanNetworkSelector({ lan, onSelectIp }) {
|
|
321
|
+
const interfaces = lan?.interfaces || [];
|
|
322
|
+
const selectedIp = lan?.selectedIp || '';
|
|
323
|
+
const currentIp = lan?.ip || '';
|
|
324
|
+
const [switching, setSwitching] = React.useState(false);
|
|
325
|
+
|
|
326
|
+
if (!interfaces || interfaces.length <= 1) return null;
|
|
327
|
+
|
|
328
|
+
const handleChange = async (e) => {
|
|
329
|
+
const val = e.target.value;
|
|
330
|
+
setSwitching(true);
|
|
331
|
+
try {
|
|
332
|
+
await onSelectIp(val || null);
|
|
333
|
+
} finally {
|
|
334
|
+
setSwitching(false);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
return React.createElement('div', {
|
|
339
|
+
style: {
|
|
340
|
+
...s.block,
|
|
341
|
+
background: 'var(--dsw-alias-bg-layer-2, rgba(243, 244, 246, 0.6))',
|
|
342
|
+
padding: '10px 12px',
|
|
343
|
+
borderRadius: 8,
|
|
344
|
+
border: '1px solid var(--dsw-alias-border-l2, #e5e7eb)',
|
|
345
|
+
marginTop: 8,
|
|
346
|
+
marginBottom: 6,
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
React.createElement('div', {
|
|
350
|
+
style: {
|
|
351
|
+
display: 'flex',
|
|
352
|
+
alignItems: 'center',
|
|
353
|
+
justifyContent: 'space-between',
|
|
354
|
+
marginBottom: 6,
|
|
355
|
+
fontSize: 12,
|
|
356
|
+
fontWeight: 500,
|
|
357
|
+
color: 'var(--dsw-alias-label-primary, currentColor)',
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 5 } },
|
|
361
|
+
'🛜 局域网网卡 / IP 选择'
|
|
362
|
+
),
|
|
363
|
+
switching && React.createElement('span', {
|
|
364
|
+
style: { fontSize: 11, color: 'var(--dsw-alias-brand-primary, #4f6ef7)' },
|
|
365
|
+
}, '切换中…')
|
|
366
|
+
),
|
|
367
|
+
React.createElement('div', { style: { ...s.muted, fontSize: 11, marginBottom: 6 } },
|
|
368
|
+
'检测到主机存在多张网卡(如物理 Wi-Fi、以太网、WSL 或虚拟机)。若默认 IP 无法被移动端访问,可手动切换:'
|
|
369
|
+
),
|
|
370
|
+
React.createElement('select', {
|
|
371
|
+
style: {
|
|
372
|
+
...s.input,
|
|
373
|
+
height: 32,
|
|
374
|
+
fontSize: 12,
|
|
375
|
+
padding: '0 8px',
|
|
376
|
+
background: 'var(--dsw-alias-bg-layer-1, #ffffff)',
|
|
377
|
+
cursor: 'pointer',
|
|
378
|
+
},
|
|
379
|
+
value: selectedIp,
|
|
380
|
+
onChange: handleChange,
|
|
381
|
+
disabled: switching,
|
|
382
|
+
},
|
|
383
|
+
React.createElement('option', { value: '' }, `⚡ 自动推荐 (${interfaces[0]?.address || currentIp} · ${interfaces[0]?.label || interfaces[0]?.name || ''})`),
|
|
384
|
+
interfaces.map((iface) => React.createElement('option', {
|
|
385
|
+
key: `${iface.name}-${iface.address}`,
|
|
386
|
+
value: iface.address,
|
|
387
|
+
}, `${iface.address} · ${iface.label || iface.name}${iface.isVirtual ? ' [虚拟/WSL]' : ''}`)),
|
|
388
|
+
),
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
|
|
320
392
|
const CustomTunnelGuide = React.memo(function CustomTunnelGuide() {
|
|
321
393
|
return React.createElement('div', { style: s.block },
|
|
322
394
|
React.createElement('a', {
|
|
@@ -2497,6 +2569,8 @@ function BridgePanel({ rpcCall }) {
|
|
|
2497
2569
|
act(BRIDGE_ENDPOINTS.saveCloudflaredConfig, { token, hostname })
|
|
2498
2570
|
, [act]);
|
|
2499
2571
|
|
|
2572
|
+
const onSelectLanIp = React.useCallback((ip) => act(BRIDGE_ENDPOINTS.setLanIp, { ip }), [act]);
|
|
2573
|
+
|
|
2500
2574
|
const onStartCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.startCustomTunnel), [act]);
|
|
2501
2575
|
const onStopCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.stopCustomTunnel), [act]);
|
|
2502
2576
|
const onToggleCustomAutoStart = React.useCallback((autoStart) =>
|
|
@@ -2536,7 +2610,12 @@ function BridgePanel({ rpcCall }) {
|
|
|
2536
2610
|
data: { running: status?.proxy?.running, url: status?.lan?.url, qr: status?.lan?.qr },
|
|
2537
2611
|
auth: status?.auth,
|
|
2538
2612
|
onNavigateSecurity: navSecurity,
|
|
2539
|
-
}
|
|
2613
|
+
},
|
|
2614
|
+
React.createElement(LanNetworkSelector, {
|
|
2615
|
+
lan: status?.lan,
|
|
2616
|
+
onSelectIp: onSelectLanIp,
|
|
2617
|
+
})
|
|
2618
|
+
);
|
|
2540
2619
|
} else if (activeTab === 'tunnel') {
|
|
2541
2620
|
tabContent = React.createElement(React.Fragment, null,
|
|
2542
2621
|
React.createElement(TunnelCard, {
|
|
@@ -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;
|
package/lib/index.js
CHANGED
|
@@ -35,18 +35,17 @@ const inject = ['connection', 'webServer', 'sessions', 'agents', 'approval', 'wo
|
|
|
35
35
|
const PACKAGE_JSON = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'));
|
|
36
36
|
const VERSION = PACKAGE_JSON.version ?? '0.0.0';
|
|
37
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
|
+
|
|
38
43
|
/**
|
|
39
|
-
*
|
|
44
|
+
* 列出所有可用的局域网 IPv4 网卡与 IP 地址(按推荐优先级排序)
|
|
40
45
|
*/
|
|
41
|
-
function
|
|
46
|
+
function listAllLanIPv4() {
|
|
42
47
|
const interfaces = networkInterfaces();
|
|
43
|
-
|
|
44
|
-
let bestScore = -1;
|
|
45
|
-
|
|
46
|
-
const VIRTUAL_KEYWORDS = [
|
|
47
|
-
'vethernet', 'wsl', 'hyper-v', 'virtual', 'vmware', 'vbox', 'docker',
|
|
48
|
-
'tailscale', 'zerotier', 'tap', 'tun', 'utun', 'wireguard', 'loopback', 'bridge',
|
|
49
|
-
];
|
|
48
|
+
const list = [];
|
|
50
49
|
|
|
51
50
|
for (const [ifname, addrs] of Object.entries(interfaces)) {
|
|
52
51
|
if (!addrs) continue;
|
|
@@ -72,14 +71,31 @@ function selectLanIPv4() {
|
|
|
72
71
|
else if (lower.includes('ethernet') || lower.includes('以太网') || lower.includes('eth') || lower.includes('en')) score += 40;
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
});
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
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;
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
/**
|
|
@@ -509,12 +525,13 @@ class ProxyServer {
|
|
|
509
525
|
* Bridge Service
|
|
510
526
|
*/
|
|
511
527
|
class BridgeService {
|
|
512
|
-
constructor({ dshPort, proxyPort, home, cloudflaredConfig, customTunnelConfig, authManager, onPersist, logger }) {
|
|
528
|
+
constructor({ dshPort, proxyPort, home, cloudflaredConfig, customTunnelConfig, lanConfig, authManager, onPersist, logger }) {
|
|
513
529
|
this.dshPort = dshPort;
|
|
514
530
|
this.proxyPort = proxyPort;
|
|
515
531
|
this.home = home;
|
|
516
532
|
this.cloudflaredConfig = cloudflaredConfig ?? { token: '', hostname: '', autoStart: false };
|
|
517
533
|
this.customTunnelConfig = customTunnelConfig ?? null;
|
|
534
|
+
this.selectedLanIp = lanConfig?.selectedIp ?? null;
|
|
518
535
|
this.authManager = authManager ?? null;
|
|
519
536
|
this.onPersist = onPersist ?? null;
|
|
520
537
|
this.logger = logger;
|
|
@@ -529,6 +546,14 @@ class BridgeService {
|
|
|
529
546
|
this.cloudflaredState = { phase: 'idle', detail: '' };
|
|
530
547
|
}
|
|
531
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
|
+
|
|
532
557
|
async startProxy() {
|
|
533
558
|
if (this.proxy) return this.proxy;
|
|
534
559
|
|
|
@@ -544,7 +569,9 @@ class BridgeService {
|
|
|
544
569
|
}
|
|
545
570
|
|
|
546
571
|
async getStatus({ adminAuthValid = false } = {}) {
|
|
547
|
-
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();
|
|
548
575
|
const token = adminAuthValid ? this.authManager?.secretToken : null;
|
|
549
576
|
const isAuthEnabled = Boolean(this.authManager?.enabled && this.authManager?.mode !== 'password_only' && token);
|
|
550
577
|
|
|
@@ -585,6 +612,8 @@ class BridgeService {
|
|
|
585
612
|
|
|
586
613
|
lan: {
|
|
587
614
|
ip: lanIp,
|
|
615
|
+
selectedIp: this.selectedLanIp || '',
|
|
616
|
+
interfaces: allInterfaces,
|
|
588
617
|
url: lanUrl,
|
|
589
618
|
rawUrl: baseLanUrl,
|
|
590
619
|
qr: lanUrl ? await this.qrCache.get(lanUrl) : null,
|
|
@@ -798,10 +827,40 @@ class BridgeService {
|
|
|
798
827
|
const pkgSpec = `@wenbin_wb/dsh-bridge@${targetVersion}`;
|
|
799
828
|
const isWin = process.platform === 'win32';
|
|
800
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
|
+
|
|
801
860
|
const tasks = [
|
|
802
861
|
{ cmd: 'dsh', args: ['plugin', '--profile', 'web', 'add', pkgSpec] },
|
|
803
|
-
{ cmd: 'npx', args: ['--yes', '@deepseek-ai/dsh', 'plugin', '--profile', 'web', 'add', pkgSpec] },
|
|
804
|
-
{ 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] },
|
|
805
864
|
];
|
|
806
865
|
|
|
807
866
|
let lastError = null;
|
|
@@ -813,7 +872,8 @@ class BridgeService {
|
|
|
813
872
|
try {
|
|
814
873
|
cp = spawn(task.cmd, task.args, {
|
|
815
874
|
windowsHide: true,
|
|
816
|
-
shell:
|
|
875
|
+
shell: true,
|
|
876
|
+
env: augmentedEnv,
|
|
817
877
|
timeout: 120000,
|
|
818
878
|
});
|
|
819
879
|
} catch (spawnErr) {
|
|
@@ -1400,6 +1460,7 @@ function apply(ctx, config = {}) {
|
|
|
1400
1460
|
home: config.home,
|
|
1401
1461
|
customTunnelConfig: config.customTunnel ?? null,
|
|
1402
1462
|
cloudflaredConfig: config.cloudflared ?? null,
|
|
1463
|
+
lanConfig: config.lan ?? null,
|
|
1403
1464
|
authManager,
|
|
1404
1465
|
onPersist: async (patch) => {
|
|
1405
1466
|
const stored = await loadConfig();
|
|
@@ -1409,8 +1470,12 @@ function apply(ctx, config = {}) {
|
|
|
1409
1470
|
logger,
|
|
1410
1471
|
});
|
|
1411
1472
|
|
|
1412
|
-
//
|
|
1473
|
+
// 启动时读取已保存的局域网网卡配置与公网隧道配置并按需自动拉起
|
|
1413
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
|
+
}
|
|
1414
1479
|
if (stored?.cloudflared) {
|
|
1415
1480
|
service.cloudflaredConfig = stored.cloudflared;
|
|
1416
1481
|
logger.info('dsh-bridge: loaded saved cloudflared config (autoStart=%s, tokenConfigured=%s)', Boolean(service.cloudflaredConfig.autoStart), Boolean(service.cloudflaredConfig.token));
|
|
@@ -1736,4 +1801,4 @@ function apply(ctx, config = {}) {
|
|
|
1736
1801
|
}, 'dsh-bridge: stop wechat, qq, feishu, telegram, proxy, auth and tunnels');
|
|
1737
1802
|
}
|
|
1738
1803
|
|
|
1739
|
-
export { name, inject, apply, ProxyServer, BridgeService, selectLanIPv4 };
|
|
1804
|
+
export { name, inject, apply, ProxyServer, BridgeService, selectLanIPv4, listAllLanIPv4 };
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenbin_wb/dsh-bridge",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.7",
|
|
4
4
|
"description": "手机扫码即可在移动端/公网继续用 DeepSeek Harness,人不在电脑前也能接着干。一键局域网二维码、Cloudflare 公网隧道、自建隧道与微信 / QQ / 飞书 / Telegram Bot(多工作区/会话持久化/媒体/卡片审批/流式输出),无需自己搭公网服务器。",
|
|
5
|
-
"releaseNotes": "【v2.8.
|
|
5
|
+
"releaseNotes": "【v2.8.7 多网卡智能切换与 macOS 升级与隧道自愈加固】\n• 🛜 支持局域网多网卡可视化切换与持久化 (Issue #5):自动探测并列出物理网卡与虚拟网卡(Wi-Fi、以太网、WSL、VMware、Docker 等),支持在面板一键切换局域网 IP 并记忆持久化,彻底解决多网卡/WSL 下移动端无法访问的问题\n• 🍏 加固 macOS 一键平滑升级:`upgradePlugin` 自动补全 macOS 环境下的全量 PATH 环境变量(自动兼容 Homebrew、NVM 等),并自动优先绑定当前 Node 配对的同级 `npm`/`npx` 绝对路径,杜绝 `spawn ENOENT` 升级失败\n• 🛡️ 优化 macOS Cloudflared 二进制探测与隔离自愈:优先复用系统全局/Homebrew 已安装的 `cloudflared`,网络下载解压后自动移除 Gatekeeper 隔离标记(`xattr -d com.apple.quarantine`)并加入 `--version` 运行期校验自愈",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
|
8
8
|
"exports": {
|