feat: 增加 Sub2API 账号容量运行时控制

This commit is contained in:
pikastech
2026-07-21 13:21:14 +02:00
parent 55acea225d
commit 0caec216ae
6 changed files with 60 additions and 7 deletions
@@ -78,7 +78,7 @@
- 当前并发是查询时快照,不代表错误发生瞬间并发;没有历史并发证据时,命令必须明确输出不可用,不能据此建议降低并发;
- 错误列表没有账号全部路由尝试数时,命令只报告错误数量,不伪造错误率;
- Ops 记录与 runtime 日志是独立证据,命令明确披露采样数量,不按日志位置强行关联;
- `runtime apply|delete` 支持 `temp-unschedulable``model-mapping` 两类 runtime 配置
- `runtime apply|delete` 支持临时不可调度与模型映射;`runtime apply` 还支持账号优先级、名称和容量的精准 runtime 更新
- 单账号必须显式使用 `--account`,已知精准批量必须显式使用 `--accounts <id-or-exact-name,...>`,不提供隐式 `all`
- 更新已配置同类 runtime 策略的手工账号时,可显式使用 `--select-configured-policy --account-type apikey` 自动选集,并用 `--exclude-accounts <id-or-exact-name,...>` 排除例外:
- 该入口只选择当前 credentials 已存在临时不可调度字段的账号,不把未配置账号自动纳入;
@@ -117,6 +117,7 @@ bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --accounts <id-or-exact-name,...> --template <template-id> --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --template <template-id> --select-configured-policy --account-type apikey --exclude-accounts <id-or-exact-name,...>
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --accounts <id-or-exact-name,...> --kind priority --priority <0-1000> --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --accounts <id-or-exact-name,...> --kind capacity --capacity <1-100000> --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --account <id-or-exact-name> --kind account-name --name <new-name> --confirm
bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --target PK01 --account <name-or-id> --kind temp-unschedulable
bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --account <name-or-id> --kind model-mapping --model <client-model> --upstream-model <upstream-model>
@@ -241,6 +242,11 @@ bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --target D60
- 数值越小优先级越高,不存在分组级 priority;
- 单账号和精准批量都必须先 dry-run,确认写入后逐账号回读并显示 before、desired、actual 与 reconciled
- 只更新账号 priority,不携带 credentials、分组、容量、代理、状态、可调度或临时不可调度字段。
- 运行时账号并发保护使用 `runtime apply --kind capacity --capacity <1-100000>`
- 单账号和精准批量账号都必须显式选择,默认 dry-run,确认后顺序写入并逐账号回读;
- 只更新 Sub2API 原生账号 `concurrency` 字段,不携带 priority、credentials、分组、代理、状态或可调度字段;
- 只有错误时段的并发重建或同等级原生证据表明上游实际并发上限低于本地 capacity 时才收敛,不能用查询时快照代替错误时证据;
- YAML-managed 账号的长期容量仍由 owning YAML 管理;未在 YAML 中预配置的手工账号使用该 runtime CRUD,不纳入 YAML profile。
- 账号人类可读名称使用 `runtime apply --kind account-name --name <new-name>`
- 只接受单个精确账号 selector,先 dry-run,确认后自动回读名称;
- 只更新名称,不携带 credentials、priority、分组、容量、代理、状态或可调度字段。
@@ -46,6 +46,15 @@ export function renderRuntimeMutationResult(ok: boolean, options: RuntimeOptions
stringValue(write.error) ?? stringValue(item.reconcileError) ?? "-",
]),
]));
else if (options.kind === "capacity") lines.push(renderTable([
["ACCOUNT", "ID", "SELECTED_BY", "CHANGE", "WRITE", "RECONCILED", "BEFORE_CAPACITY", "DESIRED_CAPACITY", "ACTUAL_CAPACITY", "FAILURE"],
...fields.map(({ item, write, before, desired, actual }) => [
stringValue(item.accountName) ?? "-", String(item.accountId ?? "-"), stringValue(item.selectionReason) ?? "-", stringValue(item.change) ?? "-",
stringValue(write.status) ?? "-", item.reconciled === true ? "true" : item.reconciled === false ? "false" : "-",
String(before.capacity ?? "-"), String(desired.capacity ?? "-"), String(actual.capacity ?? "-"),
stringValue(write.error) ?? stringValue(item.reconcileError) ?? "-",
]),
]));
else if (options.kind === "account-name") lines.push(renderTable([
["ACCOUNT", "ID", "SELECTED_BY", "CHANGE", "WRITE", "RECONCILED", "BEFORE_NAME", "DESIRED_NAME", "ACTUAL_NAME", "FAILURE"],
...fields.map(({ item, write, before, desired, actual }) => [
@@ -14,8 +14,9 @@ export interface RuntimeOptions {
platform: string | null;
pageToken: string | null;
template: string | null;
kind: "temp-unschedulable" | "priority" | "account-name";
kind: "temp-unschedulable" | "priority" | "account-name" | "capacity";
priority: number | null;
capacity: number | null;
name: string | null;
confirm: boolean;
full: boolean;
@@ -30,7 +31,7 @@ export interface RuntimeOptions {
export function parseRuntimeOptions(args: string[]): RuntimeOptions {
const [actionRaw = "list", ...rest] = args;
if (!(["list", "get", "errors", "events", "infrastructure", "apply", "delete"] as string[]).includes(actionRaw)) {
throw new Error("runtime usage: list|get|errors|events|infrastructure|apply|delete [--account <name-or-id>|--accounts <selectors>|--select-configured-policy --account-type apikey [--exclude-accounts <selectors>]] [--recent 24h] [--group <name-or-id>|--all-groups] [--platform <name>] [--page-token <token>] [--since 24h] [--tail 50000] [--template <id>] [--kind temp-unschedulable|priority|account-name] [--priority <0-1000>|--name <account-name>] [--confirm] [--target <id>] [--json|--full|--raw]");
throw new Error("runtime usage: list|get|errors|events|infrastructure|apply|delete [--account <name-or-id>|--accounts <selectors>|--select-configured-policy --account-type apikey [--exclude-accounts <selectors>]] [--recent 24h] [--group <name-or-id>|--all-groups] [--platform <name>] [--page-token <token>] [--since 24h] [--tail 50000] [--template <id>] [--kind temp-unschedulable|priority|account-name|capacity] [--priority <0-1000>|--name <account-name>|--capacity <1-100000>] [--confirm] [--target <id>] [--json|--full|--raw]");
}
let account: string | null = null;
let accounts: string[] = [];
@@ -42,8 +43,9 @@ export function parseRuntimeOptions(args: string[]): RuntimeOptions {
let platform: string | null = null;
let pageToken: string | null = null;
let template: string | null = null;
let kind: "temp-unschedulable" | "priority" | "account-name" = "temp-unschedulable";
let kind: "temp-unschedulable" | "priority" | "account-name" | "capacity" = "temp-unschedulable";
let priority: number | null = null;
let capacity: number | null = null;
let name: string | null = null;
let confirm = false;
let full = false;
@@ -88,6 +90,8 @@ export function parseRuntimeOptions(args: string[]): RuntimeOptions {
else if (arg.startsWith("--kind=")) kind = parseRuntimeKind(arg.slice("--kind=".length));
else if (arg === "--priority") priority = parsePriority(readValue("--priority"));
else if (arg.startsWith("--priority=")) priority = parsePriority(arg.slice("--priority=".length));
else if (arg === "--capacity") capacity = parseCapacity(readValue("--capacity"));
else if (arg.startsWith("--capacity=")) capacity = parseCapacity(arg.slice("--capacity=".length));
else if (arg === "--name") name = readValue("--name");
else if (arg.startsWith("--name=")) name = arg.slice("--name=".length).trim();
else if (arg === "--target") targetId = readValue("--target");
@@ -129,6 +133,10 @@ export function parseRuntimeOptions(args: string[]): RuntimeOptions {
if (kind === "priority" && priority === null) throw new Error("runtime apply --kind priority requires --priority <0-1000>");
if (kind === "priority" && template !== null) throw new Error("runtime apply --kind priority does not accept --template");
if (kind !== "priority" && priority !== null) throw new Error("--priority requires --kind priority");
if (kind === "capacity" && action !== "apply") throw new Error("runtime --kind capacity only supports apply");
if (kind === "capacity" && capacity === null) throw new Error("runtime apply --kind capacity requires --capacity <1-100000>");
if (kind === "capacity" && template !== null) throw new Error("runtime apply --kind capacity does not accept --template");
if (kind !== "capacity" && capacity !== null) throw new Error("--capacity requires --kind capacity");
if (kind === "account-name" && action !== "apply") throw new Error("runtime --kind account-name only supports apply");
if (kind === "account-name" && (name === null || name.length === 0 || name.length > 256 || /[\r\n]/u.test(name))) {
throw new Error("runtime apply --kind account-name requires --name with a supported account name");
@@ -141,7 +149,7 @@ export function parseRuntimeOptions(args: string[]): RuntimeOptions {
if (!/^\d+[mhd]$/u.test(since)) throw new Error("--since must use <number>m, <number>h, or <number>d");
if (["list", "get", "errors", "events", "infrastructure"].includes(action) && confirm) throw new Error(`runtime ${action} does not accept --confirm`);
if ([full, raw, json].filter(Boolean).length > 1) throw new Error("use only one of --json, --full, or --raw");
return { action, account, accounts, selectConfiguredPolicy, accountType, excludeAccounts, group, allGroups, platform, pageToken, template, kind, priority, name, confirm, full, raw, json, recent, since, tail, targetId };
return { action, account, accounts, selectConfiguredPolicy, accountType, excludeAccounts, group, allGroups, platform, pageToken, template, kind, priority, capacity, name, confirm, full, raw, json, recent, since, tail, targetId };
}
function parseDuration(value: string, option: string): string {
@@ -163,11 +171,17 @@ function parseAccountType(value: string): "apikey" {
return value;
}
function parseRuntimeKind(value: string): "temp-unschedulable" | "priority" | "account-name" {
if (value !== "temp-unschedulable" && value !== "priority" && value !== "account-name") throw new Error("--kind must be temp-unschedulable, priority, or account-name");
function parseRuntimeKind(value: string): "temp-unschedulable" | "priority" | "account-name" | "capacity" {
if (value !== "temp-unschedulable" && value !== "priority" && value !== "account-name" && value !== "capacity") throw new Error("--kind must be temp-unschedulable, priority, account-name, or capacity");
return value;
}
function parseCapacity(value: string): number {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100_000) throw new Error("--capacity must be an integer from 1 to 100000");
return parsed;
}
function parsePriority(value: string): number {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 1000) throw new Error("--priority must be an integer from 0 to 1000");
@@ -1159,6 +1159,20 @@ def mutation_plan(detail, action, include_rules):
"_kind": "priority",
"_desiredPriority": desired_priority,
}
if PAYLOAD.get("kind") == "capacity":
before_capacity = int(detail.get("concurrency") or 0)
desired_capacity = int(PAYLOAD.get("capacity"))
return {
"accountName": detail.get("name"),
"accountId": detail.get("id"),
"selectionReason": detail.get("_selectionReason"),
"change": "noop" if before_capacity == desired_capacity else "update",
"before": {"capacity": before_capacity},
"desired": {"capacity": desired_capacity},
"template": None,
"_kind": "capacity",
"_desiredCapacity": desired_capacity,
}
credentials = dict(detail.get("credentials") or {})
before = normalize_policy(credentials)
if action == "apply":
@@ -1195,6 +1209,8 @@ def write_mutation_plan(token, plan):
try:
if plan["_kind"] == "priority":
payload = {"priority": plan["_desiredPriority"]}
elif plan["_kind"] == "capacity":
payload = {"concurrency": plan["_desiredCapacity"]}
elif plan["_kind"] == "account-name":
payload = {"name": plan["_desiredName"]}
else:
@@ -1213,6 +1229,10 @@ def reconcile_mutation_plan(token, plan, write, include_rules):
actual_priority = int(actual_detail.get("priority") or 0)
item["actual"] = {"priority": actual_priority}
item["reconciled"] = actual_priority == plan["_desiredPriority"]
elif plan["_kind"] == "capacity":
actual_capacity = int(actual_detail.get("concurrency") or 0)
item["actual"] = {"capacity": actual_capacity}
item["reconciled"] = actual_capacity == plan["_desiredCapacity"]
elif plan["_kind"] == "account-name":
actual_name = str(actual_detail.get("name") or "")
item["actual"] = {"name": actual_name}
@@ -33,6 +33,7 @@ export async function codexPoolRuntime(config: UniDeskConfig, args: string[]): P
pageToken: options.pageToken,
kind: options.kind,
priority: options.priority,
capacity: options.capacity,
name: options.name,
confirm: options.confirm,
full: options.full,
@@ -94,6 +95,7 @@ export async function codexPoolRuntime(config: UniDeskConfig, args: string[]): P
template: options.template,
kind: options.kind,
priority: options.priority,
capacity: options.capacity,
name: options.name,
confirm: options.confirm,
recent: options.recent,
@@ -179,6 +181,7 @@ function renderRuntimeHelp(args: string[]): RenderedCliResult {
"Kinds:",
" temp-unschedulable Requires --template <id>",
" priority Requires --priority <0-1000>; lower is higher",
" capacity Requires --capacity <1-100000>; account concurrency guard",
" account-name Requires one --account and --name <new-name>",
"",
"Selection:",
@@ -443,6 +443,7 @@ export function codexPoolHelp(): unknown {
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --accounts <name-or-id,...> --template <id> [--target PK01] [--confirm] [--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --template <id> --select-configured-policy --account-type apikey [--exclude-accounts <name-or-id,...>] [--target PK01] [--confirm] [--json|--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --accounts <name-or-id,...> --kind priority --priority <0-1000> [--target PK01] [--confirm]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --accounts <name-or-id,...> --kind capacity --capacity <1-100000> [--target PK01] [--confirm]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --account <name-or-id> --kind account-name --name <new-name> [--target PK01] [--confirm]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --account <name-or-id> --kind temp-unschedulable [--target PK01] [--confirm]",
"bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --accounts <name-or-id,...> --kind temp-unschedulable [--target PK01] [--confirm] [--full|--raw]",