dsh-data-cleaning-agent 0.9.7 → 0.9.9
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 +12 -0
- package/README.en.md +3 -1
- package/README.md +4 -2
- package/docs/RELEASE-0.9.9.md +24 -0
- package/lib/client.js +108 -25
- package/lib/image-workflow.js +1 -0
- package/lib/qcc-safety.js +1 -0
- package/lib/qcc.js +18 -2
- package/lib/web.js +4 -0
- package/lib/workflow-contract.js +13 -1
- package/lib/workflow-execution.js +1 -1
- package/lib/workflow.js +13 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.9] - 2026-09-13
|
|
8
|
+
|
|
9
|
+
- 原生首页标题桥接适配新版 Host 的 composer sibling slot;菜单进入数据清洗补全后稳定显示“数据清洗补全智能体”,不再回退为“探索未至之境”。
|
|
10
|
+
- 保持桥接可逆并补充第三方插件共存回归,避免误改其它业务智能体的标题、提示区或输入框。
|
|
11
|
+
|
|
12
|
+
## [0.9.8] - 2026-09-13
|
|
13
|
+
|
|
14
|
+
- DC-SCOPE-P0:持久保存任务来源 Session/Workspace 与安全展示名称;v2 旧记录采用附加字段兼容读取,不猜测来源、不批量改写历史。
|
|
15
|
+
- Profile 历史改为独立只读详情;打开来源会话按宿主能力降级,不覆盖当前任务、规则、名单或命令。保留客户 XLSX 与次要任务报告分层。
|
|
16
|
+
- 当前绑定恢复、迟到刷新及 HTTP 写入/命令准备校验来源;跨会话请求拒绝,版本冲突仍由 Host revision 控制。
|
|
17
|
+
- 审计增加 Provider 六类状态区分;完成态迟到更新、A/B、旧记录、重启和来源导航补充回归。真实 DSH Web 未认证,真实宿主与 Provider 未验证。
|
|
18
|
+
|
|
7
19
|
## [0.9.7] - 2026-09-11
|
|
8
20
|
|
|
9
21
|
- 优化数据清洗补全工作台的区域间距、信息层级与明暗主题响应式布局,重构导入清单、字段映射、质量体检和主体匹配页面。
|
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.9** (stable release)
|
|
14
|
+
|
|
15
|
+
0.9.9 restores the “数据清洗补全智能体” native home title when newer Host builds place the plugin marker and Hero headline in sibling composer branches. The bridge remains reversible and isolated from other business plugins. The 0.9.8 read-only Profile history and origin Workspace/Session guarantees remain unchanged. Rollback: `dsh plugin --profile web add dsh-data-cleaning-agent@0.9.8`. Live DSH A/B verification remains a post-install user check.
|
|
14
16
|
|
|
15
17
|
This release refines workbench hierarchy, spacing, and responsive behavior across imported lists, field mapping, quality profiling, and entity matching. File, image, and pasted-data inputs collapse after a successful import and return through the explicit reimport action. Field mapping uses grouped choices, and searching a dimension exposes all fields in that group. Preparing a draft does not query QCC; sending starts execution. Fully restart DSH after upgrading. Existing download artifacts are not rewritten.
|
|
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.9
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
请先满足下文的 DSH、连接器及侧边栏依赖要求;安装后完整停止并重启对应 Profile。
|
|
@@ -27,7 +27,9 @@ dsh plugin --profile web add dsh-data-cleaning-agent@0.9.7
|
|
|
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.9**(正式版本)
|
|
31
|
+
|
|
32
|
+
0.9.9 修复新版 Host composer sibling slot 下的原生首页标题定位,进入数据清洗补全后稳定显示“数据清洗补全智能体”,并保持对其它业务插件的标题隔离。0.9.8 的 Profile 历史只读、来源工作区/会话校验与旧记录边界全部保留。回滚:`dsh plugin --profile web add dsh-data-cleaning-agent@0.9.8`。真实 DSH A/B 验收仍以安装后的用户验证为准,详见发布说明。
|
|
31
33
|
|
|
32
34
|
本版优化工作台的信息层级、区域间距与响应式布局,重构导入清单、字段映射、质量体检及主体匹配页面。导入成功后文件、图片与粘贴入口自动收起,通过“重新导入”再次展开;映射字段按维度分组选择,搜索维度名称时展示该组全部字段。生成说明不查询,发送才执行;升级后需完整重启 DSH,历史下载文件不自动改写。
|
|
33
35
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# 0.9.9 · Native home title compatibility
|
|
2
|
+
|
|
3
|
+
Status: stable release. Date: 2026-09-13.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
- Adapt the reversible native Hero title bridge to Host layouts where the plugin marker and headline live in sibling branches below the composer container.
|
|
8
|
+
- Keep the data-cleaning home title fixed at “数据清洗补全智能体” without modifying another business plugin's headline, prompt UI or input focus.
|
|
9
|
+
- Preserve the 0.9.8 Profile history ownership, origin Workspace/Session checks and artifact-download behavior.
|
|
10
|
+
|
|
11
|
+
## Verification
|
|
12
|
+
|
|
13
|
+
- The coexistence fixture now exercises a sibling-slot Host structure rather than the obsolete nested Hero structure.
|
|
14
|
+
- `npm run check` covers syntax, documentation version consistency, marketing metadata, package allowlist and the full Node test suite.
|
|
15
|
+
- The React/Chromium layout matrix remains a separate isolated UI gate; real DSH and production QCC Provider behavior require post-install acceptance.
|
|
16
|
+
|
|
17
|
+
## Install and rollback
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
dsh plugin --profile web add dsh-data-cleaning-agent@0.9.9
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Rollback package: `dsh plugin --profile web add dsh-data-cleaning-agent@0.9.8`.
|
|
24
|
+
Do not move or overwrite existing tags or npm versions.
|
package/lib/client.js
CHANGED
|
@@ -1941,8 +1941,20 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1941
1941
|
const time = date && Number.isFinite(date.getTime()) ? date.toLocaleString() : '';
|
|
1942
1942
|
return `${base}|${task.source?.type === 'image' && task.state === 'draft' ? '待识别' : `${task.source?.rowCount ?? 0} 条`}${time ? '|' + time : ''}`;
|
|
1943
1943
|
}
|
|
1944
|
+
function taskBelongsToSession(task, sessionId) {
|
|
1945
|
+
if (!sessionId || task?.originSessionId !== sessionId) return false;
|
|
1946
|
+
const scope = sessionWorkbenchController?.originFor?.(sessionId);
|
|
1947
|
+
return !scope?.originWorkspaceId || task.originWorkspaceId === scope.originWorkspaceId;
|
|
1948
|
+
}
|
|
1949
|
+
function workflowOriginLabel(task) {
|
|
1950
|
+
if (!task?.originSessionId) return '历史版本未保存来源';
|
|
1951
|
+
return `工作区:${task.originWorkspaceName || task.originWorkspaceId || '未取得来源工作区'} · 会话:${task.originSessionName || task.originSessionId}`;
|
|
1952
|
+
}
|
|
1944
1953
|
function cacheWorkflowTask(actions, sessionId, task) {
|
|
1945
1954
|
if (!task) return null;
|
|
1955
|
+
if (!taskBelongsToSession(task, sessionId)) throw new Error('此任务不属于当前会话;请在任务历史中只读查看或打开来源会话。');
|
|
1956
|
+
const previous = workflowTaskBySession.get(String(sessionId));
|
|
1957
|
+
if (previous?.id === task.id && previous.revision > task.revision) return previous;
|
|
1946
1958
|
if (sessionId) workflowTaskBySession.set(String(sessionId), task);
|
|
1947
1959
|
bindTask(sessionId, task.id);
|
|
1948
1960
|
actions.setWorkflowTask(task);
|
|
@@ -1955,10 +1967,13 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1955
1967
|
}
|
|
1956
1968
|
|
|
1957
1969
|
async function createWorkflowTask(actions, sessionId, draft = {}) {
|
|
1970
|
+
if (!sessionId) throw new Error('请先打开数据清洗会话。');
|
|
1958
1971
|
const key = String(sessionId || 'unassigned');
|
|
1959
1972
|
let pending = workflowTaskCreationBySession.get(key);
|
|
1960
1973
|
if (!pending) {
|
|
1961
1974
|
pending = requestJson('/data-cleaning/api/workflow/tasks', 'POST', {
|
|
1975
|
+
...sessionWorkbenchController?.originFor?.(sessionId),
|
|
1976
|
+
originSessionId: sessionId,
|
|
1962
1977
|
title: draft.title || '企业数据清洗补全任务',
|
|
1963
1978
|
objectives: draft.objectives || DEFAULT_CLEANING_KEYS,
|
|
1964
1979
|
fieldSelection: draft.fieldSelection || DEFAULT_ENRICHMENT_KEYS,
|
|
@@ -1984,9 +1999,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1984
1999
|
const saved = readTaskBindings()[key];
|
|
1985
2000
|
if (ownedSessionId(key) && /^dcw-[a-zA-Z0-9-]+$/.test(saved || '')) {
|
|
1986
2001
|
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(saved)}`);
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
2002
|
+
if (taskBelongsToSession(response.task, sessionId)) {
|
|
2003
|
+
const task = cacheWorkflowTask(actions, sessionId, response.task);
|
|
2004
|
+
if (restoreStage) actions.setStep(task.stage || 'upload');
|
|
2005
|
+
return task;
|
|
2006
|
+
}
|
|
2007
|
+
bindTask(sessionId, null);
|
|
1990
2008
|
}
|
|
1991
2009
|
return createWorkflowTask(actions, sessionId, draft);
|
|
1992
2010
|
}
|
|
@@ -2000,6 +2018,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2000
2018
|
async function ensureEditableWorkflowTask(actions, sessionId, draft = {}, preferredTask) {
|
|
2001
2019
|
const key = String(sessionId || 'unassigned');
|
|
2002
2020
|
const current = workflowTaskBySession.get(key) ?? preferredTask ?? null;
|
|
2021
|
+
if (current && !taskBelongsToSession(current, sessionId)) throw new Error('不能将历史任务绑定到当前会话。请新建清洗任务。');
|
|
2003
2022
|
if (!current) return createWorkflowTask(actions, sessionId, draft);
|
|
2004
2023
|
if (EDITABLE_WORKFLOW_STATES.has(current.state)) return cacheWorkflowTask(actions, sessionId, current);
|
|
2005
2024
|
workflowTaskBySession.delete(key);
|
|
@@ -2023,8 +2042,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2023
2042
|
async function updateWorkflowTask(actions, sessionId, task, input) {
|
|
2024
2043
|
const latest = workflowTaskBySession.get(String(sessionId || 'unassigned')) ?? task;
|
|
2025
2044
|
if (!latest) return ensureWorkflowTask(actions, sessionId, input);
|
|
2045
|
+
if (!taskBelongsToSession(latest, sessionId)) throw new Error('此任务不属于当前会话。');
|
|
2026
2046
|
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(latest.id)}`, 'PATCH', {
|
|
2027
2047
|
...input,
|
|
2048
|
+
originSessionId: sessionId, originWorkspaceId: latest.originWorkspaceId,
|
|
2028
2049
|
expectedRevision: latest.revision,
|
|
2029
2050
|
});
|
|
2030
2051
|
return cacheWorkflowTask(actions, sessionId, response.task);
|
|
@@ -2033,8 +2054,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2033
2054
|
async function workflowAction(actions, sessionId, task, action, input = {}) {
|
|
2034
2055
|
const latest = workflowTaskBySession.get(String(sessionId || 'unassigned')) ?? task;
|
|
2035
2056
|
if (!latest) throw new Error('请先创建数据清洗补全任务。');
|
|
2057
|
+
if (!taskBelongsToSession(latest, sessionId)) throw new Error('此任务不属于当前会话。');
|
|
2036
2058
|
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(latest.id)}/actions/${action}`, 'POST', {
|
|
2037
2059
|
...input,
|
|
2060
|
+
originSessionId: sessionId, originWorkspaceId: latest.originWorkspaceId,
|
|
2038
2061
|
expectedRevision: latest.revision,
|
|
2039
2062
|
});
|
|
2040
2063
|
return cacheWorkflowTask(actions, sessionId, response.task);
|
|
@@ -2048,6 +2071,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2048
2071
|
if (JSON.stringify([...(task.fieldSelection || [])].sort()) !== JSON.stringify([...(expected.enrichmentKeys || [])].sort())) throw new Error('图片任务字段配置尚未保存,请重试。');
|
|
2049
2072
|
const bound = await requestJson(`/data-cleaning/api/images/commands/${encodeURIComponent(expected.imageCommandId)}`, 'PATCH', {
|
|
2050
2073
|
taskId: task.id, expectedRevision: task.revision, executeOnSend: true,
|
|
2074
|
+
originSessionId: sessionId, originWorkspaceId: task.originWorkspaceId,
|
|
2051
2075
|
});
|
|
2052
2076
|
return { ...bound.command, prompt: buildTaskPrompt(expected) };
|
|
2053
2077
|
}
|
|
@@ -2071,6 +2095,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2071
2095
|
});
|
|
2072
2096
|
const response = await requestJson('/data-cleaning/api/g5/commands', 'POST', {
|
|
2073
2097
|
kind: 'enrich', workflowOwned: true, expectedRevision: task.revision, taskId: task.id,
|
|
2098
|
+
originSessionId: sessionId, originWorkspaceId: task.originWorkspaceId,
|
|
2074
2099
|
rows: runtime.rows, headers: runtime.headers, fieldSelection: task.fieldSelection,
|
|
2075
2100
|
});
|
|
2076
2101
|
if (!response.command?.commandId || !response.command?.prompt) throw new Error('Host 未返回执行凭证;未发送任务。');
|
|
@@ -2079,6 +2104,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2079
2104
|
|
|
2080
2105
|
async function refreshHostedTask(actions, sessionId, taskId, run, navigate = true) {
|
|
2081
2106
|
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(taskId)}`);
|
|
2107
|
+
if (!taskBelongsToSession(response.task, sessionId)) {
|
|
2108
|
+
if (readTaskBindings()[sessionId] === taskId) bindTask(sessionId, null);
|
|
2109
|
+
return null;
|
|
2110
|
+
}
|
|
2082
2111
|
const selectedTask = workflowTaskBySession.get(String(sessionId));
|
|
2083
2112
|
if (selectedTask && selectedTask.id !== taskId) return response.task;
|
|
2084
2113
|
if (selectedTask && selectedTask.revision > response.task.revision) return selectedTask;
|
|
@@ -2136,6 +2165,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2136
2165
|
workflowContract: null,
|
|
2137
2166
|
workflowTask: null,
|
|
2138
2167
|
workflowTasks: [],
|
|
2168
|
+
historyTask: null,
|
|
2139
2169
|
taskTitle: '企业数据清洗补全任务',
|
|
2140
2170
|
mappings: [],
|
|
2141
2171
|
objectives: [...DEFAULT_CLEANING_KEYS],
|
|
@@ -2178,6 +2208,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2178
2208
|
draft.workflowTasks = (Array.isArray(value) ? value : []).map(task =>
|
|
2179
2209
|
(cached.get(task.id)?.revision ?? -1) > (task.revision ?? -1) ? cached.get(task.id) : task);
|
|
2180
2210
|
},
|
|
2211
|
+
setHistoryTask: (draft, value) => { draft.historyTask = value; },
|
|
2212
|
+
refreshHistoryTask: (draft, value) => {
|
|
2213
|
+
if (value?.id === draft.historyTask?.id && value.revision >= draft.historyTask.revision) draft.historyTask = value;
|
|
2214
|
+
},
|
|
2181
2215
|
setTaskTitle: (draft, value) => { draft.taskTitle = String(value ?? ''); },
|
|
2182
2216
|
setMappings: (draft, value) => {
|
|
2183
2217
|
const next = Array.isArray(value) ? value : [];
|
|
@@ -2684,7 +2718,22 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2684
2718
|
if (!enabled || typeof document === 'undefined' || typeof document.querySelectorAll !== 'function') return () => {};
|
|
2685
2719
|
const marker = [...document.querySelectorAll('.dcAgentExperience')]
|
|
2686
2720
|
.find((element) => element?.dataset?.sessionId === sessionId);
|
|
2687
|
-
const
|
|
2721
|
+
const hasHeadline = (scope) => {
|
|
2722
|
+
if (!scope?.querySelectorAll) return false;
|
|
2723
|
+
const spans = [...scope.querySelectorAll('span')];
|
|
2724
|
+
return spans.some((element) => element.dataset?.dcAgentHeroTitle === 'true'
|
|
2725
|
+
|| ['探索未至之境', 'Into the Unknown'].includes(element.textContent?.trim()))
|
|
2726
|
+
|| Boolean(scope.querySelector?.('[class*="headlineText"]'));
|
|
2727
|
+
};
|
|
2728
|
+
// 新版 Host 会把 list slot 放在 composer seat 的独立分支;marker 不一定再是
|
|
2729
|
+
// data-phase=hero 的后代。沿祖先寻找同时包含原生标题的最小范围,保留旧结构兼容。
|
|
2730
|
+
let hero = marker?.closest?.('[data-phase="hero"]') ?? null;
|
|
2731
|
+
if (!hasHeadline(hero)) {
|
|
2732
|
+
let scope = marker?.closest?.('[data-composer-seat]') ?? marker?.parentElement ?? null;
|
|
2733
|
+
while (scope && !hasHeadline(scope)) scope = scope.parentElement;
|
|
2734
|
+
if (scope) hero = scope;
|
|
2735
|
+
}
|
|
2736
|
+
if (!hero && document.body?.querySelectorAll) hero = document.body;
|
|
2688
2737
|
if (!hero) return () => {};
|
|
2689
2738
|
const changedTitles = new Map();
|
|
2690
2739
|
const changedBadges = new Map();
|
|
@@ -3751,6 +3800,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3751
3800
|
const workflowContract = useStore((state) => state.workflowContract);
|
|
3752
3801
|
const workflowTask = useStore((state) => state.workflowTask);
|
|
3753
3802
|
const workflowTasks = useStore((state) => state.workflowTasks);
|
|
3803
|
+
const historyTask = useStore((state) => state.historyTask);
|
|
3754
3804
|
const taskTitle = useStore((state) => state.taskTitle);
|
|
3755
3805
|
const mappings = useStore((state) => state.mappings);
|
|
3756
3806
|
const objectives = useStore((state) => state.objectives);
|
|
@@ -3811,6 +3861,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3811
3861
|
try {
|
|
3812
3862
|
const task = await refreshHostedTask(actions, activeSessionId, workflowTask.id, null, false);
|
|
3813
3863
|
if (disposed) return;
|
|
3864
|
+
if (!task) return;
|
|
3814
3865
|
if (['matching', 'enriching'].includes(task.state)) timer = setTimeout(poll, 2000);
|
|
3815
3866
|
else if (step === 'match' || step === 'enrich') actions.setStep(task.stage || step);
|
|
3816
3867
|
} catch (error) { if (!disposed) actions.setError(error.message); }
|
|
@@ -4046,8 +4097,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4046
4097
|
throw new Error('当前会话无法提交数据清洗补全企查查任务');
|
|
4047
4098
|
}
|
|
4048
4099
|
const latest = workflowTaskBySession.get(String(activeSessionId)) ?? cachedTask;
|
|
4100
|
+
if (!taskBelongsToSession(latest, activeSessionId)) throw new Error('此任务不属于当前会话。');
|
|
4049
4101
|
const prepared = await api('/data-cleaning/api/g5/commands', {
|
|
4050
4102
|
...payload, workflowOwned: true, expectedRevision: latest.revision,
|
|
4103
|
+
originSessionId: activeSessionId, originWorkspaceId: latest.originWorkspaceId,
|
|
4051
4104
|
...(payload.kind === 'enrich' ? {} : { confirmPaidCalls: true }),
|
|
4052
4105
|
});
|
|
4053
4106
|
const command = prepared?.command;
|
|
@@ -4200,28 +4253,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4200
4253
|
};
|
|
4201
4254
|
|
|
4202
4255
|
const resumeWorkflowTask = async (task) => {
|
|
4256
|
+
// History selection has its own state; never hydrate the current task runtime.
|
|
4257
|
+
actions.setHistoryTask(task);
|
|
4203
4258
|
try {
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
if (task.qccRunId) {
|
|
4207
|
-
try {
|
|
4208
|
-
const response = await api(`/data-cleaning/api/g5/run/${encodeURIComponent(task.qccRunId)}`);
|
|
4209
|
-
actions.setQccRun(response);
|
|
4210
|
-
} catch { /* Expired run data does not prevent persisted artifact downloads. */ }
|
|
4211
|
-
}
|
|
4259
|
+
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(task.id)}`);
|
|
4260
|
+
actions.refreshHistoryTask(response.task);
|
|
4212
4261
|
} catch (error) { actions.setError(error.message); return; }
|
|
4213
|
-
workflowTaskBySession.set(String(activeSessionId || 'unassigned'), task);
|
|
4214
|
-
actions.setWorkflowTask(task);
|
|
4215
|
-
actions.setTaskTitle(task.title);
|
|
4216
|
-
actions.setMappings(task.mappings || []);
|
|
4217
|
-
actions.setObjectives(task.objectives || []);
|
|
4218
|
-
actions.setFieldSelection(task.fieldSelection || []);
|
|
4219
|
-
for (const [key, value] of Object.entries(task.matchRules || {})) actions.setMatchRule(key, value);
|
|
4220
|
-
actions.setStep(task.stage || 'upload');
|
|
4221
|
-
if (!runtimeFor(task.id, false)?.rows?.length && task.source?.rowCount && !(task.artifacts || []).length) {
|
|
4222
|
-
actions.setDataset(null);
|
|
4223
|
-
actions.setError('已恢复任务元数据。出于隐私保护,原始企业名单未持久化;继续处理前请重新上传源文件。');
|
|
4224
|
-
}
|
|
4225
4262
|
};
|
|
4226
4263
|
|
|
4227
4264
|
const startNewWorkflow = () => {
|
|
@@ -4251,6 +4288,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4251
4288
|
const rows = exportRows();
|
|
4252
4289
|
if (!rows?.length) throw new Error('当前页面没有可导出的明细。若任务来自历史记录,请重新上传源文件并继续处理。');
|
|
4253
4290
|
const response = await requestJson(`/data-cleaning/api/workflow/tasks/${encodeURIComponent(latest.id)}/artifacts`, 'POST', {
|
|
4291
|
+
originSessionId: activeSessionId, originWorkspaceId: latest.originWorkspaceId,
|
|
4254
4292
|
expectedRevision: latest.revision,
|
|
4255
4293
|
headers: runtime.headers,
|
|
4256
4294
|
rows,
|
|
@@ -4350,7 +4388,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4350
4388
|
let pane;
|
|
4351
4389
|
if (step === 'history') {
|
|
4352
4390
|
pane = h('div', { className: 'dcAgentPane' },
|
|
4353
|
-
h('p', { className: 'dcAgentHint' }, '
|
|
4391
|
+
h('p', { className: 'dcAgentHint' }, '当前 Profile 的任务历史,仅只读查看;不会改变当前会话的任务、名单、规则或执行范围。'),
|
|
4354
4392
|
h('div', { className: 'dcAgentRow' },
|
|
4355
4393
|
h('button', {
|
|
4356
4394
|
type: 'button',
|
|
@@ -4365,9 +4403,29 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4365
4403
|
onClick: startNewWorkflow,
|
|
4366
4404
|
}, '新建清洗任务'),
|
|
4367
4405
|
),
|
|
4406
|
+
historyTask ? h('section', { className: 'dcAgentSection', 'aria-label': '历史任务只读详情' },
|
|
4407
|
+
h('h3', null, workflowDisplayName(historyTask)),
|
|
4408
|
+
h('p', { className: 'dcAgentHint' }, workflowOriginLabel(historyTask)),
|
|
4409
|
+
h('p', null, `只读 · ${WORKFLOW_STATE_LABELS[historyTask.state] || '未知'} · ${historyTask.source?.rowCount ?? 0} 行`),
|
|
4410
|
+
h('p', { className: 'dcAgentHint' }, `任务编号:${historyTask.id}`),
|
|
4411
|
+
sessionWorkbenchController?.canOpenOrigin?.(historyTask)
|
|
4412
|
+
? h('button', { type: 'button', className: 'dcAgentButton', onClick: () => {
|
|
4413
|
+
try { sessionWorkbenchController.openOrigin(historyTask); }
|
|
4414
|
+
catch (error) { actions.setError(error.message); }
|
|
4415
|
+
} }, '打开来源会话')
|
|
4416
|
+
: h('p', { className: 'dcAgentHint' }, '来源会话未知、已不可用或宿主不支持打开;保留只读查看和已有文件下载。'),
|
|
4417
|
+
(historyTask.artifacts || []).filter(a => a.format === 'xlsx').map(artifact =>
|
|
4418
|
+
h('div', { key: artifact.id, className: 'dcAgentRow' },
|
|
4419
|
+
artifact.kind !== 'report' ? h('a', { className: 'dcAgentButton', href: `/data-cleaning/api/workflow/tasks/${encodeURIComponent(historyTask.id)}/artifacts/${encodeURIComponent(artifact.id)}?preview=1`, target: '_blank', rel: 'noopener' }, '预览 / 打开') : null,
|
|
4420
|
+
h('a', { className: artifact.kind === 'report' ? 'dcAgentHint' : 'dcAgentButton',
|
|
4421
|
+
href: `/data-cleaning/api/workflow/tasks/${encodeURIComponent(historyTask.id)}/artifacts/${encodeURIComponent(artifact.id)}`,
|
|
4422
|
+
download: artifact.fileName }, artifact.kind === 'report' ? '下载任务结果报告' : `下载 ${artifact.fileName}`))),
|
|
4423
|
+
!historyTask.artifacts?.length ? h('p', { className: 'dcAgentHint' }, '此任务暂无已保存制品;历史查看不会重新调用企查查。') : null,
|
|
4424
|
+
) : null,
|
|
4368
4425
|
workflowTasks.length ? h('div', { className: 'dcAgentPane', 'aria-label': '任务历史列表' },
|
|
4369
4426
|
workflowTasks.map((task) => h('button', { key: task.id, type: 'button', className: 'dcAgentHistoryTask', onClick: () => resumeWorkflowTask(task) },
|
|
4370
4427
|
h('h3', null, workflowDisplayName(task)),
|
|
4428
|
+
h('p', { className: 'dcAgentHint' }, workflowOriginLabel(task)),
|
|
4371
4429
|
h('div', { className: 'dcAgentRow' },
|
|
4372
4430
|
h('span', { className: 'dcAgentJobsPill', 'data-state': task.state ?? 'idle' }, WORKFLOW_STATE_LABELS[task.state] ?? task.state ?? '未知'),
|
|
4373
4431
|
h('span', { className: 'dcAgentHint' }, `${task.source?.rowCount ?? 0} 行 · ${task.updatedAt ? new Date(task.updatedAt).toLocaleString() : ''}`),
|
|
@@ -4966,6 +5024,27 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4966
5024
|
: null;
|
|
4967
5025
|
const disconnectFixture = typeof ctx.inject !== 'function' ? connectProvider(ctx.betterSidebar) : () => {};
|
|
4968
5026
|
const controller = {
|
|
5027
|
+
originFor(sessionId) {
|
|
5028
|
+
const sessions = typeof ctx.get === 'function' ? ctx.get('sessions') : ctx.sessions;
|
|
5029
|
+
const workspaces = typeof ctx.get === 'function' ? ctx.get('workspaces') : ctx.workspaces;
|
|
5030
|
+
const summary = sessions?.list?.getSnapshot?.()?.byId?.[sessionId];
|
|
5031
|
+
const workspace = workspaces?.list?.getSnapshot?.()?.items?.find(item => item.sessionIds?.includes(sessionId));
|
|
5032
|
+
return { originSessionId: sessionId, originWorkspaceId: workspace?.workspaceId || summary?.workspaceId || null,
|
|
5033
|
+
originSessionName: summary?.title || null, originWorkspaceName: workspace?.name || workspace?.title || null };
|
|
5034
|
+
},
|
|
5035
|
+
canOpenOrigin(task) {
|
|
5036
|
+
const sessions = typeof ctx.get === 'function' ? ctx.get('sessions') : ctx.sessions;
|
|
5037
|
+
if (!task?.originSessionId || !ownedSessionId(task.originSessionId) || typeof sessions?.open !== 'function') return false;
|
|
5038
|
+
const summary = sessions.list?.getSnapshot?.()?.byId?.[task.originSessionId];
|
|
5039
|
+
const scope = controller.originFor(task.originSessionId);
|
|
5040
|
+
return Boolean(summary && (!task.originWorkspaceId || scope.originWorkspaceId === task.originWorkspaceId));
|
|
5041
|
+
},
|
|
5042
|
+
openOrigin(task) {
|
|
5043
|
+
if (!controller.canOpenOrigin(task)) throw new Error('来源会话不可用;请继续只读查看。');
|
|
5044
|
+
const sessions = typeof ctx.get === 'function' ? ctx.get('sessions') : ctx.sessions;
|
|
5045
|
+
sessions.open(task.originSessionId);
|
|
5046
|
+
// Navigation only: no task binding, command preparation, or draft writes.
|
|
5047
|
+
},
|
|
4969
5048
|
storeFor: instanceFor,
|
|
4970
5049
|
actionsFor: sessionId => instanceFor(sessionId).actions,
|
|
4971
5050
|
open(sessionId, step) {
|
|
@@ -5168,6 +5247,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
5168
5247
|
ensureWorkflowTask,
|
|
5169
5248
|
ensureEditableWorkflowTask,
|
|
5170
5249
|
queueWorkflowOperation,
|
|
5250
|
+
taskBelongsToSession,
|
|
5251
|
+
workflowOriginLabel,
|
|
5252
|
+
cacheWorkflowTask,
|
|
5253
|
+
refreshHostedTask,
|
|
5171
5254
|
};
|
|
5172
5255
|
return module.exports;
|
|
5173
5256
|
},
|
package/lib/image-workflow.js
CHANGED
|
@@ -23,6 +23,7 @@ export function createImageWorkflow({ getWorkflow, execution, commands }) {
|
|
|
23
23
|
summary: { total: rows.length, valid: rows.length, missingAnchor: 0,
|
|
24
24
|
duplicates: rows.length - new Set(result.entries).size } });
|
|
25
25
|
const payload = await execution.prepare({ taskId: task.id, expectedRevision: task.revision,
|
|
26
|
+
originSessionId: task.originSessionId, originWorkspaceId: task.originWorkspaceId,
|
|
26
27
|
kind: 'enrich', rows, headers: ['主体标识'], nameField: '主体标识' });
|
|
27
28
|
const command = commands.prepare(payload, { artifactOrigin: record.workflow.artifactOrigin });
|
|
28
29
|
record.qccCommand = { commandId: command.commandId, taskId: task.id };
|
package/lib/qcc-safety.js
CHANGED
|
@@ -78,6 +78,7 @@ export function safeAuditEvent(event) {
|
|
|
78
78
|
upstreamCode: source.upstreamCode ? String(source.upstreamCode) : null,
|
|
79
79
|
durationMs: Math.max(0, Number(source.durationMs ?? 0)),
|
|
80
80
|
};
|
|
81
|
+
if (['success-data', 'success-empty', 'not-required', 'no-permission', 'failed', 'unknown'].includes(source.providerState)) safe.providerState = source.providerState;
|
|
81
82
|
if (source.catalogVersion || Array.isArray(source.missing) || Array.isArray(source.unknown)) {
|
|
82
83
|
safe.catalogVersion = source.catalogVersion ? String(source.catalogVersion).slice(0, 64) : null;
|
|
83
84
|
safe.missing = safeCatalogLabels(source.missing);
|
package/lib/qcc.js
CHANGED
|
@@ -281,12 +281,26 @@ function normalizedFailure(result, toolName, state) {
|
|
|
281
281
|
|
|
282
282
|
function emitAudit(options, event) {
|
|
283
283
|
try {
|
|
284
|
-
options?.onAudit?.(safeAuditEvent(event
|
|
284
|
+
options?.onAudit?.(safeAuditEvent({ ...event, providerState: event.providerState ?? providerResultState({
|
|
285
|
+
error: event.outcome === 'failed' || event.outcome === 'not-dispatched' ? { code: event.code, upstreamCode: event.upstreamCode } : null,
|
|
286
|
+
}) }));
|
|
285
287
|
} catch {
|
|
286
288
|
// 审计 sink 失败不能改变计费工具调用的业务结果。
|
|
287
289
|
}
|
|
288
290
|
}
|
|
289
291
|
|
|
292
|
+
/** Provider outcome is separate from execution completion and business review. */
|
|
293
|
+
export function providerResultState({ data, error, required = true } = {}) {
|
|
294
|
+
if (!required) return 'not-required';
|
|
295
|
+
if (error) return ['401', '403', 'QCC_AUTH_REQUIRED'].includes(String(error.upstreamCode || error.code)) ? 'no-permission' : 'failed';
|
|
296
|
+
if (Array.isArray(data)) return data.length ? 'success-data' : 'success-empty';
|
|
297
|
+
if (!data || typeof data !== 'object' || !Object.keys(data).length) return 'unknown';
|
|
298
|
+
const lists = ['实际控制人信息', '受益所有人信息', 'items', 'records', '企业列表'].map(k => data[k]).filter(Array.isArray);
|
|
299
|
+
if (lists.length && lists.every(v => !v.length)) return data.has_more === true ? 'unknown' : 'success-empty';
|
|
300
|
+
// No fabricated zero count for a missing/null/unrecognised transport payload.
|
|
301
|
+
return 'success-data';
|
|
302
|
+
}
|
|
303
|
+
|
|
290
304
|
function candidateView(candidate) {
|
|
291
305
|
const item = isRecord(candidate) ? candidate : {};
|
|
292
306
|
return {
|
|
@@ -835,15 +849,17 @@ export class QccHostBridge {
|
|
|
835
849
|
} : {}),
|
|
836
850
|
});
|
|
837
851
|
if (result?.isError !== true) {
|
|
852
|
+
const data = decodeQccToolValue(result?.value);
|
|
838
853
|
emitAudit(options, {
|
|
839
854
|
toolName: activeToolName,
|
|
840
855
|
callId,
|
|
841
856
|
attempt,
|
|
842
857
|
outcome: 'success',
|
|
858
|
+
providerState: providerResultState({ data }),
|
|
843
859
|
durationMs: Date.now() - attemptStarted,
|
|
844
860
|
});
|
|
845
861
|
attemptAudited = true;
|
|
846
|
-
return { callId, toolName: activeToolName, data
|
|
862
|
+
return { callId, toolName: activeToolName, data };
|
|
847
863
|
}
|
|
848
864
|
const upstreamCode = upstreamFailureCode(result);
|
|
849
865
|
if (upstreamCode === 'UNKNOWN_TOOL' && attempt === 1 && !state.signal.aborted) {
|
package/lib/web.js
CHANGED
|
@@ -424,6 +424,7 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
424
424
|
const payload = JSON.parse((await readBody(req)).toString('utf8'));
|
|
425
425
|
const record = imageIntake.require(decodeURIComponent(rest[0]));
|
|
426
426
|
const task = await (await getWorkflow()).require(payload.taskId);
|
|
427
|
+
await (await getWorkflow()).assertRequestOrigin(task.id, payload);
|
|
427
428
|
if (payload.executeOnSend !== true || record.state !== 'prepared' || task.state !== 'draft'
|
|
428
429
|
|| task.revision !== payload.expectedRevision || task.source?.type !== 'image'
|
|
429
430
|
|| task.source.fileName !== record.fileName || !task.fieldSelection.length) {
|
|
@@ -516,6 +517,7 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
516
517
|
if (req.method === 'PATCH') {
|
|
517
518
|
const body = await readBody(req);
|
|
518
519
|
const payload = body.length ? JSON.parse(body.toString('utf8')) : {};
|
|
520
|
+
await workflow.assertRequestOrigin(taskId, payload);
|
|
519
521
|
return writeJson(res, 200, {
|
|
520
522
|
ok: true,
|
|
521
523
|
marker: 'data-cleaning-workflow-v2',
|
|
@@ -545,6 +547,7 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
545
547
|
if (rest.length === 2 && req.method === 'POST') {
|
|
546
548
|
const body = await readBody(req);
|
|
547
549
|
const payload = body.length ? JSON.parse(body.toString('utf8')) : {};
|
|
550
|
+
await workflow.assertRequestOrigin(taskId, payload);
|
|
548
551
|
if (Number(payload.expectedRevision) !== current.revision) {
|
|
549
552
|
throw new WorkflowError('DC_WORKFLOW_REVISION_CONFLICT', 'Workflow task was updated by another session.', 409);
|
|
550
553
|
}
|
|
@@ -612,6 +615,7 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
612
615
|
const body = await readBody(req);
|
|
613
616
|
const payload = body.length ? JSON.parse(body.toString('utf8')) : {};
|
|
614
617
|
const action = rest[2];
|
|
618
|
+
await workflow.assertRequestOrigin(taskId, payload);
|
|
615
619
|
const actions = {
|
|
616
620
|
upload: () => workflow.recordUpload(taskId, payload),
|
|
617
621
|
rules: () => workflow.confirmRules(taskId, payload),
|
package/lib/workflow-contract.js
CHANGED
|
@@ -215,5 +215,17 @@ export function assertWorkflowRecordShape(record) {
|
|
|
215
215
|
if (!STAGE_IDS.has(record.stage) || !STATE_IDS.has(record.state)) {
|
|
216
216
|
throw new WorkflowContractError('DC_WORKFLOW_SCHEMA', 'Workflow record contains an invalid stage or state.');
|
|
217
217
|
}
|
|
218
|
-
|
|
218
|
+
// Additive v2 migration: never infer ownership from whichever Session reads history.
|
|
219
|
+
return { ...record, ...normalizeWorkflowOrigin(record) };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function normalizeWorkflowOrigin(value = {}) {
|
|
223
|
+
const id = value => typeof value === 'string' && /^[\w:.-]{1,200}$/.test(value) ? value : null;
|
|
224
|
+
const label = value => typeof value === 'string' ? value.replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, 120) || null : null;
|
|
225
|
+
return {
|
|
226
|
+
originSessionId: id(value.originSessionId),
|
|
227
|
+
originWorkspaceId: id(value.originWorkspaceId),
|
|
228
|
+
originSessionName: label(value.originSessionName),
|
|
229
|
+
originWorkspaceName: label(value.originWorkspaceName),
|
|
230
|
+
};
|
|
219
231
|
}
|
|
@@ -10,7 +10,7 @@ export function createWorkflowExecution({ getWorkflow, artifacts }) {
|
|
|
10
10
|
return {
|
|
11
11
|
async prepare(payload) {
|
|
12
12
|
const store = await requireStore();
|
|
13
|
-
const task = await store.
|
|
13
|
+
const task = await store.assertRequestOrigin(payload.taskId, payload);
|
|
14
14
|
if (!['rules_confirmed', 'diagnosed', 'review_required', 'partial'].includes(task.state)) {
|
|
15
15
|
throw new WorkflowError('DC_EXECUTION_STATE', '请先在工作台确认规则;进行中或已完成任务不能重复启动。', 409);
|
|
16
16
|
}
|
package/lib/workflow.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
assertWorkflowTransition,
|
|
13
13
|
normalizeFieldSelection,
|
|
14
14
|
normalizeWorkflowDraft,
|
|
15
|
+
normalizeWorkflowOrigin,
|
|
15
16
|
validateMappings,
|
|
16
17
|
} from './workflow-contract.js';
|
|
17
18
|
|
|
@@ -140,6 +141,7 @@ export class DataCleaningWorkflowStore {
|
|
|
140
141
|
const id = this.idFactory();
|
|
141
142
|
const record = {
|
|
142
143
|
id,
|
|
144
|
+
...normalizeWorkflowOrigin(input),
|
|
143
145
|
schemaVersion: WORKFLOW_SCHEMA_VERSION,
|
|
144
146
|
revision: 1,
|
|
145
147
|
title: draft.title,
|
|
@@ -180,6 +182,16 @@ export class DataCleaningWorkflowStore {
|
|
|
180
182
|
return record;
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
async assertRequestOrigin(id, input = {}) {
|
|
186
|
+
const task = await this.require(id);
|
|
187
|
+
// Origin-less legacy/headless records remain readable; UI never adopts them.
|
|
188
|
+
if (task.originSessionId && (input.originSessionId !== task.originSessionId
|
|
189
|
+
|| (task.originWorkspaceId && input.originWorkspaceId !== task.originWorkspaceId))) {
|
|
190
|
+
throw new WorkflowError('DC_WORKFLOW_SCOPE', '任务不属于此来源会话;历史任务仅可只读查看。', 409);
|
|
191
|
+
}
|
|
192
|
+
return task;
|
|
193
|
+
}
|
|
194
|
+
|
|
183
195
|
async mutate(id, expectedRevision, updater) {
|
|
184
196
|
const key = String(id);
|
|
185
197
|
const current = await this.require(key);
|
|
@@ -197,6 +209,7 @@ export class DataCleaningWorkflowStore {
|
|
|
197
209
|
...latest,
|
|
198
210
|
...patch,
|
|
199
211
|
id: latest.id,
|
|
212
|
+
...normalizeWorkflowOrigin(latest),
|
|
200
213
|
schemaVersion: WORKFLOW_SCHEMA_VERSION,
|
|
201
214
|
revision: latest.revision + 1,
|
|
202
215
|
updatedAt: this.nowFn(),
|
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.9",
|
|
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",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"install.sh",
|
|
21
21
|
"marketing",
|
|
22
22
|
"docs/USER-GUIDE.md",
|
|
23
|
+
"docs/RELEASE-0.9.9.md",
|
|
23
24
|
"docs/FIRST-CONTRIBUTION.md",
|
|
24
25
|
"docs/COMPATIBILITY.md",
|
|
25
26
|
"docs/UI-V1.5.0-ADOPTION.md",
|