fix: stabilize rules detail editor flow

This commit is contained in:
wren
2026-05-07 09:59:01 +08:00
parent e7bac9a33f
commit 71476fc919
7 changed files with 1071 additions and 236 deletions
+238 -1
View File
@@ -1,5 +1,8 @@
import YAML from 'yaml';
import type { ExtractFieldSummary, RuleSummary, RuleYamlPack, SubDocumentSummary } from './rules-yaml-mock.server';
export type VisualElementSummary = RuleYamlPack['visualElements'][number];
export type DependencyOption = {
value: string;
label: string;
@@ -17,12 +20,13 @@ export type ValidationIssue = {
export type EditableRuleConfig = {
metadata: RuleYamlPack['metadata'];
yamlSource: string;
documentType: string;
mainType: string;
subtype: string;
fields: ExtractFieldSummary[];
subDocuments: SubDocumentSummary[];
visualElements: RuleYamlPack['visualElements'];
visualElements: VisualElementSummary[];
rules: RuleSummary[];
};
@@ -287,6 +291,234 @@ function yamlValue(value: string | number | boolean | undefined): string {
return text ? `'${text}'` : "''";
}
function deepClone<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}
function groupBy<T>(items: T[], keyGetter: (item: T) => string): Map<string, T[]> {
const groups = new Map<string, T[]>();
items.forEach((item) => {
const key = keyGetter(item);
const list = groups.get(key) || [];
list.push(item);
groups.set(key, list);
});
return groups;
}
function normalizeBooleanText(value: string | boolean | undefined): boolean {
if (typeof value === 'boolean') return value;
return String(value || '').trim().toLowerCase() === 'true';
}
function rewriteExtractNodes(fields: ExtractFieldSummary[]): Array<Record<string, unknown>> {
const topLevelFields = fields.filter((field) => !field.name.includes('[*].'));
return Array.from(groupBy(topLevelFields, (field) => field.group || '未分组').entries()).map(([group, items]) => ({
group,
fields: items.map((field) => ({
name: field.name,
type: field.multipleEntities ? 'multi_entity' : field.type || 'verbatim',
required_from: field.requiredFrom || 'draft',
desc: field.description || '',
})),
}));
}
function rewriteSubDocumentNodes(subDocuments: SubDocumentSummary[]): Array<Record<string, unknown>> {
return subDocuments.map((document) => ({
id: document.id,
name: document.name,
required: document.required || 'false',
extract: Array.from(groupBy(document.fields || [], (field) => field.group || '未分组').entries()).map(([group, fields]) => ({
group,
fields: fields.map((field) => ({
name: field.name,
type: field.multipleEntities ? 'multi_entity' : field.type || 'verbatim',
desc: field.description || '',
})),
})),
}));
}
function rewriteVisualElementNodes(visualElements: VisualElementSummary[]): Record<string, unknown> {
const sections = {
seals: [] as Array<Record<string, unknown>>,
signatures: [] as Array<Record<string, unknown>>,
cross_page_seals: [] as Array<Record<string, unknown>>,
};
visualElements.forEach((item) => {
const node: Record<string, unknown> = {
id: item.id,
name: item.name,
required: normalizeBooleanText(item.required),
};
if (item.signerRoles && item.signerRoles.length > 0) node.signer_roles = [...item.signerRoles];
if (item.signatureTypes && item.signatureTypes.length > 0) node.signature_types = [...item.signatureTypes];
if (item.privateSealRestricted) node.private_seal_restricted = true;
if (item.type === '签名') {
sections.signatures.push(node);
return;
}
if (item.type === '骑缝章') {
sections.cross_page_seals.push(node);
return;
}
sections.seals.push(node);
});
return sections;
}
function getRuleLookupKey(rule: Pick<RuleSummary, 'ruleId' | 'name' | 'id'>): string {
return rule.ruleId || rule.name || rule.id;
}
function findAiStage(stages: Array<Record<string, unknown>>): Record<string, unknown> | undefined {
return stages.find((stage) => String(stage?.check || stage?.type || '').trim() === 'ai');
}
function buildMinimalRuleNode(rule: RuleSummary): Record<string, unknown> {
const base: Record<string, unknown> = {
rule_id: rule.ruleId,
name: rule.name,
risk: rule.risk || 'medium',
score: rule.score || '1',
type: rule.type || 'deterministic',
desc: rule.description || '',
};
if (rule.appliesIn.length > 0) {
base.applies_in = [...rule.appliesIn];
}
if (rule.type === 'rule_group') {
base.logic = rule.logic || '';
base.rules = [...rule.subRuleIds];
return base;
}
if (rule.type === 'ai_rule' || rule.checkTypes.includes('ai')) {
base.stages = [{
id: '1',
check: 'ai',
prompt: rule.prompt || '请根据规则要求检查文档内容并输出结论。',
}];
return base;
}
if (rule.dependencies.length > 0) {
base.stages = [{
id: '1',
check: rule.checkTypes[0] || 'required',
field: rule.dependencies[0],
}];
return base;
}
throw new Error(`评查点【${rule.name || rule.ruleId || rule.id}】缺少可生成的正式 stages 结构,请先补充依赖字段或改为 AI/规则组合类型。`);
}
function rewriteRuleNode(baseRule: Record<string, unknown> | undefined, rule: RuleSummary): Record<string, unknown> {
const nextRule = baseRule ? deepClone(baseRule) : buildMinimalRuleNode(rule);
nextRule.rule_id = rule.ruleId;
nextRule.name = rule.name;
nextRule.risk = rule.risk || 'medium';
nextRule.score = rule.score || '1';
nextRule.type = rule.type || 'deterministic';
nextRule.desc = rule.description || '';
if (rule.appliesIn.length > 0) nextRule.applies_in = [...rule.appliesIn];
else delete nextRule.applies_in;
delete nextRule.dependencies;
if (rule.type === 'rule_group') {
nextRule.logic = rule.logic || '';
nextRule.rules = [...rule.subRuleIds];
delete nextRule.stages;
return nextRule;
}
delete nextRule.rules;
delete nextRule.logic;
const existingStages = Array.isArray(nextRule.stages) ? (nextRule.stages as Array<Record<string, unknown>>) : [];
const stages = existingStages.length > 0 ? deepClone(existingStages) : (buildMinimalRuleNode(rule).stages as Array<Record<string, unknown>>);
if (rule.type === 'ai_rule' || rule.checkTypes.includes('ai')) {
const aiStage = findAiStage(stages);
if (aiStage) {
aiStage.check = 'ai';
aiStage.prompt = rule.prompt || aiStage.prompt || '请根据规则要求检查文档内容并输出结论。';
} else {
stages.unshift({
id: '1',
check: 'ai',
prompt: rule.prompt || '请根据规则要求检查文档内容并输出结论。',
});
}
}
nextRule.stages = stages;
return nextRule;
}
export function serializeEditableRuleConfig(config: EditableRuleConfig): string {
const parsed = YAML.parse(config.yamlSource || '') as Record<string, unknown> | null;
const root = parsed && typeof parsed === 'object' ? deepClone(parsed) : {};
const metadata = (root.metadata && typeof root.metadata === 'object' ? deepClone(root.metadata) : {}) as Record<string, unknown>;
metadata.type_id = config.metadata.typeId || metadata.type_id || 'pending.internal.document';
metadata.name = config.metadata.name || metadata.name || `${config.subtype}规则配置`;
metadata.version = config.metadata.version || metadata.version || 'v1';
metadata.last_updated = new Date().toISOString().slice(0, 10);
if (config.metadata.parent || metadata.parent) metadata.parent = config.metadata.parent || metadata.parent;
if (config.metadata.description || metadata.description) metadata.description = config.metadata.description || metadata.description;
if (Array.isArray(config.metadata.keywords) && config.metadata.keywords.length > 0) metadata.classification_keywords = [...config.metadata.keywords];
if (Array.isArray(config.metadata.inheritsFrom) && config.metadata.inheritsFrom.length > 0) metadata.inherits_from = [...config.metadata.inheritsFrom];
root.metadata = metadata;
root.extract = rewriteExtractNodes(config.fields);
root.sub_documents = rewriteSubDocumentNodes(config.subDocuments);
root.visual_elements = rewriteVisualElementNodes(config.visualElements);
const existingGroups = Array.isArray(root.rules) ? (root.rules as Array<Record<string, unknown>>) : [];
const existingRuleMap = new Map<string, Record<string, unknown>>();
existingGroups.forEach((groupBlock) => {
const groupRules = Array.isArray(groupBlock?.rules) ? (groupBlock.rules as Array<Record<string, unknown>>) : [];
groupRules.forEach((ruleNode) => {
const key = String(ruleNode?.rule_id || ruleNode?.name || '').trim();
if (key) existingRuleMap.set(key, ruleNode);
});
});
const nextGroups = new Map<string, Array<Record<string, unknown>>>();
config.rules.forEach((rule) => {
const groupName = rule.group || '未分组';
const list = nextGroups.get(groupName) || [];
const existingRule = existingRuleMap.get(getRuleLookupKey(rule));
list.push(rewriteRuleNode(existingRule, rule));
nextGroups.set(groupName, list);
});
root.rules = Array.from(nextGroups.entries()).map(([group, rules]) => ({ group, rules }));
return YAML.stringify(root).replace(
/^(\s*last_updated:\s*)(.+)$/m,
(_match, prefix, value) => `${prefix}'${String(value).replace(/^['"]|['"]$/g, '')}'`,
);
}
export function prepareDraftYamlForSave(yamlText: string): string {
return yamlText
.replace(/^(\s*version:\s*)(.+)$/m, `${'$1'}''`)
.replace(
/^(\s*last_updated:\s*)(.+)$/m,
(_match, prefix, value) => `${prefix}'${String(value).replace(/^['"]|['"]$/g, '')}'`,
);
}
export function buildRuleYamlPreview(config: EditableRuleConfig, rule: RuleSummary): string {
const lines: string[] = [
`# ${config.documentType} / ${config.mainType} / ${config.subtype}`,
@@ -331,6 +563,11 @@ export function buildRuleYamlPreview(config: EditableRuleConfig, rule: RuleSumma
}
export function buildYamlPreview(config: EditableRuleConfig): string {
try {
return serializeEditableRuleConfig(config);
} catch {
// 预览失败时仍回退旧实现,避免页面直接白屏;保存时会使用正式序列化并给出错误。
}
const lines: string[] = [
'metadata:',
` name: ${yamlValue(config.metadata.name || `${config.subtype}规则配置`)}`,
+6 -7
View File
@@ -54,13 +54,9 @@ function getMessage(payload: unknown, fallback: string): string {
function mapApiPackToRuleYamlPack(item: RuleConfigPackApi): RuleYamlPack {
const ruleTypeCode = String(item.ruleType || '').trim();
const businessType = (() => {
const segments = ruleTypeCode.split('.').map(segment => segment.trim()).filter(Boolean);
if (segments.length >= 2) {
return segments[1];
}
return item.mainType || item.documentType || '';
})();
// 业务类型必须以后端 pack 聚合返回的 mainType 为准。
// 不能再从 ruleType 第二段硬拆;例如 contract.entrust 会被错误显示成 entrust。
const businessType = item.mainType || item.documentType || '';
const yamlSource = (item.yamlText || '').trim() ? String(item.yamlText) : EMPTY_RULE_YAML;
const sourceStatus = item.sourceStatus || ((item.yamlText || '').trim() ? 'ready' : 'empty');
@@ -74,6 +70,9 @@ function mapApiPackToRuleYamlPack(item: RuleConfigPackApi): RuleYamlPack {
subtype: item.subtype || '通用',
businessType,
ruleTypeCode,
currentVersionId: item.currentVersionId ?? null,
fallbackVersionId: item.fallbackVersionId ?? null,
resolvedVersionId: item.resolvedVersionId ?? null,
},
yamlSource,
sourceStatus,
+84 -63
View File
@@ -60,6 +60,9 @@ export type RuleYamlPack = RulePackScope & {
yamlPath: string | null;
yamlSource: string;
sourceStatus: 'ready' | 'empty' | 'missing';
currentVersionId?: number | null;
fallbackVersionId?: number | null;
resolvedVersionId?: number | null;
metadata: {
typeId: string;
name: string;
@@ -195,10 +198,10 @@ function splitBlocks(section: string, marker: RegExp): string[] {
function parseRules(source: string): RuleSummary[] {
const section = getTopLevelSection(source, 'rules');
const groups = splitBlocks(section, /^-\s+group:\s*/);
const groups = splitBlocks(section, /^\s*-\s+group:\s*/);
const readExplicitDependencies = (block: string): string[] => {
const lines = block.split('\n');
const start = lines.findIndex(line => /^\s{4}dependencies:\s*$/.test(line));
const start = lines.findIndex(line => /^\s+dependencies:\s*$/.test(line));
if (start === -1) {
return [];
@@ -207,10 +210,10 @@ function parseRules(source: string): RuleSummary[] {
const dependencies: string[] = [];
for (let index = start + 1; index < lines.length; index += 1) {
const line = lines[index];
if (/^\s{4}[a-zA-Z_][^:]*:\s*/.test(line)) {
if (/^\s+[a-zA-Z_][^:]*:\s*/.test(line) && !/^\s*-\s+/.test(line)) {
break;
}
const match = line.match(/^\s{4}-\s+(.+)$/);
const match = line.match(/^\s*-\s+(.+)$/);
if (match) {
dependencies.push(stripYamlValue(match[1]));
}
@@ -254,20 +257,28 @@ function parseRules(source: string): RuleSummary[] {
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{${indent}}${key}:\\s*$`).test(line));
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];
if (new RegExp(`^\\s{${indent}}[a-zA-Z_][^:]*:\\s*`).test(line)) {
const lineIndent = line.match(/^\s*/)?.[0].length || 0;
if (line.trim() && lineIndent <= baseIndent) {
break;
}
const match = line.match(new RegExp(`^\\s{${indent}}-\\s+(.+)$`));
const match = line.match(/^\s*-\s+(.+)$/);
if (match) {
values.push(stripYamlValue(match[1]));
}
@@ -296,25 +307,28 @@ function parseRules(source: string): RuleSummary[] {
};
const readStageList = (block: string, key: string): string[] => {
const lines = block.split('\n');
const start = lines.findIndex(line => new RegExp(`^\\s{6}${key}:\\s*$`).test(line));
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];
if (/^\s{6}[a-zA-Z_][^:]*:\s*/.test(line)) {
const lineIndent = line.match(/^\s*/)?.[0].length || 0;
if (line.trim() && lineIndent <= baseIndent) {
break;
}
const match = line.match(/^\s{6}-\s+(.+)$/);
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{6}${key}:\\s*(.+)$`, 'm'))?.[1] || '');
const readStageScalar = (block: string, key: string): string => stripYamlValue(block.match(new RegExp(`^\\s+${key}:\\s*(.+)$`, 'm'))?.[1] || '');
const summarizeStage = (stageBlock: string): string => {
const fields = readStageList(stageBlock, 'fields');
const field = readStageScalar(stageBlock, 'field');
@@ -334,7 +348,7 @@ function parseRules(source: string): RuleSummary[] {
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{4}-\s+id:\s*(.+)$/m)?.[1] || '');
const id = stripYamlValue(stageBlock.match(/^\s*-\s+id:\s*(.+)$/m)?.[1] || '');
const check = readStageScalar(stageBlock, 'check') || readStageScalar(stageBlock, 'type') || '-';
return {
id,
@@ -344,16 +358,17 @@ function parseRules(source: string): RuleSummary[] {
}).filter(stage => stage.id);
return groups.flatMap(groupBlock => {
const group = stripYamlValue(groupBlock.match(/^-\s+group:\s*(.+)$/m)?.[1] || '未分组');
return splitBlocks(groupBlock, /^\s{2}-\s+rule_id:\s*/).map(ruleBlock => {
const ruleId = stripYamlValue(ruleBlock.match(/^\s{2}-\s+rule_id:\s*(.+)$/m)?.[1] || '');
const name = stripYamlValue(ruleBlock.match(/^\s{4}name:\s*(.+)$/m)?.[1] || '未命名规则');
const checkTypes = Array.from(new Set(Array.from(ruleBlock.matchAll(/^\s{6,}(?:check|type):\s*(.+)$/gm)).map(match => stripYamlValue(match[1]))));
const stageDependencies = Array.from(ruleBlock.matchAll(/^\s{6,}(?:field|number|chinese|left|right|left_field|right_field|target|element|seal_id|signature_id):\s*(.+)$/gm))
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):\s*(.+)$/gm))
.map(match => normalizeDependency(match[1]));
const dependencies = Array.from(new Set([...readExplicitDependencies(ruleBlock), ...stageDependencies]));
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 prompts = readPrompts(ruleBlock);
const promptDependencies = readPromptDependencies(prompts).map(normalizeDependency);
const dependencies = Array.from(new Set([...readExplicitDependencies(ruleBlock), ...stageDependencies, ...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 {
@@ -361,19 +376,19 @@ function parseRules(source: string): RuleSummary[] {
ruleId,
name,
group,
risk: stripYamlValue(ruleBlock.match(/^\s{4}risk:\s*(.+)$/m)?.[1] || 'medium'),
score: stripYamlValue(ruleBlock.match(/^\s{4}score:\s*(.+)$/m)?.[1] || '-'),
type: stripYamlValue(ruleBlock.match(/^\s{4}type:\s*(.+)$/m)?.[1] || 'deterministic'),
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{4}logic:\s*(.+)$/m)?.[1] || ''),
logic: stripYamlValue(ruleBlock.match(/^\s+logic:\s*(.+)$/m)?.[1] || ''),
subRules,
subRuleIds: readList(ruleBlock, 'rules'),
scope: scope.slice(0, 8),
dependencies: dependencies.slice(0, 8),
dependencies,
stageCount: subRules.length,
appliesIn: readFlexibleList(ruleBlock, 'applies_in'),
prompt: prompts.join('\n\n'),
description: stripYamlValue(ruleBlock.match(/^\s{4}desc:\s*(.+)$/m)?.[1] || '')
description: stripYamlValue(ruleBlock.match(/^\s+desc:\s*(.+)$/m)?.[1] || '')
};
});
});
@@ -385,34 +400,34 @@ export function parseRuleSummariesFromYaml(source: string): RuleSummary[] {
function parseTopLevelFields(source: string): ExtractFieldSummary[] {
const section = getTopLevelSection(source, 'extract');
const extractedFields = splitBlocks(section, /^-\s+group:\s*/).flatMap(groupBlock => {
const group = stripYamlValue(groupBlock.match(/^-\s+group:\s*(.+)$/m)?.[1] || '未分组');
return splitBlocks(groupBlock, /^\s{2}-\s+name:\s*/).flatMap(fieldBlock => {
const name = stripYamlValue(fieldBlock.match(/^\s{2}-\s+name:\s*(.+)$/m)?.[1] || '');
const rawType = stripYamlValue(fieldBlock.match(/^\s{4}type:\s*(.+)$/m)?.[1] || '-');
const extractedFields = splitBlocks(section, /^\s*-\s+group:\s*/).flatMap(groupBlock => {
const group = stripYamlValue(groupBlock.match(/^\s*-\s+group:\s*(.+)$/m)?.[1] || '未分组');
return splitBlocks(groupBlock, /^\s*-\s+name:\s*/).flatMap(fieldBlock => {
const name = stripYamlValue(fieldBlock.match(/^\s*-\s+name:\s*(.+)$/m)?.[1] || '');
const rawType = stripYamlValue(fieldBlock.match(/^\s+type:\s*(.+)$/m)?.[1] || '-');
const parentField = {
id: `${group}-${name}`,
group,
name,
type: rawType === 'multi_entity' ? 'verbatim' : rawType,
multipleEntities: rawType === 'multi_entity',
requiredFrom: stripYamlValue(fieldBlock.match(/^\s{4}required_from:\s*(.+)$/m)?.[1] || '-'),
description: stripYamlValue(fieldBlock.match(/^\s{4}desc:\s*(.+)$/m)?.[1] || '')
requiredFrom: stripYamlValue(fieldBlock.match(/^\s+required_from:\s*(.+)$/m)?.[1] || '-'),
description: stripYamlValue(fieldBlock.match(/^\s+desc:\s*(.+)$/m)?.[1] || '')
};
const childFields = Array.from(fieldBlock.matchAll(/^\s{4}-\s+name:\s*(.+)$/gm)).map(match => {
const childFields = Array.from(fieldBlock.matchAll(/^\s{2,}-\s+name:\s*(.+)$/gm)).map(match => {
const childName = stripYamlValue(match[1]);
const start = fieldBlock.indexOf(match[0]);
const next = fieldBlock.slice(start + match[0].length).search(/^\s{4}-\s+name:\s*/m);
const next = fieldBlock.slice(start + match[0].length).search(/^\s{2,}-\s+name:\s*/m);
const childBlock = next === -1 ? fieldBlock.slice(start) : fieldBlock.slice(start, start + match[0].length + next);
const childType = stripYamlValue(childBlock.match(/^\s{6}type:\s*(.+)$/m)?.[1] || 'verbatim');
const childType = stripYamlValue(childBlock.match(/^\s+type:\s*(.+)$/m)?.[1] || 'verbatim');
return {
id: `${group}-${name}-${childName}`,
group,
name: `${name}[*].${childName}`,
type: childType,
multipleEntities: false,
requiredFrom: stripYamlValue(childBlock.match(/^\s{6}required_from:\s*(.+)$/m)?.[1] || parentField.requiredFrom),
description: stripYamlValue(childBlock.match(/^\s{6}desc:\s*(.+)$/m)?.[1] || `${name}的子字段`)
requiredFrom: stripYamlValue(childBlock.match(/^\s+required_from:\s*(.+)$/m)?.[1] || parentField.requiredFrom),
description: stripYamlValue(childBlock.match(/^\s+desc:\s*(.+)$/m)?.[1] || `${name}的子字段`)
};
});
return [parentField, ...childFields];
@@ -420,16 +435,16 @@ function parseTopLevelFields(source: string): ExtractFieldSummary[] {
}).filter(field => field.name);
const derivedSection = getTopLevelSection(source, 'derived_fields');
const derivedFields = splitBlocks(derivedSection, /^-\s+name:\s*/).map(fieldBlock => {
const name = stripYamlValue(fieldBlock.match(/^-\s+name:\s*(.+)$/m)?.[1] || '');
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{2}type:\s*(.+)$/m)?.[1] || 'computed'),
type: stripYamlValue(fieldBlock.match(/^\s+type:\s*(.+)$/m)?.[1] || 'computed'),
multipleEntities: false,
requiredFrom: '-',
description: stripYamlValue(fieldBlock.match(/^\s{2}compute:\s*(.+)$/m)?.[1] || '由其他字段计算得出')
description: stripYamlValue(fieldBlock.match(/^\s+compute:\s*(.+)$/m)?.[1] || '由其他字段计算得出')
};
}).filter(field => field.name);
@@ -437,11 +452,11 @@ function parseTopLevelFields(source: string): ExtractFieldSummary[] {
}
function parseDocumentFields(docBlock: string, documentId: string): ExtractFieldSummary[] {
return splitBlocks(docBlock, /^\s{2}-\s+group:\s*/).flatMap(groupBlock => {
const group = stripYamlValue(groupBlock.match(/^\s{2}-\s+group:\s*(.+)$/m)?.[1] || '未分组');
return splitBlocks(groupBlock, /^\s{4}-\s+name:\s*/).map(fieldBlock => {
const name = stripYamlValue(fieldBlock.match(/^\s{4}-\s+name:\s*(.+)$/m)?.[1] || '');
const rawType = stripYamlValue(fieldBlock.match(/^\s{6}type:\s*(.+)$/m)?.[1] || '-');
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,
@@ -449,7 +464,7 @@ function parseDocumentFields(docBlock: string, documentId: string): ExtractField
type: rawType === 'multi_entity' ? 'verbatim' : rawType,
multipleEntities: rawType === 'multi_entity',
requiredFrom: '-',
description: stripYamlValue(fieldBlock.match(/^\s{6}desc:\s*(.+)$/m)?.[1] || '')
description: stripYamlValue(fieldBlock.match(/^\s+desc:\s*(.+)$/m)?.[1] || '')
};
});
}).filter(field => field.name);
@@ -457,9 +472,9 @@ function parseDocumentFields(docBlock: string, documentId: string): ExtractField
function parseSubDocuments(source: string): SubDocumentSummary[] {
const section = getTopLevelSection(source, 'sub_documents');
return splitBlocks(section, /^-\s+id:\s*/).map(docBlock => {
const id = stripYamlValue(docBlock.match(/^-\s+id:\s*(.+)$/m)?.[1] || '');
const groups = Array.from(new Set(Array.from(docBlock.matchAll(/^\s{2,}-\s+group:\s*(.+)$/gm)).map(match => stripYamlValue(match[1]))));
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 = '';
@@ -472,8 +487,8 @@ function parseSubDocuments(source: string): SubDocumentSummary[] {
}
return {
id,
name: stripYamlValue(docBlock.match(/^\s{2}name:\s*(.+)$/m)?.[1] || id),
required: stripYamlValue(docBlock.match(/^\s{2}required:\s*(.+)$/m)?.[1] || '-'),
name: stripYamlValue(docBlock.match(/^\s+name:\s*(.+)$/m)?.[1] || id),
required: stripYamlValue(docBlock.match(/^\s+required:\s*(.+)$/m)?.[1] || '-'),
fieldCount: fields.length,
groups,
description,
@@ -492,7 +507,7 @@ function parseVisualElements(source: string): RuleYamlPack['visualElements'] {
return typedSections.flatMap(({ key, label }) => {
const lines = section.split('\n');
const start = lines.findIndex(line => new RegExp(`^\\s{2}${key}:`).test(line));
const start = lines.findIndex(line => new RegExp(`^\\s+${key}:`).test(line));
if (start === -1) {
return [];
}
@@ -500,27 +515,33 @@ function parseVisualElements(source: string): RuleYamlPack['visualElements'] {
// 找到下一个同级分类的起始位置(2空格+字母+冒号)
let end = lines.length;
for (let i = start + 1; i < lines.length; i++) {
if (/^\s{2}[a-zA-Z_][\w-]*:/.test(lines[i])) {
if (/^\s+[a-zA-Z_][\w-]*:/.test(lines[i])) {
end = i;
break;
}
}
const subSection = lines.slice(start + 1, end).join('\n');
return splitBlocks(subSection, /^\s{2}-\s+id:\s*/).map(block => ({
id: stripYamlValue(block.match(/^\s{2}-\s+id:\s*(.+)$/m)?.[1] || ''),
name: stripYamlValue(block.match(/^\s{4}name:\s*(.+)$/m)?.[1] || ''),
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{4}required:\s*(.+)$/m)?.[1] || '-'),
signerRoles: block.match(/^\s{4}signer_roles:\s*\[(.+)\]$/m)?.[1].split(',').map(s => s.trim()) || [],
signatureTypes: block.match(/^\s{4}signature_types:\s*\[(.+)\]$/m)?.[1].split(',').map(s => s.trim()) || [],
privateSealRestricted: block.match(/^\s{4}private_seal_restricted:\s*(.+)$/m)?.[1] === 'true'
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);
}
export function buildRuleYamlPack(
config: RulePackScope & { id: string; yamlPath: string | null },
config: RulePackScope & {
id: string;
yamlPath: string | null;
currentVersionId?: number | null;
fallbackVersionId?: number | null;
resolvedVersionId?: number | null;
},
yamlSource: string,
sourceStatus: RuleYamlPack['sourceStatus']
): RuleYamlPack {