dsh-data-cleaning-agent 0.6.1 → 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 +9 -0
- package/README.en.md +3 -1
- package/README.md +3 -2
- package/docs/RELEASE-0.6.1.md +17 -3
- package/docs/RELEASE-0.6.2.md +40 -0
- package/lib/artifacts.js +47 -8
- package/lib/client.js +6 -3
- package/lib/qcc-command.js +4 -0
- package/lib/qcc-runs.js +5 -1
- package/lib/qcc.js +103 -14
- package/lib/web.js +1 -0
- package/lib/workflow-contract.js +9 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.6.2] - 2026-09-04
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- 清洗补全结果 CSV/XLSX 的插件字段与状态字段统一导出为中文表头,并按任务字段选择保留空值列,
|
|
11
|
+
不再因企查查未返回某一字段而从制品中静默丢列。
|
|
12
|
+
- 将任务选择的补全字段贯穿中央工作台、Agent-owned 命令、G5 Host Bridge、任务恢复与耐久制品;
|
|
13
|
+
工商详情补充注册地址、行政区划、经营范围、行业、营业期限和企业规模映射,企业简介等画像字段按需
|
|
14
|
+
调用 `get_company_profile`,且不把未声明层级的“企查查行业”猜测为一级/二级行业。
|
|
15
|
+
|
|
7
16
|
## [0.6.1] - 2026-09-04
|
|
8
17
|
|
|
9
18
|
### 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.2** (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.2 release record](docs/RELEASE-0.6.2.md) for Chinese export headers,
|
|
121
|
+
complete field-selection propagation, and profile-field enrichment.
|
|
120
122
|
See [the 0.6.1 release record](docs/RELEASE-0.6.1.md) for the Agent-owned QCC workbench,
|
|
121
123
|
session isolation, recovered statistics, and real end-to-end validation.
|
|
122
124
|
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.2**(正式版本)
|
|
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.2 中文导出表头、完整字段选择传递与画像字段补全见
|
|
117
|
+
[docs/RELEASE-0.6.2.md](docs/RELEASE-0.6.2.md)。0.6.1 Agent-owned QCC 工作台、会话隔离、恢复统计及真实闭环验收见
|
|
117
118
|
[docs/RELEASE-0.6.1.md](docs/RELEASE-0.6.1.md)。0.6.0 五步 taskId 工作流、耐久制品、
|
|
118
119
|
跨重启恢复与双基线验收见 [docs/RELEASE-0.6.0.md](docs/RELEASE-0.6.0.md)。
|
|
119
120
|
|
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 不会被自动重写;升级后重新生成制品即可获得中文完整表头。
|
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
|
@@ -2412,11 +2412,13 @@ window.__ModuleLoader__.load({
|
|
|
2412
2412
|
|
|
2413
2413
|
const estimateQcc = async () => {
|
|
2414
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;
|
|
2415
2417
|
actions.setQccEstimate({
|
|
2416
2418
|
uniqueCompanies,
|
|
2417
|
-
tools: ['主体查询', '工商信息'],
|
|
2418
|
-
estimatedCalls: uniqueCompanies *
|
|
2419
|
-
maxCalls:
|
|
2419
|
+
tools: requiresProfile ? ['主体查询', '工商信息', '企业画像'] : ['主体查询', '工商信息'],
|
|
2420
|
+
estimatedCalls: uniqueCompanies * callsPerCompany,
|
|
2421
|
+
maxCalls: 300,
|
|
2420
2422
|
withinLimit: runtime.rows.length <= 100,
|
|
2421
2423
|
estimateType: 'upper-bound',
|
|
2422
2424
|
});
|
|
@@ -2501,6 +2503,7 @@ window.__ModuleLoader__.load({
|
|
|
2501
2503
|
rows: runtime.rows,
|
|
2502
2504
|
headers: runtime.headers,
|
|
2503
2505
|
nameField,
|
|
2506
|
+
fieldSelection,
|
|
2504
2507
|
includeRisk: false,
|
|
2505
2508
|
concurrency: 2,
|
|
2506
2509
|
});
|
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 { 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
|
};
|
|
@@ -201,6 +203,7 @@ export class QccCommandStore {
|
|
|
201
203
|
const result = await this.bridge.enrichRows(record.input.rows, {
|
|
202
204
|
nameField: record.input.nameField,
|
|
203
205
|
includeRisk: record.input.includeRisk,
|
|
206
|
+
fieldSelection: record.input.fieldSelection,
|
|
204
207
|
concurrency: record.input.concurrency,
|
|
205
208
|
maxRows: 100,
|
|
206
209
|
execution,
|
|
@@ -210,6 +213,7 @@ export class QccCommandStore {
|
|
|
210
213
|
headers: record.input.headers,
|
|
211
214
|
nameField: record.input.nameField,
|
|
212
215
|
includeRisk: record.input.includeRisk,
|
|
216
|
+
fieldSelection: record.input.fieldSelection,
|
|
213
217
|
concurrency: record.input.concurrency,
|
|
214
218
|
result,
|
|
215
219
|
audit,
|
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/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.2",
|
|
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,7 @@
|
|
|
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",
|
|
36
37
|
"docs/UI-WORKFLOW-V2.md",
|
|
37
38
|
"docs/UI-WORKFLOW-V2-MIGRATION.md",
|
|
38
39
|
"docs/UI-WORKFLOW-V2-ACCEPTANCE.md",
|