dsh-data-cleaning-agent 0.7.0 → 0.8.1
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 +34 -0
- package/README.en.md +20 -2
- package/README.md +18 -3
- package/docs/COMPATIBILITY.md +22 -0
- package/docs/RELEASE-0.8.0.md +60 -0
- package/docs/RELEASE-0.8.1.md +75 -0
- package/docs/UI-WORKFLOW-V2.md +2 -2
- package/docs/USER-GUIDE.md +29 -3
- package/lib/client.js +367 -37
- package/lib/image-intake.js +581 -0
- package/lib/index.js +1 -0
- package/lib/web.js +77 -0
- package/package.json +4 -2
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
|
-
|
|
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,9 @@ window.__ModuleLoader__.load({
|
|
|
1119
1194
|
: [];
|
|
1120
1195
|
const entryCount = Number.isFinite(config.entryCount) ? config.entryCount : entries.length;
|
|
1121
1196
|
const source = mode === 'image'
|
|
1122
|
-
?
|
|
1197
|
+
? config.imageState === 'completed'
|
|
1198
|
+
? `图片${config.fileName ? `「${config.fileName}」` : ''},已由企查查智能文档解析识别并人工核验 ${entryCount} 条主体标识`
|
|
1199
|
+
: `图片${config.fileName ? `「${config.fileName}」` : ''},已安全暂存在本机 Host,等待企查查智能文档解析识别`
|
|
1123
1200
|
: mode === 'excel'
|
|
1124
1201
|
? `本地表格${config.fileName ? `「${config.fileName}」` : ''},已解析 ${entryCount} 条主体标识,完整数据已载入数据清洗补全工作台`
|
|
1125
1202
|
: `手工录入,共 ${entryCount} 条主体标识`;
|
|
@@ -1127,8 +1204,9 @@ window.__ModuleLoader__.load({
|
|
|
1127
1204
|
'请执行一项企业名单数据清洗补全任务。',
|
|
1128
1205
|
`输入来源:${source}。`,
|
|
1129
1206
|
];
|
|
1130
|
-
if (mode === 'image') {
|
|
1131
|
-
lines.push('
|
|
1207
|
+
if (mode === 'image' && config.imageCommandId && config.imageState !== 'completed') {
|
|
1208
|
+
lines.push('请先通过企查查智能文档解析提取图片中的企业全称、18 位统一社会信用代码或注册号;模糊字符不得猜测。识别结果回传数据清洗补全工作台供我核验,然后沿用本任务已经选择的匹配规则、清洗要求和补全字段。');
|
|
1209
|
+
lines.push(`安全图片凭证:${config.imageCommandId}。请只调用一次 data_cleaning_extract_image_companies,参数只传 commandId;该高层工具会在 Host 内调用 qcc-document-mcp 的 parse_document / get_parse_result,不要直接调用底层 mcp__qcc-document* 工具。`);
|
|
1132
1210
|
}
|
|
1133
1211
|
if (entries.length) {
|
|
1134
1212
|
lines.push(`${mode === 'excel' ? '主体预览' : '待处理主体'}:\n${entries.map((entry, index) => `${index + 1}. ${entry}`).join('\n')}`);
|
|
@@ -1523,8 +1601,8 @@ window.__ModuleLoader__.load({
|
|
|
1523
1601
|
}
|
|
1524
1602
|
|
|
1525
1603
|
/**
|
|
1526
|
-
* 图片接入兼容 Bridge
|
|
1527
|
-
*
|
|
1604
|
+
* 图片接入兼容 Bridge。返回官方 ComposerAttachment,供向导复用其 previewUrl
|
|
1605
|
+
* 显示 64px 缩略图;失败会释放浏览器临时对象,不假设 alpha 实验 API 稳定。
|
|
1528
1606
|
*/
|
|
1529
1607
|
function attachPromptImages(ctx, sessionId, files) {
|
|
1530
1608
|
const conversation = typeof ctx.get === 'function' ? ctx.get('conversation') : ctx.conversation;
|
|
@@ -1540,7 +1618,17 @@ window.__ModuleLoader__.load({
|
|
|
1540
1618
|
conversation.releaseDraftImages?.(images);
|
|
1541
1619
|
throw new Error('图片未能加入当前对话,请检查格式或数量限制。');
|
|
1542
1620
|
}
|
|
1543
|
-
return images
|
|
1621
|
+
return images;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
function removePromptImage(ctx, sessionId, attachment) {
|
|
1625
|
+
const conversation = typeof ctx.get === 'function' ? ctx.get('conversation') : ctx.conversation;
|
|
1626
|
+
const id = attachment?.id;
|
|
1627
|
+
if (!conversation || !id) return false;
|
|
1628
|
+
const shell = conversation.input?.shell?.(sessionId);
|
|
1629
|
+
shell?.removeImage?.(id);
|
|
1630
|
+
conversation.releaseDraftImage?.(id);
|
|
1631
|
+
return true;
|
|
1544
1632
|
}
|
|
1545
1633
|
|
|
1546
1634
|
async function sendQccAgentCommand(ctx, sessionId, prompt) {
|
|
@@ -1783,8 +1871,15 @@ window.__ModuleLoader__.load({
|
|
|
1783
1871
|
const changedBadges = new Map();
|
|
1784
1872
|
const hiddenForeignDocks = new Map();
|
|
1785
1873
|
const composerStack = marker?.parentElement?.parentElement;
|
|
1874
|
+
// 单一会话所有权由入口的 enabled 参数(useCleaningSession(sessionId) && composerPhase==='blank')
|
|
1875
|
+
// 与 React effect 生命周期共同保证:非清洗会话不会挂载,会话切换即走 cleanup 让位。
|
|
1876
|
+
// 有界同步:每次 fix 的 DOM 变更计入校正预算,达到上限即断开观察器,杜绝与其它插件互相改写的死循环。
|
|
1877
|
+
const MAX_HERO_CORRECTIONS = 8;
|
|
1878
|
+
let corrections = 0;
|
|
1879
|
+
let observer = null;
|
|
1786
1880
|
|
|
1787
1881
|
const fix = () => {
|
|
1882
|
+
let mutated = false;
|
|
1788
1883
|
const spans = [...hero.querySelectorAll('span')];
|
|
1789
1884
|
const title = spans.find((element) => element.dataset?.dcAgentHeroTitle === 'true')
|
|
1790
1885
|
?? spans.find((element) => ['探索未至之境', 'Into the Unknown'].includes(element.textContent?.trim()))
|
|
@@ -1794,12 +1889,12 @@ window.__ModuleLoader__.load({
|
|
|
1794
1889
|
if (title) {
|
|
1795
1890
|
if (!changedTitles.has(title)) changedTitles.set(title, title.textContent ?? '');
|
|
1796
1891
|
title.dataset.dcAgentHeroTitle = 'true';
|
|
1797
|
-
if (title.textContent !== '数据清洗补全智能体') title.textContent = '数据清洗补全智能体';
|
|
1892
|
+
if (title.textContent !== '数据清洗补全智能体') { title.textContent = '数据清洗补全智能体'; mutated = true; }
|
|
1798
1893
|
}
|
|
1799
1894
|
if (badge) {
|
|
1800
1895
|
if (!changedBadges.has(badge)) changedBadges.set(badge, badge.style?.display ?? '');
|
|
1801
1896
|
badge.dataset.dcAgentHeroBadge = 'true';
|
|
1802
|
-
if (badge.style && badge.style.display !== 'none') badge.style.display = 'none';
|
|
1897
|
+
if (badge.style && badge.style.display !== 'none') { badge.style.display = 'none'; mutated = true; }
|
|
1803
1898
|
}
|
|
1804
1899
|
|
|
1805
1900
|
const foreignSurfaces = composerStack?.querySelectorAll?.('[aria-label="尽调类型"], .qccDock') ?? [];
|
|
@@ -1815,13 +1910,20 @@ window.__ModuleLoader__.load({
|
|
|
1815
1910
|
if ((hideTarget?.parentElement === composerStack || hideTarget?.parentElement === marker.parentElement)
|
|
1816
1911
|
&& !hideTarget.contains?.(marker)) {
|
|
1817
1912
|
if (!hiddenForeignDocks.has(hideTarget)) hiddenForeignDocks.set(hideTarget, hideTarget.style?.display ?? '');
|
|
1818
|
-
if (hideTarget.style && hideTarget.style.display !== 'none') hideTarget.style.display = 'none';
|
|
1913
|
+
if (hideTarget.style && hideTarget.style.display !== 'none') { hideTarget.style.display = 'none'; mutated = true; }
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
if (mutated) {
|
|
1917
|
+
corrections += 1;
|
|
1918
|
+
if (corrections >= MAX_HERO_CORRECTIONS && observer) {
|
|
1919
|
+
observer.disconnect();
|
|
1920
|
+
observer = null;
|
|
1819
1921
|
}
|
|
1820
1922
|
}
|
|
1821
1923
|
};
|
|
1822
1924
|
|
|
1823
1925
|
fix();
|
|
1824
|
-
|
|
1926
|
+
observer = typeof MutationObserver === 'function' ? new MutationObserver(fix) : null;
|
|
1825
1927
|
observer?.observe?.(hero, { childList: true, subtree: true });
|
|
1826
1928
|
return () => {
|
|
1827
1929
|
observer?.disconnect?.();
|
|
@@ -1873,8 +1975,30 @@ window.__ModuleLoader__.load({
|
|
|
1873
1975
|
}).filter(Boolean).slice(0, 50);
|
|
1874
1976
|
}
|
|
1875
1977
|
|
|
1978
|
+
async function imageFileBase64(file) {
|
|
1979
|
+
if (!file || typeof file.arrayBuffer !== 'function') throw new Error('图片文件不可读。');
|
|
1980
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
1981
|
+
let binary = '';
|
|
1982
|
+
for (let offset = 0; offset < bytes.length; offset += 0x8000) {
|
|
1983
|
+
binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
|
|
1984
|
+
}
|
|
1985
|
+
return btoa(binary);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
async function stagePromptImage(file) {
|
|
1989
|
+
if (!['image/png', 'image/jpeg', 'image/webp'].includes(String(file?.type ?? ''))) {
|
|
1990
|
+
throw new Error('仅支持 PNG、JPEG 或 WebP 图片。');
|
|
1991
|
+
}
|
|
1992
|
+
if (Number(file?.size ?? 0) > 8 * 1024 * 1024) throw new Error('图片不能超过 8 MiB。');
|
|
1993
|
+
return api('/data-cleaning/api/images/commands', {
|
|
1994
|
+
fileName: file.name,
|
|
1995
|
+
mimeType: file.type,
|
|
1996
|
+
content: await imageFileBase64(file),
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
|
|
1876
2000
|
function PromptGenerator(props) {
|
|
1877
|
-
const { sessionId, inputActions, attachImages } = props;
|
|
2001
|
+
const { sessionId, inputActions, attachImages, removeImage } = props;
|
|
1878
2002
|
const enabled = useCleaningSession(sessionId);
|
|
1879
2003
|
const [open, setOpen] = react.useState(false);
|
|
1880
2004
|
const [wizardStep, setWizardStep] = react.useState(1);
|
|
@@ -1884,7 +2008,11 @@ window.__ModuleLoader__.load({
|
|
|
1884
2008
|
const [entryCount, setEntryCount] = react.useState(0);
|
|
1885
2009
|
const [spreadsheetFileName, setSpreadsheetFileName] = react.useState('');
|
|
1886
2010
|
const [imageFileName, setImageFileName] = react.useState('');
|
|
1887
|
-
const [
|
|
2011
|
+
const [imageSizeBytes, setImageSizeBytes] = react.useState(0);
|
|
2012
|
+
const [imageAttachment, setImageAttachment] = react.useState(null);
|
|
2013
|
+
const [imageCommand, setImageCommand] = react.useState(null);
|
|
2014
|
+
const [imageDropActive, setImageDropActive] = react.useState(false);
|
|
2015
|
+
const [imageLightbox, setImageLightbox] = react.useState(false);
|
|
1888
2016
|
const [cleaningKeys, setCleaningKeys] = react.useState(DEFAULT_CLEANING_KEYS);
|
|
1889
2017
|
const [enrichmentKeys, setEnrichmentKeys] = react.useState(DEFAULT_ENRICHMENT_KEYS);
|
|
1890
2018
|
const [anchorKeys, setAnchorKeys] = react.useState(['company_name', 'credit_no']);
|
|
@@ -1897,38 +2025,127 @@ window.__ModuleLoader__.load({
|
|
|
1897
2025
|
const [busy, setBusy] = react.useState(false);
|
|
1898
2026
|
const [error, setError] = react.useState(null);
|
|
1899
2027
|
|
|
1900
|
-
|
|
2028
|
+
react.useEffect(() => {
|
|
2029
|
+
const commandId = imageCommand?.commandId;
|
|
2030
|
+
if (!commandId || ['completed', 'failed'].includes(imageCommand?.state)) return undefined;
|
|
2031
|
+
let disposed = false;
|
|
2032
|
+
let timer = null;
|
|
2033
|
+
const poll = async () => {
|
|
2034
|
+
try {
|
|
2035
|
+
const response = await api(`/data-cleaning/api/images/commands/${encodeURIComponent(commandId)}`);
|
|
2036
|
+
if (disposed) return;
|
|
2037
|
+
const command = response?.command;
|
|
2038
|
+
if (!command) return;
|
|
2039
|
+
setImageCommand((current) => current?.commandId === commandId ? { ...current, ...command } : current);
|
|
2040
|
+
if (command.state === 'completed') {
|
|
2041
|
+
const recognized = Array.isArray(command.result?.entries) ? command.result.entries : [];
|
|
2042
|
+
setEntries(recognized);
|
|
2043
|
+
setEntryCount(recognized.length);
|
|
2044
|
+
requestWorkbenchDataset(entriesToDataset(recognized), sessionId, {
|
|
2045
|
+
type: 'image', fileName: imageFileName, sizeBytes: imageSizeBytes,
|
|
2046
|
+
});
|
|
2047
|
+
setError(null);
|
|
2048
|
+
setOpen(false);
|
|
2049
|
+
requestWorkbenchOpen('upload', sessionId);
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
if (command.state === 'failed') {
|
|
2053
|
+
setError(command.error?.message || '图片识别失败,请换用清晰图片或文本/Excel 名单。');
|
|
2054
|
+
setOpen(true);
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
timer = setTimeout(poll, 1000);
|
|
2058
|
+
} catch (pollError) {
|
|
2059
|
+
if (!disposed) {
|
|
2060
|
+
setError(pollError instanceof Error ? pollError.message : String(pollError));
|
|
2061
|
+
setOpen(true);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
};
|
|
2065
|
+
timer = setTimeout(poll, 800);
|
|
2066
|
+
return () => {
|
|
2067
|
+
disposed = true;
|
|
2068
|
+
if (timer !== null) clearTimeout(timer);
|
|
2069
|
+
};
|
|
2070
|
+
}, [imageCommand?.commandId, imageCommand?.state, imageFileName, imageSizeBytes, sessionId]);
|
|
1901
2071
|
|
|
2072
|
+
const discardCurrentImage = () => {
|
|
2073
|
+
if (imageAttachment && typeof removeImage === 'function') removeImage(sessionId, imageAttachment);
|
|
2074
|
+
if (imageCommand?.commandId && imageCommand.state !== 'running') {
|
|
2075
|
+
requestJson(`/data-cleaning/api/images/commands/${encodeURIComponent(imageCommand.commandId)}`, 'DELETE').catch(() => {});
|
|
2076
|
+
}
|
|
2077
|
+
setImageAttachment(null);
|
|
2078
|
+
setImageCommand(null);
|
|
2079
|
+
setImageFileName('');
|
|
2080
|
+
setImageSizeBytes(0);
|
|
2081
|
+
setEntries([]);
|
|
2082
|
+
setEntryCount(0);
|
|
2083
|
+
setImageLightbox(false);
|
|
2084
|
+
};
|
|
1902
2085
|
const setSourceMode = (nextMode) => {
|
|
2086
|
+
if (mode === 'image' && nextMode !== 'image') {
|
|
2087
|
+
if (imageCommand?.state === 'running') {
|
|
2088
|
+
setError('图片正在识别,请等待完成后再切换数据来源。');
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
discardCurrentImage();
|
|
2092
|
+
}
|
|
1903
2093
|
setMode(nextMode);
|
|
1904
2094
|
setError(null);
|
|
1905
2095
|
};
|
|
1906
2096
|
const toggle = (setter, values, key) => setter(values.includes(key)
|
|
1907
2097
|
? values.filter((item) => item !== key)
|
|
1908
2098
|
: [...values, key]);
|
|
2099
|
+
const handleImageFile = async (file) => {
|
|
2100
|
+
if (!file || busy) return;
|
|
2101
|
+
setBusy(true);
|
|
2102
|
+
setError(null);
|
|
2103
|
+
let attached = null;
|
|
2104
|
+
try {
|
|
2105
|
+
if (typeof attachImages !== 'function') throw new Error('当前 DSH 没有可用的图片接入 Bridge。');
|
|
2106
|
+
const images = await attachImages(sessionId, [file]);
|
|
2107
|
+
attached = Array.isArray(images) ? images[0] : null;
|
|
2108
|
+
if (!attached?.id) throw new Error('图片未能加入当前对话。');
|
|
2109
|
+
const staged = await stagePromptImage(file);
|
|
2110
|
+
if (imageAttachment && typeof removeImage === 'function') removeImage(sessionId, imageAttachment);
|
|
2111
|
+
if (imageCommand?.commandId) {
|
|
2112
|
+
requestJson(`/data-cleaning/api/images/commands/${encodeURIComponent(imageCommand.commandId)}`, 'DELETE').catch(() => {});
|
|
2113
|
+
}
|
|
2114
|
+
setImageAttachment(attached);
|
|
2115
|
+
setImageFileName(file.name);
|
|
2116
|
+
setImageSizeBytes(Number(file.size ?? 0));
|
|
2117
|
+
setImageCommand(staged.command);
|
|
2118
|
+
setEntries([]);
|
|
2119
|
+
setEntryCount(0);
|
|
2120
|
+
} catch (fileError) {
|
|
2121
|
+
if (attached && typeof removeImage === 'function') removeImage(sessionId, attached);
|
|
2122
|
+
throw fileError;
|
|
2123
|
+
} finally {
|
|
2124
|
+
setBusy(false);
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
1909
2127
|
const handleFile = async (event) => {
|
|
1910
2128
|
const file = event.target?.files?.[0];
|
|
1911
2129
|
if (!file || busy) return;
|
|
2130
|
+
if (mode === 'image') {
|
|
2131
|
+
try { await handleImageFile(file); }
|
|
2132
|
+
catch (fileError) { setError(fileError instanceof Error ? fileError.message : String(fileError)); }
|
|
2133
|
+
finally { if (event.target) event.target.value = ''; }
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
1912
2136
|
setBusy(true);
|
|
1913
2137
|
setError(null);
|
|
1914
2138
|
try {
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
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
|
-
}
|
|
2139
|
+
const result = await parseFile(file);
|
|
2140
|
+
if (!result || result.ok === false) throw new Error(result?.message || result?.error || '表格解析失败');
|
|
2141
|
+
const extracted = extractPromptEntries(result);
|
|
2142
|
+
if (!extracted.length) throw new Error('未识别到企业名称或统一社会信用代码列,请检查表头。');
|
|
2143
|
+
setEntries(extracted);
|
|
2144
|
+
setEntryCount(Number.isFinite(result.rowCount) ? result.rowCount : (Array.isArray(result.rows) ? result.rows.length : extracted.length));
|
|
2145
|
+
setSpreadsheetFileName(file.name);
|
|
2146
|
+
requestWorkbenchDataset(result, sessionId, {
|
|
2147
|
+
type: result.fmt || 'xlsx', fileName: file.name, sizeBytes: file.size || 0,
|
|
2148
|
+
});
|
|
1932
2149
|
} catch (fileError) {
|
|
1933
2150
|
setError(fileError instanceof Error ? fileError.message : String(fileError));
|
|
1934
2151
|
} finally {
|
|
@@ -1943,7 +2160,10 @@ window.__ModuleLoader__.load({
|
|
|
1943
2160
|
const values = selectedEntries();
|
|
1944
2161
|
if (mode === 'text' && !values.length) return '请先录入企业名称或统一社会信用代码,每行一条。';
|
|
1945
2162
|
if (mode === 'excel' && !values.length) return '请先上传并解析 Excel / CSV / JSON 文件。';
|
|
1946
|
-
if (mode === 'image' && !
|
|
2163
|
+
if (mode === 'image' && !imageCommand) return '请先选择、粘贴或拖入一张包含企业名单的图片。';
|
|
2164
|
+
if (mode === 'image' && imageCommand?.state === 'failed') return imageCommand.error?.message || '图片识别失败,请重新选择图片。';
|
|
2165
|
+
if (mode === 'image' && imageCommand?.state === 'running') return '图片正在通过企查查智能文档解析识别,请稍候。';
|
|
2166
|
+
if (mode === 'image' && imageCommand?.state === 'completed' && !values.length) return '图片中未提取到可核验的企业名称或统一社会信用代码。';
|
|
1947
2167
|
return null;
|
|
1948
2168
|
};
|
|
1949
2169
|
const promptConfig = () => ({
|
|
@@ -1955,6 +2175,8 @@ window.__ModuleLoader__.load({
|
|
|
1955
2175
|
enrichmentKeys,
|
|
1956
2176
|
anchorKeys,
|
|
1957
2177
|
matchRules,
|
|
2178
|
+
imageCommandId: imageCommand?.commandId ?? '',
|
|
2179
|
+
imageState: imageCommand?.state ?? null,
|
|
1958
2180
|
});
|
|
1959
2181
|
const generatePrompt = () => {
|
|
1960
2182
|
const sourceError = validateSource();
|
|
@@ -1966,6 +2188,11 @@ window.__ModuleLoader__.load({
|
|
|
1966
2188
|
return;
|
|
1967
2189
|
}
|
|
1968
2190
|
const config = promptConfig();
|
|
2191
|
+
if (mode === 'image' && imageCommand?.state === 'prepared' && imageAttachment && typeof removeImage === 'function') {
|
|
2192
|
+
// qcc-document-mcp 只读取 Host 暂存文件;发送前释放原生附件,文本模型不会收到图片。
|
|
2193
|
+
removeImage(sessionId, imageAttachment);
|
|
2194
|
+
setImageAttachment(null);
|
|
2195
|
+
}
|
|
1969
2196
|
inputActions.setDraft(buildTaskPrompt(config));
|
|
1970
2197
|
if (mode === 'text') {
|
|
1971
2198
|
requestWorkbenchDataset(entriesToDataset(config.entries), sessionId, { type: 'text', fileName: '' });
|
|
@@ -1993,6 +2220,47 @@ window.__ModuleLoader__.load({
|
|
|
1993
2220
|
generatePrompt();
|
|
1994
2221
|
};
|
|
1995
2222
|
|
|
2223
|
+
const acceptImageFiles = async (files) => {
|
|
2224
|
+
const file = [...(files || [])].find((candidate) => String(candidate?.type ?? '').startsWith('image/'));
|
|
2225
|
+
if (!file) { setError('剪贴板或拖入内容中没有可用图片。'); return; }
|
|
2226
|
+
try { await handleImageFile(file); }
|
|
2227
|
+
catch (imageError) { setError(imageError instanceof Error ? imageError.message : String(imageError)); }
|
|
2228
|
+
};
|
|
2229
|
+
const removeCurrentImage = async () => {
|
|
2230
|
+
if (busy) return;
|
|
2231
|
+
discardCurrentImage();
|
|
2232
|
+
setError(null);
|
|
2233
|
+
};
|
|
2234
|
+
|
|
2235
|
+
// 在数据清洗会话中从 window 捕获阶段接管 Composer 图片粘贴,早于
|
|
2236
|
+
// document 级视觉兼容插件。向导未打开时会自动进入图片模式;只消费包含
|
|
2237
|
+
// 图片且目标为原生 Composer 的事件,不影响文字粘贴、右侧工作台或其他会话。
|
|
2238
|
+
react.useEffect(() => {
|
|
2239
|
+
if (!enabled) return undefined;
|
|
2240
|
+
const handleWindowPaste = (event) => {
|
|
2241
|
+
const files = event.clipboardData?.files;
|
|
2242
|
+
const hasImage = [...(files || [])].some((file) => String(file?.type ?? '').startsWith('image/'));
|
|
2243
|
+
if (!hasImage) return;
|
|
2244
|
+
const target = event.target;
|
|
2245
|
+
const inComposer = !open && Boolean(target?.closest?.('textarea, [contenteditable="true"], [role="textbox"]'));
|
|
2246
|
+
const inImageStep = open && wizardStep === 1 && mode === 'image';
|
|
2247
|
+
if (!inComposer && !inImageStep) return;
|
|
2248
|
+
event.preventDefault();
|
|
2249
|
+
event.stopImmediatePropagation?.();
|
|
2250
|
+
if (!open) {
|
|
2251
|
+
setMode('image');
|
|
2252
|
+
setWizardStep(1);
|
|
2253
|
+
setOpen(true);
|
|
2254
|
+
setError(null);
|
|
2255
|
+
}
|
|
2256
|
+
acceptImageFiles(files);
|
|
2257
|
+
};
|
|
2258
|
+
window.addEventListener?.('paste', handleWindowPaste, true);
|
|
2259
|
+
return () => window.removeEventListener?.('paste', handleWindowPaste, true);
|
|
2260
|
+
}, [enabled, open, wizardStep, mode, busy, sessionId]);
|
|
2261
|
+
|
|
2262
|
+
if (!enabled) return null;
|
|
2263
|
+
|
|
1996
2264
|
const choice = (options, values, setter) => options.map(([key, label]) => h('label', {
|
|
1997
2265
|
key,
|
|
1998
2266
|
className: `dcAgentPromptChoice${values.includes(key) ? ' is-selected' : ''}`,
|
|
@@ -2067,6 +2335,55 @@ window.__ModuleLoader__.load({
|
|
|
2067
2335
|
placeholder: '企查查科技股份有限公司\n9132…\n某某信息技术(上海)有限公司',
|
|
2068
2336
|
onChange: (event) => setRawText(event.target.value),
|
|
2069
2337
|
}),
|
|
2338
|
+
) : mode === 'image' ? h('div', {
|
|
2339
|
+
className: `dcAgentPromptImageDrop${imageDropActive ? ' is-active' : ''}`,
|
|
2340
|
+
tabIndex: 0,
|
|
2341
|
+
role: 'group',
|
|
2342
|
+
'aria-label': '粘贴、拖入或选择企业名单图片',
|
|
2343
|
+
onPaste: (event) => {
|
|
2344
|
+
const files = event.clipboardData?.files;
|
|
2345
|
+
if (files?.length) {
|
|
2346
|
+
event.preventDefault();
|
|
2347
|
+
acceptImageFiles(files);
|
|
2348
|
+
}
|
|
2349
|
+
},
|
|
2350
|
+
onDragOver: (event) => { event.preventDefault(); setImageDropActive(true); },
|
|
2351
|
+
onDragLeave: () => setImageDropActive(false),
|
|
2352
|
+
onDrop: (event) => {
|
|
2353
|
+
event.preventDefault();
|
|
2354
|
+
setImageDropActive(false);
|
|
2355
|
+
acceptImageFiles(event.dataTransfer?.files);
|
|
2356
|
+
},
|
|
2357
|
+
},
|
|
2358
|
+
h('p', null, '可直接在此粘贴图片,或拖入 / 选择 PNG、JPEG、WebP。图片会在对话框显示缩略图,点击可放大。'),
|
|
2359
|
+
imageAttachment ? h('div', { className: 'dcAgentPromptImagePreview' },
|
|
2360
|
+
h('button', {
|
|
2361
|
+
type: 'button', className: 'dcAgentPromptImageThumb',
|
|
2362
|
+
'aria-label': `放大查看 ${imageFileName}`,
|
|
2363
|
+
onClick: () => setImageLightbox(true),
|
|
2364
|
+
}, imageAttachment.previewUrl ? h('img', { src: imageAttachment.previewUrl, alt: imageFileName }) : '图片'),
|
|
2365
|
+
h('div', { className: 'dcAgentPromptImageMeta' },
|
|
2366
|
+
h('b', null, imageFileName),
|
|
2367
|
+
h('small', null, imageCommand?.state === 'completed'
|
|
2368
|
+
? `已识别 ${entryCount} 条,下一步逐条核验`
|
|
2369
|
+
: imageCommand?.state === 'running'
|
|
2370
|
+
? '识别中…'
|
|
2371
|
+
: imageCommand?.state === 'failed'
|
|
2372
|
+
? '识别失败,可移除后重试'
|
|
2373
|
+
: '已暂存,可继续选择匹配规则和补全字段'),
|
|
2374
|
+
),
|
|
2375
|
+
h('button', { type: 'button', className: 'dcAgentPromptImageRemove', disabled: busy || imageCommand?.state === 'running', onClick: removeCurrentImage }, '移除'),
|
|
2376
|
+
) : h('label', { className: 'dcAgentPromptFile' },
|
|
2377
|
+
h('span', null, '选择包含企业名单的图片'),
|
|
2378
|
+
h('input', {
|
|
2379
|
+
type: 'file', accept: 'image/png,image/jpeg,image/webp',
|
|
2380
|
+
disabled: busy, onChange: handleFile,
|
|
2381
|
+
}),
|
|
2382
|
+
),
|
|
2383
|
+
imageCommand?.state === 'completed' && entries.length ? h('div', { className: 'dcAgentPromptChoices' },
|
|
2384
|
+
entries.slice(0, 12).map((entry, index) => h('span', { key: `${entry}-${index}`, className: 'dcAgentPromptChoice is-selected' }, `${index + 1}. ${entry}`)),
|
|
2385
|
+
entries.length > 12 ? h('span', { className: 'dcAgentPromptChoice' }, `另 ${entries.length - 12} 条`) : null,
|
|
2386
|
+
) : null,
|
|
2070
2387
|
) : h('label', { className: 'dcAgentPromptFile' },
|
|
2071
2388
|
h('span', null, mode === 'image' ? '选择包含企业名单的图片' : '选择 CSV / XLSX / JSON 名单'),
|
|
2072
2389
|
h('input', {
|
|
@@ -2118,9 +2435,18 @@ window.__ModuleLoader__.load({
|
|
|
2118
2435
|
error ? h('p', { className: 'dcAgentPromptError', role: 'alert' }, error) : null,
|
|
2119
2436
|
h('div', { className: 'dcAgentPromptActions' },
|
|
2120
2437
|
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
|
|
2438
|
+
h('button', { type: 'button', className: 'dcAgentPromptAction is-primary', disabled: busy || imageCommand?.state === 'running', onClick: nextWizard }, busy
|
|
2439
|
+
? '处理中…'
|
|
2440
|
+
: wizardStep === 1 && mode === 'image' && imageCommand?.state === 'running'
|
|
2441
|
+
? '图片识别中…'
|
|
2442
|
+
: wizardStep === 4 ? '回填到对话框' : '下一步'),
|
|
2122
2443
|
),
|
|
2123
2444
|
) : null,
|
|
2445
|
+
imageLightbox && imageAttachment?.previewUrl ? h('button', {
|
|
2446
|
+
type: 'button', className: 'dcAgentImageLightbox',
|
|
2447
|
+
'aria-label': '关闭图片预览',
|
|
2448
|
+
onClick: () => setImageLightbox(false),
|
|
2449
|
+
}, h('img', { src: imageAttachment.previewUrl, alt: imageFileName })) : null,
|
|
2124
2450
|
);
|
|
2125
2451
|
}
|
|
2126
2452
|
|
|
@@ -2143,11 +2469,12 @@ window.__ModuleLoader__.load({
|
|
|
2143
2469
|
);
|
|
2144
2470
|
}
|
|
2145
2471
|
|
|
2146
|
-
/**
|
|
2472
|
+
/** 模型工具 tool.call.toolview 卡片元数据(wire 名 → 展示文案)。 */
|
|
2147
2473
|
const TOOL_VIEW_META = {
|
|
2148
2474
|
data_clean_rows: { icon: '🧹', label: '数据清洗', hint: '去重 · 剔除非法金额 · 缺失补 0' },
|
|
2149
2475
|
data_complete_rows: { icon: '🧩', label: '数据补全', hint: '名称 / 金额 / 手机号归一' },
|
|
2150
2476
|
data_profile: { icon: '🩺', label: '数据体检', hint: '缺失率 · 去重值 · 金额分布' },
|
|
2477
|
+
data_cleaning_extract_image_companies: { icon: '🖼️', label: '图片名单识别', hint: 'OCR · 企业名称 / 信用代码提取 · 人工核验' },
|
|
2151
2478
|
};
|
|
2152
2479
|
const TOOL_VIEW_STATE = { running: '运行中', stopped: '已停止', error: '失败', ok: '完成' };
|
|
2153
2480
|
|
|
@@ -3419,7 +3746,10 @@ window.__ModuleLoader__.load({
|
|
|
3419
3746
|
name: 'conversation.input.overlay',
|
|
3420
3747
|
id: 'data-cleaning-agent-prompt-generator',
|
|
3421
3748
|
order: 110,
|
|
3422
|
-
inject: () => ({
|
|
3749
|
+
inject: () => ({
|
|
3750
|
+
attachImages: (sessionId, files) => attachPromptImages(ctx, sessionId, files),
|
|
3751
|
+
removeImage: (sessionId, attachment) => removePromptImage(ctx, sessionId, attachment),
|
|
3752
|
+
}),
|
|
3423
3753
|
}, PromptGenerator));
|
|
3424
3754
|
|
|
3425
3755
|
// 会话头部恢复入口:用户关闭右栏后可随时重新打开当前任务工作台。
|
|
@@ -3429,9 +3759,9 @@ window.__ModuleLoader__.load({
|
|
|
3429
3759
|
order: 110,
|
|
3430
3760
|
}, WorkbenchHeaderEntry));
|
|
3431
3761
|
|
|
3432
|
-
//
|
|
3762
|
+
// 模型工具的 tool.call.toolview 富化卡片(keyed by wire name,替代裸 JSON 摘要)。
|
|
3433
3763
|
// 说明:分三条独立 inject(而非 generator)——测试 shim 对 inject 回调仅执行一次并 push 其返回值。
|
|
3434
|
-
const toolviewKeys = ['data_clean_rows', 'data_complete_rows', 'data_profile'];
|
|
3764
|
+
const toolviewKeys = ['data_clean_rows', 'data_complete_rows', 'data_profile', 'data_cleaning_extract_image_companies'];
|
|
3435
3765
|
for (const key of toolviewKeys) {
|
|
3436
3766
|
ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
|
|
3437
3767
|
name: 'tool.call.toolview',
|