From d191852a2f6c87c4f200ef22ad5db2f61eaecb03 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 15 Jul 2026 04:21:19 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E9=80=80=E5=87=BA=E5=88=A9=E6=B6=A6=E6=83=85=E6=99=AF=E6=A0=B8?= =?UTF-8?q?=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/unidesk-sub2api/SKILL.md | 3 +- .../unidesk-sub2api/references/codex-pool.md | 7 + .../profit-scenario-render.ts | 69 +++++++++ .../platform-infra-sub2api-codex/profit.ts | 46 +++++- .../remote-python-profit.ts | 131 ++++++++++++++++++ .../src/platform-infra-sub2api-codex/types.ts | 2 +- scripts/src/platform-infra/entry.ts | 4 +- 7 files changed, 254 insertions(+), 8 deletions(-) create mode 100644 scripts/src/platform-infra-sub2api-codex/profit-scenario-render.ts diff --git a/.agents/skills/unidesk-sub2api/SKILL.md b/.agents/skills/unidesk-sub2api/SKILL.md index d253af77..2647c09f 100644 --- a/.agents/skills/unidesk-sub2api/SKILL.md +++ b/.agents/skills/unidesk-sub2api/SKILL.md @@ -3,7 +3,8 @@ name: unidesk-sub2api description: >- UniDesk Sub2API 平台运维技能。用户提到 Sub2API、platform-infra sub2api、Codex pool、统一 API key、 runtime CRUD、精准批量账号操作、临时不可调度、上游错误率、客户可见错误、换号/failover、 - 模型映射、可用模型探测、FRP 暴露、管理 UI、配置 master ~/.codex、上游账号增删或校验 /v1/models 时使用。 + 模型映射、可用模型探测、利润核算、售价与账号退役情景、FRP 暴露、管理 UI、配置 master ~/.codex、 + 上游账号增删或校验 /v1/models 时使用。 --- # UniDesk Sub2API diff --git a/.agents/skills/unidesk-sub2api/references/codex-pool.md b/.agents/skills/unidesk-sub2api/references/codex-pool.md index 5074acee..c9398c0c 100644 --- a/.agents/skills/unidesk-sub2api/references/codex-pool.md +++ b/.agents/skills/unidesk-sub2api/references/codex-pool.md @@ -109,6 +109,7 @@ bun scripts/cli.ts platform-infra sub2api codex-pool profit --target PK01 --sinc bun scripts/cli.ts platform-infra sub2api codex-pool profit --target PK01 --since 24h --missing-costs bun scripts/cli.ts platform-infra sub2api codex-pool profit --target PK01 --month 2026-07 bun scripts/cli.ts platform-infra sub2api codex-pool profit --target PK01 --month 2026-07 --forecast +bun scripts/cli.ts platform-infra sub2api codex-pool profit --target PK01 --month --forecast --sale-rate --scenario-without-account '' bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --target PK01 --active-since 24h --amount-usd 20 --reason bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status --target D601 @@ -132,6 +133,12 @@ bun scripts/cli.ts platform-infra sub2api codex-pool cleanup-probes --target D60 - `periodCosts` 声明按 `YYYY-MM` 入账的一次性人民币固定成本;`--month YYYY-MM` 使用原生 `/api/v1/admin/usage/stats` 按分组和账号聚合该月流水并一次扣除匹配期间成本,任意 `--since` 窗口继续读取逐条 usage 以保持滚动窗口精度且不分摊固定成本; - 账号调度经济性使用边际采购倍率,不把已经发生的一次性期间成本摊回请求成本或用于压低账号优先级;月度净利润必须单列期间成本后再计算; - `--month YYYY-MM --forecast` 以月初至当前的实际秒数计算线性 run-rate:收入、盈利池流量成本和自用流量成本按完整月份秒数外推,一次性 `periodCosts` 只扣一次;同时保留 MTD 实际汇总和两种整月预计利润,不能把预测值表述为已实现收益; + - 候选售价或账号退役评估必须在同一次利润查询中使用情景参数: + - `--sale-rate ` 只覆盖本次只读核算售价,不修改 YAML; + - `--scenario-without-account ` 可重复指定多个精确账号;CLI 自动移除绑定这些账号的期间成本,并把其盈利池和自用池流量分别按各自剩余账号的实际加权采购倍率重分配; + - 情景汇总直接披露理论利润上限、重分配后的两种利润、日均营业额与成本、账号流量占比、替代成本和剩余账号负载增幅;不要再使用 `--accounts` 分页后手工拼算; + - 账号 selector 不存在时必须失败;剩余账号存在未知成本、没有可用计费基数或仍有未分类流量时必须标记 `partial`,禁止输出伪精确利润; + - 负载增幅只表示容量需求,不等价于剩余账号能够稳定承接;退役建议还需结合账号质量、并发和客户可见错误评估; - 默认 Kubernetes 风格输出优先给出总收入、已确认成本、扣除已知成本后的待定利润基数、未知成本对应的 API USD 基数、完整性和参数化利润公式; - 显式 `--json` 返回同一份稳定紧凑汇总,不携带完整账号数组,避免大窗口触发全局输出保护; - `--accounts` 固定返回一页完整账号名称、账号 ID、请求数、Token、标准 API 美元计费基数、人民币采购单价、人民币收入、人民币上游成本和人民币利润贡献;存在后续页时返回由分类、分组、账号 ID 和名称生成的 opaque `--page-token` 命令,不提供手工 `limit`; diff --git a/scripts/src/platform-infra-sub2api-codex/profit-scenario-render.ts b/scripts/src/platform-infra-sub2api-codex/profit-scenario-render.ts new file mode 100644 index 00000000..295d8c88 --- /dev/null +++ b/scripts/src/platform-infra-sub2api-codex/profit-scenario-render.ts @@ -0,0 +1,69 @@ +type JsonRecord = Record; + +export function renderProfitScenarioLines(scenario: JsonRecord): string[] { + if (Object.keys(scenario).length === 0) return []; + const theoretical = record(scenario.theoretical); + const redistributed = record(scenario.redistributed); + const costs = record(redistributed.costsCny); + const profit = record(redistributed.profitCny); + const daily = record(scenario.dailyAverage); + const dailyCosts = record(daily.costsCny); + const dailyProfit = record(daily.profitCny); + const lines = [ + "", + `SCENARIO status=${text(scenario.status)} method=${text(scenario.method)} sale-rate=${text(scenario.saleRateCnyPerApiUsd)}CNY/API_USD scale=${text(scenario.scale)} removed-period-cost=${cny(scenario.removedPeriodCostCny)}`, + table( + ["SCOPE", "REVENUE_CNY", "TRAFFIC_COST_CNY", "SELF_USE_COST_CNY", "PERIOD_COST_CNY", "THEORETICAL_CEILING_CNY", "REDISTRIBUTED_PROFIT_CNY", "DAILY_PROFIT_CNY"], + [ + ["不扣自用", cny(redistributed.revenueCny), cny(costs.profitPool), "-", cny(costs.period), cny(theoretical.withoutSelfUseProfitCny), cny(profit.withoutSelfUse), cny(dailyProfit.withoutSelfUse)], + ["扣除自用", cny(redistributed.revenueCny), cny(costs.profitPool), cny(costs.selfUse), cny(costs.period), cny(theoretical.withSelfUseProfitCny), cny(profit.withSelfUse), cny(dailyProfit.withSelfUse)], + ], + ), + `DAILY days=${text(daily.days)} revenue=${cny(daily.revenueCny)} traffic-cost=${cny(dailyCosts.profitPool)} self-use-cost=${cny(dailyCosts.selfUse)} period-cost=${cny(dailyCosts.period)}`, + "SCENARIO ACCOUNTS", + ]; + const selected = records(scenario.selectedAccounts); + if (selected.length === 0) lines.push("- none; candidate sale-rate only"); + for (const row of selected) { + lines.push(`- account=${text(row.accountName)} id=${text(row.accountId)} classes=${arrayText(row.classes)}`); + } + lines.push("REDISTRIBUTION"); + for (const row of records(scenario.redistribution)) { + lines.push(`- class=${text(row.class)} selected-share=${percent(row.selectedSharePct)} remaining-rate=${text(row.remainingWeightedCostRateCnyPerApiUsd)} replacement-cost=${cny(row.replacementCostCny)} remaining-load-increase=${percent(row.remainingLoadIncreasePct)}`); + } + const warnings = Array.isArray(scenario.warnings) ? scenario.warnings : []; + for (const warning of warnings) lines.push(`WARNING ${text(warning)}`); + return lines; +} + +function record(value: unknown): JsonRecord { + return typeof value === "object" && value !== null && !Array.isArray(value) ? value as JsonRecord : {}; +} + +function records(value: unknown): JsonRecord[] { + return Array.isArray(value) ? value.filter((item): item is JsonRecord => typeof item === "object" && item !== null && !Array.isArray(item)) : []; +} + +function text(value: unknown): string { + if (value === null || value === undefined || value === "") return "-"; + return String(value).replace(/[\r\n\t]+/gu, " "); +} + +function cny(value: unknown): string { + const numeric = Number(value); + return Number.isFinite(numeric) ? `¥${numeric.toFixed(6)}` : text(value); +} + +function percent(value: unknown): string { + const numeric = Number(value); + return Number.isFinite(numeric) ? `${numeric.toFixed(2)}%` : text(value); +} + +function arrayText(value: unknown): string { + return Array.isArray(value) ? value.map(text).join(",") : text(value); +} + +function table(headers: string[], rows: string[][]): string { + const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length))); + return [headers, ...rows].map((row) => row.map((cell, index) => cell.padEnd(widths[index]!)).join(" ").trimEnd()).join("\n"); +} diff --git a/scripts/src/platform-infra-sub2api-codex/profit.ts b/scripts/src/platform-infra-sub2api-codex/profit.ts index 28ccbd8b..6310488b 100644 --- a/scripts/src/platform-infra-sub2api-codex/profit.ts +++ b/scripts/src/platform-infra-sub2api-codex/profit.ts @@ -2,6 +2,7 @@ import type { UniDeskConfig } from "../config"; import type { RenderedCliResult } from "../output"; import { readCodexPoolConfig } from "./config"; +import { renderProfitScenarioLines } from "./profit-scenario-render"; import { boolField, compactCapture, parseJsonOutput, runRemoteCodexPoolScript } from "./remote"; import { profitScript } from "./remote-scripts"; import { codexPoolRuntimeTarget, defaultCodexPoolRuntimeTargetId } from "./runtime-target"; @@ -15,6 +16,8 @@ interface ProfitOptions { accounts: boolean; missingCosts: boolean; pageToken: string | null; + saleRateCnyPerApiUsd: string | null; + scenarioWithoutAccounts: string[]; } const PROFIT_ACCOUNT_PAGE_SIZE = 8; @@ -24,13 +27,23 @@ export async function codexPoolProfit(config: UniDeskConfig, args: string[]): Pr if (args.includes("--help")) return renderProfitHelp(pool.profit.defaultWindow); const options = parseProfitOptions(args, pool.profit.defaultWindow); const target = codexPoolRuntimeTarget(options.targetId); - const payload = { since: options.since, month: options.month, forecast: options.forecast, config: pool.profit }; + const scenario = options.saleRateCnyPerApiUsd !== null || options.scenarioWithoutAccounts.length > 0 + ? { + saleRateCnyPerApiUsd: options.saleRateCnyPerApiUsd ?? String(pool.profit.saleRateCnyPerApiUsd), + withoutAccounts: options.scenarioWithoutAccounts, + } + : null; + const payload = { since: options.since, month: options.month, forecast: options.forecast, config: pool.profit, scenario }; const remote = await runRemoteCodexPoolScript(config, "profit", profitScript(payload, pool, target), target); const report = parseJsonOutput(remote.stdout); const ok = remote.exitCode === 0 && boolField(report, "ok", false); const projectedReport = projectProfitReport(report, options); const windowOption = options.month === null ? `--since ${options.since}` : `--month ${options.month}${options.forecast ? " --forecast" : ""}`; - const baseCommand = `bun scripts/cli.ts platform-infra sub2api codex-pool profit --target ${target.id} ${windowOption}`; + const scenarioOption = [ + options.saleRateCnyPerApiUsd === null ? null : `--sale-rate ${options.saleRateCnyPerApiUsd}`, + ...options.scenarioWithoutAccounts.map((selector) => `--scenario-without-account ${shellArg(selector)}`), + ].filter((item): item is string => item !== null).join(" "); + const baseCommand = `bun scripts/cli.ts platform-infra sub2api codex-pool profit --target ${target.id} ${windowOption}${scenarioOption ? ` ${scenarioOption}` : ""}`; const accountPage = record(record(projectedReport).accountPage); const response = { ok, @@ -66,6 +79,8 @@ function parseProfitOptions(args: string[], defaultWindow: string): ProfitOption let accounts = false; let missingCosts = false; let pageToken: string | null = null; + let saleRateCnyPerApiUsd: string | null = null; + const scenarioWithoutAccounts: string[] = []; const readValue = (index: number, option: string): [string, number] => { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error(`${option} requires a value`); @@ -77,6 +92,14 @@ function parseProfitOptions(args: string[], defaultWindow: string): ProfitOption else if (arg === "--forecast") forecast = true; else if (arg === "--accounts") accounts = true; else if (arg === "--missing-costs") missingCosts = true; + else if (arg === "--sale-rate") { + [saleRateCnyPerApiUsd, index] = readValue(index, "--sale-rate"); + } else if (arg.startsWith("--sale-rate=")) saleRateCnyPerApiUsd = arg.slice("--sale-rate=".length).trim(); + else if (arg === "--scenario-without-account") { + let selector: string; + [selector, index] = readValue(index, "--scenario-without-account"); + scenarioWithoutAccounts.push(selector); + } else if (arg.startsWith("--scenario-without-account=")) scenarioWithoutAccounts.push(arg.slice("--scenario-without-account=".length).trim()); else if (arg === "--month") { [month, index] = readValue(index, "--month"); } else if (arg.startsWith("--month=")) month = arg.slice("--month=".length).trim(); @@ -99,7 +122,14 @@ function parseProfitOptions(args: string[], defaultWindow: string): ProfitOption if (accounts && missingCosts) throw new Error("--accounts and --missing-costs are separate semantic disclosures"); if (pageToken !== null && !accounts) throw new Error("--page-token requires --accounts"); if (pageToken !== null && (!pageToken || /[\r\n\0\s]/u.test(pageToken))) throw new Error("--page-token has an unsupported format"); - return { since, month, forecast, targetId, json, accounts, missingCosts, pageToken }; + if (saleRateCnyPerApiUsd !== null && !/^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/u.test(saleRateCnyPerApiUsd)) { + throw new Error("--sale-rate must be a non-negative decimal CNY per API USD value"); + } + if (scenarioWithoutAccounts.some((selector) => !selector || /[\r\n\0]/u.test(selector))) { + throw new Error("--scenario-without-account requires an exact account ID or name"); + } + const uniqueScenarioAccounts = [...new Set(scenarioWithoutAccounts)]; + return { since, month, forecast, targetId, json, accounts, missingCosts, pageToken, saleRateCnyPerApiUsd, scenarioWithoutAccounts: uniqueScenarioAccounts }; } function renderProfitHelp(defaultWindow: string): RenderedCliResult { @@ -109,18 +139,21 @@ function renderProfitHelp(defaultWindow: string): RenderedCliResult { renderedText: [ "SUB2API CODEX-POOL PROFIT", "Usage:", - " bun scripts/cli.ts platform-infra sub2api codex-pool profit [--since 24h|--month YYYY-MM [--forecast]] [--target PK01] [--accounts [--page-token ]|--missing-costs] [--json]", + " bun scripts/cli.ts platform-infra sub2api codex-pool profit [--since 24h|--month YYYY-MM [--forecast]] [--target PK01] [--sale-rate ] [--scenario-without-account ...] [--accounts [--page-token ]|--missing-costs] [--json]", "Options:", ` --since Native usage window; YAML default is ${defaultWindow}.`, " --month Calendar-month usage plus YAML-declared one-time period costs.", " --forecast Extrapolate month-to-date variable revenue and costs to month end; period costs remain one-time.", " --target ", + " --sale-rate Candidate sale price in CNY per API USD; does not change YAML.", + " --scenario-without-account Read-only retirement scenario; repeat for multiple exact account IDs or names.", " --accounts Include one fixed-size account page with complete names and IDs.", " --page-token Continue from the opaque token returned by the previous account page.", " --missing-costs Include the missing-cost account list and API USD bases.", " --json Emit exact structured values.", "Notes:", " Default output is a compact summary; use --accounts or --missing-costs for semantic disclosure.", + " Retirement scenarios redistribute selected traffic at each class's remaining-account weighted cost rate and report load growth.", " This command is read-only and does not change Sub2API runtime state.", ].join("\n"), contentType: "text/plain", @@ -165,6 +198,7 @@ function renderProfit(response: Record): RenderedCliResult { ], )); } + lines.push(...renderProfitScenarioLines(record(report.scenario))); const periodCosts = arrayOfRecords(report.periodCosts); if (periodCosts.length > 0) { lines.push("", "PERIOD COSTS"); @@ -292,6 +326,10 @@ function text(value: unknown): string { return String(value).replace(/[\r\n\t]+/gu, " "); } +function shellArg(value: string): string { + return `'${value.replace(/'/gu, `'"'"'`)}'`; +} + function compactTokens(value: unknown): string { const numeric = Number(value); if (!Number.isFinite(numeric)) return text(value); 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 9f0a1587..29a1ce8a 100644 --- a/scripts/src/platform-infra-sub2api-codex/remote-python-profit.ts +++ b/scripts/src/platform-infra-sub2api-codex/remote-python-profit.ts @@ -140,6 +140,135 @@ def profit_empty_account(group, group_class, account_id, account_name): "apiAmount": profit_decimal(0), } +def profit_build_scenario(spec, account_rows, period_costs, start_at, end_at, period_end_at, forecast, unclassified): + if not isinstance(spec, dict): + return None + selectors = [str(item).strip() for item in (spec.get("withoutAccounts") or []) if str(item).strip()] + selected_rows = [row for row in account_rows if str(row.get("accountId")) in selectors or str(row.get("accountName")) in selectors] + matched_selectors = { + selector for selector in selectors + if any(str(row.get("accountId")) == selector or str(row.get("accountName")) == selector for row in selected_rows) + } + unmatched = [selector for selector in selectors if selector not in matched_selectors] + if unmatched: + raise RuntimeError("profit scenario account selector not found: " + ", ".join(unmatched)) + sale_rate = profit_decimal(spec.get("saleRateCnyPerApiUsd")) + if sale_rate < 0: + raise RuntimeError("profit scenario sale rate must be non-negative") + scale = profit_decimal(forecast.get("scale")) if isinstance(forecast, dict) else profit_decimal(1) + period_seconds = profit_decimal(forecast.get("fullPeriodSeconds")) if isinstance(forecast, dict) else profit_decimal((end_at - start_at).total_seconds()) + period_days = period_seconds / profit_decimal(86400) + if period_days <= 0: + raise RuntimeError("profit scenario has no observed period") + selected_names = {str(row.get("accountName")) for row in selected_rows} + retained_period_costs = [item for item in period_costs if str(item.get("accountName")) not in selected_names] + retained_period_cost = sum((profit_decimal(item.get("amountCny")) for item in retained_period_costs), profit_decimal(0)) + removed_period_cost = sum((profit_decimal(item.get("amountCny")) for item in period_costs), profit_decimal(0)) - retained_period_cost + warnings = [] + complete = not unclassified + if unclassified: + warnings.append("unclassified traffic remains outside the scenario") + class_results = {} + for group_class in ("profit", "self-use"): + class_rows = [row for row in account_rows if row.get("class") == group_class] + chosen = [row for row in class_rows if row in selected_rows] + remaining = [row for row in class_rows if row not in selected_rows] + missing_remaining = [row for row in remaining if row.get("completeness") != "complete"] + total_basis = sum((profit_decimal(row.get("apiAmountUsd")) for row in class_rows), profit_decimal(0)) + selected_basis = sum((profit_decimal(row.get("apiAmountUsd")) for row in chosen), profit_decimal(0)) + remaining_basis = sum((profit_decimal(row.get("apiAmountUsd")) for row in remaining), profit_decimal(0)) + remaining_cost = sum((profit_decimal(row.get("upstreamCostCny")) for row in remaining if row.get("upstreamCostCny") is not None), profit_decimal(0)) + weighted_rate = remaining_cost / remaining_basis if remaining_basis > 0 else None + replacement_cost = selected_basis * weighted_rate if weighted_rate is not None else (profit_decimal(0) if selected_basis == 0 else None) + if missing_remaining: + complete = False + warnings.append(f"{group_class} remaining accounts have missing cost rates") + if selected_basis > 0 and remaining_basis <= 0: + complete = False + warnings.append(f"{group_class} has no remaining traffic basis for redistribution") + class_results[group_class] = { + "class": group_class, + "totalBasis": total_basis, + "selectedBasis": selected_basis, + "remainingBasis": remaining_basis, + "remainingCost": remaining_cost, + "weightedRate": weighted_rate, + "replacementCost": replacement_cost, + "selectedSharePct": selected_basis * profit_decimal(100) / total_basis if total_basis > 0 else profit_decimal(0), + "remainingLoadIncreasePct": selected_basis * profit_decimal(100) / remaining_basis if remaining_basis > 0 else None, + "missingRemaining": missing_remaining, + } + profit_class = class_results["profit"] + self_class = class_results["self-use"] + revenue = profit_class["totalBasis"] * sale_rate * scale + theoretical_profit_cost = profit_class["remainingCost"] * scale + theoretical_self_cost = self_class["remainingCost"] * scale + theoretical_without_self = revenue - theoretical_profit_cost - retained_period_cost + theoretical_with_self = theoretical_without_self - theoretical_self_cost + redistributed_profit_cost = theoretical_profit_cost + (profit_class["replacementCost"] * scale if profit_class["replacementCost"] is not None else profit_decimal(0)) + redistributed_self_cost = theoretical_self_cost + (self_class["replacementCost"] * scale if self_class["replacementCost"] is not None else profit_decimal(0)) + redistributed_without_self = revenue - redistributed_profit_cost - retained_period_cost + redistributed_with_self = redistributed_without_self - redistributed_self_cost + selected_accounts = {} + for row in selected_rows: + key = (str(row.get("accountId")), str(row.get("accountName"))) + if key not in selected_accounts: + selected_accounts[key] = {"accountId": row.get("accountId"), "accountName": row.get("accountName"), "classes": []} + if row.get("class") not in selected_accounts[key]["classes"]: + selected_accounts[key]["classes"].append(row.get("class")) + redistribution = [] + for item in (profit_class, self_class): + redistribution.append({ + "class": item["class"], + "selectedApiUsd": profit_decimal_text(item["selectedBasis"] * scale), + "selectedSharePct": profit_decimal_text(item["selectedSharePct"]), + "remainingWeightedCostRateCnyPerApiUsd": profit_decimal_text(item["weightedRate"]) if item["weightedRate"] is not None else None, + "replacementCostCny": profit_decimal_text(item["replacementCost"] * scale) if item["replacementCost"] is not None else None, + "remainingLoadIncreasePct": profit_decimal_text(item["remainingLoadIncreasePct"]) if item["remainingLoadIncreasePct"] is not None else None, + "missingRemainingCostAccounts": [{"accountId": row.get("accountId"), "accountName": row.get("accountName")} for row in item["missingRemaining"]], + }) + status = "complete" if complete else "partial" + return { + "status": status, + "method": "remaining-account-weighted-cost-redistribution", + "scale": profit_decimal_text(scale), + "saleRateCnyPerApiUsd": profit_decimal_text(sale_rate), + "selectedAccounts": list(selected_accounts.values()), + "removedPeriodCostCny": profit_decimal_text(removed_period_cost), + "retainedPeriodCostCny": profit_decimal_text(retained_period_cost), + "theoretical": { + "withoutSelfUseProfitCny": profit_decimal_text(theoretical_without_self) if complete else None, + "withSelfUseProfitCny": profit_decimal_text(theoretical_with_self) if complete else None, + }, + "redistributed": { + "revenueCny": profit_decimal_text(revenue), + "costsCny": { + "profitPool": profit_decimal_text(redistributed_profit_cost) if complete else None, + "selfUse": profit_decimal_text(redistributed_self_cost) if complete else None, + "period": profit_decimal_text(retained_period_cost), + }, + "profitCny": { + "withoutSelfUse": profit_decimal_text(redistributed_without_self) if complete else None, + "withSelfUse": profit_decimal_text(redistributed_with_self) if complete else None, + }, + }, + "dailyAverage": { + "days": profit_decimal_text(period_days), + "revenueCny": profit_decimal_text(revenue / period_days), + "costsCny": { + "profitPool": profit_decimal_text(redistributed_profit_cost / period_days) if complete else None, + "selfUse": profit_decimal_text(redistributed_self_cost / period_days) if complete else None, + "period": profit_decimal_text(retained_period_cost / period_days), + }, + "profitCny": { + "withoutSelfUse": profit_decimal_text(redistributed_without_self / period_days) if complete else None, + "withSelfUse": profit_decimal_text(redistributed_with_self / period_days) if complete else None, + }, + }, + "redistribution": redistribution, + "warnings": warnings, + } + def run_profit_report(): payload = json.loads(base64.b64decode(PAYLOAD_B64).decode("utf-8")) if PAYLOAD_B64 else {} config = payload.get("config") if isinstance(payload.get("config"), dict) else {} @@ -268,6 +397,7 @@ def run_profit_report(): "withSelfUse": {"status": "complete" if with_complete else "partial"}, } account_rows.sort(key=lambda row: (row["class"], row["groupName"], str(row["accountId"]), str(row["accountName"]))) + scenario = profit_build_scenario(payload.get("scenario"), account_rows, period_costs, start_at, end_at, period_end_at, forecast, unclassified) return { "ok": True, "mode": "profit", @@ -278,6 +408,7 @@ def run_profit_report(): "groups": group_rows, "accounts": account_rows, "periodCosts": [{"id": item.get("id"), "period": item.get("period"), "accountName": item.get("accountName"), "amountCny": profit_decimal_text(profit_decimal(item.get("amountCny"))), "description": item.get("description")} for item in period_costs], + "scenario": scenario, "summary": { "revenueCny": profit_decimal_text(revenue), "confirmedCostsCny": { diff --git a/scripts/src/platform-infra-sub2api-codex/types.ts b/scripts/src/platform-infra-sub2api-codex/types.ts index f650c342..cbaa155e 100644 --- a/scripts/src/platform-infra-sub2api-codex/types.ts +++ b/scripts/src/platform-infra-sub2api-codex/types.ts @@ -400,7 +400,7 @@ export function codexPoolHelp(): unknown { "bun scripts/cli.ts platform-infra sub2api codex-pool runtime get --account [--target PK01] [--json|--full|--raw]", "bun scripts/cli.ts platform-infra sub2api codex-pool runtime errors [--group |--all-groups] [--platform ] [--page-token ] [--account ] [--since 24h] [--tail 50000] [--target PK01] [--json|--full|--raw]", "bun scripts/cli.ts platform-infra sub2api codex-pool faults [--level P0|P1|P2] [--group ] [--account ] [--model ] [--stream sync|stream] [--endpoint ] [--request-id ] [--page-token ] [--target PK01] [--json]", - "bun scripts/cli.ts platform-infra sub2api codex-pool profit [--since 24h|--month YYYY-MM [--forecast]] [--target PK01] [--accounts [--page-token ]|--missing-costs] [--json]", + "bun scripts/cli.ts platform-infra sub2api codex-pool profit [--since 24h|--month YYYY-MM [--forecast]] [--target PK01] [--sale-rate ] [--scenario-without-account ...] [--accounts [--page-token ]|--missing-costs] [--json]", "bun scripts/cli.ts platform-infra sub2api codex-pool credits grant (--active-since 24h|--users ) --amount-usd --reason [--target PK01] [--confirm] [--json]", "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]", diff --git a/scripts/src/platform-infra/entry.ts b/scripts/src/platform-infra/entry.ts index e690c7fc..fdaac348 100644 --- a/scripts/src/platform-infra/entry.ts +++ b/scripts/src/platform-infra/entry.ts @@ -424,7 +424,7 @@ export function platformInfraHelp(): unknown { "bun scripts/cli.ts platform-infra sub2api codex-pool validate", "bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id ", "bun scripts/cli.ts platform-infra sub2api codex-pool feedback --user [--window 30m] [--page-token |--request-id ] [--json]", - "bun scripts/cli.ts platform-infra sub2api codex-pool profit [--since 24h] [--target PK01] [--accounts [--page-token ]|--missing-costs] [--json]", + "bun scripts/cli.ts platform-infra sub2api codex-pool profit [--since 24h|--month YYYY-MM [--forecast]] [--target PK01] [--sale-rate ] [--scenario-without-account ...] [--accounts [--page-token ]|--missing-costs] [--json]", "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status", "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account unidesk-codex-hy --confirm", "bun scripts/cli.ts platform-infra sub2rank plan [--target NC01]", @@ -508,7 +508,7 @@ export function platformInfraHelp(): unknown { "bun scripts/cli.ts platform-infra sub2api codex-pool validate", "bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id ", "bun scripts/cli.ts platform-infra sub2api codex-pool feedback --user [--window 30m] [--page-token |--request-id ] [--json]", - "bun scripts/cli.ts platform-infra sub2api codex-pool profit [--since 24h] [--target PK01] [--accounts [--page-token ]|--missing-costs] [--json]", + "bun scripts/cli.ts platform-infra sub2api codex-pool profit [--since 24h|--month YYYY-MM [--forecast]] [--target PK01] [--sale-rate ] [--scenario-without-account ...] [--accounts [--page-token ]|--missing-costs] [--json]", "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status", ], module: "scripts/src/platform-infra-sub2api-codex.ts",