fix: 补齐 Webterm provider 可见性

This commit is contained in:
Codex
2026-07-11 12:18:21 +02:00
parent a912325697
commit 925e394169
3 changed files with 80 additions and 4 deletions
+1
View File
@@ -766,6 +766,7 @@ function platformInfraHelpSummary(): unknown {
"bun scripts/cli.ts platform-infra webterm apply --confirm",
"bun scripts/cli.ts platform-infra webterm status",
"bun scripts/cli.ts platform-infra webterm validate",
"bun scripts/cli.ts platform-infra webterm logs --target local-7682 --tail 100",
"bun scripts/cli.ts platform-infra wechat-archive plan",
"bun scripts/cli.ts platform-infra wechat-archive apply --confirm",
"bun scripts/cli.ts platform-infra wechat-archive validate --full",
+73 -4
View File
@@ -111,6 +111,11 @@ export async function runWebtermCommand(config: UniDeskConfig, args: string[]):
const result = validate(options.targetId);
return options.full || options.raw ? result : renderValidate(result);
}
if (action === "logs") {
const options = parseOpsCommonOptions(rest, { stringOptions: ["--tail"] });
const result = logs(options.targetId, parseLogTail(options.tail));
return options.full || options.raw ? result : renderLogs(result);
}
return {
ok: false,
error: "unsupported-webterm-command",
@@ -121,13 +126,14 @@ export async function runWebtermCommand(config: UniDeskConfig, args: string[]):
export function webtermHelp(): Record<string, unknown> {
return {
command: "platform-infra webterm plan|apply|status|validate",
command: "platform-infra webterm plan|apply|status|validate|logs",
examples: [
"bun scripts/cli.ts platform-infra webterm plan",
"bun scripts/cli.ts platform-infra webterm apply --dry-run",
"bun scripts/cli.ts platform-infra webterm apply --confirm",
"bun scripts/cli.ts platform-infra webterm status",
"bun scripts/cli.ts platform-infra webterm validate",
"bun scripts/cli.ts platform-infra webterm logs --target local-7682 --tail 100",
],
configPath,
description: "Deploy target-scoped host-Docker Web Terminal instances from YAML and optionally expose them through PK01 Caddy HTTPS.",
@@ -195,7 +201,7 @@ async function apply(config: UniDeskConfig, targetId: string | null, confirm: bo
cwd: target.runtime.workDir,
encoding: "utf8",
});
const localProbe = localHttpProbeWithRetry(target.runtime.hostPort, "/login", 10, 500);
const localProbe = providerHealthProbeWithRetry(target.runtime.hostPort, 10, 500);
const caddy = target.publicExposure.enabled
? await applyPk01CaddySiteBlock(
config,
@@ -231,7 +237,7 @@ function status(targetId: string | null): Record<string, unknown> {
const target = resolveTarget(targetId);
const inspect = spawnSync("docker", ["inspect", target.runtime.containerName], { encoding: "utf8" });
const ps = spawnSync("docker", ["ps", "--filter", `name=^/${target.runtime.containerName}$`, "--format", "{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"], { encoding: "utf8" });
const localProbe = localHttpProbe(target.runtime.hostPort, "/login");
const localProbe = providerHealthProbe(target.runtime.hostPort);
const publicProbe = target.publicExposure.enabled ? publicHttpProbe(target.publicExposure.publicBaseUrl, "/login") : { ok: true, skipped: true };
return {
ok: ps.status === 0 && ps.stdout.trim().length > 0 && localProbe.ok === true && publicProbe.ok === true,
@@ -254,7 +260,7 @@ function status(targetId: string | null): Record<string, unknown> {
function validate(targetId: string | null): Record<string, unknown> {
const target = resolveTarget(targetId);
const dns = dnsProbe(target.publicExposure.dns.hostname);
const localProbe = localHttpProbe(target.runtime.hostPort, "/login");
const localProbe = providerHealthProbe(target.runtime.hostPort);
const publicProbe = target.publicExposure.enabled ? publicHttpProbe(target.publicExposure.publicBaseUrl, "/login") : { ok: true, skipped: true };
return {
ok: localProbe.ok === true && publicProbe.ok === true && dns.addresses.includes(target.publicExposure.dns.expectedA),
@@ -266,6 +272,28 @@ function validate(targetId: string | null): Record<string, unknown> {
};
}
function logs(targetId: string | null, tail: number): Record<string, unknown> {
const target = resolveTarget(targetId);
const result = spawnSync("docker", ["logs", "--tail", String(tail), target.runtime.containerName], { encoding: "utf8" });
const output = `${result.stdout}${result.stderr}`.slice(-20000);
return {
ok: result.status === 0,
target: targetSummary(target),
tail,
logs: output,
truncated: result.stdout.length + result.stderr.length > output.length,
exitCode: result.status,
valuesPrinted: false,
};
}
function parseLogTail(value: unknown): number {
if (value === null || value === undefined) return 100;
const tail = Number(value);
if (!Number.isInteger(tail) || tail < 1 || tail > 1000) throw new Error("--tail must be an integer from 1 to 1000");
return tail;
}
function readWebtermConfig(): WebtermConfig {
const yaml = readYamlRecord(configPath, "platform-infra-webterm");
const defaults = recordField(yaml, "defaults", "webterm");
@@ -511,6 +539,33 @@ function localHttpProbeWithRetry(port: number, path: string, attempts: number, d
return httpProbeWithRetry(() => localHttpProbe(port, path), attempts, delayMs);
}
function providerHealthProbe(port: number): Record<string, unknown> {
const url = `http://127.0.0.1:${port}/healthz`;
const result = spawnSync("curl", ["-fsS", "--connect-timeout", "5", "--max-time", "15", url], { encoding: "utf8" });
let payload: Record<string, unknown> = {};
try {
payload = JSON.parse(result.stdout) as Record<string, unknown>;
} catch {
payload = {};
}
const providers = Number(payload.providers);
const sessions = Number(payload.sessions);
return {
ok: result.status === 0 && payload.ok === true && Number.isInteger(providers) && providers >= 1,
url,
authenticated: false,
providers: Number.isInteger(providers) ? providers : null,
sessions: Number.isInteger(sessions) ? sessions : null,
exitCode: result.status,
error: result.status === 0 && providers < 1 ? "no-provider" : undefined,
stderrTail: result.status === 0 ? "" : result.stderr.slice(-1000),
};
}
function providerHealthProbeWithRetry(port: number, attempts: number, delayMs: number): Record<string, unknown> {
return httpProbeWithRetry(() => providerHealthProbe(port), attempts, delayMs);
}
function publicHttpProbeWithRetry(baseUrl: string, path: string, attempts: number, delayMs: number): Record<string, unknown> {
return httpProbeWithRetry(() => publicHttpProbe(baseUrl, path), attempts, delayMs);
}
@@ -730,6 +785,7 @@ function renderApply(result: Record<string, unknown>): RenderedCliResult {
"CHECKS",
...table(["CHECK", "OK", "DETAIL"], [
["local", bool(localProbe.ok), str(localProbe.url, "-")],
["provider", str(localProbe.providers, "-"), `sessions=${str(localProbe.sessions, "-")}`],
["pk01Caddy", bool(caddy.ok), str(caddy.action, str(caddy.hostname, "-"))],
["public", bool(publicProbe.ok), str(publicProbe.url, "-")],
]),
@@ -756,6 +812,7 @@ function renderStatus(result: Record<string, unknown>): RenderedCliResult {
"CHECKS",
...table(["CHECK", "OK", "DETAIL"], [
["local", bool(localProbe.ok), str(localProbe.url, "-")],
["provider", str(localProbe.providers, "-"), `sessions=${str(localProbe.sessions, "-")}`],
["public", bool(publicProbe.ok), str(publicProbe.url, "-")],
]),
];
@@ -771,12 +828,24 @@ function renderValidate(result: Record<string, unknown>): RenderedCliResult {
...table(["CHECK", "OK", "DETAIL"], [
["dns", bool(dns.ok), values(dns.addresses).map(String).join(",") || "-"],
["local", bool(localProbe.ok), str(localProbe.url, "-")],
["provider", str(localProbe.providers, "-"), `sessions=${str(localProbe.sessions, "-")}`],
["public", bool(publicProbe.ok), str(publicProbe.url, "-")],
]),
];
return rendered(result, "platform-infra webterm validate", lines);
}
function renderLogs(result: Record<string, unknown>): RenderedCliResult {
const target = rec(result.target);
const lines = [
"PLATFORM-INFRA WEBTERM LOGS",
...table(["TARGET", "PORT", "TAIL", "STATUS"], [[str(target.id), str(target.hostPort), str(result.tail), result.ok === false ? "failed" : "ok"]]),
"",
str(result.logs, "-"),
];
return rendered(result, "platform-infra webterm logs", lines);
}
function rendered(result: Record<string, unknown>, command: string, lines: string[]): RenderedCliResult {
return { ok: result.ok !== false, command, renderedText: lines.join("\n"), contentType: "text/plain" };
}