@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 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(TunnelCard, {
2815
- title: "\u5C40\u57DF\u7F51\u8BBF\u95EE",
2816
- desc: "\u540C\u4E00 Wi-Fi \u4E0B\u7684\u8BBE\u5907\u53EF\u76F4\u63A5\u626B\u7801\u8BBF\u95EE",
2817
- data: { running: status?.proxy?.running, url: status?.lan?.url, qr: status?.lan?.qr },
2818
- auth: status?.auth,
2819
- onNavigateSecurity: navSecurity
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
- * 选择最佳局域网 IP
44
+ * 列出所有可用的局域网 IPv4 网卡与 IP 地址(按推荐优先级排序)
40
45
  */
41
- function selectLanIPv4() {
46
+ function listAllLanIPv4() {
42
47
  const interfaces = networkInterfaces();
43
- let best = null;
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
- if (score > bestScore) {
76
- bestScore = score;
77
- best = addr.address;
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 best;
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 lanIp = selectLanIPv4();
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: isWin ? true : false,
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.6",
3
+ "version": "2.8.7",
4
4
  "description": "手机扫码即可在移动端/公网继续用 DeepSeek Harness,人不在电脑前也能接着干。一键局域网二维码、Cloudflare 公网隧道、自建隧道与微信 / QQ / 飞书 / Telegram Bot(多工作区/会话持久化/媒体/卡片审批/流式输出),无需自己搭公网服务器。",
5
- "releaseNotes": "【v2.8.6 移动端交互适配重构 & 隧道保活与传输优化】\n• 📱 修复移动端工作区面板遮挡对话与白屏问题:重构工作区显隐状态机,新增常驻「✕ 返回对话」胶囊与多途径秒级回切机制,收起时彻底离场\n• 🗂️ 移动端侧边栏抽屉顶部集成收起图标:完整适配复用 DSH 原生自带收起图标(`[|`),支持双向顺畅开合与点击背景收起\n• 🖥️ 移动端样式与 PC 桌面端严格隔离:采用媒体查询与运行期断点隔离,PC 桌面端 100% 保持官方原生多列分栏与纯净体验\n• 🛡️ 修复 Cloudflared 守护进程异常退出问题:增加子进程退出监听与指数退避自动重启机制,确保长时间公网访问不断连\n• 优化自建隧道 SSE 稳定性与传输效率:修复弱网下 SSE 断线重连,精简 Session 列表大数据负载,显著降低移动端带宽与卡顿\n• 🤖 优化 IM 机器人会话管理与推送:会话列表按工作区分组对齐官方账本排序,产物文件采用 AI 显式驱动直传",
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": {