dsh-data-cleaning-agent 0.8.23 → 0.9.1

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/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,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
- .dcAgentWorkbench, .dcAgentPromptPanel { font-size: 14px; line-height: 1.5; }
1057
- .dcAgentWbHeader, .dcAgentManagement, .dcAgentStepper { flex: none; }
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(.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,
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; },
@@ -2067,11 +1810,20 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2067
1810
 
2068
1811
  // rc/alpha 兼容边界:原生 startSession 会复用工作区内的空白会话。
2069
1812
  // 只在清洗会话激活时替换此动作,创建普通会话;不改 connectWorkspace 或全局点击。
2070
- function installNewSessionBridge(ctx) {
1813
+ function installNewSessionBridge(ctx, navigation) {
1814
+ if (!navigation && typeof ctx.inject === 'function') {
1815
+ const legacy = typeof ctx.workspaces?.startSession === 'function'
1816
+ ? installNewSessionBridge(ctx, ctx.workspaces) : () => {};
1817
+ const fiber = ctx.inject(['uiWorkspace'], scope => {
1818
+ scope.effect(() => installNewSessionBridge(scope, scope.uiWorkspace), 'data-cleaning-agent: native new session');
1819
+ });
1820
+ return () => { legacy(); fiber?.dispose?.(); };
1821
+ }
2071
1822
  const workspaces = ctx.workspaces;
2072
- const original = workspaces?.startSession;
1823
+ navigation = navigation || workspaces;
1824
+ const original = navigation?.startSession;
2073
1825
  if (typeof original !== 'function' || typeof ctx.sessions?.create !== 'function') return () => {};
2074
- const ownDescriptor = Object.getOwnPropertyDescriptor(workspaces, 'startSession');
1826
+ const ownDescriptor = Object.getOwnPropertyDescriptor(navigation, 'startSession');
2075
1827
  let pending = null;
2076
1828
  let disposed = false;
2077
1829
  const wrapped = function (workspaceId) {
@@ -2094,13 +1846,13 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2094
1846
  console.warn('[dc-agent] new session failed; current draft retained:', error instanceof Error ? error.message : String(error));
2095
1847
  }).finally(() => { pending = null; });
2096
1848
  };
2097
- try { workspaces.startSession = wrapped; }
1849
+ try { navigation.startSession = wrapped; }
2098
1850
  catch (_error) { console.warn('[dc-agent] New Session compatibility bridge unavailable'); return () => {}; }
2099
1851
  return () => {
2100
1852
  disposed = true;
2101
- if (workspaces.startSession !== wrapped) return;
2102
- if (ownDescriptor) Object.defineProperty(workspaces, 'startSession', ownDescriptor);
2103
- else delete workspaces.startSession;
1853
+ if (navigation.startSession !== wrapped) return;
1854
+ if (ownDescriptor) Object.defineProperty(navigation, 'startSession', ownDescriptor);
1855
+ else delete navigation.startSession;
2104
1856
  };
2105
1857
  }
2106
1858
 
@@ -2212,20 +1964,19 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2212
1964
 
2213
1965
  function openWorkbench(actions, step, sessionId) {
2214
1966
  actions.openAt(step, sessionId);
2215
- startJobsPolling(actions);
1967
+ sessionWorkbenchController?.open(sessionId, step);
2216
1968
  }
2217
1969
 
2218
1970
  const WORKBENCH_OPEN_EVENT = 'dsh:data-cleaning-workbench-open';
2219
1971
  const WORKBENCH_DATASET_EVENT = 'dsh:data-cleaning-workbench-dataset';
2220
1972
  const WORKBENCH_DRAFT_EVENT = 'dsh:data-cleaning-workbench-draft';
2221
1973
  const WORKBENCH_PREPARE_EVENT = 'dsh:data-cleaning-workbench-prepare';
2222
- const WORKBENCH_OWNER_LEFT_EVENT = 'dsh:data-cleaning-workbench-owner-left';
2223
1974
 
2224
1975
  // session scope 不能复用 root scope 的 store handle。DSH 当前会分别物化 root/session
2225
1976
  // 插槽组件,因此以 document 事件跨 scope 通知;闭包引用仅作为无 DOM 测试降级。
