dsh-data-cleaning-agent 0.6.1 → 0.6.2

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,15 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.2] - 2026-09-04
8
+
9
+ ### Fixed
10
+ - 清洗补全结果 CSV/XLSX 的插件字段与状态字段统一导出为中文表头,并按任务字段选择保留空值列,
11
+ 不再因企查查未返回某一字段而从制品中静默丢列。
12
+ - 将任务选择的补全字段贯穿中央工作台、Agent-owned 命令、G5 Host Bridge、任务恢复与耐久制品;
13
+ 工商详情补充注册地址、行政区划、经营范围、行业、营业期限和企业规模映射,企业简介等画像字段按需
14
+ 调用 `get_company_profile`,且不把未声明层级的“企查查行业”猜测为一级/二级行业。
15
+
7
16
  ## [0.6.1] - 2026-09-04
8
17
 
9
18
  ### 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.6.1** (stable release)
5
+ > Current source version / 当前源码版本: **0.6.2** (stable release)
6
6
 
7
7
  [![CI](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/workflows/ci.yml)
8
8
  [![npm](https://img.shields.io/npm/v/dsh-data-cleaning-agent)](https://www.npmjs.com/package/dsh-data-cleaning-agent)
@@ -117,6 +117,8 @@ See [the Phase-3 acceptance record](docs/PHASE3-ACCEPTANCE.md) and
117
117
  See [the 0.5.1 release record](docs/RELEASE-0.5.1.md) for the README fix and release-text gate.
118
118
  See [the 0.5.2 release record](docs/RELEASE-0.5.2.md) for native DSH UI alignment, verification, and rollback.
119
119
  See [the 0.5.3 release record](docs/RELEASE-0.5.3.md) for the business landing view and prompt builder.
120
+ See [the 0.6.2 release record](docs/RELEASE-0.6.2.md) for Chinese export headers,
121
+ complete field-selection propagation, and profile-field enrichment.
120
122
  See [the 0.6.1 release record](docs/RELEASE-0.6.1.md) for the Agent-owned QCC workbench,
121
123
  session isolation, recovered statistics, and real end-to-end validation.
122
124
  See [the 0.6.0 release record](docs/RELEASE-0.6.0.md) for the taskId workflow, durable artifacts,
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.6.1**(正式版本)
5
+ > 当前源码版本 / Current source version: **0.6.2**(正式版本)
6
6
 
7
7
  [![CI](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/workflows/ci.yml)
8
8
  [![npm](https://img.shields.io/npm/v/dsh-data-cleaning-agent)](https://www.npmjs.com/package/dsh-data-cleaning-agent)
@@ -113,7 +113,8 @@ bash <(curl -fsSL https://raw.githubusercontent.com/duhu2000/dsh-data-cleaning-a
113
113
  0.5.1 文档补丁与防回归发布门见 [docs/RELEASE-0.5.1.md](docs/RELEASE-0.5.1.md)。
114
114
  0.5.2 DSH 原生 UI 对齐范围、验收与回滚见 [docs/RELEASE-0.5.2.md](docs/RELEASE-0.5.2.md)。
115
115
  0.5.3 业务首页、提示词生成和输入框下方流程栏见 [docs/RELEASE-0.5.3.md](docs/RELEASE-0.5.3.md)。
116
- 0.6.1 Agent-owned QCC 工作台、会话隔离、恢复统计及真实闭环验收见
116
+ 0.6.2 中文导出表头、完整字段选择传递与画像字段补全见
117
+ [docs/RELEASE-0.6.2.md](docs/RELEASE-0.6.2.md)。0.6.1 Agent-owned QCC 工作台、会话隔离、恢复统计及真实闭环验收见
117
118
  [docs/RELEASE-0.6.1.md](docs/RELEASE-0.6.1.md)。0.6.0 五步 taskId 工作流、耐久制品、
118
119
  跨重启恢复与双基线验收见 [docs/RELEASE-0.6.0.md](docs/RELEASE-0.6.0.md)。
119
120
 
@@ -45,6 +45,20 @@
45
45
 
46
46
  ## 外部发布核验
47
47
 
48
- Tag 推送后必须确认 Release workflow 全绿、npm `latest=0.6.1`、provenance 存在、GitHub Release
49
- draft/非 prerelease,并从公共 Registry 在全新隔离目录安装验证。实际 workflow run、tarball
50
- 哈希与公共安装结果将在发布完成后的仓库进度记录中补充。
48
+ - PR [#3](https://github.com/duhu2000/dsh-data-cleaning-agent/pull/3) 四项 CI 全绿后合并;
49
+ `v0.6.1` 指向合并提交 `77a19aa`。
50
+ - Release workflow [33880569564](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/runs/33880569564)
51
+ 全绿;版本校验、发布模式门禁、npm OIDC publish 与 GitHub Release 均成功。
52
+ - npm `latest` 为 `0.6.1`;发布时间为 2026-09-04T13:53:39Z(北京时间 2026-09-04 21:53)。
53
+ - tarball SHA-1:`ab9aaff6738e3ede08768138bc7a9000ccc28ed2`。
54
+ - tarball integrity:
55
+ `sha512-VWsVelLCxpGYDZ1TZrxlZWLGC86IjtOJDZAszukCs5LaPwkos8UcJn0IWmYtdKF7S3a8TsQGN1KhOhDL0OCHFw==`。
56
+ - npm Registry 返回 SLSA v1 provenance;隔离安装后的 `npm audit signatures` 验证 10 个 Registry
57
+ 签名和 1 个 attestation。
58
+ - 从公共 Registry 全新安装成功;包版本为 0.6.1,主入口可导入,浏览器 client 语法门通过,
59
+ `lib/qcc-command.js` 与本发布记录均存在。
60
+ - DSH `0.1.1-rc.2` 全新隔离 Profile 安装 `dsh-data-cleaning-agent@0.6.1` 成功并加载 Host 插件。
61
+ 第二个临时 Web Host 因本机全局文件监听资源耗尽(`EMFILE`)未完成 HTTP seam;未停止或改动用户
62
+ 正在运行的 3080 实例。真实工作台闭环证据见上方「验收证据」。
63
+ - GitHub Release [v0.6.1](https://github.com/duhu2000/dsh-data-cleaning-agent/releases/tag/v0.6.1)
64
+ 已发布,为 Latest,非 draft、非 prerelease。
@@ -0,0 +1,40 @@
1
+ # 0.6.2 中文字段导出与完整补全字段修复
2
+
3
+ > 版本:**0.6.2**
4
+ > 发布日期:2026-09-04
5
+ > 发布来源:`v0.6.2` Tag 触发 GitHub Actions,使用 npm OIDC Trusted Publishing。
6
+
7
+ ## 目标
8
+
9
+ 0.6.2 是 0.6.1 的结果正确性补丁,不扩大企查查工具域,修复真实任务中结果 XLSX 仅出现少量
10
+ 英文字段的问题:
11
+
12
+ 1. 任务选中的补全维度贯穿工作台、Agent-owned 命令、G5 Host Bridge、恢复/重试与耐久制品;
13
+ 2. CSV/XLSX 的插件补全字段和系统状态字段统一使用中文表头;
14
+ 3. 即使企查查未返回某个已选字段,结果文件也保留对应中文空列,不再静默丢列;
15
+ 4. 工商工具补充注册地址、行政区划、经营范围、行业、营业期限和企业规模映射;
16
+ 5. 企业简介及明确的一/二级行业字段按需调用 `get_company_profile`;未声明层级的“企查查行业”
17
+ 不会被猜测填入一级或二级行业。
18
+
19
+ ## 兼容与调用边界
20
+
21
+ - 稳定发布基线仍为 DSH `0.1.1-rc.2`;`0.1.2-alpha.2` 继续通过隔离 Bridge 兼容。
22
+ - 只有选择企业简介、一级行业或二级行业时才增加企业画像调用,工作台调用估算同步调整。
23
+ - QCC 继续采用 BYO 账号;插件不内置、不记录也不发布 OAuth token 或客户 Key。
24
+ - 历史域、人员域和招投标域仍按既定范围延期,本补丁不新增相关工具。
25
+
26
+ ## 验收
27
+
28
+ - 14 个目标维度按选择顺序完整进入结果数据和导出制品;
29
+ - CSV/XLSX 表头为统一社会信用代码、登记状态、法定代表人、注册资本、成立日期、注册地址、
30
+ 省份地区、城市、经营范围、一级行业、二级行业、营业期限、企业规模、企业简介;
31
+ - 画像接口未明确返回层级时,一/二级行业保留空值而不是生成数据;
32
+ - 未选择风险能力时不额外输出风险标签;
33
+ - `npm run check` 全绿,176/176 自动化测试通过,npm dry-run 包内容命中白名单。
34
+
35
+ ## 升级与回滚
36
+
37
+ - 升级:安装 `dsh-data-cleaning-agent@0.6.2` 并完全重启 DSH。
38
+ - 回滚:重新安装 `dsh-data-cleaning-agent@0.6.1` 并完全重启 DSH。
39
+ - 本补丁不修改 `dc_workflows_v2` 存储 schema,也不删除既有任务或工作区耐久制品。
40
+ - 0.6.1 已生成的旧 XLSX 不会被自动重写;升级后重新生成制品即可获得中文完整表头。
package/lib/artifacts.js CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { createHash, randomUUID } from 'node:crypto';
9
9
  import XLSX from 'xlsx';
10
+ import { FIELD_LABELS, normalizeFieldSelection } from './workflow-contract.js';
10
11
 
11
12
  const ROOT = '.dsh-data-cleaning-artifacts/v1';
12
13
  const MAX_ARTIFACT_BYTES = 32 * 1024 * 1024;
@@ -15,6 +16,17 @@ const MAX_ARTIFACT_BYTES = 32 * 1024 * 1024;
15
16
  const MAX_STORED_BYTES = Math.ceil(MAX_ARTIFACT_BYTES / 3) * 4 + 4;
16
17
  const SAFE_ID = /^(?:dcw|dca)-[a-zA-Z0-9-]{8,80}$/;
17
18
 
19
+ const SYSTEM_FIELD_LABELS = Object.freeze({
20
+ biz_status: '经营状态',
21
+ risk_tags: '风险标签',
22
+ qcc_match_status: '匹配状态',
23
+ qcc_source: '数据来源',
24
+ qcc_error: '错误原因',
25
+ match_status: '匹配状态',
26
+ error: '错误原因',
27
+ _exception_reason: '异常原因',
28
+ });
29
+
18
30
  export class ArtifactError extends Error {
19
31
  constructor(code, message, status = 400) {
20
32
  super(message);
@@ -44,10 +56,11 @@ function sha256(buffer) {
44
56
  return createHash('sha256').update(buffer).digest('hex');
45
57
  }
46
58
 
47
- function normalizeHeaders(headers, rows) {
59
+ function normalizeHeaders(headers, rows, fieldSelection = []) {
48
60
  const fromInput = Array.isArray(headers) ? headers.map((item) => String(item ?? '').trim()).filter(Boolean) : [];
61
+ const selected = normalizeFieldSelection(fieldSelection);
49
62
  const discovered = [];
50
- const seen = new Set(fromInput);
63
+ const seen = new Set([...fromInput, ...selected]);
51
64
  for (const row of rows) {
52
65
  for (const key of Object.keys(row ?? {})) {
53
66
  if (!seen.has(key)) {
@@ -56,7 +69,27 @@ function normalizeHeaders(headers, rows) {
56
69
  }
57
70
  }
58
71
  }
59
- return [...fromInput, ...discovered].slice(0, 256);
72
+ return [...fromInput, ...selected.filter((field) => !fromInput.includes(field)), ...discovered].slice(0, 256);
73
+ }
74
+
75
+ function exportLabel(key) {
76
+ return FIELD_LABELS[key] ?? SYSTEM_FIELD_LABELS[key] ?? key;
77
+ }
78
+
79
+ function exportColumns(headers) {
80
+ const used = new Set();
81
+ return headers.map((key) => {
82
+ const base = exportLabel(key);
83
+ let label = base;
84
+ if (used.has(label)) {
85
+ const suffix = FIELD_LABELS[key] || SYSTEM_FIELD_LABELS[key] ? '(补全)' : '(原字段)';
86
+ label = `${base}${suffix}`;
87
+ let index = 2;
88
+ while (used.has(label)) label = `${base}${suffix.slice(0, -1)}${index++})`;
89
+ }
90
+ used.add(label);
91
+ return { key, label };
92
+ });
60
93
  }
61
94
 
62
95
  function normalizeRows(value) {
@@ -104,8 +137,13 @@ export function deriveExceptionRows(rows) {
104
137
 
105
138
  function workbookBytes(rows, headers, sheetName) {
106
139
  const workbook = XLSX.utils.book_new();
107
- const worksheet = XLSX.utils.json_to_sheet(rows, { header: headers, skipHeader: false });
108
- worksheet['!cols'] = headers.map((header) => ({ wch: Math.min(42, Math.max(12, String(header).length * 2 + 4)) }));
140
+ const columns = exportColumns(headers);
141
+ const values = [
142
+ columns.map((column) => column.label),
143
+ ...rows.map((row) => columns.map((column) => row?.[column.key] ?? '')),
144
+ ];
145
+ const worksheet = XLSX.utils.aoa_to_sheet(values);
146
+ worksheet['!cols'] = columns.map((column) => ({ wch: Math.min(42, Math.max(12, String(column.label).length * 2 + 4)) }));
109
147
  XLSX.utils.book_append_sheet(workbook, worksheet, sheetName.slice(0, 31));
110
148
  workbook.Props = {
111
149
  Title: sheetName,
@@ -129,9 +167,10 @@ function csvBytes(rows, headers) {
129
167
  if (/^[\u0009\u000d\u000a ]*[=+\-@]/.test(text)) text = `'${text}`;
130
168
  return /[",\r\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
131
169
  };
170
+ const columns = exportColumns(headers);
132
171
  const lines = [
133
- headers.map(escapeCell).join(','),
134
- ...rows.map((row) => headers.map((header) => escapeCell(row?.[header])).join(',')),
172
+ columns.map((column) => escapeCell(column.label)).join(','),
173
+ ...rows.map((row) => columns.map((column) => escapeCell(row?.[column.key])).join(',')),
135
174
  ];
136
175
  return Buffer.from(`\uFEFF${lines.join('\r\n')}`, 'utf8');
137
176
  }
@@ -180,7 +219,7 @@ export class WorkflowArtifactStore {
180
219
  async createBundle(taskId, input = {}) {
181
220
  assertId(taskId, 'taskId');
182
221
  const rows = normalizeRows(input.rows);
183
- const headers = normalizeHeaders(input.headers, rows);
222
+ const headers = normalizeHeaders(input.headers, rows, input.fieldSelection);
184
223
  if (!headers.length) throw new ArtifactError('DC_ARTIFACT_HEADERS_REQUIRED', 'At least one export column is required.', 400);
185
224
  const exceptions = input.exceptionRows === undefined
186
225
  ? deriveExceptionRows(rows)
package/lib/client.js CHANGED
@@ -2412,11 +2412,13 @@ window.__ModuleLoader__.load({
2412
2412
 
2413
2413
  const estimateQcc = async () => {
2414
2414
  const uniqueCompanies = new Set(runtime.rows.map((row) => String(row?.[nameField] ?? '').trim()).filter(Boolean)).size;
2415
+ const requiresProfile = fieldSelection.some((field) => ['company_profile', 'industry_large', 'industry_middle'].includes(field));
2416
+ const callsPerCompany = requiresProfile ? 3 : 2;
2415
2417
  actions.setQccEstimate({
2416
2418
  uniqueCompanies,
2417
- tools: ['主体查询', '工商信息'],
2418
- estimatedCalls: uniqueCompanies * 2,
2419
- maxCalls: 200,
2419
+ tools: requiresProfile ? ['主体查询', '工商信息', '企业画像'] : ['主体查询', '工商信息'],
2420
+ estimatedCalls: uniqueCompanies * callsPerCompany,
2421
+ maxCalls: 300,
2420
2422
  withinLimit: runtime.rows.length <= 100,
2421
2423
  estimateType: 'upper-bound',
2422
2424
  });
@@ -2501,6 +2503,7 @@ window.__ModuleLoader__.load({
2501
2503
  rows: runtime.rows,
2502
2504
  headers: runtime.headers,
2503
2505
  nameField,
2506
+ fieldSelection,
2504
2507
  includeRisk: false,
2505
2508
  concurrency: 2,
2506
2509
  });
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { randomUUID } from 'node:crypto';
10
10
  import { QccBridgeError } from './qcc.js';
11
+ import { normalizeFieldSelection } from './workflow-contract.js';
11
12
 
12
13
  export const TOOL_QCC_COMMAND = 'data_cleaning_qcc_run';
13
14
 
@@ -47,6 +48,7 @@ function normalizedInput(input = {}) {
47
48
  rows: clone(rows),
48
49
  headers: Array.isArray(input.headers) ? input.headers.map(String) : [],
49
50
  nameField: String(input.nameField ?? 'name'),
51
+ fieldSelection: normalizeFieldSelection(input.fieldSelection),
50
52
  includeRisk: input.includeRisk === true,
51
53
  concurrency: Math.min(4, Math.max(1, Math.trunc(Number(input.concurrency ?? 2)))),
52
54
  };
@@ -201,6 +203,7 @@ export class QccCommandStore {
201
203
  const result = await this.bridge.enrichRows(record.input.rows, {
202
204
  nameField: record.input.nameField,
203
205
  includeRisk: record.input.includeRisk,
206
+ fieldSelection: record.input.fieldSelection,
204
207
  concurrency: record.input.concurrency,
205
208
  maxRows: 100,
206
209
  execution,
@@ -210,6 +213,7 @@ export class QccCommandStore {
210
213
  headers: record.input.headers,
211
214
  nameField: record.input.nameField,
212
215
  includeRisk: record.input.includeRisk,
216
+ fieldSelection: record.input.fieldSelection,
213
217
  concurrency: record.input.concurrency,
214
218
  result,
215
219
  audit,
package/lib/qcc-runs.js CHANGED
@@ -143,7 +143,7 @@ export class G5RunStore {
143
143
  return { value: await entry.promise, replayed: false };
144
144
  }
145
145
 
146
- createRun({ headers, nameField, includeRisk, concurrency, result, audit = [] }) {
146
+ createRun({ headers, nameField, fieldSelection, includeRisk, concurrency, result, audit = [] }) {
147
147
  this.cleanup();
148
148
  const id = this.runIdFactory();
149
149
  const at = this.nowIso();
@@ -156,6 +156,7 @@ export class G5RunStore {
156
156
  touchedAtMs: this.clock(),
157
157
  headers: Array.isArray(headers) ? headers.map(String) : [],
158
158
  nameField: String(nameField ?? 'name'),
159
+ fieldSelection: Array.isArray(fieldSelection) ? fieldSelection.map(String) : [],
159
160
  includeRisk: Boolean(includeRisk),
160
161
  concurrency: Number(concurrency ?? 2),
161
162
  summary: clone(result.summary),
@@ -196,6 +197,7 @@ export class G5RunStore {
196
197
  createdAt: record.createdAt,
197
198
  updatedAt: record.updatedAt,
198
199
  headers: record.headers,
200
+ fieldSelection: record.fieldSelection,
199
201
  summary: record.summary,
200
202
  rows: record.rows,
201
203
  reviewQueue: record.reviewQueue,
@@ -269,6 +271,7 @@ export class G5RunStore {
269
271
  }, {
270
272
  ...options,
271
273
  includeRisk: record.includeRisk,
274
+ fieldSelection: record.fieldSelection,
272
275
  onAudit: (event) => this.appendAudit(record, event),
273
276
  });
274
277
  this.patchCompany(record, name, queued.rowIndexes, result);
@@ -303,6 +306,7 @@ export class G5RunStore {
303
306
  result = await bridge.enrichCompany(item.companyName, {
304
307
  ...options,
305
308
  includeRisk: record.includeRisk,
309
+ fieldSelection: record.fieldSelection,
306
310
  onAudit: (event) => this.appendAudit(record, event),
307
311
  });
308
312
  } catch (error) {
package/lib/qcc.js CHANGED
@@ -24,6 +24,7 @@ export const QCC_TOOL_NAMES = Object.freeze({
24
24
  oauthStatus: 'qcc_oauth_status',
25
25
  entityLookup: 'mcp__qcc-company__get_company_by_query',
26
26
  registration: 'mcp__qcc-company__get_company_registration_info',
27
+ profile: 'mcp__qcc-company__get_company_profile',
27
28
  riskScan: 'mcp__qcc-risk__get_company_risk_scan',
28
29
  });
29
30
 
@@ -316,18 +317,98 @@ export function mapRegistrationFields(value, fallback = {}) {
316
317
  if (value.无匹配项 !== undefined) {
317
318
  throw new QccBridgeError('QCC_ENTITY_NOT_FOUND', 'Locked QCC entity could not be resolved by registration tool');
318
319
  }
320
+ const text = (...keys) => {
321
+ for (const key of keys) {
322
+ const item = value[key];
323
+ if (item !== undefined && item !== null && String(item).trim()) return String(item);
324
+ }
325
+ return '';
326
+ };
327
+ const region = text('所属地区', '所属地域', '地区');
328
+ const location = splitAdministrativeRegion(region);
319
329
  return {
320
- credit_no: String(value.统一社会信用代码 ?? fallback.creditNo ?? ''),
321
- legal_rep: String(value.法定代表人 ?? value.负责人 ?? value.经营者 ?? ''),
322
- reg_capital: String(
323
- value.注册资本 ?? value.注册资金 ?? value.开办资金 ?? value.成员出资总额 ?? value.资金数额 ?? '',
324
- ),
325
- establish_date: String(value.成立日期 ?? ''),
326
- reg_status: String(value.登记状态 ?? value.执业状态 ?? value.证书状态 ?? ''),
327
- biz_status: String(value.经营状态 ?? ''),
330
+ company_name: text('企业名称') || String(fallback.companyName ?? ''),
331
+ credit_no: text('统一社会信用代码', '信用代码') || String(fallback.creditNo ?? ''),
332
+ reg_no: text('工商注册号', '注册号'),
333
+ org_no: text('组织机构代码'),
334
+ reg_status: text('登记状态', '执业状态', '证书状态'),
335
+ legal_rep: text('法定代表人', '负责人', '经营者'),
336
+ reg_capital: text('注册资本', '注册资金', '开办资金', '成员出资总额', '资金数额'),
337
+ paid_capital: text('实缴资本'),
338
+ establish_date: text('成立日期'),
339
+ company_type: text('企业类型', '公司类型'),
340
+ registration_authority: text('登记机关'),
341
+ english_name: text('英文名', '英文名称'),
342
+ registered_address: text('注册地址', '住所', '经营场所'),
343
+ province: text('省份地区', '省份', '所属省份') || location.province,
344
+ city: text('城市', '所属城市', '市') || location.city,
345
+ district: text('区县', '所属区县') || location.district,
346
+ business_scope: text('经营范围'),
347
+ industry_category: text('国标行业'),
348
+ industry_large: text('一级行业', '企查查一级行业'),
349
+ industry_middle: text('二级行业', '企查查二级行业'),
350
+ operating_period: text('营业期限', '经营期限'),
351
+ company_size: text('企业规模', '人员规模'),
352
+ biz_status: text('经营状态'),
328
353
  };
329
354
  }
330
355
 
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
+ export function mapProfileFields(value) {
374
+ if (!isRecord(value)) {
375
+ throw new QccBridgeError('QCC_CONTRACT_MISMATCH', 'QCC profile tool returned a non-object result');
376
+ }
377
+ if (value.无匹配项 !== undefined) return {};
378
+ return {
379
+ // “企查查行业”没有声明一/二级语义,不得猜测塞入层级列。
380
+ industry_large: String(value.一级行业 ?? value.企查查一级行业 ?? ''),
381
+ industry_middle: String(value.二级行业 ?? value.企查查二级行业 ?? ''),
382
+ company_profile: String(value.企业简介 ?? value.简介 ?? ''),
383
+ };
384
+ }
385
+
386
+ const LEGACY_ENRICHMENT_FIELDS = Object.freeze([
387
+ 'credit_no', 'legal_rep', 'reg_capital', 'establish_date', 'reg_status', 'biz_status',
388
+ ]);
389
+ const PROFILE_FIELDS = new Set(['industry_large', 'industry_middle', 'company_profile']);
390
+
391
+ function requiresProfile(fieldSelection) {
392
+ return Array.isArray(fieldSelection) && fieldSelection.some((field) => PROFILE_FIELDS.has(field));
393
+ }
394
+
395
+ function mergeMappedFields(...sources) {
396
+ const output = {};
397
+ for (const source of sources) {
398
+ for (const [key, value] of Object.entries(source ?? {})) {
399
+ if (!Object.hasOwn(output, key) || (value !== '' && value !== null && value !== undefined)) output[key] = value;
400
+ }
401
+ }
402
+ return output;
403
+ }
404
+
405
+ function projectSelectedFields(fields, fieldSelection) {
406
+ const selected = Array.isArray(fieldSelection) && fieldSelection.length
407
+ ? [...new Set(fieldSelection.map(String))]
408
+ : LEGACY_ENRICHMENT_FIELDS;
409
+ return Object.fromEntries(selected.map((field) => [field, fields[field] ?? '']));
410
+ }
411
+
331
412
  export function mapRiskTags(value) {
332
413
  if (!isRecord(value)) return '';
333
414
  const rows = Array.isArray(value.风险因子扫描) ? value.风险因子扫描 : [];
@@ -399,6 +480,7 @@ export class QccHostBridge {
399
480
  oauthStatus: this.has(QCC_TOOL_NAMES.oauthStatus),
400
481
  entityLookup: this.has(QCC_TOOL_NAMES.entityLookup),
401
482
  registration: this.has(QCC_TOOL_NAMES.registration),
483
+ profile: this.has(QCC_TOOL_NAMES.profile),
402
484
  riskScan: this.has(QCC_TOOL_NAMES.riskScan),
403
485
  };
404
486
  const ready = capabilities.entityLookup && capabilities.registration;
@@ -645,12 +727,15 @@ export class QccHostBridge {
645
727
  { searchKey: lockedKey },
646
728
  options,
647
729
  );
648
- const fields = mapRegistrationFields(registration.data, match);
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));
734
+ }
735
+ const fields = projectSelectedFields(mapped, options.fieldSelection);
649
736
  if (options.includeRisk) {
650
737
  const risk = await this.call(QCC_TOOL_NAMES.riskScan, { searchKey: lockedKey }, options);
651
738
  fields.risk_tags = mapRiskTags(risk.data);
652
- } else {
653
- fields.risk_tags = '';
654
739
  }
655
740
  return {
656
741
  status: 'enriched',
@@ -670,12 +755,15 @@ export class QccHostBridge {
670
755
  { searchKey: creditNo },
671
756
  options,
672
757
  );
673
- const fields = mapRegistrationFields(registration.data, { companyName, creditNo });
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);
674
764
  if (options.includeRisk) {
675
765
  const risk = await this.call(QCC_TOOL_NAMES.riskScan, { searchKey: creditNo }, options);
676
766
  fields.risk_tags = mapRiskTags(risk.data);
677
- } else {
678
- fields.risk_tags = '';
679
767
  }
680
768
  return { status: 'enriched', companyName, fields };
681
769
  }
@@ -690,6 +778,7 @@ export class QccHostBridge {
690
778
  }
691
779
 
692
780
  const requiredTools = [QCC_TOOL_NAMES.entityLookup, QCC_TOOL_NAMES.registration];
781
+ if (requiresProfile(options.fieldSelection)) requiredTools.push(QCC_TOOL_NAMES.profile);
693
782
  if (options.includeRisk) requiredTools.push(QCC_TOOL_NAMES.riskScan);
694
783
  try {
695
784
  await Promise.all(requiredTools.map((name) => this.waitForTool(name, {
package/lib/web.js CHANGED
@@ -460,6 +460,7 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
460
460
  const artifacts = await artifactStore.createBundle(taskId, {
461
461
  rows: payload.rows,
462
462
  headers: payload.headers,
463
+ fieldSelection: readyTask.fieldSelection,
463
464
  exceptionRows: payload.exceptionRows,
464
465
  baseName: payload.baseName || readyTask.title,
465
466
  });
@@ -117,6 +117,15 @@ export const FIELD_CATALOG = Object.freeze([
117
117
  }),
118
118
  ]);
119
119
 
120
+ export const FIELD_LABELS = Object.freeze(Object.fromEntries(
121
+ FIELD_CATALOG.flatMap((group) => group.fields.map((field) => [field.id, field.label])),
122
+ ));
123
+
124
+ export function fieldLabel(fieldId) {
125
+ const id = String(fieldId ?? '').trim();
126
+ return FIELD_LABELS[id] ?? id;
127
+ }
128
+
120
129
  const FIELD_IDS = new Set(FIELD_CATALOG.flatMap((group) => group.fields.map((field) => field.id)));
121
130
  const STAGE_IDS = new Set(WORKFLOW_STAGES.map((stage) => stage.id));
122
131
  const STATE_IDS = new Set(WORKFLOW_STATES);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-data-cleaning-agent",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
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",
@@ -33,6 +33,7 @@
33
33
  "docs/RELEASE-0.5.3.md",
34
34
  "docs/RELEASE-0.6.0.md",
35
35
  "docs/RELEASE-0.6.1.md",
36
+ "docs/RELEASE-0.6.2.md",
36
37
  "docs/UI-WORKFLOW-V2.md",
37
38
  "docs/UI-WORKFLOW-V2-MIGRATION.md",
38
39
  "docs/UI-WORKFLOW-V2-ACCEPTANCE.md",