import { computed, readonly, shallowRef } from "vue"; import { createApiClient } from "@geo/http-client"; import type { AuthTokens, DesktopClientInfo, DesktopClientRegisterRequest, DesktopClientRegisterResponse, DesktopRuntimeSessionSyncRequest, LoginRequest, LoginResponse, RefreshResponse, UserInfo, } from "@geo/shared-types"; type SessionMode = "authenticated" | "preview"; interface DesktopSession { mode: SessionMode; accessToken: string | null; refreshToken: string | null; user: UserInfo | null; clientToken: string | null; clientTokenExpiresAt: number | null; desktopClient: DesktopClientInfo | null; } interface ResolvedDeviceInfo { device_name: string; os: string; cpu_arch: string; } const sessionStorageKey = "geo.desktop.session.v1"; const apiBaseURLStorageKey = "geo.desktop.api-base-url"; const session = shallowRef(readStoredSession()); const apiBaseURL = shallowRef(readStoredApiBaseURL()); const pending = shallowRef(false); const error = shallowRef(null); function readStoredSession(): DesktopSession | null { if (typeof window === "undefined") { return null; } try { const raw = window.localStorage.getItem(sessionStorageKey); if (!raw) { return null; } return JSON.parse(raw) as DesktopSession; } catch { return null; } } function persistSession(next: DesktopSession | null): void { session.value = next; if (typeof window === "undefined") { return; } if (!next) { window.localStorage.removeItem(sessionStorageKey); } else { window.localStorage.setItem(sessionStorageKey, JSON.stringify(next)); } void syncRuntimeSessionToMain(next); } function readStoredApiBaseURL(): string { if (typeof window === "undefined") { return "http://localhost:8080"; } return window.localStorage.getItem(apiBaseURLStorageKey) ?? "http://localhost:8080"; } function persistApiBaseURL(value: string): void { const trimmed = value.trim() || "http://localhost:8080"; apiBaseURL.value = trimmed; if (typeof window !== "undefined") { window.localStorage.setItem(apiBaseURLStorageKey, trimmed); } void syncRuntimeSessionToMain(); } function createPublicClient() { return createApiClient({ baseURL: apiBaseURL.value, timeoutMs: 15000, }); } function createAuthenticatedClient() { return createApiClient({ baseURL: apiBaseURL.value, timeoutMs: 15000, auth: { getAccessToken: () => session.value?.accessToken ?? null, getRefreshToken: () => session.value?.refreshToken ?? null, setTokens: (tokens: AuthTokens) => { if (!session.value) { return; } persistSession({ ...session.value, accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, }); }, clearTokens: () => persistSession(null), async refresh(refreshToken: string): Promise { const next = await createPublicClient().post( "/api/auth/refresh", { refresh_token: refreshToken }, ); return { accessToken: next.access_token, refreshToken: next.refresh_token, }; }, }, }); } function resolveFallbackPlatform(): string { const currentNavigator = globalThis.navigator as Navigator & { userAgentData?: { platform?: string }; }; return currentNavigator.userAgentData?.platform || currentNavigator.platform || "unknown"; } function resolveFallbackOS(platform: string): string { const normalized = platform.toLowerCase(); if (normalized.includes("mac")) { return "darwin"; } if (normalized.includes("win")) { return "windows"; } if (normalized.includes("linux")) { return "linux"; } return normalized || "unknown"; } function resolveFallbackDeviceName(platform: string): string { const normalized = platform.toLowerCase(); if (normalized.includes("mac")) { return "Current Mac"; } if (normalized.includes("win")) { return "Current Windows PC"; } if (normalized.includes("linux")) { return "Current Linux Desktop"; } return "Current Desktop"; } async function resolveDeviceInfo(): Promise { try { const info = await window.desktopBridge?.app?.deviceInfo?.(); if (info?.device_name && info.os && info.cpu_arch) { return info; } } catch (err) { console.warn("[desktop-session] deviceInfo bridge failed", err); } const platform = resolveFallbackPlatform(); return { device_name: resolveFallbackDeviceName(platform), os: resolveFallbackOS(platform), cpu_arch: "unknown", }; } async function resolveDesktopClientID(user: UserInfo): Promise { const clientID = await window.desktopBridge?.app?.clientId?.({ tenant_id: user.tenant_id, workspace_id: user.primary_workspace_id, user_id: user.id, }); if (!clientID) { throw new Error("无法生成本机账号 client_id,请重启桌面客户端后重试"); } return clientID; } async function syncStoredDesktopClientDeviceInfo(): Promise { const current = session.value; if (typeof window === "undefined" || current?.mode !== "authenticated" || !current.desktopClient || !current.user) { return; } const [device, clientID] = await Promise.all([ resolveDeviceInfo(), resolveDesktopClientID(current.user), ]); if (session.value !== current) { return; } const desktopClient = current.desktopClient; if (desktopClient.id !== clientID) { try { const registered = await registerDesktopClient(current.user, device); const latest = session.value; if (latest?.mode !== "authenticated" || latest.user?.id !== current.user?.id) { return; } persistSession({ ...latest, clientToken: registered.client_token, clientTokenExpiresAt: registered.expires_at, desktopClient: registered.client, }); } catch (err) { console.warn("[desktop-session] stable client migration failed", err); } return; } if ( desktopClient.device_name === device.device_name && desktopClient.os === device.os && desktopClient.cpu_arch === device.cpu_arch ) { return; } persistSession({ ...current, desktopClient: { ...desktopClient, device_name: device.device_name, os: device.os, cpu_arch: device.cpu_arch, }, }); } async function registerPayload( user: UserInfo, device?: ResolvedDeviceInfo, ): Promise { const resolvedDevice = device ?? await resolveDeviceInfo(); return { client_id: await resolveDesktopClientID(user), device_name: resolvedDevice.device_name, os: resolvedDevice.os, cpu_arch: resolvedDevice.cpu_arch, client_version: "0.1.0-dev", channel: "dev", }; } async function registerDesktopClient( user: UserInfo, device?: ResolvedDeviceInfo, ): Promise { const resolvedDevice = device ?? await resolveDeviceInfo(); return createAuthenticatedClient().post< DesktopClientRegisterResponse, DesktopClientRegisterRequest >("/api/desktop/clients/register", await registerPayload(user, resolvedDevice)); } function createPreviewUser(): UserInfo { return { id: 0, email: "preview@geo-rankly.local", name: "开发预览账号", avatar_url: null, tenant_id: 0, primary_workspace_id: 0, tenant_role: "owner", permissions: ["desktop.preview"], membership: null, kol_profile: null, }; } async function createPreviewClient(): Promise { const now = new Date().toISOString(); const device = await resolveDeviceInfo(); return { id: "preview-client", tenant_id: 0, workspace_id: 0, user_id: 0, device_name: `Preview ${device.device_name}`, os: device.os, cpu_arch: device.cpu_arch, client_version: "0.1.0-preview", channel: "dev", created_at: now, last_seen_at: now, last_rotated_at: now, revoked_at: null, }; } function buildRuntimeSessionPayload(next: DesktopSession | null): DesktopRuntimeSessionSyncRequest | null { if (!next) { return null; } return { api_base_url: apiBaseURL.value, mode: next.mode, client_token: next.clientToken, desktop_client: next.desktopClient, }; } async function syncRuntimeSessionToMain(next: DesktopSession | null = session.value): Promise { if (typeof window === "undefined") { return; } if (!window.desktopBridge?.app?.syncRuntimeSession) { return; } await window.desktopBridge.app.syncRuntimeSession(buildRuntimeSessionPayload(next)); } async function login(payload: LoginRequest): Promise { pending.value = true; error.value = null; try { const authData = await createPublicClient().post( "/api/auth/login", payload, ); persistSession({ mode: "authenticated", accessToken: authData.access_token, refreshToken: authData.refresh_token, user: authData.user, clientToken: null, clientTokenExpiresAt: null, desktopClient: null, }); const registered = await registerDesktopClient(authData.user); persistSession({ mode: "authenticated", accessToken: authData.access_token, refreshToken: authData.refresh_token, user: authData.user, clientToken: registered.client_token, clientTokenExpiresAt: registered.expires_at, desktopClient: registered.client, }); } catch (err) { error.value = err instanceof Error ? err.message : "登录失败,请稍后重试"; persistSession(null); throw err; } finally { pending.value = false; } } async function enterPreview(): Promise { error.value = null; persistSession({ mode: "preview", accessToken: null, refreshToken: null, user: createPreviewUser(), clientToken: "preview-client-token", clientTokenExpiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, desktopClient: await createPreviewClient(), }); } async function logout(): Promise { error.value = null; const current = session.value; if (current?.mode === "authenticated") { try { await window.desktopBridge?.app?.releaseRuntimeSession?.(true); } catch (err) { console.warn("[desktop-session] releaseRuntimeSession failed", err); } try { await createAuthenticatedClient().post>("/api/auth/logout", {}); } catch (err) { console.warn("[desktop-session] auth logout failed", err); } } persistSession(null); } if (typeof window !== "undefined") { void syncStoredDesktopClientDeviceInfo(); } export function useDesktopSession() { return { session: readonly(session), apiBaseURL: readonly(apiBaseURL), pending: readonly(pending), error: readonly(error), isAuthenticated: computed(() => Boolean(session.value?.clientToken)), isPreviewMode: computed(() => session.value?.mode === "preview"), setApiBaseURL: persistApiBaseURL, syncRuntimeSession: syncRuntimeSessionToMain, login, logout, enterPreview, }; }