Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running turns active while users steer or queue follow-ups and edit pending delivery. Refresh the model catalog before generation and preserve approval mode selection. Integrate scoped investigation messaging/presentation, geographic place selection, current Agent composer styles and supporting workbench contracts. Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and production build. Preserve direct Server/Agent routing and project authentication.
126 lines
4.4 KiB
TypeScript
126 lines
4.4 KiB
TypeScript
import { z } from "zod";
|
|
|
|
const runtimeBoolean = (defaultValue: boolean) =>
|
|
z
|
|
.enum(["true", "false"])
|
|
.default(defaultValue ? "true" : "false")
|
|
.transform((value) => value === "true");
|
|
|
|
const browserHttpUrl = z
|
|
.string()
|
|
.url()
|
|
.superRefine((value, context) => {
|
|
const url = new URL(value);
|
|
if (!["http:", "https:"].includes(url.protocol)) {
|
|
context.addIssue({ code: z.ZodIssueCode.custom, message: "仅支持 HTTP(S) 地址" });
|
|
}
|
|
if (url.username || url.password) {
|
|
context.addIssue({ code: z.ZodIssueCode.custom, message: "浏览器运行时地址不能包含凭据" });
|
|
}
|
|
if (url.hash) {
|
|
context.addIssue({ code: z.ZodIssueCode.custom, message: "浏览器运行时地址不能包含片段" });
|
|
}
|
|
});
|
|
|
|
const optionalBrowserHttpUrl = z
|
|
.string()
|
|
.trim()
|
|
.default("")
|
|
.superRefine((value, context) => {
|
|
if (!value) return;
|
|
const parsed = browserHttpUrl.safeParse(value);
|
|
if (!parsed.success) {
|
|
context.addIssue({ code: z.ZodIssueCode.custom, message: "必须是安全的 HTTP(S) 地址" });
|
|
}
|
|
})
|
|
.transform((value) => value.replace(/\/+$/, ""));
|
|
|
|
const keycloakIssuer = z
|
|
.string()
|
|
.trim()
|
|
.default("")
|
|
.superRefine((value, context) => {
|
|
if (!value) return;
|
|
const parsed = browserHttpUrl.safeParse(value);
|
|
if (!parsed.success) {
|
|
context.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "Keycloak issuer 必须是安全的 HTTP(S) 地址"
|
|
});
|
|
return;
|
|
}
|
|
const url = new URL(value);
|
|
if (url.search || !/\/realms\/[^/]+\/?$/.test(url.pathname)) {
|
|
context.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "Keycloak issuer 必须指向具体 Realm"
|
|
});
|
|
}
|
|
})
|
|
.transform((value) => value.replace(/\/+$/, ""));
|
|
|
|
const runtimeConfigSchema = z
|
|
.object({
|
|
TJWATER_AUTH_MODE: z.enum(["required", "disabled"]).default("required"),
|
|
TJWATER_KEYCLOAK_ISSUER: keycloakIssuer,
|
|
TJWATER_KEYCLOAK_CLIENT_ID: z.string().trim().default("next-tjwater"),
|
|
TJWATER_MAPBOX_ACCESS_TOKEN: z.string().default(""),
|
|
TJWATER_TIANDITU_TOKEN: z.string().default(""),
|
|
TJWATER_MAP_URL: browserHttpUrl.default("https://geoserver.waternetwork.cn/geoserver"),
|
|
TJWATER_GEOSERVER_WORKSPACE: z.string().trim().min(1).default("tjwater_next"),
|
|
TJWATER_SERVER_API_BASE_URL: browserHttpUrl.default("http://127.0.0.1:8000"),
|
|
TJWATER_AGENT_URL: browserHttpUrl.default("http://127.0.0.1:8787"),
|
|
TJWATER_WORKBENCH_RUNTIME_MODE: z.enum(["auto", "demo", "history", "backend"]).default("auto"),
|
|
TJWATER_PERIPHERAL_SAMPLE_API_BASE_URL: optionalBrowserHttpUrl,
|
|
TJWATER_PERIPHERAL_SAMPLE_TIME_MODE: z.enum(["demo", "recent"]).default("demo"),
|
|
TJWATER_PERIPHERAL_SAMPLE_DEMO_START: z
|
|
.string()
|
|
.datetime({ offset: true })
|
|
.default("2026-09-17T00:00:00Z"),
|
|
TJWATER_PERIPHERAL_SAMPLE_DEMO_END: z
|
|
.string()
|
|
.datetime({ offset: true })
|
|
.default("2026-09-17T12:00:00Z"),
|
|
TJWATER_PERIPHERAL_SAMPLE_RECENT_HOURS: z.coerce.number().int().min(1).max(168).default(8),
|
|
TJWATER_ENABLE_DEV_PANEL: runtimeBoolean(false),
|
|
TJWATER_ENABLE_MSW: runtimeBoolean(false)
|
|
})
|
|
.superRefine((config, context) => {
|
|
const demoStart = Date.parse(config.TJWATER_PERIPHERAL_SAMPLE_DEMO_START);
|
|
const demoEnd = Date.parse(config.TJWATER_PERIPHERAL_SAMPLE_DEMO_END);
|
|
if (demoEnd <= demoStart || demoEnd - demoStart > 7 * 24 * 60 * 60 * 1000) {
|
|
context.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["TJWATER_PERIPHERAL_SAMPLE_DEMO_END"],
|
|
message: "外围演示窗口必须晚于开始时间且不超过 7 天"
|
|
});
|
|
}
|
|
if (config.TJWATER_AUTH_MODE !== "required") return;
|
|
if (!config.TJWATER_KEYCLOAK_ISSUER) {
|
|
context.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["TJWATER_KEYCLOAK_ISSUER"],
|
|
message: "启用认证时必须配置 Keycloak issuer"
|
|
});
|
|
}
|
|
if (!config.TJWATER_KEYCLOAK_CLIENT_ID) {
|
|
context.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["TJWATER_KEYCLOAK_CLIENT_ID"],
|
|
message: "启用认证时必须配置 Keycloak client ID"
|
|
});
|
|
}
|
|
});
|
|
|
|
declare global {
|
|
var __TJWATER_CONFIG__: unknown;
|
|
}
|
|
|
|
export type RuntimeConfig = z.infer<typeof runtimeConfigSchema>;
|
|
|
|
export function parseRuntimeConfig(value: unknown): RuntimeConfig {
|
|
return runtimeConfigSchema.parse(value ?? {});
|
|
}
|
|
|
|
export const env = parseRuntimeConfig(globalThis.__TJWATER_CONFIG__);
|