Files
leaudit-platform-frontend/app/utils/rules-config-editor.ts
T

420 lines
13 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 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`;
}