feat: 构建系统重构 + VCHSM 架构迁移 + 右键菜单修复 + 尺寸参数统一

## 构建、打包与分发系统重构 (build/)

- 构建系统收敛至 build/ 目录:build.mjs + electron-builder.js + 6 个脚本

- electron-builder 配置从 package.json 提取为 build/electron-builder.js

- 新增 --edition (personal/enterprise) 和 --channel (stable/beta/alpha) 参数

- OSS 路径按渠道隔离

- 新增 pre-build-check.mjs:Git/CHANGELOG/版本/环境变量校验

- 新增 bump-version.mjs:版本号管理 + CHANGELOG 自动生成

- updater.ts 发送 channel/edition/deviceFingerprint

- 新增 src/shared/utils/rollout.ts 灰度测试工具

- 新增 10 条 NPM 脚本

## VCHSM 五层架构迁移

- 7 个业务模块 + ~500 处导入路径同步

## 修复

- MediaCardContextMenu 改用共享 ContextMenu 组件

- ComboBox 尺寸参数显示统一 (size-format.ts)

## 文档

- UpdateA.md / build/README.md / README.md / .env.example 更新

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-29 18:41:07 +08:00
parent e0d91335c7
commit 337d6c1205
187 changed files with 5252 additions and 1702 deletions

488
src/shared/utils/pricing.ts Normal file
View File

