dsh-data-cleaning-agent 0.8.5 → 0.8.7

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
@@ -2,6 +2,25 @@
2
2
 
3
3
  本文件记录 `dsh-data-cleaning-agent` 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [0.8.7] - 2026-09-06
6
+
7
+ ### Changed
8
+ - 中央首页移除大型宣传/流程/最近任务卡片,保留简短业务说明;五个快捷菜单通过自有 Portal 挂在原生输入框外、正下方,不重排共享槽位。
9
+ - 工作台分为当前任务与任务历史;五步统一为导入与核验、规则与体检、主体匹配、字段补全、结果下载。质量报告归入规则阶段,缺少前置条件时显示指引而不启动调用。
10
+ - 提示词生成改为视口居中模态向导,窄屏全屏;正文独立滚动,标题和操作区固定。支持 Escape、Tab 焦点循环、回填后聚焦原生输入框,不自动发送。
11
+ - 任务历史的新建入口统一打开提示词向导;帮助说明折叠收纳于工作台。桌面工作台普通/展开模式为输入框让位,图片放大通过独立 Portal 保持可见。
12
+
13
+ ### Tests
14
+ - 新增菜单 DOM 生命周期与流程前置条件回归;新增隔离真实 React/Chromium 布局脚本,覆盖深浅色及四种窗口尺寸,不调用 QCC。
15
+
16
+ ## [0.8.6] - 2026-09-06
17
+
18
+ ### Fixed
19
+ - 上传/OCR 核验页使用完整原始清单,每页 20 行可翻页,显示序号与全列;不再把前 5 行摘要当成核验视图。
20
+ - 匹配结果共用全量分页和中文字段展示,不再截断为前 8 行/列。
21
+ - 核验阶段支持全量 CSV 和原值 JSON 下载,不触发 QCC 调用;CSV 对公式及长编号作文本保护。明细未完整载入时禁用核验下载。
22
+ - OCR 完成后的引导明确真实核验路径、规则/体检/匹配步骤及自动回填,无需人工复制 dcq- 凭证,不将识别清单误报为匹配结果。
23
+
5
24
  ## [0.8.5] - 2026-09-06
6
25
 
7
26
  ### 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.8.5** (stable release)
