// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. remote module for scripts/src/platform-infra-sub2api-codex.ts. // Moved mechanically from scripts/src/platform-infra-sub2api-codex.ts:7231-7600 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 { codexPoolRuntimeTarget } from "./runtime-target"; import { remoteJobDir, remoteJobPollMs, remoteJobTimeoutMs } from "./types"; export async function capture(config: UniDeskConfig, target: string, args: string[], input?: string): Promise { return await runSshCommandCapture(config, target, args, input); } export type RemoteCodexPoolMode = "sync" | "validate" | "sentinel-probe" | "sentinel-image-status" | "sentinel-image-build"; export async function runRemoteCodexPoolScript(config: UniDeskConfig, mode: RemoteCodexPoolMode, script: string, target = codexPoolRuntimeTarget()): Promise { const jobName = `codex-pool-${mode}-${Date.now().toString(36)}`.slice(0, 63); const startedAtMs = Date.now(); const start = await capture(config, target.route, ["sh"], remoteJobStartScript(jobName, script)); const started = parseJsonOutput(start.stdout); if (start.exitCode !== 0 || boolField(started, "ok", false) !== true) return start; let latest: RemoteCodexPoolJobStatus | null = null; while (Date.now() - startedAtMs <= remoteJobTimeoutMs) { await sleep(remoteJobPollMs); const probe = await capture(config, target.route, ["sh"], remoteJobStatusScript(jobName)); const parsed = parseJsonOutput(probe.stdout); latest = normalizeRemoteJobStatus(parsed); process.stderr.write(`${JSON.stringify({ event: "platform-infra.sub2api.codex-pool.progress", at: new Date().toISOString(), mode, jobName, status: latest?.status ?? "unknown", exitCode: latest?.exitCode ?? null, elapsedMs: Date.now() - startedAtMs, stdoutBytes: latest?.stdoutBytes ?? null, stderrBytes: latest?.stderrBytes ?? null, })}\n`); if (probe.exitCode !== 0) { return { exitCode: probe.exitCode, stdout: probe.stdout, stderr: `${start.stderr}${probe.stderr}`, }; } if (latest?.status === "succeeded" || latest?.status === "failed") { return { exitCode: latest.exitCode ?? (latest.status === "succeeded" ? 0 : 1), stdout: latest.stdout ?? "", stderr: latest.stderr ?? "", }; } } return { exitCode: 124, stdout: latest?.stdout ?? "", stderr: [ latest?.stderr ?? "", `remote codex-pool ${mode} job ${jobName} did not finish within ${remoteJobTimeoutMs}ms`, `status command: ${codexPoolModeCommand(mode)}`, ].filter(Boolean).join("\n"), }; } export function codexPoolModeCommand(mode: RemoteCodexPoolMode): string { if (mode === "sync") return "bun scripts/cli.ts platform-infra sub2api codex-pool sync --confirm"; if (mode === "sentinel-probe") return "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account --confirm"; if (mode === "sentinel-image-status") return "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status"; if (mode === "sentinel-image-build") return "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --confirm"; return "bun scripts/cli.ts platform-infra sub2api codex-pool validate"; } export interface RemoteCodexPoolJobStatus { status: "running" | "succeeded" | "failed" | "unknown"; exitCode: number | null; stdout: string | null; stderr: string | null; stdoutBytes: number | null; stderrBytes: number | null; } export function normalizeRemoteJobStatus(parsed: Record | null): RemoteCodexPoolJobStatus | null { if (parsed === null) return null; const status = parsed.status === "running" || parsed.status === "succeeded" || parsed.status === "failed" || parsed.status === "unknown" ? parsed.status : "unknown"; return { status, exitCode: typeof parsed.exitCode === "number" ? parsed.exitCode : null, stdout: typeof parsed.stdout === "string" ? parsed.stdout : null, stderr: typeof parsed.stderr === "string" ? parsed.stderr : null, stdoutBytes: typeof parsed.stdoutBytes === "number" ? parsed.stdoutBytes : null, stderrBytes: typeof parsed.stderrBytes === "number" ? parsed.stderrBytes : null, }; } export function remoteJobStartScript(jobName: string, script: string): string { const scriptB64 = Buffer.from(script, "utf8").toString("base64"); return ` set -eu job=${shQuote(jobName)} dir=${shQuote(remoteJobDir)} mkdir -p "$dir" chmod 700 "$dir" script_path="$dir/$job.sh" stdout_path="$dir/$job.stdout" stderr_path="$dir/$job.stderr" exit_path="$dir/$job.exit" done_path="$dir/$job.done" pid_path="$dir/$job.pid" rm -f "$script_path" "$stdout_path" "$stderr_path" "$exit_path" "$done_path" "$pid_path" base64 -d > "$script_path" <<'UNIDESK_REMOTE_SCRIPT_B64' ${scriptB64} UNIDESK_REMOTE_SCRIPT_B64 chmod 700 "$script_path" ( set +e trap '' HUP sh "$script_path" > "$stdout_path" 2> "$stderr_path" code=$? printf '%s' "$code" > "$exit_path" date -u +%Y-%m-%dT%H:%M:%SZ > "$done_path" rm -f "$script_path" ) >/dev/null 2>&1 & pid=$! printf '%s' "$pid" > "$pid_path" python3 - < limit: data = data[-limit:] return data.decode("utf-8", errors="replace") def file_size(path): try: return os.path.getsize(path) except FileNotFoundError: return 0 pid_text = read_text(pid_path) done = os.path.exists(done_path) exit_text = read_text(exit_path) exit_code = None if exit_text is not None: try: exit_code = int(exit_text.strip()) except ValueError: exit_code = None running = False if pid_text is not None and not done: try: os.kill(int(pid_text.strip()), 0) running = True except Exception: running = False status = "succeeded" if done and exit_code == 0 else "failed" if done else "running" if running else "unknown" print(json.dumps({ "ok": True, "jobName": job, "status": status, "pid": int(pid_text.strip()) if pid_text and pid_text.strip().isdigit() else None, "exitCode": exit_code, "stdoutBytes": file_size(stdout_path), "stderrBytes": file_size(stderr_path), "stdout": read_text(stdout_path, 500000) if done else read_text(stdout_path, 12000), "stderr": read_text(stderr_path, 120000) if done else read_text(stderr_path, 12000), "doneAt": read_text(done_path), }, ensure_ascii=False)) PY `; } export function shQuote(value: string): string { return `'${value.replace(/'/gu, `'\\''`)}'`; } export function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } export function parseJsonOutput(stdout: string): Record | null { const trimmed = stdout.trim(); if (trimmed.length === 0) return null; const end = trimmed.lastIndexOf("}"); if (end === -1) return null; const starts = [trimmed.indexOf("{")]; for (let index = trimmed.lastIndexOf("\n{"); index !== -1; index = trimmed.lastIndexOf("\n{", index - 1)) { starts.push(index + 1); } for (const start of starts.filter((item) => item !== -1 && item < end).sort((left, right) => right - left)) { try { const parsed = JSON.parse(trimmed.slice(start, end + 1)) as unknown; return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed as Record : null; } catch { continue; } } return null; } export function boolField(value: Record | null, key: string, defaultValue: boolean): boolean { if (value === null) return defaultValue; const field = value[key]; return typeof field === "boolean" ? field : defaultValue; } export function compactCapture(result: SshCaptureResult, options: { full?: boolean } = {}): Record { const full = options.full ?? false; return { exitCode: result.exitCode, stdoutBytes: Buffer.byteLength(result.stdout, "utf8"), stderrBytes: Buffer.byteLength(result.stderr, "utf8"), stdoutTail: full || result.exitCode !== 0 ? result.stdout.slice(-8000) : "", stderrTail: full || result.exitCode !== 0 ? result.stderr.slice(-4000) : "", }; }