696 lines
24 KiB
TypeScript
696 lines
24 KiB
TypeScript
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;
|
||
source: string;
|
||
group: string;
|
||
};
|
||
|
||
export type ValidationIssue = {
|
||
id: string;
|
||
severity: 'error' | 'warning';
|
||
area: '抽取配置' | '案卷文书' | '评查规则';
|
||
target: string;
|
||
message: string;
|
||
};
|
||
|
||
export type EditableRuleConfig = {
|
||
metadata: RuleYamlPack['metadata'];
|
||
yamlSource: string;
|
||
documentType: string;
|
||
mainType: string;
|
||
subtype: string;
|
||
fields: ExtractFieldSummary[];
|
||
subDocuments: SubDocumentSummary[];
|
||
visualElements: VisualElementSummary[];
|
||
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 = '字段抽取';
|
||
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: '字段类型不能为空。'
|
||
});
|
||
}
|
||
if (field.type === 'enum' && (!Array.isArray(field.allowed) || field.allowed.length === 0)) {
|
||
issues.push({
|
||
id: `field-allowed-${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: '文书字段类型不能为空。'
|
||
});
|
||
}
|
||
if (field.type === 'enum' && (!Array.isArray(field.allowed) || field.allowed.length === 0)) {
|
||
issues.push({
|
||
id: `document-field-allowed-${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}'` : "''";
|
||
}
|
||
|
||
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',
|
||
...(field.type === 'enum' && Array.isArray(field.allowed) && field.allowed.length > 0 ? { allowed: [...field.allowed] } : {}),
|
||
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',
|
||
...(field.type === 'enum' && Array.isArray(field.allowed) && field.allowed.length > 0 ? { allowed: [...field.allowed] } : {}),
|
||
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.requiredFrom) node.required_from = item.requiredFrom;
|
||
if (item.expectedMatchField) {
|
||
node.expected_text_match = {
|
||
field: item.expectedMatchField,
|
||
...(item.expectedMatchAlternatives && item.expectedMatchAlternatives.length > 0
|
||
? { alternatives: [...item.expectedMatchAlternatives] }
|
||
: {}),
|
||
};
|
||
}
|
||
if (item.prompt) node.prompt = item.prompt;
|
||
|
||
if (item.type === '签名') {
|
||
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;
|
||
sections.signatures.push(node);
|
||
return;
|
||
}
|
||
if (item.type === '骑缝章') {
|
||
sections.cross_page_seals.push(node);
|
||
return;
|
||
}
|
||
if (item.signatureTypes && item.signatureTypes.length > 0) node.allowed_types = [...item.signatureTypes];
|
||
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}`,
|
||
`- 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 {
|
||
try {
|
||
return serializeEditableRuleConfig(config);
|
||
} catch {
|
||
// 预览失败时仍回退旧实现,避免页面直接白屏;保存时会使用正式序列化并给出错误。
|
||
}
|
||
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)}`,
|
||
...(field.type === 'enum' && Array.isArray(field.allowed) && field.allowed.length > 0
|
||
? [` allowed: [${field.allowed.map((item) => yamlValue(item)).join(', ')}]`]
|
||
: []),
|
||
` 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)}`,
|
||
...(field.type === 'enum' && Array.isArray(field.allowed) && field.allowed.length > 0
|
||
? [` allowed: [${field.allowed.map((item) => yamlValue(item)).join(', ')}]`]
|
||
: []),
|
||
` 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`;
|
||
}
|