dsh-data-cleaning-agent 0.6.2 → 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,17 @@
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
+
7
18
  ## [0.6.2] - 2026-09-04
8
19
 
9
20
  ### 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.2** (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,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.3 release record](docs/RELEASE-0.6.3.md) for multiline entity-list parsing,
121
+ editable execution summaries, and workbench/composer layout fixes.
120
122
  See [the 0.6.2 release record](docs/RELEASE-0.6.2.md) for Chinese export headers,
121
123
  complete field-selection propagation, and profile-field enrichment.
122
124
  See [the 0.6.1 release record](docs/RELEASE-0.6.1.md) for the Agent-owned QCC workbench,
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.2**(正式版本)
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,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.2 中文导出表头、完整字段选择传递与画像字段补全见
116
+ 0.6.3 多行名单解析、可编辑执行说明与工作台遮挡修复见
117
+ [docs/RELEASE-0.6.3.md](docs/RELEASE-0.6.3.md)。0.6.2 中文导出表头、完整字段选择传递与画像字段补全见
117
118
  [docs/RELEASE-0.6.2.md](docs/RELEASE-0.6.2.md)。0.6.1 Agent-owned QCC 工作台、会话隔离、恢复统计及真实闭环验收见
118
119
  [docs/RELEASE-0.6.1.md](docs/RELEASE-0.6.1.md)。0.6.0 五步 taskId 工作流、耐久制品、
119
120
  跨重启恢复与双基线验收见 [docs/RELEASE-0.6.0.md](docs/RELEASE-0.6.0.md)。
