fix: stabilize rule detail config persistence
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import YAML from 'yaml';
|
||||
|
||||
const LEAUDIT_RULES_ROOT = `${process.cwd()}/mock-data/leaudit-rules/packs/yc`;
|
||||
|
||||
@@ -451,87 +452,118 @@ function parseTopLevelFields(source: string): ExtractFieldSummary[] {
|
||||
return [...extractedFields, ...derivedFields];
|
||||
}
|
||||
|
||||
function parseDocumentFields(docBlock: string, documentId: string): ExtractFieldSummary[] {
|
||||
return splitBlocks(docBlock, /^\s*-\s+group:\s*/).flatMap(groupBlock => {
|
||||
const group = stripYamlValue(groupBlock.match(/^\s*-\s+group:\s*(.+)$/m)?.[1] || '未分组');
|
||||
return splitBlocks(groupBlock, /^\s*-\s+name:\s*/).map(fieldBlock => {
|
||||
const name = stripYamlValue(fieldBlock.match(/^\s*-\s+name:\s*(.+)$/m)?.[1] || '');
|
||||
const rawType = stripYamlValue(fieldBlock.match(/^\s+type:\s*(.+)$/m)?.[1] || '-');
|
||||
return {
|
||||
id: `${documentId}-${group}-${name}`,
|
||||
group,
|
||||
name,
|
||||
type: rawType === 'multi_entity' ? 'verbatim' : rawType,
|
||||
multipleEntities: rawType === 'multi_entity',
|
||||
requiredFrom: '-',
|
||||
description: stripYamlValue(fieldBlock.match(/^\s+desc:\s*(.+)$/m)?.[1] || '')
|
||||
};
|
||||
});
|
||||
}).filter(field => field.name);
|
||||
}
|
||||
|
||||
function parseSubDocuments(source: string): SubDocumentSummary[] {
|
||||
const section = getTopLevelSection(source, 'sub_documents');
|
||||
return splitBlocks(section, /^\s*-\s+id:\s*/).map(docBlock => {
|
||||
const id = stripYamlValue(docBlock.match(/^\s*-\s+id:\s*(.+)$/m)?.[1] || '');
|
||||
const groups = Array.from(new Set(Array.from(docBlock.matchAll(/^\s*-\s+group:\s*(.+)$/gm)).map(match => stripYamlValue(match[1]))));
|
||||
const fields = parseDocumentFields(docBlock, id);
|
||||
const classifier = docBlock.match(/^\s{2}classifier:\s*$/m);
|
||||
let description = '';
|
||||
if (classifier) {
|
||||
const keywordsMatch = docBlock.match(/keywords:\s*\n((?:\s{4}-\s+.+\n)+)/m);
|
||||
if (keywordsMatch) {
|
||||
const keywords = Array.from(keywordsMatch[1].matchAll(/^\s{4}-\s+(.+)$/gm)).map(match => stripYamlValue(match[1])).slice(0, 3);
|
||||
description = keywords.join('、');
|
||||
}
|
||||
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 [];
|
||||
}
|
||||
return {
|
||||
id,
|
||||
name: stripYamlValue(docBlock.match(/^\s+name:\s*(.+)$/m)?.[1] || id),
|
||||
required: stripYamlValue(docBlock.match(/^\s+required:\s*(.+)$/m)?.[1] || '-'),
|
||||
fieldCount: fields.length,
|
||||
groups,
|
||||
description,
|
||||
fields
|
||||
};
|
||||
}).filter(doc => doc.id);
|
||||
}
|
||||
|
||||
function parseVisualElements(source: string): RuleYamlPack['visualElements'] {
|
||||
const section = getTopLevelSection(source, 'visual_elements');
|
||||
const typedSections = [
|
||||
{ key: 'seals', label: '签章' },
|
||||
{ key: 'signatures', label: '签名' },
|
||||
{ key: 'cross_page_seals', label: '骑缝章' }
|
||||
];
|
||||
|
||||
return typedSections.flatMap(({ key, label }) => {
|
||||
const lines = section.split('\n');
|
||||
const start = lines.findIndex(line => new RegExp(`^\\s+${key}:`).test(line));
|
||||
if (start === -1) {
|
||||
const document = documentNode as Record<string, unknown>;
|
||||
const id = String(document.id || '').trim();
|
||||
if (!id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 找到下一个同级分类的起始位置(2空格+字母+冒号)
|
||||
let end = lines.length;
|
||||
for (let i = start + 1; i < lines.length; i++) {
|
||||
if (/^\s+[a-zA-Z_][\w-]*:/.test(lines[i])) {
|
||||
end = i;
|
||||
break;
|
||||
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',
|
||||
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 [];
|
||||
}
|
||||
|
||||
const subSection = lines.slice(start + 1, end).join('\n');
|
||||
return splitBlocks(subSection, /^\s*-\s+id:\s*/).map(block => ({
|
||||
id: stripYamlValue(block.match(/^\s*-\s+id:\s*(.+)$/m)?.[1] || ''),
|
||||
name: stripYamlValue(block.match(/^\s+name:\s*(.+)$/m)?.[1] || ''),
|
||||
type: label,
|
||||
required: stripYamlValue(block.match(/^\s+required:\s*(.+)$/m)?.[1] || '-'),
|
||||
signerRoles: block.match(/^\s+signer_roles:\s*\[(.+)\]$/m)?.[1].split(',').map(s => s.trim()) || [],
|
||||
signatureTypes: block.match(/^\s+signature_types:\s*\[(.+)\]$/m)?.[1].split(',').map(s => s.trim()) || [],
|
||||
privateSealRestricted: block.match(/^\s+private_seal_restricted:\s*(.+)$/m)?.[1] === 'true'
|
||||
}));
|
||||
}).filter(item => item.id);
|
||||
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)
|
||||
: []
|
||||
);
|
||||
|
||||
return [{
|
||||
id,
|
||||
name: String(node.name || id).trim(),
|
||||
type: label,
|
||||
required: String(node.required ?? '-').trim(),
|
||||
signerRoles: toStringList(node.signer_roles),
|
||||
signatureTypes: toStringList(node.signature_types),
|
||||
privateSealRestricted: Boolean(node.private_seal_restricted),
|
||||
}];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function buildRuleYamlPack(
|
||||
|
||||
Reference in New Issue
Block a user