From 2ab5fb87a2693ad6076e3abdeb3801c74269be69 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 15 Jul 2026 05:29:37 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20Sub2API=20?= =?UTF-8?q?=E5=8E=9F=E7=94=9F=E5=85=AC=E5=91=8A=E5=8F=97=E6=8E=A7=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/unidesk-sub2api/SKILL.md | 2 +- .../unidesk-sub2api/references/codex-pool.md | 10 + .../announcements.ts | 201 ++++++++++++++++++ .../src/platform-infra-sub2api-codex/index.ts | 1 + .../platform-infra-sub2api-codex/options.ts | 2 + .../remote-python-announcements.ts | 58 +++++ .../remote-python-core.ts | 2 +- .../remote-python-sentinel-probe.ts | 2 + .../remote-python-sync.ts | 4 +- .../remote-scripts.ts | 6 + .../src/platform-infra-sub2api-codex/types.ts | 3 +- 11 files changed, 287 insertions(+), 4 deletions(-) create mode 100644 scripts/src/platform-infra-sub2api-codex/announcements.ts create mode 100644 scripts/src/platform-infra-sub2api-codex/remote-python-announcements.ts diff --git a/.agents/skills/unidesk-sub2api/SKILL.md b/.agents/skills/unidesk-sub2api/SKILL.md index 2647c09f..0e126584 100644 --- a/.agents/skills/unidesk-sub2api/SKILL.md +++ b/.agents/skills/unidesk-sub2api/SKILL.md @@ -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)。 diff --git a/.agents/skills/unidesk-sub2api/references/codex-pool.md b/.agents/skills/unidesk-sub2api/references/codex-pool.md index c9398c0c..c4a65335 100644 --- a/.agents/skills/unidesk-sub2api/references/codex-pool.md +++ b/.agents/skills/unidesk-sub2api/references/codex-pool.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 --forecast --sale-rate --scenario-without-account '' bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --target PK01 --active-since 24h --amount-usd 20 --reason +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 +bun scripts/cli.ts platform-infra sub2api codex-pool announcements create --target PK01 --title --content --targeting-json '{"any_of":[]}' bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id 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 名称。 diff --git a/scripts/src/platform-infra-sub2api-codex/announcements.ts b/scripts/src/platform-infra-sub2api-codex/announcements.ts new file mode 100644 index 00000000..9619b05f --- /dev/null +++ b/scripts/src/platform-infra-sub2api-codex/announcements.ts @@ -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; + startsAt: number | null; + endsAt: number | null; + confirm: boolean; + targetId: string; + json: boolean; +} + +export async function codexPoolAnnouncements(config: UniDeskConfig, args: string[]): Promise | 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 = {}; + 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 { + 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): 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): 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 [--target PK01] [--json]", + "platform-infra sub2api codex-pool announcements create --title (--content |--content-file ) [--announcement-status draft|active|archived] [--notify-mode silent|popup] [--targeting-json ] [--starts-at ] [--ends-at ] [--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 { 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; } +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): Record { return { id: target.id, route: target.route, runtimeMode: target.runtimeMode, endpoint: target.serviceDns }; } +function record(value: unknown): Record { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : {}; } +function arrayOfRecords(value: unknown): Record[] { return Array.isArray(value) ? value.filter((item): item is Record => 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, " "); } diff --git a/scripts/src/platform-infra-sub2api-codex/index.ts b/scripts/src/platform-infra-sub2api-codex/index.ts index 13dd55f3..3d016072 100644 --- a/scripts/src/platform-infra-sub2api-codex/index.ts +++ b/scripts/src/platform-infra-sub2api-codex/index.ts @@ -16,4 +16,5 @@ export * from "./remote"; export * from "./feedback"; export * from "./profit"; export * from "./credits"; +export * from "./announcements"; export * from "./error-passthrough"; diff --git a/scripts/src/platform-infra-sub2api-codex/options.ts b/scripts/src/platform-infra-sub2api-codex/options.ts index 99cc4904..e863f121 100644 --- a/scripts/src/platform-infra-sub2api-codex/options.ts +++ b/scripts/src/platform-infra-sub2api-codex/options.ts @@ -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))); diff --git a/scripts/src/platform-infra-sub2api-codex/remote-python-announcements.ts b/scripts/src/platform-infra-sub2api-codex/remote-python-announcements.ts new file mode 100644 index 00000000..373b83d4 --- /dev/null +++ b/scripts/src/platform-infra-sub2api-codex/remote-python-announcements.ts @@ -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") +`; diff --git a/scripts/src/platform-infra-sub2api-codex/remote-python-core.ts b/scripts/src/platform-infra-sub2api-codex/remote-python-core.ts index 7dbdd257..cf04cb17 100644 --- a/scripts/src/platform-infra-sub2api-codex/remote-python-core.ts +++ b/scripts/src/platform-infra-sub2api-codex/remote-python-core.ts @@ -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 diff --git a/scripts/src/platform-infra-sub2api-codex/remote-python-sentinel-probe.ts b/scripts/src/platform-infra-sub2api-codex/remote-python-sentinel-probe.ts index 6cc3907c..62b7a6a9 100644 --- a/scripts/src/platform-infra-sub2api-codex/remote-python-sentinel-probe.ts +++ b/scripts/src/platform-infra-sub2api-codex/remote-python-sentinel-probe.ts @@ -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: diff --git a/scripts/src/platform-infra-sub2api-codex/remote-python-sync.ts b/scripts/src/platform-infra-sub2api-codex/remote-python-sync.ts index 4c64ad7d..674c7da6 100644 --- a/scripts/src/platform-infra-sub2api-codex/remote-python-sync.ts +++ b/scripts/src/platform-infra-sub2api-codex/remote-python-sync.ts @@ -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(""); } diff --git a/scripts/src/platform-infra-sub2api-codex/remote-scripts.ts b/scripts/src/platform-infra-sub2api-codex/remote-scripts.ts index a455ddf2..cf72e990 100644 --- a/scripts/src/platform-infra-sub2api-codex/remote-scripts.ts +++ b/scripts/src/platform-infra-sub2api-codex/remote-scripts.ts @@ -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; diff --git a/scripts/src/platform-infra-sub2api-codex/types.ts b/scripts/src/platform-infra-sub2api-codex/types.ts index cbaa155e..a0463a8a 100644 --- a/scripts/src/platform-infra-sub2api-codex/types.ts +++ b/scripts/src/platform-infra-sub2api-codex/types.ts @@ -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 ] [--account ] [--model ] [--stream sync|stream] [--endpoint ] [--request-id ] [--page-token ] [--target PK01] [--json]", "bun scripts/cli.ts platform-infra sub2api codex-pool profit [--since 24h|--month YYYY-MM [--forecast]] [--target PK01] [--sale-rate ] [--scenario-without-account ...] [--accounts [--page-token ]|--missing-costs] [--json]", "bun scripts/cli.ts platform-infra sub2api codex-pool credits grant (--active-since 24h|--users ) --amount-usd --reason [--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 ] [--target PK01] [--confirm] [--json]", "bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --account --template [--target PK01] [--confirm]", "bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --accounts --template [--target PK01] [--confirm] [--full|--raw]",