dsh-data-cleaning-agent 0.8.22 → 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 CHANGED
@@ -4,6 +4,19 @@
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
+
14
+ ## [0.8.23] - 2026-09-09
15
+
16
+ - 工作台顶部仅保留产品标题和展开、关闭按钮,移除重复状态与技术标识。
17
+ - 完整任务状态保留在进度卡;约束标题收缩,避免窄屏时与按钮重叠。
18
+ - 246 项测试与 10 组深浅主题、不同视口布局回归通过。
19
+
7
20
  ## [0.8.22] - 2026-09-09
8
21
 
9
22
  - 表格预览保留可读列宽、横向滚动和固定表头,避免多列挤压成竖排。
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.8.22** (stable release)
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.8.22
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.22
25
27
 
26
28
  > 在 DeepSeek Harness 中清洗、补全、画像企业名单数据的智能体插件:本地 CSV/XLSX/JSON 引擎 + 可选企查查(Qichacha/QCC)MCP 企业数据补全,由企查查(Qichacha/QCC)团队发起并维护。
27
29
  >
28
- > 当前源码版本 / Current source version: **0.8.22**(正式版本)
30
+ > 当前源码版本 / Current source version: **0.9.0**(正式版本)
29
31
 
30
32
  本版上传表格后展示完整清单及空白数量,用绿色、琥珀色、红色及文字区分已通过、待确认和未匹配映射。已映射字段自动纳入补全范围,额外字段默认折叠;多个原列可经确认使用同一非主体字段,分别补空并保留非空原值。修复 Host 草稿重置为默认五字段的问题,增加前后端版本错配提示。生成说明不查询,发送才执行;升级后需完整重启 DSH,历史下载文件不自动改写。
31
33
 