2226
- let rootWorkbenchActions = null;
1977
+ let sessionWorkbenchController = null;
2227
1978
 
2228
- /** 从 session scope 的 composer/header 触发 root scope 工作台。 */
1979
+ /** 从 composer/header 请求指定 Session 的唯一工作台 Tab。 */
2229
1980
  function requestWorkbenchOpen(step, sessionId, task = null) {
2230
1981
  if (typeof document !== 'undefined'
2231
1982
  && typeof document.createEvent === 'function'
@@ -2234,7 +1985,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2234
1985
  event.initCustomEvent(WORKBENCH_OPEN_EVENT, false, true, { step, sessionId, task });
2235
1986
  if (!document.dispatchEvent(event)) return;
2236
1987
  }
2237
- if (rootWorkbenchActions) openWorkbench(rootWorkbenchActions, step, sessionId);
1988
+ sessionWorkbenchController?.open(sessionId, step);
2238
1989
  }
2239
1990
 
2240
1991
  function requestPreparedWorkflow(sessionId, config) {
@@ -2245,7 +1996,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2245
1996
  });
2246
1997
  }
2247
1998
 
2248
- /** 把提示词生成器解析出的完整表格交给 root 工作台,不把整表塞进模型上下文。 */
1999
+ /** 完整表格交给 Session 业务控制器,不把整表塞进模型上下文。 */
2249
2000
  function requestWorkbenchDataset(result, sessionId, source) {
2250
2001
  if (typeof document !== 'undefined'
2251
2002
  && typeof document.createEvent === 'function'
@@ -2254,9 +2005,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2254
2005
  event.initCustomEvent(WORKBENCH_DATASET_EVENT, false, true, { result, sessionId, source });
2255
2006
  if (!document.dispatchEvent(event)) return;
2256
2007
  }
2257
- if (rootWorkbenchActions) {
2258
- applyParsed(result, rootWorkbenchActions, `session:${sessionId || 'unassigned'}`, source);
2259
- rootWorkbenchActions.setActiveSession(sessionId);
2008
+ if (sessionWorkbenchController?.actionsFor(sessionId)) {
2009
+ applyParsed(result, sessionWorkbenchController?.actionsFor(sessionId), `session:${sessionId || 'unassigned'}`, source);
2010
+ sessionWorkbenchController?.actionsFor(sessionId).setActiveSession(sessionId);
2260
2011
  }
2261
2012
  }
2262
2013
 
@@ -2269,11 +2020,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2269
2020
  event.initCustomEvent(WORKBENCH_DRAFT_EVENT, false, true, { draft, sessionId });
2270
2021
  if (!document.dispatchEvent(event)) return;
2271
2022
  }
2272
- if (rootWorkbenchActions) {
2273
- rootWorkbenchActions.setTaskTitle(draft.title);
2274
- rootWorkbenchActions.setObjectives(draft.objectives);
2275
- if (draft.mappings) rootWorkbenchActions.setMappings(draft.mappings);
2276
- rootWorkbenchActions.setFieldSelection(draft.fieldSelection);
2023
+ if (sessionWorkbenchController?.actionsFor(sessionId)) {
2024
+ sessionWorkbenchController?.actionsFor(sessionId).setTaskTitle(draft.title);
2025
+ sessionWorkbenchController?.actionsFor(sessionId).setObjectives(draft.objectives);
2026
+ if (draft.mappings) sessionWorkbenchController?.actionsFor(sessionId).setMappings(draft.mappings);
2027
+ sessionWorkbenchController?.actionsFor(sessionId).setFieldSelection(draft.fieldSelection);
2277
2028
  }
2278
2029
  }
2279
2030
 
@@ -2360,7 +2111,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2360
2111
  '导入企业名单或图片,核验主体、补全字段,并导出可追溯结果。');
2361
2112
  }
2362
2113
 
