feat: sync rule management and review ui fixes

This commit is contained in:
wren
2026-05-07 17:27:42 +08:00
parent 87e82d1caa
commit c00e5feff0
13 changed files with 565 additions and 161 deletions
+40 -3
View File
@@ -179,6 +179,15 @@ export function validateEditableRuleConfig(config: EditableRuleConfig): Validati
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 => {
@@ -219,6 +228,15 @@ export function validateEditableRuleConfig(config: EditableRuleConfig): Validati
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: '文书枚举字段必须配置可选值。'
});
}
});
});
@@ -318,6 +336,7 @@ function rewriteExtractNodes(fields: ExtractFieldSummary[]): Array<Record<string
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 || '',
})),
@@ -334,6 +353,7 @@ function rewriteSubDocumentNodes(subDocuments: SubDocumentSummary[]): Array<Reco
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 || '',
})),
})),
@@ -353,11 +373,21 @@ function rewriteVisualElementNodes(visualElements: VisualElementSummary[]): Reco
name: item.name,
required: normalizeBooleanText(item.required),
};
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;
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;
}
@@ -365,6 +395,7 @@ function rewriteVisualElementNodes(visualElements: VisualElementSummary[]): Reco
sections.cross_page_seals.push(node);
return;
}
if (item.signatureTypes && item.signatureTypes.length > 0) node.allowed_types = [...item.signatureTypes];
sections.seals.push(node);
});
@@ -584,6 +615,9 @@ export function buildYamlPreview(config: EditableRuleConfig): string {
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)}`
);
});
@@ -607,6 +641,9 @@ export function buildYamlPreview(config: EditableRuleConfig): string {
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)}`
);
});
+45
View File
@@ -3,6 +3,7 @@ import { getUserSession } from '~/api/login/auth.server';
import {
buildRuleYamlPack,
EMPTY_RULE_YAML,
type RuleSummary,
type RuleYamlPack,
} from './rules-yaml-mock.server';
@@ -28,6 +29,23 @@ type RuleConfigPackApi = {
sourceStatus?: 'ready' | 'empty' | 'missing';
};
export type RuleConfigPackSummary = {
id: string;
documentType: string;
moduleType: string;
mainType: string;
subtype: string;
businessType: string;
ruleTypeCode: string;
currentVersionId?: number | null;
fallbackVersionId?: number | null;
resolvedVersionId?: number | null;
yamlName: string;
sourceStatus: 'ready' | 'empty' | 'missing';
rules: RuleSummary[];
};
type ApiEnvelope<T> = {
code?: number;
message?: string;
@@ -52,6 +70,27 @@ function getMessage(payload: unknown, fallback: string): string {
return String((payload as ApiEnvelope<unknown>).message || (payload as ApiEnvelope<unknown>).msg || fallback);
}
function mapApiPackSummary(item: RuleConfigPackApi & { yamlName?: string; rules?: RuleSummary[] }): RuleConfigPackSummary {
const ruleTypeCode = String(item.ruleType || '').trim();
const businessType = item.mainType || item.documentType || '';
return {
id: String(item.packId),
documentType: item.documentType || '',
moduleType: item.moduleType || (item.documentType ? `${item.documentType}评查` : '规则配置'),
mainType: item.mainType || item.documentType || '',
subtype: item.subtype || '通用',
businessType,
ruleTypeCode,
currentVersionId: item.currentVersionId ?? null,
fallbackVersionId: item.fallbackVersionId ?? null,
resolvedVersionId: item.resolvedVersionId ?? null,
yamlName: String(item.yamlName || item.ruleName || ''),
sourceStatus: item.sourceStatus || 'empty',
rules: Array.isArray(item.rules) ? item.rules : [],
};
}
function mapApiPackToRuleYamlPack(item: RuleConfigPackApi): RuleYamlPack {
const ruleTypeCode = String(item.ruleType || '').trim();
// 业务类型必须以后端 pack 聚合返回的 mainType 为准。
@@ -101,6 +140,12 @@ async function fetchRuleConfigPayload<T>(request: Request, path: string): Promis
return payload.data;
}
export async function loadRuleConfigPackSummaries(request: Request): Promise<RuleConfigPackSummary[]> {
const items = await fetchRuleConfigPayload<Array<RuleConfigPackApi & { yamlName?: string; rules?: RuleSummary[] }>>(request, '/api/v3/rule-config-packs?summaryOnly=true');
return items.map(mapApiPackSummary);
}
export async function loadRuleConfigPacks(request: Request): Promise<RuleYamlPack[]> {
const items = await fetchRuleConfigPayload<RuleConfigPackApi[]>(request, '/api/v3/rule-config-packs');
return items.map(mapApiPackToRuleYamlPack);
+68 -24
View File
@@ -42,6 +42,7 @@ export type ExtractFieldSummary = {
name: string;
type: string;
multipleEntities: boolean;
allowed?: string[];
requiredFrom: string;
description: string;
};
@@ -89,9 +90,13 @@ export type RuleYamlPack = RulePackScope & {
name: string;
type: string;
required: string;
requiredFrom?: string;
signerRoles?: string[];
signatureTypes?: string[];
privateSealRestricted?: boolean;
expectedMatchField?: string;
expectedMatchAlternatives?: string[];
prompt?: string;
}>;
};
@@ -144,6 +149,13 @@ function stripYamlValue(value = ''): string {
return value.trim().replace(/^['"]|['"]$/g, '').replace(/\u0000/g, '');
}
function toStringList(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
return value.map((item) => String(item || '').trim()).filter(Boolean);
}
function parseScalar(section: string, key: string): string {
const match = section.match(new RegExp(`^\\s{2}${key}:\\s*(.*)$`, 'm'));
return stripYamlValue(match?.[1] || '');
@@ -400,38 +412,58 @@ export function parseRuleSummariesFromYaml(source: string): RuleSummary[] {
}
function parseTopLevelFields(source: string): ExtractFieldSummary[] {
const section = getTopLevelSection(source, 'extract');
const extractedFields = splitBlocks(section, /^\s*-\s+group:\s*/).flatMap(groupBlock => {
const group = stripYamlValue(groupBlock.match(/^\s*-\s+group:\s*(.+)$/m)?.[1] || '未分组');
return splitBlocks(groupBlock, /^\s*-\s+name:\s*/).flatMap(fieldBlock => {
const name = stripYamlValue(fieldBlock.match(/^\s*-\s+name:\s*(.+)$/m)?.[1] || '');
const rawType = stripYamlValue(fieldBlock.match(/^\s+type:\s*(.+)$/m)?.[1] || '-');
const parentField = {
const parsed = YAML.parse(source || '') as Record<string, unknown> | null;
const extractGroups = Array.isArray(parsed?.extract) ? parsed.extract : [];
const extractedFields = extractGroups.flatMap((groupNode) => {
if (!groupNode || typeof groupNode !== 'object') {
return [];
}
const groupObject = groupNode as Record<string, unknown>;
const group = String(groupObject.group || '未分组').trim() || '未分组';
const fields = Array.isArray(groupObject.fields) ? groupObject.fields : [];
return fields.flatMap((fieldNode) => {
if (!fieldNode || typeof fieldNode !== 'object') {
return [];
}
const field = fieldNode as Record<string, unknown>;
const name = String(field.name || '').trim();
if (!name) {
return [];
}
const rawType = String(field.type || '-').trim();
const requiredFrom = String(field.required_from || '-').trim() || '-';
const parentField: ExtractFieldSummary = {
id: `${group}-${name}`,
group,
name,
type: rawType === 'multi_entity' ? 'verbatim' : rawType,
multipleEntities: rawType === 'multi_entity',
requiredFrom: stripYamlValue(fieldBlock.match(/^\s+required_from:\s*(.+)$/m)?.[1] || '-'),
description: stripYamlValue(fieldBlock.match(/^\s+desc:\s*(.+)$/m)?.[1] || '')
allowed: toStringList(field.allowed),
requiredFrom,
description: String(field.desc || '').trim(),
};
const childFields = Array.from(fieldBlock.matchAll(/^\s{2,}-\s+name:\s*(.+)$/gm)).map(match => {
const childName = stripYamlValue(match[1]);
const start = fieldBlock.indexOf(match[0]);
const next = fieldBlock.slice(start + match[0].length).search(/^\s{2,}-\s+name:\s*/m);
const childBlock = next === -1 ? fieldBlock.slice(start) : fieldBlock.slice(start, start + match[0].length + next);
const childType = stripYamlValue(childBlock.match(/^\s+type:\s*(.+)$/m)?.[1] || 'verbatim');
return {
const childFields = Array.isArray(field.fields) ? field.fields : [];
const normalizedChildFields = childFields.flatMap((childNode) => {
if (!childNode || typeof childNode !== 'object') {
return [];
}
const childField = childNode as Record<string, unknown>;
const childName = String(childField.name || '').trim();
if (!childName) {
return [];
}
return [{
id: `${group}-${name}-${childName}`,
group,
name: `${name}[*].${childName}`,
type: childType,
type: String(childField.type || 'verbatim').trim() || 'verbatim',
multipleEntities: false,
requiredFrom: stripYamlValue(childBlock.match(/^\s+required_from:\s*(.+)$/m)?.[1] || parentField.requiredFrom),
description: stripYamlValue(childBlock.match(/^\s+desc:\s*(.+)$/m)?.[1] || `${name}的子字段`)
};
allowed: toStringList(childField.allowed),
requiredFrom: String(childField.required_from || requiredFrom).trim() || requiredFrom,
description: String(childField.desc || `${name}的子字段`).trim(),
}];
});
return [parentField, ...childFields];
return [parentField, ...normalizedChildFields];
});
}).filter(field => field.name);
@@ -493,6 +525,7 @@ function parseSubDocuments(source: string): SubDocumentSummary[] {
name,
type: rawType === 'multi_entity' ? 'verbatim' : rawType,
multipleEntities: rawType === 'multi_entity',
allowed: toStringList(field.allowed),
requiredFrom: '-',
description: String(field.desc || '').trim(),
}];
@@ -553,14 +586,25 @@ function parseVisualElements(source: string): RuleYamlPack['visualElements'] {
: []
);
const expectedMatch = node.expected_text_match && typeof node.expected_text_match === 'object'
? (node.expected_text_match as Record<string, unknown>)
: null;
const signatureTypes = key === 'seals'
? toStringList(node.allowed_types)
: toStringList(node.signature_types);
return [{
id,
name: String(node.name || id).trim(),
type: label,
required: String(node.required ?? '-').trim(),
signerRoles: toStringList(node.signer_roles),
signatureTypes: toStringList(node.signature_types),
privateSealRestricted: Boolean(node.private_seal_restricted),
requiredFrom: String(node.required_from ?? '').trim(),
signerRoles: key === 'signatures' ? toStringList(node.signer_roles) : [],
signatureTypes,
privateSealRestricted: key === 'signatures' ? Boolean(node.private_seal_restricted) : false,
expectedMatchField: String(expectedMatch?.field || '').trim(),
expectedMatchAlternatives: toStringList(expectedMatch?.alternatives),
prompt: String(node.prompt || '').trim(),
}];
});
});