feat: 增加 Sub2API 利润统计 CLI
This commit is contained in:
@@ -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`);
|
||||
|
||||
Reference in New Issue
Block a user