dsh-data-cleaning-agent 0.9.4 → 0.9.6

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,16 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.6] - 2026-09-11
8
+
9
+ - 任务结果报告降为统计卡片后的次要下载链接,不再与客户主结果并列展示文件卡片或预览按钮;窄侧栏自动换行。
10
+
11
+ ## [0.9.5] - 2026-09-11
12
+
13
+ - 主结果 XLSX 仅保留客户原列和选定补全字段;执行状态、来源及诊断信息改为独立任务结果报告,状态中文化。
14
+ - 来源记录实际调用的企查查 MCP server / 工具;历史缺失来源明确标注,不推断补造。
15
+ - 新任务只生成 XLSX,下载区按文件将预览与明确的下载按钮相邻展示,异常清单仅在有异常时提供。
16
+
7
17
  ## [0.9.4] - 2026-09-10
8
18
 
9
19
  - 文件预览采用 CSV 式单行表格,长文本不再撑高整行;独立双向滚动区域固定表头与序号列,下载保持完整原值。
package/README.en.md CHANGED
@@ -10,7 +10,7 @@ Related agents: [数据清洗补全](https://github.com/duhu2000/dsh-data-cleani
10
10
 
11
11
  > 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.
12
12
  >
13
- > Current source version / 当前源码版本: **0.9.4** (stable release)
13
+ > Current source version / 当前源码版本: **0.9.6** (stable release)
14
14
 
15
15
  This release unifies spreadsheet previews, column mapping, and enrichment scope across the prompt wizard and workbench. Mapping statuses use color and text; mapped fields automatically enter the scope, with optional extra fields collapsed. Multiple confirmed output columns may share a non-identity field while preserving non-empty values. It fixes saved drafts resetting the scope to five defaults and detects stale Host versions. Preparing a draft does not query QCC; sending starts execution. Fully restart DSH after upgrading. Existing download artifacts are not rewritten.
16
16
 
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  数据清洗补全智能体:面向 Excel/CSV/JSON 企业名单,提供数据清洗、表格清洗、清洗补全、去重、企业数据补全与字段补全,支持企查查 MCP 和结果导出。
10
10
 
11
11
  ```sh
12
- dsh plugin --profile web add dsh-data-cleaning-agent@0.9.4
12
+ dsh plugin --profile web add dsh-data-cleaning-agent@0.9.6
13
13
  ```
14
14
 
15
15
  请先满足下文的 DSH、连接器及侧边栏依赖要求;安装后完整停止并重启对应 Profile。
@@ -27,7 +27,7 @@ dsh plugin --profile web add dsh-data-cleaning-agent@0.9.4
27
27
 
28
28
  > 在 DeepSeek Harness 中清洗、补全、画像企业名单数据的智能体插件:本地 CSV/XLSX/JSON 引擎 + 可选企查查(Qichacha/QCC)MCP 企业数据补全,由企查查(Qichacha/QCC)团队发起并维护。
29
29
  >
30
- > 当前源码版本 / Current source version: **0.9.4**(正式版本)
30
+ > 当前源码版本 / Current source version: **0.9.6**(正式版本)
31
31
 
32
32
  本版上传表格后展示完整清单及空白数量,用绿色、琥珀色、红色及文字区分已通过、待确认和未匹配映射。已映射字段自动纳入补全范围,额外字段默认折叠;多个原列可经确认使用同一非主体字段,分别补空并保留非空原值。修复 Host 草稿重置为默认五字段的问题,增加前后端版本错配提示。生成说明不查询,发送才执行;升级后需完整重启 DSH,历史下载文件不自动改写。
33
33
 
@@ -67,7 +67,7 @@ dsh plugin --profile web add dsh-data-cleaning-agent
67
67
  DSH 原生会话:首页标题、产品说明与工作流会替换通用探索页;输入框左上角提供提示词生成器,支持
68
68
  粘贴名单、解析 Excel/CSV/JSON 或附加图片并选择清洗动作、补全字段。上传清洗、质量体检、匹配核验、
69
69
  字段补全和任务历史五个入口位于输入框下方,右侧工作台承载“上传数据 → 规则确认 → 数据匹配 →
70
- 清洗补全 → 下载数据”五步闭环。完成后可下载结果与异常清单的 CSV/XLSX 四类 Host 耐久制品。
70
+ 清洗补全 → 下载数据”五步闭环。完成后可预览、下载清洗补全结果 XLSX 和独立任务结果报告 XLSX;有异常时另提供异常清单 XLSX。执行状态和来源信息不会追加到客户主结果中。
71
71
 
72
72
  0.8.1 起,在数据清洗补全会话的原生输入框粘贴图片,或在「提示词生成」的图片页
73
73
  拖入/选择 PNG、JPEG、WebP,会显示可点击放大的原生缩略图。用户可继续完成匹配规则、清洗项与补全字段,
@@ -43,7 +43,7 @@ bash <(curl -fsSL https://raw.githubusercontent.com/duhu2000/dsh-data-cleaning-a
43
43
  2. **规则确认**:映射企业名称/统一社会信用代码/注册号,选择清洗目标与补全字段;
44
44
  3. **数据匹配**:运行质量体检与主体匹配,多候选由人工确认;
45
45
  4. **清洗补全**:执行本地确定性清洗;需要 QCC 时先零调用估算,再由当前用户确认使用自己的账号额度;
46
- 5. **下载数据**:生成结果 CSV/XLSX 与异常清单 CSV/XLSX,后续可从任务历史恢复下载。
46
+ 5. **下载数据**:生成清洗补全结果 XLSX 和独立任务结果报告 XLSX;有异常时另提供异常清单 XLSX。主结果仅保留客户原列与选定补全字段,报告记录中文执行状态、实际 MCP server / 工具及诊断信息。每个文件的预览与下载按钮相邻,后续可从任务历史恢复下载。
47
47
 
48
48
  0.8.11 起的原列回填规则:规则确认时同时检查「字段映射」和「输出字段」。已选字段优先填入已映射原列的空白单元格,非空原值保持不变,原表没有对应映射的已选字段才新增列。地址、网址、开业时间等推荐需确认含义;重复列需明确选定一列。未返回的数据不编造。结果预览、XLSX/CSV 及异常清单遵循同一规则;历史已生成文件不会自动改写。
49
49
 
package/lib/artifacts.js CHANGED
@@ -31,6 +31,18 @@ const SYSTEM_FIELD_LABELS = Object.freeze({
31
31
  _exception_reason: '异常原因',
32
32
  });
33
33
 
34
+ const isExecutionField = key => ['qcc_match_status', 'qcc_field_issues', 'qcc_field_notes', 'qcc_source', 'qcc_snapshot_note', 'qcc_error', 'match_status', '_exception_reason', 'error', '_incomplete'].includes(key);
35
+ const STATUS_LABELS = { enriched: '已匹配并补全', exact: '精确匹配', candidate: '候选主体待确认', ambiguous: '多候选待确认',
36
+ review_required: '待核验', unresolved: '未匹配', not_found: '未找到', failed: '失败', error: '失败', partial: '部分完成', 'missing-name': '缺少主体名称' };
37
+ function reportRow(row) {
38
+ const next = { ...row };
39
+ for (const key of ['qcc_match_status', 'match_status']) {
40
+ if (key in next) next[key] = STATUS_LABELS[next[key]] || (next[key] ? '待核验(未知状态)' : '未记录');
41
+ }
42
+ if (next.qcc_source === 'qcc-mcp') next.qcc_source = '企查查 MCP(历史记录未保存具体 server / 工具)';
43
+ return next;
44
+ }
45
+
34
46
  export class ArtifactError extends Error {
35
47
  constructor(code, message, status = 400) {
36
48
  super(message);
@@ -228,20 +240,22 @@ export class WorkflowArtifactStore {
228
240
  mappings: input.mappings || [], fieldSelection: normalizeFieldSelection(input.fieldSelection),
229
241
  });
230
242
  const rows = projected.rows;
231
- const headers = normalizeHeaders(projected.headers, rows);
243
+ const allowed = new Set([...(input.headers || []), ...normalizeFieldSelection(input.fieldSelection)]);
244
+ const headers = normalizeHeaders(projected.headers, rows).filter(key => !isExecutionField(key)
245
+ && (!allowed.size || allowed.has(key)));
232
246
  if (!headers.length) throw new ArtifactError('DC_ARTIFACT_HEADERS_REQUIRED', 'At least one export column is required.', 400);
233
247
  const exceptions = input.exceptionRows === undefined
234
248
  ? deriveExceptionRows(rows)
235
249
  : projectCompletionResult({ rows: normalizeRows(input.exceptionRows), headers: input.headers || [],
236
250
  mappings: input.mappings || [], fieldSelection: normalizeFieldSelection(input.fieldSelection) }).rows;
237
- const exceptionHeaders = normalizeHeaders([...headers, '_exception_reason'], exceptions);
251
+ const reportHeaders = normalizeHeaders([...headers, 'qcc_match_status', 'qcc_source'], rows).filter(key => headers.includes(key) || isExecutionField(key));
252
+ const exceptionHeaders = normalizeHeaders([...reportHeaders, '_exception_reason'], exceptions).filter(key => reportHeaders.includes(key) || isExecutionField(key));
238
253
  const baseName = safeFilePart(input.baseName, '数据清洗补全结果');
239
254
  const timestamp = this.nowFn();
240
255
  const definitions = [
241
- { kind: 'complete', format: 'csv', fileName: `${baseName}.csv`, rows, headers, sheet: '清洗补全结果' },
242
256
  { kind: 'complete', format: 'xlsx', fileName: `${baseName}.xlsx`, rows, headers, sheet: '清洗补全结果' },
243
- { kind: 'review', format: 'csv', fileName: `${baseName}-异常清单.csv`, rows: exceptions, headers: exceptionHeaders, sheet: '异常清单' },
244
- { kind: 'review', format: 'xlsx', fileName: `${baseName}-异常清单.xlsx`, rows: exceptions, headers: exceptionHeaders, sheet: '异常清单' },
257
+ { kind: 'report', format: 'xlsx', fileName: `${baseName}-任务结果报告.xlsx`, rows: rows.map(reportRow), headers: reportHeaders, sheet: '任务结果报告' },
258
+ ...(exceptions.length ? [{ kind: 'review', format: 'xlsx', fileName: `${baseName}-异常清单.xlsx`, rows: exceptions.map(reportRow), headers: exceptionHeaders, sheet: '异常清单' }] : []),
245
259
  ];
246
260
  const artifacts = [];
247
261
  for (const definition of definitions) {
package/lib/client.js CHANGED
@@ -762,6 +762,14 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
762
762
  .dcAgentArtifactList { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; min-width: 0; }
763
763
  .dcAgentArtifactList > .dcAgentButton { display: inline-flex; align-items: center; box-sizing: border-box; max-width: 100%; min-width: 0; height: auto; white-space: normal; overflow-wrap: anywhere; line-height: 1.5; text-decoration: none; }
764
764
  .dcAgentArtifactList > a.dcAgentButton { flex-basis: 100%; }
765
+ .dcAgentResultSummary { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px; min-width: 0; }
766
+ .dcAgentResultSummary > .dcAgentGrid { flex: 1 1 500px; min-width: 0; }
767
+ .dcAgentReportLink { color: var(--dc-muted); font-size: 13px; text-decoration: underline; padding: 8px 0; }
768
+ .dcAgentReportLink:hover { color: var(--dc-action); }
769
+ .dcAgentReportLink[aria-disabled="true"] { opacity: .5; cursor: wait; }
770
+ .dcAgentArtifactList > .dcAgentCard { width: 100%; min-width: 0; box-sizing: border-box; }
771
+ .dcAgentArtifactList .dcAgentCard p { overflow-wrap: anywhere; margin: 0 0 10px; }
772
+ .dcAgentArtifactList .dcAgentCard .dcAgentButton { max-width: 100%; height: auto; white-space: normal; overflow-wrap: anywhere; }
765
773
  .dcAgentPreviewTable { min-width: 0; max-width: 100%; scrollbar-gutter: stable; }
766
774
  .dcAgentPreviewTable > .dcAgentTable {
767
775
  width: max-content; min-width: 100%; max-width: none;
@@ -4087,37 +4095,43 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4087
4095
  h('div', { className: 'dcAgentRow' }, h('button', { type: 'button', className: 'dcAgentButton is-primary', onClick: () => actions.setStep('download') }, '进入下载数据')),
4088
4096
  );
4089
4097
  } else if (step === 'download') {
4090
- const availableArtifacts = (cachedTask?.artifacts || []).filter(artifact => artifact.kind !== 'review' || artifact.rowCount > 0);
4098
+ const availableArtifacts = (cachedTask?.artifacts || []).filter(artifact => artifact.format === 'xlsx' && (artifact.kind !== 'review' || artifact.rowCount > 0));
4099
+ const resultArtifacts = availableArtifacts.filter(artifact => artifact.kind !== 'report');
4100
+ const reportArtifact = availableArtifacts.find(artifact => artifact.kind === 'report');
4091
4101
  const canCreateArtifacts = Boolean(exportRows()?.length) && ['rules_confirmed', 'diagnosed', 'export_ready', 'partial'].includes(cachedTask?.state);
4092
4102
  pane = h('div', { className: 'dcAgentPane' },
4093
4103
  h('p', { className: 'dcAgentHint' }, '结果文件由 Host 保存;有失败、未匹配或待核验记录时展示异常清单。任务或插件重启后,可从任务历史继续下载;无需再次调用企查查。'),
4094
- h('div', { className: 'dcAgentGrid' },
4104
+ h('div', { className: 'dcAgentResultSummary' }, h('div', { className: 'dcAgentGrid' },
4095
4105
  stat('输入行数', dataset ? dataset.rowCount : workflowTask?.source?.rowCount ?? '—'),
4096
4106
  stat('匹配补全', qccEnrichedCount, qccEnrichedCount > 0 ? 'good' : null),
4097
4107
  stat('待核验', qccReviewCount, qccReviewCount > 0 ? 'warn' : null),
4098
4108
  stat('任务状态', WORKFLOW_STATE_LABELS[cachedTask?.state] || cachedTask?.state || '进行中'),
4099
4109
  stat('失败', qccFailedCount, qccFailedCount > 0 ? 'bad' : null),
4100
- ),
4101
- availableArtifacts.length ? h('div', { className: 'dcAgentArtifactList' },
4102
- availableArtifacts.filter(artifact => artifact.format === 'xlsx').map(artifact => h('a', {
4110
+ ), reportArtifact ? h('a', {
4111
+ className: 'dcAgentReportLink',
4112
+ href: `/data-cleaning/api/workflow/tasks/${encodeURIComponent(cachedTask.id)}/artifacts/${encodeURIComponent(reportArtifact.id)}`,
4113
+ download: reportArtifact.fileName, 'aria-disabled': busy ? 'true' : undefined,
4114
+ onClick: event => { event.preventDefault(); if (!busy) downloadArtifact(reportArtifact); },
4115
+ }, '下载任务结果报告') : null),
4116
+ resultArtifacts.length ? h('div', { className: 'dcAgentArtifactList' },
4117
+ resultArtifacts.map(artifact => h('div', { key: artifact.id, className: 'dcAgentCard' },
4118
+ h('p', { className: 'dcAgentHint' }, artifact.fileName),
4119
+ h('div', { className: 'dcAgentRow' }, h('a', {
4103
4120
  key: 'preview-' + artifact.id, className: 'dcAgentButton',
4104
4121
  href: `/data-cleaning/api/workflow/tasks/${encodeURIComponent(cachedTask.id)}/artifacts/${encodeURIComponent(artifact.id)}?preview=1`,
4105
4122
  target: '_blank', rel: 'noopener noreferrer',
4106
- }, `预览 / 打开:${artifact.fileName}`)),
4107
- availableArtifacts.map((artifact) => h('button', {
4123
+ }, '预览 / 打开'), h('button', {
4108
4124
  key: artifact.id,
4109
4125
  type: 'button',
4110
4126
  className: `dcAgentButton${artifact.kind === 'complete' && artifact.format === 'xlsx' ? ' is-primary' : ''}`,
4111
4127
  disabled: busy,
4112
4128
  onClick: () => downloadArtifact(artifact),
4113
4129
  'aria-label': `下载 ${artifact.fileName}`,
4114
- }, `${artifact.kind === 'review' ? '异常清单' : '清洗补全结果'} ${String(artifact.format).toUpperCase()} · ${artifact.rowCount} 行`)),
4130
+ }, `下载${artifact.kind === 'review' ? '异常清单' : '清洗补全结果'} XLSX · ${artifact.rowCount} 行`)))),
4115
4131
  ) : h('div', null,
4116
4132
  h('div', { className: 'dcAgentRow' },
4117
4133
  h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !canCreateArtifacts, onClick: () => createAndDownload('complete', 'xlsx') }, '生成并下载结果 XLSX'),
4118
- h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !canCreateArtifacts, onClick: () => createAndDownload('complete', 'csv') }, '生成并下载结果 CSV'),
4119
4134
  qccReviewCount > 0 ? h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !canCreateArtifacts, onClick: () => createAndDownload('review', 'xlsx') }, '生成并下载异常清单 XLSX') : null,
4120
- qccReviewCount > 0 ? h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !canCreateArtifacts, onClick: () => createAndDownload('review', 'csv') }, '生成并下载异常清单 CSV') : null,
4121
4135
  ),
