import { isTauriRuntime } from "./platform"; export const DESKTOP_SERVER_URL_KEY = "ctms_desktop_server_url"; export const DESKTOP_SERVER_URL_CHANGED_EVENT = "ctms:desktop-server-url-changed"; export type DesktopServerUrlValidationResult = | { ok: true; url: string } | { ok: false; reason: string }; const LOCAL_HTTP_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]); const getStorage = (): Storage | null => { if (typeof window === "undefined") return null; try { return window.localStorage; } catch { return null; } }; const emitServerUrlChanged = (previous: string | null, current: string | null): void => { if (typeof window === "undefined") return; window.dispatchEvent( new CustomEvent(DESKTOP_SERVER_URL_CHANGED_EVENT, { detail: { previous, current }, }), ); }; export const normalizeDesktopServerUrl = (value: string): DesktopServerUrlValidationResult => { const raw = value.trim(); if (!raw) return { ok: false, reason: "请输入服务器地址" }; let parsed: URL; try { parsed = new URL(raw); } catch { return { ok: false, reason: "服务器地址格式不正确" }; } if (parsed.username || parsed.password) { return { ok: false, reason: "服务器地址不能包含用户名或密码" }; } const isHttps = parsed.protocol === "https:"; const isLocalHttp = parsed.protocol === "http:" && LOCAL_HTTP_HOSTS.has(parsed.hostname); if (!isHttps && !isLocalHttp) { return { ok: false, reason: "非本地服务必须使用 HTTPS" }; } return { ok: true, url: `${parsed.origin}/` }; }; export const getDesktopServerUrl = (): string | null => { const stored = getStorage()?.getItem(DESKTOP_SERVER_URL_KEY); if (!stored) return null; const result = normalizeDesktopServerUrl(stored); return result.ok ? result.url : null; }; export const hasDesktopServerUrl = (): boolean => Boolean(getDesktopServerUrl()); export const setDesktopServerUrl = (value: string): DesktopServerUrlValidationResult => { const result = normalizeDesktopServerUrl(value); if (!result.ok) return result; const storage = getStorage(); if (!storage) return { ok: false, reason: "当前环境无法保存服务器地址" }; const previous = getDesktopServerUrl(); storage.setItem(DESKTOP_SERVER_URL_KEY, result.url); emitServerUrlChanged(previous, result.url); return result; }; export const clearDesktopServerUrl = (): void => { const previous = getDesktopServerUrl(); getStorage()?.removeItem(DESKTOP_SERVER_URL_KEY); emitServerUrlChanged(previous, null); }; export const shouldRequireDesktopServerUrl = (): boolean => isTauriRuntime() && !hasDesktopServerUrl();