dsh-data-cleaning-agent 0.8.11 → 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 +11 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/lib/artifacts.js +2 -0
- package/lib/client.js +61 -11
- package/lib/qcc-command.js +1 -0
- package/lib/qcc-field-catalog.js +2 -0
- package/lib/qcc-runs.js +4 -1
- package/lib/qcc.js +20 -2
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.8.13] - 2026-09-07
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- 通过共享 `qcc-field-contracts` 接入实际控制人名称、直接持股比例、总持股比例、表决权比例;新增实控人别名搜索与原列补空。多名、分页和异常不自动选第一人,字段级原因随结果和异常清单导出。
|
|
11
|
+
|
|
12
|
+
## [0.8.12] - 2026-09-07
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- 导入页以已解析数据集显示当前来源、文件名与行数,不再依赖可能重建的原生文件选择框。文件选择后自动解析;文本按钮仅处理新粘贴内容,避免成功上传后误报「请粘贴或上传数据」。
|
|
16
|
+
- 支持重新选择同一文件,失败替换保留现有数据;新粘贴内容需先解析或清空,避免带着未生效输入进入下一步。仅有历史摘要而无明细时明确要求重新导入。
|
|
17
|
+
|
|
7
18
|
## [0.8.11] - 2026-09-07
|
|
8
19
|
|
|
9
20
|
### Fixed
|
package/README.en.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
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.
|
|
5
|
+
> Current source version / 当前源码版本: **0.8.13** (stable release)
|
|
6
6
|
|
|
7
|
-
This release
|
|
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
|
|
|
9
9
|
[](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/workflows/ci.yml)
|
|
10
10
|
[](https://www.npmjs.com/package/dsh-data-cleaning-agent)
|
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> 在 DeepSeek Harness 中清洗、补全、画像企业名单数据的智能体插件:本地 CSV/XLSX/JSON 引擎 + 可选企查查(Qichacha/QCC)MCP 企业数据补全,由企查查(Qichacha/QCC)团队发起并维护。
|
|
4
4
|
>
|
|
5
|
-
> 当前源码版本 / Current source version: **0.8.
|
|
5
|
+
> 当前源码版本 / Current source version: **0.8.13**(正式版本)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
本版修复文件上传成功后的导入状态误报:文件选择后自动解析,当前来源、文件名和行数持续显示;文本解析仅处理新粘贴内容。支持重新选择同一文件,解析失败保留现有数据,新粘贴内容解析或清空后再进入下一步。保留 128 字段智能映射、原列补空、企查查蓝菜单、可调宽工作台及 DSH 原生发送机制。历史下载文件不自动改写。
|
|
8
8
|
|
|
9
9
|
[](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/workflows/ci.yml)
|
|
10
10
|
[](https://www.npmjs.com/package/dsh-data-cleaning-agent)
|
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', '工商注册号'],
|
|
@@ -3054,6 +3066,17 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3054
3066
|
}
|
|
3055
3067
|
|
|
3056
3068
|
/** 文本/JSON 直通 parse(与 web.js 内联页同构,含 JSON 数组直通)。 */
|
|
3069
|
+
function intakeState(dataset, sourceRows, input, source) {
|
|
3070
|
+
const count = Number(dataset?.rowCount || 0);
|
|
3071
|
+
const available = count > 0 && Array.isArray(sourceRows) && sourceRows.length === count;
|
|
3072
|
+
return {
|
|
3073
|
+
available,
|
|
3074
|
+
pendingText: Boolean(String(input ?? '').trim()),
|
|
3075
|
+
fileName: source?.fileName || (source?.type === 'image' ? '图片识别名单' : '粘贴 / 向导导入名单'),
|
|
3076
|
+
count,
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3057
3080
|
async function parseText(text, actions) {
|
|
3058
3081
|
const trimmed = (text ?? '').trim();
|
|
3059
3082
|
if (!trimmed) {
|
|
@@ -3361,6 +3384,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3361
3384
|
const runtime = runtimeFor(runtimeKey);
|
|
3362
3385
|
const stagedSource = runtimeFor(`session:${activeSessionId || 'unassigned'}`, false);
|
|
3363
3386
|
const sourceRows = stagedSource?.sourceRows ?? runtime.sourceRows ?? runtime.rows;
|
|
3387
|
+
const intake = intakeState(dataset, sourceRows, input, runtime.source || stagedSource?.source || cachedTask?.source);
|
|
3364
3388
|
const lastCsv = runtime.lastCsv;
|
|
3365
3389
|
const fieldByPattern = (pattern) => runtime.headers.find((field) => pattern.test(field)) ?? null;
|
|
3366
3390
|
const phoneField = fieldByPattern(/^(phone|mobile|tel|telephone|联系电话|手机号码|手机号)$/i);
|
|
@@ -3380,13 +3404,20 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3380
3404
|
|
|
3381
3405
|
const handleParse = async () => {
|
|
3382
3406
|
if (busy) return;
|
|
3407
|
+
if (!intake.pendingText) {
|
|
3408
|
+
if (intake.available) actions.setError(null);
|
|
3409
|
+
return;
|
|
3410
|
+
}
|
|
3383
3411
|
actions.setBusy(true);
|
|
3384
3412
|
actions.setError(null);
|
|
3385
3413
|
try {
|
|
3386
3414
|
const result = await parseText(input, actions);
|
|
3387
|
-
if (result) {
|
|
3415
|
+
if (result && result.ok !== false) {
|
|
3388
3416
|
applyParsed(result, actions, runtimeKey, { type: result.fmt || 'csv' });
|
|
3389
3417
|
await persistParsedWorkflow(actions, activeSessionId, result, { type: result.fmt || 'csv' }, cachedTask);
|
|
3418
|
+
actions.setInput('');
|
|
3419
|
+
} else if (result) {
|
|
3420
|
+
actions.setError(result.message || result.error || '解析失败,已导入的数据保持不变。');
|
|
3390
3421
|
}
|
|
3391
3422
|
} catch (err) {
|
|
3392
3423
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
@@ -3404,6 +3435,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3404
3435
|
return true;
|
|
3405
3436
|
};
|
|
3406
3437
|
const handleFile = async (event) => {
|
|
3438
|
+
if (busy) return;
|
|
3407
3439
|
const file = event.target && event.target.files && event.target.files[0];
|
|
3408
3440
|
if (!file) return;
|
|
3409
3441
|
if (String(file.type ?? '').startsWith('image/')) {
|
|
@@ -3418,11 +3450,13 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3418
3450
|
if (result && result.ok !== false) {
|
|
3419
3451
|
applyParsed(result, actions, runtimeKey, { type: result.fmt || 'csv', fileName: file.name, sizeBytes: file.size || 0 });
|
|
3420
3452
|
await persistParsedWorkflow(actions, activeSessionId, result, { type: result.fmt || 'csv', fileName: file.name, sizeBytes: file.size || 0 }, cachedTask);
|
|
3453
|
+
actions.setInput('');
|
|
3421
3454
|
}
|
|
3422
3455
|
else actions.setError((result && (result.message || result.error)) || '解析失败');
|
|
3423
3456
|
} catch (err) {
|
|
3424
3457
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
3425
3458
|
} finally {
|
|
3459
|
+
event.target.value = '';
|
|
3426
3460
|
actions.setBusy(false);
|
|
3427
3461
|
}
|
|
3428
3462
|
};
|
|
@@ -3562,7 +3596,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3562
3596
|
qccRunId: run.runId,
|
|
3563
3597
|
summary: {
|
|
3564
3598
|
total: Number(summary.totalRows ?? runtime.rows.length),
|
|
3565
|
-
exact: Number(summary.enriched ?? 0),
|
|
3599
|
+
exact: Number(summary.enriched ?? 0) + Number(summary.fieldReview ?? 0),
|
|
3566
3600
|
candidate: reviewRequired,
|
|
3567
3601
|
confirmed: 0,
|
|
3568
3602
|
unresolved: Number(summary.unresolved ?? summary.missingName ?? 0),
|
|
@@ -3583,7 +3617,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3583
3617
|
completed: Number(summary.enriched ?? 0),
|
|
3584
3618
|
unchanged: Number(summary.unresolved ?? 0) + Number(summary.missingName ?? 0),
|
|
3585
3619
|
failed: Number(summary.failed ?? 0),
|
|
3586
|
-
reviewRequired: 0,
|
|
3620
|
+
reviewRequired: Number(summary.fieldReview ?? 0),
|
|
3587
3621
|
callsUsed: Array.isArray(run.audit) ? run.audit.length : 0,
|
|
3588
3622
|
},
|
|
3589
3623
|
});
|
|
@@ -3882,7 +3916,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3882
3916
|
? qccRun.summary?.enriched ?? 0
|
|
3883
3917
|
: cachedTask?.enrichmentSummary?.completed;
|
|
3884
3918
|
const qccReviewCount = qccRun
|
|
3885
|
-
? qccRun.summary?.ambiguous ?? 0
|
|
3919
|
+
? (qccRun.summary?.ambiguous ?? 0) + (qccRun.summary?.fieldReview ?? 0)
|
|
3886
3920
|
: cachedTask?.enrichmentSummary?.reviewRequired
|
|
3887
3921
|
?? cachedTask?.matchSummary?.reviewRequired;
|
|
3888
3922
|
const stat = (label, value, tone) => h('div', { className: 'dcAgentCard' },
|
|
@@ -4078,7 +4112,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4078
4112
|
h('h3', null, `任务 ${qccRun.runId} · ${qccRun.state}`),
|
|
4079
4113
|
h('div', { className: 'dcAgentGrid' },
|
|
4080
4114
|
stat('已补全', qccRun.summary?.enriched ?? 0, 'good'),
|
|
4081
|
-
stat('待核验', qccRun.summary?.ambiguous ?? 0
|
|
4115
|
+
stat('待核验', (qccRun.summary?.ambiguous ?? 0) + (qccRun.summary?.fieldReview ?? 0), (qccRun.summary?.ambiguous || qccRun.summary?.fieldReview) > 0 ? 'warn' : null),
|
|
4082
4116
|
stat('未匹配', qccRun.summary?.unresolved ?? 0, (qccRun.summary?.unresolved ?? 0) > 0 ? 'warn' : null),
|
|
4083
4117
|
stat('失败', qccRun.summary?.failed ?? 0, (qccRun.summary?.failed ?? 0) > 0 ? 'bad' : null),
|
|
4084
4118
|
),
|
|
@@ -4159,13 +4193,26 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4159
4193
|
} else {
|
|
4160
4194
|
pane = h('div', { className: 'dcAgentPane' },
|
|
4161
4195
|
h('p', { className: 'dcAgentHint' }, '上传 CSV / XLSX / XLS / JSON,或粘贴数据。也可选择图片,通过企查查智能文档解析提取名单后继续清洗补全。'),
|
|
4162
|
-
h('
|
|
4196
|
+
dataset ? h('section', { className: 'dcAgentSection', 'aria-label': '当前导入数据', role: 'status' },
|
|
4197
|
+
h('h3', null, intake.available ? `已解析 ${intake.count} 条数据` : '已保存导入摘要'),
|
|
4198
|
+
h('p', { className: 'dcAgentHint', style: { overflowWrap: 'anywhere' } }, `当前来源:${intake.fileName}`),
|
|
4199
|
+
h('p', { className: 'dcAgentHint' }, intake.available
|
|
4200
|
+
? '文件选择后已自动解析,无需再次点击解析。核对下方名单后可直接进入规则与体检;重新选择文件会替换当前导入数据。'
|
|
4201
|
+
: '当前页面没有完整原始数据。请重新导入原文件;历史下载制品仍可从任务历史获取。'),
|
|
4202
|
+
) : null,
|
|
4203
|
+
h('div', { className: 'dcAgentRow' },
|
|
4204
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy,
|
|
4205
|
+
onClick: (event) => event.currentTarget.nextElementSibling?.click() },
|
|
4206
|
+
dataset ? '重新选择文件(自动解析)' : '选择文件(自动解析)'),
|
|
4207
|
+
h('input', {
|
|
4163
4208
|
type: 'file',
|
|
4164
4209
|
accept: '.csv,.json,.xlsx,.xls,image/png,image/jpeg,image/webp',
|
|
4165
4210
|
className: 'dcAgentField',
|
|
4211
|
+
style: { display: 'none' },
|
|
4212
|
+
disabled: busy,
|
|
4166
4213
|
'aria-label': '选择数据文件',
|
|
4167
4214
|
onChange: handleFile,
|
|
4168
|
-
}),
|
|
4215
|
+
})),
|
|
4169
4216
|
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy,
|
|
4170
4217
|
onClick: () => openImageIntake() }, '图片识别(粘贴 / 选择)'),
|
|
4171
4218
|
h('textarea', {
|
|
@@ -4173,6 +4220,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4173
4220
|
placeholder: '粘贴 CSV 文本,或 JSON 数组(例如 [{"name":"某公司","amount":"100"}])…',
|
|
4174
4221
|
'aria-label': '粘贴数据',
|
|
4175
4222
|
value: input,
|
|
4223
|
+
disabled: busy,
|
|
4176
4224
|
onPaste: (event) => {
|
|
4177
4225
|
const files = imageFilesFromTransfer(event.clipboardData);
|
|
4178
4226
|
if (files.length && openImageIntake(files[0])) {
|
|
@@ -4180,7 +4228,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4180
4228
|
event.stopPropagation?.();
|
|
4181
4229
|
}
|
|
4182
4230
|
},
|
|
4183
|
-
onInput: (event) => actions.setInput(event.target.value),
|
|
4231
|
+
onInput: (event) => { actions.setInput(event.target.value); actions.setError(null); },
|
|
4184
4232
|
}),
|
|
4185
4233
|
dataset ? h('div', { className: 'dcAgentChips' },
|
|
4186
4234
|
h('span', { className: 'dcAgentChip' }, `格式 ${dataset.fmt}`),
|
|
@@ -4196,8 +4244,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4196
4244
|
}) : null,
|
|
4197
4245
|
hasData ? h('p', { className: 'dcAgentHint' }, '请核对完整名单。识别清单不代表已完成企查查匹配。核对后进入「规则与体检」,确认规则生成质量体检;再进入「主体匹配」估算调用量并确认账号额度,点击「生成可编辑任务说明」自动回填中央对话框,无需手工复制任务凭证。') : null,
|
|
4198
4246
|
h('div', { className: 'dcAgentRow' },
|
|
4199
|
-
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy, 'aria-label': '解析数据', onClick: handleParse }, busy ? '解析中…' : '
|
|
4200
|
-
hasData ? h('
|
|
4247
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !intake.pendingText, 'aria-label': '解析数据', onClick: handleParse }, busy ? '解析中…' : '解析粘贴内容'),
|
|
4248
|
+
intake.pendingText && hasData ? h('p', { className: 'dcAgentHint', role: 'status' }, '有尚未解析的新粘贴内容,请先解析或清空后再继续。') : null,
|
|
4249
|
+
hasData ? h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !intake.available || intake.pendingText, onClick: () => { actions.setError(null); actions.setStep('rules'); } }, '已核对清单,下一步:字段映射与规则') : null,
|
|
4201
4250
|
),
|
|
4202
4251
|
);
|
|
4203
4252
|
}
|
|
@@ -4414,6 +4463,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4414
4463
|
isImagePathEntry,
|
|
4415
4464
|
requestPromptImage,
|
|
4416
4465
|
guessMappings,
|
|
4466
|
+
intakeState,
|
|
4417
4467
|
projectCompletionResult,
|
|
4418
4468
|
mappingRecommendations,
|
|
4419
4469
|
mergeRecommendedMappings,
|
package/lib/qcc-command.js
CHANGED
|
@@ -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' },
|
package/lib/qcc-field-catalog.js
CHANGED
|
@@ -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:
|
|
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.
|
|
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": {
|