dsh-data-cleaning-agent 0.8.12 → 0.8.13

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 CHANGED
@@ -4,6 +4,11 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.8.13] - 2026-09-07
8
+
9
+ ### Added
10
+ - 通过共享 `qcc-field-contracts` 接入实际控制人名称、直接持股比例、总持股比例、表决权比例;新增实控人别名搜索与原列补空。多名、分页和异常不自动选第一人,字段级原因随结果和异常清单导出。
11
+
7
12
  ## [0.8.12] - 2026-09-07
8
13
 
9
14
  ### Fixed
package/README.en.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > A data cleaning & completion agent plugin for DeepSeek Harness: local CSV/XLSX/JSON engine plus optional Qichacha (QCC) MCP enterprise-data enrichment. Initiated and maintained by the Qichacha (QCC) team.
4
4
  >
5
- > Current source version / 当前源码版本: **0.8.12** (stable release)
5
+ > Current source version / 当前源码版本: **0.8.13** (stable release)
6
6
 
7
7
  This release fixes misleading import errors after a successful upload. Files are parsed automatically, with persistent source, filename, and row-count status; text parsing handles only newly pasted content. The same file can be selected again, failed parsing preserves existing data, and pending pasted content must be parsed or cleared before continuing. Automatic mapping for 128 fields, blank-cell enrichment in original columns, QCC-blue menus, the resizable workbench, and native DSH sending remain unchanged. Existing download artifacts are not rewritten.
8
8
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 在 DeepSeek Harness 中清洗、补全、画像企业名单数据的智能体插件:本地 CSV/XLSX/JSON 引擎 + 可选企查查(Qichacha/QCC)MCP 企业数据补全,由企查查(Qichacha/QCC)团队发起并维护。
4
4
  >
5
- > 当前源码版本 / Current source version: **0.8.12**(正式版本)
5
+ > 当前源码版本 / Current source version: **0.8.13**(正式版本)
6
6
 
7
7
  本版修复文件上传成功后的导入状态误报:文件选择后自动解析,当前来源、文件名和行数持续显示;文本解析仅处理新粘贴内容。支持重新选择同一文件,解析失败保留现有数据,新粘贴内容解析或清空后再进入下一步。保留 128 字段智能映射、原列补空、企查查蓝菜单、可调宽工作台及 DSH 原生发送机制。历史下载文件不自动改写。
8
8
 
