dsh-data-cleaning-agent 0.8.3 → 0.8.4

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,16 @@
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.4] - 2026-09-06
6
+
7
+ ### Fixed
8
+ - 向导数据来源页在文本模式也接管图片粘贴,避免 Modlens 图片路径被当成企业名称;文本名单明确拒绝本地图片路径。
9
+ - 右侧上传允许 PNG/JPEG/WebP,并通过同会话事件桥转交图片向导;接收失败保留工作台并说明原因。
10
+ - 回填前检查原生输入机提交状态,回填后核对草稿;输入机未接收时保留向导并提示,不强制启用发送按钮。
11
+ - 移除通过全局捕获点击和按钮文案推测导航的逻辑,改为订阅真实会话选择,目标打开失败时保留清洗首页、工作台和草稿。
12
+ - 兼容无回调参数的 Store 通知;成功退出时只清理插件默认文案,保留用户自写草稿。
13
+ - 新清洗会话在准备和选择成功后才激活,避免提前撤销旧会话归属。
14
+
5
15
  ## [0.8.3] - 2026-09-06
6
16
 
7
17
  ### 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.3** (stable release)
5
+ > Current source version / 当前源码版本: **0.8.4** (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.3**(正式版本)
5
+ > 当前源码版本 / Current source version: **0.8.4**(正式版本)
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/lib/client.js CHANGED
@@ -1105,9 +1105,8 @@ window.__ModuleLoader__.load({
1105
1105
  }
1106
1106
 
1107
1107
  /**
1108
- * 清洗补全是一个显式进入的业务子系统。切换会话、点击新会话或点击侧栏其它业务
1109
- * 入口时立即撤销其激活态,即使 DSH 复用了同一个空白 sessionId 也不会残留首页、
1110
- * 提示词、能力栏或工作台。点击本插件自己的顶部入口不会提前撤销。
1108
+ * 以实际会话选择提交作为退出条件;点击其它入口但创建失败时,保留清洗首页、
1109
+ * 面板和用户草稿。新版本入口始终创建独立会话,不再依赖 DOM 文案推测导航。
1111
1110
  */
1112
1111
  function installSessionOwnershipBridge(ctx) {
1113
1112
  let staleDraftObserver = null;
@@ -1124,26 +1123,9 @@ window.__ModuleLoader__.load({
1124
1123
  };
1125
1124
  const leaveSubsystem = () => {
1126
1125
  const sessionId = activeCleaningSessionId;
1127
- if (sessionId) clearCleaningDraft(ctx, sessionId);
1126
+ if (sessionId) clearCleaningDraft(ctx, sessionId, true);
1128
1127
  deactivateCleaningSession(sessionId);
1129
1128
  };
1130
- const handleSidebarNavigation = (event) => {
1131
- if (!activeCleaningSessionId) return;
1132
- const target = event?.target;
1133
- if (!target || typeof target.closest !== 'function') return;
1134
- if (target.closest('.dcAgentLauncher, .dcAgentExperience, .dcAgentPromptPopover, .dcAgentWorkbench, .dcAgentHeaderAction')) return;
1135
-
1136
- const actionable = target.closest('button, a, [role="button"]');
1137
- if (!actionable) return;
1138
- const workspaceSlot = document.querySelector?.(SIDEBAR_WORKSPACES_SELECTOR);
1139
- const sidebarRoot = workspaceSlot?.closest?.('aside, nav, [data-slot="sidebar"], [class*="sidebar"]')
1140
- ?? workspaceSlot?.parentElement;
1141
- const label = `${actionable.getAttribute?.('aria-label') ?? ''} ${actionable.textContent ?? ''}`.trim();
1142
- const sessionOrAgentEntry = /新建?会话|新会话|new\s+session|智能体|agent|尽调/i.test(label);
1143
- if (sidebarRoot?.contains?.(actionable) || sessionOrAgentEntry) leaveSubsystem();
1144
- };
1145
-
1146
- document.addEventListener?.('click', handleSidebarNavigation, true);
1147
1129
  // 升级 / 刷新后不恢复子系统所有权;仅清理由旧版本遗留且仍未被用户编辑的默认文案。
1148
1130
  clearStaleDefaultDraft();
1149
1131
  // DSH 会在插件 apply() 之后异步恢复 composer draft。在首屏稳定期内
@@ -1163,15 +1145,17 @@ window.__ModuleLoader__.load({
1163
1145
  }, 10_000);
1164
1146
  }
1165
1147
  const unsubscribe = ctx.sessions?.list?.subscribe?.((snapshot) => {
1166
- const nextSessionId = snapshot?.current;
1167
- if (activeCleaningSessionId && nextSessionId && nextSessionId !== activeCleaningSessionId) {
1148
+ // Store subscriptions may signal without a payload; read authoritative state.
1149
+ const selected = ctx.sessions?.list?.getSnapshot?.() ?? snapshot;
1150
+ if (!selected) return;
1151
+ const nextSessionId = selected.current;
1152
+ if (activeCleaningSessionId && nextSessionId !== activeCleaningSessionId) {
1168
1153
  leaveSubsystem();
1169
1154
  } else if (!activeCleaningSessionId) {
1170
1155
  clearStaleDefaultDraft();
1171
1156
  }
1172
1157
  });
1173
1158
  return () => {
1174
- document.removeEventListener?.('click', handleSidebarNavigation, true);
1175
1159
  staleDraftObserver?.disconnect();
1176
1160
  if (staleDraftObserverTimer !== null) clearTimeout(staleDraftObserverTimer);
1177
1161
  if (typeof unsubscribe === 'function') unsubscribe();
@@ -1602,9 +1586,9 @@ window.__ModuleLoader__.load({
1602
1586
  }
1603
1587
  const conversation = typeof ctx.get === 'function' ? ctx.get('conversation') : ctx.conversation;
1604
1588
  if (!conversation) throw new Error('DSH 对话服务尚未就绪,请稍后重试');
1605
- markCleaningSession(sessionId);
1606
1589
  conversation.input.shell(sessionId).setDraft(DEFAULT_SESSION_PROMPT);
1607
1590
  ctx.sessions.open(sessionId);
1591
+ markCleaningSession(sessionId);
1608
1592
  return sessionId;
1609
1593
  }
1610
1594
 
@@ -1993,6 +1977,18 @@ window.__ModuleLoader__.load({
1993
1977
  .filter(Boolean);
1994
1978
  }
1995
1979
 
1980
+ function isImagePathEntry(value) {
1981
+ return /^(?:file:\/\/|\/|[a-z]:[\\/])/i.test(String(value).trim().replace(/^["']/, ''))
1982
+ && /\.(?:png|jpe?g|webp|gif|heic|bmp)(?:["']|\s)*$/i.test(String(value));
1983
+ }
1984
+
1985
+ function requestPromptImage(sessionId, file = null) {
1986
+ if (!sessionId || typeof document.createEvent !== 'function') return false;
1987
+ const event = document.createEvent('CustomEvent');
1988
+ event.initCustomEvent('dsh:data-cleaning-prompt-image', false, true, { sessionId, file });
1989
+ return !document.dispatchEvent(event);
1990
+ }
1991
+
1996
1992
  async function imageFileBase64(file) {
1997
1993
  if (!file || typeof file.arrayBuffer !== 'function') throw new Error('图片文件不可读。');
1998
1994
  const bytes = new Uint8Array(await file.arrayBuffer());
@@ -2016,7 +2012,7 @@ window.__ModuleLoader__.load({
2016
2012
  }
2017
2013
 
2018
2014
  function PromptGenerator(props) {
2019
- const { sessionId, inputActions, attachImages, removeImage } = props;
2015
+ const { sessionId, inputActions, attachImages, removeImage, getInputSnapshot } = props;
2020
2016
  const enabled = useCleaningSession(sessionId);
2021
2017
  const [open, setOpen] = react.useState(false);
2022
2018
  const [wizardStep, setWizardStep] = react.useState(1);
@@ -2177,6 +2173,7 @@ window.__ModuleLoader__.load({
2177
2173
  const validateSource = () => {
2178
2174
  const values = selectedEntries();
2179
2175
  if (mode === 'text' && !values.length) return '请先录入企业名称或统一社会信用代码,每行一条。';
2176
+ if (mode === 'text' && values.some(isImagePathEntry)) return '检测到图片文件路径,不是企业名单。请切换「上传图片识别」,重新选择或粘贴图片。';
2180
2177
  if (mode === 'excel' && !values.length) return '请先上传并解析 Excel / CSV / JSON 文件。';
2181
2178
  if (mode === 'image' && !imageCommand) return '请先选择、粘贴或拖入一张包含企业名单的图片。';
2182
2179
  if (mode === 'image' && imageCommand?.state === 'failed') return imageCommand.error?.message || '图片识别失败,请重新选择图片。';
@@ -2205,13 +2202,26 @@ window.__ModuleLoader__.load({
2205
2202
  setError('当前会话输入机尚未就绪,请稍后重试。');
2206
2203
  return;
2207
2204
  }
2205
+ const inputSnapshot = getInputSnapshot?.(sessionId);
2206
+ if (['adjudicating', 'submitting'].includes(inputSnapshot?.phase)) {
2207
+ setError('当前对话正在提交,请等待完成后再回填。图片与字段选择已保留。');
2208
+ return;
2209
+ }
2208
2210
  const config = promptConfig();
2209
2211
  if (mode === 'image' && imageCommand?.state === 'prepared' && imageAttachment && typeof removeImage === 'function') {
2210
2212
  // qcc-document-mcp 只读取 Host 暂存文件;发送前释放原生附件,文本模型不会收到图片。
2211
2213
  removeImage(sessionId, imageAttachment);
2212
2214
  setImageAttachment(null);
2213
2215
  }
2214
- inputActions.setDraft(buildTaskPrompt(config));
2216
+ const prompt = buildTaskPrompt(config);
2217
+ try {
2218
+ inputActions.setDraft(prompt);
2219
+ const updatedInput = getInputSnapshot?.(sessionId);
2220
+ if (updatedInput && updatedInput.draft !== prompt) throw new Error('当前对话尚未接收任务说明,请稍后重试回填。');
2221
+ } catch (draftError) {
2222
+ setError(draftError instanceof Error ? draftError.message : String(draftError));
2223
+ return;
2224
+ }
2215
2225
  if (mode === 'text') {
2216
2226
  requestWorkbenchDataset(entriesToDataset(config.entries), sessionId, { type: 'text', fileName: '' });
2217
2227
  }
@@ -2260,22 +2270,38 @@ window.__ModuleLoader__.load({
2260
2270
  const hasImage = files.length > 0;
2261
2271
  if (!hasImage) return;
2262
2272
  const target = event.target;
2263
- const inComposer = !open && Boolean(target?.closest?.('textarea, [contenteditable="true"], [role="textbox"]'));
2264
- const inImageStep = open && wizardStep === 1 && mode === 'image';
2265
- if (!inComposer && !inImageStep) return;
2273
+ const inComposer = !open && !target?.closest?.('.dcAgentWorkbench')
2274
+ && Boolean(target?.closest?.('textarea, [contenteditable="true"], [role="textbox"]'));
2275
+ const inSourceStep = open && wizardStep === 1 && Boolean(target?.closest?.('.dcAgentPromptPanel'));
2276
+ if (!inComposer && !inSourceStep) return;
2266
2277
  event.preventDefault();
2267
2278
  event.stopImmediatePropagation?.();
2268
- if (!open) {
2269
- setMode('image');
2270
- setWizardStep(1);
2271
- setOpen(true);
2272
- setError(null);
2273
- }
2279
+ if (busy || imageCommand?.state === 'running') return;
2280
+ setMode('image');
2281
+ setWizardStep(1);
2282
+ setOpen(true);
2283
+ setError(null);
2274
2284
  acceptImageFiles(files);
2275
2285
  };
2276
2286
  window.addEventListener?.('paste', handleWindowPaste, true);
2277
2287
  return () => window.removeEventListener?.('paste', handleWindowPaste, true);
2278
- }, [enabled, open, wizardStep, mode, busy, sessionId]);
2288
+ }, [enabled, open, wizardStep, mode, busy, sessionId, imageCommand, imageAttachment]);
2289
+
2290
+ // 右侧工作台共用此图片入口;仅由当前会话确认接收,不把图片交给表格解析器。
2291
+ react.useEffect(() => {
2292
+ if (!enabled) return undefined;
2293
+ const receiveImage = (event) => {
2294
+ if (event.detail?.sessionId !== sessionId || busy || imageCommand?.state === 'running') return;
2295
+ event.preventDefault();
2296
+ setMode('image');
2297
+ setWizardStep(1);
2298
+ setOpen(true);
2299
+ setError(null);
2300
+ if (event.detail.file) acceptImageFiles([event.detail.file]);
2301
+ };
2302
+ document.addEventListener('dsh:data-cleaning-prompt-image', receiveImage);
2303
+ return () => document.removeEventListener('dsh:data-cleaning-prompt-image', receiveImage);
2304
+ }, [enabled, sessionId, busy, imageCommand, imageAttachment]);
2279
2305
 
2280
2306
  if (!enabled) return null;
2281
2307
 
@@ -2858,9 +2884,22 @@ window.__ModuleLoader__.load({
2858
2884
  }
2859
2885
  };
2860
2886
 
2887
+ const openImageIntake = (file = null) => {
2888
+ if (!requestPromptImage(activeSessionId, file)) {
2889
+ actions.setError('图片入口尚未就绪或正在处理图片,请回到当前清洗会话稍后重试。');
2890
+ return false;
2891
+ }
2892
+ actions.close();
2893
+ return true;
2894
+ };
2861
2895
  const handleFile = async (event) => {
2862
2896
  const file = event.target && event.target.files && event.target.files[0];
2863
2897
  if (!file) return;
2898
+ if (String(file.type ?? '').startsWith('image/')) {
2899
+ openImageIntake(file);
2900
+ event.target.value = '';
2901
+ return;
2902
+ }
2864
2903
  actions.setBusy(true);
2865
2904
  actions.setError(null);
2866
2905
  try {
@@ -3606,19 +3645,28 @@ window.__ModuleLoader__.load({
3606
3645
  );
3607
3646
  } else {
3608
3647
  pane = h('div', { className: 'dcAgentPane' },
3609
- h('p', { className: 'dcAgentHint' }, '上传 CSV / XLSX / XLS / JSON,或粘贴数据。原始明细仅在本机 Host 与同源工作台处理,不进入模型上下文。'),
3648
+ h('p', { className: 'dcAgentHint' }, '上传 CSV / XLSX / XLS / JSON,或粘贴数据。也可选择图片,通过企查查智能文档解析提取名单后继续清洗补全。'),
3610
3649
  h('input', {
3611
3650
  type: 'file',
3612
- accept: '.csv,.json,.xlsx,.xls',
3651
+ accept: '.csv,.json,.xlsx,.xls,image/png,image/jpeg,image/webp',
3613
3652
  className: 'dcAgentField',
3614
3653
  'aria-label': '选择数据文件',
3615
3654
  onChange: handleFile,
3616
3655
  }),
3656
+ h('button', { type: 'button', className: 'dcAgentButton', disabled: busy,
3657
+ onClick: () => openImageIntake() }, '图片识别(粘贴 / 选择)'),
3617
3658
  h('textarea', {
3618
3659
  className: 'dcAgentTextarea',
3619
3660
  placeholder: '粘贴 CSV 文本,或 JSON 数组(例如 [{"name":"某公司","amount":"100"}])…',
3620
3661
  'aria-label': '粘贴数据',
3621
3662
  value: input,
3663
+ onPaste: (event) => {
3664
+ const files = imageFilesFromTransfer(event.clipboardData);
3665
+ if (files.length && openImageIntake(files[0])) {
3666
+ event.preventDefault();
3667
+ event.stopPropagation?.();
3668
+ }
3669
+ },
3622
3670
  onInput: (event) => actions.setInput(event.target.value),
3623
3671
  }),
3624
3672
  dataset ? h('div', { className: 'dcAgentChips' },
@@ -3770,6 +3818,10 @@ window.__ModuleLoader__.load({
3770
3818
  inject: () => ({
3771
3819
  attachImages: (sessionId, files) => attachPromptImages(ctx, sessionId, files),
3772
3820
  removeImage: (sessionId, attachment) => removePromptImage(ctx, sessionId, attachment),
3821
+ getInputSnapshot: (sessionId) => {
3822
+ const conversation = typeof ctx.get === 'function' ? ctx.get('conversation') : ctx.conversation;
3823
+ return conversation?.input?.shell?.(sessionId)?.snapshot;
3824
+ },
3773
3825
  }),
3774
3826
  }, PromptGenerator));
3775
3827
 
@@ -3808,6 +3860,8 @@ window.__ModuleLoader__.load({
3808
3860
  entriesToDataset,
3809
3861
  extractPromptEntries,
3810
3862
  imageFilesFromTransfer,
3863
+ isImagePathEntry,
3864
+ requestPromptImage,
3811
3865
  guessMappings,
3812
3866
  deactivateCleaningSession,
3813
3867
  installSessionOwnershipBridge,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-data-cleaning-agent",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
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",