161 lines
6.6 KiB
TypeScript
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,
|
|
}));
|
|
}
|