dsh-data-cleaning-agent 0.8.23 → 0.9.0
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 +4 -0
- package/docs/UI-V1.5.0-ADOPTION.md +43 -0
- package/lib/better-sidebar-adapter.js +76 -0
- package/lib/client.js +289 -499
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.0] - 2026-09-10
|
|
8
|
+
|
|
9
|
+
- 工作台迁移到 Better Sidebar Session 单例 Tab;移除自有固定抽屉、宽度拖拽、展开/关闭按钮和中央会话让位逻辑。
|
|
10
|
+
- 按 Session 隔离任务视图;关闭或折叠 Tab 不取消任务,五流程入口复用同一 Tab。
|
|
11
|
+
- 可选 Provider 生命周期探测,缺失时提示安装/升级,不回退到旧抽屉;需要 targetedOpen 和 stateSubscription。
|
|
12
|
+
- 完整 256 项测试及 10 组隔离 React/Chromium 回归通过;真实 DSH 四插件组合联调尚未完成,详见 docs/UI-V1.5.0-ADOPTION.md。
|
|
13
|
+
|
|
7
14
|
## [0.8.23] - 2026-09-09
|
|
8
15
|
|
|
9
16
|
- 工作台顶部仅保留产品标题和展开、关闭按钮,移除重复状态与技术标识。
|
package/README.en.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# dsh-data-cleaning-agent
|
|
2
2
|
|
|
3
|
+
> 0.9.0 upgrade: the workbench now uses a Session-singleton Better Sidebar Tab, requiring `targetedOpen` and `stateSubscription`. Missing providers show an install/upgrade message, not a private drawer. See the [adoption and validation record](docs/UI-V1.5.0-ADOPTION.md).
|
|
4
|
+
|
|
3
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.
|
|
4
6
|
|
|
5
7
|
Install a pinned release using the command in the [Chinese quick start](README.md), review the preview or evidence before confirming, and keep the previous version and task-directory backup for rollback. Qichacha calls use the customer's authorized account.
|
|
@@ -8,7 +10,7 @@ Related agents: [数据清洗补全](https://github.com/duhu2000/dsh-data-cleani
|
|
|
8
10
|
|
|
9
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.
|
|
10
12
|
>
|
|
11
|
-
> Current source version / 当前源码版本: **0.
|
|
13
|
+
> Current source version / 当前源码版本: **0.9.0** (stable release)
|
|
12
14
|
|
|
13
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.
|
|
14
16
|
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# dsh-data-cleaning-agent
|
|
2
2
|
|
|
3
|
+
> 0.9.0 升级说明:工作台已迁移到 Better Sidebar 的 Session 单例 Tab,需要兼容的 `dsh-better-sidebar`(`targetedOpen`、`stateSubscription`)。缺失时会提示安装/升级,不回退到自有抽屉;本轮验证边界见 [采用记录](docs/UI-V1.5.0-ADOPTION.md)。
|
|
4
|
+
|
|
3
5
|
本版统一向导与工作台的 Host 执行闭环:发送后自动生成新 Excel,原文件不修改,工作台入口可恢复;发布记录见 [发布说明](docs/RELEASE-0.8.22.md)。
|
|
4
6
|
|
|
5
7
|
## 安装与三分钟上手
|
|
@@ -7,7 +9,7 @@
|
|
|
7
9
|
数据清洗补全智能体:面向 Excel/CSV/JSON 企业名单,提供数据清洗、表格清洗、清洗补全、去重、企业数据补全与字段补全,支持企查查 MCP 和结果导出。
|
|
8
10
|
|
|
9
11
|
```sh
|
|
10
|
-
dsh plugin --profile web add dsh-data-cleaning-agent@0.
|
|
12
|
+
dsh plugin --profile web add dsh-data-cleaning-agent@0.9.0
|
|
11
13
|
```
|
|
12
14
|
|
|
13
15
|
请先满足下文的 DSH、连接器及侧边栏依赖要求;安装后完整停止并重启对应 Profile。
|
|
@@ -25,7 +27,7 @@ dsh plugin --profile web add dsh-data-cleaning-agent@0.8.23
|
|
|
25
27
|
|
|
26
28
|
> 在 DeepSeek Harness 中清洗、补全、画像企业名单数据的智能体插件:本地 CSV/XLSX/JSON 引擎 + 可选企查查(Qichacha/QCC)MCP 企业数据补全,由企查查(Qichacha/QCC)团队发起并维护。
|
|
27
29
|
>
|
|
28
|
-
> 当前源码版本 / Current source version: **0.
|
|
30
|
+
> 当前源码版本 / Current source version: **0.9.0**(正式版本)
|
|
29
31
|
|
|
30
32
|
本版上传表格后展示完整清单及空白数量,用绿色、琥珀色、红色及文字区分已通过、待确认和未匹配映射。已映射字段自动纳入补全范围,额外字段默认折叠;多个原列可经确认使用同一非主体字段,分别补空并保留非空原值。修复 Host 草稿重置为默认五字段的问题,增加前后端版本错配提示。生成说明不查询,发送才执行;升级后需完整重启 DSH,历史下载文件不自动改写。
|
|
31
33
|
|
package/docs/COMPATIBILITY.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# 兼容性 / Compatibility
|
|
2
2
|
|
|
3
|
+
## 0.9.0:Session 单例 Tab
|
|
4
|
+
|
|
5
|
+
工作台通过可选 `ctx.inject(['betterSidebar'], ...)` 接入 Provider,探测 `targetedOpen` 与 `stateSubscription`;缺失时保留会话和 Host 工具,不提供私有抽屉回退。当前仅完成隔离模拟服务与 Chromium 回归,尚未实装组合验收;旧版本的 Host/UI 验证不自动覆盖此次迁移。详见 [采用记录](UI-V1.5.0-ADOPTION.md)。
|
|
6
|
+
|
|
3
7
|
## 1. 目标基线
|
|
4
8
|
|
|
5
9
|
本插件面向 DeepSeek Harness(DSH)预发布阶段,双基线验证:
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# v1.5.0 统一侧拉采用记录(0.9.0 采用记录)
|
|
2
|
+
|
|
3
|
+
开发基线 main / 623408e(0.8.23)。2026-09-10 用户授权推进 commit、push、tag 和 npm 发布,本次版本为 0.9.0;不包含市场条目更新。
|
|
4
|
+
|
|
5
|
+
已完整阅读规范第 7、13.4、14、15、15.1 节,并检查配套 Mockup 的 Session 单例、历史同 Tab 和宿主管理几何语义。
|
|
6
|
+
|
|
7
|
+
## 主链路与采用范围
|
|
8
|
+
|
|
9
|
+
- lib/client.js 不再注册 shell.overlay 工作台。移除固定抽屉、拖拽分隔条、width/expanded 状态、容器展开/关闭按钮、全局 resize 与中央会话 padding/位移逻辑。
|
|
10
|
+
- WorkbenchContent 只渲染当前任务、五阶段、任务历史和结果操作。Header 由 Host Tab 提供;业务状态集中在任务进度卡。窄栏采用容器查询,不依赖浏览器窗口宽度判断布局。
|
|
11
|
+
- lib/better-sidebar-adapter.js 注册稳定命名空间 id:dsh-data-cleaning-agent:workbench,single:true。显式传入 Session scope;五个快捷入口、Header 恢复、执行进度自动打开均经过同一 adapter。
|
|
12
|
+
- 插件级控制器按 Session 保存独立业务 store,Tab 只是视图。关闭/隐藏 Tab 不删除、取消或重跑 Host 任务;重新打开恢复原数据和内部视图。读取已有任务绑定不再隐式新建任务。
|
|
13
|
+
- 跨作用域事件由一个带引用计数的控制器接收,提示词准备/OCR/数据暂存无需先挂载 Tab。跨 Session 不共用业务 store;非前台 Session reveal 等待订阅切换后消费,不挤占当前会话布局。
|
|
14
|
+
- 使用可选 ctx.inject(['betterSidebar'], ...) 子生命周期,避免访问未授予的 Cordis 服务;Provider 缺失或移除不卸载业务控制器、会话入口和 Host 工具。
|
|
15
|
+
- 缺依赖/过旧契约/Tab 禁用时显示可行动提示,无旧抽屉回退。Provider 卸载释放描述符和状态订阅;插件卸载移除事件监听、pending intent 和本地命令观察,Host 任务不受影响。
|
|
16
|
+
- 任务、规则、字段目录、OCR、MCP 和制品后端契约未重写。
|
|
17
|
+
|
|
18
|
+
## 接口与构建边界
|
|
19
|
+
|
|
20
|
+
探测 registerTab/openTab/isTabEnabled/getSnapshot/subscribeState 和 targetedOpen/stateSubscription。
|
|
21
|
+
type-only openTab 后通过该 Tab 的公开 store.reduce reveal 所在右侧/底部容器;浮窗不变更几何。
|
|
22
|
+
未使用 tabLifecycle,所以没有依赖该能力。
|
|
23
|
+
|
|
24
|
+
参考类型:本地 Better Sidebar 0.17.1 的 service 声明及 Cordis 4.0.1 的 inject/Fiber 生命周期。
|
|
25
|
+
不据此宣称其他 Provider 版本或真实组合场景已经验收。
|
|
26
|
+
|
|
27
|
+
现有 ModuleLoader 为单文件 CJS 工厂:scripts/sync-sidebar-adapter.mjs 将被测试的 adapter 嵌入 client.js。
|
|
28
|
+
修改 adapter 后运行 node scripts/sync-sidebar-adapter.mjs --write;静态测试校验嵌入内容一致,防止源码与运行代码漂移。
|
|
29
|
+
|
|
30
|
+
## 本地验证
|
|
31
|
+
|
|
32
|
+
- npm run check:语法、文档、营销/打包门禁与完整 256 项测试通过。
|
|
33
|
+
- npm run test:ui:隔离 React/Chromium + 模拟 Better Sidebar,10 组明暗主题/窗口尺寸;另外验证桌面 320px 与宽 Tab。
|
|
34
|
+
- 浏览器覆盖:上传/替换失败保留旧数据、映射搜索和重复列、表格横向滚动和固定表头、提示词准备、执行后自动打开、结果制品操作、Tab 关闭重开、宿主折叠恢复、Session 离开与普通会话隔离。
|
|
35
|
+
- 单元/静态覆盖:单例注册和入口幂等、Session store 隔离、inactive pending reveal、底部/浮窗 reveal、依赖缺失/过旧/禁用、Provider 到达/移除、重复卸载幂等、无固定几何/中央让位、无任务取消删除副作用。
|
|
36
|
+
- 证据:_scratch/ui-layout/results.json 与同目录截图(本地夹具产物,不进入 npm)。
|
|
37
|
+
- 没有调用真实 QCC,不消耗用户额度;没有覆盖上传源文件。
|
|
38
|
+
|
|
39
|
+
## 真实 DSH 验收限制与发布门
|
|
40
|
+
|
|
41
|
+
本轮未替换运行中 3080 Profile,未进行真实 DSH + Better Sidebar 或四插件共存安装验收。
|
|
42
|
+
真实侧栏生命周期、底部停靠/浮窗、四插件切换、Host 动态移除 Provider、实际 OCR/授权链路需在隔离 Profile 联调。
|
|
43
|
+
本地 UI 模拟通过不等于真实 Provider 验收;用户已授权在上述已知限制下发布 0.9.0;真实组合联调仍需补做,不将发布成功视为联调通过。
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Public Better Sidebar seam. No business task creation, cancellation or geometry. */
|
|
2
|
+
export const CLEANING_TAB_ID = 'dsh-data-cleaning-agent:workbench';
|
|
3
|
+
export const SIDEBAR_DEPENDENCY_MESSAGE = '请安装或升级兼容的 dsh-better-sidebar 并重启 DSH(需要 targetedOpen 和 stateSubscription 能力),然后重新点击流程按钮。现有任务和文件不会删除。';
|
|
4
|
+
|
|
5
|
+
export function sidebarCompatibility(service) {
|
|
6
|
+
const methods = ['registerTab', 'openTab', 'isTabEnabled', 'getSnapshot', 'subscribeState'];
|
|
7
|
+
return Boolean(service && methods.every(key => typeof service[key] === 'function')
|
|
8
|
+
&& ['targetedOpen', 'stateSubscription'].every(key => service.features?.includes(key)));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function contains(node, id) {
|
|
12
|
+
if (!node) return false;
|
|
13
|
+
return node.kind === 'leaf' ? node.tabs.some(tab => tab.id === id)
|
|
14
|
+
: node.children.some(child => contains(child, id));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function revealCleaningTab(state, id) {
|
|
18
|
+
if (state.floats?.some(item => item.tab.id === id)) return state;
|
|
19
|
+
if (contains(state.bottomSplits, id)) return state.bottomOpen ? state : { ...state, bottomOpen: true };
|
|
20
|
+
if (contains(state.splits, id)) return state.panelOpen ? state : { ...state, panelOpen: true };
|
|
21
|
+
return state;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One adapter per plugin activation. Session navigation survives closing its Tab. */
|
|
25
|
+
export function createCleaningSidebarAdapter(service, { component, icon, onUnavailable = () => {} }) {
|
|
26
|
+
const targets = new Map();
|
|
27
|
+
const destinations = new Map();
|
|
28
|
+
const pending = new Set();
|
|
29
|
+
let disposed = false;
|
|
30
|
+
const compatible = sidebarCompatibility(service);
|
|
31
|
+
const applyPending = sessionId => {
|
|
32
|
+
if (disposed || service.getSnapshot().sessionId !== sessionId || !pending.has(sessionId)) return;
|
|
33
|
+
const target = targets.get(sessionId);
|
|
34
|
+
if (!target) return;
|
|
35
|
+
pending.delete(sessionId);
|
|
36
|
+
target.navigate(destinations.get(sessionId));
|
|
37
|
+
target.store.reduce(state => revealCleaningTab(state, target.tabId));
|
|
38
|
+
};
|
|
39
|
+
const unregister = compatible ? service.registerTab({
|
|
40
|
+
id: CLEANING_TAB_ID, title: '数据清洗补全', icon, order: 30, single: true, component,
|
|
41
|
+
}) : () => {};
|
|
42
|
+
const unsubscribe = compatible ? service.subscribeState(() => {
|
|
43
|
+
const sessionId = service.getSnapshot().sessionId;
|
|
44
|
+
if (sessionId) applyPending(sessionId);
|
|
45
|
+
}) : () => {};
|
|
46
|
+
return {
|
|
47
|
+
compatible,
|
|
48
|
+
open(scope, destination = 'upload') {
|
|
49
|
+
if (disposed) return false;
|
|
50
|
+
if (!compatible || !service.isTabEnabled(CLEANING_TAB_ID)) {
|
|
51
|
+
onUnavailable(compatible ? '请在 Better Sidebar 设置中启用「数据清洗补全」Tab。' : SIDEBAR_DEPENDENCY_MESSAGE);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (!scope?.sessionId) throw new Error('A target Session is required');
|
|
55
|
+
destinations.set(scope.sessionId, destination);
|
|
56
|
+
pending.add(scope.sessionId);
|
|
57
|
+
service.openTab({ type: CLEANING_TAB_ID }, scope);
|
|
58
|
+
applyPending(scope.sessionId);
|
|
59
|
+
return true;
|
|
60
|
+
},
|
|
61
|
+
attach(scope, target) {
|
|
62
|
+
if (disposed) return () => {};
|
|
63
|
+
targets.set(scope.sessionId, target);
|
|
64
|
+
// Without a pending shortcut, preserve the Session business store's current view.
|
|
65
|
+
// Closing/reopening a Tab must not rewind an in-progress task to an older shortcut.
|
|
66
|
+
applyPending(scope.sessionId);
|
|
67
|
+
return () => { if (targets.get(scope.sessionId) === target) targets.delete(scope.sessionId); };
|
|
68
|
+
},
|
|
69
|
+
dispose() {
|
|
70
|
+
if (disposed) return;
|
|
71
|
+
disposed = true;
|
|
72
|
+
unsubscribe(); unregister();
|
|
73
|
+
targets.clear(); pending.clear(); destinations.clear();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - `sidebar.footer.action` 仅托管入口生命周期和降级渲染;实际入口 Portal 到
|
|
10
10
|
* `sidebar.workspaces` 前,位于「新会话」与「工作区」之间。
|
|
11
11
|
* - DSH 原生会话保持在中间,能力入口使用 `conversation.input.dock` 并通过稳定
|
|
12
|
-
* `data-composer-seat/card` 将自有菜单 Portal
|
|
12
|
+
* `data-composer-seat/card` 将自有菜单 Portal 到输入框下方;工作台使用 Better Sidebar Session 单例 Tab。
|
|
13
13
|
* - 提示词生成器使用 `conversation.input.overlay`,只对本插件创建的会话生效。
|
|
14
14
|
* Hero 标题没有公开替换槽位,故以可恢复、精确文本匹配的 DOM Bridge 对齐业务首页。
|
|
15
15
|
*
|
|
@@ -77,136 +77,6 @@ window.__ModuleLoader__.load({
|
|
|
77
77
|
'aria-hidden': true, focusable: false }, h('path', { d: STAGE_ICON_PATHS[kind] }));
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function workbenchWidthBounds(viewport, available = viewport) {
|
|
81
|
-
const mobile = viewport <= 760 || available < 740;
|
|
82
|
-
return { mobile, min: mobile ? viewport : 320, max: mobile ? viewport : Math.max(320, available - 420) };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// 自有抽屉的可逆兼容适配;不改 Better Sidebar 或其他插件的面板偏好。
|
|
86
|
-
function installWorkbenchSizing(drawer, { preferredWidth, expanded, onWidthChange }) {
|
|
87
|
-
const doc = drawer.ownerDocument;
|
|
88
|
-
const view = doc.defaultView;
|
|
89
|
-
const handle = drawer.querySelector('.dcAgentResizeHandle');
|
|
90
|
-
if (!handle || !view) return () => {};
|
|
91
|
-
const reservations = new Map();
|
|
92
|
-
const registryKey = Symbol.for('dsh.data-cleaning.sizing-reservations');
|
|
93
|
-
const registry = doc[registryKey] || (doc[registryKey] = new WeakMap());
|
|
94
|
-
let disposed = false;
|
|
95
|
-
let requested = expanded ? 980 : preferredWidth;
|
|
96
|
-
let current = 0;
|
|
97
|
-
let drag = null;
|
|
98
|
-
const boundsForView = () => workbenchWidthBounds(view.innerWidth,
|
|
99
|
-
doc.querySelector('[data-conversation-scroll]')?.getBoundingClientRect().width || view.innerWidth);
|
|
100
|
-
const reserve = () => {
|
|
101
|
-
if (disposed) return;
|
|
102
|
-
const width = boundsForView().mobile ? 0 : drawer.getBoundingClientRect().width;
|
|
103
|
-
for (const el of doc.querySelectorAll('[data-conversation-scroll]')) {
|
|
104
|
-
if (!reservations.has(el)) {
|
|
105
|
-
const previous = registry.get(el) || {
|
|
106
|
-
value: el.style.getPropertyValue('--dc-agent-workbench-reserve'),
|
|
107
|
-
priority: el.style.getPropertyPriority('--dc-agent-workbench-reserve'),
|
|
108
|
-
owners: 0,
|
|
109
|
-
};
|
|
110
|
-
previous.owners += 1;
|
|
111
|
-
registry.set(el, previous);
|
|
112
|
-
reservations.set(el, previous);
|
|
113
|
-
sizeObserver?.observe(el, { box: 'border-box' });
|
|
114
|
-
}
|
|
115
|
-
if (el.style.getPropertyValue('--dc-agent-workbench-reserve') !== `${width}px`) {
|
|
116
|
-
el.style.setProperty('--dc-agent-workbench-reserve', `${width}px`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
const apply = (value) => {
|
|
121
|
-
if (disposed) return;
|
|
122
|
-
const bounds = boundsForView();
|
|
123
|
-
const fallback = view.innerWidth <= 1040 ? Math.min(420, view.innerWidth * .48) : Math.min(460, view.innerWidth * .42);
|
|
124
|
-
current = Math.round(Math.min(bounds.max, Math.max(bounds.min, Number.isFinite(value) ? value : fallback)));
|
|
125
|
-
drawer.dataset.narrow = String(bounds.mobile);
|
|
126
|
-
drawer.style.setProperty('--dc-agent-panel-width', `${current}px`);
|
|
127
|
-
handle.setAttribute('aria-valuemin', String(bounds.min));
|
|
128
|
-
handle.setAttribute('aria-valuemax', String(bounds.max));
|
|
129
|
-
handle.setAttribute('aria-valuenow', String(current));
|
|
130
|
-
handle.setAttribute('aria-valuetext', `${current} 像素`);
|
|
131
|
-
reserve();
|
|
132
|
-
};
|
|
133
|
-
const end = (cancel = false) => {
|
|
134
|
-
if (!drag) return;
|
|
135
|
-
const previous = drag;
|
|
136
|
-
drag = null;
|
|
137
|
-
drawer.classList.remove('is-resizing');
|
|
138
|
-
if (handle.hasPointerCapture?.(previous.id)) handle.releasePointerCapture(previous.id);
|
|
139
|
-
if (cancel) apply(requested);
|
|
140
|
-
else { requested = current; onWidthChange(current); }
|
|
141
|
-
};
|
|
142
|
-
const down = (event) => {
|
|
143
|
-
if (event.button !== 0 || drag || boundsForView().mobile) return;
|
|
144
|
-
event.preventDefault();
|
|
145
|
-
handle.focus();
|
|
146
|
-
drag = { id: event.pointerId, startX: event.clientX, startWidth: current };
|
|
147
|
-
handle.setPointerCapture(event.pointerId);
|
|
148
|
-
drawer.classList.add('is-resizing');
|
|
149
|
-
};
|
|
150
|
-
const move = (event) => {
|
|
151
|
-
if (drag?.id !== event.pointerId) return;
|
|
152
|
-
apply(drag.startWidth + drag.startX - event.clientX);
|
|
153
|
-
};
|
|
154
|
-
const up = (event) => { if (drag?.id === event.pointerId) end(); };
|
|
155
|
-
const cancel = () => end(true);
|
|
156
|
-
const keydown = (event) => {
|
|
157
|
-
if (event.key === 'Escape' && drag) {
|
|
158
|
-
event.preventDefault(); event.stopPropagation(); cancel(); return;
|
|
159
|
-
}
|
|
160
|
-
if (drag || boundsForView().mobile) return;
|
|
161
|
-
const bounds = boundsForView();
|
|
162
|
-
const delta = event.shiftKey ? 48 : 16;
|
|
163
|
-
const value = { ArrowLeft: current + delta, ArrowRight: current - delta, Home: bounds.min, End: bounds.max }[event.key];
|
|
164
|
-
if (value === undefined) return;
|
|
165
|
-
event.preventDefault(); event.stopPropagation();
|
|
166
|
-
requested = value; apply(value); onWidthChange(current);
|
|
167
|
-
};
|
|
168
|
-
const reset = () => { cancel(); requested = null; apply(null); onWidthChange(null); };
|
|
169
|
-
const resize = () => { cancel(); apply(requested); };
|
|
170
|
-
const sizeObserver = typeof view.ResizeObserver === 'function' ? new view.ResizeObserver(resize) : null;
|
|
171
|
-
handle.addEventListener('pointerdown', down);
|
|
172
|
-
handle.addEventListener('pointermove', move);
|
|
173
|
-
handle.addEventListener('pointerup', up);
|
|
174
|
-
handle.addEventListener('pointercancel', cancel);
|
|
175
|
-
handle.addEventListener('lostpointercapture', cancel);
|
|
176
|
-
handle.addEventListener('keydown', keydown);
|
|
177
|
-
handle.addEventListener('dblclick', reset);
|
|
178
|
-
view.addEventListener('resize', resize);
|
|
179
|
-
view.addEventListener('blur', cancel);
|
|
180
|
-
// 会话挂载点可被宿主替换;只对当前存在的滚动区设置自有让位变量。
|
|
181
|
-
const observer = typeof view.MutationObserver === 'function' ? new view.MutationObserver(reserve) : null;
|
|
182
|
-
observer?.observe(doc.body, { childList: true, subtree: true });
|
|
183
|
-
apply(requested);
|
|
184
|
-
return () => {
|
|
185
|
-
disposed = true;
|
|
186
|
-
cancel();
|
|
187
|
-
observer?.disconnect();
|
|
188
|
-
sizeObserver?.disconnect();
|
|
189
|
-
handle.removeEventListener('pointerdown', down);
|
|
190
|
-
handle.removeEventListener('pointermove', move);
|
|
191
|
-
handle.removeEventListener('pointerup', up);
|
|
192
|
-
handle.removeEventListener('pointercancel', cancel);
|
|
193
|
-
handle.removeEventListener('lostpointercapture', cancel);
|
|
194
|
-
handle.removeEventListener('keydown', keydown);
|
|
195
|
-
handle.removeEventListener('dblclick', reset);
|
|
196
|
-
view.removeEventListener('resize', resize);
|
|
197
|
-
view.removeEventListener('blur', cancel);
|
|
198
|
-
drawer.style.removeProperty('--dc-agent-panel-width');
|
|
199
|
-
delete drawer.dataset.narrow;
|
|
200
|
-
for (const [el, previous] of reservations) {
|
|
201
|
-
previous.owners -= 1;
|
|
202
|
-
if (previous.owners > 0) continue;
|
|
203
|
-
registry.delete(el);
|
|
204
|
-
if (previous.value) el.style.setProperty('--dc-agent-workbench-reserve', previous.value, previous.priority);
|
|
205
|
-
else el.style.removeProperty('--dc-agent-workbench-reserve');
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
80
|
// 搜索仅改变可见字段,绝不清除已选字段或改变 Host 的字段目录。
|
|
211
81
|
function visibleCatalogFields(group, query) {
|
|
212
82
|
const needle = String(query || '').trim().toLowerCase();
|
|
@@ -224,7 +94,7 @@ window.__ModuleLoader__.load({
|
|
|
224
94
|
const stylesCss = `
|
|
225
95
|
/* QCC-blue candidate tokens from shared DSH-UX-001; never theme the global shell. */
|
|
226
96
|
:where(.dcAgentTopMount, .dcAgentLauncher, .dcAgentExperience, .dcAgentCapabilityMount,
|
|
227
|
-
.
|
|
97
|
+
.dcAgentWorkbenchContent, .dcAgentPromptBackdrop, .dcAgentPromptPanel, .dcAgentPromptLayer,
|
|
228
98
|
.dcAgentToolCard, [data-dc-agent-hero-row="true"]) {
|
|
229
99
|
--dc-action: light-dark(#0875d1, #82c3ff);
|
|
230
100
|
--dc-hover: light-dark(#0666b7, #acd7ff);
|
|
@@ -283,44 +153,17 @@ window.__ModuleLoader__.load({
|
|
|
283
153
|
width: 36px;
|
|
284
154
|
padding-right: 0;
|
|
285
155
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
display: flex;
|
|
292
|
-
align-items: stretch;
|
|
293
|
-
justify-content: flex-end;
|
|
294
|
-
pointer-events: none;
|
|
295
|
-
background: transparent;
|
|
156
|
+
.dcAgentWorkbenchContent {
|
|
157
|
+
box-sizing: border-box; display: flex; flex-direction: column;
|
|
158
|
+
width: 100%; height: 100%; min-width: 0; min-height: 0; overflow: hidden;
|
|
159
|
+
background: var(--dc-surface); color: var(--dc-text);
|
|
160
|
+
container-type: inline-size;
|
|
296
161
|
}
|
|
297
|
-
|
|
298
|
-
.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
display: flex;
|
|
302
|
-
flex-direction: column;
|
|
303
|
-
width: var(--dc-agent-panel-width, min(460px, 42vw));
|
|
304
|
-
max-width: calc(100vw - 420px);
|
|
305
|
-
min-width: 320px;
|
|
306
|
-
height: 100vh;
|
|
307
|
-
border: 1px solid var(--dc-border);
|
|
308
|
-
border-radius: 12px 0 0 12px;
|
|
309
|
-
background: var(--dc-surface);
|
|
310
|
-
color: var(--dc-text);
|
|
311
|
-
box-shadow: light-dark(-10px 0 32px rgba(33, 55, 88, .12), -12px 0 40px rgba(0, 0, 0, .32));
|
|
312
|
-
overflow: hidden;
|
|
313
|
-
pointer-events: auto;
|
|
162
|
+
@container (max-width: 520px) {
|
|
163
|
+
.dcAgentRulesGrid, .dcAgentMappingRow { grid-template-columns: 1fr; }
|
|
164
|
+
.dcAgentMappingRow > b { display: none; }
|
|
165
|
+
.dcAgentCandidate { grid-template-columns: 1fr; }
|
|
314
166
|
}
|
|
315
|
-
.dcAgentWorkbench.is-expanded { width: var(--dc-agent-panel-width, min(980px, calc(100vw - 420px))); }
|
|
316
|
-
.dcAgentResizeHandle { position: absolute; left: 0; top: 0; bottom: 0; width: 8px; z-index: 5; cursor: col-resize; touch-action: none; outline: none; }
|
|
317
|
-
.dcAgentResizeHandle::after { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: transparent; }
|
|
318
|
-
.dcAgentResizeHandle:hover::after, .dcAgentResizeHandle:focus-visible::after, .is-resizing .dcAgentResizeHandle::after { background: var(--dc-action); }
|
|
319
|
-
.dcAgentWorkbench.is-resizing { user-select: none; }
|
|
320
|
-
.dcAgentWorkbench[data-narrow="true"] { width: 100vw; max-width: none; min-width: 0; border-radius: 0; }
|
|
321
|
-
.dcAgentWorkbench[data-narrow="true"] .dcAgentResizeHandle { display: none; }
|
|
322
|
-
.dcAgentOverlay:has(.dcAgentWorkbench[data-narrow="true"]) { background: rgba(8, 10, 14, 0.22); pointer-events: auto; }
|
|
323
|
-
|
|
324
167
|
.dcAgentCapabilities {
|
|
325
168
|
display: flex;
|
|
326
169
|
justify-content: safe center;
|
|
@@ -333,8 +176,6 @@ window.__ModuleLoader__.load({
|
|
|
333
176
|
padding: 2px 16px 0;
|
|
334
177
|
overflow-x: auto;
|
|
335
178
|
scrollbar-width: none;
|
|
336
|
-
transform: translateX(var(--dc-agent-workbench-shift, 0px));
|
|
337
|
-
transition: transform 160ms ease;
|
|
338
179
|
}
|
|
339
180
|
.dcAgentCapabilities::-webkit-scrollbar { display: none; }
|
|
340
181
|
.dcAgentCapability {
|
|
@@ -610,72 +451,6 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
610
451
|
color: var(--dc-action);
|
|
611
452
|
}
|
|
612
453
|
|
|
613
|
-
.dcAgentWbHeader {
|
|
614
|
-
display: flex;
|
|
615
|
-
align-items: center;
|
|
616
|
-
gap: 8px;
|
|
617
|
-
padding: 12px 14px;
|
|
618
|
-
border-bottom: 1px solid var(--dc-border);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
.dcAgentWbTitle {
|
|
622
|
-
display: flex;
|
|
623
|
-
align-items: center;
|
|
624
|
-
gap: 10px;
|
|
625
|
-
min-width: 0;
|
|
626
|
-
flex: 1 1 auto;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
.dcAgentWbIcon {
|
|
630
|
-
width: 36px;
|
|
631
|
-
height: 36px;
|
|
632
|
-
display: grid;
|
|
633
|
-
place-items: center;
|
|
634
|
-
flex: 0 0 auto;
|
|
635
|
-
border-radius: 8px;
|
|
636
|
-
font-size: 18px;
|
|
637
|
-
background: var(--dc-selected);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
.dcAgentWbTitle b,
|
|
641
|
-
.dcAgentWbTitle small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
642
|
-
.dcAgentWbTitle > div { min-width: 0; }
|
|
643
|
-
.dcAgentWbTitle b { display: block; font-size: 15px; }
|
|
644
|
-
.dcAgentWbTitle small { display: block; max-width: 190px; color: var(--dc-muted); font-size: 12px; }
|
|
645
|
-
|
|
646
|
-
.dcAgentWbActions {
|
|
647
|
-
display: flex;
|
|
648
|
-
align-items: center;
|
|
649
|
-
gap: 6px;
|
|
650
|
-
flex: 0 0 auto;
|
|
651
|
-
margin-left: auto;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
.dcAgentQccBadge {
|
|
655
|
-
flex: 0 0 auto;
|
|
656
|
-
padding: 4px 10px;
|
|
657
|
-
border: 1px solid var(--dc-border);
|
|
658
|
-
border-radius: 999px;
|
|
659
|
-
color: var(--dc-muted);
|
|
660
|
-
font-size: 12px;
|
|
661
|
-
white-space: nowrap;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
.dcAgentWbClose {
|
|
665
|
-
flex: 0 0 auto;
|
|
666
|
-
width: 30px;
|
|
667
|
-
height: 30px;
|
|
668
|
-
display: grid;
|
|
669
|
-
place-items: center;
|
|
670
|
-
border: 1px solid var(--dc-border);
|
|
671
|
-
border-radius: 8px;
|
|
672
|
-
background: transparent;
|
|
673
|
-
color: inherit;
|
|
674
|
-
font-size: 15px;
|
|
675
|
-
cursor: pointer;
|
|
676
|
-
}
|
|
677
|
-
.dcAgentWbClose:hover, .dcAgentWbClose:focus-visible { background: var(--dc-page); }
|
|
678
|
-
|
|
679
454
|
.dcAgentStepper {
|
|
680
455
|
display: grid;
|
|
681
456
|
grid-template-columns: repeat(var(--dc-stage-count, 5), minmax(0, 1fr));
|
|
@@ -1005,33 +780,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1005
780
|
}
|
|
1006
781
|
.dcAgentHistoryTask { display: block; width: 100%; padding: 10px; text-align: left; }
|
|
1007
782
|
.dcAgentHistoryTask h3 { margin: 0; font-size: 12px; }
|
|
1008
|
-
/*
|
|
1009
|
-
* DSH 的 root.side slot 是覆盖层。桌面端工作台打开时,让带稳定标记的
|
|
1010
|
-
* 会话滚动区主动让出同宽空间,确保消息流和中央 composer 不被遮挡。
|
|
1011
|
-
* 移动端仍保留全屏工作台,不压缩底层会话。
|
|
1012
|
-
*/
|
|
1013
|
-
@media (min-width: 1041px) {
|
|
1014
|
-
body:has(.dcAgentWorkbench) [data-conversation-scroll] {
|
|
1015
|
-
box-sizing: border-box;
|
|
1016
|
-
padding-right: var(--dc-agent-workbench-reserve, min(460px, 42vw));
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
@media (min-width: 761px) and (max-width: 1040px) {
|
|
1020
|
-
body:has(.dcAgentWorkbench) [data-conversation-scroll] {
|
|
1021
|
-
box-sizing: border-box;
|
|
1022
|
-
padding-right: var(--dc-agent-workbench-reserve, min(420px, 48vw));
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
@media (min-width: 761px) {
|
|
1026
|
-
body:has(.dcAgentWorkbench.is-expanded) [data-conversation-scroll] {
|
|
1027
|
-
box-sizing: border-box;
|
|
1028
|
-
padding-right: var(--dc-agent-workbench-reserve, min(980px, calc(100vw - 420px)));
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
783
|
@media (max-width: 760px) {
|
|
1032
|
-
.dcAgentWorkbench, .dcAgentWorkbench.is-expanded { width: 100vw; max-width: none; min-width: 0; border-radius: 0; }
|
|
1033
|
-
.dcAgentOverlay { background: rgba(8, 10, 14, 0.22); pointer-events: auto; }
|
|
1034
|
-
.dcAgentResizeHandle { display: none; }
|
|
1035
784
|
.dcAgentQccBadge, .dcAgentJobsPill { display: none; }
|
|
1036
785
|
.dcAgentCandidate { grid-template-columns: 1fr; }
|
|
1037
786
|
.dcAgentCapabilities { justify-content: flex-start; padding-inline: 12px; }
|
|
@@ -1044,7 +793,6 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1044
793
|
.dcAgentMappingRow > b { display: none; }
|
|
1045
794
|
}
|
|
1046
795
|
@media (min-width: 761px) and (max-width: 1040px) {
|
|
1047
|
-
.dcAgentWorkbench { width: var(--dc-agent-panel-width, 420px); }
|
|
1048
796
|
.dcAgentQccBadge, .dcAgentJobsPill { display: none; }
|
|
1049
797
|
}
|
|
1050
798
|
@media (max-width: 360px) {
|
|
@@ -1053,8 +801,8 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1053
801
|
.dcAgentStepIcon svg { width: 18px; height: 18px; }
|
|
1054
802
|
}
|
|
1055
803
|
/* Distinct levels: management tabs → compact task steps → working content. */
|
|
1056
|
-
.
|
|
1057
|
-
.
|
|
804
|
+
.dcAgentManagement, .dcAgentStepper { flex: none; }
|
|
805
|
+
.dcAgentWorkbenchContent, .dcAgentPromptPanel { font-size: 14px; line-height: 1.5; }
|
|
1058
806
|
.dcAgentManagement { display: flex; gap: 24px; padding: 0 18px; border-bottom: 1px solid var(--dc-border); }
|
|
1059
807
|
.dcAgentManagement .dcAgentButton { border: 0; border-bottom: 3px solid transparent; border-radius: 0; padding: 10px 0; background: transparent; }
|
|
1060
808
|
.dcAgentManagement .dcAgentButton[aria-pressed="true"] { border-bottom-color: var(--dc-action); color: var(--dc-action); font-weight: 600; }
|
|
@@ -1072,7 +820,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1072
820
|
.dcAgentPromptText, .dcAgentTextarea, .dcAgentField { font-size: 14px; }
|
|
1073
821
|
.dcAgentPromptTrigger { border-radius: 6px; }
|
|
1074
822
|
.dcAgentPromptAction.is-primary:hover:not(:disabled) { background: var(--dc-hover); }
|
|
1075
|
-
:is(.
|
|
823
|
+
:is(.dcAgentWorkbenchContent, .dcAgentPromptPanel, .dcAgentCapabilities, .dcAgentPromptLayer) :is(button, input, select, textarea, summary):focus-visible,
|
|
1076
824
|
.dcAgentLauncher:focus-visible { outline: 2px solid var(--dc-action); outline-offset: 2px; }
|
|
1077
825
|
@media (max-width: 760px) {
|
|
1078
826
|
[data-dc-agent-hero-title="true"] { font-size: 20px; }
|
|
@@ -1335,11 +1083,6 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1335
1083
|
if (!activeCleaningSessionId || (sessionId && sessionId !== activeCleaningSessionId)) return false;
|
|
1336
1084
|
const previousSessionId = activeCleaningSessionId;
|
|
1337
1085
|
activeCleaningSessionId = null;
|
|
1338
|
-
if (rootWorkbenchActions) {
|
|
1339
|
-
stopJobsPolling();
|
|
1340
|
-
rootWorkbenchActions.close?.();
|
|
1341
|
-
rootWorkbenchActions.setActiveSession?.(null);
|
|
1342
|
-
}
|
|
1343
1086
|
if (typeof window.CustomEvent === 'function' && typeof window.dispatchEvent === 'function') {
|
|
1344
1087
|
window.dispatchEvent(new window.CustomEvent(CLEANING_SESSION_EVENT, {
|
|
1345
1088
|
detail: { sessionId: previousSessionId, active: false },
|
|
@@ -1723,9 +1466,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1723
1466
|
return requestJson(path, body === undefined ? 'GET' : 'POST', body);
|
|
1724
1467
|
}
|
|
1725
1468
|
|
|
1726
|
-
async function waitForQccCommand(commandId, attempts = 180, onStatus) {
|
|
1469
|
+
async function waitForQccCommand(commandId, attempts = 180, onStatus, isActive = () => true) {
|
|
1727
1470
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
1471
|
+
if (!isActive()) return null;
|
|
1728
1472
|
const response = await api(`/data-cleaning/api/g5/commands/${encodeURIComponent(commandId)}`);
|
|
1473
|
+
if (!isActive()) return null;
|
|
1729
1474
|
const command = response?.command;
|
|
1730
1475
|
await onStatus?.(command);
|
|
1731
1476
|
if (command?.state === 'completed' || command?.state === 'failed') return command;
|
|
@@ -1752,6 +1497,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1752
1497
|
if (Array.isArray(task.objectives)) actions.setObjectives(task.objectives);
|
|
1753
1498
|
if (Array.isArray(task.mappings)) actions.setMappings(task.mappings);
|
|
1754
1499
|
if (Array.isArray(task.fieldSelection)) actions.setFieldSelection(task.fieldSelection);
|
|
1500
|
+
for (const [key, value] of Object.entries(task.matchRules || {})) actions.setMatchRule(key, value);
|
|
1755
1501
|
return task;
|
|
1756
1502
|
}
|
|
1757
1503
|
|
|
@@ -1894,7 +1640,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1894
1640
|
}
|
|
1895
1641
|
return response.task;
|
|
1896
1642
|
}
|
|
1643
|
+
let commandWatchGeneration = 0;
|
|
1897
1644
|
function watchHostedCommand(actions, sessionId, command) {
|
|
1645
|
+
const generation = commandWatchGeneration;
|
|
1646
|
+
const isActive = () => generation === commandWatchGeneration;
|
|
1898
1647
|
let openedForExecution = false;
|
|
1899
1648
|
const ownerVisible = () => typeof document === 'undefined' || [...document.querySelectorAll('.dcAgentExperience')].some(node => node.dataset.sessionId === sessionId);
|
|
1900
1649
|
void waitForQccCommand(command.commandId, 1800, async (status) => {
|
|
@@ -1905,11 +1654,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1905
1654
|
openWorkbench(actions, workflowTaskBySession.get(String(sessionId))?.stage || 'match', sessionId);
|
|
1906
1655
|
}
|
|
1907
1656
|
if (isCleaningSession(sessionId) && workflowTaskBySession.get(String(sessionId))?.id === command.taskId) actions.setCommandProgress?.({ taskId: command.taskId, ...status.progress });
|
|
1908
|
-
}).then(async (completed) => {
|
|
1657
|
+
}, isActive).then(async (completed) => {
|
|
1658
|
+
if (!completed || !isActive()) return;
|
|
1909
1659
|
await refreshHostedTask(actions, sessionId, command.taskId, completed.run);
|
|
1910
1660
|
if (completed.state === 'failed') throw new Error(completed.error?.message || 'Host 执行失败,请在工作台查看状态。');
|
|
1911
1661
|
}).catch((error) => {
|
|
1912
|
-
if (isCleaningSession(sessionId)) actions.setError(error.message || String(error));
|
|
1662
|
+
if (isActive() && isCleaningSession(sessionId)) actions.setError(error.message || String(error));
|
|
1913
1663
|
});
|
|
1914
1664
|
}
|
|
1915
1665
|
|
|
@@ -1918,8 +1668,6 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1918
1668
|
return defineStore({
|
|
1919
1669
|
init: () => ({
|
|
1920
1670
|
open: false,
|
|
1921
|
-
expanded: false,
|
|
1922
|
-
preferredWidth: null,
|
|
1923
1671
|
step: 'upload',
|
|
1924
1672
|
busy: false,
|
|
1925
1673
|
error: null,
|
|
@@ -1953,11 +1701,6 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1953
1701
|
if (sessionId) draft.activeSessionId = sessionId;
|
|
1954
1702
|
},
|
|
1955
1703
|
close: (draft) => { draft.open = false; },
|
|
1956
|
-
toggleExpanded: (draft) => { draft.expanded = !draft.expanded; },
|
|
1957
|
-
setPreferredWidth: (draft, width) => {
|
|
1958
|
-
draft.preferredWidth = Number.isFinite(width) ? width : null;
|
|
1959
|
-
draft.expanded = false;
|
|
1960
|
-
},
|
|
1961
1704
|
setStep: (draft, step) => { draft.step = step; },
|
|
1962
1705
|
setActiveSession: (draft, sessionId) => { draft.activeSessionId = sessionId ?? null; },
|
|
1963
1706
|
setBusy: (draft, busy) => { draft.busy = busy; },
|
|
@@ -2212,20 +1955,19 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2212
1955
|
|
|
2213
1956
|
function openWorkbench(actions, step, sessionId) {
|
|
2214
1957
|
actions.openAt(step, sessionId);
|
|
2215
|
-
|
|
1958
|
+
sessionWorkbenchController?.open(sessionId, step);
|
|
2216
1959
|
}
|
|
2217
1960
|
|
|
2218
1961
|
const WORKBENCH_OPEN_EVENT = 'dsh:data-cleaning-workbench-open';
|
|
2219
1962
|
const WORKBENCH_DATASET_EVENT = 'dsh:data-cleaning-workbench-dataset';
|
|
2220
1963
|
const WORKBENCH_DRAFT_EVENT = 'dsh:data-cleaning-workbench-draft';
|
|
2221
1964
|
const WORKBENCH_PREPARE_EVENT = 'dsh:data-cleaning-workbench-prepare';
|
|
2222
|
-
const WORKBENCH_OWNER_LEFT_EVENT = 'dsh:data-cleaning-workbench-owner-left';
|
|
2223
1965
|
|
|
2224
1966
|
// session scope 不能复用 root scope 的 store handle。DSH 当前会分别物化 root/session
|
|
2225
1967
|
// 插槽组件,因此以 document 事件跨 scope 通知;闭包引用仅作为无 DOM 测试降级。
|
|
2226
|
-
let
|
|
1968
|
+
let sessionWorkbenchController = null;
|
|
2227
1969
|
|
|
2228
|
-
/** 从
|
|
1970
|
+
/** 从 composer/header 请求指定 Session 的唯一工作台 Tab。 */
|
|
2229
1971
|
function requestWorkbenchOpen(step, sessionId, task = null) {
|
|
2230
1972
|
if (typeof document !== 'undefined'
|
|
2231
1973
|
&& typeof document.createEvent === 'function'
|
|
@@ -2234,7 +1976,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2234
1976
|
event.initCustomEvent(WORKBENCH_OPEN_EVENT, false, true, { step, sessionId, task });
|
|
2235
1977
|
if (!document.dispatchEvent(event)) return;
|
|
2236
1978
|
}
|
|
2237
|
-
|
|
1979
|
+
sessionWorkbenchController?.open(sessionId, step);
|
|
2238
1980
|
}
|
|
2239
1981
|
|
|
2240
1982
|
function requestPreparedWorkflow(sessionId, config) {
|
|
@@ -2245,7 +1987,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2245
1987
|
});
|
|
2246
1988
|
}
|
|
2247
1989
|
|
|
2248
|
-
/**
|
|
1990
|
+
/** 完整表格交给 Session 业务控制器,不把整表塞进模型上下文。 */
|
|
2249
1991
|
function requestWorkbenchDataset(result, sessionId, source) {
|
|
2250
1992
|
if (typeof document !== 'undefined'
|
|
2251
1993
|
&& typeof document.createEvent === 'function'
|
|
@@ -2254,9 +1996,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2254
1996
|
event.initCustomEvent(WORKBENCH_DATASET_EVENT, false, true, { result, sessionId, source });
|
|
2255
1997
|
if (!document.dispatchEvent(event)) return;
|
|
2256
1998
|
}
|
|
2257
|
-
if (
|
|
2258
|
-
applyParsed(result,
|
|
2259
|
-
|
|
1999
|
+
if (sessionWorkbenchController?.actionsFor(sessionId)) {
|
|
2000
|
+
applyParsed(result, sessionWorkbenchController?.actionsFor(sessionId), `session:${sessionId || 'unassigned'}`, source);
|
|
2001
|
+
sessionWorkbenchController?.actionsFor(sessionId).setActiveSession(sessionId);
|
|
2260
2002
|
}
|
|
2261
2003
|
}
|
|
2262
2004
|
|
|
@@ -2269,11 +2011,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2269
2011
|
event.initCustomEvent(WORKBENCH_DRAFT_EVENT, false, true, { draft, sessionId });
|
|
2270
2012
|
if (!document.dispatchEvent(event)) return;
|
|
2271
2013
|
}
|
|
2272
|
-
if (
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
if (draft.mappings)
|
|
2276
|
-
|
|
2014
|
+
if (sessionWorkbenchController?.actionsFor(sessionId)) {
|
|
2015
|
+
sessionWorkbenchController?.actionsFor(sessionId).setTaskTitle(draft.title);
|
|
2016
|
+
sessionWorkbenchController?.actionsFor(sessionId).setObjectives(draft.objectives);
|
|
2017
|
+
if (draft.mappings) sessionWorkbenchController?.actionsFor(sessionId).setMappings(draft.mappings);
|
|
2018
|
+
sessionWorkbenchController?.actionsFor(sessionId).setFieldSelection(draft.fieldSelection);
|
|
2277
2019
|
}
|
|
2278
2020
|
}
|
|
2279
2021
|
|
|
@@ -2582,10 +2324,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2582
2324
|
function DataCleaningExperience(props) {
|
|
2583
2325
|
const { sessionId, session: ownerSession } = props;
|
|
2584
2326
|
const enabled = useCleaningSession(sessionId);
|
|
2327
|
+
const [dependencyMessage, setDependencyMessage] = react.useState('');
|
|
2585
2328
|
react.useEffect(() => {
|
|
2586
|
-
if (
|
|
2587
|
-
|
|
2588
|
-
|
|
2329
|
+
const listener = event => { if (event.detail?.sessionId === sessionId) setDependencyMessage(event.detail.message || ''); };
|
|
2330
|
+
document.addEventListener(DEPENDENCY_EVENT, listener);
|
|
2331
|
+
return () => document.removeEventListener(DEPENDENCY_EVENT, listener);
|
|
2332
|
+
}, [sessionId]);
|
|
2589
2333
|
const hero = ownerSession?.composerPhase === 'blank';
|
|
2590
2334
|
const [menuMount, setMenuMount] = react.useState(null);
|
|
2591
2335
|
react.useEffect(() => {
|
|
@@ -2603,6 +2347,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2603
2347
|
className: `dcAgentExperience${hero ? ' is-home' : ''}`,
|
|
2604
2348
|
'data-session-id': sessionId,
|
|
2605
2349
|
},
|
|
2350
|
+
dependencyMessage ? h('p', { className: 'dcAgentError', role: 'alert' }, dependencyMessage) : null,
|
|
2606
2351
|
menuMount ? reactDom.createPortal(h(CapabilityBar, { sessionId }), menuMount) : h(CapabilityBar, { sessionId }),
|
|
2607
2352
|
hero ? h(ProductHome, { sessionId }) : null,
|
|
2608
2353
|
);
|
|
@@ -2950,7 +2695,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2950
2695
|
const hasImage = files.length > 0;
|
|
2951
2696
|
if (!hasImage) return;
|
|
2952
2697
|
const target = event.target;
|
|
2953
|
-
const inComposer = !open && !target?.closest?.('.
|
|
2698
|
+
const inComposer = !open && !target?.closest?.('.dcAgentWorkbenchContent')
|
|
2954
2699
|
&& Boolean(target?.closest?.('textarea, [contenteditable="true"], [role="textbox"]'));
|
|
2955
2700
|
const inSourceStep = open && wizardStep === 1 && Boolean(target?.closest?.('.dcAgentPromptPanel'));
|
|
2956
2701
|
if (!inComposer && !inSourceStep) return;
|
|
@@ -3489,15 +3234,14 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3489
3234
|
);
|
|
3490
3235
|
}
|
|
3491
3236
|
|
|
3492
|
-
/**
|
|
3493
|
-
function
|
|
3237
|
+
/** Session Tab 业务内容;尺寸、停靠、关闭均由 Better Sidebar 宿主管理。 */
|
|
3238
|
+
function WorkbenchContent(props) {
|
|
3494
3239
|
const { useStore, actions } = props;
|
|
3240
|
+
const visible = props.visible !== false;
|
|
3495
3241
|
const sendSessionCommand = props.sendSessionCommand;
|
|
3496
3242
|
const setSessionDraft = props.setSessionDraft;
|
|
3497
3243
|
const open = useStore((state) => state.open);
|
|
3498
3244
|
const step = useStore((state) => state.step);
|
|
3499
|
-
const expanded = useStore((state) => state.expanded);
|
|
3500
|
-
const preferredWidth = useStore((state) => state.preferredWidth);
|
|
3501
3245
|
const busy = useStore((state) => state.busy);
|
|
3502
3246
|
const error = useStore((state) => state.error);
|
|
3503
3247
|
const input = useStore((state) => state.input);
|
|
@@ -3519,92 +3263,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3519
3263
|
const qccRun = useStore((state) => state.qccRun);
|
|
3520
3264
|
const commandProgress = useStore((state) => state.commandProgress);
|
|
3521
3265
|
const paidConfirmed = useStore((state) => state.paidConfirmed);
|
|
3522
|
-
const jobs = useStore((state) => state.jobs);
|
|
3523
3266
|
const activeSessionId = useStore((state) => state.activeSessionId);
|
|
3524
3267
|
const [fieldQuery, setFieldQuery] = react.useState('');
|
|
3525
3268
|
|
|
3526
3269
|
react.useEffect(() => {
|
|
3527
|
-
if (!open ||
|
|
3528
|
-
const drawer = document.querySelector('.dcAgentWorkbench');
|
|
3529
|
-
if (!drawer) return undefined;
|
|
3530
|
-
return installWorkbenchSizing(drawer, { preferredWidth, expanded, onWidthChange: actions.setPreferredWidth });
|
|
3531
|
-
}, [open, expanded, preferredWidth, actions]);
|
|
3532
|
-
|
|
3533
|
-
react.useEffect(() => {
|
|
3534
|
-
rootWorkbenchActions = actions;
|
|
3535
|
-
const handleOwnerLeft = () => actions.close();
|
|
3536
|
-
const handleOpen = (event) => {
|
|
3537
|
-
event?.preventDefault?.();
|
|
3538
|
-
const detail = event?.detail ?? {};
|
|
3539
|
-
if (detail.task?.id) {
|
|
3540
|
-
workflowTaskBySession.set(String(detail.sessionId || 'unassigned'), detail.task);
|
|
3541
|
-
actions.setWorkflowTask(detail.task);
|
|
3542
|
-
actions.setTaskTitle(detail.task.title);
|
|
3543
|
-
actions.setMappings(detail.task.mappings || []);
|
|
3544
|
-
actions.setObjectives(detail.task.objectives || []);
|
|
3545
|
-
actions.setFieldSelection(detail.task.fieldSelection || []);
|
|
3546
|
-
for (const [key, value] of Object.entries(detail.task.matchRules || {})) actions.setMatchRule(key, value);
|
|
3547
|
-
}
|
|
3548
|
-
openWorkbench(actions, detail.step ?? null, detail.sessionId ?? null);
|
|
3549
|
-
};
|
|
3550
|
-
const handleDataset = (event) => {
|
|
3551
|
-
event?.preventDefault?.();
|
|
3552
|
-
const detail = event?.detail ?? {};
|
|
3553
|
-
if (detail.result) applyParsed(detail.result, actions, `session:${detail.sessionId || 'unassigned'}`, detail.source);
|
|
3554
|
-
if (detail.sessionId) actions.setActiveSession(detail.sessionId);
|
|
3555
|
-
if (detail.result) {
|
|
3556
|
-
void queueWorkflowOperation(detail.sessionId, () => (
|
|
3557
|
-
persistParsedWorkflow(actions, detail.sessionId, detail.result, detail.source)
|
|
3558
|
-
)).catch((eventError) => {
|
|
3559
|
-
actions.setError(eventError instanceof Error ? eventError.message : String(eventError));
|
|
3560
|
-
});
|
|
3561
|
-
}
|
|
3562
|
-
};
|
|
3563
|
-
const handlePrepare = (event) => {
|
|
3564
|
-
event.preventDefault();
|
|
3565
|
-
const { sessionId, config, resolve, reject } = event.detail;
|
|
3566
|
-
queueWorkflowOperation(sessionId, () => stageConfirmedWorkflow(actions, sessionId, config)).then((command) => {
|
|
3567
|
-
watchHostedCommand(actions, sessionId, command);
|
|
3568
|
-
resolve(command);
|
|
3569
|
-
}, reject);
|
|
3570
|
-
};
|
|
3571
|
-
const handleDraft = (event) => {
|
|
3572
|
-
event?.preventDefault?.();
|
|
3573
|
-
const detail = event?.detail ?? {};
|
|
3574
|
-
const draft = detail.draft ?? {};
|
|
3575
|
-
if (detail.sessionId) actions.setActiveSession(detail.sessionId);
|
|
3576
|
-
if (draft.title) actions.setTaskTitle(draft.title);
|
|
3577
|
-
if (draft.objectives) actions.setObjectives(draft.objectives);
|
|
3578
|
-
if (draft.mappings) actions.setMappings(draft.mappings);
|
|
3579
|
-
if (draft.fieldSelection) actions.setFieldSelection(draft.fieldSelection);
|
|
3580
|
-
for (const [key, value] of Object.entries(draft.matchRules || {})) actions.setMatchRule(key, value);
|
|
3581
|
-
void queueWorkflowOperation(detail.sessionId, async () => {
|
|
3582
|
-
const task = await ensureEditableWorkflowTask(actions, detail.sessionId, draft);
|
|
3583
|
-
return updateWorkflowTask(actions, detail.sessionId, task, draft);
|
|
3584
|
-
}).catch((eventError) => actions.setError(eventError instanceof Error ? eventError.message : String(eventError)));
|
|
3585
|
-
};
|
|
3586
|
-
if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
|
|
3587
|
-
document.addEventListener(WORKBENCH_OPEN_EVENT, handleOpen);
|
|
3588
|
-
document.addEventListener(WORKBENCH_OWNER_LEFT_EVENT, handleOwnerLeft);
|
|
3589
|
-
document.addEventListener(WORKBENCH_DATASET_EVENT, handleDataset);
|
|
3590
|
-
document.addEventListener(WORKBENCH_DRAFT_EVENT, handleDraft);
|
|
3591
|
-
document.addEventListener(WORKBENCH_PREPARE_EVENT, handlePrepare);
|
|
3592
|
-
}
|
|
3593
|
-
return () => {
|
|
3594
|
-
if (typeof document !== 'undefined' && typeof document.removeEventListener === 'function') {
|
|
3595
|
-
document.removeEventListener(WORKBENCH_OPEN_EVENT, handleOpen);
|
|
3596
|
-
document.removeEventListener(WORKBENCH_OWNER_LEFT_EVENT, handleOwnerLeft);
|
|
3597
|
-
document.removeEventListener(WORKBENCH_DATASET_EVENT, handleDataset);
|
|
3598
|
-
document.removeEventListener(WORKBENCH_DRAFT_EVENT, handleDraft);
|
|
3599
|
-
document.removeEventListener(WORKBENCH_PREPARE_EVENT, handlePrepare);
|
|
3600
|
-
}
|
|
3601
|
-
stopJobsPolling();
|
|
3602
|
-
if (rootWorkbenchActions === actions) rootWorkbenchActions = null;
|
|
3603
|
-
};
|
|
3604
|
-
}, [actions]);
|
|
3605
|
-
|
|
3606
|
-
react.useEffect(() => {
|
|
3607
|
-
if (!open) return undefined;
|
|
3270
|
+
if (!open || !visible) return undefined;
|
|
3608
3271
|
let disposed = false;
|
|
3609
3272
|
requestJson('/data-cleaning/api/workflow/contract').then((response) => {
|
|
3610
3273
|
if (!disposed) actions.setWorkflowContract(response.contract);
|
|
@@ -3612,19 +3275,21 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3612
3275
|
loadWorkflowTasks(actions);
|
|
3613
3276
|
if (activeSessionId) {
|
|
3614
3277
|
queueWorkflowOperation(activeSessionId, async () => {
|
|
3615
|
-
const
|
|
3616
|
-
|
|
3278
|
+
const key = String(activeSessionId);
|
|
3279
|
+
const saved = ownedSessionId(key) ? readTaskBindings()[key] : null;
|
|
3280
|
+
const taskId = workflowTaskBySession.get(key)?.id || (/^dcw-[a-zA-Z0-9-]+$/.test(saved || '') ? saved : null);
|
|
3281
|
+
if (taskId) await refreshHostedTask(actions, activeSessionId, taskId, null, false);
|
|
3617
3282
|
}).catch((taskError) => {
|
|
3618
3283
|
if (!disposed) actions.setError(taskError instanceof Error ? taskError.message : String(taskError));
|
|
3619
3284
|
});
|
|
3620
3285
|
}
|
|
3621
3286
|
return () => { disposed = true; };
|
|
3622
|
-
}, [open, activeSessionId, actions]);
|
|
3287
|
+
}, [open, visible, activeSessionId, actions]);
|
|
3623
3288
|
|
|
3624
3289
|
// Reopening during execution follows Host metadata, even when the original
|
|
3625
3290
|
// command watcher was lost on page reload. This performs no QCC calls.
|
|
3626
3291
|
react.useEffect(() => {
|
|
3627
|
-
if (!open || !workflowTask?.id || !['matching', 'enriching'].includes(workflowTask.state)) return undefined;
|
|
3292
|
+
if (!open || !visible || !workflowTask?.id || !['matching', 'enriching'].includes(workflowTask.state)) return undefined;
|
|
3628
3293
|
let disposed = false, timer;
|
|
3629
3294
|
const poll = async () => {
|
|
3630
3295
|
try {
|
|
@@ -3636,51 +3301,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3636
3301
|
};
|
|
3637
3302
|
timer = setTimeout(poll, 2000);
|
|
3638
3303
|
return () => { disposed = true; clearTimeout(timer); };
|
|
3639
|
-
}, [open, workflowTask?.id, workflowTask?.state, activeSessionId, step, actions]);
|
|
3640
|
-
|
|
3641
|
-
// 非模态右栏会覆盖 Host 的中央画布;按实际重叠量把 composer 能力栏左移,
|
|
3642
|
-
// 保证 Mockup 中的五个入口在窄桌面视口仍可见、可点击。展开态专注工作台,
|
|
3643
|
-
// 不强行挤压到不足一栏的剩余区域。
|
|
3644
|
-
react.useEffect(() => {
|
|
3645
|
-
if (typeof document === 'undefined' || typeof document.querySelectorAll !== 'function') return undefined;
|
|
3646
|
-
const reset = () => {
|
|
3647
|
-
for (const bar of document.querySelectorAll('.dcAgentCapabilities')) {
|
|
3648
|
-
bar.style?.removeProperty('--dc-agent-workbench-shift');
|
|
3649
|
-
}
|
|
3650
|
-
};
|
|
3651
|
-
if (!open || expanded) {
|
|
3652
|
-
reset();
|
|
3653
|
-
return reset;
|
|
3654
|
-
}
|
|
3655
|
-
const align = () => {
|
|
3656
|
-
const drawer = document.querySelector('.dcAgentWorkbench');
|
|
3657
|
-
if (!drawer) return;
|
|
3658
|
-
const drawerRect = drawer.getBoundingClientRect();
|
|
3659
|
-
for (const bar of document.querySelectorAll('.dcAgentCapabilities')) {
|
|
3660
|
-
const barRect = bar.getBoundingClientRect();
|
|
3661
|
-
const overlap = Math.max(0, barRect.right - drawerRect.left + 8);
|
|
3662
|
-
const leftRoom = Math.max(0, barRect.left - 88);
|
|
3663
|
-
const shift = Math.min(overlap, leftRoom);
|
|
3664
|
-
bar.style?.setProperty('--dc-agent-workbench-shift', `${-shift}px`);
|
|
3665
|
-
}
|
|
3666
|
-
};
|
|
3667
|
-
const view = document.defaultView;
|
|
3668
|
-
const frameId = typeof view?.requestAnimationFrame === 'function'
|
|
3669
|
-
? view.requestAnimationFrame(align)
|
|
3670
|
-
: null;
|
|
3671
|
-
view?.addEventListener?.('resize', align);
|
|
3672
|
-
align();
|
|
3673
|
-
return () => {
|
|
3674
|
-
if (frameId !== null) view?.cancelAnimationFrame?.(frameId);
|
|
3675
|
-
view?.removeEventListener?.('resize', align);
|
|
3676
|
-
reset();
|
|
3677
|
-
};
|
|
3678
|
-
}, [open, expanded, preferredWidth]);
|
|
3304
|
+
}, [open, visible, workflowTask?.id, workflowTask?.state, activeSessionId, step, actions]);
|
|
3679
3305
|
|
|
3680
|
-
//
|
|
3681
|
-
// 把关闭态 guard 放在全部 useStore 之后,避免首次关闭、随后打开时触发
|
|
3682
|
-
// React #310(Rendered more hooks than during the previous render)。
|
|
3683
|
-
if (!open) return null;
|
|
3306
|
+
// Host 决定 Tab 挂载/可见性;内容不再按私有 open flag 隐藏。
|
|
3684
3307
|
|
|
3685
3308
|
const hasData = dataset !== null && dataset.rowCount > 0;
|
|
3686
3309
|
const requiresQcc = objectives.includes('validate_identity') || objectives.includes('complete_fields');
|
|
@@ -4514,60 +4137,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4514
4137
|
);
|
|
4515
4138
|
}
|
|
4516
4139
|
|
|
4517
|
-
return h('
|
|
4518
|
-
className: '
|
|
4140
|
+
return h('section', {
|
|
4141
|
+
className: 'dcAgentWorkbenchContent',
|
|
4519
4142
|
'data-dsh-plugin': 'data-cleaning-agent',
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
stopJobsPolling();
|
|
4523
|
-
actions.close();
|
|
4524
|
-
}
|
|
4525
|
-
},
|
|
4143
|
+
'aria-label': '数据清洗补全工作台',
|
|
4144
|
+
'data-session-id': activeSessionId ?? undefined,
|
|
4526
4145
|
},
|
|
4527
|
-
h('aside', {
|
|
4528
|
-
className: `dcAgentWorkbench${expanded ? ' is-expanded' : ''}`,
|
|
4529
|
-
role: 'dialog',
|
|
4530
|
-
'aria-modal': 'false',
|
|
4531
|
-
'aria-label': '数据清洗补全工作台',
|
|
4532
|
-
'data-session-id': activeSessionId ?? undefined,
|
|
4533
|
-
id: 'dc-agent-workbench',
|
|
4534
|
-
onKeyDown: (event) => {
|
|
4535
|
-
if (event.key === 'Escape') {
|
|
4536
|
-
event.preventDefault();
|
|
4537
|
-
stopJobsPolling();
|
|
4538
|
-
actions.close();
|
|
4539
|
-
}
|
|
4540
|
-
},
|
|
4541
|
-
},
|
|
4542
|
-
h('div', { className: 'dcAgentResizeHandle', role: 'separator', tabIndex: 0,
|
|
4543
|
-
'aria-label': '调整工作台宽度', 'aria-orientation': 'vertical', 'aria-controls': 'dc-agent-workbench',
|
|
4544
|
-
title: '拖动调整宽度;左右方向键微调,Home/End 最小/最大,双击恢复默认' }),
|
|
4545
|
-
h('header', { className: 'dcAgentWbHeader' },
|
|
4546
|
-
h('div', { className: 'dcAgentWbTitle' },
|
|
4547
|
-
h('span', { className: 'dcAgentWbIcon', 'aria-hidden': 'true' }, h(DatabaseLogo, { size: 22 })),
|
|
4548
|
-
h('div', null,
|
|
4549
|
-
h('b', null, '数据清洗补全'),
|
|
4550
|
-
),
|
|
4551
|
-
),
|
|
4552
|
-
h('div', { className: 'dcAgentWbActions' },
|
|
4553
|
-
h('button', {
|
|
4554
|
-
className: 'dcAgentWbClose',
|
|
4555
|
-
type: 'button',
|
|
4556
|
-
'aria-label': expanded ? '收起工作台' : '展开工作台',
|
|
4557
|
-
onClick: () => actions.toggleExpanded(),
|
|
4558
|
-
}, expanded ? '↘' : '↖'),
|
|
4559
|
-
h('button', {
|
|
4560
|
-
className: 'dcAgentWbClose',
|
|
4561
|
-
type: 'button',
|
|
4562
|
-
title: '关闭工作台(Esc)',
|
|
4563
|
-
'aria-label': '关闭',
|
|
4564
|
-
onClick: () => {
|
|
4565
|
-
stopJobsPolling();
|
|
4566
|
-
actions.close();
|
|
4567
|
-
},
|
|
4568
|
-
}, '✕'),
|
|
4569
|
-
),
|
|
4570
|
-
),
|
|
4571
4146
|
h('nav', { className: 'dcAgentManagement', 'aria-label': '工作台管理' },
|
|
4572
4147
|
h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step !== 'history',
|
|
4573
4148
|
onClick: () => actions.setStep(cachedTask?.stage || 'upload') }, '当前任务'),
|
|
@@ -4618,10 +4193,233 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4618
4193
|
onClick: () => actions.setStep(!hasData ? 'upload' : !cachedTask || ['draft', 'uploaded'].includes(cachedTask.state) ? 'rules' : cachedTask.state === 'rules_confirmed' ? 'profile' : step === 'download' ? 'enrich' : 'match') }, '前往前置步骤'))
|
|
4619
4194
|
: pane,
|
|
4620
4195
|
),
|
|
4621
|
-
),
|
|
4622
4196
|
);
|
|
4623
4197
|
}
|
|
4624
4198
|
|
|
4199
|
+
// BEGIN generated Better Sidebar adapter (source: lib/better-sidebar-adapter.js)
|
|
4200
|
+
/** Public Better Sidebar seam. No business task creation, cancellation or geometry. */
|
|
4201
|
+
const CLEANING_TAB_ID = 'dsh-data-cleaning-agent:workbench';
|
|
4202
|
+
const SIDEBAR_DEPENDENCY_MESSAGE = '请安装或升级兼容的 dsh-better-sidebar 并重启 DSH(需要 targetedOpen 和 stateSubscription 能力),然后重新点击流程按钮。现有任务和文件不会删除。';
|
|
4203
|
+
|
|
4204
|
+
function sidebarCompatibility(service) {
|
|
4205
|
+
const methods = ['registerTab', 'openTab', 'isTabEnabled', 'getSnapshot', 'subscribeState'];
|
|
4206
|
+
return Boolean(service && methods.every(key => typeof service[key] === 'function')
|
|
4207
|
+
&& ['targetedOpen', 'stateSubscription'].every(key => service.features?.includes(key)));
|
|
4208
|
+
}
|
|
4209
|
+
|
|
4210
|
+
function contains(node, id) {
|
|
4211
|
+
if (!node) return false;
|
|
4212
|
+
return node.kind === 'leaf' ? node.tabs.some(tab => tab.id === id)
|
|
4213
|
+
: node.children.some(child => contains(child, id));
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
function revealCleaningTab(state, id) {
|
|
4217
|
+
if (state.floats?.some(item => item.tab.id === id)) return state;
|
|
4218
|
+
if (contains(state.bottomSplits, id)) return state.bottomOpen ? state : { ...state, bottomOpen: true };
|
|
4219
|
+
if (contains(state.splits, id)) return state.panelOpen ? state : { ...state, panelOpen: true };
|
|
4220
|
+
return state;
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4223
|
+
/** One adapter per plugin activation. Session navigation survives closing its Tab. */
|
|
4224
|
+
function createCleaningSidebarAdapter(service, { component, icon, onUnavailable = () => {} }) {
|
|
4225
|
+
const targets = new Map();
|
|
4226
|
+
const destinations = new Map();
|
|
4227
|
+
const pending = new Set();
|
|
4228
|
+
let disposed = false;
|
|
4229
|
+
const compatible = sidebarCompatibility(service);
|
|
4230
|
+
const applyPending = sessionId => {
|
|
4231
|
+
if (disposed || service.getSnapshot().sessionId !== sessionId || !pending.has(sessionId)) return;
|
|
4232
|
+
const target = targets.get(sessionId);
|
|
4233
|
+
if (!target) return;
|
|
4234
|
+
pending.delete(sessionId);
|
|
4235
|
+
target.navigate(destinations.get(sessionId));
|
|
4236
|
+
target.store.reduce(state => revealCleaningTab(state, target.tabId));
|
|
4237
|
+
};
|
|
4238
|
+
const unregister = compatible ? service.registerTab({
|
|
4239
|
+
id: CLEANING_TAB_ID, title: '数据清洗补全', icon, order: 30, single: true, component,
|
|
4240
|
+
}) : () => {};
|
|
4241
|
+
const unsubscribe = compatible ? service.subscribeState(() => {
|
|
4242
|
+
const sessionId = service.getSnapshot().sessionId;
|
|
4243
|
+
if (sessionId) applyPending(sessionId);
|
|
4244
|
+
}) : () => {};
|
|
4245
|
+
return {
|
|
4246
|
+
compatible,
|
|
4247
|
+
open(scope, destination = 'upload') {
|
|
4248
|
+
if (disposed) return false;
|
|
4249
|
+
if (!compatible || !service.isTabEnabled(CLEANING_TAB_ID)) {
|
|
4250
|
+
onUnavailable(compatible ? '请在 Better Sidebar 设置中启用「数据清洗补全」Tab。' : SIDEBAR_DEPENDENCY_MESSAGE);
|
|
4251
|
+
return false;
|
|
4252
|
+
}
|
|
4253
|
+
if (!scope?.sessionId) throw new Error('A target Session is required');
|
|
4254
|
+
destinations.set(scope.sessionId, destination);
|
|
4255
|
+
pending.add(scope.sessionId);
|
|
4256
|
+
service.openTab({ type: CLEANING_TAB_ID }, scope);
|
|
4257
|
+
applyPending(scope.sessionId);
|
|
4258
|
+
return true;
|
|
4259
|
+
},
|
|
4260
|
+
attach(scope, target) {
|
|
4261
|
+
if (disposed) return () => {};
|
|
4262
|
+
targets.set(scope.sessionId, target);
|
|
4263
|
+
// Without a pending shortcut, preserve the Session business store's current view.
|
|
4264
|
+
// Closing/reopening a Tab must not rewind an in-progress task to an older shortcut.
|
|
4265
|
+
applyPending(scope.sessionId);
|
|
4266
|
+
return () => { if (targets.get(scope.sessionId) === target) targets.delete(scope.sessionId); };
|
|
4267
|
+
},
|
|
4268
|
+
dispose() {
|
|
4269
|
+
if (disposed) return;
|
|
4270
|
+
disposed = true;
|
|
4271
|
+
unsubscribe(); unregister();
|
|
4272
|
+
targets.clear(); pending.clear(); destinations.clear();
|
|
4273
|
+
},
|
|
4274
|
+
};
|
|
4275
|
+
}
|
|
4276
|
+
|
|
4277
|
+
// END generated Better Sidebar adapter
|
|
4278
|
+
|
|
4279
|
+
// A shared owner across materialized root/Session factories, never a shared UI store.
|
|
4280
|
+
const CONTROLLER_KEY = Symbol.for('dsh.data-cleaning.session-workbench');
|
|
4281
|
+
const DEPENDENCY_EVENT = 'dsh:data-cleaning-sidebar-dependency';
|
|
4282
|
+
function installSessionWorkbench(ctx) {
|
|
4283
|
+
const host = typeof document === 'undefined' ? window : document;
|
|
4284
|
+
if (host[CONTROLLER_KEY]) {
|
|
4285
|
+
const shared = host[CONTROLLER_KEY];
|
|
4286
|
+
shared.references += 1;
|
|
4287
|
+
sessionWorkbenchController = shared.controller;
|
|
4288
|
+
let released = false;
|
|
4289
|
+
return () => { if (!released) { released = true; shared.release(); } };
|
|
4290
|
+
}
|
|
4291
|
+
let disposed = false;
|
|
4292
|
+
const instances = new Map();
|
|
4293
|
+
const spec = createWorkbenchStore();
|
|
4294
|
+
const instanceFor = sessionId => {
|
|
4295
|
+
if (!sessionId) throw new Error('请先打开数据清洗会话。');
|
|
4296
|
+
if (!instances.has(sessionId)) {
|
|
4297
|
+
const instance = spec.create(sessionId);
|
|
4298
|
+
instance.actions.setActiveSession(sessionId);
|
|
4299
|
+
instances.set(sessionId, instance);
|
|
4300
|
+
}
|
|
4301
|
+
return instances.get(sessionId);
|
|
4302
|
+
};
|
|
4303
|
+
let openingSession = null;
|
|
4304
|
+
const notify = message => {
|
|
4305
|
+
if (openingSession) instanceFor(openingSession).actions.setError(message);
|
|
4306
|
+
if (typeof document?.dispatchEvent === 'function') {
|
|
4307
|
+
const event = document.createEvent('CustomEvent');
|
|
4308
|
+
event.initCustomEvent(DEPENDENCY_EVENT, false, false, { sessionId: openingSession, message });
|
|
4309
|
+
document.dispatchEvent(event);
|
|
4310
|
+
}
|
|
4311
|
+
};
|
|
4312
|
+
let adapter;
|
|
4313
|
+
function SessionWorkbenchTab(props) {
|
|
4314
|
+
const instance = instanceFor(props.scope.sessionId);
|
|
4315
|
+
const useStore = pick => react.useSyncExternalStore(instance.subscribe,
|
|
4316
|
+
() => pick(instance.getSnapshot()), () => pick(instance.getSnapshot()));
|
|
4317
|
+
react.useEffect(() => {
|
|
4318
|
+
instance.actions.openAt(null, props.scope.sessionId);
|
|
4319
|
+
return adapter.attach(props.scope, { tabId: props.tab.id, store: props.store,
|
|
4320
|
+
navigate: destination => instance.actions.setStep(destination || instance.getSnapshot().step) });
|
|
4321
|
+
}, [instance, props.scope.sessionId, props.tab.id, props.store]);
|
|
4322
|
+
return h(WorkbenchContent, { useStore, actions: instance.actions, visible: props.visible,
|
|
4323
|
+
sendSessionCommand: (sessionId, prompt) => sendQccAgentCommand(ctx, sessionId, prompt),
|
|
4324
|
+
setSessionDraft: (sessionId, prompt) => setQccAgentDraft(ctx, sessionId, prompt) });
|
|
4325
|
+
}
|
|
4326
|
+
const adapterOptions = { component: SessionWorkbenchTab, icon: size => h(DatabaseLogo, { size }), onUnavailable: notify };
|
|
4327
|
+
adapter = createCleaningSidebarAdapter(null, adapterOptions);
|
|
4328
|
+
const connectProvider = service => {
|
|
4329
|
+
if (disposed) return () => {};
|
|
4330
|
+
const next = createCleaningSidebarAdapter(service, adapterOptions);
|
|
4331
|
+
adapter.dispose();
|
|
4332
|
+
adapter = next;
|
|
4333
|
+
return () => {
|
|
4334
|
+
next.dispose();
|
|
4335
|
+
if (!disposed && adapter === next) adapter = createCleaningSidebarAdapter(null, adapterOptions);
|
|
4336
|
+
};
|
|
4337
|
+
};
|
|
4338
|
+
// Optional child injection grants Cordis service access and follows provider removal.
|
|
4339
|
+
// Core conversation / Host tools remain loaded when Better Sidebar is absent.
|
|
4340
|
+
const providerFiber = typeof ctx.inject === 'function'
|
|
4341
|
+
? ctx.inject(['betterSidebar'], providerContext => {
|
|
4342
|
+
providerContext.effect(() => connectProvider(providerContext.betterSidebar), 'data-cleaning-agent: sidebar provider');
|
|
4343
|
+
})
|
|
4344
|
+
: null;
|
|
4345
|
+
const disconnectFixture = typeof ctx.inject !== 'function' ? connectProvider(ctx.betterSidebar) : () => {};
|
|
4346
|
+
const controller = {
|
|
4347
|
+
storeFor: instanceFor,
|
|
4348
|
+
actionsFor: sessionId => instanceFor(sessionId).actions,
|
|
4349
|
+
open(sessionId, step) {
|
|
4350
|
+
if (disposed || !sessionId) return false;
|
|
4351
|
+
const instance = instanceFor(sessionId);
|
|
4352
|
+
instance.actions.openAt(step, sessionId);
|
|
4353
|
+
const sessions = typeof ctx.get === 'function' ? ctx.get('sessions') : ctx.sessions;
|
|
4354
|
+
const summary = sessions?.list?.getSnapshot?.()?.byId?.[sessionId];
|
|
4355
|
+
openingSession = sessionId;
|
|
4356
|
+
try { return adapter.open({ sessionId, ...(summary?.cwd ? { cwd: summary.cwd } : {}) },
|
|
4357
|
+
step || instance.getSnapshot().step); }
|
|
4358
|
+
finally { openingSession = null; }
|
|
4359
|
+
},
|
|
4360
|
+
};
|
|
4361
|
+
sessionWorkbenchController = controller;
|
|
4362
|
+
const listeners = {
|
|
4363
|
+
[WORKBENCH_OPEN_EVENT]: event => {
|
|
4364
|
+
const detail = event.detail || {};
|
|
4365
|
+
const actions = controller.actionsFor(detail.sessionId);
|
|
4366
|
+
if (detail.task?.id) cacheWorkflowTask(actions, detail.sessionId, detail.task);
|
|
4367
|
+
controller.open(detail.sessionId, detail.step);
|
|
4368
|
+
},
|
|
4369
|
+
[WORKBENCH_DATASET_EVENT]: event => {
|
|
4370
|
+
const { sessionId, result, source } = event.detail;
|
|
4371
|
+
const actions = controller.actionsFor(sessionId);
|
|
4372
|
+
if (!result) return;
|
|
4373
|
+
applyParsed(result, actions, `session:${sessionId}`, source);
|
|
4374
|
+
void queueWorkflowOperation(sessionId, () => persistParsedWorkflow(actions, sessionId, result, source))
|
|
4375
|
+
.catch(error => { if (!disposed) actions.setError(error.message || String(error)); });
|
|
4376
|
+
},
|
|
4377
|
+
[WORKBENCH_DRAFT_EVENT]: event => {
|
|
4378
|
+
const { sessionId, draft = {} } = event.detail;
|
|
4379
|
+
const actions = controller.actionsFor(sessionId);
|
|
4380
|
+
if (draft.title) actions.setTaskTitle(draft.title);
|
|
4381
|
+
if (draft.objectives) actions.setObjectives(draft.objectives);
|
|
4382
|
+
if (draft.mappings) actions.setMappings(draft.mappings);
|
|
4383
|
+
if (draft.fieldSelection) actions.setFieldSelection(draft.fieldSelection);
|
|
4384
|
+
for (const [key, value] of Object.entries(draft.matchRules || {})) actions.setMatchRule(key, value);
|
|
4385
|
+
void queueWorkflowOperation(sessionId, async () => {
|
|
4386
|
+
const task = await ensureEditableWorkflowTask(actions, sessionId, draft);
|
|
4387
|
+
return updateWorkflowTask(actions, sessionId, task, draft);
|
|
4388
|
+
}).catch(error => { if (!disposed) actions.setError(error.message || String(error)); });
|
|
4389
|
+
},
|
|
4390
|
+
[WORKBENCH_PREPARE_EVENT]: event => {
|
|
4391
|
+
const { sessionId, config, resolve, reject } = event.detail;
|
|
4392
|
+
const actions = controller.actionsFor(sessionId);
|
|
4393
|
+
queueWorkflowOperation(sessionId, () => stageConfirmedWorkflow(actions, sessionId, config))
|
|
4394
|
+
.then(command => { if (!disposed) watchHostedCommand(actions, sessionId, command); resolve(command); }, reject);
|
|
4395
|
+
},
|
|
4396
|
+
};
|
|
4397
|
+
const bindings = Object.entries(listeners).map(([name, handler]) => {
|
|
4398
|
+
const listener = event => {
|
|
4399
|
+
if (disposed || event.defaultPrevented || !event.detail?.sessionId) return;
|
|
4400
|
+
event.preventDefault();
|
|
4401
|
+
handler(event);
|
|
4402
|
+
};
|
|
4403
|
+
host.addEventListener?.(name, listener);
|
|
4404
|
+
return () => host.removeEventListener?.(name, listener);
|
|
4405
|
+
});
|
|
4406
|
+
const shared = { controller, references: 1, release() {
|
|
4407
|
+
if (--shared.references > 0) return;
|
|
4408
|
+
disposed = true;
|
|
4409
|
+
commandWatchGeneration += 1;
|
|
4410
|
+
for (const remove of bindings) remove();
|
|
4411
|
+
adapter.dispose();
|
|
4412
|
+
disconnectFixture();
|
|
4413
|
+
void providerFiber?.dispose();
|
|
4414
|
+
instances.clear();
|
|
4415
|
+
if (host[CONTROLLER_KEY] === shared) delete host[CONTROLLER_KEY];
|
|
4416
|
+
if (sessionWorkbenchController === controller) sessionWorkbenchController = null;
|
|
4417
|
+
} };
|
|
4418
|
+
host[CONTROLLER_KEY] = shared;
|
|
4419
|
+
let released = false;
|
|
4420
|
+
return () => { if (!released) { released = true; shared.release(); } };
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4625
4423
|
function apply(ctx) {
|
|
4626
4424
|
// eslint-disable-next-line no-console
|
|
4627
4425
|
console.log('[dc-agent] client apply() ran');
|
|
@@ -4633,7 +4431,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4633
4431
|
promptSlot: 'conversation.input.overlay',
|
|
4634
4432
|
heroTitleBridge: 'exact-text/reversible',
|
|
4635
4433
|
headerSlot: 'conversation.session.header.actions',
|
|
4636
|
-
|
|
4434
|
+
workbench: 'better-sidebar:session-singleton',
|
|
4637
4435
|
error: null,
|
|
4638
4436
|
};
|
|
4639
4437
|
window.__DC_MVP__ = state;
|
|
@@ -4642,17 +4440,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4642
4440
|
ctx.effect(() => installUiStyles(), 'data-cleaning-agent: UI styles');
|
|
4643
4441
|
ctx.effect(() => installSessionOwnershipBridge(ctx), 'data-cleaning-agent: session ownership');
|
|
4644
4442
|
|
|
4645
|
-
|
|
4646
|
-
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
4647
|
-
name: 'shell.overlay',
|
|
4648
|
-
id: 'data-cleaning-agent',
|
|
4649
|
-
order: 200,
|
|
4650
|
-
store: workbenchStore,
|
|
4651
|
-
inject: () => ({
|
|
4652
|
-
sendSessionCommand: (sessionId, prompt) => sendQccAgentCommand(ctx, sessionId, prompt),
|
|
4653
|
-
setSessionDraft: (sessionId, prompt) => setQccAgentDraft(ctx, sessionId, prompt),
|
|
4654
|
-
}),
|
|
4655
|
-
}, WorkbenchDrawer));
|
|
4443
|
+
ctx.effect(() => installSessionWorkbench(ctx), 'data-cleaning-agent: Session workbench');
|
|
4656
4444
|
|
|
4657
4445
|
// 左栏:footer 只托管生命周期和 Portal 降级;实际入口显示在工作区列表前。
|
|
4658
4446
|
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
|
|
@@ -4715,7 +4503,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4715
4503
|
exports.inject = inject;
|
|
4716
4504
|
// 测试用纯函数,不构成 Host / DSH 稳定 API。
|
|
4717
4505
|
exports.__testing = {
|
|
4718
|
-
|
|
4506
|
+
createWorkbenchStore,
|
|
4507
|
+
WorkbenchContent,
|
|
4508
|
+
installSessionWorkbench,
|
|
4719
4509
|
DatabaseLogo,
|
|
4720
4510
|
visibleCatalogFields,
|
|
4721
4511
|
installCapabilityMount,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-data-cleaning-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"docs/USER-GUIDE.md",
|
|
23
23
|
"docs/FIRST-CONTRIBUTION.md",
|
|
24
24
|
"docs/COMPATIBILITY.md",
|
|
25
|
+
"docs/UI-V1.5.0-ADOPTION.md",
|
|
25
26
|
"docs/QCC-ENRICHMENT-DESIGN.md",
|
|
26
27
|
"docs/QCC-PHASES-ROADMAP.md",
|
|
27
28
|
"docs/PHASE2-ACCEPTANCE.md",
|