277 lines
14 KiB
TypeScript
277 lines
14 KiB
TypeScript
import type { UniDeskConfig } from "../config";
|
|
import type { RenderedCliResult } from "../output";
|
|
|
|
import { readCodexPoolConfig } from "./config";
|
|
import { boolField, compactCapture, parseJsonOutput, runRemoteCodexPoolScript } from "./remote";
|
|
import { profitScript } from "./remote-scripts";
|
|
import { codexPoolRuntimeTarget, defaultCodexPoolRuntimeTargetId } from "./runtime-target";
|
|
|
|
interface ProfitOptions {
|
|
since: string;
|
|
targetId: string;
|
|
json: boolean;
|
|
accounts: boolean;
|
|
missingCosts: boolean;
|
|
pageToken: string | null;
|
|
}
|
|
|
|
const PROFIT_ACCOUNT_PAGE_SIZE = 8;
|
|
|
|
export async function codexPoolProfit(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
|
|
const pool = readCodexPoolConfig();
|
|
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, config: pool.profit };
|
|
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 baseCommand = `bun scripts/cli.ts platform-infra sub2api codex-pool profit --target ${target.id} --since ${options.since}`;
|
|
const accountPage = record(record(projectedReport).accountPage);
|
|
const response = {
|
|
ok,
|
|
action: "platform-infra-sub2api-codex-pool-profit",
|
|
target: { id: target.id, route: target.route, runtimeMode: target.runtimeMode, endpoint: target.serviceDns },
|
|
source: {
|
|
kind: "sub2api-native-admin-usage",
|
|
mutation: false,
|
|
configPath: "config/platform-infra/sub2api-codex-pool.yaml#profit",
|
|
},
|
|
report: projectedReport,
|
|
disclosure: {
|
|
accountsIncluded: options.accounts,
|
|
missingCostsIncluded: options.missingCosts,
|
|
accountCount: arrayOfRecords(record(report).accounts).length,
|
|
accounts: `${baseCommand} --accounts`,
|
|
accountsNext: accountPage.nextPageToken ? `${baseCommand} --accounts --page-token ${text(accountPage.nextPageToken)}` : null,
|
|
missingCosts: `${baseCommand} --missing-costs`,
|
|
json: `${baseCommand} --json`,
|
|
},
|
|
remote: compactCapture(remote, { full: !ok || report === null }),
|
|
valuesPrinted: false,
|
|
};
|
|
return options.json ? renderProfitJson(response) : renderProfit(response);
|
|
}
|
|
|
|
function parseProfitOptions(args: string[], defaultWindow: string): ProfitOptions {
|
|
let since = defaultWindow;
|
|
let targetId = defaultCodexPoolRuntimeTargetId();
|
|
let json = false;
|
|
let accounts = false;
|
|
let missingCosts = false;
|
|
let pageToken: string | null = null;
|
|
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`);
|
|
return [value.trim(), index + 1];
|
|
};
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
const arg = args[index]!;
|
|
if (arg === "--json") json = true;
|
|
else if (arg === "--accounts") accounts = true;
|
|
else if (arg === "--missing-costs") missingCosts = true;
|
|
else if (arg === "--page-token") {
|
|
[pageToken, index] = readValue(index, "--page-token");
|
|
} else if (arg.startsWith("--page-token=")) pageToken = arg.slice("--page-token=".length).trim();
|
|
else if (arg === "--since") {
|
|
[since, index] = readValue(index, "--since");
|
|
} else if (arg.startsWith("--since=")) since = arg.slice("--since=".length).trim();
|
|
else if (arg === "--target") {
|
|
[targetId, index] = readValue(index, "--target");
|
|
} else if (arg.startsWith("--target=")) targetId = arg.slice("--target=".length).trim();
|
|
else throw new Error(`unsupported profit option: ${arg}`);
|
|
}
|
|
if (!/^[1-9][0-9]*(?:s|m|h|d)$/u.test(since)) throw new Error("--since must be a duration such as 24h, 90m, or 7d");
|
|
if (!targetId || /[\r\n\0]/u.test(targetId)) throw new Error("--target has an unsupported format");
|
|
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, targetId, json, accounts, missingCosts, pageToken };
|
|
}
|
|
|
|
function renderProfitHelp(defaultWindow: string): RenderedCliResult {
|
|
return {
|
|
ok: true,
|
|
command: "platform-infra sub2api codex-pool profit --help",
|
|
renderedText: [
|
|
"SUB2API CODEX-POOL PROFIT",
|
|
"Usage:",
|
|
" bun scripts/cli.ts platform-infra sub2api codex-pool profit [--since 24h] [--target PK01] [--accounts [--page-token <account-id>]|--missing-costs] [--json]",
|
|
"Options:",
|
|
` --since <duration> Native usage window; YAML default is ${defaultWindow}.`,
|
|
" --target <id>",
|
|
" --accounts Include one fixed-size account page with complete names and IDs.",
|
|
" --page-token <id> Continue after the account ID returned by the previous 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.",
|
|
" This command is read-only and does not change Sub2API runtime state.",
|
|
].join("\n"),
|
|
contentType: "text/plain",
|
|
};
|
|
}
|
|
|
|
function renderProfit(response: Record<string, unknown>): RenderedCliResult {
|
|
const report = record(response.report);
|
|
const window = record(report.window);
|
|
const policy = record(report.policy);
|
|
const summary = record(report.summary);
|
|
const completeness = record(report.completeness);
|
|
const confirmedCosts = record(summary.confirmedCostsCny);
|
|
const confirmedProfit = record(summary.confirmedProfitCny);
|
|
const unknownBasis = record(summary.unknownCostBasisApiUsd);
|
|
const formulas = record(summary.profitFormula);
|
|
const disclosure = record(response.disclosure);
|
|
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 sale-rate=${text(policy.saleRateCnyPerApiUsd)}CNY/API_USD account-cost=${text(policy.accountCostSource)}`,
|
|
"",
|
|
"SUMMARY",
|
|
];
|
|
lines.push(table(
|
|
["SCOPE", "STATUS", "REVENUE_CNY", "PROFIT_POOL_COST_CNY", "SELF_USE_COST_CNY", "CONFIRMED_PROFIT_CNY", "UNKNOWN_API_USD"],
|
|
[
|
|
["不扣自用", text(record(summary.withoutSelfUse).status), cny(summary.revenueCny), cny(confirmedCosts.profitPool), "-", cny(confirmedProfit.withoutSelfUse), apiUsd(unknownBasis.profitPool)],
|
|
["扣除自用", text(record(summary.withSelfUse).status), cny(summary.revenueCny), cny(confirmedCosts.profitPool), cny(confirmedCosts.selfUse), cny(confirmedProfit.withSelfUse), apiUsd(unknownBasis.total)],
|
|
],
|
|
));
|
|
lines.push("", "FORMULA");
|
|
lines.push(`- 不扣自用: ${text(formulas.withoutSelfUse)}`);
|
|
lines.push(`- 扣除自用: ${text(formulas.withSelfUse)}`);
|
|
lines.push("", `COMPLETENESS status=${text(report.status)} missing-cost=${text(completeness.missingCostAccountCount)} unclassified=${text(completeness.unclassifiedTrafficCount)} missing-groups=${text(completeness.configuredGroupMissingCount)}`);
|
|
lines.push(`NEXT accounts: ${text(disclosure.accounts)}`);
|
|
lines.push(`NEXT missing-costs: ${text(disclosure.missingCosts)}`);
|
|
const accounts = arrayOfRecords(report.accounts);
|
|
if (accounts.length > 0) {
|
|
lines.push("", "ACCOUNTS");
|
|
lines.push(table(
|
|
["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),
|
|
apiUsd(row.apiAmountUsd), text(row.costRateCnyPerApiUsd), cny(row.revenueCny), cny(row.upstreamCostCny), cny(row.profitContributionCny), text(row.completeness),
|
|
]),
|
|
));
|
|
const accountPage = record(report.accountPage);
|
|
if (accountPage.nextPageToken) lines.push(`NEXT account-page: ${text(disclosure.accountsNext)}`);
|
|
}
|
|
const missingCosts = arrayOfRecords(completeness.missingCostAccounts);
|
|
if (missingCosts.length > 0) {
|
|
lines.push("", "MISSING COST ACCOUNTS");
|
|
for (const row of missingCosts) lines.push(`- class=${text(row.class)} group=${text(row.groupName)} account=${text(row.accountName)} id=${text(row.accountId)} api-usd=${text(row.apiAmountUsd)}`);
|
|
}
|
|
if (accounts.length > 0 || missingCosts.length > 0) {
|
|
const unclassified = arrayOfRecords(completeness.unclassifiedTraffic);
|
|
const missingGroups = Array.isArray(completeness.configuredGroupsMissingAtRuntime) ? completeness.configuredGroupsMissingAtRuntime : [];
|
|
if (unclassified.length || missingGroups.length) {
|
|
lines.push("", "COMPLETENESS DETAILS");
|
|
for (const row of unclassified) lines.push(`- unclassified-traffic group=${text(row.groupName)} account=${text(row.accountName)} requests=${text(row.requestCount)}`);
|
|
for (const group of missingGroups) lines.push(`- configured-group-missing ${text(group)}`);
|
|
}
|
|
}
|
|
return {
|
|
ok: response.ok === true,
|
|
command: "platform-infra sub2api codex-pool profit",
|
|
renderedText: lines.join("\n"),
|
|
contentType: "text/plain",
|
|
projection: response,
|
|
};
|
|
}
|
|
|
|
function projectProfitReport(report: Record<string, unknown> | null, options: ProfitOptions): Record<string, unknown> | null {
|
|
if (report === null) return null;
|
|
const { accounts: rawAccounts, completeness: rawCompleteness, ...summaryReport } = report;
|
|
const accounts = arrayOfRecords(rawAccounts);
|
|
const completeness = record(rawCompleteness);
|
|
const compactCompleteness = {
|
|
missingCostAccountCount: arrayOfRecords(completeness.missingCostAccounts).length,
|
|
unclassifiedTrafficCount: arrayOfRecords(completeness.unclassifiedTraffic).length,
|
|
configuredGroupMissingCount: Array.isArray(completeness.configuredGroupsMissingAtRuntime) ? completeness.configuredGroupsMissingAtRuntime.length : 0,
|
|
};
|
|
const compactReport = {
|
|
...summaryReport,
|
|
accountCount: accounts.length,
|
|
completeness: compactCompleteness,
|
|
};
|
|
if (options.missingCosts) {
|
|
return {
|
|
...compactReport,
|
|
completeness: {
|
|
...compactCompleteness,
|
|
missingCostAccounts: arrayOfRecords(completeness.missingCostAccounts),
|
|
},
|
|
};
|
|
}
|
|
if (!options.accounts) return compactReport;
|
|
let startIndex = 0;
|
|
if (options.pageToken !== null) {
|
|
const tokenIndex = accounts.findIndex((row) => text(row.accountId) === options.pageToken);
|
|
if (tokenIndex < 0) throw new Error(`profit account page token not found: ${options.pageToken}`);
|
|
startIndex = tokenIndex + 1;
|
|
}
|
|
const pageAccounts = accounts.slice(startIndex, startIndex + PROFIT_ACCOUNT_PAGE_SIZE);
|
|
const hasNext = startIndex + pageAccounts.length < accounts.length;
|
|
const nextPageToken = hasNext ? text(pageAccounts.at(-1)?.accountId) : null;
|
|
return {
|
|
...compactReport,
|
|
accounts: pageAccounts,
|
|
accountPage: {
|
|
pageSize: PROFIT_ACCOUNT_PAGE_SIZE,
|
|
returned: pageAccounts.length,
|
|
nextPageToken,
|
|
},
|
|
};
|
|
}
|
|
|
|
function renderProfitJson(response: Record<string, unknown>): RenderedCliResult {
|
|
return {
|
|
ok: response.ok === true,
|
|
command: "platform-infra sub2api codex-pool profit --json",
|
|
renderedText: JSON.stringify(response),
|
|
contentType: "application/json",
|
|
projection: response,
|
|
};
|
|
}
|
|
|
|
function record(value: unknown): Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
}
|
|
|
|
function arrayOfRecords(value: unknown): Record<string, unknown>[] {
|
|
return Array.isArray(value) ? value.filter((item): item is Record<string, unknown> => 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 compactTokens(value: unknown): string {
|
|
const numeric = Number(value);
|
|
if (!Number.isFinite(numeric)) return text(value);
|
|
if (Math.abs(numeric) >= 1_000_000_000) return `${(numeric / 1_000_000_000).toFixed(3)}B`;
|
|
if (Math.abs(numeric) >= 1_000_000) return `${(numeric / 1_000_000).toFixed(3)}M`;
|
|
if (Math.abs(numeric) >= 1_000) return `${(numeric / 1_000).toFixed(3)}K`;
|
|
return String(numeric);
|
|
}
|
|
|
|
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)));
|
|
return [headers, ...rows].map((row) => row.map((cell, index) => cell.padEnd(widths[index]!)).join(" ").trimEnd()).join("\n");
|
|
}
|