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

View File

@@ -0,0 +1,192 @@
// ============================================================
// useModelUI — param_schema → UIFieldConfig[] 转换 Hook
//
// 职责:
// - 遍历模型的 param_schema解析 spec/ui 字段
// - 查 WIDGET_MAP 获取对应的 React 组件
// - 解析 constraints_config.requires 计算字段间依赖
// - 组装出可直接渲染的 UIFieldConfig 数组
//
// ModelInputForm 只需遍历 fields 渲染 Form.Item 即可,
// 不需要关心 param_schema 的解析逻辑。
// ============================================================
import { useMemo } from 'react';
import type { ComponentType } from 'react';
import type { ModelsListResponseBody } from '@/modules/home/controllers/model-api';
import { WIDGET_MAP, FALLBACK_WIDGET } from '../views/left/controls';
import type { WidgetProps } from '../views/left/controls';
// ---------- 输出类型 ----------
/**
* 单个字段的 UI 配置。
*
* ModelInputForm 遍历此数组,每一项渲染一个 Form.Item。
*/
export interface UIFieldConfig {
/** 渲染顺序param_schema 中的索引,保证稳定) */
index: number;
/** 表单字段名map_to对应 Form.Item name */
name: string;
/** API 声明的 widget 名称(如 "seedance2Content"),供提交时做特殊序列化 */
widgetName: string;
/** 要渲染的 React 组件 */
component: ComponentType<WidgetProps>;
/** 显示标签ui.label → Form.Item label */
label: string;
/** 是否必填spec.required → Form.Item rules */
must: boolean;
/** 默认值spec.default → Form initialValues */
defaultValue?: unknown;
/** 提示文本ui.tips */
tips?: string;
/** Form.Item 的 valuePropNamecheckbox 用 'checked',其他用默认 'value' */
valuePropName?: string;
/** 是否为文件上传类控件(需要 getValueFromEvent 提取 fileList */
isFileWidget: boolean;
/**
* 条件依赖:当这些字段的值为空时,本字段应被禁用。
*
* 来源于 constraints_config.requires 的逆向推导:
* { if: "sref", then: ["sw", "sv"] }
* → sw.dependsOn = ["sref"], sv.dependsOn = ["sref"]
*/
dependsOn?: string[];
/** 传给 widget 组件的配置placeholder/options/min/max 等) */
widgetConfig: Record<string, unknown>;
}
// ---------- 文件类型 widget 集合 ----------
const FILE_WIDGETS = new Set(['imageInput', 'imageList', 'audioList', 'videoInput']);
// ---------- 约束解析 ----------
interface ConstraintRule {
if: string;
then: string[];
}
/**
* 从 constraints_config.requires 构建反向依赖表。
*
* 输入:
* [{ if: "sref", then: ["sw", "sv"] }]
*
* 输出:
* { sw: ["sref"], sv: ["sref"] }
*
* 含义sw 和 sv 依赖 srefsref 为空时它们应被禁用。
*/
function buildDependencyMap(
constraintsConfig: Record<string, unknown>,
): Map<string, string[]> {
const depMap = new Map<string, string[]>();
const requires = constraintsConfig.requires as ConstraintRule[] | undefined;
if (!requires || !Array.isArray(requires)) return depMap;
for (const rule of requires) {
if (!rule.if || !rule.then || !Array.isArray(rule.then)) continue;
for (const target of rule.then) {
const existing = depMap.get(target) || [];
existing.push(rule.if);
depMap.set(target, existing);
}
}
return depMap;
}
// ---------- Hook ----------
/**
* 根据模型的 param_schema 和 constraints_config 构建 UI 字段配置数组。
*
* @param paramSchema — 来自 selectedModel.param_schema
* @param constraintsConfig — 来自 selectedModel.constraints_config
* @returns UIFieldConfig[] — 渲染就绪的字段配置列表
*/
export function useModelUI(
paramSchema: ModelsListResponseBody['param_schema'],
constraintsConfig: Record<string, unknown> = {},
): UIFieldConfig[] {
return useMemo(() => {
if (!paramSchema || paramSchema.length === 0) return [];
const depMap = buildDependencyMap(constraintsConfig);
return paramSchema.map((item, index) => {
const spec = (item.spec || {}) as Record<string, unknown>;
const ui = (item.ui || {}) as Record<string, unknown>;
const widget = (ui.widget as string) || 'lineEdit';
const component = WIDGET_MAP[widget] || FALLBACK_WIDGET;
const widgetConfig: Record<string, unknown> = {
placeholder: ui.placeholder || `请输入${ui.label || item.map_to}`,
required: spec.required,
enumValues: spec.enum,
minValue: spec.min_value,
maxValue: spec.max_value,
step: spec.single_step,
minLength: spec.min_length,
maxLength: spec.max_length,
// imageInput / imageList / audioList
fileFilter: spec.file_filter,
maxFileSizeMb: spec.max_file_size_mb,
minFiles: spec.min_files,
maxFiles: spec.max_files,
enableSwitch: ui.enable_switch,
// seedance2Contentspec 中的媒体文件数量 + 上传供应商
imageFiles: (spec.image_files as number) ?? 0,
videoFiles: (spec.video_files as number) ?? 0,
audioFiles: (spec.audio_files as number) ?? 0,
uploadProvider: spec.upload_provider as string | undefined,
};
const isFile = FILE_WIDGETS.has(widget);
const isCheckbox = widget === 'checkbox';
const name = item.map_to;
const dependsOn = depMap.get(name);
return {
index,
name,
widgetName: widget,
component,
label: (ui.label as string) || name,
must: (spec.required as boolean) || false,
defaultValue: spec.default,
tips: (ui.tips as string) || undefined,
valuePropName: isCheckbox ? 'checked' : undefined,
isFileWidget: isFile,
dependsOn: dependsOn && dependsOn.length > 0 ? dependsOn : undefined,
widgetConfig,
};
});
}, [paramSchema, constraintsConfig]);
}
/** 判断 widget 类型是否为文件上传类控件 */
export function isFileWidget(widgetName: string): boolean {
return FILE_WIDGETS.has(widgetName);
}
// ---------- 值判空工具 ----------
/**
* 判断表单值是否为"空"(用于依赖禁用判断)。
*
* 注意:空数组 [] 不视为空。
* 对于 enable_switch 控件undefined = 开关关闭, [] = 开关开启但未选文件。
* 配合 constraints_config.requires开关开启即解锁依赖字段无需等待文件上传。
*/
export function isValueEmpty(value: unknown): boolean {
if (value === undefined || value === null) return true;
if (typeof value === 'string' && value.trim() === '') return true;
if (typeof value === 'boolean' && !value) return true;
return false;
}