Files
leaudit-platform-frontend/app/utils/rules-yaml-mock.server.ts
T
2026-05-07 18:31:25 +08:00

697 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFile } from 'node:fs/promises';
import YAML from 'yaml';
const LEAUDIT_RULES_ROOT = `${process.cwd()}/mock-data/leaudit-rules/packs/yc`;
export type RulePackScope = {
documentType: string;
moduleType: string;
mainType: string;
subtype: string;
businessType?: string;
ruleTypeCode?: string;
};
export type RuleSummary = {
id: string;
ruleId: string;
name: string;
group: string;
risk: string;
score: string;
type: string;
checkTypes: string[];
logic: string;
subRules: Array<{
id: string;
check: string;
content: string;
}>;
subRuleIds: string[];
scope: string[];
dependencies: string[];
stageCount: number;
appliesIn: string[];
prompt: string;
description: string;
};
export type ExtractFieldSummary = {
id: string;
group: string;
name: string;
type: string;
multipleEntities: boolean;
allowed?: string[];
requiredFrom: string;
description: string;
};
export type SubDocumentSummary = {
id: string;
name: string;
required: string;
fieldCount: number;
groups: string[];
description: string;
fields: ExtractFieldSummary[];
};
export type RuleYamlPack = RulePackScope & {
id: string;
yamlPath: string | null;
yamlSource: string;
sourceStatus: 'ready' | 'empty' | 'missing';
currentVersionId?: number | null;
fallbackVersionId?: number | null;
resolvedVersionId?: number | null;
metadata: {
typeId: string;
name: string;
version: string;
lastUpdated: string;
parent: string;
description: string;
tags: string[];
keywords: string[];
inheritsFrom: string[];
};
stats: {
ruleCount: number;
fieldCount: number;
subDocumentCount: number;
visualElementCount: number;
};
rules: RuleSummary[];
fields: ExtractFieldSummary[];
subDocuments: SubDocumentSummary[];
visualElements: Array<{
id: string;
name: string;
type: string;
required: string;
requiredFrom?: string;
signerRoles?: string[];
signatureTypes?: string[];
privateSealRestricted?: boolean;
expectedMatchField?: string;
expectedMatchAlternatives?: string[];
prompt?: string;
}>;
};
export const EMPTY_RULE_YAML = `metadata:
type_id: pending.internal.document
name: 内部公文规则配置
version: '0.1'
last_updated: '待配置'
description: '当前暂无内部公文规则 YAML。此测试页面保留规则列表与配置页流程。'
extract: []
rules: []
`;
const MOCK_RULE_PACKS: Array<RulePackScope & { id: string; yamlPath: string | null }> = [
{ id: 'contract-purchase', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '买卖合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_purchase/rules.yaml` },
{ id: 'contract-sale', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '通用买卖合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_sale/rules.yaml` },
{ id: 'contract-tech', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '技术合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_tech/rules.yaml` },
{ id: 'contract-lease', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '租赁合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_lease/rules.yaml` },
{ id: 'contract-entrust', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '委托合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_entrust/rules.yaml` },
{ id: 'contract-construction', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '建设工程合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_construction/rules.yaml` },
{ id: 'contract-evaluation', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '委托评估合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_evaluation/rules.yaml` },
{ id: 'contract-gift-general', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '赠与合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_gift_general/rules.yaml` },
{ id: 'contract-gift-charity', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '公益捐赠合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_gift_charity/rules.yaml` },
{ id: 'contract-loan', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '借款合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_loan/rules.yaml` },
{ id: 'case-penalty', documentType: '案卷', moduleType: '案卷评查', mainType: '行政处罚', subtype: '通用', yamlPath: `${LEAUDIT_RULES_ROOT}/行政处罚/rules.yaml` },
{ id: 'case-license-new', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '新办', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_新办/rules.yaml` },
{ id: 'case-license-extend', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '延续', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_延续/rules.yaml` },
{ id: 'case-license-change', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '变更', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_变更/rules.yaml` },
{ id: 'case-license-cancel', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '注销', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_注销/rules.yaml` },
{ id: 'case-license-suspend', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '停业', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_停业/rules.yaml` },
{ id: 'case-license-close', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '歇业', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_歇业/rules.yaml` },
{ id: 'case-license-reissue', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '补办', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_补办/rules.yaml` },
{ id: 'case-license-retrieve', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '收回', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_收回/rules.yaml` },
{ id: 'case-license-restore', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '恢复营业', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_恢复营业/rules.yaml` },
{ id: 'internal-document', documentType: '内部公文', moduleType: '内部公文评查', mainType: '内部公文', subtype: '通用', yamlPath: null }
];
function getTopLevelSection(source: string, key: string): string {
const lines = source.split('\n');
const start = lines.findIndex(line => line === `${key}:`);
if (start === -1) {
return '';
}
const end = lines.findIndex((line, index) => index > start && /^[a-zA-Z_][\w-]*:/.test(line));
return lines.slice(start + 1, end === -1 ? undefined : end).join('\n');
}
function stripYamlValue(value = ''): string {
return value.trim().replace(/^['"]|['"]$/g, '').replace(/\u0000/g, '');
}
function toStringList(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
return value.map((item) => String(item || '').trim()).filter(Boolean);
}
function parseScalar(section: string, key: string): string {
const match = section.match(new RegExp(`^\\s{2}${key}:\\s*(.*)$`, 'm'));
return stripYamlValue(match?.[1] || '');
}
function parseListAfterKey(section: string, key: string): string[] {
const lines = section.split('\n');
const start = lines.findIndex(line => new RegExp(`^\\s{2}${key}:\\s*$`).test(line));
if (start === -1) {
return [];
}
const values: string[] = [];
for (let index = start + 1; index < lines.length; index += 1) {
const line = lines[index];
if (/^\s{2}\w/.test(line)) {
break;
}
const match = line.match(/^\s{2,}-\s*(.+)$/);
if (match) {
values.push(stripYamlValue(match[1]));
}
}
return values;
}
function parseMetadata(source: string): RuleYamlPack['metadata'] {
const section = getTopLevelSection(source, 'metadata');
return {
typeId: parseScalar(section, 'type_id'),
name: parseScalar(section, 'name'),
version: parseScalar(section, 'version'),
lastUpdated: parseScalar(section, 'last_updated'),
parent: parseScalar(section, 'parent'),
description: stripYamlValue((section.match(/^\s{2}description:\s*'?([\s\S]*?)(?:\n\s{2}\w|\n[a-zA-Z_]|\n?$)/m)?.[1] || '').replace(/\n\s+/g, ' ').trim()),
tags: parseListAfterKey(section, 'tags'),
keywords: parseListAfterKey(section, 'classification_keywords'),
inheritsFrom: parseListAfterKey(section, 'inherits_from')
};
}
function splitBlocks(section: string, marker: RegExp): string[] {
const lines = section.split('\n');
const starts = lines.reduce<number[]>((indexes, line, index) => {
if (marker.test(line)) {
indexes.push(index);
}
return indexes;
}, []);
return starts.map((start, index) => lines.slice(start, starts[index + 1]).join('\n'));
}
function parseRules(source: string): RuleSummary[] {
const section = getTopLevelSection(source, 'rules');
const groups = splitBlocks(section, /^\s*-\s+group:\s*/);
const readExplicitDependencies = (block: string): string[] => {
const lines = block.split('\n');
const start = lines.findIndex(line => /^\s+dependencies:\s*$/.test(line));
if (start === -1) {
return [];
}
const dependencies: string[] = [];
for (let index = start + 1; index < lines.length; index += 1) {
const line = lines[index];
if (/^\s+[a-zA-Z_][^:]*:\s*/.test(line) && !/^\s*-\s+/.test(line)) {
break;
}
const match = line.match(/^\s*-\s+(.+)$/);
if (match) {
dependencies.push(stripYamlValue(match[1]));
}
}
return dependencies;
};
const normalizeDependency = (value: string) => {
const normalized = stripYamlValue(value);
if (normalized === 'cross_page_seal') return '骑缝章';
if (normalized === 'seal') return '印章';
if (normalized === 'signature') return '签名';
return normalized;
};
const readPrompts = (block: string): string[] => {
const lines = block.split('\n');
const prompts: string[] = [];
for (let index = 0; index < lines.length; index += 1) {
const match = lines[index].match(/^(\s*)prompt:\s*(.*)$/);
if (!match) continue;
const indent = match[1].length;
const parts = [match[2]];
for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex += 1) {
const line = lines[nextIndex];
const nextIndent = line.match(/^\s*/)?.[0].length || 0;
const trimmed = line.trim();
if (trimmed && nextIndent <= indent) break;
if (trimmed && nextIndent === indent + 2 && /^[a-zA-Z_][\w-]*:\s*/.test(trimmed)) break;
parts.push(line);
}
prompts.push(parts.join('\n')
.replace(/^['"]/, '')
.replace(/['"]\s*$/, '')
.split('\n')
.map(line => line.replace(/^\s{8}/, ''))
.join('\n')
.trim());
}
return prompts.filter(Boolean);
};
const readPromptDependencies = (prompts: string[]): string[] => {
return Array.from(new Set(
prompts.flatMap((prompt) => Array.from(prompt.matchAll(/\{\{\s*([^{}]+?)\s*\}\}/g)).map((match) => stripYamlValue(match[1])))
));
};
const readList = (block: string, key: string, indent = 4): string[] => {
const lines = block.split('\n');
const start = lines.findIndex(line => new RegExp(`^\\s+${key}:\\s*$`).test(line));
if (start === -1) {
return [];
}
const baseIndent = lines[start].match(/^\s*/)?.[0].length || indent;
const values: string[] = [];
for (let index = start + 1; index < lines.length; index += 1) {
const line = lines[index];
const lineIndent = line.match(/^\s*/)?.[0].length || 0;
if (line.trim() && lineIndent <= baseIndent && !/^\s*-\s+/.test(line)) {
break;
}
const match = line.match(/^\s*-\s+(.+)$/);
if (match) {
values.push(stripYamlValue(match[1]));
}
}
return values;
};
const readFlexibleList = (block: string, key: string): string[] => {
const lines = block.split('\n');
const start = lines.findIndex(line => new RegExp(`^(\\s*)${key}:\\s*$`).test(line));
if (start === -1) return [];
const indent = lines[start].match(/^\s*/)?.[0].length || 0;
const values: string[] = [];
for (let index = start + 1; index < lines.length; index += 1) {
const line = lines[index];
const lineIndent = line.match(/^\s*/)?.[0].length || 0;
const match = line.match(/^\s*-\s+(.+)$/);
if (match) {
values.push(stripYamlValue(match[1]));
continue;
}
if (line.trim() && lineIndent <= indent && !/^\s*-\s+/.test(line)) break;
}
return values;
};
const readStageList = (block: string, key: string): string[] => {
const lines = block.split('\n');
const start = lines.findIndex(line => new RegExp(`^\\s+${key}:\\s*$`).test(line));
if (start === -1) {
return [];
}
const baseIndent = lines[start].match(/^\s*/)?.[0].length || 6;
const values: string[] = [];
for (let index = start + 1; index < lines.length; index += 1) {
const line = lines[index];
const lineIndent = line.match(/^\s*/)?.[0].length || 0;
if (line.trim() && lineIndent <= baseIndent && !/^\s*-\s+/.test(line)) {
break;
}
const match = line.match(/^\s*-\s+(.+)$/);
if (match) {
values.push(stripYamlValue(match[1]));
}
}
return values;
};
const readStageScalar = (block: string, key: string): string => stripYamlValue(block.match(new RegExp(`^\\s+${key}:\\s*(.+)$`, 'm'))?.[1] || '');
const readStagePairValues = (block: string): string[] => {
const lines = block.split('\n');
const values: string[] = [];
for (let index = 0; index < lines.length; index += 1) {
const match = lines[index].match(/^\s+(?:source|target|ref_field):\s*(.+)$/);
if (match) {
values.push(stripYamlValue(match[1]));
}
}
return values;
};
const summarizeStage = (stageBlock: string): string => {
const fields = readStageList(stageBlock, 'fields');
const field = readStageScalar(stageBlock, 'field');
const left = readStageScalar(stageBlock, 'left') || readStageScalar(stageBlock, 'left_field');
const op = readStageScalar(stageBlock, 'op');
const right = readStageScalar(stageBlock, 'right') || readStageScalar(stageBlock, 'right_field');
const value = readStageScalar(stageBlock, 'value');
const prompt = readStageScalar(stageBlock, 'prompt');
const element = readStageScalar(stageBlock, 'element') || readStageScalar(stageBlock, 'seal_id') || readStageScalar(stageBlock, 'signature_id');
if (fields.length > 0) return fields.join('、');
if (left || right) return [left, op, right].filter(Boolean).join(' ');
if (field && value) return `${field} = ${value}`;
if (field) return field;
if (element) return element;
if (prompt) return prompt.slice(0, 80);
return stageBlock.split('\n').map(line => line.trim()).filter(Boolean).slice(1, 4).join('') || '未配置内容';
};
const readSubRules = (block: string) => splitBlocks(block, /^\s{4}-\s+id:\s*/).map(stageBlock => {
const id = stripYamlValue(stageBlock.match(/^\s*-\s+id:\s*(.+)$/m)?.[1] || '');
const check = readStageScalar(stageBlock, 'check') || readStageScalar(stageBlock, 'type') || '-';
return {
id,
check,
content: summarizeStage(stageBlock)
};
}).filter(stage => stage.id);
return groups.flatMap(groupBlock => {
const group = stripYamlValue(groupBlock.match(/^\s*-\s+group:\s*(.+)$/m)?.[1] || '未分组');
return splitBlocks(groupBlock, /^\s*-\s+rule_id:\s*/).map(ruleBlock => {
const ruleId = stripYamlValue(ruleBlock.match(/^\s*-\s+rule_id:\s*(.+)$/m)?.[1] || '');
const name = stripYamlValue(ruleBlock.match(/^\s+name:\s*(.+)$/m)?.[1] || '未命名规则');
const checkTypes = Array.from(new Set(Array.from(ruleBlock.matchAll(/^\s+(?:check|type):\s*(.+)$/gm)).map(match => stripYamlValue(match[1]))));
const stageDependencies = Array.from(ruleBlock.matchAll(/^\s+(?:field|number|chinese|left|right|left_field|right_field|target|element|seal_id|signature_id|ref_field):\s*(.+)$/gm))
.map(match => normalizeDependency(match[1]));
const stageFieldDependencies = splitBlocks(ruleBlock, /^\s{4,}-\s+id:\s*/)
.flatMap(stageBlock => [
...readStageList(stageBlock, 'fields'),
...readStagePairValues(stageBlock),
])
.map(normalizeDependency);
const prompts = readPrompts(ruleBlock);
const promptDependencies = readPromptDependencies(prompts).map(normalizeDependency);
const dependencies = Array.from(new Set([
...readExplicitDependencies(ruleBlock),
...stageDependencies,
...stageFieldDependencies,
...promptDependencies,
]));
const scope = Array.from(new Set(Array.from(ruleBlock.matchAll(/^\s{4,}-\s*([^:\n]+)$/gm)).map(match => stripYamlValue(match[1])).filter(value => !/^\d+$/.test(value))));
const subRules = readSubRules(ruleBlock);
return {
id: ruleId || `${group}-${name}`,
ruleId,
name,
group,
risk: stripYamlValue(ruleBlock.match(/^\s+risk:\s*(.+)$/m)?.[1] || 'medium'),
score: stripYamlValue(ruleBlock.match(/^\s+score:\s*(.+)$/m)?.[1] || '-'),
type: stripYamlValue(ruleBlock.match(/^\s+type:\s*(.+)$/m)?.[1] || 'deterministic'),
checkTypes,
logic: stripYamlValue(ruleBlock.match(/^\s+logic:\s*(.+)$/m)?.[1] || ''),
subRules,
subRuleIds: readList(ruleBlock, 'rules'),
scope: scope.slice(0, 8),
dependencies,
stageCount: subRules.length,
appliesIn: readFlexibleList(ruleBlock, 'applies_in'),
prompt: prompts.join('\n\n'),
description: stripYamlValue(ruleBlock.match(/^\s+desc:\s*(.+)$/m)?.[1] || '')
};
});
});
}
export function parseRuleSummariesFromYaml(source: string): RuleSummary[] {
return parseRules(source);
}
function parseTopLevelFields(source: string): ExtractFieldSummary[] {
const parsed = YAML.parse(source || '') as Record<string, unknown> | null;
const extractGroups = Array.isArray(parsed?.extract) ? parsed.extract : [];
const extractedFields = extractGroups.flatMap((groupNode) => {
if (!groupNode || typeof groupNode !== 'object') {
return [];
}
const groupObject = groupNode as Record<string, unknown>;
const group = String(groupObject.group || '未分组').trim() || '未分组';
const fields = Array.isArray(groupObject.fields) ? groupObject.fields : [];
return fields.flatMap((fieldNode) => {
if (!fieldNode || typeof fieldNode !== 'object') {
return [];
}
const field = fieldNode as Record<string, unknown>;
const name = String(field.name || '').trim();
if (!name) {
return [];
}
const rawType = String(field.type || '-').trim();
const requiredFrom = String(field.required_from || '-').trim() || '-';
const parentField: ExtractFieldSummary = {
id: `${group}-${name}`,
group,
name,
type: rawType === 'multi_entity' ? 'verbatim' : rawType,
multipleEntities: rawType === 'multi_entity',
allowed: toStringList(field.allowed),
requiredFrom,
description: String(field.desc || '').trim(),
};
const childFields = Array.isArray(field.fields) ? field.fields : [];
const normalizedChildFields = childFields.flatMap((childNode) => {
if (!childNode || typeof childNode !== 'object') {
return [];
}
const childField = childNode as Record<string, unknown>;
const childName = String(childField.name || '').trim();
if (!childName) {
return [];
}
return [{
id: `${group}-${name}-${childName}`,
group,
name: `${name}[*].${childName}`,
type: String(childField.type || 'verbatim').trim() || 'verbatim',
multipleEntities: false,
allowed: toStringList(childField.allowed),
requiredFrom: String(childField.required_from || requiredFrom).trim() || requiredFrom,
description: String(childField.desc || `${name}的子字段`).trim(),
}];
});
return [parentField, ...normalizedChildFields];
});
}).filter(field => field.name);
const derivedSection = getTopLevelSection(source, 'derived_fields');
const derivedFields = splitBlocks(derivedSection, /^\s*-\s+name:\s*/).map(fieldBlock => {
const name = stripYamlValue(fieldBlock.match(/^\s*-\s+name:\s*(.+)$/m)?.[1] || '');
return {
id: `derived-${name}`,
group: '派生字段',
name,
type: stripYamlValue(fieldBlock.match(/^\s+type:\s*(.+)$/m)?.[1] || 'computed'),
multipleEntities: false,
requiredFrom: '-',
description: stripYamlValue(fieldBlock.match(/^\s+compute:\s*(.+)$/m)?.[1] || '由其他字段计算得出')
};
}).filter(field => field.name);
return [...extractedFields, ...derivedFields];
}
function parseSubDocuments(source: string): SubDocumentSummary[] {
const parsed = YAML.parse(source || '') as Record<string, unknown> | null;
const subDocuments = parsed?.sub_documents;
if (!Array.isArray(subDocuments)) {
return [];
}
return subDocuments.flatMap((documentNode) => {
if (!documentNode || typeof documentNode !== 'object') {
return [];
}
const document = documentNode as Record<string, unknown>;
const id = String(document.id || '').trim();
if (!id) {
return [];
}
const extractGroups = Array.isArray(document.extract) ? document.extract : [];
const fields = extractGroups.flatMap((groupNode) => {
if (!groupNode || typeof groupNode !== 'object') {
return [];
}
const groupObject = groupNode as Record<string, unknown>;
const group = String(groupObject.group || '未分组').trim() || '未分组';
const groupFields = Array.isArray(groupObject.fields) ? groupObject.fields : [];
return groupFields.flatMap((fieldNode) => {
if (!fieldNode || typeof fieldNode !== 'object') {
return [];
}
const field = fieldNode as Record<string, unknown>;
const name = String(field.name || '').trim();
if (!name) {
return [];
}
const rawType = String(field.type || '-').trim();
return [{
id: `${id}-${group}-${name}`,
group,
name,
type: rawType === 'multi_entity' ? 'verbatim' : rawType,
multipleEntities: rawType === 'multi_entity',
allowed: toStringList(field.allowed),
requiredFrom: '-',
description: String(field.desc || '').trim(),
}];
});
});
const groups = Array.from(new Set(fields.map((field) => field.group).filter(Boolean)));
const classifier = document.classifier && typeof document.classifier === 'object'
? (document.classifier as Record<string, unknown>)
: null;
const description = Array.isArray(classifier?.keywords)
? classifier!.keywords.map((item) => String(item || '').trim()).filter(Boolean).slice(0, 3).join('、')
: '';
return [{
id,
name: String(document.name || id).trim(),
required: String(document.required ?? '-').trim(),
fieldCount: fields.length,
groups,
description,
fields,
}];
});
}
function parseVisualElements(source: string): RuleYamlPack['visualElements'] {
const parsed = YAML.parse(source || '') as Record<string, unknown> | null;
const visualRoot = parsed?.visual_elements;
if (!visualRoot || typeof visualRoot !== 'object') {
return [];
}
const typedSections = [
{ key: 'seals', label: '签章' },
{ key: 'signatures', label: '签名' },
{ key: 'cross_page_seals', label: '骑缝章' },
] as const;
return typedSections.flatMap(({ key, label }) => {
const bucket = (visualRoot as Record<string, unknown>)[key];
if (!Array.isArray(bucket)) {
return [];
}
return bucket.flatMap((item) => {
if (!item || typeof item !== 'object') {
return [];
}
const node = item as Record<string, unknown>;
const id = String(node.id || '').trim();
if (!id) {
return [];
}
const toStringList = (value: unknown): string[] => (
Array.isArray(value)
? value.map((entry) => String(entry || '').trim()).filter(Boolean)
: []
);
const expectedMatch = node.expected_text_match && typeof node.expected_text_match === 'object'
? (node.expected_text_match as Record<string, unknown>)
: null;
const signatureTypes = key === 'seals'
? toStringList(node.allowed_types)
: toStringList(node.signature_types);
return [{
id,
name: String(node.name || id).trim(),
type: label,
required: String(node.required ?? '-').trim(),
requiredFrom: String(node.required_from ?? '').trim(),
signerRoles: key === 'signatures' ? toStringList(node.signer_roles) : [],
signatureTypes,
privateSealRestricted: key === 'signatures' ? Boolean(node.private_seal_restricted) : false,
expectedMatchField: String(expectedMatch?.field || '').trim(),
expectedMatchAlternatives: toStringList(expectedMatch?.alternatives),
prompt: String(node.prompt || '').trim(),
}];
});
});
}
export function buildRuleYamlPack(
config: RulePackScope & {
id: string;
yamlPath: string | null;
currentVersionId?: number | null;
fallbackVersionId?: number | null;
resolvedVersionId?: number | null;
},
yamlSource: string,
sourceStatus: RuleYamlPack['sourceStatus']
): RuleYamlPack {
const metadata = parseMetadata(yamlSource);
const fields = parseTopLevelFields(yamlSource);
const subDocuments = parseSubDocuments(yamlSource);
const rules = parseRules(yamlSource);
const visualElements = parseVisualElements(yamlSource);
return {
...config,
yamlSource,
sourceStatus,
metadata,
rules,
fields,
subDocuments,
visualElements,
stats: {
ruleCount: rules.length,
fieldCount: fields.length + subDocuments.reduce((sum, doc) => sum + doc.fieldCount, 0),
subDocumentCount: subDocuments.length,
visualElementCount: visualElements.length
}
};
}
export async function loadRuleYamlPacks(): Promise<RuleYamlPack[]> {
return Promise.all(MOCK_RULE_PACKS.map(async config => {
// TODO(production-data-source):
// 当前测试页直接读取 leaudit 本地 YAML 作为 mock。
// 生产切换时,这里应改为调用后端接口:
// 1. 后端根据文档类型/主类型/子类型查询数据库中的 OSS YAML 路径;
// 2. 后端读取 OSS YAML 正文并返回元数据和内容;
// 3. 前端仍消费 buildPack 之后的结构化数据,页面不直接关心 OSS 实现。
if (!config.yamlPath) {
return buildRuleYamlPack(config, EMPTY_RULE_YAML, 'empty');
}
try {
const yamlSource = await readFile(config.yamlPath, 'utf8');
return buildRuleYamlPack(config, yamlSource, 'ready');
} catch {
return buildRuleYamlPack(config, EMPTY_RULE_YAML, 'missing');
}
}));
}
export async function loadRuleYamlPack(id: string): Promise<RuleYamlPack | undefined> {
const packs = await loadRuleYamlPacks();
return packs.find(pack => pack.id === id);
}