feat: 增加 Sub2API 原生公告受控 CLI
This commit is contained in:
@@ -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]",
|
||||
|
||||
Reference in New Issue
Block a user