fix: stabilize rule detail config persistence
This commit is contained in:
@@ -148,7 +148,9 @@ function matchesCurrentRuleDependency(currentRule: RuleSummary | undefined, cand
|
|||||||
if (deps.size === 0) return false;
|
if (deps.size === 0) return false;
|
||||||
return candidates.some((candidate) => {
|
return candidates.some((candidate) => {
|
||||||
const value = String(candidate || '').trim();
|
const value = String(candidate || '').trim();
|
||||||
return value ? deps.has(value) : false;
|
if (!value) return false;
|
||||||
|
if (deps.has(value)) return true;
|
||||||
|
return Array.from(deps).some((dependency) => dependency.startsWith(`${value}.`));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +189,13 @@ function makeId(prefix: string): string {
|
|||||||
return `${prefix}-${Date.now()}`;
|
return `${prefix}-${Date.now()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rewriteDependencyPrefix(dependency: string, from: string, to: string): string {
|
||||||
|
if (!from || !to || from === to) return dependency;
|
||||||
|
if (dependency === from) return to;
|
||||||
|
if (dependency.startsWith(`${from}.`)) return `${to}${dependency.slice(from.length)}`;
|
||||||
|
return dependency;
|
||||||
|
}
|
||||||
|
|
||||||
function emptyRuleDraft(group = '未分组'): RuleDraft {
|
function emptyRuleDraft(group = '未分组'): RuleDraft {
|
||||||
return {
|
return {
|
||||||
id: makeId('rule'),
|
id: makeId('rule'),
|
||||||
@@ -634,9 +643,13 @@ export default function RulesTestDetail() {
|
|||||||
() => versions.find((item) => !['published', 'rollback'].includes(item.status)),
|
() => versions.find((item) => !['published', 'rollback'].includes(item.status)),
|
||||||
[versions],
|
[versions],
|
||||||
);
|
);
|
||||||
|
const rollbackVersionOptions = useMemo(
|
||||||
|
() => versions,
|
||||||
|
[versions],
|
||||||
|
);
|
||||||
const rollbackOptions = useMemo(
|
const rollbackOptions = useMemo(
|
||||||
() => versions.filter((item) => item.id !== pack.currentVersionId),
|
() => rollbackVersionOptions.filter((item) => item.id !== pack.currentVersionId),
|
||||||
[versions, pack.currentVersionId],
|
[rollbackVersionOptions, pack.currentVersionId],
|
||||||
);
|
);
|
||||||
const rollbackTargetVersion = useMemo(
|
const rollbackTargetVersion = useMemo(
|
||||||
() => rollbackOptions.find((item) => String(item.id) === selectedRollbackVersionId) || rollbackOptions[0] || null,
|
() => rollbackOptions.find((item) => String(item.id) === selectedRollbackVersionId) || rollbackOptions[0] || null,
|
||||||
@@ -644,7 +657,15 @@ export default function RulesTestDetail() {
|
|||||||
);
|
);
|
||||||
const packFilterMainType = pack.businessType || pack.mainType;
|
const packFilterMainType = pack.businessType || pack.mainType;
|
||||||
const currentResolvedVersion = useMemo(
|
const currentResolvedVersion = useMemo(
|
||||||
() => versions.find((item) => item.id === pack.currentVersionId || item.id === pack.fallbackVersionId) || null,
|
() => {
|
||||||
|
if (pack.currentVersionId) {
|
||||||
|
return versions.find((item) => item.id === pack.currentVersionId) || null;
|
||||||
|
}
|
||||||
|
if (pack.fallbackVersionId) {
|
||||||
|
return versions.find((item) => item.id === pack.fallbackVersionId) || null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
[pack.currentVersionId, pack.fallbackVersionId, versions],
|
[pack.currentVersionId, pack.fallbackVersionId, versions],
|
||||||
);
|
);
|
||||||
const versionStatusLabel = (status: string | undefined) => {
|
const versionStatusLabel = (status: string | undefined) => {
|
||||||
@@ -743,6 +764,30 @@ export default function RulesTestDetail() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const patchCurrentRuleDependencies = (
|
||||||
|
replacements: Array<{ from: string; to: string }>,
|
||||||
|
appendedDependencies: string[],
|
||||||
|
) => {
|
||||||
|
if (!currentRule) return;
|
||||||
|
setRules((current) => current.map((rule) => {
|
||||||
|
if (rule.id !== currentRule.id) return rule;
|
||||||
|
const rewritten = rule.dependencies.map((dependency) => (
|
||||||
|
replacements.reduce(
|
||||||
|
(nextValue, item) => rewriteDependencyPrefix(nextValue, item.from, item.to),
|
||||||
|
dependency,
|
||||||
|
)
|
||||||
|
));
|
||||||
|
const merged = Array.from(new Set([
|
||||||
|
...rewritten,
|
||||||
|
...appendedDependencies.filter(Boolean),
|
||||||
|
]));
|
||||||
|
return {
|
||||||
|
...rule,
|
||||||
|
dependencies: merged,
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const saveRule = () => {
|
const saveRule = () => {
|
||||||
if (!editor || editor.kind !== 'rule') return;
|
if (!editor || editor.kind !== 'rule') return;
|
||||||
const existingRule = editor.id ? rules.find(rule => rule.id === editor.id) : undefined;
|
const existingRule = editor.id ? rules.find(rule => rule.id === editor.id) : undefined;
|
||||||
@@ -802,6 +847,9 @@ export default function RulesTestDetail() {
|
|||||||
|
|
||||||
const saveDocument = () => {
|
const saveDocument = () => {
|
||||||
if (!editor || editor.kind !== 'document') return;
|
if (!editor || editor.kind !== 'document') return;
|
||||||
|
const previousDocument = editor.mode === 'edit'
|
||||||
|
? subDocuments.find((document) => document.id === editor.id)
|
||||||
|
: undefined;
|
||||||
const normalizedDocument: SubDocumentSummary = {
|
const normalizedDocument: SubDocumentSummary = {
|
||||||
...documentDraft,
|
...documentDraft,
|
||||||
id: documentDraft.id || makeId('document'),
|
id: documentDraft.id || makeId('document'),
|
||||||
@@ -824,6 +872,13 @@ export default function RulesTestDetail() {
|
|||||||
setDraftSaved(false);
|
setDraftSaved(false);
|
||||||
setSaveMessage('');
|
setSaveMessage('');
|
||||||
setSaveError('');
|
setSaveError('');
|
||||||
|
patchCurrentRuleDependencies(
|
||||||
|
[
|
||||||
|
previousDocument ? { from: previousDocument.id, to: normalizedDocument.id } : null,
|
||||||
|
previousDocument ? { from: previousDocument.name, to: normalizedDocument.name } : null,
|
||||||
|
].filter(Boolean) as Array<{ from: string; to: string }>,
|
||||||
|
[normalizedDocument.id],
|
||||||
|
);
|
||||||
setEditor(null);
|
setEditor(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -873,6 +928,9 @@ export default function RulesTestDetail() {
|
|||||||
|
|
||||||
const saveVisual = () => {
|
const saveVisual = () => {
|
||||||
if (!editor || editor.kind !== 'visual') return;
|
if (!editor || editor.kind !== 'visual') return;
|
||||||
|
const previousVisual = editor.mode === 'edit'
|
||||||
|
? visualElements.find((item) => item.id === editor.id)
|
||||||
|
: undefined;
|
||||||
const normalizedVisual: VisualElementSummary = {
|
const normalizedVisual: VisualElementSummary = {
|
||||||
...visualDraft,
|
...visualDraft,
|
||||||
id: visualDraft.id || makeId('visual'),
|
id: visualDraft.id || makeId('visual'),
|
||||||
@@ -889,6 +947,15 @@ export default function RulesTestDetail() {
|
|||||||
setDraftSaved(false);
|
setDraftSaved(false);
|
||||||
setSaveMessage('');
|
setSaveMessage('');
|
||||||
setSaveError('');
|
setSaveError('');
|
||||||
|
patchCurrentRuleDependencies(
|
||||||
|
[
|
||||||
|
previousVisual ? { from: previousVisual.id, to: normalizedVisual.id } : null,
|
||||||
|
previousVisual ? { from: previousVisual.name, to: normalizedVisual.name } : null,
|
||||||
|
previousVisual ? { from: `visual.${previousVisual.id}`, to: `visual.${normalizedVisual.id}` } : null,
|
||||||
|
previousVisual ? { from: `visual.${previousVisual.name || previousVisual.id}`, to: `visual.${normalizedVisual.name || normalizedVisual.id}` } : null,
|
||||||
|
].filter(Boolean) as Array<{ from: string; to: string }>,
|
||||||
|
[`visual.${normalizedVisual.id}`],
|
||||||
|
);
|
||||||
setEditor(null);
|
setEditor(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1027,13 +1094,13 @@ export default function RulesTestDetail() {
|
|||||||
className="rules-version-select"
|
className="rules-version-select"
|
||||||
value={rollbackTargetVersion ? String(rollbackTargetVersion.id) : selectedRollbackVersionId}
|
value={rollbackTargetVersion ? String(rollbackTargetVersion.id) : selectedRollbackVersionId}
|
||||||
onChange={(event) => setSelectedRollbackVersionId(event.target.value)}
|
onChange={(event) => setSelectedRollbackVersionId(event.target.value)}
|
||||||
disabled={saveButtonBusy || rollbackOptions.length === 0}
|
disabled={saveButtonBusy || rollbackVersionOptions.length === 0}
|
||||||
>
|
>
|
||||||
{rollbackOptions.length === 0 ? (
|
{rollbackVersionOptions.length === 0 ? (
|
||||||
<option value="">暂无可回滚版本</option>
|
<option value="">暂无可回滚版本</option>
|
||||||
) : rollbackOptions.map((item) => (
|
) : rollbackVersionOptions.map((item) => (
|
||||||
<option key={item.id} value={item.id}>
|
<option key={item.id} value={item.id} disabled={item.id === pack.currentVersionId}>
|
||||||
{item.versionNo} · {versionStatusLabel(item.status)}
|
{item.versionNo} · {versionStatusLabel(item.status)}{item.id === pack.currentVersionId ? ' · 当前版本' : ''}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import YAML from 'yaml';
|
||||||
|
|
||||||
const LEAUDIT_RULES_ROOT = `${process.cwd()}/mock-data/leaudit-rules/packs/yc`;
|
const LEAUDIT_RULES_ROOT = `${process.cwd()}/mock-data/leaudit-rules/packs/yc`;
|
||||||
|
|
||||||
@@ -451,87 +452,118 @@ function parseTopLevelFields(source: string): ExtractFieldSummary[] {
|
|||||||
return [...extractedFields, ...derivedFields];
|
return [...extractedFields, ...derivedFields];
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDocumentFields(docBlock: string, documentId: string): ExtractFieldSummary[] {
|
|
||||||
return splitBlocks(docBlock, /^\s*-\s+group:\s*/).flatMap(groupBlock => {
|
|
||||||
const group = stripYamlValue(groupBlock.match(/^\s*-\s+group:\s*(.+)$/m)?.[1] || '未分组');
|
|
||||||
return splitBlocks(groupBlock, /^\s*-\s+name:\s*/).map(fieldBlock => {
|
|
||||||
const name = stripYamlValue(fieldBlock.match(/^\s*-\s+name:\s*(.+)$/m)?.[1] || '');
|
|
||||||
const rawType = stripYamlValue(fieldBlock.match(/^\s+type:\s*(.+)$/m)?.[1] || '-');
|
|
||||||
return {
|
|
||||||
id: `${documentId}-${group}-${name}`,
|
|
||||||
group,
|
|
||||||
name,
|
|
||||||
type: rawType === 'multi_entity' ? 'verbatim' : rawType,
|
|
||||||
multipleEntities: rawType === 'multi_entity',
|
|
||||||
requiredFrom: '-',
|
|
||||||
description: stripYamlValue(fieldBlock.match(/^\s+desc:\s*(.+)$/m)?.[1] || '')
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}).filter(field => field.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseSubDocuments(source: string): SubDocumentSummary[] {
|
function parseSubDocuments(source: string): SubDocumentSummary[] {
|
||||||
const section = getTopLevelSection(source, 'sub_documents');
|
const parsed = YAML.parse(source || '') as Record<string, unknown> | null;
|
||||||
return splitBlocks(section, /^\s*-\s+id:\s*/).map(docBlock => {
|
const subDocuments = parsed?.sub_documents;
|
||||||
const id = stripYamlValue(docBlock.match(/^\s*-\s+id:\s*(.+)$/m)?.[1] || '');
|
if (!Array.isArray(subDocuments)) {
|
||||||
const groups = Array.from(new Set(Array.from(docBlock.matchAll(/^\s*-\s+group:\s*(.+)$/gm)).map(match => stripYamlValue(match[1]))));
|
return [];
|
||||||
const fields = parseDocumentFields(docBlock, id);
|
}
|
||||||
const classifier = docBlock.match(/^\s{2}classifier:\s*$/m);
|
|
||||||
let description = '';
|
return subDocuments.flatMap((documentNode) => {
|
||||||
if (classifier) {
|
if (!documentNode || typeof documentNode !== 'object') {
|
||||||
const keywordsMatch = docBlock.match(/keywords:\s*\n((?:\s{4}-\s+.+\n)+)/m);
|
return [];
|
||||||
if (keywordsMatch) {
|
|
||||||
const keywords = Array.from(keywordsMatch[1].matchAll(/^\s{4}-\s+(.+)$/gm)).map(match => stripYamlValue(match[1])).slice(0, 3);
|
|
||||||
description = keywords.join('、');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
const document = documentNode as Record<string, unknown>;
|
||||||
id,
|
const id = String(document.id || '').trim();
|
||||||
name: stripYamlValue(docBlock.match(/^\s+name:\s*(.+)$/m)?.[1] || id),
|
if (!id) {
|
||||||
required: stripYamlValue(docBlock.match(/^\s+required:\s*(.+)$/m)?.[1] || '-'),
|
|
||||||
fieldCount: fields.length,
|
|
||||||
groups,
|
|
||||||
description,
|
|
||||||
fields
|
|
||||||
};
|
|
||||||
}).filter(doc => doc.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseVisualElements(source: string): RuleYamlPack['visualElements'] {
|
|
||||||
const section = getTopLevelSection(source, 'visual_elements');
|
|
||||||
const typedSections = [
|
|
||||||
{ key: 'seals', label: '签章' },
|
|
||||||
{ key: 'signatures', label: '签名' },
|
|
||||||
{ key: 'cross_page_seals', label: '骑缝章' }
|
|
||||||
];
|
|
||||||
|
|
||||||
return typedSections.flatMap(({ key, label }) => {
|
|
||||||
const lines = section.split('\n');
|
|
||||||
const start = lines.findIndex(line => new RegExp(`^\\s+${key}:`).test(line));
|
|
||||||
if (start === -1) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找到下一个同级分类的起始位置(2空格+字母+冒号)
|
const extractGroups = Array.isArray(document.extract) ? document.extract : [];
|
||||||
let end = lines.length;
|
const fields = extractGroups.flatMap((groupNode) => {
|
||||||
for (let i = start + 1; i < lines.length; i++) {
|
if (!groupNode || typeof groupNode !== 'object') {
|
||||||
if (/^\s+[a-zA-Z_][\w-]*:/.test(lines[i])) {
|
return [];
|
||||||
end = i;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
const groupObject = groupNode as Record<string, unknown>;
|
||||||
|
const group = String(groupObject.group || '未分组').trim() || '未分组';
|
||||||
|
const groupFields = Array.isArray(groupObject.fields) ? groupObject.fields : [];
|
||||||
|
return groupFields.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();
|
||||||
|
return [{
|
||||||
|
id: `${id}-${group}-${name}`,
|
||||||
|
group,
|
||||||
|
name,
|
||||||
|
type: rawType === 'multi_entity' ? 'verbatim' : rawType,
|
||||||
|
multipleEntities: rawType === 'multi_entity',
|
||||||
|
requiredFrom: '-',
|
||||||
|
description: String(field.desc || '').trim(),
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const groups = Array.from(new Set(fields.map((field) => field.group).filter(Boolean)));
|
||||||
|
const classifier = document.classifier && typeof document.classifier === 'object'
|
||||||
|
? (document.classifier as Record<string, unknown>)
|
||||||
|
: null;
|
||||||
|
const description = Array.isArray(classifier?.keywords)
|
||||||
|
? classifier!.keywords.map((item) => String(item || '').trim()).filter(Boolean).slice(0, 3).join('、')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return [{
|
||||||
|
id,
|
||||||
|
name: String(document.name || id).trim(),
|
||||||
|
required: String(document.required ?? '-').trim(),
|
||||||
|
fieldCount: fields.length,
|
||||||
|
groups,
|
||||||
|
description,
|
||||||
|
fields,
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVisualElements(source: string): RuleYamlPack['visualElements'] {
|
||||||
|
const parsed = YAML.parse(source || '') as Record<string, unknown> | null;
|
||||||
|
const visualRoot = parsed?.visual_elements;
|
||||||
|
if (!visualRoot || typeof visualRoot !== 'object') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const typedSections = [
|
||||||
|
{ key: 'seals', label: '签章' },
|
||||||
|
{ key: 'signatures', label: '签名' },
|
||||||
|
{ key: 'cross_page_seals', label: '骑缝章' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
return typedSections.flatMap(({ key, label }) => {
|
||||||
|
const bucket = (visualRoot as Record<string, unknown>)[key];
|
||||||
|
if (!Array.isArray(bucket)) {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const subSection = lines.slice(start + 1, end).join('\n');
|
return bucket.flatMap((item) => {
|
||||||
return splitBlocks(subSection, /^\s*-\s+id:\s*/).map(block => ({
|
if (!item || typeof item !== 'object') {
|
||||||
id: stripYamlValue(block.match(/^\s*-\s+id:\s*(.+)$/m)?.[1] || ''),
|
return [];
|
||||||
name: stripYamlValue(block.match(/^\s+name:\s*(.+)$/m)?.[1] || ''),
|
}
|
||||||
type: label,
|
const node = item as Record<string, unknown>;
|
||||||
required: stripYamlValue(block.match(/^\s+required:\s*(.+)$/m)?.[1] || '-'),
|
const id = String(node.id || '').trim();
|
||||||
signerRoles: block.match(/^\s+signer_roles:\s*\[(.+)\]$/m)?.[1].split(',').map(s => s.trim()) || [],
|
if (!id) {
|
||||||
signatureTypes: block.match(/^\s+signature_types:\s*\[(.+)\]$/m)?.[1].split(',').map(s => s.trim()) || [],
|
return [];
|
||||||
privateSealRestricted: block.match(/^\s+private_seal_restricted:\s*(.+)$/m)?.[1] === 'true'
|
}
|
||||||
}));
|
const toStringList = (value: unknown): string[] => (
|
||||||
}).filter(item => item.id);
|
Array.isArray(value)
|
||||||
|
? value.map((entry) => String(entry || '').trim()).filter(Boolean)
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
|
||||||
|
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),
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildRuleYamlPack(
|
export function buildRuleYamlPack(
|
||||||
|
|||||||
Reference in New Issue
Block a user