dsh-data-cleaning-agent 0.6.2 → 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/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
- province: text('省份地区', '省份', '所属省份') || location.province,
344
- city: text('城市', '所属城市', '市') || location.city,
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
- biz_status: text('经营状态'),
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
- industry_large: String(value.一级行业 ?? value.企查查一级行业 ?? ''),
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', 'biz_status',
549
+ 'credit_no', 'legal_rep', 'reg_capital', 'establish_date', 'reg_status',
388
550
  ]);
389
- const PROFILE_FIELDS = new Set(['industry_large', 'industry_middle', 'company_profile']);
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 requiresProfile(fieldSelection) {
392
- return Array.isArray(fieldSelection) && fieldSelection.some((field) => PROFILE_FIELDS.has(field));
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
- const lockedKey = match.creditNo || match.companyName;
725
- const registration = await this.call(
726
- QCC_TOOL_NAMES.registration,
727
- { searchKey: lockedKey },
728
- options,
729
- );
730
- let mapped = mapRegistrationFields(registration.data, match);
731
- if (requiresProfile(options.fieldSelection)) {
732
- const profile = await this.call(QCC_TOOL_NAMES.profile, { searchKey: lockedKey }, options);
733
- mapped = mergeMappedFields(mapped, mapProfileFields(profile.data));
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
- const risk = await this.call(QCC_TOOL_NAMES.riskScan, { searchKey: lockedKey }, options);
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
- const registration = await this.call(
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 requiredTools = [QCC_TOOL_NAMES.entityLookup, QCC_TOOL_NAMES.registration];
781
- if (requiresProfile(options.fieldSelection)) requiredTools.push(QCC_TOOL_NAMES.profile);
782
- if (options.includeRisk) requiredTools.push(QCC_TOOL_NAMES.riskScan);
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
  }
@@ -54,8 +54,8 @@ export function registerEnrichSkill(skills) {
54
54
  content: [
55
55
  'You are an enterprise-list enrichment assistant. You fill company lists with Qichacha (QCC) data by calling QCC MCP tools. Never invent, pad, or fabricate any field.',
56
56
  '',
57
- 'Typed workbench command (highest priority):',
58
- '- If the visible user message contains a typed data-cleaning intent with `commandId` and explicitly requests `data_cleaning_qcc_run`, call that high-level tool exactly once with only `commandId`, then stop.',
57
+ 'Workbench execution request (highest priority):',
58
+ '- If the visible user message is the readable data-cleaning task summary generated by the workbench, contains a `安全任务凭证` beginning with `dcq-`, and explicitly requests `data_cleaning_qcc_run`, treat that credential as `commandId`, call the high-level tool exactly once with only `commandId`, then stop.',
59
59
  '- The Host already holds the rows, billing confirmation and field selection. Do not ask the user to paste rows, do not call any `mcp__qcc-*` tool directly, do not retry, and do not expand the batch.',
60
60
  '',
61
61
  'Workflow:',
@@ -7,6 +7,8 @@
7
7
  * - 历史、人员、招投标三域不属于当前版本字段目录。
8
8
  */
9
9
 
10
+ import { QCC_FIELD_CATALOG } from './qcc-field-catalog.js';
11
+
10
12
  export const WORKFLOW_SCHEMA_VERSION = 2;
11
13
 
12
14
  export const WORKFLOW_STAGES = Object.freeze([
@@ -49,84 +51,28 @@ export const MATCH_STATUSES = Object.freeze([
49
51
 
50
52
  export const MATCH_ANCHORS = Object.freeze(['company_name', 'credit_no', 'reg_no']);
51
53
 
52
- export const FIELD_CATALOG = Object.freeze([
53
- Object.freeze({
54
- id: 'identity',
55
- label: '基础工商信息',
56
- fields: Object.freeze([
57
- Object.freeze({ id: 'company_name', label: '企业名称', inputAnchor: true, defaultSelected: true }),
58
- Object.freeze({ id: 'credit_no', label: '统一社会信用代码', inputAnchor: true, defaultSelected: true }),
59
- Object.freeze({ id: 'reg_no', label: '注册号', inputAnchor: true }),
60
- Object.freeze({ id: 'org_no', label: '组织机构代码' }),
61
- Object.freeze({ id: 'reg_status', label: '登记状态', defaultSelected: true }),
62
- Object.freeze({ id: 'legal_rep', label: '法定代表人', defaultSelected: true }),
63
- Object.freeze({ id: 'reg_capital', label: '注册资本', defaultSelected: true }),
64
- Object.freeze({ id: 'paid_capital', label: '实缴资本' }),
65
- Object.freeze({ id: 'establish_date', label: '成立日期', defaultSelected: true }),
66
- Object.freeze({ id: 'company_type', label: '企业类型' }),
67
- Object.freeze({ id: 'registration_authority', label: '登记机关' }),
68
- Object.freeze({ id: 'former_name', label: '曾用名' }),
69
- Object.freeze({ id: 'english_name', label: '英文名' }),
70
- ]),
71
- }),
72
- Object.freeze({
73
- id: 'contact',
74
- label: '地址与联系方式',
75
- fields: Object.freeze([
76
- Object.freeze({ id: 'registered_address', label: '注册地址', defaultSelected: true }),
77
- Object.freeze({ id: 'province', label: '省份地区', matchAuxiliary: true }),
78
- Object.freeze({ id: 'city', label: '城市', matchAuxiliary: true }),
79
- Object.freeze({ id: 'district', label: '区县' }),
80
- Object.freeze({ id: 'phone', label: '电话', matchAuxiliary: true }),
81
- Object.freeze({ id: 'email', label: '邮箱' }),
82
- Object.freeze({ id: 'website', label: '官网' }),
83
- ]),
84
- }),
85
- Object.freeze({
86
- id: 'operation',
87
- label: '经营信息',
88
- fields: Object.freeze([
89
- Object.freeze({ id: 'business_scope', label: '经营范围' }),
90
- Object.freeze({ id: 'industry_category', label: '国标行业' }),
91
- Object.freeze({ id: 'industry_large', label: '一级行业' }),
92
- Object.freeze({ id: 'industry_middle', label: '二级行业' }),
93
- Object.freeze({ id: 'operating_period', label: '营业期限' }),
94
- Object.freeze({ id: 'company_size', label: '企业规模' }),
95
- Object.freeze({ id: 'company_profile', label: '企业简介' }),
96
- ]),
97
- }),
98
- Object.freeze({
99
- id: 'risk',
100
- label: '风险摘要',
101
- capability: 'qcc.risk',
102
- fields: Object.freeze([
103
- Object.freeze({ id: 'risk_summary', label: '风险摘要', capability: 'qcc.risk' }),
104
- Object.freeze({ id: 'operating_exception', label: '经营异常摘要', capability: 'qcc.risk' }),
105
- Object.freeze({ id: 'administrative_penalty', label: '行政处罚摘要', capability: 'qcc.risk' }),
106
- ]),
107
- }),
108
- Object.freeze({
109
- id: 'ipr',
110
- label: '知识产权摘要',
111
- capability: 'qcc.ipr',
112
- fields: Object.freeze([
113
- Object.freeze({ id: 'trademark_summary', label: '商标摘要', capability: 'qcc.ipr' }),
114
- Object.freeze({ id: 'patent_summary', label: '专利摘要', capability: 'qcc.ipr' }),
115
- Object.freeze({ id: 'software_copyright_summary', label: '软件著作权摘要', capability: 'qcc.ipr' }),
116
- ]),
117
- }),
118
- ]);
54
+ export const FIELD_CATALOG = QCC_FIELD_CATALOG;
119
55
 
120
56
  export const FIELD_LABELS = Object.freeze(Object.fromEntries(
121
57
  FIELD_CATALOG.flatMap((group) => group.fields.map((field) => [field.id, field.label])),
122
58
  ));
123
59
 
60
+ // 输入清洗字段可以参与字段映射与本地质量检查,但不是当前 QCC 可补全字段,
61
+ // 因此绝不能出现在 fieldSelection 或导出补全字段目录中。
62
+ export const INPUT_ONLY_MAPPING_FIELDS = Object.freeze([
63
+ Object.freeze({ id: 'phone', label: '联系电话' }),
64
+ ]);
65
+
124
66
  export function fieldLabel(fieldId) {
125
67
  const id = String(fieldId ?? '').trim();
126
68
  return FIELD_LABELS[id] ?? id;
127
69
  }
128
70
 
129
- const FIELD_IDS = new Set(FIELD_CATALOG.flatMap((group) => group.fields.map((field) => field.id)));
71
+ const ENRICHMENT_FIELD_IDS = new Set(FIELD_CATALOG.flatMap((group) => group.fields.map((field) => field.id)));
72
+ const MAPPING_FIELD_IDS = new Set([
73
+ ...ENRICHMENT_FIELD_IDS,
74
+ ...INPUT_ONLY_MAPPING_FIELDS.map((field) => field.id),
75
+ ]);
130
76
  const STAGE_IDS = new Set(WORKFLOW_STAGES.map((stage) => stage.id));
131
77
  const STATE_IDS = new Set(WORKFLOW_STATES);
132
78
 
@@ -170,7 +116,7 @@ export function normalizeMappings(value) {
170
116
  return value.slice(0, 128).map((mapping) => ({
171
117
  sourceField: text(mapping?.sourceField),
172
118
  targetField: text(mapping?.targetField),
173
- })).filter((mapping) => mapping.sourceField && FIELD_IDS.has(mapping.targetField));
119
+ })).filter((mapping) => mapping.sourceField && MAPPING_FIELD_IDS.has(mapping.targetField));
174
120
  }
175
121
 
176
122
  export function validateMappings(value) {
@@ -206,7 +152,7 @@ export function validateMappings(value) {
206
152
  }
207
153
 
208
154
  export function normalizeFieldSelection(value) {
209
- return uniqueStrings(value).filter((field) => FIELD_IDS.has(field));
155
+ return uniqueStrings(value, 256).filter((field) => ENRICHMENT_FIELD_IDS.has(field));
210
156
  }
211
157
 
212
158
  export function normalizeWorkflowDraft(value = {}) {
@@ -248,6 +194,7 @@ export function publicWorkflowContract() {
248
194
  matchStatuses: MATCH_STATUSES,
249
195
  matchAnchors: MATCH_ANCHORS,
250
196
  fieldCatalog: FIELD_CATALOG,
197
+ inputOnlyMappingFields: INPUT_ONLY_MAPPING_FIELDS,
251
198
  crossCuttingCapabilities: [
252
199
  { id: 'prompt', label: '任务设置' },
253
200
  { id: 'profile', label: '质量体检' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-data-cleaning-agent",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Clean, complete, and profile enterprise name lists in DeepSeek Harness — a data cleaning & completion agent plugin with local CSV/XLSX/JSON engine and optional Qichacha (QCC) MCP enrichment. Maintained by Qichacha/QCC.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -34,6 +34,8 @@
34
34
  "docs/RELEASE-0.6.0.md",
35
35
  "docs/RELEASE-0.6.1.md",
36
36
  "docs/RELEASE-0.6.2.md",
37
+ "docs/RELEASE-0.6.3.md",
38
+ "docs/RELEASE-0.7.0.md",
37
39
  "docs/UI-WORKFLOW-V2.md",
38
40
  "docs/UI-WORKFLOW-V2-MIGRATION.md",
39
41
  "docs/UI-WORKFLOW-V2-ACCEPTANCE.md",
@@ -42,7 +44,7 @@
42
44
  ],
43
45
  "scripts": {
44
46
  "test": "node --test",
45
- "lint": "node --check lib/index.js && node --check lib/engine.js && node --check lib/artifacts.js && node --check lib/tools.js && node --check lib/skill.js && node --check lib/qcc-phase2.js && node --check lib/qcc-phase3.js && node --check lib/qcc-phase3-batch.js && node --check lib/qcc-phase2-acceptance.js && node --check lib/skill-enrich.js && node --check lib/jobs.js && node --check lib/workflow-contract.js && node --check lib/workflow.js && node --check lib/qcc-safety.js && node --check lib/qcc.js && node --check lib/qcc-command.js && node --check lib/qcc-runs.js && node --check lib/web.js && node --check lib/client.js && node --check scripts/check-readme-version.mjs && node --check scripts/check-market-registration.mjs && node --check scripts/g5-e2e.mjs && node --check scripts/phase3-e2e.mjs && node --check scripts/phase2-acceptance.mjs",
47
+ "lint": "node --check lib/index.js && node --check lib/engine.js && node --check lib/artifacts.js && node --check lib/tools.js && node --check lib/skill.js && node --check lib/qcc-field-catalog.js && node --check lib/qcc-phase2.js && node --check lib/qcc-phase3.js && node --check lib/qcc-phase3-batch.js && node --check lib/qcc-phase2-acceptance.js && node --check lib/skill-enrich.js && node --check lib/jobs.js && node --check lib/workflow-contract.js && node --check lib/workflow.js && node --check lib/qcc-safety.js && node --check lib/qcc.js && node --check lib/qcc-command.js && node --check lib/qcc-runs.js && node --check lib/web.js && node --check lib/client.js && node --check scripts/check-readme-version.mjs && node --check scripts/check-market-registration.mjs && node --check scripts/g5-e2e.mjs && node --check scripts/phase3-e2e.mjs && node --check scripts/phase2-acceptance.mjs",
46
48
  "docs:check": "node scripts/check-readme-version.mjs",
47
49
  "marketing:check": "node scripts/check-marketing.mjs",
48
50
  "verify-pack": "node scripts/verify-pack.mjs",