2363
- function workflowNavigationIssue(step, task, hasData, profile, qccRun) {
2114
+ function workflowNavigationIssue(step, task, hasData, profile, qccRun, hasLocalResult = false) {
2364
2115
  if (['upload', 'history'].includes(step)) return null;
2365
2116
  if (step === 'download' && task?.artifacts?.length) return null;
2366
2117
  if (!hasData) return '请先在「导入与核验」载入并核对完整名单。';
@@ -2374,6 +2125,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2374
2125
  if (step === 'enrich' && requiresQcc && !['matched', 'enriching', 'export_ready', 'completed', 'partial'].includes(task.state)) {
2375
2126
  return '请先在「主体匹配」确认企业;多候选需要人工核验。';
2376
2127
  }
2128
+ if (step === 'download' && !requiresQcc && hasLocalResult && ['rules_confirmed', 'diagnosed'].includes(task.state)) return null;
2377
2129
  if (step === 'download' && !['export_ready', 'completed', 'partial'].includes(task.state) && !['completed', 'partial'].includes(qccRun?.state)) {
2378
2130
  return '请先完成清洗或字段补全。原始清单可在「导入与核验」下载。';
2379
2131
  }
@@ -2582,10 +2334,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2582
2334
  function DataCleaningExperience(props) {
2583
2335
  const { sessionId, session: ownerSession } = props;
2584
2336
  const enabled = useCleaningSession(sessionId);
2337
+ const [dependencyMessage, setDependencyMessage] = react.useState('');
2585
2338
  react.useEffect(() => {
2586
- if (!enabled) return undefined;
2587
- return () => document.dispatchEvent(new CustomEvent(WORKBENCH_OWNER_LEFT_EVENT, { detail: { sessionId } }));
2588
- }, [enabled, sessionId]);
2339
+ const listener = event => { if (event.detail?.sessionId === sessionId) setDependencyMessage(event.detail.message || ''); };
2340
+ document.addEventListener(DEPENDENCY_EVENT, listener);
2341
+ return () => document.removeEventListener(DEPENDENCY_EVENT, listener);
2342
+ }, [sessionId]);
2589
2343
  const hero = ownerSession?.composerPhase === 'blank';
2590
2344
  const [menuMount, setMenuMount] = react.useState(null);
2591
2345
  react.useEffect(() => {
@@ -2603,6 +2357,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2603
2357
  className: `dcAgentExperience${hero ? ' is-home' : ''}`,
2604
2358
  'data-session-id': sessionId,
2605
2359
  },
2360
+ dependencyMessage ? h('p', { className: 'dcAgentError', role: 'alert' }, dependencyMessage) : null,
2606
2361
  menuMount ? reactDom.createPortal(h(CapabilityBar, { sessionId }), menuMount) : h(CapabilityBar, { sessionId }),
2607
2362
  hero ? h(ProductHome, { sessionId }) : null,
2608
2363
  );
@@ -2950,7 +2705,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2950
2705
  const hasImage = files.length > 0;
2951
2706
  if (!hasImage) return;
2952
2707
  const target = event.target;
2953
- const inComposer = !open && !target?.closest?.('.dcAgentWorkbench')
2708
+ const inComposer = !open && !target?.closest?.('.dcAgentWorkbenchContent')
2954
2709
  && Boolean(target?.closest?.('textarea, [contenteditable="true"], [role="textbox"]'));
2955
2710
  const inSourceStep = open && wizardStep === 1 && Boolean(target?.closest?.('.dcAgentPromptPanel'));
2956
2711
  if (!inComposer && !inSourceStep) return;
@@ -3489,15 +3244,14 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3489
3244
  );
3490
3245
  }
3491
3246
 
