From 248a3b429416686c1c6ce2a34ee8cc50f979aeab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Sat, 23 May 2026 17:08:58 +0800 Subject: [PATCH] fix: keep H5 LAN links on the active server port H5 access can preserve a manually selected private LAN host, but the desktop sidecar binds a fresh dynamic port on restart. Refresh plain HTTP private-LAN URLs to the current auto-discovered port while leaving reverse proxy URLs intact. Constraint: Desktop sidecar ports are dynamic across app launches. Rejected: Preserve the complete private-LAN URL unchanged | this keeps stale ports like 5179 in QR links after restart. Confidence: high Scope-risk: narrow Directive: Do not treat plain private-LAN H5 URLs as reverse proxy URLs unless they carry a custom scheme or path. Tested: bun test src/server/__tests__/h5-access-service.test.ts Tested: bun test src/server/__tests__/h5-access-api.test.ts src/server/__tests__/h5-access-auth.test.ts src/server/__tests__/h5-access-policy.test.ts Tested: bun run check:server Tested: manual smoke with stored :5179 returning the active :45679 publicBaseUrl Not-tested: Real phone scan against a packaged desktop build after reinstall. --- .../__tests__/h5-access-service.test.ts | 20 ++++++++++++-- src/server/services/h5AccessService.ts | 26 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/server/__tests__/h5-access-service.test.ts b/src/server/__tests__/h5-access-service.test.ts index f76b96a4..99ddf5c9 100644 --- a/src/server/__tests__/h5-access-service.test.ts +++ b/src/server/__tests__/h5-access-service.test.ts @@ -101,7 +101,7 @@ describe('H5AccessService', () => { expect(result.settings.publicBaseUrl).toBe('https://chat.example.com/app') }) - test('auto LAN mode fills blank or loopback URLs but preserves manual LAN URLs', () => { + test('auto LAN mode fills blank or loopback URLs and refreshes manual LAN ports', () => { expect(resolveEffectiveH5PublicBaseUrl({ enabled: true, storedPublicBaseUrl: null, @@ -121,7 +121,7 @@ describe('H5AccessService', () => { storedPublicBaseUrl: 'http://192.168.1.100:54064', configuredPublicBaseUrl: null, autoPublicBaseUrl: 'http://172.20.16.1:39876', - })).toBe('http://192.168.1.100:54064') + })).toBe('http://192.168.1.100:39876') expect(resolveEffectiveH5PublicBaseUrl({ enabled: true, @@ -131,6 +131,22 @@ describe('H5AccessService', () => { })).toBe('https://chat.example.com/app') }) + test('auto LAN mode keeps full reverse proxy URLs intact', () => { + expect(resolveEffectiveH5PublicBaseUrl({ + enabled: true, + storedPublicBaseUrl: 'https://192.168.1.100:8443', + configuredPublicBaseUrl: null, + autoPublicBaseUrl: 'http://192.168.1.100:39876', + })).toBe('https://192.168.1.100:8443') + + expect(resolveEffectiveH5PublicBaseUrl({ + enabled: true, + storedPublicBaseUrl: 'http://192.168.1.100:8080/h5', + configuredPublicBaseUrl: null, + autoPublicBaseUrl: 'http://192.168.1.100:39876', + })).toBe('http://192.168.1.100:8080/h5') + }) + test('auto LAN detection prefers physical adapters over WSL and Docker virtual adapters', () => { expect(findPrivateLanAddress({ 'vEthernet (WSL)': [{ diff --git a/src/server/services/h5AccessService.ts b/src/server/services/h5AccessService.ts index 09b3247a..30b3afba 100644 --- a/src/server/services/h5AccessService.ts +++ b/src/server/services/h5AccessService.ts @@ -179,6 +179,11 @@ export function resolveEffectiveH5PublicBaseUrl({ return autoPublicBaseUrl } + const refreshedLanUrl = refreshLanPublicBaseUrlPort(storedPublicBaseUrl, autoPublicBaseUrl) + if (refreshedLanUrl) { + return refreshedLanUrl + } + return storedPublicBaseUrl } @@ -203,6 +208,27 @@ function isLocalHost(hostname: string): boolean { hostname === '::' } +function refreshLanPublicBaseUrlPort(storedPublicBaseUrl: string, autoPublicBaseUrl: string): string | null { + try { + const stored = new URL(storedPublicBaseUrl) + const auto = new URL(autoPublicBaseUrl) + const storedPath = stored.pathname.replace(/\/+$/, '') + + if ( + stored.protocol !== 'http:' || + storedPath !== '' || + !isPrivateIPv4(stored.hostname) || + !auto.port + ) { + return null + } + + return `${stored.protocol}//${stored.hostname}:${auto.port}` + } catch { + return null + } +} + type NetworkInterfaces = ReturnType const PHYSICAL_INTERFACE_RE = /\b(wi-?fi|wlan|wireless|ethernet|lan|en\d+|eth\d+)\b/i