保存规则库 YAML 维护改造进展

This commit is contained in:
2026-04-28 22:00:00 +08:00
parent 7b86293263
commit dce5ac0c9a
96 changed files with 36801 additions and 615 deletions
+419
View File
@@ -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`;
}