3492
- /** 右侧非模态工作台:中央区域始终保留 DSH 原生会话。 */
3493
- function WorkbenchDrawer(props) {
3247
+ /** Session Tab 业务内容;尺寸、停靠、关闭均由 Better Sidebar 宿主管理。 */
3248
+ function WorkbenchContent(props) {
3494
3249
  const { useStore, actions } = props;
3250
+ const visible = props.visible !== false;
3495
3251
  const sendSessionCommand = props.sendSessionCommand;
3496
3252
  const setSessionDraft = props.setSessionDraft;
3497
3253
  const open = useStore((state) => state.open);
3498
3254
  const step = useStore((state) => state.step);
3499
- const expanded = useStore((state) => state.expanded);
3500
- const preferredWidth = useStore((state) => state.preferredWidth);
3501
3255
  const busy = useStore((state) => state.busy);
3502
3256
  const error = useStore((state) => state.error);
3503
3257
  const input = useStore((state) => state.input);
@@ -3519,92 +3273,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3519
3273
  const qccRun = useStore((state) => state.qccRun);
3520
3274
  const commandProgress = useStore((state) => state.commandProgress);
3521
3275
  const paidConfirmed = useStore((state) => state.paidConfirmed);
3522
- const jobs = useStore((state) => state.jobs);
3523
3276
  const activeSessionId = useStore((state) => state.activeSessionId);
3524
3277
  const [fieldQuery, setFieldQuery] = react.useState('');
3525
3278
 
3526
3279
  react.useEffect(() => {
3527
- if (!open || typeof document === 'undefined') return undefined;
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;
3280
+ if (!open || !visible) return undefined;
3608
3281
  let disposed = false;
3609
3282
  requestJson('/data-cleaning/api/workflow/contract').then((response) => {
3610
3283
  if (!disposed) actions.setWorkflowContract(response.contract);
@@ -3612,19 +3285,21 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3612
3285
  loadWorkflowTasks(actions);
3613
3286
  if (activeSessionId) {
3614
3287
  queueWorkflowOperation(activeSessionId, async () => {
3615
- const task = await ensureWorkflowTask(actions, activeSessionId, {}, step !== 'history');
3616
- await refreshHostedTask(actions, activeSessionId, task.id, null, false);
3288
+ const key = String(activeSessionId);
3289
+ const saved = ownedSessionId(key) ? readTaskBindings()[key] : null;
3290
+ const taskId = workflowTaskBySession.get(key)?.id || (/^dcw-[a-zA-Z0-9-]+$/.test(saved || '') ? saved : null);
3291
+ if (taskId) await refreshHostedTask(actions, activeSessionId, taskId, null, false);
3617
3292
  }).catch((taskError) => {
3618
3293
  if (!disposed) actions.setError(taskError instanceof Error ? taskError.message : String(taskError));
3619
3294
  });
3620
3295
  }
3621
3296
  return () => { disposed = true; };
3622
- }, [open, activeSessionId, actions]);
3297
+ }, [open, visible, activeSessionId, actions]);
3623
3298
 
3624
3299
  // Reopening during execution follows Host metadata, even when the original
3625
3300
  // command watcher was lost on page reload. This performs no QCC calls.
3626
3301
  react.useEffect(() => {
3627
- if (!open || !workflowTask?.id || !['matching', 'enriching'].includes(workflowTask.state)) return undefined;
3302
+ if (!open || !visible || !workflowTask?.id || !['matching', 'enriching'].includes(workflowTask.state)) return undefined;
3628
3303
  let disposed = false, timer;
3629
3304
  const poll = async () => {
3630
3305
  try {
@@ -3636,51 +3311,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3636
3311
  };
3637
3312
  timer = setTimeout(poll, 2000);
3638
3313
  return () => { disposed = true; clearTimeout(timer); };
3639
- }, [open, workflowTask?.id, workflowTask?.state, activeSessionId, step, actions]);
3314
+ }, [open, visible, workflowTask?.id, workflowTask?.state, activeSessionId, step, actions]);
3640
3315
 
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]);
3679
-
3680
- // DSH 的 useStore 基于 React hooks;所有订阅必须在每次渲染时以相同顺序调用。
3681
- // 把关闭态 guard 放在全部 useStore 之后,避免首次关闭、随后打开时触发
3682
- // React #310(Rendered more hooks than during the previous render)。
3683
- if (!open) return null;
3316
+ // Host 决定 Tab 挂载/可见性;内容不再按私有 open flag 隐藏。
3684
3317
 
3685
3318
  const hasData = dataset !== null && dataset.rowCount > 0;
3686
3319
  const requiresQcc = objectives.includes('validate_identity') || objectives.includes('complete_fields');
@@ -4514,60 +4147,12 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4514
4147
  );
4515
4148
  }
4516
4149
 
