import { existsSync, readFileSync } from "node:fs"; import { basename } from "node:path"; export interface FormMetadataEntity { oid: string; key: string; entryName: string; name: string; tableName: string; typeName: string; } export interface FormMetadataField { oid: string; key: string; name: string; fieldName: string; propertyName: string; entityKey: string; parentId: string; typeName: string; typeLabel: string; tableName: string; subTableFlag: string; refType: string; } export interface FormMetadataPlugin { key: string; name: string; className: string; assemblyName: string; serviceType: string; eventName: string; } export interface FormMetadataForm { fid?: string; formId?: string; name?: string; subsystem?: string; baseObjectId?: string; } export interface FormMetadataDocument { fid?: string; formId?: string; name?: string; subsystem?: string; baseObjectId?: string; xml: string; source?: string; } export interface FormMetadata { form: FormMetadataForm; entities: FormMetadataEntity[]; fields: FormMetadataField[]; plugins: FormMetadataPlugin[]; inheritance: Array<{ fid?: string; formId?: string; name?: string; baseObjectId?: string; source?: string }>; warnings: string[]; } interface XmlFrame { tag: string; target?: FormMetadataEntity | FormMetadataField | FormMetadataPlugin; kind?: "entity" | "field" | "plugin"; } interface ExtractedDocuments { documents: FormMetadataDocument[]; warnings: string[]; } const ENTITY_TAGS = new Set([ "HeadEntity", "SubHeadEntity", "EntryEntity", "SubEntryEntity", "TaxDetailSubEntryEntity", "SNSubEntryEntity", "MainEntity", "BillEntity", "BaseEntity", "ParameterEntity", "DynamicFormEntity", "TreeEntity", ]); const FIELD_TYPE_LABELS: Record = { TextField: "文本", BaseDataField: "基础资料", OrgField: "组织", CreaterField: "创建人", CreateDateField: "创建日期", ModifyerField: "修改人", ModifyDateField: "修改日期", UserField: "用户", DateField: "日期", AmountField: "金额", QtyField: "数量", PriceField: "价格", DecimalField: "小数", DiscountField: "折扣率", CheckBoxField: "复选框", IntegerField: "整数", BasePropertyField: "基础资料属性", BillStatusField: "单据状态", BillNoField: "单据编号", BillTypeField: "单据类型", ComboField: "下拉列表", UnitField: "单位字段", RelatedFlexGroupField: "辅助属性", SourceBillTypeField: "源单类型", SourceBillNoField: "源单编号", TaxCombinationField: "税率", LotField: "批号", DateTimeField: "日期时间", MultiLangTextField: "多语言文本", BusinessFlowField: "业务流程", BaseUnitField: "基本单位", BaseQtyField: "基本数量", }; const XML_TAG_RE = /||<\?[\s\S]*?\?>|<\/?\s*([A-Za-z_][\w:.-]*)([^>]*)>|([^<]+)/g; export function parseFormMetadataXml(xml: string, form: FormMetadataForm = {}): FormMetadata { const metadata: FormMetadata = { form: compactForm(form), entities: [], fields: [], plugins: [], inheritance: [{ fid: form.fid, formId: form.formId, name: form.name, baseObjectId: form.baseObjectId, source: undefined }], warnings: [], }; const stack: XmlFrame[] = []; let match: RegExpExecArray | null; while ((match = XML_TAG_RE.exec(xml)) !== null) { const cdata = match[1]; const rawTag = match[2]; const rawAttrs = match[3] ?? ""; const text = match[4] ?? cdata; if (text !== undefined) { applyText(stack, decodeXml(text).trim()); continue; } if (!rawTag) continue; const isClosing = /^<\s*\//.test(match[0]); const selfClosing = /\/\s*>$/.test(match[0]); const tag = localName(rawTag); if (isClosing) { closeFrame(stack, tag, metadata); continue; } const currentTarget = nearestTarget(stack); const attrs = parseAttributes(rawAttrs); let frame: XmlFrame = { tag }; if (!currentTarget && isEntityTag(tag)) { frame = { tag, kind: "entity", target: { oid: attrs.oid ?? "", key: "", entryName: "", name: "", tableName: "", typeName: tag }, }; } else if (!currentTarget && isFieldTag(tag)) { frame = { tag, kind: "field", target: { oid: attrs.oid ?? "", key: "", name: "", fieldName: "", propertyName: "", entityKey: "", parentId: "", typeName: tag, typeLabel: fieldTypeLabel(tag), tableName: "", subTableFlag: "", refType: "", }, }; } else if (!currentTarget && /^PlugIn$/i.test(tag)) { frame = { tag, kind: "plugin", target: { key: attrs.Key ?? attrs.key ?? "", name: "", className: "", assemblyName: "", serviceType: "", eventName: "", }, }; } stack.push(frame); if (selfClosing) closeFrame(stack, tag, metadata); } resolveFieldTables(metadata); deduplicateFields(metadata); addConcreteWarnings(metadata); return metadata; } export function resolveFormMetadataInheritance(documents: FormMetadataDocument[]): FormMetadata { if (documents.length === 0) { return { form: {}, entities: [], fields: [], plugins: [], inheritance: [], warnings: ["未提供可解析的 FKERNELXML/fdata 元数据。"], }; } let merged = parseFormMetadataXml(documents[documents.length - 1].xml, documentForm(documents[documents.length - 1])); for (let index = documents.length - 2; index >= 0; index--) { merged = mergeFormMetadata(merged, parseFormMetadataXml(documents[index].xml, documentForm(documents[index]))); } const current = documents[0]; merged.form = compactForm({ ...merged.form, fid: current.fid ?? merged.form.fid, formId: current.formId ?? merged.form.formId, name: current.name ?? merged.form.name, subsystem: current.subsystem ?? merged.form.subsystem, baseObjectId: current.baseObjectId ?? merged.form.baseObjectId, }); merged.inheritance = documents.map((doc) => ({ fid: doc.fid, formId: doc.formId, name: doc.name, baseObjectId: doc.baseObjectId, source: doc.source, })); resolveFieldTables(merged); deduplicateFields(merged); addConcreteWarnings(merged); return merged; } export function parseFormMetadataInput(text: string, fallback: FormMetadataForm = {}): FormMetadata { const extracted = extractDocuments(text, fallback); const metadata = extracted.documents.length > 0 ? resolveFormMetadataInheritance(extracted.documents) : parseFormMetadataXml(text, fallback); metadata.warnings.push(...extracted.warnings); return metadata; } export function parseFormMetadataFile(path: string, fallback: FormMetadataForm = {}): FormMetadata { const content = readFileSync(path, "utf8"); const metadata = parseFormMetadataInput(content, { ...fallback }); if (!metadata.inheritance.some((item) => item.source)) { metadata.inheritance = metadata.inheritance.map((item, index) => ({ ...item, source: index === 0 ? basename(path) : item.source, })); } return metadata; } export function formatFormMetadataMarkdown(metadata: FormMetadata): string { const formId = metadata.form.formId ?? metadata.form.fid ?? "未提供"; const displayFields = metadata.fields.filter(fieldHasContent); const hiddenSkeletonFields = metadata.fields.length - displayFields.length; const lines = [ "# 金蝶元数据解析证据", "", "## 表单", "", `- FormId/单据或表单标识:${formId}`, `- FID:${metadata.form.fid ?? "未提供"}`, `- 名称:${metadata.form.name ?? "未提供"}`, `- 子系统:${metadata.form.subsystem ?? "未提供"}`, `- 父对象 FBASEOBJECTID:${metadata.form.baseObjectId ?? "无"}`, `- 解析时间:${new Date().toISOString()}`, "", "## 继承链", "", "| 顺序 | FID/FormId | 名称 | 父对象 | 来源 |", "| :--- | :--- | :--- | :--- | :--- |", ...metadata.inheritance.map((item, index) => `| ${index + 1} | \`${item.formId ?? item.fid ?? "-"}\` | ${item.name ?? "-"} | \`${item.baseObjectId ?? "-"}\` | ${item.source ?? "-"} |`), "", "## 实体和数据库表", "", "| 实体标识 | EntryName | 类型 | 表名 | 名称 |", "| :--- | :--- | :--- | :--- | :--- |", ...metadata.entities.map((entity) => `| \`${entity.key || entity.entryName || "-"}\` | \`${entity.entryName || "-"}\` | ${entity.typeName} | \`${entity.tableName || "-"}\` | ${entity.name || "-"} |`), "", "## 字段和数据库列", "", "| 字段名称 | 字段标识 | 类型 | 实体/分录标识 | 表名 | 数据库字段名 |", "| :--- | :--- | :--- | :--- | :--- | :--- |", ...displayFields.map((field) => `| ${field.name || "-"} | \`${field.key || "-"}\` | ${field.typeLabel || field.typeName} | \`${field.entityKey || "-"}\` | \`${field.tableName || "-"}\` | \`${field.fieldName || "-"}\` |`), ]; if (metadata.plugins.length > 0) { lines.push( "", "## 插件引用", "", "| 名称 | 标识 | 类名 | 程序集 | 服务/事件 |", "| :--- | :--- | :--- | :--- | :--- |", ...metadata.plugins.map((plugin) => `| ${plugin.name || "-"} | \`${plugin.key || "-"}\` | \`${plugin.className || "-"}\` | \`${plugin.assemblyName || "-"}\` | ${plugin.serviceType || plugin.eventName || "-"} |`), ); } lines.push( "", "## 解析结论", "", `- 实体数量:${metadata.entities.length}`, `- 字段数量:${displayFields.length}`, `- 插件引用数量:${metadata.plugins.length}`, "- 数据源证据来源:企业版 T_META_OBJECTTYPE.FKERNELXML、苍穹/星瀚/旗舰版 t_meta_entitydesign.fdata 或等价官方元数据解析结果;数据库事实必须以当前业务库查询结果为准。", ); if (hiddenSkeletonFields > 0) lines.splice(lines.length - 2, 0, `- 已隐藏继承/重置骨架字段:${hiddenSkeletonFields}`); lines.push( "", "## 使用规则", "", "- 表单插件、操作插件、DynamicObject 和单据模型读写使用 FormId、实体/分录标识和字段标识。", "- SQL、KSQL、报表、存储过程和直接查库使用数据库表名和数据库字段名。", "- 需求未明确数据访问方式时,必须先确认是模型 API 还是 SQL/直接查库;禁止把字段标识当数据库列名,或把数据库列名当字段标识。", ); if (metadata.warnings.length > 0) { lines.push("", "## 警告", "", ...[...new Set(metadata.warnings)].map((warning) => `- ${warning}`)); } return `${lines.join("\n")}\n`; } export function formatFormMetadataJson(metadata: FormMetadata): string { return `${JSON.stringify(metadata, null, 2)}\n`; } function mergeFormMetadata(parent: FormMetadata, child: FormMetadata): FormMetadata { const merged: FormMetadata = { form: { ...parent.form, ...compactForm(child.form) }, entities: [...parent.entities], fields: [...parent.fields], plugins: [...parent.plugins], inheritance: [...parent.inheritance, ...child.inheritance], warnings: [...parent.warnings, ...child.warnings], }; for (const entity of child.entities) { const pos = merged.entities.findIndex((existing) => sameMetadataIdentity(existing.key, entity.key) || sameMetadataIdentity(existing.oid, entity.oid)); if (pos >= 0) merged.entities[pos] = mergeObject(merged.entities[pos], entity); else merged.entities.push(entity); } for (const field of child.fields) { const pos = merged.fields.findIndex((existing) => sameMetadataIdentity(existing.key, field.key) || sameMetadataIdentity(existing.oid, field.oid)); if (pos >= 0) merged.fields[pos] = mergeObject(merged.fields[pos], field); else if (fieldHasContent(field)) merged.fields.push(field); } for (const plugin of child.plugins) { const pos = merged.plugins.findIndex((existing) => sameMetadataIdentity(existing.key, plugin.key) || sameMetadataIdentity(existing.className, plugin.className)); if (pos >= 0) merged.plugins[pos] = mergeObject(merged.plugins[pos], plugin); else merged.plugins.push(plugin); } return merged; } function extractDocuments(text: string, fallback: FormMetadataForm): ExtractedDocuments { const trimmed = text.trim(); if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return { documents: [], warnings: [] }; try { const parsed = JSON.parse(trimmed) as unknown; const rows = collectRows(parsed); const documents = rows.flatMap((row, index): FormMetadataDocument[] => { const xml = rowString(row, ["FKERNELXML", "fkernelxml", "kernelXml", "FDATA", "fdata", "xml"]); if (!xml) return []; return [ { xml, fid: rowString(row, ["FID", "fid", "FFormId", "formId"]) ?? fallback.fid, formId: rowString(row, ["FORMID", "FormId", "formId", "FNUMBER", "fnumber", "number"]) ?? fallback.formId, name: rowString(row, ["FNAME", "fname", "name"]) ?? fallback.name, subsystem: rowString(row, ["FSUBSYSTEM", "subsystem"]) ?? fallback.subsystem, baseObjectId: rowString(row, ["FBASEOBJECTID", "fbaseobjectid", "FPARENTID", "fparentid", "baseObjectId", "parentId"]) ?? fallback.baseObjectId, source: `json-row-${index + 1}`, }, ]; }); const warnings = rows.length > 0 && documents.length === 0 ? ["JSON 输入中未找到 FKERNELXML/fdata/xml 字段。"] : []; return { documents, warnings }; } catch { return { documents: [], warnings: ["输入看起来像 JSON,但无法解析;已按原始 XML 文本处理。"] }; } } function collectRows(value: unknown): Record[] { if (Array.isArray(value)) return value.filter(isObject); if (!isObject(value)) return []; const directRows = ["rows", "recordset", "recordsets", "data", "result", "results"].flatMap((key) => collectRows(value[key])); if (directRows.length > 0) return directRows; return Object.values(value).some((item) => typeof item === "string" && / 0) { const frame = stack.pop(); if (!frame) return; if (frame.target && frame.tag === tag) { if (frame.kind === "entity" && entityIsValid(frame.target as FormMetadataEntity)) metadata.entities.push(frame.target as FormMetadataEntity); if (frame.kind === "field" && fieldIsValid(frame.target as FormMetadataField)) metadata.fields.push(frame.target as FormMetadataField); if (frame.kind === "plugin" && pluginIsValid(frame.target as FormMetadataPlugin)) metadata.plugins.push(frame.target as FormMetadataPlugin); } if (frame.tag === tag) return; } } function applyEntityValue(entity: FormMetadataEntity, tag: string, value: string): void { if (tag === "Id" && !entity.oid) entity.oid = value; else if (tag === "Key") entity.key = value; else if (tag === "Name") entity.name = value; else if (tag === "EntryName") entity.entryName = value; else if (tag === "TableName") entity.tableName = value; } function applyFieldValue(field: FormMetadataField, tag: string, value: string): void { if (tag === "Id" && !field.oid) field.oid = value; else if (tag === "Key") field.key = value; else if (tag === "Name") field.name = value; else if (tag === "FieldName") field.fieldName = value; else if (tag === "PropertyName") field.propertyName = value; else if (tag === "EntityKey") field.entityKey = value; else if (tag === "ParentId") field.parentId = value; else if (tag === "Suffix") field.subTableFlag = value; else if (tag === "RefType" || tag === "BaseObject" || tag === "BaseDataType") field.refType = value; } function applyPluginValue(plugin: FormMetadataPlugin, tag: string, value: string): void { if (tag === "Key") plugin.key = value; else if (tag === "Name") plugin.name = value; else if (tag === "ClassName") plugin.className = value; else if (tag === "AssemblyName") plugin.assemblyName = value; else if (tag === "ServiceType" || tag === "PlugInType") plugin.serviceType = value; else if (tag === "EventName" || tag === "Event") plugin.eventName = value; } function resolveFieldTables(metadata: FormMetadata): void { const entityLookup = new Map(); for (const entity of metadata.entities) { for (const key of entityLookupKeys(entity)) entityLookup.set(key.toUpperCase(), entity); } const defaultEntity = metadata.entities[0]; for (const field of metadata.fields) { if (!field.name) field.name = field.typeLabel || field.typeName; if (!field.entityKey && field.parentId) field.entityKey = field.parentId; if (!field.entityKey && defaultEntity) field.entityKey = defaultEntity.key || defaultEntity.entryName || defaultEntity.oid; const entity = entityLookup.get(field.entityKey.toUpperCase()); if (entity) { if (!field.tableName) field.tableName = entity.tableName; field.entityKey = entity.key || entity.entryName || entity.oid || field.entityKey; } else if (!field.tableName && defaultEntity?.tableName) { field.tableName = defaultEntity.tableName; } field.tableName = tableNameWithSuffix(field.tableName, field.subTableFlag); } } function addConcreteWarnings(metadata: FormMetadata): void { if (metadata.entities.length === 0) metadata.warnings.push("未解析到实体/分录定义;请确认 FKERNELXML 是否完整,或是否需要加载继承链父对象。"); if (metadata.fields.length === 0) metadata.warnings.push("未解析到字段定义;请确认 XML 是否包含 BusinessInfo/Elements。"); if (metadata.fields.some((field) => !field.fieldName)) metadata.warnings.push("部分字段缺少数据库字段名 FieldName,SQL/KSQL 编码前必须补齐。"); if (metadata.fields.some((field) => !field.tableName)) metadata.warnings.push("部分字段未能解析所属表名,SQL/KSQL 编码前必须补齐实体表映射。"); } function deduplicateFields(metadata: FormMetadata): void { const best = new Map(); const passthrough: FormMetadataField[] = []; for (const field of metadata.fields) { const identity = fieldIdentity(field); if (!identity) { passthrough.push(field); continue; } const existing = best.get(identity); if (!existing || fieldScore(field) > fieldScore(existing)) best.set(identity, field); } metadata.fields = [...passthrough, ...best.values()]; } function fieldIdentity(field: FormMetadataField): string | undefined { if (!field.key) return undefined; return `${field.entityKey.toLowerCase()}::${field.key.toLowerCase()}`; } function fieldScore(field: FormMetadataField): number { return (field.fieldName ? 8 : 0) + (field.tableName ? 4 : 0) + (field.propertyName ? 2 : 0) + (field.name ? 1 : 0); } function entityLookupKeys(entity: FormMetadataEntity): string[] { const keys = [entity.key, entity.entryName, entity.oid].filter(Boolean); if (entity.entryName && !entity.entryName.startsWith("F")) keys.push(`F${entity.entryName}`); return keys; } function tableNameWithSuffix(tableName: string, suffix: string): string { const cleanSuffix = suffix.trim(); if (!tableName || !cleanSuffix) return tableName; const tableIsLower = tableName === tableName.toLowerCase(); const casedSuffix = tableIsLower ? cleanSuffix.toLowerCase() : cleanSuffix.toUpperCase(); const normalizedSuffix = casedSuffix.startsWith("_") ? casedSuffix : `_${casedSuffix}`; return tableName.toUpperCase().endsWith(normalizedSuffix.toUpperCase()) ? tableName : `${tableName}${normalizedSuffix}`; } function parseAttributes(raw: string): Record { const attrs: Record = {}; for (const match of raw.matchAll(/([A-Za-z_][\w:.-]*)\s*=\s*("([^"]*)"|'([^']*)')/g)) { attrs[localName(match[1])] = decodeXml(match[3] ?? match[4] ?? ""); } return attrs; } function nearestTarget(stack: XmlFrame[]): XmlFrame | undefined { for (let index = stack.length - 1; index >= 0; index--) { if (stack[index].target) return stack[index]; } return undefined; } function isEntityTag(tag: string): boolean { return ENTITY_TAGS.has(tag); } function isFieldTag(tag: string): boolean { return Boolean(FIELD_TYPE_LABELS[tag]) || (/^[A-Za-z][A-Za-z0-9_]*Field$/.test(tag) && tag !== "KeyField"); } function fieldTypeLabel(typeName: string): string { return FIELD_TYPE_LABELS[typeName] ?? typeName; } function entityIsValid(entity: FormMetadataEntity): boolean { return Boolean(entity.key || entity.entryName || entity.tableName || entity.oid); } function fieldIsValid(field: FormMetadataField): boolean { return Boolean(field.key || field.fieldName || field.propertyName || field.oid); } function pluginIsValid(plugin: FormMetadataPlugin): boolean { return Boolean(plugin.key || plugin.className || plugin.assemblyName); } function fieldHasContent(field: FormMetadataField): boolean { return Boolean(field.key || field.fieldName || field.propertyName); } function sameMetadataIdentity(left: string | undefined, right: string | undefined): boolean { return Boolean(left && right && left.toLowerCase() === right.toLowerCase()); } function mergeObject(parent: T, child: T): T { const merged = { ...parent }; for (const key of Object.keys(child) as Array) { const value = child[key]; if (typeof value === "string" && value) merged[key] = value; } return merged; } function documentForm(document: FormMetadataDocument): FormMetadataForm { return { fid: document.fid, formId: document.formId, name: document.name, subsystem: document.subsystem, baseObjectId: document.baseObjectId, }; } function compactForm(form: FormMetadataForm): FormMetadataForm { return { fid: clean(form.fid), formId: clean(form.formId), name: clean(form.name), subsystem: clean(form.subsystem), baseObjectId: clean(form.baseObjectId), }; } function clean(value: string | undefined): string | undefined { const trimmed = value?.trim(); return trimmed || undefined; } function rowString(row: Record, keys: string[]): string | undefined { for (const key of keys) { const direct = row[key]; if (typeof direct === "string" && direct.trim()) return direct.trim(); } const found = Object.entries(row).find(([key, value]) => keys.some((candidate) => candidate.toLowerCase() === key.toLowerCase()) && typeof value === "string" && value.trim()); return typeof found?.[1] === "string" ? found[1].trim() : undefined; } function isObject(value: unknown): value is Record { return Boolean(value && typeof value === "object" && !Array.isArray(value)); } function localName(tag: string): string { return tag.includes(":") ? tag.split(":").at(-1) ?? tag : tag; } function decodeXml(value: string): string { return value .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, '"') .replace(/'/g, "'") .replace(/&/g, "&"); } export function formMetadataInputExists(path: string): boolean { return existsSync(path); }