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(); 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(); 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): DependencyOption[] { const topLevelFields = config.fields.flatMap(field => { const source = '字段抽取'; const group = field.group || '未分组'; const options = [{ value: field.name, label: field.name, source, group }]; 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 }); } 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 = '视觉要素'; const group = item.type || '未分组'; return [ { value: item.id, label, source, group }, { value: item.name || item.id, label, source, group }, { value: `visual.${item.id}`, label, source, group }, { value: `visual.${item.name || item.id}`, label, source, group }, { 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`; }