diff --git a/apps/desktop-client/src/main/runtime-controller.ts b/apps/desktop-client/src/main/runtime-controller.ts index 2aa68d2..2f6890c 100644 --- a/apps/desktop-client/src/main/runtime-controller.ts +++ b/apps/desktop-client/src/main/runtime-controller.ts @@ -22,6 +22,7 @@ import { normalizeAccountPlatformUid, sameAccountPlatformUid, } from '../shared/account-identity' +import { normalizeDesktopApiBaseURL } from '../shared/api-base-url' import { type PublishAccountIdentity, type PublishAccountProfile } from './account-binder' import { ensureAccountReady, @@ -3267,7 +3268,7 @@ function normalizeSession( } return { - api_base_url: session.api_base_url.trim() || 'http://localhost:8080', + api_base_url: normalizeDesktopApiBaseURL(session.api_base_url), mode: session.mode, client_token: session.client_token?.trim() || null, desktop_client: session.desktop_client ? { ...session.desktop_client } : null, diff --git a/apps/desktop-client/src/main/transport/api-client.ts b/apps/desktop-client/src/main/transport/api-client.ts index 5917781..40bc438 100644 --- a/apps/desktop-client/src/main/transport/api-client.ts +++ b/apps/desktop-client/src/main/transport/api-client.ts @@ -35,6 +35,7 @@ import { startObservedRequest, } from '../network-observer' import { canUseRendererDevtoolsProxy, rendererDevtoolsFetch } from '../renderer-devtools-proxy' +import { normalizeDesktopApiBaseURL } from '../../shared/api-base-url' type TransportAuthState = 'pending' | 'registered' | 'expired' type TransportDispatchState = 'idle' | 'connecting' | 'streaming' @@ -64,7 +65,7 @@ interface MonitoringCancelTaskResponse { let desktopApiClient: ApiClient | null = null let transportSession: DesktopTransportSession = { - baseURL: process.env.DESKTOP_API_BASE_URL ?? 'http://localhost:8080', + baseURL: normalizeDesktopApiBaseURL(process.env.DESKTOP_API_BASE_URL), clientToken: null, } const ACCOUNT_UPSERT_CACHE_TTL_MS = 5 * 60_000 @@ -364,7 +365,7 @@ export function configureTransport( ): ApiClient | null { if (!session) { transportSession = { - baseURL: process.env.DESKTOP_API_BASE_URL ?? 'http://localhost:8080', + baseURL: normalizeDesktopApiBaseURL(process.env.DESKTOP_API_BASE_URL), clientToken: null, } transportState.baseURL = transportSession.baseURL @@ -377,7 +378,7 @@ export function configureTransport( } transportSession = { - baseURL: session.api_base_url.trim() || 'http://localhost:8080', + baseURL: normalizeDesktopApiBaseURL(session.api_base_url), clientToken: session.client_token, } transportState.baseURL = transportSession.baseURL diff --git a/apps/desktop-client/src/renderer/composables/useDesktopSession.ts b/apps/desktop-client/src/renderer/composables/useDesktopSession.ts index 5569513..079ede0 100644 --- a/apps/desktop-client/src/renderer/composables/useDesktopSession.ts +++ b/apps/desktop-client/src/renderer/composables/useDesktopSession.ts @@ -13,6 +13,8 @@ import type { UserInfo, } from '@geo/shared-types' +import { normalizeDesktopApiBaseURL } from '../../shared/api-base-url' + type SessionMode = 'authenticated' | 'preview' interface DesktopSession { @@ -35,6 +37,7 @@ const sessionStorageKey = 'geo.desktop.session.v1' const apiBaseURLStorageKey = 'geo.desktop.api-base-url' export const DEFAULT_API_BASE_URL = import.meta.env.VITE_DESKTOP_API_BASE_URL ?? 'https://api.shengxintui.com' +const normalizedDefaultApiBaseURL = normalizeDesktopApiBaseURL(DEFAULT_API_BASE_URL) const session = shallowRef(readStoredSession()) const apiBaseURL = shallowRef(readStoredApiBaseURL()) @@ -77,18 +80,23 @@ function persistSession(next: DesktopSession | null): void { function readStoredApiBaseURL(): string { if (typeof window === 'undefined') { - return DEFAULT_API_BASE_URL + return normalizedDefaultApiBaseURL } - return window.localStorage.getItem(apiBaseURLStorageKey) ?? DEFAULT_API_BASE_URL + const stored = window.localStorage.getItem(apiBaseURLStorageKey) + const normalized = normalizeDesktopApiBaseURL(stored, normalizedDefaultApiBaseURL) + if (stored && stored !== normalized) { + window.localStorage.setItem(apiBaseURLStorageKey, normalized) + } + return normalized } function persistApiBaseURL(value: string): void { - const trimmed = value.trim() || DEFAULT_API_BASE_URL - apiBaseURL.value = trimmed + const normalized = normalizeDesktopApiBaseURL(value, normalizedDefaultApiBaseURL) + apiBaseURL.value = normalized if (typeof window !== 'undefined') { - window.localStorage.setItem(apiBaseURLStorageKey, trimmed) + window.localStorage.setItem(apiBaseURLStorageKey, normalized) } void syncRuntimeSessionToMain() @@ -528,7 +536,7 @@ async function logout(): Promise { if (typeof window !== 'undefined') { window.addEventListener('storage', (event) => { if (event.key === apiBaseURLStorageKey) { - apiBaseURL.value = event.newValue?.trim() || DEFAULT_API_BASE_URL + apiBaseURL.value = normalizeDesktopApiBaseURL(event.newValue, normalizedDefaultApiBaseURL) void syncRuntimeSessionToMain() } }) diff --git a/apps/desktop-client/src/shared/api-base-url.ts b/apps/desktop-client/src/shared/api-base-url.ts new file mode 100644 index 0000000..0692bf9 --- /dev/null +++ b/apps/desktop-client/src/shared/api-base-url.ts @@ -0,0 +1,51 @@ +const DEFAULT_LOCAL_API_BASE_URL = 'http://localhost:8080' +const VITE_DEV_PORT_PATTERN = /^517\d$/ + +function isLoopbackHost(hostname: string): boolean { + const normalized = hostname.toLowerCase() + return ( + normalized === 'localhost' || + normalized === '127.0.0.1' || + normalized === '::1' || + normalized === '[::1]' + ) +} + +function isViteDevOrigin(url: URL): boolean { + return isLoopbackHost(url.hostname) && VITE_DEV_PORT_PATTERN.test(url.port) +} + +function cleanApiBaseURL(url: URL): string { + const pathname = url.pathname.replace(/\/+$/, '') + const normalizedPath = pathname.replace(/\/api$/i, '') + + url.pathname = normalizedPath || '/' + url.search = '' + url.hash = '' + + return url.toString().replace(/\/$/, '') +} + +export function normalizeDesktopApiBaseURL( + value: string | null | undefined, + fallback = DEFAULT_LOCAL_API_BASE_URL, +): string { + const trimmed = String(value ?? '').trim() + const candidate = trimmed || fallback + + try { + const parsed = new URL(candidate) + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + return normalizeDesktopApiBaseURL(fallback, DEFAULT_LOCAL_API_BASE_URL) + } + if (isViteDevOrigin(parsed)) { + return DEFAULT_LOCAL_API_BASE_URL + } + return cleanApiBaseURL(parsed) + } catch { + if (candidate === fallback) { + return DEFAULT_LOCAL_API_BASE_URL + } + return normalizeDesktopApiBaseURL(fallback, DEFAULT_LOCAL_API_BASE_URL) + } +}