4517
- return h('div', {
4518
- className: 'dcAgentOverlay',
4150
+ return h('section', {
4151
+ className: 'dcAgentWorkbenchContent',
4519
4152
  'data-dsh-plugin': 'data-cleaning-agent',
4520
- onClick: (event) => {
4521
- if (event.target === event.currentTarget) {
4522
- stopJobsPolling();
4523
- actions.close();
4524
- }
4525
- },
4153
+ 'aria-label': '数据清洗补全工作台',
4154
+ 'data-session-id': activeSessionId ?? undefined,
4526
4155
  },
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
4156
  h('nav', { className: 'dcAgentManagement', 'aria-label': '工作台管理' },
4572
4157
  h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step !== 'history',
4573
4158
  onClick: () => actions.setStep(cachedTask?.stage || 'upload') }, '当前任务'),
@@ -4611,17 +4196,240 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4611
4196
  h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step === 'rules', onClick: () => actions.setStep('rules') }, '字段映射与规则'),
4612
4197
  h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step === 'profile', onClick: () => actions.setStep('profile') }, '质量体检报告'),
4613
4198
  ) : null,
4614
- workflowNavigationIssue(step, cachedTask, hasData, profile, qccRun)
4199
+ workflowNavigationIssue(step, cachedTask, hasData, profile, qccRun, Boolean((clean || complete) && exportRows()?.length))
4615
4200
  ? h('section', { className: 'dcAgentSection', role: 'status' },
4616
- h('p', null, workflowNavigationIssue(step, cachedTask, hasData, profile, qccRun)),
4201
+ h('p', null, workflowNavigationIssue(step, cachedTask, hasData, profile, qccRun, Boolean((clean || complete) && exportRows()?.length))),
4617
4202
  h('button', { type: 'button', className: 'dcAgentButton',
4618
4203
  onClick: () => actions.setStep(!hasData ? 'upload' : !cachedTask || ['draft', 'uploaded'].includes(cachedTask.state) ? 'rules' : cachedTask.state === 'rules_confirmed' ? 'profile' : step === 'download' ? 'enrich' : 'match') }, '前往前置步骤'))
4619
4204
  : pane,
4620
4205
  ),
4621
- ),
4622
4206
  );
4623
4207
  }
4624
4208
 
