Files
pikasTech-unidesk/scripts/src/platform-infra-sub2api-codex/config-utils.ts
T
2026-06-25 16:16:25 +00:00

161 lines
6.6 KiB
TypeScript

// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. config-utils module for scripts/src/platform-infra-sub2api-codex.ts.
// Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:3686-3799 for #903.
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { UniDeskConfig } from "../config";
import { rootPath } from "../config";
import type { RenderedCliResult } from "../output";
import { applyPk01CaddyBlock, prepareFrpcSecret, renderFrpcManifest, type PublicServiceExposure, type PublicServiceTarget } from "../platform-infra-public-service";
import { shortSha256Fingerprint } from "../platform-infra-ops-library";
import {
codexPoolSentinelSummary,
codexPoolSentinelRuntimeImage,
readCodexPoolSentinelConfig,
renderCodexPoolSentinelManifest,
type CodexPoolSentinelConfig,
type CodexPoolSentinelProfileSecret,
} from "../platform-infra-sub2api-codex-sentinel";
import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
import type { CodexSentinelProtectPolicy } from "./types";
import { codexPoolConfigPath } from "./types";
export function normalizeBaseUrl(value: string | null): string | null {
if (value === null) return null;
const trimmed = value.trim().replace(/\/+$/u, "");
if (trimmed.length === 0) return null;
try {
const parsed = new URL(trimmed);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
return parsed.toString().replace(/\/+$/u, "");
} catch {
return null;
}
}
export function stringValue(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
export function requiredStringConfigField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringValue(obj[key]);
const prefix = path.length > 0 ? `${path}.` : "";
if (value === null) throw new Error(`${codexPoolConfigPath}.${prefix}${key} must be a non-empty string`);
return value;
}
export function readRequiredDescription(value: unknown, key: string, maxChars: number): string {
const text = stringValue(value);
if (text === null) throw new Error(`${codexPoolConfigPath}.${key} must be a non-empty string`);
if (text.length > maxChars) throw new Error(`${codexPoolConfigPath}.${key} must be at most ${maxChars} characters`);
if (/[\r\n]/u.test(text)) throw new Error(`${codexPoolConfigPath}.${key} must not contain newlines`);
return text;
}
export function readRequiredEmail(value: unknown, key: string): string {
const text = stringValue(value);
if (text === null) throw new Error(`${codexPoolConfigPath}.${key} must be a non-empty string`);
if (!/^[^@\s]+@[^@\s]+[.][^@\s]+$/u.test(text)) throw new Error(`${codexPoolConfigPath}.${key} must be an email address`);
return text;
}
export function readRequiredBaseUrl(value: unknown, key: string): string {
const baseUrl = normalizeBaseUrl(stringValue(value));
if (baseUrl === null) throw new Error(`${codexPoolConfigPath}.${key} must be a valid http(s) URL`);
return baseUrl;
}
export function readRequiredPort(value: unknown, key: string): number {
const port = numberValue(value);
if (port === null || !Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error(`${codexPoolConfigPath}.${key} must be an integer port`);
}
return port;
}
export function readRequiredPositiveNumber(value: unknown, key: string): number {
const number = numberValue(value);
if (number === null || number <= 0) throw new Error(`${codexPoolConfigPath}.${key} must be > 0`);
return number;
}
export function requiredRecordConfigField(obj: Record<string, unknown>, key: string, path: string): Record<string, unknown> {
const value = obj[key];
const prefix = path.length > 0 ? `${path}.` : "";
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.${prefix}${key} must be a YAML object`);
return value;
}
export function integerConfigField(obj: Record<string, unknown>, key: string, path: string): number {
const value = obj[key];
const prefix = path.length > 0 ? `${path}.` : "";
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${codexPoolConfigPath}.${prefix}${key} must be an integer`);
return value;
}
export function integerArrayConfigField(obj: Record<string, unknown>, key: string, path: string): number[] {
const value = obj[key];
const prefix = path.length > 0 ? `${path}.` : "";
if (!Array.isArray(value) || !value.every((item) => typeof item === "number" && Number.isInteger(item))) {
throw new Error(`${codexPoolConfigPath}.${prefix}${key} must be an array of integers`);
}
return value;
}
export function numberValue(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim().length > 0) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
export function booleanValue(value: unknown): boolean | null {
if (typeof value === "boolean") return value;
if (typeof value === "string") {
if (value.trim() === "true") return true;
if (value.trim() === "false") return false;
}
return null;
}
export function validateKubernetesName(value: string, key: string, strictDns: boolean): void {
const pattern = strictDns ? /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u : /^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/u;
if (!pattern.test(value)) throw new Error(`${codexPoolConfigPath}.${key} has an unsupported format`);
}
export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function fingerprint(value: string): string {
return shortSha256Fingerprint(value);
}
export function codexPoolSentinelProbeConfigFingerprint(input: {
accountName: string;
profile: string;
baseUrl: string;
apiKeyFingerprint: string | null;
upstreamUserAgent: string | null;
openaiResponsesWebSocketsV2Mode: string | null;
trustUpstream: boolean;
sentinelProtect: CodexSentinelProtectPolicy;
}): string {
return fingerprint(JSON.stringify({
accountName: input.accountName,
profile: input.profile,
baseUrl: normalizeBaseUrl(input.baseUrl) ?? input.baseUrl,
apiKeyFingerprint: input.apiKeyFingerprint,
upstreamUserAgent: input.upstreamUserAgent,
openaiResponsesWebSocketsV2Mode: input.openaiResponsesWebSocketsV2Mode,
trustUpstream: input.trustUpstream,
sentinelProtect: input.sentinelProtect,
}));
}