dsh-data-cleaning-agent 0.9.15 → 0.9.16
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 +7 -0
- package/README.en.md +3 -1
- package/README.md +4 -2
- package/docs/COMPATIBILITY.md +7 -0
- package/docs/UNIFIED-HOME-1.5.4-ADOPTION.md +22 -1
- package/lib/client.js +317 -98
- package/lib/skill-enrich.js +1 -0
- package/lib/skill.js +1 -0
- package/lib/web.js +16 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# 0.9.16 · 2026-09-15
|
|
2
|
+
|
|
3
|
+
- UX-49:仅清洗入口新建的独立 Session,通过 Host 原生草稿接口预填一次;空草稿、附件/引用、输入就绪、IME、草稿版本与晚到竞态二次核验,不自动发送、建业务任务或展开工作台。
|
|
4
|
+
- 用户编辑/主动清空、旧会话、刷新、重挂载和切回不补回、不清理草稿;保留 UX-48 接纳后开台与 settling→hero 品牌修复。
|
|
5
|
+
- 初始引导采用稳定模板 ID/版本和精确文本指纹;向导排除未改模板,修改后的内容不整体忽略,已有草稿提供替换/追加/取消,异步回填前重新核验。
|
|
6
|
+
- 未补占位符或缺名单/待补字段,在清洗业务 Skill 中优先澄清,不调 OAuth/OCR/MCP 或创建 workflow;不拦截 DSH 全局发送。
|
|
7
|
+
|
|
1
8
|
# 0.9.15 · 2026-09-14
|
|
2
9
|
|
|
3
10
|
- 保留 0.9.14 的 Host 驱动单向工作流、导航保护、导入核验和紧凑状态区功能。
|
package/README.en.md
CHANGED
|
@@ -10,7 +10,9 @@ 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.16** (stable release)
|
|
14
|
+
|
|
15
|
+
0.9.16 implements UX-49: a one-time editable native guide for new cleaning-entry Sessions, with existing text, attachment, IME and asynchronous-race guards. Clearing, refreshing or returning never restores it. Initialization does not send, call tools or open the workbench. Incomplete templates request the missing list and fields first. See the [adoption record](docs/UNIFIED-HOME-1.5.4-ADOPTION.md) for isolated DSH evidence and unverified boundaries.
|
|
14
16
|
|
|
15
17
|
0.9.15 releases the Host-driven forward-only workflow across import, rules, entity matching, enrichment, and download. Future stages provide explicit locked feedback, while completed stages remain viewable without business-state mutations. Imported rows stay visible for full review before the bottom confirmation advances to rules. The compact status area reports stage, source, time, scope, and explicit result counts. The 0.9.13 submission-admission, background-session isolation, and manual-collapse guarantees remain intact. Start a new cleaning Session after upgrading; legacy Session migration is outside this release's acceptance scope.
|
|
16
18
|
|
package/README.md
CHANGED
|
@@ -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.16
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
请先满足下文的 DSH、连接器及侧边栏依赖要求;安装后完整停止并重启对应 Profile。
|
|
@@ -27,7 +27,9 @@ dsh plugin --profile web add dsh-data-cleaning-agent@0.9.15
|
|
|
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.16**(正式版本)
|
|
31
|
+
|
|
32
|
+
0.9.16 实施 UX-49:首页仅为清洗入口新建的会话预填一次原生可编辑引导,保护已有文字、附件、中文组合输入与异步竞态。主动清空、刷新或切回不会补回;不自动发送、调用工具或展开工作台。未补全模板先澄清名单和字段。真实 DSH 隔离验收及未验证边界见[规范采用记录](docs/UNIFIED-HOME-1.5.4-ADOPTION.md)。
|
|
31
33
|
|
|
32
34
|
0.9.15 发布 Host 驱动的单向工作流:统一“导入与核验 → 规则与体检 → 主体匹配 → 字段补全 → 结果下载”五步导航。未来节点给出明确锁定反馈,历史节点保留查看但不挂载业务写操作;导入后先核验完整名单,再从表格底部确认进入规则页。紧凑状态区展示节点、来源、时间、任务范围和明确结果计数。0.9.13 的真实提交接纳、后台会话隔离与手动收起保护保持不变。升级后请新建清洗会话验证,旧 Session 迁移不在本版验收范围。
|
|
33
35
|
|
package/docs/COMPATIBILITY.md
CHANGED
|
@@ -206,3 +206,10 @@ Host 制品下载按钮。alpha.2 继续只检查 Host/路由/制品 Bridge,
|
|
|
206
206
|
XLSX 因此以 Base64 文本落盘。未来切换二进制 seam 必须保留旧 `v1` 制品读取兼容。
|
|
207
207
|
- v2 已生成制品可跨 Host 重启恢复,但浏览器 runtime 中尚未导出的原始行不会持久化;恢复中途任务
|
|
208
208
|
仍需用户重新提供输入。制品只在当前工作区,不承诺跨设备同步。
|
|
209
|
+
# UX-49 原生初始草稿兼容边界(0.9.16,2026-09-15)
|
|
210
|
+
|
|
211
|
+
- 采用记录见 `UNIFIED-HOME-1.5.4-ADOPTION.md` 的 v1.5.6 小节。仅新建清洗 Session 初始化,旧 Session/刷新/重挂载只恢复归属,不迁移或补回草稿。
|
|
212
|
+
- 本机核对 DSH 0.1.2-rc.1 的公开 `conversation.input.shell(sessionId)`:`snapshot.draft / draftRev / phase / imageIds / occurrences` 与 `setDraft`。缺失任一必要快照能力时保守跳过,不使用 DOM 空文本推断可写,不读取私有 editor。
|
|
213
|
+
- Host InputState 没有 IME 字段:使用 compositionstart/end 与入口异步期间的 beforeinput/input/paste/drop 事件,仅用于取消本插件初始化,不阻断默认事件、不修改其他产品。公开 shell 可在新 Session 的输入 DOM 挂载前使用;不把 hero/settling 或 session.blank 当草稿就绪证据。
|
|
214
|
+
- 未填写的模板发送由本业务 Skill 澄清;不改 Host submit/beginSubmission 全局语义。UX-48 原有 observed 接纳桥保持独立,草稿写入不产生业务接纳事件。
|
|
215
|
+
- 本轮不升级 DSH、不修改正式 Profile、不请求 QCC/OCR/模型凭据。无法验证的 Host 版本或真实模型决策明确保留为外部边界。
|
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
# DSH-UX-001
|
|
1
|
+
# DSH-UX-001 首页规范采用记录
|
|
2
|
+
|
|
3
|
+
## v1.5.6 / UX49-CLEAN(2026-09-15,0.9.16)
|
|
4
|
+
|
|
5
|
+
发布授权补充:用户于实施与 PR 验证后明确要求 commit、push、tag、npm,按仓库既有 CI、合并及 OIDC 发布流程推进 0.9.16;原实施阶段仅授权 PR 的边界作为历史记录保留。
|
|
6
|
+
|
|
7
|
+
依据共享 `DSH智能体开发交互规范方案.md` 的 UX-49、§11.1、§14,并核对 UX-48;协同清单 `DSH-UX-049-四智能体首页初始引导协同.md` 及总账同日交接。这里只记录采用与差异,不复制规范全文。
|
|
8
|
+
|
|
9
|
+
- 审计基线:remote main `fc7d57240b843e1a57998cf0ebbe4f39c086befb`,npm latest 0.9.15 / gitHead `6d8b37a7a0efae1aa2fddc72d61fc5e732f3231f`。原仓 main 的未提交 client/test 修改保持原样,另建独立分支。
|
|
10
|
+
- 已有正确项:独立清洗 Session namespace、宿主原生输入接口、实际接纳后开台、标题 settling→hero 修复、历史来源只读与主结果/报告分层。
|
|
11
|
+
- 真实缺陷:入口无条件 setDraft;普通会话异步恢复时按宽泛签名清理草稿;已有业务入口再次点击会开台;向导缺少已有内容确认与晚到写入检查。以上已收紧。
|
|
12
|
+
- 初始模板 `dsh-initial-draft/data-cleaning/1` / version 1,UTF-8 SHA-256 `2b84b86f669e64e970381e4c7ab8547c77d5e10ee7d5441543c893b9f1272e2b`。仅精确相等才排除;不 trim、不按前后缀忽略用户修改。
|
|
13
|
+
- 初始化仅由本入口成功创建的全新 ID 触发,在打开会话前操作未挂载的原生 shell,避免 Host setDraft 的选区写入抢占已挂载输入焦点。snapshot 缺失或非 plain、draft 非空、draftRev 非零、imageIds/occurrences 非空均跳过。建立一次性记录、订阅后重读、微任务后再次快照;期间用户输入/附件/组合事件、会话切换或卸载使写入失效,无轮询补回。
|
|
14
|
+
- 清洗业务 Skill 将信息充分性检查置于任何工具之前:占位符/缺名单或待补字段先澄清;没有注册全局发送拦截。安全凭证所指暂存来源仍可提供名单和范围,不重复要求上传。
|
|
15
|
+
- 向导按 UX-10 提供替换/追加/取消,默认保留用户内容;同一 Session 未改的向导草稿更新而非重复叠加。异步回填重读草稿版本、附件和当前业务归属。
|
|
16
|
+
- 本地门禁:`npm run check` 通过(296 项测试,语法/文档/营销/69 文件 pack 白名单);Chromium 浅深色 × 5 尺寸 10/10;XLSX 预览 390/800/1440px 3/3。包含附件、IME 事件、晚到修改/清空、卸载、A/B、普通 Session、Tab X/折叠恢复、草稿回填替换/追加/取消与零开台断言。
|
|
17
|
+
- 真实 DSH:`scripts/ux49-native-smoke.mjs`,独立 Profile、无模型/MCP 凭据。DSH 0.1.2-rc.1 + Sidebar 0.18.1 + 清洗候选/招投标 0.5.11/访前尽调 0.1.35/填表 0.2.30 共装通过。原生模板、正确 Hero 标题、不移动焦点、主动清空不补回、刷新保留修改、A/B/普通会话隔离、零业务 API mutation、零可见工作台、零浏览器异常均通过。
|
|
18
|
+
- 实机客户端 SHA-256 `bacfd1faa7eff584ddec3e948ee3f2e19f1d710224de9068d9a62415345a28a1`,测试包 SHA-256 `e93fac1541934a43bc54789283d57c378fbd721fe0d8fc86b922b2a69c5747c5`。原始报告/截图在 `/var/folders/ws/tmsn44b140lf8b89ml5qqjf00000gn/T/cleaning-ux49-native-HtQGy0/`;本地日志 `/private/tmp/ux49-{check,ui,artifact,native}-release-candidate.log`,UI 截图 `_scratch/ui-layout/`。测试包不是 npm 发布物;随后文档和验收脚本路径清理不改变已验收客户端字节。
|
|
19
|
+
- 未验证边界:真实模型执行占位符澄清(仅验证 Skill 优先规则,不冒充实际模型决策)、系统级真实中文输入法(已覆盖组合事件)、收费 QCC/OCR、多账号/其他 Host 版本,以及其余三产品各自 UX49 候选版本。正式 3080/Profile 未修改。
|
|
20
|
+
- 本次仅 commit/push/PR/CI;不合并、不升版本、不 tag/Release/npm publish。
|
|
21
|
+
|
|
22
|
+
## v1.5.4 历史采用记录
|
|
2
23
|
|
|
3
24
|
日期:2026-09-14。基线:0.9.11 / a0ec971db0d25d42f67f99e4328cca6f49a2bc2f。
|
|
4
25
|
本轮仅本地代码与验收,不发布,不修改正式 Profile 或全局 DSH。
|
package/lib/client.js
CHANGED
|
@@ -1605,19 +1605,104 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1605
1605
|
return () => { style.remove(); };
|
|
1606
1606
|
}
|
|
1607
1607
|
|
|
1608
|
-
// 原始数据不进入 Host storageDomain
|
|
1608
|
+
// 原始数据不进入 Host storageDomain 或模型上下文;仅在当前浏览器页签内按 taskId 隔离。
|
|
1609
|
+
// sessionStorage 只用于刷新恢复,关闭页签后不承诺保留;写入失败时降级为同源重新载入。
|
|
1610
|
+
const RUNTIME_SESSION_CACHE_KEY = 'dsh.data-cleaning-agent.runtime-tasks.v1';
|
|
1611
|
+
const RUNTIME_SESSION_CACHE_LIMIT = 20;
|
|
1612
|
+
const RUNTIME_SESSION_CACHE_MAX_ROWS = 5_000;
|
|
1613
|
+
const RUNTIME_SESSION_CACHE_MAX_CHARS = 2 * 1024 * 1024;
|
|
1609
1614
|
const runtimeTasks = new Map();
|
|
1610
1615
|
const workflowTaskBySession = new Map();
|
|
1611
1616
|
const workflowTaskCreationBySession = new Map();
|
|
1612
1617
|
const workflowOperationBySession = new Map();
|
|
1613
1618
|
|
|
1614
|
-
function
|
|
1619
|
+
function readRuntimeSessionCache() {
|
|
1620
|
+
try {
|
|
1621
|
+
const value = JSON.parse(window.sessionStorage?.getItem(RUNTIME_SESSION_CACHE_KEY) || '{}');
|
|
1622
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
1623
|
+
} catch { return {}; }
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
function runtimeFromSession(taskId, expectedChecksum = '') {
|
|
1615
1627
|
const key = String(taskId || 'unassigned');
|
|
1628
|
+
const value = readRuntimeSessionCache()[key];
|
|
1629
|
+
const rows = Array.isArray(value?.rows)
|
|
1630
|
+
? value.rows.filter((row) => row && typeof row === 'object' && !Array.isArray(row))
|
|
1631
|
+
: [];
|
|
1632
|
+
const checksum = String(value?.source?.checksum || '');
|
|
1633
|
+
if (!rows.length || !expectedChecksum || checksum !== expectedChecksum) return null;
|
|
1634
|
+
return {
|
|
1635
|
+
rows,
|
|
1636
|
+
sourceRows: rows,
|
|
1637
|
+
headers: Array.isArray(value.headers) ? value.headers.map(String) : [],
|
|
1638
|
+
source: value.source && typeof value.source === 'object' ? value.source : null,
|
|
1639
|
+
restoredFromSession: true,
|
|
1640
|
+
lastCsv: { clean: null, complete: null, qcc: null, review: null },
|
|
1641
|
+
resultRows: { clean: null, complete: null, qcc: null },
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function persistRuntimeSession(taskId, runtime, removeTaskId = null) {
|
|
1646
|
+
const rows = Array.isArray(runtime?.sourceRows) && runtime.sourceRows.length
|
|
1647
|
+
? runtime.sourceRows
|
|
1648
|
+
: Array.isArray(runtime?.rows) ? runtime.rows : [];
|
|
1649
|
+
if (!rows.length || rows.length > RUNTIME_SESSION_CACHE_MAX_ROWS
|
|
1650
|
+
|| typeof window.sessionStorage?.setItem !== 'function') return false;
|
|
1651
|
+
try {
|
|
1652
|
+
const key = String(taskId || 'unassigned');
|
|
1653
|
+
const cache = readRuntimeSessionCache();
|
|
1654
|
+
if (removeTaskId !== null) delete cache[String(removeTaskId || 'unassigned')];
|
|
1655
|
+
delete cache[key];
|
|
1656
|
+
cache[key] = {
|
|
1657
|
+
rows,
|
|
1658
|
+
headers: Array.isArray(runtime.headers) ? runtime.headers : [],
|
|
1659
|
+
source: runtime.source && typeof runtime.source === 'object' ? runtime.source : null,
|
|
1660
|
+
};
|
|
1661
|
+
const entries = Object.entries(cache).slice(-RUNTIME_SESSION_CACHE_LIMIT);
|
|
1662
|
+
while (entries.length) {
|
|
1663
|
+
const serialized = JSON.stringify(Object.fromEntries(entries));
|
|
1664
|
+
if (serialized.length <= RUNTIME_SESSION_CACHE_MAX_CHARS) {
|
|
1665
|
+
window.sessionStorage?.setItem(RUNTIME_SESSION_CACHE_KEY, serialized);
|
|
1666
|
+
return true;
|
|
1667
|
+
}
|
|
1668
|
+
entries.shift();
|
|
1669
|
+
}
|
|
1670
|
+
return false;
|
|
1671
|
+
} catch { return false; }
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function removeRuntimeSession(taskId) {
|
|
1675
|
+
try {
|
|
1676
|
+
const key = String(taskId || 'unassigned');
|
|
1677
|
+
const cache = readRuntimeSessionCache();
|
|
1678
|
+
if (!(key in cache)) return;
|
|
1679
|
+
delete cache[key];
|
|
1680
|
+
window.sessionStorage?.setItem(RUNTIME_SESSION_CACHE_KEY, JSON.stringify(cache));
|
|
1681
|
+
} catch {}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
function removeRuntime(taskId) {
|
|
1685
|
+
runtimeTasks.delete(String(taskId || 'unassigned'));
|
|
1686
|
+
removeRuntimeSession(taskId);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function runtimeFor(taskId, create = true, expectedChecksum = '') {
|
|
1690
|
+
const key = String(taskId || 'unassigned');
|
|
1691
|
+
const current = runtimeTasks.get(key);
|
|
1692
|
+
if (current && expectedChecksum && current.source?.checksum !== expectedChecksum) {
|
|
1693
|
+
runtimeTasks.delete(key);
|
|
1694
|
+
removeRuntimeSession(key);
|
|
1695
|
+
}
|
|
1696
|
+
if (!runtimeTasks.has(key)) {
|
|
1697
|
+
const restored = runtimeFromSession(key, expectedChecksum);
|
|
1698
|
+
if (restored) runtimeTasks.set(key, restored);
|
|
1699
|
+
}
|
|
1616
1700
|
if (!runtimeTasks.has(key) && create) {
|
|
1617
1701
|
runtimeTasks.set(key, {
|
|
1618
1702
|
rows: [],
|
|
1619
1703
|
headers: [],
|
|
1620
1704
|
source: null,
|
|
1705
|
+
restoredFromSession: false,
|
|
1621
1706
|
lastCsv: { clean: null, complete: null, qcc: null, review: null },
|
|
1622
1707
|
resultRows: { clean: null, complete: null, qcc: null },
|
|
1623
1708
|
});
|
|
@@ -1633,6 +1718,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1633
1718
|
target.sourceRows = from.sourceRows;
|
|
1634
1719
|
target.headers = from.headers;
|
|
1635
1720
|
target.source = from.source;
|
|
1721
|
+
target.restoredFromSession = from.restoredFromSession;
|
|
1636
1722
|
target.lastCsv = from.lastCsv;
|
|
1637
1723
|
target.resultRows = from.resultRows;
|
|
1638
1724
|
runtimeTasks.delete(String(fromId || 'unassigned'));
|
|
@@ -1781,7 +1867,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1781
1867
|
];
|
|
1782
1868
|
const DEFAULT_CLEANING_KEYS = ['clean_name', 'deduplicate', 'validate_identity', 'complete_fields'];
|
|
1783
1869
|
const DEFAULT_ENRICHMENT_KEYS = ['credit_no', 'legal_rep', 'reg_capital', 'establish_date', 'reg_status'];
|
|
1784
|
-
const DEFAULT_SESSION_PROMPT = '
|
|
1870
|
+
const DEFAULT_SESSION_PROMPT = '请帮我清洗并补全企业名单。请粘贴企业名称,或上传 Excel / 名单图片,并说明需要补全的字段;也可点击左上角「提示词生成」设置匹配与输出。例如:补全【企业名单】的统一社会信用代码、法定代表人和联系方式,结果导出为 Excel。';
|
|
1871
|
+
const INITIAL_DRAFT_TEMPLATE = Object.freeze({ id: 'dsh-initial-draft/data-cleaning/1', version: 1,
|
|
1872
|
+
fingerprint: 'sha256:2b84b86f669e64e970381e4c7ab8547c77d5e10ee7d5441543c893b9f1272e2b', text: DEFAULT_SESSION_PROMPT });
|
|
1873
|
+
const INITIAL_DRAFT_LEDGER = Symbol.for('dsh.data-cleaning.initial-draft.v1');
|
|
1874
|
+
const compositionState = document[Symbol.for('dsh.data-cleaning.composition.v1')] ||= { active: false };
|
|
1875
|
+
let initialDraftEpoch = 0;
|
|
1785
1876
|
const CLEANING_SESSION_STORAGE_KEYS = [
|
|
1786
1877
|
'dsh.data-cleaning-agent.active-session.v2',
|
|
1787
1878
|
'dsh.data-cleaning-agent.sessions.v1',
|
|
@@ -1827,28 +1918,17 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1827
1918
|
}
|
|
1828
1919
|
|
|
1829
1920
|
function isKnownCleaningDraft(value) {
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
const wizardDraft = value.startsWith('请执行一项企业名单数据清洗补全任务。')
|
|
1833
|
-
&& value.includes('企查查连接、套餐额度和费用均由当前用户自己的账号承担。')
|
|
1834
|
-
&& value.includes('提供结果和待复核清单的导出。');
|
|
1835
|
-
const executionDraft = value.startsWith('请执行已在「数据清洗补全工作台」确认的企业数据任务。')
|
|
1836
|
-
&& value.includes('安全任务凭证:dcq-')
|
|
1837
|
-
&& value.includes('data_cleaning_qcc_run');
|
|
1838
|
-
const imageDraft = value.startsWith('请识别我刚刚在向导中安全暂存的企业名单图片')
|
|
1839
|
-
&& value.includes('安全图片凭证:dci-')
|
|
1840
|
-
&& value.includes('data_cleaning_extract_image_companies');
|
|
1841
|
-
return wizardDraft || executionDraft || imageDraft;
|
|
1921
|
+
// Exact text is the fingerprint: no trimming, substring or fuzzy recognition.
|
|
1922
|
+
return value === INITIAL_DRAFT_TEMPLATE.text;
|
|
1842
1923
|
}
|
|
1843
1924
|
|
|
1844
|
-
function
|
|
1845
|
-
if (
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
return true;
|
|
1925
|
+
function wizardListEntries(value) {
|
|
1926
|
+
if (isKnownCleaningDraft(value)) return [];
|
|
1927
|
+
return String(value || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
function invalidListInstruction(values) {
|
|
1931
|
+
return values.some((value) => /【[^】]*】|^(?:请|例如[::]|输入来源[::]|结果导出|需要补全)/u.test(value));
|
|
1852
1932
|
}
|
|
1853
1933
|
|
|
1854
1934
|
function deactivateCleaningSession(sessionId = activeCleaningSessionId) {
|
|
@@ -1889,43 +1969,19 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1889
1969
|
*/
|
|
1890
1970
|
function installSessionOwnershipBridge(ctx) {
|
|
1891
1971
|
const releaseNewSessionBridge = installNewSessionBridge(ctx);
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
const
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
const cleared = clearCleaningDraft(ctx, currentSessionId, true);
|
|
1898
|
-
if (cleared && staleDraftObserver) {
|
|
1899
|
-
staleDraftObserver.disconnect();
|
|
1900
|
-
staleDraftObserver = null;
|
|
1901
|
-
}
|
|
1902
|
-
return cleared;
|
|
1903
|
-
};
|
|
1972
|
+
// Observe composition only; never read/write another product's draft.
|
|
1973
|
+
const compositionStart = () => { compositionState.active = true; };
|
|
1974
|
+
const compositionEnd = () => { compositionState.active = false; };
|
|
1975
|
+
document.addEventListener?.('compositionstart', compositionStart, true);
|
|
1976
|
+
document.addEventListener?.('compositionend', compositionEnd, true);
|
|
1904
1977
|
const leaveSubsystem = () => {
|
|
1905
1978
|
const sessionId = activeCleaningSessionId;
|
|
1906
|
-
if (sessionId && !ownedSessionId(sessionId)) clearCleaningDraft(ctx, sessionId, true);
|
|
1907
1979
|
deactivateCleaningSession(sessionId);
|
|
1908
1980
|
};
|
|
1909
1981
|
// Only our independently-created native session namespace may regain ownership.
|
|
1910
1982
|
const initialSession = ctx.sessions?.list?.getSnapshot?.()?.current;
|
|
1911
1983
|
if (ownedSessionId(initialSession)) markCleaningSession(initialSession);
|
|
1912
|
-
|
|
1913
|
-
// DSH 会在插件 apply() 之后异步恢复 composer draft。在首屏稳定期内
|
|
1914
|
-
// 监听 DOM 变化并仅清理完全匹配本插件默认文案,或同时命中
|
|
1915
|
-
// 提示词向导固定首尾与费用声明签名的草稿;不触碰用户自写内容。
|
|
1916
|
-
if (typeof MutationObserver === 'function' && document.documentElement) {
|
|
1917
|
-
staleDraftObserver = new MutationObserver(clearStaleDefaultDraft);
|
|
1918
|
-
staleDraftObserver.observe(document.documentElement, {
|
|
1919
|
-
childList: true,
|
|
1920
|
-
characterData: true,
|
|
1921
|
-
subtree: true,
|
|
1922
|
-
});
|
|
1923
|
-
staleDraftObserverTimer = setTimeout(() => {
|
|
1924
|
-
staleDraftObserver?.disconnect();
|
|
1925
|
-
staleDraftObserver = null;
|
|
1926
|
-
staleDraftObserverTimer = null;
|
|
1927
|
-
}, 10_000);
|
|
1928
|
-
}
|
|
1984
|
+
// Restore ownership only. Refresh/remount/navigation must never seed or clear drafts.
|
|
1929
1985
|
const unsubscribe = ctx.sessions?.list?.subscribe?.((snapshot) => {
|
|
1930
1986
|
// Store subscriptions may signal without a payload; read authoritative state.
|
|
1931
1987
|
const selected = ctx.sessions?.list?.getSnapshot?.() ?? snapshot;
|
|
@@ -1936,14 +1992,13 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1936
1992
|
}
|
|
1937
1993
|
if (!activeCleaningSessionId && ownedSessionId(nextSessionId)) {
|
|
1938
1994
|
markCleaningSession(nextSessionId);
|
|
1939
|
-
} else if (!activeCleaningSessionId) {
|
|
1940
|
-
clearStaleDefaultDraft();
|
|
1941
1995
|
}
|
|
1942
1996
|
});
|
|
1943
1997
|
return () => {
|
|
1998
|
+
initialDraftEpoch++;
|
|
1944
1999
|
releaseNewSessionBridge();
|
|
1945
|
-
|
|
1946
|
-
|
|
2000
|
+
document.removeEventListener?.('compositionstart', compositionStart, true);
|
|
2001
|
+
document.removeEventListener?.('compositionend', compositionEnd, true);
|
|
1947
2002
|
if (typeof unsubscribe === 'function') unsubscribe();
|
|
1948
2003
|
};
|
|
1949
2004
|
}
|
|
@@ -2406,6 +2461,32 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2406
2461
|
return cacheWorkflowTask(actions, sessionId, response.task, reason);
|
|
2407
2462
|
}
|
|
2408
2463
|
|
|
2464
|
+
async function verifyRestoredRuntime(actions, sessionId, task, runtime) {
|
|
2465
|
+
if (!runtime?.restoredFromSession) return runtime;
|
|
2466
|
+
if (!task?.id || !task.source?.checksum || !runtime.sourceRows?.length) {
|
|
2467
|
+
if (task?.id) removeRuntime(task.id);
|
|
2468
|
+
actions.setDataset?.(null);
|
|
2469
|
+
throw new Error('刷新缓存缺少同源校验信息,请重新载入原文件或原文本。');
|
|
2470
|
+
}
|
|
2471
|
+
const response = await requestJson(
|
|
2472
|
+
`/data-cleaning/api/workflow/tasks/${encodeURIComponent(task.id)}/verify-source`,
|
|
2473
|
+
'POST',
|
|
2474
|
+
{
|
|
2475
|
+
originSessionId: sessionId,
|
|
2476
|
+
originWorkspaceId: task.originWorkspaceId,
|
|
2477
|
+
headers: runtime.headers,
|
|
2478
|
+
rows: runtime.sourceRows,
|
|
2479
|
+
},
|
|
2480
|
+
);
|
|
2481
|
+
if (response.verified !== true) {
|
|
2482
|
+
removeRuntime(task.id);
|
|
2483
|
+
actions.setDataset?.(null);
|
|
2484
|
+
throw new Error('刷新缓存与原任务不一致,已停止本次操作。请重新载入原文件或新建任务。');
|
|
2485
|
+
}
|
|
2486
|
+
runtime.restoredFromSession = false;
|
|
2487
|
+
return runtime;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2409
2490
|
async function stageConfirmedWorkflow(actions, sessionId, expected = null) {
|
|
2410
2491
|
const host = await requestJson('/data-cleaning/api/g5/capabilities');
|
|
2411
2492
|
if (host?.workflowExecutionVersion !== 1) throw new Error('页面与 Host 版本不一致。请保存任务后升级并完整重启 DSH;当前 Host 不支持自动生成结果文件。');
|
|
@@ -2427,9 +2508,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2427
2508
|
throw new Error('工作台保存的范围与向导不一致,请重试确认;未生成执行凭证。');
|
|
2428
2509
|
}
|
|
2429
2510
|
}
|
|
2430
|
-
const runtime = runtimeFor(task.id, false);
|
|
2511
|
+
const runtime = runtimeFor(task.id, false, task.source?.checksum || '');
|
|
2431
2512
|
if (!task.fieldSelection?.length) throw new Error('当前未选择企查查补全字段。请在规则页选择补全字段,或使用工作台本地清洗与结果导出;不会发起企查查调用。');
|
|
2432
2513
|
if (!runtime?.rows?.length) throw new Error('Host 名单尚未载入,请先解析数据。');
|
|
2514
|
+
await verifyRestoredRuntime(actions, sessionId, task, runtime);
|
|
2433
2515
|
if (task.state === 'uploaded') task = await workflowAction(actions, sessionId, task, 'rules', {
|
|
2434
2516
|
objectives: task.objectives, fieldSelection: task.fieldSelection, mappings: task.mappings, matchRules: task.matchRules,
|
|
2435
2517
|
});
|
|
@@ -2725,13 +2807,45 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2725
2807
|
return `${CLEANING_SESSION_ID_PREFIX}${globalThis.crypto.randomUUID()}`;
|
|
2726
2808
|
}
|
|
2727
2809
|
|
|
2810
|
+
function emptyInitialInput(snapshot) {
|
|
2811
|
+
// Missing Host capabilities fail closed. draftRev also detects type-then-clear.
|
|
2812
|
+
return snapshot?.phase === 'plain' && snapshot.draft === ''
|
|
2813
|
+
&& snapshot.draftRev === 0 && Array.isArray(snapshot.imageIds) && snapshot.imageIds.length === 0
|
|
2814
|
+
&& Array.isArray(snapshot.occurrences) && snapshot.occurrences.length === 0;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
async function initializeCleaningDraft(ctx, sessionId, entry) {
|
|
2818
|
+
const ledger = document[INITIAL_DRAFT_LEDGER] ||= new Map();
|
|
2819
|
+
if (!ownedSessionId(sessionId) || entry?.createdId !== sessionId || ledger.has(sessionId)) return false;
|
|
2820
|
+
// Reserve before yielding: no duplicate initialization, even on plugin remount.
|
|
2821
|
+
ledger.set(sessionId, { templateId: INITIAL_DRAFT_TEMPLATE.id, state: 'skipped' });
|
|
2822
|
+
const conversation = typeof ctx.get === 'function' ? ctx.get('conversation') : ctx.conversation;
|
|
2823
|
+
const shell = conversation?.input?.shell?.(sessionId);
|
|
2824
|
+
const eligible = () => !compositionState.active && !entry.interrupted()
|
|
2825
|
+
&& ctx.sessions.list.getSnapshot().current === entry.previousSession
|
|
2826
|
+
&& typeof shell?.setDraft === 'function' && typeof shell.state?.subscribe === 'function'
|
|
2827
|
+
&& emptyInitialInput(shell.snapshot);
|
|
2828
|
+
if (!eligible()) return false;
|
|
2829
|
+
const revision = shell.snapshot.draftRev;
|
|
2830
|
+
let changed = false;
|
|
2831
|
+
const release = shell.state?.subscribe?.(() => { if (!emptyInitialInput(shell.snapshot)) changed = true; });
|
|
2832
|
+
try {
|
|
2833
|
+
await Promise.resolve();
|
|
2834
|
+
// Authoritative second snapshot, not captured render props. No await between CAS and write.
|
|
2835
|
+
if (changed || !eligible() || shell.snapshot.draftRev !== revision) return false;
|
|
2836
|
+
shell.setDraft(INITIAL_DRAFT_TEMPLATE.text);
|
|
2837
|
+
const accepted = shell.snapshot?.draft === INITIAL_DRAFT_TEMPLATE.text;
|
|
2838
|
+
ledger.set(sessionId, { templateId: INITIAL_DRAFT_TEMPLATE.id, state: accepted ? 'initialized' : 'skipped' });
|
|
2839
|
+
return accepted;
|
|
2840
|
+
} finally { if (typeof release === 'function') release(); }
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2728
2843
|
/** 使用 DSH 原生工作区、会话和输入机打开中央对话,不构造第二套聊天界面。 */
|
|
2729
2844
|
async function startCleaningSession(ctx) {
|
|
2730
2845
|
const workspace = ctx.workspaces.list.getSnapshot();
|
|
2731
2846
|
const current = ctx.sessions.list.getSnapshot().current;
|
|
2732
2847
|
if (ownedSessionId(current)) {
|
|
2733
2848
|
markCleaningSession(current);
|
|
2734
|
-
requestWorkbenchOpen(workflowTaskBySession.get(String(current))?.stage || 'upload', current);
|
|
2735
2849
|
return current;
|
|
2736
2850
|
}
|
|
2737
2851
|
const items = Array.isArray(workspace.items) ? workspace.items : [];
|
|
@@ -2752,16 +2866,34 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2752
2866
|
throw new Error('当前 DSH 版本没有可用的独立会话创建能力');
|
|
2753
2867
|
}
|
|
2754
2868
|
const requestedId = createCleaningSessionId();
|
|
2755
|
-
const
|
|
2756
|
-
|
|
2757
|
-
|
|
2869
|
+
const entryEpoch = initialDraftEpoch;
|
|
2870
|
+
let interrupted = compositionState.active;
|
|
2871
|
+
const interrupt = () => { interrupted = true; };
|
|
2872
|
+
const events = ['beforeinput', 'input', 'paste', 'drop', 'compositionstart'];
|
|
2873
|
+
for (const event of events) document.addEventListener?.(event, interrupt, true);
|
|
2874
|
+
const stopNavigation = ctx.sessions.list.subscribe?.(() => {
|
|
2875
|
+
if (ctx.sessions.list.getSnapshot().current !== current) interrupt();
|
|
2876
|
+
});
|
|
2877
|
+
let sessionId;
|
|
2878
|
+
try {
|
|
2879
|
+
sessionId = await ctx.sessions.create({ workspaceId: targetWorkspace.workspaceId, sessionId: requestedId });
|
|
2880
|
+
if (sessionId !== requestedId) {
|
|
2881
|
+
throw new Error('当前 DSH 版本不支持独立会话创建,请升级 DSH 后再打开数据清洗补全智能体');
|
|
2882
|
+
}
|
|
2883
|
+
await initializeCleaningDraft(ctx, sessionId, {
|
|
2884
|
+
createdId: requestedId, previousSession: current,
|
|
2885
|
+
interrupted: () => interrupted || entryEpoch !== initialDraftEpoch,
|
|
2886
|
+
});
|
|
2887
|
+
// A late create must not bring the user back from another Session/product.
|
|
2888
|
+
if (!interrupted && entryEpoch === initialDraftEpoch && ctx.sessions.list.getSnapshot().current === current) {
|
|
2889
|
+
ctx.sessions.open(sessionId);
|
|
2890
|
+
markCleaningSession(sessionId);
|
|
2891
|
+
}
|
|
2892
|
+
return sessionId;
|
|
2893
|
+
} finally {
|
|
2894
|
+
if (typeof stopNavigation === 'function') stopNavigation();
|
|
2895
|
+
for (const event of events) document.removeEventListener?.(event, interrupt, true);
|
|
2758
2896
|
}
|
|
2759
|
-
const conversation = typeof ctx.get === 'function' ? ctx.get('conversation') : ctx.conversation;
|
|
2760
|
-
if (!conversation) throw new Error('DSH 对话服务尚未就绪,请稍后重试');
|
|
2761
|
-
conversation.input.shell(sessionId).setDraft(DEFAULT_SESSION_PROMPT);
|
|
2762
|
-
ctx.sessions.open(sessionId);
|
|
2763
|
-
markCleaningSession(sessionId);
|
|
2764
|
-
return sessionId;
|
|
2765
2897
|
}
|
|
2766
2898
|
|
|
2767
2899
|
/**
|
|
@@ -3385,6 +3517,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3385
3517
|
return rows.map((row) => {
|
|
3386
3518
|
const name = nameField ? String(row?.[nameField] ?? '').trim() : '';
|
|
3387
3519
|
const credit = creditField ? String(row?.[creditField] ?? '').trim() : '';
|
|
3520
|
+
if (isKnownCleaningDraft(name)) return '';
|
|
3521
|
+
if (invalidListInstruction([name, credit])) {
|
|
3522
|
+
throw new Error('名单包含占位符、示例或任务说明,请提供实际企业名称或信用代码后再继续。');
|
|
3523
|
+
}
|
|
3388
3524
|
return [name, credit].filter(Boolean).join(' | ');
|
|
3389
3525
|
}).filter(Boolean).slice(0, 50);
|
|
3390
3526
|
}
|
|
@@ -3465,6 +3601,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3465
3601
|
});
|
|
3466
3602
|
const [busy, setBusy] = react.useState(false);
|
|
3467
3603
|
const [error, setError] = react.useState(null);
|
|
3604
|
+
const [draftPolicy, setDraftPolicy] = react.useState('cancel');
|
|
3605
|
+
const lastWizardDraft = react.useRef(null);
|
|
3606
|
+
|
|
3607
|
+
react.useEffect(() => { setDraftPolicy('cancel'); lastWizardDraft.current = null; }, [sessionId]);
|
|
3468
3608
|
|
|
3469
3609
|
react.useEffect(() => {
|
|
3470
3610
|
const commandId = imageCommand?.commandId;
|
|
@@ -3625,11 +3765,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3625
3765
|
}
|
|
3626
3766
|
};
|
|
3627
3767
|
const selectedEntries = () => mode === 'text'
|
|
3628
|
-
? rawText
|
|
3768
|
+
? wizardListEntries(rawText)
|
|
3629
3769
|
: entries;
|
|
3630
3770
|
const validateSource = () => {
|
|
3631
3771
|
const values = selectedEntries();
|
|
3632
3772
|
if (mode === 'text' && !values.length) return '请先录入企业名称或统一社会信用代码,每行一条。';
|
|
3773
|
+
if (mode === 'text' && invalidListInstruction(values)) return '请提供实际企业名单,每行一个名称或信用代码;请勿将【企业名单】、示例或任务说明作为企业。';
|
|
3633
3774
|
if (mode === 'text' && values.some(isImagePathEntry)) return '检测到图片文件路径,不是企业名单。请切换「上传图片识别」,重新选择或粘贴图片。';
|
|
3634
3775
|
if (mode === 'excel' && !values.length) return '请先上传并解析 Excel / CSV / JSON 文件。';
|
|
3635
3776
|
if (mode === 'image' && !imageCommand) return '请先选择、粘贴或拖入一张包含企业名单的图片。';
|
|
@@ -3661,16 +3802,26 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3661
3802
|
return;
|
|
3662
3803
|
}
|
|
3663
3804
|
const inputSnapshot = getInputSnapshot?.(sessionId);
|
|
3664
|
-
if (
|
|
3805
|
+
if (!inputSnapshot || inputSnapshot.phase !== 'plain' || compositionState.active) {
|
|
3665
3806
|
setError('当前对话正在提交,请等待完成后再回填。图片与字段选择已保留。');
|
|
3666
3807
|
return;
|
|
3667
3808
|
}
|
|
3809
|
+
const unchangedWizard = lastWizardDraft.current?.sessionId === sessionId
|
|
3810
|
+
&& lastWizardDraft.current.draft === inputSnapshot.draft;
|
|
3811
|
+
const userDraft = inputSnapshot.draft && !isKnownCleaningDraft(inputSnapshot.draft) && !unchangedWizard;
|
|
3812
|
+
if (userDraft && draftPolicy === 'cancel') {
|
|
3813
|
+
setError('已保留当前草稿。请选择“替换”或“追加”后回填,或关闭向导取消。'); return;
|
|
3814
|
+
}
|
|
3815
|
+
const prefix = unchangedWizard ? lastWizardDraft.current.prefix
|
|
3816
|
+
: userDraft && draftPolicy === 'append' ? inputSnapshot.draft + '\n\n' : '';
|
|
3668
3817
|
const config = promptConfig();
|
|
3669
3818
|
if (mode === 'image' && imageCommand?.state === 'prepared' && imageAttachment && typeof removeImage === 'function') {
|
|
3670
3819
|
// qcc-document-mcp 只读取 Host 暂存文件;发送前释放原生附件,文本模型不会收到图片。
|
|
3671
3820
|
removeImage(sessionId, imageAttachment);
|
|
3672
3821
|
setImageAttachment(null);
|
|
3673
3822
|
}
|
|
3823
|
+
const attachmentSnapshot = JSON.stringify({imageIds:getInputSnapshot?.(sessionId)?.imageIds,
|
|
3824
|
+
occurrences:getInputSnapshot?.(sessionId)?.occurrences});
|
|
3674
3825
|
setBusy(true);
|
|
3675
3826
|
try {
|
|
3676
3827
|
if (mode === 'text') {
|
|
@@ -3697,9 +3848,18 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3697
3848
|
prompt = mode === 'image' && imageCommand?.state === 'prepared' ? command.prompt : command.prompt + '\n\n' + buildTaskPrompt(config);
|
|
3698
3849
|
if (mode === 'image' && imageCommand?.state === 'prepared') setImageCommand(command);
|
|
3699
3850
|
}
|
|
3851
|
+
const latest = getInputSnapshot?.(sessionId);
|
|
3852
|
+
if (!isCleaningSession(sessionId) || compositionState.active || latest?.phase !== 'plain'
|
|
3853
|
+
|| latest.draft !== inputSnapshot.draft || latest.draftRev !== inputSnapshot.draftRev
|
|
3854
|
+
|| JSON.stringify({imageIds:latest.imageIds,occurrences:latest.occurrences}) !== attachmentSnapshot) {
|
|
3855
|
+
throw new Error('草稿或会话已变化,未覆盖您的内容;请确认后再次回填。');
|
|
3856
|
+
}
|
|
3857
|
+
prompt = prefix + prompt;
|
|
3700
3858
|
inputActions.setDraft(prompt);
|
|
3701
3859
|
const updatedInput = getInputSnapshot?.(sessionId);
|
|
3702
3860
|
if (updatedInput && updatedInput.draft !== prompt) throw new Error('当前对话尚未接收任务说明,请稍后重试回填。');
|
|
3861
|
+
lastWizardDraft.current = {sessionId,draft:prompt,prefix};
|
|
3862
|
+
setDraftPolicy('cancel');
|
|
3703
3863
|
} catch (draftError) {
|
|
3704
3864
|
setError(draftError instanceof Error ? draftError.message : String(draftError));
|
|
3705
3865
|
return;
|
|
@@ -3990,6 +4150,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3990
4150
|
wizardStep === 4 ? h('div', { className: 'dcAgentWizardPane' },
|
|
3991
4151
|
h('h4', null, '确认任务描述'),
|
|
3992
4152
|
h('p', null, '回填后仍可在对话框继续编辑;回填本身不会调用企查查 MCP。'),
|
|
4153
|
+
h('label', null, '已有草稿时的回填方式 ',
|
|
4154
|
+
h('select', { 'aria-label':'已有草稿时的回填方式', value:draftPolicy, onChange:event=>setDraftPolicy(event.target.value) },
|
|
4155
|
+
h('option', {value:'cancel'}, '保留草稿(取消回填)'),
|
|
4156
|
+
h('option', {value:'replace'}, '替换已有草稿'),
|
|
4157
|
+
h('option', {value:'append'}, '追加到已有草稿'))),
|
|
3993
4158
|
h('pre', { className: 'dcAgentPromptPreview' }, buildTaskPrompt(promptConfig())),
|
|
3994
4159
|
) : null,
|
|
3995
4160
|
h('p', { className: 'dcAgentPromptNote' }, '向导只生成任务描述并保存任务设置。只有你在任务中明确确认后,才会使用当前客户自己的企查查 MCP 连接、账号和额度。'),
|
|
@@ -4185,6 +4350,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4185
4350
|
sizeBytes: source.sizeBytes || 0,
|
|
4186
4351
|
checksum: result.checksum || source.checksum || '',
|
|
4187
4352
|
};
|
|
4353
|
+
runtime.restoredFromSession = false;
|
|
4188
4354
|
const mappings = guessMappings(runtime.headers);
|
|
4189
4355
|
const guessedNameField = mappings.find((mapping) => mapping.targetField === 'company_name')?.sourceField
|
|
4190
4356
|
?? mappings.find((mapping) => mapping.targetField === 'credit_no')?.sourceField
|
|
@@ -4214,6 +4380,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4214
4380
|
sizeBytes: source.sizeBytes || task.source?.sizeBytes || 0,
|
|
4215
4381
|
checksum: result.checksum || '',
|
|
4216
4382
|
};
|
|
4383
|
+
runtime.restoredFromSession = false;
|
|
4217
4384
|
actions.setDataset({
|
|
4218
4385
|
fmt: result.fmt ?? task.source?.type ?? 'csv',
|
|
4219
4386
|
headers: runtime.headers,
|
|
@@ -4222,6 +4389,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4222
4389
|
});
|
|
4223
4390
|
const primary = (task.mappings || []).find((mapping) => ['company_name', 'credit_no', 'reg_no'].includes(mapping.targetField));
|
|
4224
4391
|
if (primary) actions.setNameField(primary.sourceField);
|
|
4392
|
+
persistRuntimeSession(task.id, runtime);
|
|
4225
4393
|
return runtime;
|
|
4226
4394
|
}
|
|
4227
4395
|
|
|
@@ -4244,6 +4412,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4244
4412
|
sizeBytes: source.sizeBytes || 0,
|
|
4245
4413
|
checksum: result.checksum || source.checksum || '',
|
|
4246
4414
|
};
|
|
4415
|
+
target.restoredFromSession = false;
|
|
4247
4416
|
if (['draft', 'uploaded', 'parse_failed'].includes(current.state)) {
|
|
4248
4417
|
current = await workflowAction(actions, sessionId, current, 'upload', {
|
|
4249
4418
|
source: {
|
|
@@ -4258,6 +4427,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4258
4427
|
}, 'import');
|
|
4259
4428
|
}
|
|
4260
4429
|
if (current.state === 'uploaded') current = await updateWorkflowTask(actions, sessionId, current, importedDraft);
|
|
4430
|
+
persistRuntimeSession(current.id, target, stagingKey);
|
|
4261
4431
|
return current;
|
|
4262
4432
|
}
|
|
4263
4433
|
|
|
@@ -4443,7 +4613,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4443
4613
|
const objectives = useStore((state) => state.objectives);
|
|
4444
4614
|
const fieldSelection = useStore((state) => state.fieldSelection);
|
|
4445
4615
|
const matchRules = useStore((state) => state.matchRules);
|
|
4446
|
-
const
|
|
4616
|
+
const storedDataset = useStore((state) => state.dataset);
|
|
4447
4617
|
const profile = useStore((state) => state.profile);
|
|
4448
4618
|
const clean = useStore((state) => state.clean);
|
|
4449
4619
|
const complete = useStore((state) => state.complete);
|
|
@@ -4455,11 +4625,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4455
4625
|
const paidConfirmed = useStore((state) => state.paidConfirmed);
|
|
4456
4626
|
const activeSessionId = useStore((state) => state.activeSessionId);
|
|
4457
4627
|
const [fieldQuery, setFieldQuery] = react.useState('');
|
|
4458
|
-
const [intakeEditorOpen, setIntakeEditorOpen] = react.useState(() =>
|
|
4628
|
+
const [intakeEditorOpen, setIntakeEditorOpen] = react.useState(() => storedDataset == null);
|
|
4459
4629
|
|
|
4460
4630
|
react.useEffect(() => {
|
|
4461
|
-
setIntakeEditorOpen(
|
|
4462
|
-
}, [
|
|
4631
|
+
setIntakeEditorOpen(storedDataset == null);
|
|
4632
|
+
}, [storedDataset]);
|
|
4463
4633
|
|
|
4464
4634
|
react.useEffect(() => {
|
|
4465
4635
|
if (!open || !visible) return;
|
|
@@ -4514,13 +4684,19 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4514
4684
|
|
|
4515
4685
|
// Host 决定 Tab 挂载/可见性;内容不再按私有 open flag 隐藏。
|
|
4516
4686
|
|
|
4517
|
-
const hasData = dataset !== null && dataset.rowCount > 0;
|
|
4518
|
-
const requiresQcc = objectives.includes('validate_identity') || objectives.includes('complete_fields');
|
|
4519
4687
|
const cachedTask = workflowTaskBySession.get(String(activeSessionId || 'unassigned')) ?? workflowTask;
|
|
4520
4688
|
const runtimeKey = cachedTask?.id ?? `session:${activeSessionId || 'unassigned'}`;
|
|
4521
|
-
const runtime = runtimeFor(runtimeKey);
|
|
4689
|
+
const runtime = runtimeFor(runtimeKey, true, cachedTask?.source?.checksum || '');
|
|
4522
4690
|
const stagedSource = runtimeFor(`session:${activeSessionId || 'unassigned'}`, false);
|
|
4523
4691
|
const sourceRows = stagedSource?.sourceRows ?? runtime.sourceRows ?? runtime.rows;
|
|
4692
|
+
const dataset = storedDataset ?? (sourceRows.length ? {
|
|
4693
|
+
fmt: runtime.source?.type || cachedTask?.source?.type || 'csv',
|
|
4694
|
+
headers: runtime.headers.length ? runtime.headers : cachedTask?.source?.headers || [],
|
|
4695
|
+
rowCount: cachedTask?.source?.rowCount ?? sourceRows.length,
|
|
4696
|
+
preview: sourceRows.slice(0, 5),
|
|
4697
|
+
} : null);
|
|
4698
|
+
const hasData = dataset !== null && dataset.rowCount > 0;
|
|
4699
|
+
const requiresQcc = objectives.includes('validate_identity') || objectives.includes('complete_fields');
|
|
4524
4700
|
const intake = intakeState(dataset, sourceRows, input, runtime.source || stagedSource?.source || cachedTask?.source);
|
|
4525
4701
|
const normalizedStage = normalizeViewStage(step);
|
|
4526
4702
|
const flowCompatible = cachedTask?.flow?.flowVersion === SUPPORTED_FLOW_VERSION;
|
|
@@ -4542,8 +4718,8 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4542
4718
|
const showIntakeEditor = canAction('import-data') && (!dataset || intakeEditorOpen);
|
|
4543
4719
|
const runtimeRecoveryNeeded = Boolean(cachedTask?.source?.rowCount) && runtime.rows.length === 0;
|
|
4544
4720
|
const runtimeRecoveryAllowed = runtimeRecoveryNeeded
|
|
4545
|
-
&& isCurrentView
|
|
4546
|
-
|
|
4721
|
+
&& ((isCurrentView && ['uploaded', 'rules_confirmed', 'diagnosed'].includes(cachedTask?.state))
|
|
4722
|
+
|| (normalizedStage === 'upload' && viewAccess === 'read'))
|
|
4547
4723
|
&& cachedTask?.source?.type !== 'image';
|
|
4548
4724
|
const lastCsv = runtime.lastCsv;
|
|
4549
4725
|
const fieldByPattern = (pattern) => runtime.headers.find((field) => pattern.test(field)) ?? null;
|
|
@@ -4665,6 +4841,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4665
4841
|
};
|
|
4666
4842
|
|
|
4667
4843
|
const performProfile = async (task) => {
|
|
4844
|
+
await verifyRestoredRuntime(actions, activeSessionId, task, runtime);
|
|
4668
4845
|
const r = await api('/data-cleaning/api/mvp/profile', {
|
|
4669
4846
|
rows: runtime.rows,
|
|
4670
4847
|
headers: runtime.headers,
|
|
@@ -4700,6 +4877,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4700
4877
|
actions.setBusy(true);
|
|
4701
4878
|
actions.setError(null);
|
|
4702
4879
|
try {
|
|
4880
|
+
await verifyRestoredRuntime(actions, activeSessionId, cachedTask, runtime);
|
|
4703
4881
|
const r = await api('/data-cleaning/api/mvp/clean', {
|
|
4704
4882
|
rows: runtime.rows,
|
|
4705
4883
|
headers: runtime.headers,
|
|
@@ -4728,6 +4906,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4728
4906
|
actions.setBusy(true);
|
|
4729
4907
|
actions.setError(null);
|
|
4730
4908
|
try {
|
|
4909
|
+
await verifyRestoredRuntime(actions, activeSessionId, cachedTask, runtime);
|
|
4731
4910
|
const r = await api('/data-cleaning/api/mvp/complete', {
|
|
4732
4911
|
rows: runtime.rows,
|
|
4733
4912
|
headers: runtime.headers,
|
|
@@ -4900,6 +5079,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4900
5079
|
actions.setBusy(true);
|
|
4901
5080
|
actions.setError(null);
|
|
4902
5081
|
try {
|
|
5082
|
+
await verifyRestoredRuntime(actions, activeSessionId, cachedTask, runtime);
|
|
4903
5083
|
let current = await ensureWorkflowTask(actions, activeSessionId, {
|
|
4904
5084
|
title: taskTitle, objectives, fieldSelection, mappings, matchRules,
|
|
4905
5085
|
});
|
|
@@ -4957,9 +5137,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4957
5137
|
actions.setError('当前会话的任务向导尚未就绪或正在解析图片,请稍后重试。');
|
|
4958
5138
|
return;
|
|
4959
5139
|
}
|
|
4960
|
-
if (cachedTask?.id)
|
|
4961
|
-
|
|
4962
|
-
|
|
5140
|
+
if (cachedTask?.id) removeRuntime(cachedTask.id);
|
|
5141
|
+
removeRuntime(`session:${activeSessionId || 'unassigned'}`);
|
|
5142
|
+
removeRuntime('unassigned');
|
|
4963
5143
|
workflowTaskBySession.delete(String(activeSessionId || 'unassigned'));
|
|
4964
5144
|
actions.resetWorkflow();
|
|
4965
5145
|
actions.close();
|
|
@@ -4977,6 +5157,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4977
5157
|
if (Array.isArray(latest.artifacts) && latest.artifacts.length) return latest;
|
|
4978
5158
|
const rows = exportRows();
|
|
4979
5159
|
if (!rows?.length) throw new Error('当前页面没有可导出的明细。若任务来自历史记录,请重新上传源文件并继续处理。');
|
|
5160
|
+
await verifyRestoredRuntime(actions, activeSessionId, latest, runtime);
|
|
4980
5161
|
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(latest.id)}/artifacts`, 'POST', {
|
|
4981
5162
|
originSessionId: activeSessionId, originWorkspaceId: latest.originWorkspaceId,
|
|
4982
5163
|
expectedRevision: latest.revision,
|
|
@@ -5287,22 +5468,30 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
5287
5468
|
),
|
|
5288
5469
|
);
|
|
5289
5470
|
} else if (step === 'profile') {
|
|
5471
|
+
const persistedQuality = cachedTask?.qualitySummary;
|
|
5290
5472
|
pane = h('div', { className: 'dcAgentPane dcAgentProfilePane' },
|
|
5291
5473
|
h('section', { className: 'dcAgentSection dcAgentProfileSurface', 'aria-label': '质量体检报告' },
|
|
5292
5474
|
h('header', { className: 'dcAgentProfileHeader' },
|
|
5293
5475
|
h('h3', null, '质量体检报告'),
|
|
5294
5476
|
h('p', { className: 'dcAgentHint' }, '基于已解析数据生成本地质量画像(缺失率、去重值、金额分布)。本步骤不发起企查查调用。'),
|
|
5295
5477
|
),
|
|
5296
|
-
profile ? h('div', { className: 'dcAgentProfileReport' },
|
|
5478
|
+
profile || persistedQuality ? h('div', { className: 'dcAgentProfileReport' },
|
|
5479
|
+
!profile ? h('p', { className: 'dcAgentHint', role: 'status' }, '以下为 Host 保存的体检摘要;字段级明细未持久化。') : null,
|
|
5297
5480
|
h('div', { className: 'dcAgentGrid dcAgentProfileMetrics', 'aria-label': '体检摘要' },
|
|
5298
|
-
stat('行数', profile
|
|
5299
|
-
stat('列数', profile
|
|
5300
|
-
profile
|
|
5301
|
-
profile
|
|
5302
|
-
profile
|
|
5303
|
-
profile
|
|
5481
|
+
stat('行数', profile?.rowCount ?? persistedQuality?.total ?? cachedTask?.source?.rowCount ?? '—'),
|
|
5482
|
+
stat('列数', profile?.columnCount ?? cachedTask?.source?.columnCount ?? '—'),
|
|
5483
|
+
profile?.amountStats ? stat('金额 最小', profile.amountStats.min) : null,
|
|
5484
|
+
profile?.amountStats ? stat('金额 最大', profile.amountStats.max) : null,
|
|
5485
|
+
profile?.amountStats ? stat('金额 总和', profile.amountStats.sum) : null,
|
|
5486
|
+
profile?.amountStats ? stat('金额 均值', typeof profile.amountStats.mean === 'number' ? profile.amountStats.mean.toFixed(2) : profile.amountStats.mean) : null,
|
|
5487
|
+
!profile && persistedQuality ? stat('有效主体', persistedQuality.valid, persistedQuality.valid > 0 ? 'good' : null) : null,
|
|
5488
|
+
!profile && persistedQuality ? stat('缺少主体标识', persistedQuality.missingAnchor, persistedQuality.missingAnchor > 0 ? 'bad' : null) : null,
|
|
5489
|
+
!profile && persistedQuality ? stat('重复主体', persistedQuality.duplicates, persistedQuality.duplicates > 0 ? 'warn' : null) : null,
|
|
5490
|
+
!profile && persistedQuality ? stat('信用代码异常', persistedQuality.invalidCreditNo, persistedQuality.invalidCreditNo > 0 ? 'warn' : null) : null,
|
|
5491
|
+
!profile && persistedQuality ? stat('联系电话异常', persistedQuality.invalidPhone, persistedQuality.invalidPhone > 0 ? 'warn' : null) : null,
|
|
5492
|
+
!profile && persistedQuality ? stat('空白字段', persistedQuality.emptyFields, persistedQuality.emptyFields > 0 ? 'warn' : null) : null,
|
|
5304
5493
|
),
|
|
5305
|
-
Array.isArray(profile
|
|
5494
|
+
Array.isArray(profile?.columns) && profile.columns.length ? h('section', { className: 'dcAgentProfileTableSection', 'aria-label': '字段质量明细' },
|
|
5306
5495
|
h('div', { className: 'dcAgentProfileTableHead' },
|
|
5307
5496
|
h('h4', null, '字段质量明细'),
|
|
5308
5497
|
h('span', { className: 'dcAgentHint' }, `共 ${profile.columns.length} 列`),
|
|
@@ -5393,9 +5582,22 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
5393
5582
|
canAction('resolve-candidate') ? h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !paidConfirmed, onClick: () => resolveCandidate(item, candidate) }, '确认此主体') : null,
|
|
5394
5583
|
)),
|
|
5395
5584
|
)),
|
|
5585
|
+
) : cachedTask?.matchSummary ? h('section', { className: 'dcAgentSection', 'aria-label': '主体匹配结果摘要' },
|
|
5586
|
+
h('h3', null, '主体匹配结果'),
|
|
5587
|
+
h('p', { className: 'dcAgentHint', role: 'status' }, '以下为 Host 保存的任务摘要;逐行结果和候选详情未持久化。'),
|
|
5588
|
+
h('div', { className: 'dcAgentGrid' },
|
|
5589
|
+
stat('总数', cachedTask.matchSummary.total),
|
|
5590
|
+
stat('精确匹配', (cachedTask.matchSummary.exact ?? 0) + (cachedTask.matchSummary.confirmed ?? 0), 'good'),
|
|
5591
|
+
stat('候选记录', cachedTask.matchSummary.candidate, cachedTask.matchSummary.candidate > 0 ? 'warn' : null),
|
|
5592
|
+
stat('待核验', cachedTask.matchSummary.reviewRequired, cachedTask.matchSummary.reviewRequired > 0 ? 'warn' : null),
|
|
5593
|
+
stat('未匹配', cachedTask.matchSummary.unresolved, cachedTask.matchSummary.unresolved > 0 ? 'warn' : null),
|
|
5594
|
+
stat('失败', cachedTask.matchSummary.failed, cachedTask.matchSummary.failed > 0 ? 'bad' : null),
|
|
5595
|
+
),
|
|
5396
5596
|
) : null,
|
|
5397
5597
|
);
|
|
5398
5598
|
} else if (step === 'enrich') {
|
|
5599
|
+
const persistedEnrichment = cachedTask?.enrichmentSummary;
|
|
5600
|
+
const hasRuntimeEnrichment = Boolean(clean || complete || qccRun);
|
|
5399
5601
|
pane = h('div', { className: 'dcAgentPane' },
|
|
5400
5602
|
h('p', { className: 'dcAgentHint' }, '执行本地确定性清洗并核对本任务所选补全字段。外部字段以当前用户企查查 MCP 的真实返回为准,缺失值不会被编造。'),
|
|
5401
5603
|
h('section', { className: 'dcAgentSection' },
|
|
@@ -5414,13 +5616,23 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
5414
5616
|
stat('重复', clean.badDuplicate, clean.badDuplicate > 0 ? 'warn' : null),
|
|
5415
5617
|
) : null,
|
|
5416
5618
|
),
|
|
5417
|
-
h('
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5619
|
+
persistedEnrichment && !hasRuntimeEnrichment ? h('section', { className: 'dcAgentSection', 'aria-label': '字段补全结果摘要' },
|
|
5620
|
+
h('h3', null, '字段补全结果'),
|
|
5621
|
+
h('p', { className: 'dcAgentHint', role: 'status' }, '以下为 Host 保存的任务摘要;逐行补全结果请从结果下载查看。'),
|
|
5622
|
+
h('div', { className: 'dcAgentGrid' },
|
|
5623
|
+
stat('总数', persistedEnrichment.total),
|
|
5624
|
+
stat('已补全', persistedEnrichment.completed, persistedEnrichment.completed > 0 ? 'good' : null),
|
|
5625
|
+
stat('原值保留', persistedEnrichment.unchanged),
|
|
5626
|
+
stat('待核验', persistedEnrichment.reviewRequired, persistedEnrichment.reviewRequired > 0 ? 'warn' : null),
|
|
5627
|
+
stat('失败', persistedEnrichment.failed, persistedEnrichment.failed > 0 ? 'bad' : null),
|
|
5628
|
+
),
|
|
5629
|
+
) : h('div', { className: 'dcAgentGrid' },
|
|
5630
|
+
stat('输入行数', dataset ? dataset.rowCount : '—'),
|
|
5631
|
+
stat('清洗保留', clean ? clean.kept : '—', clean && clean.kept > 0 ? 'good' : null),
|
|
5632
|
+
stat('本地补全', complete ? complete.completed : '—', complete && complete.completed > 0 ? 'good' : null),
|
|
5633
|
+
stat('QCC 已补全', qccEnrichedCount, qccEnrichedCount > 0 ? 'good' : null),
|
|
5634
|
+
stat('待核验', qccReviewCount, qccReviewCount > 0 ? 'warn' : null),
|
|
5635
|
+
),
|
|
5424
5636
|
canAction('create-local-artifacts') ? h('div', { className: 'dcAgentRow' }, h('button', {
|
|
5425
5637
|
type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !exportRows()?.length,
|
|
5426
5638
|
onClick: () => createAndDownload('complete', 'xlsx'),
|
|
@@ -6114,7 +6326,6 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
6114
6326
|
DatasetReview,
|
|
6115
6327
|
reviewColumns,
|
|
6116
6328
|
reviewCsv,
|
|
6117
|
-
clearCleaningDraft,
|
|
6118
6329
|
entriesToDataset,
|
|
6119
6330
|
extractPromptEntries,
|
|
6120
6331
|
imageFilesFromTransfer,
|
|
@@ -6123,6 +6334,8 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
6123
6334
|
guessMappings,
|
|
6124
6335
|
intakeState,
|
|
6125
6336
|
restoreParsedRuntime,
|
|
6337
|
+
runtimeFor,
|
|
6338
|
+
verifyRestoredRuntime,
|
|
6126
6339
|
projectCompletionResult,
|
|
6127
6340
|
mappingRecommendations,
|
|
6128
6341
|
mappedOutputFields,
|
|
@@ -6135,6 +6348,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
6135
6348
|
installNewSessionBridge,
|
|
6136
6349
|
isCleaningSession,
|
|
6137
6350
|
isKnownCleaningDraft,
|
|
6351
|
+
wizardListEntries,
|
|
6352
|
+
invalidListInstruction,
|
|
6353
|
+
INITIAL_DRAFT_TEMPLATE,
|
|
6354
|
+
initializeCleaningDraft,
|
|
6355
|
+
emptyInitialInput,
|
|
6356
|
+
startCleaningSession,
|
|
6138
6357
|
markCleaningSession,
|
|
6139
6358
|
qualitySummaryFor,
|
|
6140
6359
|
plainEntityListDataset,
|
package/lib/skill-enrich.js
CHANGED
|
@@ -52,6 +52,7 @@ export function registerEnrichSkill(skills) {
|
|
|
52
52
|
'When the user gives company names and asks to enrich / complete them with Qichacha (企查查), including registration fields, company panorama, ownership penetration, governance, historical changes, risk tags, intellectual property, or operational / bidding dimensions.',
|
|
53
53
|
source: 'dsh-data-cleaning-agent',
|
|
54
54
|
content: [
|
|
55
|
+
'UX-49 最高优先级:先检查本业务请求的信息是否充分,再考虑以下任何工具或执行规则。若仍含未补全的【】占位符,或没有实际企业名单/Excel/名单图片与待补字段,先用中文要求提供名单或文件及待补字段;零工具调用,包括 OAuth 检查、体检、OCR、MCP、创建 workflow。例子、说明、【企业名单】均不是企业。用户改过的初始模板按普通内容理解,不得整体忽略。有真实暂存来源和已选字段的安全任务凭证可提供这些信息,不要求重复提供。此规则只作用于数据清洗补全业务,不得全局拦截 DSH 发送或其它产品。',
|
|
55
56
|
'You are an enterprise-list enrichment assistant. You fill company lists with Qichacha (QCC) data by calling QCC MCP tools. Never invent, pad, or fabricate any field.',
|
|
56
57
|
'',
|
|
57
58
|
'Workbench execution request (highest priority):',
|
package/lib/skill.js
CHANGED
|
@@ -15,6 +15,7 @@ export function registerSkill(skills) {
|
|
|
15
15
|
'When the user asks to clean, complete, validate, de-duplicate, or summarize a batch of raw data rows, CSV records, or table-like data.',
|
|
16
16
|
source: 'dsh-data-cleaning-agent',
|
|
17
17
|
content: [
|
|
18
|
+
'UX-49 — Highest priority, before ANY tool: within this data-cleaning business request, if the message still contains an unfilled 【】 placeholder, or lacks actual rows/list/file and requested completion fields, ask the user to provide the actual enterprise list or Excel/list image and the fields to complete. Make ZERO tool calls (including profile, OAuth checks, OCR, MCP and workflow creation). Do not treat examples, instructions or 【企业名单】 as enterprises. Do not intercept other DSH products or the global send action. A valid staged-source task credential can supply the list/field scope; do not demand the same data twice. An edited initial template is ordinary user content, never discard it wholesale.',
|
|
18
19
|
'You are a data cleaning and completion assistant. Work only on the rows the user actually provided; never invent, pad, or fabricate extra rows.',
|
|
19
20
|
'',
|
|
20
21
|
'Workflow:',
|
package/lib/web.js
CHANGED
|
@@ -653,6 +653,22 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
653
653
|
return writeJson(res, 405, { ok: false, code: 'DC_METHOD', message: 'Artifact route requires GET or POST.' });
|
|
654
654
|
}
|
|
655
655
|
|
|
656
|
+
if (rest.length === 2 && rest[1] === 'verify-source') {
|
|
657
|
+
if (req.method !== 'POST') {
|
|
658
|
+
return writeJson(res, 405, { ok: false, code: 'DC_METHOD', message: 'Source verification requires POST.' });
|
|
659
|
+
}
|
|
660
|
+
const body = await readBody(req);
|
|
661
|
+
const payload = body.length ? JSON.parse(body.toString('utf8')) : {};
|
|
662
|
+
const current = await workflow.assertRequestOrigin(taskId, payload);
|
|
663
|
+
const verified = Boolean(current.source?.checksum)
|
|
664
|
+
&& sourceVerifier(payload.headers, payload.rows, current.source.checksum) === current.source.checksum;
|
|
665
|
+
return writeJson(res, 200, {
|
|
666
|
+
ok: true,
|
|
667
|
+
marker: 'data-cleaning-source-verifier-v1',
|
|
668
|
+
verified,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
656
672
|
if (rest.length !== 3 || rest[1] !== 'actions' || req.method !== 'POST') {
|
|
657
673
|
return writeJson(res, 404, { ok: false, code: 'DC_WORKFLOW_ROUTE', message: 'Workflow route not found.' });
|
|
658
674
|
}
|
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.16",
|
|
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",
|