diff --git a/.agents/skills/unidesk-sub2api/references/codex-pool.md b/.agents/skills/unidesk-sub2api/references/codex-pool.md index d4f96504..56049e47 100644 --- a/.agents/skills/unidesk-sub2api/references/codex-pool.md +++ b/.agents/skills/unidesk-sub2api/references/codex-pool.md @@ -97,8 +97,9 @@ bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --target D60 ``` - `profit` 只读聚合 Sub2API 原生 `/api/v1/admin/usage`: - - 默认窗口、盈利分组、自用分组、排除分组、净收入系数和账号采购成本来源由 `config/platform-infra/sub2api-codex-pool.yaml#profit` 声明; - - 默认 Kubernetes 列表按完整账号名称显示请求数、Token、原生 `actual_cost`、采购成本系数、收入、上游成本和利润贡献; + - 默认窗口、盈利分组、自用分组、排除分组、人民币售价和账号采购成本来源由 `config/platform-infra/sub2api-codex-pool.yaml#profit` 声明; + - 原生 `actual_cost` 是标准 API 美元计费基数;`saleRateCnyPerApiUsd` 和账号名称末尾成本都表示每 1 美元标准 API 计费对应的人民币单价,不做汇率换算; + - 默认 Kubernetes 列表按完整账号名称显示请求数、Token、标准 API 美元计费基数、人民币采购单价、人民币收入、人民币上游成本和人民币利润贡献; - 汇总同时给出“不扣自用成本”和“扣除自用成本”两种口径,自用分组不计收入; - 账号名称末尾缺少采购成本系数,或存在未分类流量时,相关口径必须显示 `partial`,禁止把缺失值当作零; - `--json` 保留精确十进制字符串、原生来源、时间窗、分页数和完整性明细;命令不修改 runtime。 diff --git a/config/platform-infra/sub2api-codex-pool.yaml b/config/platform-infra/sub2api-codex-pool.yaml index 1ff2b71b..2ff205f5 100644 --- a/config/platform-infra/sub2api-codex-pool.yaml +++ b/config/platform-infra/sub2api-codex-pool.yaml @@ -48,8 +48,8 @@ runtime: profit: defaultWindow: 24h usageCostField: actual_cost - accountCostSource: account-name-trailing-coefficient - netRevenueCoefficient: 0.10 + accountCostSource: account-name-trailing-cny-per-api-usd + saleRateCnyPerApiUsd: 0.10 groups: profitable: [unidesk-codex-pool] selfUse: [自用] diff --git a/scripts/src/platform-infra-sub2api-codex/config.ts b/scripts/src/platform-infra-sub2api-codex/config.ts index e169d034..539808c7 100644 --- a/scripts/src/platform-infra-sub2api-codex/config.ts +++ b/scripts/src/platform-infra-sub2api-codex/config.ts @@ -210,9 +210,9 @@ function readProfitConfig(value: unknown): CodexPoolProfitConfig { 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 saleRateCnyPerApiUsd = numberValue(value.saleRateCnyPerApiUsd); + if (saleRateCnyPerApiUsd === null || saleRateCnyPerApiUsd < 0) { + throw new Error(`${codexPoolConfigPath}.profit.saleRateCnyPerApiUsd must be >= 0`); } const defaultWindow = requiredStringConfigField(value, "defaultWindow", "profit"); if (!/^[1-9][0-9]*(?:s|m|h|d)$/u.test(defaultWindow)) { @@ -223,14 +223,14 @@ function readProfitConfig(value: unknown): CodexPoolProfitConfig { 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`); + if (accountCostSource !== "account-name-trailing-cny-per-api-usd") { + throw new Error(`${codexPoolConfigPath}.profit.accountCostSource must be account-name-trailing-cny-per-api-usd`); } return { defaultWindow, usageCostField, accountCostSource, - netRevenueCoefficient, + saleRateCnyPerApiUsd, groups: { profitable, selfUse, excluded }, }; } diff --git a/scripts/src/platform-infra-sub2api-codex/profit.ts b/scripts/src/platform-infra-sub2api-codex/profit.ts index 3ade35f8..f2199b3d 100644 --- a/scripts/src/platform-infra-sub2api-codex/profit.ts +++ b/scripts/src/platform-infra-sub2api-codex/profit.ts @@ -91,24 +91,24 @@ function renderProfit(response: Record): RenderedCliResult { const lines = [ `SUB2API PROFIT status=${text(report.status)} target=${text(record(response.target).id)} window=${text(window.since)}`, `WINDOW ${text(window.startAt)} -> ${text(window.endAt)} SOURCE native-admin-usage/${text(record(report.source).usageCostField)}`, - `POLICY net-revenue=${text(policy.netRevenueCoefficient)} account-cost=${text(policy.accountCostSource)}`, + `POLICY sale-rate=${text(policy.saleRateCnyPerApiUsd)}CNY/API_USD account-cost=${text(policy.accountCostSource)}`, "", "ACCOUNTS", ]; const accounts = arrayOfRecords(report.accounts); lines.push(table( - ["GROUP", "CLASS", "ACCOUNT", "ID", "REQ", "TOKENS", "API_AMOUNT", "COST_RATE", "REVENUE", "UPSTREAM_COST", "CONTRIBUTION", "STATUS"], + ["GROUP", "CLASS", "ACCOUNT", "ID", "REQ", "TOKENS", "API_USD", "COST_CNY/USD", "REVENUE_CNY", "UPSTREAM_CNY", "PROFIT_CNY", "STATUS"], accounts.map((row) => [ text(row.groupName), text(row.class), text(row.accountName), text(row.accountId), text(row.requestCount), compactTokens(row.tokenCount), - usd(row.apiAmountUsd), text(row.costCoefficient), usd(row.revenueUsd), usd(row.upstreamCostUsd), usd(row.profitContributionUsd), text(row.completeness), + apiUsd(row.apiAmountUsd), text(row.costRateCnyPerApiUsd), cny(row.revenueCny), cny(row.upstreamCostCny), cny(row.profitContributionCny), text(row.completeness), ]), )); lines.push("", "PROFIT"); lines.push(table( - ["SCOPE", "STATUS", "REVENUE", "PROFIT_POOL_COST", "SELF_USE_COST", "PROFIT"], + ["SCOPE", "STATUS", "REVENUE_CNY", "PROFIT_POOL_CNY", "SELF_USE_CNY", "PROFIT_CNY"], [ - ["不扣自用", text(record(summary.withoutSelfUse).status), usd(summary.revenueUsd), usd(summary.profitPoolCostUsd), "-", usd(record(summary.withoutSelfUse).profitUsd)], - ["扣除自用", text(record(summary.withSelfUse).status), usd(summary.revenueUsd), usd(summary.profitPoolCostUsd), usd(summary.selfUseCostUsd), usd(record(summary.withSelfUse).profitUsd)], + ["不扣自用", text(record(summary.withoutSelfUse).status), cny(summary.revenueCny), cny(summary.profitPoolCostCny), "-", cny(record(summary.withoutSelfUse).profitCny)], + ["扣除自用", text(record(summary.withSelfUse).status), cny(summary.revenueCny), cny(summary.profitPoolCostCny), cny(summary.selfUseCostCny), cny(record(summary.withSelfUse).profitCny)], ], )); const missingCosts = arrayOfRecords(completeness.missingCostAccounts); @@ -161,12 +161,18 @@ function compactTokens(value: unknown): string { return String(numeric); } -function usd(value: unknown): string { +function apiUsd(value: unknown): string { if (value === null || value === undefined || value === "") return "-"; const numeric = Number(value); return Number.isFinite(numeric) ? `$${numeric.toFixed(6)}` : text(value); } +function cny(value: unknown): string { + if (value === null || value === undefined || value === "") return "-"; + const numeric = Number(value); + return Number.isFinite(numeric) ? `¥${numeric.toFixed(6)}` : text(value); +} + function table(headers: string[], rows: string[][]): string { if (rows.length === 0) return `${headers.join(" ")}\n-`; const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length))); diff --git a/scripts/src/platform-infra-sub2api-codex/remote-python-profit.ts b/scripts/src/platform-infra-sub2api-codex/remote-python-profit.ts index 87e6d1a9..405f7a34 100644 --- a/scripts/src/platform-infra-sub2api-codex/remote-python-profit.ts +++ b/scripts/src/platform-infra-sub2api-codex/remote-python-profit.ts @@ -112,7 +112,7 @@ def run_profit_report(): config = payload.get("config") if isinstance(payload.get("config"), dict) else {} window = payload.get("since") cost_field = config.get("usageCostField") - net_rate = profit_decimal(config.get("netRevenueCoefficient")) + sale_rate = profit_decimal(config.get("saleRateCnyPerApiUsd")) end_at = datetime.now(timezone.utc) start_at = profit_window_start(window, end_at) _, token, _ = login() @@ -149,14 +149,14 @@ def run_profit_report(): for aggregate in aggregates.values(): rate = profit_account_rate(aggregate["accountName"]) amount = aggregate.pop("apiAmount") - revenue = amount * net_rate if group_class == "profit" else profit_decimal(0) + revenue = amount * sale_rate if group_class == "profit" else profit_decimal(0) upstream_cost = amount * rate if rate is not None and group_class in ("profit", "self-use") else profit_decimal(0) aggregate["tokenCount"] = aggregate.pop("inputTokens") + aggregate.pop("outputTokens") aggregate["apiAmountUsd"] = profit_decimal_text(amount) - aggregate["costCoefficient"] = profit_decimal_text(rate) if rate is not None else None - aggregate["revenueUsd"] = profit_decimal_text(revenue) - aggregate["upstreamCostUsd"] = profit_decimal_text(upstream_cost) if rate is not None or group_class not in ("profit", "self-use") else None - aggregate["profitContributionUsd"] = profit_decimal_text(revenue - upstream_cost) if rate is not None or group_class not in ("profit", "self-use") else None + aggregate["costRateCnyPerApiUsd"] = profit_decimal_text(rate) if rate is not None else None + aggregate["revenueCny"] = profit_decimal_text(revenue) + aggregate["upstreamCostCny"] = profit_decimal_text(upstream_cost) if rate is not None or group_class not in ("profit", "self-use") else None + aggregate["profitContributionCny"] = profit_decimal_text(revenue - upstream_cost) if rate is not None or group_class not in ("profit", "self-use") else None aggregate["completeness"] = "complete" if rate is not None or group_class not in ("profit", "self-use") else "partial" account_rows.append(aggregate) group_amount += amount @@ -175,9 +175,9 @@ def run_profit_report(): missing_profit = [row for row in profit_accounts if row["completeness"] != "complete"] missing_self = [row for row in self_accounts if row["completeness"] != "complete"] unclassified = [row for row in account_rows if row["class"] == "unclassified" and row["requestCount"] > 0] - revenue = sum((profit_decimal(row["revenueUsd"]) for row in profit_accounts), profit_decimal(0)) - profit_cost = sum((profit_decimal(row["upstreamCostUsd"]) for row in profit_accounts if row["upstreamCostUsd"] is not None), profit_decimal(0)) - self_cost = sum((profit_decimal(row["upstreamCostUsd"]) for row in self_accounts if row["upstreamCostUsd"] is not None), profit_decimal(0)) + revenue = sum((profit_decimal(row["revenueCny"]) for row in profit_accounts), profit_decimal(0)) + profit_cost = sum((profit_decimal(row["upstreamCostCny"]) for row in profit_accounts if row["upstreamCostCny"] is not None), profit_decimal(0)) + self_cost = sum((profit_decimal(row["upstreamCostCny"]) for row in self_accounts if row["upstreamCostCny"] is not None), profit_decimal(0)) without_self = revenue - profit_cost with_self = without_self - self_cost without_complete = not missing_profit and not unclassified @@ -193,11 +193,11 @@ def run_profit_report(): "groups": group_rows, "accounts": account_rows, "summary": { - "revenueUsd": profit_decimal_text(revenue), - "profitPoolCostUsd": profit_decimal_text(profit_cost), - "selfUseCostUsd": profit_decimal_text(self_cost), - "withoutSelfUse": {"status": "complete" if without_complete else "partial", "profitUsd": profit_decimal_text(without_self) if without_complete else None}, - "withSelfUse": {"status": "complete" if with_complete else "partial", "profitUsd": profit_decimal_text(with_self) if with_complete else None}, + "revenueCny": profit_decimal_text(revenue), + "profitPoolCostCny": profit_decimal_text(profit_cost), + "selfUseCostCny": profit_decimal_text(self_cost), + "withoutSelfUse": {"status": "complete" if without_complete else "partial", "profitCny": profit_decimal_text(without_self) if without_complete else None}, + "withSelfUse": {"status": "complete" if with_complete else "partial", "profitCny": profit_decimal_text(with_self) if with_complete else None}, }, "completeness": { "missingCostAccounts": [{"groupName": row["groupName"], "accountId": row["accountId"], "accountName": row["accountName"]} for row in missing_profit + missing_self], diff --git a/scripts/src/platform-infra-sub2api-codex/types.ts b/scripts/src/platform-infra-sub2api-codex/types.ts index f027e3c4..08d407ae 100644 --- a/scripts/src/platform-infra-sub2api-codex/types.ts +++ b/scripts/src/platform-infra-sub2api-codex/types.ts @@ -184,8 +184,8 @@ export interface CodexRuntimeConfig { export interface CodexPoolProfitConfig { defaultWindow: string; usageCostField: "actual_cost"; - accountCostSource: "account-name-trailing-coefficient"; - netRevenueCoefficient: number; + accountCostSource: "account-name-trailing-cny-per-api-usd"; + saleRateCnyPerApiUsd: number; groups: { profitable: string[]; selfUse: string[];