dsh-data-cleaning-agent 0.9.1 → 0.9.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 +13 -0
- package/README.en.md +3 -3
- package/README.md +6 -4
- package/docs/COMPATIBILITY.md +14 -0
- package/install.sh +1 -0
- package/lib/better-sidebar-adapter.js +1 -1
- package/lib/client.js +93 -16
- package/lib/image-intake.js +17 -2
- package/lib/image-workflow.js +31 -0
- package/lib/install-preflight.js +11 -10
- package/lib/web.js +23 -1
- package/lib/workflow.js +3 -2
- package/package.json +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.3] - 2026-09-10
|
|
8
|
+
|
|
9
|
+
- 图片向导保留独立缩略图与来源元数据,识别前显示待识别而非表格零行。
|
|
10
|
+
- 明确发送授权后,图片高层工具沿用确认版本自动完成识别、体检、匹配、补全及新 XLSX 交付;不再要求第二次发送。歧义、超限及过期配置阻断企业查询,重复调用保持幂等。
|
|
11
|
+
- 文件预览优先复用当前会话的侧栏预览 Tab,下载及无侧栏浏览器回退保持独立。
|
|
12
|
+
|
|
13
|
+
## [0.9.2] - 2026-09-10
|
|
14
|
+
|
|
15
|
+
- Better Sidebar 改为可选 peer;缺失侧栏不阻断基础安装、原生会话和本地清洗/补全/画像工具。
|
|
16
|
+
- 基础预检与 `--workbench` 预检分离;已安装的已知不兼容组合仍明确阻断。
|
|
17
|
+
- 工作台缺失提示明确可用边界,保留草稿与任务,不创建私有抽屉。
|
|
18
|
+
- 隔离真实 Host 验证无侧栏工具执行、兼容侧栏 XLSX 闭环及已知不兼容阻断;真实计费 MCP、OCR 与四产品共存不在本次验收范围。
|
|
19
|
+
|
|
7
20
|
## [0.9.1] - 2026-09-10
|
|
8
21
|
|
|
9
22
|
- 新宿主 uiWorkspace 生命周期适配:新会话不再复用清洗空会话,普通草稿与菜单隔离。
|
package/README.en.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh-data-cleaning-agent
|
|
2
2
|
|
|
3
|
-
> 0.9.
|
|
3
|
+
> 0.9.2 upgrade: Better Sidebar is an optional workbench dependency. Base tools and conversations remain available without it. The workbench requires `targetedOpen` and `stateSubscription`; no private drawer fallback is created. See the [compatibility record](docs/COMPATIBILITY.md).
|
|
4
4
|
|
|
5
5
|
Data cleaning and data enrichment for CSV/XLSX/JSON enterprise lists in DeepSeek Harness, including spreadsheet cleaning, deduplication, profiling, optional Qichacha MCP and exports.
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ Related agents: [数据清洗补全](https://github.com/duhu2000/dsh-data-cleani
|
|
|
10
10
|
|
|
11
11
|
> 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.
|
|
12
12
|
>
|
|
13
|
-
> Current source version / 当前源码版本: **0.9.
|
|
13
|
+
> Current source version / 当前源码版本: **0.9.3** (stable release)
|
|
14
14
|
|
|
15
15
|
This release unifies spreadsheet previews, column mapping, and enrichment scope across the prompt wizard and workbench. Mapping statuses use color and text; mapped fields automatically enter the scope, with optional extra fields collapsed. Multiple confirmed output columns may share a non-identity field while preserving non-empty values. It fixes saved drafts resetting the scope to five defaults and detects stale Host versions. Preparing a draft does not query QCC; sending starts execution. Fully restart DSH after upgrading. Existing download artifacts are not rewritten.
|
|
16
16
|
|
|
@@ -72,7 +72,7 @@ temporary files. The local server uploads the explicitly submitted file to the Q
|
|
|
72
72
|
Both use the current customer's own QCC account and quota. Without the local connector,
|
|
73
73
|
image intake fails closed with actionable guidance; text and spreadsheet paths remain available.
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
Better Sidebar is optional and is not automatically installed. Native conversations and the clean/complete/profile engine tools work without it; interactive import, mapping confirmation, workbench preview/export and navigation do not. Workbench entry explains the limitation without deleting drafts/tasks or creating a private drawer. For the workbench, use the tested DSH `0.1.2-rc.1` + Sidebar `0.18.1` pair; an exact optional peer is not a general compatibility claim. Existing incompatible sidebar installations still block preflight. Context is optional; an existing `0.36.0` must be upgraded to `0.48.0`. Use `node lib/install-preflight.js <profile> <dsh-executable> --workbench` for workbench checks; base checks permit a missing sidebar. Runtime capabilities are checked separately. No global host upgrades are performed.
|
|
76
76
|
|
|
77
77
|
Run from a full checkout or unpacked npm package with the actual `dsh` CLI (streamed shell installation is no longer supported):
|
|
78
78
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh-data-cleaning-agent
|
|
2
2
|
|
|
3
|
-
> 0.9.
|
|
3
|
+
> 0.9.2 升级说明:Better Sidebar 是可选工作台依赖;缺失时基础工具和会话仍可用。工作台需要兼容的 `dsh-better-sidebar`(`targetedOpen`、`stateSubscription`),不回退到自有抽屉;验证边界见 [兼容记录](docs/COMPATIBILITY.md)。
|
|
4
4
|
|
|
5
5
|
本版统一向导与工作台的 Host 执行闭环:发送后自动生成新 Excel,原文件不修改,工作台入口可恢复;发布记录见 [发布说明](docs/RELEASE-0.8.22.md)。
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
数据清洗补全智能体:面向 Excel/CSV/JSON 企业名单,提供数据清洗、表格清洗、清洗补全、去重、企业数据补全与字段补全,支持企查查 MCP 和结果导出。
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
dsh plugin --profile web add dsh-data-cleaning-agent@0.9.
|
|
12
|
+
dsh plugin --profile web add dsh-data-cleaning-agent@0.9.3
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
请先满足下文的 DSH、连接器及侧边栏依赖要求;安装后完整停止并重启对应 Profile。
|
|
@@ -27,7 +27,7 @@ dsh plugin --profile web add dsh-data-cleaning-agent@0.9.1
|
|
|
27
27
|
|
|
28
28
|
> 在 DeepSeek Harness 中清洗、补全、画像企业名单数据的智能体插件:本地 CSV/XLSX/JSON 引擎 + 可选企查查(Qichacha/QCC)MCP 企业数据补全,由企查查(Qichacha/QCC)团队发起并维护。
|
|
29
29
|
>
|
|
30
|
-
> 当前源码版本 / Current source version: **0.9.
|
|
30
|
+
> 当前源码版本 / Current source version: **0.9.3**(正式版本)
|
|
31
31
|
|
|
32
32
|
本版上传表格后展示完整清单及空白数量,用绿色、琥珀色、红色及文字区分已通过、待确认和未匹配映射。已映射字段自动纳入补全范围,额外字段默认折叠;多个原列可经确认使用同一非主体字段,分别补空并保留非空原值。修复 Host 草稿重置为默认五字段的问题,增加前后端版本错配提示。生成说明不查询,发送才执行;升级后需完整重启 DSH,历史下载文件不自动改写。
|
|
33
33
|
|
|
@@ -81,7 +81,9 @@ Composer 图片附件,因此不支持视觉输入的文本模型也能调度 A
|
|
|
81
81
|
或分发凭据。本地服务会把用户明确提交的图片上传到企查查文档解析网关;未连接本地服务时功能会
|
|
82
82
|
fail closed,并明确提示配置要求,文本与 Excel 流程不受影响。
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
基础安装不需要 Better Sidebar,也不会自动安装它。交互工作台才需要完整 DSH `0.1.2-rc.1` + 可选 Better Sidebar `0.18.1`;optional peer 的精确版本不代表其它配对均兼容。context 不是必装依赖;若已装 `0.36.0`,需先升级到 `0.48.0`。已安装不兼容侧栏仍会阻断预检。
|
|
85
|
+
|
|
86
|
+
无侧栏可用:原生会话、`data_clean_rows`、`data_complete_rows`、`data_profile` 引擎工具。不可用:交互工作台的表格导入、映射确认、结果预览/下载及流程导航;点击入口显示原因并保留草稿/任务,不创建私有抽屉。不要把会话可打开等同于完整清洗流程可用。基础预检默认允许侧栏缺失;如需校验工作台安装,运行 `node lib/install-preflight.js <Profile目录> <dsh可执行文件> --workbench`,运行时再探测 capabilities。预检不会升级全局宿主。
|
|
85
87
|
|
|
86
88
|
从完整源码或解压的 npm 包运行安装脚本(需要实际 `dsh` CLI;不再支持流式脚本):
|
|
87
89
|
|
package/docs/COMPATIBILITY.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# 兼容性 / Compatibility
|
|
2
2
|
|
|
3
|
+
## 0.9.2:可选侧栏验收
|
|
4
|
+
|
|
5
|
+
Node v25.9.0,完整 DSH 0.1.2-rc.1;使用 npm pack 产物、独立 DSH_HOME 和端口,未修改生产 Profile 或调用计费 MCP。
|
|
6
|
+
|
|
7
|
+
| 组合 | 实测结果 |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| 无 Sidebar / 无 context | 基础预检允许;Host 启动、原生会话、缺失提示及草稿保留通过。真实 tools.execute 执行 data_clean_rows、data_complete_rows、data_profile 合成数据成功。工作台不可用。 |
|
|
10
|
+
| Sidebar 0.18.1 / context 0.48.0 | Host 与浏览器无 pageErrors;合成 XLSX 导入、映射确认、本地清洗/补全、导出回读、HTTP 预览及普通会话草稿通过。 |
|
|
11
|
+
| Sidebar 0.17.1 | 基础预检仍阻断;隔离启动复现 settingsNamespace 缺失,不因 optional 忽略冲突。 |
|
|
12
|
+
|
|
13
|
+
基础安装不强装侧栏;`--workbench` 模式在侧栏缺失时阻断,运行时另检 targetedOpen/stateSubscription。工具未发现 session.events/snapshotEvents 依赖,不套用其他产品补丁。无侧栏不支持交互导入、映射确认、结果预览下载与工作台导航,不等于完整工作流可用。
|
|
14
|
+
|
|
15
|
+
验收脚本:scripts/host-no-sidebar-smoke.cjs、scripts/host-rc1-smoke.cjs。测试数据为合成数据;真实 QCC、OCR、多候选确认和四产品完整共存仍未验证。以下章节是历史版本证据,不覆盖这些未测能力。
|
|
16
|
+
|
|
3
17
|
## 0.9.0:Session 单例 Tab
|
|
4
18
|
|
|
5
19
|
工作台通过可选 `ctx.inject(['betterSidebar'], ...)` 接入 Provider,探测 `targetedOpen` 与 `stateSubscription`;缺失时保留会话和 Host 工具,不提供私有抽屉回退。当前仅完成隔离模拟服务与 Chromium 回归,尚未实装组合验收;旧版本的 Host/UI 验证不自动覆盖此次迁移。详见 [采用记录](UI-V1.5.0-ADOPTION.md)。
|
package/install.sh
CHANGED
|
@@ -28,6 +28,7 @@ if [ ! -f "$SCRIPT_DIR/lib/install-preflight.js" ]; then
|
|
|
28
28
|
exit 1
|
|
29
29
|
fi
|
|
30
30
|
node "$SCRIPT_DIR/lib/install-preflight.js" "$PROFILE_DIR"
|
|
31
|
+
echo "==> 基础智能体安装:不会强制安装侧栏;交互工作台需兼容 Better Sidebar,可用 --workbench 单独预检。"
|
|
31
32
|
|
|
32
33
|
# ── 1) 安装依赖 ──
|
|
33
34
|
if command -v dsh >/dev/null 2>&1; then
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Public Better Sidebar seam. No business task creation, cancellation or geometry. */
|
|
2
2
|
export const CLEANING_TAB_ID = 'dsh-data-cleaning-agent:workbench';
|
|
3
|
-
export const SIDEBAR_DEPENDENCY_MESSAGE = '
|
|
3
|
+
export const SIDEBAR_DEPENDENCY_MESSAGE = '交互工作台暂不可用;基础清洗、规则补全、画像工具和原生会话仍可使用。启用工作台请安装或升级兼容的 dsh-better-sidebar 并重启 DSH(需要 targetedOpen 和 stateSubscription 能力),然后重新点击流程按钮。现有任务和文件不会删除。';
|
|
4
4
|
|
|
5
5
|
export function sidebarCompatibility(service) {
|
|
6
6
|
const methods = ['registerTab', 'openTab', 'isTabEnabled', 'getSnapshot', 'subscribeState'];
|
package/lib/client.js
CHANGED
|
@@ -1190,7 +1190,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1190
1190
|
const entryCount = Number.isFinite(config.entryCount) ? config.entryCount : entries.length;
|
|
1191
1191
|
const source = mode === 'image'
|
|
1192
1192
|
? config.imageState === 'completed'
|
|
1193
|
-
? `图片${config.fileName ? `「${config.fileName}」` : ''}
|
|
1193
|
+
? `图片${config.fileName ? `「${config.fileName}」` : ''},已由企查查智能文档解析识别 ${entryCount} 条主体标识`
|
|
1194
1194
|
: `图片${config.fileName ? `「${config.fileName}」` : ''},已安全暂存在本机 Host,等待企查查智能文档解析识别`
|
|
1195
1195
|
: mode === 'excel'
|
|
1196
1196
|
? `本地表格${config.fileName ? `「${config.fileName}」` : ''},已解析 ${entryCount} 条主体标识,完整数据已载入数据清洗补全工作台`
|
|
@@ -1200,7 +1200,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1200
1200
|
`输入来源:${source}。`,
|
|
1201
1201
|
];
|
|
1202
1202
|
if (mode === 'image' && config.imageCommandId && config.imageState !== 'completed') {
|
|
1203
|
-
lines.push('
|
|
1203
|
+
lines.push('本次发送即授权使用本人企查查账号额度,按向导已确认范围执行图片识别、质量体检、主体匹配和字段补全,生成新 Excel。识别后自动沿用已选规则与字段,不重复确认或要求二次发送;仅识别歧义、多候选或权限异常时暂停,模糊字符不得猜测。');
|
|
1204
1204
|
lines.push(`安全图片凭证:${config.imageCommandId}。请只调用一次 data_cleaning_extract_image_companies,参数只传 commandId;该高层工具会在 Host 内调用 qcc-document-mcp 的 parse_document / get_parse_result,不要直接调用底层 mcp__qcc-document* 工具。`);
|
|
1205
1205
|
}
|
|
1206
1206
|
if (entries.length) {
|
|
@@ -1486,7 +1486,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1486
1486
|
const base = task.source?.fileName?.replace(/\.[^.]+$/, '') || (task.source?.type === 'image' ? '图片名单' : '粘贴名单');
|
|
1487
1487
|
const date = task.createdAt ? new Date(task.createdAt) : null;
|
|
1488
1488
|
const time = date && Number.isFinite(date.getTime()) ? date.toLocaleString() : '';
|
|
1489
|
-
return `${base}|${task.source?.rowCount ?? 0}
|
|
1489
|
+
return `${base}|${task.source?.type === 'image' && task.state === 'draft' ? '待识别' : `${task.source?.rowCount ?? 0} 条`}${time ? '|' + time : ''}`;
|
|
1490
1490
|
}
|
|
1491
1491
|
function cacheWorkflowTask(actions, sessionId, task) {
|
|
1492
1492
|
if (!task) return null;
|
|
@@ -1511,6 +1511,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1511
1511
|
fieldSelection: draft.fieldSelection || DEFAULT_ENRICHMENT_KEYS,
|
|
1512
1512
|
mappings: draft.mappings || [],
|
|
1513
1513
|
matchRules: draft.matchRules,
|
|
1514
|
+
...(draft.source?.type === 'image' ? { source: draft.source } : {}),
|
|
1514
1515
|
});
|
|
1515
1516
|
workflowTaskCreationBySession.set(key, pending);
|
|
1516
1517
|
const clearPending = () => {
|
|
@@ -1590,6 +1591,13 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1590
1591
|
const host = await requestJson('/data-cleaning/api/g5/capabilities');
|
|
1591
1592
|
if (host?.workflowExecutionVersion !== 1) throw new Error('页面与 Host 版本不一致。请保存任务后升级并完整重启 DSH;当前 Host 不支持自动生成结果文件。');
|
|
1592
1593
|
let task = await ensureWorkflowTask(actions, sessionId);
|
|
1594
|
+
if (expected?.mode === 'image' && expected.imageState === 'prepared') {
|
|
1595
|
+
if (JSON.stringify([...(task.fieldSelection || [])].sort()) !== JSON.stringify([...(expected.enrichmentKeys || [])].sort())) throw new Error('图片任务字段配置尚未保存,请重试。');
|
|
1596
|
+
const bound = await requestJson(`/data-cleaning/api/images/commands/${encodeURIComponent(expected.imageCommandId)}`, 'PATCH', {
|
|
1597
|
+
taskId: task.id, expectedRevision: task.revision, executeOnSend: true,
|
|
1598
|
+
});
|
|
1599
|
+
return { ...bound.command, prompt: buildTaskPrompt(expected) };
|
|
1600
|
+
}
|
|
1593
1601
|
if (expected) {
|
|
1594
1602
|
const sameSet = (a, b) => JSON.stringify([...(a || [])].sort()) === JSON.stringify([...(b || [])].sort());
|
|
1595
1603
|
if (!sameSet(task.fieldSelection, expected.enrichmentKeys)
|
|
@@ -2438,6 +2446,8 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2438
2446
|
const [imageFileName, setImageFileName] = react.useState('');
|
|
2439
2447
|
const [imageSizeBytes, setImageSizeBytes] = react.useState(0);
|
|
2440
2448
|
const [imageAttachment, setImageAttachment] = react.useState(null);
|
|
2449
|
+
// 独立预览不依赖原生附件的 blob URL;发送前移除附件可能撤销该 URL。
|
|
2450
|
+
const [imagePreviewUrl, setImagePreviewUrl] = react.useState('');
|
|
2441
2451
|
const [imageCommand, setImageCommand] = react.useState(null);
|
|
2442
2452
|
const [imageDropActive, setImageDropActive] = react.useState(false);
|
|
2443
2453
|
const [imageLightbox, setImageLightbox] = react.useState(false);
|
|
@@ -2466,6 +2476,28 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2466
2476
|
const command = response?.command;
|
|
2467
2477
|
if (!command) return;
|
|
2468
2478
|
setImageCommand((current) => current?.commandId === commandId ? { ...current, ...command } : current);
|
|
2479
|
+
if (command.taskId) {
|
|
2480
|
+
const actions = sessionWorkbenchController?.actionsFor(sessionId);
|
|
2481
|
+
if (actions) {
|
|
2482
|
+
await refreshHostedTask(actions, sessionId, command.taskId, null, true);
|
|
2483
|
+
if (command.state === 'running') requestWorkbenchOpen(command.qccCommand ? 'match' : 'upload', sessionId);
|
|
2484
|
+
if (command.qccCommand) {
|
|
2485
|
+
const status = await requestJson(`/data-cleaning/api/g5/commands/${encodeURIComponent(command.qccCommand.commandId)}`);
|
|
2486
|
+
actions.setCommandProgress?.({ taskId: command.taskId, ...(status.command?.progress || {}) });
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
if (command.state === 'completed') {
|
|
2490
|
+
const recognized = command.result?.entries || [];
|
|
2491
|
+
setEntries(recognized); setEntryCount(recognized.length);
|
|
2492
|
+
const result = entriesToDataset(recognized);
|
|
2493
|
+
const runtime = runtimeFor(command.taskId);
|
|
2494
|
+
runtime.rows = result.rows; runtime.sourceRows = result.rows; runtime.headers = result.headers;
|
|
2495
|
+
runtime.source = { type: 'image', fileName: imageFileName, sizeBytes: imageSizeBytes };
|
|
2496
|
+
if (actions) actions.setDataset(result);
|
|
2497
|
+
setOpen(false);
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2469
2501
|
if (command.state === 'completed') {
|
|
2470
2502
|
const recognized = Array.isArray(command.result?.entries) ? command.result.entries : [];
|
|
2471
2503
|
setEntries(recognized);
|
|
@@ -2504,6 +2536,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2504
2536
|
requestJson(`/data-cleaning/api/images/commands/${encodeURIComponent(imageCommand.commandId)}`, 'DELETE').catch(() => {});
|
|
2505
2537
|
}
|
|
2506
2538
|
setImageAttachment(null);
|
|
2539
|
+
setImagePreviewUrl('');
|
|
2507
2540
|
setImageCommand(null);
|
|
2508
2541
|
setImageFileName('');
|
|
2509
2542
|
setImageSizeBytes(0);
|
|
@@ -2536,11 +2569,13 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2536
2569
|
attached = Array.isArray(images) ? images[0] : null;
|
|
2537
2570
|
if (!attached?.id) throw new Error('图片未能加入当前对话。');
|
|
2538
2571
|
const staged = await stagePromptImage(file);
|
|
2572
|
+
const previewUrl = `data:${file.type};base64,${await imageFileBase64(file)}`;
|
|
2539
2573
|
if (imageAttachment && typeof removeImage === 'function') removeImage(sessionId, imageAttachment);
|
|
2540
2574
|
if (imageCommand?.commandId) {
|
|
2541
2575
|
requestJson(`/data-cleaning/api/images/commands/${encodeURIComponent(imageCommand.commandId)}`, 'DELETE').catch(() => {});
|
|
2542
2576
|
}
|
|
2543
2577
|
setImageAttachment(attached);
|
|
2578
|
+
setImagePreviewUrl(previewUrl);
|
|
2544
2579
|
setImageFileName(file.name);
|
|
2545
2580
|
setImageSizeBytes(Number(file.size ?? 0));
|
|
2546
2581
|
setImageCommand(staged.command);
|
|
@@ -2650,9 +2685,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2650
2685
|
setBusy(true);
|
|
2651
2686
|
try {
|
|
2652
2687
|
let prompt = buildTaskPrompt(config);
|
|
2653
|
-
if (mode !== 'image' ||
|
|
2688
|
+
if (mode !== 'image' || ['prepared', 'completed'].includes(imageCommand?.state)) {
|
|
2654
2689
|
const command = await requestPreparedWorkflow(sessionId, config);
|
|
2655
|
-
prompt = command.prompt + '\n\n' + buildTaskPrompt(config);
|
|
2690
|
+
prompt = mode === 'image' && imageCommand?.state === 'prepared' ? command.prompt : command.prompt + '\n\n' + buildTaskPrompt(config);
|
|
2691
|
+
if (mode === 'image' && imageCommand?.state === 'prepared') setImageCommand(command);
|
|
2656
2692
|
}
|
|
2657
2693
|
inputActions.setDraft(prompt);
|
|
2658
2694
|
const updatedInput = getInputSnapshot?.(sessionId);
|
|
@@ -2840,16 +2876,16 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2840
2876
|
},
|
|
2841
2877
|
},
|
|
2842
2878
|
h('p', null, '可直接在此粘贴图片,或拖入 / 选择 PNG、JPEG、WebP。图片会在对话框显示缩略图,点击可放大。'),
|
|
2843
|
-
|
|
2879
|
+
imagePreviewUrl ? h('div', { className: 'dcAgentPromptImagePreview' },
|
|
2844
2880
|
h('button', {
|
|
2845
2881
|
type: 'button', className: 'dcAgentPromptImageThumb',
|
|
2846
2882
|
'aria-label': `放大查看 ${imageFileName}`,
|
|
2847
2883
|
onClick: () => setImageLightbox(true),
|
|
2848
|
-
},
|
|
2884
|
+
}, h('img', { src: imagePreviewUrl, alt: imageFileName })),
|
|
2849
2885
|
h('div', { className: 'dcAgentPromptImageMeta' },
|
|
2850
2886
|
h('b', null, imageFileName),
|
|
2851
2887
|
h('small', null, imageCommand?.state === 'completed'
|
|
2852
|
-
? `已识别 ${entryCount} 条,下一步逐条核验`
|
|
2888
|
+
? imageCommand.result?.workflowResult ? `已识别 ${entryCount} 条,任务结果见右侧工作台` : `已识别 ${entryCount} 条,下一步逐条核验`
|
|
2853
2889
|
: imageCommand?.state === 'running'
|
|
2854
2890
|
? '识别中…'
|
|
2855
2891
|
: imageCommand?.state === 'failed'
|
|
@@ -2962,8 +2998,8 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2962
2998
|
: wizardStep === 4 ? '回填到对话框' : '下一步'),
|
|
2963
2999
|
),
|
|
2964
3000
|
) : null,
|
|
2965
|
-
imageLightbox &&
|
|
2966
|
-
src:
|
|
3001
|
+
imageLightbox && imagePreviewUrl ? h(ImagePreview, {
|
|
3002
|
+
src: imagePreviewUrl, alt: imageFileName, sessionId, onClose: () => setImageLightbox(false),
|
|
2967
3003
|
}) : null,
|
|
2968
3004
|
);
|
|
2969
3005
|
}
|
|
@@ -3026,7 +3062,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3026
3062
|
),
|
|
3027
3063
|
meta.hint ? h('div', { className: 'dcAgentToolCardHint' }, meta.hint) : null,
|
|
3028
3064
|
summary ? h('div', { className: 'dcAgentToolCardBody' }, summary) : null,
|
|
3029
|
-
|
|
3065
|
+
['data_cleaning_qcc_run', 'data_cleaning_extract_image_companies'].includes(toolName) ? h('div', { className: 'dcAgentArtifactList' },
|
|
3030
3066
|
[...summary.matchAll(/\[([^\]]+)\]\(((?:https?:\/\/[a-zA-Z0-9.:[\]-]+)?\/data-cleaning\/api\/workflow\/tasks\/[a-zA-Z0-9%_-]+\/artifacts\/[a-zA-Z0-9%_-]+(?:\?preview=1)?)\)/g)].filter(match => match[2].startsWith('/') || (typeof location !== 'undefined' && new URL(match[2]).origin === location.origin)).map((match, index) =>
|
|
3031
3067
|
h('a', { key: index, className: 'dcAgentButton', href: match[2], target: '_blank', rel: 'noopener noreferrer' }, match[1]))) : null,
|
|
3032
3068
|
);
|
|
@@ -4174,17 +4210,17 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4174
4210
|
) : null,
|
|
4175
4211
|
h('div', { className: 'dcAgentWbBody' },
|
|
4176
4212
|
cachedTask && step !== 'history' ? h('section', { className: 'dcAgentSection', 'aria-label': '当前任务进展', 'aria-live': 'polite' },
|
|
4177
|
-
h('b', null, WORKFLOW_STATE_LABELS[cachedTask.state] || cachedTask.state),
|
|
4213
|
+
h('b', null, cachedTask.source?.type === 'image' && cachedTask.state === 'draft' ? '图片已暂存,待识别' : WORKFLOW_STATE_LABELS[cachedTask.state] || cachedTask.state),
|
|
4178
4214
|
h('h3', null, workflowDisplayName(cachedTask)),
|
|
4179
4215
|
h('details', null, h('summary', null, '任务详情'), h('p', { className: 'dcAgentHint' }, `任务编号:${cachedTask.id}`)),
|
|
4180
|
-
h('p', null, `${cachedTask.source?.type === 'image' ? '图片识别' : cachedTask.source?.type === 'text' ? '粘贴名单' : '上传表格'
|
|
4216
|
+
h('p', null, `${cachedTask.source?.type === 'image' ? '图片识别' : cachedTask.source?.type === 'text' ? '粘贴名单' : cachedTask.source ? '上传表格' : '待选择数据来源'}${cachedTask.source?.fileName ? ' · ' + cachedTask.source.fileName : ''} · ${cachedTask.source?.type === 'image' && cachedTask.state === 'draft' ? '条目数待识别' : `${cachedTask.source?.rowCount || 0} 条`} · ${cachedTask.fieldSelection?.length || 0} 个补全字段`),
|
|
4181
4217
|
commandProgress?.taskId === cachedTask.id && Number.isFinite(commandProgress.totalUnique) ? h('p', null, `已处理主体 ${commandProgress.completedUnique}/${commandProgress.totalUnique}(去重后,含未匹配或失败,不代表补全成功)`) : null,
|
|
4182
4218
|
commandProgress?.taskId === cachedTask.id && Number.isFinite(commandProgress.totalUnique) && commandProgress.totalUnique > 0 ? h('progress', {
|
|
4183
4219
|
max: commandProgress.totalUnique, value: Math.min(commandProgress.totalUnique, Math.max(0, commandProgress.completedUnique || 0)),
|
|
4184
4220
|
'aria-label': '主体处理进度', style: { width: '100%', accentColor: cachedTask.state === 'completed' ? '#16845b' : cachedTask.state === 'failed' ? '#c62828' : cachedTask.state === 'partial' ? '#a66a00' : '#087dcc' },
|
|
4185
4221
|
}) : null,
|
|
4186
4222
|
h('p', { style: { color: qccFailedCount > 0 ? '#c62828' : Number(qccReviewCount) > 0 ? '#a66a00' : qccEnrichedCount > 0 ? '#16845b' : 'inherit' } },
|
|
4187
|
-
Number.isFinite(qccEnrichedCount) ? `已处理并补全 ${qccEnrichedCount} 条 · 待核验 ${Number.isFinite(qccReviewCount) ? qccReviewCount : '统计中'} 条 · 失败 ${qccFailedCount} 条(按输入行统计;未披露字段保留空白)` : '结果统计中,处理结束后展示补全与待核验数量'),
|
|
4223
|
+
cachedTask.source?.type === 'image' && cachedTask.state === 'draft' ? '请发送对话中的任务说明开始图片识别,识别后核验企业名单。' : Number.isFinite(qccEnrichedCount) ? `已处理并补全 ${qccEnrichedCount} 条 · 待核验 ${Number.isFinite(qccReviewCount) ? qccReviewCount : '统计中'} 条 · 失败 ${qccFailedCount} 条(按输入行统计;未披露字段保留空白)` : '结果统计中,处理结束后展示补全与待核验数量'),
|
|
4188
4224
|
) : null,
|
|
4189
4225
|
error ? h('div', { className: 'dcAgentError', role: 'alert' }, error) : null,
|
|
4190
4226
|
step !== 'history' ? h('details', { className: 'dcAgentSection' },
|
|
@@ -4209,7 +4245,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4209
4245
|
// BEGIN generated Better Sidebar adapter (source: lib/better-sidebar-adapter.js)
|
|
4210
4246
|
/** Public Better Sidebar seam. No business task creation, cancellation or geometry. */
|
|
4211
4247
|
const CLEANING_TAB_ID = 'dsh-data-cleaning-agent:workbench';
|
|
4212
|
-
const SIDEBAR_DEPENDENCY_MESSAGE = '
|
|
4248
|
+
const SIDEBAR_DEPENDENCY_MESSAGE = '交互工作台暂不可用;基础清洗、规则补全、画像工具和原生会话仍可使用。启用工作台请安装或升级兼容的 dsh-better-sidebar 并重启 DSH(需要 targetedOpen 和 stateSubscription 能力),然后重新点击流程按钮。现有任务和文件不会删除。';
|
|
4213
4249
|
|
|
4214
4250
|
function sidebarCompatibility(service) {
|
|
4215
4251
|
const methods = ['registerTab', 'openTab', 'isTabEnabled', 'getSnapshot', 'subscribeState'];
|
|
@@ -4320,6 +4356,38 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4320
4356
|
}
|
|
4321
4357
|
};
|
|
4322
4358
|
let adapter;
|
|
4359
|
+
let previewService = null;
|
|
4360
|
+
const previewTabId = 'dsh-data-cleaning-agent:artifact-preview';
|
|
4361
|
+
const previewUrls = new Map();
|
|
4362
|
+
const previewListeners = new Set();
|
|
4363
|
+
function ArtifactPreviewTab(props) {
|
|
4364
|
+
const url = react.useSyncExternalStore(listener => {
|
|
4365
|
+
previewListeners.add(listener);
|
|
4366
|
+
return () => previewListeners.delete(listener);
|
|
4367
|
+
}, () => previewUrls.get(props.scope.sessionId) || '', () => '');
|
|
4368
|
+
return url ? h('iframe', { src: url, title: '清洗结果 Excel 预览',
|
|
4369
|
+
sandbox: 'allow-same-origin allow-downloads',
|
|
4370
|
+
style: { display: 'block', width: '100%', height: '100%', minHeight: '320px', border: 0 } })
|
|
4371
|
+
: h('p', null, '请点击清洗结果文件的「预览 / 打开」。');
|
|
4372
|
+
}
|
|
4373
|
+
const previewClick = event => {
|
|
4374
|
+
if (disposed || event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
|
|
4375
|
+
const anchor = event.target?.closest?.('a[href]');
|
|
4376
|
+
if (!anchor || !previewService) return;
|
|
4377
|
+
let url;
|
|
4378
|
+
try { url = new URL(anchor.href, location.href); } catch { return; }
|
|
4379
|
+
if (url.origin !== location.origin || url.search !== '?preview=1'
|
|
4380
|
+
|| !/^\/data-cleaning\/api\/workflow\/tasks\/dcw-[a-zA-Z0-9-]+\/artifacts\/dca-[a-zA-Z0-9-]+$/.test(url.pathname)) return;
|
|
4381
|
+
const sessionId = previewService.getSnapshot().sessionId;
|
|
4382
|
+
if (!sessionId || !previewService.isTabEnabled(previewTabId)) return;
|
|
4383
|
+
previewUrls.set(sessionId, url.pathname + url.search);
|
|
4384
|
+
try { previewService.openTab({ type: previewTabId }, { sessionId }); }
|
|
4385
|
+
catch { return; } // Native browser link remains usable if the provider cannot open.
|
|
4386
|
+
event.preventDefault();
|
|
4387
|
+
event.stopPropagation();
|
|
4388
|
+
for (const listener of previewListeners) listener();
|
|
4389
|
+
};
|
|
4390
|
+
host.addEventListener?.('click', previewClick, true);
|
|
4323
4391
|
function SessionWorkbenchTab(props) {
|
|
4324
4392
|
const instance = instanceFor(props.scope.sessionId);
|
|
4325
4393
|
const useStore = pick => react.useSyncExternalStore(instance.subscribe,
|
|
@@ -4340,7 +4408,14 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4340
4408
|
const next = createCleaningSidebarAdapter(service, adapterOptions);
|
|
4341
4409
|
adapter.dispose();
|
|
4342
4410
|
adapter = next;
|
|
4411
|
+
const unregisterPreview = next.compatible ? service.registerTab({
|
|
4412
|
+
id: previewTabId, title: '清洗结果预览', order: 31, single: true,
|
|
4413
|
+
icon: size => h(DatabaseLogo, { size }), component: ArtifactPreviewTab,
|
|
4414
|
+
}) : () => {};
|
|
4415
|
+
previewService = next.compatible ? service : null;
|
|
4343
4416
|
return () => {
|
|
4417
|
+
unregisterPreview();
|
|
4418
|
+
if (previewService === service) previewService = null;
|
|
4344
4419
|
next.dispose();
|
|
4345
4420
|
if (!disposed && adapter === next) adapter = createCleaningSidebarAdapter(null, adapterOptions);
|
|
4346
4421
|
};
|
|
@@ -4401,7 +4476,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4401
4476
|
const { sessionId, config, resolve, reject } = event.detail;
|
|
4402
4477
|
const actions = controller.actionsFor(sessionId);
|
|
4403
4478
|
queueWorkflowOperation(sessionId, () => stageConfirmedWorkflow(actions, sessionId, config))
|
|
4404
|
-
.then(command => { if (!disposed) watchHostedCommand(actions, sessionId, command); resolve(command); }, reject);
|
|
4479
|
+
.then(command => { if (!disposed && command.commandId?.startsWith('dcq-')) watchHostedCommand(actions, sessionId, command); resolve(command); }, reject);
|
|
4405
4480
|
},
|
|
4406
4481
|
};
|
|
4407
4482
|
const bindings = Object.entries(listeners).map(([name, handler]) => {
|
|
@@ -4418,6 +4493,8 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4418
4493
|
disposed = true;
|
|
4419
4494
|
commandWatchGeneration += 1;
|
|
4420
4495
|
for (const remove of bindings) remove();
|
|
4496
|
+
host.removeEventListener?.('click', previewClick, true);
|
|
4497
|
+
previewUrls.clear(); previewListeners.clear();
|
|
4421
4498
|
adapter.dispose();
|
|
4422
4499
|
disconnectFixture();
|
|
4423
4500
|
void providerFiber?.dispose();
|
package/lib/image-intake.js
CHANGED
|
@@ -260,11 +260,16 @@ function publicRecord(record, now = Date.now()) {
|
|
|
260
260
|
updatedAt: record.updatedAt,
|
|
261
261
|
expiresInMs: Math.max(0, record.expiresAt - now),
|
|
262
262
|
result: record.result,
|
|
263
|
+
taskId: record.workflow?.taskId,
|
|
264
|
+
qccCommand: record.qccCommand,
|
|
263
265
|
error: record.error,
|
|
264
266
|
});
|
|
265
267
|
}
|
|
266
268
|
|
|
267
269
|
function safeFailure(error) {
|
|
270
|
+
if (['WorkflowError', 'QccBridgeError'].includes(error?.name)) {
|
|
271
|
+
return { code: error.code, message: error.message };
|
|
272
|
+
}
|
|
268
273
|
if (error instanceof ImageIntakeError && error.code !== 'DC_IMAGE_PROVIDER_FAILED') {
|
|
269
274
|
return { code: error.code, message: error.message };
|
|
270
275
|
}
|
|
@@ -281,6 +286,7 @@ export class ImageIntakeStore {
|
|
|
281
286
|
ttlMs = IMAGE_LIMITS.ttlMs,
|
|
282
287
|
pollMs = 500,
|
|
283
288
|
maxPolls = 60,
|
|
289
|
+
continueWorkflow = null,
|
|
284
290
|
} = {}) {
|
|
285
291
|
if (!tools || typeof tools.get !== 'function' || typeof tools.execute !== 'function') {
|
|
286
292
|
throw new TypeError('ImageIntakeStore requires ctx.tools get/execute');
|
|
@@ -291,6 +297,7 @@ export class ImageIntakeStore {
|
|
|
291
297
|
this.pollMs = pollMs;
|
|
292
298
|
this.maxPolls = maxPolls;
|
|
293
299
|
this.records = new Map();
|
|
300
|
+
this.continueWorkflow = continueWorkflow;
|
|
294
301
|
}
|
|
295
302
|
|
|
296
303
|
toolDefinition(name) {
|
|
@@ -506,7 +513,11 @@ export class ImageIntakeStore {
|
|
|
506
513
|
entries,
|
|
507
514
|
entryCount: entries.length,
|
|
508
515
|
truncated: entries.length >= IMAGE_LIMITS.maxEntries,
|
|
516
|
+
needsReview: /[�??]|无法辨认|识别不清|待确认/.test(text),
|
|
509
517
|
};
|
|
518
|
+
if (record.workflow && this.continueWorkflow) {
|
|
519
|
+
record.result.workflowResult = await this.continueWorkflow(record, record.result, exec);
|
|
520
|
+
}
|
|
510
521
|
record.state = 'completed';
|
|
511
522
|
record.error = null;
|
|
512
523
|
record.updatedAt = new Date(this.clock()).toISOString();
|
|
@@ -559,7 +570,7 @@ export function serializeImageExtractionPrompt(command) {
|
|
|
559
570
|
export function registerImageIntakeTool(tools, store) {
|
|
560
571
|
return tools.register({
|
|
561
572
|
name: TOOL_IMAGE_EXTRACT,
|
|
562
|
-
description: '
|
|
573
|
+
description: 'Execute one staged company-list image task using a visible dci-* commandId. The Host parses the image once. If the wizard bound an explicitly confirmed execute-on-send workflow, the same Agent-owned call continues quality checks, entity matching, selected-field enrichment and XLSX delivery. Ambiguous recognition or entity candidates require review; never call underlying MCP tools separately or repeat the task.',
|
|
563
574
|
parameters: {
|
|
564
575
|
type: 'object',
|
|
565
576
|
additionalProperties: false,
|
|
@@ -575,12 +586,16 @@ export function registerImageIntakeTool(tools, store) {
|
|
|
575
586
|
entries: { type: 'array', items: { type: 'string' } },
|
|
576
587
|
entryCount: { type: 'integer' },
|
|
577
588
|
truncated: { type: 'boolean' },
|
|
589
|
+
needsReview: { type: 'boolean' },
|
|
590
|
+
workflowResult: { type: 'object', additionalProperties: true },
|
|
578
591
|
},
|
|
579
592
|
required: ['commandId', 'entries', 'entryCount', 'truncated'],
|
|
580
593
|
},
|
|
581
594
|
render: (_args, value) => [{
|
|
582
595
|
type: 'text',
|
|
583
|
-
text:
|
|
596
|
+
text: value.workflowResult
|
|
597
|
+
? `图片识别及已确认任务已执行,识别 ${value.entryCount} 条。真实执行统计:${JSON.stringify(value.workflowResult)}。${value.workflowResult.reviewReason || '无需重复确认规则或再次发送;仅按真实异常提示处理。'}\n${(value.workflowResult.artifacts || []).map(a => `${String(a.fileName).replace(/[^a-zA-Z0-9\u4e00-\u9fff._ -]/g, '_')}:[预览 / 打开](${a.url}?preview=1) · [下载 Excel](${a.url})`).join('\n')}`
|
|
598
|
+
: `图片已识别 ${value.entryCount} 条,等待核验。此图片未绑定已确认的补全任务,请在「导入与核验」核验后配置后续处理;识别名单不代表已匹配。`,
|
|
584
599
|
}],
|
|
585
600
|
},
|
|
586
601
|
async execute(args, exec) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { WorkflowError } from './workflow.js';
|
|
2
|
+
|
|
3
|
+
// The wizard binds its confirmed revision. Execution reuses the original Agent token.
|
|
4
|
+
export function createImageWorkflow({ getWorkflow, execution, commands }) {
|
|
5
|
+
return async (record, result, exec) => {
|
|
6
|
+
const store = await getWorkflow();
|
|
7
|
+
let task = await store.require(record.workflow.taskId);
|
|
8
|
+
if (task.revision !== record.workflow.revision || task.state !== 'draft') {
|
|
9
|
+
throw new WorkflowError('DC_WORKFLOW_REVISION_CONFLICT', '图片任务配置已变更,请重新生成说明;未执行企业查询。', 409);
|
|
10
|
+
}
|
|
11
|
+
const rows = result.entries.map(value => ({ 主体标识: value }));
|
|
12
|
+
const mappings = [{ sourceField: '主体标识', targetField: 'company_name' }];
|
|
13
|
+
task = await store.recordUpload(task.id, { expectedRevision: task.revision,
|
|
14
|
+
source: { type: 'image', fileName: record.fileName, sizeBytes: record.sizeBytes,
|
|
15
|
+
rowCount: rows.length, columnCount: 1, headers: ['主体标识'] } });
|
|
16
|
+
task = await store.updateDraft(task.id, { expectedRevision: task.revision, mappings });
|
|
17
|
+
if (result.truncated || result.needsReview || rows.length > 100) {
|
|
18
|
+
return { taskId: task.id, deliveryState: 'review_required', artifacts: [],
|
|
19
|
+
reviewReason: rows.length > 100 || result.truncated ? '识别名单超出单批范围,请核验并拆分。' : '识别文本含模糊字符或待确认提示,请核验名单。' };
|
|
20
|
+
}
|
|
21
|
+
task = await store.confirmRules(task.id, { expectedRevision: task.revision, mappings });
|
|
22
|
+
task = await store.recordQuality(task.id, { expectedRevision: task.revision,
|
|
23
|
+
summary: { total: rows.length, valid: rows.length, missingAnchor: 0,
|
|
24
|
+
duplicates: rows.length - new Set(result.entries).size } });
|
|
25
|
+
const payload = await execution.prepare({ taskId: task.id, expectedRevision: task.revision,
|
|
26
|
+
kind: 'enrich', rows, headers: ['主体标识'], nameField: '主体标识' });
|
|
27
|
+
const command = commands.prepare(payload, { artifactOrigin: record.workflow.artifactOrigin });
|
|
28
|
+
record.qccCommand = { commandId: command.commandId, taskId: task.id };
|
|
29
|
+
return commands.run(command.commandId, exec);
|
|
30
|
+
};
|
|
31
|
+
}
|
package/lib/install-preflight.js
CHANGED
|
@@ -5,7 +5,7 @@ import { createRequire } from 'node:module';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { execFileSync } from 'node:child_process';
|
|
7
7
|
|
|
8
|
-
export function assessCombination({ host, sidebar, context }) {
|
|
8
|
+
export function assessCombination({ host, sidebar, context, workbench = false }) {
|
|
9
9
|
const errors = [], warnings = [];
|
|
10
10
|
if (host === '0.1.2-rc.1' && sidebar === '0.17.1') errors.push('DSH 0.1.2-rc.1 与 Better Sidebar 0.17.1 不兼容:缺少 settingsNamespace;请升级侧边栏到 0.18.1。');
|
|
11
11
|
if (!host) errors.push('无法识别实际 DSH CLI 版本;请先安装完整 DSH,再重新预检。');
|
|
@@ -14,15 +14,15 @@ export function assessCombination({ host, sidebar, context }) {
|
|
|
14
14
|
errors.push('Better Sidebar 0.18.1 不兼容此旧宿主。备份后由宿主管理者升级完整 @deepseek-ai/dsh@0.1.2-rc.1;不要只升级 Session 子包。');
|
|
15
15
|
} else warnings.push('此宿主与 Better Sidebar 0.18.1 组合未验证。');
|
|
16
16
|
}
|
|
17
|
-
if (!sidebar) errors.push('
|
|
18
|
-
else if (sidebar !== '0.18.1') warnings.push('此侧边栏版本未验证;仍需 targetedOpen 和 stateSubscription 能力。');
|
|
17
|
+
if (!sidebar) (workbench ? errors : warnings).push('未安装可选 dsh-better-sidebar:基础引擎、工具与原生会话可用;交互工作台不可用。如需工作台,请配套安装 0.18.1 与完整 DSH 0.1.2-rc.1。');
|
|
18
|
+
else if (sidebar !== '0.18.1' && !(host === '0.1.2-rc.1' && sidebar === '0.17.1')) warnings.push('此侧边栏版本未验证;仍需 targetedOpen 和 stateSubscription 能力。');
|
|
19
19
|
if (host === '0.1.2-rc.1' && context === '0.36.0') errors.push('已安装的 dsh-context 0.36.0 使用旧 settingsNamespace;先升级该插件到 0.48.0。context 并非本产品必装依赖。');
|
|
20
20
|
else if (context && context !== '0.48.0') warnings.push('已安装的 context 版本尚未做共存验证。');
|
|
21
21
|
if (host && host !== '0.1.2-rc.1' && !warnings.length && !errors.length) warnings.push('此宿主组合未验证。');
|
|
22
|
-
return { host, sidebar, context, errors, warnings, ok: errors.length === 0 };
|
|
22
|
+
return { host, sidebar, context, mode: workbench ? 'workbench' : 'base', errors, warnings, ok: errors.length === 0 };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export function inspectProfile(profile, host) {
|
|
25
|
+
export function inspectProfile(profile, host, workbench = false) {
|
|
26
26
|
const req = createRequire(resolve(profile, 'package.json'));
|
|
27
27
|
const version = name => {
|
|
28
28
|
let path;
|
|
@@ -33,16 +33,17 @@ export function inspectProfile(profile, host) {
|
|
|
33
33
|
if (dirname(dir) === dir) return null;
|
|
34
34
|
}
|
|
35
35
|
};
|
|
36
|
-
return assessCombination({ host, sidebar: version('dsh-better-sidebar'), context: version('dsh-context') });
|
|
36
|
+
return assessCombination({ host, sidebar: version('dsh-better-sidebar'), context: version('dsh-context'), workbench });
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
40
|
-
const
|
|
41
|
-
|
|
40
|
+
const positional = process.argv.slice(2).filter(arg => arg !== '--workbench');
|
|
41
|
+
const profile = positional[0];
|
|
42
|
+
if (!profile) { console.error('Usage: node lib/install-preflight.js <profile-directory> [dsh-executable] [--workbench]'); process.exitCode = 2; }
|
|
42
43
|
else {
|
|
43
44
|
let host = null;
|
|
44
|
-
try { host = execFileSync(
|
|
45
|
-
const report = inspectProfile(profile, host);
|
|
45
|
+
try { host = execFileSync(positional[1] || 'dsh', ['--version'], { encoding: 'utf8', timeout: 15000 }).match(/\b\d+\.\d+\.\d+(?:-[\w.]+)?\b/)?.[0] ?? null; } catch { /* reported below */ }
|
|
46
|
+
const report = inspectProfile(profile, host, process.argv.includes('--workbench'));
|
|
46
47
|
console.log(JSON.stringify(report, null, 2));
|
|
47
48
|
process.exitCode = report.ok ? 0 : 1;
|
|
48
49
|
}
|
package/lib/web.js
CHANGED
|
@@ -17,6 +17,7 @@ import { publicWorkflowContract } from './workflow-contract.js';
|
|
|
17
17
|
import { DataCleaningWorkflowStore, WorkflowError } from './workflow.js';
|
|
18
18
|
import { ArtifactError, WorkflowArtifactStore } from './artifacts.js';
|
|
19
19
|
import { createWorkflowExecution } from './workflow-execution.js';
|
|
20
|
+
import { createImageWorkflow } from './image-workflow.js';
|
|
20
21
|
import {
|
|
21
22
|
ImageIntakeError,
|
|
22
23
|
ImageIntakeStore,
|
|
@@ -304,10 +305,11 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
304
305
|
const skills = wctx.skills;
|
|
305
306
|
const disposers = [];
|
|
306
307
|
const qccBridge = new QccHostBridge({ tools, logger });
|
|
307
|
-
const imageIntake = new ImageIntakeStore({ tools });
|
|
308
308
|
const g5Runs = new G5RunStore();
|
|
309
309
|
const execution = createWorkflowExecution({ getWorkflow: () => getWorkflow(), artifacts: wctx.fs ? new WorkflowArtifactStore({ fs: wctx.fs }) : null });
|
|
310
310
|
const qccCommands = new QccCommandStore({ bridge: qccBridge, runs: g5Runs, lifecycle: execution });
|
|
311
|
+
const imageIntake = new ImageIntakeStore({ tools,
|
|
312
|
+
continueWorkflow: createImageWorkflow({ getWorkflow: () => getWorkflow(), execution, commands: qccCommands }) });
|
|
311
313
|
if (typeof tools?.register === 'function') {
|
|
312
314
|
disposers.push(registerQccCommandTool(tools, qccCommands));
|
|
313
315
|
disposers.push(registerImageIntakeTool(tools, imageIntake));
|
|
@@ -417,6 +419,26 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
417
419
|
command: imageIntake.status(decodeURIComponent(rest[0])),
|
|
418
420
|
});
|
|
419
421
|
}
|
|
422
|
+
if (rest.length === 1 && req.method === 'PATCH') {
|
|
423
|
+
const payload = JSON.parse((await readBody(req)).toString('utf8'));
|
|
424
|
+
const record = imageIntake.require(decodeURIComponent(rest[0]));
|
|
425
|
+
const task = await (await getWorkflow()).require(payload.taskId);
|
|
426
|
+
if (payload.executeOnSend !== true || record.state !== 'prepared' || task.state !== 'draft'
|
|
427
|
+
|| task.revision !== payload.expectedRevision || task.source?.type !== 'image'
|
|
428
|
+
|| task.source.fileName !== record.fileName || !task.fieldSelection.length) {
|
|
429
|
+
throw new ImageIntakeError('DC_IMAGE_CONFIG_CONFLICT', '图片或任务配置已变更,或未选择补全字段,请重新确认。', 409);
|
|
430
|
+
}
|
|
431
|
+
let artifactOrigin = '';
|
|
432
|
+
const requestHost = req.headers?.host;
|
|
433
|
+
if (typeof requestHost === 'string' && /^[a-zA-Z0-9.:[\]-]+$/.test(requestHost)) {
|
|
434
|
+
try {
|
|
435
|
+
const origin = new URL(req.headers.origin || `${req.socket?.encrypted ? 'https' : 'http'}://${requestHost}`);
|
|
436
|
+
if (['http:', 'https:'].includes(origin.protocol) && origin.host === requestHost) artifactOrigin = origin.origin;
|
|
437
|
+
} catch { /* Use relative artifact links. */ }
|
|
438
|
+
}
|
|
439
|
+
record.workflow = { taskId: task.id, revision: task.revision, artifactOrigin };
|
|
440
|
+
return writeJson(res, 200, { ok: true, command: imageIntake.status(record.commandId) });
|
|
441
|
+
}
|
|
420
442
|
if (rest.length === 1 && req.method === 'DELETE') {
|
|
421
443
|
await imageIntake.remove(decodeURIComponent(rest[0]));
|
|
422
444
|
return writeJson(res, 200, { ok: true, marker: 'data-cleaning-image-intake-v1', removed: true });
|
package/lib/workflow.js
CHANGED
|
@@ -149,7 +149,7 @@ export class DataCleaningWorkflowStore {
|
|
|
149
149
|
fieldSelection: draft.fieldSelection,
|
|
150
150
|
mappings: draft.mappings,
|
|
151
151
|
matchRules: draft.matchRules,
|
|
152
|
-
source: null,
|
|
152
|
+
source: input.source?.type === 'image' ? sanitizeSource({ ...input.source, rowCount: 0 }) : null,
|
|
153
153
|
qualitySummary: null,
|
|
154
154
|
matchSummary: null,
|
|
155
155
|
enrichmentSummary: null,
|
|
@@ -218,7 +218,8 @@ export class DataCleaningWorkflowStore {
|
|
|
218
218
|
mappings: input.mappings ?? record.mappings,
|
|
219
219
|
matchRules: input.matchRules ?? record.matchRules,
|
|
220
220
|
});
|
|
221
|
-
return draft
|
|
221
|
+
return { ...draft, ...(record.state === 'draft' && input.source?.type === 'image'
|
|
222
|
+
? { source: sanitizeSource({ ...input.source, rowCount: 0 }) } : {}) };
|
|
222
223
|
});
|
|
223
224
|
}
|
|
224
225
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-data-cleaning-agent",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Data cleaning and data enrichment for CSV/XLSX/JSON enterprise lists in DeepSeek Harness, including spreadsheet cleaning, deduplication, profiling, optional Qichacha MCP and exports.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -123,11 +123,17 @@
|
|
|
123
123
|
"xlsx": "^0.18.5"
|
|
124
124
|
},
|
|
125
125
|
"peerDependencies": {
|
|
126
|
+
"dsh-better-sidebar": "0.18.1",
|
|
126
127
|
"@deepseek-ai/dsh-client-ui-conversation": "*",
|
|
127
128
|
"@deepseek-ai/dsh-client-ui-layout": "*",
|
|
128
129
|
"react": "^18.2.0",
|
|
129
130
|
"react-dom": "^18.2.0"
|
|
130
131
|
},
|
|
132
|
+
"peerDependenciesMeta": {
|
|
133
|
+
"dsh-better-sidebar": {
|
|
134
|
+
"optional": true
|
|
135
|
+
}
|
|
136
|
+
},
|
|
131
137
|
"dsh": {
|
|
132
138
|
"bundle": {
|
|
133
139
|
"patch": "./cordis.patch.yml"
|