Merge remote-tracking branch 'origin/master' into feat/pikaoa-production-backup

This commit is contained in:
Codex
2026-07-15 06:19:38 +02:00
10 changed files with 156 additions and 24 deletions
@@ -111,6 +111,7 @@ 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 credits grant --target PK01 --active-start 2026-07-14T17:00:00 --active-end 2026-07-14T18:00:00 --amount-usd 10 --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":[]}'
@@ -151,7 +152,9 @@ bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --target D60
- `--json` 保留精确十进制字符串、原生来源、时间窗、分页数和完整性明细;命令不修改 runtime。
- `credits grant` 管理客户余额补偿:
- `--active-since <duration>` 从原生 usage 选取窗口内有请求的非管理员活跃用户;`--users <id-or-email,...>` 使用精确 selector;两者必须且只能选择一个;
- `--active-since <duration>` 从原生 usage 选取相对窗口内有请求的非管理员活跃用户;
- `--active-start <ISO> --active-end <ISO>` 使用精确半开区间 `[start,end)`;未携带 UTC 偏移的输入按 `config/platform-infra/sub2api-codex-pool.yaml#credits.timezone` 解释,并同时披露本地与 UTC 边界;
- `--users <id-or-email,...>` 使用精确 selector;相对窗口、固定窗口和精确 selector 三者必须且只能选择一个;
- 默认 dry-run,逐用户披露完整邮箱、用户 ID、请求数、当前余额、增加额和目标余额;
- 活跃窗口只允许生成计划;实际写入必须把 dry-run 返回的用户 ID 固定为精确 `--users` selectors,再显式 `--confirm`,防止移动窗口在确认时扩大或缩小名单;
- 确认写入为每个用户生成由精确名单、金额和原因确定的 `Idempotency-Key`,调用原生 `POST /api/v1/admin/users/:id/balance``add` 操作,随后逐用户回读余额并对账;
@@ -76,6 +76,9 @@ profit:
selfUse: [自用]
excluded: [grok]
credits:
timezone: Asia/Shanghai
pool:
groupName: unidesk-codex-pool
groupDescription: UniDesk-managed Codex API-key pool for YAML-selected Sub2API clients.
@@ -0,0 +1,6 @@
# R1 任务报告
- WebTerm issue #3 由 Artificer 在独立 worktree 完成,PR #5 已合并到集成分支,merge commit `d4e6bb68f88eadff9648b1b746eec0731bad3e31`
- 删除桌面端与移动端 `config-head`,标签栏固定可见;桌面配置隐藏后终端占用释放高度,移动端始终固定 50/50 以保留手机键盘/控制区。
- 真实 Chromium 验证:1440×1000 桌面终端高度由 500 增至 883drawer 由 500 收至 117390×844 移动端为 422/422;标签栏可见且 `config-head` 不存在。
- `npm run check``npm test` 通过;最终随 WebTerm 集成提交部署到 `local-7682`,未触碰 7683。
@@ -0,0 +1,7 @@
# R2 任务报告
- WebTerm issue #4 由同一 Artificer 在 R1 基线上完成,PR #6 已合并,merge commit `32bc9f5adbac0ba5340217d27d1538a057ee95b1`;主审发现并修复跨 port claim 未迁移 `provider_id` 的 blocker,追加提交 `b1db65a`
- PG 增加 runtime active/closed、各 data-domain 的 YAML preset 生命周期,以及跨 port runtime closed 共享登记;runtime claim 在事务中迁移 `data_domain` 与目标 local `provider_id`,避免目标 port 重启后跳过恢复。
- 加号面板同时展示本端 YAML 关闭项和共享 runtime 关闭项;打开面板主动刷新,服务端每 2 秒低频同步共享目录;运行项不重复出现。
- `npm run check``npm test` 与隔离 Chromium UI 验证通过。真实 7682 验收完成:关闭 TERM 后 sessions 从 6 降至 5;仅重启 7682 后仍为 5,证明 closed 状态未被 YAML 自动拉起;加号显示 `TERM / YAML 预装 / mycx / /root/unidesk`,点击恢复后 sessions 回到 6。
- 已部署到 `local-7682`,最终 health 为 providers=1、sessions=6、runtime records=4、backgroundMode=live7683 未触碰。跨 port runtime 原子 claim 由 store 回归测试覆盖,未为测试部署或扰动其他端口。
+3 -3
View File
@@ -4,10 +4,10 @@
- 交付:源码修改进入 `pikasTech/webterm`,本文件只登记跨代理任务与有界报告。
## R1 [in_progress]
## R1 [completed]
依据 [WebTerm #3](https://github.com/pikasTech/webterm/issues/3) 改进配置页纵向空间:桌面模式支持隐藏“新建终端”“正在管理”等配置且终端实际占满释放高度;移动端与桌面端删除无意义的 config-head 行并固定展示标签栏;保持会话、恢复、持久化和后台绘制协议不变,由 Artificer 在独立 worktree 实现并提交 PR,完成任务后将详细报告写入[任务报告](./details/webterm-product-experience/R1_Task_Report.md)。
## R2
## R2 [completed]
依据 [WebTerm #4](https://github.com/pikasTech/webterm/issues/4) 让已关闭的可恢复终端保留恢复定义并进入加号创建面板的待恢复列表:区分停止与永久删除,服务端投影当前数据域的已关闭运行时和预置恢复项,选择后以 CLI 参数恢复且不模拟键盘;三个 PostgreSQL 数据域继续隔离,预置与运行时记录共存;依赖 R1 布局基线后再派实现,完成任务后将详细报告写入[任务报告](./details/webterm-product-experience/R2_Task_Report.md)。
依据 [WebTerm #4](https://github.com/pikasTech/webterm/issues/4) 让已关闭的可恢复终端保留恢复定义并进入加号创建面板YAML 预装项按各 port/data-domain 保存生命周期且 owning YAML 仍是定义真相;runtime 定义保留在 host PG,关闭登记跨 port 共享并由目标 port 原子 claim 迁移 data-domain/provider;服务重启只恢复 active,关闭项可从加号恢复且不模拟键盘;依赖 R1 布局基线,由 Artificer 实现并提交 PR,完成任务后将详细报告写入[任务报告](./details/webterm-product-experience/R2_Task_Report.md)。
@@ -21,7 +21,7 @@ import {
import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "../secrets";
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
import type { CodexPoolCaddyEdgeRetryConfig, CodexPoolConfig, CodexPoolLocalCodexConfig, CodexPoolManualAccountGroupBinding, CodexPoolManualAccountProtection, CodexPoolManualAccountProxyBinding, CodexPoolManualAccountsConfig, CodexPoolManualBindingKind, CodexPoolManualBindingProvider, CodexPoolManualBindingSource, CodexPoolManualBindingSourcesConfig, CodexPoolProfileConfig, CodexPoolProfitConfig, CodexPoolPublicExposureConfig, CodexProfile, CodexRuntimeConfig, CodexRuntimeTemplate, CodexSentinelProtectPolicy, CodexTempUnschedulablePolicy, CodexTempUnschedulableRule, OpenAIResponsesWebSocketsV2Mode } from "./types";
import type { CodexPoolCaddyEdgeRetryConfig, CodexPoolConfig, CodexPoolCreditsConfig, CodexPoolLocalCodexConfig, CodexPoolManualAccountGroupBinding, CodexPoolManualAccountProtection, CodexPoolManualAccountProxyBinding, CodexPoolManualAccountsConfig, CodexPoolManualBindingKind, CodexPoolManualBindingProvider, CodexPoolManualBindingSource, CodexPoolManualBindingSourcesConfig, CodexPoolProfileConfig, CodexPoolProfitConfig, CodexPoolPublicExposureConfig, CodexProfile, CodexRuntimeConfig, CodexRuntimeTemplate, CodexSentinelProtectPolicy, CodexTempUnschedulablePolicy, CodexTempUnschedulableRule, OpenAIResponsesWebSocketsV2Mode } from "./types";
import { booleanValue, integerArrayConfigField, integerConfigField, isRecord, normalizeBaseUrl, numberValue, readRequiredBaseUrl, readRequiredDescription, readRequiredEmail, readRequiredPort, readRequiredPositiveNumber, requiredRecordConfigField, requiredStringConfigField, stringValue, validateKubernetesName } from "./config-utils";
import { readAuthAPIKey, uniqueAccountName } from "./redaction";
import { codexPoolConfigPath } from "./types";
@@ -180,6 +180,7 @@ export function readCodexPoolConfig(): CodexPoolConfig {
profiles,
runtime,
profit: readProfitConfig(parsed.profit),
credits: readCreditsConfig(parsed.credits),
manualAccounts,
publicExposure: readPublicExposureConfig(parsed.publicExposure),
localCodex: readLocalCodexConfig(parsed.localCodex),
@@ -200,6 +201,17 @@ export function readCodexPoolConfig(): CodexPoolConfig {
return config;
}
function readCreditsConfig(value: unknown): CodexPoolCreditsConfig {
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.credits must be a YAML object`);
const timezone = requiredStringConfigField(value, "timezone", "credits");
try {
new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format(new Date(0));
} catch {
throw new Error(`${codexPoolConfigPath}.credits.timezone must be a valid IANA timezone`);
}
return { timezone };
}
function readProfitConfig(value: unknown): CodexPoolProfitConfig {
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.profit must be a YAML object`);
const groups = requiredRecordConfigField(value, "groups", "profit");
@@ -4,9 +4,15 @@ import { renderTable } from "./render";
import { boolField, compactCapture, parseJsonOutput, runRemoteCodexPoolScript } from "./remote";
import { creditsScript } from "./remote-scripts";
import { codexPoolRuntimeTarget, defaultCodexPoolRuntimeTargetId } from "./runtime-target";
import { readCodexPoolConfig } from "./config";
interface CreditsOptions {
activeSince: string | null;
activeStart: string | null;
activeEnd: string | null;
activeStartUtc: string | null;
activeEndUtc: string | null;
timezone: string;
users: string[];
amountUsd: number;
reason: string;
@@ -17,9 +23,10 @@ interface CreditsOptions {
export async function codexPoolCredits(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
if (args.includes("--help")) return renderCreditsHelp();
const options = parseCreditsOptions(args);
const pool = readCodexPoolConfig();
const options = parseCreditsOptions(args, pool.credits.timezone);
const target = codexPoolRuntimeTarget(options.targetId);
const remote = await runRemoteCodexPoolScript(config, "credits", creditsScript(options, target), target);
const remote = await runRemoteCodexPoolScript(config, "credits", creditsScript(options, pool, target), target);
const report = parseJsonOutput(remote.stdout);
const ok = remote.exitCode === 0 && boolField(report, "ok", false);
const response = {
@@ -27,8 +34,13 @@ export async function codexPoolCredits(config: UniDeskConfig, args: string[]): P
action: "platform-infra-sub2api-codex-pool-credits",
target: { id: target.id, route: target.route, runtimeMode: target.runtimeMode, endpoint: target.serviceDns },
request: {
selection: options.activeSince === null ? "exact-users" : "active-users",
selection: options.users.length > 0 ? "exact-users" : options.activeSince === null ? "active-window" : "active-users",
activeSince: options.activeSince,
activeStart: options.activeStart,
activeEnd: options.activeEnd,
activeStartUtc: options.activeStartUtc,
activeEndUtc: options.activeEndUtc,
timezone: options.timezone,
users: options.users,
amountUsd: options.amountUsd,
reason: options.reason,
@@ -41,10 +53,12 @@ export async function codexPoolCredits(config: UniDeskConfig, args: string[]): P
return options.json ? renderCreditsJson(response) : renderCredits(response);
}
function parseCreditsOptions(args: string[]): CreditsOptions {
function parseCreditsOptions(args: string[], timezone: string): CreditsOptions {
const [action = "grant", ...rest] = args;
if (action !== "grant") throw new Error("credits usage: grant (--active-since 24h|--users <id-or-email,...>) --amount-usd <amount> --reason <text> [--confirm] [--target <id>] [--json]");
if (action !== "grant") throw new Error("credits usage: grant (--active-since 24h|--active-start <ISO> --active-end <ISO>|--users <id-or-email,...>) --amount-usd <amount> --reason <text> [--confirm] [--target <id>] [--json]");
let activeSince: string | null = null;
let activeStart: string | null = null;
let activeEnd: string | null = null;
let users: string[] = [];
let amountUsd: number | null = null;
let reason: string | null = null;
@@ -62,6 +76,10 @@ function parseCreditsOptions(args: string[]): CreditsOptions {
else if (arg === "--json") json = true;
else if (arg === "--active-since") [activeSince, index] = readValue(index, arg);
else if (arg.startsWith("--active-since=")) activeSince = arg.slice("--active-since=".length).trim();
else if (arg === "--active-start") [activeStart, index] = readValue(index, arg);
else if (arg.startsWith("--active-start=")) activeStart = arg.slice("--active-start=".length).trim();
else if (arg === "--active-end") [activeEnd, index] = readValue(index, arg);
else if (arg.startsWith("--active-end=")) activeEnd = arg.slice("--active-end=".length).trim();
else if (arg === "--users") {
let value: string;
[value, index] = readValue(index, arg);
@@ -78,12 +96,64 @@ function parseCreditsOptions(args: string[]): CreditsOptions {
else if (arg.startsWith("--target=")) targetId = arg.slice("--target=".length).trim();
else throw new Error(`unsupported credits option: ${arg}`);
}
if ((activeSince === null) === (users.length === 0)) throw new Error("use exactly one of --active-since or --users");
if ((activeStart === null) !== (activeEnd === null)) throw new Error("--active-start and --active-end must be provided together");
const selectionCount = Number(activeSince !== null) + Number(activeStart !== null) + Number(users.length > 0);
if (selectionCount !== 1) throw new Error("use exactly one of --active-since, --active-start/--active-end, or --users");
if (activeSince !== null && !/^[1-9][0-9]*(?:m|h|d)$/u.test(activeSince)) throw new Error("--active-since must use a duration such as 24h");
if (confirm && activeSince !== null) throw new Error("confirmed credits require the exact --users selectors returned by an active-user dry-run");
if (activeStart !== null && !isIsoDateTime(activeStart)) throw new Error("--active-start must use ISO date-time format");
if (activeEnd !== null && !isIsoDateTime(activeEnd)) throw new Error("--active-end must use ISO date-time format");
const activeStartUtc = activeStart === null ? null : isoInTimezoneToUtc(activeStart, timezone);
const activeEndUtc = activeEnd === null ? null : isoInTimezoneToUtc(activeEnd, timezone);
if (activeStartUtc !== null && activeEndUtc !== null && activeEndUtc <= activeStartUtc) {
throw new Error("credits active window end must be after start");
}
if (confirm && users.length === 0) throw new Error("confirmed credits require the exact --users selectors returned by an active-user dry-run");
if (amountUsd === null) throw new Error("credits grant requires --amount-usd");
if (reason === null || reason.length === 0 || reason.length > 500 || /[\r\n\0]/u.test(reason)) throw new Error("credits grant requires a single-line --reason up to 500 characters");
return { activeSince, users, amountUsd, reason, confirm, targetId, json };
return { activeSince, activeStart, activeEnd, activeStartUtc, activeEndUtc, timezone, users, amountUsd, reason, confirm, targetId, json };
}
function isIsoDateTime(value: string): boolean {
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,6})?)?(?:Z|[+-]\d{2}:\d{2})?$/u.test(value);
}
function isoInTimezoneToUtc(value: string, timezone: string): string {
if (/(?:Z|[+-]\d{2}:\d{2})$/u.test(value)) {
const instant = new Date(value);
if (!Number.isFinite(instant.getTime())) throw new Error("fixed-window date-time is invalid");
return instant.toISOString();
}
const matched = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,6}))?)?$/u.exec(value);
if (matched === null) throw new Error("fixed-window date-time is invalid");
const components = matched.slice(1, 7).map((item) => Number(item ?? 0));
const milliseconds = Number((matched[7] ?? "").padEnd(3, "0").slice(0, 3));
const localEpoch = Date.UTC(components[0]!, components[1]! - 1, components[2]!, components[3]!, components[4]!, components[5]!, milliseconds);
let instant = localEpoch;
for (let attempt = 0; attempt < 3; attempt += 1) {
const actual = timezoneComponents(instant, timezone);
const represented = Date.UTC(actual[0], actual[1] - 1, actual[2], actual[3], actual[4], actual[5], milliseconds);
instant = localEpoch - (represented - instant);
}
const actual = timezoneComponents(instant, timezone);
if (actual.some((item, index) => item !== components[index])) {
throw new Error(`fixed-window date-time does not exist in timezone ${timezone}: ${value}`);
}
return new Date(instant).toISOString();
}
function timezoneComponents(epochMs: number, timezone: string): number[] {
const parts = new Intl.DateTimeFormat("en-CA", {
timeZone: timezone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: "h23",
}).formatToParts(new Date(epochMs));
const values = new Map(parts.map((part) => [part.type, Number(part.value)]));
return ["year", "month", "day", "hour", "minute", "second"].map((key) => values.get(key) ?? Number.NaN);
}
function parseSelectors(value: string): string[] {
@@ -108,9 +178,11 @@ function renderCreditsHelp(): RenderedCliResult {
"SUB2API USER CREDITS",
"Usage:",
" bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --active-since 24h --amount-usd 20 --reason <text> [--confirm]",
" bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --active-start <ISO> --active-end <ISO> --amount-usd 20 --reason <text>",
" bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --users <id-or-email,...> --amount-usd 20 --reason <text> [--confirm]",
"Notes:",
" Default mode is dry-run. --active-since selects distinct active non-admin users from native usage.",
" Offset-free fixed-window values use config/platform-infra/sub2api-codex-pool.yaml#credits.timezone and the interval is [start,end).",
" --confirm performs additive balance writes and then reads every selected user back.",
].join("\n"),
contentType: "text/plain",
@@ -125,6 +197,10 @@ function renderCredits(response: Record<string, unknown>): RenderedCliResult {
`SUB2API USER CREDITS ok=${response.ok === true} mode=${text(report.mode)} selection=${text(record(report.selection).kind)} selected=${text(summary.selected)} amount-usd=${text(summary.amountUsd)} total-usd=${text(summary.totalAmountUsd)}`,
`WRITE attempted=${text(summary.writeAttempted)} succeeded=${text(summary.writeSucceeded)} failed=${text(summary.writeFailed)} reconciled=${text(summary.reconciled)} mutation=${text(report.mutation)}`,
];
const selection = record(report.selection);
if (selection.kind === "active-users" || selection.kind === "active-window") {
lines.push(`WINDOW timezone=${text(selection.timezone)} start=${text(selection.startAt)} end=${text(selection.endAt)} start-utc=${text(selection.startAtUtc)} end-utc=${text(selection.endAtUtc)}`);
}
if (typeof report.error === "string") lines.push(`ERROR ${text(report.error)}`);
if (rows.length > 0) {
lines.push("", renderTable([
@@ -28,9 +28,17 @@ def credits_all_users(token):
return users
page += 1
def credits_active_usage(token, since):
end_at = datetime.now(timezone.utc)
start_at = credits_window_start(since, end_at)
def credits_active_usage(token, since, active_start, active_end, active_start_utc, active_end_utc, timezone_name):
if since:
end_at = datetime.now(timezone.utc)
start_at = credits_window_start(since, end_at)
selection_kind = "active-users"
else:
start_at = credits_parse_time(active_start_utc)
end_at = credits_parse_time(active_end_utc)
selection_kind = "active-window"
if start_at is None or end_at is None or end_at <= start_at:
raise RuntimeError("credits active window end must be after start")
user_requests = {}
page = 1
while True:
@@ -46,11 +54,19 @@ def credits_active_usage(token, since):
reached_start = True
continue
user_id = row.get("user_id") if row.get("user_id") is not None else row.get("userId")
if user_id is not None and (created_at is None or created_at <= end_at):
if user_id is not None and created_at is not None and start_at <= created_at < end_at:
user_requests[str(user_id)] = user_requests.get(str(user_id), 0) + 1
total = int(data.get("total", 0)) if isinstance(data, dict) else 0
if not batch or len(batch) < 100 or reached_start or page * 100 >= total:
return user_requests, start_at, end_at
return user_requests, start_at, end_at, {
"kind": selection_kind,
"since": since,
"timezone": timezone_name if selection_kind == "active-window" else "UTC",
"startAt": active_start if selection_kind == "active-window" else start_at.isoformat(),
"endAt": active_end if selection_kind == "active-window" else end_at.isoformat(),
"startAtUtc": start_at.isoformat().replace("+00:00", "Z"),
"endAtUtc": end_at.isoformat().replace("+00:00", "Z"),
}
page += 1
def credits_selected_users(token, payload):
@@ -58,8 +74,13 @@ def credits_selected_users(token, payload):
users_by_id = {str(item.get("id")): item for item in users if item.get("id") is not None}
users_by_email = {str(item.get("email") or "").strip().lower(): item for item in users if item.get("email")}
active_since = payload.get("activeSince")
if active_since:
requests, start_at, end_at = credits_active_usage(token, active_since)
active_start = payload.get("activeStart")
active_end = payload.get("activeEnd")
if active_since or (active_start and active_end):
requests, _, _, selection = credits_active_usage(
token, active_since, active_start, active_end,
payload.get("activeStartUtc"), payload.get("activeEndUtc"), payload.get("timezone"),
)
selected = []
excluded = []
for user_id, request_count in requests.items():
@@ -72,7 +93,7 @@ def credits_selected_users(token, payload):
excluded.append({"userId": user.get("id"), "email": user.get("email"), "reason": "inactive-or-deleted"})
else:
selected.append((user, request_count))
return selected, excluded, {"kind": "active-users", "since": active_since, "startAt": start_at.isoformat(), "endAt": end_at.isoformat()}
return selected, excluded, selection
selected = []
seen = set()
for selector in payload.get("users") or []:
@@ -57,8 +57,7 @@ export function profitScript(payload: unknown, pool: CodexPoolConfig, target: Co
return remotePythonScript("profit", encoded, pool, target);
}
export function creditsScript(payload: unknown, target: CodexPoolRuntimeTarget): string {
const pool = readCodexPoolConfig();
export function creditsScript(payload: unknown, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
return remotePythonScript("credits", encoded, pool, target);
}
@@ -219,6 +219,10 @@ export interface CodexPoolProfitConfig {
};
}
export interface CodexPoolCreditsConfig {
timezone: string;
}
export interface CodexPoolConfig {
version: number;
kind: string;
@@ -245,6 +249,7 @@ export interface CodexPoolConfig {
profiles: CodexPoolProfileConfig[];
runtime: CodexRuntimeConfig;
profit: CodexPoolProfitConfig;
credits: CodexPoolCreditsConfig;
manualAccounts: CodexPoolManualAccountsConfig;
publicExposure: CodexPoolPublicExposureConfig;
localCodex: CodexPoolLocalCodexConfig;
@@ -401,7 +406,7 @@ export function codexPoolHelp(): unknown {
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime errors [--group <name-or-id>|--all-groups] [--platform <name>] [--page-token <group-id>] [--account <name-or-id>] [--since 24h] [--tail 50000] [--target PK01] [--json|--full|--raw]",
"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 credits grant (--active-since 24h|--active-start <ISO> --active-end <ISO>|--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]",