dsh-data-cleaning-agent 0.6.1 → 0.6.3

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,26 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.3] - 2026-09-05
8
+
9
+ ### Fixed
10
+ - 无表头、每行一家企业的粘贴名单改按「主体标识」逐行解析,不再把第一家企业误识为 CSV 表头。
11
+ - 已完成或已确认规则的任务再录入名单时自动创建新 taskId,避免新数据继承旧任务的锁定状态、默认字段或运行结果。
12
+ - 匹配补全预览的内部字段键改为中文表头,并改善窄工作台下的列宽和横向滚动。
13
+ - 缩小桌面端右侧工作台宽度,打开时让中央会话区留出同宽空间,并固定头部操作组、在窄屏隐藏次要状态,避免输入框被遮挡或关闭按钮被挤出可见区。
14
+
15
+ ### Changed
16
+ - 首次主体匹配不再立即发送内部 JSON 命令;改为向中央对话框回填可阅读、可编辑的中文任务说明,关闭右栏后由用户显式发送才执行 Agent-owned QCC 高层工具。
17
+
18
+ ## [0.6.2] - 2026-09-04
19
+
20
+ ### Fixed
21
+ - 清洗补全结果 CSV/XLSX 的插件字段与状态字段统一导出为中文表头,并按任务字段选择保留空值列,
22
+ 不再因企查查未返回某一字段而从制品中静默丢列。
23
+ - 将任务选择的补全字段贯穿中央工作台、Agent-owned 命令、G5 Host Bridge、任务恢复与耐久制品;
24
+ 工商详情补充注册地址、行政区划、经营范围、行业、营业期限和企业规模映射,企业简介等画像字段按需
25
+ 调用 `get_company_profile`,且不把未声明层级的“企查查行业”猜测为一级/二级行业。
26
+
7
27
  ## [0.6.1] - 2026-09-04
8
28
 
9
29
  ### 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.3** (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,10 @@ 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.3 release record](docs/RELEASE-0.6.3.md) for multiline entity-list parsing,
121
+ editable execution summaries, and workbench/composer layout fixes.
122
+ See [the 0.6.2 release record](docs/RELEASE-0.6.2.md) for Chinese export headers,
123
+ complete field-selection propagation, and profile-field enrichment.
120
124
  See [the 0.6.1 release record](docs/RELEASE-0.6.1.md) for the Agent-owned QCC workbench,
121
125
  session isolation, recovered statistics, and real end-to-end validation.
122
126
  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.3**(正式版本)
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,9 @@ 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.3 多行名单解析、可编辑执行说明与工作台遮挡修复见
117
+ [docs/RELEASE-0.6.3.md](docs/RELEASE-0.6.3.md)。0.6.2 中文导出表头、完整字段选择传递与画像字段补全见
118
+ [docs/RELEASE-0.6.2.md](docs/RELEASE-0.6.2.md)。0.6.1 Agent-owned QCC 工作台、会话隔离、恢复统计及真实闭环验收见
117
119
  [docs/RELEASE-0.6.1.md](docs/RELEASE-0.6.1.md)。0.6.0 五步 taskId 工作流、耐久制品、
118
120
  跨重启恢复与双基线验收见 [docs/RELEASE-0.6.0.md](docs/RELEASE-0.6.0.md)。
119
121
 