@@ -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 到输入框下方;右侧工作台使用 `shell.overlay`。
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
- .dcAgentOverlay, .dcAgentPromptLayer, .dcAgentPromptBackdrop, .dcAgentHeaderAction,
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
- .dcAgentOverlay {
288
- position: fixed;
289
- inset: 0;
290
- z-index: 1000;
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
- .dcAgentWorkbench {
299
- position: relative;
300
- box-sizing: border-box;
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,71 +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 b { display: block; font-size: 15px; }
643
- .dcAgentWbTitle small { display: block; max-width: 190px; color: var(--dc-muted); font-size: 12px; }
644
-
645
- .dcAgentWbActions {
646
- display: flex;
647
- align-items: center;
648
- gap: 6px;
649
- flex: 0 0 auto;
650
- margin-left: auto;
651
- }
652
-
653
- .dcAgentQccBadge {
654
- flex: 0 0 auto;
655
- padding: 4px 10px;
656
- border: 1px solid var(--dc-border);
657
- border-radius: 999px;
658
- color: var(--dc-muted);
659
- font-size: 12px;
660
- white-space: nowrap;
661
- }
662
-
663
- .dcAgentWbClose {
664
- flex: 0 0 auto;
665
- width: 30px;
666
- height: 30px;
667
- display: grid;
668
- place-items: center;
669
- border: 1px solid var(--dc-border);
670
- border-radius: 8px;
671
- background: transparent;
672
- color: inherit;
673
- font-size: 15px;
674
- cursor: pointer;
675
- }
676
- .dcAgentWbClose:hover, .dcAgentWbClose:focus-visible { background: var(--dc-page); }
677
-
678
454
  .dcAgentStepper {
679
455
  display: grid;
680
456
  grid-template-columns: repeat(var(--dc-stage-count, 5), minmax(0, 1fr));
@@ -1004,33 +780,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1004
780
  }
1005
781
  .dcAgentHistoryTask { display: block; width: 100%; padding: 10px; text-align: left; }
1006
782
  .dcAgentHistoryTask h3 { margin: 0; font-size: 12px; }
1007
- /*
1008
- * DSH 的 root.side slot 是覆盖层。桌面端工作台打开时,让带稳定标记的
1009
- * 会话滚动区主动让出同宽空间,确保消息流和中央 composer 不被遮挡。
1010
- * 移动端仍保留全屏工作台,不压缩底层会话。
1011
- */
1012
- @media (min-width: 1041px) {
1013
- body:has(.dcAgentWorkbench) [data-conversation-scroll] {
1014
- box-sizing: border-box;
1015
- padding-right: var(--dc-agent-workbench-reserve, min(460px, 42vw));
1016
- }
1017
- }
1018
- @media (min-width: 761px) and (max-width: 1040px) {
1019
- body:has(.dcAgentWorkbench) [data-conversation-scroll] {
1020
- box-sizing: border-box;
1021
- padding-right: var(--dc-agent-workbench-reserve, min(420px, 48vw));
1022
- }
1023
- }
1024
- @media (min-width: 761px) {
1025
- body:has(.dcAgentWorkbench.is-expanded) [data-conversation-scroll] {
1026
- box-sizing: border-box;
1027
- padding-right: var(--dc-agent-workbench-reserve, min(980px, calc(100vw - 420px)));
1028
- }
1029
- }
1030
783
  @media (max-width: 760px) {
1031
- .dcAgentWorkbench, .dcAgentWorkbench.is-expanded { width: 100vw; max-width: none; min-width: 0; border-radius: 0; }
1032
- .dcAgentOverlay { background: rgba(8, 10, 14, 0.22); pointer-events: auto; }
1033
- .dcAgentResizeHandle { display: none; }
1034
784
  .dcAgentQccBadge, .dcAgentJobsPill { display: none; }
1035
785
  .dcAgentCandidate { grid-template-columns: 1fr; }
1036
786
  .dcAgentCapabilities { justify-content: flex-start; padding-inline: 12px; }
@@ -1043,7 +793,6 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1043
793
  .dcAgentMappingRow > b { display: none; }
1044
794
  }
1045
795
  @media (min-width: 761px) and (max-width: 1040px) {
1046
- .dcAgentWorkbench { width: var(--dc-agent-panel-width, 420px); }
1047
796
  .dcAgentQccBadge, .dcAgentJobsPill { display: none; }
1048
797
  }
1049
798
  @media (max-width: 360px) {
@@ -1052,8 +801,8 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1052
801
  .dcAgentStepIcon svg { width: 18px; height: 18px; }
1053
802
  }
1054
803
  /* Distinct levels: management tabs → compact task steps → working content. */
1055
- .dcAgentWorkbench, .dcAgentPromptPanel { font-size: 14px; line-height: 1.5; }
1056
- .dcAgentWbHeader, .dcAgentManagement, .dcAgentStepper { flex: none; }
804
+ .dcAgentManagement, .dcAgentStepper { flex: none; }
805
+ .dcAgentWorkbenchContent, .dcAgentPromptPanel { font-size: 14px; line-height: 1.5; }
1057
806
  .dcAgentManagement { display: flex; gap: 24px; padding: 0 18px; border-bottom: 1px solid var(--dc-border); }
1058
807
  .dcAgentManagement .dcAgentButton { border: 0; border-bottom: 3px solid transparent; border-radius: 0; padding: 10px 0; background: transparent; }
1059
808
  .dcAgentManagement .dcAgentButton[aria-pressed="true"] { border-bottom-color: var(--dc-action); color: var(--dc-action); font-weight: 600; }
@@ -1071,7 +820,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1071
820
  .dcAgentPromptText, .dcAgentTextarea, .dcAgentField { font-size: 14px; }
1072
821
  .dcAgentPromptTrigger { border-radius: 6px; }
1073
822
  .dcAgentPromptAction.is-primary:hover:not(:disabled) { background: var(--dc-hover); }
1074
- :is(.dcAgentWorkbench, .dcAgentPromptPanel, .dcAgentCapabilities, .dcAgentPromptLayer) :is(button, input, select, textarea, summary):focus-visible,
823
+ :is(.dcAgentWorkbenchContent, .dcAgentPromptPanel, .dcAgentCapabilities, .dcAgentPromptLayer) :is(button, input, select, textarea, summary):focus-visible,
1075
824
  .dcAgentLauncher:focus-visible { outline: 2px solid var(--dc-action); outline-offset: 2px; }
1076
825
  @media (max-width: 760px) {
1077
826
  [data-dc-agent-hero-title="true"] { font-size: 20px; }
@@ -1334,11 +1083,6 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1334
1083
  if (!activeCleaningSessionId || (sessionId && sessionId !== activeCleaningSessionId)) return false;
1335
1084
  const previousSessionId = activeCleaningSessionId;
1336
1085
  activeCleaningSessionId = null;
1337
- if (rootWorkbenchActions) {
1338
- stopJobsPolling();
1339
- rootWorkbenchActions.close?.();
1340
- rootWorkbenchActions.setActiveSession?.(null);
1341
- }
1342
1086
  if (typeof window.CustomEvent === 'function' && typeof window.dispatchEvent === 'function') {
1343
1087
  window.dispatchEvent(new window.CustomEvent(CLEANING_SESSION_EVENT, {
1344
1088
  detail: { sessionId: previousSessionId, active: false },
@@ -1722,9 +1466,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1722
1466
  return requestJson(path, body === undefined ? 'GET' : 'POST', body);
1723
1467
  }
1724
1468
 
1725
- async function waitForQccCommand(commandId, attempts = 180, onStatus) {
1469
+ async function waitForQccCommand(commandId, attempts = 180, onStatus, isActive = () => true) {
1726
1470
  for (let attempt = 0; attempt < attempts; attempt += 1) {
1471
+ if (!isActive()) return null;
1727
1472
  const response = await api(`/data-cleaning/api/g5/commands/${encodeURIComponent(commandId)}`);
1473
+ if (!isActive()) return null;
1728
1474
  const command = response?.command;
1729
1475
  await onStatus?.(command);
1730
1476
  if (command?.state === 'completed' || command?.state === 'failed') return command;
@@ -1751,6 +1497,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1751
1497
  if (Array.isArray(task.objectives)) actions.setObjectives(task.objectives);
1752
1498
  if (Array.isArray(task.mappings)) actions.setMappings(task.mappings);
1753
1499
  if (Array.isArray(task.fieldSelection)) actions.setFieldSelection(task.fieldSelection);
1500
+ for (const [key, value] of Object.entries(task.matchRules || {})) actions.setMatchRule(key, value);
1754
1501
  return task;
1755
1502
  }
1756
1503
 
@@ -1893,7 +1640,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1893
1640
  }
1894
1641
  return response.task;
1895
1642
  }
1643
+ let commandWatchGeneration = 0;
1896
1644
  function watchHostedCommand(actions, sessionId, command) {
1645
+ const generation = commandWatchGeneration;
1646
+ const isActive = () => generation === commandWatchGeneration;
1897
1647
  let openedForExecution = false;
1898
1648
  const ownerVisible = () => typeof document === 'undefined' || [...document.querySelectorAll('.dcAgentExperience')].some(node => node.dataset.sessionId === sessionId);
1899
1649
  void waitForQccCommand(command.commandId, 1800, async (status) => {
@@ -1904,11 +1654,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1904
1654
  openWorkbench(actions, workflowTaskBySession.get(String(sessionId))?.stage || 'match', sessionId);
1905
1655
  }
1906
1656
  if (isCleaningSession(sessionId) && workflowTaskBySession.get(String(sessionId))?.id === command.taskId) actions.setCommandProgress?.({ taskId: command.taskId, ...status.progress });
1907
- }).then(async (completed) => {
1657
+ }, isActive).then(async (completed) => {
1658
+ if (!completed || !isActive()) return;
1908
1659
  await refreshHostedTask(actions, sessionId, command.taskId, completed.run);
1909
1660
  if (completed.state === 'failed') throw new Error(completed.error?.message || 'Host 执行失败,请在工作台查看状态。');
1910
1661
  }).catch((error) => {
1911
- if (isCleaningSession(sessionId)) actions.setError(error.message || String(error));
1662
+ if (isActive() && isCleaningSession(sessionId)) actions.setError(error.message || String(error));
1912
1663
  });
1913
1664
  }
1914
1665
 
@@ -1917,8 +1668,6 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1917
1668
  return defineStore({
1918
1669
  init: () => ({
1919
1670
  open: false,
1920
- expanded: false,
1921
- preferredWidth: null,
1922
1671
  step: 'upload',
1923
1672
  busy: false,
1924
1673
  error: null,
@@ -1952,11 +1701,6 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1952
1701
  if (sessionId) draft.activeSessionId = sessionId;
1953
1702
  },
1954
1703
  close: (draft) => { draft.open = false; },
1955
- toggleExpanded: (draft) => { draft.expanded = !draft.expanded; },
1956
- setPreferredWidth: (draft, width) => {
1957
- draft.preferredWidth = Number.isFinite(width) ? width : null;
1958
- draft.expanded = false;
1959
- },
1960
1704
  setStep: (draft, step) => { draft.step = step; },
1961
1705
  setActiveSession: (draft, sessionId) => { draft.activeSessionId = sessionId ?? null; },
1962
1706
  setBusy: (draft, busy) => { draft.busy = busy; },
@@ -2211,20 +1955,19 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2211
1955
 
2212
1956
  function openWorkbench(actions, step, sessionId) {
2213
1957
  actions.openAt(step, sessionId);
2214
- startJobsPolling(actions);
1958
+ sessionWorkbenchController?.open(sessionId, step);
2215
1959
  }
2216
1960
 
2217
1961
  const WORKBENCH_OPEN_EVENT = 'dsh:data-cleaning-workbench-open';
2218
1962
  const WORKBENCH_DATASET_EVENT = 'dsh:data-cleaning-workbench-dataset';
2219
1963
  const WORKBENCH_DRAFT_EVENT = 'dsh:data-cleaning-workbench-draft';
2220
1964
  const WORKBENCH_PREPARE_EVENT = 'dsh:data-cleaning-workbench-prepare';
2221
- const WORKBENCH_OWNER_LEFT_EVENT = 'dsh:data-cleaning-workbench-owner-left';
2222
1965
 
2223
1966
  // session scope 不能复用 root scope 的 store handle。DSH 当前会分别物化 root/session
2224
1967
  // 插槽组件,因此以 document 事件跨 scope 通知;闭包引用仅作为无 DOM 测试降级。
2225
- let rootWorkbenchActions = null;
1968
+ let sessionWorkbenchController = null;
2226
1969
 
2227
- /** 从 session scope 的 composer/header 触发 root scope 工作台。 */
1970
+ /** 从 composer/header 请求指定 Session 的唯一工作台 Tab。 */
2228
1971
  function requestWorkbenchOpen(step, sessionId, task = null) {
2229
1972
  if (typeof document !== 'undefined'
2230
1973
  && typeof document.createEvent === 'function'
@@ -2233,7 +1976,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2233
1976
  event.initCustomEvent(WORKBENCH_OPEN_EVENT, false, true, { step, sessionId, task });
2234
1977
  if (!document.dispatchEvent(event)) return;
2235
1978
  }
2236
- if (rootWorkbenchActions) openWorkbench(rootWorkbenchActions, step, sessionId);
1979
+ sessionWorkbenchController?.open(sessionId, step);
2237
1980
  }
2238
1981
 
2239
1982
  function requestPreparedWorkflow(sessionId, config) {
@@ -2244,7 +1987,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2244
1987
  });
2245
1988
  }
2246
1989
 
2247
- /** 把提示词生成器解析出的完整表格交给 root 工作台,不把整表塞进模型上下文。 */
1990
+ /** 完整表格交给 Session 业务控制器,不把整表塞进模型上下文。 */
2248
1991
  function requestWorkbenchDataset(result, sessionId, source) {
2249
1992
  if (typeof document !== 'undefined'
2250
1993
  && typeof document.createEvent === 'function'
@@ -2253,9 +1996,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2253
1996
  event.initCustomEvent(WORKBENCH_DATASET_EVENT, false, true, { result, sessionId, source });
2254
1997
  if (!document.dispatchEvent(event)) return;
2255
1998
  }
2256
- if (rootWorkbenchActions) {
2257
- applyParsed(result, rootWorkbenchActions, `session:${sessionId || 'unassigned'}`, source);
2258
- rootWorkbenchActions.setActiveSession(sessionId);
1999
+ if (sessionWorkbenchController?.actionsFor(sessionId)) {
2000
+ applyParsed(result, sessionWorkbenchController?.actionsFor(sessionId), `session:${sessionId || 'unassigned'}`, source);
2001
+ sessionWorkbenchController?.actionsFor(sessionId).setActiveSession(sessionId);
2259
2002
  }
2260
2003
  }
2261
2004
 
@@ -2268,11 +2011,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2268
2011
  event.initCustomEvent(WORKBENCH_DRAFT_EVENT, false, true, { draft, sessionId });
2269
2012
  if (!document.dispatchEvent(event)) return;
2270
2013
  }
2271
- if (rootWorkbenchActions) {
2272
- rootWorkbenchActions.setTaskTitle(draft.title);
2273
- rootWorkbenchActions.setObjectives(draft.objectives);
2274
- if (draft.mappings) rootWorkbenchActions.setMappings(draft.mappings);
2275
- rootWorkbenchActions.setFieldSelection(draft.fieldSelection);
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);
2276
2019
  }
2277
2020
  }
2278
2021
 
@@ -2581,10 +2324,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2581
2324
  function DataCleaningExperience(props) {
2582
2325
  const { sessionId, session: ownerSession } = props;
2583
2326
  const enabled = useCleaningSession(sessionId);
2327
+ const [dependencyMessage, setDependencyMessage] = react.useState('');
2584
2328
  react.useEffect(() => {
2585
- if (!enabled) return undefined;
2586
- return () => document.dispatchEvent(new CustomEvent(WORKBENCH_OWNER_LEFT_EVENT, { detail: { sessionId } }));
2587
- }, [enabled, sessionId]);
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]);
2588
2333
  const hero = ownerSession?.composerPhase === 'blank';
2589
2334
  const [menuMount, setMenuMount] = react.useState(null);
2590
2335
  react.useEffect(() => {
@@ -2602,6 +2347,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2602
2347
  className: `dcAgentExperience${hero ? ' is-home' : ''}`,
2603
2348
  'data-session-id': sessionId,
2604
2349
  },
2350
+ dependencyMessage ? h('p', { className: 'dcAgentError', role: 'alert' }, dependencyMessage) : null,
2605
2351
  menuMount ? reactDom.createPortal(h(CapabilityBar, { sessionId }), menuMount) : h(CapabilityBar, { sessionId }),
2606
2352
  hero ? h(ProductHome, { sessionId }) : null,
2607
2353
  );
@@ -2949,7 +2695,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2949
2695
  const hasImage = files.length > 0;
2950
2696
  if (!hasImage) return;
2951
2697
  const target = event.target;
2952
- const inComposer = !open && !target?.closest?.('.dcAgentWorkbench')
2698
+ const inComposer = !open && !target?.closest?.('.dcAgentWorkbenchContent')
2953
2699
  && Boolean(target?.closest?.('textarea, [contenteditable="true"], [role="textbox"]'));
2954
2700
  const inSourceStep = open && wizardStep === 1 && Boolean(target?.closest?.('.dcAgentPromptPanel'));
2955
2701
  if (!inComposer && !inSourceStep) return;
@@ -3488,15 +3234,14 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3488
3234
  );
3489
3235
  }
3490
3236
 
3491
- /** 右侧非模态工作台:中央区域始终保留 DSH 原生会话。 */
3492
- function WorkbenchDrawer(props) {
3237
+ /** Session Tab 业务内容;尺寸、停靠、关闭均由 Better Sidebar 宿主管理。 */
3238
+ function WorkbenchContent(props) {
3493
3239
  const { useStore, actions } = props;
3240
+ const visible = props.visible !== false;
3494
3241
  const sendSessionCommand = props.sendSessionCommand;
3495
3242
  const setSessionDraft = props.setSessionDraft;
3496
3243
  const open = useStore((state) => state.open);
3497
3244
  const step = useStore((state) => state.step);
3498
- const expanded = useStore((state) => state.expanded);
3499
- const preferredWidth = useStore((state) => state.preferredWidth);
3500
3245
  const busy = useStore((state) => state.busy);
3501
3246
  const error = useStore((state) => state.error);
3502
3247
  const input = useStore((state) => state.input);
@@ -3518,92 +3263,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3518
3263
  const qccRun = useStore((state) => state.qccRun);
3519
3264
  const commandProgress = useStore((state) => state.commandProgress);
3520
3265
  const paidConfirmed = useStore((state) => state.paidConfirmed);
3521
- const jobs = useStore((state) => state.jobs);
3522
3266
  const activeSessionId = useStore((state) => state.activeSessionId);
3523
3267
  const [fieldQuery, setFieldQuery] = react.useState('');
3524
3268
 
3525
3269
  react.useEffect(() => {
3526
- if (!open || typeof document === 'undefined') return undefined;
3527
- const drawer = document.querySelector('.dcAgentWorkbench');
3528
- if (!drawer) return undefined;
3529
- return installWorkbenchSizing(drawer, { preferredWidth, expanded, onWidthChange: actions.setPreferredWidth });
3530
- }, [open, expanded, preferredWidth, actions]);
3531
-
3532
- react.useEffect(() => {
3533
- rootWorkbenchActions = actions;
3534
- const handleOwnerLeft = () => actions.close();
3535
- const handleOpen = (event) => {
3536
- event?.preventDefault?.();
3537
- const detail = event?.detail ?? {};
3538
- if (detail.task?.id) {
3539
- workflowTaskBySession.set(String(detail.sessionId || 'unassigned'), detail.task);
3540
- actions.setWorkflowTask(detail.task);
3541
- actions.setTaskTitle(detail.task.title);
3542
- actions.setMappings(detail.task.mappings || []);
3543
- actions.setObjectives(detail.task.objectives || []);
3544
- actions.setFieldSelection(detail.task.fieldSelection || []);
3545
- for (const [key, value] of Object.entries(detail.task.matchRules || {})) actions.setMatchRule(key, value);
3546
- }
3547
- openWorkbench(actions, detail.step ?? null, detail.sessionId ?? null);
3548
- };
3549
- const handleDataset = (event) => {
3550
- event?.preventDefault?.();
3551
- const detail = event?.detail ?? {};
3552
- if (detail.result) applyParsed(detail.result, actions, `session:${detail.sessionId || 'unassigned'}`, detail.source);
3553
- if (detail.sessionId) actions.setActiveSession(detail.sessionId);
3554
- if (detail.result) {
3555
- void queueWorkflowOperation(detail.sessionId, () => (
3556
- persistParsedWorkflow(actions, detail.sessionId, detail.result, detail.source)
3557
- )).catch((eventError) => {
3558
- actions.setError(eventError instanceof Error ? eventError.message : String(eventError));
3559
- });
3560
- }
3561
- };
3562
- const handlePrepare = (event) => {
3563
- event.preventDefault();
3564
- const { sessionId, config, resolve, reject } = event.detail;
3565
- queueWorkflowOperation(sessionId, () => stageConfirmedWorkflow(actions, sessionId, config)).then((command) => {
3566
- watchHostedCommand(actions, sessionId, command);
3567
- resolve(command);
3568
- }, reject);
3569
- };
3570
- const handleDraft = (event) => {
3571
- event?.preventDefault?.();
3572
- const detail = event?.detail ?? {};
3573
- const draft = detail.draft ?? {};
3574
- if (detail.sessionId) actions.setActiveSession(detail.sessionId);
3575
- if (draft.title) actions.setTaskTitle(draft.title);
3576
- if (draft.objectives) actions.setObjectives(draft.objectives);
3577
- if (draft.mappings) actions.setMappings(draft.mappings);
3578
- if (draft.fieldSelection) actions.setFieldSelection(draft.fieldSelection);
3579
- for (const [key, value] of Object.entries(draft.matchRules || {})) actions.setMatchRule(key, value);
3580
- void queueWorkflowOperation(detail.sessionId, async () => {
3581
- const task = await ensureEditableWorkflowTask(actions, detail.sessionId, draft);
3582
- return updateWorkflowTask(actions, detail.sessionId, task, draft);
3583
- }).catch((eventError) => actions.setError(eventError instanceof Error ? eventError.message : String(eventError)));
3584
- };
3585
- if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
3586
- document.addEventListener(WORKBENCH_OPEN_EVENT, handleOpen);
3587
- document.addEventListener(WORKBENCH_OWNER_LEFT_EVENT, handleOwnerLeft);
3588
- document.addEventListener(WORKBENCH_DATASET_EVENT, handleDataset);
3589
- document.addEventListener(WORKBENCH_DRAFT_EVENT, handleDraft);
3590
- document.addEventListener(WORKBENCH_PREPARE_EVENT, handlePrepare);
3591
- }
3592
- return () => {
3593
- if (typeof document !== 'undefined' && typeof document.removeEventListener === 'function') {
3594
- document.removeEventListener(WORKBENCH_OPEN_EVENT, handleOpen);
3595
- document.removeEventListener(WORKBENCH_OWNER_LEFT_EVENT, handleOwnerLeft);
3596
- document.removeEventListener(WORKBENCH_DATASET_EVENT, handleDataset);
3597
- document.removeEventListener(WORKBENCH_DRAFT_EVENT, handleDraft);
3598
- document.removeEventListener(WORKBENCH_PREPARE_EVENT, handlePrepare);
3599
- }
3600
- stopJobsPolling();
3601
- if (rootWorkbenchActions === actions) rootWorkbenchActions = null;
3602
- };
3603
- }, [actions]);
3604
-
3605
- react.useEffect(() => {
3606
- if (!open) return undefined;
3270
+ if (!open || !visible) return undefined;
3607
3271
  let disposed = false;
3608
3272
  requestJson('/data-cleaning/api/workflow/contract').then((response) => {
3609
3273
  if (!disposed) actions.setWorkflowContract(response.contract);
@@ -3611,19 +3275,21 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3611
3275
  loadWorkflowTasks(actions);
3612
3276
  if (activeSessionId) {
3613
3277
  queueWorkflowOperation(activeSessionId, async () => {
3614
- const task = await ensureWorkflowTask(actions, activeSessionId, {}, step !== 'history');
3615
- await refreshHostedTask(actions, activeSessionId, task.id, null, false);
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);
3616
3282
  }).catch((taskError) => {
3617
3283
  if (!disposed) actions.setError(taskError instanceof Error ? taskError.message : String(taskError));
3618
3284
  });
3619
3285
  }
3620
3286
  return () => { disposed = true; };
3621
- }, [open, activeSessionId, actions]);
3287
+ }, [open, visible, activeSessionId, actions]);
3622
3288
 
3623
3289
  // Reopening during execution follows Host metadata, even when the original
3624
3290
  // command watcher was lost on page reload. This performs no QCC calls.
3625
3291
  react.useEffect(() => {
3626
- if (!open || !workflowTask?.id || !['matching', 'enriching'].includes(workflowTask.state)) return undefined;
3292
+ if (!open || !visible || !workflowTask?.id || !['matching', 'enriching'].includes(workflowTask.state)) return undefined;
3627
3293
  let disposed = false, timer;
3628
3294
  const poll = async () => {
3629
3295
  try {
@@ -3635,51 +3301,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3635
3301
  };
3636
3302
  timer = setTimeout(poll, 2000);
3637
3303
  return () => { disposed = true; clearTimeout(timer); };
3638
- }, [open, workflowTask?.id, workflowTask?.state, activeSessionId, step, actions]);
3639
-
3640
- // 非模态右栏会覆盖 Host 的中央画布;按实际重叠量把 composer 能力栏左移,
3641
- // 保证 Mockup 中的五个入口在窄桌面视口仍可见、可点击。展开态专注工作台,
3642
- // 不强行挤压到不足一栏的剩余区域。
3643
- react.useEffect(() => {
3644
- if (typeof document === 'undefined' || typeof document.querySelectorAll !== 'function') return undefined;
3645
- const reset = () => {
3646
- for (const bar of document.querySelectorAll('.dcAgentCapabilities')) {
3647
- bar.style?.removeProperty('--dc-agent-workbench-shift');
3648
- }
3649
- };
3650
- if (!open || expanded) {
3651
- reset();
3652
- return reset;
3653
- }
3654
- const align = () => {
3655
- const drawer = document.querySelector('.dcAgentWorkbench');
3656
- if (!drawer) return;
3657
- const drawerRect = drawer.getBoundingClientRect();
3658
- for (const bar of document.querySelectorAll('.dcAgentCapabilities')) {
3659
- const barRect = bar.getBoundingClientRect();
3660
- const overlap = Math.max(0, barRect.right - drawerRect.left + 8);
3661
- const leftRoom = Math.max(0, barRect.left - 88);
3662
- const shift = Math.min(overlap, leftRoom);
3663
- bar.style?.setProperty('--dc-agent-workbench-shift', `${-shift}px`);
3664
- }
3665
- };
3666
- const view = document.defaultView;
3667
- const frameId = typeof view?.requestAnimationFrame === 'function'
3668
- ? view.requestAnimationFrame(align)
3669
- : null;
3670
- view?.addEventListener?.('resize', align);
3671
- align();
3672
- return () => {
3673
- if (frameId !== null) view?.cancelAnimationFrame?.(frameId);
3674
- view?.removeEventListener?.('resize', align);
3675
- reset();
3676
- };
3677
- }, [open, expanded, preferredWidth]);
3304
+ }, [open, visible, workflowTask?.id, workflowTask?.state, activeSessionId, step, actions]);
3678
3305
 
