dsh-data-cleaning-agent 0.6.1 → 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 +20 -0
- package/README.en.md +5 -1
- package/README.md +4 -2
- package/docs/RELEASE-0.6.1.md +17 -3
- package/docs/RELEASE-0.6.2.md +40 -0
- package/docs/RELEASE-0.6.3.md +38 -0
- package/lib/artifacts.js +47 -8
- package/lib/client.js +212 -45
- package/lib/qcc-command.js +30 -15
- package/lib/qcc-runs.js +5 -1
- package/lib/qcc.js +103 -14
- package/lib/skill-enrich.js +2 -2
- package/lib/web.js +1 -0
- package/lib/workflow-contract.js +9 -0
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@
|
|
|
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
|
+
|
|
18
|
+
## [0.6.2] - 2026-09-04
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- 清洗补全结果 CSV/XLSX 的插件字段与状态字段统一导出为中文表头,并按任务字段选择保留空值列,
|
|
22
|
+
不再因企查查未返回某一字段而从制品中静默丢列。
|
|
23
|
+
- 将任务选择的补全字段贯穿中央工作台、Agent-owned 命令、G5 Host Bridge、任务恢复与耐久制品;
|
|
24
|
+
工商详情补充注册地址、行政区划、经营范围、行业、营业期限和企业规模映射,企业简介等画像字段按需
|
|
25
|
+
调用 `get_company_profile`,且不把未声明层级的“企查查行业”猜测为一级/二级行业。
|
|
26
|
+
|
|
7
27
|
## [0.6.1] - 2026-09-04
|
|
8
28
|
|
|
9
29
|
### 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,10 @@ 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.
|
|
122
|
+
See [the 0.6.2 release record](docs/RELEASE-0.6.2.md) for Chinese export headers,
|
|
123
|
+
complete field-selection propagation, and profile-field enrichment.
|
|
120
124
|
See [the 0.6.1 release record](docs/RELEASE-0.6.1.md) for the Agent-owned QCC workbench,
|
|
121
125
|
session isolation, recovered statistics, and real end-to-end validation.
|
|
122
126
|
See [the 0.6.0 release record](docs/RELEASE-0.6.0.md) for the taskId workflow, durable artifacts,
|
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,9 @@ 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 中文导出表头、完整字段选择传递与画像字段补全见
|
|
118
|
+
[docs/RELEASE-0.6.2.md](docs/RELEASE-0.6.2.md)。0.6.1 Agent-owned QCC 工作台、会话隔离、恢复统计及真实闭环验收见
|
|
117
119
|
[docs/RELEASE-0.6.1.md](docs/RELEASE-0.6.1.md)。0.6.0 五步 taskId 工作流、耐久制品、
|
|
118
120
|
跨重启恢复与双基线验收见 [docs/RELEASE-0.6.0.md](docs/RELEASE-0.6.0.md)。
|
|
119
121
|
|
package/docs/RELEASE-0.6.1.md
CHANGED
|
@@ -45,6 +45,20 @@
|
|
|
45
45
|
|
|
46
46
|
## 外部发布核验
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
- PR [#3](https://github.com/duhu2000/dsh-data-cleaning-agent/pull/3) 四项 CI 全绿后合并;
|
|
49
|
+
`v0.6.1` 指向合并提交 `77a19aa`。
|
|
50
|
+
- Release workflow [33880569564](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/runs/33880569564)
|
|
51
|
+
全绿;版本校验、发布模式门禁、npm OIDC publish 与 GitHub Release 均成功。
|
|
52
|
+
- npm `latest` 为 `0.6.1`;发布时间为 2026-09-04T13:53:39Z(北京时间 2026-09-04 21:53)。
|
|
53
|
+
- tarball SHA-1:`ab9aaff6738e3ede08768138bc7a9000ccc28ed2`。
|
|
54
|
+
- tarball integrity:
|
|
55
|
+
`sha512-VWsVelLCxpGYDZ1TZrxlZWLGC86IjtOJDZAszukCs5LaPwkos8UcJn0IWmYtdKF7S3a8TsQGN1KhOhDL0OCHFw==`。
|
|
56
|
+
- npm Registry 返回 SLSA v1 provenance;隔离安装后的 `npm audit signatures` 验证 10 个 Registry
|
|
57
|
+
签名和 1 个 attestation。
|
|
58
|
+
- 从公共 Registry 全新安装成功;包版本为 0.6.1,主入口可导入,浏览器 client 语法门通过,
|
|
59
|
+
`lib/qcc-command.js` 与本发布记录均存在。
|
|
60
|
+
- DSH `0.1.1-rc.2` 全新隔离 Profile 安装 `dsh-data-cleaning-agent@0.6.1` 成功并加载 Host 插件。
|
|
61
|
+
第二个临时 Web Host 因本机全局文件监听资源耗尽(`EMFILE`)未完成 HTTP seam;未停止或改动用户
|
|
62
|
+
正在运行的 3080 实例。真实工作台闭环证据见上方「验收证据」。
|
|
63
|
+
- GitHub Release [v0.6.1](https://github.com/duhu2000/dsh-data-cleaning-agent/releases/tag/v0.6.1)
|
|
64
|
+
已发布,为 Latest,非 draft、非 prerelease。
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# 0.6.2 中文字段导出与完整补全字段修复
|
|
2
|
+
|
|
3
|
+
> 版本:**0.6.2**
|
|
4
|
+
> 发布日期:2026-09-04
|
|
5
|
+
> 发布来源:`v0.6.2` Tag 触发 GitHub Actions,使用 npm OIDC Trusted Publishing。
|
|
6
|
+
|
|
7
|
+
## 目标
|
|
8
|
+
|
|
9
|
+
0.6.2 是 0.6.1 的结果正确性补丁,不扩大企查查工具域,修复真实任务中结果 XLSX 仅出现少量
|
|
10
|
+
英文字段的问题:
|
|
11
|
+
|
|
12
|
+
1. 任务选中的补全维度贯穿工作台、Agent-owned 命令、G5 Host Bridge、恢复/重试与耐久制品;
|
|
13
|
+
2. CSV/XLSX 的插件补全字段和系统状态字段统一使用中文表头;
|
|
14
|
+
3. 即使企查查未返回某个已选字段,结果文件也保留对应中文空列,不再静默丢列;
|
|
15
|
+
4. 工商工具补充注册地址、行政区划、经营范围、行业、营业期限和企业规模映射;
|
|
16
|
+
5. 企业简介及明确的一/二级行业字段按需调用 `get_company_profile`;未声明层级的“企查查行业”
|
|
17
|
+
不会被猜测填入一级或二级行业。
|
|
18
|
+
|
|
19
|
+
## 兼容与调用边界
|
|
20
|
+
|
|
21
|
+
- 稳定发布基线仍为 DSH `0.1.1-rc.2`;`0.1.2-alpha.2` 继续通过隔离 Bridge 兼容。
|
|
22
|
+
- 只有选择企业简介、一级行业或二级行业时才增加企业画像调用,工作台调用估算同步调整。
|
|
23
|
+
- QCC 继续采用 BYO 账号;插件不内置、不记录也不发布 OAuth token 或客户 Key。
|
|
24
|
+
- 历史域、人员域和招投标域仍按既定范围延期,本补丁不新增相关工具。
|
|
25
|
+
|
|
26
|
+
## 验收
|
|
27
|
+
|
|
28
|
+
- 14 个目标维度按选择顺序完整进入结果数据和导出制品;
|
|
29
|
+
- CSV/XLSX 表头为统一社会信用代码、登记状态、法定代表人、注册资本、成立日期、注册地址、
|
|
30
|
+
省份地区、城市、经营范围、一级行业、二级行业、营业期限、企业规模、企业简介;
|
|
31
|
+
- 画像接口未明确返回层级时,一/二级行业保留空值而不是生成数据;
|
|
32
|
+
- 未选择风险能力时不额外输出风险标签;
|
|
33
|
+
- `npm run check` 全绿,176/176 自动化测试通过,npm dry-run 包内容命中白名单。
|
|
34
|
+
|
|
35
|
+
## 升级与回滚
|
|
36
|
+
|
|
37
|
+
- 升级:安装 `dsh-data-cleaning-agent@0.6.2` 并完全重启 DSH。
|
|
38
|
+
- 回滚:重新安装 `dsh-data-cleaning-agent@0.6.1` 并完全重启 DSH。
|
|
39
|
+
- 本补丁不修改 `dc_workflows_v2` 存储 schema,也不删除既有任务或工作区耐久制品。
|
|
40
|
+
- 0.6.1 已生成的旧 XLSX 不会被自动重写;升级后重新生成制品即可获得中文完整表头。
|
|
@@ -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/artifacts.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { createHash, randomUUID } from 'node:crypto';
|
|
9
9
|
import XLSX from 'xlsx';
|
|
10
|
+
import { FIELD_LABELS, normalizeFieldSelection } from './workflow-contract.js';
|
|
10
11
|
|
|
11
12
|
const ROOT = '.dsh-data-cleaning-artifacts/v1';
|
|
12
13
|
const MAX_ARTIFACT_BYTES = 32 * 1024 * 1024;
|
|
@@ -15,6 +16,17 @@ const MAX_ARTIFACT_BYTES = 32 * 1024 * 1024;
|
|
|
15
16
|
const MAX_STORED_BYTES = Math.ceil(MAX_ARTIFACT_BYTES / 3) * 4 + 4;
|
|
16
17
|
const SAFE_ID = /^(?:dcw|dca)-[a-zA-Z0-9-]{8,80}$/;
|
|
17
18
|
|
|
19
|
+
const SYSTEM_FIELD_LABELS = Object.freeze({
|
|
20
|
+
biz_status: '经营状态',
|
|
21
|
+
risk_tags: '风险标签',
|
|
22
|
+
qcc_match_status: '匹配状态',
|
|
23
|
+
qcc_source: '数据来源',
|
|
24
|
+
qcc_error: '错误原因',
|
|
25
|
+
match_status: '匹配状态',
|
|
26
|
+
error: '错误原因',
|
|
27
|
+
_exception_reason: '异常原因',
|
|
28
|
+
});
|
|
29
|
+
|
|
18
30
|
export class ArtifactError extends Error {
|
|
19
31
|
constructor(code, message, status = 400) {
|
|
20
32
|
super(message);
|
|
@@ -44,10 +56,11 @@ function sha256(buffer) {
|
|
|
44
56
|
return createHash('sha256').update(buffer).digest('hex');
|
|
45
57
|
}
|
|
46
58
|
|
|
47
|
-
function normalizeHeaders(headers, rows) {
|
|
59
|
+
function normalizeHeaders(headers, rows, fieldSelection = []) {
|
|
48
60
|
const fromInput = Array.isArray(headers) ? headers.map((item) => String(item ?? '').trim()).filter(Boolean) : [];
|
|
61
|
+
const selected = normalizeFieldSelection(fieldSelection);
|
|
49
62
|
const discovered = [];
|
|
50
|
-
const seen = new Set(fromInput);
|
|
63
|
+
const seen = new Set([...fromInput, ...selected]);
|
|
51
64
|
for (const row of rows) {
|
|
52
65
|
for (const key of Object.keys(row ?? {})) {
|
|
53
66
|
if (!seen.has(key)) {
|
|
@@ -56,7 +69,27 @@ function normalizeHeaders(headers, rows) {
|
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
}
|
|
59
|
-
return [...fromInput, ...discovered].slice(0, 256);
|
|
72
|
+
return [...fromInput, ...selected.filter((field) => !fromInput.includes(field)), ...discovered].slice(0, 256);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function exportLabel(key) {
|
|
76
|
+
return FIELD_LABELS[key] ?? SYSTEM_FIELD_LABELS[key] ?? key;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function exportColumns(headers) {
|
|
80
|
+
const used = new Set();
|
|
81
|
+
return headers.map((key) => {
|
|
82
|
+
const base = exportLabel(key);
|
|
83
|
+
let label = base;
|
|
84
|
+
if (used.has(label)) {
|
|
85
|
+
const suffix = FIELD_LABELS[key] || SYSTEM_FIELD_LABELS[key] ? '(补全)' : '(原字段)';
|
|
86
|
+
label = `${base}${suffix}`;
|
|
87
|
+
let index = 2;
|
|
88
|
+
while (used.has(label)) label = `${base}${suffix.slice(0, -1)}${index++})`;
|
|
89
|
+
}
|
|
90
|
+
used.add(label);
|
|
91
|
+
return { key, label };
|
|
92
|
+
});
|
|
60
93
|
}
|
|
61
94
|
|
|
62
95
|
function normalizeRows(value) {
|
|
@@ -104,8 +137,13 @@ export function deriveExceptionRows(rows) {
|
|
|
104
137
|
|
|
105
138
|
function workbookBytes(rows, headers, sheetName) {
|
|
106
139
|
const workbook = XLSX.utils.book_new();
|
|
107
|
-
const
|
|
108
|
-
|
|
140
|
+
const columns = exportColumns(headers);
|
|
141
|
+
const values = [
|
|
142
|
+
columns.map((column) => column.label),
|
|
143
|
+
...rows.map((row) => columns.map((column) => row?.[column.key] ?? '')),
|
|
144
|
+
];
|
|
145
|
+
const worksheet = XLSX.utils.aoa_to_sheet(values);
|
|
146
|
+
worksheet['!cols'] = columns.map((column) => ({ wch: Math.min(42, Math.max(12, String(column.label).length * 2 + 4)) }));
|
|
109
147
|
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName.slice(0, 31));
|
|
110
148
|
workbook.Props = {
|
|
111
149
|
Title: sheetName,
|
|
@@ -129,9 +167,10 @@ function csvBytes(rows, headers) {
|
|
|
129
167
|
if (/^[\u0009\u000d\u000a ]*[=+\-@]/.test(text)) text = `'${text}`;
|
|
130
168
|
return /[",\r\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
|
|
131
169
|
};
|
|
170
|
+
const columns = exportColumns(headers);
|
|
132
171
|
const lines = [
|
|
133
|
-
|
|
134
|
-
...rows.map((row) =>
|
|
172
|
+
columns.map((column) => escapeCell(column.label)).join(','),
|
|
173
|
+
...rows.map((row) => columns.map((column) => escapeCell(row?.[column.key])).join(',')),
|
|
135
174
|
];
|
|
136
175
|
return Buffer.from(`\uFEFF${lines.join('\r\n')}`, 'utf8');
|
|
137
176
|
}
|
|
@@ -180,7 +219,7 @@ export class WorkflowArtifactStore {
|
|
|
180
219
|
async createBundle(taskId, input = {}) {
|
|
181
220
|
assertId(taskId, 'taskId');
|
|
182
221
|
const rows = normalizeRows(input.rows);
|
|
183
|
-
const headers = normalizeHeaders(input.headers, rows);
|
|
222
|
+
const headers = normalizeHeaders(input.headers, rows, input.fieldSelection);
|
|
184
223
|
if (!headers.length) throw new ArtifactError('DC_ARTIFACT_HEADERS_REQUIRED', 'At least one export column is required.', 400);
|
|
185
224
|
const exceptions = input.exceptionRows === undefined
|
|
186
225
|
? deriveExceptionRows(rows)
|
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);
|
|
@@ -2412,11 +2540,13 @@ window.__ModuleLoader__.load({
|
|
|
2412
2540
|
|
|
2413
2541
|
const estimateQcc = async () => {
|
|
2414
2542
|
const uniqueCompanies = new Set(runtime.rows.map((row) => String(row?.[nameField] ?? '').trim()).filter(Boolean)).size;
|
|
2543
|
+
const requiresProfile = fieldSelection.some((field) => ['company_profile', 'industry_large', 'industry_middle'].includes(field));
|
|
2544
|
+
const callsPerCompany = requiresProfile ? 3 : 2;
|
|
2415
2545
|
actions.setQccEstimate({
|
|
2416
2546
|
uniqueCompanies,
|
|
2417
|
-
tools: ['主体查询', '工商信息'],
|
|
2418
|
-
estimatedCalls: uniqueCompanies *
|
|
2419
|
-
maxCalls:
|
|
2547
|
+
tools: requiresProfile ? ['主体查询', '工商信息', '企业画像'] : ['主体查询', '工商信息'],
|
|
2548
|
+
estimatedCalls: uniqueCompanies * callsPerCompany,
|
|
2549
|
+
maxCalls: 300,
|
|
2420
2550
|
withinLimit: runtime.rows.length <= 100,
|
|
2421
2551
|
estimateType: 'upper-bound',
|
|
2422
2552
|
});
|
|
@@ -2489,26 +2619,55 @@ window.__ModuleLoader__.load({
|
|
|
2489
2619
|
return completedCommand.run;
|
|
2490
2620
|
};
|
|
2491
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
|
+
|
|
2492
2655
|
const runQcc = async () => {
|
|
2493
2656
|
if (busy || !qccEstimate || !paidConfirmed || !qccEstimate.withinLimit) return;
|
|
2494
2657
|
actions.setBusy(true);
|
|
2495
2658
|
actions.setError(null);
|
|
2496
2659
|
try {
|
|
2497
|
-
|
|
2498
|
-
const r = await executePreparedQccCommand({
|
|
2660
|
+
await prepareEditableQccCommand({
|
|
2499
2661
|
kind: 'enrich',
|
|
2500
|
-
taskId:
|
|
2662
|
+
taskId: cachedTask.id,
|
|
2501
2663
|
rows: runtime.rows,
|
|
2502
2664
|
headers: runtime.headers,
|
|
2503
2665
|
nameField,
|
|
2666
|
+
fieldSelection,
|
|
2504
2667
|
includeRisk: false,
|
|
2505
2668
|
concurrency: 2,
|
|
2506
2669
|
});
|
|
2507
|
-
await applyQccRun(r);
|
|
2508
2670
|
} 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
|
-
}
|
|
2512
2671
|
actions.setError(err instanceof Error ? err.message : String(err));
|
|
2513
2672
|
} finally {
|
|
2514
2673
|
actions.setBusy(false);
|
|
@@ -2883,8 +3042,9 @@ window.__ModuleLoader__.load({
|
|
|
2883
3042
|
h('input', { type: 'checkbox', checked: paidConfirmed, 'aria-label': '确认使用当前用户的企查查账号额度', onChange: (event) => actions.setPaidConfirmed(event.target.checked) }),
|
|
2884
3043
|
'我已核对企业数量和调用上界,并确认使用自己连接的企查查 MCP 账号;额度或费用由该账号自行承担',
|
|
2885
3044
|
),
|
|
3045
|
+
h('p', { className: 'dcAgentHint' }, '下一步只会把中文任务说明回填到中央对话框。你可先阅读、补充备注,点击发送后才开始企查查调用。'),
|
|
2886
3046
|
h('div', { className: 'dcAgentRow' },
|
|
2887
|
-
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 ? '生成中…' : '生成可编辑任务说明'),
|
|
2888
3048
|
),
|
|
2889
3049
|
) : null,
|
|
2890
3050
|
),
|
|
@@ -2899,10 +3059,10 @@ window.__ModuleLoader__.load({
|
|
|
2899
3059
|
Array.isArray(qccRun.rows) && qccRun.rows.length ? h('div', { className: 'dcAgentTableWrap' },
|
|
2900
3060
|
h('table', { className: 'dcAgentTable', 'aria-label': '匹配补全结果预览' },
|
|
2901
3061
|
h('thead', null, h('tr', null,
|
|
2902
|
-
|
|
3062
|
+
qccPreviewColumns.map((key) => h('th', { key }, resultFieldLabel(key))),
|
|
2903
3063
|
)),
|
|
2904
3064
|
h('tbody', null, qccRun.rows.slice(0, 8).map((row, index) => h('tr', { key: `${index}-${row?.qcc_credit_no || row?.credit_no || ''}` },
|
|
2905
|
-
|
|
3065
|
+
qccPreviewColumns.map((key) => h('td', { key }, String(row?.[key] ?? ''))),
|
|
2906
3066
|
))),
|
|
2907
3067
|
),
|
|
2908
3068
|
h('p', { className: 'dcAgentHint' }, `显示前 ${Math.min(8, qccRun.rows.length)} 行;完整结果在下载阶段生成耐久制品。`),
|
|
@@ -3044,26 +3204,29 @@ window.__ModuleLoader__.load({
|
|
|
3044
3204
|
h('small', null, cachedTask ? `${WORKFLOW_STATE_LABELS[cachedTask.state] || cachedTask.state} · ${cachedTask.id.slice(0, 18)}` : '正在创建 Host taskId…'),
|
|
3045
3205
|
),
|
|
3046
3206
|
),
|
|
3047
|
-
h('
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
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
|
+
),
|
|
3067
3230
|
),
|
|
3068
3231
|
h('nav', { className: 'dcAgentStepper', 'aria-label': '清洗流程' },
|
|
3069
3232
|
STEPS.map((st) => h('button', {
|
|
@@ -3111,6 +3274,7 @@ window.__ModuleLoader__.load({
|
|
|
3111
3274
|
store: workbenchStore,
|
|
3112
3275
|
inject: () => ({
|
|
3113
3276
|
sendSessionCommand: (sessionId, prompt) => sendQccAgentCommand(ctx, sessionId, prompt),
|
|
3277
|
+
setSessionDraft: (sessionId, prompt) => setQccAgentDraft(ctx, sessionId, prompt),
|
|
3114
3278
|
}),
|
|
3115
3279
|
}, WorkbenchDrawer));
|
|
3116
3280
|
|
|
@@ -3179,8 +3343,11 @@ window.__ModuleLoader__.load({
|
|
|
3179
3343
|
isKnownCleaningDraft,
|
|
3180
3344
|
markCleaningSession,
|
|
3181
3345
|
qualitySummaryFor,
|
|
3346
|
+
plainEntityListDataset,
|
|
3347
|
+
resultFieldLabel,
|
|
3182
3348
|
rewriteHeroChrome,
|
|
3183
3349
|
ensureWorkflowTask,
|
|
3350
|
+
ensureEditableWorkflowTask,
|
|
3184
3351
|
queueWorkflowOperation,
|
|
3185
3352
|
};
|
|
3186
3353
|
return module.exports;
|
package/lib/qcc-command.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
10
10
|
import { QccBridgeError } from './qcc.js';
|
|
11
|
+
import { fieldLabel, normalizeFieldSelection } from './workflow-contract.js';
|
|
11
12
|
|
|
12
13
|
export const TOOL_QCC_COMMAND = 'data_cleaning_qcc_run';
|
|
13
14
|
|
|
@@ -47,6 +48,7 @@ function normalizedInput(input = {}) {
|
|
|
47
48
|
rows: clone(rows),
|
|
48
49
|
headers: Array.isArray(input.headers) ? input.headers.map(String) : [],
|
|
49
50
|
nameField: String(input.nameField ?? 'name'),
|
|
51
|
+
fieldSelection: normalizeFieldSelection(input.fieldSelection),
|
|
50
52
|
includeRisk: input.includeRisk === true,
|
|
51
53
|
concurrency: Math.min(4, Math.max(1, Math.trunc(Number(input.concurrency ?? 2)))),
|
|
52
54
|
};
|
|
@@ -68,23 +70,34 @@ function normalizedInput(input = {}) {
|
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
export function serializeQccCommandPrompt(command) {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
: [];
|
|
77
87
|
return [
|
|
78
|
-
'
|
|
88
|
+
'请执行已在「数据清洗补全工作台」确认的企业数据任务。',
|
|
79
89
|
'',
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
'
|
|
90
|
+
`处理内容:${action}。`,
|
|
91
|
+
`任务编号:${command.taskId}。`,
|
|
92
|
+
`处理数量:${rowCount} 条企业记录。`,
|
|
93
|
+
...(fields.length ? [`需要补全:${fields.join('、')}。`] : []),
|
|
94
|
+
'执行边界:名单、字段和额度确认已安全暂存在本机 Host;使用当前用户自己连接的企查查 MCP 账号。',
|
|
84
95
|
'',
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
'
|
|
96
|
+
'你可以在发送前补充任务备注。如需修改名单、匹配规则或补全字段,请先返回工作台调整后重新生成。',
|
|
97
|
+
`安全任务凭证:${command.commandId}`,
|
|
98
|
+
'',
|
|
99
|
+
`发送本说明后,请仅调用一次数据清洗补全执行工具(${TOOL_QCC_COMMAND}),参数只传递上述安全任务凭证。`,
|
|
100
|
+
'不要要求重复粘贴企业名单,不要直接调用 mcp__qcc-* 工具,不要重试、扩大名单或追加字段。',
|
|
88
101
|
].join('\n');
|
|
89
102
|
}
|
|
90
103
|
|
|
@@ -201,6 +214,7 @@ export class QccCommandStore {
|
|
|
201
214
|
const result = await this.bridge.enrichRows(record.input.rows, {
|
|
202
215
|
nameField: record.input.nameField,
|
|
203
216
|
includeRisk: record.input.includeRisk,
|
|
217
|
+
fieldSelection: record.input.fieldSelection,
|
|
204
218
|
concurrency: record.input.concurrency,
|
|
205
219
|
maxRows: 100,
|
|
206
220
|
execution,
|
|
@@ -210,6 +224,7 @@ export class QccCommandStore {
|
|
|
210
224
|
headers: record.input.headers,
|
|
211
225
|
nameField: record.input.nameField,
|
|
212
226
|
includeRisk: record.input.includeRisk,
|
|
227
|
+
fieldSelection: record.input.fieldSelection,
|
|
213
228
|
concurrency: record.input.concurrency,
|
|
214
229
|
result,
|
|
215
230
|
audit,
|
|
@@ -230,7 +245,7 @@ export class QccCommandStore {
|
|
|
230
245
|
export function registerQccCommandTool(tools, commands) {
|
|
231
246
|
return tools.register({
|
|
232
247
|
name: TOOL_QCC_COMMAND,
|
|
233
|
-
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.',
|
|
234
249
|
parameters: {
|
|
235
250
|
type: 'object',
|
|
236
251
|
additionalProperties: false,
|
package/lib/qcc-runs.js
CHANGED
|
@@ -143,7 +143,7 @@ export class G5RunStore {
|
|
|
143
143
|
return { value: await entry.promise, replayed: false };
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
createRun({ headers, nameField, includeRisk, concurrency, result, audit = [] }) {
|
|
146
|
+
createRun({ headers, nameField, fieldSelection, includeRisk, concurrency, result, audit = [] }) {
|
|
147
147
|
this.cleanup();
|
|
148
148
|
const id = this.runIdFactory();
|
|
149
149
|
const at = this.nowIso();
|
|
@@ -156,6 +156,7 @@ export class G5RunStore {
|
|
|
156
156
|
touchedAtMs: this.clock(),
|
|
157
157
|
headers: Array.isArray(headers) ? headers.map(String) : [],
|
|
158
158
|
nameField: String(nameField ?? 'name'),
|
|
159
|
+
fieldSelection: Array.isArray(fieldSelection) ? fieldSelection.map(String) : [],
|
|
159
160
|
includeRisk: Boolean(includeRisk),
|
|
160
161
|
concurrency: Number(concurrency ?? 2),
|
|
161
162
|
summary: clone(result.summary),
|
|
@@ -196,6 +197,7 @@ export class G5RunStore {
|
|
|
196
197
|
createdAt: record.createdAt,
|
|
197
198
|
updatedAt: record.updatedAt,
|
|
198
199
|
headers: record.headers,
|
|
200
|
+
fieldSelection: record.fieldSelection,
|
|
199
201
|
summary: record.summary,
|
|
200
202
|
rows: record.rows,
|
|
201
203
|
reviewQueue: record.reviewQueue,
|
|
@@ -269,6 +271,7 @@ export class G5RunStore {
|
|
|
269
271
|
}, {
|
|
270
272
|
...options,
|
|
271
273
|
includeRisk: record.includeRisk,
|
|
274
|
+
fieldSelection: record.fieldSelection,
|
|
272
275
|
onAudit: (event) => this.appendAudit(record, event),
|
|
273
276
|
});
|
|
274
277
|
this.patchCompany(record, name, queued.rowIndexes, result);
|
|
@@ -303,6 +306,7 @@ export class G5RunStore {
|
|
|
303
306
|
result = await bridge.enrichCompany(item.companyName, {
|
|
304
307
|
...options,
|
|
305
308
|
includeRisk: record.includeRisk,
|
|
309
|
+
fieldSelection: record.fieldSelection,
|
|
306
310
|
onAudit: (event) => this.appendAudit(record, event),
|
|
307
311
|
});
|
|
308
312
|
} catch (error) {
|
package/lib/qcc.js
CHANGED
|
@@ -24,6 +24,7 @@ export const QCC_TOOL_NAMES = Object.freeze({
|
|
|
24
24
|
oauthStatus: 'qcc_oauth_status',
|
|
25
25
|
entityLookup: 'mcp__qcc-company__get_company_by_query',
|
|
26
26
|
registration: 'mcp__qcc-company__get_company_registration_info',
|
|
27
|
+
profile: 'mcp__qcc-company__get_company_profile',
|
|
27
28
|
riskScan: 'mcp__qcc-risk__get_company_risk_scan',
|
|
28
29
|
});
|
|
29
30
|
|
|
@@ -316,18 +317,98 @@ export function mapRegistrationFields(value, fallback = {}) {
|
|
|
316
317
|
if (value.无匹配项 !== undefined) {
|
|
317
318
|
throw new QccBridgeError('QCC_ENTITY_NOT_FOUND', 'Locked QCC entity could not be resolved by registration tool');
|
|
318
319
|
}
|
|
320
|
+
const text = (...keys) => {
|
|
321
|
+
for (const key of keys) {
|
|
322
|
+
const item = value[key];
|
|
323
|
+
if (item !== undefined && item !== null && String(item).trim()) return String(item);
|
|
324
|
+
}
|
|
325
|
+
return '';
|
|
326
|
+
};
|
|
327
|
+
const region = text('所属地区', '所属地域', '地区');
|
|
328
|
+
const location = splitAdministrativeRegion(region);
|
|
319
329
|
return {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
),
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
330
|
+
company_name: text('企业名称') || String(fallback.companyName ?? ''),
|
|
331
|
+
credit_no: text('统一社会信用代码', '信用代码') || String(fallback.creditNo ?? ''),
|
|
332
|
+
reg_no: text('工商注册号', '注册号'),
|
|
333
|
+
org_no: text('组织机构代码'),
|
|
334
|
+
reg_status: text('登记状态', '执业状态', '证书状态'),
|
|
335
|
+
legal_rep: text('法定代表人', '负责人', '经营者'),
|
|
336
|
+
reg_capital: text('注册资本', '注册资金', '开办资金', '成员出资总额', '资金数额'),
|
|
337
|
+
paid_capital: text('实缴资本'),
|
|
338
|
+
establish_date: text('成立日期'),
|
|
339
|
+
company_type: text('企业类型', '公司类型'),
|
|
340
|
+
registration_authority: text('登记机关'),
|
|
341
|
+
english_name: text('英文名', '英文名称'),
|
|
342
|
+
registered_address: text('注册地址', '住所', '经营场所'),
|
|
343
|
+
province: text('省份地区', '省份', '所属省份') || location.province,
|
|
344
|
+
city: text('城市', '所属城市', '市') || location.city,
|
|
345
|
+
district: text('区县', '所属区县') || location.district,
|
|
346
|
+
business_scope: text('经营范围'),
|
|
347
|
+
industry_category: text('国标行业'),
|
|
348
|
+
industry_large: text('一级行业', '企查查一级行业'),
|
|
349
|
+
industry_middle: text('二级行业', '企查查二级行业'),
|
|
350
|
+
operating_period: text('营业期限', '经营期限'),
|
|
351
|
+
company_size: text('企业规模', '人员规模'),
|
|
352
|
+
biz_status: text('经营状态'),
|
|
328
353
|
};
|
|
329
354
|
}
|
|
330
355
|
|
|
356
|
+
function splitAdministrativeRegion(value) {
|
|
357
|
+
const region = String(value ?? '').trim().replace(/\s+/g, '');
|
|
358
|
+
if (!region) return { province: '', city: '', district: '' };
|
|
359
|
+
const municipality = region.match(/^((?:北京|上海|天津|重庆)市)(.*)$/);
|
|
360
|
+
if (municipality) {
|
|
361
|
+
const district = municipality[2].match(/^(.+?(?:区|县))/)?.[1] ?? '';
|
|
362
|
+
return { province: municipality[1], city: municipality[1], district };
|
|
363
|
+
}
|
|
364
|
+
const provinceMatch = region.match(/^(.+?(?:省|自治区|特别行政区))/);
|
|
365
|
+
const province = provinceMatch?.[1] ?? '';
|
|
366
|
+
const remainder = province ? region.slice(province.length) : region;
|
|
367
|
+
const city = remainder.match(/^(.+?(?:市|自治州|地区|盟))/)?.[1] ?? '';
|
|
368
|
+
const districtRemainder = city ? remainder.slice(city.length) : remainder;
|
|
369
|
+
const district = districtRemainder.match(/^(.+?(?:区|县|旗|市))/)?.[1] ?? '';
|
|
370
|
+
return { province, city, district };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function mapProfileFields(value) {
|
|
374
|
+
if (!isRecord(value)) {
|
|
375
|
+
throw new QccBridgeError('QCC_CONTRACT_MISMATCH', 'QCC profile tool returned a non-object result');
|
|
376
|
+
}
|
|
377
|
+
if (value.无匹配项 !== undefined) return {};
|
|
378
|
+
return {
|
|
379
|
+
// “企查查行业”没有声明一/二级语义,不得猜测塞入层级列。
|
|
380
|
+
industry_large: String(value.一级行业 ?? value.企查查一级行业 ?? ''),
|
|
381
|
+
industry_middle: String(value.二级行业 ?? value.企查查二级行业 ?? ''),
|
|
382
|
+
company_profile: String(value.企业简介 ?? value.简介 ?? ''),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const LEGACY_ENRICHMENT_FIELDS = Object.freeze([
|
|
387
|
+
'credit_no', 'legal_rep', 'reg_capital', 'establish_date', 'reg_status', 'biz_status',
|
|
388
|
+
]);
|
|
389
|
+
const PROFILE_FIELDS = new Set(['industry_large', 'industry_middle', 'company_profile']);
|
|
390
|
+
|
|
391
|
+
function requiresProfile(fieldSelection) {
|
|
392
|
+
return Array.isArray(fieldSelection) && fieldSelection.some((field) => PROFILE_FIELDS.has(field));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function mergeMappedFields(...sources) {
|
|
396
|
+
const output = {};
|
|
397
|
+
for (const source of sources) {
|
|
398
|
+
for (const [key, value] of Object.entries(source ?? {})) {
|
|
399
|
+
if (!Object.hasOwn(output, key) || (value !== '' && value !== null && value !== undefined)) output[key] = value;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return output;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function projectSelectedFields(fields, fieldSelection) {
|
|
406
|
+
const selected = Array.isArray(fieldSelection) && fieldSelection.length
|
|
407
|
+
? [...new Set(fieldSelection.map(String))]
|
|
408
|
+
: LEGACY_ENRICHMENT_FIELDS;
|
|
409
|
+
return Object.fromEntries(selected.map((field) => [field, fields[field] ?? '']));
|
|
410
|
+
}
|
|
411
|
+
|
|
331
412
|
export function mapRiskTags(value) {
|
|
332
413
|
if (!isRecord(value)) return '';
|
|
333
414
|
const rows = Array.isArray(value.风险因子扫描) ? value.风险因子扫描 : [];
|
|
@@ -399,6 +480,7 @@ export class QccHostBridge {
|
|
|
399
480
|
oauthStatus: this.has(QCC_TOOL_NAMES.oauthStatus),
|
|
400
481
|
entityLookup: this.has(QCC_TOOL_NAMES.entityLookup),
|
|
401
482
|
registration: this.has(QCC_TOOL_NAMES.registration),
|
|
483
|
+
profile: this.has(QCC_TOOL_NAMES.profile),
|
|
402
484
|
riskScan: this.has(QCC_TOOL_NAMES.riskScan),
|
|
403
485
|
};
|
|
404
486
|
const ready = capabilities.entityLookup && capabilities.registration;
|
|
@@ -645,12 +727,15 @@ export class QccHostBridge {
|
|
|
645
727
|
{ searchKey: lockedKey },
|
|
646
728
|
options,
|
|
647
729
|
);
|
|
648
|
-
|
|
730
|
+
let mapped = mapRegistrationFields(registration.data, match);
|
|
731
|
+
if (requiresProfile(options.fieldSelection)) {
|
|
732
|
+
const profile = await this.call(QCC_TOOL_NAMES.profile, { searchKey: lockedKey }, options);
|
|
733
|
+
mapped = mergeMappedFields(mapped, mapProfileFields(profile.data));
|
|
734
|
+
}
|
|
735
|
+
const fields = projectSelectedFields(mapped, options.fieldSelection);
|
|
649
736
|
if (options.includeRisk) {
|
|
650
737
|
const risk = await this.call(QCC_TOOL_NAMES.riskScan, { searchKey: lockedKey }, options);
|
|
651
738
|
fields.risk_tags = mapRiskTags(risk.data);
|
|
652
|
-
} else {
|
|
653
|
-
fields.risk_tags = '';
|
|
654
739
|
}
|
|
655
740
|
return {
|
|
656
741
|
status: 'enriched',
|
|
@@ -670,12 +755,15 @@ export class QccHostBridge {
|
|
|
670
755
|
{ searchKey: creditNo },
|
|
671
756
|
options,
|
|
672
757
|
);
|
|
673
|
-
|
|
758
|
+
let mapped = mapRegistrationFields(registration.data, { companyName, creditNo });
|
|
759
|
+
if (requiresProfile(options.fieldSelection)) {
|
|
760
|
+
const profile = await this.call(QCC_TOOL_NAMES.profile, { searchKey: creditNo }, options);
|
|
761
|
+
mapped = mergeMappedFields(mapped, mapProfileFields(profile.data));
|
|
762
|
+
}
|
|
763
|
+
const fields = projectSelectedFields(mapped, options.fieldSelection);
|
|
674
764
|
if (options.includeRisk) {
|
|
675
765
|
const risk = await this.call(QCC_TOOL_NAMES.riskScan, { searchKey: creditNo }, options);
|
|
676
766
|
fields.risk_tags = mapRiskTags(risk.data);
|
|
677
|
-
} else {
|
|
678
|
-
fields.risk_tags = '';
|
|
679
767
|
}
|
|
680
768
|
return { status: 'enriched', companyName, fields };
|
|
681
769
|
}
|
|
@@ -690,6 +778,7 @@ export class QccHostBridge {
|
|
|
690
778
|
}
|
|
691
779
|
|
|
692
780
|
const requiredTools = [QCC_TOOL_NAMES.entityLookup, QCC_TOOL_NAMES.registration];
|
|
781
|
+
if (requiresProfile(options.fieldSelection)) requiredTools.push(QCC_TOOL_NAMES.profile);
|
|
693
782
|
if (options.includeRisk) requiredTools.push(QCC_TOOL_NAMES.riskScan);
|
|
694
783
|
try {
|
|
695
784
|
await Promise.all(requiredTools.map((name) => this.waitForTool(name, {
|
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/lib/web.js
CHANGED
|
@@ -460,6 +460,7 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
460
460
|
const artifacts = await artifactStore.createBundle(taskId, {
|
|
461
461
|
rows: payload.rows,
|
|
462
462
|
headers: payload.headers,
|
|
463
|
+
fieldSelection: readyTask.fieldSelection,
|
|
463
464
|
exceptionRows: payload.exceptionRows,
|
|
464
465
|
baseName: payload.baseName || readyTask.title,
|
|
465
466
|
});
|
package/lib/workflow-contract.js
CHANGED
|
@@ -117,6 +117,15 @@ export const FIELD_CATALOG = Object.freeze([
|
|
|
117
117
|
}),
|
|
118
118
|
]);
|
|
119
119
|
|
|
120
|
+
export const FIELD_LABELS = Object.freeze(Object.fromEntries(
|
|
121
|
+
FIELD_CATALOG.flatMap((group) => group.fields.map((field) => [field.id, field.label])),
|
|
122
|
+
));
|
|
123
|
+
|
|
124
|
+
export function fieldLabel(fieldId) {
|
|
125
|
+
const id = String(fieldId ?? '').trim();
|
|
126
|
+
return FIELD_LABELS[id] ?? id;
|
|
127
|
+
}
|
|
128
|
+
|
|
120
129
|
const FIELD_IDS = new Set(FIELD_CATALOG.flatMap((group) => group.fields.map((field) => field.id)));
|
|
121
130
|
const STAGE_IDS = new Set(WORKFLOW_STAGES.map((stage) => stage.id));
|
|
122
131
|
const STATE_IDS = new Set(WORKFLOW_STATES);
|
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",
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"docs/RELEASE-0.5.3.md",
|
|
34
34
|
"docs/RELEASE-0.6.0.md",
|
|
35
35
|
"docs/RELEASE-0.6.1.md",
|
|
36
|
+
"docs/RELEASE-0.6.2.md",
|
|
37
|
+
"docs/RELEASE-0.6.3.md",
|
|
36
38
|
"docs/UI-WORKFLOW-V2.md",
|
|
37
39
|
"docs/UI-WORKFLOW-V2-MIGRATION.md",
|
|
38
40
|
"docs/UI-WORKFLOW-V2-ACCEPTANCE.md",
|