dsh-data-cleaning-agent 0.8.9 → 0.8.11
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 +19 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/docs/USER-GUIDE.md +2 -0
- package/lib/artifacts.js +9 -3
- package/lib/client.js +289 -41
- package/lib/engine.js +43 -2
- package/lib/qcc-runs.js +1 -1
- package/lib/qcc.js +1 -1
- package/lib/web.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录 `dsh-data-cleaning-agent` 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.8.11] - 2026-09-07
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- 补全结果按已确认映射填入原列空白单元格,保留非空值和列顺序;工作台预览、CSV/XLSX 和异常清单一致,不再为已映射列追加「(补全)」副列。首次调用及候选续跑均保护原表英文键的非空值。
|
|
11
|
+
- 字段映射覆盖当前 128 个可补全字段的标准中文名、英文键和明确别名;移除未知首列强制映射企业名称的兜底。
|
|
12
|
+
- 参照 AI 填表的推荐确认方式,为多义表头提供推荐、折叠分组选择和搜索;重复目标需人工选列,保留已有选择及输出字段范围。
|
|
13
|
+
- CSV/XLSX 重复表头生成唯一列键,防止法定代表人等同名列相互覆盖。旧任务已丢失的原值需重新导入原文件恢复。
|
|
14
|
+
|
|
15
|
+
## [0.8.10] - 2026-09-07
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- 依据共享 DSH-UX-001 v1.3.0 / UX-39,为自有右侧工作台补充左侧连续拖拽分隔条、方向键和 Home/End 调宽、双击复位、拖动取消、手动宽度恢复;中央会话按实际面板宽度同步让位。
|
|
19
|
+
- 按宿主会话区域实际可用宽度约束上下限,空间不足时全屏降级;关闭、切换或卸载恢复自有布局变量并清理监听,不改变 Host 任务、QCC 调用或发送机制。
|
|
20
|
+
|
|
21
|
+
### Tests
|
|
22
|
+
- 补充实际拖拽、取消、展开转手动、键盘调宽、关闭重开、宿主区域尺寸变化及让位清理回归;发布前通过 214 项检查测试与 10 组隔离 Chromium UI 回归。
|
|
23
|
+
|
|
5
24
|
## [0.8.9] - 2026-09-07
|
|
6
25
|
|
|
7
26
|
### Changed
|
package/README.en.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
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.8.
|
|
5
|
+
> Current source version / 当前源码版本: **0.8.11** (stable release)
|
|
6
6
|
|
|
7
|
-
This release
|
|
7
|
+
This release adds automatic mapping for all 128 fields using standard labels, field keys, and explicit aliases, with searchable grouped choices and confirmation for ambiguous or duplicate columns. Enrichment fills empty cells in mapped original columns and preserves nonempty values; selected fields without a mapped column are appended. Duplicate CSV/XLSX headers retain independent values. Preview and exports follow the same rules. QCC-blue menus, resizable workbench, and native DSH input/send behavior remain unchanged. Existing download artifacts are not rewritten.
|
|
8
8
|
|
|
9
9
|
[](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/workflows/ci.yml)
|
|
10
10
|
[](https://www.npmjs.com/package/dsh-data-cleaning-agent)
|
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> 在 DeepSeek Harness 中清洗、补全、画像企业名单数据的智能体插件:本地 CSV/XLSX/JSON 引擎 + 可选企查查(Qichacha/QCC)MCP 企业数据补全,由企查查(Qichacha/QCC)团队发起并维护。
|
|
4
4
|
>
|
|
5
|
-
> 当前源码版本 / Current source version: **0.8.
|
|
5
|
+
> 当前源码版本 / Current source version: **0.8.11**(正式版本)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
本版为全部 128 个字段加入标准名、英文键及明确别名自动映射,多义表头和重复列提供可确认推荐与分组搜索。补全结果优先写入已映射原列的空白单元格,保留非空原值;无对应原列的已选字段才新增列。CSV/XLSX 重复表头独立保留,预览与导出规则一致。保留企查查蓝菜单、可调宽工作台及 DSH 原生输入和发送机制。历史下载文件不自动改写。
|
|
8
8
|
|
|
9
9
|
[](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/workflows/ci.yml)
|
|
10
10
|
[](https://www.npmjs.com/package/dsh-data-cleaning-agent)
|
package/docs/USER-GUIDE.md
CHANGED
|
@@ -45,6 +45,8 @@ bash <(curl -fsSL https://raw.githubusercontent.com/duhu2000/dsh-data-cleaning-a
|
|
|
45
45
|
4. **清洗补全**:执行本地确定性清洗;需要 QCC 时先零调用估算,再由当前用户确认使用自己的账号额度;
|
|
46
46
|
5. **下载数据**:生成结果 CSV/XLSX 与异常清单 CSV/XLSX,后续可从任务历史恢复下载。
|
|
47
47
|
|
|
48
|
+
0.8.11 起的原列回填规则:规则确认时同时检查「字段映射」和「输出字段」。已选字段优先填入已映射原列的空白单元格,非空原值保持不变,原表没有对应映射的已选字段才新增列。地址、网址、开业时间等推荐需确认含义;重复列需明确选定一列。未返回的数据不编造。结果预览、XLSX/CSV 及异常清单遵循同一规则;历史已生成文件不会自动改写。
|
|
49
|
+
|
|
48
50
|
输入框左上角的「提示词生成」提供三种名单录入方式:
|
|
49
51
|
|
|
50
52
|
- **粘贴名单**:每行一个企业名称或统一社会信用代码;
|
package/lib/artifacts.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { createHash, randomUUID } from 'node:crypto';
|
|
9
9
|
import XLSX from 'xlsx';
|
|
10
10
|
import { FIELD_LABELS, normalizeFieldSelection } from './workflow-contract.js';
|
|
11
|
+
import { projectCompletionResult } from './engine.js';
|
|
11
12
|
|
|
12
13
|
const ROOT = '.dsh-data-cleaning-artifacts/v1';
|
|
13
14
|
const MAX_ARTIFACT_BYTES = 32 * 1024 * 1024;
|
|
@@ -218,12 +219,17 @@ export class WorkflowArtifactStore {
|
|
|
218
219
|
|
|
219
220
|
async createBundle(taskId, input = {}) {
|
|
220
221
|
assertId(taskId, 'taskId');
|
|
221
|
-
const
|
|
222
|
-
|
|
222
|
+
const projected = projectCompletionResult({
|
|
223
|
+
rows: normalizeRows(input.rows), headers: input.headers || [],
|
|
224
|
+
mappings: input.mappings || [], fieldSelection: normalizeFieldSelection(input.fieldSelection),
|
|
225
|
+
});
|
|
226
|
+
const rows = projected.rows;
|
|
227
|
+
const headers = normalizeHeaders(projected.headers, rows);
|
|
223
228
|
if (!headers.length) throw new ArtifactError('DC_ARTIFACT_HEADERS_REQUIRED', 'At least one export column is required.', 400);
|
|
224
229
|
const exceptions = input.exceptionRows === undefined
|
|
225
230
|
? deriveExceptionRows(rows)
|
|
226
|
-
: normalizeRows(input.exceptionRows)
|
|
231
|
+
: projectCompletionResult({ rows: normalizeRows(input.exceptionRows), headers: input.headers || [],
|
|
232
|
+
mappings: input.mappings || [], fieldSelection: normalizeFieldSelection(input.fieldSelection) }).rows;
|
|
227
233
|
const exceptionHeaders = normalizeHeaders([...headers, '_exception_reason'], exceptions);
|
|
228
234
|
const baseName = safeFilePart(input.baseName, '数据清洗补全结果');
|
|
229
235
|
const timestamp = this.nowFn();
|
package/lib/client.js
CHANGED
|
@@ -77,6 +77,123 @@ window.__ModuleLoader__.load({
|
|
|
77
77
|
'aria-hidden': true, focusable: false }, h('path', { d: STAGE_ICON_PATHS[kind] }));
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function workbenchWidthBounds(viewport, available = viewport) {
|
|
81
|
+
const mobile = viewport <= 760 || available < 740;
|
|
82
|
+
return { mobile, min: mobile ? viewport : 320, max: mobile ? viewport : Math.max(320, available - 420) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 自有抽屉的可逆兼容适配;不改 Better Sidebar 或其他插件的面板偏好。
|
|
86
|
+
function installWorkbenchSizing(drawer, { preferredWidth, expanded, onWidthChange }) {
|
|
87
|
+
const doc = drawer.ownerDocument;
|
|
88
|
+
const view = doc.defaultView;
|
|
89
|
+
const handle = drawer.querySelector('.dcAgentResizeHandle');
|
|
90
|
+
if (!handle || !view) return () => {};
|
|
91
|
+
const reservations = new Map();
|
|
92
|
+
let requested = expanded ? 980 : preferredWidth;
|
|
93
|
+
let current = 0;
|
|
94
|
+
let drag = null;
|
|
95
|
+
const boundsForView = () => workbenchWidthBounds(view.innerWidth,
|
|
96
|
+
doc.querySelector('[data-conversation-scroll]')?.getBoundingClientRect().width || view.innerWidth);
|
|
97
|
+
const reserve = () => {
|
|
98
|
+
const width = boundsForView().mobile ? 0 : drawer.getBoundingClientRect().width;
|
|
99
|
+
for (const el of doc.querySelectorAll('[data-conversation-scroll]')) {
|
|
100
|
+
if (!reservations.has(el)) {
|
|
101
|
+
reservations.set(el, {
|
|
102
|
+
value: el.style.getPropertyValue('--dc-agent-workbench-reserve'),
|
|
103
|
+
priority: el.style.getPropertyPriority('--dc-agent-workbench-reserve'),
|
|
104
|
+
});
|
|
105
|
+
sizeObserver?.observe(el, { box: 'border-box' });
|
|
106
|
+
}
|
|
107
|
+
if (el.style.getPropertyValue('--dc-agent-workbench-reserve') !== `${width}px`) {
|
|
108
|
+
el.style.setProperty('--dc-agent-workbench-reserve', `${width}px`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const apply = (value) => {
|
|
113
|
+
const bounds = boundsForView();
|
|
114
|
+
const fallback = view.innerWidth <= 1040 ? Math.min(420, view.innerWidth * .48) : Math.min(460, view.innerWidth * .42);
|
|
115
|
+
current = Math.round(Math.min(bounds.max, Math.max(bounds.min, Number.isFinite(value) ? value : fallback)));
|
|
116
|
+
drawer.dataset.narrow = String(bounds.mobile);
|
|
117
|
+
drawer.style.setProperty('--dc-agent-panel-width', `${current}px`);
|
|
118
|
+
handle.setAttribute('aria-valuemin', String(bounds.min));
|
|
119
|
+
handle.setAttribute('aria-valuemax', String(bounds.max));
|
|
120
|
+
handle.setAttribute('aria-valuenow', String(current));
|
|
121
|
+
handle.setAttribute('aria-valuetext', `${current} 像素`);
|
|
122
|
+
reserve();
|
|
123
|
+
};
|
|
124
|
+
const end = (cancel = false) => {
|
|
125
|
+
if (!drag) return;
|
|
126
|
+
const previous = drag;
|
|
127
|
+
drag = null;
|
|
128
|
+
drawer.classList.remove('is-resizing');
|
|
129
|
+
if (handle.hasPointerCapture?.(previous.id)) handle.releasePointerCapture(previous.id);
|
|
130
|
+
if (cancel) apply(requested);
|
|
131
|
+
else { requested = current; onWidthChange(current); }
|
|
132
|
+
};
|
|
133
|
+
const down = (event) => {
|
|
134
|
+
if (event.button !== 0 || drag || boundsForView().mobile) return;
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
handle.focus();
|
|
137
|
+
drag = { id: event.pointerId, startX: event.clientX, startWidth: current };
|
|
138
|
+
handle.setPointerCapture(event.pointerId);
|
|
139
|
+
drawer.classList.add('is-resizing');
|
|
140
|
+
};
|
|
141
|
+
const move = (event) => {
|
|
142
|
+
if (drag?.id !== event.pointerId) return;
|
|
143
|
+
apply(drag.startWidth + drag.startX - event.clientX);
|
|
144
|
+
};
|
|
145
|
+
const up = (event) => { if (drag?.id === event.pointerId) end(); };
|
|
146
|
+
const cancel = () => end(true);
|
|
147
|
+
const keydown = (event) => {
|
|
148
|
+
if (event.key === 'Escape' && drag) {
|
|
149
|
+
event.preventDefault(); event.stopPropagation(); cancel(); return;
|
|
150
|
+
}
|
|
151
|
+
if (drag || boundsForView().mobile) return;
|
|
152
|
+
const bounds = boundsForView();
|
|
153
|
+
const delta = event.shiftKey ? 48 : 16;
|
|
154
|
+
const value = { ArrowLeft: current + delta, ArrowRight: current - delta, Home: bounds.min, End: bounds.max }[event.key];
|
|
155
|
+
if (value === undefined) return;
|
|
156
|
+
event.preventDefault(); event.stopPropagation();
|
|
157
|
+
requested = value; apply(value); onWidthChange(current);
|
|
158
|
+
};
|
|
159
|
+
const reset = () => { cancel(); requested = null; apply(null); onWidthChange(null); };
|
|
160
|
+
const resize = () => { cancel(); apply(requested); };
|
|
161
|
+
const sizeObserver = typeof view.ResizeObserver === 'function' ? new view.ResizeObserver(resize) : null;
|
|
162
|
+
handle.addEventListener('pointerdown', down);
|
|
163
|
+
handle.addEventListener('pointermove', move);
|
|
164
|
+
handle.addEventListener('pointerup', up);
|
|
165
|
+
handle.addEventListener('pointercancel', cancel);
|
|
166
|
+
handle.addEventListener('lostpointercapture', cancel);
|
|
167
|
+
handle.addEventListener('keydown', keydown);
|
|
168
|
+
handle.addEventListener('dblclick', reset);
|
|
169
|
+
view.addEventListener('resize', resize);
|
|
170
|
+
view.addEventListener('blur', cancel);
|
|
171
|
+
// 会话挂载点可被宿主替换;只对当前存在的滚动区设置自有让位变量。
|
|
172
|
+
const observer = typeof view.MutationObserver === 'function' ? new view.MutationObserver(reserve) : null;
|
|
173
|
+
observer?.observe(doc.body, { childList: true, subtree: true });
|
|
174
|
+
apply(requested);
|
|
175
|
+
return () => {
|
|
176
|
+
cancel();
|
|
177
|
+
observer?.disconnect();
|
|
178
|
+
sizeObserver?.disconnect();
|
|
179
|
+
handle.removeEventListener('pointerdown', down);
|
|
180
|
+
handle.removeEventListener('pointermove', move);
|
|
181
|
+
handle.removeEventListener('pointerup', up);
|
|
182
|
+
handle.removeEventListener('pointercancel', cancel);
|
|
183
|
+
handle.removeEventListener('lostpointercapture', cancel);
|
|
184
|
+
handle.removeEventListener('keydown', keydown);
|
|
185
|
+
handle.removeEventListener('dblclick', reset);
|
|
186
|
+
view.removeEventListener('resize', resize);
|
|
187
|
+
view.removeEventListener('blur', cancel);
|
|
188
|
+
drawer.style.removeProperty('--dc-agent-panel-width');
|
|
189
|
+
delete drawer.dataset.narrow;
|
|
190
|
+
for (const [el, previous] of reservations) {
|
|
191
|
+
if (previous.value) el.style.setProperty('--dc-agent-workbench-reserve', previous.value, previous.priority);
|
|
192
|
+
else el.style.removeProperty('--dc-agent-workbench-reserve');
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
80
197
|
// 搜索仅改变可见字段,绝不清除已选字段或改变 Host 的字段目录。
|
|
81
198
|
function visibleCatalogFields(group, query) {
|
|
82
199
|
const needle = String(query || '').trim().toLowerCase();
|
|
@@ -166,12 +283,13 @@ window.__ModuleLoader__.load({
|
|
|
166
283
|
}
|
|
167
284
|
|
|
168
285
|
.dcAgentWorkbench {
|
|
286
|
+
position: relative;
|
|
169
287
|
box-sizing: border-box;
|
|
170
288
|
display: flex;
|
|
171
289
|
flex-direction: column;
|
|
172
|
-
width: min(460px, 42vw);
|
|
290
|
+
width: var(--dc-agent-panel-width, min(460px, 42vw));
|
|
173
291
|
max-width: calc(100vw - 420px);
|
|
174
|
-
min-width:
|
|
292
|
+
min-width: 320px;
|
|
175
293
|
height: 100vh;
|
|
176
294
|
border: 1px solid var(--dc-border);
|
|
177
295
|
border-radius: 12px 0 0 12px;
|
|
@@ -181,7 +299,14 @@ window.__ModuleLoader__.load({
|
|
|
181
299
|
overflow: hidden;
|
|
182
300
|
pointer-events: auto;
|
|
183
301
|
}
|
|
184
|
-
.dcAgentWorkbench.is-expanded { width: min(980px, calc(100vw -
|
|
302
|
+
.dcAgentWorkbench.is-expanded { width: var(--dc-agent-panel-width, min(980px, calc(100vw - 420px))); }
|
|
303
|
+
.dcAgentResizeHandle { position: absolute; left: 0; top: 0; bottom: 0; width: 8px; z-index: 5; cursor: col-resize; touch-action: none; outline: none; }
|
|
304
|
+
.dcAgentResizeHandle::after { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: transparent; }
|
|
305
|
+
.dcAgentResizeHandle:hover::after, .dcAgentResizeHandle:focus-visible::after, .is-resizing .dcAgentResizeHandle::after { background: var(--dc-action); }
|
|
306
|
+
.dcAgentWorkbench.is-resizing { user-select: none; }
|
|
307
|
+
.dcAgentWorkbench[data-narrow="true"] { width: 100vw; max-width: none; min-width: 0; border-radius: 0; }
|
|
308
|
+
.dcAgentWorkbench[data-narrow="true"] .dcAgentResizeHandle { display: none; }
|
|
309
|
+
.dcAgentOverlay:has(.dcAgentWorkbench[data-narrow="true"]) { background: rgba(8, 10, 14, 0.22); pointer-events: auto; }
|
|
185
310
|
|
|
186
311
|
.dcAgentCapabilities {
|
|
187
312
|
display: flex;
|
|
@@ -819,6 +944,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
819
944
|
.dcAgentMappingRow { display: grid; grid-template-columns: minmax(0, 1fr) 24px minmax(180px, 1fr); align-items: center; gap: 8px; margin-top: 7px; font-size: 12px; }
|
|
820
945
|
.dcAgentMappingRow > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
821
946
|
.dcAgentMappingRow > b { text-align: center; color: light-dark(#8792a3, #8290a5); }
|
|
947
|
+
.dcAgentMappingPicker summary { cursor: pointer; overflow-wrap: anywhere; }
|
|
948
|
+
.dcAgentMappingPicker[open] { padding: 8px; border: 1px solid var(--dc-border); border-radius: 8px; }
|
|
949
|
+
.dcAgentMappingPicker input, .dcAgentMappingPicker select { width: 100%; margin-top: 8px; max-width: 100%; min-width: 0; }
|
|
950
|
+
.dcAgentMappingRow .dcAgentButton { max-width: 100%; white-space: normal; overflow-wrap: anywhere; }
|
|
951
|
+
.dcAgentMappingRow small { display: block; margin: 4px 0; overflow-wrap: anywhere; }
|
|
822
952
|
.dcAgentRulesGrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
|
823
953
|
.dcAgentFieldGroup { display: grid; gap: 7px; margin-top: 12px; }
|
|
824
954
|
.dcAgentFieldGroupHead { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
|
|
@@ -847,24 +977,25 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
847
977
|
@media (min-width: 1041px) {
|
|
848
978
|
body:has(.dcAgentWorkbench) [data-conversation-scroll] {
|
|
849
979
|
box-sizing: border-box;
|
|
850
|
-
padding-right: min(460px, 42vw);
|
|
980
|
+
padding-right: var(--dc-agent-workbench-reserve, min(460px, 42vw));
|
|
851
981
|
}
|
|
852
982
|
}
|
|
853
983
|
@media (min-width: 761px) and (max-width: 1040px) {
|
|
854
984
|
body:has(.dcAgentWorkbench) [data-conversation-scroll] {
|
|
855
985
|
box-sizing: border-box;
|
|
856
|
-
padding-right: min(420px, 48vw);
|
|
986
|
+
padding-right: var(--dc-agent-workbench-reserve, min(420px, 48vw));
|
|
857
987
|
}
|
|
858
988
|
}
|
|
859
989
|
@media (min-width: 761px) {
|
|
860
990
|
body:has(.dcAgentWorkbench.is-expanded) [data-conversation-scroll] {
|
|
861
991
|
box-sizing: border-box;
|
|
862
|
-
padding-right: min(980px, calc(100vw - 420px));
|
|
992
|
+
padding-right: var(--dc-agent-workbench-reserve, min(980px, calc(100vw - 420px)));
|
|
863
993
|
}
|
|
864
994
|
}
|
|
865
995
|
@media (max-width: 760px) {
|
|
866
996
|
.dcAgentWorkbench, .dcAgentWorkbench.is-expanded { width: 100vw; max-width: none; min-width: 0; border-radius: 0; }
|
|
867
997
|
.dcAgentOverlay { background: rgba(8, 10, 14, 0.22); pointer-events: auto; }
|
|
998
|
+
.dcAgentResizeHandle { display: none; }
|
|
868
999
|
.dcAgentQccBadge, .dcAgentJobsPill { display: none; }
|
|
869
1000
|
.dcAgentCandidate { grid-template-columns: 1fr; }
|
|
870
1001
|
.dcAgentCapabilities { justify-content: flex-start; padding-inline: 12px; }
|
|
@@ -877,7 +1008,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
877
1008
|
.dcAgentMappingRow > b { display: none; }
|
|
878
1009
|
}
|
|
879
1010
|
@media (min-width: 761px) and (max-width: 1040px) {
|
|
880
|
-
.dcAgentWorkbench { width:
|
|
1011
|
+
.dcAgentWorkbench { width: var(--dc-agent-panel-width, 420px); }
|
|
881
1012
|
.dcAgentQccBadge, .dcAgentJobsPill { display: none; }
|
|
882
1013
|
}
|
|
883
1014
|
@media (max-width: 360px) {
|
|
@@ -1302,30 +1433,128 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1302
1433
|
return RESULT_FIELD_LABELS.get(String(key ?? '')) ?? String(key ?? '');
|
|
1303
1434
|
}
|
|
1304
1435
|
|
|
1305
|
-
const
|
|
1306
|
-
['
|
|
1307
|
-
['
|
|
1308
|
-
['
|
|
1309
|
-
['
|
|
1310
|
-
['
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1436
|
+
const MAPPING_ALIASES = {
|
|
1437
|
+
company_name: ['name', 'company', '公司名称', '单位名称', '企业全称', '公司全称', '主体标识'],
|
|
1438
|
+
credit_no: ['creditCode', 'unified_credit_code', '信用代码', '社会信用代码', '统一信用代码', 'USCC'],
|
|
1439
|
+
reg_no: ['registration_no', '工商注册号'],
|
|
1440
|
+
legal_rep: ['法定代表', '法人代表', 'legalRepresentative', 'legal_person'],
|
|
1441
|
+
reg_capital: ['注册资金'], establish_date: ['成立时间', '企业成立日期', 'establishmentDate'],
|
|
1442
|
+
reg_status: ['工商登记状态'], registered_address: ['企业注册地址', '公司注册地址'],
|
|
1443
|
+
phone: ['mobile', 'tel', 'telephone', '电话', '手机号码', '手机号'],
|
|
1444
|
+
contact_official_website: ['官网', '企业官网', '官方网站地址'],
|
|
1445
|
+
contact_preferred_email: ['首选电子邮箱'],
|
|
1446
|
+
};
|
|
1447
|
+
// 语义不完全等同:只推荐,绝不冒充精确匹配或发起调用。
|
|
1448
|
+
const MAPPING_HINTS = {
|
|
1449
|
+
地址: ['registered_address', 'mailing_address', 'invoice_address'],
|
|
1450
|
+
address: ['registered_address', 'mailing_address'],
|
|
1451
|
+
企业地址: ['registered_address', 'mailing_address'], 公司地址: ['registered_address', 'mailing_address'],
|
|
1452
|
+
网址: ['contact_official_website'], website: ['contact_official_website'],
|
|
1453
|
+
开业时间: ['establish_date'], 开业日期: ['establish_date'],
|
|
1454
|
+
行业: ['industry_category', 'qcc_industry'],
|
|
1455
|
+
法人: ['legal_rep'], 邮箱: ['contact_preferred_email'], email: ['contact_preferred_email'],
|
|
1456
|
+
};
|
|
1457
|
+
function normalizeMappingLabel(value) {
|
|
1458
|
+
return String(value ?? '').normalize('NFKC').toLowerCase().replace(/[\s_\-·]/g, '');
|
|
1459
|
+
}
|
|
1460
|
+
function mappingRecommendations(headers, fields = [...ENRICHMENT_OPTIONS, ...INPUT_ONLY_MAPPING_OPTIONS]) {
|
|
1461
|
+
const rows = (Array.isArray(headers) ? headers : []).map((sourceField) => {
|
|
1462
|
+
const label = normalizeMappingLabel(String(sourceField).replace(/(重复列 \d+)$/, ''));
|
|
1463
|
+
let candidates = fields.filter(([id, name]) => [id, name].some((value) => normalizeMappingLabel(value) === label));
|
|
1464
|
+
let reason = '标准字段名匹配';
|
|
1465
|
+
if (!candidates.length) {
|
|
1466
|
+
candidates = fields.filter(([id]) => (MAPPING_ALIASES[id] || []).some((value) => normalizeMappingLabel(value) === label));
|
|
1467
|
+
reason = '常用别名匹配';
|
|
1468
|
+
}
|
|
1469
|
+
const exact = candidates.length === 1;
|
|
1470
|
+
if (!candidates.length) {
|
|
1471
|
+
const hints = Object.entries(MAPPING_HINTS).find(([key]) => normalizeMappingLabel(key) === label)?.[1] || [];
|
|
1472
|
+
candidates = hints.flatMap((key) => fields.filter(([id]) => id === key));
|
|
1473
|
+
reason = candidates.length ? '含义需确认,点击推荐后采用' : '未识别表头,请手动选择';
|
|
1474
|
+
}
|
|
1475
|
+
return { sourceField: String(sourceField), candidates, automatic: exact, reason };
|
|
1476
|
+
});
|
|
1477
|
+
for (const row of rows) {
|
|
1478
|
+
if (row.automatic && rows.filter((other) => other.automatic && other.candidates[0][0] === row.candidates[0][0]).length > 1) {
|
|
1479
|
+
row.conflict = true;
|
|
1480
|
+
row.reason = '多列推荐同一字段,请选择其中一列;其他列保留原值';
|
|
1321
1481
|
}
|
|
1322
1482
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1483
|
+
return rows.map((row) => ({ ...row, automatic: row.automatic && !row.conflict }));
|
|
1484
|
+
}
|
|
1485
|
+
function guessMappings(headers, fields) {
|
|
1486
|
+
return mappingRecommendations(headers, fields).filter((row) => row.automatic)
|
|
1487
|
+
.map((row) => ({ sourceField: row.sourceField, targetField: row.candidates[0][0] }));
|
|
1488
|
+
}
|
|
1489
|
+
function mergeRecommendedMappings(headers, mappings, fields) {
|
|
1490
|
+
const output = [...mappings];
|
|
1491
|
+
for (const item of guessMappings(headers, fields)) {
|
|
1492
|
+
if (!output.some((current) => current.sourceField === item.sourceField || current.targetField === item.targetField)) output.push(item);
|
|
1325
1493
|
}
|
|
1326
1494
|
return output;
|
|
1327
1495
|
}
|
|
1328
1496
|
|
|
1497
|
+
function MappingPicker({ sourceField, mappings, groups, recommendation, onChange }) {
|
|
1498
|
+
const [query, setQuery] = react.useState('');
|
|
1499
|
+
const value = mappings.find((item) => item.sourceField === sourceField)?.targetField || '';
|
|
1500
|
+
const occupied = (id) => mappings.some((item) => item.sourceField !== sourceField && item.targetField === id);
|
|
1501
|
+
const fields = groups.flatMap(([, , items]) => items);
|
|
1502
|
+
const label = fields.find(([id]) => id === value)?.[1] || '不参与匹配 / 补全';
|
|
1503
|
+
const matches = (id, name) => normalizeMappingLabel([id, name, ...(MAPPING_ALIASES[id] || [])].join(' ')).includes(normalizeMappingLabel(query));
|
|
1504
|
+
return h('div', { className: 'dcAgentMappingRow' },
|
|
1505
|
+
h('span', { title: sourceField }, sourceField), h('b', { 'aria-hidden': 'true' }, '→'),
|
|
1506
|
+
h('div', { style: { minWidth: 0 } },
|
|
1507
|
+
h('details', { className: 'dcAgentMappingPicker', onKeyDown: (event) => {
|
|
1508
|
+
if (event.key === 'Escape' && event.currentTarget.open) {
|
|
1509
|
+
event.preventDefault(); event.stopPropagation();
|
|
1510
|
+
event.currentTarget.open = false; event.currentTarget.querySelector('summary')?.focus();
|
|
1511
|
+
}
|
|
1512
|
+
} },
|
|
1513
|
+
h('summary', { className: 'dcAgentField', 'aria-label': `${sourceField} 当前映射` }, label),
|
|
1514
|
+
h('input', { className: 'dcAgentField', value: query, placeholder: '搜索字段名称 / 英文键 / 别名', 'aria-label': `${sourceField} 搜索映射字段`, onChange: (event) => setQuery(event.target.value) }),
|
|
1515
|
+
h('select', { className: 'dcAgentField', value, 'aria-label': `${sourceField} 字段映射`, onChange: (event) => {
|
|
1516
|
+
onChange(sourceField, event.target.value);
|
|
1517
|
+
const details = event.currentTarget.closest('details');
|
|
1518
|
+
if (details) { details.open = false; details.querySelector('summary')?.focus(); }
|
|
1519
|
+
} },
|
|
1520
|
+
h('option', { value: '' }, '不参与匹配 / 补全'),
|
|
1521
|
+
groups.map(([id, title, items]) => h('optgroup', { key: id, label: title }, items.filter(([key, name]) => key === value || matches(key, name)).map(([key, name]) =>
|
|
1522
|
+
h('option', { key, value: key, disabled: occupied(key) }, `${name}${occupied(key) ? '(已被其他列使用)' : ''}`)))),
|
|
1523
|
+
),
|
|
1524
|
+
query && !fields.some(([id, name]) => matches(id, name)) ? h('p', { className: 'dcAgentHint', role: 'status' }, '无匹配字段;当前选择保持不变。') : null,
|
|
1525
|
+
),
|
|
1526
|
+
h('small', { className: 'dcAgentHint' }, value ? `已映射:${label};可手动调整` : recommendation.reason),
|
|
1527
|
+
!value && recommendation.candidates.length ? h('div', { className: 'dcAgentRow', 'aria-label': `${sourceField} 推荐映射` }, recommendation.candidates.map(([id, name]) =>
|
|
1528
|
+
h('button', { key: id, type: 'button', className: 'dcAgentButton', disabled: occupied(id), onClick: () => onChange(sourceField, id) }, `推荐:${name}${occupied(id) ? '(已占用)' : ''}`))) : null,
|
|
1529
|
+
),
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function projectCompletionResult({ rows = [], headers = [], mappings = [], fieldSelection = [] } = {}) {
|
|
1534
|
+
const sourceHeaders = [...new Set(headers)];
|
|
1535
|
+
const selected = [...new Set(fieldSelection)];
|
|
1536
|
+
const pairs = mappings.filter((item) => sourceHeaders.includes(item.sourceField) && selected.includes(item.targetField)
|
|
1537
|
+
&& mappings.filter((other) => other.sourceField === item.sourceField).length === 1
|
|
1538
|
+
&& mappings.filter((other) => other.targetField === item.targetField).length === 1);
|
|
1539
|
+
const hidden = new Set(pairs.filter((item) => item.sourceField !== item.targetField && !sourceHeaders.includes(item.targetField)).map((item) => item.targetField));
|
|
1540
|
+
const blank = (value) => value == null || (typeof value === 'string' && value.trim() === '');
|
|
1541
|
+
const output = rows.map((row) => {
|
|
1542
|
+
const next = { ...row };
|
|
1543
|
+
const status = row.qcc_match_status;
|
|
1544
|
+
if (!status || ['enriched', 'exact'].includes(status)) {
|
|
1545
|
+
for (const { sourceField, targetField } of pairs) {
|
|
1546
|
+
if (blank(row[sourceField]) && !blank(row[targetField])) next[sourceField] = row[targetField];
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
for (const key of hidden) delete next[key];
|
|
1550
|
+
return next;
|
|
1551
|
+
});
|
|
1552
|
+
return {
|
|
1553
|
+
rows: output,
|
|
1554
|
+
headers: [...new Set([...sourceHeaders, ...selected, ...output.flatMap((row) => Object.keys(row))])].filter((key) => !hidden.has(key)),
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1329
1558
|
function qualitySummaryFor(rows, mappings) {
|
|
1330
1559
|
const list = Array.isArray(rows) ? rows : [];
|
|
1331
1560
|
const anchors = (Array.isArray(mappings) ? mappings : []).filter((mapping) => ['company_name', 'credit_no', 'reg_no'].includes(mapping.targetField));
|
|
@@ -1487,6 +1716,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1487
1716
|
init: () => ({
|
|
1488
1717
|
open: false,
|
|
1489
1718
|
expanded: false,
|
|
1719
|
+
preferredWidth: null,
|
|
1490
1720
|
step: 'upload',
|
|
1491
1721
|
busy: false,
|
|
1492
1722
|
error: null,
|
|
@@ -1521,6 +1751,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1521
1751
|
},
|
|
1522
1752
|
close: (draft) => { draft.open = false; },
|
|
1523
1753
|
toggleExpanded: (draft) => { draft.expanded = !draft.expanded; },
|
|
1754
|
+
setPreferredWidth: (draft, width) => {
|
|
1755
|
+
draft.preferredWidth = Number.isFinite(width) ? width : null;
|
|
1756
|
+
draft.expanded = false;
|
|
1757
|
+
},
|
|
1524
1758
|
setStep: (draft, step) => { draft.step = step; },
|
|
1525
1759
|
setActiveSession: (draft, sessionId) => { draft.activeSessionId = sessionId ?? null; },
|
|
1526
1760
|
setBusy: (draft, busy) => { draft.busy = busy; },
|
|
@@ -1532,7 +1766,8 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1532
1766
|
setTaskTitle: (draft, value) => { draft.taskTitle = String(value ?? ''); },
|
|
1533
1767
|
setMappings: (draft, value) => { draft.mappings = Array.isArray(value) ? value : []; },
|
|
1534
1768
|
setMapping: (draft, sourceField, targetField) => {
|
|
1535
|
-
|
|
1769
|
+
if (targetField && draft.mappings.some((item) => item.sourceField !== sourceField && item.targetField === targetField)) return;
|
|
1770
|
+
const mappings = draft.mappings.filter((item) => item.sourceField !== sourceField);
|
|
1536
1771
|
if (targetField) mappings.push({ sourceField, targetField });
|
|
1537
1772
|
draft.mappings = mappings;
|
|
1538
1773
|
},
|
|
@@ -2968,6 +3203,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2968
3203
|
const open = useStore((state) => state.open);
|
|
2969
3204
|
const step = useStore((state) => state.step);
|
|
2970
3205
|
const expanded = useStore((state) => state.expanded);
|
|
3206
|
+
const preferredWidth = useStore((state) => state.preferredWidth);
|
|
2971
3207
|
const busy = useStore((state) => state.busy);
|
|
2972
3208
|
const error = useStore((state) => state.error);
|
|
2973
3209
|
const input = useStore((state) => state.input);
|
|
@@ -2993,6 +3229,13 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2993
3229
|
const activeSessionId = useStore((state) => state.activeSessionId);
|
|
2994
3230
|
const [fieldQuery, setFieldQuery] = react.useState('');
|
|
2995
3231
|
|
|
3232
|
+
react.useEffect(() => {
|
|
3233
|
+
if (!open || typeof document === 'undefined') return undefined;
|
|
3234
|
+
const drawer = document.querySelector('.dcAgentWorkbench');
|
|
3235
|
+
if (!drawer) return undefined;
|
|
3236
|
+
return installWorkbenchSizing(drawer, { preferredWidth, expanded, onWidthChange: actions.setPreferredWidth });
|
|
3237
|
+
}, [open, expanded, preferredWidth, actions]);
|
|
3238
|
+
|
|
2996
3239
|
react.useEffect(() => {
|
|
2997
3240
|
rootWorkbenchActions = actions;
|
|
2998
3241
|
const handleOpen = (event) => {
|
|
@@ -3104,7 +3347,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3104
3347
|
view?.removeEventListener?.('resize', align);
|
|
3105
3348
|
reset();
|
|
3106
3349
|
};
|
|
3107
|
-
}, [open, expanded]);
|
|
3350
|
+
}, [open, expanded, preferredWidth]);
|
|
3108
3351
|
|
|
3109
3352
|
// DSH 的 useStore 基于 React hooks;所有订阅必须在每次渲染时以相同顺序调用。
|
|
3110
3353
|
// 把关闭态 guard 放在全部 useStore 之后,避免首次关闭、随后打开时触发
|
|
@@ -3653,6 +3896,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3653
3896
|
? workflowContract.inputOnlyMappingFields.map((field) => [field.id, field.label])
|
|
3654
3897
|
: INPUT_ONLY_MAPPING_OPTIONS;
|
|
3655
3898
|
const targetFields = [...uiFieldGroups.flatMap(([, , fields]) => fields), ...inputOnlyMappingFields];
|
|
3899
|
+
const mappingHints = mappingRecommendations(dataset?.headers || [], targetFields);
|
|
3656
3900
|
const setWorkbenchFieldGroup = (fields, enabledGroup) => {
|
|
3657
3901
|
const selected = new Set(fieldSelection);
|
|
3658
3902
|
for (const [id] of fields) {
|
|
@@ -3703,19 +3947,13 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3703
3947
|
h('section', { className: 'dcAgentSection' },
|
|
3704
3948
|
h('h3', null, '字段映射'),
|
|
3705
3949
|
h('p', { className: 'dcAgentHint' }, '至少映射企业名称、统一社会信用代码或注册号其中一项。未映射列会原样保留,但不参与匹配。'),
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
onChange: (event) => actions.setMapping(sourceField, event.target.value),
|
|
3714
|
-
},
|
|
3715
|
-
h('option', { value: '' }, '不参与匹配 / 补全'),
|
|
3716
|
-
targetFields.map(([id, label]) => h('option', { key: id, value: id }, label)),
|
|
3717
|
-
),
|
|
3718
|
-
)),
|
|
3950
|
+
h('p', { className: 'dcAgentHint' }, `已映射 ${mappings.length} / ${dataset?.headers?.length || 0} 列。标准字段名和明确别名自动匹配;多义表头、重复列请确认推荐。仅在本机匹配表头,不调用模型或企查查,不改变下方输出字段。`),
|
|
3951
|
+
h('button', { type: 'button', className: 'dcAgentButton', onClick: () => actions.setMappings(mergeRecommendedMappings(dataset?.headers || [], mappings, targetFields)) }, '补充自动映射(保留已有选择)'),
|
|
3952
|
+
mappingHints.map((recommendation, index) => h(MappingPicker, {
|
|
3953
|
+
key: `${index}:${recommendation.sourceField}`, sourceField: recommendation.sourceField,
|
|
3954
|
+
mappings, groups: [...uiFieldGroups, ['input-only', '输入校验字段', inputOnlyMappingFields]],
|
|
3955
|
+
recommendation, onChange: actions.setMapping,
|
|
3956
|
+
})),
|
|
3719
3957
|
),
|
|
3720
3958
|
h('section', { className: 'dcAgentSection' },
|
|
3721
3959
|
h('h3', null, '任务目标'),
|
|
@@ -3733,6 +3971,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3733
3971
|
),
|
|
3734
3972
|
h('section', { className: 'dcAgentSection' },
|
|
3735
3973
|
h('h3', null, `输出字段 · 已选 ${fieldSelection.length}`),
|
|
3974
|
+
h('p', { className: 'dcAgentHint' }, '补全结果优先填入已映射原列的空白单元格,保留非空原值;没有对应映射列的已选字段新增列。重复或多义列请先确认映射。'),
|
|
3736
3975
|
h('div', { className: 'dcAgentRow', 'aria-label': '补全字段批次概览' },
|
|
3737
3976
|
h('span', { className: 'dcAgentChip' }, '基础字段 30'),
|
|
3738
3977
|
h('span', { className: 'dcAgentChip' }, '第一批 40'),
|
|
@@ -3844,7 +4083,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3844
4083
|
stat('失败', qccRun.summary?.failed ?? 0, (qccRun.summary?.failed ?? 0) > 0 ? 'bad' : null),
|
|
3845
4084
|
),
|
|
3846
4085
|
Array.isArray(qccRun.rows) && qccRun.rows.length ? h(DatasetReview, {
|
|
3847
|
-
key: qccRun.runId, rows: qccRun.rows, headers: runtime.headers,
|
|
4086
|
+
key: qccRun.runId, ...projectCompletionResult({ rows: qccRun.rows, headers: runtime.headers, mappings: cachedTask?.mappings || mappings, fieldSelection: cachedTask?.fieldSelection || fieldSelection }),
|
|
3848
4087
|
title: '企查查匹配补全结果', expectedCount: qccRun.summary?.totalRows,
|
|
3849
4088
|
onError: actions.setError,
|
|
3850
4089
|
}) : null,
|
|
@@ -3979,6 +4218,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3979
4218
|
'aria-modal': 'false',
|
|
3980
4219
|
'aria-label': '数据清洗补全工作台',
|
|
3981
4220
|
'data-session-id': activeSessionId ?? undefined,
|
|
4221
|
+
id: 'dc-agent-workbench',
|
|
3982
4222
|
onKeyDown: (event) => {
|
|
3983
4223
|
if (event.key === 'Escape') {
|
|
3984
4224
|
event.preventDefault();
|
|
@@ -3987,6 +4227,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3987
4227
|
}
|
|
3988
4228
|
},
|
|
3989
4229
|
},
|
|
4230
|
+
h('div', { className: 'dcAgentResizeHandle', role: 'separator', tabIndex: 0,
|
|
4231
|
+
'aria-label': '调整工作台宽度', 'aria-orientation': 'vertical', 'aria-controls': 'dc-agent-workbench',
|
|
4232
|
+
title: '拖动调整宽度;左右方向键微调,Home/End 最小/最大,双击恢复默认' }),
|
|
3990
4233
|
h('header', { className: 'dcAgentWbHeader' },
|
|
3991
4234
|
h('div', { className: 'dcAgentWbTitle' },
|
|
3992
4235
|
h('span', { className: 'dcAgentWbIcon', 'aria-hidden': 'true' }, h(DatabaseLogo, { size: 22 })),
|
|
@@ -4153,6 +4396,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4153
4396
|
exports.inject = inject;
|
|
4154
4397
|
// 测试用纯函数,不构成 Host / DSH 稳定 API。
|
|
4155
4398
|
exports.__testing = {
|
|
4399
|
+
workbenchWidthBounds,
|
|
4156
4400
|
DatabaseLogo,
|
|
4157
4401
|
visibleCatalogFields,
|
|
4158
4402
|
installCapabilityMount,
|
|
@@ -4170,6 +4414,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4170
4414
|
isImagePathEntry,
|
|
4171
4415
|
requestPromptImage,
|
|
4172
4416
|
guessMappings,
|
|
4417
|
+
projectCompletionResult,
|
|
4418
|
+
mappingRecommendations,
|
|
4419
|
+
mergeRecommendedMappings,
|
|
4420
|
+
MappingPicker,
|
|
4173
4421
|
deactivateCleaningSession,
|
|
4174
4422
|
installSessionOwnershipBridge,
|
|
4175
4423
|
installNewSessionBridge,
|
package/lib/engine.js
CHANGED
|
@@ -9,6 +9,47 @@
|
|
|
9
9
|
* - 引擎层返回完整明细(供下载/入库),模型工具层只回摘要(安全边界在工具层)。
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
// 重复表头必须获得稳定、无碰撞的键,否则行对象会覆盖前一列原值。
|
|
13
|
+
function uniqueHeaders(values) {
|
|
14
|
+
const bases = Array.from(values, (value, i) => String(value ?? '').trim() || `col_${i + 1}`);
|
|
15
|
+
const reserved = new Set(bases);
|
|
16
|
+
const used = new Set();
|
|
17
|
+
return bases.map((base) => {
|
|
18
|
+
let key = base;
|
|
19
|
+
let ordinal = 2;
|
|
20
|
+
// 原表也可能包含同名的重复列标记,不能覆盖它。
|
|
21
|
+
while (used.has(key) || (key !== base && reserved.has(key))) key = `${base}(重复列 ${ordinal++})`;
|
|
22
|
+
used.add(key);
|
|
23
|
+
return key;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 原列补空投影:由已确认映射决定目的列,非空原值(包括 0/false)不覆盖。 */
|
|
28
|
+
export function projectCompletionResult({ rows = [], headers = [], mappings = [], fieldSelection = [] } = {}) {
|
|
29
|
+
const sourceHeaders = [...new Set(headers)];
|
|
30
|
+
const selected = [...new Set(fieldSelection)];
|
|
31
|
+
const pairs = mappings.filter((item) => sourceHeaders.includes(item.sourceField) && selected.includes(item.targetField)
|
|
32
|
+
&& mappings.filter((other) => other.sourceField === item.sourceField).length === 1
|
|
33
|
+
&& mappings.filter((other) => other.targetField === item.targetField).length === 1);
|
|
34
|
+
const hidden = new Set(pairs.filter((item) => item.sourceField !== item.targetField && !sourceHeaders.includes(item.targetField)).map((item) => item.targetField));
|
|
35
|
+
const blank = (value) => value == null || (typeof value === 'string' && value.trim() === '');
|
|
36
|
+
const output = rows.map((row) => {
|
|
37
|
+
const next = { ...row };
|
|
38
|
+
const status = row.qcc_match_status;
|
|
39
|
+
if (!status || ['enriched', 'exact'].includes(status)) {
|
|
40
|
+
for (const { sourceField, targetField } of pairs) {
|
|
41
|
+
if (blank(row[sourceField]) && !blank(row[targetField])) next[sourceField] = row[targetField];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const key of hidden) delete next[key];
|
|
45
|
+
return next;
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
rows: output,
|
|
49
|
+
headers: [...new Set([...sourceHeaders, ...selected, ...output.flatMap((row) => Object.keys(row))])].filter((key) => !hidden.has(key)),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
12
53
|
/** 去掉 BOM、按 CSV/RFC4180 子集解析为 { headers, rows }。 */
|
|
13
54
|
export function parseCsv(text) {
|
|
14
55
|
const src = String(text ?? '').replace(/^\uFEFF/, '');
|
|
@@ -48,7 +89,7 @@ export function parseCsv(text) {
|
|
|
48
89
|
const nonEmpty = records.filter((r) => r.some((c) => c.trim() !== ''));
|
|
49
90
|
if (nonEmpty.length === 0) return { headers: [], rows: [] };
|
|
50
91
|
|
|
51
|
-
const headers = nonEmpty[0]
|
|
92
|
+
const headers = uniqueHeaders(nonEmpty[0]);
|
|
52
93
|
const rows = nonEmpty.slice(1).map((r) => {
|
|
53
94
|
const o = {};
|
|
54
95
|
headers.forEach((h, i) => { o[h] = r[i] === undefined ? '' : String(r[i]); });
|
|
@@ -72,7 +113,7 @@ export async function parseXlsx(buffer) {
|
|
|
72
113
|
if (!sheetName) return { headers: [], rows: [] };
|
|
73
114
|
const aoa = XLSX.utils.sheet_to_json(wb.Sheets[sheetName], { header: 1, defval: '', raw: false });
|
|
74
115
|
if (!aoa.length) return { headers: [], rows: [] };
|
|
75
|
-
const headers = aoa[0]
|
|
116
|
+
const headers = uniqueHeaders(aoa[0]);
|
|
76
117
|
const rows = aoa.slice(1).map((r) => {
|
|
77
118
|
const o = {};
|
|
78
119
|
headers.forEach((h, i) => { o[h] = r[i] === undefined || r[i] === null ? '' : String(r[i]); });
|
package/lib/qcc-runs.js
CHANGED
|
@@ -229,7 +229,7 @@ export class G5RunStore {
|
|
|
229
229
|
if (result.status === 'enriched') {
|
|
230
230
|
record.rows[index] = {
|
|
231
231
|
...row,
|
|
232
|
-
...result.fields,
|
|
232
|
+
...Object.fromEntries(Object.entries(result.fields || {}).filter(([key]) => row[key] == null || (typeof row[key] === 'string' && row[key].trim() === ''))),
|
|
233
233
|
qcc_match_status: 'enriched',
|
|
234
234
|
qcc_source: 'qcc-mcp',
|
|
235
235
|
};
|
package/lib/qcc.js
CHANGED
|
@@ -1027,7 +1027,7 @@ export class QccHostBridge {
|
|
|
1027
1027
|
if (result?.status === 'enriched') {
|
|
1028
1028
|
return {
|
|
1029
1029
|
...row,
|
|
1030
|
-
...result.fields,
|
|
1030
|
+
...Object.fromEntries(Object.entries(result.fields || {}).filter(([key]) => row[key] == null || (typeof row[key] === 'string' && row[key].trim() === ''))),
|
|
1031
1031
|
qcc_match_status: 'enriched',
|
|
1032
1032
|
qcc_source: 'qcc-mcp',
|
|
1033
1033
|
};
|
package/lib/web.js
CHANGED
|
@@ -537,6 +537,7 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
537
537
|
rows: payload.rows,
|
|
538
538
|
headers: payload.headers,
|
|
539
539
|
fieldSelection: readyTask.fieldSelection,
|
|
540
|
+
mappings: readyTask.mappings,
|
|
540
541
|
exceptionRows: payload.exceptionRows,
|
|
541
542
|
baseName: payload.baseName || readyTask.title,
|
|
542
543
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-data-cleaning-agent",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.11",
|
|
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",
|