// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. entry module for scripts/src/platform-infra.ts. // Moved mechanically from scripts/src/platform-infra.ts:1-337 for #903. import { createHash, randomBytes } from "node:crypto"; import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, isAbsolute, join } from "node:path"; import type { UniDeskConfig } from "../config"; import { rootPath } from "../config"; import { startJob } from "../jobs"; import type { RenderedCliResult } from "../output"; import { pk01CaddyMergeManagedBlocksPython, renderCaddyManagedBlock, renderSimpleReverseProxyCaddySiteBlock } from "../pk01-caddy"; import { capture, compactCapture, parseJsonOutput, prepareFrpcSecret, shQuote } from "../platform-infra-public-service"; import { yamlBooleanField, yamlFieldLabel, yamlIntegerField } from "../platform-infra-ops-library"; import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets"; import type { SubscriptionNode } from "./pk01-public-exposure"; import { sub2ApiHelpTargetSummary } from "./options"; export const serviceName = "sub2api"; export const fieldManager = "unidesk-platform-infra"; export const manifestPath = rootPath("src", "components", "platform-infra", "sub2api", "sub2api.k8s.yaml"); export const configPath = rootPath("config", "platform-infra", "sub2api.yaml"); export const codexPoolConfigPath = rootPath("config", "platform-infra", "sub2api-codex-pool.yaml"); export const legacySub2apiCaddyManagedMarker = "sub2api"; export function sub2apiCaddyManagedMarker(target: Sub2ApiTargetConfig): string { return target.id.toUpperCase() === "D601" ? legacySub2apiCaddyManagedMarker : `${legacySub2apiCaddyManagedMarker}-${target.id.toLowerCase()}`; } export const requiredSecretKeys = ["POSTGRES_PASSWORD", "ADMIN_PASSWORD", "JWT_SECRET", "TOTP_ENCRYPTION_KEY"] as const; export type DatabaseMode = "bundled" | "external-pending" | "external-active"; export type RedisMode = "bundled-persistent" | "local-ephemeral"; export interface Sub2ApiConfig { version: number; kind: string; metadata: { id: string; owner: string; relatedIssues: number[]; }; defaults: Sub2ApiDefaults; image: { repository: string; tag: string; pullPolicy: "Always" | "IfNotPresent" | "Never"; }; dependencyImages: { postgres: string; redis: string; }; security: { urlAllowlist: { enabled: boolean; allowInsecureHttp: boolean; allowPrivateHosts: boolean; upstreamHosts: string[]; }; }; targets: Sub2ApiTargetConfig[]; runtime: { database: ExternalDatabaseConfig; secrets: RuntimeSecretsConfig; redis: RuntimeRedisConfig; appData: { mode: "persistent-pvc" | "empty-dir"; }; sentinel: { mode: "singleton"; enabledOnTargets: string[]; }; }; } export interface Sub2ApiDefaults { targetId: string; cleanup: { externalDbState: { postgresStatefulSetName: string; postgresServiceName: string; postgresPvcName: string; appDataPvcName: string; }; redisPersistentState: { pvcName: string; }; publicExposure: { deploymentName: string; configMapName: string; secretName: string; }; egressProxy: { deploymentName: string; serviceName: string; secretName: string; }; }; } export interface Sub2ApiTargetConfig { id: string; route: string; namespace: string; role: string; enabled: boolean; databaseMode: DatabaseMode; redisMode: RedisMode; appReplicas: number; redisReplicas: number; image: Partial; dependencyImages: Partial; publicExposure: Sub2ApiPublicExposureConfig | null; egressProxy: Sub2ApiEgressProxyConfig | null; } export interface Sub2ApiPublicExposureConfig { enabled: boolean; publicBaseUrl: string; dns: { hostname: string; expectedA: string; resolvers: string[]; }; frpc: { deploymentName: string; secretName: string; secretKey: string; image: string; serverAddr: string; serverPort: number; proxyName: string; remotePort: number; localIP: string; localPort: number; tokenSourceRef: string; tokenSourceKey: string; }; pk01: { route: string; caddyBinaryPath: string; caddyDownloadUrl: string; caddyDownloadProxyUrl: string | null; caddyConfigPath: string; caddyServiceName: string; caddyStorageDir: string; caddyEmail: string; pikanodeRoot: string; pikanodeContainerName: string; pikanodeImage: string; pikanodeHttpHostPort: number; responseHeaderTimeoutSeconds: number; }; } export interface Sub2ApiEgressProxyConfig { enabled: boolean; deploymentName: string; serviceName: string; secretName: string; secretKey: string; image: string; imagePullPolicy: "Always" | "IfNotPresent" | "Never"; listenPort: number; sourceConfigRef: string | null; sourceFingerprint: string | null; sourceRef: string; sourceKey: string; sourceType: "subscription-url" | "master-shadowsocks"; preferredOutbound: "vless-reality" | "hysteria2" | null; masterShadowsocks: Sub2ApiMasterShadowsocksConfig | null; noProxy: string[]; applyToSub2Api: boolean; applyToSentinel: boolean; healthProbeUrl: string; } export interface Sub2ApiMasterShadowsocksConfig { serverHost: string; serverPort: number; method: string; } export interface ExternalDatabaseConfig { mode: "external"; sourceRef: string; sourceKeys: { user: string; password: string; dbName: string; }; secretName: string; passwordKey: string; host: string; port: number; user: string; dbName: string; sslMode: string; pendingAllowed: boolean; } export interface RuntimeSecretsConfig { root: string; appSourceRef: string; } export interface RuntimeRedisConfig { serviceName: string; persistence: boolean; } export interface ExternalActiveSecretMaterial { sourceRef: string; sourcePath: string; appSourceRef: string; appSourcePath: string; action: "create" | "update" | "none"; fingerprint: string; values: Record; } export interface PublicExposureSecretMaterial { sourceRef: string; sourcePath: string; secretName: string; secretKey: string; fingerprint: string; frpcToml: string; valuesPrinted: false; } export interface EgressProxySecretMaterial { sourceRef: string; sourcePath: string; secretName: string; secretKey: string; fingerprint: string; sourceBytes: number; selectedOutbound: string; sourceDiagnostics: EgressProxySubscriptionDiagnostics; configJson: string; proxyUrl: string; noProxy: string; valuesPrinted: false; } export interface EgressProxySubscriptionCandidateSummary { sourceLine: number; kind: SubscriptionNode["kind"] | "shadowsocks"; fingerprint: string; paramKeys: string[]; selected: boolean; } export interface EgressProxySubscriptionDiagnostics { ok: boolean; sourceType: Sub2ApiEgressProxyConfig["sourceType"]; preferredOutbound: Sub2ApiEgressProxyConfig["preferredOutbound"]; candidateCount: number; supportedCount: number; unsupportedCount: number; byKind: Record; selectedOutbound: string | null; selectedFingerprint: string | null; candidates: EgressProxySubscriptionCandidateSummary[]; error?: string; valuesPrinted: false; } export interface ManagedResourceCleanupPlan { externalDbState: { enabled: boolean; postgresStatefulSetName: string; postgresServiceName: string; postgresPvcName: string; appDataPvcName: string; }; redisPersistentState: { enabled: boolean; pvcName: string; }; publicExposure: { enabled: boolean; deploymentName: string; configMapName: string; secretName: string; }; egressProxy: { enabled: boolean; deploymentName: string; serviceName: string; secretName: string; }; sentinel: { enabled: boolean; cronJobName: string; configMapName: string; credentialsSecretName: string; stateConfigMapName: string; serviceAccountName: string; roleName: string; roleBindingName: string; }; } export function platformInfraHelp(): unknown { const target = sub2ApiHelpTargetSummary(); return { command: "platform-infra sub2api|langbot|n8n|wechat-archive|observability ...", output: "json", usage: [ "bun scripts/cli.ts platform-infra sub2api plan [--target G14|D601]", "bun scripts/cli.ts platform-infra sub2api apply [--target G14|D601] --dry-run", "bun scripts/cli.ts platform-infra sub2api apply [--target G14|D601] --confirm", "bun scripts/cli.ts platform-infra sub2api status [--target G14|D601] [--full|--raw]", "bun scripts/cli.ts platform-infra sub2api validate [--target G14|D601] [--full|--raw]", "bun scripts/cli.ts platform-infra sub2api codex-pool plan", "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm", "bun scripts/cli.ts platform-infra sub2api codex-pool validate", "bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id ", "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status", "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account unidesk-codex-hy --confirm", "bun scripts/cli.ts platform-infra egress-proxy traffic --target D601 --sample-seconds 15", "bun scripts/cli.ts platform-infra langbot plan [--target G14]", "bun scripts/cli.ts platform-infra langbot apply [--target G14] --confirm", "bun scripts/cli.ts platform-infra langbot status [--target G14] [--full|--raw]", "bun scripts/cli.ts platform-infra langbot logs [--target G14] [--component app|plugin-runtime|frpc|all]", "bun scripts/cli.ts platform-infra langbot bootstrap-api-key --confirm", "bun scripts/cli.ts platform-infra langbot query --path /api/v1/platform/bots", "bun scripts/cli.ts platform-infra n8n plan [--target G14]", "bun scripts/cli.ts platform-infra n8n apply [--target G14] --confirm", "bun scripts/cli.ts platform-infra n8n status [--target G14] [--full|--raw]", "bun scripts/cli.ts platform-infra n8n logs [--target G14] [--component app|frpc|all]", "bun scripts/cli.ts platform-infra n8n validate [--target G14] [--full|--raw]", "bun scripts/cli.ts platform-infra wechat-archive plan [--target G14]", "bun scripts/cli.ts platform-infra wechat-archive apply [--target G14] --confirm", "bun scripts/cli.ts platform-infra wechat-archive status [--target G14] [--full|--raw]", "bun scripts/cli.ts platform-infra wechat-archive validate [--target G14] [--full|--raw]", "bun scripts/cli.ts platform-infra wechat-archive pull --remote-path /UniDesk/WeChatArchive/...", "bun scripts/cli.ts platform-infra wechat-archive wcf-host-prepare --confirm", "bun scripts/cli.ts platform-infra wechat-archive wcf-host-start --confirm", "bun scripts/cli.ts platform-infra wechat-archive collector-image-build --confirm", "bun scripts/cli.ts platform-infra wechat-archive collector-apply --confirm", "bun scripts/cli.ts platform-infra wechat-archive collector-status --full", "bun scripts/cli.ts platform-infra observability plan --target D601", "bun scripts/cli.ts platform-infra observability apply --target D601 --dry-run", "bun scripts/cli.ts platform-infra observability apply --target D601 --confirm", "bun scripts/cli.ts platform-infra observability status --target D601", "bun scripts/cli.ts platform-infra observability validate --target D601", "bun scripts/cli.ts platform-infra observability trace --target D601 --trace-id [--grep text] [--limit 40] [--full|--raw]", "bun scripts/cli.ts platform-infra observability search --target D601 --grep 'no rollout found' [--lookback-minutes 360] [--candidate-limit 80] [--limit 20]", "bun scripts/cli.ts platform-infra observability diagnose-code-agent --target D601 --business-trace-id [--full|--raw]", ], description: "Operate YAML-controlled platform-infra services such as Sub2API, LangBot, n8n, WeChat archive workflows and OpenTelemetry tracing. Public services use PK01 Caddy+FRP rather than Kubernetes Ingress, NodePort, or LoadBalancer.", target, codexPool: { usage: [ "bun scripts/cli.ts platform-infra sub2api codex-pool plan", "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm", "bun scripts/cli.ts platform-infra sub2api codex-pool validate", "bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id ", "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status", ], module: "scripts/src/platform-infra-sub2api-codex.ts", }, }; }