From bb988a027efcd4d9de63f9b2667c6067b083f1d1 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 15 Jul 2026 02:48:27 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=20Sub2API=20?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=BD=92=E5=9B=A0=E4=B8=8E=E4=BC=98=E5=85=88?= =?UTF-8?q?=E7=BA=A7=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unidesk-sub2api/references/codex-pool.md | 5 + .../runtime-mutation-render.ts | 48 ++++++++++ .../runtime-options.ts | 28 ++++-- .../platform-infra-sub2api-codex/runtime.ts | 92 +++++++------------ .../src/platform-infra-sub2api-codex/types.ts | 1 + 5 files changed, 106 insertions(+), 68 deletions(-) create mode 100644 scripts/src/platform-infra-sub2api-codex/runtime-mutation-render.ts diff --git a/.agents/skills/unidesk-sub2api/references/codex-pool.md b/.agents/skills/unidesk-sub2api/references/codex-pool.md index 810e9280..4b17aee4 100644 --- a/.agents/skills/unidesk-sub2api/references/codex-pool.md +++ b/.agents/skills/unidesk-sub2api/references/codex-pool.md @@ -83,6 +83,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 --account --template --confirm bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --accounts --template bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --accounts --template --confirm +bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --accounts --kind priority --priority <0-1000> --confirm bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --target PK01 --account --kind temp-unschedulable bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --target PK01 --account --kind model-mapping --model --upstream-model bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --target PK01 --account --kind model-mapping --model @@ -135,6 +136,10 @@ bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --target D60 - 手工账号需要采用模板时: - 写入使用显式 `runtime apply --account ...` 或 `--accounts ... --template ... --confirm`。 - 删除使用相同显式 selector 的 `runtime delete --kind temp-unschedulable --confirm`。 +- 账号全局 `priority` 使用 `runtime apply --kind priority --priority <0-1000>`: + - 数值越小优先级越高,不存在分组级 priority; + - 单账号和精准批量都必须先 dry-run,确认写入后逐账号回读并显示 before、desired、actual 与 reconciled; + - 只更新账号 priority,不携带 credentials、分组、容量、代理、状态、可调度或临时不可调度字段。 - 模型映射先用 `runtime models --account ...` 获取原生配置与实时上游能力: - 只有实时模型列表或真实模型请求证明支持时才确认写入。 - 禁止把名称相近的模型猜测为别名。 diff --git a/scripts/src/platform-infra-sub2api-codex/runtime-mutation-render.ts b/scripts/src/platform-infra-sub2api-codex/runtime-mutation-render.ts new file mode 100644 index 00000000..7f35cafd --- /dev/null +++ b/scripts/src/platform-infra-sub2api-codex/runtime-mutation-render.ts @@ -0,0 +1,48 @@ +import type { RenderedCliResult } from "../output"; +import { isRecord, stringValue } from "./config-utils"; +import { renderTable } from "./render"; +import type { RuntimeOptions } from "./runtime-options"; + +export function renderRuntimeMutationResult(ok: boolean, options: RuntimeOptions, parsed: Record | null, exitCode: number): RenderedCliResult { + const summary = parsed !== null && isRecord(parsed.summary) ? parsed.summary : {}; + const items = parsed !== null && Array.isArray(parsed.items) ? parsed.items.filter(isRecord) : []; + const lines = [[ + "SUB2API RUNTIME", `operation=${options.action}`, `mode=${stringValue(parsed?.mode) ?? (options.confirm ? "confirmed" : "dry-run")}`, + `ok=${ok ? "true" : "false"}`, `selected=${summary.selected ?? items.length}`, `changed=${summary.changed ?? "?"}`, + `writeSucceeded=${summary.writeSucceeded ?? "?"}`, `writeFailed=${summary.writeFailed ?? "?"}`, + `reconciled=${summary.reconciled ?? "?"}`, `mismatched=${summary.mismatched ?? "?"}`, + ].join(" ")]; + if (items.length > 0) { + const fields = items.map((item) => ({ + item, + write: isRecord(item.write) ? item.write : {}, + before: isRecord(item.before) ? item.before : {}, + desired: isRecord(item.desired) ? item.desired : {}, + actual: isRecord(item.actual) ? item.actual : {}, + })); + if (options.kind === "priority") lines.push(renderTable([ + ["ACCOUNT", "ID", "CHANGE", "WRITE", "RECONCILED", "BEFORE_PRIORITY", "DESIRED_PRIORITY", "ACTUAL_PRIORITY", "FAILURE"], + ...fields.map(({ item, write, before, desired, actual }) => [ + stringValue(item.accountName) ?? "-", String(item.accountId ?? "-"), stringValue(item.change) ?? "-", + stringValue(write.status) ?? "-", item.reconciled === true ? "true" : item.reconciled === false ? "false" : "-", + String(before.priority ?? "-"), String(desired.priority ?? "-"), String(actual.priority ?? "-"), + stringValue(write.error) ?? stringValue(item.reconcileError) ?? "-", + ]), + ])); + else lines.push(renderTable([ + ["ACCOUNT", "ID", "CHANGE", "WRITE", "RECONCILED", "BEFORE", "DESIRED", "ACTUAL", "BEFORE_CODES", "DESIRED_CODES", "ACTUAL_CODES", "FAILURE"], + ...fields.map(({ item, write, before, desired, actual }) => { + const codes = (value: unknown): string => Array.isArray(value) ? value.join(",") || "-" : "-"; + return [ + stringValue(item.accountName) ?? "-", String(item.accountId ?? "-"), stringValue(item.change) ?? "-", + stringValue(write.status) ?? "-", item.reconciled === true ? "true" : item.reconciled === false ? "false" : "-", + String(before.ruleCount ?? "-"), String(desired.ruleCount ?? "-"), String(actual.ruleCount ?? "-"), + codes(before.statusCodes), codes(desired.statusCodes), codes(actual.statusCodes), + stringValue(write.error) ?? stringValue(item.reconcileError) ?? "-", + ]; + }), + ])); + } else if (parsed !== null && typeof parsed.error === "string") lines.push(`ERROR ${parsed.error}`); + else if (parsed === null) lines.push(`ERROR runtime output unavailable remote_exit=${exitCode}`); + return { ok, command: "platform-infra sub2api codex-pool runtime", renderedText: lines.join("\n"), contentType: "text/plain", projection: parsed ?? undefined }; +} diff --git a/scripts/src/platform-infra-sub2api-codex/runtime-options.ts b/scripts/src/platform-infra-sub2api-codex/runtime-options.ts index da45d5dd..72178073 100644 --- a/scripts/src/platform-infra-sub2api-codex/runtime-options.ts +++ b/scripts/src/platform-infra-sub2api-codex/runtime-options.ts @@ -11,7 +11,8 @@ export interface RuntimeOptions { platform: string | null; pageToken: string | null; template: string | null; - kind: "temp-unschedulable"; + kind: "temp-unschedulable" | "priority"; + priority: number | null; confirm: boolean; full: boolean; raw: boolean; @@ -24,7 +25,7 @@ export interface RuntimeOptions { export function parseRuntimeOptions(args: string[]): RuntimeOptions { const [actionRaw = "list", ...rest] = args; if (!(["list", "get", "errors", "infrastructure", "apply", "delete"] as string[]).includes(actionRaw)) { - throw new Error("runtime usage: list|get|errors|infrastructure|apply|delete [--account |--accounts ] [--group |--all-groups] [--platform ] [--page-token ] [--since 24h] [--tail 50000] [--template ] [--kind temp-unschedulable] [--confirm] [--target ] [--json|--full|--raw]"); + throw new Error("runtime usage: list|get|errors|infrastructure|apply|delete [--account |--accounts ] [--group |--all-groups] [--platform ] [--page-token ] [--since 24h] [--tail 50000] [--template ] [--kind temp-unschedulable|priority] [--priority <0-1000>] [--confirm] [--target ] [--json|--full|--raw]"); } let account: string | null = null; let accounts: string[] = []; @@ -33,7 +34,8 @@ 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" as const; + let kind: "temp-unschedulable" | "priority" = "temp-unschedulable"; + let priority: number | null = null; let confirm = false; let full = false; let raw = false; @@ -69,6 +71,8 @@ export function parseRuntimeOptions(args: string[]): RuntimeOptions { else if (arg.startsWith("--template=")) template = arg.slice("--template=".length).trim(); else if (arg === "--kind") kind = parseRuntimeKind(readValue("--kind")); 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 === "--target") targetId = readValue("--target"); else if (arg.startsWith("--target=")) targetId = arg.slice("--target=".length).trim(); else if (arg === "--since") since = readValue("--since"); @@ -91,13 +95,17 @@ export function parseRuntimeOptions(args: string[]): RuntimeOptions { if ((group !== null || platform !== null) && action !== "errors" && action !== "infrastructure") throw new Error(`runtime ${action} does not accept group scope options`); if (pageToken !== null && !allGroups) throw new Error("--page-token requires --all-groups"); if (account !== null && allGroups) throw new Error("--account cannot be combined with --all-groups; use --group first"); - if (action === "apply" && !template) throw new Error("runtime apply requires --template "); + if (action === "apply" && kind === "temp-unschedulable" && !template) throw new Error("runtime apply --kind temp-unschedulable requires --template "); + if (kind === "priority" && action !== "apply") throw new Error("runtime --kind priority only supports apply"); + 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 (action !== "apply" && template !== null) throw new Error(`runtime ${action} does not accept --template`); if (action !== "errors" && action !== "infrastructure" && (since !== "24h" || tail !== 50_000)) throw new Error(`runtime ${action} does not accept --since or --tail`); if (!/^\d+[mhd]$/u.test(since)) throw new Error("--since must use m, h, or d"); if (["list", "get", "errors", "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, group, allGroups, platform, pageToken, template, kind, confirm, full, raw, json, since, tail, targetId }; + return { action, account, accounts, group, allGroups, platform, pageToken, template, kind, priority, confirm, full, raw, json, since, tail, targetId }; } function parseAccountSelectors(value: string): string[] { @@ -108,11 +116,17 @@ function parseAccountSelectors(value: string): string[] { return selectors; } -function parseRuntimeKind(value: string): "temp-unschedulable" { - if (value !== "temp-unschedulable") throw new Error("--kind must be temp-unschedulable"); +function parseRuntimeKind(value: string): "temp-unschedulable" | "priority" { + if (value !== "temp-unschedulable" && value !== "priority") throw new Error("--kind must be temp-unschedulable or priority"); return value; } +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"); + return parsed; +} + function parseRuntimeTail(value: string): number { const parsed = Number(value); if (!Number.isInteger(parsed) || parsed < 100 || parsed > 200_000) throw new Error("--tail must be an integer from 100 to 200000"); diff --git a/scripts/src/platform-infra-sub2api-codex/runtime.ts b/scripts/src/platform-infra-sub2api-codex/runtime.ts index 128ffc8f..d8929ede 100644 --- a/scripts/src/platform-infra-sub2api-codex/runtime.ts +++ b/scripts/src/platform-infra-sub2api-codex/runtime.ts @@ -2,18 +2,18 @@ import type { UniDeskConfig } from "../config"; import type { RenderedCliResult } from "../output"; import { desiredAccountNames } from "./accounts"; import { readCodexPoolConfig } from "./config"; -import { isRecord, stringValue } from "./config-utils"; import { protectedManualAccountsForTarget } from "./public-exposure"; import { renderSub2ApiTempUnschedulableCredentials } from "./redaction"; import { boolField, compactCapture, parseJsonOutput, runRemoteCodexPoolScript } from "./remote"; import { renderedCliResult, renderTable } from "./render"; import { parseRuntimeOptions, type RuntimeOptions } from "./runtime-options"; +import { renderRuntimeMutationResult } from "./runtime-mutation-render"; import { codexPoolRuntimeTarget } from "./runtime-target"; export async function codexPoolRuntime(config: UniDeskConfig, args: string[]): Promise | RenderedCliResult> { const options = parseRuntimeOptions(args); const pool = readCodexPoolConfig(); const target = codexPoolRuntimeTarget(options.targetId); - const selectedTemplate = options.template === null ? null : pool.runtime.templatesById[options.template]; + const selectedTemplate = options.kind === "priority" || options.template === null ? null : pool.runtime.templatesById[options.template]; if (options.template !== null && selectedTemplate === undefined) { throw new Error(`unknown runtime template ${options.template}; available: ${pool.runtime.templates.map((item) => item.id).join(", ")}`); } @@ -26,6 +26,7 @@ export async function codexPoolRuntime(config: UniDeskConfig, args: string[]): P platform: options.platform, pageToken: options.pageToken, kind: options.kind, + priority: options.priority, confirm: options.confirm, full: options.full, since: options.since, @@ -81,6 +82,7 @@ export async function codexPoolRuntime(config: UniDeskConfig, args: string[]): P pageToken: options.pageToken, template: options.template, kind: options.kind, + priority: options.priority, confirm: options.confirm, since: options.action === "errors" || options.action === "infrastructure" ? options.since : undefined, tail: options.action === "errors" || options.action === "infrastructure" ? options.tail : undefined, @@ -290,6 +292,7 @@ function compactCustomerErrors(value: unknown, detailed: boolean): unknown { accountBuckets: detailed ? runtimeRecords(profile.accountBuckets).slice(0, 5) : undefined, statusBuckets: runtimeRecords(profile.statusBuckets).slice(0, 5), streamBuckets: runtimeRecords(profile.streamBuckets).slice(0, 5), + messageBuckets: runtimeRecords(profile.messageBuckets).slice(0, detailed ? 10 : 5), }; } function compactAccountQuality(value: unknown, detailed: boolean): unknown { @@ -715,60 +718,6 @@ function renderRuntimeMutation(lines: string[], runtime: Record ])); } -function renderRuntimeMutationResult(ok: boolean, options: RuntimeOptions, parsed: Record | null, exitCode: number): RenderedCliResult { - const summary = parsed !== null && isRecord(parsed.summary) ? parsed.summary : {}; - const items = parsed !== null && Array.isArray(parsed.items) ? parsed.items.filter(isRecord) : []; - const lines = [[ - "SUB2API RUNTIME", - `operation=${options.action}`, - `mode=${stringValue(parsed?.mode) ?? (options.confirm ? "confirmed" : "dry-run")}`, - `ok=${ok ? "true" : "false"}`, - `selected=${summary.selected ?? items.length}`, - `changed=${summary.changed ?? "?"}`, - `writeSucceeded=${summary.writeSucceeded ?? "?"}`, - `writeFailed=${summary.writeFailed ?? "?"}`, - `reconciled=${summary.reconciled ?? "?"}`, - `mismatched=${summary.mismatched ?? "?"}`, - ].join(" ")]; - if (items.length > 0) { - lines.push(renderTable([ - ["ACCOUNT", "ID", "CHANGE", "WRITE", "RECONCILED", "BEFORE", "DESIRED", "ACTUAL", "BEFORE_CODES", "DESIRED_CODES", "ACTUAL_CODES", "FAILURE"], - ...items.map((item) => { - const write = isRecord(item.write) ? item.write : {}; - const before = isRecord(item.before) ? item.before : {}; - const desired = isRecord(item.desired) ? item.desired : {}; - const actual = isRecord(item.actual) ? item.actual : {}; - const codes = (value: unknown): string => Array.isArray(value) ? value.join(",") || "-" : "-"; - return [ - stringValue(item.accountName) ?? "-", - String(item.accountId ?? "-"), - stringValue(item.change) ?? "-", - stringValue(write.status) ?? "-", - item.reconciled === true ? "true" : item.reconciled === false ? "false" : "-", - String(before.ruleCount ?? "-"), - String(desired.ruleCount ?? "-"), - String(actual.ruleCount ?? "-"), - codes(before.statusCodes), - codes(desired.statusCodes), - codes(actual.statusCodes), - stringValue(write.error) ?? stringValue(item.reconcileError) ?? "-", - ]; - }), - ])); - } else if (parsed !== null && typeof parsed.error === "string") { - lines.push(`ERROR ${parsed.error}`); - } else if (parsed === null) { - lines.push(`ERROR runtime output unavailable remote_exit=${exitCode}`); - } - return { - ok, - command: "platform-infra sub2api codex-pool runtime", - renderedText: lines.join("\n"), - contentType: "text/plain", - projection: parsed ?? undefined, - }; -} - function pyJson(value: unknown): string { return `json.loads(${JSON.stringify(JSON.stringify(value))})`; } @@ -1918,6 +1867,7 @@ def native_customer_error_profile(token, group_id, platform): "accountBuckets": buckets(lambda row: row.get("account_id")), "statusBuckets": buckets(lambda row: row.get("status_code")), "streamBuckets": buckets(lambda row: "stream" if row.get("stream") is True else "sync" if row.get("stream") is False else "unknown"), + "messageBuckets": buckets(lambda row: compact_error(row.get("message") or row.get("error_message"))), "phaseBuckets": buckets(lambda row: row.get("phase")), "endpointBuckets": buckets(lambda row: row.get("inbound_endpoint") or row.get("request_path")), "pathBuckets": buckets(lambda row: row.get("request_path") or row.get("inbound_endpoint")), @@ -2851,13 +2801,26 @@ def selected_account_details(token, accounts): account_id = detail.get("id") if account_id in selected_ids: raise RuntimeError("duplicate-account-selector: " + selector) - if detail.get("type") != "apikey": + if PAYLOAD.get("kind") == "temp-unschedulable" and detail.get("type") != "apikey": raise RuntimeError("runtime temp-unschedulable policy requires an apikey account: " + selector) selected_ids.add(account_id) details.append(detail) return details def mutation_plan(detail, action, include_rules): + if PAYLOAD.get("kind") == "priority": + before_priority = int(detail.get("priority") or 0) + desired_priority = int(PAYLOAD.get("priority")) + return { + "accountName": detail.get("name"), + "accountId": detail.get("id"), + "change": "noop" if before_priority == desired_priority else "update", + "before": {"priority": before_priority}, + "desired": {"priority": desired_priority}, + "template": None, + "_kind": "priority", + "_desiredPriority": desired_priority, + } credentials = dict(detail.get("credentials") or {}) before = normalize_policy(credentials) if action == "apply": @@ -2884,13 +2847,15 @@ def mutation_plan(detail, action, include_rules): "template": PAYLOAD.get("selectedTemplate", {}).get("id") if isinstance(PAYLOAD.get("selectedTemplate"), dict) else None, "_desiredCredentials": desired, "_desiredPolicy": desired_policy, + "_kind": "temp-unschedulable", } def write_mutation_plan(token, plan): if plan["change"] == "noop": return {"attempted": False, "status": "noop", "error": None} try: - data_of(curl_api("PUT", f"/api/v1/admin/accounts/{plan['accountId']}", bearer=token, payload={"credentials": plan["_desiredCredentials"]}), "update account runtime config") + payload = {"priority": plan["_desiredPriority"]} if plan["_kind"] == "priority" else {"credentials": plan["_desiredCredentials"]} + data_of(curl_api("PUT", f"/api/v1/admin/accounts/{plan['accountId']}", bearer=token, payload=payload), "update account runtime config") return {"attempted": True, "status": "succeeded", "error": None} except Exception as exc: return {"attempted": True, "status": "failed", "error": compact_error(str(exc))} @@ -2900,9 +2865,14 @@ def reconcile_mutation_plan(token, plan, write, include_rules): item["write"] = write try: actual_detail = account_detail(token, plan["accountId"]) - actual_policy = normalize_policy(actual_detail.get("credentials")) - item["actual"] = policy_summary(actual_policy, include_rules) - item["reconciled"] = actual_policy == plan["_desiredPolicy"] + if plan["_kind"] == "priority": + actual_priority = int(actual_detail.get("priority") or 0) + item["actual"] = {"priority": actual_priority} + item["reconciled"] = actual_priority == plan["_desiredPriority"] + else: + actual_policy = normalize_policy(actual_detail.get("credentials")) + item["actual"] = policy_summary(actual_policy, include_rules) + item["reconciled"] = actual_policy == plan["_desiredPolicy"] item["reconcileError"] = None except Exception as exc: item["actual"] = None diff --git a/scripts/src/platform-infra-sub2api-codex/types.ts b/scripts/src/platform-infra-sub2api-codex/types.ts index 69c3614e..c8ce134b 100644 --- a/scripts/src/platform-infra-sub2api-codex/types.ts +++ b/scripts/src/platform-infra-sub2api-codex/types.ts @@ -397,6 +397,7 @@ export function codexPoolHelp(): unknown { "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]", + "bun scripts/cli.ts platform-infra sub2api codex-pool runtime apply --accounts --kind priority --priority <0-1000> [--target PK01] [--confirm]", "bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --account --kind temp-unschedulable [--target PK01] [--confirm]", "bun scripts/cli.ts platform-infra sub2api codex-pool runtime delete --accounts --kind temp-unschedulable [--target PK01] [--confirm] [--full|--raw]", "bun scripts/cli.ts platform-infra sub2api codex-pool trace [--target D601] --request-id [--since 24h|--tail 20000|--context-seconds 300|--show-lines|--raw]",