dsh-data-cleaning-agent 0.7.0 → 0.8.0

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/lib/client.js CHANGED
@@ -347,6 +347,78 @@ window.__ModuleLoader__.load({
347
347
  font-size: 12px;
348
348
  }
349
349
  .dcAgentPromptFile input { max-width: 100%; }
350
+ .dcAgentPromptImageDrop {
351
+ display: grid;
352
+ gap: 10px;
353
+ padding: 12px;
354
+ border: 1px dashed light-dark(#b9c5d8, #43516a);
355
+ border-radius: 12px;
356
+ background: light-dark(#fbfcfe, #11171f);
357
+ outline: none;
358
+ }
359
+ .dcAgentPromptImageDrop.is-active {
360
+ border-color: #2869e6;
361
+ background: light-dark(#edf4ff, #172841);
362
+ }
363
+ .dcAgentPromptImageDrop > p {
364
+ margin: 0;
365
+ color: light-dark(#697386, #aab5c7);
366
+ font-size: 11px;
367
+ line-height: 17px;
368
+ }
369
+ .dcAgentPromptImagePreview {
370
+ display: flex;
371
+ align-items: center;
372
+ gap: 10px;
373
+ min-width: 0;
374
+ }
375
+ .dcAgentPromptImageThumb {
376
+ width: 64px;
377
+ height: 64px;
378
+ flex: 0 0 auto;
379
+ overflow: hidden;
380
+ padding: 0;
381
+ border: 1px solid light-dark(#d9dee8, #394352);
382
+ border-radius: 10px;
383
+ background: light-dark(#f3f6fa, #202733);
384
+ cursor: zoom-in;
385
+ }
386
+ .dcAgentPromptImageThumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
387
+ .dcAgentPromptImageMeta { min-width: 0; flex: 1 1 auto; }
388
+ .dcAgentPromptImageMeta b,
389
+ .dcAgentPromptImageMeta small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
390
+ .dcAgentPromptImageMeta small { margin-top: 3px; color: light-dark(#697386, #aab5c7); }
391
+ .dcAgentPromptImageRemove {
392
+ flex: 0 0 auto;
393
+ padding: 5px 9px;
394
+ border: 1px solid light-dark(#d9dee8, #394352);
395
+ border-radius: 8px;
396
+ background: transparent;
397
+ color: inherit;
398
+ font: inherit;
399
+ font-size: 11px;
400
+ cursor: pointer;
401
+ }
402
+ .dcAgentImageLightbox {
403
+ position: fixed;
404
+ inset: 0;
405
+ z-index: 1400;
406
+ display: grid;
407
+ place-items: center;
408
+ padding: 32px;
409
+ border: 0;
410
+ background: rgba(8, 12, 20, .76);
411
+ cursor: zoom-out;
412
+ pointer-events: auto;
413
+ }
414
+ .dcAgentImageLightbox img {
415
+ display: block;
416
+ max-width: min(1100px, 92vw);
417
+ max-height: 88vh;
418
+ object-fit: contain;
419
+ border-radius: 12px;
420
+ box-shadow: 0 24px 80px rgba(0, 0, 0, .4);
421
+ }
350
422
  .dcAgentPromptGroup { margin-top: 12px; }
351
423
  .dcAgentPromptGroup > b { display: block; margin-bottom: 7px; font-size: 12px; }
352
424
  .dcAgentPromptChoices { display: flex; flex-wrap: wrap; gap: 6px; }
@@ -982,7 +1054,10 @@ window.__ModuleLoader__.load({
982
1054
  const executionDraft = value.startsWith('请执行已在「数据清洗补全工作台」确认的企业数据任务。')
983
1055
  && value.includes('安全任务凭证:dcq-')
984
1056
  && value.includes('data_cleaning_qcc_run');
985
- return wizardDraft || executionDraft;
1057
+ const imageDraft = value.startsWith('请识别我刚刚在向导中安全暂存的企业名单图片')
1058
+ && value.includes('安全图片凭证:dci-')
1059
+ && value.includes('data_cleaning_extract_image_companies');
1060
+ return wizardDraft || executionDraft || imageDraft;
986
1061
  }
987
1062
 
988
1063
  function clearCleaningDraft(ctx, sessionId, onlyDefault = false) {
@@ -1119,7 +1194,7 @@ window.__ModuleLoader__.load({
1119
1194
  : [];
1120
1195
  const entryCount = Number.isFinite(config.entryCount) ? config.entryCount : entries.length;
1121
1196
  const source = mode === 'image'
1122
- ? `已附加图片${config.fileName ? `「${config.fileName}」` : ''}`
1197
+ ? `图片${config.fileName ? `「${config.fileName}」` : ''},已识别并人工核验 ${entryCount} 条主体标识`
1123
1198
  : mode === 'excel'
1124
1199
  ? `本地表格${config.fileName ? `「${config.fileName}」` : ''},已解析 ${entryCount} 条主体标识,完整数据已载入数据清洗补全工作台`
1125
1200
  : `手工录入,共 ${entryCount} 条主体标识`;
@@ -1127,9 +1202,6 @@ window.__ModuleLoader__.load({
1127
1202
  '请执行一项企业名单数据清洗补全任务。',
1128
1203
  `输入来源:${source}。`,
1129
1204
  ];
1130
- if (mode === 'image') {
1131
- lines.push('请先使用当前已连接且可用的企查查智能文档解析 MCP 能力,从图片中提取企业全称或统一社会信用代码;若该能力未连接或不可用,请先提示我改用文本或 Excel。');
1132
- }
1133
1205
  if (entries.length) {
1134
1206
  lines.push(`${mode === 'excel' ? '主体预览' : '待处理主体'}:\n${entries.map((entry, index) => `${index + 1}. ${entry}`).join('\n')}`);
1135
1207
  }
@@ -1523,8 +1595,8 @@ window.__ModuleLoader__.load({
1523
1595
  }
1524
1596
 
1525
1597
  /**
1526
- * 图片接入兼容 Bridge。仅在运行时同时探测到 createDraftImages / input.shell.addImages
1527
- * 时使用;失败会释放浏览器临时对象,不假设 alpha 实验 API 稳定。
1598
+ * 图片接入兼容 Bridge。返回官方 ComposerAttachment,供向导复用其 previewUrl
1599
+ * 显示 64px 缩略图;失败会释放浏览器临时对象,不假设 alpha 实验 API 稳定。
1528
1600
  */
1529
1601
  function attachPromptImages(ctx, sessionId, files) {
1530
1602
  const conversation = typeof ctx.get === 'function' ? ctx.get('conversation') : ctx.conversation;
@@ -1540,7 +1612,17 @@ window.__ModuleLoader__.load({
1540
1612
  conversation.releaseDraftImages?.(images);
1541
1613
  throw new Error('图片未能加入当前对话,请检查格式或数量限制。');
1542
1614
  }
1543
- return images.length;
1615
+ return images;
1616
+ }
1617
+
1618
+ function removePromptImage(ctx, sessionId, attachment) {
1619
+ const conversation = typeof ctx.get === 'function' ? ctx.get('conversation') : ctx.conversation;
1620
+ const id = attachment?.id;
1621
+ if (!conversation || !id) return false;
1622
+ const shell = conversation.input?.shell?.(sessionId);
1623
+ shell?.removeImage?.(id);
1624
+ conversation.releaseDraftImage?.(id);
1625
+ return true;
1544
1626
  }
1545
1627
 
1546
1628
  async function sendQccAgentCommand(ctx, sessionId, prompt) {
@@ -1873,8 +1955,30 @@ window.__ModuleLoader__.load({
1873
1955
  }).filter(Boolean).slice(0, 50);
1874
1956
  }
1875
1957
 
1958
+ async function imageFileBase64(file) {
1959
+ if (!file || typeof file.arrayBuffer !== 'function') throw new Error('图片文件不可读。');
1960
+ const bytes = new Uint8Array(await file.arrayBuffer());
1961
+ let binary = '';
1962
+ for (let offset = 0; offset < bytes.length; offset += 0x8000) {
1963
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
1964
+ }
1965
+ return btoa(binary);
1966
+ }
1967
+
1968
+ async function stagePromptImage(file) {
1969
+ if (!['image/png', 'image/jpeg', 'image/webp'].includes(String(file?.type ?? ''))) {
1970
+ throw new Error('仅支持 PNG、JPEG 或 WebP 图片。');
1971
+ }
1972
+ if (Number(file?.size ?? 0) > 8 * 1024 * 1024) throw new Error('图片不能超过 8 MiB。');
1973
+ return api('/data-cleaning/api/images/commands', {
1974
+ fileName: file.name,
1975
+ mimeType: file.type,
1976
+ content: await imageFileBase64(file),
1977
+ });
1978
+ }
1979
+
1876
1980
  function PromptGenerator(props) {
1877
- const { sessionId, inputActions, attachImages } = props;
1981
+ const { sessionId, inputActions, attachImages, removeImage } = props;
1878
1982
  const enabled = useCleaningSession(sessionId);
1879
1983
  const [open, setOpen] = react.useState(false);
1880
1984
  const [wizardStep, setWizardStep] = react.useState(1);
@@ -1884,7 +1988,11 @@ window.__ModuleLoader__.load({
1884
1988
  const [entryCount, setEntryCount] = react.useState(0);
1885
1989
  const [spreadsheetFileName, setSpreadsheetFileName] = react.useState('');
1886
1990
  const [imageFileName, setImageFileName] = react.useState('');
1887
- const [imageAttached, setImageAttached] = react.useState(false);
1991
+ const [imageSizeBytes, setImageSizeBytes] = react.useState(0);
1992
+ const [imageAttachment, setImageAttachment] = react.useState(null);
1993
+ const [imageCommand, setImageCommand] = react.useState(null);
1994
+ const [imageDropActive, setImageDropActive] = react.useState(false);
1995
+ const [imageLightbox, setImageLightbox] = react.useState(false);
1888
1996
  const [cleaningKeys, setCleaningKeys] = react.useState(DEFAULT_CLEANING_KEYS);
1889
1997
  const [enrichmentKeys, setEnrichmentKeys] = react.useState(DEFAULT_ENRICHMENT_KEYS);
1890
1998
  const [anchorKeys, setAnchorKeys] = react.useState(['company_name', 'credit_no']);
@@ -1897,38 +2005,127 @@ window.__ModuleLoader__.load({
1897
2005
  const [busy, setBusy] = react.useState(false);
1898
2006
  const [error, setError] = react.useState(null);
1899
2007
 
1900
- if (!enabled) return null;
2008
+ react.useEffect(() => {
2009
+ const commandId = imageCommand?.commandId;
2010
+ if (!commandId || ['completed', 'failed'].includes(imageCommand?.state)) return undefined;
2011
+ let disposed = false;
2012
+ let timer = null;
2013
+ const poll = async () => {
2014
+ try {
2015
+ const response = await api(`/data-cleaning/api/images/commands/${encodeURIComponent(commandId)}`);
2016
+ if (disposed) return;
2017
+ const command = response?.command;
2018
+ if (!command) return;
2019
+ setImageCommand((current) => current?.commandId === commandId ? { ...current, ...command } : current);
2020
+ if (command.state === 'completed') {
2021
+ const recognized = Array.isArray(command.result?.entries) ? command.result.entries : [];
2022
+ setEntries(recognized);
2023
+ setEntryCount(recognized.length);
2024
+ requestWorkbenchDataset(entriesToDataset(recognized), sessionId, {
2025
+ type: 'image', fileName: imageFileName, sizeBytes: imageSizeBytes,
2026
+ });
2027
+ setError(null);
2028
+ setWizardStep(2);
2029
+ setOpen(true);
2030
+ return;
2031
+ }
2032
+ if (command.state === 'failed') {
2033
+ setError(command.error?.message || '图片识别失败,请换用清晰图片或文本/Excel 名单。');
2034
+ setOpen(true);
2035
+ return;
2036
+ }
2037
+ timer = setTimeout(poll, 1000);
2038
+ } catch (pollError) {
2039
+ if (!disposed) {
2040
+ setError(pollError instanceof Error ? pollError.message : String(pollError));
2041
+ setOpen(true);
2042
+ }
2043
+ }
2044
+ };
2045
+ timer = setTimeout(poll, 800);
2046
+ return () => {
2047
+ disposed = true;
2048
+ if (timer !== null) clearTimeout(timer);
2049
+ };
2050
+ }, [imageCommand?.commandId, imageCommand?.state, imageFileName, imageSizeBytes, sessionId]);
1901
2051
 
2052
+ const discardCurrentImage = () => {
2053
+ if (imageAttachment && typeof removeImage === 'function') removeImage(sessionId, imageAttachment);
2054
+ if (imageCommand?.commandId && imageCommand.state !== 'running') {
2055
+ requestJson(`/data-cleaning/api/images/commands/${encodeURIComponent(imageCommand.commandId)}`, 'DELETE').catch(() => {});
2056
+ }
2057
+ setImageAttachment(null);
2058
+ setImageCommand(null);
2059
+ setImageFileName('');
2060
+ setImageSizeBytes(0);
2061
+ setEntries([]);
2062
+ setEntryCount(0);
2063
+ setImageLightbox(false);
2064
+ };
1902
2065
  const setSourceMode = (nextMode) => {
2066
+ if (mode === 'image' && nextMode !== 'image') {
2067
+ if (imageCommand?.state === 'running') {
2068
+ setError('图片正在识别,请等待完成后再切换数据来源。');
2069
+ return;
2070
+ }
2071
+ discardCurrentImage();
2072
+ }
1903
2073
  setMode(nextMode);
1904
2074
  setError(null);
1905
2075
  };
1906
2076
  const toggle = (setter, values, key) => setter(values.includes(key)
1907
2077
  ? values.filter((item) => item !== key)
1908
2078
  : [...values, key]);
2079
+ const handleImageFile = async (file) => {
2080
+ if (!file || busy) return;
2081
+ setBusy(true);
2082
+ setError(null);
2083
+ let attached = null;
2084
+ try {
2085
+ if (typeof attachImages !== 'function') throw new Error('当前 DSH 没有可用的图片接入 Bridge。');
2086
+ const images = await attachImages(sessionId, [file]);
2087
+ attached = Array.isArray(images) ? images[0] : null;
2088
+ if (!attached?.id) throw new Error('图片未能加入当前对话。');
2089
+ const staged = await stagePromptImage(file);
2090
+ if (imageAttachment && typeof removeImage === 'function') removeImage(sessionId, imageAttachment);
2091
+ if (imageCommand?.commandId) {
2092
+ requestJson(`/data-cleaning/api/images/commands/${encodeURIComponent(imageCommand.commandId)}`, 'DELETE').catch(() => {});
2093
+ }
2094
+ setImageAttachment(attached);
2095
+ setImageFileName(file.name);
2096
+ setImageSizeBytes(Number(file.size ?? 0));
2097
+ setImageCommand(staged.command);
2098
+ setEntries([]);
2099
+ setEntryCount(0);
2100
+ } catch (fileError) {
2101
+ if (attached && typeof removeImage === 'function') removeImage(sessionId, attached);
2102
+ throw fileError;
2103
+ } finally {
2104
+ setBusy(false);
2105
+ }
2106
+ };
1909
2107
  const handleFile = async (event) => {
1910
2108
  const file = event.target?.files?.[0];
1911
2109
  if (!file || busy) return;
2110
+ if (mode === 'image') {
2111
+ try { await handleImageFile(file); }
2112
+ catch (fileError) { setError(fileError instanceof Error ? fileError.message : String(fileError)); }
2113
+ finally { if (event.target) event.target.value = ''; }
2114
+ return;
2115
+ }
1912
2116
  setBusy(true);
1913
2117
  setError(null);
1914
2118
  try {
1915
- if (mode === 'image') {
1916
- if (typeof attachImages !== 'function') throw new Error('当前 DSH 没有可用的图片接入 Bridge。');
1917
- await attachImages(sessionId, [file]);
1918
- setImageFileName(file.name);
1919
- setImageAttached(true);
1920
- } else {
1921
- const result = await parseFile(file);
1922
- if (!result || result.ok === false) throw new Error(result?.message || result?.error || '表格解析失败');
1923
- const extracted = extractPromptEntries(result);
1924
- if (!extracted.length) throw new Error('未识别到企业名称或统一社会信用代码列,请检查表头。');
1925
- setEntries(extracted);
1926
- setEntryCount(Number.isFinite(result.rowCount) ? result.rowCount : (Array.isArray(result.rows) ? result.rows.length : extracted.length));
1927
- setSpreadsheetFileName(file.name);
1928
- requestWorkbenchDataset(result, sessionId, {
1929
- type: result.fmt || 'xlsx', fileName: file.name, sizeBytes: file.size || 0,
1930
- });
1931
- }
2119
+ const result = await parseFile(file);
2120
+ if (!result || result.ok === false) throw new Error(result?.message || result?.error || '表格解析失败');
2121
+ const extracted = extractPromptEntries(result);
2122
+ if (!extracted.length) throw new Error('未识别到企业名称或统一社会信用代码列,请检查表头。');
2123
+ setEntries(extracted);
2124
+ setEntryCount(Number.isFinite(result.rowCount) ? result.rowCount : (Array.isArray(result.rows) ? result.rows.length : extracted.length));
2125
+ setSpreadsheetFileName(file.name);
2126
+ requestWorkbenchDataset(result, sessionId, {
2127
+ type: result.fmt || 'xlsx', fileName: file.name, sizeBytes: file.size || 0,
2128
+ });
1932
2129
  } catch (fileError) {
1933
2130
  setError(fileError instanceof Error ? fileError.message : String(fileError));
1934
2131
  } finally {
@@ -1943,7 +2140,9 @@ window.__ModuleLoader__.load({
1943
2140
  const values = selectedEntries();
1944
2141
  if (mode === 'text' && !values.length) return '请先录入企业名称或统一社会信用代码,每行一条。';
1945
2142
  if (mode === 'excel' && !values.length) return '请先上传并解析 Excel / CSV / JSON 文件。';
1946
- if (mode === 'image' && !imageAttached) return '请先选择并附加一张包含企业名单的图片。';
2143
+ if (mode === 'image' && !imageCommand) return '请先选择、粘贴或拖入一张包含企业名单的图片。';
2144
+ if (mode === 'image' && imageCommand?.state !== 'completed') return '请先把图片识别指令回填到对话框并发送,等待名单识别完成。';
2145
+ if (mode === 'image' && !values.length) return '图片中未提取到可核验的企业名称或统一社会信用代码。';
1947
2146
  return null;
1948
2147
  };
1949
2148
  const promptConfig = () => ({
@@ -1984,6 +2183,22 @@ window.__ModuleLoader__.load({
1984
2183
 
1985
2184
  const nextWizard = () => {
1986
2185
  if (wizardStep === 1) {
2186
+ if (mode === 'image' && imageCommand?.state === 'prepared') {
2187
+ if (!inputActions || typeof inputActions.setDraft !== 'function') {
2188
+ setError('当前会话输入机尚未就绪,请稍后重试。');
2189
+ return;
2190
+ }
2191
+ // Host 已持久暂存图片;发送识别命令前释放 Composer 附件,避免不支持
2192
+ // 视觉输入的文本模型拒绝本轮。识别工具只按 commandId 读取 Host 暂存副本。
2193
+ if (imageAttachment && typeof removeImage === 'function') {
2194
+ removeImage(sessionId, imageAttachment);
2195
+ setImageAttachment(null);
2196
+ }
2197
+ inputActions.setDraft(imageCommand.prompt);
2198
+ setError(null);
2199
+ setOpen(false);
2200
+ return;
2201
+ }
1987
2202
  const sourceError = validateSource();
1988
2203
  if (sourceError) { setError(sourceError); return; }
1989
2204
  }
@@ -1993,6 +2208,47 @@ window.__ModuleLoader__.load({
1993
2208
  generatePrompt();
1994
2209
  };
1995
2210
 
2211
+ const acceptImageFiles = async (files) => {
2212
+ const file = [...(files || [])].find((candidate) => String(candidate?.type ?? '').startsWith('image/'));
2213
+ if (!file) { setError('剪贴板或拖入内容中没有可用图片。'); return; }
2214
+ try { await handleImageFile(file); }
2215
+ catch (imageError) { setError(imageError instanceof Error ? imageError.message : String(imageError)); }
2216
+ };
2217
+ const removeCurrentImage = async () => {
2218
+ if (busy) return;
2219
+ discardCurrentImage();
2220
+ setError(null);
2221
+ };
2222
+
2223
+ // 在数据清洗会话中从 window 捕获阶段接管 Composer 图片粘贴,早于
2224
+ // document 级视觉兼容插件。向导未打开时会自动进入图片模式;只消费包含
2225
+ // 图片且目标为原生 Composer 的事件,不影响文字粘贴、右侧工作台或其他会话。
2226
+ react.useEffect(() => {
2227
+ if (!enabled) return undefined;
2228
+ const handleWindowPaste = (event) => {
2229
+ const files = event.clipboardData?.files;
2230
+ const hasImage = [...(files || [])].some((file) => String(file?.type ?? '').startsWith('image/'));
2231
+ if (!hasImage) return;
2232
+ const target = event.target;
2233
+ const inComposer = !open && Boolean(target?.closest?.('textarea, [contenteditable="true"], [role="textbox"]'));
2234
+ const inImageStep = open && wizardStep === 1 && mode === 'image';
2235
+ if (!inComposer && !inImageStep) return;
2236
+ event.preventDefault();
2237
+ event.stopImmediatePropagation?.();
2238
+ if (!open) {
2239
+ setMode('image');
2240
+ setWizardStep(1);
2241
+ setOpen(true);
2242
+ setError(null);
2243
+ }
2244
+ acceptImageFiles(files);
2245
+ };
2246
+ window.addEventListener?.('paste', handleWindowPaste, true);
2247
+ return () => window.removeEventListener?.('paste', handleWindowPaste, true);
2248
+ }, [enabled, open, wizardStep, mode, busy, sessionId]);
2249
+
2250
+ if (!enabled) return null;
2251
+
1996
2252
  const choice = (options, values, setter) => options.map(([key, label]) => h('label', {
1997
2253
  key,
1998
2254
  className: `dcAgentPromptChoice${values.includes(key) ? ' is-selected' : ''}`,
@@ -2067,6 +2323,55 @@ window.__ModuleLoader__.load({
2067
2323
  placeholder: '企查查科技股份有限公司\n9132…\n某某信息技术(上海)有限公司',
2068
2324
  onChange: (event) => setRawText(event.target.value),
2069
2325
  }),
2326
+ ) : mode === 'image' ? h('div', {
2327
+ className: `dcAgentPromptImageDrop${imageDropActive ? ' is-active' : ''}`,
2328
+ tabIndex: 0,
2329
+ role: 'group',
2330
+ 'aria-label': '粘贴、拖入或选择企业名单图片',
2331
+ onPaste: (event) => {
2332
+ const files = event.clipboardData?.files;
2333
+ if (files?.length) {
2334
+ event.preventDefault();
2335
+ acceptImageFiles(files);
2336
+ }
2337
+ },
2338
+ onDragOver: (event) => { event.preventDefault(); setImageDropActive(true); },
2339
+ onDragLeave: () => setImageDropActive(false),
2340
+ onDrop: (event) => {
2341
+ event.preventDefault();
2342
+ setImageDropActive(false);
2343
+ acceptImageFiles(event.dataTransfer?.files);
2344
+ },
2345
+ },
2346
+ h('p', null, '可直接在此粘贴图片,或拖入 / 选择 PNG、JPEG、WebP。图片会在对话框显示缩略图,点击可放大。'),
2347
+ imageAttachment ? h('div', { className: 'dcAgentPromptImagePreview' },
2348
+ h('button', {
2349
+ type: 'button', className: 'dcAgentPromptImageThumb',
2350
+ 'aria-label': `放大查看 ${imageFileName}`,
2351
+ onClick: () => setImageLightbox(true),
2352
+ }, imageAttachment.previewUrl ? h('img', { src: imageAttachment.previewUrl, alt: imageFileName }) : '图片'),
2353
+ h('div', { className: 'dcAgentPromptImageMeta' },
2354
+ h('b', null, imageFileName),
2355
+ h('small', null, imageCommand?.state === 'completed'
2356
+ ? `已识别 ${entryCount} 条,下一步逐条核验`
2357
+ : imageCommand?.state === 'running'
2358
+ ? '识别中…'
2359
+ : imageCommand?.state === 'failed'
2360
+ ? '识别失败,可移除后重试'
2361
+ : '已暂存,下一步生成可编辑识别指令'),
2362
+ ),
2363
+ h('button', { type: 'button', className: 'dcAgentPromptImageRemove', disabled: busy || imageCommand?.state === 'running', onClick: removeCurrentImage }, '移除'),
2364
+ ) : h('label', { className: 'dcAgentPromptFile' },
2365
+ h('span', null, '选择包含企业名单的图片'),
2366
+ h('input', {
2367
+ type: 'file', accept: 'image/png,image/jpeg,image/webp',
2368
+ disabled: busy, onChange: handleFile,
2369
+ }),
2370
+ ),
2371
+ imageCommand?.state === 'completed' && entries.length ? h('div', { className: 'dcAgentPromptChoices' },
2372
+ entries.slice(0, 12).map((entry, index) => h('span', { key: `${entry}-${index}`, className: 'dcAgentPromptChoice is-selected' }, `${index + 1}. ${entry}`)),
2373
+ entries.length > 12 ? h('span', { className: 'dcAgentPromptChoice' }, `另 ${entries.length - 12} 条`) : null,
2374
+ ) : null,
2070
2375
  ) : h('label', { className: 'dcAgentPromptFile' },
2071
2376
  h('span', null, mode === 'image' ? '选择包含企业名单的图片' : '选择 CSV / XLSX / JSON 名单'),
2072
2377
  h('input', {
@@ -2118,9 +2423,20 @@ window.__ModuleLoader__.load({
2118
2423
  error ? h('p', { className: 'dcAgentPromptError', role: 'alert' }, error) : null,
2119
2424
  h('div', { className: 'dcAgentPromptActions' },
2120
2425
  h('button', { type: 'button', className: 'dcAgentPromptAction', disabled: wizardStep === 1, onClick: () => setWizardStep(Math.max(1, wizardStep - 1)) }, '上一步'),
2121
- h('button', { type: 'button', className: 'dcAgentPromptAction is-primary', disabled: busy, onClick: nextWizard }, busy ? '处理中…' : wizardStep === 4 ? '回填到对话框' : '下一步'),
2426
+ h('button', { type: 'button', className: 'dcAgentPromptAction is-primary', disabled: busy || imageCommand?.state === 'running', onClick: nextWizard }, busy
2427
+ ? '处理中…'
2428
+ : wizardStep === 1 && mode === 'image' && imageCommand?.state === 'prepared'
2429
+ ? '回填图片识别指令'
2430
+ : wizardStep === 1 && mode === 'image' && imageCommand?.state === 'running'
2431
+ ? '图片识别中…'
2432
+ : wizardStep === 4 ? '回填到对话框' : '下一步'),
2122
2433
  ),
2123
2434
  ) : null,
2435
+ imageLightbox && imageAttachment?.previewUrl ? h('button', {
2436
+ type: 'button', className: 'dcAgentImageLightbox',
2437
+ 'aria-label': '关闭图片预览',
2438
+ onClick: () => setImageLightbox(false),
2439
+ }, h('img', { src: imageAttachment.previewUrl, alt: imageFileName })) : null,
2124
2440
  );
2125
2441
  }
2126
2442
 
@@ -2143,11 +2459,12 @@ window.__ModuleLoader__.load({
2143
2459
  );
2144
2460
  }
2145
2461
 
2146
- /** M3 · 三工具 tool.call.toolview 卡片元数据(wire 名 → 展示文案)。 */
2462
+ /** 模型工具 tool.call.toolview 卡片元数据(wire 名 → 展示文案)。 */
2147
2463
  const TOOL_VIEW_META = {
2148
2464
  data_clean_rows: { icon: '🧹', label: '数据清洗', hint: '去重 · 剔除非法金额 · 缺失补 0' },
2149
2465
  data_complete_rows: { icon: '🧩', label: '数据补全', hint: '名称 / 金额 / 手机号归一' },
2150
2466
  data_profile: { icon: '🩺', label: '数据体检', hint: '缺失率 · 去重值 · 金额分布' },
2467
+ data_cleaning_extract_image_companies: { icon: '🖼️', label: '图片名单识别', hint: 'OCR · 企业名称 / 信用代码提取 · 人工核验' },
2151
2468
  };
2152
2469
  const TOOL_VIEW_STATE = { running: '运行中', stopped: '已停止', error: '失败', ok: '完成' };
2153
2470
 
@@ -3419,7 +3736,10 @@ window.__ModuleLoader__.load({
3419
3736
  name: 'conversation.input.overlay',
3420
3737
  id: 'data-cleaning-agent-prompt-generator',
3421
3738
  order: 110,
3422
- inject: () => ({ attachImages: (sessionId, files) => attachPromptImages(ctx, sessionId, files) }),
3739
+ inject: () => ({
3740
+ attachImages: (sessionId, files) => attachPromptImages(ctx, sessionId, files),
3741
+ removeImage: (sessionId, attachment) => removePromptImage(ctx, sessionId, attachment),
3742
+ }),
3423
3743
  }, PromptGenerator));
3424
3744
 
3425
3745
  // 会话头部恢复入口:用户关闭右栏后可随时重新打开当前任务工作台。
@@ -3429,9 +3749,9 @@ window.__ModuleLoader__.load({
3429
3749
  order: 110,
3430
3750
  }, WorkbenchHeaderEntry));
3431
3751
 
3432
- // M3:三个工具的 tool.call.toolview 富化卡片(keyed by wire name,替代裸 JSON 摘要)。
3752
+ // 模型工具的 tool.call.toolview 富化卡片(keyed by wire name,替代裸 JSON 摘要)。
3433
3753
  // 说明:分三条独立 inject(而非 generator)——测试 shim 对 inject 回调仅执行一次并 push 其返回值。
3434
- const toolviewKeys = ['data_clean_rows', 'data_complete_rows', 'data_profile'];
3754
+ const toolviewKeys = ['data_clean_rows', 'data_complete_rows', 'data_profile', 'data_cleaning_extract_image_companies'];
3435
3755
  for (const key of toolviewKeys) {
3436
3756
  ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
3437
3757
  name: 'tool.call.toolview',