feat: 增加 Sub2API 利润统计 CLI

This commit is contained in:
Codex
2026-07-14 15:54:11 +02:00
parent f4e30bc02b
commit 30dc651170
14 changed files with 481 additions and 5 deletions
@@ -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, CodexPoolPublicExposureConfig, CodexProfile, CodexRuntimeConfig, CodexRuntimeTemplate, CodexSentinelProtectPolicy, CodexTempUnschedulablePolicy, CodexTempUnschedulableRule, OpenAIResponsesWebSocketsV2Mode } from "./types";
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 { 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";
@@ -179,6 +179,7 @@ export function readCodexPoolConfig(): CodexPoolConfig {
defaultSentinelProtect,
profiles,
runtime,
profit: readProfitConfig(parsed.profit),
manualAccounts,
publicExposure: readPublicExposureConfig(parsed.publicExposure),
localCodex: readLocalCodexConfig(parsed.localCodex),
@@ -199,6 +200,51 @@ export function readCodexPoolConfig(): CodexPoolConfig {
return config;
}
function readProfitConfig(value: unknown): CodexPoolProfitConfig {
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.profit must be a YAML object`);
const groups = requiredRecordConfigField(value, "groups", "profit");
const profitable = readUniqueStringArray(groups.profitable, "profit.groups.profitable", false);
const selfUse = readUniqueStringArray(groups.selfUse, "profit.groups.selfUse", false);
const excluded = readUniqueStringArray(groups.excluded, "profit.groups.excluded", true);
const allGroups = [...profitable, ...selfUse, ...excluded];
if (new Set(allGroups).size !== allGroups.length) {
throw new Error(`${codexPoolConfigPath}.profit.groups entries must be disjoint`);
}
const netRevenueCoefficient = numberValue(value.netRevenueCoefficient);
if (netRevenueCoefficient === null || netRevenueCoefficient < 0) {
throw new Error(`${codexPoolConfigPath}.profit.netRevenueCoefficient must be >= 0`);
}
const defaultWindow = requiredStringConfigField(value, "defaultWindow", "profit");
if (!/^[1-9][0-9]*(?:s|m|h|d)$/u.test(defaultWindow)) {
throw new Error(`${codexPoolConfigPath}.profit.defaultWindow must be a duration such as 24h`);
}
const usageCostField = requiredStringConfigField(value, "usageCostField", "profit");
if (usageCostField !== "actual_cost") {
throw new Error(`${codexPoolConfigPath}.profit.usageCostField must be actual_cost`);
}
const accountCostSource = requiredStringConfigField(value, "accountCostSource", "profit");
if (accountCostSource !== "account-name-trailing-coefficient") {
throw new Error(`${codexPoolConfigPath}.profit.accountCostSource must be account-name-trailing-coefficient`);
}
return {
defaultWindow,
usageCostField,
accountCostSource,
netRevenueCoefficient,
groups: { profitable, selfUse, excluded },
};
}
function readUniqueStringArray(value: unknown, path: string, allowEmpty: boolean): string[] {
if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && item.trim().length > 0)) {
throw new Error(`${codexPoolConfigPath}.${path} must be an array of non-empty strings`);
}
const normalized = value.map((item) => item.trim());
if (!allowEmpty && normalized.length === 0) throw new Error(`${codexPoolConfigPath}.${path} must not be empty`);
if (new Set(normalized).size !== normalized.length) throw new Error(`${codexPoolConfigPath}.${path} must not contain duplicates`);
return normalized;
}
export function readRuntimeConfig(value: unknown): CodexRuntimeConfig {
if (!isRecord(value)) throw new Error(`${codexPoolConfigPath}.runtime must be a YAML object`);
if (!Array.isArray(value.templates)) throw new Error(`${codexPoolConfigPath}.runtime.templates must be a YAML array`);