3679
- // DSH 的 useStore 基于 React hooks;所有订阅必须在每次渲染时以相同顺序调用。
3680
- // 把关闭态 guard 放在全部 useStore 之后,避免首次关闭、随后打开时触发
3681
- // React #310(Rendered more hooks than during the previous render)。
3682
- if (!open) return null;
3306
+ // Host 决定 Tab 挂载/可见性;内容不再按私有 open flag 隐藏。
3683
3307
 
3684
3308
  const hasData = dataset !== null && dataset.rowCount > 0;
3685
3309
  const requiresQcc = objectives.includes('validate_identity') || objectives.includes('complete_fields');
@@ -4513,66 +4137,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4513
4137
  );
4514
4138
  }
4515
4139
 
4516
- return h('div', {
4517
- className: 'dcAgentOverlay',
4140
+ return h('section', {
4141
+ className: 'dcAgentWorkbenchContent',
4518
4142
  'data-dsh-plugin': 'data-cleaning-agent',
4519
- onClick: (event) => {
4520
- if (event.target === event.currentTarget) {
4521
- stopJobsPolling();
4522
- actions.close();
4523
- }
4524
- },
4143
+ 'aria-label': '数据清洗补全工作台',
4144
+ 'data-session-id': activeSessionId ?? undefined,
4525
4145
  },
4526
- h('aside', {
4527
- className: `dcAgentWorkbench${expanded ? ' is-expanded' : ''}`,
4528
- role: 'dialog',
4529
- 'aria-modal': 'false',
4530
- 'aria-label': '数据清洗补全工作台',
4531
- 'data-session-id': activeSessionId ?? undefined,
4532
- id: 'dc-agent-workbench',
4533
- onKeyDown: (event) => {
4534
- if (event.key === 'Escape') {
4535
- event.preventDefault();
4536
- stopJobsPolling();
4537
- actions.close();
4538
- }
4539
- },
4540
- },
4541
- h('div', { className: 'dcAgentResizeHandle', role: 'separator', tabIndex: 0,
4542
- 'aria-label': '调整工作台宽度', 'aria-orientation': 'vertical', 'aria-controls': 'dc-agent-workbench',
4543
- title: '拖动调整宽度;左右方向键微调,Home/End 最小/最大,双击恢复默认' }),
4544
- h('header', { className: 'dcAgentWbHeader' },
4545
- h('div', { className: 'dcAgentWbTitle' },
4546
- h('span', { className: 'dcAgentWbIcon', 'aria-hidden': 'true' }, h(DatabaseLogo, { size: 22 })),
4547
- h('div', null,
4548
- h('b', null, '数据清洗补全'),
4549
- h('small', null, cachedTask ? `${WORKFLOW_STATE_LABELS[cachedTask.state] || cachedTask.state} · ${cachedTask.id.slice(0, 18)}` : '正在创建 Host taskId…'),
4550
- ),
4551
- ),
4552
- h('div', { className: 'dcAgentWbActions' },
4553
- h('span', {
4554
- className: 'dcAgentQccBadge',
4555
- title: qccRun ? `任务 ${qccRun.runId}` : '仅在当前用户确认使用自己的企查查账号后调用',
4556
- }, qccRun ? `QCC · ${qccRun.state}` : qccCapabilities ? (qccCapabilities.capabilities?.ready ? 'QCC · 工具已就绪' : 'QCC · 能力不完整') : 'QCC · 自动校验'),
4557
- h('span', { className: 'dcAgentJobsPill', 'data-state': cachedTask?.state || 'idle', title: '当前清洗任务状态' }, cachedTask ? WORKFLOW_STATE_LABELS[cachedTask.state] || cachedTask.state : '尚未开始'),
4558
- h('button', {
4559
- className: 'dcAgentWbClose',
4560
- type: 'button',
4561
- 'aria-label': expanded ? '收起工作台' : '展开工作台',
4562
- onClick: () => actions.toggleExpanded(),
4563
- }, expanded ? '↘' : '↖'),
4564
- h('button', {
4565
- className: 'dcAgentWbClose',
4566
- type: 'button',
4567
- title: '关闭工作台(Esc)',
4568
- 'aria-label': '关闭',
4569
- onClick: () => {
4570
- stopJobsPolling();
4571
- actions.close();
4572
- },
4573
- }, '✕'),
4574
- ),
4575
- ),
4576
4146
  h('nav', { className: 'dcAgentManagement', 'aria-label': '工作台管理' },
4577
4147
  h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step !== 'history',
4578
4148
  onClick: () => actions.setStep(cachedTask?.stage || 'upload') }, '当前任务'),
@@ -4623,10 +4193,233 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4623
4193
  onClick: () => actions.setStep(!hasData ? 'upload' : !cachedTask || ['draft', 'uploaded'].includes(cachedTask.state) ? 'rules' : cachedTask.state === 'rules_confirmed' ? 'profile' : step === 'download' ? 'enrich' : 'match') }, '前往前置步骤'))