5
+ > Current source version / 当前源码版本: **0.8.7** (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)
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.8.5**(正式版本)
5
+ > 当前源码版本 / Current source version: **0.8.7**(正式版本)
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)
@@ -16,6 +16,15 @@ bash <(curl -fsSL https://raw.githubusercontent.com/duhu2000/dsh-data-cleaning-a
16
16
 
17
17
  ## 2. 使用方式
18
18
 
19
+ ### 0.8.7 页面入口与分工
20
+
21
+ - 左侧「数据清洗补全」进入业务会话;中央保留原生对话框和一句说明,不再显示大型工作台介绍卡片。
22
+ - 对话框下方「导入名单 / 质量体检 / 匹配核验 / 字段补全 / 任务历史」是同一工作台的快捷菜单,不是五个独立任务,也不会直接启动企查查调用。
23
+ - 「提示词生成」打开居中向导:录入文字、Excel 或图片 → 匹配规则 → 清洗与补全字段 → 确认描述。可关闭后继续编辑;回填不自动发送。
24
+ - 工作台「当前任务」使用五步导航:导入与核验 → 规则与体检 → 主体匹配 → 字段补全 → 结果下载。质量体检报告位于第二阶段;未满足前置条件会显示明确提示。
25
+ - 「任务历史」管理已有任务、恢复下载,也可新建任务进入向导。原始识别清单下载在第一阶段,最终结果与异常清单下载在第五阶段。
26
+ - 向导正文可独立滚动,底部操作区保持可见;Esc 关闭向导或工作台。桌面工作台与对话区并排,窄屏工作台为覆盖式页面,可关闭返回。
27
+
19
28
  ### 2.1 对话式(推荐)
20
29
 
21
30
  在对话中说:
package/lib/client.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * - `sidebar.footer.action` 仅托管入口生命周期和降级渲染;实际入口 Portal 到
10
10
  * `sidebar.workspaces` 前,位于「新会话」与「工作区」之间。
11
11
  * - DSH 原生会话保持在中间,能力入口使用 `conversation.input.dock` 并通过稳定
12
- * `data-slot` 排到输入框下方;右侧非模态工作台使用 `shell.overlay`。
12
+ * `data-composer-seat/card` 将自有菜单 Portal 到输入框下方;右侧工作台使用 `shell.overlay`。
13
13
  * - 提示词生成器使用 `conversation.input.overlay`,只对本插件创建的会话生效。
14
14
  * Hero 标题没有公开替换槽位,故以可恢复、精确文本匹配的 DOM Bridge 对齐业务首页。
15
15
  *
@@ -111,6 +111,7 @@ window.__ModuleLoader__.load({
111
111
  }
112
112
 
113
113
  .dcAgentWorkbench {
114
+ box-sizing: border-box;
114
115
  display: flex;
115
116
  flex-direction: column;
116
117
  width: min(460px, 42vw);
@@ -170,72 +171,17 @@ window.__ModuleLoader__.load({
170
171
  }
171
172
  .dcAgentCapability svg { flex: 0 0 auto; }
172
173
 
173
- /* DSH 官方 input.dock 默认在 composer 上方;只把本插件自己的 cell 排到下方。 */
174
- [data-slot="conversation.input.dock"]:has(.dcAgentExperience) {
175
- order: 20;
176
- }
174
+ /* 原生 input.dock 保留轻量简介;菜单独立 Portal 到 composer 后,不重排共享 slot。 */
177
175
 
178
176
  .dcAgentExperience {
179
177
  width: 100%;
180
178
  box-sizing: border-box;
181
179
  }
180
+ .dcAgentCapabilityMount { width: 100%; padding: 8px 0; flex: none; }
181
+ .dcAgentHomeSummary { margin: 0 20px 8px; text-align: center; color: light-dark(#697386, #aab5c7); font-size: 13px; line-height: 20px; }
182
+ .dcAgentPromptBackdrop { position: fixed; inset: 0; z-index: 2000; display: grid; place-items: center; padding: 16px; box-sizing: border-box; background: rgba(8, 15, 28, .36); pointer-events: auto; }
183
+ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
182
184
 
183
- .dcAgentProductHome {
184
- width: 100%;
185
- max-width: 980px;
186
- box-sizing: border-box;
187
- margin: 10px auto 0;
188
- padding: 16px 18px;
189
- border: 1px solid light-dark(#e4e8f0, #303947);
190
- border-radius: 16px;
191
- background: light-dark(rgba(255,255,255,.82), rgba(20,25,33,.86));
192
- box-shadow: light-dark(0 10px 30px rgba(42,75,120,.06), 0 14px 34px rgba(0,0,0,.14));
193
- }
194
- .dcAgentProductHome h2 {
195
- margin: 0;
196
- color: light-dark(#172033, #edf2fa);
197
- font-size: 16px;
198
- line-height: 24px;
199
- }
200
- .dcAgentProductHome > p {
201
- margin: 4px 0 12px;
202
- color: light-dark(#697386, #aab5c7);
203
- font-size: 12px;
204
- line-height: 18px;
205
- }
206
- .dcAgentHomeFlow {
207
- display: grid;
208
- grid-template-columns: repeat(7, minmax(0, 1fr));
209
- gap: 8px;
210
- }
211
- .dcAgentHomeStep {
212
- min-width: 0;
213
- padding: 9px 8px;
214
- border-radius: 10px;
215
- background: light-dark(#f4f7fc, #1c2330);
216
- color: light-dark(#33415a, #c9d4e5);
217
- font-size: 12px;
218
- line-height: 18px;
219
- }
220
- .dcAgentHomeStep b {
221
- display: block;
222
- margin-bottom: 2px;
223
- color: light-dark(#1556cf, #8cb3ff);
224
- font-size: 11px;
225
- }
226
- .dcAgentHomeTrust {
227
- display: flex;
228
- flex-wrap: wrap;
229
- gap: 6px;
230
- margin-top: 10px;
231
- }
232
- .dcAgentHomeTrust span {
233
- padding: 3px 8px;
234
- border: 1px solid light-dark(#dce4f1, #344052);
235
- border-radius: 999px;
236
- color: light-dark(#59677c, #b1bdd0);
237
- font-size: 11px;
238
- }
239
185
 
240
186
  /* input.overlay 是 DSH 官方浮层锚点;给触发器让出输入卡片顶部空间。 */
241
187
  [data-composer-card]:has(.dcAgentPromptTrigger) {
@@ -263,13 +209,13 @@ window.__ModuleLoader__.load({
263
209
  pointer-events: auto;
264
210
  }
265
211
  .dcAgentPromptPanel {
266
- position: absolute;
267
- top: 44px;
268
- left: 16px;
269
- width: min(660px, calc(100vw - 120px));
270
- max-height: min(68vh, 660px);
212
+ position: relative;
213
+ display: flex;
214
+ flex-direction: column;
215
+ width: min(860px, 100%);
216
+ max-height: calc(100dvh - 32px);
271
217
  box-sizing: border-box;
272
- overflow: auto;
218
+ overflow: hidden;
273
219
  padding: 16px;
274
220
  border: 1px solid light-dark(#d5dae4, #303947);
275
221
  border-radius: 16px;
@@ -278,6 +224,9 @@ window.__ModuleLoader__.load({
278
224
  box-shadow: light-dark(0 18px 48px rgba(33,55,88,.20), 0 20px 54px rgba(0,0,0,.40));
279
225
  pointer-events: auto;
280
226
  }
227
+ .dcAgentPromptBody { min-height: 0; overflow: auto; overscroll-behavior: contain; padding: 0 2px; }
228
+ .dcAgentPromptHead, .dcAgentPromptActions { flex: none; }
229
+ .dcAgentPromptActions { border-top: 1px solid light-dark(#e4e8f0, #303947); padding-top: 12px; }
281
230
  .dcAgentPromptHead {
282
231
  display: flex;
283
232
  align-items: flex-start;
@@ -402,7 +351,7 @@ window.__ModuleLoader__.load({
402
351
  .dcAgentImageLightbox {
403
352
  position: fixed;
404
353
  inset: 0;
405
- z-index: 1400;
354
+ z-index: 2100;
406
355
  display: grid;
407
356
  place-items: center;
408
357
  padding: 32px;
@@ -766,18 +715,7 @@ window.__ModuleLoader__.load({
766
715
  line-height: 1.6;
767
716
  color: light-dark(#33415a, #c9d4e5);
768
717
  }
769
- .dcAgentHomeHero { display: flex; align-items: center; justify-content: space-between; gap: 18px; }
770
- .dcAgentHomeHero p { margin: 5px 0 0; color: light-dark(#697386, #aab5c7); font-size: 12px; line-height: 18px; }
771
- .dcAgentHomeEyebrow { display: block; margin-bottom: 4px; color: light-dark(#2869e6, #8cb3ff); font-size: 11px; font-weight: 700; letter-spacing: .08em; }
772
- .dcAgentHomeActions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
773
- .dcAgentHomeActions button,
774
- .dcAgentHomeRecent button,
775
718
  .dcAgentHistoryTask { border: 1px solid light-dark(#dce4f1, #344052); border-radius: 9px; background: light-dark(#fff, #1b222e); color: inherit; font: inherit; cursor: pointer; }
776
- .dcAgentHomeActions button { padding: 6px 9px; font-size: 11px; }
777
- .dcAgentHomeRecent { display: grid; gap: 6px; margin-top: 12px; }
778
- .dcAgentHomeRecent > b { font-size: 12px; }
779
- .dcAgentHomeRecent button { display: flex; justify-content: space-between; gap: 12px; padding: 7px 9px; text-align: left; font-size: 11px; }
780
- .dcAgentHomeRecent small { color: light-dark(#697386, #aab5c7); }
781
719
  .dcAgentWizardNav { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-bottom: 12px; }
782
720
  .dcAgentWizardStep { display: flex; align-items: center; gap: 6px; padding: 7px; border: 1px solid light-dark(#d9dee8, #394352); border-radius: 9px; background: transparent; color: inherit; font: inherit; font-size: 11px; cursor: pointer; }
783
721
  .dcAgentWizardStep b { display: grid; place-items: center; width: 20px; height: 20px; border-radius: 50%; background: light-dark(#eef1f6, #252e3b); color: light-dark(#607088, #b9c6da); }
@@ -830,6 +768,12 @@ window.__ModuleLoader__.load({
830
768
  padding-right: min(420px, 48vw);
831
769
  }
832
770
  }
771
+ @media (min-width: 761px) {
772
+ body:has(.dcAgentWorkbench.is-expanded) [data-conversation-scroll] {
773
+ box-sizing: border-box;
774
+ padding-right: min(980px, calc(100vw - 420px));
775
+ }
776
+ }
833
777
  @media (max-width: 760px) {
834
778
  .dcAgentWorkbench, .dcAgentWorkbench.is-expanded { width: 100vw; max-width: none; min-width: 0; border-radius: 0; }
835
779
  .dcAgentOverlay { background: rgba(8, 10, 14, 0.22); pointer-events: auto; }
@@ -838,9 +782,8 @@ window.__ModuleLoader__.load({
838
782
  .dcAgentCandidate { grid-template-columns: 1fr; }
839
783
  .dcAgentCapabilities { justify-content: flex-start; padding-inline: 12px; }
840
784
  .dcAgentCapability { min-width: 92px; }
841
- .dcAgentPromptPanel { left: 8px; width: calc(100vw - 88px); max-height: 72vh; }
842
- .dcAgentHomeFlow { grid-template-columns: repeat(2, minmax(0, 1fr)); }
843
- .dcAgentHomeHero { align-items: flex-start; flex-direction: column; }
785
+ .dcAgentPromptBackdrop { padding: 0; }
786
+ .dcAgentPromptPanel { width: 100%; height: 100dvh; max-height: 100dvh; border-radius: 0; }
844
787
  .dcAgentWizardNav { grid-template-columns: repeat(2, 1fr); }
845
788
  .dcAgentRulesGrid { grid-template-columns: 1fr; }
846
789
  .dcAgentMappingRow { grid-template-columns: 1fr; }
@@ -887,6 +830,7 @@ window.__ModuleLoader__.load({
887
830
  if (!from || !toId || String(fromId) === String(toId)) return runtimeFor(toId);
888
831
  const target = runtimeFor(toId);
889
832
  target.rows = from.rows;
833
+ target.sourceRows = from.sourceRows;
890
834
  target.headers = from.headers;
891
835
  target.source = from.source;
892
836
  target.lastCsv = from.lastCsv;
@@ -896,15 +840,15 @@ window.__ModuleLoader__.load({
896
840
  }
897
841
 
898
842
  const STEPS = [
899
- { key: 'upload', label: '上传数据', icon: '📄' },
900
- { key: 'rules', label: '规则确认', icon: '' },
901
- { key: 'match', label: '数据匹配', icon: '🔎' },
902
- { key: 'enrich', label: '清洗补全', icon: '' },
903
- { key: 'download', label: '下载数据', icon: '⬇️' },
843
+ { key: 'upload', label: '导入与核验', icon: '1' },
844
+ { key: 'rules', label: '规则与体检', icon: '2' },
845
+ { key: 'match', label: '主体匹配', icon: '3' },
846
+ { key: 'enrich', label: '字段补全', icon: '4' },
847
+ { key: 'download', label: '结果下载', icon: '5' },
904
848
  ];
905
849
 
906
850
  const CAPABILITIES = [
907
- { key: 'upload', label: '上传清洗', icon: IconPaperclipOutline16, fallback: '+' },
851
+ { key: 'upload', label: '导入名单', icon: IconPaperclipOutline16, fallback: '+' },
908
852
  { key: 'profile', label: '质量体检', icon: IconChecklistOutline14, fallback: '✓' },
909
853
  { key: 'match', label: '匹配核验', icon: IconSearchOutline16, fallback: '⌕' },
910
854
  { key: 'enrich', label: '字段补全', icon: IconDataOutline16, fallback: '▦' },
@@ -1800,7 +1744,7 @@ window.__ModuleLoader__.load({
1800
1744
  }, launcher), topMount);
1801
1745
  }
1802
1746
 
1803
- /** Mockup 五能力入口:由 input.dock 承载,并在 CSS 中排到原生 composer 下方。 */
1747
+ /** 快捷菜单只切换同一工作台视图,由自有 Portal 挂载在原生 composer 下方。 */
1804
1748
  function CapabilityBar(props) {
1805
1749
  const { sessionId } = props;
1806
1750
  return h('div', { className: 'dcAgentCapabilities', 'aria-label': '数据清洗补全智能体能力' },
@@ -1825,66 +1769,109 @@ window.__ModuleLoader__.load({
1825
1769
  authorization_required: '待连接企查查', cancelled: '已取消', parse_failed: '解析失败',
1826
1770
  };
1827
1771
 
1828
- function ProductHome(props = {}) {
1829
- const { sessionId } = props;
1830
- const [recentTasks, setRecentTasks] = react.useState([]);
1831
- const flow = [
1832
- ['01', '任务设置', '提示词向导明确目标'],
1833
- ['02', '上传数据', '文本、Excel、JSON 或图片'],
1834
- ['03', '规则确认', '字段映射与匹配主键'],
1835
- ['04', '质量体检', '缺失、重复与格式诊断'],
1836
- ['05', '数据匹配', '精确优先,多候选复核'],
1837
- ['06', '清洗补全', '按需选择当前企业字段'],
1838
- ['07', '下载数据', '保留原值、依据与状态'],
1839
- ];
1772
+ function ProductHome() {
1773
+ return h('p', { className: 'dcAgentHomeSummary' },
1774
+ '导入企业名单或图片,核验主体、补全字段,并导出可追溯结果。');
1775
+ }
1776
+
1777
+ function workflowNavigationIssue(step, task, hasData, profile, qccRun) {
1778
+ if (['upload', 'history'].includes(step)) return null;
1779
+ if (step === 'download' && task?.artifacts?.length) return null;
1780
+ if (!hasData) return '请先在「导入与核验」载入并核对完整名单。';
1781
+ if (step === 'rules') return null;
1782
+ if (!task || ['draft', 'uploaded'].includes(task.state)) return '请先在「规则与体检」确认字段映射和处理规则。';
1783
+ if (step === 'profile') return null;
1784
+ if (!profile && task.state === 'rules_confirmed') return '请先生成质量体检报告,再推进主体匹配。';
1785
+ const requiresQcc = Array.isArray(task.objectives)
1786
+ ? task.objectives.some((key) => ['validate_identity', 'complete_fields'].includes(key))
1787
+ : Boolean(task.fieldSelection?.length);
1788
+ if (step === 'enrich' && requiresQcc && !['matched', 'enriching', 'export_ready', 'completed', 'partial'].includes(task.state)) {
1789
+ return '请先在「主体匹配」确认企业;多候选需要人工核验。';
1790
+ }
1791
+ if (step === 'download' && !['export_ready', 'completed', 'partial'].includes(task.state) && !['completed', 'partial'].includes(qccRun?.state)) {
1792
+ return '请先完成清洗或字段补全。原始清单可在「导入与核验」下载。';
1793
+ }
1794
+ return null;
1795
+ }
1796
+
1797
+ // 只新增自己的 Portal 容器,不移动 DSH/其他插件拥有的 DOM。
1798
+ function installCapabilityMount(marker, onMount) {
1799
+ const owned = new Set();
1800
+ const sync = () => {
1801
+ const seat = marker?.closest?.('[data-composer-seat]');
1802
+ const card = seat?.querySelector?.('[data-composer-card]');
1803
+ if (!card) { onMount(null); return; }
1804
+ let branch = card;
1805
+ while (branch.parentElement && branch.parentElement !== seat && !branch.parentElement.contains(marker)) {
1806
+ branch = branch.parentElement;
1807
+ }
1808
+ const parent = branch.parentElement;
1809
+ if (!parent?.contains(marker) || branch.contains(marker)) { onMount(null); return; }
1810
+ let mount = [...owned].find((node) => node.parentElement === parent);
1811
+ if (!mount) {
1812
+ for (const node of owned) node.remove();
1813
+ owned.clear();
1814
+ mount = document.createElement('div');
1815
+ mount.className = 'dcAgentCapabilityMount';
1816
+ owned.add(mount);
1817
+ }
1818
+ if (branch.nextSibling !== mount) parent.insertBefore(mount, branch.nextSibling);
1819
+ onMount(mount);
1820
+ };
1821
+ sync();
1822
+ const observer = typeof MutationObserver === 'function' ? new MutationObserver(sync) : null;
1823
+ observer?.observe(marker.closest('[data-composer-seat]') || marker, { childList: true, subtree: true });
1824
+ return () => { observer?.disconnect(); for (const node of owned) node.remove(); };
1825
+ }
1826
+
1827
+ function PromptDialog({ sessionId, onClose, children }) {
1840
1828
  react.useEffect(() => {
1841
- let disposed = false;
1842
- requestJson('/data-cleaning/api/workflow/tasks').then((response) => {
1843
- if (!disposed) setRecentTasks((response.tasks || []).slice(0, 3));
1844
- }).catch(() => {});
1845
- return () => { disposed = true; };
1829
+ const panel = [...(document.querySelectorAll?.('.dcAgentPromptPanel') || [])]
1830
+ .find((node) => node.dataset.sessionId === sessionId);
1831
+ if (!panel) return undefined;
1832
+ const previous = document.activeElement;
1833
+ const focusable = () => [...panel.querySelectorAll('button:not(:disabled), input:not(:disabled), textarea:not(:disabled), select:not(:disabled), [tabindex="0"]')]
1834
+ .filter((node) => node.getClientRects().length);
1835
+ (focusable()[0] || panel).focus();
1836
+ const keydown = (event) => {
1837
+ if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); onClose(); }
1838
+ if (event.key !== 'Tab') return;
1839
+ const nodes = focusable();
1840
+ const first = nodes[0] || panel, last = nodes[nodes.length - 1] || panel;
1841
+ if (event.shiftKey && (document.activeElement === first || document.activeElement === panel)) {
1842
+ event.preventDefault(); last.focus();
1843
+ } else if (!event.shiftKey && (document.activeElement === last || !panel.contains(document.activeElement))) {
1844
+ event.preventDefault(); first.focus();
1845
+ }
1846
+ };
1847
+ panel.addEventListener('keydown', keydown);
1848
+ return () => {
1849
+ panel.removeEventListener('keydown', keydown);
1850
+ if (panel.contains(document.activeElement) && previous?.isConnected) previous.focus();
1851
+ };
1846
1852
  }, [sessionId]);
1847
- return h('section', { className: 'dcAgentProductHome', 'aria-label': '数据清洗补全产品介绍' },
1848
- h('div', { className: 'dcAgentHomeHero' },
1849
- h('div', null,
1850
- h('span', { className: 'dcAgentHomeEyebrow' }, '企业主数据治理工作台'),
1851
- h('h2', null, '把企业名单变成可核验、可回写的标准数据'),
1852
- h('p', null, '面向销售线索、客户尽调、供应商管理与 CRM / ERP 数据治理,先确认任务和规则,再按需使用当前用户连接的企查查 MCP。'),
1853
- ),
1854
- h('button', { type: 'button', className: 'dcAgentButton is-primary', onClick: () => requestWorkbenchOpen('upload', sessionId) }, '开始新任务'),
1855
- ),
1856
- h('div', { className: 'dcAgentHomeFlow', 'aria-label': '数据清洗补全工作流' },
1857
- flow.map(([index, title, hint]) => h('div', { className: 'dcAgentHomeStep', key: index },
1858
- h('b', null, index),
1859
- h('span', null, title),
1860
- h('small', { style: { display: 'block', opacity: .72 } }, hint),
1861
- )),
1862
- ),
1863
- h('div', { className: 'dcAgentHomeTrust' },
1864
- h('span', null, '本地解析与确定性清洗'),
1865
- h('span', null, '匹配候选人工确认'),
1866
- h('span', null, '客户自带企查查连接与额度'),
1867
- h('span', null, '结果与待复核清单可导出'),
1868
- ),
1869
- h('div', { className: 'dcAgentHomeActions' },
1870
- h('button', { type: 'button', onClick: () => requestWorkbenchOpen('profile', sessionId) }, '🩺 查看质量体检'),
1871
- h('button', { type: 'button', onClick: () => requestWorkbenchOpen('match', sessionId) }, '🔎 进入匹配核验'),
1872
- h('button', { type: 'button', onClick: () => requestWorkbenchOpen('history', sessionId) }, '◷ 查看任务历史'),
1873
- ),
1874
- recentTasks.length ? h('div', { className: 'dcAgentHomeRecent', 'aria-label': '最近任务' },
1875
- h('b', null, '最近任务'),
1876
- recentTasks.map((task) => h('button', {
1877
- key: task.id,
1878
- type: 'button',
1879
- onClick: () => requestWorkbenchOpen(task.stage || 'upload', sessionId, task),
1880
- },
1881
- h('span', null, task.title || '未命名任务'),
1882
- h('small', null, WORKFLOW_STATE_LABELS[task.state] || task.state),
1883
- )),
1884
- ) : null,
1885
- );
1853
+ const dialog = h('div', { className: 'dcAgentPromptBackdrop' },
1854
+ h('section', { className: 'dcAgentPromptPanel', role: 'dialog', 'aria-modal': 'true',
1855
+ 'aria-label': '数据清洗补全任务生成器', 'data-session-id': sessionId, tabIndex: -1 }, children));
1856
+ return document.body && typeof reactDom.createPortal === 'function'
1857
+ ? reactDom.createPortal(dialog, document.body) : dialog;
1858
+ }
1859
+
1860
+ function ImagePreview({ src, alt, onClose, sessionId }) {
1861
+ react.useEffect(() => () => {
1862
+ const panel = [...(document.querySelectorAll?.('.dcAgentPromptPanel') || [])]
1863
+ .find((node) => node.dataset.sessionId === sessionId);
1864
+ panel?.querySelector('.dcAgentPromptImageThumb')?.focus();
1865
+ }, [sessionId]);
1866
+ const view = h('button', { type: 'button', className: 'dcAgentImageLightbox',
1867
+ 'aria-label': '关闭图片预览', onClick: onClose, autoFocus: true,
1868
+ onKeyDown: (event) => {
1869
+ if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); onClose(); }
1870
+ if (event.key === 'Tab') event.preventDefault();
1871
+ },
1872
+ }, h('img', { src, alt }));
1873
+ return document.body && typeof reactDom.createPortal === 'function' ? reactDom.createPortal(view, document.body) : view;
1886
1874
  }
1887
-
1888
1875
  /**
1889
1876
  * DSH 当前只公开 hero brand.mark,没有 headline slot。这里仅在本插件会话且 blank hero
1890
1877
  * 阶段替换标题,并在卸载时恢复,避免污染普通会话。
@@ -1978,6 +1965,13 @@ window.__ModuleLoader__.load({
1978
1965
  const { sessionId, session: ownerSession } = props;
1979
1966
  const enabled = useCleaningSession(sessionId);
1980
1967
  const hero = ownerSession?.composerPhase === 'blank';
1968
+ const [menuMount, setMenuMount] = react.useState(null);
1969
+ react.useEffect(() => {
1970
+ if (!enabled) return undefined;
1971
+ const marker = [...document.querySelectorAll('.dcAgentExperience')]
1972
+ .find((node) => node.dataset.sessionId === sessionId);
1973
+ return marker ? installCapabilityMount(marker, (mount) => setMenuMount((current) => current === mount ? current : mount)) : undefined;
1974
+ }, [enabled, sessionId, hero]);
1981
1975
  react.useEffect(
1982
1976
  () => rewriteHeroChrome(sessionId, enabled && hero),
1983
1977
  [sessionId, enabled, hero, ownerSession?.openState]
@@ -1987,7 +1981,7 @@ window.__ModuleLoader__.load({
1987
1981
  className: `dcAgentExperience${hero ? ' is-home' : ''}`,
1988
1982
  'data-session-id': sessionId,
1989
1983
  },
1990
- h(CapabilityBar, { sessionId }),
1984
+ menuMount ? reactDom.createPortal(h(CapabilityBar, { sessionId }), menuMount) : h(CapabilityBar, { sessionId }),
1991
1985
  hero ? h(ProductHome, { sessionId }) : null,
1992
1986
  );
1993
1987
  }
@@ -2275,6 +2269,12 @@ window.__ModuleLoader__.load({
2275
2269
  }, sessionId);
2276
2270
  setError(null);
2277
2271
  setOpen(false);
2272
+ setTimeout(() => {
2273
+ if (typeof document === 'undefined' || !isCleaningSession(sessionId)) return;
2274
+ const marker = [...(document.querySelectorAll?.('.dcAgentExperience') || [])]
2275
+ .find((node) => node.dataset.sessionId === sessionId);
2276
+ marker?.closest?.('[data-composer-seat]')?.querySelector?.('textarea, [contenteditable="true"], [role="textbox"]')?.focus();
2277
+ }, 0);
2278
2278
  setWizardStep(1);
2279
2279
  };
2280
2280
 
@@ -2331,6 +2331,16 @@ window.__ModuleLoader__.load({
2331
2331
  // 右侧工作台共用此图片入口;仅由当前会话确认接收,不把图片交给表格解析器。
2332
2332
  react.useEffect(() => {
2333
2333
  if (!enabled) return undefined;
2334
+ const receiveNewTask = (event) => {
2335
+ if (event.detail?.sessionId !== sessionId || busy || imageCommand?.state === 'running') return;
2336
+ event.preventDefault();
2337
+ discardCurrentImage();
2338
+ setMode('text'); setRawText(''); setEntries([]); setEntryCount(0); setSpreadsheetFileName('');
2339
+ setCleaningKeys(DEFAULT_CLEANING_KEYS); setEnrichmentKeys(DEFAULT_ENRICHMENT_KEYS);
2340
+ setAnchorKeys(['company_name', 'credit_no']);
2341
+ setPromptMatchRules({ normalizeNames: true, preferCreditNo: true, deduplicate: true, manualReviewAmbiguous: true });
2342
+ setWizardStep(1); setOpen(true); setError(null);
2343
+ };
2334
2344
  const receiveImage = (event) => {
2335
2345
  if (event.detail?.sessionId !== sessionId || busy || imageCommand?.state === 'running') return;
2336
2346
  event.preventDefault();
@@ -2341,7 +2351,11 @@ window.__ModuleLoader__.load({
2341
2351
  if (event.detail.file) acceptImageFiles([event.detail.file]);
2342
2352
  };
2343
2353
  document.addEventListener('dsh:data-cleaning-prompt-image', receiveImage);
2344
- return () => document.removeEventListener('dsh:data-cleaning-prompt-image', receiveImage);
2354
+ document.addEventListener('dsh:data-cleaning-prompt-new', receiveNewTask);
2355
+ return () => {
2356
+ document.removeEventListener('dsh:data-cleaning-prompt-image', receiveImage);
2357
+ document.removeEventListener('dsh:data-cleaning-prompt-new', receiveNewTask);
2358
+ };
2345
2359
  }, [enabled, sessionId, busy, imageCommand, imageAttachment]);
2346
2360
 
2347
2361
  if (!enabled) return null;
@@ -2375,18 +2389,7 @@ window.__ModuleLoader__.load({
2375
2389
  'aria-expanded': open,
2376
2390
  onClick: () => setOpen(!open),
2377
2391
  }, '✨ 提示词生成'),
2378
- open ? h('section', {
2379
- className: 'dcAgentPromptPanel',
2380
- role: 'dialog',
2381
- 'aria-modal': 'false',
2382
- 'aria-label': '数据清洗补全任务生成器',
2383
- onKeyDown: (event) => {
2384
- if (event.key === 'Escape') {
2385
- event.preventDefault();
2386
- setOpen(false);
2387
- }
2388
- },
2389
- },
2392
+ open ? h(PromptDialog, { sessionId, onClose: () => setOpen(false) },
2390
2393
  h('div', { className: 'dcAgentPromptHead' },
2391
2394
  h('div', null,
2392
2395
  h('h3', null, '生成数据清洗补全任务'),
@@ -2394,6 +2397,7 @@ window.__ModuleLoader__.load({
2394
2397
  ),
2395
2398
  h('button', { type: 'button', className: 'dcAgentPromptClose', 'aria-label': '关闭提示词生成', onClick: () => setOpen(false) }, '✕'),
2396
2399
  ),
2400
+ h('div', { className: 'dcAgentPromptBody' },
2397
2401
  h('nav', { className: 'dcAgentWizardNav', 'aria-label': '任务设置步骤' },
2398
2402
  [['1', '数据来源'], ['2', '匹配规则'], ['3', '清洗与补全'], ['4', '确认描述']].map(([index, label]) => h('button', {
2399
2403
  key: index,
@@ -2521,6 +2525,7 @@ window.__ModuleLoader__.load({
2521
2525
  ? h('p', { className: 'dcAgentPromptNote', role: 'status' }, `图片已载入,可继续选择规则和字段;暂存有效期 15 分钟。${imageCommand.providerIssue.message}`)
2522
2526
  : null,
2523
2527
  error ? h('p', { className: 'dcAgentPromptError', role: 'alert' }, error) : null,
2528
+ ),
2524
2529
  h('div', { className: 'dcAgentPromptActions' },
2525
2530
  h('button', { type: 'button', className: 'dcAgentPromptAction', disabled: wizardStep === 1, onClick: () => setWizardStep(Math.max(1, wizardStep - 1)) }, '上一步'),
2526
2531
  h('button', { type: 'button', className: 'dcAgentPromptAction is-primary', disabled: busy || imageCommand?.state === 'running', onClick: nextWizard }, busy
@@ -2530,11 +2535,9 @@ window.__ModuleLoader__.load({
2530
2535
  : wizardStep === 4 ? '回填到对话框' : '下一步'),
2531
2536
  ),
2532
2537
  ) : null,
2533
- imageLightbox && imageAttachment?.previewUrl ? h('button', {
2534
- type: 'button', className: 'dcAgentImageLightbox',
2535
- 'aria-label': '关闭图片预览',
2536
- onClick: () => setImageLightbox(false),
2537
- }, h('img', { src: imageAttachment.previewUrl, alt: imageFileName })) : null,
2538
+ imageLightbox && imageAttachment?.previewUrl ? h(ImagePreview, {
2539
+ src: imageAttachment.previewUrl, alt: imageFileName, sessionId, onClose: () => setImageLightbox(false),
2540
+ }) : null,
2538
2541
  );
2539
2542
  }
2540
2543
 
@@ -2681,6 +2684,7 @@ window.__ModuleLoader__.load({
2681
2684
  function applyParsed(result, actions, taskId = 'unassigned', source = {}) {
2682
2685
  const runtime = runtimeFor(taskId);
2683
2686
  runtime.rows = Array.isArray(result.rows) ? result.rows : [];
2687
+ runtime.sourceRows = runtime.rows;
2684
2688
  runtime.headers = Array.isArray(result.headers) ? result.headers : [];
2685
2689
  runtime.source = {
2686
2690
  type: source.type || result.fmt || 'csv',
@@ -2712,6 +2716,7 @@ window.__ModuleLoader__.load({
2712
2716
  // 都以本次解析结果覆盖目标 runtime,避免复用已完成任务的一行旧数据。
2713
2717
  const target = runtimeFor(current.id);
2714
2718
  target.rows = Array.isArray(result.rows) ? result.rows : [];
2719
+ target.sourceRows = target.rows;
2715
2720
  target.headers = Array.isArray(result.headers) ? result.headers : [];
2716
2721
  target.source = {
2717
2722
  type: source.type || result.fmt || 'csv',
@@ -2733,6 +2738,67 @@ window.__ModuleLoader__.load({
2733
2738
  return current;
2734
2739
  }
2735
2740
 
2741
+ function reviewColumns(rows, headers = []) {
2742
+ return [...new Set([...headers, ...rows.flatMap((row) => Object.keys(row || {}))])];
2743
+ }
2744
+
2745
+ function reviewCsv(rows, headers) {
2746
+ const cell = (value) => {
2747
+ let text = value == null ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value);
2748
+ // 保留长编号的文本形式,阻止外部 OCR/企业字段成为 Excel 公式。
2749
+ if (/^[\s]*[=+@-]/.test(text) || /^\d{15,}$/.test(text) || /^0\d+/.test(text)) text = "'" + text;
2750
+ return '"' + text.replace(/"/g, '""') + '"';
2751
+ };
2752
+ return '\uFEFF' + [headers.map((key) => cell(resultFieldLabel(key))).join(','),
2753
+ ...rows.map((row) => headers.map((key) => cell(row?.[key])).join(','))].join('\r\n');
2754
+ }
2755
+
2756
+ /** 页内查看和核验下载均使用完整 rows,不使用 store 的五行摘要。 */
2757
+ function DatasetReview({ rows, headers, title, expectedCount, onError }) {
2758
+ const [page, setPage] = react.useState(0);
2759
+ const pageSize = 20;
2760
+ const total = rows.length;
2761
+ const columns = reviewColumns(rows, headers);
2762
+ const currentPage = Math.min(page, Math.max(0, Math.ceil(total / pageSize) - 1));
2763
+ const start = currentPage * pageSize;
2764
+ const download = (format) => {
2765
+ try {
2766
+ const content = format === 'csv' ? reviewCsv(rows, columns) : JSON.stringify(rows, null, 2);
2767
+ const url = URL.createObjectURL(new Blob([content], { type: format === 'csv' ? 'text/csv;charset=utf-8' : 'application/json;charset=utf-8' }));
2768
+ const anchor = document.createElement('a');
2769
+ anchor.href = url;
2770
+ anchor.download = title + '.' + format;
2771
+ try { anchor.click(); } finally { setTimeout(() => URL.revokeObjectURL(url), 1000); }
2772
+ } catch (error) { onError?.(error instanceof Error ? error.message : String(error)); }
2773
+ };
2774
+ const complete = expectedCount == null || expectedCount === total;
2775
+ return h('section', { className: 'dcAgentSection', 'aria-label': title },
2776
+ h('h3', null, title),
2777
+ h('p', { className: 'dcAgentHint', role: 'status' }, complete
2778
+ ? `共 ${total} 条,当前显示 ${total ? start + 1 : 0}–${Math.min(start + pageSize, total)} 条;共 ${columns.length} 列,可横向滚动。`
2779
+ : `完整清单尚未就绪:预期 ${expectedCount} 条,当前载入 ${total} 条。请等待同步;刷新后若仍缺失,请重新上传源文件。`),
2780
+ h('div', { className: 'dcAgentPreviewTable', style: { overflow: 'auto', maxHeight: '420px' } },
2781
+ h('table', { className: 'dcAgentTable' },
2782
+ h('thead', null, h('tr', null, h('th', null, '序号'), columns.map((key) => h('th', { key }, resultFieldLabel(key))))),
2783
+ h('tbody', null, rows.slice(start, start + pageSize).map((row, index) => h('tr', { key: start + index },
2784
+ h('td', null, String(start + index + 1)),
2785
+ columns.map((key) => h('td', { key }, row?.[key] == null ? '' : typeof row[key] === 'object' ? JSON.stringify(row[key]) : String(row[key]))),
2786
+ ))),
2787
+ ),
2788
+ ),
2789
+ h('div', { className: 'dcAgentRow' },
2790
+ h('button', { type: 'button', className: 'dcAgentButton', disabled: currentPage === 0, onClick: () => setPage(currentPage - 1) }, '上一页'),
2791
+ h('span', null, `第 ${currentPage + 1} / ${Math.max(1, Math.ceil(total / pageSize))} 页`),
2792
+ h('button', { type: 'button', className: 'dcAgentButton', disabled: start + pageSize >= total, onClick: () => setPage(currentPage + 1) }, '下一页'),
2793
+ ),
2794
+ h('div', { className: 'dcAgentRow' },
2795
+ h('button', { type: 'button', className: 'dcAgentButton', disabled: !complete || !total, onClick: () => download('csv') }, `下载全部 ${total} 条 CSV`),
2796
+ h('button', { type: 'button', className: 'dcAgentButton', disabled: !complete || !total, onClick: () => download('json') }, '下载原值 JSON'),
2797
+ ),
2798
+ h('p', { className: 'dcAgentHint' }, '下载包含全部行,不受分页影响。CSV 对公式及长编号作文本保护;JSON 保留原值。仅导出当前清单,不触发企查查调用。'),
2799
+ );
2800
+ }
2801
+
2736
2802
  /** 右侧非模态工作台:中央区域始终保留 DSH 原生会话。 */
2737
2803
  function WorkbenchDrawer(props) {
2738
2804
  const { useStore, actions } = props;
@@ -2888,10 +2954,9 @@ window.__ModuleLoader__.load({
2888
2954
  const cachedTask = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? workflowTask;
2889
2955
  const runtimeKey = cachedTask?.id ?? `session:${activeSessionId || 'unassigned'}`;
2890
2956
  const runtime = runtimeFor(runtimeKey);
2957
+ const stagedSource = runtimeFor(`session:${activeSessionId || 'unassigned'}`, false);
2958
+ const sourceRows = stagedSource?.sourceRows ?? runtime.sourceRows ?? runtime.rows;
2891
2959
  const lastCsv = runtime.lastCsv;
2892
- const qccPreviewColumns = Array.isArray(qccRun?.rows) && qccRun.rows.length
2893
- ? Object.keys(qccRun.rows[0] || {}).slice(0, 8)
2894
- : [];
2895
2960
  const fieldByPattern = (pattern) => runtime.headers.find((field) => pattern.test(field)) ?? null;
2896
2961
  const phoneField = fieldByPattern(/^(phone|mobile|tel|telephone|联系电话|手机号码|手机号)$/i);
2897
2962
  const amountField = fieldByPattern(/^(amount|price|金额|注册资本)$/i);
@@ -3315,10 +3380,18 @@ window.__ModuleLoader__.load({
3315
3380
  };
3316
3381
 
3317
3382
  const startNewWorkflow = () => {
3383
+ const event = document.createEvent('CustomEvent');
3384
+ event.initCustomEvent('dsh:data-cleaning-prompt-new', false, true, { sessionId: activeSessionId });
3385
+ if (document.dispatchEvent(event)) {
3386
+ actions.setError('当前会话的任务向导尚未就绪或正在解析图片,请稍后重试。');
3387
+ return;
3388
+ }
3318
3389
  if (cachedTask?.id) runtimeTasks.delete(String(cachedTask.id));
3390
+ runtimeTasks.delete(`session:${activeSessionId || 'unassigned'}`);
3319
3391
  runtimeTasks.delete('unassigned');
3320
3392
  workflowTaskBySession.delete(String(activeSessionId || 'unassigned'));
3321
3393
  actions.resetWorkflow();
3394
+ actions.close();
3322
3395
  };
3323
3396
 
3324
3397
  const exportRows = () => runtime.resultRows.qcc
@@ -3604,17 +3677,11 @@ window.__ModuleLoader__.load({
3604
3677
  stat('未匹配', qccRun.summary?.unresolved ?? 0, (qccRun.summary?.unresolved ?? 0) > 0 ? 'warn' : null),
3605
3678
  stat('失败', qccRun.summary?.failed ?? 0, (qccRun.summary?.failed ?? 0) > 0 ? 'bad' : null),
3606
3679
  ),
3607
- Array.isArray(qccRun.rows) && qccRun.rows.length ? h('div', { className: 'dcAgentTableWrap' },
3608
- h('table', { className: 'dcAgentTable', 'aria-label': '匹配补全结果预览' },
3609
- h('thead', null, h('tr', null,
3610
- qccPreviewColumns.map((key) => h('th', { key }, resultFieldLabel(key))),
3611
- )),
3612
- h('tbody', null, qccRun.rows.slice(0, 8).map((row, index) => h('tr', { key: `${index}-${row?.qcc_credit_no || row?.credit_no || ''}` },
3613
- qccPreviewColumns.map((key) => h('td', { key }, String(row?.[key] ?? ''))),
3614
- ))),
3615
- ),
3616
- h('p', { className: 'dcAgentHint' }, `显示前 ${Math.min(8, qccRun.rows.length)} 行;完整结果在下载阶段生成耐久制品。`),
3617
- ) : null,
3680
+ Array.isArray(qccRun.rows) && qccRun.rows.length ? h(DatasetReview, {
3681
+ key: qccRun.runId, rows: qccRun.rows, headers: runtime.headers,
3682
+ title: '企查查匹配补全结果', expectedCount: qccRun.summary?.totalRows,
3683
+ onError: actions.setError,
3684
+ }) : null,
3618
3685
  (qccRun.reviewQueue || []).map((item) => h('div', { key: item.companyName, className: 'dcAgentSection' },
3619
3686
  h('h3', null, `待核验:${item.companyName}`),
3620
3687
  item.candidates.map((candidate) => h('div', { key: candidate.creditNo, className: 'dcAgentCandidate' },
@@ -3716,15 +3783,16 @@ window.__ModuleLoader__.load({
3716
3783
  (dataset.headers || []).slice(0, 12).map((name) => h('span', { key: name, className: 'dcAgentChip' }, name)),
3717
3784
  (dataset.headers || []).length > 12 ? h('span', { className: 'dcAgentChip' }, `+${dataset.headers.length - 12} 列`) : null,
3718
3785
  ) : null,
3719
- dataset ? h('div', { className: 'dcAgentPreviewTable', 'aria-label': '数据预览' },
3720
- h('table', { className: 'dcAgentTable' },
3721
- h('thead', null, h('tr', null, (dataset.headers || []).slice(0, 8).map((name) => h('th', { key: name }, name)))),
3722
- h('tbody', null, (dataset.preview || []).slice(0, 5).map((row, index) => h('tr', { key: index }, (dataset.headers || []).slice(0, 8).map((name) => h('td', { key: name }, String(row?.[name] ?? '')))))),
3723
- ),
3724
- ) : null,
3786
+ dataset ? h(DatasetReview, {
3787
+ key: runtimeKey, rows: sourceRows, headers: dataset.headers || [],
3788
+ expectedCount: dataset.rowCount,
3789
+ title: runtime.source?.type === 'image' ? '图片识别原始名单(尚未匹配)' : '导入原始清单',
3790
+ onError: actions.setError,
3791
+ }) : null,
3792
+ hasData ? h('p', { className: 'dcAgentHint' }, '请核对完整名单。识别清单不代表已完成企查查匹配。核对后进入「规则与体检」,确认规则生成质量体检;再进入「主体匹配」估算调用量并确认账号额度,点击「生成可编辑任务说明」自动回填中央对话框,无需手工复制任务凭证。') : null,
3725
3793
  h('div', { className: 'dcAgentRow' },
3726
3794
  h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy, 'aria-label': '解析数据', onClick: handleParse }, busy ? '解析中…' : '解析数据'),
3727
- hasData ? h('button', { type: 'button', className: 'dcAgentButton', onClick: () => actions.setStep('rules') }, '下一步:字段映射与规则') : null,
3795
+ hasData ? h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || sourceRows.length !== dataset.rowCount, onClick: () => actions.setStep('rules') }, '已核对清单,下一步:字段映射与规则') : null,
3728
3796
  ),
3729
3797
  );
3730
3798
  }
@@ -3785,19 +3853,39 @@ window.__ModuleLoader__.load({
3785
3853
  }, '✕'),
3786
3854
  ),
3787
3855
  ),
3788
- h('nav', { className: 'dcAgentStepper', 'aria-label': '清洗流程' },
3856
+ h('nav', { className: 'dcAgentRow', style: { padding: '10px 16px' }, 'aria-label': '工作台管理' },
3857
+ h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step !== 'history',
3858
+ onClick: () => actions.setStep(cachedTask?.stage || 'upload') }, '当前任务'),
3859
+ h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step === 'history',
3860
+ onClick: () => actions.setStep('history') }, '任务历史'),
3861
+ ),
3862
+ step !== 'history' ? h('nav', { className: 'dcAgentStepper', 'aria-label': '清洗流程' },
3789
3863
  STEPS.map((st) => h('button', {
3790
3864
  key: st.key,
3791
3865
  type: 'button',
3792
- className: `dcAgentStep${step === st.key ? ' is-active' : ''}`,
3866
+ className: `dcAgentStep${(step === 'profile' ? 'rules' : step) === st.key ? ' is-active' : ''}`,
3793
3867
  'aria-label': st.label,
3794
- 'aria-current': step === st.key ? 'step' : undefined,
3868
+ 'aria-current': (step === 'profile' ? 'rules' : step) === st.key ? 'step' : undefined,
3795
3869
  onClick: () => actions.setStep(st.key),
3796
3870
  }, `${st.icon} ${st.label}`)),
3797
- ),
3871
+ ) : null,
3798
3872
  h('div', { className: 'dcAgentWbBody' },
3799
3873
  error ? h('div', { className: 'dcAgentError', role: 'alert' }, error) : null,
3800
- pane,
3874
+ step !== 'history' ? h('details', { className: 'dcAgentSection' },
3875
+ h('summary', null, '使用说明与任务流程'),
3876
+ h('p', { className: 'dcAgentHint' }, '导入与核验 → 规则与体检 → 主体匹配 → 字段补全 → 结果下载。菜单只切换视图,不启动调用;按页面主按钮逐步确认。'),
3877
+ h('p', { className: 'dcAgentHint' }, '支持文本、Excel、JSON 和图片名单。原值保留、多候选人工核验;企查查调用使用当前用户自己的连接与额度。'),
3878
+ ) : null,
3879
+ ['rules', 'profile'].includes(step) ? h('nav', { className: 'dcAgentRow', 'aria-label': '规则与体检视图' },
3880
+ h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step === 'rules', onClick: () => actions.setStep('rules') }, '字段映射与规则'),
3881
+ h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step === 'profile', onClick: () => actions.setStep('profile') }, '质量体检报告'),
3882
+ ) : null,
3883
+ workflowNavigationIssue(step, cachedTask, hasData, profile, qccRun)
3884
+ ? h('section', { className: 'dcAgentSection', role: 'status' },
3885
+ h('p', null, workflowNavigationIssue(step, cachedTask, hasData, profile, qccRun)),
3886
+ h('button', { type: 'button', className: 'dcAgentButton',
3887
+ onClick: () => actions.setStep(!hasData ? 'upload' : !cachedTask || ['draft', 'uploaded'].includes(cachedTask.state) ? 'rules' : cachedTask.state === 'rules_confirmed' ? 'profile' : step === 'download' ? 'enrich' : 'match') }, '前往前置步骤'))
3888
+ : pane,
3801
3889
  ),
3802
3890
  ),
3803
3891
  );
@@ -3844,7 +3932,7 @@ window.__ModuleLoader__.load({
3844
3932
  inject: () => ({ startSession: () => startCleaningSession(ctx) }),
3845
3933
  }, SidebarEntry));
3846
3934
 
3847
- // 独立 dock 行:对齐 Mockup 的五个能力 ICON,CSS 只把本 cell 排到输入框下方。
3935
+ // dock 托管会话生命周期;菜单 Portal 到输入框后,不重排共享槽位。
3848
3936
  ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
3849
3937
  name: 'conversation.input.dock',
3850
3938
  id: 'data-cleaning-agent-capabilities',
@@ -3896,7 +3984,14 @@ window.__ModuleLoader__.load({
3896
3984
  exports.inject = inject;
3897
3985
  // 测试用纯函数,不构成 Host / DSH 稳定 API。
3898
3986
  exports.__testing = {
3987
+ installCapabilityMount,
3988
+ PromptDialog,
3989
+ ProductHome,
3990
+ workflowNavigationIssue,
3899
3991
  buildTaskPrompt,
3992
+ DatasetReview,
3993
+ reviewColumns,
3994
+ reviewCsv,
3900
3995
  clearCleaningDraft,
3901
3996
  entriesToDataset,
3902
3997
  extractPromptEntries,
@@ -580,7 +580,7 @@ export function registerImageIntakeTool(tools, store) {
580
580
  },
581
581
  render: (_args, value) => [{
582
582
  type: 'text',
583
- text: `企查查智能文档解析已识别图片企业名单:${value.entryCount} 条,已同步回数据清洗补全工作台等待核验。`,
583
+ text: `企查查智能文档解析已识别图片企业名单:${value.entryCount} 条,已同步回数据清洗补全工作台等待核验。请在「导入与核验」查看原始清单全部行或下载全量 CSV/JSON,核对后进入「规则与体检」确认规则并生成质量体检,再到「主体匹配」估算调用量并确认账号额度,点击「生成可编辑任务说明」自动回填中央对话框。无需手工复制 dcq- 凭证或重新粘贴名单;识别名单不代表已匹配,尚未查询时不要编造匹配摘要。`,
584
584
  }],
585
585
  },
586
586
  async execute(args, exec) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-data-cleaning-agent",
3
- "version": "0.8.5",
3
+ "version": "0.8.7",
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",
@@ -45,6 +45,7 @@
45
45
  "docs/G5-E2E-RUNBOOK.md"
46
46
  ],
47
47
  "scripts": {
48
+ "test:ui": "node scripts/ui-layout-regression.mjs",
48
49
  "test": "node --test",
49
50
  "lint": "node --check lib/index.js && node --check lib/engine.js && node --check lib/artifacts.js && node --check lib/image-intake.js && node --check lib/tools.js && node --check lib/skill.js && node --check lib/qcc-field-catalog.js && node --check lib/qcc-phase2.js && node --check lib/qcc-phase3.js && node --check lib/qcc-phase3-batch.js && node --check lib/qcc-phase2-acceptance.js && node --check lib/skill-enrich.js && node --check lib/jobs.js && node --check lib/workflow-contract.js && node --check lib/workflow.js && node --check lib/qcc-safety.js && node --check lib/qcc.js && node --check lib/qcc-command.js && node --check lib/qcc-runs.js && node --check lib/web.js && node --check lib/client.js && node --check scripts/check-readme-version.mjs && node --check scripts/check-market-registration.mjs && node --check scripts/g5-e2e.mjs && node --check scripts/phase3-e2e.mjs && node --check scripts/phase2-acceptance.mjs",
50
51
  "docs:check": "node scripts/check-readme-version.mjs",