@liuhange/dsh-gov-data-inspector 3.1.3 → 3.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cordis.patch.yml +9 -0
- package/lib/__tests__/exceptionInput.test.js +180 -0
- package/lib/__tests__/semanticRuleEngine.test.js +44 -9
- package/lib/dataScaleGuard.js +19 -0
- package/lib/degradedModeController.js +21 -0
- package/lib/encodingDetector.js +31 -0
- package/lib/endpointResolver.js +21 -0
- package/lib/index.js +98 -25
- package/lib/inspectionOrchestrator.js +8 -3
- package/lib/knowledgeBaseLoader.js +9 -4
- package/lib/nationalDataSource.js +34 -19
- package/lib/provincialDataSource.js +34 -19
- package/lib/semanticRuleEngine.js +41 -12
- package/lib/unmatchedWarningBuilder.js +10 -0
- package/lib/urlReachabilityChecker.js +28 -0
- package/package.json +3 -2
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Cordis Framework Patch Declaration for @liuhange/dsh-gov-data-inspector
|
|
2
|
+
# This file declares the plugin's runtime patch for the Cordis framework.
|
|
3
|
+
# Used by DSH plugin manager to apply framework-level patches during installation.
|
|
4
|
+
plugin: @liuhange/dsh-gov-data-inspector
|
|
5
|
+
version: 3.1.2
|
|
6
|
+
patch:
|
|
7
|
+
- target: tools
|
|
8
|
+
action: register
|
|
9
|
+
description: Register plugin tools into Cordis runtime
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
const tmpDir = resolve(tmpdir(), 'gov-data-inspector-test');
|
|
6
|
+
const badJsonPath = resolve(tmpDir, 'bad.json');
|
|
7
|
+
const emptyPath = resolve(tmpDir, 'empty.json');
|
|
8
|
+
const emptyArrayPath = resolve(tmpDir, 'empty-array.json');
|
|
9
|
+
const nonUtf8Path = resolve(tmpDir, 'non-utf8.json');
|
|
10
|
+
const partialDataPath = resolve(tmpDir, 'partial.json');
|
|
11
|
+
const validDataPath = resolve(tmpDir, 'valid.json');
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
14
|
+
writeFileSync(badJsonPath, '{ "事项名称": "测试", invalid json }', 'utf-8');
|
|
15
|
+
writeFileSync(emptyPath, '', 'utf-8');
|
|
16
|
+
writeFileSync(emptyArrayPath, '[]', 'utf-8');
|
|
17
|
+
const gbkBuffer = Buffer.from([0xB2, 0xE2, 0xCA, 0xD4]);
|
|
18
|
+
writeFileSync(nonUtf8Path, gbkBuffer);
|
|
19
|
+
writeFileSync(partialDataPath, JSON.stringify([{ 事项名称: '测试事项' }]), 'utf-8');
|
|
20
|
+
writeFileSync(validDataPath, JSON.stringify([{ 事项名称: '食品经营许可', 办理时限: '20个工作日' }]), 'utf-8');
|
|
21
|
+
});
|
|
22
|
+
afterAll(() => {
|
|
23
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
async function getExecute() {
|
|
26
|
+
const mod = await import('../index.js');
|
|
27
|
+
const tools = [];
|
|
28
|
+
mod.apply({ tools: { register: (t) => tools.push(t) } });
|
|
29
|
+
const tool = tools[0];
|
|
30
|
+
return tool.execute;
|
|
31
|
+
}
|
|
32
|
+
describe('异常输入处理', () => {
|
|
33
|
+
it('格式错误的JSON → 返回GOV_DATA_INPUT_INVALID', async () => {
|
|
34
|
+
const execute = await getExecute();
|
|
35
|
+
const result = JSON.parse(await execute({ dataSource: badJsonPath }));
|
|
36
|
+
expect(result.error).toBe('GOV_DATA_INPUT_INVALID');
|
|
37
|
+
expect(result.message).toContain('JSON 格式错误');
|
|
38
|
+
});
|
|
39
|
+
it('空数据(空文件) → 返回无数据提示', async () => {
|
|
40
|
+
const execute = await getExecute();
|
|
41
|
+
const result = JSON.parse(await execute({ dataSource: emptyPath }));
|
|
42
|
+
expect(result.error).toBe('GOV_DATA_INPUT_INVALID');
|
|
43
|
+
expect(result.message).toContain('无数据');
|
|
44
|
+
});
|
|
45
|
+
it('空数据(空数组) → 返回无数据提示', async () => {
|
|
46
|
+
const execute = await getExecute();
|
|
47
|
+
const result = JSON.parse(await execute({ dataSource: emptyArrayPath }));
|
|
48
|
+
expect(result.error).toBe('GOV_DATA_INPUT_INVALID');
|
|
49
|
+
expect(result.message).toContain('无数据');
|
|
50
|
+
});
|
|
51
|
+
it('无效URL → 返回不可达提示而非崩溃', async () => {
|
|
52
|
+
const execute = await getExecute();
|
|
53
|
+
const result = JSON.parse(await execute({ dataSource: 'http://invalid.localhost.nonexistent:99999/data.json' }));
|
|
54
|
+
expect(result.error).toBeDefined();
|
|
55
|
+
expect(result.message).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
it('部分字段缺失的数据 → 正常处理并报告缺失字段', async () => {
|
|
58
|
+
const execute = await getExecute();
|
|
59
|
+
const result = JSON.parse(await execute({ dataSource: partialDataPath, inspectionMode: 'guide' }));
|
|
60
|
+
expect(result.error).toBeUndefined();
|
|
61
|
+
expect(result.guideInspection).toBeDefined();
|
|
62
|
+
expect(result.guideInspection.missingFields).toBeGreaterThan(0);
|
|
63
|
+
});
|
|
64
|
+
it('非UTF-8编码文件 → 返回编码错误提示', async () => {
|
|
65
|
+
const execute = await getExecute();
|
|
66
|
+
const result = JSON.parse(await execute({ dataSource: nonUtf8Path }));
|
|
67
|
+
expect(result.error).toBe('GOV_DATA_ENCODING_ERROR');
|
|
68
|
+
expect(result.message).toContain('UTF-8');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('EncodingDetector', () => {
|
|
72
|
+
it('UTF-8文件正常读取', async () => {
|
|
73
|
+
const { EncodingDetector } = await import('../encodingDetector.js');
|
|
74
|
+
const result = EncodingDetector.detect(validDataPath);
|
|
75
|
+
expect(result.content).not.toBeNull();
|
|
76
|
+
expect(result.encodingError).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
it('非UTF-8文件返回编码错误', async () => {
|
|
79
|
+
const { EncodingDetector } = await import('../encodingDetector.js');
|
|
80
|
+
const result = EncodingDetector.detect(nonUtf8Path);
|
|
81
|
+
expect(result.content).toBeNull();
|
|
82
|
+
expect(result.encodingError).toContain('UTF-8');
|
|
83
|
+
});
|
|
84
|
+
it('不存在的文件返回读取错误', async () => {
|
|
85
|
+
const { EncodingDetector } = await import('../encodingDetector.js');
|
|
86
|
+
const result = EncodingDetector.detect(resolve(tmpDir, 'nonexistent.json'));
|
|
87
|
+
expect(result.content).toBeNull();
|
|
88
|
+
expect(result.encodingError).toContain('无法读取');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('DataScaleGuard', () => {
|
|
92
|
+
it('正常规模数据通过', async () => {
|
|
93
|
+
const { DataScaleGuard } = await import('../dataScaleGuard.js');
|
|
94
|
+
const data = [{ a: 1, b: 2 }, { c: 3 }];
|
|
95
|
+
const result = DataScaleGuard.check(data);
|
|
96
|
+
expect(result.exceeded).toBe(false);
|
|
97
|
+
expect(result.fieldCount).toBe(3);
|
|
98
|
+
});
|
|
99
|
+
it('超100万字段数据返回exceeded=true', async () => {
|
|
100
|
+
const { DataScaleGuard } = await import('../dataScaleGuard.js');
|
|
101
|
+
const data = [];
|
|
102
|
+
for (let i = 0; i < 100001; i++)
|
|
103
|
+
data.push({ f: i });
|
|
104
|
+
const result = DataScaleGuard.check(data, 100000);
|
|
105
|
+
expect(result.exceeded).toBe(true);
|
|
106
|
+
expect(result.fieldCount).toBeGreaterThan(100000);
|
|
107
|
+
});
|
|
108
|
+
it('自定义上限', async () => {
|
|
109
|
+
const { DataScaleGuard } = await import('../dataScaleGuard.js');
|
|
110
|
+
const result = DataScaleGuard.check([{ a: 1, b: 2, c: 3 }], 2);
|
|
111
|
+
expect(result.exceeded).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('UrlReachabilityChecker', () => {
|
|
115
|
+
it('本地文件存在 → reachable=true', async () => {
|
|
116
|
+
const { UrlReachabilityChecker } = await import('../urlReachabilityChecker.js');
|
|
117
|
+
const result = await UrlReachabilityChecker.check(validDataPath);
|
|
118
|
+
expect(result.reachable).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
it('不存在的文件 → reachable=false', async () => {
|
|
121
|
+
const { UrlReachabilityChecker } = await import('../urlReachabilityChecker.js');
|
|
122
|
+
const result = await UrlReachabilityChecker.check(resolve(tmpDir, 'nonexistent.json'));
|
|
123
|
+
expect(result.reachable).toBe(false);
|
|
124
|
+
expect(result.error).toBeDefined();
|
|
125
|
+
});
|
|
126
|
+
it('空路径 → reachable=false', async () => {
|
|
127
|
+
const { UrlReachabilityChecker } = await import('../urlReachabilityChecker.js');
|
|
128
|
+
const result = await UrlReachabilityChecker.check('');
|
|
129
|
+
expect(result.reachable).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('EndpointResolver', () => {
|
|
133
|
+
it('HTTPS URL → kind=https', async () => {
|
|
134
|
+
const { EndpointResolver } = await import('../endpointResolver.js');
|
|
135
|
+
const result = EndpointResolver.resolve('https://api.example.com/data');
|
|
136
|
+
expect(result?.kind).toBe('https');
|
|
137
|
+
});
|
|
138
|
+
it('HTTP URL → kind=http', async () => {
|
|
139
|
+
const { EndpointResolver } = await import('../endpointResolver.js');
|
|
140
|
+
const result = EndpointResolver.resolve('http://api.example.com/data');
|
|
141
|
+
expect(result?.kind).toBe('http');
|
|
142
|
+
});
|
|
143
|
+
it('本地文件路径 → kind=file', async () => {
|
|
144
|
+
const { EndpointResolver } = await import('../endpointResolver.js');
|
|
145
|
+
const result = EndpointResolver.resolve(validDataPath);
|
|
146
|
+
expect(result?.kind).toBe('file');
|
|
147
|
+
});
|
|
148
|
+
it('相对路径 → kind=file', async () => {
|
|
149
|
+
const { EndpointResolver } = await import('../endpointResolver.js');
|
|
150
|
+
const result = EndpointResolver.resolve('./data.json');
|
|
151
|
+
expect(result?.kind).toBe('file');
|
|
152
|
+
});
|
|
153
|
+
it('非法字符串 → null', async () => {
|
|
154
|
+
const { EndpointResolver } = await import('../endpointResolver.js');
|
|
155
|
+
const result = EndpointResolver.resolve('not-a-valid-endpoint');
|
|
156
|
+
expect(result).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
describe('DegradedModeController', () => {
|
|
160
|
+
it('activate返回degradedMode=true', async () => {
|
|
161
|
+
const { DegradedModeController } = await import('../degradedModeController.js');
|
|
162
|
+
const result = DegradedModeController.activate('测试降级原因');
|
|
163
|
+
expect(result.degradedMode).toBe(true);
|
|
164
|
+
expect(result.degradedReason).toBe('测试降级原因');
|
|
165
|
+
expect(result.localKb).toBeDefined();
|
|
166
|
+
expect(result.localKb.timeLimits).toEqual([]);
|
|
167
|
+
expect(result.localKb.materials).toEqual([]);
|
|
168
|
+
expect(result.localKb.conditions).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
describe('UnmatchedWarningBuilder', () => {
|
|
172
|
+
it('build返回正确警告结构', async () => {
|
|
173
|
+
const { UnmatchedWarningBuilder } = await import('../unmatchedWarningBuilder.js');
|
|
174
|
+
const warning = UnmatchedWarningBuilder.build('guide-001');
|
|
175
|
+
expect(warning.type).toBe('UNMATCHED_ITEM_TYPE');
|
|
176
|
+
expect(warning.guideId).toBe('guide-001');
|
|
177
|
+
expect(warning.message).toContain('未匹配到检测规则');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
//# sourceMappingURL=exceptionInput.test.js.map
|
|
@@ -25,27 +25,62 @@ const itemTypeMatching = {
|
|
|
25
25
|
describe('SemanticRuleEngine', () => {
|
|
26
26
|
it('时限为"90个工作日"且法定上限为20 → 输出semantic错误', () => {
|
|
27
27
|
const guide = { 事项名称: '食品经营许可', 办理时限: '90个工作日' };
|
|
28
|
-
const
|
|
29
|
-
const timeError = details.find((d) => d.field === '办理时限');
|
|
28
|
+
const result = SemanticRuleEngine.detect(guide, 'g1', kb, itemTypeMatching);
|
|
29
|
+
const timeError = result.details.find((d) => d.field === '办理时限');
|
|
30
30
|
expect(timeError).toBeDefined();
|
|
31
31
|
expect(timeError.errorType).toBe('semantic');
|
|
32
32
|
expect(timeError.suggestion).toContain('20');
|
|
33
33
|
});
|
|
34
34
|
it('材料含"营业执照复映件"且标准为"营业执照复印件" → 输出semantic错误', () => {
|
|
35
35
|
const guide = { 事项名称: '食品经营许可', 申请材料: '营业执照复映件、行政许可申请表' };
|
|
36
|
-
const
|
|
37
|
-
const matError = details.find((d) => d.field === '申请材料');
|
|
36
|
+
const result = SemanticRuleEngine.detect(guide, 'g1', kb, itemTypeMatching);
|
|
37
|
+
const matError = result.details.find((d) => d.field === '申请材料');
|
|
38
38
|
expect(matError).toBeDefined();
|
|
39
39
|
expect(matError.errorType).toBe('semantic');
|
|
40
40
|
expect(matError.suggestion).toContain('营业执照复印件');
|
|
41
41
|
});
|
|
42
|
-
it('事项名称在知识库无对应条目 →
|
|
42
|
+
it('事项名称在知识库无对应条目 → 返回unmatched警告', () => {
|
|
43
43
|
const guide = { 事项名称: '未知事项类型', 办理时限: '90个工作日' };
|
|
44
|
-
const
|
|
45
|
-
expect(details.length).toBe(0);
|
|
44
|
+
const result = SemanticRuleEngine.detect(guide, 'g1', kb, itemTypeMatching);
|
|
45
|
+
expect(result.details.length).toBe(0);
|
|
46
|
+
expect(result.unmatched).toBe(true);
|
|
47
|
+
expect(result.warning).toBeDefined();
|
|
48
|
+
expect(result.warning.type).toBe('UNMATCHED_ITEM_TYPE');
|
|
46
49
|
});
|
|
47
|
-
it('
|
|
48
|
-
expect(() => SemanticRuleEngine.detect({}, 'g1', {}, itemTypeMatching)).toThrow();
|
|
50
|
+
it('标准知识库不可用且非降级 → 抛出GOV_DATA_KB_MISSING', () => {
|
|
51
|
+
expect(() => SemanticRuleEngine.detect({ 事项名称: '食品经营许可' }, 'g1', {}, itemTypeMatching)).toThrow();
|
|
52
|
+
});
|
|
53
|
+
it('标准知识库不可用且降级模式 → 返回空details不抛错', () => {
|
|
54
|
+
const result = SemanticRuleEngine.detect({ 事项名称: '食品经营许可' }, 'g1', {}, itemTypeMatching, undefined, { degradedMode: true });
|
|
55
|
+
expect(result.details).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
it('流程字段含关键词可匹配事项类型', () => {
|
|
58
|
+
const guide = { 事项名称: '建设工程规划核实', 办理流程: '受理→现场勘查→核实→出证', 办理时限: '90个工作日' };
|
|
59
|
+
const itemTypeMatchingWithFlow = {
|
|
60
|
+
现场勘查类: { keywords: ['现场勘查', '实地核查'], codePrefix: 'XC' },
|
|
61
|
+
};
|
|
62
|
+
const result = SemanticRuleEngine.detect(guide, 'g1', kb, itemTypeMatchingWithFlow);
|
|
63
|
+
expect(result.unmatched).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
it('itemTypeOverride手动指定事项类型', () => {
|
|
66
|
+
const guide = { 事项名称: '建设工程规划核实', 办理时限: '90个工作日' };
|
|
67
|
+
const result = SemanticRuleEngine.detect(guide, 'g1', kb, itemTypeMatching, undefined, { itemTypeOverride: '行政许可' });
|
|
68
|
+
expect(result.unmatched).toBeUndefined();
|
|
69
|
+
const timeError = result.details.find((d) => d.field === '办理时限');
|
|
70
|
+
expect(timeError).toBeDefined();
|
|
71
|
+
});
|
|
72
|
+
it('matchItemType返回matchSource', () => {
|
|
73
|
+
const nameMatch = SemanticRuleEngine.matchItemType({ 事项名称: '食品经营许可' }, itemTypeMatching);
|
|
74
|
+
expect(nameMatch.matchSource).toBe('name');
|
|
75
|
+
const codeMatch = SemanticRuleEngine.matchItemType({ 事项编码: 'XK001' }, itemTypeMatching);
|
|
76
|
+
expect(codeMatch.matchSource).toBe('code');
|
|
77
|
+
const flowMatch = SemanticRuleEngine.matchItemType({ 事项名称: '测试', 办理流程: '现场勘查' }, { 现场勘查类: { keywords: ['现场勘查'], codePrefix: 'XC' } });
|
|
78
|
+
expect(flowMatch.matchSource).toBe('flow');
|
|
79
|
+
const manualMatch = SemanticRuleEngine.matchItemType({ 事项名称: '测试' }, itemTypeMatching, { itemTypeOverride: '行政许可' });
|
|
80
|
+
expect(manualMatch.matchSource).toBe('manual');
|
|
81
|
+
const noMatch = SemanticRuleEngine.matchItemType({ 事项名称: '测试' }, itemTypeMatching);
|
|
82
|
+
expect(noMatch.matchSource).toBeNull();
|
|
83
|
+
expect(noMatch.itemType).toBeNull();
|
|
49
84
|
});
|
|
50
85
|
it('时限数值提取', () => {
|
|
51
86
|
expect(SemanticRuleEngine.extractTimeLimitDays('20个工作日')).toBe(20);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const DataScaleGuard = {
|
|
2
|
+
check(data, maxFieldCount) {
|
|
3
|
+
const limit = maxFieldCount ?? 1000000;
|
|
4
|
+
let fieldCount = 0;
|
|
5
|
+
for (const row of data) {
|
|
6
|
+
if (typeof row === 'object' && row !== null) {
|
|
7
|
+
fieldCount += Object.keys(row).length;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
fieldCount++;
|
|
11
|
+
}
|
|
12
|
+
if (fieldCount > limit) {
|
|
13
|
+
return { exceeded: true, fieldCount };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return { exceeded: false, fieldCount };
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=dataScaleGuard.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const DEGRADED_STATUS = { status: 'degraded', fetchedAt: '', recordCount: 0 };
|
|
2
|
+
const EMPTY_KB = {
|
|
3
|
+
timeLimits: [],
|
|
4
|
+
materials: [],
|
|
5
|
+
conditions: [],
|
|
6
|
+
dataSourceStatus: {
|
|
7
|
+
national: DEGRADED_STATUS,
|
|
8
|
+
provincial: DEGRADED_STATUS,
|
|
9
|
+
standard: DEGRADED_STATUS,
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
export const DegradedModeController = {
|
|
13
|
+
activate(reason) {
|
|
14
|
+
return {
|
|
15
|
+
degradedMode: true,
|
|
16
|
+
degradedReason: reason,
|
|
17
|
+
localKb: { ...EMPTY_KB, dataSourceStatus: { ...EMPTY_KB.dataSourceStatus } },
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=degradedModeController.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const EncodingDetector = {
|
|
2
|
+
detect(filePath) {
|
|
3
|
+
const { readFileSync } = require('node:fs');
|
|
4
|
+
let buffer;
|
|
5
|
+
try {
|
|
6
|
+
buffer = readFileSync(filePath);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return { content: null, encodingError: `无法读取文件: ${filePath}` };
|
|
10
|
+
}
|
|
11
|
+
if (buffer.length === 0) {
|
|
12
|
+
return { content: '', encodingError: '文件为空' };
|
|
13
|
+
}
|
|
14
|
+
const isUtf8 = this.isValidUtf8(buffer);
|
|
15
|
+
if (!isUtf8) {
|
|
16
|
+
return { content: null, encodingError: '文件编码非 UTF-8,请将文件转换为 UTF-8 编码后重试' };
|
|
17
|
+
}
|
|
18
|
+
return { content: buffer.toString('utf-8') };
|
|
19
|
+
},
|
|
20
|
+
isValidUtf8(buffer) {
|
|
21
|
+
try {
|
|
22
|
+
const str = buffer.toString('utf-8');
|
|
23
|
+
const reencoded = Buffer.from(str, 'utf-8');
|
|
24
|
+
return reencoded.equals(buffer);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=encodingDetector.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const EndpointResolver = {
|
|
2
|
+
resolve(endpoint) {
|
|
3
|
+
if (!endpoint || typeof endpoint !== 'string')
|
|
4
|
+
return null;
|
|
5
|
+
if (endpoint.startsWith('https://')) {
|
|
6
|
+
return { kind: 'https', target: endpoint };
|
|
7
|
+
}
|
|
8
|
+
if (endpoint.startsWith('http://')) {
|
|
9
|
+
return { kind: 'http', target: endpoint };
|
|
10
|
+
}
|
|
11
|
+
const { existsSync } = require('node:fs');
|
|
12
|
+
if (existsSync(endpoint)) {
|
|
13
|
+
return { kind: 'file', target: endpoint };
|
|
14
|
+
}
|
|
15
|
+
if (endpoint.startsWith('./') || endpoint.startsWith('../') || endpoint.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(endpoint)) {
|
|
16
|
+
return { kind: 'file', target: endpoint };
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=endpointResolver.js.map
|
package/lib/index.js
CHANGED
|
@@ -5,6 +5,9 @@ import { resolvePolicyBasis } from './policyBasis.js';
|
|
|
5
5
|
import { KnowledgeBaseLoader } from './knowledgeBaseLoader.js';
|
|
6
6
|
import { InspectionOrchestrator } from './inspectionOrchestrator.js';
|
|
7
7
|
import { StandardRuleSource } from './standardRuleSource.js';
|
|
8
|
+
import { EncodingDetector } from './encodingDetector.js';
|
|
9
|
+
import { DataScaleGuard } from './dataScaleGuard.js';
|
|
10
|
+
import { UrlReachabilityChecker } from './urlReachabilityChecker.js';
|
|
8
11
|
export function apply(ctx) {
|
|
9
12
|
ctx.tools.register({
|
|
10
13
|
name: 'inspect_gov_data',
|
|
@@ -14,6 +17,7 @@ export function apply(ctx) {
|
|
|
14
17
|
properties: {
|
|
15
18
|
dataSource: { type: 'string', description: '政务数据文件路径' },
|
|
16
19
|
inspectionMode: { type: 'string', enum: ['guide', 'classification', 'full'] },
|
|
20
|
+
itemTypeOverride: { type: 'string', description: '手动指定事项类型,覆盖自动匹配结果' },
|
|
17
21
|
},
|
|
18
22
|
required: ['dataSource'],
|
|
19
23
|
},
|
|
@@ -29,11 +33,82 @@ export function apply(ctx) {
|
|
|
29
33
|
catch {
|
|
30
34
|
return JSON.stringify({ error: 'GOV_DATA_RULES_MISSING', message: '无法加载business-rules.json' });
|
|
31
35
|
}
|
|
32
|
-
const
|
|
33
|
-
if (!
|
|
36
|
+
const govConfigRaw = rulesConfig.govDataInspection;
|
|
37
|
+
if (!govConfigRaw || typeof govConfigRaw !== 'object') {
|
|
34
38
|
return JSON.stringify({ error: 'GOV_DATA_RULES_MISSING', message: 'govDataInspection配置段缺失' });
|
|
35
39
|
}
|
|
36
|
-
const
|
|
40
|
+
const govConfig = govConfigRaw;
|
|
41
|
+
const requiredFields = {
|
|
42
|
+
guideRequiredElements: 'string[]',
|
|
43
|
+
logicErrorRules: 'array',
|
|
44
|
+
formatRules: 'array',
|
|
45
|
+
severityMapping: 'object',
|
|
46
|
+
scoreWeights: 'object',
|
|
47
|
+
convenienceWeights: 'object',
|
|
48
|
+
gbt47949Mapping: 'object',
|
|
49
|
+
itemTypeMatching: 'object',
|
|
50
|
+
materialConciseThreshold: 'number',
|
|
51
|
+
missingFieldStandardClause: 'string',
|
|
52
|
+
dataSourcePriority: 'array',
|
|
53
|
+
dataSourceCredentials: 'object',
|
|
54
|
+
};
|
|
55
|
+
for (const [field, expectedType] of Object.entries(requiredFields)) {
|
|
56
|
+
const val = govConfig[field];
|
|
57
|
+
if (val === undefined || val === null) {
|
|
58
|
+
return JSON.stringify({ error: 'GOV_DATA_RULES_MISSING', message: `govDataInspection.${field}配置缺失` });
|
|
59
|
+
}
|
|
60
|
+
if (expectedType === 'array' && !Array.isArray(val)) {
|
|
61
|
+
return JSON.stringify({ error: 'GOV_DATA_RULES_MISSING', message: `govDataInspection.${field}应为数组` });
|
|
62
|
+
}
|
|
63
|
+
if (expectedType === 'object' && (typeof val !== 'object' || Array.isArray(val))) {
|
|
64
|
+
return JSON.stringify({ error: 'GOV_DATA_RULES_MISSING', message: `govDataInspection.${field}应为对象` });
|
|
65
|
+
}
|
|
66
|
+
if (expectedType === 'string' && typeof val !== 'string') {
|
|
67
|
+
return JSON.stringify({ error: 'GOV_DATA_RULES_MISSING', message: `govDataInspection.${field}应为字符串` });
|
|
68
|
+
}
|
|
69
|
+
if (expectedType === 'number' && typeof val !== 'number') {
|
|
70
|
+
return JSON.stringify({ error: 'GOV_DATA_RULES_MISSING', message: `govDataInspection.${field}应为数字` });
|
|
71
|
+
}
|
|
72
|
+
if (expectedType === 'string[]' && (!Array.isArray(val) || val.some((v) => typeof v !== 'string'))) {
|
|
73
|
+
return JSON.stringify({ error: 'GOV_DATA_RULES_MISSING', message: `govDataInspection.${field}应为字符串数组` });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const govConfigTyped = govConfig;
|
|
77
|
+
const encodingResult = EncodingDetector.detect(args.dataSource);
|
|
78
|
+
if (encodingResult.content === null) {
|
|
79
|
+
if (encodingResult.encodingError === '文件为空') {
|
|
80
|
+
return JSON.stringify({ error: 'GOV_DATA_INPUT_INVALID', message: '无数据,请确认输入文件内容' });
|
|
81
|
+
}
|
|
82
|
+
if (encodingResult.encodingError?.includes('无法读取文件')) {
|
|
83
|
+
const reachability = await UrlReachabilityChecker.check(args.dataSource);
|
|
84
|
+
if (!reachability.reachable) {
|
|
85
|
+
return JSON.stringify({ error: 'GOV_DATA_URL_UNREACHABLE', message: reachability.error ?? '数据源不可达,请检查路径或 URL' });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return JSON.stringify({ error: 'GOV_DATA_ENCODING_ERROR', message: encodingResult.encodingError ?? '文件编码错误' });
|
|
89
|
+
}
|
|
90
|
+
let data;
|
|
91
|
+
try {
|
|
92
|
+
const raw = encodingResult.content;
|
|
93
|
+
if (raw.trim() === '') {
|
|
94
|
+
return JSON.stringify({ error: 'GOV_DATA_INPUT_INVALID', message: '无数据,请确认输入文件内容' });
|
|
95
|
+
}
|
|
96
|
+
data = JSON.parse(raw);
|
|
97
|
+
if (!Array.isArray(data))
|
|
98
|
+
data = [data];
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
return JSON.stringify({ error: 'GOV_DATA_INPUT_INVALID', message: `JSON 格式错误,请检查文件内容: ${e.message}` });
|
|
102
|
+
}
|
|
103
|
+
if (data.length === 0) {
|
|
104
|
+
return JSON.stringify({ error: 'GOV_DATA_INPUT_INVALID', message: '无数据,请确认输入文件内容' });
|
|
105
|
+
}
|
|
106
|
+
const maxFieldCount = rulesConfig.govDataInspection?.maxFieldCount;
|
|
107
|
+
const scaleResult = DataScaleGuard.check(data, maxFieldCount);
|
|
108
|
+
if (scaleResult.exceeded) {
|
|
109
|
+
return JSON.stringify({ error: 'GOV_DATA_SCALE_EXCEEDED', message: `数据量超出处理上限(字段数${scaleResult.fieldCount}),请分批处理或增加maxFieldCount配置` });
|
|
110
|
+
}
|
|
111
|
+
const credentials = govConfigTyped.dataSourceCredentials;
|
|
37
112
|
const credConfig = {
|
|
38
113
|
national: {
|
|
39
114
|
apiKey: process.env[credentials.national.apiKey],
|
|
@@ -44,13 +119,10 @@ export function apply(ctx) {
|
|
|
44
119
|
endpoint: process.env[credentials.provincial.endpoint],
|
|
45
120
|
},
|
|
46
121
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
catch {
|
|
52
|
-
return JSON.stringify({ error: 'GOV_DATA_KB_MISSING', message: '官方数据源全部不可用,语义错误检测已中止' });
|
|
53
|
-
}
|
|
122
|
+
const kbLoadResult = await KnowledgeBaseLoader.load(credConfig, govConfigTyped.dataSourcePriority);
|
|
123
|
+
const kb = kbLoadResult.kb;
|
|
124
|
+
const degradedMode = kbLoadResult.degraded;
|
|
125
|
+
const degradedReason = kbLoadResult.degradedReason;
|
|
54
126
|
const dataSourceStatus = KnowledgeBaseLoader.getDataSourceStatus();
|
|
55
127
|
let standardRules;
|
|
56
128
|
const stdRuleResult = await StandardRuleSource.fetch({
|
|
@@ -62,10 +134,13 @@ export function apply(ctx) {
|
|
|
62
134
|
dataSourceStatus.standard = stdRuleResult.status;
|
|
63
135
|
}
|
|
64
136
|
else {
|
|
65
|
-
standardRules =
|
|
137
|
+
standardRules = govConfigTyped.logicErrorRules;
|
|
66
138
|
}
|
|
67
139
|
if (!standardRules || standardRules.length === 0) {
|
|
68
|
-
|
|
140
|
+
if (!degradedMode) {
|
|
141
|
+
return JSON.stringify({ error: 'GOV_DATA_LOGIC_RULES_MISSING', message: '标准规则集缺失,逻辑错误检测已中止' });
|
|
142
|
+
}
|
|
143
|
+
standardRules = [];
|
|
69
144
|
}
|
|
70
145
|
let policyBasis;
|
|
71
146
|
try {
|
|
@@ -77,18 +152,8 @@ export function apply(ctx) {
|
|
|
77
152
|
catch {
|
|
78
153
|
return JSON.stringify({ error: 'GOV_DATA_POLICY_MISSING', message: '政策依据配置加载失败' });
|
|
79
154
|
}
|
|
80
|
-
let data;
|
|
81
|
-
try {
|
|
82
|
-
const { readFileSync } = await import('node:fs');
|
|
83
|
-
const raw = readFileSync(args.dataSource, 'utf-8');
|
|
84
|
-
data = JSON.parse(raw);
|
|
85
|
-
if (!Array.isArray(data))
|
|
86
|
-
data = [data];
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
return JSON.stringify({ error: 'GOV_DATA_INPUT_INVALID', message: `无法加载数据: ${args.dataSource}` });
|
|
90
|
-
}
|
|
91
155
|
const mode = args.inspectionMode ?? 'full';
|
|
156
|
+
const itemTypeOverride = args.itemTypeOverride;
|
|
92
157
|
const result = {
|
|
93
158
|
dataSource: args.dataSource,
|
|
94
159
|
inspectionMode: mode,
|
|
@@ -96,11 +161,19 @@ export function apply(ctx) {
|
|
|
96
161
|
policyBasis,
|
|
97
162
|
dataSourceStatus,
|
|
98
163
|
};
|
|
164
|
+
if (degradedMode) {
|
|
165
|
+
result.degradedMode = true;
|
|
166
|
+
result.degradedReason = degradedReason;
|
|
167
|
+
}
|
|
99
168
|
if (mode === 'guide' || mode === 'full') {
|
|
100
|
-
|
|
169
|
+
const orchestrateResult = InspectionOrchestrator.orchestrate(data, govConfigTyped, kb, standardRules, { degradedMode, itemTypeOverride });
|
|
170
|
+
result.guideInspection = orchestrateResult;
|
|
171
|
+
if (orchestrateResult.warnings && orchestrateResult.warnings.length > 0) {
|
|
172
|
+
result.warnings = orchestrateResult.warnings;
|
|
173
|
+
}
|
|
101
174
|
}
|
|
102
175
|
if (mode === 'classification' || mode === 'full') {
|
|
103
|
-
result.classification = classifyGovData(data,
|
|
176
|
+
result.classification = classifyGovData(data, govConfigTyped.gbt47949Mapping);
|
|
104
177
|
}
|
|
105
178
|
return JSON.stringify(result, null, 2);
|
|
106
179
|
}
|
|
@@ -5,9 +5,10 @@ import { FormatValidator } from './formatValidator.js';
|
|
|
5
5
|
import { QualityMetricsCalculator } from './qualityMetricsCalculator.js';
|
|
6
6
|
import { ErrorDetailBuilder } from './errorDetailBuilder.js';
|
|
7
7
|
export const InspectionOrchestrator = {
|
|
8
|
-
orchestrate(data, config, kb, standardRules) {
|
|
8
|
+
orchestrate(data, config, kb, standardRules, options) {
|
|
9
9
|
const allErrorDetails = [];
|
|
10
10
|
const allFormatIssues = [];
|
|
11
|
+
const allWarnings = [];
|
|
11
12
|
let completeCount = 0;
|
|
12
13
|
let serviceConvenience = 0;
|
|
13
14
|
for (let i = 0; i < data.length; i++) {
|
|
@@ -21,8 +22,11 @@ export const InspectionOrchestrator = {
|
|
|
21
22
|
const isComplete = missingDetails.length === 0;
|
|
22
23
|
if (isComplete)
|
|
23
24
|
completeCount++;
|
|
24
|
-
const
|
|
25
|
-
allErrorDetails.push(...
|
|
25
|
+
const semanticResult = SemanticRuleEngine.detect(guide, guideId, kb, config.itemTypeMatching, config.severityMapping, { degradedMode: options?.degradedMode, itemTypeOverride: options?.itemTypeOverride });
|
|
26
|
+
allErrorDetails.push(...semanticResult.details);
|
|
27
|
+
if (semanticResult.warning) {
|
|
28
|
+
allWarnings.push(semanticResult.warning);
|
|
29
|
+
}
|
|
26
30
|
const logicalDetails = LogicRuleEngine.detect(guide, guideId, standardRules, config.severityMapping);
|
|
27
31
|
allErrorDetails.push(...logicalDetails);
|
|
28
32
|
const formatIssues = FormatValidator.validate(guide, guideId, config.formatRules);
|
|
@@ -64,6 +68,7 @@ export const InspectionOrchestrator = {
|
|
|
64
68
|
serviceConvenience: Math.round(serviceConvenience * 100) / 100,
|
|
65
69
|
totalGuidesChecked: data.length,
|
|
66
70
|
detectionRates,
|
|
71
|
+
warnings: allWarnings,
|
|
67
72
|
};
|
|
68
73
|
},
|
|
69
74
|
};
|
|
@@ -71,15 +71,20 @@ export const KnowledgeBaseLoader = {
|
|
|
71
71
|
standard: FAILED_STATUS,
|
|
72
72
|
};
|
|
73
73
|
const allFailed = nationalResult.status.status === 'failed' && provincialResult.status.status === 'failed';
|
|
74
|
-
|
|
75
|
-
throw new Error('GOV_DATA_KB_MISSING: 官方数据源全部不可用,语义错误检测已中止');
|
|
76
|
-
}
|
|
77
|
-
return {
|
|
74
|
+
const kb = {
|
|
78
75
|
timeLimits: cachedTimeLimits,
|
|
79
76
|
materials: cachedMaterials,
|
|
80
77
|
conditions: cachedConditions,
|
|
81
78
|
dataSourceStatus: cachedDataSourceStatus,
|
|
82
79
|
};
|
|
80
|
+
if (allFailed) {
|
|
81
|
+
return {
|
|
82
|
+
kb,
|
|
83
|
+
degraded: true,
|
|
84
|
+
degradedReason: '官方数据源全部不可用,已降级为本地规则检测模式',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return { kb, degraded: false };
|
|
83
88
|
},
|
|
84
89
|
getTimeLimits() {
|
|
85
90
|
if (!cachedTimeLimits)
|
|
@@ -1,7 +1,26 @@
|
|
|
1
|
+
import { EndpointResolver } from './endpointResolver.js';
|
|
1
2
|
const FAILED_RESULT = {
|
|
2
3
|
data: [],
|
|
3
4
|
status: { status: 'failed', fetchedAt: '', recordCount: 0 },
|
|
4
5
|
};
|
|
6
|
+
function parseOfficialData(raw, source) {
|
|
7
|
+
if (!Array.isArray(raw))
|
|
8
|
+
return [];
|
|
9
|
+
return raw
|
|
10
|
+
.filter((item) => item && typeof item === 'object')
|
|
11
|
+
.map((item) => ({
|
|
12
|
+
dataSource: source,
|
|
13
|
+
itemType: String(item.itemType ?? ''),
|
|
14
|
+
legalUpperLimit: item.legalUpperLimit,
|
|
15
|
+
legalLowerLimit: item.legalLowerLimit,
|
|
16
|
+
standardName: item.standardName,
|
|
17
|
+
isRequired: item.isRequired,
|
|
18
|
+
elementName: item.elementName,
|
|
19
|
+
standardValue: item.standardValue,
|
|
20
|
+
standardClause: String(item.standardClause ?? ''),
|
|
21
|
+
}))
|
|
22
|
+
.filter((d) => d.itemType && d.standardClause);
|
|
23
|
+
}
|
|
5
24
|
export const NationalDataSource = {
|
|
6
25
|
async fetch(credentials) {
|
|
7
26
|
const apiKey = credentials?.apiKey ?? process.env.NATIONAL_API_KEY;
|
|
@@ -9,12 +28,23 @@ export const NationalDataSource = {
|
|
|
9
28
|
if (!apiKey || !endpoint) {
|
|
10
29
|
return { ...FAILED_RESULT, status: { ...FAILED_RESULT.status } };
|
|
11
30
|
}
|
|
12
|
-
|
|
31
|
+
const resolved = EndpointResolver.resolve(endpoint);
|
|
32
|
+
if (!resolved) {
|
|
13
33
|
return { ...FAILED_RESULT, status: { ...FAILED_RESULT.status } };
|
|
14
34
|
}
|
|
15
35
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
36
|
+
if (resolved.kind === 'file') {
|
|
37
|
+
const { readFileSync } = require('node:fs');
|
|
38
|
+
const raw = JSON.parse(readFileSync(resolved.target, 'utf-8'));
|
|
39
|
+
const data = parseOfficialData(raw, 'national');
|
|
40
|
+
return {
|
|
41
|
+
data,
|
|
42
|
+
status: { status: 'success', fetchedAt: new Date().toISOString(), recordCount: data.length },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const url = new URL(resolved.target);
|
|
46
|
+
if (apiKey)
|
|
47
|
+
url.searchParams.set('apiKey', apiKey);
|
|
18
48
|
const response = await fetch(url.toString(), {
|
|
19
49
|
method: 'GET',
|
|
20
50
|
headers: { 'Accept': 'application/json' },
|
|
@@ -23,22 +53,7 @@ export const NationalDataSource = {
|
|
|
23
53
|
if (!response.ok)
|
|
24
54
|
return { ...FAILED_RESULT, status: { ...FAILED_RESULT.status } };
|
|
25
55
|
const raw = await response.json();
|
|
26
|
-
|
|
27
|
-
return { ...FAILED_RESULT, status: { ...FAILED_RESULT.status } };
|
|
28
|
-
const data = raw
|
|
29
|
-
.filter((item) => item && typeof item === 'object')
|
|
30
|
-
.map((item) => ({
|
|
31
|
-
dataSource: 'national',
|
|
32
|
-
itemType: String(item.itemType ?? ''),
|
|
33
|
-
legalUpperLimit: item.legalUpperLimit,
|
|
34
|
-
legalLowerLimit: item.legalLowerLimit,
|
|
35
|
-
standardName: item.standardName,
|
|
36
|
-
isRequired: item.isRequired,
|
|
37
|
-
elementName: item.elementName,
|
|
38
|
-
standardValue: item.standardValue,
|
|
39
|
-
standardClause: String(item.standardClause ?? ''),
|
|
40
|
-
}))
|
|
41
|
-
.filter((d) => d.itemType && d.standardClause);
|
|
56
|
+
const data = parseOfficialData(raw, 'national');
|
|
42
57
|
return {
|
|
43
58
|
data,
|
|
44
59
|
status: { status: 'success', fetchedAt: new Date().toISOString(), recordCount: data.length },
|
|
@@ -1,7 +1,26 @@
|
|
|
1
|
+
import { EndpointResolver } from './endpointResolver.js';
|
|
1
2
|
const FAILED_RESULT = {
|
|
2
3
|
data: [],
|
|
3
4
|
status: { status: 'failed', fetchedAt: '', recordCount: 0 },
|
|
4
5
|
};
|
|
6
|
+
function parseOfficialData(raw, source) {
|
|
7
|
+
if (!Array.isArray(raw))
|
|
8
|
+
return [];
|
|
9
|
+
return raw
|
|
10
|
+
.filter((item) => item && typeof item === 'object')
|
|
11
|
+
.map((item) => ({
|
|
12
|
+
dataSource: source,
|
|
13
|
+
itemType: String(item.itemType ?? ''),
|
|
14
|
+
legalUpperLimit: item.legalUpperLimit,
|
|
15
|
+
legalLowerLimit: item.legalLowerLimit,
|
|
16
|
+
standardName: item.standardName,
|
|
17
|
+
isRequired: item.isRequired,
|
|
18
|
+
elementName: item.elementName,
|
|
19
|
+
standardValue: item.standardValue,
|
|
20
|
+
standardClause: String(item.standardClause ?? ''),
|
|
21
|
+
}))
|
|
22
|
+
.filter((d) => d.itemType && d.standardClause);
|
|
23
|
+
}
|
|
5
24
|
export const ProvincialDataSource = {
|
|
6
25
|
async fetch(credentials) {
|
|
7
26
|
const apiKey = credentials?.apiKey ?? process.env.PROVINCIAL_API_KEY;
|
|
@@ -9,12 +28,23 @@ export const ProvincialDataSource = {
|
|
|
9
28
|
if (!apiKey || !endpoint) {
|
|
10
29
|
return { ...FAILED_RESULT, status: { ...FAILED_RESULT.status } };
|
|
11
30
|
}
|
|
12
|
-
|
|
31
|
+
const resolved = EndpointResolver.resolve(endpoint);
|
|
32
|
+
if (!resolved) {
|
|
13
33
|
return { ...FAILED_RESULT, status: { ...FAILED_RESULT.status } };
|
|
14
34
|
}
|
|
15
35
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
36
|
+
if (resolved.kind === 'file') {
|
|
37
|
+
const { readFileSync } = require('node:fs');
|
|
38
|
+
const raw = JSON.parse(readFileSync(resolved.target, 'utf-8'));
|
|
39
|
+
const data = parseOfficialData(raw, 'provincial');
|
|
40
|
+
return {
|
|
41
|
+
data,
|
|
42
|
+
status: { status: 'success', fetchedAt: new Date().toISOString(), recordCount: data.length },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const url = new URL(resolved.target);
|
|
46
|
+
if (apiKey)
|
|
47
|
+
url.searchParams.set('apiKey', apiKey);
|
|
18
48
|
const response = await fetch(url.toString(), {
|
|
19
49
|
method: 'GET',
|
|
20
50
|
headers: { 'Accept': 'application/json' },
|
|
@@ -23,22 +53,7 @@ export const ProvincialDataSource = {
|
|
|
23
53
|
if (!response.ok)
|
|
24
54
|
return { ...FAILED_RESULT, status: { ...FAILED_RESULT.status } };
|
|
25
55
|
const raw = await response.json();
|
|
26
|
-
|
|
27
|
-
return { ...FAILED_RESULT, status: { ...FAILED_RESULT.status } };
|
|
28
|
-
const data = raw
|
|
29
|
-
.filter((item) => item && typeof item === 'object')
|
|
30
|
-
.map((item) => ({
|
|
31
|
-
dataSource: 'provincial',
|
|
32
|
-
itemType: String(item.itemType ?? ''),
|
|
33
|
-
legalUpperLimit: item.legalUpperLimit,
|
|
34
|
-
legalLowerLimit: item.legalLowerLimit,
|
|
35
|
-
standardName: item.standardName,
|
|
36
|
-
isRequired: item.isRequired,
|
|
37
|
-
elementName: item.elementName,
|
|
38
|
-
standardValue: item.standardValue,
|
|
39
|
-
standardClause: String(item.standardClause ?? ''),
|
|
40
|
-
}))
|
|
41
|
-
.filter((d) => d.itemType && d.standardClause);
|
|
56
|
+
const data = parseOfficialData(raw, 'provincial');
|
|
42
57
|
return {
|
|
43
58
|
data,
|
|
44
59
|
status: { status: 'success', fetchedAt: new Date().toISOString(), recordCount: data.length },
|
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
import { ErrorDetailBuilder } from './errorDetailBuilder.js';
|
|
2
|
-
|
|
2
|
+
import { UnmatchedWarningBuilder } from './unmatchedWarningBuilder.js';
|
|
3
|
+
const FLOW_FIELD_KEYS = ['办理流程', '工作流程', '流程描述', '业务流程', '办理环节'];
|
|
4
|
+
function matchItemType(guide, itemTypeMatching, options) {
|
|
5
|
+
if (options?.itemTypeOverride) {
|
|
6
|
+
return { itemType: options.itemTypeOverride, matchSource: 'manual' };
|
|
7
|
+
}
|
|
3
8
|
if (!itemTypeMatching)
|
|
4
|
-
return null;
|
|
9
|
+
return { itemType: null, matchSource: null };
|
|
5
10
|
const itemName = String(guide['事项名称'] ?? '');
|
|
6
11
|
const itemCode = String(guide['事项编码'] ?? '');
|
|
7
12
|
for (const [itemType, rule] of Object.entries(itemTypeMatching)) {
|
|
8
|
-
if (rule.keywords.some((kw) => itemName.includes(kw)))
|
|
9
|
-
return itemType;
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
if (rule.keywords.some((kw) => itemName.includes(kw))) {
|
|
14
|
+
return { itemType, matchSource: 'name' };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
for (const [itemType, rule] of Object.entries(itemTypeMatching)) {
|
|
18
|
+
if (itemCode && itemCode.startsWith(rule.codePrefix)) {
|
|
19
|
+
return { itemType, matchSource: 'code' };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
for (const flowKey of FLOW_FIELD_KEYS) {
|
|
23
|
+
const flowText = String(guide[flowKey] ?? '');
|
|
24
|
+
if (!flowText)
|
|
25
|
+
continue;
|
|
26
|
+
for (const [itemType, rule] of Object.entries(itemTypeMatching)) {
|
|
27
|
+
if (rule.keywords.some((kw) => flowText.includes(kw))) {
|
|
28
|
+
return { itemType, matchSource: 'flow' };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
12
31
|
}
|
|
13
|
-
return null;
|
|
32
|
+
return { itemType: null, matchSource: null };
|
|
14
33
|
}
|
|
15
34
|
function extractTimeLimitDays(timeLimit) {
|
|
16
35
|
const str = String(timeLimit ?? '');
|
|
@@ -141,18 +160,28 @@ function detectConditions(guide, guideId, itemType, kb, severityMapping) {
|
|
|
141
160
|
return details;
|
|
142
161
|
}
|
|
143
162
|
export const SemanticRuleEngine = {
|
|
144
|
-
detect(guide, guideId, kb, itemTypeMatching, severityMapping) {
|
|
163
|
+
detect(guide, guideId, kb, itemTypeMatching, severityMapping, options) {
|
|
164
|
+
const matchResult = matchItemType(guide, itemTypeMatching, options);
|
|
165
|
+
const itemType = matchResult.itemType;
|
|
166
|
+
if (!itemType) {
|
|
167
|
+
const warning = UnmatchedWarningBuilder.build(guideId);
|
|
168
|
+
return { details: [], unmatched: true, warning };
|
|
169
|
+
}
|
|
145
170
|
if (!kb || !kb.timeLimits || !kb.materials || !kb.conditions) {
|
|
171
|
+
if (options?.degradedMode) {
|
|
172
|
+
return { details: [] };
|
|
173
|
+
}
|
|
146
174
|
throw new Error('GOV_DATA_KB_MISSING: 标准知识库不可用,语义错误检测已中止');
|
|
147
175
|
}
|
|
148
|
-
const
|
|
149
|
-
if (!
|
|
150
|
-
return [];
|
|
176
|
+
const hasKbData = kb.timeLimits.length > 0 || kb.materials.length > 0 || kb.conditions.length > 0;
|
|
177
|
+
if (!hasKbData && options?.degradedMode) {
|
|
178
|
+
return { details: [] };
|
|
179
|
+
}
|
|
151
180
|
const details = [];
|
|
152
181
|
details.push(...detectTimeLimit(guide, guideId, itemType, kb, severityMapping));
|
|
153
182
|
details.push(...detectMaterials(guide, guideId, itemType, kb, severityMapping));
|
|
154
183
|
details.push(...detectConditions(guide, guideId, itemType, kb, severityMapping));
|
|
155
|
-
return details;
|
|
184
|
+
return { details };
|
|
156
185
|
},
|
|
157
186
|
matchItemType,
|
|
158
187
|
extractTimeLimitDays,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const UrlReachabilityChecker = {
|
|
2
|
+
async check(dataSource) {
|
|
3
|
+
if (!dataSource || typeof dataSource !== 'string') {
|
|
4
|
+
return { reachable: false, error: '数据源路径为空' };
|
|
5
|
+
}
|
|
6
|
+
if (dataSource.startsWith('http://') || dataSource.startsWith('https://')) {
|
|
7
|
+
try {
|
|
8
|
+
const response = await fetch(dataSource, {
|
|
9
|
+
method: 'HEAD',
|
|
10
|
+
signal: AbortSignal.timeout(5000),
|
|
11
|
+
});
|
|
12
|
+
if (response.ok) {
|
|
13
|
+
return { reachable: true };
|
|
14
|
+
}
|
|
15
|
+
return { reachable: false, error: `数据源不可达,HTTP状态码: ${response.status}` };
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
return { reachable: false, error: `数据源不可达: ${e.message}` };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const { existsSync } = require('node:fs');
|
|
22
|
+
if (existsSync(dataSource)) {
|
|
23
|
+
return { reachable: true };
|
|
24
|
+
}
|
|
25
|
+
return { reachable: false, error: '数据源不可达,请检查路径或 URL' };
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=urlReachabilityChecker.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liuhange/dsh-gov-data-inspector",
|
|
3
3
|
"description": "Government data inspection plugin: service guide quality check and public data asset classification",
|
|
4
|
-
"version": "3.1.
|
|
4
|
+
"version": "3.1.4",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"lib/**/*.js",
|
|
26
|
-
"lib/types/**/*.d.ts"
|
|
26
|
+
"lib/types/**/*.d.ts",
|
|
27
|
+
"cordis.patch.yml"
|
|
27
28
|
],
|
|
28
29
|
"license": "MIT",
|
|
29
30
|
"dependencies": {
|