package/lib/artifacts.js CHANGED
@@ -21,6 +21,7 @@ const SYSTEM_FIELD_LABELS = Object.freeze({
21
21
  biz_status: '经营状态',
22
22
  risk_tags: '风险标签',
23
23
  qcc_match_status: '匹配状态',
24
+ qcc_field_issues: '字段补全待核验原因',
24
25
  qcc_source: '数据来源',
25
26
  qcc_error: '错误原因',
26
27
  match_status: '匹配状态',
@@ -115,6 +116,7 @@ function normalizeRows(value) {
115
116
  }
116
117
 
117
118
  function exceptionReason(row) {
119
+ if (row?.qcc_field_issues) return String(row.qcc_field_issues).slice(0, 2000);
118
120
  const status = String(
119
121
  row?.qcc_match_status
120
122
  ?? row?.match_status
package/lib/client.js CHANGED
@@ -199,7 +199,7 @@ window.__ModuleLoader__.load({
199
199
  const needle = String(query || '').trim().toLowerCase();
200
200
  const [, label, fields, tool] = group;
201
201
  if (!needle || `${label} ${tool || ''}`.toLowerCase().includes(needle)) return fields;
202
- return fields.filter(([id, fieldLabel]) => `${id} ${fieldLabel}`.toLowerCase().includes(needle));
202
+ return fields.filter(([id, fieldLabel]) => `${id} ${fieldLabel} ${(MAPPING_ALIASES[id] || []).join(' ')}`.toLowerCase().includes(needle));
203
203
  }
204
204
 
205
205
  /** 客户端所需服务:与 mcp-connector 对齐(槽位 + 会话/工作区/输入机)。 */
@@ -1139,6 +1139,13 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1139
1139
  ['equity_freeze', '股权冻结'],
1140
1140
  ];
1141
1141
  const FIELD_GROUPS = [
1142
+ // Shared contract mirror: checked against qcc-field-contracts in tests.
1143
+ ['actual_controller', '实际控制人', [
1144
+ ['actual_controller_name', '实际控制人名称'],
1145
+ ['actual_controller_direct_ratio', '实控人直接持股比例'],
1146
+ ['actual_controller_total_ratio', '实控人总持股比例'],
1147
+ ['actual_controller_voting_ratio', '实控人表决权比例'],
1148
+ ], 'get_actual_controller'],
1142
1149
  ['company_registration', '企业工商信息', [
1143
1150
  ['company_name', '企业名称'], ['credit_no', '统一社会信用代码'], ['reg_no', '注册号'],
1144
1151
  ['org_no', '组织机构代码'], ['tax_no', '纳税人识别号'], ['reg_status', '登记状态'],
@@ -1203,6 +1210,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1203
1210
  ['risk_tags', '风险标签'],
1204
1211
  ['qcc_match_status', '匹配状态'],
1205
1212
  ['qcc_source', '数据来源'],
1213
+ ['qcc_field_issues', '字段补全待核验原因'],
1206
1214
  ['qcc_error', '错误原因'],
1207
1215
  ['match_status', '匹配状态'],
1208
1216
  ['input_company', '原始企业名称'],
@@ -1434,6 +1442,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1434
1442
  }
1435
1443
 
1436
1444
  const MAPPING_ALIASES = {
1445
+ actual_controller_name: ['实控人', '实际控制人', '企业实控人名称', '企业实际控制人', '企业实控人名称(自然人请填写姓名)'],
1446
+ actual_controller_direct_ratio: ['实际控制人直接持股比例'],
1447
+ actual_controller_total_ratio: ['实际控制人总持股比例'],
1448
+ actual_controller_voting_ratio: ['实际控制人表决权比例'],
1437
1449
  company_name: ['name', 'company', '公司名称', '单位名称', '企业全称', '公司全称', '主体标识'],
1438
1450
  credit_no: ['creditCode', 'unified_credit_code', '信用代码', '社会信用代码', '统一信用代码', 'USCC'],
1439
1451
  reg_no: ['registration_no', '工商注册号'],
@@ -3584,7 +3596,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3584
3596
  qccRunId: run.runId,
3585
3597
  summary: {
3586
3598
  total: Number(summary.totalRows ?? runtime.rows.length),
3587
- exact: Number(summary.enriched ?? 0),
3599
+ exact: Number(summary.enriched ?? 0) + Number(summary.fieldReview ?? 0),
3588
3600
  candidate: reviewRequired,
3589
3601
  confirmed: 0,
3590
3602
  unresolved: Number(summary.unresolved ?? summary.missingName ?? 0),
@@ -3605,7 +3617,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3605
3617
  completed: Number(summary.enriched ?? 0),
3606
3618
  unchanged: Number(summary.unresolved ?? 0) + Number(summary.missingName ?? 0),
3607
3619
  failed: Number(summary.failed ?? 0),
3608
- reviewRequired: 0,
3620
+ reviewRequired: Number(summary.fieldReview ?? 0),
3609
3621
  callsUsed: Array.isArray(run.audit) ? run.audit.length : 0,
3610
3622
  },
3611
3623
  });
@@ -3904,7 +3916,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3904
3916
  ? qccRun.summary?.enriched ?? 0
3905
3917
  : cachedTask?.enrichmentSummary?.completed;
3906
3918
  const qccReviewCount = qccRun
3907
- ? qccRun.summary?.ambiguous ?? 0
3919
+ ? (qccRun.summary?.ambiguous ?? 0) + (qccRun.summary?.fieldReview ?? 0)
3908
3920
  : cachedTask?.enrichmentSummary?.reviewRequired
3909
3921
  ?? cachedTask?.matchSummary?.reviewRequired;
3910
3922
  const stat = (label, value, tone) => h('div', { className: 'dcAgentCard' },
@@ -4100,7 +4112,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4100
4112
  h('h3', null, `任务 ${qccRun.runId} · ${qccRun.state}`),
4101
4113
  h('div', { className: 'dcAgentGrid' },
4102
4114
  stat('已补全', qccRun.summary?.enriched ?? 0, 'good'),
4103
- stat('待核验', qccRun.summary?.ambiguous ?? 0, (qccRun.summary?.ambiguous ?? 0) > 0 ? 'warn' : null),
4115
+ stat('待核验', (qccRun.summary?.ambiguous ?? 0) + (qccRun.summary?.fieldReview ?? 0), (qccRun.summary?.ambiguous || qccRun.summary?.fieldReview) > 0 ? 'warn' : null),
4104
4116
  stat('未匹配', qccRun.summary?.unresolved ?? 0, (qccRun.summary?.unresolved ?? 0) > 0 ? 'warn' : null),
4105
4117
  stat('失败', qccRun.summary?.failed ?? 0, (qccRun.summary?.failed ?? 0) > 0 ? 'bad' : null),
4106
4118
  ),
@@ -268,6 +268,7 @@ export function registerQccCommandTool(tools, commands) {
268
268
  totalRows: { type: 'integer' },
269
269
  uniqueCompanies: { type: 'integer' },
270
270
  enriched: { type: 'integer' },
271
+ fieldReview: { type: 'integer' },
271
272
  ambiguous: { type: 'integer' },
272
273
  unresolved: { type: 'integer' },
273
274
  failed: { type: 'integer' },
@@ -5,6 +5,7 @@
5
5
  * 任何明细列表(电话全集、海关资质、风险关联方等)都不得进入本目录。
6
6
  */
7
7
 
8
+ import { ACTUAL_CONTROLLER_GROUP } from 'qcc-field-contracts';
8
9
  const field = (id, label, options = {}) => Object.freeze({ id, label, ...options });
9
10
  const group = (id, label, sourceTool, fields, options = {}) => Object.freeze({
10
11
  id,
@@ -173,6 +174,7 @@ export const QCC_FIELD_CATALOG = Object.freeze([
173
174
  ...RELATED_RISK_FACTORS.map(([id, label]) => field(`related_risk_${id}_count`, `关联风险-${label}条目数`)),
174
175
  ...RELATED_RISK_KEY_FACTORS.map(([id, label]) => field(`related_risk_${id}_party_count`, `关联风险-${label}命中关联方数`)),
175
176
  ], { releaseBatch: 'batch-2', catalogVersion: RISK_FACTOR_CATALOG_VERSION }),
177
+ ACTUAL_CONTROLLER_GROUP,
176
178
  ]);
177
179
 
178
180
  export const QCC_FIELD_SOURCE_TOOL = Object.freeze(Object.fromEntries(
package/lib/qcc-runs.js CHANGED
@@ -47,6 +47,7 @@ function deriveState(record) {
47
47
  if (record.reviewQueue.length > 0) return 'awaiting-review';
48
48
  if (record.errors.some((item) => item.error?.retryable)) return 'needs-retry';
49
49
  if (record.errors.length > 0) return 'completed-with-errors';
50
+ if (record.rows.some(row => row.qcc_field_issues)) return 'completed-with-errors';
50
51
  return 'completed';
51
52
  }
52
53
 
@@ -55,7 +56,8 @@ function recomputeSummary(record) {
55
56
  record.summary = {
56
57
  ...record.summary,
57
58
  totalRows: record.rows.length,
58
- enriched: count('enriched'),
59
+ enriched: record.rows.filter(row => row.qcc_match_status === 'enriched' && !row.qcc_field_issues).length,
60
+ ...(record.summary.fieldReview !== undefined || record.rows.some(row => row.qcc_field_issues) ? { fieldReview: record.rows.filter(row => row.qcc_field_issues).length } : {}),
59
61
  ambiguous: count('ambiguous'),
60
62
  unresolved: count('unresolved'),
61
63
  failed: count('failed'),
@@ -231,6 +233,7 @@ export class G5RunStore {
231
233
  ...row,
232
234
  ...Object.fromEntries(Object.entries(result.fields || {}).filter(([key]) => row[key] == null || (typeof row[key] === 'string' && row[key].trim() === ''))),
233
235
  qcc_match_status: 'enriched',
236
+ ...(row.qcc_field_issues || result.fieldIssues ? { qcc_field_issues: Object.entries(result.fieldIssues || {}).map(([field, issue]) => field + ': ' + issue.message).join(';') } : {}),
234
237
  qcc_source: 'qcc-mcp',
235
238
  };
236
239
  } else {
package/lib/qcc.js CHANGED
@@ -8,6 +8,7 @@
8
8
  * - 批量补全只在 Host/Web 内处理明细;模型仍只接收摘要。
9
9
  */
10
10
  import { safeAuditEvent } from './qcc-safety.js';
11
+ import { projectActualController } from 'qcc-field-contracts';
11
12
  import {
12
13
  QCC_PHASE2_COMPANY_TOOLS,
13
14
  QCC_PHASE2_HISTORY_TOOLS,
@@ -32,6 +33,7 @@ export const QCC_TOOL_NAMES = Object.freeze({
32
33
  entityLookup: 'mcp__qcc-company__get_company_by_query',
33
34
  registration: 'mcp__qcc-company__get_company_registration_info',
34
35
  profile: 'mcp__qcc-company__get_company_profile',
36
+ actualController: 'mcp__qcc-company__get_actual_controller',
35
37
  contact: 'mcp__qcc-company__get_contact_info',
36
38
  listing: 'mcp__qcc-company__get_listing_info',
37
39
  taxInvoice: 'mcp__qcc-company__get_tax_invoice_info',
@@ -548,7 +550,15 @@ export function mapCompanyRelatedRiskScanFields(value) {
548
550
  const LEGACY_ENRICHMENT_FIELDS = Object.freeze([
549
551
  'credit_no', 'legal_rep', 'reg_capital', 'establish_date', 'reg_status',
550
552
  ]);
553
+ export function mapActualControllerFields(value, match) {
554
+ if (value?.企业名称 && match?.companyName && value.企业名称 !== match.companyName) {
555
+ throw new QccBridgeError('QCC_ENTITY_MISMATCH', '实控人返回企业与已确认主体不一致');
556
+ }
557
+ const result=projectActualController(value);
558
+ return result.values;
559
+ }
551
560
  const SOURCE_TOOL_CONFIG = Object.freeze({
561
+ get_actual_controller: Object.freeze({ name: QCC_TOOL_NAMES.actualController, map: mapActualControllerFields }),
552
562
  get_company_registration_info: Object.freeze({ name: QCC_TOOL_NAMES.registration, map: mapRegistrationFields }),
553
563
  get_company_profile: Object.freeze({ name: QCC_TOOL_NAMES.profile, map: mapProfileFields }),
554
564
  get_contact_info: Object.freeze({ name: QCC_TOOL_NAMES.contact, map: mapContactFields, args: { excludeInvalidPhone: false } }),
@@ -917,6 +927,7 @@ export class QccHostBridge {
917
927
  const lockedKey = String(match?.creditNo || match?.companyName || '').trim();
918
928
  const sourceTools = sourceToolsForFieldSelection(options.fieldSelection, options.includeRisk);
919
929
  let mapped = {};
930
+ const fieldIssues = {};
920
931
  let legacyRiskTags = '';
921
932
  for (const sourceTool of sourceTools) {
922
933
  const config = SOURCE_TOOL_CONFIG[sourceTool];
@@ -924,6 +935,10 @@ export class QccHostBridge {
924
935
  const response = await this.call(config.name, { searchKey: lockedKey, ...(config.args ?? {}) }, options);
925
936
  const fields = config.map(response.data, match);
926
937
  mapped = mergeMappedFields(mapped, fields);
938
+ if (sourceTool === 'get_actual_controller') {
939
+ Object.assign(fieldIssues, Object.fromEntries(Object.entries(projectActualController(response.data).issues)
940
+ .filter(([field]) => options.fieldSelection?.includes(field))));
941
+ }
927
942
  if (sourceTool === 'get_company_risk_scan') legacyRiskTags = mapRiskTags(response.data);
928
943
  if (config.inspect) {
929
944
  const catalog = config.inspect(response.data);
@@ -942,7 +957,8 @@ export class QccHostBridge {
942
957
  }
943
958
  const fields = projectSelectedFields(mapped, options.fieldSelection);
944
959
  if (options.includeRisk) fields.risk_tags = legacyRiskTags;
945
- return { status: 'enriched', companyName: String(match?.companyName ?? ''), fields };
960
+ return { status: 'enriched', companyName: String(match?.companyName ?? ''), fields,
961
+ ...(Object.keys(fieldIssues).length ? { fieldIssues } : {}) };
946
962
  }
947
963
 
948
964
  async enrichLockedCompany(selection, options = {}) {
@@ -1030,6 +1046,7 @@ export class QccHostBridge {
1030
1046
  ...Object.fromEntries(Object.entries(result.fields || {}).filter(([key]) => row[key] == null || (typeof row[key] === 'string' && row[key].trim() === ''))),
1031
1047
  qcc_match_status: 'enriched',
1032
1048
  qcc_source: 'qcc-mcp',
1049
+ ...(row.qcc_field_issues || result.fieldIssues ? { qcc_field_issues: Object.entries(result.fieldIssues || {}).map(([field, issue]) => field + ': ' + issue.message).join(';') } : {}),
1033
1050
  };
1034
1051
  }
1035
1052
  return { ...row, qcc_match_status: result?.status ?? 'failed' };
@@ -1046,7 +1063,8 @@ export class QccHostBridge {
1046
1063
  const summary = {
1047
1064
  totalRows: rows.length,
1048
1065
  uniqueCompanies: names.length,
1049
- enriched: outputRows.filter((row) => row.qcc_match_status === 'enriched').length,
1066
+ enriched: outputRows.filter((row) => row.qcc_match_status === 'enriched' && !row.qcc_field_issues).length,
1067
+ ...(outputRows.some(row => row.qcc_field_issues) ? { fieldReview: outputRows.filter(row => row.qcc_field_issues).length } : {}),
1050
1068
  ambiguous: outputRows.filter((row) => row.qcc_match_status === 'ambiguous').length,
1051
1069
  unresolved: outputRows.filter((row) => row.qcc_match_status === 'unresolved').length,
1052
1070
  failed: outputRows.filter((row) => row.qcc_match_status === 'failed').length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-data-cleaning-agent",
3
- "version": "0.8.12",
3
+ "version": "0.8.13",
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",
@@ -99,6 +99,7 @@
99
99
  "node": ">=20"
100
100
  },
101
101
  "dependencies": {
102
+ "qcc-field-contracts": "0.1.0",
102
103
  "xlsx": "^0.18.5"
103
104
  },
104
105
  "peerDependencies": {