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.
This commit is contained in:
程序员阿江(Relakkes) 2026-05-23 17:08:58 +08:00
parent 77552375e9
commit 248a3b4294
2 changed files with 44 additions and 2 deletions

View File

@ -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)': [{

View File

@ -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<typeof os.networkInterfaces>
const PHYSICAL_INTERFACE_RE = /\b(wi-?fi|wlan|wireless|ethernet|lan|en\d+|eth\d+)\b/i