4209
+ // BEGIN generated Better Sidebar adapter (source: lib/better-sidebar-adapter.js)
4210
+ /** Public Better Sidebar seam. No business task creation, cancellation or geometry. */
4211
+ const CLEANING_TAB_ID = 'dsh-data-cleaning-agent:workbench';
4212
+ const SIDEBAR_DEPENDENCY_MESSAGE = '请安装或升级兼容的 dsh-better-sidebar 并重启 DSH(需要 targetedOpen 和 stateSubscription 能力),然后重新点击流程按钮。现有任务和文件不会删除。';
4213
+
4214
+ function sidebarCompatibility(service) {
4215
+ const methods = ['registerTab', 'openTab', 'isTabEnabled', 'getSnapshot', 'subscribeState'];
4216
+ return Boolean(service && methods.every(key => typeof service[key] === 'function')
4217
+ && ['targetedOpen', 'stateSubscription'].every(key => service.features?.includes(key)));
4218
+ }
4219
+
4220
+ function contains(node, id) {
4221
+ if (!node) return false;
4222
+ return node.kind === 'leaf' ? node.tabs.some(tab => tab.id === id)
4223
+ : node.children.some(child => contains(child, id));
4224
+ }
4225
+
4226
+ function revealCleaningTab(state, id) {
4227
+ if (state.floats?.some(item => item.tab.id === id)) return state;
4228
+ if (contains(state.bottomSplits, id)) return state.bottomOpen ? state : { ...state, bottomOpen: true };
4229
+ if (contains(state.splits, id)) return state.panelOpen ? state : { ...state, panelOpen: true };
4230
+ return state;
4231
+ }
4232
+
4233
+ /** One adapter per plugin activation. Session navigation survives closing its Tab. */
4234
+ function createCleaningSidebarAdapter(service, { component, icon, onUnavailable = () => {} }) {
4235
+ const targets = new Map();
4236
+ const destinations = new Map();
4237
+ const pending = new Set();
4238
+ let disposed = false;
4239
+ const compatible = sidebarCompatibility(service);
4240
+ const applyPending = sessionId => {
4241
+ if (disposed || service.getSnapshot().sessionId !== sessionId || !pending.has(sessionId)) return;
4242
+ const target = targets.get(sessionId);
4243
+ if (!target) return;
4244
+ pending.delete(sessionId);
4245
+ target.navigate(destinations.get(sessionId));
4246
+ target.store.reduce(state => revealCleaningTab(state, target.tabId));
4247
+ };
4248
+ const unregister = compatible ? service.registerTab({
4249
+ id: CLEANING_TAB_ID, title: '数据清洗补全', icon, order: 30, single: true, component,
4250
+ }) : () => {};
4251
+ const unsubscribe = compatible ? service.subscribeState(() => {
4252
+ const sessionId = service.getSnapshot().sessionId;
4253
+ if (sessionId) applyPending(sessionId);
4254
+ }) : () => {};
4255
+ return {
4256
+ compatible,
4257
+ open(scope, destination = 'upload') {
4258
+ if (disposed) return false;
4259
+ if (!compatible || !service.isTabEnabled(CLEANING_TAB_ID)) {
4260
+ onUnavailable(compatible ? '请在 Better Sidebar 设置中启用「数据清洗补全」Tab。' : SIDEBAR_DEPENDENCY_MESSAGE);
4261
+ return false;
4262
+ }
4263
+ if (!scope?.sessionId) throw new Error('A target Session is required');
4264
+ destinations.set(scope.sessionId, destination);
4265
+ pending.add(scope.sessionId);
4266
+ service.openTab({ type: CLEANING_TAB_ID }, scope);
4267
+ applyPending(scope.sessionId);
4268
+ return true;
4269
+ },
4270
+ attach(scope, target) {
4271
+ if (disposed) return () => {};
4272
+ targets.set(scope.sessionId, target);
4273
+ // Without a pending shortcut, preserve the Session business store's current view.
4274
+ // Closing/reopening a Tab must not rewind an in-progress task to an older shortcut.
4275
+ applyPending(scope.sessionId);
4276
+ return () => { if (targets.get(scope.sessionId) === target) targets.delete(scope.sessionId); };
4277
+ },
4278
+ dispose() {
4279
+ if (disposed) return;
4280
+ disposed = true;
4281
+ unsubscribe(); unregister();
4282
+ targets.clear(); pending.clear(); destinations.clear();
4283
+ },
4284
+ };
4285
+ }
4286
+
4287
+ // END generated Better Sidebar adapter
4288
+
4289
+ // A shared owner across materialized root/Session factories, never a shared UI store.
4290
+ const CONTROLLER_KEY = Symbol.for('dsh.data-cleaning.session-workbench');
4291
+ const DEPENDENCY_EVENT = 'dsh:data-cleaning-sidebar-dependency';
4292
+ function installSessionWorkbench(ctx) {
4293
+ const host = typeof document === 'undefined' ? window : document;
4294
+ if (host[CONTROLLER_KEY]) {
4295
+ const shared = host[CONTROLLER_KEY];
4296
+ shared.references += 1;
4297
+ sessionWorkbenchController = shared.controller;
4298
+ let released = false;
4299
+ return () => { if (!released) { released = true; shared.release(); } };
4300
+ }
4301
+ let disposed = false;
4302
+ const instances = new Map();
4303
+ const spec = createWorkbenchStore();
4304
+ const instanceFor = sessionId => {
4305
+ if (!sessionId) throw new Error('请先打开数据清洗会话。');
4306
+ if (!instances.has(sessionId)) {
4307
+ const instance = spec.create(sessionId);
4308
+ instance.actions.setActiveSession(sessionId);
4309
+ instances.set(sessionId, instance);
4310
+ }
4311
+ return instances.get(sessionId);
4312
+ };
4313
+ let openingSession = null;
4314
+ const notify = message => {
4315
+ if (openingSession) instanceFor(openingSession).actions.setError(message);
4316
+ if (typeof document?.dispatchEvent === 'function') {
4317
+ const event = document.createEvent('CustomEvent');
4318
+ event.initCustomEvent(DEPENDENCY_EVENT, false, false, { sessionId: openingSession, message });
4319
+ document.dispatchEvent(event);
4320
+ }
4321
+ };
4322
+ let adapter;
4323
+ function SessionWorkbenchTab(props) {
4324
+ const instance = instanceFor(props.scope.sessionId);
4325
+ const useStore = pick => react.useSyncExternalStore(instance.subscribe,
4326
+ () => pick(instance.getSnapshot()), () => pick(instance.getSnapshot()));
4327
+ react.useEffect(() => {
4328
+ instance.actions.openAt(null, props.scope.sessionId);
4329
+ return adapter.attach(props.scope, { tabId: props.tab.id, store: props.store,
4330
+ navigate: destination => instance.actions.setStep(destination || instance.getSnapshot().step) });
4331
+ }, [instance, props.scope.sessionId, props.tab.id, props.store]);
4332
+ return h(WorkbenchContent, { useStore, actions: instance.actions, visible: props.visible,
4333
+ sendSessionCommand: (sessionId, prompt) => sendQccAgentCommand(ctx, sessionId, prompt),
4334
+ setSessionDraft: (sessionId, prompt) => setQccAgentDraft(ctx, sessionId, prompt) });
4335
+ }
4336
+ const adapterOptions = { component: SessionWorkbenchTab, icon: size => h(DatabaseLogo, { size }), onUnavailable: notify };
4337
+ adapter = createCleaningSidebarAdapter(null, adapterOptions);
4338
+ const connectProvider = service => {
4339
+ if (disposed) return () => {};
4340
+ const next = createCleaningSidebarAdapter(service, adapterOptions);
4341
+ adapter.dispose();
4342
+ adapter = next;
4343
+ return () => {
4344
+ next.dispose();
4345
+ if (!disposed && adapter === next) adapter = createCleaningSidebarAdapter(null, adapterOptions);
4346
+ };
4347
+ };
4348
+ // Optional child injection grants Cordis service access and follows provider removal.
4349
+ // Core conversation / Host tools remain loaded when Better Sidebar is absent.
4350
+ const providerFiber = typeof ctx.inject === 'function'
4351
+ ? ctx.inject(['betterSidebar'], providerContext => {
4352
+ providerContext.effect(() => connectProvider(providerContext.betterSidebar), 'data-cleaning-agent: sidebar provider');
4353
+ })
4354
+ : null;
4355
+ const disconnectFixture = typeof ctx.inject !== 'function' ? connectProvider(ctx.betterSidebar) : () => {};
4356
+ const controller = {
4357
+ storeFor: instanceFor,
4358
+ actionsFor: sessionId => instanceFor(sessionId).actions,
4359
+ open(sessionId, step) {
4360
+ if (disposed || !sessionId) return false;
4361
+ const instance = instanceFor(sessionId);
4362
+ instance.actions.openAt(step, sessionId);
4363
+ const sessions = typeof ctx.get === 'function' ? ctx.get('sessions') : ctx.sessions;
4364
+ const summary = sessions?.list?.getSnapshot?.()?.byId?.[sessionId];
4365
+ openingSession = sessionId;
4366
+ try { return adapter.open({ sessionId, ...(summary?.cwd ? { cwd: summary.cwd } : {}) },
4367
+ step || instance.getSnapshot().step); }
4368
+ finally { openingSession = null; }
4369
+ },
4370
+ };
4371
+ sessionWorkbenchController = controller;
4372
+ const listeners = {
4373
+ [WORKBENCH_OPEN_EVENT]: event => {
4374
+ const detail = event.detail || {};
4375
+ const actions = controller.actionsFor(detail.sessionId);
4376
+ if (detail.task?.id) cacheWorkflowTask(actions, detail.sessionId, detail.task);
4377
+ controller.open(detail.sessionId, detail.step);
4378
+ },
4379
+ [WORKBENCH_DATASET_EVENT]: event => {
4380
+ const { sessionId, result, source } = event.detail;
4381
+ const actions = controller.actionsFor(sessionId);
4382
+ if (!result) return;
4383
+ applyParsed(result, actions, `session:${sessionId}`, source);
4384
+ void queueWorkflowOperation(sessionId, () => persistParsedWorkflow(actions, sessionId, result, source))
4385
+ .catch(error => { if (!disposed) actions.setError(error.message || String(error)); });
4386
+ },
4387
+ [WORKBENCH_DRAFT_EVENT]: event => {
4388
+ const { sessionId, draft = {} } = event.detail;
4389
+ const actions = controller.actionsFor(sessionId);
4390
+ if (draft.title) actions.setTaskTitle(draft.title);
4391
+ if (draft.objectives) actions.setObjectives(draft.objectives);
4392
+ if (draft.mappings) actions.setMappings(draft.mappings);
4393
+ if (draft.fieldSelection) actions.setFieldSelection(draft.fieldSelection);
4394
+ for (const [key, value] of Object.entries(draft.matchRules || {})) actions.setMatchRule(key, value);
4395
+ void queueWorkflowOperation(sessionId, async () => {
4396
+ const task = await ensureEditableWorkflowTask(actions, sessionId, draft);
4397
+ return updateWorkflowTask(actions, sessionId, task, draft);
4398
+ }).catch(error => { if (!disposed) actions.setError(error.message || String(error)); });
4399
+ },
4400
+ [WORKBENCH_PREPARE_EVENT]: event => {
4401
+ const { sessionId, config, resolve, reject } = event.detail;
4402
+ const actions = controller.actionsFor(sessionId);
4403
+ queueWorkflowOperation(sessionId, () => stageConfirmedWorkflow(actions, sessionId, config))
4404
+ .then(command => { if (!disposed) watchHostedCommand(actions, sessionId, command); resolve(command); }, reject);
4405
+ },
4406
+ };
4407
+ const bindings = Object.entries(listeners).map(([name, handler]) => {
4408
+ const listener = event => {
4409
+ if (disposed || event.defaultPrevented || !event.detail?.sessionId) return;
4410
+ event.preventDefault();
4411
+ handler(event);
4412
+ };
4413
+ host.addEventListener?.(name, listener);
4414
+ return () => host.removeEventListener?.(name, listener);
4415
+ });
4416
+ const shared = { controller, references: 1, release() {
4417
+ if (--shared.references > 0) return;
4418
+ disposed = true;
4419
+ commandWatchGeneration += 1;
4420
+ for (const remove of bindings) remove();
4421
+ adapter.dispose();
4422
+ disconnectFixture();
4423
+ void providerFiber?.dispose();
4424
+ instances.clear();
4425
+ if (host[CONTROLLER_KEY] === shared) delete host[CONTROLLER_KEY];
4426
+ if (sessionWorkbenchController === controller) sessionWorkbenchController = null;
4427
+ } };
4428
+ host[CONTROLLER_KEY] = shared;
4429
+ let released = false;
4430
+ return () => { if (!released) { released = true; shared.release(); } };
4431
+ }
4432
+
4625
4433
  function apply(ctx) {
4626
4434
  // eslint-disable-next-line no-console
4627
4435
  console.log('[dc-agent] client apply() ran');
@@ -4633,7 +4441,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4633
4441
  promptSlot: 'conversation.input.overlay',
4634
4442
  heroTitleBridge: 'exact-text/reversible',
4635
4443
  headerSlot: 'conversation.session.header.actions',
4636
- overlay: 'shell.overlay',
4444
+ workbench: 'better-sidebar:session-singleton',
4637
4445
  error: null,
4638
4446
  };
4639
4447
  window.__DC_MVP__ = state;
@@ -4642,17 +4450,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4642
4450
  ctx.effect(() => installUiStyles(), 'data-cleaning-agent: UI styles');
4643
4451
  ctx.effect(() => installSessionOwnershipBridge(ctx), 'data-cleaning-agent: session ownership');
4644
4452
 
4645
- // 右侧工作台:additive overlay,不替换 DSH 单占位 details 面板。
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));
4453
+ ctx.effect(() => installSessionWorkbench(ctx), 'data-cleaning-agent: Session workbench');
4656
4454
 
4657
4455
  // 左栏:footer 只托管生命周期和 Portal 降级;实际入口显示在工作区列表前。
4658
4456
  ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
@@ -4715,7 +4513,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4715
4513
  exports.inject = inject;
4716
4514
  // 测试用纯函数,不构成 Host / DSH 稳定 API。
4717
4515
  exports.__testing = {
4718
- workbenchWidthBounds,
4516
+ createWorkbenchStore,
4517
+ WorkbenchContent,
4518
+ installSessionWorkbench,
4719
4519
  DatabaseLogo,
4720
4520
  visibleCatalogFields,
4721
4521
  installCapabilityMount,