dsh-data-cleaning-agent 0.5.3 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/README.en.md +17 -8
- package/README.md +15 -7
- package/docs/COMPATIBILITY.md +28 -7
- package/docs/G5-HOST-BRIDGE.md +34 -8
- package/docs/QCC-ENRICHMENT-DESIGN.md +4 -2
- package/docs/QCC-PHASES-ROADMAP.md +3 -2
- package/docs/RELEASE-0.5.3.md +10 -3
- package/docs/RELEASE-0.6.0.md +57 -0
- package/docs/RELEASE-0.6.1.md +50 -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 +173 -0
- package/docs/USER-GUIDE.md +11 -8
- package/lib/artifacts.js +239 -0
- package/lib/client.js +1298 -306
- package/lib/engine.js +1 -1
- package/lib/index.js +10 -5
- package/lib/jobs.js +1 -1
- package/lib/qcc-command.js +276 -0
- package/lib/qcc.js +39 -1
- package/lib/skill-enrich.js +7 -2
- package/lib/tools.js +1 -1
- package/lib/web.js +268 -1
- package/lib/workflow-contract.js +263 -0
- package/lib/workflow.js +452 -0
- package/package.json +7 -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,76 +737,151 @@ 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', '补全所选企业字段'],
|
|
794
|
+
];
|
|
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
|
+
]],
|
|
726
817
|
];
|
|
727
|
-
const ENRICHMENT_OPTIONS = [
|
|
728
|
-
|
|
729
|
-
['
|
|
730
|
-
['reg_capital', '注册资本'],
|
|
731
|
-
['establish_date', '成立日期'],
|
|
732
|
-
['reg_status', '登记 / 经营状态'],
|
|
733
|
-
['panorama', '企业全景与联系方式'],
|
|
734
|
-
['ownership', '股权与对外投资'],
|
|
735
|
-
['governance', '主要人员与变更年报'],
|
|
736
|
-
['risk', '风险信息'],
|
|
737
|
-
['ipr', '知识产权'],
|
|
738
|
-
['operation', '经营信息'],
|
|
818
|
+
const ENRICHMENT_OPTIONS = FIELD_GROUPS.flatMap(([, , fields]) => fields);
|
|
819
|
+
const MATCH_ANCHOR_OPTIONS = [
|
|
820
|
+
['company_name', '企业名称'], ['credit_no', '统一社会信用代码'], ['reg_no', '注册号'],
|
|
739
821
|
];
|
|
740
|
-
const DEFAULT_CLEANING_KEYS = ['
|
|
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
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
825
|
+
const CLEANING_SESSION_STORAGE_KEYS = [
|
|
826
|
+
'dsh.data-cleaning-agent.active-session.v2',
|
|
827
|
+
'dsh.data-cleaning-agent.sessions.v1',
|
|
828
|
+
];
|
|
829
|
+
const CLEANING_SESSION_EVENT = 'dsh:data-cleaning-session-ownership';
|
|
830
|
+
let activeCleaningSessionId = null;
|
|
746
831
|
|
|
747
832
|
try {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
}
|
|
833
|
+
// 旧版本持久化过 sessionId;但 DSH 会复用空白新会话,无法仅凭 ID 判断入口来源。
|
|
834
|
+
// 子系统归属因此只在当前页面、经菜单显式激活,并在升级时清除所有旧标记。
|
|
835
|
+
for (const key of CLEANING_SESSION_STORAGE_KEYS) window.sessionStorage?.removeItem(key);
|
|
752
836
|
} catch (_error) {
|
|
753
|
-
// sessionStorage
|
|
837
|
+
// sessionStorage 可能被禁用;内存态不受影响。
|
|
754
838
|
}
|
|
755
839
|
|
|
756
840
|
function isCleaningSession(sessionId) {
|
|
757
|
-
return typeof sessionId === 'string' &&
|
|
841
|
+
return typeof sessionId === 'string' && sessionId === activeCleaningSessionId;
|
|
758
842
|
}
|
|
759
843
|
|
|
760
844
|
function markCleaningSession(sessionId) {
|
|
761
845
|
if (typeof sessionId !== 'string' || !sessionId) return;
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
window.
|
|
765
|
-
}
|
|
766
|
-
|
|
846
|
+
activeCleaningSessionId = sessionId;
|
|
847
|
+
if (typeof window.CustomEvent === 'function' && typeof window.dispatchEvent === 'function') {
|
|
848
|
+
window.dispatchEvent(new window.CustomEvent(CLEANING_SESSION_EVENT, { detail: { sessionId, active: true } }));
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function isKnownCleaningDraft(value) {
|
|
853
|
+
if (value === DEFAULT_SESSION_PROMPT) return true;
|
|
854
|
+
if (typeof value !== 'string') return false;
|
|
855
|
+
return value.startsWith('请执行一项企业名单数据清洗补全任务。')
|
|
856
|
+
&& value.includes('企查查连接、套餐额度和费用均由当前用户自己的账号承担。')
|
|
857
|
+
&& value.includes('提供结果和待复核清单的导出。');
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function clearCleaningDraft(ctx, sessionId, onlyDefault = false) {
|
|
861
|
+
if (!sessionId) return false;
|
|
862
|
+
const conversation = typeof ctx.get === 'function' ? ctx.get('conversation') : ctx.conversation;
|
|
863
|
+
const shell = conversation?.input?.shell?.(sessionId);
|
|
864
|
+
if (!shell || typeof shell.setDraft !== 'function') return false;
|
|
865
|
+
if (onlyDefault && !isKnownCleaningDraft(shell.snapshot?.draft)) return false;
|
|
866
|
+
shell.setDraft('');
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function deactivateCleaningSession(sessionId = activeCleaningSessionId) {
|
|
871
|
+
if (!activeCleaningSessionId || (sessionId && sessionId !== activeCleaningSessionId)) return false;
|
|
872
|
+
const previousSessionId = activeCleaningSessionId;
|
|
873
|
+
activeCleaningSessionId = null;
|
|
874
|
+
if (rootWorkbenchActions) {
|
|
875
|
+
stopJobsPolling();
|
|
876
|
+
rootWorkbenchActions.close?.();
|
|
877
|
+
rootWorkbenchActions.setActiveSession?.(null);
|
|
767
878
|
}
|
|
768
879
|
if (typeof window.CustomEvent === 'function' && typeof window.dispatchEvent === 'function') {
|
|
769
|
-
window.dispatchEvent(new window.CustomEvent(CLEANING_SESSION_EVENT, {
|
|
880
|
+
window.dispatchEvent(new window.CustomEvent(CLEANING_SESSION_EVENT, {
|
|
881
|
+
detail: { sessionId: previousSessionId, active: false },
|
|
882
|
+
}));
|
|
770
883
|
}
|
|
884
|
+
return true;
|
|
771
885
|
}
|
|
772
886
|
|
|
773
887
|
function useCleaningSession(sessionId) {
|
|
@@ -776,14 +890,89 @@ window.__ModuleLoader__.load({
|
|
|
776
890
|
);
|
|
777
891
|
react.useEffect(() => {
|
|
778
892
|
const handleMarked = (event) => {
|
|
779
|
-
|
|
893
|
+
const detail = event?.detail ?? {};
|
|
894
|
+
setMarkedSessionId(detail.active === true && detail.sessionId === sessionId ? sessionId : null);
|
|
780
895
|
};
|
|
781
896
|
window.addEventListener?.(CLEANING_SESSION_EVENT, handleMarked);
|
|
782
897
|
return () => window.removeEventListener?.(CLEANING_SESSION_EVENT, handleMarked);
|
|
783
898
|
}, [sessionId]);
|
|
784
899
|
// DSH 可能复用同一个 slot component 切换会话;状态必须绑定具体 sessionId,
|
|
785
900
|
// 避免从清洗会话切回普通会话后仍残留业务首页与提示词入口。
|
|
786
|
-
return markedSessionId === sessionId
|
|
901
|
+
return markedSessionId === sessionId && isCleaningSession(sessionId);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* 清洗补全是一个显式进入的业务子系统。切换会话、点击新会话或点击侧栏其它业务
|
|
906
|
+
* 入口时立即撤销其激活态,即使 DSH 复用了同一个空白 sessionId 也不会残留首页、
|
|
907
|
+
* 提示词、能力栏或工作台。点击本插件自己的顶部入口不会提前撤销。
|
|
908
|
+
*/
|
|
909
|
+
function installSessionOwnershipBridge(ctx) {
|
|
910
|
+
let staleDraftObserver = null;
|
|
911
|
+
let staleDraftObserverTimer = null;
|
|
912
|
+
const clearStaleDefaultDraft = () => {
|
|
913
|
+
if (activeCleaningSessionId) return false;
|
|
914
|
+
const currentSessionId = ctx.sessions?.list?.getSnapshot?.()?.current;
|
|
915
|
+
const cleared = clearCleaningDraft(ctx, currentSessionId, true);
|
|
916
|
+
if (cleared && staleDraftObserver) {
|
|
917
|
+
staleDraftObserver.disconnect();
|
|
918
|
+
staleDraftObserver = null;
|
|
919
|
+
}
|
|
920
|
+
return cleared;
|
|
921
|
+
};
|
|
922
|
+
const leaveSubsystem = () => {
|
|
923
|
+
const sessionId = activeCleaningSessionId;
|
|
924
|
+
if (sessionId) clearCleaningDraft(ctx, sessionId);
|
|
925
|
+
deactivateCleaningSession(sessionId);
|
|
926
|
+
};
|
|
927
|
+
const handleSidebarNavigation = (event) => {
|
|
928
|
+
if (!activeCleaningSessionId) return;
|
|
929
|
+
const target = event?.target;
|
|
930
|
+
if (!target || typeof target.closest !== 'function') return;
|
|
931
|
+
if (target.closest('.dcAgentLauncher, .dcAgentExperience, .dcAgentPromptPopover, .dcAgentWorkbench, .dcAgentHeaderAction')) return;
|
|
932
|
+
|
|
933
|
+
const actionable = target.closest('button, a, [role="button"]');
|
|
934
|
+
if (!actionable) return;
|
|
935
|
+
const workspaceSlot = document.querySelector?.(SIDEBAR_WORKSPACES_SELECTOR);
|
|
936
|
+
const sidebarRoot = workspaceSlot?.closest?.('aside, nav, [data-slot="sidebar"], [class*="sidebar"]')
|
|
937
|
+
?? workspaceSlot?.parentElement;
|
|
938
|
+
const label = `${actionable.getAttribute?.('aria-label') ?? ''} ${actionable.textContent ?? ''}`.trim();
|
|
939
|
+
const sessionOrAgentEntry = /新建?会话|新会话|new\s+session|智能体|agent|尽调/i.test(label);
|
|
940
|
+
if (sidebarRoot?.contains?.(actionable) || sessionOrAgentEntry) leaveSubsystem();
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
document.addEventListener?.('click', handleSidebarNavigation, true);
|
|
944
|
+
// 升级 / 刷新后不恢复子系统所有权;仅清理由旧版本遗留且仍未被用户编辑的默认文案。
|
|
945
|
+
clearStaleDefaultDraft();
|
|
946
|
+
// DSH 会在插件 apply() 之后异步恢复 composer draft。在首屏稳定期内
|
|
947
|
+
// 监听 DOM 变化并仅清理完全匹配本插件默认文案,或同时命中
|
|
948
|
+
// 提示词向导固定首尾与费用声明签名的草稿;不触碰用户自写内容。
|
|
949
|
+
if (typeof MutationObserver === 'function' && document.documentElement) {
|
|
950
|
+
staleDraftObserver = new MutationObserver(clearStaleDefaultDraft);
|
|
951
|
+
staleDraftObserver.observe(document.documentElement, {
|
|
952
|
+
childList: true,
|
|
953
|
+
characterData: true,
|
|
954
|
+
subtree: true,
|
|
955
|
+
});
|
|
956
|
+
staleDraftObserverTimer = setTimeout(() => {
|
|
957
|
+
staleDraftObserver?.disconnect();
|
|
958
|
+
staleDraftObserver = null;
|
|
959
|
+
staleDraftObserverTimer = null;
|
|
960
|
+
}, 10_000);
|
|
961
|
+
}
|
|
962
|
+
const unsubscribe = ctx.sessions?.list?.subscribe?.((snapshot) => {
|
|
963
|
+
const nextSessionId = snapshot?.current;
|
|
964
|
+
if (activeCleaningSessionId && nextSessionId && nextSessionId !== activeCleaningSessionId) {
|
|
965
|
+
leaveSubsystem();
|
|
966
|
+
} else if (!activeCleaningSessionId) {
|
|
967
|
+
clearStaleDefaultDraft();
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
return () => {
|
|
971
|
+
document.removeEventListener?.('click', handleSidebarNavigation, true);
|
|
972
|
+
staleDraftObserver?.disconnect();
|
|
973
|
+
if (staleDraftObserverTimer !== null) clearTimeout(staleDraftObserverTimer);
|
|
974
|
+
if (typeof unsubscribe === 'function') unsubscribe();
|
|
975
|
+
};
|
|
787
976
|
}
|
|
788
977
|
|
|
789
978
|
function optionLabels(options, keys) {
|
|
@@ -796,6 +985,7 @@ window.__ModuleLoader__.load({
|
|
|
796
985
|
const mode = config.mode ?? 'text';
|
|
797
986
|
const cleanLabels = optionLabels(CLEANING_OPTIONS, config.cleaningKeys);
|
|
798
987
|
const enrichLabels = optionLabels(ENRICHMENT_OPTIONS, config.enrichmentKeys);
|
|
988
|
+
const anchorLabels = optionLabels(MATCH_ANCHOR_OPTIONS, config.anchorKeys);
|
|
799
989
|
const entries = Array.isArray(config.entries)
|
|
800
990
|
? config.entries.map((value) => String(value ?? '').trim()).filter(Boolean)
|
|
801
991
|
: [];
|
|
@@ -817,23 +1007,188 @@ window.__ModuleLoader__.load({
|
|
|
817
1007
|
}
|
|
818
1008
|
lines.push(`清洗要求:${cleanLabels.length ? cleanLabels.join('、') : '仅解析,不自动修改'}。`);
|
|
819
1009
|
lines.push(`需要补全的字段 / 维度:${enrichLabels.length ? enrichLabels.join('、') : '不调用外部补全,仅输出本地清洗结果'}。`);
|
|
820
|
-
lines.push('
|
|
1010
|
+
lines.push(`处理规则:优先使用${anchorLabels.length ? anchorLabels.join('、') : '企业名称、统一社会信用代码或注册号'}精确匹配;${config.matchRules?.manualReviewAmbiguous === false ? '精确匹配失败时直接输出未匹配记录' : '精确匹配失败时再进入模糊候选,存在多个候选必须暂停并让我确认,不得默认选择第一项'}。`);
|
|
821
1011
|
lines.push('请先生成数据质量体检与匹配摘要,经我确认后再执行需要消耗额度的企查查 MCP 调用;企查查连接、套餐额度和费用均由当前用户自己的账号承担。缺失或无权限字段请留空并标记原因,不得编造。');
|
|
822
1012
|
lines.push('完成后保留来源原值、标准主体、匹配状态与字段来源,并提供结果和待复核清单的导出。');
|
|
823
1013
|
return lines.join('\n\n');
|
|
824
1014
|
}
|
|
825
1015
|
|
|
826
|
-
|
|
827
|
-
|
|
1016
|
+
function entriesToDataset(entries) {
|
|
1017
|
+
// 文本向导允许企业名称和信用代码混输。统一为一个检索锚点列,避免 G5 Bridge
|
|
1018
|
+
// 只能选择单一 nameField 时遗漏信用代码行;QCC entity lookup 对两者均可检索。
|
|
1019
|
+
const rows = (Array.isArray(entries) ? entries : [])
|
|
1020
|
+
.map((value) => ({ 主体标识: String(value ?? '').trim() }))
|
|
1021
|
+
.filter((row) => row.主体标识);
|
|
1022
|
+
return {
|
|
1023
|
+
ok: true,
|
|
1024
|
+
fmt: 'text',
|
|
1025
|
+
headers: ['主体标识'],
|
|
1026
|
+
rowCount: rows.length,
|
|
1027
|
+
rows,
|
|
1028
|
+
preview: rows.slice(0, 5),
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const MAPPING_PATTERNS = [
|
|
1033
|
+
['company_name', /^(name|company|company_name|企业名称|公司名称|单位名称)$/i],
|
|
1034
|
+
['credit_no', /^(credit_no|creditCode|unified_credit_code|统一社会信用代码|信用代码)$/i],
|
|
1035
|
+
['reg_no', /^(reg_no|registration_no|注册号)$/i],
|
|
1036
|
+
['phone', /^(phone|mobile|tel|telephone|联系电话|手机号码|手机号)$/i],
|
|
1037
|
+
['province', /^(province|省份|省份地区)$/i],
|
|
1038
|
+
['registered_address', /^(address|registered_address|注册地址|企业地址|公司地址)$/i],
|
|
1039
|
+
];
|
|
1040
|
+
|
|
1041
|
+
function guessMappings(headers) {
|
|
1042
|
+
const output = [];
|
|
1043
|
+
const usedTargets = new Set();
|
|
1044
|
+
for (const sourceField of Array.isArray(headers) ? headers : []) {
|
|
1045
|
+
const match = MAPPING_PATTERNS.find(([targetField, pattern]) => !usedTargets.has(targetField) && pattern.test(String(sourceField)));
|
|
1046
|
+
if (match) {
|
|
1047
|
+
output.push({ sourceField: String(sourceField), targetField: match[0] });
|
|
1048
|
+
usedTargets.add(match[0]);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (!output.length && Array.isArray(headers) && headers[0]) {
|
|
1052
|
+
output.push({ sourceField: String(headers[0]), targetField: 'company_name' });
|
|
1053
|
+
}
|
|
1054
|
+
return output;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function qualitySummaryFor(rows, mappings) {
|
|
1058
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
1059
|
+
const anchors = (Array.isArray(mappings) ? mappings : []).filter((mapping) => ['company_name', 'credit_no', 'reg_no'].includes(mapping.targetField));
|
|
1060
|
+
const signatures = new Set();
|
|
1061
|
+
let missingAnchor = 0;
|
|
1062
|
+
let duplicates = 0;
|
|
1063
|
+
let invalidCreditNo = 0;
|
|
1064
|
+
let invalidPhone = 0;
|
|
1065
|
+
let emptyFields = 0;
|
|
1066
|
+
const creditSource = mappings?.find((mapping) => mapping.targetField === 'credit_no')?.sourceField;
|
|
1067
|
+
const phoneSource = mappings?.find((mapping) => mapping.targetField === 'phone')?.sourceField;
|
|
1068
|
+
for (const row of list) {
|
|
1069
|
+
const values = anchors.map((mapping) => String(row?.[mapping.sourceField] ?? '').trim()).filter(Boolean);
|
|
1070
|
+
if (!values.length) missingAnchor += 1;
|
|
1071
|
+
const signature = values.join('|').toUpperCase();
|
|
1072
|
+
if (signature && signatures.has(signature)) duplicates += 1;
|
|
1073
|
+
if (signature) signatures.add(signature);
|
|
1074
|
+
const credit = creditSource ? String(row?.[creditSource] ?? '').trim() : '';
|
|
1075
|
+
if (credit && !/^[0-9A-Z]{18}$/.test(credit)) invalidCreditNo += 1;
|
|
1076
|
+
const phone = phoneSource ? String(row?.[phoneSource] ?? '').replace(/[\s-]/g, '') : '';
|
|
1077
|
+
if (phone && !/^(?:\+?86)?(?:1\d{10}|0\d{9,11})$/.test(phone)) invalidPhone += 1;
|
|
1078
|
+
emptyFields += Object.values(row ?? {}).filter((value) => String(value ?? '').trim() === '').length;
|
|
1079
|
+
}
|
|
1080
|
+
return {
|
|
1081
|
+
total: list.length,
|
|
1082
|
+
valid: Math.max(0, list.length - missingAnchor),
|
|
1083
|
+
missingAnchor,
|
|
1084
|
+
duplicates,
|
|
1085
|
+
invalidCreditNo,
|
|
1086
|
+
invalidPhone,
|
|
1087
|
+
emptyFields,
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/** 同源 Host API;保留旧 api(path, body) 调用,同时为工作流开放 PATCH。 */
|
|
1092
|
+
async function requestJson(path, method = 'GET', body) {
|
|
828
1093
|
const res = await fetch(path, {
|
|
829
|
-
method
|
|
830
|
-
headers: body ? { 'content-type': 'application/json' } : undefined,
|
|
831
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
1094
|
+
method,
|
|
1095
|
+
headers: body !== undefined ? { 'content-type': 'application/json' } : undefined,
|
|
1096
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
1097
|
+
});
|
|
1098
|
+
const payload = await res.json();
|
|
1099
|
+
if (res.ok === false || payload?.ok === false) {
|
|
1100
|
+
const error = new Error(payload?.message || payload?.error || `Host request failed (${res.status})`);
|
|
1101
|
+
error.code = payload?.code;
|
|
1102
|
+
throw error;
|
|
1103
|
+
}
|
|
1104
|
+
return payload;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
async function api(path, body) {
|
|
1108
|
+
return requestJson(path, body === undefined ? 'GET' : 'POST', body);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
async function waitForQccCommand(commandId, attempts = 180) {
|
|
1112
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
1113
|
+
const response = await api(`/data-cleaning/api/g5/commands/${encodeURIComponent(commandId)}`);
|
|
1114
|
+
const command = response?.command;
|
|
1115
|
+
if (command?.state === 'completed' || command?.state === 'failed') return command;
|
|
1116
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1117
|
+
}
|
|
1118
|
+
const error = new Error('等待智能体执行企查查任务超时;可稍后从任务历史恢复结果。');
|
|
1119
|
+
error.code = 'QCC_COMMAND_TIMEOUT';
|
|
1120
|
+
throw error;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function cacheWorkflowTask(actions, sessionId, task) {
|
|
1124
|
+
if (!task) return null;
|
|
1125
|
+
if (sessionId) workflowTaskBySession.set(String(sessionId), task);
|
|
1126
|
+
actions.setWorkflowTask(task);
|
|
1127
|
+
if (task.title) actions.setTaskTitle(task.title);
|
|
1128
|
+
if (Array.isArray(task.objectives)) actions.setObjectives(task.objectives);
|
|
1129
|
+
if (Array.isArray(task.fieldSelection)) actions.setFieldSelection(task.fieldSelection);
|
|
1130
|
+
if (Array.isArray(task.mappings) && task.mappings.length) actions.setMappings(task.mappings);
|
|
1131
|
+
return task;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async function ensureWorkflowTask(actions, sessionId, draft = {}) {
|
|
1135
|
+
const key = String(sessionId || 'unassigned');
|
|
1136
|
+
const cached = workflowTaskBySession.get(key);
|
|
1137
|
+
if (cached) return cacheWorkflowTask(actions, sessionId, cached);
|
|
1138
|
+
let pending = workflowTaskCreationBySession.get(key);
|
|
1139
|
+
if (!pending) {
|
|
1140
|
+
pending = requestJson('/data-cleaning/api/workflow/tasks', 'POST', {
|
|
1141
|
+
title: draft.title || '企业数据清洗补全任务',
|
|
1142
|
+
objectives: draft.objectives || DEFAULT_CLEANING_KEYS,
|
|
1143
|
+
fieldSelection: draft.fieldSelection || DEFAULT_ENRICHMENT_KEYS,
|
|
1144
|
+
mappings: draft.mappings || [],
|
|
1145
|
+
matchRules: draft.matchRules,
|
|
1146
|
+
});
|
|
1147
|
+
workflowTaskCreationBySession.set(key, pending);
|
|
1148
|
+
const clearPending = () => {
|
|
1149
|
+
if (workflowTaskCreationBySession.get(key) === pending) workflowTaskCreationBySession.delete(key);
|
|
1150
|
+
};
|
|
1151
|
+
pending.then(clearPending, clearPending);
|
|
1152
|
+
}
|
|
1153
|
+
const response = await pending;
|
|
1154
|
+
moveRuntime(`session:${key}`, response.task.id);
|
|
1155
|
+
return cacheWorkflowTask(actions, sessionId, response.task);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/** 同一会话的 Host 写操作串行化,确保 expectedRevision 不因 UI 事件竞态失效。 */
|
|
1159
|
+
function queueWorkflowOperation(sessionId, operation) {
|
|
1160
|
+
const key = String(sessionId || 'unassigned');
|
|
1161
|
+
const previous = workflowOperationBySession.get(key) ?? Promise.resolve();
|
|
1162
|
+
const next = previous.catch(() => {}).then(operation);
|
|
1163
|
+
workflowOperationBySession.set(key, next);
|
|
1164
|
+
const clear = () => {
|
|
1165
|
+
if (workflowOperationBySession.get(key) === next) workflowOperationBySession.delete(key);
|
|
1166
|
+
};
|
|
1167
|
+
next.then(clear, clear);
|
|
1168
|
+
return next;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
async function updateWorkflowTask(actions, sessionId, task, input) {
|
|
1172
|
+
const latest = workflowTaskBySession.get(String(sessionId || 'unassigned')) ?? task;
|
|
1173
|
+
if (!latest) return ensureWorkflowTask(actions, sessionId, input);
|
|
1174
|
+
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(latest.id)}`, 'PATCH', {
|
|
1175
|
+
...input,
|
|
1176
|
+
expectedRevision: latest.revision,
|
|
832
1177
|
});
|
|
833
|
-
return
|
|
1178
|
+
return cacheWorkflowTask(actions, sessionId, response.task);
|
|
834
1179
|
}
|
|
835
1180
|
|
|
836
|
-
|
|
1181
|
+
async function workflowAction(actions, sessionId, task, action, input = {}) {
|
|
1182
|
+
const latest = workflowTaskBySession.get(String(sessionId || 'unassigned')) ?? task;
|
|
1183
|
+
if (!latest) throw new Error('请先创建数据清洗补全任务。');
|
|
1184
|
+
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(latest.id)}/actions/${action}`, 'POST', {
|
|
1185
|
+
...input,
|
|
1186
|
+
expectedRevision: latest.revision,
|
|
1187
|
+
});
|
|
1188
|
+
return cacheWorkflowTask(actions, sessionId, response.task);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/** 工作台 store:入口按钮与工作台共享 open,并保存当前 taskId 的可持久化 UI 元数据。 */
|
|
837
1192
|
function createWorkbenchStore() {
|
|
838
1193
|
return defineStore({
|
|
839
1194
|
init: () => ({
|
|
@@ -843,6 +1198,14 @@ window.__ModuleLoader__.load({
|
|
|
843
1198
|
busy: false,
|
|
844
1199
|
error: null,
|
|
845
1200
|
input: '',
|
|
1201
|
+
workflowContract: null,
|
|
1202
|
+
workflowTask: null,
|
|
1203
|
+
workflowTasks: [],
|
|
1204
|
+
taskTitle: '企业数据清洗补全任务',
|
|
1205
|
+
mappings: [],
|
|
1206
|
+
objectives: [...DEFAULT_CLEANING_KEYS],
|
|
1207
|
+
fieldSelection: [...DEFAULT_ENRICHMENT_KEYS],
|
|
1208
|
+
matchRules: { normalizeNames: true, preferCreditNo: true, deduplicate: true, manualReviewAmbiguous: true },
|
|
846
1209
|
dataset: null, // { fmt, headers, rowCount, preview }
|
|
847
1210
|
profile: null, // summary
|
|
848
1211
|
clean: null, // summary
|
|
@@ -870,6 +1233,46 @@ window.__ModuleLoader__.load({
|
|
|
870
1233
|
setBusy: (draft, busy) => { draft.busy = busy; },
|
|
871
1234
|
setError: (draft, error) => { draft.error = error; },
|
|
872
1235
|
setInput: (draft, input) => { draft.input = input; },
|
|
1236
|
+
setWorkflowContract: (draft, value) => { draft.workflowContract = value; },
|
|
1237
|
+
setWorkflowTask: (draft, value) => { draft.workflowTask = value ?? null; },
|
|
1238
|
+
setWorkflowTasks: (draft, value) => { draft.workflowTasks = Array.isArray(value) ? value : []; },
|
|
1239
|
+
setTaskTitle: (draft, value) => { draft.taskTitle = String(value ?? ''); },
|
|
1240
|
+
setMappings: (draft, value) => { draft.mappings = Array.isArray(value) ? value : []; },
|
|
1241
|
+
setMapping: (draft, sourceField, targetField) => {
|
|
1242
|
+
const mappings = draft.mappings.filter((item) => item.sourceField !== sourceField && item.targetField !== targetField);
|
|
1243
|
+
if (targetField) mappings.push({ sourceField, targetField });
|
|
1244
|
+
draft.mappings = mappings;
|
|
1245
|
+
},
|
|
1246
|
+
setObjectives: (draft, value) => { draft.objectives = Array.isArray(value) ? value : []; },
|
|
1247
|
+
toggleObjective: (draft, value) => {
|
|
1248
|
+
const selected = new Set(draft.objectives);
|
|
1249
|
+
if (selected.has(value)) selected.delete(value); else selected.add(value);
|
|
1250
|
+
draft.objectives = [...selected];
|
|
1251
|
+
},
|
|
1252
|
+
setFieldSelection: (draft, value) => { draft.fieldSelection = Array.isArray(value) ? value : []; },
|
|
1253
|
+
toggleField: (draft, value) => {
|
|
1254
|
+
const selected = new Set(draft.fieldSelection);
|
|
1255
|
+
if (selected.has(value)) selected.delete(value); else selected.add(value);
|
|
1256
|
+
draft.fieldSelection = [...selected];
|
|
1257
|
+
},
|
|
1258
|
+
setMatchRule: (draft, key, value) => { draft.matchRules = { ...draft.matchRules, [key]: Boolean(value) }; },
|
|
1259
|
+
resetWorkflow: (draft) => {
|
|
1260
|
+
draft.step = 'upload';
|
|
1261
|
+
draft.workflowTask = null;
|
|
1262
|
+
draft.taskTitle = '企业数据清洗补全任务';
|
|
1263
|
+
draft.mappings = [];
|
|
1264
|
+
draft.objectives = [...DEFAULT_CLEANING_KEYS];
|
|
1265
|
+
draft.fieldSelection = [...DEFAULT_ENRICHMENT_KEYS];
|
|
1266
|
+
draft.matchRules = { normalizeNames: true, preferCreditNo: true, deduplicate: true, manualReviewAmbiguous: true };
|
|
1267
|
+
draft.dataset = null;
|
|
1268
|
+
draft.profile = null;
|
|
1269
|
+
draft.clean = null;
|
|
1270
|
+
draft.complete = null;
|
|
1271
|
+
draft.qccEstimate = null;
|
|
1272
|
+
draft.qccRun = null;
|
|
1273
|
+
draft.paidConfirmed = false;
|
|
1274
|
+
draft.error = null;
|
|
1275
|
+
},
|
|
873
1276
|
setDataset: (draft, dataset) => { draft.dataset = dataset; draft.error = null; },
|
|
874
1277
|
setProfile: (draft, profile) => { draft.profile = profile; draft.error = null; },
|
|
875
1278
|
setClean: (draft, clean) => { draft.clean = clean; draft.error = null; },
|
|
@@ -967,6 +1370,15 @@ window.__ModuleLoader__.load({
|
|
|
967
1370
|
return images.length;
|
|
968
1371
|
}
|
|
969
1372
|
|
|
1373
|
+
async function sendQccAgentCommand(ctx, sessionId, prompt) {
|
|
1374
|
+
const scoped = typeof ctx.sessions?.scope === 'function' ? ctx.sessions.scope(sessionId) : null;
|
|
1375
|
+
const sessionConversation = scoped?.get?.('conversation');
|
|
1376
|
+
if (!sessionConversation || typeof sessionConversation.send !== 'function') {
|
|
1377
|
+
throw new Error('当前 DSH 版本没有可用的 Session conversation.send 能力');
|
|
1378
|
+
}
|
|
1379
|
+
await sessionConversation.send(prompt);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
970
1382
|
function capabilityIcon(item, size = 16) {
|
|
971
1383
|
return typeof item.icon === 'function'
|
|
972
1384
|
? h(item.icon, { size, 'aria-hidden': 'true' })
|
|
@@ -980,38 +1392,55 @@ window.__ModuleLoader__.load({
|
|
|
980
1392
|
|
|
981
1393
|
const WORKBENCH_OPEN_EVENT = 'dsh:data-cleaning-workbench-open';
|
|
982
1394
|
const WORKBENCH_DATASET_EVENT = 'dsh:data-cleaning-workbench-dataset';
|
|
1395
|
+
const WORKBENCH_DRAFT_EVENT = 'dsh:data-cleaning-workbench-draft';
|
|
983
1396
|
|
|
984
1397
|
// session scope 不能复用 root scope 的 store handle。DSH 当前会分别物化 root/session
|
|
985
1398
|
// 插槽组件,因此以 document 事件跨 scope 通知;闭包引用仅作为无 DOM 测试降级。
|
|
986
1399
|
let rootWorkbenchActions = null;
|
|
987
1400
|
|
|
988
1401
|
/** 从 session scope 的 composer/header 触发 root scope 工作台。 */
|
|
989
|
-
function requestWorkbenchOpen(step, sessionId) {
|
|
1402
|
+
function requestWorkbenchOpen(step, sessionId, task = null) {
|
|
990
1403
|
if (typeof document !== 'undefined'
|
|
991
1404
|
&& typeof document.createEvent === 'function'
|
|
992
1405
|
&& typeof document.dispatchEvent === 'function') {
|
|
993
1406
|
const event = document.createEvent('CustomEvent');
|
|
994
|
-
event.initCustomEvent(WORKBENCH_OPEN_EVENT, false, true, { step, sessionId });
|
|
1407
|
+
event.initCustomEvent(WORKBENCH_OPEN_EVENT, false, true, { step, sessionId, task });
|
|
995
1408
|
if (!document.dispatchEvent(event)) return;
|
|
996
1409
|
}
|
|
997
1410
|
if (rootWorkbenchActions) openWorkbench(rootWorkbenchActions, step, sessionId);
|
|
998
1411
|
}
|
|
999
1412
|
|
|
1000
1413
|
/** 把提示词生成器解析出的完整表格交给 root 工作台,不把整表塞进模型上下文。 */
|
|
1001
|
-
function requestWorkbenchDataset(result, sessionId) {
|
|
1414
|
+
function requestWorkbenchDataset(result, sessionId, source) {
|
|
1002
1415
|
if (typeof document !== 'undefined'
|
|
1003
1416
|
&& typeof document.createEvent === 'function'
|
|
1004
1417
|
&& typeof document.dispatchEvent === 'function') {
|
|
1005
1418
|
const event = document.createEvent('CustomEvent');
|
|
1006
|
-
event.initCustomEvent(WORKBENCH_DATASET_EVENT, false, true, { result, sessionId });
|
|
1419
|
+
event.initCustomEvent(WORKBENCH_DATASET_EVENT, false, true, { result, sessionId, source });
|
|
1007
1420
|
if (!document.dispatchEvent(event)) return;
|
|
1008
1421
|
}
|
|
1009
1422
|
if (rootWorkbenchActions) {
|
|
1010
|
-
applyParsed(result, rootWorkbenchActions);
|
|
1423
|
+
applyParsed(result, rootWorkbenchActions, `session:${sessionId || 'unassigned'}`, source);
|
|
1011
1424
|
rootWorkbenchActions.setActiveSession(sessionId);
|
|
1012
1425
|
}
|
|
1013
1426
|
}
|
|
1014
1427
|
|
|
1428
|
+
/** 提示词向导只提交可持久化的任务设置;名单明细仍通过 dataset 事件进入 task runtime。 */
|
|
1429
|
+
function requestWorkbenchDraft(draft, sessionId) {
|
|
1430
|
+
if (typeof document !== 'undefined'
|
|
1431
|
+
&& typeof document.createEvent === 'function'
|
|
1432
|
+
&& typeof document.dispatchEvent === 'function') {
|
|
1433
|
+
const event = document.createEvent('CustomEvent');
|
|
1434
|
+
event.initCustomEvent(WORKBENCH_DRAFT_EVENT, false, true, { draft, sessionId });
|
|
1435
|
+
if (!document.dispatchEvent(event)) return;
|
|
1436
|
+
}
|
|
1437
|
+
if (rootWorkbenchActions) {
|
|
1438
|
+
rootWorkbenchActions.setTaskTitle(draft.title);
|
|
1439
|
+
rootWorkbenchActions.setObjectives(draft.objectives);
|
|
1440
|
+
rootWorkbenchActions.setFieldSelection(draft.fieldSelection);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1015
1444
|
/** 左栏入口按钮:公开 footer 托管,真实按钮 Portal 到「新会话 / 工作区」之间。 */
|
|
1016
1445
|
function SidebarEntry(props) {
|
|
1017
1446
|
const { wide, useStore, actions, startSession } = props;
|
|
@@ -1083,16 +1512,41 @@ window.__ModuleLoader__.load({
|
|
|
1083
1512
|
);
|
|
1084
1513
|
}
|
|
1085
1514
|
|
|
1086
|
-
|
|
1515
|
+
const WORKFLOW_STATE_LABELS = {
|
|
1516
|
+
draft: '待上传', uploaded: '待确认规则', rules_confirmed: '规则已确认', diagnosed: '体检完成',
|
|
1517
|
+
matching: '匹配中', review_required: '待人工核验', matched: '匹配完成', enriching: '补全中',
|
|
1518
|
+
export_ready: '可下载', completed: '已完成', partial: '部分完成', failed: '失败',
|
|
1519
|
+
authorization_required: '待连接企查查', cancelled: '已取消', parse_failed: '解析失败',
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
function ProductHome(props = {}) {
|
|
1523
|
+
const { sessionId } = props;
|
|
1524
|
+
const [recentTasks, setRecentTasks] = react.useState([]);
|
|
1087
1525
|
const flow = [
|
|
1088
|
-
['01', '
|
|
1089
|
-
['02', '
|
|
1090
|
-
['03', '
|
|
1091
|
-
['04', '
|
|
1526
|
+
['01', '任务设置', '提示词向导明确目标'],
|
|
1527
|
+
['02', '上传数据', '文本、Excel、JSON 或图片'],
|
|
1528
|
+
['03', '规则确认', '字段映射与匹配主键'],
|
|
1529
|
+
['04', '质量体检', '缺失、重复与格式诊断'],
|
|
1530
|
+
['05', '数据匹配', '精确优先,多候选复核'],
|
|
1531
|
+
['06', '清洗补全', '按需选择当前企业字段'],
|
|
1532
|
+
['07', '下载数据', '保留原值、依据与状态'],
|
|
1092
1533
|
];
|
|
1534
|
+
react.useEffect(() => {
|
|
1535
|
+
let disposed = false;
|
|
1536
|
+
requestJson('/data-cleaning/api/workflow/tasks').then((response) => {
|
|
1537
|
+
if (!disposed) setRecentTasks((response.tasks || []).slice(0, 3));
|
|
1538
|
+
}).catch(() => {});
|
|
1539
|
+
return () => { disposed = true; };
|
|
1540
|
+
}, [sessionId]);
|
|
1093
1541
|
return h('section', { className: 'dcAgentProductHome', 'aria-label': '数据清洗补全产品介绍' },
|
|
1094
|
-
h('
|
|
1095
|
-
|
|
1542
|
+
h('div', { className: 'dcAgentHomeHero' },
|
|
1543
|
+
h('div', null,
|
|
1544
|
+
h('span', { className: 'dcAgentHomeEyebrow' }, '企业主数据治理工作台'),
|
|
1545
|
+
h('h2', null, '把企业名单变成可核验、可回写的标准数据'),
|
|
1546
|
+
h('p', null, '面向销售线索、客户尽调、供应商管理与 CRM / ERP 数据治理,先确认任务和规则,再按需使用当前用户连接的企查查 MCP。'),
|
|
1547
|
+
),
|
|
1548
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', onClick: () => requestWorkbenchOpen('upload', sessionId) }, '开始新任务'),
|
|
1549
|
+
),
|
|
1096
1550
|
h('div', { className: 'dcAgentHomeFlow', 'aria-label': '数据清洗补全工作流' },
|
|
1097
1551
|
flow.map(([index, title, hint]) => h('div', { className: 'dcAgentHomeStep', key: index },
|
|
1098
1552
|
h('b', null, index),
|
|
@@ -1106,12 +1560,33 @@ window.__ModuleLoader__.load({
|
|
|
1106
1560
|
h('span', null, '客户自带企查查连接与额度'),
|
|
1107
1561
|
h('span', null, '结果与待复核清单可导出'),
|
|
1108
1562
|
),
|
|
1563
|
+
h('div', { className: 'dcAgentHomeActions' },
|
|
1564
|
+
h('button', { type: 'button', onClick: () => requestWorkbenchOpen('profile', sessionId) }, '🩺 查看质量体检'),
|
|
1565
|
+
h('button', { type: 'button', onClick: () => requestWorkbenchOpen('match', sessionId) }, '🔎 进入匹配核验'),
|
|
1566
|
+
h('button', { type: 'button', onClick: () => requestWorkbenchOpen('history', sessionId) }, '◷ 查看任务历史'),
|
|
1567
|
+
),
|
|
1568
|
+
recentTasks.length ? h('div', { className: 'dcAgentHomeRecent', 'aria-label': '最近任务' },
|
|
1569
|
+
h('b', null, '最近任务'),
|
|
1570
|
+
recentTasks.map((task) => h('button', {
|
|
1571
|
+
key: task.id,
|
|
1572
|
+
type: 'button',
|
|
1573
|
+
onClick: () => requestWorkbenchOpen(task.stage || 'upload', sessionId, task),
|
|
1574
|
+
},
|
|
1575
|
+
h('span', null, task.title || '未命名任务'),
|
|
1576
|
+
h('small', null, WORKFLOW_STATE_LABELS[task.state] || task.state),
|
|
1577
|
+
)),
|
|
1578
|
+
) : null,
|
|
1109
1579
|
);
|
|
1110
1580
|
}
|
|
1111
1581
|
|
|
1112
1582
|
/**
|
|
1113
1583
|
* DSH 当前只公开 hero brand.mark,没有 headline slot。这里仅在本插件会话且 blank hero
|
|
1114
|
-
*
|
|
1584
|
+
* 阶段替换标题,并在卸载时恢复,避免污染普通会话。
|
|
1585
|
+
*
|
|
1586
|
+
* 某些第三方插件会全局改写 hero headline、向所有会话注入自己的 dock。清洗会话必须
|
|
1587
|
+
* 仍然保持自己的标题和单一业务入口,因此用 headline class 作为最后降级,并只隐藏与
|
|
1588
|
+
* 当前清洗 hero 同一 composer stack 内、带明确「尽调类型」aria 标签的已知外来 dock。
|
|
1589
|
+
* 所有变更均记录原值并在会话离开 / 组件卸载时恢复。
|
|
1115
1590
|
*/
|
|
1116
1591
|
function rewriteHeroChrome(sessionId, enabled) {
|
|
1117
1592
|
if (!enabled || typeof document === 'undefined' || typeof document.querySelectorAll !== 'function') return () => {};
|
|
@@ -1119,32 +1594,63 @@ window.__ModuleLoader__.load({
|
|
|
1119
1594
|
.find((element) => element?.dataset?.sessionId === sessionId);
|
|
1120
1595
|
const hero = marker?.closest?.('[data-phase="hero"]');
|
|
1121
1596
|
if (!hero) return () => {};
|
|
1122
|
-
const
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
title.
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1597
|
+
const changedTitles = new Map();
|
|
1598
|
+
const changedBadges = new Map();
|
|
1599
|
+
const hiddenForeignDocks = new Map();
|
|
1600
|
+
const composerStack = marker?.parentElement?.parentElement;
|
|
1601
|
+
|
|
1602
|
+
const fix = () => {
|
|
1603
|
+
const spans = [...hero.querySelectorAll('span')];
|
|
1604
|
+
const title = spans.find((element) => element.dataset?.dcAgentHeroTitle === 'true')
|
|
1605
|
+
?? spans.find((element) => ['探索未至之境', 'Into the Unknown'].includes(element.textContent?.trim()))
|
|
1606
|
+
?? hero.querySelector?.('[class*="headlineText"]');
|
|
1607
|
+
const badge = spans.find((element) => element.dataset?.dcAgentHeroBadge === 'true')
|
|
1608
|
+
?? spans.find((element) => ['预览版', 'Preview'].includes(element.textContent?.trim()));
|
|
1609
|
+
if (title) {
|
|
1610
|
+
if (!changedTitles.has(title)) changedTitles.set(title, title.textContent ?? '');
|
|
1611
|
+
title.dataset.dcAgentHeroTitle = 'true';
|
|
1612
|
+
if (title.textContent !== '数据清洗补全智能体') title.textContent = '数据清洗补全智能体';
|
|
1613
|
+
}
|
|
1614
|
+
if (badge) {
|
|
1615
|
+
if (!changedBadges.has(badge)) changedBadges.set(badge, badge.style?.display ?? '');
|
|
1616
|
+
badge.dataset.dcAgentHeroBadge = 'true';
|
|
1617
|
+
if (badge.style && badge.style.display !== 'none') badge.style.display = 'none';
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
const foreignSurfaces = composerStack?.querySelectorAll?.('[aria-label="尽调类型"], .qccDock') ?? [];
|
|
1621
|
+
for (const foreignSurface of foreignSurfaces) {
|
|
1622
|
+
let hideTarget = foreignSurface;
|
|
1623
|
+
while (hideTarget?.parentElement
|
|
1624
|
+
&& hideTarget.parentElement !== composerStack
|
|
1625
|
+
&& hideTarget.parentElement !== marker.parentElement) {
|
|
1626
|
+
hideTarget = hideTarget.parentElement;
|
|
1627
|
+
}
|
|
1628
|
+
// DSH 的 list slot 可能把多个插件放进同一个 data-slot 容器。此时只能隐藏
|
|
1629
|
+
// 对方自己的根节点,不能隐藏共享 slot;独立 host 则隐藏其直接容器。
|
|
1630
|
+
if ((hideTarget?.parentElement === composerStack || hideTarget?.parentElement === marker.parentElement)
|
|
1631
|
+
&& !hideTarget.contains?.(marker)) {
|
|
1632
|
+
if (!hiddenForeignDocks.has(hideTarget)) hiddenForeignDocks.set(hideTarget, hideTarget.style?.display ?? '');
|
|
1633
|
+
if (hideTarget.style && hideTarget.style.display !== 'none') hideTarget.style.display = 'none';
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
fix();
|
|
1639
|
+
const observer = typeof MutationObserver === 'function' ? new MutationObserver(fix) : null;
|
|
1640
|
+
observer?.observe?.(hero, { childList: true, subtree: true });
|
|
1137
1641
|
return () => {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1642
|
+
observer?.disconnect?.();
|
|
1643
|
+
for (const [title, originalText] of changedTitles) {
|
|
1644
|
+
if (title.textContent === '数据清洗补全智能体') title.textContent = originalText;
|
|
1141
1645
|
delete title.dataset.dcAgentHeroTitle;
|
|
1142
1646
|
}
|
|
1143
|
-
|
|
1144
|
-
if (badge.style) badge.style.display =
|
|
1145
|
-
delete badge.dataset.dcAgentOriginalDisplay;
|
|
1647
|
+
for (const [badge, originalDisplay] of changedBadges) {
|
|
1648
|
+
if (badge.style) badge.style.display = originalDisplay;
|
|
1146
1649
|
delete badge.dataset.dcAgentHeroBadge;
|
|
1147
1650
|
}
|
|
1651
|
+
for (const [foreignDock, originalDisplay] of hiddenForeignDocks) {
|
|
1652
|
+
if (foreignDock.style) foreignDock.style.display = originalDisplay;
|
|
1653
|
+
}
|
|
1148
1654
|
};
|
|
1149
1655
|
}
|
|
1150
1656
|
|
|
@@ -1161,8 +1667,8 @@ window.__ModuleLoader__.load({
|
|
|
1161
1667
|
className: `dcAgentExperience${hero ? ' is-home' : ''}`,
|
|
1162
1668
|
'data-session-id': sessionId,
|
|
1163
1669
|
},
|
|
1164
|
-
CapabilityBar
|
|
1165
|
-
hero ? ProductHome
|
|
1670
|
+
h(CapabilityBar, { sessionId }),
|
|
1671
|
+
hero ? h(ProductHome, { sessionId }) : null,
|
|
1166
1672
|
);
|
|
1167
1673
|
}
|
|
1168
1674
|
|
|
@@ -1186,6 +1692,7 @@ window.__ModuleLoader__.load({
|
|
|
1186
1692
|
const { sessionId, inputActions, attachImages } = props;
|
|
1187
1693
|
const enabled = useCleaningSession(sessionId);
|
|
1188
1694
|
const [open, setOpen] = react.useState(false);
|
|
1695
|
+
const [wizardStep, setWizardStep] = react.useState(1);
|
|
1189
1696
|
const [mode, setMode] = react.useState('text');
|
|
1190
1697
|
const [rawText, setRawText] = react.useState('');
|
|
1191
1698
|
const [entries, setEntries] = react.useState([]);
|
|
@@ -1195,6 +1702,13 @@ window.__ModuleLoader__.load({
|
|
|
1195
1702
|
const [imageAttached, setImageAttached] = react.useState(false);
|
|
1196
1703
|
const [cleaningKeys, setCleaningKeys] = react.useState(DEFAULT_CLEANING_KEYS);
|
|
1197
1704
|
const [enrichmentKeys, setEnrichmentKeys] = react.useState(DEFAULT_ENRICHMENT_KEYS);
|
|
1705
|
+
const [anchorKeys, setAnchorKeys] = react.useState(['company_name', 'credit_no']);
|
|
1706
|
+
const [matchRules, setPromptMatchRules] = react.useState({
|
|
1707
|
+
normalizeNames: true,
|
|
1708
|
+
preferCreditNo: true,
|
|
1709
|
+
deduplicate: true,
|
|
1710
|
+
manualReviewAmbiguous: true,
|
|
1711
|
+
});
|
|
1198
1712
|
const [busy, setBusy] = react.useState(false);
|
|
1199
1713
|
const [error, setError] = react.useState(null);
|
|
1200
1714
|
|
|
@@ -1226,7 +1740,9 @@ window.__ModuleLoader__.load({
|
|
|
1226
1740
|
setEntries(extracted);
|
|
1227
1741
|
setEntryCount(Number.isFinite(result.rowCount) ? result.rowCount : (Array.isArray(result.rows) ? result.rows.length : extracted.length));
|
|
1228
1742
|
setSpreadsheetFileName(file.name);
|
|
1229
|
-
requestWorkbenchDataset(result, sessionId
|
|
1743
|
+
requestWorkbenchDataset(result, sessionId, {
|
|
1744
|
+
type: result.fmt || 'xlsx', fileName: file.name, sizeBytes: file.size || 0,
|
|
1745
|
+
});
|
|
1230
1746
|
}
|
|
1231
1747
|
} catch (fileError) {
|
|
1232
1748
|
setError(fileError instanceof Error ? fileError.message : String(fileError));
|
|
@@ -1235,35 +1751,61 @@ window.__ModuleLoader__.load({
|
|
|
1235
1751
|
if (event.target) event.target.value = '';
|
|
1236
1752
|
}
|
|
1237
1753
|
};
|
|
1754
|
+
const selectedEntries = () => mode === 'text'
|
|
1755
|
+
? rawText.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
1756
|
+
: entries;
|
|
1757
|
+
const validateSource = () => {
|
|
1758
|
+
const values = selectedEntries();
|
|
1759
|
+
if (mode === 'text' && !values.length) return '请先录入企业名称或统一社会信用代码,每行一条。';
|
|
1760
|
+
if (mode === 'excel' && !values.length) return '请先上传并解析 Excel / CSV / JSON 文件。';
|
|
1761
|
+
if (mode === 'image' && !imageAttached) return '请先选择并附加一张包含企业名单的图片。';
|
|
1762
|
+
return null;
|
|
1763
|
+
};
|
|
1764
|
+
const promptConfig = () => ({
|
|
1765
|
+
mode,
|
|
1766
|
+
entries: selectedEntries(),
|
|
1767
|
+
entryCount: mode === 'excel' ? entryCount : selectedEntries().length,
|
|
1768
|
+
fileName: mode === 'image' ? imageFileName : spreadsheetFileName,
|
|
1769
|
+
cleaningKeys,
|
|
1770
|
+
enrichmentKeys,
|
|
1771
|
+
anchorKeys,
|
|
1772
|
+
matchRules,
|
|
1773
|
+
});
|
|
1238
1774
|
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
|
-
}
|
|
1775
|
+
const sourceError = validateSource();
|
|
1776
|
+
if (sourceError) { setError(sourceError); return; }
|
|
1777
|
+
if (!anchorKeys.length) { setError('请至少选择一个企业主体匹配主键。'); return; }
|
|
1778
|
+
if (!cleaningKeys.length) { setError('请至少选择一个清洗或补全目标。'); return; }
|
|
1253
1779
|
if (!inputActions || typeof inputActions.setDraft !== 'function') {
|
|
1254
1780
|
setError('当前会话输入机尚未就绪,请稍后重试。');
|
|
1255
1781
|
return;
|
|
1256
1782
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1783
|
+
const config = promptConfig();
|
|
1784
|
+
inputActions.setDraft(buildTaskPrompt(config));
|
|
1785
|
+
if (mode === 'text') {
|
|
1786
|
+
requestWorkbenchDataset(entriesToDataset(config.entries), sessionId, { type: 'text', fileName: '' });
|
|
1787
|
+
}
|
|
1788
|
+
requestWorkbenchDraft({
|
|
1789
|
+
title: `企业数据清洗补全任务 · ${config.entryCount || '待解析'} 条`,
|
|
1790
|
+
objectives: cleaningKeys,
|
|
1791
|
+
fieldSelection: enrichmentKeys,
|
|
1792
|
+
matchRules,
|
|
1793
|
+
source: { type: mode === 'excel' ? 'xlsx' : mode, fileName: config.fileName || '', rowCount: config.entryCount || 0 },
|
|
1794
|
+
}, sessionId);
|
|
1265
1795
|
setError(null);
|
|
1266
1796
|
setOpen(false);
|
|
1797
|
+
setWizardStep(1);
|
|
1798
|
+
};
|
|
1799
|
+
|
|
1800
|
+
const nextWizard = () => {
|
|
1801
|
+
if (wizardStep === 1) {
|
|
1802
|
+
const sourceError = validateSource();
|
|
1803
|
+
if (sourceError) { setError(sourceError); return; }
|
|
1804
|
+
}
|
|
1805
|
+
if (wizardStep === 2 && !anchorKeys.length) { setError('请至少选择一个匹配主键。'); return; }
|
|
1806
|
+
if (wizardStep === 3 && !cleaningKeys.length) { setError('请至少选择一个清洗或补全目标。'); return; }
|
|
1807
|
+
if (wizardStep < 4) { setError(null); setWizardStep(wizardStep + 1); return; }
|
|
1808
|
+
generatePrompt();
|
|
1267
1809
|
};
|
|
1268
1810
|
|
|
1269
1811
|
const choice = (options, values, setter) => options.map(([key, label]) => h('label', {
|
|
@@ -1305,49 +1847,75 @@ window.__ModuleLoader__.load({
|
|
|
1305
1847
|
),
|
|
1306
1848
|
h('button', { type: 'button', className: 'dcAgentPromptClose', 'aria-label': '关闭提示词生成', onClick: () => setOpen(false) }, '✕'),
|
|
1307
1849
|
),
|
|
1308
|
-
h('
|
|
1309
|
-
[['
|
|
1310
|
-
key,
|
|
1850
|
+
h('nav', { className: 'dcAgentWizardNav', 'aria-label': '任务设置步骤' },
|
|
1851
|
+
[['1', '数据来源'], ['2', '匹配规则'], ['3', '清洗与补全'], ['4', '确认描述']].map(([index, label]) => h('button', {
|
|
1852
|
+
key: index,
|
|
1311
1853
|
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)),
|
|
1854
|
+
className: `dcAgentWizardStep${wizardStep === Number(index) ? ' is-active' : ''}`,
|
|
1855
|
+
'aria-current': wizardStep === Number(index) ? 'step' : undefined,
|
|
1856
|
+
onClick: () => setWizardStep(Number(index)),
|
|
1857
|
+
}, h('b', null, index), h('span', null, label))),
|
|
1341
1858
|
),
|
|
1342
|
-
h('div', { className: '
|
|
1343
|
-
h('
|
|
1344
|
-
h('
|
|
1345
|
-
|
|
1346
|
-
|
|
1859
|
+
wizardStep === 1 ? h('div', { className: 'dcAgentWizardPane' },
|
|
1860
|
+
h('h4', null, '先告诉我数据从哪里来'),
|
|
1861
|
+
h('p', null, '企业名称、统一社会信用代码、注册号任一项都可作为匹配起点。'),
|
|
1862
|
+
h('div', { className: 'dcAgentPromptTabs', role: 'tablist', 'aria-label': '名单录入方式' },
|
|
1863
|
+
[['text', '粘贴企业名单'], ['excel', '上传本地文件'], ['image', '上传图片识别']].map(([key, label]) => h('button', {
|
|
1864
|
+
key, type: 'button', role: 'tab', 'aria-selected': mode === key,
|
|
1865
|
+
className: `dcAgentPromptTab${mode === key ? ' is-active' : ''}`,
|
|
1866
|
+
onClick: () => setSourceMode(key),
|
|
1867
|
+
}, label)),
|
|
1868
|
+
),
|
|
1869
|
+
mode === 'text' ? h('label', { className: 'dcAgentPromptField' },
|
|
1870
|
+
h('span', null, '企业名称或统一社会信用代码(每行一条)'),
|
|
1871
|
+
h('textarea', {
|
|
1872
|
+
className: 'dcAgentPromptText', value: rawText,
|
|
1873
|
+
placeholder: '企查查科技股份有限公司\n9132…\n某某信息技术(上海)有限公司',
|
|
1874
|
+
onChange: (event) => setRawText(event.target.value),
|
|
1875
|
+
}),
|
|
1876
|
+
) : h('label', { className: 'dcAgentPromptFile' },
|
|
1877
|
+
h('span', null, mode === 'image' ? '选择包含企业名单的图片' : '选择 CSV / XLSX / JSON 名单'),
|
|
1878
|
+
h('input', {
|
|
1879
|
+
type: 'file', accept: mode === 'image' ? 'image/png,image/jpeg,image/webp' : '.xlsx,.xls,.csv,.json',
|
|
1880
|
+
disabled: busy, onChange: handleFile,
|
|
1881
|
+
}),
|
|
1882
|
+
(mode === 'excel' ? spreadsheetFileName : imageFileName)
|
|
1883
|
+
? h('b', null, `${mode === 'excel' ? spreadsheetFileName : imageFileName}${mode === 'excel' ? ` · ${entryCount} 条` : ' · 已附加'}`)
|
|
1884
|
+
: null,
|
|
1885
|
+
),
|
|
1886
|
+
) : null,
|
|
1887
|
+
wizardStep === 2 ? h('div', { className: 'dcAgentWizardPane' },
|
|
1888
|
+
h('h4', null, '确认匹配规则'),
|
|
1889
|
+
h('p', null, '强标识优先;名称定位不明确时生成候选,不自动写回。'),
|
|
1890
|
+
h('div', { className: 'dcAgentPromptChoices' }, choice(MATCH_ANCHOR_OPTIONS, anchorKeys, setAnchorKeys)),
|
|
1891
|
+
h('div', { className: 'dcAgentPromptRules' },
|
|
1892
|
+
[['normalizeNames', '匹配前统一企业名称格式'], ['preferCreditNo', '有信用代码时优先精确匹配'], ['deduplicate', '匹配前合并重复主体'], ['manualReviewAmbiguous', '多候选必须人工确认']].map(([key, label]) => h('label', { key },
|
|
1893
|
+
h('input', { type: 'checkbox', checked: matchRules[key], onChange: (event) => setPromptMatchRules({ ...matchRules, [key]: event.target.checked }) }), label,
|
|
1894
|
+
)),
|
|
1895
|
+
),
|
|
1896
|
+
) : null,
|
|
1897
|
+
wizardStep === 3 ? h('div', { className: 'dcAgentWizardPane' },
|
|
1898
|
+
h('h4', null, '选择清洗目标和补全字段'),
|
|
1899
|
+
h('p', null, '仅展示当前企业数据范围;最终可用字段以当前用户连接能力为准。'),
|
|
1900
|
+
h('div', { className: 'dcAgentPromptGroup' },
|
|
1901
|
+
h('b', null, '清洗目标'),
|
|
1902
|
+
h('div', { className: 'dcAgentPromptChoices' }, choice(CLEANING_OPTIONS, cleaningKeys, setCleaningKeys)),
|
|
1903
|
+
),
|
|
1904
|
+
FIELD_GROUPS.map(([groupId, label, fields]) => h('div', { className: 'dcAgentPromptGroup', key: groupId },
|
|
1905
|
+
h('b', null, label),
|
|
1906
|
+
h('div', { className: 'dcAgentPromptChoices' }, choice(fields, enrichmentKeys, setEnrichmentKeys)),
|
|
1907
|
+
)),
|
|
1908
|
+
) : null,
|
|
1909
|
+
wizardStep === 4 ? h('div', { className: 'dcAgentWizardPane' },
|
|
1910
|
+
h('h4', null, '确认任务描述'),
|
|
1911
|
+
h('p', null, '回填后仍可在对话框继续编辑;回填本身不会调用企查查 MCP。'),
|
|
1912
|
+
h('pre', { className: 'dcAgentPromptPreview' }, buildTaskPrompt(promptConfig())),
|
|
1913
|
+
) : null,
|
|
1914
|
+
h('p', { className: 'dcAgentPromptNote' }, '向导只生成任务描述并保存任务设置。只有你在任务中明确确认后,才会使用当前客户自己的企查查 MCP 连接、账号和额度。'),
|
|
1347
1915
|
error ? h('p', { className: 'dcAgentPromptError', role: 'alert' }, error) : null,
|
|
1348
1916
|
h('div', { className: 'dcAgentPromptActions' },
|
|
1349
|
-
h('button', { type: 'button', className: 'dcAgentPromptAction', onClick: () =>
|
|
1350
|
-
h('button', { type: 'button', className: 'dcAgentPromptAction is-primary', disabled: busy, onClick:
|
|
1917
|
+
h('button', { type: 'button', className: 'dcAgentPromptAction', disabled: wizardStep === 1, onClick: () => setWizardStep(Math.max(1, wizardStep - 1)) }, '上一步'),
|
|
1918
|
+
h('button', { type: 'button', className: 'dcAgentPromptAction is-primary', disabled: busy, onClick: nextWizard }, busy ? '处理中…' : wizardStep === 4 ? '回填到对话框' : '下一步'),
|
|
1351
1919
|
),
|
|
1352
1920
|
) : null,
|
|
1353
1921
|
);
|
|
@@ -1433,6 +2001,15 @@ window.__ModuleLoader__.load({
|
|
|
1433
2001
|
// 静默:jobs 不可用不影响工作台主流程。
|
|
1434
2002
|
}
|
|
1435
2003
|
}
|
|
2004
|
+
async function loadWorkflowTasks(actions) {
|
|
2005
|
+
try {
|
|
2006
|
+
const response = await requestJson('/data-cleaning/api/workflow/tasks');
|
|
2007
|
+
actions.setWorkflowTasks(response.tasks || []);
|
|
2008
|
+
return response.tasks || [];
|
|
2009
|
+
} catch (_error) {
|
|
2010
|
+
return [];
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
1436
2013
|
function startJobsPolling(actions) {
|
|
1437
2014
|
if (jobsTimer !== null) return;
|
|
1438
2015
|
pollJobsOnce(actions);
|
|
@@ -1469,14 +2046,9 @@ window.__ModuleLoader__.load({
|
|
|
1469
2046
|
if (trimmed.startsWith('[')) {
|
|
1470
2047
|
try {
|
|
1471
2048
|
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
|
-
});
|
|
2049
|
+
const list = Array.isArray(rows) ? rows : [];
|
|
2050
|
+
const headers = list.length ? Object.keys(list[0]) : [];
|
|
2051
|
+
return { ok: true, fmt: 'json', headers, rowCount: list.length, rows: list, preview: list.slice(0, 5) };
|
|
1480
2052
|
} catch (error) {
|
|
1481
2053
|
actions.setError(`JSON 解析失败:${error instanceof Error ? error.message : String(error)}`);
|
|
1482
2054
|
}
|
|
@@ -1485,32 +2057,70 @@ window.__ModuleLoader__.load({
|
|
|
1485
2057
|
return api('/data-cleaning/api/mvp/parse', { filename: 'data.csv', content: trimmed });
|
|
1486
2058
|
}
|
|
1487
2059
|
|
|
1488
|
-
/**
|
|
1489
|
-
function applyParsed(result, actions) {
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
2060
|
+
/** 解析成功进入 taskId runtime;不把整表写入 Host store。 */
|
|
2061
|
+
function applyParsed(result, actions, taskId = 'unassigned', source = {}) {
|
|
2062
|
+
const runtime = runtimeFor(taskId);
|
|
2063
|
+
runtime.rows = Array.isArray(result.rows) ? result.rows : [];
|
|
2064
|
+
runtime.headers = Array.isArray(result.headers) ? result.headers : [];
|
|
2065
|
+
runtime.source = {
|
|
2066
|
+
type: source.type || result.fmt || 'csv',
|
|
2067
|
+
fileName: source.fileName || '',
|
|
2068
|
+
sizeBytes: source.sizeBytes || 0,
|
|
2069
|
+
};
|
|
2070
|
+
const mappings = guessMappings(runtime.headers);
|
|
2071
|
+
const guessedNameField = mappings.find((mapping) => mapping.targetField === 'company_name')?.sourceField
|
|
2072
|
+
?? mappings.find((mapping) => mapping.targetField === 'credit_no')?.sourceField
|
|
2073
|
+
?? mappings.find((mapping) => mapping.targetField === 'reg_no')?.sourceField
|
|
2074
|
+
?? runtime.headers[0]
|
|
1495
2075
|
?? 'name';
|
|
1496
2076
|
actions.setNameField(guessedNameField);
|
|
2077
|
+
actions.setMappings(mappings);
|
|
1497
2078
|
actions.setDataset({
|
|
1498
2079
|
fmt: result.fmt ?? 'csv',
|
|
1499
|
-
headers:
|
|
1500
|
-
rowCount: typeof result.rowCount === 'number' ? result.rowCount :
|
|
1501
|
-
preview: Array.isArray(result.preview) ? result.preview :
|
|
2080
|
+
headers: runtime.headers,
|
|
2081
|
+
rowCount: typeof result.rowCount === 'number' ? result.rowCount : runtime.rows.length,
|
|
2082
|
+
preview: Array.isArray(result.preview) ? result.preview : runtime.rows.slice(0, 5),
|
|
1502
2083
|
});
|
|
2084
|
+
return runtime;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
async function persistParsedWorkflow(actions, sessionId, result, source = {}, task) {
|
|
2088
|
+
let current = task ?? await ensureWorkflowTask(actions, sessionId);
|
|
2089
|
+
const stagingKey = `session:${sessionId || 'unassigned'}`;
|
|
2090
|
+
moveRuntime(stagingKey, current.id);
|
|
2091
|
+
if (['draft', 'uploaded', 'parse_failed'].includes(current.state)) {
|
|
2092
|
+
current = await workflowAction(actions, sessionId, current, 'upload', {
|
|
2093
|
+
source: {
|
|
2094
|
+
type: source.type || result.fmt || 'csv',
|
|
2095
|
+
fileName: source.fileName || '',
|
|
2096
|
+
rowCount: typeof result.rowCount === 'number' ? result.rowCount : (result.rows || []).length,
|
|
2097
|
+
columnCount: Array.isArray(result.headers) ? result.headers.length : 0,
|
|
2098
|
+
headers: Array.isArray(result.headers) ? result.headers : [],
|
|
2099
|
+
sizeBytes: source.sizeBytes || 0,
|
|
2100
|
+
},
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
return current;
|
|
1503
2104
|
}
|
|
1504
2105
|
|
|
1505
2106
|
/** 右侧非模态工作台:中央区域始终保留 DSH 原生会话。 */
|
|
1506
2107
|
function WorkbenchDrawer(props) {
|
|
1507
2108
|
const { useStore, actions } = props;
|
|
2109
|
+
const sendSessionCommand = props.sendSessionCommand;
|
|
1508
2110
|
const open = useStore((state) => state.open);
|
|
1509
2111
|
const step = useStore((state) => state.step);
|
|
1510
2112
|
const expanded = useStore((state) => state.expanded);
|
|
1511
2113
|
const busy = useStore((state) => state.busy);
|
|
1512
2114
|
const error = useStore((state) => state.error);
|
|
1513
2115
|
const input = useStore((state) => state.input);
|
|
2116
|
+
const workflowContract = useStore((state) => state.workflowContract);
|
|
2117
|
+
const workflowTask = useStore((state) => state.workflowTask);
|
|
2118
|
+
const workflowTasks = useStore((state) => state.workflowTasks);
|
|
2119
|
+
const taskTitle = useStore((state) => state.taskTitle);
|
|
2120
|
+
const mappings = useStore((state) => state.mappings);
|
|
2121
|
+
const objectives = useStore((state) => state.objectives);
|
|
2122
|
+
const fieldSelection = useStore((state) => state.fieldSelection);
|
|
2123
|
+
const matchRules = useStore((state) => state.matchRules);
|
|
1514
2124
|
const dataset = useStore((state) => state.dataset);
|
|
1515
2125
|
const profile = useStore((state) => state.profile);
|
|
1516
2126
|
const clean = useStore((state) => state.clean);
|
|
@@ -1529,28 +2139,75 @@ window.__ModuleLoader__.load({
|
|
|
1529
2139
|
const handleOpen = (event) => {
|
|
1530
2140
|
event?.preventDefault?.();
|
|
1531
2141
|
const detail = event?.detail ?? {};
|
|
2142
|
+
if (detail.task?.id) {
|
|
2143
|
+
workflowTaskBySession.set(String(detail.sessionId || 'unassigned'), detail.task);
|
|
2144
|
+
actions.setWorkflowTask(detail.task);
|
|
2145
|
+
actions.setTaskTitle(detail.task.title);
|
|
2146
|
+
actions.setMappings(detail.task.mappings || []);
|
|
2147
|
+
actions.setObjectives(detail.task.objectives || []);
|
|
2148
|
+
actions.setFieldSelection(detail.task.fieldSelection || []);
|
|
2149
|
+
for (const [key, value] of Object.entries(detail.task.matchRules || {})) actions.setMatchRule(key, value);
|
|
2150
|
+
}
|
|
1532
2151
|
openWorkbench(actions, detail.step ?? null, detail.sessionId ?? null);
|
|
1533
2152
|
};
|
|
1534
2153
|
const handleDataset = (event) => {
|
|
1535
2154
|
event?.preventDefault?.();
|
|
1536
2155
|
const detail = event?.detail ?? {};
|
|
1537
|
-
if (detail.result) applyParsed(detail.result, actions);
|
|
2156
|
+
if (detail.result) applyParsed(detail.result, actions, `session:${detail.sessionId || 'unassigned'}`, detail.source);
|
|
2157
|
+
if (detail.sessionId) actions.setActiveSession(detail.sessionId);
|
|
2158
|
+
if (detail.result) {
|
|
2159
|
+
void queueWorkflowOperation(detail.sessionId, () => (
|
|
2160
|
+
persistParsedWorkflow(actions, detail.sessionId, detail.result, detail.source)
|
|
2161
|
+
)).catch((eventError) => {
|
|
2162
|
+
actions.setError(eventError instanceof Error ? eventError.message : String(eventError));
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
const handleDraft = (event) => {
|
|
2167
|
+
event?.preventDefault?.();
|
|
2168
|
+
const detail = event?.detail ?? {};
|
|
2169
|
+
const draft = detail.draft ?? {};
|
|
1538
2170
|
if (detail.sessionId) actions.setActiveSession(detail.sessionId);
|
|
2171
|
+
if (draft.title) actions.setTaskTitle(draft.title);
|
|
2172
|
+
if (draft.objectives) actions.setObjectives(draft.objectives);
|
|
2173
|
+
if (draft.fieldSelection) actions.setFieldSelection(draft.fieldSelection);
|
|
2174
|
+
for (const [key, value] of Object.entries(draft.matchRules || {})) actions.setMatchRule(key, value);
|
|
2175
|
+
void queueWorkflowOperation(detail.sessionId, async () => {
|
|
2176
|
+
const task = await ensureWorkflowTask(actions, detail.sessionId, draft);
|
|
2177
|
+
return updateWorkflowTask(actions, detail.sessionId, task, draft);
|
|
2178
|
+
}).catch((eventError) => actions.setError(eventError instanceof Error ? eventError.message : String(eventError)));
|
|
1539
2179
|
};
|
|
1540
2180
|
if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
|
|
1541
2181
|
document.addEventListener(WORKBENCH_OPEN_EVENT, handleOpen);
|
|
1542
2182
|
document.addEventListener(WORKBENCH_DATASET_EVENT, handleDataset);
|
|
2183
|
+
document.addEventListener(WORKBENCH_DRAFT_EVENT, handleDraft);
|
|
1543
2184
|
}
|
|
1544
2185
|
return () => {
|
|
1545
2186
|
if (typeof document !== 'undefined' && typeof document.removeEventListener === 'function') {
|
|
1546
2187
|
document.removeEventListener(WORKBENCH_OPEN_EVENT, handleOpen);
|
|
1547
2188
|
document.removeEventListener(WORKBENCH_DATASET_EVENT, handleDataset);
|
|
2189
|
+
document.removeEventListener(WORKBENCH_DRAFT_EVENT, handleDraft);
|
|
1548
2190
|
}
|
|
1549
2191
|
stopJobsPolling();
|
|
1550
2192
|
if (rootWorkbenchActions === actions) rootWorkbenchActions = null;
|
|
1551
2193
|
};
|
|
1552
2194
|
}, [actions]);
|
|
1553
2195
|
|
|
2196
|
+
react.useEffect(() => {
|
|
2197
|
+
if (!open) return undefined;
|
|
2198
|
+
let disposed = false;
|
|
2199
|
+
requestJson('/data-cleaning/api/workflow/contract').then((response) => {
|
|
2200
|
+
if (!disposed) actions.setWorkflowContract(response.contract);
|
|
2201
|
+
}).catch(() => {});
|
|
2202
|
+
loadWorkflowTasks(actions);
|
|
2203
|
+
if (activeSessionId && !workflowTaskBySession.has(String(activeSessionId))) {
|
|
2204
|
+
queueWorkflowOperation(activeSessionId, () => ensureWorkflowTask(actions, activeSessionId)).catch((taskError) => {
|
|
2205
|
+
if (!disposed) actions.setError(taskError instanceof Error ? taskError.message : String(taskError));
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
return () => { disposed = true; };
|
|
2209
|
+
}, [open, activeSessionId, actions]);
|
|
2210
|
+
|
|
1554
2211
|
// 非模态右栏会覆盖 Host 的中央画布;按实际重叠量把 composer 能力栏左移,
|
|
1555
2212
|
// 保证 Mockup 中的五个入口在窄桌面视口仍可见、可点击。展开态专注工作台,
|
|
1556
2213
|
// 不强行挤压到不足一栏的剩余区域。
|
|
@@ -1596,7 +2253,12 @@ window.__ModuleLoader__.load({
|
|
|
1596
2253
|
if (!open) return null;
|
|
1597
2254
|
|
|
1598
2255
|
const hasData = dataset !== null && dataset.rowCount > 0;
|
|
1599
|
-
const
|
|
2256
|
+
const requiresQcc = objectives.includes('validate_identity') || objectives.includes('complete_fields');
|
|
2257
|
+
const cachedTask = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? workflowTask;
|
|
2258
|
+
const runtimeKey = cachedTask?.id ?? `session:${activeSessionId || 'unassigned'}`;
|
|
2259
|
+
const runtime = runtimeFor(runtimeKey);
|
|
2260
|
+
const lastCsv = runtime.lastCsv;
|
|
2261
|
+
const fieldByPattern = (pattern) => runtime.headers.find((field) => pattern.test(field)) ?? null;
|
|
1600
2262
|
const phoneField = fieldByPattern(/^(phone|mobile|tel|telephone|联系电话|手机号码|手机号)$/i);
|
|
1601
2263
|
const amountField = fieldByPattern(/^(amount|price|金额|注册资本)$/i);
|
|
1602
2264
|
const localCleanOptions = {
|
|
@@ -1618,7 +2280,10 @@ window.__ModuleLoader__.load({
|
|
|
1618
2280
|
actions.setError(null);
|
|
1619
2281
|
try {
|
|
1620
2282
|
const result = await parseText(input, actions);
|
|
1621
|
-
if (result)
|
|
2283
|
+
if (result) {
|
|
2284
|
+
applyParsed(result, actions, runtimeKey, { type: result.fmt || 'csv' });
|
|
2285
|
+
await persistParsedWorkflow(actions, activeSessionId, result, { type: result.fmt || 'csv' }, cachedTask);
|
|
2286
|
+
}
|
|
1622
2287
|
} catch (err) {
|
|
1623
2288
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
1624
2289
|
} finally {
|
|
@@ -1633,7 +2298,10 @@ window.__ModuleLoader__.load({
|
|
|
1633
2298
|
actions.setError(null);
|
|
1634
2299
|
try {
|
|
1635
2300
|
const result = await parseFile(file);
|
|
1636
|
-
if (result && result.ok !== false)
|
|
2301
|
+
if (result && result.ok !== false) {
|
|
2302
|
+
applyParsed(result, actions, runtimeKey, { type: result.fmt || 'csv', fileName: file.name, sizeBytes: file.size || 0 });
|
|
2303
|
+
await persistParsedWorkflow(actions, activeSessionId, result, { type: result.fmt || 'csv', fileName: file.name, sizeBytes: file.size || 0 }, cachedTask);
|
|
2304
|
+
}
|
|
1637
2305
|
else actions.setError((result && (result.message || result.error)) || '解析失败');
|
|
1638
2306
|
} catch (err) {
|
|
1639
2307
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
@@ -1642,25 +2310,28 @@ window.__ModuleLoader__.load({
|
|
|
1642
2310
|
}
|
|
1643
2311
|
};
|
|
1644
2312
|
|
|
2313
|
+
const performProfile = async (task) => {
|
|
2314
|
+
const r = await api('/data-cleaning/api/mvp/profile', {
|
|
2315
|
+
rows: runtime.rows,
|
|
2316
|
+
headers: runtime.headers,
|
|
2317
|
+
options: { amountField },
|
|
2318
|
+
});
|
|
2319
|
+
if (!r || r.ok === false) throw new Error((r && (r.message || r.error)) || '体检失败');
|
|
2320
|
+
const quality = qualitySummaryFor(runtime.rows, mappings);
|
|
2321
|
+
actions.setProfile({ ...(r.summary ?? r), workflowSummary: quality });
|
|
2322
|
+
return workflowAction(actions, activeSessionId, task, 'quality', { summary: quality });
|
|
2323
|
+
};
|
|
2324
|
+
|
|
1645
2325
|
const runProfile = async () => {
|
|
1646
|
-
if (busy || !
|
|
1647
|
-
if (!
|
|
2326
|
+
if (busy || !runtime.rows.length) {
|
|
2327
|
+
if (!runtime.rows.length) actions.setError('请先上传并解析数据。');
|
|
1648
2328
|
return;
|
|
1649
2329
|
}
|
|
1650
2330
|
actions.setBusy(true);
|
|
1651
2331
|
actions.setError(null);
|
|
1652
2332
|
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
|
-
}
|
|
2333
|
+
await performProfile(cachedTask);
|
|
2334
|
+
actions.setStep(requiresQcc ? 'match' : 'enrich');
|
|
1664
2335
|
} catch (err) {
|
|
1665
2336
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
1666
2337
|
} finally {
|
|
@@ -1669,21 +2340,23 @@ window.__ModuleLoader__.load({
|
|
|
1669
2340
|
};
|
|
1670
2341
|
|
|
1671
2342
|
const runClean = async () => {
|
|
1672
|
-
if (busy || !
|
|
1673
|
-
if (!
|
|
2343
|
+
if (busy || !runtime.rows.length) {
|
|
2344
|
+
if (!runtime.rows.length) actions.setError('请先上传并解析数据。');
|
|
1674
2345
|
return;
|
|
1675
2346
|
}
|
|
1676
2347
|
actions.setBusy(true);
|
|
1677
2348
|
actions.setError(null);
|
|
1678
2349
|
try {
|
|
1679
2350
|
const r = await api('/data-cleaning/api/mvp/clean', {
|
|
1680
|
-
rows:
|
|
1681
|
-
headers:
|
|
2351
|
+
rows: runtime.rows,
|
|
2352
|
+
headers: runtime.headers,
|
|
1682
2353
|
options: localCleanOptions,
|
|
1683
2354
|
});
|
|
1684
2355
|
if (r && r.ok !== false) {
|
|
1685
2356
|
actions.setClean(r.summary ?? r);
|
|
1686
2357
|
lastCsv.clean = { csv: r.csv ?? '', name: r.downloadName ?? 'cleaned.csv' };
|
|
2358
|
+
runtime.resultRows.clean = Array.isArray(r.rows) ? r.rows : null;
|
|
2359
|
+
if (Array.isArray(r.headers) && r.headers.length) runtime.headers = r.headers;
|
|
1687
2360
|
} else {
|
|
1688
2361
|
actions.setError((r && (r.message || r.error)) || '清洗失败');
|
|
1689
2362
|
}
|
|
@@ -1695,21 +2368,23 @@ window.__ModuleLoader__.load({
|
|
|
1695
2368
|
};
|
|
1696
2369
|
|
|
1697
2370
|
const runComplete = async () => {
|
|
1698
|
-
if (busy || !
|
|
1699
|
-
if (!
|
|
2371
|
+
if (busy || !runtime.rows.length) {
|
|
2372
|
+
if (!runtime.rows.length) actions.setError('请先上传并解析数据。');
|
|
1700
2373
|
return;
|
|
1701
2374
|
}
|
|
1702
2375
|
actions.setBusy(true);
|
|
1703
2376
|
actions.setError(null);
|
|
1704
2377
|
try {
|
|
1705
2378
|
const r = await api('/data-cleaning/api/mvp/complete', {
|
|
1706
|
-
rows:
|
|
1707
|
-
headers:
|
|
2379
|
+
rows: runtime.rows,
|
|
2380
|
+
headers: runtime.headers,
|
|
1708
2381
|
options: localCompleteOptions,
|
|
1709
2382
|
});
|
|
1710
2383
|
if (r && r.ok !== false) {
|
|
1711
2384
|
actions.setComplete(r.summary ?? r);
|
|
1712
2385
|
lastCsv.complete = { csv: r.csv ?? '', name: r.downloadName ?? 'completed.csv' };
|
|
2386
|
+
runtime.resultRows.complete = Array.isArray(r.rows) ? r.rows : null;
|
|
2387
|
+
if (Array.isArray(r.headers) && r.headers.length) runtime.headers = r.headers;
|
|
1713
2388
|
} else {
|
|
1714
2389
|
actions.setError((r && (r.message || r.error)) || '补全失败');
|
|
1715
2390
|
}
|
|
@@ -1725,7 +2400,7 @@ window.__ModuleLoader__.load({
|
|
|
1725
2400
|
actions.setBusy(true);
|
|
1726
2401
|
actions.setError(null);
|
|
1727
2402
|
try {
|
|
1728
|
-
const r = await api('/data-cleaning/api/
|
|
2403
|
+
const r = await api('/data-cleaning/api/g5/capabilities');
|
|
1729
2404
|
if (r && r.ok !== false) actions.setQccCapabilities(r);
|
|
1730
2405
|
else actions.setError((r && (r.message || r.error)) || '企查查能力检测失败');
|
|
1731
2406
|
} catch (err) {
|
|
@@ -1736,31 +2411,82 @@ window.__ModuleLoader__.load({
|
|
|
1736
2411
|
};
|
|
1737
2412
|
|
|
1738
2413
|
const estimateQcc = async () => {
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
2414
|
+
const uniqueCompanies = new Set(runtime.rows.map((row) => String(row?.[nameField] ?? '').trim()).filter(Boolean)).size;
|
|
2415
|
+
actions.setQccEstimate({
|
|
2416
|
+
uniqueCompanies,
|
|
2417
|
+
tools: ['主体查询', '工商信息'],
|
|
2418
|
+
estimatedCalls: uniqueCompanies * 2,
|
|
2419
|
+
maxCalls: 200,
|
|
2420
|
+
withinLimit: runtime.rows.length <= 100,
|
|
2421
|
+
estimateType: 'upper-bound',
|
|
2422
|
+
});
|
|
2423
|
+
};
|
|
2424
|
+
|
|
2425
|
+
const applyQccRun = async (run) => {
|
|
2426
|
+
actions.setQccRun(run);
|
|
2427
|
+
runtime.rows = Array.isArray(run.rows) ? run.rows : runtime.rows;
|
|
2428
|
+
runtime.resultRows.qcc = Array.isArray(run.rows) ? run.rows : null;
|
|
2429
|
+
lastCsv.qcc = { csv: run.csv ?? '', name: run.downloadName ?? 'qcc-enriched.csv' };
|
|
2430
|
+
if (run.reviewCsv) lastCsv.review = { csv: run.reviewCsv, name: run.reviewDownloadName ?? 'qcc-review.csv' };
|
|
2431
|
+
const summary = run.summary ?? {};
|
|
2432
|
+
const reviewRequired = Number(summary.ambiguous ?? run.reviewQueue?.length ?? 0);
|
|
2433
|
+
const currentTask = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? cachedTask;
|
|
2434
|
+
let current = currentTask;
|
|
2435
|
+
// partial 表示上一轮仍有失败项。重试成功后可直接重进 enrichment,
|
|
2436
|
+
// 无需伪造一次新的 match 转换;若重试产生候选,再回到人工核验。
|
|
2437
|
+
if (currentTask?.state !== 'partial' || reviewRequired > 0) {
|
|
2438
|
+
current = await workflowAction(actions, activeSessionId, currentTask, 'match', {
|
|
2439
|
+
qccRunId: run.runId,
|
|
2440
|
+
summary: {
|
|
2441
|
+
total: Number(summary.totalRows ?? runtime.rows.length),
|
|
2442
|
+
exact: Number(summary.enriched ?? 0),
|
|
2443
|
+
candidate: reviewRequired,
|
|
2444
|
+
confirmed: 0,
|
|
2445
|
+
unresolved: Number(summary.unresolved ?? summary.missingName ?? 0),
|
|
2446
|
+
failed: Number(summary.failed ?? 0),
|
|
2447
|
+
reviewRequired,
|
|
2448
|
+
},
|
|
1748
2449
|
});
|
|
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
2450
|
}
|
|
2451
|
+
if (reviewRequired > 0) {
|
|
2452
|
+
actions.setStep('match');
|
|
2453
|
+
return current;
|
|
2454
|
+
}
|
|
2455
|
+
current = await workflowAction(actions, activeSessionId, current, 'enrich-start', { fieldSelection });
|
|
2456
|
+
current = await workflowAction(actions, activeSessionId, current, 'enrichment', {
|
|
2457
|
+
qccRunId: run.runId,
|
|
2458
|
+
summary: {
|
|
2459
|
+
total: Number(summary.totalRows ?? runtime.rows.length),
|
|
2460
|
+
completed: Number(summary.enriched ?? 0),
|
|
2461
|
+
unchanged: Number(summary.unresolved ?? 0) + Number(summary.missingName ?? 0),
|
|
2462
|
+
failed: Number(summary.failed ?? 0),
|
|
2463
|
+
reviewRequired: 0,
|
|
2464
|
+
callsUsed: Array.isArray(run.audit) ? run.audit.length : 0,
|
|
2465
|
+
},
|
|
2466
|
+
});
|
|
2467
|
+
actions.setStep(current.stage === 'download' ? 'download' : 'enrich');
|
|
2468
|
+
return current;
|
|
1756
2469
|
};
|
|
1757
2470
|
|
|
1758
|
-
const
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
2471
|
+
const executePreparedQccCommand = async (payload) => {
|
|
2472
|
+
if (!activeSessionId || typeof sendSessionCommand !== 'function') {
|
|
2473
|
+
throw new Error('当前会话无法提交数据清洗补全企查查任务');
|
|
2474
|
+
}
|
|
2475
|
+
const prepared = await api('/data-cleaning/api/g5/commands', {
|
|
2476
|
+
...payload,
|
|
2477
|
+
confirmPaidCalls: true,
|
|
2478
|
+
});
|
|
2479
|
+
const command = prepared?.command;
|
|
2480
|
+
if (!command?.commandId || !command?.prompt) throw new Error('Host 未返回有效的企查查任务命令');
|
|
2481
|
+
await sendSessionCommand(activeSessionId, command.prompt);
|
|
2482
|
+
const completedCommand = await waitForQccCommand(command.commandId);
|
|
2483
|
+
if (completedCommand.state === 'failed') {
|
|
2484
|
+
const commandError = new Error(completedCommand.error?.message || '智能体企查查任务执行失败');
|
|
2485
|
+
commandError.code = completedCommand.error?.code;
|
|
2486
|
+
throw commandError;
|
|
2487
|
+
}
|
|
2488
|
+
if (!completedCommand.run) throw new Error('智能体企查查任务完成但结果已失效');
|
|
2489
|
+
return completedCommand.run;
|
|
1764
2490
|
};
|
|
1765
2491
|
|
|
1766
2492
|
const runQcc = async () => {
|
|
@@ -1768,20 +2494,21 @@ window.__ModuleLoader__.load({
|
|
|
1768
2494
|
actions.setBusy(true);
|
|
1769
2495
|
actions.setError(null);
|
|
1770
2496
|
try {
|
|
1771
|
-
const
|
|
1772
|
-
const r = await
|
|
1773
|
-
|
|
1774
|
-
|
|
2497
|
+
const current = await workflowAction(actions, activeSessionId, cachedTask, 'match-start');
|
|
2498
|
+
const r = await executePreparedQccCommand({
|
|
2499
|
+
kind: 'enrich',
|
|
2500
|
+
taskId: current.id,
|
|
2501
|
+
rows: runtime.rows,
|
|
2502
|
+
headers: runtime.headers,
|
|
1775
2503
|
nameField,
|
|
1776
|
-
|
|
1777
|
-
maxCalls: qccEstimate.maxCalls,
|
|
2504
|
+
includeRisk: false,
|
|
1778
2505
|
concurrency: 2,
|
|
1779
|
-
confirmPaidCalls: true,
|
|
1780
|
-
idempotencyKey: key,
|
|
1781
2506
|
});
|
|
1782
|
-
|
|
1783
|
-
else actions.setError((r && (r.message || r.error)) || '三域补全失败');
|
|
2507
|
+
await applyQccRun(r);
|
|
1784
2508
|
} catch (err) {
|
|
2509
|
+
if (['QCC_NOT_CONNECTED', 'QCC_TOOL_UNAVAILABLE', 'QCC_AUTH_REQUIRED'].includes(err?.code)) {
|
|
2510
|
+
try { await workflowAction(actions, activeSessionId, cachedTask, 'authorization-required'); } catch (_workflowError) {}
|
|
2511
|
+
}
|
|
1785
2512
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
1786
2513
|
} finally {
|
|
1787
2514
|
actions.setBusy(false);
|
|
@@ -1793,15 +2520,15 @@ window.__ModuleLoader__.load({
|
|
|
1793
2520
|
actions.setBusy(true);
|
|
1794
2521
|
actions.setError(null);
|
|
1795
2522
|
try {
|
|
1796
|
-
const
|
|
2523
|
+
const task = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? cachedTask;
|
|
2524
|
+
const r = await executePreparedQccCommand({
|
|
2525
|
+
kind: 'resolve',
|
|
2526
|
+
taskId: task.id,
|
|
1797
2527
|
runId: qccRun.runId,
|
|
1798
2528
|
companyName: item.companyName,
|
|
1799
2529
|
selectedCreditNo: candidate.creditNo,
|
|
1800
|
-
confirmPaidCalls: true,
|
|
1801
|
-
idempotencyKey: `phase3-resolve-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1802
2530
|
});
|
|
1803
|
-
|
|
1804
|
-
else actions.setError((r && (r.message || r.error)) || '候选确认失败');
|
|
2531
|
+
await applyQccRun(r);
|
|
1805
2532
|
} catch (err) {
|
|
1806
2533
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
1807
2534
|
} finally {
|
|
@@ -1816,14 +2543,14 @@ window.__ModuleLoader__.load({
|
|
|
1816
2543
|
actions.setBusy(true);
|
|
1817
2544
|
actions.setError(null);
|
|
1818
2545
|
try {
|
|
1819
|
-
const
|
|
2546
|
+
const task = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? cachedTask;
|
|
2547
|
+
const r = await executePreparedQccCommand({
|
|
2548
|
+
kind: 'retry',
|
|
2549
|
+
taskId: task.id,
|
|
1820
2550
|
runId: qccRun.runId,
|
|
1821
2551
|
companyNames: names,
|
|
1822
|
-
confirmPaidCalls: true,
|
|
1823
|
-
idempotencyKey: `phase3-retry-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1824
2552
|
});
|
|
1825
|
-
|
|
1826
|
-
else actions.setError((r && (r.message || r.error)) || '失败项重试失败');
|
|
2553
|
+
await applyQccRun(r);
|
|
1827
2554
|
} catch (err) {
|
|
1828
2555
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
1829
2556
|
} finally {
|
|
@@ -1831,46 +2558,206 @@ window.__ModuleLoader__.load({
|
|
|
1831
2558
|
}
|
|
1832
2559
|
};
|
|
1833
2560
|
|
|
1834
|
-
const
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2561
|
+
const saveTaskSettings = async () => {
|
|
2562
|
+
actions.setBusy(true);
|
|
2563
|
+
actions.setError(null);
|
|
2564
|
+
try {
|
|
2565
|
+
const current = await ensureWorkflowTask(actions, activeSessionId, {
|
|
2566
|
+
title: taskTitle, objectives, fieldSelection, mappings, matchRules,
|
|
2567
|
+
});
|
|
2568
|
+
await updateWorkflowTask(actions, activeSessionId, current, {
|
|
2569
|
+
title: taskTitle, objectives, fieldSelection, mappings, matchRules,
|
|
2570
|
+
});
|
|
2571
|
+
} catch (saveError) {
|
|
2572
|
+
actions.setError(saveError instanceof Error ? saveError.message : String(saveError));
|
|
2573
|
+
} finally {
|
|
2574
|
+
actions.setBusy(false);
|
|
2575
|
+
}
|
|
2576
|
+
};
|
|
2577
|
+
|
|
2578
|
+
const confirmRuleSettings = async () => {
|
|
2579
|
+
if (!hasData) { actions.setError('请先上传并解析数据。'); return; }
|
|
2580
|
+
const anchorMappings = mappings.filter((mapping) => ['company_name', 'credit_no', 'reg_no'].includes(mapping.targetField));
|
|
2581
|
+
if (!anchorMappings.length) { actions.setError('请至少映射企业名称、统一社会信用代码或注册号。'); return; }
|
|
2582
|
+
if (!objectives.length) { actions.setError('请至少选择一个清洗或补全目标。'); return; }
|
|
2583
|
+
actions.setBusy(true);
|
|
2584
|
+
actions.setError(null);
|
|
2585
|
+
try {
|
|
2586
|
+
let current = await ensureWorkflowTask(actions, activeSessionId, {
|
|
2587
|
+
title: taskTitle, objectives, fieldSelection, mappings, matchRules,
|
|
2588
|
+
});
|
|
2589
|
+
if (current.state === 'draft') {
|
|
2590
|
+
current = await workflowAction(actions, activeSessionId, current, 'upload', {
|
|
2591
|
+
source: {
|
|
2592
|
+
type: runtime.source?.type || dataset.fmt || 'csv',
|
|
2593
|
+
fileName: runtime.source?.fileName || '',
|
|
2594
|
+
rowCount: dataset.rowCount,
|
|
2595
|
+
columnCount: dataset.headers?.length || 0,
|
|
2596
|
+
headers: dataset.headers || [],
|
|
2597
|
+
sizeBytes: runtime.source?.sizeBytes || 0,
|
|
2598
|
+
},
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
current = await updateWorkflowTask(actions, activeSessionId, current, {
|
|
2602
|
+
title: taskTitle, objectives, fieldSelection, mappings, matchRules,
|
|
2603
|
+
});
|
|
2604
|
+
current = await workflowAction(actions, activeSessionId, current, 'rules', {
|
|
2605
|
+
objectives, fieldSelection, mappings, matchRules,
|
|
2606
|
+
});
|
|
2607
|
+
current = await performProfile(current);
|
|
2608
|
+
const primary = mappings.find((mapping) => mapping.targetField === 'company_name')
|
|
2609
|
+
?? mappings.find((mapping) => mapping.targetField === 'credit_no')
|
|
2610
|
+
?? mappings.find((mapping) => mapping.targetField === 'reg_no');
|
|
2611
|
+
if (primary) actions.setNameField(primary.sourceField);
|
|
2612
|
+
actions.setStep('profile');
|
|
2613
|
+
} catch (ruleError) {
|
|
2614
|
+
actions.setError(ruleError instanceof Error ? ruleError.message : String(ruleError));
|
|
2615
|
+
} finally {
|
|
2616
|
+
actions.setBusy(false);
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2619
|
+
|
|
2620
|
+
const resumeWorkflowTask = (task) => {
|
|
2621
|
+
workflowTaskBySession.set(String(activeSessionId || 'unassigned'), task);
|
|
2622
|
+
actions.setWorkflowTask(task);
|
|
2623
|
+
actions.setTaskTitle(task.title);
|
|
2624
|
+
actions.setMappings(task.mappings || []);
|
|
2625
|
+
actions.setObjectives(task.objectives || []);
|
|
2626
|
+
actions.setFieldSelection(task.fieldSelection || []);
|
|
2627
|
+
for (const [key, value] of Object.entries(task.matchRules || {})) actions.setMatchRule(key, value);
|
|
2628
|
+
actions.setStep(task.stage || 'upload');
|
|
2629
|
+
if (!runtimeFor(task.id, false)?.rows?.length && task.source?.rowCount && !(task.artifacts || []).length) {
|
|
2630
|
+
actions.setDataset(null);
|
|
2631
|
+
actions.setError('已恢复任务元数据。出于隐私保护,原始企业名单未持久化;继续处理前请重新上传源文件。');
|
|
2632
|
+
}
|
|
2633
|
+
};
|
|
2634
|
+
|
|
2635
|
+
const startNewWorkflow = () => {
|
|
2636
|
+
if (cachedTask?.id) runtimeTasks.delete(String(cachedTask.id));
|
|
2637
|
+
runtimeTasks.delete('unassigned');
|
|
2638
|
+
workflowTaskBySession.delete(String(activeSessionId || 'unassigned'));
|
|
2639
|
+
actions.resetWorkflow();
|
|
2640
|
+
};
|
|
2641
|
+
|
|
2642
|
+
const exportRows = () => runtime.resultRows.qcc
|
|
2643
|
+
?? runtime.resultRows.complete
|
|
2644
|
+
?? runtime.resultRows.clean
|
|
2645
|
+
?? (runtime.rows.length ? runtime.rows : null);
|
|
2646
|
+
|
|
2647
|
+
const createArtifactBundle = async () => {
|
|
2648
|
+
let latest = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? cachedTask;
|
|
2649
|
+
if (!latest) throw new Error('请先创建并执行数据清洗补全任务。');
|
|
2650
|
+
if (Array.isArray(latest.artifacts) && latest.artifacts.length) return latest;
|
|
2651
|
+
const rows = exportRows();
|
|
2652
|
+
if (!rows?.length) throw new Error('当前页面没有可导出的明细。若任务来自历史记录,请重新上传源文件并继续处理。');
|
|
2653
|
+
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(latest.id)}/artifacts`, 'POST', {
|
|
2654
|
+
expectedRevision: latest.revision,
|
|
2655
|
+
headers: runtime.headers,
|
|
2656
|
+
rows,
|
|
2657
|
+
baseName: latest.title || taskTitle,
|
|
2658
|
+
summary: {
|
|
2659
|
+
total: rows.length,
|
|
2660
|
+
completed: rows.length,
|
|
2661
|
+
unchanged: 0,
|
|
2662
|
+
failed: 0,
|
|
2663
|
+
},
|
|
2664
|
+
});
|
|
2665
|
+
latest = cacheWorkflowTask(actions, activeSessionId, response.task);
|
|
2666
|
+
loadWorkflowTasks(actions);
|
|
2667
|
+
return latest;
|
|
2668
|
+
};
|
|
2669
|
+
|
|
2670
|
+
const downloadArtifact = async (artifact) => {
|
|
2671
|
+
if (!artifact || busy) return;
|
|
2672
|
+
actions.setBusy(true);
|
|
2673
|
+
actions.setError(null);
|
|
2674
|
+
try {
|
|
2675
|
+
const latest = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? cachedTask;
|
|
2676
|
+
const response = await fetch(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(latest.id)}/artifacts/${encodeURIComponent(artifact.id)}`, {
|
|
2677
|
+
method: 'GET', credentials: 'same-origin', headers: { accept: artifact.mediaType || 'application/octet-stream' },
|
|
2678
|
+
});
|
|
2679
|
+
if (!response.ok) {
|
|
2680
|
+
let payload = null;
|
|
2681
|
+
try { payload = await response.json(); } catch {}
|
|
2682
|
+
throw new Error(payload?.message || `下载失败(HTTP ${response.status})`);
|
|
2683
|
+
}
|
|
2684
|
+
const blob = await response.blob();
|
|
2685
|
+
const href = URL.createObjectURL(blob);
|
|
2686
|
+
const anchor = document.createElement('a');
|
|
2687
|
+
anchor.href = href;
|
|
2688
|
+
anchor.download = artifact.fileName || `export.${artifact.format || 'bin'}`;
|
|
2689
|
+
anchor.click();
|
|
2690
|
+
URL.revokeObjectURL(href);
|
|
2691
|
+
} catch (downloadError) {
|
|
2692
|
+
actions.setError(downloadError instanceof Error ? downloadError.message : String(downloadError));
|
|
2693
|
+
} finally {
|
|
2694
|
+
actions.setBusy(false);
|
|
2695
|
+
}
|
|
2696
|
+
};
|
|
2697
|
+
|
|
2698
|
+
const createAndDownload = async (kind, format) => {
|
|
2699
|
+
if (busy) return;
|
|
2700
|
+
actions.setBusy(true);
|
|
2701
|
+
actions.setError(null);
|
|
2702
|
+
try {
|
|
2703
|
+
const latest = await createArtifactBundle();
|
|
2704
|
+
const artifact = (latest.artifacts || []).find((item) => item.kind === kind && item.format === format);
|
|
2705
|
+
if (!artifact) throw new Error('Host 未生成所选格式的制品。');
|
|
2706
|
+
actions.setBusy(false);
|
|
2707
|
+
await downloadArtifact(artifact);
|
|
2708
|
+
} catch (downloadError) {
|
|
2709
|
+
actions.setError(downloadError instanceof Error ? downloadError.message : String(downloadError));
|
|
2710
|
+
actions.setBusy(false);
|
|
2711
|
+
}
|
|
1843
2712
|
};
|
|
1844
2713
|
|
|
2714
|
+
const displayStatValue = (value) => {
|
|
2715
|
+
if (typeof value === 'string' || typeof value === 'number') return value;
|
|
2716
|
+
if (typeof value === 'bigint' || typeof value === 'boolean') return String(value);
|
|
2717
|
+
return '—';
|
|
2718
|
+
};
|
|
2719
|
+
// 浏览器运行时只保留当前会话的原始明细;进程重启后,已完成任务应从
|
|
2720
|
+
// Host 持久化摘要恢复计数,不能因 qccRun 仅存在于内存而显示“—”。
|
|
2721
|
+
const qccEnrichedCount = qccRun
|
|
2722
|
+
? qccRun.summary?.enriched ?? 0
|
|
2723
|
+
: cachedTask?.enrichmentSummary?.completed;
|
|
2724
|
+
const qccReviewCount = qccRun
|
|
2725
|
+
? qccRun.summary?.ambiguous ?? 0
|
|
2726
|
+
: cachedTask?.enrichmentSummary?.reviewRequired
|
|
2727
|
+
?? cachedTask?.matchSummary?.reviewRequired;
|
|
1845
2728
|
const stat = (label, value, tone) => h('div', { className: 'dcAgentCard' },
|
|
1846
2729
|
h('span', null, label),
|
|
1847
|
-
h('b', { className: tone ? `is-${tone}` : null }, value)
|
|
2730
|
+
h('b', { className: tone ? `is-${tone}` : null }, displayStatValue(value))
|
|
1848
2731
|
);
|
|
2732
|
+
const uiFieldGroups = Array.isArray(workflowContract?.fieldCatalog)
|
|
2733
|
+
? workflowContract.fieldCatalog.map((group) => [group.id, group.label, (group.fields || []).map((field) => [field.id, field.label])])
|
|
2734
|
+
: FIELD_GROUPS;
|
|
2735
|
+
const targetFields = uiFieldGroups.flatMap(([, , fields]) => fields);
|
|
1849
2736
|
|
|
1850
2737
|
let pane;
|
|
1851
2738
|
if (step === 'history') {
|
|
1852
2739
|
pane = h('div', { className: 'dcAgentPane' },
|
|
1853
|
-
h('p', { className: 'dcAgentHint' }, '查看当前 Host
|
|
2740
|
+
h('p', { className: 'dcAgentHint' }, '查看当前 Host 的数据清洗补全任务。Host 只保存任务元数据和摘要,不保存原始企业名单。'),
|
|
1854
2741
|
h('div', { className: 'dcAgentRow' },
|
|
1855
2742
|
h('button', {
|
|
1856
2743
|
type: 'button',
|
|
1857
2744
|
className: 'dcAgentButton',
|
|
1858
2745
|
disabled: busy,
|
|
1859
2746
|
'aria-label': '刷新任务历史',
|
|
1860
|
-
onClick: () =>
|
|
2747
|
+
onClick: () => loadWorkflowTasks(actions),
|
|
1861
2748
|
}, '刷新任务'),
|
|
1862
2749
|
h('button', {
|
|
1863
2750
|
type: 'button',
|
|
1864
2751
|
className: 'dcAgentButton is-primary',
|
|
1865
|
-
onClick:
|
|
2752
|
+
onClick: startNewWorkflow,
|
|
1866
2753
|
}, '新建清洗任务'),
|
|
1867
2754
|
),
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
h('h3', null,
|
|
2755
|
+
workflowTasks.length ? h('div', { className: 'dcAgentPane', 'aria-label': '任务历史列表' },
|
|
2756
|
+
workflowTasks.map((task) => h('button', { key: task.id, type: 'button', className: 'dcAgentHistoryTask', onClick: () => resumeWorkflowTask(task) },
|
|
2757
|
+
h('h3', null, task.title || task.id || '数据清洗任务'),
|
|
1871
2758
|
h('div', { className: 'dcAgentRow' },
|
|
1872
|
-
h('span', { className: 'dcAgentJobsPill', 'data-state':
|
|
1873
|
-
h('span', { className: 'dcAgentHint' },
|
|
2759
|
+
h('span', { className: 'dcAgentJobsPill', 'data-state': task.state ?? 'idle' }, WORKFLOW_STATE_LABELS[task.state] ?? task.state ?? '未知'),
|
|
2760
|
+
h('span', { className: 'dcAgentHint' }, `${task.source?.rowCount ?? 0} 行 · ${task.updatedAt ? new Date(task.updatedAt).toLocaleString() : ''}`),
|
|
1874
2761
|
),
|
|
1875
2762
|
)),
|
|
1876
2763
|
) : h('section', { className: 'dcAgentSection' },
|
|
@@ -1878,6 +2765,58 @@ window.__ModuleLoader__.load({
|
|
|
1878
2765
|
h('p', { className: 'dcAgentHint' }, '上传企业名单并开始处理后,任务状态会显示在这里。'),
|
|
1879
2766
|
),
|
|
1880
2767
|
);
|
|
2768
|
+
} else if (step === 'rules') {
|
|
2769
|
+
pane = h('div', { className: 'dcAgentPane' },
|
|
2770
|
+
h('p', { className: 'dcAgentHint' }, '确认任务名称、字段映射、匹配规则和输出字段。保存草稿不会调用企查查;确认规则后才可进入质量体检。'),
|
|
2771
|
+
h('label', { className: 'dcAgentFormField' },
|
|
2772
|
+
h('span', null, '任务名称'),
|
|
2773
|
+
h('input', { className: 'dcAgentField', value: taskTitle, maxLength: 120, onChange: (event) => actions.setTaskTitle(event.target.value) }),
|
|
2774
|
+
),
|
|
2775
|
+
h('section', { className: 'dcAgentSection' },
|
|
2776
|
+
h('h3', null, '字段映射'),
|
|
2777
|
+
h('p', { className: 'dcAgentHint' }, '至少映射企业名称、统一社会信用代码或注册号其中一项。未映射列会原样保留,但不参与匹配。'),
|
|
2778
|
+
(dataset?.headers || []).map((sourceField) => h('label', { className: 'dcAgentMappingRow', key: sourceField },
|
|
2779
|
+
h('span', { title: sourceField }, sourceField),
|
|
2780
|
+
h('b', { 'aria-hidden': 'true' }, '→'),
|
|
2781
|
+
h('select', {
|
|
2782
|
+
className: 'dcAgentField',
|
|
2783
|
+
value: mappings.find((mapping) => mapping.sourceField === sourceField)?.targetField || '',
|
|
2784
|
+
'aria-label': `${sourceField} 字段映射`,
|
|
2785
|
+
onChange: (event) => actions.setMapping(sourceField, event.target.value),
|
|
2786
|
+
},
|
|
2787
|
+
h('option', { value: '' }, '不参与匹配 / 补全'),
|
|
2788
|
+
targetFields.map(([id, label]) => h('option', { key: id, value: id }, label)),
|
|
2789
|
+
),
|
|
2790
|
+
)),
|
|
2791
|
+
),
|
|
2792
|
+
h('section', { className: 'dcAgentSection' },
|
|
2793
|
+
h('h3', null, '任务目标'),
|
|
2794
|
+
h('div', { className: 'dcAgentChips' }, CLEANING_OPTIONS.map(([key, label]) => h('label', { key, className: `dcAgentChip${objectives.includes(key) ? ' is-selected' : ''}` },
|
|
2795
|
+
h('input', { type: 'checkbox', checked: objectives.includes(key), onChange: () => actions.toggleObjective(key) }), ` ${label}`,
|
|
2796
|
+
))),
|
|
2797
|
+
),
|
|
2798
|
+
h('section', { className: 'dcAgentSection' },
|
|
2799
|
+
h('h3', null, '匹配规则'),
|
|
2800
|
+
h('div', { className: 'dcAgentRulesGrid' },
|
|
2801
|
+
[['normalizeNames', '匹配前规范企业名称'], ['preferCreditNo', '信用代码优先精确匹配'], ['deduplicate', '重复主体合并处理'], ['manualReviewAmbiguous', '多候选必须人工确认']].map(([key, label]) => h('label', { key, className: 'dcAgentCheck' },
|
|
2802
|
+
h('input', { type: 'checkbox', checked: matchRules[key], onChange: (event) => actions.setMatchRule(key, event.target.checked) }), label,
|
|
2803
|
+
)),
|
|
2804
|
+
),
|
|
2805
|
+
),
|
|
2806
|
+
h('section', { className: 'dcAgentSection' },
|
|
2807
|
+
h('h3', null, `输出字段 · 已选 ${fieldSelection.length}`),
|
|
2808
|
+
uiFieldGroups.map(([groupId, label, fields]) => h('div', { key: groupId, className: 'dcAgentFieldGroup' },
|
|
2809
|
+
h('b', null, label),
|
|
2810
|
+
h('div', { className: 'dcAgentChips' }, fields.map(([key, fieldLabel]) => h('label', { key, className: `dcAgentChip${fieldSelection.includes(key) ? ' is-selected' : ''}` },
|
|
2811
|
+
h('input', { type: 'checkbox', checked: fieldSelection.includes(key), onChange: () => actions.toggleField(key) }), ` ${fieldLabel}`,
|
|
2812
|
+
))),
|
|
2813
|
+
)),
|
|
2814
|
+
),
|
|
2815
|
+
h('div', { className: 'dcAgentRow' },
|
|
2816
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy, onClick: saveTaskSettings }, '保存草稿'),
|
|
2817
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !hasData, 'aria-label': '确认规则并运行质量体检', onClick: confirmRuleSettings }, busy ? '保存中…' : '确认规则并运行质量体检'),
|
|
2818
|
+
),
|
|
2819
|
+
);
|
|
1881
2820
|
} else if (step === 'profile') {
|
|
1882
2821
|
pane = h('div', { className: 'dcAgentPane' },
|
|
1883
2822
|
h('p', { className: 'dcAgentHint' }, '基于已解析数据生成本地质量画像(缺失率、去重值、金额分布)。本步骤不发起企查查调用。'),
|
|
@@ -1905,45 +2844,33 @@ window.__ModuleLoader__.load({
|
|
|
1905
2844
|
))),
|
|
1906
2845
|
) : null,
|
|
1907
2846
|
h('div', { className: 'dcAgentRow' },
|
|
1908
|
-
h('button', {
|
|
2847
|
+
h('button', {
|
|
2848
|
+
type: 'button',
|
|
2849
|
+
className: 'dcAgentButton is-primary',
|
|
2850
|
+
disabled: busy,
|
|
2851
|
+
onClick: () => actions.setStep(requiresQcc ? 'match' : 'enrich'),
|
|
2852
|
+
}, requiresQcc ? '下一步:匹配核验' : '下一步:本地清洗补全'),
|
|
1909
2853
|
),
|
|
1910
2854
|
) : h('div', { className: 'dcAgentRow' },
|
|
1911
2855
|
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !hasData, 'aria-label': '生成体检报告', onClick: runProfile }, busy ? '体检中…' : '生成体检报告'),
|
|
1912
2856
|
),
|
|
1913
2857
|
);
|
|
1914
|
-
} else if (step === '
|
|
2858
|
+
} else if (step === 'match') {
|
|
1915
2859
|
pane = h('div', { className: 'dcAgentPane' },
|
|
1916
|
-
h('p', { className: 'dcAgentHint' }, '
|
|
2860
|
+
h('p', { className: 'dcAgentHint' }, '通过当前用户连接的企查查 MCP 定位企业主体并补全基础工商字段。多候选始终由人工选择;界面不生成虚构置信度。'),
|
|
1917
2861
|
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,
|
|
2862
|
+
h('h3', null, '匹配依据'),
|
|
2863
|
+
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}`))),
|
|
2864
|
+
h('p', { className: 'dcAgentHint' }, '统一社会信用代码等强标识优先;名称匹配不明确时进入人工核验队列。'),
|
|
1929
2865
|
),
|
|
1930
2866
|
h('section', { className: 'dcAgentSection' },
|
|
1931
|
-
h('h3', null, '
|
|
2867
|
+
h('h3', null, '企查查基础企业能力'),
|
|
1932
2868
|
h('div', { className: 'dcAgentRow' },
|
|
1933
2869
|
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
|
-
)),
|
|
2870
|
+
qccCapabilities ? h('span', { className: 'dcAgentHint' }, qccCapabilities.capabilities?.ready ? '主体查询与工商信息工具已就绪' : '连接未就绪或基础工具不完整') : null,
|
|
1944
2871
|
),
|
|
1945
2872
|
h('div', { className: 'dcAgentRow' },
|
|
1946
|
-
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !
|
|
2873
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !runtime.rows.length, onClick: estimateQcc }, '估算调用量'),
|
|
1947
2874
|
),
|
|
1948
2875
|
qccEstimate ? h('div', null,
|
|
1949
2876
|
h('div', { className: 'dcAgentGrid' },
|
|
@@ -1954,10 +2881,10 @@ window.__ModuleLoader__.load({
|
|
|
1954
2881
|
),
|
|
1955
2882
|
h('label', { className: 'dcAgentCheck' },
|
|
1956
2883
|
h('input', { type: 'checkbox', checked: paidConfirmed, 'aria-label': '确认使用当前用户的企查查账号额度', onChange: (event) => actions.setPaidConfirmed(event.target.checked) }),
|
|
1957
|
-
'
|
|
2884
|
+
'我已核对企业数量和调用上界,并确认使用自己连接的企查查 MCP 账号;额度或费用由该账号自行承担',
|
|
1958
2885
|
),
|
|
1959
2886
|
h('div', { className: 'dcAgentRow' },
|
|
1960
|
-
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !paidConfirmed || !qccEstimate.withinLimit, onClick: runQcc }, busy ? '执行中…' : '
|
|
2887
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !paidConfirmed || !qccEstimate.withinLimit, onClick: runQcc }, busy ? '执行中…' : '开始主体匹配'),
|
|
1961
2888
|
),
|
|
1962
2889
|
) : null,
|
|
1963
2890
|
),
|
|
@@ -1965,11 +2892,21 @@ window.__ModuleLoader__.load({
|
|
|
1965
2892
|
h('h3', null, `任务 ${qccRun.runId} · ${qccRun.state}`),
|
|
1966
2893
|
h('div', { className: 'dcAgentGrid' },
|
|
1967
2894
|
stat('已补全', qccRun.summary?.enriched ?? 0, 'good'),
|
|
1968
|
-
stat('部分成功', qccRun.summary?.partial ?? 0, (qccRun.summary?.partial ?? 0) > 0 ? 'warn' : null),
|
|
1969
2895
|
stat('待核验', qccRun.summary?.ambiguous ?? 0, (qccRun.summary?.ambiguous ?? 0) > 0 ? 'warn' : null),
|
|
2896
|
+
stat('未匹配', qccRun.summary?.unresolved ?? 0, (qccRun.summary?.unresolved ?? 0) > 0 ? 'warn' : null),
|
|
1970
2897
|
stat('失败', qccRun.summary?.failed ?? 0, (qccRun.summary?.failed ?? 0) > 0 ? 'bad' : null),
|
|
1971
|
-
stat('实际调用', qccRun.summary?.actualCalls ?? 0),
|
|
1972
2898
|
),
|
|
2899
|
+
Array.isArray(qccRun.rows) && qccRun.rows.length ? h('div', { className: 'dcAgentTableWrap' },
|
|
2900
|
+
h('table', { className: 'dcAgentTable', 'aria-label': '匹配补全结果预览' },
|
|
2901
|
+
h('thead', null, h('tr', null,
|
|
2902
|
+
Object.keys(qccRun.rows[0] || {}).slice(0, 6).map((key) => h('th', { key }, key)),
|
|
2903
|
+
)),
|
|
2904
|
+
h('tbody', null, qccRun.rows.slice(0, 8).map((row, index) => h('tr', { key: `${index}-${row?.qcc_credit_no || row?.credit_no || ''}` },
|
|
2905
|
+
Object.keys(qccRun.rows[0] || {}).slice(0, 6).map((key) => h('td', { key }, String(row?.[key] ?? ''))),
|
|
2906
|
+
))),
|
|
2907
|
+
),
|
|
2908
|
+
h('p', { className: 'dcAgentHint' }, `显示前 ${Math.min(8, qccRun.rows.length)} 行;完整结果在下载阶段生成耐久制品。`),
|
|
2909
|
+
) : null,
|
|
1973
2910
|
(qccRun.reviewQueue || []).map((item) => h('div', { key: item.companyName, className: 'dcAgentSection' },
|
|
1974
2911
|
h('h3', null, `待核验:${item.companyName}`),
|
|
1975
2912
|
item.candidates.map((candidate) => h('div', { key: candidate.creditNo, className: 'dcAgentCandidate' },
|
|
@@ -1979,28 +2916,64 @@ window.__ModuleLoader__.load({
|
|
|
1979
2916
|
)),
|
|
1980
2917
|
)),
|
|
1981
2918
|
(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
|
-
),
|
|
2919
|
+
) : null,
|
|
1988
2920
|
);
|
|
1989
2921
|
} else if (step === 'enrich') {
|
|
1990
2922
|
pane = h('div', { className: 'dcAgentPane' },
|
|
1991
|
-
h('p', { className: 'dcAgentHint' }, '
|
|
2923
|
+
h('p', { className: 'dcAgentHint' }, '执行本地确定性清洗并核对本任务所选补全字段。外部字段以当前用户企查查 MCP 的真实返回为准,缺失值不会被编造。'),
|
|
2924
|
+
h('section', { className: 'dcAgentSection' },
|
|
2925
|
+
h('h3', null, `已选补全字段 · ${fieldSelection.length}`),
|
|
2926
|
+
h('div', { className: 'dcAgentChips' }, fieldSelection.map((id) => h('span', { className: 'dcAgentChip', key: id }, ENRICHMENT_OPTIONS.find(([fieldId]) => fieldId === id)?.[1] || id))),
|
|
2927
|
+
),
|
|
2928
|
+
h('section', { className: 'dcAgentSection' },
|
|
2929
|
+
h('h3', null, '本地清洗预处理'),
|
|
2930
|
+
h('div', { className: 'dcAgentRow' },
|
|
2931
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !hasData, 'aria-label': '执行清洗', onClick: runClean }, busy ? '处理中…' : '执行确定性清洗'),
|
|
2932
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !hasData, 'aria-label': '执行补全', onClick: runComplete }, busy ? '处理中…' : '本地规则补全'),
|
|
2933
|
+
),
|
|
2934
|
+
clean ? h('div', { className: 'dcAgentGrid' },
|
|
2935
|
+
stat('总数', clean.total), stat('保留', clean.kept, 'good'), stat('剔除', clean.dropped, 'bad'),
|
|
2936
|
+
stat('缺失关键字段', clean.badMissing, clean.badMissing > 0 ? 'bad' : null),
|
|
2937
|
+
stat('重复', clean.badDuplicate, clean.badDuplicate > 0 ? 'warn' : null),
|
|
2938
|
+
) : null,
|
|
2939
|
+
),
|
|
1992
2940
|
h('div', { className: 'dcAgentGrid' },
|
|
1993
2941
|
stat('输入行数', dataset ? dataset.rowCount : '—'),
|
|
1994
2942
|
stat('清洗保留', clean ? clean.kept : '—', clean && clean.kept > 0 ? 'good' : null),
|
|
1995
2943
|
stat('本地补全', complete ? complete.completed : '—', complete && complete.completed > 0 ? 'good' : null),
|
|
1996
|
-
stat('QCC 已补全',
|
|
1997
|
-
stat('待核验',
|
|
2944
|
+
stat('QCC 已补全', qccEnrichedCount, qccEnrichedCount > 0 ? 'good' : null),
|
|
2945
|
+
stat('待核验', qccReviewCount, qccReviewCount > 0 ? 'warn' : null),
|
|
1998
2946
|
),
|
|
1999
|
-
h('div', { className: 'dcAgentRow' },
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2947
|
+
h('div', { className: 'dcAgentRow' }, h('button', { type: 'button', className: 'dcAgentButton is-primary', onClick: () => actions.setStep('download') }, '进入下载数据')),
|
|
2948
|
+
);
|
|
2949
|
+
} else if (step === 'download') {
|
|
2950
|
+
const availableArtifacts = cachedTask?.artifacts || [];
|
|
2951
|
+
const canCreateArtifacts = Boolean(exportRows()?.length) && ['rules_confirmed', 'diagnosed', 'export_ready', 'partial'].includes(cachedTask?.state);
|
|
2952
|
+
pane = h('div', { className: 'dcAgentPane' },
|
|
2953
|
+
h('p', { className: 'dcAgentHint' }, '结果与异常清单由 Host 生成 CSV/XLSX 四件套并耐久保存。任务或插件重启后,可从任务历史继续下载;无需再次调用企查查。'),
|
|
2954
|
+
h('div', { className: 'dcAgentGrid' },
|
|
2955
|
+
stat('输入行数', dataset ? dataset.rowCount : workflowTask?.source?.rowCount ?? '—'),
|
|
2956
|
+
stat('匹配补全', qccEnrichedCount, qccEnrichedCount > 0 ? 'good' : null),
|
|
2957
|
+
stat('待核验', qccReviewCount, qccReviewCount > 0 ? 'warn' : null),
|
|
2958
|
+
stat('任务状态', WORKFLOW_STATE_LABELS[cachedTask?.state] || cachedTask?.state || '进行中'),
|
|
2959
|
+
),
|
|
2960
|
+
availableArtifacts.length ? h('div', { className: 'dcAgentArtifactList' },
|
|
2961
|
+
availableArtifacts.map((artifact) => h('button', {
|
|
2962
|
+
key: artifact.id,
|
|
2963
|
+
type: 'button',
|
|
2964
|
+
className: `dcAgentButton${artifact.kind === 'complete' && artifact.format === 'xlsx' ? ' is-primary' : ''}`,
|
|
2965
|
+
disabled: busy,
|
|
2966
|
+
onClick: () => downloadArtifact(artifact),
|
|
2967
|
+
'aria-label': `下载 ${artifact.fileName}`,
|
|
2968
|
+
}, `${artifact.kind === 'review' ? '异常清单' : '清洗补全结果'} ${String(artifact.format).toUpperCase()} · ${artifact.rowCount} 行`)),
|
|
2969
|
+
) : h('div', null,
|
|
2970
|
+
h('div', { className: 'dcAgentRow' },
|
|
2971
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !canCreateArtifacts, onClick: () => createAndDownload('complete', 'xlsx') }, '生成并下载结果 XLSX'),
|
|
2972
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !canCreateArtifacts, onClick: () => createAndDownload('complete', 'csv') }, '生成并下载结果 CSV'),
|
|
2973
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !canCreateArtifacts, onClick: () => createAndDownload('review', 'xlsx') }, '生成并下载异常清单 XLSX'),
|
|
2974
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !canCreateArtifacts, onClick: () => createAndDownload('review', 'csv') }, '生成并下载异常清单 CSV'),
|
|
2975
|
+
),
|
|
2976
|
+
!canCreateArtifacts ? h('p', { className: 'dcAgentHint' }, '请先完成规则确认与清洗补全;历史任务若尚未生成制品,需要重新上传源文件。') : null,
|
|
2004
2977
|
),
|
|
2005
2978
|
);
|
|
2006
2979
|
} else {
|
|
@@ -2026,15 +2999,15 @@ window.__ModuleLoader__.load({
|
|
|
2026
2999
|
(dataset.headers || []).slice(0, 12).map((name) => h('span', { key: name, className: 'dcAgentChip' }, name)),
|
|
2027
3000
|
(dataset.headers || []).length > 12 ? h('span', { className: 'dcAgentChip' }, `+${dataset.headers.length - 12} 列`) : null,
|
|
2028
3001
|
) : null,
|
|
2029
|
-
dataset ? h('
|
|
2030
|
-
h('
|
|
2031
|
-
|
|
2032
|
-
(dataset.
|
|
3002
|
+
dataset ? h('div', { className: 'dcAgentPreviewTable', 'aria-label': '数据预览' },
|
|
3003
|
+
h('table', { className: 'dcAgentTable' },
|
|
3004
|
+
h('thead', null, h('tr', null, (dataset.headers || []).slice(0, 8).map((name) => h('th', { key: name }, name)))),
|
|
3005
|
+
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
3006
|
),
|
|
2034
3007
|
) : null,
|
|
2035
3008
|
h('div', { className: 'dcAgentRow' },
|
|
2036
3009
|
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('
|
|
3010
|
+
hasData ? h('button', { type: 'button', className: 'dcAgentButton', onClick: () => actions.setStep('rules') }, '下一步:字段映射与规则') : null,
|
|
2038
3011
|
),
|
|
2039
3012
|
);
|
|
2040
3013
|
}
|
|
@@ -2068,7 +3041,7 @@ window.__ModuleLoader__.load({
|
|
|
2068
3041
|
h('span', { className: 'dcAgentWbIcon', 'aria-hidden': 'true' }, '🧹'),
|
|
2069
3042
|
h('div', null,
|
|
2070
3043
|
h('b', null, '数据清洗补全'),
|
|
2071
|
-
h('small', null,
|
|
3044
|
+
h('small', null, cachedTask ? `${WORKFLOW_STATE_LABELS[cachedTask.state] || cachedTask.state} · ${cachedTask.id.slice(0, 18)}` : '正在创建 Host taskId…'),
|
|
2072
3045
|
),
|
|
2073
3046
|
),
|
|
2074
3047
|
h('span', {
|
|
@@ -2128,6 +3101,7 @@ window.__ModuleLoader__.load({
|
|
|
2128
3101
|
try {
|
|
2129
3102
|
const workbenchStore = createWorkbenchStore();
|
|
2130
3103
|
ctx.effect(() => installUiStyles(), 'data-cleaning-agent: UI styles');
|
|
3104
|
+
ctx.effect(() => installSessionOwnershipBridge(ctx), 'data-cleaning-agent: session ownership');
|
|
2131
3105
|
|
|
2132
3106
|
// 右侧工作台:additive overlay,不替换 DSH 单占位 details 面板。
|
|
2133
3107
|
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
@@ -2135,6 +3109,9 @@ window.__ModuleLoader__.load({
|
|
|
2135
3109
|
id: 'data-cleaning-agent',
|
|
2136
3110
|
order: 200,
|
|
2137
3111
|
store: workbenchStore,
|
|
3112
|
+
inject: () => ({
|
|
3113
|
+
sendSessionCommand: (sessionId, prompt) => sendQccAgentCommand(ctx, sessionId, prompt),
|
|
3114
|
+
}),
|
|
2138
3115
|
}, WorkbenchDrawer));
|
|
2139
3116
|
|
|
2140
3117
|
// 左栏:footer 只托管生命周期和 Portal 降级;实际入口显示在工作区列表前。
|
|
@@ -2190,7 +3167,22 @@ window.__ModuleLoader__.load({
|
|
|
2190
3167
|
exports.apply = apply;
|
|
2191
3168
|
exports.inject = inject;
|
|
2192
3169
|
// 测试用纯函数,不构成 Host / DSH 稳定 API。
|
|
2193
|
-
exports.__testing = {
|
|
3170
|
+
exports.__testing = {
|
|
3171
|
+
buildTaskPrompt,
|
|
3172
|
+
clearCleaningDraft,
|
|
3173
|
+
entriesToDataset,
|
|
3174
|
+
extractPromptEntries,
|
|
3175
|
+
guessMappings,
|
|
3176
|
+
deactivateCleaningSession,
|
|
3177
|
+
installSessionOwnershipBridge,
|
|
3178
|
+
isCleaningSession,
|
|
3179
|
+
isKnownCleaningDraft,
|
|
3180
|
+
markCleaningSession,
|
|
3181
|
+
qualitySummaryFor,
|
|
3182
|
+
rewriteHeroChrome,
|
|
3183
|
+
ensureWorkflowTask,
|
|
3184
|
+
queueWorkflowOperation,
|
|
3185
|
+
};
|
|
2194
3186
|
return module.exports;
|
|
2195
3187
|
},
|
|
2196
3188
|
});
|