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/CHANGELOG.md +14 -0
- package/README.en.md +14 -2
- package/README.md +12 -3
- package/docs/COMPATIBILITY.md +14 -0
- package/docs/RELEASE-0.8.0.md +60 -0
- package/docs/UI-WORKFLOW-V2.md +2 -2
- package/docs/USER-GUIDE.md +7 -3
- package/lib/client.js +354 -34
- package/lib/image-intake.js +414 -0
- package/lib/index.js +1 -0
- package/lib/web.js +76 -0
- package/package.json +3 -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,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
|
-
?
|
|
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
|
|
1527
|
-
*
|
|
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
|
|
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 [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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' && !
|
|
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
|
|
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
|
-
/**
|
|
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: () => ({
|
|
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
|
-
//
|
|
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',
|