dsh-data-cleaning-agent 0.5.3 → 0.6.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 +46 -0
- package/README.en.md +8 -3
- package/README.md +8 -3
- package/docs/COMPATIBILITY.md +24 -4
- package/docs/RELEASE-0.5.3.md +10 -3
- package/docs/RELEASE-0.6.0.md +46 -0
- package/docs/UI-WORKFLOW-V2-ACCEPTANCE.md +84 -0
- package/docs/UI-WORKFLOW-V2-MIGRATION.md +62 -0
- package/docs/UI-WORKFLOW-V2.md +174 -0
- package/docs/USER-GUIDE.md +11 -8
- package/lib/artifacts.js +239 -0
- package/lib/client.js +1037 -259
- package/lib/index.js +1 -1
- package/lib/web.js +214 -0
- package/lib/workflow-contract.js +263 -0
- package/lib/workflow.js +452 -0
- package/package.json +6 -2
package/lib/client.js
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* - 提示词生成器使用 `conversation.input.overlay`,只对本插件创建的会话生效。
|
|
14
14
|
* Hero 标题没有公开替换槽位,故以可恢复、精确文本匹配的 DOM Bridge 对齐业务首页。
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* v2 工作台把上传、规则、匹配、补全、下载正式绑定到 Host taskId 工作流;原始行只保留
|
|
17
|
+
* 在浏览器内按 taskId 隔离的 runtime 中,Host 仅持久化任务元数据、摘要与制品引用。
|
|
18
|
+
* 当前业务闭环使用基础企业 G5 Bridge;历史、人员、招投标域仍按产品决策延期。
|
|
18
19
|
*/
|
|
19
20
|
window.__ModuleLoader__.load({
|
|
20
21
|
id: 'dsh-data-cleaning-agent',
|
|
@@ -179,7 +180,7 @@ window.__ModuleLoader__.load({
|
|
|
179
180
|
|
|
180
181
|
.dcAgentProductHome {
|
|
181
182
|
width: 100%;
|
|
182
|
-
max-width:
|
|
183
|
+
max-width: 980px;
|
|
183
184
|
box-sizing: border-box;
|
|
184
185
|
margin: 10px auto 0;
|
|
185
186
|
padding: 16px 18px;
|
|
@@ -202,7 +203,7 @@ window.__ModuleLoader__.load({
|
|
|
202
203
|
}
|
|
203
204
|
.dcAgentHomeFlow {
|
|
204
205
|
display: grid;
|
|
205
|
-
grid-template-columns: repeat(
|
|
206
|
+
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
206
207
|
gap: 8px;
|
|
207
208
|
}
|
|
208
209
|
.dcAgentHomeStep {
|
|
@@ -675,6 +676,39 @@ window.__ModuleLoader__.load({
|
|
|
675
676
|
line-height: 1.6;
|
|
676
677
|
color: light-dark(#33415a, #c9d4e5);
|
|
677
678
|
}
|
|
679
|
+
.dcAgentHomeHero { display: flex; align-items: center; justify-content: space-between; gap: 18px; }
|
|
680
|
+
.dcAgentHomeHero p { margin: 5px 0 0; color: light-dark(#697386, #aab5c7); font-size: 12px; line-height: 18px; }
|
|
681
|
+
.dcAgentHomeEyebrow { display: block; margin-bottom: 4px; color: light-dark(#2869e6, #8cb3ff); font-size: 11px; font-weight: 700; letter-spacing: .08em; }
|
|
682
|
+
.dcAgentHomeActions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
|
|
683
|
+
.dcAgentHomeActions button,
|
|
684
|
+
.dcAgentHomeRecent button,
|
|
685
|
+
.dcAgentHistoryTask { border: 1px solid light-dark(#dce4f1, #344052); border-radius: 9px; background: light-dark(#fff, #1b222e); color: inherit; font: inherit; cursor: pointer; }
|
|
686
|
+
.dcAgentHomeActions button { padding: 6px 9px; font-size: 11px; }
|
|
687
|
+
.dcAgentHomeRecent { display: grid; gap: 6px; margin-top: 12px; }
|
|
688
|
+
.dcAgentHomeRecent > b { font-size: 12px; }
|
|
689
|
+
.dcAgentHomeRecent button { display: flex; justify-content: space-between; gap: 12px; padding: 7px 9px; text-align: left; font-size: 11px; }
|
|
690
|
+
.dcAgentHomeRecent small { color: light-dark(#697386, #aab5c7); }
|
|
691
|
+
.dcAgentWizardNav { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-bottom: 12px; }
|
|
692
|
+
.dcAgentWizardStep { display: flex; align-items: center; gap: 6px; padding: 7px; border: 1px solid light-dark(#d9dee8, #394352); border-radius: 9px; background: transparent; color: inherit; font: inherit; font-size: 11px; cursor: pointer; }
|
|
693
|
+
.dcAgentWizardStep b { display: grid; place-items: center; width: 20px; height: 20px; border-radius: 50%; background: light-dark(#eef1f6, #252e3b); color: light-dark(#607088, #b9c6da); }
|
|
694
|
+
.dcAgentWizardStep.is-active { border-color: #2869e6; background: light-dark(#edf4ff, #172841); color: light-dark(#1556cf, #8cb3ff); }
|
|
695
|
+
.dcAgentWizardStep.is-active b { background: #2869e6; color: white; }
|
|
696
|
+
.dcAgentWizardPane { min-height: 230px; }
|
|
697
|
+
.dcAgentWizardPane h4 { margin: 0 0 3px; font-size: 14px; }
|
|
698
|
+
.dcAgentWizardPane > p { margin: 0 0 12px; color: light-dark(#697386, #aab5c7); font-size: 11px; line-height: 17px; }
|
|
699
|
+
.dcAgentPromptRules { display: grid; gap: 8px; margin-top: 14px; }
|
|
700
|
+
.dcAgentPromptRules label { font-size: 12px; }
|
|
701
|
+
.dcAgentPromptPreview { max-height: 260px; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 10px; background: light-dark(#f6f8fb, #11171f); white-space: pre-wrap; word-break: break-word; font: inherit; font-size: 11px; line-height: 18px; }
|
|
702
|
+
.dcAgentFormField { display: grid; gap: 6px; font-size: 12px; font-weight: 600; }
|
|
703
|
+
.dcAgentMappingRow { display: grid; grid-template-columns: minmax(0, 1fr) 24px minmax(180px, 1fr); align-items: center; gap: 8px; margin-top: 7px; font-size: 12px; }
|
|
704
|
+
.dcAgentMappingRow > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
705
|
+
.dcAgentMappingRow > b { text-align: center; color: light-dark(#8792a3, #8290a5); }
|
|
706
|
+
.dcAgentRulesGrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
|
707
|
+
.dcAgentFieldGroup { display: grid; gap: 7px; margin-top: 12px; }
|
|
708
|
+
.dcAgentFieldGroup > b { font-size: 12px; }
|
|
709
|
+
.dcAgentPreviewTable { margin-top: 12px; overflow: auto; border: 1px solid light-dark(#e4e8f0, #303947); border-radius: 10px; }
|
|
710
|
+
.dcAgentHistoryTask { display: block; width: 100%; padding: 10px; text-align: left; }
|
|
711
|
+
.dcAgentHistoryTask h3 { margin: 0; font-size: 12px; }
|
|
678
712
|
@media (max-width: 760px) {
|
|
679
713
|
.dcAgentWorkbench, .dcAgentWorkbench.is-expanded { width: 100vw; border-radius: 0; }
|
|
680
714
|
.dcAgentOverlay { background: rgba(8, 10, 14, 0.22); pointer-events: auto; }
|
|
@@ -685,6 +719,11 @@ window.__ModuleLoader__.load({
|
|
|
685
719
|
.dcAgentCapability { min-width: 92px; }
|
|
686
720
|
.dcAgentPromptPanel { left: 8px; width: calc(100vw - 88px); max-height: 72vh; }
|
|
687
721
|
.dcAgentHomeFlow { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
722
|
+
.dcAgentHomeHero { align-items: flex-start; flex-direction: column; }
|
|
723
|
+
.dcAgentWizardNav { grid-template-columns: repeat(2, 1fr); }
|
|
724
|
+
.dcAgentRulesGrid { grid-template-columns: 1fr; }
|
|
725
|
+
.dcAgentMappingRow { grid-template-columns: 1fr; }
|
|
726
|
+
.dcAgentMappingRow > b { display: none; }
|
|
688
727
|
}
|
|
689
728
|
`;
|
|
690
729
|
|
|
@@ -698,46 +737,89 @@ window.__ModuleLoader__.load({
|
|
|
698
737
|
return () => { style.remove(); };
|
|
699
738
|
}
|
|
700
739
|
|
|
701
|
-
//
|
|
702
|
-
|
|
703
|
-
|
|
740
|
+
// 原始数据不进入 Host storageDomain 或模型上下文;只在当前页面按 taskId 隔离。
|
|
741
|
+
const runtimeTasks = new Map();
|
|
742
|
+
const workflowTaskBySession = new Map();
|
|
743
|
+
const workflowTaskCreationBySession = new Map();
|
|
744
|
+
const workflowOperationBySession = new Map();
|
|
745
|
+
|
|
746
|
+
function runtimeFor(taskId, create = true) {
|
|
747
|
+
const key = String(taskId || 'unassigned');
|
|
748
|
+
if (!runtimeTasks.has(key) && create) {
|
|
749
|
+
runtimeTasks.set(key, {
|
|
750
|
+
rows: [],
|
|
751
|
+
headers: [],
|
|
752
|
+
source: null,
|
|
753
|
+
lastCsv: { clean: null, complete: null, qcc: null, review: null },
|
|
754
|
+
resultRows: { clean: null, complete: null, qcc: null },
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
return runtimeTasks.get(key) ?? null;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function moveRuntime(fromId, toId) {
|
|
761
|
+
const from = runtimeFor(fromId, false);
|
|
762
|
+
if (!from || !toId || String(fromId) === String(toId)) return runtimeFor(toId);
|
|
763
|
+
const target = runtimeFor(toId);
|
|
764
|
+
target.rows = from.rows;
|
|
765
|
+
target.headers = from.headers;
|
|
766
|
+
target.source = from.source;
|
|
767
|
+
target.lastCsv = from.lastCsv;
|
|
768
|
+
target.resultRows = from.resultRows;
|
|
769
|
+
runtimeTasks.delete(String(fromId || 'unassigned'));
|
|
770
|
+
return target;
|
|
771
|
+
}
|
|
704
772
|
|
|
705
773
|
const STEPS = [
|
|
706
|
-
{ key: 'upload', label: '
|
|
707
|
-
{ key: '
|
|
708
|
-
{ key: '
|
|
709
|
-
{ key: 'enrich', label: '
|
|
774
|
+
{ key: 'upload', label: '上传数据', icon: '📄' },
|
|
775
|
+
{ key: 'rules', label: '规则确认', icon: '⚙' },
|
|
776
|
+
{ key: 'match', label: '数据匹配', icon: '🔎' },
|
|
777
|
+
{ key: 'enrich', label: '清洗补全', icon: '▦' },
|
|
778
|
+
{ key: 'download', label: '下载数据', icon: '⬇️' },
|
|
710
779
|
];
|
|
711
780
|
|
|
712
781
|
const CAPABILITIES = [
|
|
713
782
|
{ key: 'upload', label: '上传清洗', icon: IconPaperclipOutline16, fallback: '+' },
|
|
714
783
|
{ key: 'profile', label: '质量体检', icon: IconChecklistOutline14, fallback: '✓' },
|
|
715
|
-
{ key: '
|
|
784
|
+
{ key: 'match', label: '匹配核验', icon: IconSearchOutline16, fallback: '⌕' },
|
|
716
785
|
{ key: 'enrich', label: '字段补全', icon: IconDataOutline16, fallback: '▦' },
|
|
717
786
|
{ key: 'history', label: '任务历史', icon: IconArchiveOutline20, fallback: '◷' },
|
|
718
787
|
];
|
|
719
788
|
|
|
720
789
|
const CLEANING_OPTIONS = [
|
|
721
|
-
['
|
|
722
|
-
['complete_name', '补全 / 修正旧名称'],
|
|
723
|
-
['verify_credit', '校验 / 补全信用代码'],
|
|
790
|
+
['clean_name', '名称补全与规范'],
|
|
724
791
|
['deduplicate', '重复企业去重'],
|
|
725
|
-
['
|
|
792
|
+
['validate_identity', '主体标识校验'],
|
|
793
|
+
['complete_fields', '补全所选企业字段'],
|
|
726
794
|
];
|
|
727
|
-
const
|
|
728
|
-
['
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
['
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
['operation', '经营信息'
|
|
795
|
+
const FIELD_GROUPS = [
|
|
796
|
+
['identity', '基础工商信息', [
|
|
797
|
+
['company_name', '企业名称'], ['credit_no', '统一社会信用代码'], ['reg_no', '注册号'],
|
|
798
|
+
['reg_status', '登记状态'], ['legal_rep', '法定代表人'], ['reg_capital', '注册资本'],
|
|
799
|
+
['paid_capital', '实缴资本'], ['establish_date', '成立日期'], ['company_type', '企业类型'],
|
|
800
|
+
['registration_authority', '登记机关'], ['former_name', '曾用名'], ['english_name', '英文名'],
|
|
801
|
+
]],
|
|
802
|
+
['contact', '地址与联系方式', [
|
|
803
|
+
['registered_address', '注册地址'], ['province', '省份地区'], ['city', '城市'],
|
|
804
|
+
['district', '区县'], ['phone', '电话'], ['email', '邮箱'], ['website', '官网'],
|
|
805
|
+
]],
|
|
806
|
+
['operation', '经营信息', [
|
|
807
|
+
['business_scope', '经营范围'], ['industry_category', '国标行业'], ['industry_large', '一级行业'],
|
|
808
|
+
['industry_middle', '二级行业'], ['operating_period', '营业期限'], ['company_size', '企业规模'],
|
|
809
|
+
['company_profile', '企业简介'],
|
|
810
|
+
]],
|
|
811
|
+
['risk', '风险摘要', [
|
|
812
|
+
['risk_summary', '风险摘要'], ['operating_exception', '经营异常摘要'], ['administrative_penalty', '行政处罚摘要'],
|
|
813
|
+
]],
|
|
814
|
+
['ipr', '知识产权摘要', [
|
|
815
|
+
['trademark_summary', '商标摘要'], ['patent_summary', '专利摘要'], ['software_copyright_summary', '软件著作权摘要'],
|
|
816
|
+
]],
|
|
739
817
|
];
|
|
740
|
-
const
|
|
818
|
+
const ENRICHMENT_OPTIONS = FIELD_GROUPS.flatMap(([, , fields]) => fields);
|
|
819
|
+
const MATCH_ANCHOR_OPTIONS = [
|
|
820
|
+
['company_name', '企业名称'], ['credit_no', '统一社会信用代码'], ['reg_no', '注册号'],
|
|
821
|
+
];
|
|
822
|
+
const DEFAULT_CLEANING_KEYS = ['clean_name', 'deduplicate', 'validate_identity', 'complete_fields'];
|
|
741
823
|
const DEFAULT_ENRICHMENT_KEYS = ['credit_no', 'legal_rep', 'reg_capital', 'establish_date', 'reg_status'];
|
|
742
824
|
const DEFAULT_SESSION_PROMPT = '请帮我清洗并补全企业名单。可点击输入框左上角「提示词生成」录入名单、上传 Excel 或图片,也可直接修改本段任务说明后开始。';
|
|
743
825
|
const CLEANING_SESSION_STORAGE_KEY = 'dsh.data-cleaning-agent.sessions.v1';
|
|
@@ -796,6 +878,7 @@ window.__ModuleLoader__.load({
|
|
|
796
878
|
const mode = config.mode ?? 'text';
|
|
797
879
|
const cleanLabels = optionLabels(CLEANING_OPTIONS, config.cleaningKeys);
|
|
798
880
|
const enrichLabels = optionLabels(ENRICHMENT_OPTIONS, config.enrichmentKeys);
|
|
881
|
+
const anchorLabels = optionLabels(MATCH_ANCHOR_OPTIONS, config.anchorKeys);
|
|
799
882
|
const entries = Array.isArray(config.entries)
|
|
800
883
|
? config.entries.map((value) => String(value ?? '').trim()).filter(Boolean)
|
|
801
884
|
: [];
|
|
@@ -817,23 +900,176 @@ window.__ModuleLoader__.load({
|
|
|
817
900
|
}
|
|
818
901
|
lines.push(`清洗要求:${cleanLabels.length ? cleanLabels.join('、') : '仅解析,不自动修改'}。`);
|
|
819
902
|
lines.push(`需要补全的字段 / 维度:${enrichLabels.length ? enrichLabels.join('、') : '不调用外部补全,仅输出本地清洗结果'}。`);
|
|
820
|
-
lines.push('
|
|
903
|
+
lines.push(`处理规则:优先使用${anchorLabels.length ? anchorLabels.join('、') : '企业名称、统一社会信用代码或注册号'}精确匹配;${config.matchRules?.manualReviewAmbiguous === false ? '精确匹配失败时直接输出未匹配记录' : '精确匹配失败时再进入模糊候选,存在多个候选必须暂停并让我确认,不得默认选择第一项'}。`);
|
|
821
904
|
lines.push('请先生成数据质量体检与匹配摘要,经我确认后再执行需要消耗额度的企查查 MCP 调用;企查查连接、套餐额度和费用均由当前用户自己的账号承担。缺失或无权限字段请留空并标记原因,不得编造。');
|
|
822
905
|
lines.push('完成后保留来源原值、标准主体、匹配状态与字段来源,并提供结果和待复核清单的导出。');
|
|
823
906
|
return lines.join('\n\n');
|
|
824
907
|
}
|
|
825
908
|
|
|
826
|
-
|
|
827
|
-
|
|
909
|
+
function entriesToDataset(entries) {
|
|
910
|
+
// 文本向导允许企业名称和信用代码混输。统一为一个检索锚点列,避免 G5 Bridge
|
|
911
|
+
// 只能选择单一 nameField 时遗漏信用代码行;QCC entity lookup 对两者均可检索。
|
|
912
|
+
const rows = (Array.isArray(entries) ? entries : [])
|
|
913
|
+
.map((value) => ({ 主体标识: String(value ?? '').trim() }))
|
|
914
|
+
.filter((row) => row.主体标识);
|
|
915
|
+
return {
|
|
916
|
+
ok: true,
|
|
917
|
+
fmt: 'text',
|
|
918
|
+
headers: ['主体标识'],
|
|
919
|
+
rowCount: rows.length,
|
|
920
|
+
rows,
|
|
921
|
+
preview: rows.slice(0, 5),
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const MAPPING_PATTERNS = [
|
|
926
|
+
['company_name', /^(name|company|company_name|企业名称|公司名称|单位名称)$/i],
|
|
927
|
+
['credit_no', /^(credit_no|creditCode|unified_credit_code|统一社会信用代码|信用代码)$/i],
|
|
928
|
+
['reg_no', /^(reg_no|registration_no|注册号)$/i],
|
|
929
|
+
['phone', /^(phone|mobile|tel|telephone|联系电话|手机号码|手机号)$/i],
|
|
930
|
+
['province', /^(province|省份|省份地区)$/i],
|
|
931
|
+
['registered_address', /^(address|registered_address|注册地址|企业地址|公司地址)$/i],
|
|
932
|
+
];
|
|
933
|
+
|
|
934
|
+
function guessMappings(headers) {
|
|
935
|
+
const output = [];
|
|
936
|
+
const usedTargets = new Set();
|
|
937
|
+
for (const sourceField of Array.isArray(headers) ? headers : []) {
|
|
938
|
+
const match = MAPPING_PATTERNS.find(([targetField, pattern]) => !usedTargets.has(targetField) && pattern.test(String(sourceField)));
|
|
939
|
+
if (match) {
|
|
940
|
+
output.push({ sourceField: String(sourceField), targetField: match[0] });
|
|
941
|
+
usedTargets.add(match[0]);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (!output.length && Array.isArray(headers) && headers[0]) {
|
|
945
|
+
output.push({ sourceField: String(headers[0]), targetField: 'company_name' });
|
|
946
|
+
}
|
|
947
|
+
return output;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function qualitySummaryFor(rows, mappings) {
|
|
951
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
952
|
+
const anchors = (Array.isArray(mappings) ? mappings : []).filter((mapping) => ['company_name', 'credit_no', 'reg_no'].includes(mapping.targetField));
|
|
953
|
+
const signatures = new Set();
|
|
954
|
+
let missingAnchor = 0;
|
|
955
|
+
let duplicates = 0;
|
|
956
|
+
let invalidCreditNo = 0;
|
|
957
|
+
let invalidPhone = 0;
|
|
958
|
+
let emptyFields = 0;
|
|
959
|
+
const creditSource = mappings?.find((mapping) => mapping.targetField === 'credit_no')?.sourceField;
|
|
960
|
+
const phoneSource = mappings?.find((mapping) => mapping.targetField === 'phone')?.sourceField;
|
|
961
|
+
for (const row of list) {
|
|
962
|
+
const values = anchors.map((mapping) => String(row?.[mapping.sourceField] ?? '').trim()).filter(Boolean);
|
|
963
|
+
if (!values.length) missingAnchor += 1;
|
|
964
|
+
const signature = values.join('|').toUpperCase();
|
|
965
|
+
if (signature && signatures.has(signature)) duplicates += 1;
|
|
966
|
+
if (signature) signatures.add(signature);
|
|
967
|
+
const credit = creditSource ? String(row?.[creditSource] ?? '').trim() : '';
|
|
968
|
+
if (credit && !/^[0-9A-Z]{18}$/.test(credit)) invalidCreditNo += 1;
|
|
969
|
+
const phone = phoneSource ? String(row?.[phoneSource] ?? '').replace(/[\s-]/g, '') : '';
|
|
970
|
+
if (phone && !/^(?:\+?86)?(?:1\d{10}|0\d{9,11})$/.test(phone)) invalidPhone += 1;
|
|
971
|
+
emptyFields += Object.values(row ?? {}).filter((value) => String(value ?? '').trim() === '').length;
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
total: list.length,
|
|
975
|
+
valid: Math.max(0, list.length - missingAnchor),
|
|
976
|
+
missingAnchor,
|
|
977
|
+
duplicates,
|
|
978
|
+
invalidCreditNo,
|
|
979
|
+
invalidPhone,
|
|
980
|
+
emptyFields,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/** 同源 Host API;保留旧 api(path, body) 调用,同时为工作流开放 PATCH。 */
|
|
985
|
+
async function requestJson(path, method = 'GET', body) {
|
|
828
986
|
const res = await fetch(path, {
|
|
829
|
-
method
|
|
830
|
-
headers: body ? { 'content-type': 'application/json' } : undefined,
|
|
831
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
987
|
+
method,
|
|
988
|
+
headers: body !== undefined ? { 'content-type': 'application/json' } : undefined,
|
|
989
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
832
990
|
});
|
|
833
|
-
|
|
991
|
+
const payload = await res.json();
|
|
992
|
+
if (res.ok === false || payload?.ok === false) {
|
|
993
|
+
const error = new Error(payload?.message || payload?.error || `Host request failed (${res.status})`);
|
|
994
|
+
error.code = payload?.code;
|
|
995
|
+
throw error;
|
|
996
|
+
}
|
|
997
|
+
return payload;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async function api(path, body) {
|
|
1001
|
+
return requestJson(path, body === undefined ? 'GET' : 'POST', body);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function cacheWorkflowTask(actions, sessionId, task) {
|
|
1005
|
+
if (!task) return null;
|
|
1006
|
+
if (sessionId) workflowTaskBySession.set(String(sessionId), task);
|
|
1007
|
+
actions.setWorkflowTask(task);
|
|
1008
|
+
if (task.title) actions.setTaskTitle(task.title);
|
|
1009
|
+
if (Array.isArray(task.objectives)) actions.setObjectives(task.objectives);
|
|
1010
|
+
if (Array.isArray(task.fieldSelection)) actions.setFieldSelection(task.fieldSelection);
|
|
1011
|
+
if (Array.isArray(task.mappings) && task.mappings.length) actions.setMappings(task.mappings);
|
|
1012
|
+
return task;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
async function ensureWorkflowTask(actions, sessionId, draft = {}) {
|
|
1016
|
+
const key = String(sessionId || 'unassigned');
|
|
1017
|
+
const cached = workflowTaskBySession.get(key);
|
|
1018
|
+
if (cached) return cacheWorkflowTask(actions, sessionId, cached);
|
|
1019
|
+
let pending = workflowTaskCreationBySession.get(key);
|
|
1020
|
+
if (!pending) {
|
|
1021
|
+
pending = requestJson('/data-cleaning/api/workflow/tasks', 'POST', {
|
|
1022
|
+
title: draft.title || '企业数据清洗补全任务',
|
|
1023
|
+
objectives: draft.objectives || DEFAULT_CLEANING_KEYS,
|
|
1024
|
+
fieldSelection: draft.fieldSelection || DEFAULT_ENRICHMENT_KEYS,
|
|
1025
|
+
mappings: draft.mappings || [],
|
|
1026
|
+
matchRules: draft.matchRules,
|
|
1027
|
+
});
|
|
1028
|
+
workflowTaskCreationBySession.set(key, pending);
|
|
1029
|
+
const clearPending = () => {
|
|
1030
|
+
if (workflowTaskCreationBySession.get(key) === pending) workflowTaskCreationBySession.delete(key);
|
|
1031
|
+
};
|
|
1032
|
+
pending.then(clearPending, clearPending);
|
|
1033
|
+
}
|
|
1034
|
+
const response = await pending;
|
|
1035
|
+
moveRuntime(`session:${key}`, response.task.id);
|
|
1036
|
+
return cacheWorkflowTask(actions, sessionId, response.task);
|
|
834
1037
|
}
|
|
835
1038
|
|
|
836
|
-
/**
|
|
1039
|
+
/** 同一会话的 Host 写操作串行化,确保 expectedRevision 不因 UI 事件竞态失效。 */
|
|
1040
|
+
function queueWorkflowOperation(sessionId, operation) {
|
|
1041
|
+
const key = String(sessionId || 'unassigned');
|
|
1042
|
+
const previous = workflowOperationBySession.get(key) ?? Promise.resolve();
|
|
1043
|
+
const next = previous.catch(() => {}).then(operation);
|
|
1044
|
+
workflowOperationBySession.set(key, next);
|
|
1045
|
+
const clear = () => {
|
|
1046
|
+
if (workflowOperationBySession.get(key) === next) workflowOperationBySession.delete(key);
|
|
1047
|
+
};
|
|
1048
|
+
next.then(clear, clear);
|
|
1049
|
+
return next;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
async function updateWorkflowTask(actions, sessionId, task, input) {
|
|
1053
|
+
const latest = workflowTaskBySession.get(String(sessionId || 'unassigned')) ?? task;
|
|
1054
|
+
if (!latest) return ensureWorkflowTask(actions, sessionId, input);
|
|
1055
|
+
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(latest.id)}`, 'PATCH', {
|
|
1056
|
+
...input,
|
|
1057
|
+
expectedRevision: latest.revision,
|
|
1058
|
+
});
|
|
1059
|
+
return cacheWorkflowTask(actions, sessionId, response.task);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async function workflowAction(actions, sessionId, task, action, input = {}) {
|
|
1063
|
+
const latest = workflowTaskBySession.get(String(sessionId || 'unassigned')) ?? task;
|
|
1064
|
+
if (!latest) throw new Error('请先创建数据清洗补全任务。');
|
|
1065
|
+
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(latest.id)}/actions/${action}`, 'POST', {
|
|
1066
|
+
...input,
|
|
1067
|
+
expectedRevision: latest.revision,
|
|
1068
|
+
});
|
|
1069
|
+
return cacheWorkflowTask(actions, sessionId, response.task);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/** 工作台 store:入口按钮与工作台共享 open,并保存当前 taskId 的可持久化 UI 元数据。 */
|
|
837
1073
|
function createWorkbenchStore() {
|
|
838
1074
|
return defineStore({
|
|
839
1075
|
init: () => ({
|
|
@@ -843,6 +1079,14 @@ window.__ModuleLoader__.load({
|
|
|
843
1079
|
busy: false,
|
|
844
1080
|
error: null,
|
|
845
1081
|
input: '',
|
|
1082
|
+
workflowContract: null,
|
|
1083
|
+
workflowTask: null,
|
|
1084
|
+
workflowTasks: [],
|
|
1085
|
+
taskTitle: '企业数据清洗补全任务',
|
|
1086
|
+
mappings: [],
|
|
1087
|
+
objectives: [...DEFAULT_CLEANING_KEYS],
|
|
1088
|
+
fieldSelection: [...DEFAULT_ENRICHMENT_KEYS],
|
|
1089
|
+
matchRules: { normalizeNames: true, preferCreditNo: true, deduplicate: true, manualReviewAmbiguous: true },
|
|
846
1090
|
dataset: null, // { fmt, headers, rowCount, preview }
|
|
847
1091
|
profile: null, // summary
|
|
848
1092
|
clean: null, // summary
|
|
@@ -870,6 +1114,46 @@ window.__ModuleLoader__.load({
|
|
|
870
1114
|
setBusy: (draft, busy) => { draft.busy = busy; },
|
|
871
1115
|
setError: (draft, error) => { draft.error = error; },
|
|
872
1116
|
setInput: (draft, input) => { draft.input = input; },
|
|
1117
|
+
setWorkflowContract: (draft, value) => { draft.workflowContract = value; },
|
|
1118
|
+
setWorkflowTask: (draft, value) => { draft.workflowTask = value ?? null; },
|
|
1119
|
+
setWorkflowTasks: (draft, value) => { draft.workflowTasks = Array.isArray(value) ? value : []; },
|
|
1120
|
+
setTaskTitle: (draft, value) => { draft.taskTitle = String(value ?? ''); },
|
|
1121
|
+
setMappings: (draft, value) => { draft.mappings = Array.isArray(value) ? value : []; },
|
|
1122
|
+
setMapping: (draft, sourceField, targetField) => {
|
|
1123
|
+
const mappings = draft.mappings.filter((item) => item.sourceField !== sourceField && item.targetField !== targetField);
|
|
1124
|
+
if (targetField) mappings.push({ sourceField, targetField });
|
|
1125
|
+
draft.mappings = mappings;
|
|
1126
|
+
},
|
|
1127
|
+
setObjectives: (draft, value) => { draft.objectives = Array.isArray(value) ? value : []; },
|
|
1128
|
+
toggleObjective: (draft, value) => {
|
|
1129
|
+
const selected = new Set(draft.objectives);
|
|
1130
|
+
if (selected.has(value)) selected.delete(value); else selected.add(value);
|
|
1131
|
+
draft.objectives = [...selected];
|
|
1132
|
+
},
|
|
1133
|
+
setFieldSelection: (draft, value) => { draft.fieldSelection = Array.isArray(value) ? value : []; },
|
|
1134
|
+
toggleField: (draft, value) => {
|
|
1135
|
+
const selected = new Set(draft.fieldSelection);
|
|
1136
|
+
if (selected.has(value)) selected.delete(value); else selected.add(value);
|
|
1137
|
+
draft.fieldSelection = [...selected];
|
|
1138
|
+
},
|
|
1139
|
+
setMatchRule: (draft, key, value) => { draft.matchRules = { ...draft.matchRules, [key]: Boolean(value) }; },
|
|
1140
|
+
resetWorkflow: (draft) => {
|
|
1141
|
+
draft.step = 'upload';
|
|
1142
|
+
draft.workflowTask = null;
|
|
1143
|
+
draft.taskTitle = '企业数据清洗补全任务';
|
|
1144
|
+
draft.mappings = [];
|
|
1145
|
+
draft.objectives = [...DEFAULT_CLEANING_KEYS];
|
|
1146
|
+
draft.fieldSelection = [...DEFAULT_ENRICHMENT_KEYS];
|
|
1147
|
+
draft.matchRules = { normalizeNames: true, preferCreditNo: true, deduplicate: true, manualReviewAmbiguous: true };
|
|
1148
|
+
draft.dataset = null;
|
|
1149
|
+
draft.profile = null;
|
|
1150
|
+
draft.clean = null;
|
|
1151
|
+
draft.complete = null;
|
|
1152
|
+
draft.qccEstimate = null;
|
|
1153
|
+
draft.qccRun = null;
|
|
1154
|
+
draft.paidConfirmed = false;
|
|
1155
|
+
draft.error = null;
|
|
1156
|
+
},
|
|
873
1157
|
setDataset: (draft, dataset) => { draft.dataset = dataset; draft.error = null; },
|
|
874
1158
|
setProfile: (draft, profile) => { draft.profile = profile; draft.error = null; },
|
|
875
1159
|
setClean: (draft, clean) => { draft.clean = clean; draft.error = null; },
|
|
@@ -980,38 +1264,55 @@ window.__ModuleLoader__.load({
|
|
|
980
1264
|
|
|
981
1265
|
const WORKBENCH_OPEN_EVENT = 'dsh:data-cleaning-workbench-open';
|
|
982
1266
|
const WORKBENCH_DATASET_EVENT = 'dsh:data-cleaning-workbench-dataset';
|
|
1267
|
+
const WORKBENCH_DRAFT_EVENT = 'dsh:data-cleaning-workbench-draft';
|
|
983
1268
|
|
|
984
1269
|
// session scope 不能复用 root scope 的 store handle。DSH 当前会分别物化 root/session
|
|
985
1270
|
// 插槽组件,因此以 document 事件跨 scope 通知;闭包引用仅作为无 DOM 测试降级。
|
|
986
1271
|
let rootWorkbenchActions = null;
|
|
987
1272
|
|
|
988
1273
|
/** 从 session scope 的 composer/header 触发 root scope 工作台。 */
|
|
989
|
-
function requestWorkbenchOpen(step, sessionId) {
|
|
1274
|
+
function requestWorkbenchOpen(step, sessionId, task = null) {
|
|
990
1275
|
if (typeof document !== 'undefined'
|
|
991
1276
|
&& typeof document.createEvent === 'function'
|
|
992
1277
|
&& typeof document.dispatchEvent === 'function') {
|
|
993
1278
|
const event = document.createEvent('CustomEvent');
|
|
994
|
-
event.initCustomEvent(WORKBENCH_OPEN_EVENT, false, true, { step, sessionId });
|
|
1279
|
+
event.initCustomEvent(WORKBENCH_OPEN_EVENT, false, true, { step, sessionId, task });
|
|
995
1280
|
if (!document.dispatchEvent(event)) return;
|
|
996
1281
|
}
|
|
997
1282
|
if (rootWorkbenchActions) openWorkbench(rootWorkbenchActions, step, sessionId);
|
|
998
1283
|
}
|
|
999
1284
|
|
|
1000
1285
|
/** 把提示词生成器解析出的完整表格交给 root 工作台,不把整表塞进模型上下文。 */
|
|
1001
|
-
function requestWorkbenchDataset(result, sessionId) {
|
|
1286
|
+
function requestWorkbenchDataset(result, sessionId, source) {
|
|
1002
1287
|
if (typeof document !== 'undefined'
|
|
1003
1288
|
&& typeof document.createEvent === 'function'
|
|
1004
1289
|
&& typeof document.dispatchEvent === 'function') {
|
|
1005
1290
|
const event = document.createEvent('CustomEvent');
|
|
1006
|
-
event.initCustomEvent(WORKBENCH_DATASET_EVENT, false, true, { result, sessionId });
|
|
1291
|
+
event.initCustomEvent(WORKBENCH_DATASET_EVENT, false, true, { result, sessionId, source });
|
|
1007
1292
|
if (!document.dispatchEvent(event)) return;
|
|
1008
1293
|
}
|
|
1009
1294
|
if (rootWorkbenchActions) {
|
|
1010
|
-
applyParsed(result, rootWorkbenchActions);
|
|
1295
|
+
applyParsed(result, rootWorkbenchActions, `session:${sessionId || 'unassigned'}`, source);
|
|
1011
1296
|
rootWorkbenchActions.setActiveSession(sessionId);
|
|
1012
1297
|
}
|
|
1013
1298
|
}
|
|
1014
1299
|
|
|
1300
|
+
/** 提示词向导只提交可持久化的任务设置;名单明细仍通过 dataset 事件进入 task runtime。 */
|
|
1301
|
+
function requestWorkbenchDraft(draft, sessionId) {
|
|
1302
|
+
if (typeof document !== 'undefined'
|
|
1303
|
+
&& typeof document.createEvent === 'function'
|
|
1304
|
+
&& typeof document.dispatchEvent === 'function') {
|
|
1305
|
+
const event = document.createEvent('CustomEvent');
|
|
1306
|
+
event.initCustomEvent(WORKBENCH_DRAFT_EVENT, false, true, { draft, sessionId });
|
|
1307
|
+
if (!document.dispatchEvent(event)) return;
|
|
1308
|
+
}
|
|
1309
|
+
if (rootWorkbenchActions) {
|
|
1310
|
+
rootWorkbenchActions.setTaskTitle(draft.title);
|
|
1311
|
+
rootWorkbenchActions.setObjectives(draft.objectives);
|
|
1312
|
+
rootWorkbenchActions.setFieldSelection(draft.fieldSelection);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1015
1316
|
/** 左栏入口按钮:公开 footer 托管,真实按钮 Portal 到「新会话 / 工作区」之间。 */
|
|
1016
1317
|
function SidebarEntry(props) {
|
|
1017
1318
|
const { wide, useStore, actions, startSession } = props;
|
|
@@ -1083,16 +1384,41 @@ window.__ModuleLoader__.load({
|
|
|
1083
1384
|
);
|
|
1084
1385
|
}
|
|
1085
1386
|
|
|
1086
|
-
|
|
1387
|
+
const WORKFLOW_STATE_LABELS = {
|
|
1388
|
+
draft: '待上传', uploaded: '待确认规则', rules_confirmed: '规则已确认', diagnosed: '体检完成',
|
|
1389
|
+
matching: '匹配中', review_required: '待人工核验', matched: '匹配完成', enriching: '补全中',
|
|
1390
|
+
export_ready: '可下载', completed: '已完成', partial: '部分完成', failed: '失败',
|
|
1391
|
+
authorization_required: '待连接企查查', cancelled: '已取消', parse_failed: '解析失败',
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
function ProductHome(props = {}) {
|
|
1395
|
+
const { sessionId } = props;
|
|
1396
|
+
const [recentTasks, setRecentTasks] = react.useState([]);
|
|
1087
1397
|
const flow = [
|
|
1088
|
-
['01', '
|
|
1089
|
-
['02', '
|
|
1090
|
-
['03', '
|
|
1091
|
-
['04', '
|
|
1398
|
+
['01', '任务设置', '提示词向导明确目标'],
|
|
1399
|
+
['02', '上传数据', '文本、Excel、JSON 或图片'],
|
|
1400
|
+
['03', '规则确认', '字段映射与匹配主键'],
|
|
1401
|
+
['04', '质量体检', '缺失、重复与格式诊断'],
|
|
1402
|
+
['05', '数据匹配', '精确优先,多候选复核'],
|
|
1403
|
+
['06', '清洗补全', '按需选择当前企业字段'],
|
|
1404
|
+
['07', '下载数据', '保留原值、依据与状态'],
|
|
1092
1405
|
];
|
|
1406
|
+
react.useEffect(() => {
|
|
1407
|
+
let disposed = false;
|
|
1408
|
+
requestJson('/data-cleaning/api/workflow/tasks').then((response) => {
|
|
1409
|
+
if (!disposed) setRecentTasks((response.tasks || []).slice(0, 3));
|
|
1410
|
+
}).catch(() => {});
|
|
1411
|
+
return () => { disposed = true; };
|
|
1412
|
+
}, [sessionId]);
|
|
1093
1413
|
return h('section', { className: 'dcAgentProductHome', 'aria-label': '数据清洗补全产品介绍' },
|
|
1094
|
-
h('
|
|
1095
|
-
|
|
1414
|
+
h('div', { className: 'dcAgentHomeHero' },
|
|
1415
|
+
h('div', null,
|
|
1416
|
+
h('span', { className: 'dcAgentHomeEyebrow' }, '企业主数据治理工作台'),
|
|
1417
|
+
h('h2', null, '把企业名单变成可核验、可回写的标准数据'),
|
|
1418
|
+
h('p', null, '面向销售线索、客户尽调、供应商管理与 CRM / ERP 数据治理,先确认任务和规则,再按需使用当前用户连接的企查查 MCP。'),
|
|
1419
|
+
),
|
|
1420
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', onClick: () => requestWorkbenchOpen('upload', sessionId) }, '开始新任务'),
|
|
1421
|
+
),
|
|
1096
1422
|
h('div', { className: 'dcAgentHomeFlow', 'aria-label': '数据清洗补全工作流' },
|
|
1097
1423
|
flow.map(([index, title, hint]) => h('div', { className: 'dcAgentHomeStep', key: index },
|
|
1098
1424
|
h('b', null, index),
|
|
@@ -1106,6 +1432,22 @@ window.__ModuleLoader__.load({
|
|
|
1106
1432
|
h('span', null, '客户自带企查查连接与额度'),
|
|
1107
1433
|
h('span', null, '结果与待复核清单可导出'),
|
|
1108
1434
|
),
|
|
1435
|
+
h('div', { className: 'dcAgentHomeActions' },
|
|
1436
|
+
h('button', { type: 'button', onClick: () => requestWorkbenchOpen('profile', sessionId) }, '🩺 查看质量体检'),
|
|
1437
|
+
h('button', { type: 'button', onClick: () => requestWorkbenchOpen('match', sessionId) }, '🔎 进入匹配核验'),
|
|
1438
|
+
h('button', { type: 'button', onClick: () => requestWorkbenchOpen('history', sessionId) }, '◷ 查看任务历史'),
|
|
1439
|
+
),
|
|
1440
|
+
recentTasks.length ? h('div', { className: 'dcAgentHomeRecent', 'aria-label': '最近任务' },
|
|
1441
|
+
h('b', null, '最近任务'),
|
|
1442
|
+
recentTasks.map((task) => h('button', {
|
|
1443
|
+
key: task.id,
|
|
1444
|
+
type: 'button',
|
|
1445
|
+
onClick: () => requestWorkbenchOpen(task.stage || 'upload', sessionId, task),
|
|
1446
|
+
},
|
|
1447
|
+
h('span', null, task.title || '未命名任务'),
|
|
1448
|
+
h('small', null, WORKFLOW_STATE_LABELS[task.state] || task.state),
|
|
1449
|
+
)),
|
|
1450
|
+
) : null,
|
|
1109
1451
|
);
|
|
1110
1452
|
}
|
|
1111
1453
|
|
|
@@ -1161,8 +1503,8 @@ window.__ModuleLoader__.load({
|
|
|
1161
1503
|
className: `dcAgentExperience${hero ? ' is-home' : ''}`,
|
|
1162
1504
|
'data-session-id': sessionId,
|
|
1163
1505
|
},
|
|
1164
|
-
CapabilityBar
|
|
1165
|
-
hero ? ProductHome
|
|
1506
|
+
h(CapabilityBar, { sessionId }),
|
|
1507
|
+
hero ? h(ProductHome, { sessionId }) : null,
|
|
1166
1508
|
);
|
|
1167
1509
|
}
|
|
1168
1510
|
|
|
@@ -1186,6 +1528,7 @@ window.__ModuleLoader__.load({
|
|
|
1186
1528
|
const { sessionId, inputActions, attachImages } = props;
|
|
1187
1529
|
const enabled = useCleaningSession(sessionId);
|
|
1188
1530
|
const [open, setOpen] = react.useState(false);
|
|
1531
|
+
const [wizardStep, setWizardStep] = react.useState(1);
|
|
1189
1532
|
const [mode, setMode] = react.useState('text');
|
|
1190
1533
|
const [rawText, setRawText] = react.useState('');
|
|
1191
1534
|
const [entries, setEntries] = react.useState([]);
|
|
@@ -1195,6 +1538,13 @@ window.__ModuleLoader__.load({
|
|
|
1195
1538
|
const [imageAttached, setImageAttached] = react.useState(false);
|
|
1196
1539
|
const [cleaningKeys, setCleaningKeys] = react.useState(DEFAULT_CLEANING_KEYS);
|
|
1197
1540
|
const [enrichmentKeys, setEnrichmentKeys] = react.useState(DEFAULT_ENRICHMENT_KEYS);
|
|
1541
|
+
const [anchorKeys, setAnchorKeys] = react.useState(['company_name', 'credit_no']);
|
|
1542
|
+
const [matchRules, setPromptMatchRules] = react.useState({
|
|
1543
|
+
normalizeNames: true,
|
|
1544
|
+
preferCreditNo: true,
|
|
1545
|
+
deduplicate: true,
|
|
1546
|
+
manualReviewAmbiguous: true,
|
|
1547
|
+
});
|
|
1198
1548
|
const [busy, setBusy] = react.useState(false);
|
|
1199
1549
|
const [error, setError] = react.useState(null);
|
|
1200
1550
|
|
|
@@ -1226,7 +1576,9 @@ window.__ModuleLoader__.load({
|
|
|
1226
1576
|
setEntries(extracted);
|
|
1227
1577
|
setEntryCount(Number.isFinite(result.rowCount) ? result.rowCount : (Array.isArray(result.rows) ? result.rows.length : extracted.length));
|
|
1228
1578
|
setSpreadsheetFileName(file.name);
|
|
1229
|
-
requestWorkbenchDataset(result, sessionId
|
|
1579
|
+
requestWorkbenchDataset(result, sessionId, {
|
|
1580
|
+
type: result.fmt || 'xlsx', fileName: file.name, sizeBytes: file.size || 0,
|
|
1581
|
+
});
|
|
1230
1582
|
}
|
|
1231
1583
|
} catch (fileError) {
|
|
1232
1584
|
setError(fileError instanceof Error ? fileError.message : String(fileError));
|
|
@@ -1235,35 +1587,61 @@ window.__ModuleLoader__.load({
|
|
|
1235
1587
|
if (event.target) event.target.value = '';
|
|
1236
1588
|
}
|
|
1237
1589
|
};
|
|
1590
|
+
const selectedEntries = () => mode === 'text'
|
|
1591
|
+
? rawText.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
1592
|
+
: entries;
|
|
1593
|
+
const validateSource = () => {
|
|
1594
|
+
const values = selectedEntries();
|
|
1595
|
+
if (mode === 'text' && !values.length) return '请先录入企业名称或统一社会信用代码,每行一条。';
|
|
1596
|
+
if (mode === 'excel' && !values.length) return '请先上传并解析 Excel / CSV / JSON 文件。';
|
|
1597
|
+
if (mode === 'image' && !imageAttached) return '请先选择并附加一张包含企业名单的图片。';
|
|
1598
|
+
return null;
|
|
1599
|
+
};
|
|
1600
|
+
const promptConfig = () => ({
|
|
1601
|
+
mode,
|
|
1602
|
+
entries: selectedEntries(),
|
|
1603
|
+
entryCount: mode === 'excel' ? entryCount : selectedEntries().length,
|
|
1604
|
+
fileName: mode === 'image' ? imageFileName : spreadsheetFileName,
|
|
1605
|
+
cleaningKeys,
|
|
1606
|
+
enrichmentKeys,
|
|
1607
|
+
anchorKeys,
|
|
1608
|
+
matchRules,
|
|
1609
|
+
});
|
|
1238
1610
|
const generatePrompt = () => {
|
|
1239
|
-
const
|
|
1240
|
-
|
|
1241
|
-
if (
|
|
1242
|
-
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
if (mode === 'excel' && !selectedEntries.length) {
|
|
1246
|
-
setError('请先上传并解析 Excel / CSV / JSON 文件。');
|
|
1247
|
-
return;
|
|
1248
|
-
}
|
|
1249
|
-
if (mode === 'image' && !imageAttached) {
|
|
1250
|
-
setError('请先选择并附加一张包含企业名单的图片。');
|
|
1251
|
-
return;
|
|
1252
|
-
}
|
|
1611
|
+
const sourceError = validateSource();
|
|
1612
|
+
if (sourceError) { setError(sourceError); return; }
|
|
1613
|
+
if (!anchorKeys.length) { setError('请至少选择一个企业主体匹配主键。'); return; }
|
|
1614
|
+
if (!cleaningKeys.length) { setError('请至少选择一个清洗或补全目标。'); return; }
|
|
1253
1615
|
if (!inputActions || typeof inputActions.setDraft !== 'function') {
|
|
1254
1616
|
setError('当前会话输入机尚未就绪,请稍后重试。');
|
|
1255
1617
|
return;
|
|
1256
1618
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1619
|
+
const config = promptConfig();
|
|
1620
|
+
inputActions.setDraft(buildTaskPrompt(config));
|
|
1621
|
+
if (mode === 'text') {
|
|
1622
|
+
requestWorkbenchDataset(entriesToDataset(config.entries), sessionId, { type: 'text', fileName: '' });
|
|
1623
|
+
}
|
|
1624
|
+
requestWorkbenchDraft({
|
|
1625
|
+
title: `企业数据清洗补全任务 · ${config.entryCount || '待解析'} 条`,
|
|
1626
|
+
objectives: cleaningKeys,
|
|
1627
|
+
fieldSelection: enrichmentKeys,
|
|
1628
|
+
matchRules,
|
|
1629
|
+
source: { type: mode === 'excel' ? 'xlsx' : mode, fileName: config.fileName || '', rowCount: config.entryCount || 0 },
|
|
1630
|
+
}, sessionId);
|
|
1265
1631
|
setError(null);
|
|
1266
1632
|
setOpen(false);
|
|
1633
|
+
setWizardStep(1);
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1636
|
+
const nextWizard = () => {
|
|
1637
|
+
if (wizardStep === 1) {
|
|
1638
|
+
const sourceError = validateSource();
|
|
1639
|
+
if (sourceError) { setError(sourceError); return; }
|
|
1640
|
+
}
|
|
1641
|
+
if (wizardStep === 2 && !anchorKeys.length) { setError('请至少选择一个匹配主键。'); return; }
|
|
1642
|
+
if (wizardStep === 3 && !cleaningKeys.length) { setError('请至少选择一个清洗或补全目标。'); return; }
|
|
1643
|
+
if (wizardStep < 4) { setError(null); setWizardStep(wizardStep + 1); return; }
|
|
1644
|
+
generatePrompt();
|
|
1267
1645
|
};
|
|
1268
1646
|
|
|
1269
1647
|
const choice = (options, values, setter) => options.map(([key, label]) => h('label', {
|
|
@@ -1305,49 +1683,75 @@ window.__ModuleLoader__.load({
|
|
|
1305
1683
|
),
|
|
1306
1684
|
h('button', { type: 'button', className: 'dcAgentPromptClose', 'aria-label': '关闭提示词生成', onClick: () => setOpen(false) }, '✕'),
|
|
1307
1685
|
),
|
|
1308
|
-
h('
|
|
1309
|
-
[['
|
|
1310
|
-
key,
|
|
1686
|
+
h('nav', { className: 'dcAgentWizardNav', 'aria-label': '任务设置步骤' },
|
|
1687
|
+
[['1', '数据来源'], ['2', '匹配规则'], ['3', '清洗与补全'], ['4', '确认描述']].map(([index, label]) => h('button', {
|
|
1688
|
+
key: index,
|
|
1311
1689
|
type: 'button',
|
|
1312
|
-
|
|
1313
|
-
'aria-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
}, label)),
|
|
1317
|
-
),
|
|
1318
|
-
mode === 'text' ? h('label', { className: 'dcAgentPromptField' },
|
|
1319
|
-
h('span', null, '企业名称或统一社会信用代码(每行一条)'),
|
|
1320
|
-
h('textarea', {
|
|
1321
|
-
className: 'dcAgentPromptText',
|
|
1322
|
-
value: rawText,
|
|
1323
|
-
placeholder: '企查查科技股份有限公司\n9132…\n某某信息技术(上海)有限公司',
|
|
1324
|
-
onChange: (event) => setRawText(event.target.value),
|
|
1325
|
-
}),
|
|
1326
|
-
) : h('label', { className: 'dcAgentPromptFile' },
|
|
1327
|
-
h('span', null, mode === 'image' ? '选择企业名单图片' : '选择 Excel / CSV / JSON 名单'),
|
|
1328
|
-
h('input', {
|
|
1329
|
-
type: 'file',
|
|
1330
|
-
accept: mode === 'image' ? 'image/png,image/jpeg,image/webp' : '.xlsx,.xls,.csv,.json',
|
|
1331
|
-
disabled: busy,
|
|
1332
|
-
onChange: handleFile,
|
|
1333
|
-
}),
|
|
1334
|
-
(mode === 'excel' ? spreadsheetFileName : imageFileName)
|
|
1335
|
-
? h('b', null, `${mode === 'excel' ? spreadsheetFileName : imageFileName}${mode === 'excel' ? ` · ${entryCount} 条` : ' · 已附加'}`)
|
|
1336
|
-
: null,
|
|
1337
|
-
),
|
|
1338
|
-
h('div', { className: 'dcAgentPromptGroup' },
|
|
1339
|
-
h('b', null, '需要执行哪些清洗?'),
|
|
1340
|
-
h('div', { className: 'dcAgentPromptChoices' }, choice(CLEANING_OPTIONS, cleaningKeys, setCleaningKeys)),
|
|
1690
|
+
className: `dcAgentWizardStep${wizardStep === Number(index) ? ' is-active' : ''}`,
|
|
1691
|
+
'aria-current': wizardStep === Number(index) ? 'step' : undefined,
|
|
1692
|
+
onClick: () => setWizardStep(Number(index)),
|
|
1693
|
+
}, h('b', null, index), h('span', null, label))),
|
|
1341
1694
|
),
|
|
1342
|
-
h('div', { className: '
|
|
1343
|
-
h('
|
|
1344
|
-
h('
|
|
1345
|
-
|
|
1346
|
-
|
|
1695
|
+
wizardStep === 1 ? h('div', { className: 'dcAgentWizardPane' },
|
|
1696
|
+
h('h4', null, '先告诉我数据从哪里来'),
|
|
1697
|
+
h('p', null, '企业名称、统一社会信用代码、注册号任一项都可作为匹配起点。'),
|
|
1698
|
+
h('div', { className: 'dcAgentPromptTabs', role: 'tablist', 'aria-label': '名单录入方式' },
|
|
1699
|
+
[['text', '粘贴企业名单'], ['excel', '上传本地文件'], ['image', '上传图片识别']].map(([key, label]) => h('button', {
|
|
1700
|
+
key, type: 'button', role: 'tab', 'aria-selected': mode === key,
|
|
1701
|
+
className: `dcAgentPromptTab${mode === key ? ' is-active' : ''}`,
|
|
1702
|
+
onClick: () => setSourceMode(key),
|
|
1703
|
+
}, label)),
|
|
1704
|
+
),
|
|
1705
|
+
mode === 'text' ? h('label', { className: 'dcAgentPromptField' },
|
|
1706
|
+
h('span', null, '企业名称或统一社会信用代码(每行一条)'),
|
|
1707
|
+
h('textarea', {
|
|
1708
|
+
className: 'dcAgentPromptText', value: rawText,
|
|
1709
|
+
placeholder: '企查查科技股份有限公司\n9132…\n某某信息技术(上海)有限公司',
|
|
1710
|
+
onChange: (event) => setRawText(event.target.value),
|
|
1711
|
+
}),
|
|
1712
|
+
) : h('label', { className: 'dcAgentPromptFile' },
|
|
1713
|
+
h('span', null, mode === 'image' ? '选择包含企业名单的图片' : '选择 CSV / XLSX / JSON 名单'),
|
|
1714
|
+
h('input', {
|
|
1715
|
+
type: 'file', accept: mode === 'image' ? 'image/png,image/jpeg,image/webp' : '.xlsx,.xls,.csv,.json',
|
|
1716
|
+
disabled: busy, onChange: handleFile,
|
|
1717
|
+
}),
|
|
1718
|
+
(mode === 'excel' ? spreadsheetFileName : imageFileName)
|
|
1719
|
+
? h('b', null, `${mode === 'excel' ? spreadsheetFileName : imageFileName}${mode === 'excel' ? ` · ${entryCount} 条` : ' · 已附加'}`)
|
|
1720
|
+
: null,
|
|
1721
|
+
),
|
|
1722
|
+
) : null,
|
|
1723
|
+
wizardStep === 2 ? h('div', { className: 'dcAgentWizardPane' },
|
|
1724
|
+
h('h4', null, '确认匹配规则'),
|
|
1725
|
+
h('p', null, '强标识优先;名称定位不明确时生成候选,不自动写回。'),
|
|
1726
|
+
h('div', { className: 'dcAgentPromptChoices' }, choice(MATCH_ANCHOR_OPTIONS, anchorKeys, setAnchorKeys)),
|
|
1727
|
+
h('div', { className: 'dcAgentPromptRules' },
|
|
1728
|
+
[['normalizeNames', '匹配前统一企业名称格式'], ['preferCreditNo', '有信用代码时优先精确匹配'], ['deduplicate', '匹配前合并重复主体'], ['manualReviewAmbiguous', '多候选必须人工确认']].map(([key, label]) => h('label', { key },
|
|
1729
|
+
h('input', { type: 'checkbox', checked: matchRules[key], onChange: (event) => setPromptMatchRules({ ...matchRules, [key]: event.target.checked }) }), label,
|
|
1730
|
+
)),
|
|
1731
|
+
),
|
|
1732
|
+
) : null,
|
|
1733
|
+
wizardStep === 3 ? h('div', { className: 'dcAgentWizardPane' },
|
|
1734
|
+
h('h4', null, '选择清洗目标和补全字段'),
|
|
1735
|
+
h('p', null, '仅展示当前企业数据范围;最终可用字段以当前用户连接能力为准。'),
|
|
1736
|
+
h('div', { className: 'dcAgentPromptGroup' },
|
|
1737
|
+
h('b', null, '清洗目标'),
|
|
1738
|
+
h('div', { className: 'dcAgentPromptChoices' }, choice(CLEANING_OPTIONS, cleaningKeys, setCleaningKeys)),
|
|
1739
|
+
),
|
|
1740
|
+
FIELD_GROUPS.map(([groupId, label, fields]) => h('div', { className: 'dcAgentPromptGroup', key: groupId },
|
|
1741
|
+
h('b', null, label),
|
|
1742
|
+
h('div', { className: 'dcAgentPromptChoices' }, choice(fields, enrichmentKeys, setEnrichmentKeys)),
|
|
1743
|
+
)),
|
|
1744
|
+
) : null,
|
|
1745
|
+
wizardStep === 4 ? h('div', { className: 'dcAgentWizardPane' },
|
|
1746
|
+
h('h4', null, '确认任务描述'),
|
|
1747
|
+
h('p', null, '回填后仍可在对话框继续编辑;回填本身不会调用企查查 MCP。'),
|
|
1748
|
+
h('pre', { className: 'dcAgentPromptPreview' }, buildTaskPrompt(promptConfig())),
|
|
1749
|
+
) : null,
|
|
1750
|
+
h('p', { className: 'dcAgentPromptNote' }, '向导只生成任务描述并保存任务设置。只有你在任务中明确确认后,才会使用当前客户自己的企查查 MCP 连接、账号和额度。'),
|
|
1347
1751
|
error ? h('p', { className: 'dcAgentPromptError', role: 'alert' }, error) : null,
|
|
1348
1752
|
h('div', { className: 'dcAgentPromptActions' },
|
|
1349
|
-
h('button', { type: 'button', className: 'dcAgentPromptAction', onClick: () =>
|
|
1350
|
-
h('button', { type: 'button', className: 'dcAgentPromptAction is-primary', disabled: busy, onClick:
|
|
1753
|
+
h('button', { type: 'button', className: 'dcAgentPromptAction', disabled: wizardStep === 1, onClick: () => setWizardStep(Math.max(1, wizardStep - 1)) }, '上一步'),
|
|
1754
|
+
h('button', { type: 'button', className: 'dcAgentPromptAction is-primary', disabled: busy, onClick: nextWizard }, busy ? '处理中…' : wizardStep === 4 ? '回填到对话框' : '下一步'),
|
|
1351
1755
|
),
|
|
1352
1756
|
) : null,
|
|
1353
1757
|
);
|
|
@@ -1433,6 +1837,15 @@ window.__ModuleLoader__.load({
|
|
|
1433
1837
|
// 静默:jobs 不可用不影响工作台主流程。
|
|
1434
1838
|
}
|
|
1435
1839
|
}
|
|
1840
|
+
async function loadWorkflowTasks(actions) {
|
|
1841
|
+
try {
|
|
1842
|
+
const response = await requestJson('/data-cleaning/api/workflow/tasks');
|
|
1843
|
+
actions.setWorkflowTasks(response.tasks || []);
|
|
1844
|
+
return response.tasks || [];
|
|
1845
|
+
} catch (_error) {
|
|
1846
|
+
return [];
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1436
1849
|
function startJobsPolling(actions) {
|
|
1437
1850
|
if (jobsTimer !== null) return;
|
|
1438
1851
|
pollJobsOnce(actions);
|
|
@@ -1469,14 +1882,9 @@ window.__ModuleLoader__.load({
|
|
|
1469
1882
|
if (trimmed.startsWith('[')) {
|
|
1470
1883
|
try {
|
|
1471
1884
|
const rows = JSON.parse(trimmed);
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
fmt: 'json',
|
|
1476
|
-
headers: session.headers,
|
|
1477
|
-
rowCount: session.rows.length,
|
|
1478
|
-
preview: session.rows.slice(0, 5),
|
|
1479
|
-
});
|
|
1885
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
1886
|
+
const headers = list.length ? Object.keys(list[0]) : [];
|
|
1887
|
+
return { ok: true, fmt: 'json', headers, rowCount: list.length, rows: list, preview: list.slice(0, 5) };
|
|
1480
1888
|
} catch (error) {
|
|
1481
1889
|
actions.setError(`JSON 解析失败:${error instanceof Error ? error.message : String(error)}`);
|
|
1482
1890
|
}
|
|
@@ -1485,21 +1893,50 @@ window.__ModuleLoader__.load({
|
|
|
1485
1893
|
return api('/data-cleaning/api/mvp/parse', { filename: 'data.csv', content: trimmed });
|
|
1486
1894
|
}
|
|
1487
1895
|
|
|
1488
|
-
/**
|
|
1489
|
-
function applyParsed(result, actions) {
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1896
|
+
/** 解析成功进入 taskId runtime;不把整表写入 Host store。 */
|
|
1897
|
+
function applyParsed(result, actions, taskId = 'unassigned', source = {}) {
|
|
1898
|
+
const runtime = runtimeFor(taskId);
|
|
1899
|
+
runtime.rows = Array.isArray(result.rows) ? result.rows : [];
|
|
1900
|
+
runtime.headers = Array.isArray(result.headers) ? result.headers : [];
|
|
1901
|
+
runtime.source = {
|
|
1902
|
+
type: source.type || result.fmt || 'csv',
|
|
1903
|
+
fileName: source.fileName || '',
|
|
1904
|
+
sizeBytes: source.sizeBytes || 0,
|
|
1905
|
+
};
|
|
1906
|
+
const mappings = guessMappings(runtime.headers);
|
|
1907
|
+
const guessedNameField = mappings.find((mapping) => mapping.targetField === 'company_name')?.sourceField
|
|
1908
|
+
?? mappings.find((mapping) => mapping.targetField === 'credit_no')?.sourceField
|
|
1909
|
+
?? mappings.find((mapping) => mapping.targetField === 'reg_no')?.sourceField
|
|
1910
|
+
?? runtime.headers[0]
|
|
1495
1911
|
?? 'name';
|
|
1496
1912
|
actions.setNameField(guessedNameField);
|
|
1913
|
+
actions.setMappings(mappings);
|
|
1497
1914
|
actions.setDataset({
|
|
1498
1915
|
fmt: result.fmt ?? 'csv',
|
|
1499
|
-
headers:
|
|
1500
|
-
rowCount: typeof result.rowCount === 'number' ? result.rowCount :
|
|
1501
|
-
preview: Array.isArray(result.preview) ? result.preview :
|
|
1916
|
+
headers: runtime.headers,
|
|
1917
|
+
rowCount: typeof result.rowCount === 'number' ? result.rowCount : runtime.rows.length,
|
|
1918
|
+
preview: Array.isArray(result.preview) ? result.preview : runtime.rows.slice(0, 5),
|
|
1502
1919
|
});
|
|
1920
|
+
return runtime;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
async function persistParsedWorkflow(actions, sessionId, result, source = {}, task) {
|
|
1924
|
+
let current = task ?? await ensureWorkflowTask(actions, sessionId);
|
|
1925
|
+
const stagingKey = `session:${sessionId || 'unassigned'}`;
|
|
1926
|
+
moveRuntime(stagingKey, current.id);
|
|
1927
|
+
if (['draft', 'uploaded', 'parse_failed'].includes(current.state)) {
|
|
1928
|
+
current = await workflowAction(actions, sessionId, current, 'upload', {
|
|
1929
|
+
source: {
|
|
1930
|
+
type: source.type || result.fmt || 'csv',
|
|
1931
|
+
fileName: source.fileName || '',
|
|
1932
|
+
rowCount: typeof result.rowCount === 'number' ? result.rowCount : (result.rows || []).length,
|
|
1933
|
+
columnCount: Array.isArray(result.headers) ? result.headers.length : 0,
|
|
1934
|
+
headers: Array.isArray(result.headers) ? result.headers : [],
|
|
1935
|
+
sizeBytes: source.sizeBytes || 0,
|
|
1936
|
+
},
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
return current;
|
|
1503
1940
|
}
|
|
1504
1941
|
|
|
1505
1942
|
/** 右侧非模态工作台:中央区域始终保留 DSH 原生会话。 */
|
|
@@ -1511,6 +1948,14 @@ window.__ModuleLoader__.load({
|
|
|
1511
1948
|
const busy = useStore((state) => state.busy);
|
|
1512
1949
|
const error = useStore((state) => state.error);
|
|
1513
1950
|
const input = useStore((state) => state.input);
|
|
1951
|
+
const workflowContract = useStore((state) => state.workflowContract);
|
|
1952
|
+
const workflowTask = useStore((state) => state.workflowTask);
|
|
1953
|
+
const workflowTasks = useStore((state) => state.workflowTasks);
|
|
1954
|
+
const taskTitle = useStore((state) => state.taskTitle);
|
|
1955
|
+
const mappings = useStore((state) => state.mappings);
|
|
1956
|
+
const objectives = useStore((state) => state.objectives);
|
|
1957
|
+
const fieldSelection = useStore((state) => state.fieldSelection);
|
|
1958
|
+
const matchRules = useStore((state) => state.matchRules);
|
|
1514
1959
|
const dataset = useStore((state) => state.dataset);
|
|
1515
1960
|
const profile = useStore((state) => state.profile);
|
|
1516
1961
|
const clean = useStore((state) => state.clean);
|
|
@@ -1529,28 +1974,75 @@ window.__ModuleLoader__.load({
|
|
|
1529
1974
|
const handleOpen = (event) => {
|
|
1530
1975
|
event?.preventDefault?.();
|
|
1531
1976
|
const detail = event?.detail ?? {};
|
|
1977
|
+
if (detail.task?.id) {
|
|
1978
|
+
workflowTaskBySession.set(String(detail.sessionId || 'unassigned'), detail.task);
|
|
1979
|
+
actions.setWorkflowTask(detail.task);
|
|
1980
|
+
actions.setTaskTitle(detail.task.title);
|
|
1981
|
+
actions.setMappings(detail.task.mappings || []);
|
|
1982
|
+
actions.setObjectives(detail.task.objectives || []);
|
|
1983
|
+
actions.setFieldSelection(detail.task.fieldSelection || []);
|
|
1984
|
+
for (const [key, value] of Object.entries(detail.task.matchRules || {})) actions.setMatchRule(key, value);
|
|
1985
|
+
}
|
|
1532
1986
|
openWorkbench(actions, detail.step ?? null, detail.sessionId ?? null);
|
|
1533
1987
|
};
|
|
1534
1988
|
const handleDataset = (event) => {
|
|
1535
1989
|
event?.preventDefault?.();
|
|
1536
1990
|
const detail = event?.detail ?? {};
|
|
1537
|
-
if (detail.result) applyParsed(detail.result, actions);
|
|
1991
|
+
if (detail.result) applyParsed(detail.result, actions, `session:${detail.sessionId || 'unassigned'}`, detail.source);
|
|
1538
1992
|
if (detail.sessionId) actions.setActiveSession(detail.sessionId);
|
|
1993
|
+
if (detail.result) {
|
|
1994
|
+
void queueWorkflowOperation(detail.sessionId, () => (
|
|
1995
|
+
persistParsedWorkflow(actions, detail.sessionId, detail.result, detail.source)
|
|
1996
|
+
)).catch((eventError) => {
|
|
1997
|
+
actions.setError(eventError instanceof Error ? eventError.message : String(eventError));
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
const handleDraft = (event) => {
|
|
2002
|
+
event?.preventDefault?.();
|
|
2003
|
+
const detail = event?.detail ?? {};
|
|
2004
|
+
const draft = detail.draft ?? {};
|
|
2005
|
+
if (detail.sessionId) actions.setActiveSession(detail.sessionId);
|
|
2006
|
+
if (draft.title) actions.setTaskTitle(draft.title);
|
|
2007
|
+
if (draft.objectives) actions.setObjectives(draft.objectives);
|
|
2008
|
+
if (draft.fieldSelection) actions.setFieldSelection(draft.fieldSelection);
|
|
2009
|
+
for (const [key, value] of Object.entries(draft.matchRules || {})) actions.setMatchRule(key, value);
|
|
2010
|
+
void queueWorkflowOperation(detail.sessionId, async () => {
|
|
2011
|
+
const task = await ensureWorkflowTask(actions, detail.sessionId, draft);
|
|
2012
|
+
return updateWorkflowTask(actions, detail.sessionId, task, draft);
|
|
2013
|
+
}).catch((eventError) => actions.setError(eventError instanceof Error ? eventError.message : String(eventError)));
|
|
1539
2014
|
};
|
|
1540
2015
|
if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
|
|
1541
2016
|
document.addEventListener(WORKBENCH_OPEN_EVENT, handleOpen);
|
|
1542
2017
|
document.addEventListener(WORKBENCH_DATASET_EVENT, handleDataset);
|
|
2018
|
+
document.addEventListener(WORKBENCH_DRAFT_EVENT, handleDraft);
|
|
1543
2019
|
}
|
|
1544
2020
|
return () => {
|
|
1545
2021
|
if (typeof document !== 'undefined' && typeof document.removeEventListener === 'function') {
|
|
1546
2022
|
document.removeEventListener(WORKBENCH_OPEN_EVENT, handleOpen);
|
|
1547
2023
|
document.removeEventListener(WORKBENCH_DATASET_EVENT, handleDataset);
|
|
2024
|
+
document.removeEventListener(WORKBENCH_DRAFT_EVENT, handleDraft);
|
|
1548
2025
|
}
|
|
1549
2026
|
stopJobsPolling();
|
|
1550
2027
|
if (rootWorkbenchActions === actions) rootWorkbenchActions = null;
|
|
1551
2028
|
};
|
|
1552
2029
|
}, [actions]);
|
|
1553
2030
|
|
|
2031
|
+
react.useEffect(() => {
|
|
2032
|
+
if (!open) return undefined;
|
|
2033
|
+
let disposed = false;
|
|
2034
|
+
requestJson('/data-cleaning/api/workflow/contract').then((response) => {
|
|
2035
|
+
if (!disposed) actions.setWorkflowContract(response.contract);
|
|
2036
|
+
}).catch(() => {});
|
|
2037
|
+
loadWorkflowTasks(actions);
|
|
2038
|
+
if (activeSessionId && !workflowTaskBySession.has(String(activeSessionId))) {
|
|
2039
|
+
queueWorkflowOperation(activeSessionId, () => ensureWorkflowTask(actions, activeSessionId)).catch((taskError) => {
|
|
2040
|
+
if (!disposed) actions.setError(taskError instanceof Error ? taskError.message : String(taskError));
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
return () => { disposed = true; };
|
|
2044
|
+
}, [open, activeSessionId, actions]);
|
|
2045
|
+
|
|
1554
2046
|
// 非模态右栏会覆盖 Host 的中央画布;按实际重叠量把 composer 能力栏左移,
|
|
1555
2047
|
// 保证 Mockup 中的五个入口在窄桌面视口仍可见、可点击。展开态专注工作台,
|
|
1556
2048
|
// 不强行挤压到不足一栏的剩余区域。
|
|
@@ -1596,7 +2088,11 @@ window.__ModuleLoader__.load({
|
|
|
1596
2088
|
if (!open) return null;
|
|
1597
2089
|
|
|
1598
2090
|
const hasData = dataset !== null && dataset.rowCount > 0;
|
|
1599
|
-
const
|
|
2091
|
+
const cachedTask = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? workflowTask;
|
|
2092
|
+
const runtimeKey = cachedTask?.id ?? `session:${activeSessionId || 'unassigned'}`;
|
|
2093
|
+
const runtime = runtimeFor(runtimeKey);
|
|
2094
|
+
const lastCsv = runtime.lastCsv;
|
|
2095
|
+
const fieldByPattern = (pattern) => runtime.headers.find((field) => pattern.test(field)) ?? null;
|
|
1600
2096
|
const phoneField = fieldByPattern(/^(phone|mobile|tel|telephone|联系电话|手机号码|手机号)$/i);
|
|
1601
2097
|
const amountField = fieldByPattern(/^(amount|price|金额|注册资本)$/i);
|
|
1602
2098
|
const localCleanOptions = {
|
|
@@ -1618,7 +2114,10 @@ window.__ModuleLoader__.load({
|
|
|
1618
2114
|
actions.setError(null);
|
|
1619
2115
|
try {
|
|
1620
2116
|
const result = await parseText(input, actions);
|
|
1621
|
-
if (result)
|
|
2117
|
+
if (result) {
|
|
2118
|
+
applyParsed(result, actions, runtimeKey, { type: result.fmt || 'csv' });
|
|
2119
|
+
await persistParsedWorkflow(actions, activeSessionId, result, { type: result.fmt || 'csv' }, cachedTask);
|
|
2120
|
+
}
|
|
1622
2121
|
} catch (err) {
|
|
1623
2122
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
1624
2123
|
} finally {
|
|
@@ -1633,7 +2132,10 @@ window.__ModuleLoader__.load({
|
|
|
1633
2132
|
actions.setError(null);
|
|
1634
2133
|
try {
|
|
1635
2134
|
const result = await parseFile(file);
|
|
1636
|
-
if (result && result.ok !== false)
|
|
2135
|
+
if (result && result.ok !== false) {
|
|
2136
|
+
applyParsed(result, actions, runtimeKey, { type: result.fmt || 'csv', fileName: file.name, sizeBytes: file.size || 0 });
|
|
2137
|
+
await persistParsedWorkflow(actions, activeSessionId, result, { type: result.fmt || 'csv', fileName: file.name, sizeBytes: file.size || 0 }, cachedTask);
|
|
2138
|
+
}
|
|
1637
2139
|
else actions.setError((result && (result.message || result.error)) || '解析失败');
|
|
1638
2140
|
} catch (err) {
|
|
1639
2141
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
@@ -1642,25 +2144,28 @@ window.__ModuleLoader__.load({
|
|
|
1642
2144
|
}
|
|
1643
2145
|
};
|
|
1644
2146
|
|
|
2147
|
+
const performProfile = async (task) => {
|
|
2148
|
+
const r = await api('/data-cleaning/api/mvp/profile', {
|
|
2149
|
+
rows: runtime.rows,
|
|
2150
|
+
headers: runtime.headers,
|
|
2151
|
+
options: { amountField },
|
|
2152
|
+
});
|
|
2153
|
+
if (!r || r.ok === false) throw new Error((r && (r.message || r.error)) || '体检失败');
|
|
2154
|
+
const quality = qualitySummaryFor(runtime.rows, mappings);
|
|
2155
|
+
actions.setProfile({ ...(r.summary ?? r), workflowSummary: quality });
|
|
2156
|
+
return workflowAction(actions, activeSessionId, task, 'quality', { summary: quality });
|
|
2157
|
+
};
|
|
2158
|
+
|
|
1645
2159
|
const runProfile = async () => {
|
|
1646
|
-
if (busy || !
|
|
1647
|
-
if (!
|
|
2160
|
+
if (busy || !runtime.rows.length) {
|
|
2161
|
+
if (!runtime.rows.length) actions.setError('请先上传并解析数据。');
|
|
1648
2162
|
return;
|
|
1649
2163
|
}
|
|
1650
2164
|
actions.setBusy(true);
|
|
1651
2165
|
actions.setError(null);
|
|
1652
2166
|
try {
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
headers: session.headers,
|
|
1656
|
-
options: { amountField },
|
|
1657
|
-
});
|
|
1658
|
-
if (r && r.ok !== false) {
|
|
1659
|
-
actions.setProfile(r.summary ?? r);
|
|
1660
|
-
actions.setStep('review');
|
|
1661
|
-
} else {
|
|
1662
|
-
actions.setError((r && (r.message || r.error)) || '体检失败');
|
|
1663
|
-
}
|
|
2167
|
+
await performProfile(cachedTask);
|
|
2168
|
+
actions.setStep('match');
|
|
1664
2169
|
} catch (err) {
|
|
1665
2170
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
1666
2171
|
} finally {
|
|
@@ -1669,21 +2174,23 @@ window.__ModuleLoader__.load({
|
|
|
1669
2174
|
};
|
|
1670
2175
|
|
|
1671
2176
|
const runClean = async () => {
|
|
1672
|
-
if (busy || !
|
|
1673
|
-
if (!
|
|
2177
|
+
if (busy || !runtime.rows.length) {
|
|
2178
|
+
if (!runtime.rows.length) actions.setError('请先上传并解析数据。');
|
|
1674
2179
|
return;
|
|
1675
2180
|
}
|
|
1676
2181
|
actions.setBusy(true);
|
|
1677
2182
|
actions.setError(null);
|
|
1678
2183
|
try {
|
|
1679
2184
|
const r = await api('/data-cleaning/api/mvp/clean', {
|
|
1680
|
-
rows:
|
|
1681
|
-
headers:
|
|
2185
|
+
rows: runtime.rows,
|
|
2186
|
+
headers: runtime.headers,
|
|
1682
2187
|
options: localCleanOptions,
|
|
1683
2188
|
});
|
|
1684
2189
|
if (r && r.ok !== false) {
|
|
1685
2190
|
actions.setClean(r.summary ?? r);
|
|
1686
2191
|
lastCsv.clean = { csv: r.csv ?? '', name: r.downloadName ?? 'cleaned.csv' };
|
|
2192
|
+
runtime.resultRows.clean = Array.isArray(r.rows) ? r.rows : null;
|
|
2193
|
+
if (Array.isArray(r.headers) && r.headers.length) runtime.headers = r.headers;
|
|
1687
2194
|
} else {
|
|
1688
2195
|
actions.setError((r && (r.message || r.error)) || '清洗失败');
|
|
1689
2196
|
}
|
|
@@ -1695,21 +2202,23 @@ window.__ModuleLoader__.load({
|
|
|
1695
2202
|
};
|
|
1696
2203
|
|
|
1697
2204
|
const runComplete = async () => {
|
|
1698
|
-
if (busy || !
|
|
1699
|
-
if (!
|
|
2205
|
+
if (busy || !runtime.rows.length) {
|
|
2206
|
+
if (!runtime.rows.length) actions.setError('请先上传并解析数据。');
|
|
1700
2207
|
return;
|
|
1701
2208
|
}
|
|
1702
2209
|
actions.setBusy(true);
|
|
1703
2210
|
actions.setError(null);
|
|
1704
2211
|
try {
|
|
1705
2212
|
const r = await api('/data-cleaning/api/mvp/complete', {
|
|
1706
|
-
rows:
|
|
1707
|
-
headers:
|
|
2213
|
+
rows: runtime.rows,
|
|
2214
|
+
headers: runtime.headers,
|
|
1708
2215
|
options: localCompleteOptions,
|
|
1709
2216
|
});
|
|
1710
2217
|
if (r && r.ok !== false) {
|
|
1711
2218
|
actions.setComplete(r.summary ?? r);
|
|
1712
2219
|
lastCsv.complete = { csv: r.csv ?? '', name: r.downloadName ?? 'completed.csv' };
|
|
2220
|
+
runtime.resultRows.complete = Array.isArray(r.rows) ? r.rows : null;
|
|
2221
|
+
if (Array.isArray(r.headers) && r.headers.length) runtime.headers = r.headers;
|
|
1713
2222
|
} else {
|
|
1714
2223
|
actions.setError((r && (r.message || r.error)) || '补全失败');
|
|
1715
2224
|
}
|
|
@@ -1725,7 +2234,7 @@ window.__ModuleLoader__.load({
|
|
|
1725
2234
|
actions.setBusy(true);
|
|
1726
2235
|
actions.setError(null);
|
|
1727
2236
|
try {
|
|
1728
|
-
const r = await api('/data-cleaning/api/
|
|
2237
|
+
const r = await api('/data-cleaning/api/g5/capabilities');
|
|
1729
2238
|
if (r && r.ok !== false) actions.setQccCapabilities(r);
|
|
1730
2239
|
else actions.setError((r && (r.message || r.error)) || '企查查能力检测失败');
|
|
1731
2240
|
} catch (err) {
|
|
@@ -1736,31 +2245,61 @@ window.__ModuleLoader__.load({
|
|
|
1736
2245
|
};
|
|
1737
2246
|
|
|
1738
2247
|
const estimateQcc = async () => {
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
});
|
|
1749
|
-
if (r && r.ok !== false) actions.setQccEstimate(r.estimate);
|
|
1750
|
-
else actions.setError((r && (r.message || r.error)) || '调用估算失败');
|
|
1751
|
-
} catch (err) {
|
|
1752
|
-
actions.setError(err instanceof Error ? err.message : String(err));
|
|
1753
|
-
} finally {
|
|
1754
|
-
actions.setBusy(false);
|
|
1755
|
-
}
|
|
2248
|
+
const uniqueCompanies = new Set(runtime.rows.map((row) => String(row?.[nameField] ?? '').trim()).filter(Boolean)).size;
|
|
2249
|
+
actions.setQccEstimate({
|
|
2250
|
+
uniqueCompanies,
|
|
2251
|
+
tools: ['主体查询', '工商信息'],
|
|
2252
|
+
estimatedCalls: uniqueCompanies * 2,
|
|
2253
|
+
maxCalls: 200,
|
|
2254
|
+
withinLimit: runtime.rows.length <= 100,
|
|
2255
|
+
estimateType: 'upper-bound',
|
|
2256
|
+
});
|
|
1756
2257
|
};
|
|
1757
2258
|
|
|
1758
|
-
const applyQccRun = (run) => {
|
|
2259
|
+
const applyQccRun = async (run) => {
|
|
1759
2260
|
actions.setQccRun(run);
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
lastCsv.
|
|
1763
|
-
if (
|
|
2261
|
+
runtime.rows = Array.isArray(run.rows) ? run.rows : runtime.rows;
|
|
2262
|
+
runtime.resultRows.qcc = Array.isArray(run.rows) ? run.rows : null;
|
|
2263
|
+
lastCsv.qcc = { csv: run.csv ?? '', name: run.downloadName ?? 'qcc-enriched.csv' };
|
|
2264
|
+
if (run.reviewCsv) lastCsv.review = { csv: run.reviewCsv, name: run.reviewDownloadName ?? 'qcc-review.csv' };
|
|
2265
|
+
const summary = run.summary ?? {};
|
|
2266
|
+
const reviewRequired = Number(summary.ambiguous ?? run.reviewQueue?.length ?? 0);
|
|
2267
|
+
const currentTask = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? cachedTask;
|
|
2268
|
+
let current = currentTask;
|
|
2269
|
+
// partial 表示上一轮仍有失败项。重试成功后可直接重进 enrichment,
|
|
2270
|
+
// 无需伪造一次新的 match 转换;若重试产生候选,再回到人工核验。
|
|
2271
|
+
if (currentTask?.state !== 'partial' || reviewRequired > 0) {
|
|
2272
|
+
current = await workflowAction(actions, activeSessionId, currentTask, 'match', {
|
|
2273
|
+
qccRunId: run.runId,
|
|
2274
|
+
summary: {
|
|
2275
|
+
total: Number(summary.totalRows ?? runtime.rows.length),
|
|
2276
|
+
exact: Number(summary.enriched ?? 0),
|
|
2277
|
+
candidate: reviewRequired,
|
|
2278
|
+
confirmed: 0,
|
|
2279
|
+
unresolved: Number(summary.unresolved ?? summary.missingName ?? 0),
|
|
2280
|
+
failed: Number(summary.failed ?? 0),
|
|
2281
|
+
reviewRequired,
|
|
2282
|
+
},
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
if (reviewRequired > 0) {
|
|
2286
|
+
actions.setStep('match');
|
|
2287
|
+
return current;
|
|
2288
|
+
}
|
|
2289
|
+
current = await workflowAction(actions, activeSessionId, current, 'enrich-start', { fieldSelection });
|
|
2290
|
+
current = await workflowAction(actions, activeSessionId, current, 'enrichment', {
|
|
2291
|
+
qccRunId: run.runId,
|
|
2292
|
+
summary: {
|
|
2293
|
+
total: Number(summary.totalRows ?? runtime.rows.length),
|
|
2294
|
+
completed: Number(summary.enriched ?? 0),
|
|
2295
|
+
unchanged: Number(summary.unresolved ?? 0) + Number(summary.missingName ?? 0),
|
|
2296
|
+
failed: Number(summary.failed ?? 0),
|
|
2297
|
+
reviewRequired: 0,
|
|
2298
|
+
callsUsed: Array.isArray(run.audit) ? run.audit.length : 0,
|
|
2299
|
+
},
|
|
2300
|
+
});
|
|
2301
|
+
actions.setStep(current.stage === 'download' ? 'download' : 'enrich');
|
|
2302
|
+
return current;
|
|
1764
2303
|
};
|
|
1765
2304
|
|
|
1766
2305
|
const runQcc = async () => {
|
|
@@ -1768,20 +2307,23 @@ window.__ModuleLoader__.load({
|
|
|
1768
2307
|
actions.setBusy(true);
|
|
1769
2308
|
actions.setError(null);
|
|
1770
2309
|
try {
|
|
1771
|
-
|
|
1772
|
-
const
|
|
1773
|
-
|
|
1774
|
-
|
|
2310
|
+
await workflowAction(actions, activeSessionId, cachedTask, 'match-start');
|
|
2311
|
+
const key = `g5-ui-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2312
|
+
const r = await api('/data-cleaning/api/g5/enrich', {
|
|
2313
|
+
rows: runtime.rows,
|
|
2314
|
+
headers: runtime.headers,
|
|
1775
2315
|
nameField,
|
|
1776
|
-
|
|
1777
|
-
maxCalls: qccEstimate.maxCalls,
|
|
2316
|
+
includeRisk: false,
|
|
1778
2317
|
concurrency: 2,
|
|
1779
2318
|
confirmPaidCalls: true,
|
|
1780
2319
|
idempotencyKey: key,
|
|
1781
2320
|
});
|
|
1782
|
-
if (r && r.ok !== false) applyQccRun(r);
|
|
1783
|
-
else actions.setError((r && (r.message || r.error)) || '
|
|
2321
|
+
if (r && r.ok !== false) await applyQccRun(r);
|
|
2322
|
+
else actions.setError((r && (r.message || r.error)) || '企业匹配补全失败');
|
|
1784
2323
|
} catch (err) {
|
|
2324
|
+
if (['QCC_NOT_CONNECTED', 'QCC_TOOL_UNAVAILABLE', 'QCC_AUTH_REQUIRED'].includes(err?.code)) {
|
|
2325
|
+
try { await workflowAction(actions, activeSessionId, cachedTask, 'authorization-required'); } catch (_workflowError) {}
|
|
2326
|
+
}
|
|
1785
2327
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
1786
2328
|
} finally {
|
|
1787
2329
|
actions.setBusy(false);
|
|
@@ -1793,14 +2335,14 @@ window.__ModuleLoader__.load({
|
|
|
1793
2335
|
actions.setBusy(true);
|
|
1794
2336
|
actions.setError(null);
|
|
1795
2337
|
try {
|
|
1796
|
-
const r = await api('/data-cleaning/api/
|
|
2338
|
+
const r = await api('/data-cleaning/api/g5/resolve', {
|
|
1797
2339
|
runId: qccRun.runId,
|
|
1798
2340
|
companyName: item.companyName,
|
|
1799
2341
|
selectedCreditNo: candidate.creditNo,
|
|
1800
2342
|
confirmPaidCalls: true,
|
|
1801
|
-
idempotencyKey: `
|
|
2343
|
+
idempotencyKey: `g5-resolve-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1802
2344
|
});
|
|
1803
|
-
if (r && r.ok !== false) applyQccRun(r);
|
|
2345
|
+
if (r && r.ok !== false) await applyQccRun(r);
|
|
1804
2346
|
else actions.setError((r && (r.message || r.error)) || '候选确认失败');
|
|
1805
2347
|
} catch (err) {
|
|
1806
2348
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
@@ -1816,13 +2358,13 @@ window.__ModuleLoader__.load({
|
|
|
1816
2358
|
actions.setBusy(true);
|
|
1817
2359
|
actions.setError(null);
|
|
1818
2360
|
try {
|
|
1819
|
-
const r = await api('/data-cleaning/api/
|
|
2361
|
+
const r = await api('/data-cleaning/api/g5/retry', {
|
|
1820
2362
|
runId: qccRun.runId,
|
|
1821
2363
|
companyNames: names,
|
|
1822
2364
|
confirmPaidCalls: true,
|
|
1823
|
-
idempotencyKey: `
|
|
2365
|
+
idempotencyKey: `g5-retry-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1824
2366
|
});
|
|
1825
|
-
if (r && r.ok !== false) applyQccRun(r);
|
|
2367
|
+
if (r && r.ok !== false) await applyQccRun(r);
|
|
1826
2368
|
else actions.setError((r && (r.message || r.error)) || '失败项重试失败');
|
|
1827
2369
|
} catch (err) {
|
|
1828
2370
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
@@ -1831,46 +2373,192 @@ window.__ModuleLoader__.load({
|
|
|
1831
2373
|
}
|
|
1832
2374
|
};
|
|
1833
2375
|
|
|
1834
|
-
const
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2376
|
+
const saveTaskSettings = async () => {
|
|
2377
|
+
actions.setBusy(true);
|
|
2378
|
+
actions.setError(null);
|
|
2379
|
+
try {
|
|
2380
|
+
const current = await ensureWorkflowTask(actions, activeSessionId, {
|
|
2381
|
+
title: taskTitle, objectives, fieldSelection, mappings, matchRules,
|
|
2382
|
+
});
|
|
2383
|
+
await updateWorkflowTask(actions, activeSessionId, current, {
|
|
2384
|
+
title: taskTitle, objectives, fieldSelection, mappings, matchRules,
|
|
2385
|
+
});
|
|
2386
|
+
} catch (saveError) {
|
|
2387
|
+
actions.setError(saveError instanceof Error ? saveError.message : String(saveError));
|
|
2388
|
+
} finally {
|
|
2389
|
+
actions.setBusy(false);
|
|
2390
|
+
}
|
|
2391
|
+
};
|
|
2392
|
+
|
|
2393
|
+
const confirmRuleSettings = async () => {
|
|
2394
|
+
if (!hasData) { actions.setError('请先上传并解析数据。'); return; }
|
|
2395
|
+
const anchorMappings = mappings.filter((mapping) => ['company_name', 'credit_no', 'reg_no'].includes(mapping.targetField));
|
|
2396
|
+
if (!anchorMappings.length) { actions.setError('请至少映射企业名称、统一社会信用代码或注册号。'); return; }
|
|
2397
|
+
if (!objectives.length) { actions.setError('请至少选择一个清洗或补全目标。'); return; }
|
|
2398
|
+
actions.setBusy(true);
|
|
2399
|
+
actions.setError(null);
|
|
2400
|
+
try {
|
|
2401
|
+
let current = await ensureWorkflowTask(actions, activeSessionId, {
|
|
2402
|
+
title: taskTitle, objectives, fieldSelection, mappings, matchRules,
|
|
2403
|
+
});
|
|
2404
|
+
if (current.state === 'draft') {
|
|
2405
|
+
current = await workflowAction(actions, activeSessionId, current, 'upload', {
|
|
2406
|
+
source: {
|
|
2407
|
+
type: runtime.source?.type || dataset.fmt || 'csv',
|
|
2408
|
+
fileName: runtime.source?.fileName || '',
|
|
2409
|
+
rowCount: dataset.rowCount,
|
|
2410
|
+
columnCount: dataset.headers?.length || 0,
|
|
2411
|
+
headers: dataset.headers || [],
|
|
2412
|
+
sizeBytes: runtime.source?.sizeBytes || 0,
|
|
2413
|
+
},
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
current = await updateWorkflowTask(actions, activeSessionId, current, {
|
|
2417
|
+
title: taskTitle, objectives, fieldSelection, mappings, matchRules,
|
|
2418
|
+
});
|
|
2419
|
+
current = await workflowAction(actions, activeSessionId, current, 'rules', {
|
|
2420
|
+
objectives, fieldSelection, mappings, matchRules,
|
|
2421
|
+
});
|
|
2422
|
+
current = await performProfile(current);
|
|
2423
|
+
const primary = mappings.find((mapping) => mapping.targetField === 'company_name')
|
|
2424
|
+
?? mappings.find((mapping) => mapping.targetField === 'credit_no')
|
|
2425
|
+
?? mappings.find((mapping) => mapping.targetField === 'reg_no');
|
|
2426
|
+
if (primary) actions.setNameField(primary.sourceField);
|
|
2427
|
+
actions.setStep('profile');
|
|
2428
|
+
} catch (ruleError) {
|
|
2429
|
+
actions.setError(ruleError instanceof Error ? ruleError.message : String(ruleError));
|
|
2430
|
+
} finally {
|
|
2431
|
+
actions.setBusy(false);
|
|
2432
|
+
}
|
|
2433
|
+
};
|
|
2434
|
+
|
|
2435
|
+
const resumeWorkflowTask = (task) => {
|
|
2436
|
+
workflowTaskBySession.set(String(activeSessionId || 'unassigned'), task);
|
|
2437
|
+
actions.setWorkflowTask(task);
|
|
2438
|
+
actions.setTaskTitle(task.title);
|
|
2439
|
+
actions.setMappings(task.mappings || []);
|
|
2440
|
+
actions.setObjectives(task.objectives || []);
|
|
2441
|
+
actions.setFieldSelection(task.fieldSelection || []);
|
|
2442
|
+
for (const [key, value] of Object.entries(task.matchRules || {})) actions.setMatchRule(key, value);
|
|
2443
|
+
actions.setStep(task.stage || 'upload');
|
|
2444
|
+
if (!runtimeFor(task.id, false)?.rows?.length && task.source?.rowCount && !(task.artifacts || []).length) {
|
|
2445
|
+
actions.setDataset(null);
|
|
2446
|
+
actions.setError('已恢复任务元数据。出于隐私保护,原始企业名单未持久化;继续处理前请重新上传源文件。');
|
|
2447
|
+
}
|
|
2448
|
+
};
|
|
2449
|
+
|
|
2450
|
+
const startNewWorkflow = () => {
|
|
2451
|
+
if (cachedTask?.id) runtimeTasks.delete(String(cachedTask.id));
|
|
2452
|
+
runtimeTasks.delete('unassigned');
|
|
2453
|
+
workflowTaskBySession.delete(String(activeSessionId || 'unassigned'));
|
|
2454
|
+
actions.resetWorkflow();
|
|
2455
|
+
};
|
|
2456
|
+
|
|
2457
|
+
const exportRows = () => runtime.resultRows.qcc
|
|
2458
|
+
?? runtime.resultRows.complete
|
|
2459
|
+
?? runtime.resultRows.clean
|
|
2460
|
+
?? (runtime.rows.length ? runtime.rows : null);
|
|
2461
|
+
|
|
2462
|
+
const createArtifactBundle = async () => {
|
|
2463
|
+
let latest = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? cachedTask;
|
|
2464
|
+
if (!latest) throw new Error('请先创建并执行数据清洗补全任务。');
|
|
2465
|
+
if (Array.isArray(latest.artifacts) && latest.artifacts.length) return latest;
|
|
2466
|
+
const rows = exportRows();
|
|
2467
|
+
if (!rows?.length) throw new Error('当前页面没有可导出的明细。若任务来自历史记录,请重新上传源文件并继续处理。');
|
|
2468
|
+
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(latest.id)}/artifacts`, 'POST', {
|
|
2469
|
+
expectedRevision: latest.revision,
|
|
2470
|
+
headers: runtime.headers,
|
|
2471
|
+
rows,
|
|
2472
|
+
baseName: latest.title || taskTitle,
|
|
2473
|
+
summary: {
|
|
2474
|
+
total: rows.length,
|
|
2475
|
+
completed: rows.length,
|
|
2476
|
+
unchanged: 0,
|
|
2477
|
+
failed: 0,
|
|
2478
|
+
},
|
|
2479
|
+
});
|
|
2480
|
+
latest = cacheWorkflowTask(actions, activeSessionId, response.task);
|
|
2481
|
+
loadWorkflowTasks(actions);
|
|
2482
|
+
return latest;
|
|
2483
|
+
};
|
|
2484
|
+
|
|
2485
|
+
const downloadArtifact = async (artifact) => {
|
|
2486
|
+
if (!artifact || busy) return;
|
|
2487
|
+
actions.setBusy(true);
|
|
2488
|
+
actions.setError(null);
|
|
2489
|
+
try {
|
|
2490
|
+
const latest = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? cachedTask;
|
|
2491
|
+
const response = await fetch(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(latest.id)}/artifacts/${encodeURIComponent(artifact.id)}`, {
|
|
2492
|
+
method: 'GET', credentials: 'same-origin', headers: { accept: artifact.mediaType || 'application/octet-stream' },
|
|
2493
|
+
});
|
|
2494
|
+
if (!response.ok) {
|
|
2495
|
+
let payload = null;
|
|
2496
|
+
try { payload = await response.json(); } catch {}
|
|
2497
|
+
throw new Error(payload?.message || `下载失败(HTTP ${response.status})`);
|
|
2498
|
+
}
|
|
2499
|
+
const blob = await response.blob();
|
|
2500
|
+
const href = URL.createObjectURL(blob);
|
|
2501
|
+
const anchor = document.createElement('a');
|
|
2502
|
+
anchor.href = href;
|
|
2503
|
+
anchor.download = artifact.fileName || `export.${artifact.format || 'bin'}`;
|
|
2504
|
+
anchor.click();
|
|
2505
|
+
URL.revokeObjectURL(href);
|
|
2506
|
+
} catch (downloadError) {
|
|
2507
|
+
actions.setError(downloadError instanceof Error ? downloadError.message : String(downloadError));
|
|
2508
|
+
} finally {
|
|
2509
|
+
actions.setBusy(false);
|
|
2510
|
+
}
|
|
2511
|
+
};
|
|
2512
|
+
|
|
2513
|
+
const createAndDownload = async (kind, format) => {
|
|
2514
|
+
if (busy) return;
|
|
2515
|
+
actions.setBusy(true);
|
|
2516
|
+
actions.setError(null);
|
|
2517
|
+
try {
|
|
2518
|
+
const latest = await createArtifactBundle();
|
|
2519
|
+
const artifact = (latest.artifacts || []).find((item) => item.kind === kind && item.format === format);
|
|
2520
|
+
if (!artifact) throw new Error('Host 未生成所选格式的制品。');
|
|
2521
|
+
actions.setBusy(false);
|
|
2522
|
+
await downloadArtifact(artifact);
|
|
2523
|
+
} catch (downloadError) {
|
|
2524
|
+
actions.setError(downloadError instanceof Error ? downloadError.message : String(downloadError));
|
|
2525
|
+
actions.setBusy(false);
|
|
2526
|
+
}
|
|
1843
2527
|
};
|
|
1844
2528
|
|
|
1845
2529
|
const stat = (label, value, tone) => h('div', { className: 'dcAgentCard' },
|
|
1846
2530
|
h('span', null, label),
|
|
1847
2531
|
h('b', { className: tone ? `is-${tone}` : null }, value)
|
|
1848
2532
|
);
|
|
2533
|
+
const uiFieldGroups = Array.isArray(workflowContract?.fieldCatalog)
|
|
2534
|
+
? workflowContract.fieldCatalog.map((group) => [group.id, group.label, (group.fields || []).map((field) => [field.id, field.label])])
|
|
2535
|
+
: FIELD_GROUPS;
|
|
2536
|
+
const targetFields = uiFieldGroups.flatMap(([, , fields]) => fields);
|
|
1849
2537
|
|
|
1850
2538
|
let pane;
|
|
1851
2539
|
if (step === 'history') {
|
|
1852
2540
|
pane = h('div', { className: 'dcAgentPane' },
|
|
1853
|
-
h('p', { className: 'dcAgentHint' }, '查看当前 Host
|
|
2541
|
+
h('p', { className: 'dcAgentHint' }, '查看当前 Host 的数据清洗补全任务。Host 只保存任务元数据和摘要,不保存原始企业名单。'),
|
|
1854
2542
|
h('div', { className: 'dcAgentRow' },
|
|
1855
2543
|
h('button', {
|
|
1856
2544
|
type: 'button',
|
|
1857
2545
|
className: 'dcAgentButton',
|
|
1858
2546
|
disabled: busy,
|
|
1859
2547
|
'aria-label': '刷新任务历史',
|
|
1860
|
-
onClick: () =>
|
|
2548
|
+
onClick: () => loadWorkflowTasks(actions),
|
|
1861
2549
|
}, '刷新任务'),
|
|
1862
2550
|
h('button', {
|
|
1863
2551
|
type: 'button',
|
|
1864
2552
|
className: 'dcAgentButton is-primary',
|
|
1865
|
-
onClick:
|
|
2553
|
+
onClick: startNewWorkflow,
|
|
1866
2554
|
}, '新建清洗任务'),
|
|
1867
2555
|
),
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
h('h3', null,
|
|
2556
|
+
workflowTasks.length ? h('div', { className: 'dcAgentPane', 'aria-label': '任务历史列表' },
|
|
2557
|
+
workflowTasks.map((task) => h('button', { key: task.id, type: 'button', className: 'dcAgentHistoryTask', onClick: () => resumeWorkflowTask(task) },
|
|
2558
|
+
h('h3', null, task.title || task.id || '数据清洗任务'),
|
|
1871
2559
|
h('div', { className: 'dcAgentRow' },
|
|
1872
|
-
h('span', { className: 'dcAgentJobsPill', 'data-state':
|
|
1873
|
-
h('span', { className: 'dcAgentHint' },
|
|
2560
|
+
h('span', { className: 'dcAgentJobsPill', 'data-state': task.state ?? 'idle' }, WORKFLOW_STATE_LABELS[task.state] ?? task.state ?? '未知'),
|
|
2561
|
+
h('span', { className: 'dcAgentHint' }, `${task.source?.rowCount ?? 0} 行 · ${task.updatedAt ? new Date(task.updatedAt).toLocaleString() : ''}`),
|
|
1874
2562
|
),
|
|
1875
2563
|
)),
|
|
1876
2564
|
) : h('section', { className: 'dcAgentSection' },
|
|
@@ -1878,6 +2566,58 @@ window.__ModuleLoader__.load({
|
|
|
1878
2566
|
h('p', { className: 'dcAgentHint' }, '上传企业名单并开始处理后,任务状态会显示在这里。'),
|
|
1879
2567
|
),
|
|
1880
2568
|
);
|
|
2569
|
+
} else if (step === 'rules') {
|
|
2570
|
+
pane = h('div', { className: 'dcAgentPane' },
|
|
2571
|
+
h('p', { className: 'dcAgentHint' }, '确认任务名称、字段映射、匹配规则和输出字段。保存草稿不会调用企查查;确认规则后才可进入质量体检。'),
|
|
2572
|
+
h('label', { className: 'dcAgentFormField' },
|
|
2573
|
+
h('span', null, '任务名称'),
|
|
2574
|
+
h('input', { className: 'dcAgentField', value: taskTitle, maxLength: 120, onChange: (event) => actions.setTaskTitle(event.target.value) }),
|
|
2575
|
+
),
|
|
2576
|
+
h('section', { className: 'dcAgentSection' },
|
|
2577
|
+
h('h3', null, '字段映射'),
|
|
2578
|
+
h('p', { className: 'dcAgentHint' }, '至少映射企业名称、统一社会信用代码或注册号其中一项。未映射列会原样保留,但不参与匹配。'),
|
|
2579
|
+
(dataset?.headers || []).map((sourceField) => h('label', { className: 'dcAgentMappingRow', key: sourceField },
|
|
2580
|
+
h('span', { title: sourceField }, sourceField),
|
|
2581
|
+
h('b', { 'aria-hidden': 'true' }, '→'),
|
|
2582
|
+
h('select', {
|
|
2583
|
+
className: 'dcAgentField',
|
|
2584
|
+
value: mappings.find((mapping) => mapping.sourceField === sourceField)?.targetField || '',
|
|
2585
|
+
'aria-label': `${sourceField} 字段映射`,
|
|
2586
|
+
onChange: (event) => actions.setMapping(sourceField, event.target.value),
|
|
2587
|
+
},
|
|
2588
|
+
h('option', { value: '' }, '不参与匹配 / 补全'),
|
|
2589
|
+
targetFields.map(([id, label]) => h('option', { key: id, value: id }, label)),
|
|
2590
|
+
),
|
|
2591
|
+
)),
|
|
2592
|
+
),
|
|
2593
|
+
h('section', { className: 'dcAgentSection' },
|
|
2594
|
+
h('h3', null, '任务目标'),
|
|
2595
|
+
h('div', { className: 'dcAgentChips' }, CLEANING_OPTIONS.map(([key, label]) => h('label', { key, className: `dcAgentChip${objectives.includes(key) ? ' is-selected' : ''}` },
|
|
2596
|
+
h('input', { type: 'checkbox', checked: objectives.includes(key), onChange: () => actions.toggleObjective(key) }), ` ${label}`,
|
|
2597
|
+
))),
|
|
2598
|
+
),
|
|
2599
|
+
h('section', { className: 'dcAgentSection' },
|
|
2600
|
+
h('h3', null, '匹配规则'),
|
|
2601
|
+
h('div', { className: 'dcAgentRulesGrid' },
|
|
2602
|
+
[['normalizeNames', '匹配前规范企业名称'], ['preferCreditNo', '信用代码优先精确匹配'], ['deduplicate', '重复主体合并处理'], ['manualReviewAmbiguous', '多候选必须人工确认']].map(([key, label]) => h('label', { key, className: 'dcAgentCheck' },
|
|
2603
|
+
h('input', { type: 'checkbox', checked: matchRules[key], onChange: (event) => actions.setMatchRule(key, event.target.checked) }), label,
|
|
2604
|
+
)),
|
|
2605
|
+
),
|
|
2606
|
+
),
|
|
2607
|
+
h('section', { className: 'dcAgentSection' },
|
|
2608
|
+
h('h3', null, `输出字段 · 已选 ${fieldSelection.length}`),
|
|
2609
|
+
uiFieldGroups.map(([groupId, label, fields]) => h('div', { key: groupId, className: 'dcAgentFieldGroup' },
|
|
2610
|
+
h('b', null, label),
|
|
2611
|
+
h('div', { className: 'dcAgentChips' }, fields.map(([key, fieldLabel]) => h('label', { key, className: `dcAgentChip${fieldSelection.includes(key) ? ' is-selected' : ''}` },
|
|
2612
|
+
h('input', { type: 'checkbox', checked: fieldSelection.includes(key), onChange: () => actions.toggleField(key) }), ` ${fieldLabel}`,
|
|
2613
|
+
))),
|
|
2614
|
+
)),
|
|
2615
|
+
),
|
|
2616
|
+
h('div', { className: 'dcAgentRow' },
|
|
2617
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy, onClick: saveTaskSettings }, '保存草稿'),
|
|
2618
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !hasData, 'aria-label': '确认规则并运行质量体检', onClick: confirmRuleSettings }, busy ? '保存中…' : '确认规则并运行质量体检'),
|
|
2619
|
+
),
|
|
2620
|
+
);
|
|
1881
2621
|
} else if (step === 'profile') {
|
|
1882
2622
|
pane = h('div', { className: 'dcAgentPane' },
|
|
1883
2623
|
h('p', { className: 'dcAgentHint' }, '基于已解析数据生成本地质量画像(缺失率、去重值、金额分布)。本步骤不发起企查查调用。'),
|
|
@@ -1905,45 +2645,28 @@ window.__ModuleLoader__.load({
|
|
|
1905
2645
|
))),
|
|
1906
2646
|
) : null,
|
|
1907
2647
|
h('div', { className: 'dcAgentRow' },
|
|
1908
|
-
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy, onClick: () => actions.setStep('
|
|
2648
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy, onClick: () => actions.setStep('match') }, '下一步:匹配核验'),
|
|
1909
2649
|
),
|
|
1910
2650
|
) : h('div', { className: 'dcAgentRow' },
|
|
1911
2651
|
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !hasData, 'aria-label': '生成体检报告', onClick: runProfile }, busy ? '体检中…' : '生成体检报告'),
|
|
1912
2652
|
),
|
|
1913
2653
|
);
|
|
1914
|
-
} else if (step === '
|
|
2654
|
+
} else if (step === 'match') {
|
|
1915
2655
|
pane = h('div', { className: 'dcAgentPane' },
|
|
1916
|
-
h('p', { className: 'dcAgentHint' }, '
|
|
2656
|
+
h('p', { className: 'dcAgentHint' }, '通过当前用户连接的企查查 MCP 定位企业主体并补全基础工商字段。多候选始终由人工选择;界面不生成虚构置信度。'),
|
|
1917
2657
|
h('section', { className: 'dcAgentSection' },
|
|
1918
|
-
h('h3', null, '
|
|
1919
|
-
h('div', { className: '
|
|
1920
|
-
|
|
1921
|
-
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy, 'aria-label': '执行补全', onClick: runComplete }, busy ? '处理中…' : '本地规则补全'),
|
|
1922
|
-
),
|
|
1923
|
-
clean ? h('div', { className: 'dcAgentGrid' },
|
|
1924
|
-
stat('总数', clean.total), stat('保留', clean.kept, 'good'), stat('剔除', clean.dropped, 'bad'),
|
|
1925
|
-
stat('缺失关键字段', clean.badMissing, clean.badMissing > 0 ? 'bad' : null),
|
|
1926
|
-
stat('非法金额', clean.badAmount, clean.badAmount > 0 ? 'warn' : null),
|
|
1927
|
-
stat('重复', clean.badDuplicate, clean.badDuplicate > 0 ? 'warn' : null),
|
|
1928
|
-
) : null,
|
|
2658
|
+
h('h3', null, '匹配依据'),
|
|
2659
|
+
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}`))),
|
|
2660
|
+
h('p', { className: 'dcAgentHint' }, '统一社会信用代码等强标识优先;名称匹配不明确时进入人工核验队列。'),
|
|
1929
2661
|
),
|
|
1930
2662
|
h('section', { className: 'dcAgentSection' },
|
|
1931
|
-
h('h3', null, '
|
|
2663
|
+
h('h3', null, '企查查基础企业能力'),
|
|
1932
2664
|
h('div', { className: 'dcAgentRow' },
|
|
1933
2665
|
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy, onClick: loadQccCapabilities }, busy ? '检测中…' : '检测企查查连接'),
|
|
1934
|
-
qccCapabilities ? h('span', { className: 'dcAgentHint' }, qccCapabilities.capabilities?.ready ? '
|
|
1935
|
-
),
|
|
1936
|
-
h('div', { className: 'dcAgentChips' },
|
|
1937
|
-
[['risk', '风险信息 · 38'], ['ipr', '知识产权 · 18'], ['operation', '经营信息 · 35']].map(([domain, label]) => h('label', {
|
|
1938
|
-
key: domain,
|
|
1939
|
-
className: `dcAgentChip${selectedDomains.includes(domain) ? ' is-selected' : ''}`,
|
|
1940
|
-
},
|
|
1941
|
-
h('input', { type: 'checkbox', checked: selectedDomains.includes(domain), 'aria-label': label, onChange: () => actions.toggleDomain(domain) }),
|
|
1942
|
-
` ${label}`,
|
|
1943
|
-
)),
|
|
2666
|
+
qccCapabilities ? h('span', { className: 'dcAgentHint' }, qccCapabilities.capabilities?.ready ? '主体查询与工商信息工具已就绪' : '连接未就绪或基础工具不完整') : null,
|
|
1944
2667
|
),
|
|
1945
2668
|
h('div', { className: 'dcAgentRow' },
|
|
1946
|
-
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !
|
|
2669
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !runtime.rows.length, onClick: estimateQcc }, '估算调用量'),
|
|
1947
2670
|
),
|
|
1948
2671
|
qccEstimate ? h('div', null,
|
|
1949
2672
|
h('div', { className: 'dcAgentGrid' },
|
|
@@ -1954,10 +2677,10 @@ window.__ModuleLoader__.load({
|
|
|
1954
2677
|
),
|
|
1955
2678
|
h('label', { className: 'dcAgentCheck' },
|
|
1956
2679
|
h('input', { type: 'checkbox', checked: paidConfirmed, 'aria-label': '确认使用当前用户的企查查账号额度', onChange: (event) => actions.setPaidConfirmed(event.target.checked) }),
|
|
1957
|
-
'
|
|
2680
|
+
'我已核对企业数量和调用上界,并确认使用自己连接的企查查 MCP 账号;额度或费用由该账号自行承担',
|
|
1958
2681
|
),
|
|
1959
2682
|
h('div', { className: 'dcAgentRow' },
|
|
1960
|
-
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !paidConfirmed || !qccEstimate.withinLimit, onClick: runQcc }, busy ? '执行中…' : '
|
|
2683
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !paidConfirmed || !qccEstimate.withinLimit, onClick: runQcc }, busy ? '执行中…' : '开始主体匹配'),
|
|
1961
2684
|
),
|
|
1962
2685
|
) : null,
|
|
1963
2686
|
),
|
|
@@ -1965,11 +2688,21 @@ window.__ModuleLoader__.load({
|
|
|
1965
2688
|
h('h3', null, `任务 ${qccRun.runId} · ${qccRun.state}`),
|
|
1966
2689
|
h('div', { className: 'dcAgentGrid' },
|
|
1967
2690
|
stat('已补全', qccRun.summary?.enriched ?? 0, 'good'),
|
|
1968
|
-
stat('部分成功', qccRun.summary?.partial ?? 0, (qccRun.summary?.partial ?? 0) > 0 ? 'warn' : null),
|
|
1969
2691
|
stat('待核验', qccRun.summary?.ambiguous ?? 0, (qccRun.summary?.ambiguous ?? 0) > 0 ? 'warn' : null),
|
|
2692
|
+
stat('未匹配', qccRun.summary?.unresolved ?? 0, (qccRun.summary?.unresolved ?? 0) > 0 ? 'warn' : null),
|
|
1970
2693
|
stat('失败', qccRun.summary?.failed ?? 0, (qccRun.summary?.failed ?? 0) > 0 ? 'bad' : null),
|
|
1971
|
-
stat('实际调用', qccRun.summary?.actualCalls ?? 0),
|
|
1972
2694
|
),
|
|
2695
|
+
Array.isArray(qccRun.rows) && qccRun.rows.length ? h('div', { className: 'dcAgentTableWrap' },
|
|
2696
|
+
h('table', { className: 'dcAgentTable', 'aria-label': '匹配补全结果预览' },
|
|
2697
|
+
h('thead', null, h('tr', null,
|
|
2698
|
+
Object.keys(qccRun.rows[0] || {}).slice(0, 6).map((key) => h('th', { key }, key)),
|
|
2699
|
+
)),
|
|
2700
|
+
h('tbody', null, qccRun.rows.slice(0, 8).map((row, index) => h('tr', { key: `${index}-${row?.qcc_credit_no || row?.credit_no || ''}` },
|
|
2701
|
+
Object.keys(qccRun.rows[0] || {}).slice(0, 6).map((key) => h('td', { key }, String(row?.[key] ?? ''))),
|
|
2702
|
+
))),
|
|
2703
|
+
),
|
|
2704
|
+
h('p', { className: 'dcAgentHint' }, `显示前 ${Math.min(8, qccRun.rows.length)} 行;完整结果在下载阶段生成耐久制品。`),
|
|
2705
|
+
) : null,
|
|
1973
2706
|
(qccRun.reviewQueue || []).map((item) => h('div', { key: item.companyName, className: 'dcAgentSection' },
|
|
1974
2707
|
h('h3', null, `待核验:${item.companyName}`),
|
|
1975
2708
|
item.candidates.map((candidate) => h('div', { key: candidate.creditNo, className: 'dcAgentCandidate' },
|
|
@@ -1979,16 +2712,27 @@ window.__ModuleLoader__.load({
|
|
|
1979
2712
|
)),
|
|
1980
2713
|
)),
|
|
1981
2714
|
(qccRun.errors || []).some((item) => item.error?.retryable) ? h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !paidConfirmed, onClick: retryFailures }, '重试可恢复失败项') : null,
|
|
1982
|
-
|
|
1983
|
-
h('button', { type: 'button', className: 'dcAgentButton is-primary', onClick: () => actions.setStep('enrich') }, '进入补全与导出'),
|
|
1984
|
-
),
|
|
1985
|
-
) : h('div', { className: 'dcAgentRow' },
|
|
1986
|
-
h('button', { type: 'button', className: 'dcAgentButton', onClick: () => actions.setStep('enrich') }, '仅使用本地结果并导出'),
|
|
1987
|
-
),
|
|
2715
|
+
) : null,
|
|
1988
2716
|
);
|
|
1989
2717
|
} else if (step === 'enrich') {
|
|
1990
2718
|
pane = h('div', { className: 'dcAgentPane' },
|
|
1991
|
-
h('p', { className: 'dcAgentHint' }, '
|
|
2719
|
+
h('p', { className: 'dcAgentHint' }, '执行本地确定性清洗并核对本任务所选补全字段。外部字段以当前用户企查查 MCP 的真实返回为准,缺失值不会被编造。'),
|
|
2720
|
+
h('section', { className: 'dcAgentSection' },
|
|
2721
|
+
h('h3', null, `已选补全字段 · ${fieldSelection.length}`),
|
|
2722
|
+
h('div', { className: 'dcAgentChips' }, fieldSelection.map((id) => h('span', { className: 'dcAgentChip', key: id }, ENRICHMENT_OPTIONS.find(([fieldId]) => fieldId === id)?.[1] || id))),
|
|
2723
|
+
),
|
|
2724
|
+
h('section', { className: 'dcAgentSection' },
|
|
2725
|
+
h('h3', null, '本地清洗预处理'),
|
|
2726
|
+
h('div', { className: 'dcAgentRow' },
|
|
2727
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !hasData, 'aria-label': '执行清洗', onClick: runClean }, busy ? '处理中…' : '执行确定性清洗'),
|
|
2728
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !hasData, 'aria-label': '执行补全', onClick: runComplete }, busy ? '处理中…' : '本地规则补全'),
|
|
2729
|
+
),
|
|
2730
|
+
clean ? h('div', { className: 'dcAgentGrid' },
|
|
2731
|
+
stat('总数', clean.total), stat('保留', clean.kept, 'good'), stat('剔除', clean.dropped, 'bad'),
|
|
2732
|
+
stat('缺失关键字段', clean.badMissing, clean.badMissing > 0 ? 'bad' : null),
|
|
2733
|
+
stat('重复', clean.badDuplicate, clean.badDuplicate > 0 ? 'warn' : null),
|
|
2734
|
+
) : null,
|
|
2735
|
+
),
|
|
1992
2736
|
h('div', { className: 'dcAgentGrid' },
|
|
1993
2737
|
stat('输入行数', dataset ? dataset.rowCount : '—'),
|
|
1994
2738
|
stat('清洗保留', clean ? clean.kept : '—', clean && clean.kept > 0 ? 'good' : null),
|
|
@@ -1996,11 +2740,36 @@ window.__ModuleLoader__.load({
|
|
|
1996
2740
|
stat('QCC 已补全', qccRun ? qccRun.summary?.enriched ?? 0 : '—', qccRun && qccRun.summary?.enriched > 0 ? 'good' : null),
|
|
1997
2741
|
stat('待核验', qccRun ? qccRun.summary?.ambiguous ?? 0 : '—', qccRun && qccRun.summary?.ambiguous > 0 ? 'warn' : null),
|
|
1998
2742
|
),
|
|
1999
|
-
h('div', { className: 'dcAgentRow' },
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2743
|
+
h('div', { className: 'dcAgentRow' }, h('button', { type: 'button', className: 'dcAgentButton is-primary', onClick: () => actions.setStep('download') }, '进入下载数据')),
|
|
2744
|
+
);
|
|
2745
|
+
} else if (step === 'download') {
|
|
2746
|
+
const availableArtifacts = cachedTask?.artifacts || [];
|
|
2747
|
+
const canCreateArtifacts = Boolean(exportRows()?.length) && ['rules_confirmed', 'diagnosed', 'export_ready', 'partial'].includes(cachedTask?.state);
|
|
2748
|
+
pane = h('div', { className: 'dcAgentPane' },
|
|
2749
|
+
h('p', { className: 'dcAgentHint' }, '结果与异常清单由 Host 生成 CSV/XLSX 四件套并耐久保存。任务或插件重启后,可从任务历史继续下载;无需再次调用企查查。'),
|
|
2750
|
+
h('div', { className: 'dcAgentGrid' },
|
|
2751
|
+
stat('输入行数', dataset ? dataset.rowCount : workflowTask?.source?.rowCount ?? '—'),
|
|
2752
|
+
stat('匹配补全', qccRun ? qccRun.summary?.enriched ?? 0 : '—', qccRun && qccRun.summary?.enriched > 0 ? 'good' : null),
|
|
2753
|
+
stat('待核验', qccRun ? qccRun.summary?.ambiguous ?? 0 : '—', qccRun && qccRun.summary?.ambiguous > 0 ? 'warn' : null),
|
|
2754
|
+
stat('任务状态', WORKFLOW_STATE_LABELS[cachedTask?.state] || cachedTask?.state || '进行中'),
|
|
2755
|
+
),
|
|
2756
|
+
availableArtifacts.length ? h('div', { className: 'dcAgentArtifactList' },
|
|
2757
|
+
availableArtifacts.map((artifact) => h('button', {
|
|
2758
|
+
key: artifact.id,
|
|
2759
|
+
type: 'button',
|
|
2760
|
+
className: `dcAgentButton${artifact.kind === 'complete' && artifact.format === 'xlsx' ? ' is-primary' : ''}`,
|
|
2761
|
+
disabled: busy,
|
|
2762
|
+
onClick: () => downloadArtifact(artifact),
|
|
2763
|
+
'aria-label': `下载 ${artifact.fileName}`,
|
|
2764
|
+
}, `${artifact.kind === 'review' ? '异常清单' : '清洗补全结果'} ${String(artifact.format).toUpperCase()} · ${artifact.rowCount} 行`)),
|
|
2765
|
+
) : h('div', null,
|
|
2766
|
+
h('div', { className: 'dcAgentRow' },
|
|
2767
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !canCreateArtifacts, onClick: () => createAndDownload('complete', 'xlsx') }, '生成并下载结果 XLSX'),
|
|
2768
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !canCreateArtifacts, onClick: () => createAndDownload('complete', 'csv') }, '生成并下载结果 CSV'),
|
|
2769
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !canCreateArtifacts, onClick: () => createAndDownload('review', 'xlsx') }, '生成并下载异常清单 XLSX'),
|
|
2770
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !canCreateArtifacts, onClick: () => createAndDownload('review', 'csv') }, '生成并下载异常清单 CSV'),
|
|
2771
|
+
),
|
|
2772
|
+
!canCreateArtifacts ? h('p', { className: 'dcAgentHint' }, '请先完成规则确认与清洗补全;历史任务若尚未生成制品,需要重新上传源文件。') : null,
|
|
2004
2773
|
),
|
|
2005
2774
|
);
|
|
2006
2775
|
} else {
|
|
@@ -2026,15 +2795,15 @@ window.__ModuleLoader__.load({
|
|
|
2026
2795
|
(dataset.headers || []).slice(0, 12).map((name) => h('span', { key: name, className: 'dcAgentChip' }, name)),
|
|
2027
2796
|
(dataset.headers || []).length > 12 ? h('span', { className: 'dcAgentChip' }, `+${dataset.headers.length - 12} 列`) : null,
|
|
2028
2797
|
) : null,
|
|
2029
|
-
dataset ? h('
|
|
2030
|
-
h('
|
|
2031
|
-
|
|
2032
|
-
(dataset.
|
|
2798
|
+
dataset ? h('div', { className: 'dcAgentPreviewTable', 'aria-label': '数据预览' },
|
|
2799
|
+
h('table', { className: 'dcAgentTable' },
|
|
2800
|
+
h('thead', null, h('tr', null, (dataset.headers || []).slice(0, 8).map((name) => h('th', { key: name }, name)))),
|
|
2801
|
+
h('tbody', null, (dataset.preview || []).slice(0, 5).map((row, index) => h('tr', { key: index }, (dataset.headers || []).slice(0, 8).map((name) => h('td', { key: name }, String(row?.[name] ?? '')))))),
|
|
2033
2802
|
),
|
|
2034
2803
|
) : null,
|
|
2035
2804
|
h('div', { className: 'dcAgentRow' },
|
|
2036
2805
|
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy, 'aria-label': '解析数据', onClick: handleParse }, busy ? '解析中…' : '解析数据'),
|
|
2037
|
-
hasData ? h('button', { type: 'button', className: 'dcAgentButton', onClick: () => actions.setStep('
|
|
2806
|
+
hasData ? h('button', { type: 'button', className: 'dcAgentButton', onClick: () => actions.setStep('rules') }, '下一步:字段映射与规则') : null,
|
|
2038
2807
|
),
|
|
2039
2808
|
);
|
|
2040
2809
|
}
|
|
@@ -2068,7 +2837,7 @@ window.__ModuleLoader__.load({
|
|
|
2068
2837
|
h('span', { className: 'dcAgentWbIcon', 'aria-hidden': 'true' }, '🧹'),
|
|
2069
2838
|
h('div', null,
|
|
2070
2839
|
h('b', null, '数据清洗补全'),
|
|
2071
|
-
h('small', null,
|
|
2840
|
+
h('small', null, cachedTask ? `${WORKFLOW_STATE_LABELS[cachedTask.state] || cachedTask.state} · ${cachedTask.id.slice(0, 18)}` : '正在创建 Host taskId…'),
|
|
2072
2841
|
),
|
|
2073
2842
|
),
|
|
2074
2843
|
h('span', {
|
|
@@ -2190,7 +2959,16 @@ window.__ModuleLoader__.load({
|
|
|
2190
2959
|
exports.apply = apply;
|
|
2191
2960
|
exports.inject = inject;
|
|
2192
2961
|
// 测试用纯函数,不构成 Host / DSH 稳定 API。
|
|
2193
|
-
exports.__testing = {
|
|
2962
|
+
exports.__testing = {
|
|
2963
|
+
buildTaskPrompt,
|
|
2964
|
+
entriesToDataset,
|
|
2965
|
+
extractPromptEntries,
|
|
2966
|
+
guessMappings,
|
|
2967
|
+
markCleaningSession,
|
|
2968
|
+
qualitySummaryFor,
|
|
2969
|
+
ensureWorkflowTask,
|
|
2970
|
+
queueWorkflowOperation,
|
|
2971
|
+
};
|
|
2194
2972
|
return module.exports;
|
|
2195
2973
|
},
|
|
2196
2974
|
});
|