@@ -0,0 +1,488 @@
export interface ModelPricingFloorTableConfig {
field: string | null;
mapping: Record<string, unknown>;
}
export interface ModelPricingAddonConfig {
field_key: string | null;
operator: string;
condition_value: unknown;
condition_values: unknown[];
price: number | null;
unit_field: string | null;
}
export interface ModelPricingMatrixRowConfig {
price: number | null;
price_cent: number | null;
rate: number | null;
price_per_unit: number | null;
unit_price: number | null;
raw: Record<string, unknown>;
}
export interface ModelPricingConfig {
mode: string | null;
price_type: string | null;
billing_mode: string | null;
price: number | null;
price_per_k: number | null;
price_cent: number | null;
price_per_k_cent: number | null;
field: string | null;
unit_field: string | null;
unit_scale: number | null;
round_digits: number | null;
dimensions: string[];
rate_dimensions: string[];
matrix: ModelPricingMatrixRowConfig[];
conditional_addons: ModelPricingAddonConfig[];
floor_table: ModelPricingFloorTableConfig | null;
submit_warning_threshold: number | null;
raw: Record<string, unknown>;
}
export interface ModelDefinition {
id: string;
name: string;
pricing_config: ModelPricingConfig | null;
}
// ============================================================
// 工具函数
// ============================================================
/** 对应 Python: _as_float(value) -> Optional[float] */
function asFloat(value: unknown): number | null {
if (value === null || value === undefined) return null;
if (typeof value === "number" && !Number.isNaN(value)) return value;
if (typeof value === "string") {
const text = value.trim();
if (!text) return null;
const parsed = Number(text);
return Number.isNaN(parsed) ? null : parsed;
}
return null;
}
/** 对应 Python: _to_int(value, *, default=0) -> int */
function toInt(value: unknown, defaultValue: number = 0): number {
if (value === null || value === undefined) return defaultValue;
if (typeof value === "boolean") return value ? 1 : 0;
if (typeof value === "number") return Math.trunc(value);
if (typeof value === "string") {
const text = value.trim();
if (!text) return defaultValue;
const parsed = parseInt(text, 10);
return Number.isNaN(parsed) ? defaultValue : parsed;
}
return defaultValue;
}
/** 对应 Python: _cent_to_yuan(value) -> float */
function centToYuan(value: unknown): number {
return toInt(value, 0) / 100.0;
}
/** 对应 Python: _lookup_mapping_value(mapping, key, default="") -> object */
function lookupMappingValue(
mapping: Record<string, unknown> | null | undefined,
key: unknown,
defaultValue: unknown = "",
): unknown {
const keyStr = String(key);
if (mapping == null) return defaultValue;
if (keyStr in mapping) return mapping[keyStr];
return defaultValue;
}
/** 对应 Python: _match_dimensions(entry, dimensions, inputs) -> bool */
function matchDimensions(
entry: Record<string, unknown> | null | undefined,
dimensions: string[],
inputs: Record<string, unknown>,
): boolean {
if (entry == null) return false;
for (const dim of dimensions) {
const entryValue = lookupMappingValue(entry, dim, null);
if (entryValue === null || entryValue === undefined) continue;
const inputVal = String(lookupMappingValue(inputs, dim, "")).trim().toLowerCase();
const entryVal = String(entryValue).trim().toLowerCase();
if (inputVal !== entryVal) return false;
}
return true;
}
/**
* 从 matrix 行提取维度数据。
*
* API 返回的 matrix 行中,维度字段(如 resolution、quality在顶层
* 而非嵌套在 raw 子对象内。本函数合并 raw如果存在和行自身字段
* 兼容两种数据格式,确保 matchDimensions 能正确匹配。
*/
function matrixRowData(row: ModelPricingMatrixRowConfig): Record<string, unknown> {
return { ...(row.raw ?? {}), ...row as unknown as Record<string, unknown> };
}
// ============================================================
// 金额格式化函数
// ============================================================
/**
* 将元数值格式化为显示字符串,保留两位小数,去除尾部无意义的零和小数点。
* 对应 workbench.py / billing_dialog.py: _format_yuan(value: float) -> str
*/
export function formatYuan(value: number): string {
const rounded = Math.round(value * 100) / 100;
// .toFixed(2) → 去除尾部零 → 去除尾部小数点
return rounded.toFixed(2).replace(/\.?0+$/, "");
}
/**
* 将分(cent)转换为元并格式化。
* 对应 workbench.py / billing_dialog.py / account_dialog.py: _format_yuan_from_cent(cent: int) -> str
*/
export function formatYuanFromCent(cent: number): string {
return formatYuan(cent / 100.0);
}
/**
* 格式化金额(分→元),带符号和¥前缀。
* 对应 task_table.py: _format_money_cent(value: object) -> str
*/
export function formatMoneyCent(value: unknown): string {
if (value === null || value === undefined) return "-";
if (typeof value === "boolean") return String(value);
let cent: number;
if (typeof value === "number") {
cent = Math.trunc(value);
} else if (typeof value === "string") {
const parsed = parseInt(value, 10);
if (Number.isNaN(parsed)) return value;
cent = parsed;
} else {
return String(value);
}
const sign = cent < 0 ? "-" : "";
const centAbs = Math.abs(cent);
const yuan = centAbs / 100.0;
const text = yuan.toFixed(2).replace(/\.?0+$/, "");
return `${sign}${text}`;
}
/**
* 格式化分→元,带符号和¥前缀。
* 对应 admin_console_dialog.py: _format_cent(cent: int) -> str
*/
export function formatCent(cent: number): string {
const sign = cent < 0 ? "-" : "";
const valueAbs = Math.abs(Math.trunc(cent));
const yuan = valueAbs / 100.0;
const text = yuan.toFixed(2).replace(/\.?0+$/, "");
return `${sign}${text}`;
}
// ============================================================
// 服务端计价逻辑
// ============================================================
/** 对应 Python: _calculate_server_simple(pricing, inputs) -> float */
function calculateServerSimple(
pricing: ModelPricingConfig,
inputs: Record<string, unknown>,
): number {
const priceCent = pricing.price_cent ?? 0;
const unitField = (pricing.unit_field ?? "").trim();
if (!unitField) {
return priceCent / 100.0;
}
const amount = asFloat(inputs[unitField]);
const amountValue = amount === null ? 1.0 : amount;
// (priceCent * amountValue) / 100 * 100 在 IEEE 754 下可能产生
// 浮点累积误差(例如 0.03*100→2.999...),直接 round 分子避免
return Math.round(priceCent * amountValue) / 100;
}
/** 对应 Python: _calculate_server_multi_dimension(pricing, inputs) -> Optional[float] */
function calculateServerMultiDimension(
pricing: ModelPricingConfig,
inputs: Record<string, unknown>,
): number | null {
const { dimensions, matrix } = pricing;
if (!matrix || matrix.length === 0) return null;
for (const row of matrix) {
if (matchDimensions(matrixRowData(row), dimensions, inputs)) {
return centToYuan(row.price_cent);
}
}
const firstRow = matrix[0];
if (firstRow.price_cent !== null && firstRow.price_cent !== undefined) {
return centToYuan(firstRow.price_cent);
}
return null;
}
/** 对应 Python: _calculate_server_billing_adapter(pricing, inputs) -> Optional[float] */
function calculateServerBillingAdapter(
pricing: ModelPricingConfig,
inputs: Record<string, unknown>,
): number | null {
const billingMode = (pricing.billing_mode ?? "rate").trim().toLowerCase();
let baseCostCent = 0;
if (billingMode === "per_k_chars") {
const field = (pricing.field ?? "prompt").trim() || "prompt";
const text = String(inputs[field] ?? "");
const pricePerKCent = pricing.price_per_k_cent ?? 0;
baseCostCent = Math.floor((text.length + 999) / 1000) * pricePerKCent;
} else {
const matrix = pricing.matrix;
if (!matrix || matrix.length === 0) return null;
const dimensions = pricing.dimensions;
const unitField = (pricing.unit_field ?? "duration").trim() || "duration";
const unitsRaw = asFloat(inputs[unitField]);
let units = unitsRaw === null ? 1.0 : unitsRaw;
// 底表修正
const floorTable = pricing.floor_table;
if (floorTable) {
const floorField = (floorTable.field ?? "").trim();
if (floorField === unitField) {
const floorValue = asFloat(floorTable.mapping[String(Math.trunc(units))]);
if (floorValue !== null && units < floorValue) {
units = floorValue;
}
}
}
let rate: number | null = null;
for (const row of matrix) {
if (matchDimensions(matrixRowData(row), dimensions, inputs)) {
rate = row.rate;
break;
}
}
if (rate === null) {
rate = matrix[0].rate;
}
if (rate === null || rate === undefined) return null;
baseCostCent = Math.round(rate * units * 100);
}
// 条件附加费
for (const addon of pricing.conditional_addons ?? []) {
const fieldKey = (addon.field_key ?? "").trim();
const operator = addon.operator;
let matched = false;
if (operator === "PRESENT") {
matched = Boolean(inputs[fieldKey]);
} else if (operator === "EQ") {
matched = String(inputs[fieldKey] ?? "") === String(addon.condition_value ?? "");
} else if (operator === "IN") {
const conditionValues = (addon.condition_values ?? []).map((v) => String(v));
matched = conditionValues.includes(String(inputs[fieldKey] ?? ""));
}
if (!matched) continue;
const addonPrice = addon.price;
if (addonPrice === null || addonPrice === undefined) continue;
const addonUnitField = String(
addon.unit_field ?? pricing.unit_field ?? "duration",
).trim() || "duration";
const addonUnitsRaw = asFloat(inputs[addonUnitField]);
const addonUnits = addonUnitsRaw === null ? 1.0 : addonUnitsRaw;
baseCostCent += Math.round(addonPrice * addonUnits * 100);
}
return baseCostCent / 100.0;
}
// ============================================================
// 主计价入口
// ============================================================
/**
* 根据模型定义和输入参数,动态计算消耗金额(单位:元)。
* 对应 Python: calculate_cost(model: ModelDefinition, inputs: Mapping) -> Optional[float]
*/
export function calculateCost(
model: ModelDefinition,
inputs: Record<string, unknown>,
): number | null {
const pricing = model.pricing_config;
if (!pricing) return null;
// ── 服务端定价 (price_type 存在且 mode 不存在) ──
if (pricing.price_type && !pricing.mode) {
const priceType = (pricing.price_type ?? "SIMPLE").trim().toUpperCase();
switch (priceType) {
case "SIMPLE":
return calculateServerSimple(pricing, inputs);
case "MULTI_DIMENSION":
return calculateServerMultiDimension(pricing, inputs);
case "BILLING_ADAPTER":
return calculateServerBillingAdapter(pricing, inputs);
default:
return null;
}
}
// ── 客户端定价 ──
const mode = pricing.mode;
if (mode === "fixed") {
if (pricing.price !== null && pricing.price !== undefined) {
return pricing.price;
}
return Number(pricing.raw?.price ?? 0);
}
if (mode === "per_k_chars") {
const fieldId = String(pricing.field ?? "text");
const rawText = inputs[fieldId] ?? "";
const text = rawText !== null && rawText !== undefined ? String(rawText) : "";
const pricePerK =
pricing.price_per_k ?? Number(pricing.raw?.price_per_k ?? 0);
return (text.length / 1000) * pricePerK;
}
if (mode === "rate_matrix") {
const unitField = String(pricing.unit_field ?? "duration");
const unitRaw = inputs[unitField];
const unitValue = asFloat(unitRaw);
if (unitValue === null) return null;
const { rate_dimensions, matrix } = pricing;
if (!rate_dimensions || rate_dimensions.length === 0 || !matrix || matrix.length === 0) {
return null;
}
for (const entry of matrix) {
if (matchDimensions(matrixRowData(entry), rate_dimensions, inputs)) {
let rate = entry.rate;
if (rate === null || rate === undefined) rate = entry.price_per_unit;
if (rate === null || rate === undefined) rate = entry.unit_price;
if (rate === null || rate === undefined) rate = 0.0;
const rawRound = pricing.round_digits ?? pricing.raw?.round ?? 2;
const roundDigits =
typeof rawRound === "number" || (typeof rawRound === "string" && String(rawRound).trim())
? Number(rawRound)
: 2;
return (
Math.round(unitValue * rate * Math.pow(10, roundDigits)) /
Math.pow(10, roundDigits)
);
}
}
return null;
}
if (mode === "matrix") {
const { dimensions, matrix } = pricing;
// 与 MULTI_DIMENSION 对齐dimensions 为空时 matchDimensions 自动全匹配
if (!matrix || matrix.length === 0) {
return null;
}
for (const entry of matrix) {
if (matchDimensions(matrixRowData(entry), dimensions, inputs)) {
// price_cent 优先Python 端矩阵定价统一走 cent 分度值,
// entry.price 可能为衍生/展示值price_cent 才是计价基准
if (entry.price_cent !== null && entry.price_cent !== undefined) {
return entry.price_cent / 100;
}
if (entry.price !== null && entry.price !== undefined) {
return entry.price;
}
return Number(entry.raw?.price ?? 0);
}
}
return null;
}
return null;
}
// ============================================================
// 定价依赖字段收集
// ============================================================
/**
* 收集定价计算所依赖的输入字段名集合。
* 对应 Python: collect_pricing_dependent_fields(model: ModelDefinition) -> set[str]
*/
export function collectPricingDependentFields(model: ModelDefinition): Set<string> {
const pricing = model.pricing_config;
if (!pricing) return new Set();
const fields = new Set<string>();
const priceType = (pricing.price_type ?? "").trim().toUpperCase();
const mode = (pricing.mode ?? "").trim().toLowerCase();
const billingMode = (pricing.billing_mode ?? "").trim().toLowerCase();
if (priceType === "SIMPLE") {
const unitField = (pricing.unit_field ?? "").trim();
if (unitField) fields.add(unitField);
} else if (priceType === "MULTI_DIMENSION") {
for (const dim of pricing.dimensions) {
const dimText = String(dim).trim();
if (dimText) fields.add(dimText);
}
} else if (priceType === "BILLING_ADAPTER") {
if (billingMode === "per_k_chars") {
const field = String(pricing.field ?? "prompt").trim() || "prompt";
fields.add(field);
} else {
const unitField = String(pricing.unit_field ?? "duration").trim() || "duration";
fields.add(unitField);
const floorTable = pricing.floor_table;
if (floorTable) {
const floorField = String(floorTable.field ?? "").trim();
if (floorField) fields.add(floorField);
}
for (const dim of pricing.dimensions) {
const dimText = String(dim).trim();
if (dimText) fields.add(dimText);
}
}
for (const addon of pricing.conditional_addons ?? []) {
const fieldKey = String(addon.field_key ?? "").trim();
if (fieldKey) fields.add(fieldKey);
const addonUnitField = String(
addon.unit_field ?? pricing.unit_field ?? "duration",
).trim();
if (addonUnitField) fields.add(addonUnitField);
}
} else if (mode === "per_k_chars") {
const field = String(pricing.field ?? "text").trim() || "text";
fields.add(field);
} else if (mode === "rate_matrix") {
const unitField = String(pricing.unit_field ?? "duration").trim() || "duration";
fields.add(unitField);
for (const dim of pricing.rate_dimensions) {
const dimText = String(dim).trim();
if (dimText) fields.add(dimText);
}
} else if (mode === "matrix") {
for (const dim of pricing.dimensions) {
const dimText = String(dim).trim();
if (dimText) fields.add(dimText);
}
}
return fields;
}