保存规则库 YAML 维护改造进展
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
const permissionRouteAliases: Array<[RegExp, string]> = [
|
||||
[/^\/reviewsTest(?=\/|$)/, '/reviews'],
|
||||
[/^\/rulesTest\/list(?=\/|$)/, '/rules/list'],
|
||||
[/^\/rulesTest\/detail(?=\/|$)/, '/rules/new'],
|
||||
];
|
||||
|
||||
export function normalizeRoutePathForPermission(pathname: string): string {
|
||||
for (const [pattern, replacement] of permissionRouteAliases) {
|
||||
if (pattern.test(pathname)) {
|
||||
return pathname.replace(pattern, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
return pathname;
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
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<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 = field.group ? `字段抽取 / ${field.group}` : '字段抽取';
|
||||
const options = [{
|
||||
value: field.name,
|
||||
label: field.name,
|
||||
source,
|
||||
group: source
|
||||
}];
|
||||
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: source
|
||||
});
|
||||
}
|
||||
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 = `视觉要素 / ${item.type}`;
|
||||
return [
|
||||
{
|
||||
value: item.id,
|
||||
label,
|
||||
source,
|
||||
group: source
|
||||
},
|
||||
{
|
||||
value: item.name || item.id,
|
||||
label,
|
||||
source,
|
||||
group: source
|
||||
},
|
||||
{
|
||||
value: `visual.${item.id}`,
|
||||
label,
|
||||
source,
|
||||
group: source
|
||||
},
|
||||
{
|
||||
value: `visual.${item.name || item.id}`,
|
||||
label,
|
||||
source,
|
||||
group: source
|
||||
},
|
||||
{
|
||||
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`;
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
const LEAUDIT_RULES_ROOT = `${process.cwd()}/mock-data/leaudit-rules/packs/yc`;
|
||||
|
||||
export type RulePackScope = {
|
||||
documentType: string;
|
||||
moduleType: string;
|
||||
mainType: string;
|
||||
subtype: string;
|
||||
};
|
||||
|
||||
export type RuleSummary = {
|
||||
id: string;
|
||||
ruleId: string;
|
||||
name: string;
|
||||
group: string;
|
||||
risk: string;
|
||||
score: string;
|
||||
type: string;
|
||||
checkTypes: string[];
|
||||
logic: string;
|
||||
subRules: Array<{
|
||||
id: string;
|
||||
check: string;
|
||||
content: string;
|
||||
}>;
|
||||
subRuleIds: string[];
|
||||
scope: string[];
|
||||
dependencies: string[];
|
||||
stageCount: number;
|
||||
appliesIn: string[];
|
||||
prompt: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type ExtractFieldSummary = {
|
||||
id: string;
|
||||
group: string;
|
||||
name: string;
|
||||
type: string;
|
||||
multipleEntities: boolean;
|
||||
requiredFrom: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type SubDocumentSummary = {
|
||||
id: string;
|
||||
name: string;
|
||||
required: string;
|
||||
fieldCount: number;
|
||||
groups: string[];
|
||||
description: string;
|
||||
fields: ExtractFieldSummary[];
|
||||
};
|
||||
|
||||
export type RuleYamlPack = RulePackScope & {
|
||||
id: string;
|
||||
yamlPath: string | null;
|
||||
yamlSource: string;
|
||||
sourceStatus: 'ready' | 'empty' | 'missing';
|
||||
metadata: {
|
||||
typeId: string;
|
||||
name: string;
|
||||
version: string;
|
||||
lastUpdated: string;
|
||||
parent: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
keywords: string[];
|
||||
inheritsFrom: string[];
|
||||
};
|
||||
stats: {
|
||||
ruleCount: number;
|
||||
fieldCount: number;
|
||||
subDocumentCount: number;
|
||||
visualElementCount: number;
|
||||
};
|
||||
rules: RuleSummary[];
|
||||
fields: ExtractFieldSummary[];
|
||||
subDocuments: SubDocumentSummary[];
|
||||
visualElements: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
required: string;
|
||||
signerRoles?: string[];
|
||||
signatureTypes?: string[];
|
||||
privateSealRestricted?: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
const EMPTY_YAML = `metadata:
|
||||
type_id: pending.internal.document
|
||||
name: 内部公文规则配置
|
||||
version: '0.1'
|
||||
last_updated: '待配置'
|
||||
description: '当前暂无内部公文规则 YAML。此测试页面保留规则列表与配置页流程。'
|
||||
extract: []
|
||||
rules: []
|
||||
`;
|
||||
|
||||
const MOCK_RULE_PACKS: Array<RulePackScope & { id: string; yamlPath: string | null }> = [
|
||||
{ id: 'contract-purchase', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '买卖合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_purchase/rules.yaml` },
|
||||
{ id: 'contract-sale', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '通用买卖合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_sale/rules.yaml` },
|
||||
{ id: 'contract-tech', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '技术合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_tech/rules.yaml` },
|
||||
{ id: 'contract-lease', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '租赁合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_lease/rules.yaml` },
|
||||
{ id: 'contract-entrust', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '委托合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_entrust/rules.yaml` },
|
||||
{ id: 'contract-construction', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '建设工程合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_construction/rules.yaml` },
|
||||
{ id: 'contract-evaluation', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '委托评估合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_evaluation/rules.yaml` },
|
||||
{ id: 'contract-gift-general', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '赠与合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_gift_general/rules.yaml` },
|
||||
{ id: 'contract-gift-charity', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '公益捐赠合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_gift_charity/rules.yaml` },
|
||||
{ id: 'contract-loan', documentType: '合同', moduleType: '合同评查', mainType: '合同', subtype: '借款合同', yamlPath: `${LEAUDIT_RULES_ROOT}/contract_loan/rules.yaml` },
|
||||
{ id: 'case-penalty', documentType: '案卷', moduleType: '案卷评查', mainType: '行政处罚', subtype: '通用', yamlPath: `${LEAUDIT_RULES_ROOT}/行政处罚/rules.yaml` },
|
||||
{ id: 'case-license-new', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '新办', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_新办/rules.yaml` },
|
||||
{ id: 'case-license-extend', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '延续', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_延续/rules.yaml` },
|
||||
{ id: 'case-license-change', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '变更', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_变更/rules.yaml` },
|
||||
{ id: 'case-license-cancel', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '注销', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_注销/rules.yaml` },
|
||||
{ id: 'case-license-suspend', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '停业', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_停业/rules.yaml` },
|
||||
{ id: 'case-license-close', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '歇业', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_歇业/rules.yaml` },
|
||||
{ id: 'case-license-reissue', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '补办', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_补办/rules.yaml` },
|
||||
{ id: 'case-license-retrieve', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '收回', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_收回/rules.yaml` },
|
||||
{ id: 'case-license-restore', documentType: '案卷', moduleType: '案卷评查', mainType: '行政许可', subtype: '恢复营业', yamlPath: `${LEAUDIT_RULES_ROOT}/行政许可_恢复营业/rules.yaml` },
|
||||
{ id: 'internal-document', documentType: '内部公文', moduleType: '内部公文评查', mainType: '内部公文', subtype: '通用', yamlPath: null }
|
||||
];
|
||||
|
||||
function getTopLevelSection(source: string, key: string): string {
|
||||
const lines = source.split('\n');
|
||||
const start = lines.findIndex(line => line === `${key}:`);
|
||||
if (start === -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const end = lines.findIndex((line, index) => index > start && /^[a-zA-Z_][\w-]*:/.test(line));
|
||||
return lines.slice(start + 1, end === -1 ? undefined : end).join('\n');
|
||||
}
|
||||
|
||||
function stripYamlValue(value = ''): string {
|
||||
return value.trim().replace(/^['"]|['"]$/g, '').replace(/\u0000/g, '');
|
||||
}
|
||||
|
||||
function parseScalar(section: string, key: string): string {
|
||||
const match = section.match(new RegExp(`^\\s{2}${key}:\\s*(.*)$`, 'm'));
|
||||
return stripYamlValue(match?.[1] || '');
|
||||
}
|
||||
|
||||
function parseListAfterKey(section: string, key: string): string[] {
|
||||
const lines = section.split('\n');
|
||||
const start = lines.findIndex(line => new RegExp(`^\\s{2}${key}:\\s*$`).test(line));
|
||||
if (start === -1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const values: string[] = [];
|
||||
for (let index = start + 1; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
if (/^\s{2}\w/.test(line)) {
|
||||
break;
|
||||
}
|
||||
const match = line.match(/^\s{2,}-\s*(.+)$/);
|
||||
if (match) {
|
||||
values.push(stripYamlValue(match[1]));
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function parseMetadata(source: string): RuleYamlPack['metadata'] {
|
||||
const section = getTopLevelSection(source, 'metadata');
|
||||
return {
|
||||
typeId: parseScalar(section, 'type_id'),
|
||||
name: parseScalar(section, 'name'),
|
||||
version: parseScalar(section, 'version'),
|
||||
lastUpdated: parseScalar(section, 'last_updated'),
|
||||
parent: parseScalar(section, 'parent'),
|
||||
description: stripYamlValue((section.match(/^\s{2}description:\s*'?([\s\S]*?)(?:\n\s{2}\w|\n[a-zA-Z_]|\n?$)/m)?.[1] || '').replace(/\n\s+/g, ' ').trim()),
|
||||
tags: parseListAfterKey(section, 'tags'),
|
||||
keywords: parseListAfterKey(section, 'classification_keywords'),
|
||||
inheritsFrom: parseListAfterKey(section, 'inherits_from')
|
||||
};
|
||||
}
|
||||
|
||||
function splitBlocks(section: string, marker: RegExp): string[] {
|
||||
const lines = section.split('\n');
|
||||
const starts = lines.reduce<number[]>((indexes, line, index) => {
|
||||
if (marker.test(line)) {
|
||||
indexes.push(index);
|
||||
}
|
||||
return indexes;
|
||||
}, []);
|
||||
|
||||
return starts.map((start, index) => lines.slice(start, starts[index + 1]).join('\n'));
|
||||
}
|
||||
|
||||
function parseRules(source: string): RuleSummary[] {
|
||||
const section = getTopLevelSection(source, 'rules');
|
||||
const groups = splitBlocks(section, /^-\s+group:\s*/);
|
||||
const readExplicitDependencies = (block: string): string[] => {
|
||||
const lines = block.split('\n');
|
||||
const start = lines.findIndex(line => /^\s{4}dependencies:\s*$/.test(line));
|
||||
|
||||
if (start === -1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dependencies: string[] = [];
|
||||
for (let index = start + 1; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
if (/^\s{4}[a-zA-Z_][^:]*:\s*/.test(line)) {
|
||||
break;
|
||||
}
|
||||
const match = line.match(/^\s{4}-\s+(.+)$/);
|
||||
if (match) {
|
||||
dependencies.push(stripYamlValue(match[1]));
|
||||
}
|
||||
}
|
||||
return dependencies;
|
||||
};
|
||||
const normalizeDependency = (value: string) => {
|
||||
const normalized = stripYamlValue(value);
|
||||
if (normalized === 'cross_page_seal') return '骑缝章';
|
||||
if (normalized === 'seal') return '印章';
|
||||
if (normalized === 'signature') return '签名';
|
||||
return normalized;
|
||||
};
|
||||
const readPrompts = (block: string): string[] => {
|
||||
const lines = block.split('\n');
|
||||
const prompts: string[] = [];
|
||||
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const match = lines[index].match(/^(\s*)prompt:\s*(.*)$/);
|
||||
if (!match) continue;
|
||||
|
||||
const indent = match[1].length;
|
||||
const parts = [match[2]];
|
||||
for (let nextIndex = index + 1; nextIndex < lines.length; nextIndex += 1) {
|
||||
const line = lines[nextIndex];
|
||||
const nextIndent = line.match(/^\s*/)?.[0].length || 0;
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && nextIndent <= indent) break;
|
||||
if (trimmed && nextIndent === indent + 2 && /^[a-zA-Z_][\w-]*:\s*/.test(trimmed)) break;
|
||||
parts.push(line);
|
||||
}
|
||||
|
||||
prompts.push(parts.join('\n')
|
||||
.replace(/^['"]/, '')
|
||||
.replace(/['"]\s*$/, '')
|
||||
.split('\n')
|
||||
.map(line => line.replace(/^\s{8}/, ''))
|
||||
.join('\n')
|
||||
.trim());
|
||||
}
|
||||
|
||||
return prompts.filter(Boolean);
|
||||
};
|
||||
const readList = (block: string, key: string, indent = 4): string[] => {
|
||||
const lines = block.split('\n');
|
||||
const start = lines.findIndex(line => new RegExp(`^\\s{${indent}}${key}:\\s*$`).test(line));
|
||||
if (start === -1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const values: string[] = [];
|
||||
for (let index = start + 1; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
if (new RegExp(`^\\s{${indent}}[a-zA-Z_][^:]*:\\s*`).test(line)) {
|
||||
break;
|
||||
}
|
||||
const match = line.match(new RegExp(`^\\s{${indent}}-\\s+(.+)$`));
|
||||
if (match) {
|
||||
values.push(stripYamlValue(match[1]));
|
||||
}
|
||||
}
|
||||
return values;
|
||||
};
|
||||
const readFlexibleList = (block: string, key: string): string[] => {
|
||||
const lines = block.split('\n');
|
||||
const start = lines.findIndex(line => new RegExp(`^(\\s*)${key}:\\s*$`).test(line));
|
||||
if (start === -1) return [];
|
||||
const indent = lines[start].match(/^\s*/)?.[0].length || 0;
|
||||
const values: string[] = [];
|
||||
|
||||
for (let index = start + 1; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
const lineIndent = line.match(/^\s*/)?.[0].length || 0;
|
||||
const match = line.match(/^\s*-\s+(.+)$/);
|
||||
if (match) {
|
||||
values.push(stripYamlValue(match[1]));
|
||||
continue;
|
||||
}
|
||||
if (line.trim() && lineIndent <= indent) break;
|
||||
}
|
||||
|
||||
return values;
|
||||
};
|
||||
const readStageList = (block: string, key: string): string[] => {
|
||||
const lines = block.split('\n');
|
||||
const start = lines.findIndex(line => new RegExp(`^\\s{6}${key}:\\s*$`).test(line));
|
||||
if (start === -1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const values: string[] = [];
|
||||
for (let index = start + 1; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
if (/^\s{6}[a-zA-Z_][^:]*:\s*/.test(line)) {
|
||||
break;
|
||||
}
|
||||
const match = line.match(/^\s{6}-\s+(.+)$/);
|
||||
if (match) {
|
||||
values.push(stripYamlValue(match[1]));
|
||||
}
|
||||
}
|
||||
return values;
|
||||
};
|
||||
const readStageScalar = (block: string, key: string): string => stripYamlValue(block.match(new RegExp(`^\\s{6}${key}:\\s*(.+)$`, 'm'))?.[1] || '');
|
||||
const summarizeStage = (stageBlock: string): string => {
|
||||
const fields = readStageList(stageBlock, 'fields');
|
||||
const field = readStageScalar(stageBlock, 'field');
|
||||
const left = readStageScalar(stageBlock, 'left') || readStageScalar(stageBlock, 'left_field');
|
||||
const op = readStageScalar(stageBlock, 'op');
|
||||
const right = readStageScalar(stageBlock, 'right') || readStageScalar(stageBlock, 'right_field');
|
||||
const value = readStageScalar(stageBlock, 'value');
|
||||
const prompt = readStageScalar(stageBlock, 'prompt');
|
||||
const element = readStageScalar(stageBlock, 'element') || readStageScalar(stageBlock, 'seal_id') || readStageScalar(stageBlock, 'signature_id');
|
||||
|
||||
if (fields.length > 0) return fields.join('、');
|
||||
if (left || right) return [left, op, right].filter(Boolean).join(' ');
|
||||
if (field && value) return `${field} = ${value}`;
|
||||
if (field) return field;
|
||||
if (element) return element;
|
||||
if (prompt) return prompt.slice(0, 80);
|
||||
return stageBlock.split('\n').map(line => line.trim()).filter(Boolean).slice(1, 4).join(';') || '未配置内容';
|
||||
};
|
||||
const readSubRules = (block: string) => splitBlocks(block, /^\s{4}-\s+id:\s*/).map(stageBlock => {
|
||||
const id = stripYamlValue(stageBlock.match(/^\s{4}-\s+id:\s*(.+)$/m)?.[1] || '');
|
||||
const check = readStageScalar(stageBlock, 'check') || readStageScalar(stageBlock, 'type') || '-';
|
||||
return {
|
||||
id,
|
||||
check,
|
||||
content: summarizeStage(stageBlock)
|
||||
};
|
||||
}).filter(stage => stage.id);
|
||||
|
||||
return groups.flatMap(groupBlock => {
|
||||
const group = stripYamlValue(groupBlock.match(/^-\s+group:\s*(.+)$/m)?.[1] || '未分组');
|
||||
return splitBlocks(groupBlock, /^\s{2}-\s+rule_id:\s*/).map(ruleBlock => {
|
||||
const ruleId = stripYamlValue(ruleBlock.match(/^\s{2}-\s+rule_id:\s*(.+)$/m)?.[1] || '');
|
||||
const name = stripYamlValue(ruleBlock.match(/^\s{4}name:\s*(.+)$/m)?.[1] || '未命名规则');
|
||||
const checkTypes = Array.from(new Set(Array.from(ruleBlock.matchAll(/^\s{6,}(?:check|type):\s*(.+)$/gm)).map(match => stripYamlValue(match[1]))));
|
||||
const stageDependencies = Array.from(ruleBlock.matchAll(/^\s{6,}(?:field|number|chinese|left|right|left_field|right_field|target|element|seal_id|signature_id):\s*(.+)$/gm))
|
||||
.map(match => normalizeDependency(match[1]));
|
||||
const dependencies = Array.from(new Set([...readExplicitDependencies(ruleBlock), ...stageDependencies]));
|
||||
const scope = Array.from(new Set(Array.from(ruleBlock.matchAll(/^\s{4,}-\s*([^:\n]+)$/gm)).map(match => stripYamlValue(match[1])).filter(value => !/^\d+$/.test(value))));
|
||||
const prompts = readPrompts(ruleBlock);
|
||||
const subRules = readSubRules(ruleBlock);
|
||||
|
||||
return {
|
||||
id: ruleId || `${group}-${name}`,
|
||||
ruleId,
|
||||
name,
|
||||
group,
|
||||
risk: stripYamlValue(ruleBlock.match(/^\s{4}risk:\s*(.+)$/m)?.[1] || 'medium'),
|
||||
score: stripYamlValue(ruleBlock.match(/^\s{4}score:\s*(.+)$/m)?.[1] || '-'),
|
||||
type: stripYamlValue(ruleBlock.match(/^\s{4}type:\s*(.+)$/m)?.[1] || 'deterministic'),
|
||||
checkTypes,
|
||||
logic: stripYamlValue(ruleBlock.match(/^\s{4}logic:\s*(.+)$/m)?.[1] || ''),
|
||||
subRules,
|
||||
subRuleIds: readList(ruleBlock, 'rules'),
|
||||
scope: scope.slice(0, 8),
|
||||
dependencies: dependencies.slice(0, 8),
|
||||
stageCount: subRules.length,
|
||||
appliesIn: readFlexibleList(ruleBlock, 'applies_in'),
|
||||
prompt: prompts.join('\n\n'),
|
||||
description: stripYamlValue(ruleBlock.match(/^\s{4}desc:\s*(.+)$/m)?.[1] || '')
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function parseTopLevelFields(source: string): ExtractFieldSummary[] {
|
||||
const section = getTopLevelSection(source, 'extract');
|
||||
const extractedFields = splitBlocks(section, /^-\s+group:\s*/).flatMap(groupBlock => {
|
||||
const group = stripYamlValue(groupBlock.match(/^-\s+group:\s*(.+)$/m)?.[1] || '未分组');
|
||||
return splitBlocks(groupBlock, /^\s{2}-\s+name:\s*/).flatMap(fieldBlock => {
|
||||
const name = stripYamlValue(fieldBlock.match(/^\s{2}-\s+name:\s*(.+)$/m)?.[1] || '');
|
||||
const rawType = stripYamlValue(fieldBlock.match(/^\s{4}type:\s*(.+)$/m)?.[1] || '-');
|
||||
const parentField = {
|
||||
id: `${group}-${name}`,
|
||||
group,
|
||||
name,
|
||||
type: rawType === 'multi_entity' ? 'verbatim' : rawType,
|
||||
multipleEntities: rawType === 'multi_entity',
|
||||
requiredFrom: stripYamlValue(fieldBlock.match(/^\s{4}required_from:\s*(.+)$/m)?.[1] || '-'),
|
||||
description: stripYamlValue(fieldBlock.match(/^\s{4}desc:\s*(.+)$/m)?.[1] || '')
|
||||
};
|
||||
const childFields = Array.from(fieldBlock.matchAll(/^\s{4}-\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{4}-\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{6}type:\s*(.+)$/m)?.[1] || 'verbatim');
|
||||
return {
|
||||
id: `${group}-${name}-${childName}`,
|
||||
group,
|
||||
name: `${name}[*].${childName}`,
|
||||
type: childType,
|
||||
multipleEntities: false,
|
||||
requiredFrom: stripYamlValue(childBlock.match(/^\s{6}required_from:\s*(.+)$/m)?.[1] || parentField.requiredFrom),
|
||||
description: stripYamlValue(childBlock.match(/^\s{6}desc:\s*(.+)$/m)?.[1] || `${name}的子字段`)
|
||||
};
|
||||
});
|
||||
return [parentField, ...childFields];
|
||||
});
|
||||
}).filter(field => field.name);
|
||||
|
||||
const derivedSection = getTopLevelSection(source, 'derived_fields');
|
||||
const derivedFields = splitBlocks(derivedSection, /^-\s+name:\s*/).map(fieldBlock => {
|
||||
const name = stripYamlValue(fieldBlock.match(/^-\s+name:\s*(.+)$/m)?.[1] || '');
|
||||
return {
|
||||
id: `derived-${name}`,
|
||||
group: '派生字段',
|
||||
name,
|
||||
type: stripYamlValue(fieldBlock.match(/^\s{2}type:\s*(.+)$/m)?.[1] || 'computed'),
|
||||
multipleEntities: false,
|
||||
requiredFrom: '-',
|
||||
description: stripYamlValue(fieldBlock.match(/^\s{2}compute:\s*(.+)$/m)?.[1] || '由其他字段计算得出')
|
||||
};
|
||||
}).filter(field => field.name);
|
||||
|
||||
return [...extractedFields, ...derivedFields];
|
||||
}
|
||||
|
||||
function parseDocumentFields(docBlock: string, documentId: string): ExtractFieldSummary[] {
|
||||
return splitBlocks(docBlock, /^\s{2}-\s+group:\s*/).flatMap(groupBlock => {
|
||||
const group = stripYamlValue(groupBlock.match(/^\s{2}-\s+group:\s*(.+)$/m)?.[1] || '未分组');
|
||||
return splitBlocks(groupBlock, /^\s{4}-\s+name:\s*/).map(fieldBlock => {
|
||||
const name = stripYamlValue(fieldBlock.match(/^\s{4}-\s+name:\s*(.+)$/m)?.[1] || '');
|
||||
const rawType = stripYamlValue(fieldBlock.match(/^\s{6}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{6}desc:\s*(.+)$/m)?.[1] || '')
|
||||
};
|
||||
});
|
||||
}).filter(field => field.name);
|
||||
}
|
||||
|
||||
function parseSubDocuments(source: string): SubDocumentSummary[] {
|
||||
const section = getTopLevelSection(source, 'sub_documents');
|
||||
return splitBlocks(section, /^-\s+id:\s*/).map(docBlock => {
|
||||
const id = stripYamlValue(docBlock.match(/^-\s+id:\s*(.+)$/m)?.[1] || '');
|
||||
const groups = Array.from(new Set(Array.from(docBlock.matchAll(/^\s{2,}-\s+group:\s*(.+)$/gm)).map(match => stripYamlValue(match[1]))));
|
||||
const fields = parseDocumentFields(docBlock, id);
|
||||
const classifier = docBlock.match(/^\s{2}classifier:\s*$/m);
|
||||
let description = '';
|
||||
if (classifier) {
|
||||
const keywordsMatch = docBlock.match(/keywords:\s*\n((?:\s{4}-\s+.+\n)+)/m);
|
||||
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 {
|
||||
id,
|
||||
name: stripYamlValue(docBlock.match(/^\s{2}name:\s*(.+)$/m)?.[1] || id),
|
||||
required: stripYamlValue(docBlock.match(/^\s{2}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{2}${key}:`).test(line));
|
||||
if (start === -1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 找到下一个同级分类的起始位置(2空格+字母+冒号)
|
||||
let end = lines.length;
|
||||
for (let i = start + 1; i < lines.length; i++) {
|
||||
if (/^\s{2}[a-zA-Z_][\w-]*:/.test(lines[i])) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const subSection = lines.slice(start + 1, end).join('\n');
|
||||
return splitBlocks(subSection, /^\s{2}-\s+id:\s*/).map(block => ({
|
||||
id: stripYamlValue(block.match(/^\s{2}-\s+id:\s*(.+)$/m)?.[1] || ''),
|
||||
name: stripYamlValue(block.match(/^\s{4}name:\s*(.+)$/m)?.[1] || ''),
|
||||
type: label,
|
||||
required: stripYamlValue(block.match(/^\s{4}required:\s*(.+)$/m)?.[1] || '-'),
|
||||
signerRoles: block.match(/^\s{4}signer_roles:\s*\[(.+)\]$/m)?.[1].split(',').map(s => s.trim()) || [],
|
||||
signatureTypes: block.match(/^\s{4}signature_types:\s*\[(.+)\]$/m)?.[1].split(',').map(s => s.trim()) || [],
|
||||
privateSealRestricted: block.match(/^\s{4}private_seal_restricted:\s*(.+)$/m)?.[1] === 'true'
|
||||
}));
|
||||
}).filter(item => item.id);
|
||||
}
|
||||
|
||||
function buildPack(config: RulePackScope & { id: string; yamlPath: string | null }, yamlSource: string, sourceStatus: RuleYamlPack['sourceStatus']): RuleYamlPack {
|
||||
const metadata = parseMetadata(yamlSource);
|
||||
const fields = parseTopLevelFields(yamlSource);
|
||||
const subDocuments = parseSubDocuments(yamlSource);
|
||||
const rules = parseRules(yamlSource);
|
||||
const visualElements = parseVisualElements(yamlSource);
|
||||
|
||||
return {
|
||||
...config,
|
||||
yamlSource,
|
||||
sourceStatus,
|
||||
metadata,
|
||||
rules,
|
||||
fields,
|
||||
subDocuments,
|
||||
visualElements,
|
||||
stats: {
|
||||
ruleCount: rules.length,
|
||||
fieldCount: fields.length + subDocuments.reduce((sum, doc) => sum + doc.fieldCount, 0),
|
||||
subDocumentCount: subDocuments.length,
|
||||
visualElementCount: visualElements.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadRuleYamlPacks(): Promise<RuleYamlPack[]> {
|
||||
return Promise.all(MOCK_RULE_PACKS.map(async config => {
|
||||
// TODO(production-data-source):
|
||||
// 当前测试页直接读取 leaudit 本地 YAML 作为 mock。
|
||||
// 生产切换时,这里应改为调用后端接口:
|
||||
// 1. 后端根据文档类型/主类型/子类型查询数据库中的 OSS YAML 路径;
|
||||
// 2. 后端读取 OSS YAML 正文并返回元数据和内容;
|
||||
// 3. 前端仍消费 buildPack 之后的结构化数据,页面不直接关心 OSS 实现。
|
||||
if (!config.yamlPath) {
|
||||
return buildPack(config, EMPTY_YAML, 'empty');
|
||||
}
|
||||
|
||||
try {
|
||||
const yamlSource = await readFile(config.yamlPath, 'utf8');
|
||||
return buildPack(config, yamlSource, 'ready');
|
||||
} catch {
|
||||
return buildPack(config, EMPTY_YAML, 'missing');
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
export async function loadRuleYamlPack(id: string): Promise<RuleYamlPack | undefined> {
|
||||
const packs = await loadRuleYamlPacks();
|
||||
return packs.find(pack => pack.id === id);
|
||||
}
|
||||
Reference in New Issue
Block a user