dsh-data-cleaning-agent 0.6.2 → 0.6.3
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 +11 -0
- package/README.en.md +3 -1
- package/README.md +3 -2
- package/docs/RELEASE-0.6.3.md +38 -0
- package/lib/client.js +206 -42
- package/lib/qcc-command.js +27 -16
- package/lib/skill-enrich.js +2 -2
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.6.3] - 2026-09-05
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- 无表头、每行一家企业的粘贴名单改按「主体标识」逐行解析,不再把第一家企业误识为 CSV 表头。
|
|
11
|
+
- 已完成或已确认规则的任务再录入名单时自动创建新 taskId,避免新数据继承旧任务的锁定状态、默认字段或运行结果。
|
|
12
|
+
- 匹配补全预览的内部字段键改为中文表头,并改善窄工作台下的列宽和横向滚动。
|
|
13
|
+
- 缩小桌面端右侧工作台宽度,打开时让中央会话区留出同宽空间,并固定头部操作组、在窄屏隐藏次要状态,避免输入框被遮挡或关闭按钮被挤出可见区。
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- 首次主体匹配不再立即发送内部 JSON 命令;改为向中央对话框回填可阅读、可编辑的中文任务说明,关闭右栏后由用户显式发送才执行 Agent-owned QCC 高层工具。
|
|
17
|
+
|
|
7
18
|
## [0.6.2] - 2026-09-04
|
|
8
19
|
|
|
9
20
|
### Fixed
|
package/README.en.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> A data cleaning & completion agent plugin for DeepSeek Harness: local CSV/XLSX/JSON engine plus optional Qichacha (QCC) MCP enterprise-data enrichment. Initiated and maintained by the Qichacha (QCC) team.
|
|
4
4
|
>
|
|
5
|
-
> Current source version / 当前源码版本: **0.6.
|
|
5
|
+
> Current source version / 当前源码版本: **0.6.3** (stable release)
|
|
6
6
|
|
|
7
7
|
[](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/workflows/ci.yml)
|
|
8
8
|
[](https://www.npmjs.com/package/dsh-data-cleaning-agent)
|
|
@@ -117,6 +117,8 @@ See [the Phase-3 acceptance record](docs/PHASE3-ACCEPTANCE.md) and
|
|
|
117
117
|
See [the 0.5.1 release record](docs/RELEASE-0.5.1.md) for the README fix and release-text gate.
|
|
118
118
|
See [the 0.5.2 release record](docs/RELEASE-0.5.2.md) for native DSH UI alignment, verification, and rollback.
|
|
119
119
|
See [the 0.5.3 release record](docs/RELEASE-0.5.3.md) for the business landing view and prompt builder.
|
|
120
|
+
See [the 0.6.3 release record](docs/RELEASE-0.6.3.md) for multiline entity-list parsing,
|
|
121
|
+
editable execution summaries, and workbench/composer layout fixes.
|
|
120
122
|
See [the 0.6.2 release record](docs/RELEASE-0.6.2.md) for Chinese export headers,
|
|
121
123
|
complete field-selection propagation, and profile-field enrichment.
|
|
122
124
|
See [the 0.6.1 release record](docs/RELEASE-0.6.1.md) for the Agent-owned QCC workbench,
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 在 DeepSeek Harness 中清洗、补全、画像企业名单数据的智能体插件:本地 CSV/XLSX/JSON 引擎 + 可选企查查(Qichacha/QCC)MCP 企业数据补全,由企查查(Qichacha/QCC)团队发起并维护。
|
|
4
4
|
>
|
|
5
|
-
> 当前源码版本 / Current source version: **0.6.
|
|
5
|
+
> 当前源码版本 / Current source version: **0.6.3**(正式版本)
|
|
6
6
|
|
|
7
7
|
[](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/workflows/ci.yml)
|
|
8
8
|
[](https://www.npmjs.com/package/dsh-data-cleaning-agent)
|
|
@@ -113,7 +113,8 @@ bash <(curl -fsSL https://raw.githubusercontent.com/duhu2000/dsh-data-cleaning-a
|
|
|
113
113
|
0.5.1 文档补丁与防回归发布门见 [docs/RELEASE-0.5.1.md](docs/RELEASE-0.5.1.md)。
|
|
114
114
|
0.5.2 DSH 原生 UI 对齐范围、验收与回滚见 [docs/RELEASE-0.5.2.md](docs/RELEASE-0.5.2.md)。
|
|
115
115
|
0.5.3 业务首页、提示词生成和输入框下方流程栏见 [docs/RELEASE-0.5.3.md](docs/RELEASE-0.5.3.md)。
|
|
116
|
-
0.6.
|
|
116
|
+
0.6.3 多行名单解析、可编辑执行说明与工作台遮挡修复见
|
|
117
|
+
[docs/RELEASE-0.6.3.md](docs/RELEASE-0.6.3.md)。0.6.2 中文导出表头、完整字段选择传递与画像字段补全见
|
|
117
118
|
[docs/RELEASE-0.6.2.md](docs/RELEASE-0.6.2.md)。0.6.1 Agent-owned QCC 工作台、会话隔离、恢复统计及真实闭环验收见
|
|
118
119
|
[docs/RELEASE-0.6.1.md](docs/RELEASE-0.6.1.md)。0.6.0 五步 taskId 工作流、耐久制品、
|
|
119
120
|
跨重启恢复与双基线验收见 [docs/RELEASE-0.6.0.md](docs/RELEASE-0.6.0.md)。
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# 0.6.3 多行名单解析与可编辑执行体验修复
|
|
2
|
+
|
|
3
|
+
> 版本:**0.6.3**
|
|
4
|
+
> 发布日期:2026-09-05
|
|
5
|
+
> 发布来源:`v0.6.3` Tag 触发 GitHub Actions,使用 npm OIDC Trusted Publishing。
|
|
6
|
+
|
|
7
|
+
## 目标
|
|
8
|
+
|
|
9
|
+
0.6.3 是 0.6.2 的 UI 与任务正确性补丁,不扩大企查查工具域:
|
|
10
|
+
|
|
11
|
+
1. 无表头、每行一家企业的粘贴名单按「主体标识」逐行解析,不再把首家企业误作表头;
|
|
12
|
+
2. 完成或确认规则后的任务再次录入名单时创建新 taskId,不复用旧任务锁定状态和结果;
|
|
13
|
+
3. 首次主体匹配先将可阅读、可编辑的中文任务说明回填中央对话框,由用户发送后才执行企查查调用;
|
|
14
|
+
4. 桌面端右侧工作台打开时为中央会话和输入框让位,窄屏保持关闭按钮可见;
|
|
15
|
+
5. 匹配补全预览使用中文字段名,并保留横向滚动能力。
|
|
16
|
+
|
|
17
|
+
## 安全与调用边界
|
|
18
|
+
|
|
19
|
+
- 名单、字段和额度确认仍只暂存在本机 Host;可见任务说明不包含企业名单。
|
|
20
|
+
- 生成任务说明不会产生企查查调用;用户显式发送后才调用一次 `data_cleaning_qcc_run`。
|
|
21
|
+
- 使用当前用户自己连接的企查查 MCP 账号,插件不内置、不保存或分发 OAuth token、Key。
|
|
22
|
+
- 历史域、人员域和招投标域仍按既定范围延期,本补丁不新增相关工具。
|
|
23
|
+
|
|
24
|
+
## 验收
|
|
25
|
+
|
|
26
|
+
- 两行纯文本企业名单解析为 2 行,显式表头输入仍走 CSV 解析;
|
|
27
|
+
- 已完成任务重新录入数据后生成新的 taskId;
|
|
28
|
+
- 可见任务说明展示处理动作、数量和中文字段,不展示 `schemaVersion` 或 JSON;
|
|
29
|
+
- 1280px 与 900px 视口下右侧工作台不遮挡中央输入框,720px 下关闭按钮可见;
|
|
30
|
+
- 结果预览和新生成的 CSV/XLSX 使用中文字段名;已选择但无返回值的字段保留空列;
|
|
31
|
+
- `npm run check` 全绿,177/177 自动化测试通过,发布包内容命中白名单。
|
|
32
|
+
|
|
33
|
+
## 升级与回滚
|
|
34
|
+
|
|
35
|
+
- 升级:安装 `dsh-data-cleaning-agent@0.6.3` 并完全停止、重新运行 `dsh web`。
|
|
36
|
+
- 回滚:重新安装 `dsh-data-cleaning-agent@0.6.2` 并完全重启 DSH。
|
|
37
|
+
- 本补丁不修改 `dc_workflows_v2` 存储 schema,也不删除既有任务或耐久制品。
|
|
38
|
+
- 旧任务制品不会自动重写;升级后需新建任务并重新生成结果,才能获得修正后的行数和表头。
|
package/lib/client.js
CHANGED
|
@@ -113,7 +113,9 @@ window.__ModuleLoader__.load({
|
|
|
113
113
|
.dcAgentWorkbench {
|
|
114
114
|
display: flex;
|
|
115
115
|
flex-direction: column;
|
|
116
|
-
width: min(
|
|
116
|
+
width: min(460px, 42vw);
|
|
117
|
+
max-width: calc(100vw - 420px);
|
|
118
|
+
min-width: 360px;
|
|
117
119
|
height: 100vh;
|
|
118
120
|
border: 1px solid light-dark(#d5dae4, #2a3038);
|
|
119
121
|
border-radius: 12px 0 0 12px;
|
|
@@ -397,8 +399,8 @@ window.__ModuleLoader__.load({
|
|
|
397
399
|
.dcAgentWbHeader {
|
|
398
400
|
display: flex;
|
|
399
401
|
align-items: center;
|
|
400
|
-
gap:
|
|
401
|
-
padding: 14px
|
|
402
|
+
gap: 8px;
|
|
403
|
+
padding: 12px 14px;
|
|
402
404
|
border-bottom: 1px solid light-dark(#e4e8f0, #303947);
|
|
403
405
|
}
|
|
404
406
|
|
|
@@ -421,8 +423,18 @@ window.__ModuleLoader__.load({
|
|
|
421
423
|
background: light-dark(#edf4ff, #172841);
|
|
422
424
|
}
|
|
423
425
|
|
|
426
|
+
.dcAgentWbTitle b,
|
|
427
|
+
.dcAgentWbTitle small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
424
428
|
.dcAgentWbTitle b { display: block; font-size: 15px; }
|
|
425
|
-
.dcAgentWbTitle small { display: block; color: light-dark(#697386, #aab5c7); font-size: 11px; }
|
|
429
|
+
.dcAgentWbTitle small { display: block; max-width: 190px; color: light-dark(#697386, #aab5c7); font-size: 11px; }
|
|
430
|
+
|
|
431
|
+
.dcAgentWbActions {
|
|
432
|
+
display: flex;
|
|
433
|
+
align-items: center;
|
|
434
|
+
gap: 6px;
|
|
435
|
+
flex: 0 0 auto;
|
|
436
|
+
margin-left: auto;
|
|
437
|
+
}
|
|
426
438
|
|
|
427
439
|
.dcAgentQccBadge {
|
|
428
440
|
flex: 0 0 auto;
|
|
@@ -562,6 +574,12 @@ window.__ModuleLoader__.load({
|
|
|
562
574
|
}
|
|
563
575
|
.dcAgentTable th { color: light-dark(#697386, #aab5c7); font-weight: 600; }
|
|
564
576
|
.dcAgentTable td.num { font-variant-numeric: tabular-nums; }
|
|
577
|
+
.dcAgentTableWrap { max-width: 100%; overflow-x: auto; }
|
|
578
|
+
.dcAgentTableWrap .dcAgentTable { min-width: 720px; table-layout: auto; }
|
|
579
|
+
.dcAgentTableWrap .dcAgentTable th,
|
|
580
|
+
.dcAgentTableWrap .dcAgentTable td { min-width: 112px; white-space: nowrap; }
|
|
581
|
+
.dcAgentTableWrap .dcAgentTable th:first-child,
|
|
582
|
+
.dcAgentTableWrap .dcAgentTable td:first-child { min-width: 190px; }
|
|
565
583
|
|
|
566
584
|
.dcAgentError {
|
|
567
585
|
padding: 10px 12px;
|
|
@@ -709,11 +727,28 @@ window.__ModuleLoader__.load({
|
|
|
709
727
|
.dcAgentPreviewTable { margin-top: 12px; overflow: auto; border: 1px solid light-dark(#e4e8f0, #303947); border-radius: 10px; }
|
|
710
728
|
.dcAgentHistoryTask { display: block; width: 100%; padding: 10px; text-align: left; }
|
|
711
729
|
.dcAgentHistoryTask h3 { margin: 0; font-size: 12px; }
|
|
730
|
+
/*
|
|
731
|
+
* DSH 的 root.side slot 是覆盖层。桌面端工作台打开时,让带稳定标记的
|
|
732
|
+
* 会话滚动区主动让出同宽空间,确保消息流和中央 composer 不被遮挡。
|
|
733
|
+
* 移动端仍保留全屏工作台,不压缩底层会话。
|
|
734
|
+
*/
|
|
735
|
+
@media (min-width: 1041px) {
|
|
736
|
+
body:has(.dcAgentWorkbench) [data-conversation-scroll] {
|
|
737
|
+
box-sizing: border-box;
|
|
738
|
+
padding-right: min(460px, 42vw);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
@media (min-width: 761px) and (max-width: 1040px) {
|
|
742
|
+
body:has(.dcAgentWorkbench) [data-conversation-scroll] {
|
|
743
|
+
box-sizing: border-box;
|
|
744
|
+
padding-right: min(420px, 48vw);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
712
747
|
@media (max-width: 760px) {
|
|
713
|
-
.dcAgentWorkbench, .dcAgentWorkbench.is-expanded { width: 100vw; border-radius: 0; }
|
|
748
|
+
.dcAgentWorkbench, .dcAgentWorkbench.is-expanded { width: 100vw; max-width: none; min-width: 0; border-radius: 0; }
|
|
714
749
|
.dcAgentOverlay { background: rgba(8, 10, 14, 0.22); pointer-events: auto; }
|
|
715
750
|
.dcAgentStepper { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
716
|
-
.dcAgentQccBadge { display: none; }
|
|
751
|
+
.dcAgentQccBadge, .dcAgentJobsPill { display: none; }
|
|
717
752
|
.dcAgentCandidate { grid-template-columns: 1fr; }
|
|
718
753
|
.dcAgentCapabilities { justify-content: flex-start; padding-inline: 12px; }
|
|
719
754
|
.dcAgentCapability { min-width: 92px; }
|
|
@@ -725,6 +760,10 @@ window.__ModuleLoader__.load({
|
|
|
725
760
|
.dcAgentMappingRow { grid-template-columns: 1fr; }
|
|
726
761
|
.dcAgentMappingRow > b { display: none; }
|
|
727
762
|
}
|
|
763
|
+
@media (min-width: 761px) and (max-width: 1040px) {
|
|
764
|
+
.dcAgentWorkbench { width: 420px; max-width: 48vw; min-width: 360px; }
|
|
765
|
+
.dcAgentQccBadge, .dcAgentJobsPill { display: none; }
|
|
766
|
+
}
|
|
728
767
|
`;
|
|
729
768
|
|
|
730
769
|
/** 安装入口、会话能力按钮与右侧工作台样式。 */
|
|
@@ -816,6 +855,17 @@ window.__ModuleLoader__.load({
|
|
|
816
855
|
]],
|
|
817
856
|
];
|
|
818
857
|
const ENRICHMENT_OPTIONS = FIELD_GROUPS.flatMap(([, , fields]) => fields);
|
|
858
|
+
const RESULT_FIELD_LABELS = new Map([
|
|
859
|
+
...ENRICHMENT_OPTIONS,
|
|
860
|
+
['biz_status', '经营状态'],
|
|
861
|
+
['risk_tags', '风险标签'],
|
|
862
|
+
['qcc_match_status', '匹配状态'],
|
|
863
|
+
['qcc_source', '数据来源'],
|
|
864
|
+
['qcc_error', '错误原因'],
|
|
865
|
+
['match_status', '匹配状态'],
|
|
866
|
+
['input_company', '原始企业名称'],
|
|
867
|
+
['candidate_company', '候选企业名称'],
|
|
868
|
+
]);
|
|
819
869
|
const MATCH_ANCHOR_OPTIONS = [
|
|
820
870
|
['company_name', '企业名称'], ['credit_no', '统一社会信用代码'], ['reg_no', '注册号'],
|
|
821
871
|
];
|
|
@@ -852,9 +902,13 @@ window.__ModuleLoader__.load({
|
|
|
852
902
|
function isKnownCleaningDraft(value) {
|
|
853
903
|
if (value === DEFAULT_SESSION_PROMPT) return true;
|
|
854
904
|
if (typeof value !== 'string') return false;
|
|
855
|
-
|
|
905
|
+
const wizardDraft = value.startsWith('请执行一项企业名单数据清洗补全任务。')
|
|
856
906
|
&& value.includes('企查查连接、套餐额度和费用均由当前用户自己的账号承担。')
|
|
857
907
|
&& value.includes('提供结果和待复核清单的导出。');
|
|
908
|
+
const executionDraft = value.startsWith('请执行已在「数据清洗补全工作台」确认的企业数据任务。')
|
|
909
|
+
&& value.includes('安全任务凭证:dcq-')
|
|
910
|
+
&& value.includes('data_cleaning_qcc_run');
|
|
911
|
+
return wizardDraft || executionDraft;
|
|
858
912
|
}
|
|
859
913
|
|
|
860
914
|
function clearCleaningDraft(ctx, sessionId, onlyDefault = false) {
|
|
@@ -1029,6 +1083,22 @@ window.__ModuleLoader__.load({
|
|
|
1029
1083
|
};
|
|
1030
1084
|
}
|
|
1031
1085
|
|
|
1086
|
+
/**
|
|
1087
|
+
* 工作台粘贴区兼容「每行一个企业名称 / 信用代码」。
|
|
1088
|
+
* 无分隔符且首行不是显式表头时,不得把第一家企业误当 CSV 表头。
|
|
1089
|
+
*/
|
|
1090
|
+
function plainEntityListDataset(text) {
|
|
1091
|
+
const lines = String(text ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1092
|
+
if (!lines.length || lines.some((line) => /[,\t;]/.test(line))) return null;
|
|
1093
|
+
const explicitHeader = /^(?:name|company|company_name|credit_no|creditcode|unified_credit_code|主体标识|企业名称|公司名称|单位名称|统一社会信用代码|信用代码|注册号)$/i;
|
|
1094
|
+
if (explicitHeader.test(lines[0])) return null;
|
|
1095
|
+
return entriesToDataset(lines);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function resultFieldLabel(key) {
|
|
1099
|
+
return RESULT_FIELD_LABELS.get(String(key ?? '')) ?? String(key ?? '');
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1032
1102
|
const MAPPING_PATTERNS = [
|
|
1033
1103
|
['company_name', /^(name|company|company_name|企业名称|公司名称|单位名称)$/i],
|
|
1034
1104
|
['credit_no', /^(credit_no|creditCode|unified_credit_code|统一社会信用代码|信用代码)$/i],
|
|
@@ -1131,10 +1201,8 @@ window.__ModuleLoader__.load({
|
|
|
1131
1201
|
return task;
|
|
1132
1202
|
}
|
|
1133
1203
|
|
|
1134
|
-
async function
|
|
1204
|
+
async function createWorkflowTask(actions, sessionId, draft = {}) {
|
|
1135
1205
|
const key = String(sessionId || 'unassigned');
|
|
1136
|
-
const cached = workflowTaskBySession.get(key);
|
|
1137
|
-
if (cached) return cacheWorkflowTask(actions, sessionId, cached);
|
|
1138
1206
|
let pending = workflowTaskCreationBySession.get(key);
|
|
1139
1207
|
if (!pending) {
|
|
1140
1208
|
pending = requestJson('/data-cleaning/api/workflow/tasks', 'POST', {
|
|
@@ -1155,6 +1223,29 @@ window.__ModuleLoader__.load({
|
|
|
1155
1223
|
return cacheWorkflowTask(actions, sessionId, response.task);
|
|
1156
1224
|
}
|
|
1157
1225
|
|
|
1226
|
+
async function ensureWorkflowTask(actions, sessionId, draft = {}) {
|
|
1227
|
+
const key = String(sessionId || 'unassigned');
|
|
1228
|
+
const cached = workflowTaskBySession.get(key);
|
|
1229
|
+
if (cached) return cacheWorkflowTask(actions, sessionId, cached);
|
|
1230
|
+
return createWorkflowTask(actions, sessionId, draft);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const EDITABLE_WORKFLOW_STATES = new Set(['draft', 'uploaded', 'parse_failed']);
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* 完成 / 已确认规则的任务不能就地改写。用户再次录入名单时,
|
|
1237
|
+
* 在同一清洗会话内自动创建新 taskId,避免把新名单写入旧任务。
|
|
1238
|
+
*/
|
|
1239
|
+
async function ensureEditableWorkflowTask(actions, sessionId, draft = {}, preferredTask) {
|
|
1240
|
+
const key = String(sessionId || 'unassigned');
|
|
1241
|
+
const current = workflowTaskBySession.get(key) ?? preferredTask ?? null;
|
|
1242
|
+
if (!current) return createWorkflowTask(actions, sessionId, draft);
|
|
1243
|
+
if (EDITABLE_WORKFLOW_STATES.has(current.state)) return cacheWorkflowTask(actions, sessionId, current);
|
|
1244
|
+
workflowTaskBySession.delete(key);
|
|
1245
|
+
actions.resetRunForNewDataset?.();
|
|
1246
|
+
return createWorkflowTask(actions, sessionId, draft);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1158
1249
|
/** 同一会话的 Host 写操作串行化,确保 expectedRevision 不因 UI 事件竞态失效。 */
|
|
1159
1250
|
function queueWorkflowOperation(sessionId, operation) {
|
|
1160
1251
|
const key = String(sessionId || 'unassigned');
|
|
@@ -1273,6 +1364,15 @@ window.__ModuleLoader__.load({
|
|
|
1273
1364
|
draft.paidConfirmed = false;
|
|
1274
1365
|
draft.error = null;
|
|
1275
1366
|
},
|
|
1367
|
+
resetRunForNewDataset: (draft) => {
|
|
1368
|
+
draft.profile = null;
|
|
1369
|
+
draft.clean = null;
|
|
1370
|
+
draft.complete = null;
|
|
1371
|
+
draft.qccEstimate = null;
|
|
1372
|
+
draft.qccRun = null;
|
|
1373
|
+
draft.paidConfirmed = false;
|
|
1374
|
+
draft.error = null;
|
|
1375
|
+
},
|
|
1276
1376
|
setDataset: (draft, dataset) => { draft.dataset = dataset; draft.error = null; },
|
|
1277
1377
|
setProfile: (draft, profile) => { draft.profile = profile; draft.error = null; },
|
|
1278
1378
|
setClean: (draft, clean) => { draft.clean = clean; draft.error = null; },
|
|
@@ -1379,6 +1479,18 @@ window.__ModuleLoader__.load({
|
|
|
1379
1479
|
await sessionConversation.send(prompt);
|
|
1380
1480
|
}
|
|
1381
1481
|
|
|
1482
|
+
function setQccAgentDraft(ctx, sessionId, prompt) {
|
|
1483
|
+
const conversation = typeof ctx.get === 'function' ? ctx.get('conversation') : ctx.conversation;
|
|
1484
|
+
const shell = conversation?.input?.shell?.(sessionId);
|
|
1485
|
+
if (!shell || typeof shell.setDraft !== 'function') {
|
|
1486
|
+
throw new Error('当前 DSH 版本没有可用的会话草稿回填能力');
|
|
1487
|
+
}
|
|
1488
|
+
const accepted = shell.setDraft(prompt);
|
|
1489
|
+
if (accepted === false) throw new Error('任务说明未能回填到当前对话框');
|
|
1490
|
+
ctx.sessions?.open?.(sessionId);
|
|
1491
|
+
return true;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1382
1494
|
function capabilityIcon(item, size = 16) {
|
|
1383
1495
|
return typeof item.icon === 'function'
|
|
1384
1496
|
? h(item.icon, { size, 'aria-hidden': 'true' })
|
|
@@ -2054,6 +2166,8 @@ window.__ModuleLoader__.load({
|
|
|
2054
2166
|
}
|
|
2055
2167
|
return;
|
|
2056
2168
|
}
|
|
2169
|
+
const entityList = plainEntityListDataset(trimmed);
|
|
2170
|
+
if (entityList) return entityList;
|
|
2057
2171
|
return api('/data-cleaning/api/mvp/parse', { filename: 'data.csv', content: trimmed });
|
|
2058
2172
|
}
|
|
2059
2173
|
|
|
@@ -2085,9 +2199,19 @@ window.__ModuleLoader__.load({
|
|
|
2085
2199
|
}
|
|
2086
2200
|
|
|
2087
2201
|
async function persistParsedWorkflow(actions, sessionId, result, source = {}, task) {
|
|
2088
|
-
let current =
|
|
2202
|
+
let current = await ensureEditableWorkflowTask(actions, sessionId, {}, task);
|
|
2089
2203
|
const stagingKey = `session:${sessionId || 'unassigned'}`;
|
|
2090
2204
|
moveRuntime(stagingKey, current.id);
|
|
2205
|
+
// handleParse 可能先把新数据写入旧 task runtime。无论是否新建 taskId,
|
|
2206
|
+
// 都以本次解析结果覆盖目标 runtime,避免复用已完成任务的一行旧数据。
|
|
2207
|
+
const target = runtimeFor(current.id);
|
|
2208
|
+
target.rows = Array.isArray(result.rows) ? result.rows : [];
|
|
2209
|
+
target.headers = Array.isArray(result.headers) ? result.headers : [];
|
|
2210
|
+
target.source = {
|
|
2211
|
+
type: source.type || result.fmt || 'csv',
|
|
2212
|
+
fileName: source.fileName || '',
|
|
2213
|
+
sizeBytes: source.sizeBytes || 0,
|
|
2214
|
+
};
|
|
2091
2215
|
if (['draft', 'uploaded', 'parse_failed'].includes(current.state)) {
|
|
2092
2216
|
current = await workflowAction(actions, sessionId, current, 'upload', {
|
|
2093
2217
|
source: {
|
|
@@ -2107,6 +2231,7 @@ window.__ModuleLoader__.load({
|
|
|
2107
2231
|
function WorkbenchDrawer(props) {
|
|
2108
2232
|
const { useStore, actions } = props;
|
|
2109
2233
|
const sendSessionCommand = props.sendSessionCommand;
|
|
2234
|
+
const setSessionDraft = props.setSessionDraft;
|
|
2110
2235
|
const open = useStore((state) => state.open);
|
|
2111
2236
|
const step = useStore((state) => state.step);
|
|
2112
2237
|
const expanded = useStore((state) => state.expanded);
|
|
@@ -2173,7 +2298,7 @@ window.__ModuleLoader__.load({
|
|
|
2173
2298
|
if (draft.fieldSelection) actions.setFieldSelection(draft.fieldSelection);
|
|
2174
2299
|
for (const [key, value] of Object.entries(draft.matchRules || {})) actions.setMatchRule(key, value);
|
|
2175
2300
|
void queueWorkflowOperation(detail.sessionId, async () => {
|
|
2176
|
-
const task = await
|
|
2301
|
+
const task = await ensureEditableWorkflowTask(actions, detail.sessionId, draft);
|
|
2177
2302
|
return updateWorkflowTask(actions, detail.sessionId, task, draft);
|
|
2178
2303
|
}).catch((eventError) => actions.setError(eventError instanceof Error ? eventError.message : String(eventError)));
|
|
2179
2304
|
};
|
|
@@ -2258,6 +2383,9 @@ window.__ModuleLoader__.load({
|
|
|
2258
2383
|
const runtimeKey = cachedTask?.id ?? `session:${activeSessionId || 'unassigned'}`;
|
|
2259
2384
|
const runtime = runtimeFor(runtimeKey);
|
|
2260
2385
|
const lastCsv = runtime.lastCsv;
|
|
2386
|
+
const qccPreviewColumns = Array.isArray(qccRun?.rows) && qccRun.rows.length
|
|
2387
|
+
? Object.keys(qccRun.rows[0] || {}).slice(0, 8)
|
|
2388
|
+
: [];
|
|
2261
2389
|
const fieldByPattern = (pattern) => runtime.headers.find((field) => pattern.test(field)) ?? null;
|
|
2262
2390
|
const phoneField = fieldByPattern(/^(phone|mobile|tel|telephone|联系电话|手机号码|手机号)$/i);
|
|
2263
2391
|
const amountField = fieldByPattern(/^(amount|price|金额|注册资本)$/i);
|
|
@@ -2491,15 +2619,47 @@ window.__ModuleLoader__.load({
|
|
|
2491
2619
|
return completedCommand.run;
|
|
2492
2620
|
};
|
|
2493
2621
|
|
|
2622
|
+
const prepareEditableQccCommand = async (payload) => {
|
|
2623
|
+
if (!activeSessionId || typeof setSessionDraft !== 'function') {
|
|
2624
|
+
throw new Error('当前会话无法回填可编辑的数据清洗补全任务说明');
|
|
2625
|
+
}
|
|
2626
|
+
const prepared = await api('/data-cleaning/api/g5/commands', {
|
|
2627
|
+
...payload,
|
|
2628
|
+
confirmPaidCalls: true,
|
|
2629
|
+
});
|
|
2630
|
+
const command = prepared?.command;
|
|
2631
|
+
if (!command?.commandId || !command?.prompt) throw new Error('Host 未返回有效的企查查任务命令');
|
|
2632
|
+
setSessionDraft(activeSessionId, command.prompt);
|
|
2633
|
+
actions.close();
|
|
2634
|
+
|
|
2635
|
+
// 发送权仍在用户手中:草稿回填不会调用 QCC。用户发送后,
|
|
2636
|
+
// Agent-owned 工具改变 command 状态;本地安全轮询只读取结果并回写工作流。
|
|
2637
|
+
void waitForQccCommand(command.commandId, 1800).then(async (completedCommand) => {
|
|
2638
|
+
if (completedCommand.state === 'failed') {
|
|
2639
|
+
const commandError = new Error(completedCommand.error?.message || '智能体企查查任务执行失败');
|
|
2640
|
+
commandError.code = completedCommand.error?.code;
|
|
2641
|
+
throw commandError;
|
|
2642
|
+
}
|
|
2643
|
+
if (!completedCommand.run) throw new Error('智能体企查查任务完成但结果已失效');
|
|
2644
|
+
await queueWorkflowOperation(activeSessionId, () => applyQccRun(completedCommand.run));
|
|
2645
|
+
}).catch(async (commandError) => {
|
|
2646
|
+
if (['QCC_NOT_CONNECTED', 'QCC_TOOL_UNAVAILABLE', 'QCC_AUTH_REQUIRED'].includes(commandError?.code)) {
|
|
2647
|
+
const latest = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? cachedTask;
|
|
2648
|
+
try { await workflowAction(actions, activeSessionId, latest, 'authorization-required'); } catch (_workflowError) {}
|
|
2649
|
+
}
|
|
2650
|
+
actions.setError(commandError instanceof Error ? commandError.message : String(commandError));
|
|
2651
|
+
});
|
|
2652
|
+
return command;
|
|
2653
|
+
};
|
|
2654
|
+
|
|
2494
2655
|
const runQcc = async () => {
|
|
2495
2656
|
if (busy || !qccEstimate || !paidConfirmed || !qccEstimate.withinLimit) return;
|
|
2496
2657
|
actions.setBusy(true);
|
|
2497
2658
|
actions.setError(null);
|
|
2498
2659
|
try {
|
|
2499
|
-
|
|
2500
|
-
const r = await executePreparedQccCommand({
|
|
2660
|
+
await prepareEditableQccCommand({
|
|
2501
2661
|
kind: 'enrich',
|
|
2502
|
-
taskId:
|
|
2662
|
+
taskId: cachedTask.id,
|
|
2503
2663
|
rows: runtime.rows,
|
|
2504
2664
|
headers: runtime.headers,
|
|
2505
2665
|
nameField,
|
|
@@ -2507,11 +2667,7 @@ window.__ModuleLoader__.load({
|
|
|
2507
2667
|
includeRisk: false,
|
|
2508
2668
|
concurrency: 2,
|
|
2509
2669
|
});
|
|
2510
|
-
await applyQccRun(r);
|
|
2511
2670
|
} catch (err) {
|
|
2512
|
-
if (['QCC_NOT_CONNECTED', 'QCC_TOOL_UNAVAILABLE', 'QCC_AUTH_REQUIRED'].includes(err?.code)) {
|
|
2513
|
-
try { await workflowAction(actions, activeSessionId, cachedTask, 'authorization-required'); } catch (_workflowError) {}
|
|
2514
|
-
}
|
|
2515
2671
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
2516
2672
|
} finally {
|
|
2517
2673
|
actions.setBusy(false);
|
|
@@ -2886,8 +3042,9 @@ window.__ModuleLoader__.load({
|
|
|
2886
3042
|
h('input', { type: 'checkbox', checked: paidConfirmed, 'aria-label': '确认使用当前用户的企查查账号额度', onChange: (event) => actions.setPaidConfirmed(event.target.checked) }),
|
|
2887
3043
|
'我已核对企业数量和调用上界,并确认使用自己连接的企查查 MCP 账号;额度或费用由该账号自行承担',
|
|
2888
3044
|
),
|
|
3045
|
+
h('p', { className: 'dcAgentHint' }, '下一步只会把中文任务说明回填到中央对话框。你可先阅读、补充备注,点击发送后才开始企查查调用。'),
|
|
2889
3046
|
h('div', { className: 'dcAgentRow' },
|
|
2890
|
-
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !paidConfirmed || !qccEstimate.withinLimit, onClick: runQcc }, busy ? '
|
|
3047
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !paidConfirmed || !qccEstimate.withinLimit, onClick: runQcc }, busy ? '生成中…' : '生成可编辑任务说明'),
|
|
2891
3048
|
),
|
|
2892
3049
|
) : null,
|
|
2893
3050
|
),
|
|
@@ -2902,10 +3059,10 @@ window.__ModuleLoader__.load({
|
|
|
2902
3059
|
Array.isArray(qccRun.rows) && qccRun.rows.length ? h('div', { className: 'dcAgentTableWrap' },
|
|
2903
3060
|
h('table', { className: 'dcAgentTable', 'aria-label': '匹配补全结果预览' },
|
|
2904
3061
|
h('thead', null, h('tr', null,
|
|
2905
|
-
|
|
3062
|
+
qccPreviewColumns.map((key) => h('th', { key }, resultFieldLabel(key))),
|
|
2906
3063
|
)),
|
|
2907
3064
|
h('tbody', null, qccRun.rows.slice(0, 8).map((row, index) => h('tr', { key: `${index}-${row?.qcc_credit_no || row?.credit_no || ''}` },
|
|
2908
|
-
|
|
3065
|
+
qccPreviewColumns.map((key) => h('td', { key }, String(row?.[key] ?? ''))),
|
|
2909
3066
|
))),
|
|
2910
3067
|
),
|
|
2911
3068
|
h('p', { className: 'dcAgentHint' }, `显示前 ${Math.min(8, qccRun.rows.length)} 行;完整结果在下载阶段生成耐久制品。`),
|
|
@@ -3047,26 +3204,29 @@ window.__ModuleLoader__.load({
|
|
|
3047
3204
|
h('small', null, cachedTask ? `${WORKFLOW_STATE_LABELS[cachedTask.state] || cachedTask.state} · ${cachedTask.id.slice(0, 18)}` : '正在创建 Host taskId…'),
|
|
3048
3205
|
),
|
|
3049
3206
|
),
|
|
3050
|
-
h('
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3207
|
+
h('div', { className: 'dcAgentWbActions' },
|
|
3208
|
+
h('span', {
|
|
3209
|
+
className: 'dcAgentQccBadge',
|
|
3210
|
+
title: qccRun ? `任务 ${qccRun.runId}` : '仅在当前用户确认使用自己的企查查账号后调用',
|
|
3211
|
+
}, qccRun ? `QCC · ${qccRun.state}` : qccCapabilities ? (qccCapabilities.capabilities?.ready ? 'QCC · 已连接' : 'QCC · 能力不完整') : 'QCC · 待检测'),
|
|
3212
|
+
h('span', { className: 'dcAgentJobsPill', 'data-state': jobsPill(jobs).state, title: '后台任务状态' }, jobsPill(jobs).label),
|
|
3213
|
+
h('button', {
|
|
3214
|
+
className: 'dcAgentWbClose',
|
|
3215
|
+
type: 'button',
|
|
3216
|
+
'aria-label': expanded ? '收起工作台' : '展开工作台',
|
|
3217
|
+
onClick: () => actions.toggleExpanded(),
|
|
3218
|
+
}, expanded ? '↘' : '↖'),
|
|
3219
|
+
h('button', {
|
|
3220
|
+
className: 'dcAgentWbClose',
|
|
3221
|
+
type: 'button',
|
|
3222
|
+
title: '关闭工作台(Esc)',
|
|
3223
|
+
'aria-label': '关闭',
|
|
3224
|
+
onClick: () => {
|
|
3225
|
+
stopJobsPolling();
|
|
3226
|
+
actions.close();
|
|
3227
|
+
},
|
|
3228
|
+
}, '✕'),
|
|
3229
|
+
),
|
|
3070
3230
|
),
|
|
3071
3231
|
h('nav', { className: 'dcAgentStepper', 'aria-label': '清洗流程' },
|
|
3072
3232
|
STEPS.map((st) => h('button', {
|
|
@@ -3114,6 +3274,7 @@ window.__ModuleLoader__.load({
|
|
|
3114
3274
|
store: workbenchStore,
|
|
3115
3275
|
inject: () => ({
|
|
3116
3276
|
sendSessionCommand: (sessionId, prompt) => sendQccAgentCommand(ctx, sessionId, prompt),
|
|
3277
|
+
setSessionDraft: (sessionId, prompt) => setQccAgentDraft(ctx, sessionId, prompt),
|
|
3117
3278
|
}),
|
|
3118
3279
|
}, WorkbenchDrawer));
|
|
3119
3280
|
|
|
@@ -3182,8 +3343,11 @@ window.__ModuleLoader__.load({
|
|
|
3182
3343
|
isKnownCleaningDraft,
|
|
3183
3344
|
markCleaningSession,
|
|
3184
3345
|
qualitySummaryFor,
|
|
3346
|
+
plainEntityListDataset,
|
|
3347
|
+
resultFieldLabel,
|
|
3185
3348
|
rewriteHeroChrome,
|
|
3186
3349
|
ensureWorkflowTask,
|
|
3350
|
+
ensureEditableWorkflowTask,
|
|
3187
3351
|
queueWorkflowOperation,
|
|
3188
3352
|
};
|
|
3189
3353
|
return module.exports;
|
package/lib/qcc-command.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
10
10
|
import { QccBridgeError } from './qcc.js';
|
|
11
|
-
import { normalizeFieldSelection } from './workflow-contract.js';
|
|
11
|
+
import { fieldLabel, normalizeFieldSelection } from './workflow-contract.js';
|
|
12
12
|
|
|
13
13
|
export const TOOL_QCC_COMMAND = 'data_cleaning_qcc_run';
|
|
14
14
|
|
|
@@ -70,23 +70,34 @@ function normalizedInput(input = {}) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
export function serializeQccCommandPrompt(command) {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
const input = command.input ?? {};
|
|
74
|
+
const action = command.kind === 'resolve'
|
|
75
|
+
? '确认并继续处理已选的企业主体'
|
|
76
|
+
: command.kind === 'retry'
|
|
77
|
+
? '重试已选的可恢复失败项'
|
|
78
|
+
: '执行企业主体匹配与字段补全';
|
|
79
|
+
const rowCount = command.kind === 'enrich'
|
|
80
|
+
? (Array.isArray(input.rows) ? input.rows.length : 0)
|
|
81
|
+
: command.kind === 'retry'
|
|
82
|
+
? (Array.isArray(input.companyNames) ? input.companyNames.length : 0)
|
|
83
|
+
: 1;
|
|
84
|
+
const fields = command.kind === 'enrich'
|
|
85
|
+
? normalizeFieldSelection(input.fieldSelection).map(fieldLabel)
|
|
86
|
+
: [];
|
|
79
87
|
return [
|
|
80
|
-
'
|
|
88
|
+
'请执行已在「数据清洗补全工作台」确认的企业数据任务。',
|
|
81
89
|
'',
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
'
|
|
90
|
+
`处理内容:${action}。`,
|
|
91
|
+
`任务编号:${command.taskId}。`,
|
|
92
|
+
`处理数量:${rowCount} 条企业记录。`,
|
|
93
|
+
...(fields.length ? [`需要补全:${fields.join('、')}。`] : []),
|
|
94
|
+
'执行边界:名单、字段和额度确认已安全暂存在本机 Host;使用当前用户自己连接的企查查 MCP 账号。',
|
|
86
95
|
'',
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
'
|
|
96
|
+
'你可以在发送前补充任务备注。如需修改名单、匹配规则或补全字段,请先返回工作台调整后重新生成。',
|
|
97
|
+
`安全任务凭证:${command.commandId}`,
|
|
98
|
+
'',
|
|
99
|
+
`发送本说明后,请仅调用一次数据清洗补全执行工具(${TOOL_QCC_COMMAND}),参数只传递上述安全任务凭证。`,
|
|
100
|
+
'不要要求重复粘贴企业名单,不要直接调用 mcp__qcc-* 工具,不要重试、扩大名单或追加字段。',
|
|
90
101
|
].join('\n');
|
|
91
102
|
}
|
|
92
103
|
|
|
@@ -234,7 +245,7 @@ export class QccCommandStore {
|
|
|
234
245
|
export function registerQccCommandTool(tools, commands) {
|
|
235
246
|
return tools.register({
|
|
236
247
|
name: TOOL_QCC_COMMAND,
|
|
237
|
-
description: 'Execute one already-staged data-cleaning QCC command. Call only when a visible
|
|
248
|
+
description: 'Execute one already-staged data-cleaning QCC command. Call only when a visible readable workbench task summary supplies a dcq-* commandId. The Host owns rows, billing confirmation, idempotency and result artifacts.',
|
|
238
249
|
parameters: {
|
|
239
250
|
type: 'object',
|
|
240
251
|
additionalProperties: false,
|
package/lib/skill-enrich.js
CHANGED
|
@@ -54,8 +54,8 @@ export function registerEnrichSkill(skills) {
|
|
|
54
54
|
content: [
|
|
55
55
|
'You are an enterprise-list enrichment assistant. You fill company lists with Qichacha (QCC) data by calling QCC MCP tools. Never invent, pad, or fabricate any field.',
|
|
56
56
|
'',
|
|
57
|
-
'
|
|
58
|
-
'- If the visible user message
|
|
57
|
+
'Workbench execution request (highest priority):',
|
|
58
|
+
'- If the visible user message is the readable data-cleaning task summary generated by the workbench, contains a `安全任务凭证` beginning with `dcq-`, and explicitly requests `data_cleaning_qcc_run`, treat that credential as `commandId`, call the high-level tool exactly once with only `commandId`, then stop.',
|
|
59
59
|
'- The Host already holds the rows, billing confirmation and field selection. Do not ask the user to paste rows, do not call any `mcp__qcc-*` tool directly, do not retry, and do not expand the batch.',
|
|
60
60
|
'',
|
|
61
61
|
'Workflow:',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-data-cleaning-agent",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Clean, complete, and profile enterprise name lists in DeepSeek Harness — a data cleaning & completion agent plugin with local CSV/XLSX/JSON engine and optional Qichacha (QCC) MCP enrichment. Maintained by Qichacha/QCC.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"docs/RELEASE-0.6.0.md",
|
|
35
35
|
"docs/RELEASE-0.6.1.md",
|
|
36
36
|
"docs/RELEASE-0.6.2.md",
|
|
37
|
+
"docs/RELEASE-0.6.3.md",
|
|
37
38
|
"docs/UI-WORKFLOW-V2.md",
|
|
38
39
|
"docs/UI-WORKFLOW-V2-MIGRATION.md",
|
|
39
40
|
"docs/UI-WORKFLOW-V2-ACCEPTANCE.md",
|