Merge pull request #1884 from pikasTech/fix/2514-hwlab-entry
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / platform-infra-gitea-nc01- Success
Pipelines as Code CI / unidesk-host- Success

fix: 从受控配置解析 HWLAB 正式入口
This commit is contained in:
Lyon
2026-07-13 09:41:52 +08:00
committed by GitHub
4 changed files with 71 additions and 8 deletions
+3
View File
@@ -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
+3 -2
View File
@@ -1,11 +1,12 @@
import React from "react";
import { LoadingTitle } from "./loading-indicator";
import { externalApplicationConfig } from "./runtime-config";
type AnyRecord = Record<string, any>;
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" }, "后端依赖"),
+39 -6
View File
@@ -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<string, unknown>;
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")
@@ -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, "") };
}