diff --git a/config/frontend.yaml b/config/frontend.yaml index ea4fc089..187fc69e 100644 --- a/config/frontend.yaml +++ b/config/frontend.yaml @@ -6,6 +6,9 @@ displayTime: timeZone: Asia/Shanghai locale: zh-CN label: 北京时间 +applications: + hwlab: + publicUrl: https://hwlab.pikapython.com publicExposure: enabled: true publicBaseUrl: https://unidesk.pikapython.com diff --git a/src/components/frontend/src/hwlab.tsx b/src/components/frontend/src/hwlab.tsx index b5d2e42a..f74bdd95 100644 --- a/src/components/frontend/src/hwlab.tsx +++ b/src/components/frontend/src/hwlab.tsx @@ -1,11 +1,12 @@ import React from "react"; import { LoadingTitle } from "./loading-indicator"; +import { externalApplicationConfig } from "./runtime-config"; type AnyRecord = Record; const h = React.createElement; -export const HWLAB_URL = "http://74.48.78.17:6666"; +export const HWLAB_URL = externalApplicationConfig("hwlab").publicUrl; function StatusBadge({ status, children }: AnyRecord) { const normalized = String(status || "unknown").toLowerCase(); @@ -81,7 +82,7 @@ export function HwlabPage(): any { h("article", { className: "metric-card" }, h("div", { className: "metric-label" }, "目标 URL"), h("div", { className: "metric-value hwlab-url" }, HWLAB_URL), - h("div", { className: "metric-hint" }, "fixed destination"), + h("div", { className: "metric-hint" }, "config/frontend.yaml"), ), h("article", { className: "metric-card" }, h("div", { className: "metric-label" }, "后端依赖"), diff --git a/src/components/frontend/src/index.ts b/src/components/frontend/src/index.ts index bcad3675..87a0303b 100644 --- a/src/components/frontend/src/index.ts +++ b/src/components/frontend/src/index.ts @@ -11,6 +11,7 @@ interface RuntimeConfig { frontendPublicUrl: string; providerIngressPublicUrl: string; displayTime: DisplayTimeConfig; + applications: FrontendApplicationsConfig; authUsername: string; authPassword: string; providerToken: string | null; @@ -46,6 +47,14 @@ interface DisplayTimeConfig { label: string; } +interface FrontendApplicationsConfig { + hwlab: ExternalApplicationConfig; +} + +interface ExternalApplicationConfig { + publicUrl: string; +} + interface SessionPayload { username: string; expiresAt: number; @@ -96,6 +105,7 @@ const clientConfig = JSON.stringify({ frontendPublicUrl: config.frontendPublicUrl, providerIngressPublicUrl: config.providerIngressPublicUrl, displayTime: config.displayTime, + applications: config.applications, authUsername: config.authUsername, sessionTtlSeconds: config.sessionTtlSeconds, apiBaseUrl: "/api", @@ -214,21 +224,42 @@ function validateLocale(value: string, path: string): void { } } -function readDisplayTimeConfig(): DisplayTimeConfig { +function validateHttpUrl(value: string, path: string): void { + let parsed: URL; + try { + parsed = new URL(value); + } catch { + throw new Error(`${path} must be a valid URL`); + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new Error(`${path} must use http or https`); + } +} + +function readFrontendFileConfig(): { displayTime: DisplayTimeConfig; applications: FrontendApplicationsConfig } { const configPath = join(import.meta.dir, "../../../..", "config", "frontend.yaml"); const parsed = Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown; if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("config/frontend.yaml must be an object"); const root = parsed as Record; if (root.kind !== "UniDeskFrontendConfig") throw new Error("config/frontend.yaml.kind must be UniDeskFrontendConfig"); const displayTime = recordField(root, "displayTime", "config/frontend.yaml"); - const result = { + const displayTimeConfig = { timeZone: requiredStringField(displayTime, "timeZone", "config/frontend.yaml.displayTime"), locale: requiredStringField(displayTime, "locale", "config/frontend.yaml.displayTime"), label: requiredStringField(displayTime, "label", "config/frontend.yaml.displayTime"), }; - validateTimeZone(result.timeZone, "config/frontend.yaml.displayTime.timeZone"); - validateLocale(result.locale, "config/frontend.yaml.displayTime.locale"); - return result; + validateTimeZone(displayTimeConfig.timeZone, "config/frontend.yaml.displayTime.timeZone"); + validateLocale(displayTimeConfig.locale, "config/frontend.yaml.displayTime.locale"); + const applications = recordField(root, "applications", "config/frontend.yaml"); + const hwlab = recordField(applications, "hwlab", "config/frontend.yaml.applications"); + const hwlabPublicUrl = requiredStringField(hwlab, "publicUrl", "config/frontend.yaml.applications.hwlab"); + validateHttpUrl(hwlabPublicUrl, "config/frontend.yaml.applications.hwlab.publicUrl"); + return { + displayTime: displayTimeConfig, + applications: { + hwlab: { publicUrl: hwlabPublicUrl }, + }, + }; } function readNumberEnv(name: string): number { @@ -247,6 +278,7 @@ function deployEnv(name: "REF" | "SERVICE_ID" | "REPO" | "COMMIT" | "REQUESTED_C } function readConfig(): RuntimeConfig { + const frontendFileConfig = readFrontendFileConfig(); const serviceId = deployEnv("SERVICE_ID") || "frontend"; const repo = deployEnv("REPO"); const commit = deployEnv("COMMIT"); @@ -256,7 +288,8 @@ function readConfig(): RuntimeConfig { coreInternalUrl: requiredEnv("CORE_INTERNAL_URL"), frontendPublicUrl: requiredEnv("FRONTEND_PUBLIC_URL"), providerIngressPublicUrl: requiredEnv("PROVIDER_INGRESS_PUBLIC_URL"), - displayTime: readDisplayTimeConfig(), + displayTime: frontendFileConfig.displayTime, + applications: frontendFileConfig.applications, authUsername: requiredEnv("AUTH_USERNAME"), authPassword: requiredEnv("AUTH_PASSWORD"), providerToken: optionalEnv("PROVIDER_TOKEN") diff --git a/src/components/frontend/src/runtime-config.ts b/src/components/frontend/src/runtime-config.ts index bbe9e0f9..93ddcb2e 100644 --- a/src/components/frontend/src/runtime-config.ts +++ b/src/components/frontend/src/runtime-config.ts @@ -6,6 +6,10 @@ export interface DisplayTimeConfig { label: string; } +export interface ExternalApplicationConfig { + publicUrl: string; +} + let cachedFrontendConfig: AnyRecord | null = null; let cachedDisplayTimeConfig: DisplayTimeConfig | null = null; @@ -69,3 +73,25 @@ export function displayTimeConfig(): DisplayTimeConfig { cachedDisplayTimeConfig = config; return config; } + +export function externalApplicationConfig(id: string): ExternalApplicationConfig { + const applications = frontendConfig().applications; + if (typeof applications !== "object" || applications === null || Array.isArray(applications)) { + throw new Error("frontend data-config.applications is required"); + } + const application = (applications as AnyRecord)[id]; + if (typeof application !== "object" || application === null || Array.isArray(application)) { + throw new Error(`frontend data-config.applications.${id} is required`); + } + const publicUrl = requiredStringField(application as AnyRecord, "publicUrl", `frontend data-config.applications.${id}`); + let parsed: URL; + try { + parsed = new URL(publicUrl); + } catch { + throw new Error(`frontend data-config.applications.${id}.publicUrl must be a valid URL`); + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new Error(`frontend data-config.applications.${id}.publicUrl must use http or https`); + } + return { publicUrl: parsed.toString().replace(/\/$/u, "") }; +}