4122
4136
  !canCreateArtifacts ? h('p', { className: 'dcAgentHint' }, '请先完成规则确认与清洗补全;历史任务若尚未生成制品,需要重新上传源文件。') : null,
4123
4137
  ),
package/lib/qcc.js CHANGED
@@ -921,7 +921,7 @@ export class QccHostBridge {
921
921
  );
922
922
  const match = classifyEntityMatch(lookup.data);
923
923
  if (match.status !== 'exact') return match;
924
- return this.enrichMatchedCompany(match, options);
924
+ return this.enrichMatchedCompany(match, { ...options, lookupTool: lookup.toolName });
925
925
  }
926
926
 
927
927
  async enrichMatchedCompany(match, options = {}) {
@@ -930,11 +930,13 @@ export class QccHostBridge {
930
930
  let mapped = {};
931
931
  const fieldIssues = {};
932
932
  const snapshotNotes = [];
933
+ const actualTools = options.lookupTool ? [options.lookupTool] : [];
933
934
  let legacyRiskTags = '';
934
935
  for (const sourceTool of sourceTools) {
935
936
  const config = SOURCE_TOOL_CONFIG[sourceTool];
936
937
  if (!config) continue;
937
938
  const response = await this.call(config.name, { searchKey: lockedKey, ...(config.args ?? {}) }, options);
939
+ actualTools.push(response.toolName);
938
940
  const fields = config.map(response.data, match);
939
941
  mapped = mergeMappedFields(mapped, fields);
940
942
  if (['get_beneficial_owners', 'get_financial_data'].includes(sourceTool)) {
@@ -965,6 +967,10 @@ export class QccHostBridge {
965
967
  const fields = projectSelectedFields(mapped, options.fieldSelection);
966
968
  if (options.includeRisk) fields.risk_tags = legacyRiskTags;
967
969
  return { status: 'enriched', companyName: String(match?.companyName ?? ''), fields,
970
+ source: [...new Set(actualTools)].map(name => {
971
+ const parts = /^mcp__(.+?)__(.+)$/.exec(name || '');
972
+ return parts ? `企查查 MCP · server: ${parts[1]} · 工具: ${parts[2]}` : `企查查 MCP · 工具: ${name || '未记录'}`;
973
+ }).join(';'),
968
974
  ...(snapshotNotes.length ? { snapshotNote: snapshotNotes.join(';') } : {}),
969
975
  ...(Object.keys(fieldIssues).length ? { fieldIssues } : {}) };
970
976
  }
@@ -1068,7 +1074,7 @@ export class QccHostBridge {
1068
1074
  ...row,
1069
1075
  ...Object.fromEntries(Object.entries(result.fields || {}).filter(([key]) => row[key] == null || (typeof row[key] === 'string' && row[key].trim() === ''))),
1070
1076
  qcc_match_status: 'enriched',
1071
- qcc_source: 'qcc-mcp',
1077
+ qcc_source: result.source || '企查查 MCP(未记录具体 server / 工具)',
1072
1078
  ...(result.snapshotNote ? { qcc_snapshot_note: result.snapshotNote } : {}),
1073
1079
  ...(row.qcc_field_issues || result.fieldIssues ? { qcc_field_issues: Object.entries(result.fieldIssues || {}).filter(([, issue]) => issue.reviewRequired !== false).map(([field, issue]) => field + ': ' + issue.message).join(';') } : {}),
1074
1080
  ...(result.fieldIssues ? { qcc_field_notes: Object.entries(result.fieldIssues).filter(([, issue]) => issue.reviewRequired === false).map(([field, issue]) => field + ': ' + issue.message).join(';') } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-data-cleaning-agent",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "Data cleaning and data enrichment for CSV/XLSX/JSON enterprise lists in DeepSeek Harness, including spreadsheet cleaning, deduplication, profiling, optional Qichacha MCP and exports.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",