dsh-data-cleaning-agent 0.6.3 → 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; }
@@ -723,7 +795,21 @@ window.__ModuleLoader__.load({
723
795
  .dcAgentMappingRow > b { text-align: center; color: light-dark(#8792a3, #8290a5); }
724
796
  .dcAgentRulesGrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
725
797
  .dcAgentFieldGroup { display: grid; gap: 7px; margin-top: 12px; }
726
- .dcAgentFieldGroup > b { font-size: 12px; }
798
+ .dcAgentFieldGroupHead { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
799
+ .dcAgentFieldGroupHead > div:first-child { display: grid; gap: 2px; min-width: 0; }
800
+ .dcAgentFieldGroupHead b { font-size: 12px; }
801
+ .dcAgentFieldGroupActions { display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto; }
802
+ .dcAgentFieldAction {
803
+ padding: 2px 7px;
804
+ border: 0;
805
+ border-radius: 6px;
806
+ background: transparent;
807
+ color: light-dark(#2869e6, #8cb3ff);
808
+ font: inherit;
809
+ font-size: 11px;
810
+ cursor: pointer;
811
+ }
812
+ .dcAgentFieldAction:hover, .dcAgentFieldAction:focus-visible { background: light-dark(#edf4ff, #172841); }
727
813
  .dcAgentPreviewTable { margin-top: 12px; overflow: auto; border: 1px solid light-dark(#e4e8f0, #303947); border-radius: 10px; }
728
814
  .dcAgentHistoryTask { display: block; width: 100%; padding: 10px; text-align: left; }
729
815
  .dcAgentHistoryTask h3 { margin: 0; font-size: 12px; }
@@ -831,32 +917,92 @@ window.__ModuleLoader__.load({
831
917
  ['validate_identity', '主体标识校验'],
832
918
  ['complete_fields', '补全所选企业字段'],
833
919
  ];
920
+ const SELF_RISK_FIELD_OPTIONS = [
921
+ ['dishonest', '失信信息'], ['judgment_debtor', '被执行人'], ['consumption_restriction', '限制高消费'],
922
+ ['terminated_case', '终本案件'], ['judicial_document', '裁判文书'], ['case_filing', '立案信息'],
923
+ ['hearing_announcement', '开庭公告'], ['court_announcement', '法院公告'], ['service_notice', '送达公告'],
924
+ ['bankruptcy_reorganization', '破产重整'], ['equity_freeze', '股权冻结'], ['judicial_auction', '司法拍卖'],
925
+ ['valuation_inquiry', '询价评估'], ['pre_litigation_mediation', '诉前调解'], ['exit_restriction', '限制出境'],
926
+ ['administrative_penalty', '行政处罚'], ['operating_exception', '经营异常'], ['serious_violation', '严重违法'],
927
+ ['environmental_penalty', '环保处罚'], ['abnormal_taxpayer', '税务非正常户'], ['tax_arrears', '欠税公告'],
928
+ ['tax_violation', '税收违法'], ['disciplinary_list', '惩戒名单'], ['default_matter', '违约事项'],
929
+ ['guarantee', '担保信息'], ['equity_pledge_registration', '股权出质'], ['stock_pledge', '股权质押'],
930
+ ['chattel_mortgage', '动产抵押'], ['land_mortgage', '土地抵押'], ['simple_cancellation', '简易注销'],
931
+ ['cancellation_filing', '注销备案'], ['liquidation', '清算信息'], ['labor_arbitration', '劳动仲裁'],
932
+ ['public_notice', '公示催告'], ['property_reward_notice', '财产悬赏公告'],
933
+ ];
934
+ const RELATED_RISK_FIELD_OPTIONS = [
935
+ ['dishonest', '失信被执行人'], ['judgment_debtor', '被执行人'], ['consumption_restriction', '限制高消费'],
936
+ ['serious_violation', '严重违法'], ['tax_violation', '税收违法'], ['administrative_penalty', '行政处罚'],
937
+ ['bankruptcy_reorganization', '破产重整'], ['terminated_case', '终本案件'], ['equity_freeze', '股权冻结'],
938
+ ['tax_arrears', '欠税公告'], ['operating_exception', '经营异常'],
939
+ ];
940
+ const RELATED_RISK_KEY_FIELD_OPTIONS = [
941
+ ['dishonest', '失信被执行人'], ['serious_violation', '严重违法'], ['bankruptcy_reorganization', '破产重整'],
942
+ ['tax_violation', '税收违法'], ['judgment_debtor', '被执行人'], ['terminated_case', '终本案件'],
943
+ ['equity_freeze', '股权冻结'],
944
+ ];
834
945
  const FIELD_GROUPS = [
835
- ['identity', '基础工商信息', [
946
+ ['company_registration', '企业工商信息', [
836
947
  ['company_name', '企业名称'], ['credit_no', '统一社会信用代码'], ['reg_no', '注册号'],
837
- ['reg_status', '登记状态'], ['legal_rep', '法定代表人'], ['reg_capital', '注册资本'],
948
+ ['org_no', '组织机构代码'], ['tax_no', '纳税人识别号'], ['reg_status', '登记状态'],
949
+ ['legal_rep', '法定代表人'], ['reg_capital', '注册资本'],
838
950
  ['paid_capital', '实缴资本'], ['establish_date', '成立日期'], ['company_type', '企业类型'],
839
- ['registration_authority', '登记机关'], ['former_name', '曾用名'], ['english_name', '英文名'],
840
- ]],
841
- ['contact', '地址与联系方式', [
842
- ['registered_address', '注册地址'], ['province', '省份地区'], ['city', '城市'],
843
- ['district', '区县'], ['phone', '电话'], ['email', '邮箱'], ['website', '官网'],
844
- ]],
845
- ['operation', '经营信息', [
846
- ['business_scope', '经营范围'], ['industry_category', '国标行业'], ['industry_large', '一级行业'],
847
- ['industry_middle', '二级行业'], ['operating_period', '营业期限'], ['company_size', '企业规模'],
848
- ['company_profile', '企业简介'],
849
- ]],
850
- ['risk', '风险摘要', [
851
- ['risk_summary', '风险摘要'], ['operating_exception', '经营异常摘要'], ['administrative_penalty', '行政处罚摘要'],
852
- ]],
853
- ['ipr', '知识产权摘要', [
854
- ['trademark_summary', '商标摘要'], ['patent_summary', '专利摘要'], ['software_copyright_summary', '软件著作权摘要'],
855
- ]],
951
+ ['approval_date', '核准日期'], ['registration_authority', '登记机关'],
952
+ ['taxpayer_qualification', '纳税人资质'], ['payment_line_no', '支付系统行号'],
953
+ ['import_export_company_code', '进出口企业代码'], ['short_name', '企业简称'], ['english_name', '英文名'],
954
+ ['registered_address', '注册地址'], ['mailing_address', '通信地址'], ['region', '所属地区'],
955
+ ['business_scope', '经营范围'], ['industry_category', '国标行业'],
956
+ ['operating_period', '营业期限'], ['company_size', '人员规模'], ['insured_count', '参保人数'],
957
+ ['branch_insured_count', '分支机构参保人数'],
958
+ ], 'get_company_registration_info'],
959
+ ['company_profile', '企业简介', [
960
+ ['qcc_industry', '企查查行业'], ['company_profile', '企业简介'],
961
+ ['industry_chain_overview', '产业链概览'],
962
+ ], 'get_company_profile'],
963
+ ['contact_info', '联系方式', [
964
+ ['contact_preferred_phone', '首选联系电话'], ['contact_phone_invalid_flag', '首选电话无效标记'],
965
+ ['contact_phone_tags', '首选电话标签'], ['contact_preferred_email', '首选邮箱'],
966
+ ['contact_official_website', '官方网站'], ['contact_official_website_icp', '官网 ICP 备案'],
967
+ ], 'get_contact_info'],
968
+ ['listing_info', '上市信息', [
969
+ ['listing_date', '上市日期'], ['listing_short_name', '股票简称'], ['listing_stock_code', '股票代码'],
970
+ ['listing_exchange', '上市交易所'], ['listing_board', '上市板块'], ['listing_former_short_name', '上市曾用名'],
971
+ ['listing_total_market_value', '总市值'], ['listing_total_shares', '总股本'], ['listing_predicted_pe', '预测市盈率'],
972
+ ['listing_float_market_value', '流通值'], ['listing_float_shares', '流通股'], ['listing_pb_ratio', '市净率'],
973
+ ['listing_eps', 'EPS'], ['listing_voting_rights_difference', '表决权差异'],
974
+ ['listing_registration_based', '是否注册制'],
975
+ ], 'get_listing_info'],
976
+ ['tax_invoice_info', '税务开票信息', [
977
+ ['tax_company_name', '税务主体名称'], ['tax_identification_no', '税务纳税人识别号'],
978
+ ['tax_company_type', '税务企业类型'], ['tax_business_status', '税务经营状态'],
979
+ ['invoice_address', '开票地址'], ['invoice_phone', '开票联系电话'],
980
+ ['invoice_bank', '开户行'], ['invoice_bank_account', '开户行账号'],
981
+ ], 'get_tax_invoice_info'],
982
+ ['import_export_credit', '进出口信用', [
983
+ ['import_export_credit_no', '进出口统一社会信用代码'], ['import_export_customs', '所在地海关'],
984
+ ['import_export_admin_division', '进出口行政区划'], ['import_export_address', '进出口备案地址'],
985
+ ['import_export_economic_area', '经济区划'], ['import_export_trade_type', '经营类别'],
986
+ ['import_export_statistical_economic_area', '统计经济区划'], ['import_export_industry', '进出口行业种类'],
987
+ ['import_export_ecommerce_type', '跨境贸易电子商务类型'], ['import_export_credit_grade', '海关信用等级'],
988
+ ['import_export_filing_date', '进出口备案日期'],
989
+ ], 'get_import_export_credit'],
990
+ ['company_risk_scan', '企业自身风险扫描', [
991
+ ['risk_recorded_factor_count', '风险有记录因子数'], ['risk_no_record_factor_count', '风险无记录因子数'],
992
+ ['risk_hit_summary', '企业自身风险命中摘要'],
993
+ ...SELF_RISK_FIELD_OPTIONS.map(([id, label]) => [`risk_${id}_count`, `${label}条目数`]),
994
+ ], 'get_company_risk_scan'],
995
+ ['company_related_risk_scan', '企业关联风险扫描', [
996
+ ['related_risk_party_count', '有风险关联方数'], ['related_risk_summary', '企业关联风险摘要'],
997
+ ...RELATED_RISK_FIELD_OPTIONS.map(([id, label]) => [`related_risk_${id}_count`, `关联风险-${label}条目数`]),
998
+ ...RELATED_RISK_KEY_FIELD_OPTIONS.map(([id, label]) => [`related_risk_${id}_party_count`, `关联风险-${label}命中关联方数`]),
999
+ ], 'get_company_related_risk_scan'],
856
1000
  ];
857
1001
  const ENRICHMENT_OPTIONS = FIELD_GROUPS.flatMap(([, , fields]) => fields);
1002
+ const INPUT_ONLY_MAPPING_OPTIONS = [['phone', '联系电话']];
858
1003
  const RESULT_FIELD_LABELS = new Map([
859
1004
  ...ENRICHMENT_OPTIONS,
1005
+ ...INPUT_ONLY_MAPPING_OPTIONS,
860
1006
  ['biz_status', '经营状态'],
861
1007
  ['risk_tags', '风险标签'],
862
1008
  ['qcc_match_status', '匹配状态'],
@@ -908,7 +1054,10 @@ window.__ModuleLoader__.load({
908
1054
  const executionDraft = value.startsWith('请执行已在「数据清洗补全工作台」确认的企业数据任务。')
909
1055
  && value.includes('安全任务凭证:dcq-')
910
1056
  && value.includes('data_cleaning_qcc_run');
911
- 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;
912
1061
  }
913
1062
 
914
1063
  function clearCleaningDraft(ctx, sessionId, onlyDefault = false) {
@@ -1045,7 +1194,7 @@ window.__ModuleLoader__.load({
1045
1194
  : [];
1046
1195
  const entryCount = Number.isFinite(config.entryCount) ? config.entryCount : entries.length;
1047
1196
  const source = mode === 'image'
1048
- ? `已附加图片${config.fileName ? `「${config.fileName}」` : ''}`
1197
+ ? `图片${config.fileName ? `「${config.fileName}」` : ''},已识别并人工核验 ${entryCount} 条主体标识`
1049
1198
  : mode === 'excel'
1050
1199
  ? `本地表格${config.fileName ? `「${config.fileName}」` : ''},已解析 ${entryCount} 条主体标识,完整数据已载入数据清洗补全工作台`
1051
1200
  : `手工录入,共 ${entryCount} 条主体标识`;
@@ -1053,9 +1202,6 @@ window.__ModuleLoader__.load({
1053
1202
  '请执行一项企业名单数据清洗补全任务。',
1054
1203
  `输入来源:${source}。`,
1055
1204
  ];
1056
- if (mode === 'image') {
1057
- lines.push('请先使用当前已连接且可用的企查查智能文档解析 MCP 能力,从图片中提取企业全称或统一社会信用代码;若该能力未连接或不可用,请先提示我改用文本或 Excel。');
1058
- }
1059
1205
  if (entries.length) {
1060
1206
  lines.push(`${mode === 'excel' ? '主体预览' : '待处理主体'}:\n${entries.map((entry, index) => `${index + 1}. ${entry}`).join('\n')}`);
1061
1207
  }
@@ -1104,7 +1250,6 @@ window.__ModuleLoader__.load({
1104
1250
  ['credit_no', /^(credit_no|creditCode|unified_credit_code|统一社会信用代码|信用代码)$/i],
1105
1251
  ['reg_no', /^(reg_no|registration_no|注册号)$/i],
1106
1252
  ['phone', /^(phone|mobile|tel|telephone|联系电话|手机号码|手机号)$/i],
1107
- ['province', /^(province|省份|省份地区)$/i],
1108
1253
  ['registered_address', /^(address|registered_address|注册地址|企业地址|公司地址)$/i],
1109
1254
  ];
1110
1255
 
@@ -1450,8 +1595,8 @@ window.__ModuleLoader__.load({
1450
1595
  }
1451
1596
 
1452
1597
  /**
1453
- * 图片接入兼容 Bridge。仅在运行时同时探测到 createDraftImages / input.shell.addImages
1454
- * 时使用;失败会释放浏览器临时对象,不假设 alpha 实验 API 稳定。
1598
+ * 图片接入兼容 Bridge。返回官方 ComposerAttachment,供向导复用其 previewUrl
1599
+ * 显示 64px 缩略图;失败会释放浏览器临时对象,不假设 alpha 实验 API 稳定。
1455
1600
  */
1456
1601
  function attachPromptImages(ctx, sessionId, files) {
1457
1602
  const conversation = typeof ctx.get === 'function' ? ctx.get('conversation') : ctx.conversation;
@@ -1467,7 +1612,17 @@ window.__ModuleLoader__.load({
1467
1612
  conversation.releaseDraftImages?.(images);
1468
1613
  throw new Error('图片未能加入当前对话,请检查格式或数量限制。');
1469
1614
  }
1470
- 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;
1471
1626
  }
1472
1627
 
1473
1628
  async function sendQccAgentCommand(ctx, sessionId, prompt) {
@@ -1800,8 +1955,30 @@ window.__ModuleLoader__.load({
1800
1955
  }).filter(Boolean).slice(0, 50);
1801
1956
  }
1802
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
+
1803
1980
  function PromptGenerator(props) {
1804
- const { sessionId, inputActions, attachImages } = props;
1981
+ const { sessionId, inputActions, attachImages, removeImage } = props;
1805
1982
  const enabled = useCleaningSession(sessionId);
1806
1983
  const [open, setOpen] = react.useState(false);
1807
1984
  const [wizardStep, setWizardStep] = react.useState(1);
@@ -1811,7 +1988,11 @@ window.__ModuleLoader__.load({
1811
1988
  const [entryCount, setEntryCount] = react.useState(0);
1812
1989
  const [spreadsheetFileName, setSpreadsheetFileName] = react.useState('');
1813
1990
  const [imageFileName, setImageFileName] = react.useState('');
1814
- 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);
1815
1996
  const [cleaningKeys, setCleaningKeys] = react.useState(DEFAULT_CLEANING_KEYS);
1816
1997
  const [enrichmentKeys, setEnrichmentKeys] = react.useState(DEFAULT_ENRICHMENT_KEYS);
1817
1998
  const [anchorKeys, setAnchorKeys] = react.useState(['company_name', 'credit_no']);
@@ -1824,38 +2005,127 @@ window.__ModuleLoader__.load({
1824
2005
  const [busy, setBusy] = react.useState(false);
1825
2006
  const [error, setError] = react.useState(null);
1826
2007
 
1827
- 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]);
1828
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
+ };
1829
2065
  const setSourceMode = (nextMode) => {
2066
+ if (mode === 'image' && nextMode !== 'image') {
2067
+ if (imageCommand?.state === 'running') {
2068
+ setError('图片正在识别,请等待完成后再切换数据来源。');
2069
+ return;
2070
+ }
2071
+ discardCurrentImage();
2072
+ }
1830
2073
  setMode(nextMode);
1831
2074
  setError(null);
1832
2075
  };
1833
2076
  const toggle = (setter, values, key) => setter(values.includes(key)
1834
2077
  ? values.filter((item) => item !== key)
1835
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
+ };
1836
2107
  const handleFile = async (event) => {
1837
2108
  const file = event.target?.files?.[0];
1838
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
+ }
1839
2116
  setBusy(true);
1840
2117
  setError(null);
1841
2118
  try {
1842
- if (mode === 'image') {
1843
- if (typeof attachImages !== 'function') throw new Error('当前 DSH 没有可用的图片接入 Bridge。');
1844
- await attachImages(sessionId, [file]);
1845
- setImageFileName(file.name);
1846
- setImageAttached(true);
1847
- } else {
1848
- const result = await parseFile(file);
1849
- if (!result || result.ok === false) throw new Error(result?.message || result?.error || '表格解析失败');
1850
- const extracted = extractPromptEntries(result);
1851
- if (!extracted.length) throw new Error('未识别到企业名称或统一社会信用代码列,请检查表头。');
1852
- setEntries(extracted);
1853
- setEntryCount(Number.isFinite(result.rowCount) ? result.rowCount : (Array.isArray(result.rows) ? result.rows.length : extracted.length));
1854
- setSpreadsheetFileName(file.name);
1855
- requestWorkbenchDataset(result, sessionId, {
1856
- type: result.fmt || 'xlsx', fileName: file.name, sizeBytes: file.size || 0,
1857
- });
1858
- }
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
+ });
1859
2129
  } catch (fileError) {
1860
2130
  setError(fileError instanceof Error ? fileError.message : String(fileError));
1861
2131
  } finally {
@@ -1870,7 +2140,9 @@ window.__ModuleLoader__.load({
1870
2140
  const values = selectedEntries();
1871
2141
  if (mode === 'text' && !values.length) return '请先录入企业名称或统一社会信用代码,每行一条。';
1872
2142
  if (mode === 'excel' && !values.length) return '请先上传并解析 Excel / CSV / JSON 文件。';
1873
- 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 '图片中未提取到可核验的企业名称或统一社会信用代码。';
1874
2146
  return null;
1875
2147
  };
1876
2148
  const promptConfig = () => ({
@@ -1911,6 +2183,22 @@ window.__ModuleLoader__.load({
1911
2183
 
1912
2184
  const nextWizard = () => {
1913
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
+ }
1914
2202
  const sourceError = validateSource();
1915
2203
  if (sourceError) { setError(sourceError); return; }
1916
2204
  }
@@ -1920,6 +2208,47 @@ window.__ModuleLoader__.load({
1920
2208
  generatePrompt();
1921
2209
  };
1922
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
+
1923
2252
  const choice = (options, values, setter) => options.map(([key, label]) => h('label', {
1924
2253
  key,
1925
2254
  className: `dcAgentPromptChoice${values.includes(key) ? ' is-selected' : ''}`,
@@ -1931,6 +2260,15 @@ window.__ModuleLoader__.load({
1931
2260
  }),
1932
2261
  label,
1933
2262
  ));
2263
+ const setPromptFieldGroup = (fields, enabledGroup) => {
2264
+ const groupIds = new Set(fields.map(([id]) => id));
2265
+ const selected = new Set(enrichmentKeys);
2266
+ for (const id of groupIds) {
2267
+ if (enabledGroup) selected.add(id); else selected.delete(id);
2268
+ }
2269
+ const catalogOrder = FIELD_GROUPS.flatMap(([, , groupFields]) => groupFields.map(([id]) => id));
2270
+ setEnrichmentKeys(catalogOrder.filter((id) => selected.has(id)));
2271
+ };
1934
2272
 
1935
2273
  return h('div', { className: 'dcAgentPromptLayer' },
1936
2274
  h('button', {
@@ -1985,6 +2323,55 @@ window.__ModuleLoader__.load({
1985
2323
  placeholder: '企查查科技股份有限公司\n9132…\n某某信息技术(上海)有限公司',
1986
2324
  onChange: (event) => setRawText(event.target.value),
1987
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,
1988
2375
  ) : h('label', { className: 'dcAgentPromptFile' },
1989
2376
  h('span', null, mode === 'image' ? '选择包含企业名单的图片' : '选择 CSV / XLSX / JSON 名单'),
1990
2377
  h('input', {
@@ -2008,13 +2395,22 @@ window.__ModuleLoader__.load({
2008
2395
  ) : null,
2009
2396
  wizardStep === 3 ? h('div', { className: 'dcAgentWizardPane' },
2010
2397
  h('h4', null, '选择清洗目标和补全字段'),
2011
- h('p', null, '仅展示当前企业数据范围;最终可用字段以当前用户连接能力为准。'),
2398
+ h('p', null, '共 128 个一企一行字段:基础 30、第一批 40、第二批 58。最终可用字段以当前用户连接的 QCC MCP 能力为准。'),
2012
2399
  h('div', { className: 'dcAgentPromptGroup' },
2013
2400
  h('b', null, '清洗目标'),
2014
2401
  h('div', { className: 'dcAgentPromptChoices' }, choice(CLEANING_OPTIONS, cleaningKeys, setCleaningKeys)),
2015
2402
  ),
2016
- FIELD_GROUPS.map(([groupId, label, fields]) => h('div', { className: 'dcAgentPromptGroup', key: groupId },
2017
- h('b', null, label),
2403
+ FIELD_GROUPS.map(([groupId, label, fields, sourceTool]) => h('div', { className: 'dcAgentPromptGroup', key: groupId },
2404
+ h('div', { className: 'dcAgentFieldGroupHead' },
2405
+ h('div', null,
2406
+ h('b', null, `${label} · ${fields.filter(([id]) => enrichmentKeys.includes(id)).length}/${fields.length}`),
2407
+ sourceTool ? h('small', { className: 'dcAgentHint' }, `来源:${sourceTool}`) : null,
2408
+ ),
2409
+ h('div', { className: 'dcAgentFieldGroupActions' },
2410
+ h('button', { type: 'button', className: 'dcAgentFieldAction', onClick: () => setPromptFieldGroup(fields, true) }, '全选'),
2411
+ h('button', { type: 'button', className: 'dcAgentFieldAction', onClick: () => setPromptFieldGroup(fields, false) }, '清空'),
2412
+ ),
2413
+ ),
2018
2414
  h('div', { className: 'dcAgentPromptChoices' }, choice(fields, enrichmentKeys, setEnrichmentKeys)),
2019
2415
  )),
2020
2416
  ) : null,
@@ -2027,9 +2423,20 @@ window.__ModuleLoader__.load({
2027
2423
  error ? h('p', { className: 'dcAgentPromptError', role: 'alert' }, error) : null,
2028
2424
  h('div', { className: 'dcAgentPromptActions' },
2029
2425
  h('button', { type: 'button', className: 'dcAgentPromptAction', disabled: wizardStep === 1, onClick: () => setWizardStep(Math.max(1, wizardStep - 1)) }, '上一步'),
2030
- 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 ? '回填到对话框' : '下一步'),
2031
2433
  ),
2032
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,
2033
2440
  );
2034
2441
  }
2035
2442
 
@@ -2052,11 +2459,12 @@ window.__ModuleLoader__.load({
2052
2459
  );
2053
2460
  }
2054
2461
 
2055
- /** M3 · 三工具 tool.call.toolview 卡片元数据(wire 名 → 展示文案)。 */
2462
+ /** 模型工具 tool.call.toolview 卡片元数据(wire 名 → 展示文案)。 */
2056
2463
  const TOOL_VIEW_META = {
2057
2464
  data_clean_rows: { icon: '🧹', label: '数据清洗', hint: '去重 · 剔除非法金额 · 缺失补 0' },
2058
2465
  data_complete_rows: { icon: '🧩', label: '数据补全', hint: '名称 / 金额 / 手机号归一' },
2059
2466
  data_profile: { icon: '🩺', label: '数据体检', hint: '缺失率 · 去重值 · 金额分布' },
2467
+ data_cleaning_extract_image_companies: { icon: '🖼️', label: '图片名单识别', hint: 'OCR · 企业名称 / 信用代码提取 · 人工核验' },
2060
2468
  };
2061
2469
  const TOOL_VIEW_STATE = { running: '运行中', stopped: '已停止', error: '失败', ok: '完成' };
2062
2470
 
@@ -2540,14 +2948,18 @@ window.__ModuleLoader__.load({
2540
2948
 
2541
2949
  const estimateQcc = async () => {
2542
2950
  const uniqueCompanies = new Set(runtime.rows.map((row) => String(row?.[nameField] ?? '').trim()).filter(Boolean)).size;
2543
- const requiresProfile = fieldSelection.some((field) => ['company_profile', 'industry_large', 'industry_middle'].includes(field));
2544
- const callsPerCompany = requiresProfile ? 3 : 2;
2951
+ const selected = fieldSelection.length ? fieldSelection : DEFAULT_ENRICHMENT_KEYS;
2952
+ const selectedGroups = uiFieldGroups.filter(([, , fields]) => fields.some(([id]) => selected.includes(id)));
2953
+ const sourceTools = [...new Set(selectedGroups.map(([, , , sourceTool]) => sourceTool).filter(Boolean))];
2954
+ const callsPerCompany = 1 + sourceTools.length;
2955
+ const estimatedCalls = uniqueCompanies * callsPerCompany;
2545
2956
  actions.setQccEstimate({
2546
2957
  uniqueCompanies,
2547
- tools: requiresProfile ? ['主体查询', '工商信息', '企业画像'] : ['主体查询', '工商信息'],
2548
- estimatedCalls: uniqueCompanies * callsPerCompany,
2958
+ tools: ['主体查询', ...selectedGroups.map(([, label]) => label)],
2959
+ sourceTools,
2960
+ estimatedCalls,
2549
2961
  maxCalls: 300,
2550
- withinLimit: runtime.rows.length <= 100,
2962
+ withinLimit: runtime.rows.length <= 100 && estimatedCalls <= 300,
2551
2963
  estimateType: 'upper-bound',
2552
2964
  });
2553
2965
  };
@@ -2889,9 +3301,20 @@ window.__ModuleLoader__.load({
2889
3301
  h('b', { className: tone ? `is-${tone}` : null }, displayStatValue(value))
2890
3302
  );
2891
3303
  const uiFieldGroups = Array.isArray(workflowContract?.fieldCatalog)
2892
- ? workflowContract.fieldCatalog.map((group) => [group.id, group.label, (group.fields || []).map((field) => [field.id, field.label])])
3304
+ ? workflowContract.fieldCatalog.map((group) => [group.id, group.label, (group.fields || []).map((field) => [field.id, field.label]), group.sourceTool])
2893
3305
  : FIELD_GROUPS;
2894
- const targetFields = uiFieldGroups.flatMap(([, , fields]) => fields);
3306
+ const inputOnlyMappingFields = Array.isArray(workflowContract?.inputOnlyMappingFields)
3307
+ ? workflowContract.inputOnlyMappingFields.map((field) => [field.id, field.label])
3308
+ : INPUT_ONLY_MAPPING_OPTIONS;
3309
+ const targetFields = [...uiFieldGroups.flatMap(([, , fields]) => fields), ...inputOnlyMappingFields];
3310
+ const setWorkbenchFieldGroup = (fields, enabledGroup) => {
3311
+ const selected = new Set(fieldSelection);
3312
+ for (const [id] of fields) {
3313
+ if (enabledGroup) selected.add(id); else selected.delete(id);
3314
+ }
3315
+ const catalogOrder = uiFieldGroups.flatMap(([, , groupFields]) => groupFields.map(([id]) => id));
3316
+ actions.setFieldSelection(catalogOrder.filter((id) => selected.has(id)));
3317
+ };
2895
3318
 
2896
3319
  let pane;
2897
3320
  if (step === 'history') {
@@ -2964,8 +3387,22 @@ window.__ModuleLoader__.load({
2964
3387
  ),
2965
3388
  h('section', { className: 'dcAgentSection' },
2966
3389
  h('h3', null, `输出字段 · 已选 ${fieldSelection.length}`),
2967
- uiFieldGroups.map(([groupId, label, fields]) => h('div', { key: groupId, className: 'dcAgentFieldGroup' },
2968
- h('b', null, label),
3390
+ h('div', { className: 'dcAgentRow', 'aria-label': '补全字段批次概览' },
3391
+ h('span', { className: 'dcAgentChip' }, '基础字段 30'),
3392
+ h('span', { className: 'dcAgentChip' }, '第一批 40'),
3393
+ h('span', { className: 'dcAgentChip' }, '第二批 58'),
3394
+ ),
3395
+ uiFieldGroups.map(([groupId, label, fields, sourceTool]) => h('div', { key: groupId, className: 'dcAgentFieldGroup' },
3396
+ h('div', { className: 'dcAgentFieldGroupHead' },
3397
+ h('div', null,
3398
+ h('b', null, `${label} · ${fields.filter(([id]) => fieldSelection.includes(id)).length}/${fields.length}`),
3399
+ sourceTool ? h('small', { className: 'dcAgentHint' }, `来源:${sourceTool}`) : null,
3400
+ ),
3401
+ h('div', { className: 'dcAgentFieldGroupActions' },
3402
+ h('button', { type: 'button', className: 'dcAgentFieldAction', onClick: () => setWorkbenchFieldGroup(fields, true) }, '全选'),
3403
+ h('button', { type: 'button', className: 'dcAgentFieldAction', onClick: () => setWorkbenchFieldGroup(fields, false) }, '清空'),
3404
+ ),
3405
+ ),
2969
3406
  h('div', { className: 'dcAgentChips' }, fields.map(([key, fieldLabel]) => h('label', { key, className: `dcAgentChip${fieldSelection.includes(key) ? ' is-selected' : ''}` },
2970
3407
  h('input', { type: 'checkbox', checked: fieldSelection.includes(key), onChange: () => actions.toggleField(key) }), ` ${fieldLabel}`,
2971
3408
  ))),
@@ -3016,14 +3453,14 @@ window.__ModuleLoader__.load({
3016
3453
  );
3017
3454
  } else if (step === 'match') {
3018
3455
  pane = h('div', { className: 'dcAgentPane' },
3019
- h('p', { className: 'dcAgentHint' }, '通过当前用户连接的企查查 MCP 定位企业主体并补全基础工商字段。多候选始终由人工选择;界面不生成虚构置信度。'),
3456
+ h('p', { className: 'dcAgentHint' }, '通过当前用户连接的企查查 MCP 定位企业主体,并按已选维度补全一企一行字段。多候选始终由人工选择;界面不生成虚构置信度。'),
3020
3457
  h('section', { className: 'dcAgentSection' },
3021
3458
  h('h3', null, '匹配依据'),
3022
- h('div', { className: 'dcAgentChips' }, mappings.filter((mapping) => ['company_name', 'credit_no', 'reg_no'].includes(mapping.targetField)).map((mapping) => h('span', { key: mapping.sourceField, className: 'dcAgentChip' }, `${mapping.sourceField} → ${ENRICHMENT_OPTIONS.find(([id]) => id === mapping.targetField)?.[1] || mapping.targetField}`))),
3459
+ h('div', { className: 'dcAgentChips' }, mappings.filter((mapping) => ['company_name', 'credit_no', 'reg_no'].includes(mapping.targetField)).map((mapping) => h('span', { key: mapping.sourceField, className: 'dcAgentChip' }, `${mapping.sourceField} → ${resultFieldLabel(mapping.targetField)}`))),
3023
3460
  h('p', { className: 'dcAgentHint' }, '统一社会信用代码等强标识优先;名称匹配不明确时进入人工核验队列。'),
3024
3461
  ),
3025
3462
  h('section', { className: 'dcAgentSection' },
3026
- h('h3', null, '企查查基础企业能力'),
3463
+ h('h3', null, '企查查一企一行补全能力'),
3027
3464
  h('div', { className: 'dcAgentRow' },
3028
3465
  h('button', { type: 'button', className: 'dcAgentButton', disabled: busy, onClick: loadQccCapabilities }, busy ? '检测中…' : '检测企查查连接'),
3029
3466
  qccCapabilities ? h('span', { className: 'dcAgentHint' }, qccCapabilities.capabilities?.ready ? '主体查询与工商信息工具已就绪' : '连接未就绪或基础工具不完整') : null,
@@ -3083,7 +3520,7 @@ window.__ModuleLoader__.load({
3083
3520
  h('p', { className: 'dcAgentHint' }, '执行本地确定性清洗并核对本任务所选补全字段。外部字段以当前用户企查查 MCP 的真实返回为准,缺失值不会被编造。'),
3084
3521
  h('section', { className: 'dcAgentSection' },
3085
3522
  h('h3', null, `已选补全字段 · ${fieldSelection.length}`),
3086
- h('div', { className: 'dcAgentChips' }, fieldSelection.map((id) => h('span', { className: 'dcAgentChip', key: id }, ENRICHMENT_OPTIONS.find(([fieldId]) => fieldId === id)?.[1] || id))),
3523
+ h('div', { className: 'dcAgentChips' }, fieldSelection.map((id) => h('span', { className: 'dcAgentChip', key: id }, resultFieldLabel(id)))),
3087
3524
  ),
3088
3525
  h('section', { className: 'dcAgentSection' },
3089
3526
  h('h3', null, '本地清洗预处理'),
@@ -3299,7 +3736,10 @@ window.__ModuleLoader__.load({
3299
3736
  name: 'conversation.input.overlay',
3300
3737
  id: 'data-cleaning-agent-prompt-generator',
3301
3738
  order: 110,
3302
- 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
+ }),
3303
3743
  }, PromptGenerator));
3304
3744
 
3305
3745
  // 会话头部恢复入口:用户关闭右栏后可随时重新打开当前任务工作台。
@@ -3309,9 +3749,9 @@ window.__ModuleLoader__.load({
3309
3749
  order: 110,
3310
3750
  }, WorkbenchHeaderEntry));
3311
3751
 
3312
- // M3:三个工具的 tool.call.toolview 富化卡片(keyed by wire name,替代裸 JSON 摘要)。
3752
+ // 模型工具的 tool.call.toolview 富化卡片(keyed by wire name,替代裸 JSON 摘要)。
3313
3753
  // 说明:分三条独立 inject(而非 generator)——测试 shim 对 inject 回调仅执行一次并 push 其返回值。
3314
- 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'];
3315
3755
  for (const key of toolviewKeys) {
3316
3756
  ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
3317
3757
  name: 'tool.call.toolview',