feat: sync rule management and review ui fixes
This commit is contained in:
@@ -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)}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
}];
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user