4624
4194
  : pane,
4625
4195
  ),
4626
- ),
4627
4196
  );
4628
4197
  }
4629
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
+
4630
4423
  function apply(ctx) {
4631
4424
  // eslint-disable-next-line no-console
4632
4425
  console.log('[dc-agent] client apply() ran');
@@ -4638,7 +4431,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4638
4431
  promptSlot: 'conversation.input.overlay',
4639
4432
  heroTitleBridge: 'exact-text/reversible',
4640
4433
  headerSlot: 'conversation.session.header.actions',
4641
- overlay: 'shell.overlay',
4434
+ workbench: 'better-sidebar:session-singleton',
4642
4435
  error: null,
4643
4436
  };
4644
4437
  window.__DC_MVP__ = state;
@@ -4647,17 +4440,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4647
4440
  ctx.effect(() => installUiStyles(), 'data-cleaning-agent: UI styles');
4648
4441
  ctx.effect(() => installSessionOwnershipBridge(ctx), 'data-cleaning-agent: session ownership');
4649
4442
 
4650
- // 右侧工作台:additive overlay,不替换 DSH 单占位 details 面板。
4651
- ctx.slots.inject('shell.overlay', () => ctx.slots.register({
4652
- name: 'shell.overlay',
4653
- id: 'data-cleaning-agent',
4654
- order: 200,
4655
- store: workbenchStore,
4656
- inject: () => ({
4657
- sendSessionCommand: (sessionId, prompt) => sendQccAgentCommand(ctx, sessionId, prompt),
4658
- setSessionDraft: (sessionId, prompt) => setQccAgentDraft(ctx, sessionId, prompt),
4659
- }),
4660
- }, WorkbenchDrawer));
4443
+ ctx.effect(() => installSessionWorkbench(ctx), 'data-cleaning-agent: Session workbench');
4661
4444
 
4662
4445
  // 左栏:footer 只托管生命周期和 Portal 降级;实际入口显示在工作区列表前。
4663
4446
  ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
@@ -4720,7 +4503,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4720
4503
  exports.inject = inject;
4721
4504
  // 测试用纯函数,不构成 Host / DSH 稳定 API。
4722
4505
  exports.__testing = {
4723
- workbenchWidthBounds,
4506
+ createWorkbenchStore,
4507
+ WorkbenchContent,
4508
+ installSessionWorkbench,
4724
4509
  DatabaseLogo,
4725
4510
  visibleCatalogFields,
4726
4511
  installCapabilityMount,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-data-cleaning-agent",
3
- "version": "0.8.22",
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",