dsh-data-cleaning-agent 0.8.6 → 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,17 @@
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
+
5
16
  ## [0.8.6] - 2026-09-06
6
17
 
7
18
  ### Fixed
package/README.en.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > A data cleaning & completion agent plugin for DeepSeek Harness: local CSV/XLSX/JSON engine plus optional Qichacha (QCC) MCP enterprise-data enrichment. Initiated and maintained by the Qichacha (QCC) team.
4
4
  >
5
- > Current source version / 当前源码版本: **0.8.6** (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.6**(正式版本)
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; }
@@ -897,15 +840,15 @@ window.__ModuleLoader__.load({
897
840
  }
898
841
 
899
842
  const STEPS = [
900
- { key: 'upload', label: '上传数据', icon: '📄' },
901
- { key: 'rules', label: '规则确认', icon: '' },
902
- { key: 'match', label: '数据匹配', icon: '🔎' },
903
- { key: 'enrich', label: '清洗补全', icon: '' },
904
- { 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' },
905
848
  ];
906
849
 
907
850
  const CAPABILITIES = [
908
- { key: 'upload', label: '上传清洗', icon: IconPaperclipOutline16, fallback: '+' },
851
+ { key: 'upload', label: '导入名单', icon: IconPaperclipOutline16, fallback: '+' },
909
852
  { key: 'profile', label: '质量体检', icon: IconChecklistOutline14, fallback: '✓' },
910
853
  { key: 'match', label: '匹配核验', icon: IconSearchOutline16, fallback: '⌕' },
911
854
  { key: 'enrich', label: '字段补全', icon: IconDataOutline16, fallback: '▦' },
@@ -1801,7 +1744,7 @@ window.__ModuleLoader__.load({
1801
1744
  }, launcher), topMount);
1802
1745
  }
1803
1746
 
1804
- /** Mockup 五能力入口:由 input.dock 承载,并在 CSS 中排到原生 composer 下方。 */
1747
+ /** 快捷菜单只切换同一工作台视图,由自有 Portal 挂载在原生 composer 下方。 */
1805
1748
  function CapabilityBar(props) {
1806
1749
  const { sessionId } = props;
1807
1750
  return h('div', { className: 'dcAgentCapabilities', 'aria-label': '数据清洗补全智能体能力' },
@@ -1826,66 +1769,109 @@ window.__ModuleLoader__.load({
1826
1769
  authorization_required: '待连接企查查', cancelled: '已取消', parse_failed: '解析失败',
1827
1770
  };
1828
1771
 
1829
- function ProductHome(props = {}) {
1830
- const { sessionId } = props;
1831
- const [recentTasks, setRecentTasks] = react.useState([]);
1832
- const flow = [
1833
- ['01', '任务设置', '提示词向导明确目标'],
1834
- ['02', '上传数据', '文本、Excel、JSON 或图片'],
1835
- ['03', '规则确认', '字段映射与匹配主键'],
1836
- ['04', '质量体检', '缺失、重复与格式诊断'],
1837
- ['05', '数据匹配', '精确优先,多候选复核'],
1838
- ['06', '清洗补全', '按需选择当前企业字段'],
1839
- ['07', '下载数据', '保留原值、依据与状态'],
1840
- ];
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 }) {
1841
1828
  react.useEffect(() => {
1842
- let disposed = false;
1843
- requestJson('/data-cleaning/api/workflow/tasks').then((response) => {
1844
- if (!disposed) setRecentTasks((response.tasks || []).slice(0, 3));
1845
- }).catch(() => {});
1846
- 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
+ };
1847
1852
  }, [sessionId]);
1848
- return h('section', { className: 'dcAgentProductHome', 'aria-label': '数据清洗补全产品介绍' },
1849
- h('div', { className: 'dcAgentHomeHero' },
1850
- h('div', null,
1851
- h('span', { className: 'dcAgentHomeEyebrow' }, '企业主数据治理工作台'),
1852
- h('h2', null, '把企业名单变成可核验、可回写的标准数据'),
1853
- h('p', null, '面向销售线索、客户尽调、供应商管理与 CRM / ERP 数据治理,先确认任务和规则,再按需使用当前用户连接的企查查 MCP。'),
1854
- ),
1855
- h('button', { type: 'button', className: 'dcAgentButton is-primary', onClick: () => requestWorkbenchOpen('upload', sessionId) }, '开始新任务'),
1856
- ),
1857
- h('div', { className: 'dcAgentHomeFlow', 'aria-label': '数据清洗补全工作流' },
1858
- flow.map(([index, title, hint]) => h('div', { className: 'dcAgentHomeStep', key: index },
1859
- h('b', null, index),
1860
- h('span', null, title),
1861
- h('small', { style: { display: 'block', opacity: .72 } }, hint),
1862
- )),
1863
- ),
1864
- h('div', { className: 'dcAgentHomeTrust' },
1865
- h('span', null, '本地解析与确定性清洗'),
1866
- h('span', null, '匹配候选人工确认'),
1867
- h('span', null, '客户自带企查查连接与额度'),
1868
- h('span', null, '结果与待复核清单可导出'),
1869
- ),
1870
- h('div', { className: 'dcAgentHomeActions' },
1871
- h('button', { type: 'button', onClick: () => requestWorkbenchOpen('profile', sessionId) }, '🩺 查看质量体检'),
1872
- h('button', { type: 'button', onClick: () => requestWorkbenchOpen('match', sessionId) }, '🔎 进入匹配核验'),
1873
- h('button', { type: 'button', onClick: () => requestWorkbenchOpen('history', sessionId) }, '◷ 查看任务历史'),
1874
- ),
1875
- recentTasks.length ? h('div', { className: 'dcAgentHomeRecent', 'aria-label': '最近任务' },
1876
- h('b', null, '最近任务'),
1877
- recentTasks.map((task) => h('button', {
1878
- key: task.id,
1879
- type: 'button',
1880
- onClick: () => requestWorkbenchOpen(task.stage || 'upload', sessionId, task),
1881
- },
1882
- h('span', null, task.title || '未命名任务'),
1883
- h('small', null, WORKFLOW_STATE_LABELS[task.state] || task.state),
1884
- )),
1885
- ) : null,
1886
- );
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;
1887
1874
  }
1888
-
1889
1875
  /**
1890
1876
  * DSH 当前只公开 hero brand.mark,没有 headline slot。这里仅在本插件会话且 blank hero
1891
1877
  * 阶段替换标题,并在卸载时恢复,避免污染普通会话。
@@ -1979,6 +1965,13 @@ window.__ModuleLoader__.load({
1979
1965
  const { sessionId, session: ownerSession } = props;
1980
1966
  const enabled = useCleaningSession(sessionId);
1981
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]);
1982
1975
  react.useEffect(
1983
1976
  () => rewriteHeroChrome(sessionId, enabled && hero),
1984
1977
  [sessionId, enabled, hero, ownerSession?.openState]
@@ -1988,7 +1981,7 @@ window.__ModuleLoader__.load({
1988
1981
  className: `dcAgentExperience${hero ? ' is-home' : ''}`,
1989
1982
  'data-session-id': sessionId,
1990
1983
  },
1991
- h(CapabilityBar, { sessionId }),
1984
+ menuMount ? reactDom.createPortal(h(CapabilityBar, { sessionId }), menuMount) : h(CapabilityBar, { sessionId }),
1992
1985
  hero ? h(ProductHome, { sessionId }) : null,
1993
1986
  );
1994
1987
  }
@@ -2276,6 +2269,12 @@ window.__ModuleLoader__.load({
2276
2269
  }, sessionId);
2277
2270
  setError(null);
2278
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);
2279
2278
  setWizardStep(1);
2280
2279
  };
2281
2280
 
@@ -2332,6 +2331,16 @@ window.__ModuleLoader__.load({
2332
2331
  // 右侧工作台共用此图片入口;仅由当前会话确认接收,不把图片交给表格解析器。
2333
2332
  react.useEffect(() => {
2334
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
+ };
2335
2344
  const receiveImage = (event) => {
2336
2345
  if (event.detail?.sessionId !== sessionId || busy || imageCommand?.state === 'running') return;
2337
2346
  event.preventDefault();
@@ -2342,7 +2351,11 @@ window.__ModuleLoader__.load({
2342
2351
  if (event.detail.file) acceptImageFiles([event.detail.file]);
2343
2352
  };
2344
2353
  document.addEventListener('dsh:data-cleaning-prompt-image', receiveImage);
2345
- 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
+ };
2346
2359
  }, [enabled, sessionId, busy, imageCommand, imageAttachment]);
2347
2360
 
2348
2361
  if (!enabled) return null;
@@ -2376,18 +2389,7 @@ window.__ModuleLoader__.load({
2376
2389
  'aria-expanded': open,
2377
2390
  onClick: () => setOpen(!open),
2378
2391
  }, '✨ 提示词生成'),
2379
- open ? h('section', {
2380
- className: 'dcAgentPromptPanel',
2381
- role: 'dialog',
2382
- 'aria-modal': 'false',
2383
- 'aria-label': '数据清洗补全任务生成器',
2384
- onKeyDown: (event) => {
2385
- if (event.key === 'Escape') {
2386
- event.preventDefault();
2387
- setOpen(false);
2388
- }
2389
- },
2390
- },
2392
+ open ? h(PromptDialog, { sessionId, onClose: () => setOpen(false) },
2391
2393
  h('div', { className: 'dcAgentPromptHead' },
2392
2394
  h('div', null,
2393
2395
  h('h3', null, '生成数据清洗补全任务'),
@@ -2395,6 +2397,7 @@ window.__ModuleLoader__.load({
2395
2397
  ),
2396
2398
  h('button', { type: 'button', className: 'dcAgentPromptClose', 'aria-label': '关闭提示词生成', onClick: () => setOpen(false) }, '✕'),
2397
2399
  ),
2400
+ h('div', { className: 'dcAgentPromptBody' },
2398
2401
  h('nav', { className: 'dcAgentWizardNav', 'aria-label': '任务设置步骤' },
2399
2402
  [['1', '数据来源'], ['2', '匹配规则'], ['3', '清洗与补全'], ['4', '确认描述']].map(([index, label]) => h('button', {
2400
2403
  key: index,
@@ -2522,6 +2525,7 @@ window.__ModuleLoader__.load({
2522
2525
  ? h('p', { className: 'dcAgentPromptNote', role: 'status' }, `图片已载入,可继续选择规则和字段;暂存有效期 15 分钟。${imageCommand.providerIssue.message}`)
2523
2526
  : null,
2524
2527
  error ? h('p', { className: 'dcAgentPromptError', role: 'alert' }, error) : null,
2528
+ ),
2525
2529
  h('div', { className: 'dcAgentPromptActions' },
2526
2530
  h('button', { type: 'button', className: 'dcAgentPromptAction', disabled: wizardStep === 1, onClick: () => setWizardStep(Math.max(1, wizardStep - 1)) }, '上一步'),
2527
2531
  h('button', { type: 'button', className: 'dcAgentPromptAction is-primary', disabled: busy || imageCommand?.state === 'running', onClick: nextWizard }, busy
@@ -2531,11 +2535,9 @@ window.__ModuleLoader__.load({
2531
2535
  : wizardStep === 4 ? '回填到对话框' : '下一步'),
2532
2536
  ),
2533
2537
  ) : null,
2534
- imageLightbox && imageAttachment?.previewUrl ? h('button', {
2535
- type: 'button', className: 'dcAgentImageLightbox',
2536
- 'aria-label': '关闭图片预览',
2537
- onClick: () => setImageLightbox(false),
2538
- }, 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,
2539
2541
  );
2540
2542
  }
2541
2543
 
@@ -3378,10 +3380,18 @@ window.__ModuleLoader__.load({
3378
3380
  };
3379
3381
 
3380
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
+ }
3381
3389
  if (cachedTask?.id) runtimeTasks.delete(String(cachedTask.id));
3390
+ runtimeTasks.delete(`session:${activeSessionId || 'unassigned'}`);
3382
3391
  runtimeTasks.delete('unassigned');
3383
3392
  workflowTaskBySession.delete(String(activeSessionId || 'unassigned'));
3384
3393
  actions.resetWorkflow();
3394
+ actions.close();
3385
3395
  };
3386
3396
 
3387
3397
  const exportRows = () => runtime.resultRows.qcc
@@ -3779,7 +3789,7 @@ window.__ModuleLoader__.load({
3779
3789
  title: runtime.source?.type === 'image' ? '图片识别原始名单(尚未匹配)' : '导入原始清单',
3780
3790
  onError: actions.setError,
3781
3791
  }) : null,
3782
- hasData ? h('p', { className: 'dcAgentHint' }, '请核对完整名单。识别清单不代表已完成企查查匹配。核对后进入「字段映射与规则」,确认规则生成质量体检;再进入「数据匹配」估算调用量并确认账号额度,点击「生成可编辑任务说明」自动回填中央对话框,无需手工复制任务凭证。') : null,
3792
+ hasData ? h('p', { className: 'dcAgentHint' }, '请核对完整名单。识别清单不代表已完成企查查匹配。核对后进入「规则与体检」,确认规则生成质量体检;再进入「主体匹配」估算调用量并确认账号额度,点击「生成可编辑任务说明」自动回填中央对话框,无需手工复制任务凭证。') : null,
3783
3793
  h('div', { className: 'dcAgentRow' },
3784
3794
  h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy, 'aria-label': '解析数据', onClick: handleParse }, busy ? '解析中…' : '解析数据'),
3785
3795
  hasData ? h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || sourceRows.length !== dataset.rowCount, onClick: () => actions.setStep('rules') }, '已核对清单,下一步:字段映射与规则') : null,
@@ -3843,19 +3853,39 @@ window.__ModuleLoader__.load({
3843
3853
  }, '✕'),
3844
3854
  ),
3845
3855
  ),
3846
- 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': '清洗流程' },
3847
3863
  STEPS.map((st) => h('button', {
3848
3864
  key: st.key,
3849
3865
  type: 'button',
3850
- className: `dcAgentStep${step === st.key ? ' is-active' : ''}`,
3866
+ className: `dcAgentStep${(step === 'profile' ? 'rules' : step) === st.key ? ' is-active' : ''}`,
3851
3867
  'aria-label': st.label,
3852
- 'aria-current': step === st.key ? 'step' : undefined,
3868
+ 'aria-current': (step === 'profile' ? 'rules' : step) === st.key ? 'step' : undefined,
3853
3869
  onClick: () => actions.setStep(st.key),
3854
3870
  }, `${st.icon} ${st.label}`)),
3855
- ),
3871
+ ) : null,
3856
3872
  h('div', { className: 'dcAgentWbBody' },
3857
3873
  error ? h('div', { className: 'dcAgentError', role: 'alert' }, error) : null,
3858
- 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,
3859
3889
  ),
3860
3890
  ),
3861
3891
  );
@@ -3902,7 +3932,7 @@ window.__ModuleLoader__.load({
3902
3932
  inject: () => ({ startSession: () => startCleaningSession(ctx) }),
3903
3933
  }, SidebarEntry));
3904
3934
 
3905
- // 独立 dock 行:对齐 Mockup 的五个能力 ICON,CSS 只把本 cell 排到输入框下方。
3935
+ // dock 托管会话生命周期;菜单 Portal 到输入框后,不重排共享槽位。
3906
3936
  ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({
3907
3937
  name: 'conversation.input.dock',
3908
3938
  id: 'data-cleaning-agent-capabilities',
@@ -3954,6 +3984,10 @@ window.__ModuleLoader__.load({
3954
3984
  exports.inject = inject;
3955
3985
  // 测试用纯函数,不构成 Host / DSH 稳定 API。
3956
3986
  exports.__testing = {
3987
+ installCapabilityMount,
3988
+ PromptDialog,
3989
+ ProductHome,
3990
+ workflowNavigationIssue,
3957
3991
  buildTaskPrompt,
3958
3992
  DatasetReview,
3959
3993
  reviewColumns,
@@ -580,7 +580,7 @@ export function registerImageIntakeTool(tools, store) {
580
580
  },
581
581
  render: (_args, value) => [{
582
582
  type: 'text',
583
- text: `企查查智能文档解析已识别图片企业名单:${value.entryCount} 条,已同步回数据清洗补全工作台等待核验。请在「上传数据」查看原始清单全部行或下载全量 CSV/JSON,核对后进入「字段映射与规则」确认规则并生成质量体检,再到「数据匹配」估算调用量并确认账号额度,点击「生成可编辑任务说明」自动回填中央对话框。无需手工复制 dcq- 凭证或重新粘贴名单;识别名单不代表已匹配,尚未查询时不要编造匹配摘要。`,
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.6",
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",