@@ -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 不会被自动重写;升级后重新生成制品即可获得中文完整表头。
@@ -0,0 +1,38 @@
1
+ # 0.6.3 多行名单解析与可编辑执行体验修复
2
+
3
+ > 版本:**0.6.3**
4
+ > 发布日期:2026-09-05
5
+ > 发布来源:`v0.6.3` Tag 触发 GitHub Actions,使用 npm OIDC Trusted Publishing。
6
+
7
+ ## 目标
8
+
9
+ 0.6.3 是 0.6.2 的 UI 与任务正确性补丁,不扩大企查查工具域:
10
+
11
+ 1. 无表头、每行一家企业的粘贴名单按「主体标识」逐行解析,不再把首家企业误作表头;
12
+ 2. 完成或确认规则后的任务再次录入名单时创建新 taskId,不复用旧任务锁定状态和结果;
13
+ 3. 首次主体匹配先将可阅读、可编辑的中文任务说明回填中央对话框,由用户发送后才执行企查查调用;
14
+ 4. 桌面端右侧工作台打开时为中央会话和输入框让位,窄屏保持关闭按钮可见;
15
+ 5. 匹配补全预览使用中文字段名,并保留横向滚动能力。
16
+
17
+ ## 安全与调用边界
18
+
19
+ - 名单、字段和额度确认仍只暂存在本机 Host;可见任务说明不包含企业名单。
20
+ - 生成任务说明不会产生企查查调用;用户显式发送后才调用一次 `data_cleaning_qcc_run`。
21
+ - 使用当前用户自己连接的企查查 MCP 账号,插件不内置、不保存或分发 OAuth token、Key。
22
+ - 历史域、人员域和招投标域仍按既定范围延期,本补丁不新增相关工具。
23
+
24
+ ## 验收
25
+
26
+ - 两行纯文本企业名单解析为 2 行,显式表头输入仍走 CSV 解析;
27
+ - 已完成任务重新录入数据后生成新的 taskId;
28
+ - 可见任务说明展示处理动作、数量和中文字段,不展示 `schemaVersion` 或 JSON;
29
+ - 1280px 与 900px 视口下右侧工作台不遮挡中央输入框,720px 下关闭按钮可见;
30
+ - 结果预览和新生成的 CSV/XLSX 使用中文字段名;已选择但无返回值的字段保留空列;
31
+ - `npm run check` 全绿,177/177 自动化测试通过,发布包内容命中白名单。
32
+
33
+ ## 升级与回滚
34
+
35
+ - 升级:安装 `dsh-data-cleaning-agent@0.6.3` 并完全停止、重新运行 `dsh web`。
36
+ - 回滚:重新安装 `dsh-data-cleaning-agent@0.6.2` 并完全重启 DSH。
37
+ - 本补丁不修改 `dc_workflows_v2` 存储 schema,也不删除既有任务或耐久制品。
38
+ - 旧任务制品不会自动重写;升级后需新建任务并重新生成结果,才能获得修正后的行数和表头。
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
@@ -113,7 +113,9 @@ window.__ModuleLoader__.load({
113
113
  .dcAgentWorkbench {
114
114
  display: flex;
115
115
  flex-direction: column;
116
- width: min(510px, calc(100vw - 72px));
116
+ width: min(460px, 42vw);
117
+ max-width: calc(100vw - 420px);
118
+ min-width: 360px;
117
119
  height: 100vh;
118
120
  border: 1px solid light-dark(#d5dae4, #2a3038);
119
121
  border-radius: 12px 0 0 12px;
@@ -397,8 +399,8 @@ window.__ModuleLoader__.load({
397
399
  .dcAgentWbHeader {
398
400
  display: flex;
399
401
  align-items: center;
400
- gap: 12px;
401
- padding: 14px 18px;
402
+ gap: 8px;
403
+ padding: 12px 14px;
402
404
  border-bottom: 1px solid light-dark(#e4e8f0, #303947);
403
405
  }
404
406
 
@@ -421,8 +423,18 @@ window.__ModuleLoader__.load({
421
423
  background: light-dark(#edf4ff, #172841);
422
424
  }
423
425
 
426
+ .dcAgentWbTitle b,
427
+ .dcAgentWbTitle small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
424
428
  .dcAgentWbTitle b { display: block; font-size: 15px; }
425
- .dcAgentWbTitle small { display: block; color: light-dark(#697386, #aab5c7); font-size: 11px; }
429
+ .dcAgentWbTitle small { display: block; max-width: 190px; color: light-dark(#697386, #aab5c7); font-size: 11px; }
430
+
431
+ .dcAgentWbActions {
432
+ display: flex;
433
+ align-items: center;
434
+ gap: 6px;
435
+ flex: 0 0 auto;
436
+ margin-left: auto;
437
+ }
426
438
 
427
439
  .dcAgentQccBadge {
428
440
  flex: 0 0 auto;
@@ -562,6 +574,12 @@ window.__ModuleLoader__.load({
562
574
  }
563
575
  .dcAgentTable th { color: light-dark(#697386, #aab5c7); font-weight: 600; }
564
576
  .dcAgentTable td.num { font-variant-numeric: tabular-nums; }
577
+ .dcAgentTableWrap { max-width: 100%; overflow-x: auto; }
578
+ .dcAgentTableWrap .dcAgentTable { min-width: 720px; table-layout: auto; }
579
+ .dcAgentTableWrap .dcAgentTable th,
580
+ .dcAgentTableWrap .dcAgentTable td { min-width: 112px; white-space: nowrap; }
581
+ .dcAgentTableWrap .dcAgentTable th:first-child,
582
+ .dcAgentTableWrap .dcAgentTable td:first-child { min-width: 190px; }
565
583
 
566
584
  .dcAgentError {
567
585
  padding: 10px 12px;
@@ -709,11 +727,28 @@ window.__ModuleLoader__.load({
709
727
  .dcAgentPreviewTable { margin-top: 12px; overflow: auto; border: 1px solid light-dark(#e4e8f0, #303947); border-radius: 10px; }
710
728
  .dcAgentHistoryTask { display: block; width: 100%; padding: 10px; text-align: left; }
711
729
  .dcAgentHistoryTask h3 { margin: 0; font-size: 12px; }
730
+ /*
731
+ * DSH 的 root.side slot 是覆盖层。桌面端工作台打开时,让带稳定标记的
732
+ * 会话滚动区主动让出同宽空间,确保消息流和中央 composer 不被遮挡。
733
+ * 移动端仍保留全屏工作台,不压缩底层会话。
734
+ */
735
+ @media (min-width: 1041px) {
736
+ body:has(.dcAgentWorkbench) [data-conversation-scroll] {
737
+ box-sizing: border-box;
738
+ padding-right: min(460px, 42vw);
739
+ }
740
+ }
741
+ @media (min-width: 761px) and (max-width: 1040px) {
742
+ body:has(.dcAgentWorkbench) [data-conversation-scroll] {
743
+ box-sizing: border-box;
744
+ padding-right: min(420px, 48vw);
745
+ }
746
+ }
712
747
  @media (max-width: 760px) {
713
- .dcAgentWorkbench, .dcAgentWorkbench.is-expanded { width: 100vw; border-radius: 0; }
748
+ .dcAgentWorkbench, .dcAgentWorkbench.is-expanded { width: 100vw; max-width: none; min-width: 0; border-radius: 0; }
714
749
  .dcAgentOverlay { background: rgba(8, 10, 14, 0.22); pointer-events: auto; }
715
750
  .dcAgentStepper { grid-template-columns: repeat(2, minmax(0, 1fr)); }
716
- .dcAgentQccBadge { display: none; }
751
+ .dcAgentQccBadge, .dcAgentJobsPill { display: none; }
717
752
  .dcAgentCandidate { grid-template-columns: 1fr; }
718
753
  .dcAgentCapabilities { justify-content: flex-start; padding-inline: 12px; }
719
754
  .dcAgentCapability { min-width: 92px; }
@@ -725,6 +760,10 @@ window.__ModuleLoader__.load({
725
760
  .dcAgentMappingRow { grid-template-columns: 1fr; }
726
761
  .dcAgentMappingRow > b { display: none; }
727
762
  }
763
+ @media (min-width: 761px) and (max-width: 1040px) {
764
+ .dcAgentWorkbench { width: 420px; max-width: 48vw; min-width: 360px; }
765
+ .dcAgentQccBadge, .dcAgentJobsPill { display: none; }
766
+ }
728
767
  `;
729
768
 
730
769
  /** 安装入口、会话能力按钮与右侧工作台样式。 */
@@ -816,6 +855,17 @@ window.__ModuleLoader__.load({
816
855
  ]],
817
856
  ];
818
857
  const ENRICHMENT_OPTIONS = FIELD_GROUPS.flatMap(([, , fields]) => fields);
858
+ const RESULT_FIELD_LABELS = new Map([
859
+ ...ENRICHMENT_OPTIONS,
860
+ ['biz_status', '经营状态'],
861
+ ['risk_tags', '风险标签'],
862
+ ['qcc_match_status', '匹配状态'],
863
+ ['qcc_source', '数据来源'],
864
+ ['qcc_error', '错误原因'],
865
+ ['match_status', '匹配状态'],
866
+ ['input_company', '原始企业名称'],
867
+ ['candidate_company', '候选企业名称'],
868
+ ]);
819
869
  const MATCH_ANCHOR_OPTIONS = [
820
870
  ['company_name', '企业名称'], ['credit_no', '统一社会信用代码'], ['reg_no', '注册号'],
821
871
  ];
@@ -852,9 +902,13 @@ window.__ModuleLoader__.load({
852
902
  function isKnownCleaningDraft(value) {
853
903
  if (value === DEFAULT_SESSION_PROMPT) return true;
854
904
  if (typeof value !== 'string') return false;
855
- return value.startsWith('请执行一项企业名单数据清洗补全任务。')
905
+ const wizardDraft = value.startsWith('请执行一项企业名单数据清洗补全任务。')
856
906
  && value.includes('企查查连接、套餐额度和费用均由当前用户自己的账号承担。')
857
907
  && value.includes('提供结果和待复核清单的导出。');
908
+ const executionDraft = value.startsWith('请执行已在「数据清洗补全工作台」确认的企业数据任务。')
909
+ && value.includes('安全任务凭证:dcq-')
910
+ && value.includes('data_cleaning_qcc_run');
911
+ return wizardDraft || executionDraft;
858
912
  }
859
913
 
860
914
  function clearCleaningDraft(ctx, sessionId, onlyDefault = false) {
@@ -1029,6 +1083,22 @@ window.__ModuleLoader__.load({
1029
1083
  };
1030
1084
  }
1031
1085
 
1086
+ /**
1087
+ * 工作台粘贴区兼容「每行一个企业名称 / 信用代码」。
1088
+ * 无分隔符且首行不是显式表头时,不得把第一家企业误当 CSV 表头。
1089
+ */
1090
+ function plainEntityListDataset(text) {
1091
+ const lines = String(text ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1092
+ if (!lines.length || lines.some((line) => /[,\t;]/.test(line))) return null;
1093
+ const explicitHeader = /^(?:name|company|company_name|credit_no|creditcode|unified_credit_code|主体标识|企业名称|公司名称|单位名称|统一社会信用代码|信用代码|注册号)$/i;
1094
+ if (explicitHeader.test(lines[0])) return null;
1095
+ return entriesToDataset(lines);
1096
+ }
1097
+
1098
+ function resultFieldLabel(key) {
1099
+ return RESULT_FIELD_LABELS.get(String(key ?? '')) ?? String(key ?? '');
1100
+ }
1101
+
1032
1102
  const MAPPING_PATTERNS = [
1033
1103
  ['company_name', /^(name|company|company_name|企业名称|公司名称|单位名称)$/i],
1034
1104
  ['credit_no', /^(credit_no|creditCode|unified_credit_code|统一社会信用代码|信用代码)$/i],
@@ -1131,10 +1201,8 @@ window.__ModuleLoader__.load({
1131
1201
  return task;
1132
1202
  }
1133
1203
 
1134
- async function ensureWorkflowTask(actions, sessionId, draft = {}) {
1204
+ async function createWorkflowTask(actions, sessionId, draft = {}) {
1135
1205
  const key = String(sessionId || 'unassigned');
1136
- const cached = workflowTaskBySession.get(key);
1137
- if (cached) return cacheWorkflowTask(actions, sessionId, cached);
1138
1206
  let pending = workflowTaskCreationBySession.get(key);
1139
1207
  if (!pending) {
1140
1208
  pending = requestJson('/data-cleaning/api/workflow/tasks', 'POST', {
@@ -1155,6 +1223,29 @@ window.__ModuleLoader__.load({
1155
1223
  return cacheWorkflowTask(actions, sessionId, response.task);
1156
1224
  }
1157
1225
 
1226
+ async function ensureWorkflowTask(actions, sessionId, draft = {}) {
1227
+ const key = String(sessionId || 'unassigned');
1228
+ const cached = workflowTaskBySession.get(key);
1229
+ if (cached) return cacheWorkflowTask(actions, sessionId, cached);
1230
+ return createWorkflowTask(actions, sessionId, draft);
1231
+ }
1232
+
1233
+ const EDITABLE_WORKFLOW_STATES = new Set(['draft', 'uploaded', 'parse_failed']);
1234
+
1235
+ /**
1236
+ * 完成 / 已确认规则的任务不能就地改写。用户再次录入名单时,
1237
+ * 在同一清洗会话内自动创建新 taskId,避免把新名单写入旧任务。
1238
+ */
1239
+ async function ensureEditableWorkflowTask(actions, sessionId, draft = {}, preferredTask) {
1240
+ const key = String(sessionId || 'unassigned');
1241
+ const current = workflowTaskBySession.get(key) ?? preferredTask ?? null;
1242
+ if (!current) return createWorkflowTask(actions, sessionId, draft);
1243
+ if (EDITABLE_WORKFLOW_STATES.has(current.state)) return cacheWorkflowTask(actions, sessionId, current);
1244
+ workflowTaskBySession.delete(key);
1245
+ actions.resetRunForNewDataset?.();
1246
+ return createWorkflowTask(actions, sessionId, draft);
1247
+ }
1248
+
1158
1249
  /** 同一会话的 Host 写操作串行化,确保 expectedRevision 不因 UI 事件竞态失效。 */
1159
1250
  function queueWorkflowOperation(sessionId, operation) {
1160
1251
  const key = String(sessionId || 'unassigned');
@@ -1273,6 +1364,15 @@ window.__ModuleLoader__.load({
1273
1364
  draft.paidConfirmed = false;
1274
1365
  draft.error = null;
1275
1366
  },
1367
+ resetRunForNewDataset: (draft) => {
1368
+ draft.profile = null;
1369
+ draft.clean = null;
1370
+ draft.complete = null;
1371
+ draft.qccEstimate = null;
1372
+ draft.qccRun = null;
1373
+ draft.paidConfirmed = false;
1374
+ draft.error = null;
1375
+ },
1276
1376
  setDataset: (draft, dataset) => { draft.dataset = dataset; draft.error = null; },
1277
1377
  setProfile: (draft, profile) => { draft.profile = profile; draft.error = null; },
1278
1378
  setClean: (draft, clean) => { draft.clean = clean; draft.error = null; },
@@ -1379,6 +1479,18 @@ window.__ModuleLoader__.load({
1379
1479
  await sessionConversation.send(prompt);
1380
1480
  }
1381
1481
 
1482
+ function setQccAgentDraft(ctx, sessionId, prompt) {
1483
+ const conversation = typeof ctx.get === 'function' ? ctx.get('conversation') : ctx.conversation;
1484
+ const shell = conversation?.input?.shell?.(sessionId);
1485
+ if (!shell || typeof shell.setDraft !== 'function') {
1486
+ throw new Error('当前 DSH 版本没有可用的会话草稿回填能力');
1487
+ }
1488
+ const accepted = shell.setDraft(prompt);
1489
+ if (accepted === false) throw new Error('任务说明未能回填到当前对话框');
1490
+ ctx.sessions?.open?.(sessionId);
1491
+ return true;
1492
+ }
1493
+
1382
1494
  function capabilityIcon(item, size = 16) {
1383
1495
  return typeof item.icon === 'function'
1384
1496
  ? h(item.icon, { size, 'aria-hidden': 'true' })
@@ -2054,6 +2166,8 @@ window.__ModuleLoader__.load({
2054
2166
  }
2055
2167
  return;
2056
2168
  }
2169
+ const entityList = plainEntityListDataset(trimmed);
2170
+ if (entityList) return entityList;
2057
2171
  return api('/data-cleaning/api/mvp/parse', { filename: 'data.csv', content: trimmed });
2058
2172
  }
2059
2173
 
@@ -2085,9 +2199,19 @@ window.__ModuleLoader__.load({
2085
2199
  }
2086
2200
 
2087
2201
  async function persistParsedWorkflow(actions, sessionId, result, source = {}, task) {
2088
- let current = task ?? await ensureWorkflowTask(actions, sessionId);
2202
+ let current = await ensureEditableWorkflowTask(actions, sessionId, {}, task);
2089
2203
  const stagingKey = `session:${sessionId || 'unassigned'}`;
2090
2204
  moveRuntime(stagingKey, current.id);
2205
+ // handleParse 可能先把新数据写入旧 task runtime。无论是否新建 taskId,
2206
+ // 都以本次解析结果覆盖目标 runtime,避免复用已完成任务的一行旧数据。
2207
+ const target = runtimeFor(current.id);
2208
+ target.rows = Array.isArray(result.rows) ? result.rows : [];
2209
+ target.headers = Array.isArray(result.headers) ? result.headers : [];
2210
+ target.source = {
2211
+ type: source.type || result.fmt || 'csv',
2212
+ fileName: source.fileName || '',
2213
+ sizeBytes: source.sizeBytes || 0,
2214
+ };
2091
2215
  if (['draft', 'uploaded', 'parse_failed'].includes(current.state)) {
2092
2216
  current = await workflowAction(actions, sessionId, current, 'upload', {
2093
2217
  source: {
@@ -2107,6 +2231,7 @@ window.__ModuleLoader__.load({
2107
2231
  function WorkbenchDrawer(props) {
2108
2232
  const { useStore, actions } = props;
2109
2233
  const sendSessionCommand = props.sendSessionCommand;
2234
+ const setSessionDraft = props.setSessionDraft;
2110
2235
  const open = useStore((state) => state.open);
2111
2236
  const step = useStore((state) => state.step);
2112
2237
  const expanded = useStore((state) => state.expanded);
@@ -2173,7 +2298,7 @@ window.__ModuleLoader__.load({
2173
2298
  if (draft.fieldSelection) actions.setFieldSelection(draft.fieldSelection);
2174
2299
  for (const [key, value] of Object.entries(draft.matchRules || {})) actions.setMatchRule(key, value);
2175
2300
  void queueWorkflowOperation(detail.sessionId, async () => {
2176
- const task = await ensureWorkflowTask(actions, detail.sessionId, draft);
2301
+ const task = await ensureEditableWorkflowTask(actions, detail.sessionId, draft);
2177
2302
  return updateWorkflowTask(actions, detail.sessionId, task, draft);
2178
2303
  }).catch((eventError) => actions.setError(eventError instanceof Error ? eventError.message : String(eventError)));
2179
2304
  };
@@ -2258,6 +2383,9 @@ window.__ModuleLoader__.load({
2258
2383
  const runtimeKey = cachedTask?.id ?? `session:${activeSessionId || 'unassigned'}`;
2259
2384
  const runtime = runtimeFor(runtimeKey);
2260
2385
  const lastCsv = runtime.lastCsv;
2386
+ const qccPreviewColumns = Array.isArray(qccRun?.rows) && qccRun.rows.length
2387
+ ? Object.keys(qccRun.rows[0] || {}).slice(0, 8)
2388
+ : [];
2261
2389
  const fieldByPattern = (pattern) => runtime.headers.find((field) => pattern.test(field)) ?? null;
2262
2390
  const phoneField = fieldByPattern(/^(phone|mobile|tel|telephone|联系电话|手机号码|手机号)$/i);
2263
2391
  const amountField = fieldByPattern(/^(amount|price|金额|注册资本)$/i);
@@ -2412,11 +2540,13 @@ window.__ModuleLoader__.load({
2412
2540
 
2413
2541
  const estimateQcc = async () => {
2414
2542
  const uniqueCompanies = new Set(runtime.rows.map((row) => String(row?.[nameField] ?? '').trim()).filter(Boolean)).size;
2543
+ const requiresProfile = fieldSelection.some((field) => ['company_profile', 'industry_large', 'industry_middle'].includes(field));
2544
+ const callsPerCompany = requiresProfile ? 3 : 2;
2415
2545
  actions.setQccEstimate({
2416
2546
  uniqueCompanies,
2417
- tools: ['主体查询', '工商信息'],
2418
- estimatedCalls: uniqueCompanies * 2,
2419
- maxCalls: 200,
2547
+ tools: requiresProfile ? ['主体查询', '工商信息', '企业画像'] : ['主体查询', '工商信息'],
2548
+ estimatedCalls: uniqueCompanies * callsPerCompany,
2549
+ maxCalls: 300,
2420
2550
  withinLimit: runtime.rows.length <= 100,
2421
2551
  estimateType: 'upper-bound',
2422
2552
  });
@@ -2489,26 +2619,55 @@ window.__ModuleLoader__.load({
2489
2619
  return completedCommand.run;
2490
2620
  };
2491
2621
 
2622
+ const prepareEditableQccCommand = async (payload) => {
2623
+ if (!activeSessionId || typeof setSessionDraft !== 'function') {
2624
+ throw new Error('当前会话无法回填可编辑的数据清洗补全任务说明');
2625
+ }
2626
+ const prepared = await api('/data-cleaning/api/g5/commands', {
2627
+ ...payload,
2628
+ confirmPaidCalls: true,
2629
+ });
2630
+ const command = prepared?.command;
2631
+ if (!command?.commandId || !command?.prompt) throw new Error('Host 未返回有效的企查查任务命令');
2632
+ setSessionDraft(activeSessionId, command.prompt);
2633
+ actions.close();
2634
+
2635
+ // 发送权仍在用户手中:草稿回填不会调用 QCC。用户发送后,
2636
+ // Agent-owned 工具改变 command 状态;本地安全轮询只读取结果并回写工作流。
2637
+ void waitForQccCommand(command.commandId, 1800).then(async (completedCommand) => {
2638
+ if (completedCommand.state === 'failed') {
2639
+ const commandError = new Error(completedCommand.error?.message || '智能体企查查任务执行失败');
2640
+ commandError.code = completedCommand.error?.code;
2641
+ throw commandError;
2642
+ }
2643
+ if (!completedCommand.run) throw new Error('智能体企查查任务完成但结果已失效');
2644
+ await queueWorkflowOperation(activeSessionId, () => applyQccRun(completedCommand.run));
2645
+ }).catch(async (commandError) => {
2646
+ if (['QCC_NOT_CONNECTED', 'QCC_TOOL_UNAVAILABLE', 'QCC_AUTH_REQUIRED'].includes(commandError?.code)) {
2647
+ const latest = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? cachedTask;
2648
+ try { await workflowAction(actions, activeSessionId, latest, 'authorization-required'); } catch (_workflowError) {}
2649
+ }
2650
+ actions.setError(commandError instanceof Error ? commandError.message : String(commandError));
2651
+ });
2652
+ return command;
2653
+ };
2654
+
2492
2655
  const runQcc = async () => {
2493
2656
  if (busy || !qccEstimate || !paidConfirmed || !qccEstimate.withinLimit) return;
2494
2657
  actions.setBusy(true);
2495
2658
  actions.setError(null);
2496
2659
  try {
2497
- const current = await workflowAction(actions, activeSessionId, cachedTask, 'match-start');
2498
- const r = await executePreparedQccCommand({
2660
+ await prepareEditableQccCommand({
2499
2661
  kind: 'enrich',
2500
- taskId: current.id,
2662
+ taskId: cachedTask.id,
2501
2663
  rows: runtime.rows,
2502
2664
  headers: runtime.headers,
2503
2665
  nameField,
2666
+ fieldSelection,
2504
2667
  includeRisk: false,
2505
2668
  concurrency: 2,
2506
2669
  });
2507
- await applyQccRun(r);
2508
2670
  } catch (err) {
2509
- if (['QCC_NOT_CONNECTED', 'QCC_TOOL_UNAVAILABLE', 'QCC_AUTH_REQUIRED'].includes(err?.code)) {
2510
- try { await workflowAction(actions, activeSessionId, cachedTask, 'authorization-required'); } catch (_workflowError) {}
2511
- }
2512
2671
  actions.setError(err instanceof Error ? err.message : String(err));
2513
2672
  } finally {
2514
2673
  actions.setBusy(false);
@@ -2883,8 +3042,9 @@ window.__ModuleLoader__.load({
2883
3042
  h('input', { type: 'checkbox', checked: paidConfirmed, 'aria-label': '确认使用当前用户的企查查账号额度', onChange: (event) => actions.setPaidConfirmed(event.target.checked) }),
2884
3043
  '我已核对企业数量和调用上界,并确认使用自己连接的企查查 MCP 账号;额度或费用由该账号自行承担',
2885
3044
  ),
3045
+ h('p', { className: 'dcAgentHint' }, '下一步只会把中文任务说明回填到中央对话框。你可先阅读、补充备注,点击发送后才开始企查查调用。'),
2886
3046
  h('div', { className: 'dcAgentRow' },
2887
- h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !paidConfirmed || !qccEstimate.withinLimit, onClick: runQcc }, busy ? '执行中…' : '开始主体匹配'),
3047
+ h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !paidConfirmed || !qccEstimate.withinLimit, onClick: runQcc }, busy ? '生成中…' : '生成可编辑任务说明'),
2888
3048
  ),
2889
3049
  ) : null,
2890
3050
  ),
@@ -2899,10 +3059,10 @@ window.__ModuleLoader__.load({
2899
3059
  Array.isArray(qccRun.rows) && qccRun.rows.length ? h('div', { className: 'dcAgentTableWrap' },
2900
3060
  h('table', { className: 'dcAgentTable', 'aria-label': '匹配补全结果预览' },
2901
3061
  h('thead', null, h('tr', null,
2902
- Object.keys(qccRun.rows[0] || {}).slice(0, 6).map((key) => h('th', { key }, key)),
3062
+ qccPreviewColumns.map((key) => h('th', { key }, resultFieldLabel(key))),
2903
3063
  )),
2904
3064
  h('tbody', null, qccRun.rows.slice(0, 8).map((row, index) => h('tr', { key: `${index}-${row?.qcc_credit_no || row?.credit_no || ''}` },
2905
- Object.keys(qccRun.rows[0] || {}).slice(0, 6).map((key) => h('td', { key }, String(row?.[key] ?? ''))),
3065
+ qccPreviewColumns.map((key) => h('td', { key }, String(row?.[key] ?? ''))),
2906
3066
  ))),
2907
3067
  ),
2908
3068
  h('p', { className: 'dcAgentHint' }, `显示前 ${Math.min(8, qccRun.rows.length)} 行;完整结果在下载阶段生成耐久制品。`),
@@ -3044,26 +3204,29 @@ window.__ModuleLoader__.load({
3044
3204
  h('small', null, cachedTask ? `${WORKFLOW_STATE_LABELS[cachedTask.state] || cachedTask.state} · ${cachedTask.id.slice(0, 18)}` : '正在创建 Host taskId…'),
3045
3205
  ),
3046
3206
  ),
3047
- h('span', {
3048
- className: 'dcAgentQccBadge',
3049
- title: qccRun ? `任务 ${qccRun.runId}` : '仅在当前用户确认使用自己的企查查账号后调用',
3050
- }, qccRun ? `QCC · ${qccRun.state}` : qccCapabilities ? (qccCapabilities.capabilities?.ready ? 'QCC · 已连接' : 'QCC · 能力不完整') : 'QCC · 待检测'),
3051
- h('span', { className: 'dcAgentJobsPill', 'data-state': jobsPill(jobs).state, title: '后台任务状态' }, jobsPill(jobs).label),
3052
- h('button', {
3053
- className: 'dcAgentWbClose',
3054
- type: 'button',
3055
- 'aria-label': expanded ? '收起工作台' : '展开工作台',
3056
- onClick: () => actions.toggleExpanded(),
3057
- }, expanded ? '↘' : '↖'),
3058
- h('button', {
3059
- className: 'dcAgentWbClose',
3060
- type: 'button',
3061
- 'aria-label': '关闭',
3062
- onClick: () => {
3063
- stopJobsPolling();
3064
- actions.close();
3065
- },
3066
- }, '✕'),
3207
+ h('div', { className: 'dcAgentWbActions' },
3208
+ h('span', {
3209
+ className: 'dcAgentQccBadge',
3210
+ title: qccRun ? `任务 ${qccRun.runId}` : '仅在当前用户确认使用自己的企查查账号后调用',
3211
+ }, qccRun ? `QCC · ${qccRun.state}` : qccCapabilities ? (qccCapabilities.capabilities?.ready ? 'QCC · 已连接' : 'QCC · 能力不完整') : 'QCC · 待检测'),
3212
+ h('span', { className: 'dcAgentJobsPill', 'data-state': jobsPill(jobs).state, title: '后台任务状态' }, jobsPill(jobs).label),
3213
+ h('button', {
3214
+ className: 'dcAgentWbClose',
3215
+ type: 'button',
3216
+ 'aria-label': expanded ? '收起工作台' : '展开工作台',
3217
+ onClick: () => actions.toggleExpanded(),
3218
+ }, expanded ? '' : '↖'),
3219
+ h('button', {
3220
+ className: 'dcAgentWbClose',
3221
+ type: 'button',
3222
+ title: '关闭工作台(Esc)',
3223
+ 'aria-label': '关闭',
3224
+ onClick: () => {
3225
+ stopJobsPolling();
3226
+ actions.close();
3227
+ },
3228
+ }, '✕'),
3229
+ ),
3067
3230
  ),
3068
3231
  h('nav', { className: 'dcAgentStepper', 'aria-label': '清洗流程' },
3069
3232
  STEPS.map((st) => h('button', {
@@ -3111,6 +3274,7 @@ window.__ModuleLoader__.load({
3111
3274
  store: workbenchStore,
3112
3275
  inject: () => ({
3113
3276
  sendSessionCommand: (sessionId, prompt) => sendQccAgentCommand(ctx, sessionId, prompt),
3277
+ setSessionDraft: (sessionId, prompt) => setQccAgentDraft(ctx, sessionId, prompt),
3114
3278
  }),
3115
3279
  }, WorkbenchDrawer));
3116
3280
 
@@ -3179,8 +3343,11 @@ window.__ModuleLoader__.load({
3179
3343
  isKnownCleaningDraft,
3180
3344
  markCleaningSession,
3181
3345
  qualitySummaryFor,
3346
+ plainEntityListDataset,
3347
+ resultFieldLabel,
3182
3348
  rewriteHeroChrome,
3183
3349
  ensureWorkflowTask,
3350
+ ensureEditableWorkflowTask,
3184
3351
  queueWorkflowOperation,
3185
3352
  };
3186
3353
  return module.exports;
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { randomUUID } from 'node:crypto';
10
10
  import { QccBridgeError } from './qcc.js';
11
+ import { fieldLabel, 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
  };
@@ -68,23 +70,34 @@ function normalizedInput(input = {}) {
68
70
  }
69
71
 
70
72
  export function serializeQccCommandPrompt(command) {
71
- const visible = {
72
- schemaVersion: 1,
73
- commandId: command.commandId,
74
- taskId: command.taskId,
75
- kind: command.kind,
76
- };
73
+ const input = command.input ?? {};
74
+ const action = command.kind === 'resolve'
75
+ ? '确认并继续处理已选的企业主体'
76
+ : command.kind === 'retry'
77
+ ? '重试已选的可恢复失败项'
78
+ : '执行企业主体匹配与字段补全';
79
+ const rowCount = command.kind === 'enrich'
80
+ ? (Array.isArray(input.rows) ? input.rows.length : 0)
81
+ : command.kind === 'retry'
82
+ ? (Array.isArray(input.companyNames) ? input.companyNames.length : 0)
83
+ : 1;
84
+ const fields = command.kind === 'enrich'
85
+ ? normalizeFieldSelection(input.fieldSelection).map(fieldLabel)
86
+ : [];
77
87
  return [
78
- '请执行数据清洗补全企查查任务。',
88
+ '请执行已在「数据清洗补全工作台」确认的企业数据任务。',
79
89
  '',
80
- '类型化任务意图(schemaVersion 1):',
81
- '```json',
82
- JSON.stringify(visible, null, 2),
83
- '```',
90
+ `处理内容:${action}。`,
91
+ `任务编号:${command.taskId}。`,
92
+ `处理数量:${rowCount} 条企业记录。`,
93
+ ...(fields.length ? [`需要补全:${fields.join('')}。`] : []),
94
+ '执行边界:名单、字段和额度确认已安全暂存在本机 Host;使用当前用户自己连接的企查查 MCP 账号。',
84
95
  '',
85
- `请准确调用一次 ${TOOL_QCC_COMMAND} 并只传递 commandId。`,
86
- '企业名单和字段选择已安全暂存在本机 Host,不得要求用户在对话中重复粘贴,不得直接调用任何 mcp__qcc-* 工具。',
87
- '高层工具返回后立即结束本轮;不得重试、扩大名单或追加字段。',
96
+ '你可以在发送前补充任务备注。如需修改名单、匹配规则或补全字段,请先返回工作台调整后重新生成。',
97
+ `安全任务凭证:${command.commandId}`,
98
+ '',
99
+ `发送本说明后,请仅调用一次数据清洗补全执行工具(${TOOL_QCC_COMMAND}),参数只传递上述安全任务凭证。`,
100
+ '不要要求重复粘贴企业名单,不要直接调用 mcp__qcc-* 工具,不要重试、扩大名单或追加字段。',
88
101
  ].join('\n');
89
102
  }
90
103
 
@@ -201,6 +214,7 @@ export class QccCommandStore {
201
214
  const result = await this.bridge.enrichRows(record.input.rows, {
202
215
  nameField: record.input.nameField,
203
216
  includeRisk: record.input.includeRisk,
217
+ fieldSelection: record.input.fieldSelection,
204
218
  concurrency: record.input.concurrency,
205
219
  maxRows: 100,
206
220
  execution,
@@ -210,6 +224,7 @@ export class QccCommandStore {
210
224
  headers: record.input.headers,
211
225
  nameField: record.input.nameField,
212
226
  includeRisk: record.input.includeRisk,
227
+ fieldSelection: record.input.fieldSelection,
213
228
  concurrency: record.input.concurrency,
214
229
  result,
215
230
  audit,
@@ -230,7 +245,7 @@ export class QccCommandStore {
230
245
  export function registerQccCommandTool(tools, commands) {
231
246
  return tools.register({
232
247
  name: TOOL_QCC_COMMAND,
233
- description: 'Execute one already-staged data-cleaning QCC command. Call only when a visible typed intent supplies commandId. The Host owns rows, billing confirmation, idempotency and result artifacts.',
248
+ description: 'Execute one already-staged data-cleaning QCC command. Call only when a visible readable workbench task summary supplies a dcq-* commandId. The Host owns rows, billing confirmation, idempotency and result artifacts.',
234
249
  parameters: {
235
250
  type: 'object',
236
251
  additionalProperties: false,
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, {
@@ -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:',
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.3",
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,8 @@
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",
37
+ "docs/RELEASE-0.6.3.md",
36
38
  "docs/UI-WORKFLOW-V2.md",
37
39
  "docs/UI-WORKFLOW-V2-MIGRATION.md",
38
40
  "docs/UI-WORKFLOW-V2-ACCEPTANCE.md",