dsh-data-cleaning-agent 0.6.3 → 0.7.0
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/CHANGELOG.md +10 -0
- package/README.en.md +3 -1
- package/README.md +3 -2
- package/docs/RELEASE-0.7.0.md +44 -0
- package/docs/UI-WORKFLOW-V2-ACCEPTANCE.md +40 -0
- package/docs/UI-WORKFLOW-V2.md +16 -7
- package/lib/client.js +157 -37
- package/lib/qcc-field-catalog.js +185 -0
- package/lib/qcc-safety.js +11 -1
- package/lib/qcc.js +276 -79
- package/lib/workflow-contract.js +17 -70
- package/package.json +3 -2
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数据清洗补全的一企一行字段目录。
|
|
3
|
+
*
|
|
4
|
+
* 这里只收录已核对 QCC MCP 一手实现、能够稳定投影为单个 Excel 单元格的字段。
|
|
5
|
+
* 任何明细列表(电话全集、海关资质、风险关联方等)都不得进入本目录。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const field = (id, label, options = {}) => Object.freeze({ id, label, ...options });
|
|
9
|
+
const group = (id, label, sourceTool, fields, options = {}) => Object.freeze({
|
|
10
|
+
id,
|
|
11
|
+
label,
|
|
12
|
+
sourceTool,
|
|
13
|
+
...options,
|
|
14
|
+
fields: Object.freeze(fields),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const SELF_RISK_FACTORS = Object.freeze([
|
|
18
|
+
['dishonest', '失信信息'],
|
|
19
|
+
['judgment_debtor', '被执行人'],
|
|
20
|
+
['consumption_restriction', '限制高消费'],
|
|
21
|
+
['terminated_case', '终本案件'],
|
|
22
|
+
['judicial_document', '裁判文书'],
|
|
23
|
+
['case_filing', '立案信息'],
|
|
24
|
+
['hearing_announcement', '开庭公告'],
|
|
25
|
+
['court_announcement', '法院公告'],
|
|
26
|
+
['service_notice', '送达公告'],
|
|
27
|
+
['bankruptcy_reorganization', '破产重整'],
|
|
28
|
+
['equity_freeze', '股权冻结'],
|
|
29
|
+
['judicial_auction', '司法拍卖'],
|
|
30
|
+
['valuation_inquiry', '询价评估'],
|
|
31
|
+
['pre_litigation_mediation', '诉前调解'],
|
|
32
|
+
['exit_restriction', '限制出境'],
|
|
33
|
+
['administrative_penalty', '行政处罚'],
|
|
34
|
+
['operating_exception', '经营异常'],
|
|
35
|
+
['serious_violation', '严重违法'],
|
|
36
|
+
['environmental_penalty', '环保处罚'],
|
|
37
|
+
['abnormal_taxpayer', '税务非正常户'],
|
|
38
|
+
['tax_arrears', '欠税公告'],
|
|
39
|
+
['tax_violation', '税收违法'],
|
|
40
|
+
['disciplinary_list', '惩戒名单'],
|
|
41
|
+
['default_matter', '违约事项'],
|
|
42
|
+
['guarantee', '担保信息'],
|
|
43
|
+
['equity_pledge_registration', '股权出质'],
|
|
44
|
+
['stock_pledge', '股权质押'],
|
|
45
|
+
['chattel_mortgage', '动产抵押'],
|
|
46
|
+
['land_mortgage', '土地抵押'],
|
|
47
|
+
['simple_cancellation', '简易注销'],
|
|
48
|
+
['cancellation_filing', '注销备案'],
|
|
49
|
+
['liquidation', '清算信息'],
|
|
50
|
+
['labor_arbitration', '劳动仲裁'],
|
|
51
|
+
['public_notice', '公示催告'],
|
|
52
|
+
['property_reward_notice', '财产悬赏公告'],
|
|
53
|
+
].map(Object.freeze));
|
|
54
|
+
|
|
55
|
+
export const RELATED_RISK_FACTORS = Object.freeze([
|
|
56
|
+
['dishonest', '失信被执行人'],
|
|
57
|
+
['judgment_debtor', '被执行人'],
|
|
58
|
+
['consumption_restriction', '限制高消费'],
|
|
59
|
+
['serious_violation', '严重违法'],
|
|
60
|
+
['tax_violation', '税收违法'],
|
|
61
|
+
['administrative_penalty', '行政处罚'],
|
|
62
|
+
['bankruptcy_reorganization', '破产重整'],
|
|
63
|
+
['terminated_case', '终本案件'],
|
|
64
|
+
['equity_freeze', '股权冻结'],
|
|
65
|
+
['tax_arrears', '欠税公告'],
|
|
66
|
+
['operating_exception', '经营异常'],
|
|
67
|
+
].map(Object.freeze));
|
|
68
|
+
|
|
69
|
+
export const RELATED_RISK_KEY_FACTORS = Object.freeze([
|
|
70
|
+
['dishonest', '失信被执行人'],
|
|
71
|
+
['serious_violation', '严重违法'],
|
|
72
|
+
['bankruptcy_reorganization', '破产重整'],
|
|
73
|
+
['tax_violation', '税收违法'],
|
|
74
|
+
['judgment_debtor', '被执行人'],
|
|
75
|
+
['terminated_case', '终本案件'],
|
|
76
|
+
['equity_freeze', '股权冻结'],
|
|
77
|
+
].map(Object.freeze));
|
|
78
|
+
|
|
79
|
+
export const RISK_FACTOR_CATALOG_VERSION = 'qcc-risk-snapshot-2026-09-05';
|
|
80
|
+
|
|
81
|
+
export const QCC_FIELD_CATALOG = Object.freeze([
|
|
82
|
+
group('company_registration', '企业工商信息', 'get_company_registration_info', [
|
|
83
|
+
field('company_name', '企业名称', { inputAnchor: true, defaultSelected: true }),
|
|
84
|
+
field('credit_no', '统一社会信用代码', { inputAnchor: true, defaultSelected: true }),
|
|
85
|
+
field('reg_no', '注册号', { inputAnchor: true }),
|
|
86
|
+
field('org_no', '组织机构代码'),
|
|
87
|
+
field('tax_no', '纳税人识别号'),
|
|
88
|
+
field('reg_status', '登记状态', { defaultSelected: true }),
|
|
89
|
+
field('legal_rep', '法定代表人', { defaultSelected: true }),
|
|
90
|
+
field('reg_capital', '注册资本', { defaultSelected: true }),
|
|
91
|
+
field('paid_capital', '实缴资本'),
|
|
92
|
+
field('establish_date', '成立日期', { defaultSelected: true }),
|
|
93
|
+
field('company_type', '企业类型'),
|
|
94
|
+
field('approval_date', '核准日期'),
|
|
95
|
+
field('registration_authority', '登记机关'),
|
|
96
|
+
field('taxpayer_qualification', '纳税人资质'),
|
|
97
|
+
field('payment_line_no', '支付系统行号'),
|
|
98
|
+
field('import_export_company_code', '进出口企业代码'),
|
|
99
|
+
field('short_name', '企业简称'),
|
|
100
|
+
field('english_name', '英文名'),
|
|
101
|
+
field('registered_address', '注册地址', { defaultSelected: true }),
|
|
102
|
+
field('mailing_address', '通信地址'),
|
|
103
|
+
field('region', '所属地区', { matchAuxiliary: true }),
|
|
104
|
+
field('business_scope', '经营范围'),
|
|
105
|
+
field('industry_category', '国标行业'),
|
|
106
|
+
field('operating_period', '营业期限'),
|
|
107
|
+
field('company_size', '人员规模'),
|
|
108
|
+
field('insured_count', '参保人数'),
|
|
109
|
+
field('branch_insured_count', '分支机构参保人数'),
|
|
110
|
+
], { releaseBatch: 'current' }),
|
|
111
|
+
group('company_profile', '企业简介', 'get_company_profile', [
|
|
112
|
+
field('qcc_industry', '企查查行业'),
|
|
113
|
+
field('company_profile', '企业简介'),
|
|
114
|
+
field('industry_chain_overview', '产业链概览'),
|
|
115
|
+
], { releaseBatch: 'current' }),
|
|
116
|
+
group('contact_info', '联系方式', 'get_contact_info', [
|
|
117
|
+
field('contact_preferred_phone', '首选联系电话'),
|
|
118
|
+
field('contact_phone_invalid_flag', '首选电话无效标记'),
|
|
119
|
+
field('contact_phone_tags', '首选电话标签'),
|
|
120
|
+
field('contact_preferred_email', '首选邮箱'),
|
|
121
|
+
field('contact_official_website', '官方网站'),
|
|
122
|
+
field('contact_official_website_icp', '官网 ICP 备案'),
|
|
123
|
+
], { releaseBatch: 'batch-1' }),
|
|
124
|
+
group('listing_info', '上市信息', 'get_listing_info', [
|
|
125
|
+
field('listing_date', '上市日期'),
|
|
126
|
+
field('listing_short_name', '股票简称'),
|
|
127
|
+
field('listing_stock_code', '股票代码'),
|
|
128
|
+
field('listing_exchange', '上市交易所'),
|
|
129
|
+
field('listing_board', '上市板块'),
|
|
130
|
+
field('listing_former_short_name', '上市曾用名'),
|
|
131
|
+
field('listing_total_market_value', '总市值'),
|
|
132
|
+
field('listing_total_shares', '总股本'),
|
|
133
|
+
field('listing_predicted_pe', '预测市盈率'),
|
|
134
|
+
field('listing_float_market_value', '流通值'),
|
|
135
|
+
field('listing_float_shares', '流通股'),
|
|
136
|
+
field('listing_pb_ratio', '市净率'),
|
|
137
|
+
field('listing_eps', 'EPS'),
|
|
138
|
+
field('listing_voting_rights_difference', '表决权差异'),
|
|
139
|
+
field('listing_registration_based', '是否注册制'),
|
|
140
|
+
], { releaseBatch: 'batch-1', selectionNote: '首个 A 股或红筹证券快照' }),
|
|
141
|
+
group('tax_invoice_info', '税务开票信息', 'get_tax_invoice_info', [
|
|
142
|
+
field('tax_company_name', '税务主体名称'),
|
|
143
|
+
field('tax_identification_no', '税务纳税人识别号'),
|
|
144
|
+
field('tax_company_type', '税务企业类型'),
|
|
145
|
+
field('tax_business_status', '税务经营状态'),
|
|
146
|
+
field('invoice_address', '开票地址'),
|
|
147
|
+
field('invoice_phone', '开票联系电话'),
|
|
148
|
+
field('invoice_bank', '开户行'),
|
|
149
|
+
field('invoice_bank_account', '开户行账号'),
|
|
150
|
+
], { releaseBatch: 'batch-1' }),
|
|
151
|
+
group('import_export_credit', '进出口信用', 'get_import_export_credit', [
|
|
152
|
+
field('import_export_credit_no', '进出口统一社会信用代码'),
|
|
153
|
+
field('import_export_customs', '所在地海关'),
|
|
154
|
+
field('import_export_admin_division', '进出口行政区划'),
|
|
155
|
+
field('import_export_address', '进出口备案地址'),
|
|
156
|
+
field('import_export_economic_area', '经济区划'),
|
|
157
|
+
field('import_export_trade_type', '经营类别'),
|
|
158
|
+
field('import_export_statistical_economic_area', '统计经济区划'),
|
|
159
|
+
field('import_export_industry', '进出口行业种类'),
|
|
160
|
+
field('import_export_ecommerce_type', '跨境贸易电子商务类型'),
|
|
161
|
+
field('import_export_credit_grade', '海关信用等级'),
|
|
162
|
+
field('import_export_filing_date', '进出口备案日期'),
|
|
163
|
+
], { releaseBatch: 'batch-1' }),
|
|
164
|
+
group('company_risk_scan', '企业自身风险扫描', 'get_company_risk_scan', [
|
|
165
|
+
field('risk_recorded_factor_count', '风险有记录因子数'),
|
|
166
|
+
field('risk_no_record_factor_count', '风险无记录因子数'),
|
|
167
|
+
field('risk_hit_summary', '企业自身风险命中摘要'),
|
|
168
|
+
...SELF_RISK_FACTORS.map(([id, label]) => field(`risk_${id}_count`, `${label}条目数`)),
|
|
169
|
+
], { releaseBatch: 'batch-2', catalogVersion: RISK_FACTOR_CATALOG_VERSION }),
|
|
170
|
+
group('company_related_risk_scan', '企业关联风险扫描', 'get_company_related_risk_scan', [
|
|
171
|
+
field('related_risk_party_count', '有风险关联方数'),
|
|
172
|
+
field('related_risk_summary', '企业关联风险摘要'),
|
|
173
|
+
...RELATED_RISK_FACTORS.map(([id, label]) => field(`related_risk_${id}_count`, `关联风险-${label}条目数`)),
|
|
174
|
+
...RELATED_RISK_KEY_FACTORS.map(([id, label]) => field(`related_risk_${id}_party_count`, `关联风险-${label}命中关联方数`)),
|
|
175
|
+
], { releaseBatch: 'batch-2', catalogVersion: RISK_FACTOR_CATALOG_VERSION }),
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
export const QCC_FIELD_SOURCE_TOOL = Object.freeze(Object.fromEntries(
|
|
179
|
+
QCC_FIELD_CATALOG.flatMap((entry) => entry.fields.map((item) => [item.id, entry.sourceTool])),
|
|
180
|
+
));
|
|
181
|
+
|
|
182
|
+
export function selectedSourceTools(fieldSelection, fallbackFields = []) {
|
|
183
|
+
const fields = Array.isArray(fieldSelection) && fieldSelection.length ? fieldSelection : fallbackFields;
|
|
184
|
+
return [...new Set(fields.map((id) => QCC_FIELD_SOURCE_TOOL[String(id)]).filter(Boolean))];
|
|
185
|
+
}
|
package/lib/qcc-safety.js
CHANGED
|
@@ -63,7 +63,11 @@ export function redactSensitive(value, options = {}) {
|
|
|
63
63
|
|
|
64
64
|
export function safeAuditEvent(event) {
|
|
65
65
|
const source = event && typeof event === 'object' ? event : {};
|
|
66
|
-
|
|
66
|
+
const safeCatalogLabels = (value) => (Array.isArray(value) ? value : [])
|
|
67
|
+
.slice(0, 64)
|
|
68
|
+
.map((item) => redactSensitiveText(String(item ?? '')).slice(0, 128))
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
const safe = {
|
|
67
71
|
at: String(source.at ?? new Date().toISOString()),
|
|
68
72
|
event: 'qcc-tool-call',
|
|
69
73
|
toolName: String(source.toolName ?? ''),
|
|
@@ -74,4 +78,10 @@ export function safeAuditEvent(event) {
|
|
|
74
78
|
upstreamCode: source.upstreamCode ? String(source.upstreamCode) : null,
|
|
75
79
|
durationMs: Math.max(0, Number(source.durationMs ?? 0)),
|
|
76
80
|
};
|
|
81
|
+
if (source.catalogVersion || Array.isArray(source.missing) || Array.isArray(source.unknown)) {
|
|
82
|
+
safe.catalogVersion = source.catalogVersion ? String(source.catalogVersion).slice(0, 64) : null;
|
|
83
|
+
safe.missing = safeCatalogLabels(source.missing);
|
|
84
|
+
safe.unknown = safeCatalogLabels(source.unknown);
|
|
85
|
+
}
|
|
86
|
+
return safe;
|
|
77
87
|
}
|
package/lib/qcc.js
CHANGED
|
@@ -18,6 +18,13 @@ import {
|
|
|
18
18
|
QCC_PHASE3_DOMAIN_META,
|
|
19
19
|
QCC_PHASE3_TOOL_NAMES,
|
|
20
20
|
} from './qcc-phase3.js';
|
|
21
|
+
import {
|
|
22
|
+
RELATED_RISK_FACTORS,
|
|
23
|
+
RELATED_RISK_KEY_FACTORS,
|
|
24
|
+
RISK_FACTOR_CATALOG_VERSION,
|
|
25
|
+
SELF_RISK_FACTORS,
|
|
26
|
+
selectedSourceTools,
|
|
27
|
+
} from './qcc-field-catalog.js';
|
|
21
28
|
|
|
22
29
|
export const QCC_TOOL_NAMES = Object.freeze({
|
|
23
30
|
oauthConnect: 'qcc_oauth_connect',
|
|
@@ -25,7 +32,12 @@ export const QCC_TOOL_NAMES = Object.freeze({
|
|
|
25
32
|
entityLookup: 'mcp__qcc-company__get_company_by_query',
|
|
26
33
|
registration: 'mcp__qcc-company__get_company_registration_info',
|
|
27
34
|
profile: 'mcp__qcc-company__get_company_profile',
|
|
35
|
+
contact: 'mcp__qcc-company__get_contact_info',
|
|
36
|
+
listing: 'mcp__qcc-company__get_listing_info',
|
|
37
|
+
taxInvoice: 'mcp__qcc-company__get_tax_invoice_info',
|
|
38
|
+
importExportCredit: 'mcp__qcc-operation__get_import_export_credit',
|
|
28
39
|
riskScan: 'mcp__qcc-risk__get_company_risk_scan',
|
|
40
|
+
relatedRiskScan: 'mcp__qcc-risk__get_company_related_risk_scan',
|
|
29
41
|
});
|
|
30
42
|
|
|
31
43
|
const EXACT_MATCH = '唯一精确匹配';
|
|
@@ -35,6 +47,7 @@ const DEFAULT_TIMEOUT_MS = 20_000;
|
|
|
35
47
|
const DEFAULT_TOOL_WAIT_MS = 1_500;
|
|
36
48
|
const DEFAULT_POLL_MS = 50;
|
|
37
49
|
const DEFAULT_MAX_ROWS = 100;
|
|
50
|
+
const DEFAULT_MAX_CALLS = 300;
|
|
38
51
|
const MAX_CONCURRENCY = 4;
|
|
39
52
|
|
|
40
53
|
function isRecord(value) {
|
|
@@ -324,72 +337,244 @@ export function mapRegistrationFields(value, fallback = {}) {
|
|
|
324
337
|
}
|
|
325
338
|
return '';
|
|
326
339
|
};
|
|
327
|
-
const region = text('所属地区', '所属地域', '地区');
|
|
328
|
-
const location = splitAdministrativeRegion(region);
|
|
329
340
|
return {
|
|
330
341
|
company_name: text('企业名称') || String(fallback.companyName ?? ''),
|
|
331
342
|
credit_no: text('统一社会信用代码', '信用代码') || String(fallback.creditNo ?? ''),
|
|
332
343
|
reg_no: text('工商注册号', '注册号'),
|
|
333
344
|
org_no: text('组织机构代码'),
|
|
345
|
+
tax_no: text('纳税人识别号'),
|
|
334
346
|
reg_status: text('登记状态', '执业状态', '证书状态'),
|
|
335
347
|
legal_rep: text('法定代表人', '负责人', '经营者'),
|
|
336
348
|
reg_capital: text('注册资本', '注册资金', '开办资金', '成员出资总额', '资金数额'),
|
|
337
349
|
paid_capital: text('实缴资本'),
|
|
338
350
|
establish_date: text('成立日期'),
|
|
339
351
|
company_type: text('企业类型', '公司类型'),
|
|
352
|
+
approval_date: text('核准日期'),
|
|
340
353
|
registration_authority: text('登记机关'),
|
|
354
|
+
taxpayer_qualification: text('纳税人资质'),
|
|
355
|
+
payment_line_no: text('支付系统行号'),
|
|
356
|
+
import_export_company_code: text('进出口企业代码'),
|
|
357
|
+
short_name: text('企业简称'),
|
|
341
358
|
english_name: text('英文名', '英文名称'),
|
|
342
359
|
registered_address: text('注册地址', '住所', '经营场所'),
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
district: text('区县', '所属区县') || location.district,
|
|
360
|
+
mailing_address: text('通信地址'),
|
|
361
|
+
region: text('所属地区'),
|
|
346
362
|
business_scope: text('经营范围'),
|
|
347
363
|
industry_category: text('国标行业'),
|
|
348
|
-
industry_large: text('一级行业', '企查查一级行业'),
|
|
349
|
-
industry_middle: text('二级行业', '企查查二级行业'),
|
|
350
364
|
operating_period: text('营业期限', '经营期限'),
|
|
351
|
-
company_size: text('
|
|
352
|
-
|
|
365
|
+
company_size: text('人员规模'),
|
|
366
|
+
insured_count: text('参保人数'),
|
|
367
|
+
branch_insured_count: text('分支机构参保人数'),
|
|
353
368
|
};
|
|
354
369
|
}
|
|
355
370
|
|
|
356
|
-
function splitAdministrativeRegion(value) {
|
|
357
|
-
const region = String(value ?? '').trim().replace(/\s+/g, '');
|
|
358
|
-
if (!region) return { province: '', city: '', district: '' };
|
|
359
|
-
const municipality = region.match(/^((?:北京|上海|天津|重庆)市)(.*)$/);
|
|
360
|
-
if (municipality) {
|
|
361
|
-
const district = municipality[2].match(/^(.+?(?:区|县))/)?.[1] ?? '';
|
|
362
|
-
return { province: municipality[1], city: municipality[1], district };
|
|
363
|
-
}
|
|
364
|
-
const provinceMatch = region.match(/^(.+?(?:省|自治区|特别行政区))/);
|
|
365
|
-
const province = provinceMatch?.[1] ?? '';
|
|
366
|
-
const remainder = province ? region.slice(province.length) : region;
|
|
367
|
-
const city = remainder.match(/^(.+?(?:市|自治州|地区|盟))/)?.[1] ?? '';
|
|
368
|
-
const districtRemainder = city ? remainder.slice(city.length) : remainder;
|
|
369
|
-
const district = districtRemainder.match(/^(.+?(?:区|县|旗|市))/)?.[1] ?? '';
|
|
370
|
-
return { province, city, district };
|
|
371
|
-
}
|
|
372
|
-
|
|
373
371
|
export function mapProfileFields(value) {
|
|
374
372
|
if (!isRecord(value)) {
|
|
375
373
|
throw new QccBridgeError('QCC_CONTRACT_MISMATCH', 'QCC profile tool returned a non-object result');
|
|
376
374
|
}
|
|
377
375
|
if (value.无匹配项 !== undefined) return {};
|
|
378
376
|
return {
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
industry_middle: String(value.二级行业 ?? value.企查查二级行业 ?? ''),
|
|
377
|
+
// 工具只返回“企查查行业”这一最细层级展示值,不得猜测为一级或二级行业。
|
|
378
|
+
qcc_industry: String(value.企查查行业 ?? ''),
|
|
382
379
|
company_profile: String(value.企业简介 ?? value.简介 ?? ''),
|
|
380
|
+
industry_chain_overview: String(value.产业链概览 ?? ''),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function scalarText(value, ...keys) {
|
|
385
|
+
if (!isRecord(value)) return '';
|
|
386
|
+
for (const key of keys) {
|
|
387
|
+
const item = value[key];
|
|
388
|
+
if (item !== undefined && item !== null && String(item).trim()) return String(item);
|
|
389
|
+
}
|
|
390
|
+
return '';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function scalarMap(value, mapping) {
|
|
394
|
+
if (!isRecord(value) || value.无匹配项 !== undefined || value.地域限制 !== undefined) return {};
|
|
395
|
+
return Object.fromEntries(mapping.map(([id, ...keys]) => [id, scalarText(value, ...keys)]));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function mapContactFields(value) {
|
|
399
|
+
if (!isRecord(value)) return {};
|
|
400
|
+
const contact = isRecord(value.联系方式信息) ? value.联系方式信息 : {};
|
|
401
|
+
const phone = Array.isArray(contact.电话) && isRecord(contact.电话[0]) ? contact.电话[0] : {};
|
|
402
|
+
const email = Array.isArray(contact.邮箱) && isRecord(contact.邮箱[0]) ? contact.邮箱[0] : {};
|
|
403
|
+
const website = Array.isArray(contact.网址)
|
|
404
|
+
? contact.网址.find((item) => isRecord(item) && item.是否是官网 === '是') ?? {}
|
|
405
|
+
: {};
|
|
406
|
+
return {
|
|
407
|
+
contact_preferred_phone: scalarText(phone, '电话号码'),
|
|
408
|
+
contact_phone_invalid_flag: scalarText(phone, '是否无效'),
|
|
409
|
+
contact_phone_tags: Array.isArray(phone.标签)
|
|
410
|
+
? phone.标签.map((item) => String(item).trim()).filter(Boolean).join(';')
|
|
411
|
+
: '',
|
|
412
|
+
contact_preferred_email: scalarText(email, '邮箱'),
|
|
413
|
+
contact_official_website: scalarText(website, '网址'),
|
|
414
|
+
contact_official_website_icp: scalarText(website, 'ICP备案'),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function mapListingFields(value) {
|
|
419
|
+
return scalarMap(value, [
|
|
420
|
+
['listing_date', '上市日期'],
|
|
421
|
+
['listing_short_name', '股票简称'],
|
|
422
|
+
['listing_stock_code', '股票代码'],
|
|
423
|
+
['listing_exchange', '上市交易所'],
|
|
424
|
+
['listing_board', '上市板块'],
|
|
425
|
+
['listing_former_short_name', '上市曾用名'],
|
|
426
|
+
['listing_total_market_value', '总市值'],
|
|
427
|
+
['listing_total_shares', '总股本'],
|
|
428
|
+
['listing_predicted_pe', '预测市盈率'],
|
|
429
|
+
['listing_float_market_value', '流通值'],
|
|
430
|
+
['listing_float_shares', '流通股'],
|
|
431
|
+
['listing_pb_ratio', '市净率'],
|
|
432
|
+
['listing_eps', 'EPS'],
|
|
433
|
+
['listing_voting_rights_difference', '表决权差异'],
|
|
434
|
+
['listing_registration_based', '是否注册制'],
|
|
435
|
+
]);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function mapTaxInvoiceFields(value) {
|
|
439
|
+
return scalarMap(value, [
|
|
440
|
+
['tax_company_name', '企业名称'],
|
|
441
|
+
['tax_identification_no', '纳税人识别号'],
|
|
442
|
+
['tax_company_type', '企业类型'],
|
|
443
|
+
['tax_business_status', '经营状态'],
|
|
444
|
+
['invoice_address', '地址'],
|
|
445
|
+
['invoice_phone', '联系电话'],
|
|
446
|
+
['invoice_bank', '开户行'],
|
|
447
|
+
['invoice_bank_account', '开户行账号'],
|
|
448
|
+
]);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function mapImportExportCreditFields(value) {
|
|
452
|
+
return scalarMap(value, [
|
|
453
|
+
['import_export_credit_no', '统一社会信用代码'],
|
|
454
|
+
['import_export_customs', '所在地海关'],
|
|
455
|
+
['import_export_admin_division', '行政区划'],
|
|
456
|
+
['import_export_address', '地址'],
|
|
457
|
+
['import_export_economic_area', '经济区划'],
|
|
458
|
+
['import_export_trade_type', '经营类别'],
|
|
459
|
+
['import_export_statistical_economic_area', '统计经济区划'],
|
|
460
|
+
['import_export_industry', '行业种类'],
|
|
461
|
+
['import_export_ecommerce_type', '跨境贸易电子商务类型'],
|
|
462
|
+
['import_export_credit_grade', '信用等级'],
|
|
463
|
+
['import_export_filing_date', '备案日期'],
|
|
464
|
+
]);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function countValue(value) {
|
|
468
|
+
if (value === '' || value === null || value === undefined) return '';
|
|
469
|
+
const number = Number(value);
|
|
470
|
+
return Number.isFinite(number) && number >= 0 ? number : '';
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function selfRiskRows(value) {
|
|
474
|
+
return Array.isArray(value?.风险因子扫描) ? value.风险因子扫描.filter(isRecord) : [];
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function inspectSelfRiskCatalog(value) {
|
|
478
|
+
const applicable = Array.isArray(value?.风险因子扫描);
|
|
479
|
+
const actual = new Set(selfRiskRows(value).map((row) => String(row.风险因子 ?? '').trim()).filter(Boolean));
|
|
480
|
+
const expected = new Set(SELF_RISK_FACTORS.map(([, label]) => label));
|
|
481
|
+
return {
|
|
482
|
+
applicable,
|
|
483
|
+
version: RISK_FACTOR_CATALOG_VERSION,
|
|
484
|
+
missing: [...expected].filter((label) => !actual.has(label)),
|
|
485
|
+
unknown: [...actual].filter((label) => !expected.has(label)),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function mapCompanyRiskScanFields(value) {
|
|
490
|
+
if (!isRecord(value) || !Array.isArray(value.风险因子扫描)) return {};
|
|
491
|
+
const counts = new Map(selfRiskRows(value).map((row) => [String(row.风险因子 ?? '').trim(), countValue(row.条目数)]));
|
|
492
|
+
const hits = SELF_RISK_FACTORS.flatMap(([, label]) => {
|
|
493
|
+
const count = counts.get(label);
|
|
494
|
+
return typeof count === 'number' && count > 0 ? [`${label}(${count})`] : [];
|
|
495
|
+
});
|
|
496
|
+
return {
|
|
497
|
+
risk_recorded_factor_count: countValue(value.有记录因子数),
|
|
498
|
+
risk_no_record_factor_count: countValue(value.无记录因子数),
|
|
499
|
+
risk_hit_summary: hits.join(';'),
|
|
500
|
+
...Object.fromEntries(SELF_RISK_FACTORS.map(([id, label]) => [`risk_${id}_count`, counts.get(label) ?? ''])),
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export function inspectRelatedRiskCatalog(value) {
|
|
505
|
+
const applicable = isRecord(value?.维度计数汇总);
|
|
506
|
+
// QCC MCP 当前契约名为“重要风险”;“关键风险”仅作早期预发环境兼容,
|
|
507
|
+
// 不得反向把兼容名称当成稳定上游契约。
|
|
508
|
+
const importantSource = value?.维度计数汇总?.重要风险 ?? value?.维度计数汇总?.关键风险;
|
|
509
|
+
const important = isRecord(importantSource) ? importantSource : {};
|
|
510
|
+
const locating = Array.isArray(value?.重点维度关联方定位) ? value.重点维度关联方定位.filter(isRecord) : [];
|
|
511
|
+
const importantActual = new Set(Object.keys(important));
|
|
512
|
+
const keyActual = new Set(locating.map((row) => String(row.维度 ?? '').trim()).filter(Boolean));
|
|
513
|
+
const importantExpected = new Set(RELATED_RISK_FACTORS.map(([, label]) => label));
|
|
514
|
+
const keyExpected = new Set(RELATED_RISK_KEY_FACTORS.map(([, label]) => label));
|
|
515
|
+
return {
|
|
516
|
+
applicable,
|
|
517
|
+
version: RISK_FACTOR_CATALOG_VERSION,
|
|
518
|
+
missing: [
|
|
519
|
+
...[...importantExpected].filter((label) => !importantActual.has(label)).map((label) => `重要风险:${label}`),
|
|
520
|
+
...[...keyExpected].filter((label) => !keyActual.has(label)).map((label) => `重点维度:${label}`),
|
|
521
|
+
],
|
|
522
|
+
unknown: [
|
|
523
|
+
...[...importantActual].filter((label) => !importantExpected.has(label)).map((label) => `重要风险:${label}`),
|
|
524
|
+
...[...keyActual].filter((label) => !keyExpected.has(label)).map((label) => `重点维度:${label}`),
|
|
525
|
+
],
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function mapCompanyRelatedRiskScanFields(value) {
|
|
530
|
+
if (!isRecord(value) || !isRecord(value.维度计数汇总)) return {};
|
|
531
|
+
const importantSource = value.维度计数汇总.重要风险 ?? value.维度计数汇总.关键风险;
|
|
532
|
+
const important = isRecord(importantSource) ? importantSource : {};
|
|
533
|
+
const locating = Array.isArray(value.重点维度关联方定位) ? value.重点维度关联方定位.filter(isRecord) : [];
|
|
534
|
+
const partyCounts = new Map(locating.map((row) => [String(row.维度 ?? '').trim(), countValue(row.命中关联方数)]));
|
|
535
|
+
const hits = RELATED_RISK_FACTORS.flatMap(([, label]) => {
|
|
536
|
+
const count = countValue(important[label]);
|
|
537
|
+
return typeof count === 'number' && count > 0 ? [`${label}(${count})`] : [];
|
|
538
|
+
});
|
|
539
|
+
const partyCount = countValue(value.有风险关联方数);
|
|
540
|
+
return {
|
|
541
|
+
related_risk_party_count: partyCount,
|
|
542
|
+
related_risk_summary: `${partyCount === '' ? '' : `有风险关联方${partyCount}个`}${hits.length ? `${partyCount === '' ? '' : ';'}${hits.join(';')}` : ''}`,
|
|
543
|
+
...Object.fromEntries(RELATED_RISK_FACTORS.map(([id, label]) => [`related_risk_${id}_count`, countValue(important[label])])),
|
|
544
|
+
...Object.fromEntries(RELATED_RISK_KEY_FACTORS.map(([id, label]) => [`related_risk_${id}_party_count`, partyCounts.get(label) ?? ''])),
|
|
383
545
|
};
|
|
384
546
|
}
|
|
385
547
|
|
|
386
548
|
const LEGACY_ENRICHMENT_FIELDS = Object.freeze([
|
|
387
|
-
'credit_no', 'legal_rep', 'reg_capital', 'establish_date', 'reg_status',
|
|
549
|
+
'credit_no', 'legal_rep', 'reg_capital', 'establish_date', 'reg_status',
|
|
388
550
|
]);
|
|
389
|
-
const
|
|
551
|
+
const SOURCE_TOOL_CONFIG = Object.freeze({
|
|
552
|
+
get_company_registration_info: Object.freeze({ name: QCC_TOOL_NAMES.registration, map: mapRegistrationFields }),
|
|
553
|
+
get_company_profile: Object.freeze({ name: QCC_TOOL_NAMES.profile, map: mapProfileFields }),
|
|
554
|
+
get_contact_info: Object.freeze({ name: QCC_TOOL_NAMES.contact, map: mapContactFields, args: { excludeInvalidPhone: false } }),
|
|
555
|
+
get_listing_info: Object.freeze({ name: QCC_TOOL_NAMES.listing, map: mapListingFields }),
|
|
556
|
+
get_tax_invoice_info: Object.freeze({ name: QCC_TOOL_NAMES.taxInvoice, map: mapTaxInvoiceFields }),
|
|
557
|
+
get_import_export_credit: Object.freeze({ name: QCC_TOOL_NAMES.importExportCredit, map: mapImportExportCreditFields }),
|
|
558
|
+
get_company_risk_scan: Object.freeze({ name: QCC_TOOL_NAMES.riskScan, map: mapCompanyRiskScanFields, inspect: inspectSelfRiskCatalog }),
|
|
559
|
+
get_company_related_risk_scan: Object.freeze({ name: QCC_TOOL_NAMES.relatedRiskScan, map: mapCompanyRelatedRiskScanFields, inspect: inspectRelatedRiskCatalog }),
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
export function sourceToolsForFieldSelection(fieldSelection, includeRisk = false) {
|
|
563
|
+
const selected = Array.isArray(fieldSelection) && fieldSelection.length ? fieldSelection : LEGACY_ENRICHMENT_FIELDS;
|
|
564
|
+
const tools = selectedSourceTools(selected, LEGACY_ENRICHMENT_FIELDS);
|
|
565
|
+
if (includeRisk && !tools.includes('get_company_risk_scan')) tools.push('get_company_risk_scan');
|
|
566
|
+
return tools;
|
|
567
|
+
}
|
|
390
568
|
|
|
391
|
-
function
|
|
392
|
-
|
|
569
|
+
export function estimateQccCalls(uniqueCompanies, fieldSelection, includeRisk = false) {
|
|
570
|
+
const companies = Math.max(0, Math.trunc(Number(uniqueCompanies) || 0));
|
|
571
|
+
const sourceTools = sourceToolsForFieldSelection(fieldSelection, includeRisk);
|
|
572
|
+
return {
|
|
573
|
+
uniqueCompanies: companies,
|
|
574
|
+
sourceTools,
|
|
575
|
+
callsPerCompany: 1 + sourceTools.length,
|
|
576
|
+
estimatedCalls: companies * (1 + sourceTools.length),
|
|
577
|
+
};
|
|
393
578
|
}
|
|
394
579
|
|
|
395
580
|
function mergeMappedFields(...sources) {
|
|
@@ -481,7 +666,12 @@ export class QccHostBridge {
|
|
|
481
666
|
entityLookup: this.has(QCC_TOOL_NAMES.entityLookup),
|
|
482
667
|
registration: this.has(QCC_TOOL_NAMES.registration),
|
|
483
668
|
profile: this.has(QCC_TOOL_NAMES.profile),
|
|
669
|
+
contact: this.has(QCC_TOOL_NAMES.contact),
|
|
670
|
+
listing: this.has(QCC_TOOL_NAMES.listing),
|
|
671
|
+
taxInvoice: this.has(QCC_TOOL_NAMES.taxInvoice),
|
|
672
|
+
importExportCredit: this.has(QCC_TOOL_NAMES.importExportCredit),
|
|
484
673
|
riskScan: this.has(QCC_TOOL_NAMES.riskScan),
|
|
674
|
+
relatedRiskScan: this.has(QCC_TOOL_NAMES.relatedRiskScan),
|
|
485
675
|
};
|
|
486
676
|
const ready = capabilities.entityLookup && capabilities.registration;
|
|
487
677
|
return {
|
|
@@ -720,28 +910,39 @@ export class QccHostBridge {
|
|
|
720
910
|
);
|
|
721
911
|
const match = classifyEntityMatch(lookup.data);
|
|
722
912
|
if (match.status !== 'exact') return match;
|
|
913
|
+
return this.enrichMatchedCompany(match, options);
|
|
914
|
+
}
|
|
723
915
|
|
|
724
|
-
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
)
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const
|
|
733
|
-
|
|
916
|
+
async enrichMatchedCompany(match, options = {}) {
|
|
917
|
+
const lockedKey = String(match?.creditNo || match?.companyName || '').trim();
|
|
918
|
+
const sourceTools = sourceToolsForFieldSelection(options.fieldSelection, options.includeRisk);
|
|
919
|
+
let mapped = {};
|
|
920
|
+
let legacyRiskTags = '';
|
|
921
|
+
for (const sourceTool of sourceTools) {
|
|
922
|
+
const config = SOURCE_TOOL_CONFIG[sourceTool];
|
|
923
|
+
if (!config) continue;
|
|
924
|
+
const response = await this.call(config.name, { searchKey: lockedKey, ...(config.args ?? {}) }, options);
|
|
925
|
+
const fields = config.map(response.data, match);
|
|
926
|
+
mapped = mergeMappedFields(mapped, fields);
|
|
927
|
+
if (sourceTool === 'get_company_risk_scan') legacyRiskTags = mapRiskTags(response.data);
|
|
928
|
+
if (config.inspect) {
|
|
929
|
+
const catalog = config.inspect(response.data);
|
|
930
|
+
if (catalog.applicable && (catalog.missing.length || catalog.unknown.length)) {
|
|
931
|
+
emitAudit(options, {
|
|
932
|
+
toolName: response.toolName,
|
|
933
|
+
callId: response.callId,
|
|
934
|
+
outcome: 'catalog-drift',
|
|
935
|
+
code: 'QCC_RISK_CATALOG_DRIFT',
|
|
936
|
+
catalogVersion: catalog.version,
|
|
937
|
+
missing: catalog.missing,
|
|
938
|
+
unknown: catalog.unknown,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
}
|
|
734
942
|
}
|
|
735
943
|
const fields = projectSelectedFields(mapped, options.fieldSelection);
|
|
736
|
-
if (options.includeRisk)
|
|
737
|
-
|
|
738
|
-
fields.risk_tags = mapRiskTags(risk.data);
|
|
739
|
-
}
|
|
740
|
-
return {
|
|
741
|
-
status: 'enriched',
|
|
742
|
-
companyName: match.companyName,
|
|
743
|
-
fields,
|
|
744
|
-
};
|
|
944
|
+
if (options.includeRisk) fields.risk_tags = legacyRiskTags;
|
|
945
|
+
return { status: 'enriched', companyName: String(match?.companyName ?? ''), fields };
|
|
745
946
|
}
|
|
746
947
|
|
|
747
948
|
async enrichLockedCompany(selection, options = {}) {
|
|
@@ -750,22 +951,7 @@ export class QccHostBridge {
|
|
|
750
951
|
if (!creditNo) {
|
|
751
952
|
throw new QccBridgeError('QCC_CANDIDATE_INVALID', 'A selected QCC candidate must include a credit number');
|
|
752
953
|
}
|
|
753
|
-
|
|
754
|
-
QCC_TOOL_NAMES.registration,
|
|
755
|
-
{ searchKey: creditNo },
|
|
756
|
-
options,
|
|
757
|
-
);
|
|
758
|
-
let mapped = mapRegistrationFields(registration.data, { companyName, creditNo });
|
|
759
|
-
if (requiresProfile(options.fieldSelection)) {
|
|
760
|
-
const profile = await this.call(QCC_TOOL_NAMES.profile, { searchKey: creditNo }, options);
|
|
761
|
-
mapped = mergeMappedFields(mapped, mapProfileFields(profile.data));
|
|
762
|
-
}
|
|
763
|
-
const fields = projectSelectedFields(mapped, options.fieldSelection);
|
|
764
|
-
if (options.includeRisk) {
|
|
765
|
-
const risk = await this.call(QCC_TOOL_NAMES.riskScan, { searchKey: creditNo }, options);
|
|
766
|
-
fields.risk_tags = mapRiskTags(risk.data);
|
|
767
|
-
}
|
|
768
|
-
return { status: 'enriched', companyName, fields };
|
|
954
|
+
return this.enrichMatchedCompany({ companyName, creditNo }, options);
|
|
769
955
|
}
|
|
770
956
|
|
|
771
957
|
async enrichRows(rows, options = {}) {
|
|
@@ -777,9 +963,27 @@ export class QccHostBridge {
|
|
|
777
963
|
});
|
|
778
964
|
}
|
|
779
965
|
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
966
|
+
const nameField = String(options.nameField ?? 'name');
|
|
967
|
+
const normalized = rows.map((row, index) => ({
|
|
968
|
+
index,
|
|
969
|
+
row: isRecord(row) ? { ...row } : {},
|
|
970
|
+
companyName: String(isRecord(row) ? row[nameField] ?? '' : '').trim(),
|
|
971
|
+
}));
|
|
972
|
+
const names = [...new Set(normalized.map((item) => item.companyName).filter(Boolean))];
|
|
973
|
+
const estimate = estimateQccCalls(names.length, options.fieldSelection, options.includeRisk);
|
|
974
|
+
const maxCalls = Math.max(1, Math.trunc(options.maxCalls ?? DEFAULT_MAX_CALLS));
|
|
975
|
+
if (estimate.estimatedCalls > maxCalls) {
|
|
976
|
+
throw new QccBridgeError('QCC_CALL_BUDGET_EXCEEDED', `QCC call estimate exceeds ${maxCalls}`, {
|
|
977
|
+
details: { ...estimate, maxCalls },
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const requiredTools = [
|
|
982
|
+
QCC_TOOL_NAMES.entityLookup,
|
|
983
|
+
...estimate.sourceTools
|
|
984
|
+
.map((sourceTool) => SOURCE_TOOL_CONFIG[sourceTool]?.name)
|
|
985
|
+
.filter(Boolean),
|
|
986
|
+
];
|
|
783
987
|
try {
|
|
784
988
|
await Promise.all(requiredTools.map((name) => this.waitForTool(name, {
|
|
785
989
|
signal: options.signal,
|
|
@@ -796,13 +1000,6 @@ export class QccHostBridge {
|
|
|
796
1000
|
});
|
|
797
1001
|
}
|
|
798
1002
|
|
|
799
|
-
const nameField = String(options.nameField ?? 'name');
|
|
800
|
-
const normalized = rows.map((row, index) => ({
|
|
801
|
-
index,
|
|
802
|
-
row: isRecord(row) ? { ...row } : {},
|
|
803
|
-
companyName: String(isRecord(row) ? row[nameField] ?? '' : '').trim(),
|
|
804
|
-
}));
|
|
805
|
-
const names = [...new Set(normalized.map((item) => item.companyName).filter(Boolean))];
|
|
806
1003
|
const concurrency = Math.min(MAX_CONCURRENCY, Math.max(1, Math.trunc(options.concurrency ?? 2)));
|
|
807
1004
|
let completedUnique = 0;
|
|
808
1005
|
|
|
@@ -857,6 +1054,6 @@ export class QccHostBridge {
|
|
|
857
1054
|
includeRisk: Boolean(options.includeRisk),
|
|
858
1055
|
};
|
|
859
1056
|
|
|
860
|
-
return { summary, rows: outputRows, reviewQueue, errors };
|
|
1057
|
+
return { summary, rows: outputRows, reviewQueue, errors, estimate };
|
|
861
1058
|
}
|
|
862
1059
|
}
|