/** * Excel 配置生成脚本(增强版) * * 功能: * - 将 /excels 目录下的 .xlsx 文件转换为 JSON(assets/configs/generated/data) * - 依据表头(字段:类型 / 可选? / 默认=值 / //备注)生成 TS 数据类与解析函数(assets/configs/generated/core) * - 从 __enums__.xlsx 生成 Enums.ts;从 __beans__.xlsx 生成 Beans.ts 与解析函数 * - 生成 Manager.ts(缺失时),支持唯一索引 getByXxx * - 生成时进行唯一约束校验与跨表引用校验 * * 使用: * - 在 extensions/max-studio/package.json 中暴露脚本:"gen:configs" * - 运行:npm run gen:configs */ import path from "path"; import fs from "fs"; import { ensureDirSync, writeFileSync, existsSync } from "fs-extra"; import xlsx from "xlsx"; type PrimitiveType = "number" | "string" | "boolean"; type CcType = "Color" | "Vec2" | "Vec3" | "Vec4" | "Size" | "Rect" | "Quat" | "Mat4"; interface FieldDef { name: string; type: string; // number|string|boolean|CcType|Enum|Bean // 原始类型标识,用于从 Excel 表头保留 `int` / `float` 信息 rawType?: string; isArray: boolean; optional: boolean; defaultValue?: string; comment?: string; unique?: boolean; refTarget?: string; // 跨表引用目标(表名) } interface TableDef { name: string; // e.g. PetConfig fields: FieldDef[]; } interface EnumItem { name: string; value: number; comment?: string; } interface EnumDef { name: string; items: EnumItem[]; } interface BeanDef { name: string; fields: FieldDef[]; } const PROJECT_ROOT = path.resolve(__dirname, "../../.."); const EXCEL_DIR = path.join(PROJECT_ROOT, "excels"); const OUTPUT_CORE_DIR = path.join(PROJECT_ROOT, "assets/configs/generated/core"); const OUTPUT_DATA_DIR = path.join(PROJECT_ROOT, "assets/configs/generated/data"); const CC_COMPLEX_TYPES: ReadonlySet = new Set([ "Color", "Vec2", "Vec3", "Vec4", "Size", "Rect", "Quat", "Mat4", ]); function log(...args: any[]) { // 统一日志输出 console.log("[gen-configs]", ...args); } function parseHeaderCell(cell: string): FieldDef | null { const raw = cell?.trim(); if (!raw) return null; const [left, commentRaw] = raw.split("//"); const comment = commentRaw?.trim(); const [namePartRaw, typePartRaw = ""] = left.split(":").map((v) => v.trim()); const optional = /\?$/.test(namePartRaw); const name = namePartRaw.replace(/\?$/, ""); const [typeIdentRaw, defaultRaw] = typePartRaw.split("=").map((v) => v.trim()); let typeIdent = typeIdentRaw || (name === "id" ? "number" : "string"); const isArray = /\[\]$/.test(typeIdent); if (isArray) typeIdent = typeIdent.replace(/\[\]$/, ""); let refTarget: string | undefined; if (/^Ref:/i.test(typeIdent)) { refTarget = typeIdent.replace(/^Ref:/i, ""); typeIdent = "number"; // 引用用目标 id(数值) } // 规范 Cocos 原生类型:去掉 "cc." 前缀 if (/^cc\./i.test(typeIdent)) { typeIdent = typeIdent.replace(/^cc\./i, ""); } // 记录原始数值类型,并将 int/float 规范化为 TS 的 number const rawType = /^(int|float)$/i.test(typeIdent) ? typeIdent.toLowerCase() : undefined; if (rawType === "int" || rawType === "float") { typeIdent = "number"; } const unique = /\[\s*unique\s*\]/i.test(comment || ""); const refMatch = (comment || "").match(/\[\s*ref\s*=\s*([A-Za-z0-9_]+)\s*\]/i); if (refMatch) refTarget = refMatch[1]; const def: FieldDef = { name, type: typeIdent, rawType, isArray, optional, defaultValue: defaultRaw || undefined, comment, unique, refTarget, }; return def; } function readWorkbookTables(filePath: string): { table: TableDef; rows: any[] } { const workbook = xlsx.readFile(filePath); const sheetName = workbook.SheetNames[0]; const sheet = workbook.Sheets[sheetName]; // 读取为二维数组,支持三行表头:第一行字段名、第二行类型、第三行备注 const rows2d = xlsx.utils.sheet_to_json(sheet, { header: 1, defval: "" }) as any[][]; const headerNames = (rows2d[0] || []).map((v) => String(v ?? "").trim()); const headerTypes = (rows2d[1] || []).map((v) => String(v ?? "").trim()); const headerComments = (rows2d[2] || []).map((v) => String(v ?? "").trim()); const fields: FieldDef[] = []; for (let i = 0; i < headerNames.length; i++) { const nameRaw = headerNames[i]; if (!nameRaw) continue; const typeSpecRaw = headerTypes[i] || ""; const commentRaw = headerComments[i] || ""; // 解析类型行:支持 Ref:Table、数组[]、默认值=xxx、可选?(写在类型末尾) let typeIdent = typeSpecRaw; let defaultValue: string | undefined; const optional = /\?$/.test(typeIdent); if (optional) typeIdent = typeIdent.replace(/\?$/, ""); const isArray = /\[\]$/.test(typeIdent); if (isArray) typeIdent = typeIdent.replace(/\[\]$/, ""); const parts = typeIdent.split("="); if (parts.length > 1) { typeIdent = parts[0].trim(); defaultValue = parts.slice(1).join("=").trim(); } let refTarget: string | undefined; if (/^Ref:/i.test(typeIdent)) { refTarget = typeIdent.replace(/^Ref:/i, ""); typeIdent = "number"; // 引用以目标 id(数值)表达 } // 规范 Cocos 原生类型:去掉 "cc." 前缀 if (/^cc\./i.test(typeIdent)) { typeIdent = typeIdent.replace(/^cc\./i, ""); } // 记录原始数值类型,并将 int/float 规范化为 TS 的 number const rawType = /^(int|float)$/i.test(typeIdent) ? typeIdent.toLowerCase() : undefined; if (rawType === "int" || rawType === "float") { typeIdent = "number"; } const unique = /\[\s*unique\s*\]/i.test(commentRaw); const refMatch = commentRaw.match(/\[\s*ref\s*=\s*([A-Za-z0-9_]+)\s*\]/i); if (refMatch) refTarget = refMatch[1]; const name = String(nameRaw).replace(/\?$/, ""); const type = typeIdent || (name === "id" ? "number" : "string"); fields.push({ name, type, rawType, isArray, optional, defaultValue, comment: commentRaw || undefined, unique, refTarget, }); } // 将第 4 行开始的数据行转换为对象数组(按第一行字段名映射) const dataRows: any[] = []; for (let r = 3; r < rows2d.length; r++) { const rowArr = rows2d[r] || []; // 空行跳过 if (rowArr.every((v: any) => String(v ?? "").trim() === "")) continue; const obj: Record = {}; for (let c = 0; c < headerNames.length; c++) { const key = headerNames[c]; if (!key) continue; obj[key.replace(/\?$/, "")] = rowArr[c]; } dataRows.push(obj); } const tableName = path.basename(filePath, path.extname(filePath)); return { table: { name: tableName, fields }, rows: dataRows }; } function writeFileSafe(filePath: string, content: string) { ensureDirSync(path.dirname(filePath)); writeFileSync(filePath, content, { encoding: "utf-8" }); } function generateEnumsTs(enums: EnumDef[]): string { const parts: string[] = []; parts.push("/** 自动生成的枚举定义 */"); for (const e of enums) { parts.push(`export enum ${e.name} {`); for (const item of e.items) { const comment = item.comment ? ` // ${item.comment}` : ""; parts.push(` ${item.name} = ${item.value},${comment}`); } parts.push("}\n"); } return parts.join("\n"); } function readEnumsExcel(absPath: string): EnumDef[] { const workbook = xlsx.readFile(absPath); const results: EnumDef[] = []; for (const sheetName of workbook.SheetNames) { const sheet = workbook.Sheets[sheetName]; const rows: any[] = xlsx.utils.sheet_to_json(sheet, { defval: "" }); // 检测是否为分组格式:包含 EnumName/ValueName/Value/Comment 列 const hasGrouped = rows.length > 0 && Object.keys(rows[0]).some((k) => /^(EnumName|enumName)$/i.test(String(k))) && Object.keys(rows[0]).some((k) => /^(ValueName|valueName|Name|name)$/i.test(String(k))) && Object.keys(rows[0]).some((k) => /^(Value|value)$/i.test(String(k))); if (hasGrouped) { const enumMap = new Map(); let currentEnum = ""; for (const row of rows) { const enumNameRaw = String(row.EnumName ?? row.enumName ?? "").trim(); if (enumNameRaw) currentEnum = enumNameRaw; if (!currentEnum) continue; const valueName = String(row.ValueName ?? row.valueName ?? row.Name ?? row.name ?? "").trim(); if (!valueName) continue; const valueRaw = row.Value ?? row.value ?? 0; const value = Number(valueRaw); const comment = String(row.Comment ?? row.comment ?? "").trim(); if (!enumMap.has(currentEnum)) enumMap.set(currentEnum, []); enumMap.get(currentEnum)!.push({ name: valueName, value, comment }); } for (const [name, items] of enumMap.entries()) { results.push({ name, items }); } } else { // 兼容每个 Sheet 一个枚举:使用 Name/Value/Comment const items: EnumItem[] = []; for (const row of rows) { const name = String(row.ValueName ?? row.valueName ?? row.Name ?? row.name ?? "").trim(); if (!name) continue; const valueRaw = row.Value ?? row.value ?? 0; const value = Number(valueRaw); const comment = String(row.Comment ?? row.comment ?? "").trim(); items.push({ name, value, comment }); } results.push({ name: sheetName, items }); } } return results; } function generateBeansTs(beans: BeanDef[]): string { const usedCcTypes = new Set(); for (const b of beans) { for (const f of b.fields) { const base = f.type; if (CC_COMPLEX_TYPES.has(base)) usedCcTypes.add(base); } } const ccImports = usedCcTypes.size > 0 ? `import { ${Array.from(usedCcTypes).join(", ")} } from 'cc';\n\n` : ""; const parts: string[] = []; parts.push(`${ccImports}import { ConfigParseUtils } from './ConfigParseUtils';`); for (const b of beans) { parts.push(`\n/** Bean: ${b.name} */`); const ifaceLines: string[] = []; for (const f of b.fields) { const tsType = `${f.type}${f.isArray ? "[]" : ""}`; ifaceLines.push(` ${f.name}${f.optional ? "?" : ""}: ${tsType};`); } parts.push(`export interface ${b.name} {\n${ifaceLines.join("\n")}\n}`); // 解析函数 const parseExprs: string[] = []; for (const f of b.fields) { const expr = buildParseExpression(f, new Set(), new Set(), true); parseExprs.push(` ${f.name}: ${expr},`); } parts.push( `export function parse${b.name}(data: any): ${b.name} {\n return {\n${parseExprs.join("\n")}\n };\n}`, ); } return parts.join("\n"); } function readBeansExcel(absPath: string): BeanDef[] { const workbook = xlsx.readFile(absPath); const results: BeanDef[] = []; for (const sheetName of workbook.SheetNames) { const sheet = workbook.Sheets[sheetName]; const headerRow = xlsx.utils.sheet_to_json(sheet, { header: 1 })[0] as string[]; const fields: FieldDef[] = []; for (const cell of headerRow) { const def = parseHeaderCell(String(cell || "")); if (def) fields.push(def); } results.push({ name: sheetName, fields }); } return results; } function isPrimitive(t: string): t is PrimitiveType { return t === "number" || t === "string" || t === "boolean"; } function isCcType(t: string): t is CcType { return CC_COMPLEX_TYPES.has(t); } function buildParseExpression( field: FieldDef, enumNames: ReadonlySet, beanNames: ReadonlySet, inBean = false, ): string { const source = inBean ? `data.${field.name}` : `data.${field.name}`; const base = field.type; if (!field.isArray) { if (isPrimitive(base)) { if (base === "number") { // 根据原始类型选择解析函数:int -> parseInt,float -> parseFloat return field.rawType === "float" ? `ConfigParseUtils.parseFloat(${source})` : `ConfigParseUtils.parseInt(${source})`; } if (base === "boolean") return `ConfigParseUtils.parseBoolean(${source})`; return `ConfigParseUtils.parseString(${source})`; } if (isCcType(base)) { const fn = base === "Color" ? "parseColor" : base === "Vec2" ? "parseVec2" : base === "Vec3" ? "parseVec3" : base === "Vec4" ? "parseVec4" : base === "Size" ? "parseSize" : base === "Rect" ? "parseRect" : base === "Quat" ? "parseQuat" : base === "Mat4" ? "parseMat4" : "parseString"; return `ConfigParseUtils.${fn}(${source})`; } if (enumNames.has(base)) { // 支持 name 或 number,两者均可 return `(typeof ${source} === 'number' ? ${source} : ((${"Enums"}).${base}[String(${source}) as keyof typeof ${"Enums"}.${base}] ?? 0))`; } if (beanNames.has(base)) { return `parse${base}(${source})`; } // 未知类型按字符串解析 return `ConfigParseUtils.parseString(${source})`; } else { // 数组解析 if (isPrimitive(base)) { if (base === "number") return `ConfigParseUtils.parseNumberArray(${source})`; if (base === "string") return `ConfigParseUtils.parseStringArray(${source})`; // boolean[]:先按字符串数组再逐项转布尔 return `ConfigParseUtils.parseStringArray(${source}).map((v) => ConfigParseUtils.parseBoolean(v))`; } if (isCcType(base)) { const fn = base === "Color" ? "parseColor" : base === "Vec2" ? "parseVec2" : base === "Vec3" ? "parseVec3" : base === "Vec4" ? "parseVec4" : base === "Size" ? "parseSize" : base === "Rect" ? "parseRect" : base === "Quat" ? "parseQuat" : base === "Mat4" ? "parseMat4" : "parseString"; // 使用 ';' 分隔元素,每个元素用解析函数处理 return `String(${source}).split(';').filter(Boolean).map((chunk) => ConfigParseUtils.${fn}(chunk))`; } if (enumNames.has(base)) { return `ConfigParseUtils.parseStringArray(${source}).map((v) => (isNaN(Number(v)) ? ((${"Enums"}).${base}[String(v) as keyof typeof ${"Enums"}.${base}] ?? 0) : Number(v)))`; } if (beanNames.has(base)) { return `String(${source}).split(';').filter(Boolean).map((json) => parse${base}(JSON.parse(json)))`; } return `ConfigParseUtils.parseStringArray(${source})`; } } function generateDataJson(table: TableDef, rows: any[]): string { return JSON.stringify(rows, null, 2); } function generateDataTs(table: TableDef, enumNames: ReadonlySet, beanNames: ReadonlySet): string { const usedCcTypes = new Set(); const usedEnums = new Set(); const usedBeans = new Set(); for (const f of table.fields) { const base = f.type; if (isCcType(base)) usedCcTypes.add(base); if (enumNames.has(base)) usedEnums.add(base); if (beanNames.has(base)) usedBeans.add(base); } const importLines: string[] = []; if (usedCcTypes.size > 0) { importLines.push(`import { ${Array.from(usedCcTypes).join(", ")} } from "cc";`); } if (usedEnums.size > 0) { importLines.push(`import * as Enums from "./Enums";`); } if (usedBeans.size > 0) { importLines.push( `import { ${Array.from(usedBeans) .map((b) => `${b}, parse${b}`) .join(", ")} } from "./Beans";`, ); } importLines.push(`import { ConfigParseUtils } from "./ConfigParseUtils";`); const imports = `${importLines.join("\n")}\n\n`; const className = `${table.name}Data`; const fieldsPrivate = table.fields .map((f) => { let value = `\n private _${f.name}: ${f.type}${f.isArray ? "[]" : ""};`; value += `\n public get ${f.name}(): ${f.type}${f.isArray ? "[]" : ""} {`; value += `\n return this._${f.name};`; value += `\n }`; return value; }) .join("\n"); const getters = table.fields .map((f) => { const title = f.comment ? `${f.comment}` : f.name; const tsType = `${f.type}${f.isArray ? "[]" : ""}`; const wrap = isCcType(f.type) ? `ConfigParseUtils.createReadonlyProxy(ConfigParseUtils.deepFreeze(this._${f.name}), '${title}')` : `this._${f.name}`; return `\n\n /** ${title} */\n public get ${f.name}(): ${tsType} {\n return ${wrap};\n }`; }) .join("\n"); const ctorParams = table.fields.map((f) => `${f.name}: ${f.type}${f.isArray ? "[]" : ""}`).join(",\n "); const ctorAssigns = table.fields.map((f) => `\n this._${f.name} = ${f.name};`).join(""); const parseArgs = table.fields.map((f) => `\n ${buildParseExpression(f, enumNames, beanNames)}`).join(",\n"); const file = `${imports}/**\n * ${ table.name }数据结构\n */\nexport class ${className} {\n${fieldsPrivate}\n\n public constructor(\n${ " " + ctorParams }\n ) {${ctorAssigns}\n }\n}\n\n/**\n * 解析配置数据\n */\nexport function parse${ table.name }Data(data: any): ${className} {\n return new ${className}(${parseArgs}\n );\n}\n`; return file; } function generateManagerTs(table: TableDef): string { const name = table.name; const uniqueFields = table.fields.filter( (f) => f.unique && !f.isArray && (isPrimitive(f.type) || isCcType(f.type) === false), ); const idxDecls = uniqueFields .map((f) => ` private by_${f.name} = new Map<${f.type}, ${name}Data>();`) .join("\n"); const idxAssigns = uniqueFields .map((f) => ` this.by_${f.name}.set((config as any).${f.name}, config);`) .join("\n"); const idxGetters = uniqueFields .map( (f) => `\n public getBy${f.name.charAt(0).toUpperCase() + f.name.slice(1)}(key: ${ f.type }): ${name}Data | null {\n if (!this.isLoaded) {\n LogUtils.warn("${name}Manager", "${name} 配置尚未加载完成,请等待加载完成");\n return null;\n }\n return this.by_${ f.name }.get(key) || null;\n }\n`, ) .join(""); return `import { JsonAsset } from "cc";\n\nimport { singleton, Singleton } from "@max-studio/core/Singleton";\nimport LogUtils from "@max-studio/core/utils/LogUtils";\n\nimport { ConfigParseUtils } from "./ConfigParseUtils";\nimport { ${name}Data, parse${name}Data } from "./${name}Data";\nimport ResManager from "@max-studio/core/res/ResManager";\n\n/**\n * ${name}配置管理器\n *\n * ⚠️ 此文件由配置表生成器自动生成,请勿手动修改!\n * 如需修改,请编辑对应的Excel配置文件,然后重新生成\n */\n@singleton({ auto: true })\nexport class ${name}Manager extends Singleton {\n private configList: readonly ${name}Data[] = [];\n private configMap = new Map();\n ${idxDecls}\n private isLoaded = false;\n\n protected async onInit(): Promise {\n await this.loadConfig();\n }\n\n private async loadConfig(): Promise {\n try {\n const {err, asset} = await ResManager.getInstance().loadAsset({\n path: "generated/data/${name}",\n type: JsonAsset,\n bundle: "configs",\n });\n this.parseConfig(asset.json);\n this.isLoaded = true;\n } catch (err) {\n LogUtils.error("${name}Manager", "加载 ${name} 配置失败:", err);\n }\n }\n\n private parseConfig(data: any[]): void {\n this.configList = Object.freeze(\n data.map((item) => {\n const config = parse${name}Data(item);\n const frozenConfig = ConfigParseUtils.deepFreeze(config);\n return ConfigParseUtils.createReadonlyProxy(frozenConfig, "${name}Data配置");\n }),\n );\n this.configMap.clear();\n ${ uniqueFields.length > 0 ? uniqueFields.map((f) => `this.by_${f.name}.clear();`).join(" ") : "" }\n for (const config of this.configList) {\n this.configMap.set((config as any).id, config);\n${ idxAssigns ? ` ${idxAssigns}\n` : "" } }\n ConfigParseUtils.deepFreeze(this.configMap);\n }\n\n public getConfig(id: number): ${name}Data | null {\n if (!this.isLoaded) {\n LogUtils.warn("${name}Manager", "${name} 配置尚未加载完成,请等待加载完成");\n return null;\n }\n return this.configMap.get(id) || null;\n }\n\n public getAllConfigs(): ${name}Data[] {\n if (!this.isLoaded) {\n LogUtils.warn("${name}Manager", "${name} 配置尚未加载完成,请等待加载完成");\n return [];\n }\n return [...this.configList];\n }\n\n public isConfigLoaded(): boolean {\n return this.isLoaded;\n }\n ${idxGetters} } `; } function validateUnique(table: TableDef, rows: any[]): void { for (const f of table.fields) { if (!f.unique) continue; const seen = new Set(); let dupCount = 0; for (let i = 0; i < rows.length; i++) { const v = String(rows[i][f.name] ?? ""); if (!v) continue; if (seen.has(v)) { dupCount++; log(`⚠️ [唯一约束] ${table.name}.${f.name} 在第 ${i + 2} 行出现重复值: ${v}`); } else { seen.add(v); } } if (dupCount === 0) { log(`✅ [唯一约束] ${table.name}.${f.name} 校验通过`); } } } function validateRefsAll(tables: { table: TableDef; rows: any[] }[]): void { const idMap = new Map>(); for (const { table, rows } of tables) { const ids = new Set(); for (const r of rows) { const id = String(r.id ?? ""); if (id) ids.add(id); } idMap.set(table.name, ids); } for (const { table, rows } of tables) { for (const f of table.fields) { if (!f.refTarget) continue; const targetIds = idMap.get(f.refTarget); if (!targetIds) { log(`⚠️ [引用校验] ${table.name}.${f.name} 指向的表 ${f.refTarget} 未在本次生成中发现`); continue; } for (let i = 0; i < rows.length; i++) { const v = String(rows[i][f.name] ?? ""); if (!v) continue; if (!targetIds.has(v)) { log(`⚠️ [引用校验] ${table.name}.${f.name} 第 ${i + 2} 行引用不存在的 ${f.refTarget}.id=${v}`); } } } } } function genForExcel(file: string, enumNames: ReadonlySet, beanNames: ReadonlySet) { const absPath = path.join(EXCEL_DIR, file); log("处理 Excel:", absPath); const { table, rows } = readWorkbookTables(absPath); validateUnique(table, rows); const jsonOut = path.join(OUTPUT_DATA_DIR, `${table.name}.json`); writeFileSafe(jsonOut, generateDataJson(table, rows)); log("生成 JSON:", jsonOut); const dataTsOut = path.join(OUTPUT_CORE_DIR, `${table.name}Data.ts`); writeFileSafe(dataTsOut, generateDataTs(table, enumNames, beanNames)); log("生成 TS 数据类:", dataTsOut); const mgrTsOut = path.join(OUTPUT_CORE_DIR, `${table.name}Manager.ts`); if (!existsSync(mgrTsOut)) { writeFileSafe(mgrTsOut, generateManagerTs(table)); log("生成 TS 管理器:", mgrTsOut); } else { log("检测到已有管理器,跳过生成:", mgrTsOut); } } function main() { try { if (!existsSync(EXCEL_DIR)) { throw new Error(`未找到 Excel 目录: ${EXCEL_DIR}`); } const files = fs.readdirSync(EXCEL_DIR).filter((f) => f.endsWith(".xlsx")); if (files.length === 0) { log("excels 目录下没有 .xlsx 文件,无需生成"); return; } const enumsFile = files.find((f) => f === "__enums__.xlsx"); const beansFile = files.find((f) => f === "__beans__.xlsx"); const tableFiles = files.filter((f) => f !== "__enums__.xlsx" && f !== "__beans__.xlsx"); // 1) 生成枚举 const enumDefs = enumsFile ? readEnumsExcel(path.join(EXCEL_DIR, enumsFile)) : []; const enumNames = new Set(enumDefs.map((e) => e.name)); if (enumDefs.length > 0) { const enumsOut = path.join(OUTPUT_CORE_DIR, "Enums.ts"); writeFileSafe(enumsOut, generateEnumsTs(enumDefs)); log("生成枚举:", enumsOut); } // 2) 生成 Beans const beanDefs = beansFile ? readBeansExcel(path.join(EXCEL_DIR, beansFile)) : []; const beanNames = new Set(beanDefs.map((b) => b.name)); if (beanDefs.length > 0) { const beansOut = path.join(OUTPUT_CORE_DIR, "Beans.ts"); writeFileSafe(beansOut, generateBeansTs(beanDefs)); log("生成 Beans:", beansOut); } // 3) 读取所有表用于跨表引用校验 const parsedTables: { table: TableDef; rows: any[] }[] = []; for (const f of tableFiles) { const absPath = path.join(EXCEL_DIR, f); const { table, rows } = readWorkbookTables(absPath); parsedTables.push({ table, rows }); } validateRefsAll(parsedTables); // 4) 为每个表生成产物 for (const f of tableFiles) { genForExcel(f, enumNames, beanNames); } log("配置生成完成 ✅"); } catch (err) { console.error("[gen-configs] 生成失败:", err); process.exitCode = 1; } } main();