feat: 增加 Sub2API 账号运行态查询

This commit is contained in:
Codex
2026-07-13 19:35:14 +02:00
parent 3c353d8069
commit bdab46b3dd
9 changed files with 238 additions and 8 deletions
@@ -22,7 +22,7 @@ import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "..
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
import type { CodexPoolConfig, CodexPoolRuntimeTarget } from "./types";
import { desiredAccountCapacityMap, desiredAccountLoadFactorMap, desiredAccountTempUnschedulableMap, desiredAccountWebSocketsV2ModeMap } from "./accounts";
import { desiredAccountCapacityMap, desiredAccountLoadFactorMap, desiredAccountTempUnschedulableMap, desiredAccountWebSocketsV2ModeMap, desiredDefaultAccountTempUnschedulable } from "./accounts";
import { resolvedManualAccountProtections } from "./public-exposure";
import { fieldManager } from "./types";
@@ -30,7 +30,7 @@ function pyJson(value: unknown): string {
return `json.loads(${JSON.stringify(JSON.stringify(value))})`;
}
export function remotePythonScript(mode: "sync" | "validate" | "trace" | "cleanup-probes" | "sentinel-probe", encodedPayload: string, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
export function remotePythonScript(mode: "sync" | "validate" | "runtime-state" | "trace" | "cleanup-probes" | "sentinel-probe", encodedPayload: string, pool: CodexPoolConfig, target: CodexPoolRuntimeTarget): string {
const hostDockerEnvPath = target.runtimeMode === "host-docker" ? target.hostDockerEnvPath : null;
return `
set -u
@@ -75,6 +75,7 @@ EXPECTED_ACCOUNT_CAPACITIES = ${pyJson(desiredAccountCapacityMap(pool))}
EXPECTED_ACCOUNT_LOAD_FACTORS = ${pyJson(desiredAccountLoadFactorMap(pool))}
EXPECTED_ACCOUNT_WS_MODES = json.loads(${JSON.stringify(JSON.stringify(desiredAccountWebSocketsV2ModeMap(pool)))})
EXPECTED_ACCOUNT_TEMP_UNSCHEDULABLE = json.loads(${JSON.stringify(JSON.stringify(desiredAccountTempUnschedulableMap(pool)))})
EXPECTED_DEFAULT_TEMP_UNSCHEDULABLE = json.loads(${JSON.stringify(JSON.stringify(desiredDefaultAccountTempUnschedulable(pool)))})
MANUAL_ACCOUNT_PROTECTIONS = json.loads(${JSON.stringify(JSON.stringify(resolvedManualAccountProtections(pool, target)))})
SENTINEL_CONFIG = json.loads(${JSON.stringify(JSON.stringify(pool.sentinel))})
TARGET_EGRESS_PROXY = json.loads(${JSON.stringify(JSON.stringify(target.egressProxy))})
@@ -2436,6 +2437,134 @@ def account_temp_unschedulable_status(token):
"valuesPrinted": False,
}
def normalized_int(value):
if isinstance(value, bool):
return None
if isinstance(value, int):
return value
if isinstance(value, float):
return int(value)
if isinstance(value, str):
try:
return int(value.strip())
except ValueError:
return None
return None
def normalized_status_codes(value):
if not isinstance(value, list):
return None
codes = []
for item in value:
code = normalized_int(item)
if code is not None and 100 <= code <= 599 and code not in codes:
codes.append(code)
return sorted(codes)
def temp_unschedulable_active(until):
if not isinstance(until, str) or not until.strip():
return False
try:
parsed = datetime.fromisoformat(until.replace("Z", "+00:00"))
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed > datetime.now(timezone.utc)
except ValueError:
return False
def account_runtime_state(token):
group = next((item for item in list_groups(token) if item.get("name") == POOL_GROUP_NAME), None)
if not isinstance(group, dict) or group.get("id") is None:
return {
"ok": False,
"group": {"name": POOL_GROUP_NAME, "id": None, "found": False},
"accounts": [],
"error": "pool-group-not-found",
"valuesPrinted": False,
}
baseline = normalize_temp_unschedulable_credentials(EXPECTED_DEFAULT_TEMP_UNSCHEDULABLE)
baseline_codes = sorted(set(rule.get("error_code") for rule in baseline["rules"] if isinstance(rule.get("error_code"), int)))
protected_names = set(item.get("accountName") for item in MANUAL_ACCOUNT_PROTECTIONS if isinstance(item, dict) and isinstance(item.get("accountName"), str))
accounts = list_accounts_for_group(token, group["id"])
items = []
for account in sorted(accounts, key=lambda item: str(item.get("name") or "")):
detail = get_account_detail(token, account)
credentials = runtime_account_credentials(detail)
runtime_policy = normalize_temp_unschedulable_credentials(credentials)
runtime_codes = sorted(set(rule.get("error_code") for rule in runtime_policy["rules"] if isinstance(rule.get("error_code"), int)))
name = detail.get("name") or account.get("name")
pool_mode = bool_value(credentials.get("pool_mode"))
temp_until = detail.get("temp_unschedulable_until") or detail.get("tempUnschedulableUntil")
temp_reason = detail.get("temp_unschedulable_reason") or detail.get("tempUnschedulableReason") or ""
origin = "yaml-managed" if name in EXPECTED_ACCOUNT_TEMP_UNSCHEDULABLE else ("yaml-protected-manual" if name in protected_names else "runtime-manual")
supports_policy = (detail.get("type") or account.get("type")) == "apikey"
policy_matches = runtime_policy == baseline if supports_policy else None
missing_codes = [code for code in baseline_codes if code not in runtime_codes] if supports_policy else []
items.append({
"accountName": name,
"accountId": detail.get("id") or account.get("id"),
"origin": origin,
"platform": detail.get("platform") or account.get("platform"),
"type": detail.get("type") or account.get("type"),
"status": detail.get("status") or account.get("status"),
"schedulable": detail.get("schedulable") if detail.get("schedulable") is not None else account.get("schedulable"),
"proxyId": detail.get("proxy_id") if detail.get("proxy_id") is not None else account.get("proxy_id"),
"priority": detail.get("priority") if detail.get("priority") is not None else account.get("priority"),
"concurrency": detail.get("concurrency") if detail.get("concurrency") is not None else account.get("concurrency"),
"currentConcurrency": account.get("current_concurrency"),
"baseUrl": normalize_runtime_base_url(credentials.get("base_url")),
"tempUnschedulable": {
"enabled": runtime_policy["enabled"],
"ruleCount": len(runtime_policy["rules"]),
"statusCodes": runtime_codes,
"rules": summarize_temp_unschedulable_rules(runtime_policy["rules"]),
"until": temp_until,
"reasonPreview": text(str(temp_reason), 500) if temp_reason else "",
"stateRecorded": temp_until is not None or bool(temp_reason),
"active": temp_unschedulable_active(temp_until),
"matchesYamlBaseline": policy_matches,
"missingYamlBaselineStatusCodes": missing_codes,
},
"poolMode": {
"enabled": pool_mode,
"retryCountConfigured": normalized_int(credentials.get("pool_mode_retry_count")) if "pool_mode_retry_count" in credentials else None,
"retryStatusCodesConfigured": normalized_status_codes(credentials.get("pool_mode_retry_status_codes")) if "pool_mode_retry_status_codes" in credentials else None,
},
"optimization": {
"tempUnschedulableNeedsAlignment": supports_policy and runtime_policy != baseline,
"poolModeShouldBeDisabled": pool_mode,
},
"credentialsPrinted": False,
})
temp_alignment = [item["accountName"] for item in items if item["optimization"]["tempUnschedulableNeedsAlignment"]]
pool_mode_enabled = [item["accountName"] for item in items if item["poolMode"]["enabled"]]
return {
"ok": True,
"group": {
"name": POOL_GROUP_NAME,
"id": group.get("id"),
"found": True,
"platform": group.get("platform"),
"status": group.get("status"),
},
"yamlBaseline": {
"enabled": baseline["enabled"],
"ruleCount": len(baseline["rules"]),
"statusCodes": baseline_codes,
},
"summary": {
"accountCount": len(items),
"tempUnschedulableEnabledCount": sum(1 for item in items if item["tempUnschedulable"]["enabled"]),
"tempUnschedulableNeedsAlignmentCount": len(temp_alignment),
"tempUnschedulableNeedsAlignment": temp_alignment,
"poolModeEnabledCount": len(pool_mode_enabled),
"poolModeEnabled": pool_mode_enabled,
"optimizationAvailable": bool(temp_alignment or pool_mode_enabled),
},
"accounts": items,
"valuesPrinted": False,
}
def account_capacity_status(token):
accounts = list_accounts(token)
by_name = {item.get("name"): item for item in accounts if isinstance(item.get("name"), str)}
@@ -2776,6 +2905,21 @@ def run_validate():
"validation": {"gatewayModels": gateway, "gatewayResponses": responses_smoke, "gatewayResponsesRecent": responses_evidence, "gatewayCompactRecent": compact_evidence},
}
def run_runtime_state():
admin_email, token, admin_compliance = login()
state = account_runtime_state(token)
return {
"ok": state.get("ok") is True,
"mode": "runtime-state",
"namespace": NAMESPACE,
"serviceDns": SERVICE_DNS,
"appPod": APP_POD,
"admin": {"email": admin_email, "tokenPrinted": False, "compliance": admin_compliance},
"runtimeImage": app_pod_runtime_image(),
"state": state,
"valuesPrinted": False,
}
def parse_log_line(line):
json_start = line.find("{")
if json_start < 0:
@@ -3286,6 +3430,8 @@ def run_cleanup_probes():
try:
if MODE == "sync":
result = run_sync()
elif MODE == "runtime-state":
result = run_runtime_state()
elif MODE == "trace":
result = run_trace()
elif MODE == "cleanup-probes":