保存规则库 YAML 维护改造进展
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
import type { ExtractFieldSummary, RuleSummary, RuleYamlPack, SubDocumentSummary } from './rules-yaml-mock.server';
|
||||
|
||||
export type DependencyOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
source: string;
|
||||
group: string;
|
||||
};
|
||||
|
||||
export type ValidationIssue = {
|
||||
id: string;
|
||||
severity: 'error' | 'warning';
|
||||
area: '抽取配置' | '案卷文书' | '评查规则';
|
||||
target: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type EditableRuleConfig = {
|
||||
metadata: RuleYamlPack['metadata'];
|
||||
documentType: string;
|
||||
mainType: string;
|
||||
subtype: string;
|
||||
fields: ExtractFieldSummary[];
|
||||
subDocuments: SubDocumentSummary[];
|
||||
visualElements: RuleYamlPack['visualElements'];
|
||||
rules: RuleSummary[];
|
||||
};
|
||||
|
||||
function uniqueByValue(options: DependencyOption[]): DependencyOption[] {
|
||||
const seen = new Set<string>();
|
||||
return options.filter(option => {
|
||||
if (!option.value || seen.has(option.value)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(option.value);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function getDefaultExpandedDependencyGroups(options: DependencyOption[], selectedValues: string[]): string[] {
|
||||
const selected = new Set(selectedValues);
|
||||
const seen = new Set<string>();
|
||||
return options
|
||||
.filter(option => selected.has(option.value))
|
||||
.map(option => option.group)
|
||||
.filter(group => {
|
||||
if (!group || seen.has(group)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(group);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function collectDependencyOptions(config: Pick<EditableRuleConfig, 'fields' | 'subDocuments' | 'visualElements'>): DependencyOption[] {
|
||||
const topLevelFields = config.fields.flatMap(field => {
|
||||
const source = field.group ? `字段抽取 / ${field.group}` : '字段抽取';
|
||||
const options = [{
|
||||
value: field.name,
|
||||
label: field.name,
|
||||
source,
|
||||
group: source
|
||||
}];
|
||||
if (field.group === '派生字段') {
|
||||
options.push({
|
||||
value: `derived.${field.name}`,
|
||||
label: field.name,
|
||||
source: '派生字段',
|
||||
group: '派生字段'
|
||||
});
|
||||
}
|
||||
if (field.name.includes('[*].')) {
|
||||
options.push({
|
||||
value: field.name.replace('[*].', '.'),
|
||||
label: field.name.replace('[*].', ' / '),
|
||||
source,
|
||||
group: source
|
||||
});
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
const documentFields = config.subDocuments.flatMap(document => [
|
||||
{
|
||||
value: document.name,
|
||||
label: document.name,
|
||||
source: '案卷文书',
|
||||
group: '案卷文书'
|
||||
},
|
||||
...(document.fields || []).flatMap(field => [
|
||||
{
|
||||
value: `${document.name}.${field.name}`,
|
||||
label: `${document.name} / ${field.name}`,
|
||||
source: field.group ? `案卷文书 / ${field.group}` : '案卷文书',
|
||||
group: field.group ? `${document.name} / ${field.group}` : document.name
|
||||
},
|
||||
{
|
||||
value: field.name,
|
||||
label: `${field.name}(${document.name})`,
|
||||
source: field.group ? `案卷文书 / ${field.group}` : '案卷文书',
|
||||
group: field.group ? `${document.name} / ${field.group}` : document.name
|
||||
}
|
||||
])
|
||||
]);
|
||||
|
||||
const visualElements = config.visualElements.flatMap(item => {
|
||||
const label = item.name || item.id;
|
||||
const source = `视觉要素 / ${item.type}`;
|
||||
return [
|
||||
{
|
||||
value: item.id,
|
||||
label,
|
||||
source,
|
||||
group: source
|
||||
},
|
||||
{
|
||||
value: item.name || item.id,
|
||||
label,
|
||||
source,
|
||||
group: source
|
||||
},
|
||||
{
|
||||
value: `visual.${item.id}`,
|
||||
label,
|
||||
source,
|
||||
group: source
|
||||
},
|
||||
{
|
||||
value: `visual.${item.name || item.id}`,
|
||||
label,
|
||||
source,
|
||||
group: source
|
||||
},
|
||||
{
|
||||
value: item.type,
|
||||
label: item.type,
|
||||
source: '视觉要素',
|
||||
group: '视觉要素'
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
return uniqueByValue([...topLevelFields, ...documentFields, ...visualElements]);
|
||||
}
|
||||
|
||||
export function validateEditableRuleConfig(config: EditableRuleConfig): ValidationIssue[] {
|
||||
const issues: ValidationIssue[] = [];
|
||||
const dependencyOptions = collectDependencyOptions(config);
|
||||
const dependencyValues = new Set(dependencyOptions.map(option => option.value));
|
||||
const hasKnownDependency = (dependency: string) => {
|
||||
if (/^-?\d+(\.\d+)?$/.test(dependency)) return true;
|
||||
if (dependencyValues.has(dependency)) return true;
|
||||
const prefix = dependency.split('.')[0];
|
||||
return dependency.includes('.') && dependencyValues.has(prefix);
|
||||
};
|
||||
|
||||
config.fields.forEach(field => {
|
||||
if (!field.name.trim()) {
|
||||
issues.push({
|
||||
id: `field-name-${field.id}`,
|
||||
severity: 'error',
|
||||
area: '抽取配置',
|
||||
target: field.group || '未分组字段',
|
||||
message: '字段名称不能为空。'
|
||||
});
|
||||
}
|
||||
if (!field.type.trim() || field.type === '-') {
|
||||
issues.push({
|
||||
id: `field-type-${field.id}`,
|
||||
severity: 'error',
|
||||
area: '抽取配置',
|
||||
target: field.name || '未命名字段',
|
||||
message: '字段类型不能为空。'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
config.subDocuments.forEach(document => {
|
||||
if (!document.name.trim()) {
|
||||
issues.push({
|
||||
id: `document-name-${document.id}`,
|
||||
severity: 'error',
|
||||
area: '案卷文书',
|
||||
target: document.id,
|
||||
message: '文书名称不能为空。'
|
||||
});
|
||||
}
|
||||
if ((document.fields || []).length === 0) {
|
||||
issues.push({
|
||||
id: `document-fields-${document.id}`,
|
||||
severity: 'warning',
|
||||
area: '案卷文书',
|
||||
target: document.name || document.id,
|
||||
message: '当前文书还没有配置文书字段。'
|
||||
});
|
||||
}
|
||||
(document.fields || []).forEach(field => {
|
||||
if (!field.name.trim()) {
|
||||
issues.push({
|
||||
id: `document-field-name-${document.id}-${field.id}`,
|
||||
severity: 'error',
|
||||
area: '案卷文书',
|
||||
target: document.name || document.id,
|
||||
message: '文书字段名称不能为空。'
|
||||
});
|
||||
}
|
||||
if (!field.type.trim() || field.type === '-') {
|
||||
issues.push({
|
||||
id: `document-field-type-${document.id}-${field.id}`,
|
||||
severity: 'error',
|
||||
area: '案卷文书',
|
||||
target: field.name || '未命名字段',
|
||||
message: '文书字段类型不能为空。'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
config.rules.forEach(rule => {
|
||||
if (!rule.name.trim()) {
|
||||
issues.push({
|
||||
id: `rule-name-${rule.id}`,
|
||||
severity: 'error',
|
||||
area: '评查规则',
|
||||
target: rule.ruleId || rule.id,
|
||||
message: '评查点名称不能为空。'
|
||||
});
|
||||
}
|
||||
if (!rule.group.trim()) {
|
||||
issues.push({
|
||||
id: `rule-group-${rule.id}`,
|
||||
severity: 'error',
|
||||
area: '评查规则',
|
||||
target: rule.name || rule.ruleId || rule.id,
|
||||
message: '评查点必须选择规则组。'
|
||||
});
|
||||
}
|
||||
if (!rule.score.trim() || rule.score === '-') {
|
||||
issues.push({
|
||||
id: `rule-score-${rule.id}`,
|
||||
severity: 'error',
|
||||
area: '评查规则',
|
||||
target: rule.name || rule.ruleId || rule.id,
|
||||
message: '评查点必须设置分值。'
|
||||
});
|
||||
}
|
||||
if ((rule.type === 'ai_rule' || rule.checkTypes.includes('ai')) && !rule.prompt.trim()) {
|
||||
issues.push({
|
||||
id: `rule-prompt-${rule.id}`,
|
||||
severity: 'warning',
|
||||
area: '评查规则',
|
||||
target: rule.name || rule.ruleId || rule.id,
|
||||
message: '智能语义检查建议维护提示词,便于后续重组 YAML。'
|
||||
});
|
||||
}
|
||||
if (rule.type === 'rule_group' && !rule.logic.trim()) {
|
||||
issues.push({
|
||||
id: `rule-group-logic-${rule.id}`,
|
||||
severity: 'error',
|
||||
area: '评查规则',
|
||||
target: rule.name || rule.ruleId || rule.id,
|
||||
message: '规则组合必须维护逻辑运算式。'
|
||||
});
|
||||
}
|
||||
rule.dependencies.forEach(dependency => {
|
||||
if (!hasKnownDependency(dependency)) {
|
||||
issues.push({
|
||||
id: `rule-dependency-${rule.id}-${dependency}`,
|
||||
severity: 'warning',
|
||||
area: '评查规则',
|
||||
target: rule.name || rule.ruleId || rule.id,
|
||||
message: `依赖字段【${dependency}】未在当前 YAML 的字段配置或视觉要素中找到。`
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
function yamlValue(value: string | number | boolean | undefined): string {
|
||||
if (typeof value === 'boolean') return String(value);
|
||||
if (typeof value === 'number') return String(value);
|
||||
const text = String(value || '').replace(/'/g, "''");
|
||||
return text ? `'${text}'` : "''";
|
||||
}
|
||||
|
||||
export function buildRuleYamlPreview(config: EditableRuleConfig, rule: RuleSummary): string {
|
||||
const lines: string[] = [
|
||||
`# ${config.documentType} / ${config.mainType} / ${config.subtype}`,
|
||||
`- group: ${yamlValue(rule.group || '未分组')}`,
|
||||
' rules:',
|
||||
` - rule_id: ${yamlValue(rule.ruleId)}`,
|
||||
` name: ${yamlValue(rule.name)}`,
|
||||
` risk: ${yamlValue(rule.risk)}`,
|
||||
` score: ${yamlValue(rule.score)}`,
|
||||
` type: ${yamlValue(rule.type)}`,
|
||||
` desc: ${yamlValue(rule.description)}`
|
||||
];
|
||||
|
||||
if (rule.appliesIn.length > 0) {
|
||||
lines.push(' applies_in:');
|
||||
rule.appliesIn.forEach(phase => lines.push(` - ${yamlValue(phase)}`));
|
||||
}
|
||||
|
||||
if (rule.type === 'rule_group' && rule.logic.trim()) {
|
||||
lines.push(` logic: ${yamlValue(rule.logic)}`);
|
||||
if (rule.subRuleIds.length > 0) {
|
||||
lines.push(' rules:');
|
||||
rule.subRuleIds.forEach(ruleId => lines.push(` - ${yamlValue(ruleId)}`));
|
||||
}
|
||||
}
|
||||
|
||||
if ((rule.type === 'ai_rule' || rule.checkTypes.includes('ai')) && rule.prompt.trim()) {
|
||||
lines.push(
|
||||
' stages:',
|
||||
` - id: '1'`,
|
||||
` check: ai`,
|
||||
` prompt: ${yamlValue(rule.prompt)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (rule.dependencies.length > 0) {
|
||||
lines.push(' dependencies:');
|
||||
rule.dependencies.forEach(dependency => lines.push(` - ${yamlValue(dependency)}`));
|
||||
}
|
||||
|
||||
return `${lines.join('\n')}\n`;
|
||||
}
|
||||
|
||||
export function buildYamlPreview(config: EditableRuleConfig): string {
|
||||
const lines: string[] = [
|
||||
'metadata:',
|
||||
` name: ${yamlValue(config.metadata.name || `${config.subtype}规则配置`)}`,
|
||||
` version: ${yamlValue(config.metadata.version || 'mock')}`,
|
||||
` description: ${yamlValue(config.metadata.description || '前端交互验证草稿')}`
|
||||
];
|
||||
|
||||
if (config.fields.length > 0) {
|
||||
lines.push('extract:');
|
||||
const groups = Array.from(new Set(config.fields.map(field => field.group || '未分组')));
|
||||
groups.forEach(group => {
|
||||
lines.push(`- group: ${yamlValue(group)}`, ' fields:');
|
||||
config.fields.filter(field => (field.group || '未分组') === group).forEach(field => {
|
||||
lines.push(
|
||||
` - name: ${yamlValue(field.name)}`,
|
||||
` type: ${yamlValue(field.multipleEntities ? 'multi_entity' : field.type)}`,
|
||||
` desc: ${yamlValue(field.description)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (config.subDocuments.length > 0) {
|
||||
lines.push('sub_documents:');
|
||||
config.subDocuments.forEach(document => {
|
||||
lines.push(
|
||||
`- id: ${yamlValue(document.id)}`,
|
||||
` name: ${yamlValue(document.name)}`,
|
||||
` required: ${yamlValue(document.required)}`
|
||||
);
|
||||
if ((document.fields || []).length > 0) {
|
||||
lines.push(' extract:');
|
||||
const groups = Array.from(new Set(document.fields.map(field => field.group || '未分组')));
|
||||
groups.forEach(group => {
|
||||
lines.push(` - group: ${yamlValue(group)}`, ' fields:');
|
||||
document.fields.filter(field => (field.group || '未分组') === group).forEach(field => {
|
||||
lines.push(
|
||||
` - name: ${yamlValue(field.name)}`,
|
||||
` type: ${yamlValue(field.multipleEntities ? 'multi_entity' : field.type)}`,
|
||||
` desc: ${yamlValue(field.description)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lines.push('rules:');
|
||||
const ruleGroups = Array.from(new Set(config.rules.map(rule => rule.group || '未分组')));
|
||||
ruleGroups.forEach(group => {
|
||||
lines.push(`- group: ${yamlValue(group)}`, ' rules:');
|
||||
config.rules.filter(rule => (rule.group || '未分组') === group).forEach(rule => {
|
||||
lines.push(
|
||||
` - rule_id: ${yamlValue(rule.ruleId)}`,
|
||||
` name: ${yamlValue(rule.name)}`,
|
||||
` risk: ${yamlValue(rule.risk)}`,
|
||||
` score: ${yamlValue(rule.score)}`,
|
||||
` type: ${yamlValue(rule.type)}`,
|
||||
` desc: ${yamlValue(rule.description)}`
|
||||
);
|
||||
if (rule.appliesIn.length > 0) {
|
||||
lines.push(' applies_in:');
|
||||
rule.appliesIn.forEach(phase => lines.push(` - ${yamlValue(phase)}`));
|
||||
}
|
||||
if (rule.type === 'rule_group' && rule.logic.trim()) {
|
||||
lines.push(` logic: ${yamlValue(rule.logic)}`);
|
||||
if (rule.subRuleIds.length > 0) {
|
||||
lines.push(' rules:');
|
||||
rule.subRuleIds.forEach(ruleId => lines.push(` - ${yamlValue(ruleId)}`));
|
||||
}
|
||||
}
|
||||
if ((rule.type === 'ai_rule' || rule.checkTypes.includes('ai')) && rule.prompt.trim()) {
|
||||
lines.push(
|
||||
' stages:',
|
||||
` - id: '1'`,
|
||||
` check: ai`,
|
||||
` prompt: ${yamlValue(rule.prompt)}`
|
||||
);
|
||||
}
|
||||
if (rule.dependencies.length > 0) {
|
||||
lines.push(' dependencies:');
|
||||
rule.dependencies.forEach(dependency => lines.push(` - ${yamlValue(dependency)}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return `${lines.join('\n')}\n`;
|
||||
}
|
||||
Reference in New Issue
Block a user