@liuhange/dsh-gov-data-inspector 3.1.2 → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 liuhange789
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -36,3 +36,80 @@ apply(ctx); // 注册工具: inspect_gov_data
36
36
  ## License
37
37
 
38
38
  MIT
39
+ ## 无凭据降级模式
40
+
41
+ 当官方数据源(国家/省级)凭据未配置或不可达时,插件自动进入**降级模式**而非中止运行。
42
+
43
+ ### 降级触发条件
44
+
45
+ - 国家和省级数据源均未配置 API Key / Endpoint
46
+ - 国家和省级数据源均不可达(网络超时、HTTP 错误等)
47
+
48
+ ### 降级表现
49
+
50
+ | 检测模式 | 完整模式表现 | 降级模式表现 |
51
+ |---------|------------|------------|
52
+ | classification | 正常分类 | **正常分类**(不依赖知识库) |
53
+ | guide | 语义检测基于官方知识库 | 语义检测降级为本地规则检测 |
54
+ | full | 全量检测 | 分类正常 + 语义/逻辑降级 |
55
+
56
+ ### 降级标记
57
+
58
+ 降级模式下,巡检报告包含以下额外字段:
59
+
60
+ ```json
61
+ {
62
+ "degradedMode": true,
63
+ "degradedReason": "官方数据源全部不可用,已降级为本地规则检测模式"
64
+ }
65
+ ```
66
+
67
+ ### 端点协议宽松化
68
+
69
+ 数据源端点支持以下三种协议:
70
+ - `https://` — HTTPS 远程端点
71
+ - `http://` — HTTP 远程端点(不再强制 HTTPS)
72
+ - 本地文件路径 — 直接读取本地 JSON 文件
73
+
74
+ ## 异常输入处理
75
+
76
+ 插件对以下异常输入场景提供明确的友好错误提示,不会崩溃或产生未捕获异常:
77
+
78
+ | 异常场景 | 错误码 | 错误提示 |
79
+ |---------|--------|---------|
80
+ | 格式错误的 JSON | `GOV_DATA_INPUT_INVALID` | JSON 格式错误,请检查文件内容 |
81
+ | 空数据(空文件/空数组) | `GOV_DATA_INPUT_INVALID` | 无数据,请确认输入文件内容 |
82
+ | 超长数据(超 100 万字段) | `GOV_DATA_SCALE_EXCEEDED` | 数据量超出处理上限 |
83
+ | 无效 URL / 不可达路径 | `GOV_DATA_URL_UNREACHABLE` | 数据源不可达,请检查路径或 URL |
84
+ | 非 UTF-8 编码文件 | `GOV_DATA_ENCODING_ERROR` | 文件编码非 UTF-8 |
85
+ | 部分字段缺失 | 正常处理 | 报告中列出缺失字段清单 |
86
+
87
+ ### itemTypeOverride 参数
88
+
89
+ 当自动匹配无法识别事项类型时,用户可通过 `itemTypeOverride` 参数手动指定:
90
+
91
+ ```javascript
92
+ execute({
93
+ dataSource: './data.json',
94
+ inspectionMode: 'guide',
95
+ itemTypeOverride: '现场勘查类'
96
+ })
97
+ ```
98
+
99
+ ### 未匹配事项类型警告
100
+
101
+ 当事项无法匹配到任何检测规则时,报告中会包含 `UNMATCHED_ITEM_TYPE` 警告:
102
+
103
+ ```json
104
+ {
105
+ "warnings": [
106
+ {
107
+ "type": "UNMATCHED_ITEM_TYPE",
108
+ "guideId": "建设工程规划核实",
109
+ "message": "该事项未匹配到检测规则,语义检测被跳过"
110
+ }
111
+ ]
112
+ }
113
+ ```
114
+
115
+ 匹配逻辑支持四通道:手动指定 → 名称关键词 → 编码前缀 → 流程字段关键词。
@@ -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 details = SemanticRuleEngine.detect(guide, 'g1', kb, itemTypeMatching);
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 details = SemanticRuleEngine.detect(guide, 'g1', kb, itemTypeMatching);
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 details = SemanticRuleEngine.detect(guide, 'g1', kb, itemTypeMatching);
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('标准知识库不可用 → 抛出GOV_DATA_KB_MISSING', () => {
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 govConfig = rulesConfig.govDataInspection;
33
- if (!govConfig) {
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 credentials = govConfig.dataSourceCredentials;
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
- let kb;
48
- try {
49
- kb = await KnowledgeBaseLoader.load(credConfig, govConfig.dataSourcePriority);
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 = govConfig.logicErrorRules;
137
+ standardRules = govConfigTyped.logicErrorRules;
66
138
  }
67
139
  if (!standardRules || standardRules.length === 0) {
68
- return JSON.stringify({ error: 'GOV_DATA_LOGIC_RULES_MISSING', message: '标准规则集缺失,逻辑错误检测已中止' });
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
- result.guideInspection = InspectionOrchestrator.orchestrate(data, govConfig, kb, standardRules);
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, govConfig.gbt47949Mapping);
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 semanticDetails = SemanticRuleEngine.detect(guide, guideId, kb, config.itemTypeMatching, config.severityMapping);
25
- allErrorDetails.push(...semanticDetails);
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
- if (allFailed) {
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
- if (!endpoint.startsWith('https://')) {
31
+ const resolved = EndpointResolver.resolve(endpoint);
32
+ if (!resolved) {
13
33
  return { ...FAILED_RESULT, status: { ...FAILED_RESULT.status } };
14
34
  }
15
35
  try {
16
- const url = new URL(endpoint);
17
- url.searchParams.set('apiKey', apiKey);
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
- if (!Array.isArray(raw))
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
- if (!endpoint.startsWith('https://')) {
31
+ const resolved = EndpointResolver.resolve(endpoint);
32
+ if (!resolved) {
13
33
  return { ...FAILED_RESULT, status: { ...FAILED_RESULT.status } };
14
34
  }
15
35
  try {
16
- const url = new URL(endpoint);
17
- url.searchParams.set('apiKey', apiKey);
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
- if (!Array.isArray(raw))
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
- function matchItemType(guide, itemTypeMatching) {
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
- if (itemCode && itemCode.startsWith(rule.codePrefix))
11
- return itemType;
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 itemType = matchItemType(guide, itemTypeMatching);
149
- if (!itemType)
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,10 @@
1
+ export const UnmatchedWarningBuilder = {
2
+ build(guideId) {
3
+ return {
4
+ type: 'UNMATCHED_ITEM_TYPE',
5
+ guideId,
6
+ message: '该事项未匹配到检测规则,语义检测被跳过',
7
+ };
8
+ },
9
+ };
10
+ //# sourceMappingURL=unmatchedWarningBuilder.js.map
@@ -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.2",
4
+ "version": "3.1.4",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -23,14 +23,10 @@
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
- "scripts": {
30
- "typecheck": "tsc --noEmit",
31
- "test": "vitest run",
32
- "build": "tsc"
33
- },
34
30
  "dependencies": {
35
31
  "@liuhange/dsh-data-asset-shared": "^3.1.0"
36
32
  },
@@ -40,7 +36,7 @@
40
36
  },
41
37
  "dsh": {
42
38
  "bundle": {
43
- "patch": true
39
+ "patch": "./cordis.patch.yml"
44
40
  }
45
41
  },
46
42
  "keywords": [
@@ -56,7 +52,22 @@
56
52
  "gbt-47949",
57
53
  "政务数据",
58
54
  "公共数据",
59
- "办事指南"
55
+ "办事指南",
56
+ "dsh",
57
+ "typescript",
58
+ "nodejs",
59
+ "automation",
60
+ "audit",
61
+ "compliance",
62
+ "data-governance",
63
+ "data-quality",
64
+ "plugin",
65
+ "open-source"
60
66
  ],
61
- "homepage": "https://github.com/liuhange789/data-asset-inspector#readme"
62
- }
67
+ "homepage": "https://github.com/liuhange789/data-asset-inspector#readme",
68
+ "scripts": {
69
+ "typecheck": "tsc --noEmit",
70
+ "test": "vitest run",
71
+ "build": "tsc"
72
+ }
73
+ }