feat: 增加 Sub2API 原生公告受控 CLI

This commit is contained in:
Codex
2026-07-15 05:29:37 +02:00
parent 407ca55c70
commit 2ab5fb87a2
11 changed files with 287 additions and 4 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ bun scripts/cli.ts platform-infra sub2api image-prepull --target PK01 --confirm
## 何时读取 reference
- 部署、状态、target 边界、PK01 host-Docker、k3s target、egress proxy、镜像升级:读 [references/operations.md](references/operations.md)。
- Codex pool、统一 key、runtime CRUD、精准批量、模型探测、trace、account temp-unschedulable、`codex-pool sync|validate`:读 [references/codex-pool.md](references/codex-pool.md)。
- Codex pool、统一 key、runtime CRUD、精准批量、模型探测、原生公告、trace、account temp-unschedulable、`codex-pool sync|validate`:读 [references/codex-pool.md](references/codex-pool.md)。
- Sentinel、marker-only 判定、账号冻结/恢复、`sentinel-report|sentinel-probe|sentinel-image`:读 [references/sentinel.md](references/sentinel.md)。
- 受保护手动账号代理、分组绑定、WebUI account test:读 [references/manual-accounts.md](references/manual-accounts.md)。
- 添加或删除上游 profile/account:读 [references/upstreams.md](references/upstreams.md)。
@@ -111,6 +111,9 @@ bun scripts/cli.ts platform-infra sub2api codex-pool profit --target PK01 --mont
bun scripts/cli.ts platform-infra sub2api codex-pool profit --target PK01 --month 2026-07 --forecast
bun scripts/cli.ts platform-infra sub2api codex-pool profit --target PK01 --month <YYYY-MM> --forecast --sale-rate <CNY/API_USD> --scenario-without-account '<exact-account-name>'
bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --target PK01 --active-since 24h --amount-usd 20 --reason <text>
bun scripts/cli.ts platform-infra sub2api codex-pool announcements list --target PK01
bun scripts/cli.ts platform-infra sub2api codex-pool announcements get --target PK01 --id <announcement-id>
bun scripts/cli.ts platform-infra sub2api codex-pool announcements create --target PK01 --title <text> --content <markdown> --targeting-json '{"any_of":[]}'
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 --target D601
bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image build --target D601 --confirm
@@ -154,6 +157,13 @@ bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --target D60
- 确认写入为每个用户生成由精确名单、金额和原因确定的 `Idempotency-Key`,调用原生 `POST /api/v1/admin/users/:id/balance``add` 操作,随后逐用户回读余额并对账;
- 实际批量充值属于外部财务影响操作,执行前必须取得用户对本次具体名单、金额和原因的明确授权;仅提出设计目标或要求 dry-run 不构成授权。
- `announcements list|get|create` 管理 Sub2API 原生公告:
- `list``get` 只读调用原生 `/api/v1/admin/announcements`,支持分页、状态、搜索、排序和按 ID 下钻;
- `create` 支持标题、Markdown 内容、`draft|active|archived``silent|popup`、原生 `any_of/all_of` targeting 以及可选 Unix 秒或 RFC3339 开始/结束时间;targeting 必须通过 `--targeting-json` 的 JSON 解析器输入,不拼接临时结构;
- `create` 默认只输出完整预览,固定 `mutation=false``writeAttempted=0`,不登录运行面也不调用创建 API
- 只有显式 `--confirm` 才调用原生创建 API,并按返回的正整数 ID 回读对账;发布公告属于面向客户的外部影响操作,必须取得用户对本次具体标题、内容、状态、通知方式、targeting 和时间窗的明确授权;
- 默认输出 Kubernetes 风格紧凑文本,显式 `--json` 输出同一份机器结构;命令不打印管理凭据或 Secret。
`config/platform-infra/sub2api-codex-pool.yaml` 控制:
- `pool.groupName`: Sub2API group 名称。
@@ -0,0 +1,201 @@
import { readFileSync } from "node:fs";
import type { UniDeskConfig } from "../config";
import type { RenderedCliResult } from "../output";
import { renderTable } from "./render";
import { boolField, compactCapture, parseJsonOutput, runRemoteCodexPoolScript } from "./remote";
import { announcementsScript } from "./remote-scripts";
import { codexPoolRuntimeTarget, defaultCodexPoolRuntimeTargetId } from "./runtime-target";
type AnnouncementAction = "list" | "get" | "create";
interface AnnouncementOptions {
action: AnnouncementAction;
id: number | null;
page: number;
pageSize: number;
status: string | null;
search: string | null;
sortBy: string;
sortOrder: "asc" | "desc";
title: string | null;
content: string | null;
announcementStatus: "draft" | "active" | "archived";
notifyMode: "silent" | "popup";
targeting: Record<string, unknown>;
startsAt: number | null;
endsAt: number | null;
confirm: boolean;
targetId: string;
json: boolean;
}
export async function codexPoolAnnouncements(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
if (args[0] === "help" || args.includes("--help")) return renderAnnouncementsHelp();
const options = parseAnnouncementOptions(args);
const target = codexPoolRuntimeTarget(options.targetId);
const payload = announcementPayload(options);
if (options.action === "create" && !options.confirm) {
const response = {
ok: true,
action: "platform-infra-sub2api-codex-pool-announcements",
target: targetSummary(target),
request: payload,
report: {
ok: true,
mode: "dry-run",
mutation: false,
writeAttempted: 0,
announcement: payload.announcement,
valuesPrinted: false,
},
valuesPrinted: false,
};
return options.json ? renderAnnouncementsJson(response) : renderAnnouncements(response);
}
const remote = await runRemoteCodexPoolScript(config, "announcements", announcementsScript(payload, target), target);
const report = parseJsonOutput(remote.stdout);
const ok = remote.exitCode === 0 && boolField(report, "ok", false);
const response = {
ok,
action: "platform-infra-sub2api-codex-pool-announcements",
target: targetSummary(target),
request: payload,
report,
remote: compactCapture(remote, { full: !ok || report === null }),
valuesPrinted: false,
};
return options.json ? renderAnnouncementsJson(response) : renderAnnouncements(response);
}
function parseAnnouncementOptions(args: string[]): AnnouncementOptions {
const [actionRaw = "list", ...rest] = args;
if (!(["list", "get", "create"] as string[]).includes(actionRaw)) throw new Error("announcements usage: list|get|create [options]");
const action = actionRaw as AnnouncementAction;
let id: number | null = null;
let page = 1;
let pageSize = 20;
let status: string | null = null;
let search: string | null = null;
let sortBy = "created_at";
let sortOrder: "asc" | "desc" = "desc";
let title: string | null = null;
let content: string | null = null;
let contentFile: string | null = null;
let announcementStatus: "draft" | "active" | "archived" = "draft";
let notifyMode: "silent" | "popup" = "silent";
let targeting: Record<string, unknown> = {};
let startsAt: number | null = null;
let endsAt: number | null = null;
let confirm = false;
let targetId = defaultCodexPoolRuntimeTargetId();
let json = false;
const readValue = (index: number, option: string): [string, number] => {
const value = rest[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${option} requires a value`);
return [value, index + 1];
};
for (let index = 0; index < rest.length; index += 1) {
const arg = rest[index]!;
if (arg === "--confirm") confirm = true;
else if (arg === "--json") json = true;
else if (arg === "--id") { let value: string; [value, index] = readValue(index, arg); id = positiveInteger(value, arg); }
else if (arg.startsWith("--id=")) id = positiveInteger(arg.slice(5), "--id");
else if (arg === "--page") { let value: string; [value, index] = readValue(index, arg); page = positiveInteger(value, arg); }
else if (arg.startsWith("--page=")) page = positiveInteger(arg.slice(7), "--page");
else if (arg === "--page-size") { let value: string; [value, index] = readValue(index, arg); pageSize = boundedPageSize(value); }
else if (arg.startsWith("--page-size=")) pageSize = boundedPageSize(arg.slice(12));
else if (arg === "--status") [status, index] = readValue(index, arg);
else if (arg.startsWith("--status=")) status = arg.slice(9);
else if (arg === "--search") [search, index] = readValue(index, arg);
else if (arg.startsWith("--search=")) search = arg.slice(9);
else if (arg === "--sort-by") [sortBy, index] = readValue(index, arg);
else if (arg.startsWith("--sort-by=")) sortBy = arg.slice(10);
else if (arg === "--sort-order") { let value: string; [value, index] = readValue(index, arg); sortOrder = parseSortOrder(value); }
else if (arg.startsWith("--sort-order=")) sortOrder = parseSortOrder(arg.slice(13));
else if (arg === "--title") [title, index] = readValue(index, arg);
else if (arg.startsWith("--title=")) title = arg.slice(8);
else if (arg === "--content") [content, index] = readValue(index, arg);
else if (arg.startsWith("--content=")) content = arg.slice(10);
else if (arg === "--content-file") [contentFile, index] = readValue(index, arg);
else if (arg.startsWith("--content-file=")) contentFile = arg.slice(15);
else if (arg === "--announcement-status") { let value: string; [value, index] = readValue(index, arg); announcementStatus = parseAnnouncementStatus(value); }
else if (arg.startsWith("--announcement-status=")) announcementStatus = parseAnnouncementStatus(arg.slice(22));
else if (arg === "--notify-mode") { let value: string; [value, index] = readValue(index, arg); notifyMode = parseNotifyMode(value); }
else if (arg.startsWith("--notify-mode=")) notifyMode = parseNotifyMode(arg.slice(14));
else if (arg === "--targeting-json") { let value: string; [value, index] = readValue(index, arg); targeting = parseTargeting(value); }
else if (arg.startsWith("--targeting-json=")) targeting = parseTargeting(arg.slice(17));
else if (arg === "--starts-at") { let value: string; [value, index] = readValue(index, arg); startsAt = parseTimestamp(value, arg); }
else if (arg.startsWith("--starts-at=")) startsAt = parseTimestamp(arg.slice(12), "--starts-at");
else if (arg === "--ends-at") { let value: string; [value, index] = readValue(index, arg); endsAt = parseTimestamp(value, arg); }
else if (arg.startsWith("--ends-at=")) endsAt = parseTimestamp(arg.slice(10), "--ends-at");
else if (arg === "--target") [targetId, index] = readValue(index, arg);
else if (arg.startsWith("--target=")) targetId = arg.slice(9);
else throw new Error(`unsupported announcements option: ${arg}`);
}
if (action !== "create" && confirm) throw new Error("--confirm is only valid for announcements create");
if (action === "get" && id === null) throw new Error("announcements get requires --id");
if (action === "create") {
if ((content === null) === (contentFile === null)) throw new Error("announcements create requires exactly one of --content or --content-file");
if (contentFile !== null) content = readFileSync(contentFile, "utf8");
title = title?.trim() ?? null;
content = content?.trim() ?? null;
if (title === null || title.length === 0 || title.length > 200) throw new Error("announcements create requires --title up to 200 characters");
if (content === null || content.length === 0) throw new Error("announcements create requires non-empty Markdown content");
if (startsAt !== null && endsAt !== null && startsAt >= endsAt) throw new Error("--starts-at must be earlier than --ends-at");
}
return { action, id, page, pageSize, status, search, sortBy, sortOrder, title, content, announcementStatus, notifyMode, targeting, startsAt, endsAt, confirm, targetId, json };
}
function announcementPayload(options: AnnouncementOptions): Record<string, unknown> {
if (options.action === "list") return { action: "list", page: options.page, pageSize: options.pageSize, status: options.status, search: options.search, sortBy: options.sortBy, sortOrder: options.sortOrder };
if (options.action === "get") return { action: "get", id: options.id };
return {
action: "create",
confirm: options.confirm,
announcement: { title: options.title, content: options.content, status: options.announcementStatus, notify_mode: options.notifyMode, targeting: options.targeting, starts_at: options.startsAt, ends_at: options.endsAt },
};
}
function renderAnnouncements(response: Record<string, unknown>): RenderedCliResult {
const request = record(response.request);
const report = record(response.report);
const action = String(request.action ?? "-");
const lines = [`ANNOUNCEMENTS action=${action} mode=${text(report.mode)} ok=${text(response.ok)} mutation=${text(report.mutation)}`];
if (action === "list") {
const items = arrayOfRecords(report.items);
lines.push(`SUMMARY total=${text(report.total)} page=${text(report.page)} page-size=${text(report.pageSize)} returned=${items.length}`);
if (items.length > 0) lines.push("", renderTable([["ID", "STATUS", "NOTIFY", "TITLE", "STARTS_AT", "ENDS_AT"], ...items.map((item) => [text(item.id), text(item.status), text(item.notify_mode), text(item.title), text(item.starts_at), text(item.ends_at)])]));
} else {
const announcement = record(report.announcement);
lines.push(`WRITE attempted=${text(report.writeAttempted)} reconciled=${text(report.reconciled)} id=${text(announcement.id)}`);
lines.push(`TITLE ${text(announcement.title)}`, `STATUS ${text(announcement.status)} NOTIFY_MODE ${text(announcement.notify_mode)}`, `STARTS_AT ${text(announcement.starts_at)} ENDS_AT ${text(announcement.ends_at)}`, `TARGETING ${JSON.stringify(record(announcement.targeting))}`, "CONTENT", String(announcement.content ?? ""));
}
if (typeof report.error === "string") lines.push(`ERROR ${text(report.error)}`);
return { ok: response.ok === true, command: "platform-infra sub2api codex-pool announcements", renderedText: lines.join("\n"), contentType: "text/plain", projection: response };
}
function renderAnnouncementsJson(response: Record<string, unknown>): RenderedCliResult {
return { ok: response.ok === true, command: "platform-infra sub2api codex-pool announcements --json", renderedText: JSON.stringify(response), contentType: "application/json", projection: response };
}
function renderAnnouncementsHelp(): RenderedCliResult {
const renderedText = [
"platform-infra sub2api codex-pool announcements list [--page 1] [--page-size 20] [--status draft|active|archived] [--search text] [--sort-by created_at] [--sort-order asc|desc] [--target PK01] [--json]",
"platform-infra sub2api codex-pool announcements get --id <positive-integer> [--target PK01] [--json]",
"platform-infra sub2api codex-pool announcements create --title <text> (--content <markdown>|--content-file <path>) [--announcement-status draft|active|archived] [--notify-mode silent|popup] [--targeting-json <json-object>] [--starts-at <unix-seconds|RFC3339>] [--ends-at <unix-seconds|RFC3339>] [--target PK01] [--confirm] [--json]",
"create defaults to dry-run; only --confirm calls the native Sub2API create API and then reads the returned ID back.",
].join("\n");
return { ok: true, command: "platform-infra sub2api codex-pool announcements --help", renderedText, contentType: "text/plain", projection: { ok: true, mutation: false } };
}
function parseTargeting(value: string): Record<string, unknown> { const parsed: unknown = JSON.parse(value); if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("--targeting-json requires a JSON object"); return parsed as Record<string, unknown>; }
function parseTimestamp(value: string, option: string): number { if (/^[1-9][0-9]*$/u.test(value)) return Number(value); const millis = Date.parse(value); if (!Number.isFinite(millis)) throw new Error(`${option} requires Unix seconds or RFC3339`); return Math.floor(millis / 1000); }
function positiveInteger(value: string, option: string): number { const parsed = Number(value); if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new Error(`${option} requires a positive integer`); return parsed; }
function boundedPageSize(value: string): number { const parsed = positiveInteger(value, "--page-size"); if (parsed > 100) throw new Error("--page-size must be at most 100"); return parsed; }
function parseSortOrder(value: string): "asc" | "desc" { if (value !== "asc" && value !== "desc") throw new Error("--sort-order must be asc or desc"); return value; }
function parseAnnouncementStatus(value: string): "draft" | "active" | "archived" { if (value !== "draft" && value !== "active" && value !== "archived") throw new Error("--announcement-status must be draft, active, or archived"); return value; }
function parseNotifyMode(value: string): "silent" | "popup" { if (value !== "silent" && value !== "popup") throw new Error("--notify-mode must be silent or popup"); return value; }
function targetSummary(target: ReturnType<typeof codexPoolRuntimeTarget>): Record<string, unknown> { return { id: target.id, route: target.route, runtimeMode: target.runtimeMode, endpoint: target.serviceDns }; }
function record(value: unknown): Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {}; }
function arrayOfRecords(value: unknown): Record<string, unknown>[] { return Array.isArray(value) ? value.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null && !Array.isArray(item)) : []; }
function text(value: unknown): string { return value === null || value === undefined || value === "" ? "-" : String(value).replace(/[\r\n\t]+/gu, " "); }
@@ -16,4 +16,5 @@ export * from "./remote";
export * from "./feedback";
export * from "./profit";
export * from "./credits";
export * from "./announcements";
export * from "./error-passthrough";
@@ -29,6 +29,7 @@ import { codexPoolRuntime } from "./runtime";
import { codexPoolFaults } from "./faults";
import { codexPoolProfit } from "./profit";
import { codexPoolCredits } from "./credits";
import { codexPoolAnnouncements } from "./announcements";
import { codexPoolErrorPassthrough } from "./error-passthrough";
import { defaultCodexPoolRuntimeTargetId } from "./runtime-target";
import { codexPoolHelp } from "./types";
@@ -46,6 +47,7 @@ export async function runCodexPoolCommand(config: UniDeskConfig, args: string[])
if (action === "faults") return await codexPoolFaults(config, args.slice(1));
if (action === "profit") return await codexPoolProfit(config, args.slice(1));
if (action === "credits") return await codexPoolCredits(config, args.slice(1));
if (action === "announcements") return await codexPoolAnnouncements(config, args.slice(1));
if (action === "error-passthrough") return await codexPoolErrorPassthrough(config, args.slice(1));
if (action === "trace") return await codexPoolTrace(config, parseTraceOptions(args.slice(1)));
if (action === "feedback") return await codexPoolFeedback(config, parseFeedbackOptions(args.slice(1)));
@@ -0,0 +1,58 @@
export const remotePythonAnnouncementsScript = String.raw`
def announcements_payload():
return json.loads(base64.b64decode(PAYLOAD_B64).decode("utf-8"))
def announcements_list(token, payload):
query = [
"page=" + quote(str(payload.get("page") or 1), safe=""),
"page_size=" + quote(str(payload.get("pageSize") or 20), safe=""),
"sort_by=" + quote(str(payload.get("sortBy") or "created_at"), safe=""),
"sort_order=" + quote(str(payload.get("sortOrder") or "desc"), safe=""),
]
if payload.get("status"):
query.append("status=" + quote(str(payload.get("status")), safe=""))
if payload.get("search"):
query.append("search=" + quote(str(payload.get("search")), safe=""))
data = ensure_success(curl_api("GET", "/api/v1/admin/announcements?" + "&".join(query), bearer=token), "list announcements")
items = extract_items(data)
return {
"ok": True, "mode": "read-only", "mutation": False, "writeAttempted": 0,
"items": items,
"total": data.get("total", len(items)) if isinstance(data, dict) else len(items),
"page": data.get("page", payload.get("page")) if isinstance(data, dict) else payload.get("page"),
"pageSize": data.get("page_size", payload.get("pageSize")) if isinstance(data, dict) else payload.get("pageSize"),
"totalPages": data.get("total_pages") if isinstance(data, dict) else None,
"valuesPrinted": False,
}
def announcements_get(token, announcement_id):
item = ensure_success(curl_api("GET", f"/api/v1/admin/announcements/{announcement_id}", bearer=token), "get announcement")
return {"ok": isinstance(item, dict), "mode": "read-only", "mutation": False, "writeAttempted": 0, "announcement": item, "valuesPrinted": False}
def announcements_create(token, payload):
announcement = payload.get("announcement") if isinstance(payload.get("announcement"), dict) else {}
created = ensure_success(curl_api("POST", "/api/v1/admin/announcements", bearer=token, payload=announcement), "create announcement")
announcement_id = created.get("id") if isinstance(created, dict) else None
if not isinstance(announcement_id, int) or announcement_id <= 0:
raise RuntimeError("create announcement response missing positive id")
actual = ensure_success(curl_api("GET", f"/api/v1/admin/announcements/{announcement_id}", bearer=token), "read announcement after create")
reconciled = isinstance(actual, dict) and actual.get("id") == announcement_id
return {
"ok": reconciled, "mode": "confirmed", "mutation": True, "writeAttempted": 1,
"writeSucceeded": 1, "writeFailed": 0, "reconciled": reconciled,
"announcement": actual, "valuesPrinted": False,
}
def run_announcements():
payload = announcements_payload()
_, token, _ = login()
action = payload.get("action")
if action == "list":
return announcements_list(token, payload)
if action == "get":
return announcements_get(token, payload.get("id"))
if action == "create" and payload.get("confirm") is True:
return announcements_create(token, payload)
raise RuntimeError("unsupported announcements action")
`;
@@ -30,7 +30,7 @@ function pyJson(value: unknown): string {
return `json.loads(${JSON.stringify(JSON.stringify(value))})`;
}
export function remotePythonCoreScript(mode: "sync" | "validate" | "trace" | "cleanup-probes" | "sentinel-probe" | "profit" | "credits", encodedPayload: string, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
export function remotePythonCoreScript(mode: "sync" | "validate" | "trace" | "cleanup-probes" | "sentinel-probe" | "profit" | "credits" | "announcements", encodedPayload: string, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
const hostDockerEnvPath = target.runtimeMode === "host-docker" ? target.hostDockerEnvPath : null;
return `
set -u
@@ -174,6 +174,8 @@ try:
result = run_profit_report()
elif MODE == "credits":
result = run_credits_grant()
elif MODE == "announcements":
result = run_announcements()
else:
result = run_validate()
except Exception as exc:
@@ -9,8 +9,9 @@ import { remotePythonTraceScript } from "./remote-python-trace";
import { remotePythonSentinelProbeScript } from "./remote-python-sentinel-probe";
import { remotePythonProfitScript } from "./remote-python-profit";
import { remotePythonCreditsScript } from "./remote-python-credits";
import { remotePythonAnnouncementsScript } from "./remote-python-announcements";
export function remotePythonScript(mode: "sync" | "validate" | "trace" | "cleanup-probes" | "sentinel-probe" | "profit" | "credits", encodedPayload: string, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
export function remotePythonScript(mode: "sync" | "validate" | "trace" | "cleanup-probes" | "sentinel-probe" | "profit" | "credits" | "announcements", encodedPayload: string, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
return [
remotePythonCoreScript(mode, encodedPayload, pool, target),
remotePythonSentinelScript,
@@ -19,6 +20,7 @@ export function remotePythonScript(mode: "sync" | "validate" | "trace" | "cleanu
remotePythonTraceScript,
remotePythonProfitScript,
remotePythonCreditsScript,
remotePythonAnnouncementsScript,
remotePythonSentinelProbeScript,
].join("");
}
@@ -63,6 +63,12 @@ export function creditsScript(payload: unknown, target: CodexPoolRuntimeTarget):
return remotePythonScript("credits", encoded, pool, target);
}
export function announcementsScript(payload: unknown, target: CodexPoolRuntimeTarget): string {
const pool = readCodexPoolConfig();
const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
return remotePythonScript("announcements", encoded, pool, target);
}
export function sentinelReportScript(pool: CodexPoolConfig, events: number, target: CodexPoolRuntimeTarget): string {
const stateName = pool.sentinel.stateConfigMapName;
const cronJobName = pool.sentinel.cronJobName;
@@ -389,7 +389,7 @@ export function codexPoolHelp(): unknown {
const pool = readCodexPoolConfig();
const runtimeTarget = codexPoolRuntimeTarget();
return {
command: "platform-infra sub2api codex-pool plan|sync|validate|runtime|faults|profit|credits|error-passthrough|trace|feedback|sentinel-image|sentinel-probe|sentinel-report|cleanup-probes|expose|configure-local",
command: "platform-infra sub2api codex-pool plan|sync|validate|runtime|faults|profit|credits|announcements|error-passthrough|trace|feedback|sentinel-image|sentinel-probe|sentinel-report|cleanup-probes|expose|configure-local",
output: "json, except trace, feedback, and sentinel-report default to low-noise text tables",
usage: [
"bun scripts/cli.ts platform-infra sub2api codex-pool plan",
@@ -402,6 +402,7 @@ export function codexPoolHelp(): unknown {
"bun scripts/cli.ts platform-infra sub2api codex-pool faults [--level P0|P1|P2] [--group <name-or-id>] [--account <name-or-id>] [--model <model>] [--stream sync|stream] [--endpoint <path>] [--request-id <id>] [--page-token <token>] [--target PK01] [--json]",
"bun scripts/cli.ts platform-infra sub2api codex-pool profit [--since 24h|--month YYYY-MM [--forecast]] [--target PK01] [--sale-rate <CNY/API_USD>] [--scenario-without-account <id-or-exact-name> ...] [--accounts [--page-token <token>]|--missing-costs] [--json]",
"bun scripts/cli.ts platform-infra sub2api codex-pool credits grant (--active-since 24h|--users <id-or-email,...>) --amount-usd <amount> --reason <text> [--target PK01] [--confirm] [--json]",
"bun scripts/cli.ts platform-infra sub2api codex-pool announcements list|get|create [--target PK01] [--confirm for create] [--json]",
"bun scripts/cli.ts platform-infra sub2api codex-pool error-passthrough list|apply|delete [--template <id>] [--target PK01] [--confirm] [--json]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --account <name-or-id> --template <id> [--target PK01] [--confirm]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --accounts <name-or-id,...> --template <id> [--target PK01] [--confirm] [--full|--raw]",