Files
pikasTech-unidesk/scripts/src/platform-infra/entry.ts
T
2026-06-26 10:32:43 +00:00

374 lines
13 KiB
TypeScript

// 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<Sub2ApiConfig["image"]>;
dependencyImages: Partial<Sub2ApiConfig["dependencyImages"]>;
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<typeof requiredSecretKeys[number], string>;
}
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<string, number>;
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 <requestId>",
"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 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 <traceId> [--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 <trc_...> [--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 <requestId>",
"bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status",
],
module: "scripts/src/platform-infra-sub2api-codex.ts",
},
};
}