dsh-data-cleaning-agent 0.6.0 → 0.6.2
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 +29 -0
- package/README.en.md +12 -6
- package/README.md +11 -7
- package/docs/COMPATIBILITY.md +7 -6
- 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.6.0.md +14 -3
- package/docs/RELEASE-0.6.1.md +64 -0
- package/docs/RELEASE-0.6.2.md +40 -0
- package/docs/UI-WORKFLOW-V2.md +2 -3
- package/lib/artifacts.js +47 -8
- package/lib/client.js +283 -66
- package/lib/engine.js +1 -1
- package/lib/index.js +9 -4
- package/lib/jobs.js +1 -1
- package/lib/qcc-command.js +280 -0
- package/lib/qcc-runs.js +5 -1
- package/lib/qcc.js +142 -15
- package/lib/skill-enrich.js +7 -2
- package/lib/tools.js +1 -1
- package/lib/web.js +55 -1
- package/lib/workflow-contract.js +9 -0
- package/package.json +4 -2
package/lib/client.js
CHANGED
|
@@ -822,34 +822,66 @@ window.__ModuleLoader__.load({
|
|
|
822
822
|
const DEFAULT_CLEANING_KEYS = ['clean_name', 'deduplicate', 'validate_identity', 'complete_fields'];
|
|
823
823
|
const DEFAULT_ENRICHMENT_KEYS = ['credit_no', 'legal_rep', 'reg_capital', 'establish_date', 'reg_status'];
|
|
824
824
|
const DEFAULT_SESSION_PROMPT = '请帮我清洗并补全企业名单。可点击输入框左上角「提示词生成」录入名单、上传 Excel 或图片,也可直接修改本段任务说明后开始。';
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
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;
|
|
828
831
|
|
|
829
832
|
try {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
}
|
|
833
|
+
// 旧版本持久化过 sessionId;但 DSH 会复用空白新会话,无法仅凭 ID 判断入口来源。
|
|
834
|
+
// 子系统归属因此只在当前页面、经菜单显式激活,并在升级时清除所有旧标记。
|
|
835
|
+
for (const key of CLEANING_SESSION_STORAGE_KEYS) window.sessionStorage?.removeItem(key);
|
|
834
836
|
} catch (_error) {
|
|
835
|
-
// sessionStorage
|
|
837
|
+
// sessionStorage 可能被禁用;内存态不受影响。
|
|
836
838
|
}
|
|
837
839
|
|
|
838
840
|
function isCleaningSession(sessionId) {
|
|
839
|
-
return typeof sessionId === 'string' &&
|
|
841
|
+
return typeof sessionId === 'string' && sessionId === activeCleaningSessionId;
|
|
840
842
|
}
|
|
841
843
|
|
|
842
844
|
function markCleaningSession(sessionId) {
|
|
843
845
|
if (typeof sessionId !== 'string' || !sessionId) return;
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
window.
|
|
847
|
-
}
|
|
848
|
-
|
|
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);
|
|
849
878
|
}
|
|
850
879
|
if (typeof window.CustomEvent === 'function' && typeof window.dispatchEvent === 'function') {
|
|
851
|
-
window.dispatchEvent(new window.CustomEvent(CLEANING_SESSION_EVENT, {
|
|
880
|
+
window.dispatchEvent(new window.CustomEvent(CLEANING_SESSION_EVENT, {
|
|
881
|
+
detail: { sessionId: previousSessionId, active: false },
|
|
882
|
+
}));
|
|
852
883
|
}
|
|
884
|
+
return true;
|
|
853
885
|
}
|
|
854
886
|
|
|
855
887
|
function useCleaningSession(sessionId) {
|
|
@@ -858,14 +890,89 @@ window.__ModuleLoader__.load({
|
|
|
858
890
|
);
|
|
859
891
|
react.useEffect(() => {
|
|
860
892
|
const handleMarked = (event) => {
|
|
861
|
-
|
|
893
|
+
const detail = event?.detail ?? {};
|
|
894
|
+
setMarkedSessionId(detail.active === true && detail.sessionId === sessionId ? sessionId : null);
|
|
862
895
|
};
|
|
863
896
|
window.addEventListener?.(CLEANING_SESSION_EVENT, handleMarked);
|
|
864
897
|
return () => window.removeEventListener?.(CLEANING_SESSION_EVENT, handleMarked);
|
|
865
898
|
}, [sessionId]);
|
|
866
899
|
// DSH 可能复用同一个 slot component 切换会话;状态必须绑定具体 sessionId,
|
|
867
900
|
// 避免从清洗会话切回普通会话后仍残留业务首页与提示词入口。
|
|
868
|
-
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
|
+
};
|
|
869
976
|
}
|
|
870
977
|
|
|
871
978
|
function optionLabels(options, keys) {
|
|
@@ -1001,6 +1108,18 @@ window.__ModuleLoader__.load({
|
|
|
1001
1108
|
return requestJson(path, body === undefined ? 'GET' : 'POST', body);
|
|
1002
1109
|
}
|
|
1003
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
|
+
|
|
1004
1123
|
function cacheWorkflowTask(actions, sessionId, task) {
|
|
1005
1124
|
if (!task) return null;
|
|
1006
1125
|
if (sessionId) workflowTaskBySession.set(String(sessionId), task);
|
|
@@ -1251,6 +1370,15 @@ window.__ModuleLoader__.load({
|
|
|
1251
1370
|
return images.length;
|
|
1252
1371
|
}
|
|
1253
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
|
+
|
|
1254
1382
|
function capabilityIcon(item, size = 16) {
|
|
1255
1383
|
return typeof item.icon === 'function'
|
|
1256
1384
|
? h(item.icon, { size, 'aria-hidden': 'true' })
|
|
@@ -1453,7 +1581,12 @@ window.__ModuleLoader__.load({
|
|
|
1453
1581
|
|
|
1454
1582
|
/**
|
|
1455
1583
|
* DSH 当前只公开 hero brand.mark,没有 headline slot。这里仅在本插件会话且 blank hero
|
|
1456
|
-
*
|
|
1584
|
+
* 阶段替换标题,并在卸载时恢复,避免污染普通会话。
|
|
1585
|
+
*
|
|
1586
|
+
* 某些第三方插件会全局改写 hero headline、向所有会话注入自己的 dock。清洗会话必须
|
|
1587
|
+
* 仍然保持自己的标题和单一业务入口,因此用 headline class 作为最后降级,并只隐藏与
|
|
1588
|
+
* 当前清洗 hero 同一 composer stack 内、带明确「尽调类型」aria 标签的已知外来 dock。
|
|
1589
|
+
* 所有变更均记录原值并在会话离开 / 组件卸载时恢复。
|
|
1457
1590
|
*/
|
|
1458
1591
|
function rewriteHeroChrome(sessionId, enabled) {
|
|
1459
1592
|
if (!enabled || typeof document === 'undefined' || typeof document.querySelectorAll !== 'function') return () => {};
|
|
@@ -1461,32 +1594,63 @@ window.__ModuleLoader__.load({
|
|
|
1461
1594
|
.find((element) => element?.dataset?.sessionId === sessionId);
|
|
1462
1595
|
const hero = marker?.closest?.('[data-phase="hero"]');
|
|
1463
1596
|
if (!hero) return () => {};
|
|
1464
|
-
const
|
|
1465
|
-
const
|
|
1466
|
-
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
title.
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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 });
|
|
1479
1641
|
return () => {
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1642
|
+
observer?.disconnect?.();
|
|
1643
|
+
for (const [title, originalText] of changedTitles) {
|
|
1644
|
+
if (title.textContent === '数据清洗补全智能体') title.textContent = originalText;
|
|
1483
1645
|
delete title.dataset.dcAgentHeroTitle;
|
|
1484
1646
|
}
|
|
1485
|
-
|
|
1486
|
-
if (badge.style) badge.style.display =
|
|
1487
|
-
delete badge.dataset.dcAgentOriginalDisplay;
|
|
1647
|
+
for (const [badge, originalDisplay] of changedBadges) {
|
|
1648
|
+
if (badge.style) badge.style.display = originalDisplay;
|
|
1488
1649
|
delete badge.dataset.dcAgentHeroBadge;
|
|
1489
1650
|
}
|
|
1651
|
+
for (const [foreignDock, originalDisplay] of hiddenForeignDocks) {
|
|
1652
|
+
if (foreignDock.style) foreignDock.style.display = originalDisplay;
|
|
1653
|
+
}
|
|
1490
1654
|
};
|
|
1491
1655
|
}
|
|
1492
1656
|
|
|
@@ -1942,6 +2106,7 @@ window.__ModuleLoader__.load({
|
|
|
1942
2106
|
/** 右侧非模态工作台:中央区域始终保留 DSH 原生会话。 */
|
|
1943
2107
|
function WorkbenchDrawer(props) {
|
|
1944
2108
|
const { useStore, actions } = props;
|
|
2109
|
+
const sendSessionCommand = props.sendSessionCommand;
|
|
1945
2110
|
const open = useStore((state) => state.open);
|
|
1946
2111
|
const step = useStore((state) => state.step);
|
|
1947
2112
|
const expanded = useStore((state) => state.expanded);
|
|
@@ -2088,6 +2253,7 @@ window.__ModuleLoader__.load({
|
|
|
2088
2253
|
if (!open) return null;
|
|
2089
2254
|
|
|
2090
2255
|
const hasData = dataset !== null && dataset.rowCount > 0;
|
|
2256
|
+
const requiresQcc = objectives.includes('validate_identity') || objectives.includes('complete_fields');
|
|
2091
2257
|
const cachedTask = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? workflowTask;
|
|
2092
2258
|
const runtimeKey = cachedTask?.id ?? `session:${activeSessionId || 'unassigned'}`;
|
|
2093
2259
|
const runtime = runtimeFor(runtimeKey);
|
|
@@ -2165,7 +2331,7 @@ window.__ModuleLoader__.load({
|
|
|
2165
2331
|
actions.setError(null);
|
|
2166
2332
|
try {
|
|
2167
2333
|
await performProfile(cachedTask);
|
|
2168
|
-
actions.setStep('match');
|
|
2334
|
+
actions.setStep(requiresQcc ? 'match' : 'enrich');
|
|
2169
2335
|
} catch (err) {
|
|
2170
2336
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
2171
2337
|
} finally {
|
|
@@ -2246,11 +2412,13 @@ window.__ModuleLoader__.load({
|
|
|
2246
2412
|
|
|
2247
2413
|
const estimateQcc = async () => {
|
|
2248
2414
|
const uniqueCompanies = new Set(runtime.rows.map((row) => String(row?.[nameField] ?? '').trim()).filter(Boolean)).size;
|
|
2415
|
+
const requiresProfile = fieldSelection.some((field) => ['company_profile', 'industry_large', 'industry_middle'].includes(field));
|
|
2416
|
+
const callsPerCompany = requiresProfile ? 3 : 2;
|
|
2249
2417
|
actions.setQccEstimate({
|
|
2250
2418
|
uniqueCompanies,
|
|
2251
|
-
tools: ['主体查询', '工商信息'],
|
|
2252
|
-
estimatedCalls: uniqueCompanies *
|
|
2253
|
-
maxCalls:
|
|
2419
|
+
tools: requiresProfile ? ['主体查询', '工商信息', '企业画像'] : ['主体查询', '工商信息'],
|
|
2420
|
+
estimatedCalls: uniqueCompanies * callsPerCompany,
|
|
2421
|
+
maxCalls: 300,
|
|
2254
2422
|
withinLimit: runtime.rows.length <= 100,
|
|
2255
2423
|
estimateType: 'upper-bound',
|
|
2256
2424
|
});
|
|
@@ -2302,24 +2470,44 @@ window.__ModuleLoader__.load({
|
|
|
2302
2470
|
return current;
|
|
2303
2471
|
};
|
|
2304
2472
|
|
|
2473
|
+
const executePreparedQccCommand = async (payload) => {
|
|
2474
|
+
if (!activeSessionId || typeof sendSessionCommand !== 'function') {
|
|
2475
|
+
throw new Error('当前会话无法提交数据清洗补全企查查任务');
|
|
2476
|
+
}
|
|
2477
|
+
const prepared = await api('/data-cleaning/api/g5/commands', {
|
|
2478
|
+
...payload,
|
|
2479
|
+
confirmPaidCalls: true,
|
|
2480
|
+
});
|
|
2481
|
+
const command = prepared?.command;
|
|
2482
|
+
if (!command?.commandId || !command?.prompt) throw new Error('Host 未返回有效的企查查任务命令');
|
|
2483
|
+
await sendSessionCommand(activeSessionId, command.prompt);
|
|
2484
|
+
const completedCommand = await waitForQccCommand(command.commandId);
|
|
2485
|
+
if (completedCommand.state === 'failed') {
|
|
2486
|
+
const commandError = new Error(completedCommand.error?.message || '智能体企查查任务执行失败');
|
|
2487
|
+
commandError.code = completedCommand.error?.code;
|
|
2488
|
+
throw commandError;
|
|
2489
|
+
}
|
|
2490
|
+
if (!completedCommand.run) throw new Error('智能体企查查任务完成但结果已失效');
|
|
2491
|
+
return completedCommand.run;
|
|
2492
|
+
};
|
|
2493
|
+
|
|
2305
2494
|
const runQcc = async () => {
|
|
2306
2495
|
if (busy || !qccEstimate || !paidConfirmed || !qccEstimate.withinLimit) return;
|
|
2307
2496
|
actions.setBusy(true);
|
|
2308
2497
|
actions.setError(null);
|
|
2309
2498
|
try {
|
|
2310
|
-
await workflowAction(actions, activeSessionId, cachedTask, 'match-start');
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2499
|
+
const current = await workflowAction(actions, activeSessionId, cachedTask, 'match-start');
|
|
2500
|
+
const r = await executePreparedQccCommand({
|
|
2501
|
+
kind: 'enrich',
|
|
2502
|
+
taskId: current.id,
|
|
2313
2503
|
rows: runtime.rows,
|
|
2314
2504
|
headers: runtime.headers,
|
|
2315
2505
|
nameField,
|
|
2506
|
+
fieldSelection,
|
|
2316
2507
|
includeRisk: false,
|
|
2317
2508
|
concurrency: 2,
|
|
2318
|
-
confirmPaidCalls: true,
|
|
2319
|
-
idempotencyKey: key,
|
|
2320
2509
|
});
|
|
2321
|
-
|
|
2322
|
-
else actions.setError((r && (r.message || r.error)) || '企业匹配补全失败');
|
|
2510
|
+
await applyQccRun(r);
|
|
2323
2511
|
} catch (err) {
|
|
2324
2512
|
if (['QCC_NOT_CONNECTED', 'QCC_TOOL_UNAVAILABLE', 'QCC_AUTH_REQUIRED'].includes(err?.code)) {
|
|
2325
2513
|
try { await workflowAction(actions, activeSessionId, cachedTask, 'authorization-required'); } catch (_workflowError) {}
|
|
@@ -2335,15 +2523,15 @@ window.__ModuleLoader__.load({
|
|
|
2335
2523
|
actions.setBusy(true);
|
|
2336
2524
|
actions.setError(null);
|
|
2337
2525
|
try {
|
|
2338
|
-
const
|
|
2526
|
+
const task = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? cachedTask;
|
|
2527
|
+
const r = await executePreparedQccCommand({
|
|
2528
|
+
kind: 'resolve',
|
|
2529
|
+
taskId: task.id,
|
|
2339
2530
|
runId: qccRun.runId,
|
|
2340
2531
|
companyName: item.companyName,
|
|
2341
2532
|
selectedCreditNo: candidate.creditNo,
|
|
2342
|
-
confirmPaidCalls: true,
|
|
2343
|
-
idempotencyKey: `g5-resolve-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
2344
2533
|
});
|
|
2345
|
-
|
|
2346
|
-
else actions.setError((r && (r.message || r.error)) || '候选确认失败');
|
|
2534
|
+
await applyQccRun(r);
|
|
2347
2535
|
} catch (err) {
|
|
2348
2536
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
2349
2537
|
} finally {
|
|
@@ -2358,14 +2546,14 @@ window.__ModuleLoader__.load({
|
|
|
2358
2546
|
actions.setBusy(true);
|
|
2359
2547
|
actions.setError(null);
|
|
2360
2548
|
try {
|
|
2361
|
-
const
|
|
2549
|
+
const task = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? cachedTask;
|
|
2550
|
+
const r = await executePreparedQccCommand({
|
|
2551
|
+
kind: 'retry',
|
|
2552
|
+
taskId: task.id,
|
|
2362
2553
|
runId: qccRun.runId,
|
|
2363
2554
|
companyNames: names,
|
|
2364
|
-
confirmPaidCalls: true,
|
|
2365
|
-
idempotencyKey: `g5-retry-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
2366
2555
|
});
|
|
2367
|
-
|
|
2368
|
-
else actions.setError((r && (r.message || r.error)) || '失败项重试失败');
|
|
2556
|
+
await applyQccRun(r);
|
|
2369
2557
|
} catch (err) {
|
|
2370
2558
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
2371
2559
|
} finally {
|
|
@@ -2526,9 +2714,23 @@ window.__ModuleLoader__.load({
|
|
|
2526
2714
|
}
|
|
2527
2715
|
};
|
|
2528
2716
|
|
|
2717
|
+
const displayStatValue = (value) => {
|
|
2718
|
+
if (typeof value === 'string' || typeof value === 'number') return value;
|
|
2719
|
+
if (typeof value === 'bigint' || typeof value === 'boolean') return String(value);
|
|
2720
|
+
return '—';
|
|
2721
|
+
};
|
|
2722
|
+
// 浏览器运行时只保留当前会话的原始明细;进程重启后,已完成任务应从
|
|
2723
|
+
// Host 持久化摘要恢复计数,不能因 qccRun 仅存在于内存而显示“—”。
|
|
2724
|
+
const qccEnrichedCount = qccRun
|
|
2725
|
+
? qccRun.summary?.enriched ?? 0
|
|
2726
|
+
: cachedTask?.enrichmentSummary?.completed;
|
|
2727
|
+
const qccReviewCount = qccRun
|
|
2728
|
+
? qccRun.summary?.ambiguous ?? 0
|
|
2729
|
+
: cachedTask?.enrichmentSummary?.reviewRequired
|
|
2730
|
+
?? cachedTask?.matchSummary?.reviewRequired;
|
|
2529
2731
|
const stat = (label, value, tone) => h('div', { className: 'dcAgentCard' },
|
|
2530
2732
|
h('span', null, label),
|
|
2531
|
-
h('b', { className: tone ? `is-${tone}` : null }, value)
|
|
2733
|
+
h('b', { className: tone ? `is-${tone}` : null }, displayStatValue(value))
|
|
2532
2734
|
);
|
|
2533
2735
|
const uiFieldGroups = Array.isArray(workflowContract?.fieldCatalog)
|
|
2534
2736
|
? workflowContract.fieldCatalog.map((group) => [group.id, group.label, (group.fields || []).map((field) => [field.id, field.label])])
|
|
@@ -2645,7 +2847,12 @@ window.__ModuleLoader__.load({
|
|
|
2645
2847
|
))),
|
|
2646
2848
|
) : null,
|
|
2647
2849
|
h('div', { className: 'dcAgentRow' },
|
|
2648
|
-
h('button', {
|
|
2850
|
+
h('button', {
|
|
2851
|
+
type: 'button',
|
|
2852
|
+
className: 'dcAgentButton is-primary',
|
|
2853
|
+
disabled: busy,
|
|
2854
|
+
onClick: () => actions.setStep(requiresQcc ? 'match' : 'enrich'),
|
|
2855
|
+
}, requiresQcc ? '下一步:匹配核验' : '下一步:本地清洗补全'),
|
|
2649
2856
|
),
|
|
2650
2857
|
) : h('div', { className: 'dcAgentRow' },
|
|
2651
2858
|
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !hasData, 'aria-label': '生成体检报告', onClick: runProfile }, busy ? '体检中…' : '生成体检报告'),
|
|
@@ -2737,8 +2944,8 @@ window.__ModuleLoader__.load({
|
|
|
2737
2944
|
stat('输入行数', dataset ? dataset.rowCount : '—'),
|
|
2738
2945
|
stat('清洗保留', clean ? clean.kept : '—', clean && clean.kept > 0 ? 'good' : null),
|
|
2739
2946
|
stat('本地补全', complete ? complete.completed : '—', complete && complete.completed > 0 ? 'good' : null),
|
|
2740
|
-
stat('QCC 已补全',
|
|
2741
|
-
stat('待核验',
|
|
2947
|
+
stat('QCC 已补全', qccEnrichedCount, qccEnrichedCount > 0 ? 'good' : null),
|
|
2948
|
+
stat('待核验', qccReviewCount, qccReviewCount > 0 ? 'warn' : null),
|
|
2742
2949
|
),
|
|
2743
2950
|
h('div', { className: 'dcAgentRow' }, h('button', { type: 'button', className: 'dcAgentButton is-primary', onClick: () => actions.setStep('download') }, '进入下载数据')),
|
|
2744
2951
|
);
|
|
@@ -2749,8 +2956,8 @@ window.__ModuleLoader__.load({
|
|
|
2749
2956
|
h('p', { className: 'dcAgentHint' }, '结果与异常清单由 Host 生成 CSV/XLSX 四件套并耐久保存。任务或插件重启后,可从任务历史继续下载;无需再次调用企查查。'),
|
|
2750
2957
|
h('div', { className: 'dcAgentGrid' },
|
|
2751
2958
|
stat('输入行数', dataset ? dataset.rowCount : workflowTask?.source?.rowCount ?? '—'),
|
|
2752
|
-
stat('匹配补全',
|
|
2753
|
-
stat('待核验',
|
|
2959
|
+
stat('匹配补全', qccEnrichedCount, qccEnrichedCount > 0 ? 'good' : null),
|
|
2960
|
+
stat('待核验', qccReviewCount, qccReviewCount > 0 ? 'warn' : null),
|
|
2754
2961
|
stat('任务状态', WORKFLOW_STATE_LABELS[cachedTask?.state] || cachedTask?.state || '进行中'),
|
|
2755
2962
|
),
|
|
2756
2963
|
availableArtifacts.length ? h('div', { className: 'dcAgentArtifactList' },
|
|
@@ -2897,6 +3104,7 @@ window.__ModuleLoader__.load({
|
|
|
2897
3104
|
try {
|
|
2898
3105
|
const workbenchStore = createWorkbenchStore();
|
|
2899
3106
|
ctx.effect(() => installUiStyles(), 'data-cleaning-agent: UI styles');
|
|
3107
|
+
ctx.effect(() => installSessionOwnershipBridge(ctx), 'data-cleaning-agent: session ownership');
|
|
2900
3108
|
|
|
2901
3109
|
// 右侧工作台:additive overlay,不替换 DSH 单占位 details 面板。
|
|
2902
3110
|
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
@@ -2904,6 +3112,9 @@ window.__ModuleLoader__.load({
|
|
|
2904
3112
|
id: 'data-cleaning-agent',
|
|
2905
3113
|
order: 200,
|
|
2906
3114
|
store: workbenchStore,
|
|
3115
|
+
inject: () => ({
|
|
3116
|
+
sendSessionCommand: (sessionId, prompt) => sendQccAgentCommand(ctx, sessionId, prompt),
|
|
3117
|
+
}),
|
|
2907
3118
|
}, WorkbenchDrawer));
|
|
2908
3119
|
|
|
2909
3120
|
// 左栏:footer 只托管生命周期和 Portal 降级;实际入口显示在工作区列表前。
|
|
@@ -2961,11 +3172,17 @@ window.__ModuleLoader__.load({
|
|
|
2961
3172
|
// 测试用纯函数,不构成 Host / DSH 稳定 API。
|
|
2962
3173
|
exports.__testing = {
|
|
2963
3174
|
buildTaskPrompt,
|
|
3175
|
+
clearCleaningDraft,
|
|
2964
3176
|
entriesToDataset,
|
|
2965
3177
|
extractPromptEntries,
|
|
2966
3178
|
guessMappings,
|
|
3179
|
+
deactivateCleaningSession,
|
|
3180
|
+
installSessionOwnershipBridge,
|
|
3181
|
+
isCleaningSession,
|
|
3182
|
+
isKnownCleaningDraft,
|
|
2967
3183
|
markCleaningSession,
|
|
2968
3184
|
qualitySummaryFor,
|
|
3185
|
+
rewriteHeroChrome,
|
|
2969
3186
|
ensureWorkflowTask,
|
|
2970
3187
|
queueWorkflowOperation,
|
|
2971
3188
|
};
|
package/lib/engine.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -60,11 +60,16 @@ export function apply(ctx, config) {
|
|
|
60
60
|
// 3. web 半区(仅 web 组合存在;headless 组合无 webServer/webRuntime,inject 会失败)
|
|
61
61
|
try {
|
|
62
62
|
ctx.inject(['webServer', 'webRuntime', 'tools', 'skills', 'jobs', 'storageDomain', 'fs'], (wctx) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
try {
|
|
64
|
+
const dispose = mountWebRoutes(wctx, { logger: ctx.logger, report, TOOL_NAME: TOOL_CLEAN, SKILL_NAME });
|
|
65
|
+
if (typeof dispose === 'function' && typeof ctx.effect === 'function') {
|
|
66
|
+
ctx.effect(() => () => dispose(), 'data-cleaning-agent: web routes');
|
|
67
|
+
}
|
|
68
|
+
report.webMounted = true;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
report.webSkipped = true;
|
|
71
|
+
console.warn(`[dc-agent] web half failed during deferred mount: ${error instanceof Error ? error.stack : String(error)}`);
|
|
66
72
|
}
|
|
67
|
-
report.webMounted = true;
|
|
68
73
|
});
|
|
69
74
|
} catch (error) {
|
|
70
75
|
report.webSkipped = true;
|
package/lib/jobs.js
CHANGED
|
@@ -35,7 +35,7 @@ export function runSync(kind, rows, opts = {}) {
|
|
|
35
35
|
}
|
|
36
36
|
case 'complete': {
|
|
37
37
|
const r = completeRows(rows, opts);
|
|
38
|
-
return { kind, summary: { total: r.total, completed: r.
|
|
38
|
+
return { kind, summary: { total: r.total, completed: r.completedCount, incompleteCount: r.incompleteCount, name: r.fillStats.name, amount: r.fillStats.amount, phoneNormalized: r.fillStats.phoneNormalized }, rows: r.completed, headers: opts.headers ?? [] };
|
|
39
39
|
}
|
|
40
40
|
case 'profile': {
|
|
41
41
|
const r = profileRows(rows, opts);
|