@@ -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/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);
@@ -2491,15 +2619,47 @@ window.__ModuleLoader__.load({
2491
2619
  return completedCommand.run;
2492
2620
  };
2493
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
+
2494
2655
  const runQcc = async () => {
2495
2656
  if (busy || !qccEstimate || !paidConfirmed || !qccEstimate.withinLimit) return;
2496
2657
  actions.setBusy(true);
2497
2658
  actions.setError(null);
2498
2659
  try {
2499
- const current = await workflowAction(actions, activeSessionId, cachedTask, 'match-start');
2500
- const r = await executePreparedQccCommand({
2660
+ await prepareEditableQccCommand({
2501
2661
  kind: 'enrich',
2502
- taskId: current.id,
2662
+ taskId: cachedTask.id,
2503
2663
  rows: runtime.rows,
2504
2664
  headers: runtime.headers,
2505
2665
  nameField,
@@ -2507,11 +2667,7 @@ window.__ModuleLoader__.load({
2507
2667
  includeRisk: false,
2508
2668
  concurrency: 2,
2509
2669
  });
2510
- await applyQccRun(r);
2511
2670
  } catch (err) {
2512
- if (['QCC_NOT_CONNECTED', 'QCC_TOOL_UNAVAILABLE', 'QCC_AUTH_REQUIRED'].includes(err?.code)) {
2513
- try { await workflowAction(actions, activeSessionId, cachedTask, 'authorization-required'); } catch (_workflowError) {}
2514
- }
2515
2671
  actions.setError(err instanceof Error ? err.message : String(err));
2516
2672
  } finally {
2517
2673
  actions.setBusy(false);
@@ -2886,8 +3042,9 @@ window.__ModuleLoader__.load({
2886
3042
  h('input', { type: 'checkbox', checked: paidConfirmed, 'aria-label': '确认使用当前用户的企查查账号额度', onChange: (event) => actions.setPaidConfirmed(event.target.checked) }),
2887
3043
  '我已核对企业数量和调用上界,并确认使用自己连接的企查查 MCP 账号;额度或费用由该账号自行承担',
2888
3044
  ),
3045
+ h('p', { className: 'dcAgentHint' }, '下一步只会把中文任务说明回填到中央对话框。你可先阅读、补充备注,点击发送后才开始企查查调用。'),
2889
3046
  h('div', { className: 'dcAgentRow' },
2890
- 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 ? '生成中…' : '生成可编辑任务说明'),
2891
3048
  ),
2892
3049
  ) : null,
2893
3050
  ),
@@ -2902,10 +3059,10 @@ window.__ModuleLoader__.load({
2902
3059
  Array.isArray(qccRun.rows) && qccRun.rows.length ? h('div', { className: 'dcAgentTableWrap' },
2903
3060
  h('table', { className: 'dcAgentTable', 'aria-label': '匹配补全结果预览' },
2904
3061
  h('thead', null, h('tr', null,
2905
- Object.keys(qccRun.rows[0] || {}).slice(0, 6).map((key) => h('th', { key }, key)),
3062
+ qccPreviewColumns.map((key) => h('th', { key }, resultFieldLabel(key))),
2906
3063
  )),
2907
3064
  h('tbody', null, qccRun.rows.slice(0, 8).map((row, index) => h('tr', { key: `${index}-${row?.qcc_credit_no || row?.credit_no || ''}` },
2908
- 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] ?? ''))),
2909
3066
  ))),
2910
3067
  ),
2911
3068
  h('p', { className: 'dcAgentHint' }, `显示前 ${Math.min(8, qccRun.rows.length)} 行;完整结果在下载阶段生成耐久制品。`),
@@ -3047,26 +3204,29 @@ window.__ModuleLoader__.load({
3047
3204
  h('small', null, cachedTask ? `${WORKFLOW_STATE_LABELS[cachedTask.state] || cachedTask.state} · ${cachedTask.id.slice(0, 18)}` : '正在创建 Host taskId…'),
3048
3205
  ),
3049
3206
  ),
3050
- h('span', {
3051
- className: 'dcAgentQccBadge',
3052
- title: qccRun ? `任务 ${qccRun.runId}` : '仅在当前用户确认使用自己的企查查账号后调用',
3053
- }, qccRun ? `QCC · ${qccRun.state}` : qccCapabilities ? (qccCapabilities.capabilities?.ready ? 'QCC · 已连接' : 'QCC · 能力不完整') : 'QCC · 待检测'),
3054
- h('span', { className: 'dcAgentJobsPill', 'data-state': jobsPill(jobs).state, title: '后台任务状态' }, jobsPill(jobs).label),
3055
- h('button', {
3056
- className: 'dcAgentWbClose',
3057
- type: 'button',
3058
- 'aria-label': expanded ? '收起工作台' : '展开工作台',
3059
- onClick: () => actions.toggleExpanded(),
3060
- }, expanded ? '↘' : '↖'),
3061
- h('button', {
3062
- className: 'dcAgentWbClose',
3063
- type: 'button',
3064
- 'aria-label': '关闭',
3065
- onClick: () => {
3066
- stopJobsPolling();
3067
- actions.close();
3068
- },
3069
- }, '✕'),
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
+ ),
3070
3230
  ),
3071
3231
  h('nav', { className: 'dcAgentStepper', 'aria-label': '清洗流程' },
3072
3232
  STEPS.map((st) => h('button', {
@@ -3114,6 +3274,7 @@ window.__ModuleLoader__.load({
3114
3274
  store: workbenchStore,
3115
3275
  inject: () => ({
3116
3276
  sendSessionCommand: (sessionId, prompt) => sendQccAgentCommand(ctx, sessionId, prompt),
3277
+ setSessionDraft: (sessionId, prompt) => setQccAgentDraft(ctx, sessionId, prompt),
3117
3278
  }),
3118
3279
  }, WorkbenchDrawer));
3119
3280
 
@@ -3182,8 +3343,11 @@ window.__ModuleLoader__.load({
3182
3343
  isKnownCleaningDraft,
3183
3344
  markCleaningSession,
3184
3345
  qualitySummaryFor,
3346
+ plainEntityListDataset,
3347
+ resultFieldLabel,
3185
3348
  rewriteHeroChrome,
3186
3349
  ensureWorkflowTask,
3350
+ ensureEditableWorkflowTask,
3187
3351
  queueWorkflowOperation,
3188
3352
  };
3189
3353
  return module.exports;
@@ -8,7 +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
+ import { fieldLabel, normalizeFieldSelection } from './workflow-contract.js';
12
12
 
13
13
  export const TOOL_QCC_COMMAND = 'data_cleaning_qcc_run';
14
14
 
@@ -70,23 +70,34 @@ function normalizedInput(input = {}) {
70
70
  }
71
71
 
72
72
  export function serializeQccCommandPrompt(command) {
73
- const visible = {
74
- schemaVersion: 1,
75
- commandId: command.commandId,
76
- taskId: command.taskId,
77
- kind: command.kind,
78
- };
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
+ : [];
79
87
  return [
80
- '请执行数据清洗补全企查查任务。',
88
+ '请执行已在「数据清洗补全工作台」确认的企业数据任务。',
81
89
  '',
82
- '类型化任务意图(schemaVersion 1):',
83
- '```json',
84
- JSON.stringify(visible, null, 2),
85
- '```',
90
+ `处理内容:${action}。`,
91
+ `任务编号:${command.taskId}。`,
92
+ `处理数量:${rowCount} 条企业记录。`,
93
+ ...(fields.length ? [`需要补全:${fields.join('')}。`] : []),
94
+ '执行边界:名单、字段和额度确认已安全暂存在本机 Host;使用当前用户自己连接的企查查 MCP 账号。',
86
95
  '',
87
- `请准确调用一次 ${TOOL_QCC_COMMAND} 并只传递 commandId。`,
88
- '企业名单和字段选择已安全暂存在本机 Host,不得要求用户在对话中重复粘贴,不得直接调用任何 mcp__qcc-* 工具。',
89
- '高层工具返回后立即结束本轮;不得重试、扩大名单或追加字段。',
96
+ '你可以在发送前补充任务备注。如需修改名单、匹配规则或补全字段,请先返回工作台调整后重新生成。',
97
+ `安全任务凭证:${command.commandId}`,
98
+ '',
99
+ `发送本说明后,请仅调用一次数据清洗补全执行工具(${TOOL_QCC_COMMAND}),参数只传递上述安全任务凭证。`,
100
+ '不要要求重复粘贴企业名单,不要直接调用 mcp__qcc-* 工具,不要重试、扩大名单或追加字段。',
90
101
  ].join('\n');
91
102
  }
92
103
 
@@ -234,7 +245,7 @@ export class QccCommandStore {
234
245
  export function registerQccCommandTool(tools, commands) {
235
246
  return tools.register({
236
247
  name: TOOL_QCC_COMMAND,
237
- 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.',
238
249
  parameters: {
239
250
  type: 'object',
240
251
  additionalProperties: false,
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-data-cleaning-agent",
3
- "version": "0.6.2",
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",
@@ -34,6 +34,7 @@
34
34
  "docs/RELEASE-0.6.0.md",
35
35
  "docs/RELEASE-0.6.1.md",
36
36
  "docs/RELEASE-0.6.2.md",
37
+ "docs/RELEASE-0.6.3.md",
37
38
  "docs/UI-WORKFLOW-V2.md",
38
39
  "docs/UI-WORKFLOW-V2-MIGRATION.md",
39
40
  "docs/UI-WORKFLOW-V2-ACCEPTANCE.md",