dsh-data-cleaning-agent 0.8.8 → 0.8.10

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
@@ -2,6 +2,26 @@
2
2
 
3
3
  本文件记录 `dsh-data-cleaning-agent` 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [0.8.10] - 2026-09-07
8
+
9
+ ### Changed
10
+ - 依据共享 DSH-UX-001 v1.3.0 / UX-39,为自有右侧工作台补充左侧连续拖拽分隔条、方向键和 Home/End 调宽、双击复位、拖动取消、手动宽度恢复;中央会话按实际面板宽度同步让位。
11
+ - 按宿主会话区域实际可用宽度约束上下限,空间不足时全屏降级;关闭、切换或卸载恢复自有布局变量并清理监听,不改变 Host 任务、QCC 调用或发送机制。
12
+
13
+ ### Tests
14
+ - 补充实际拖拽、取消、展开转手动、键盘调宽、关闭重开、宿主区域尺寸变化及让位清理回归;发布前通过 214 项检查测试与 10 组隔离 Chromium UI 回归。
15
+
16
+ ## [0.8.9] - 2026-09-07
17
+
18
+ ### Changed
19
+ - 按共享 DSH-UX-001 v1.1.2 对齐招投标工作台的阶段菜单:单行五个等宽分栏、上图标下短标题、分隔线及选中蓝色底线;取消数字前缀和横向滚动布局。说明与状态仅保留在内容区和工作台头部。
20
+ - 窄屏缩小图标与字号,长标题在分栏内省略并保留完整可访问名称和悬停提示;阶段导航、质量体检子视图及 Host 工作流保持不变。
21
+
22
+ ### Tests
23
+ - 增补五阶段图标/标题结构断言与隔离 Chromium 深浅色布局回归,覆盖 320px 窄屏、普通/展开工作台、长标题防溢出,以及切换阶段不修改 Host 任务。
24
+
5
25
  ## [0.8.8] - 2026-09-06
6
26
 
7
27
  ### Changed
package/README.en.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  > 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.
4
4
  >
5
- > Current source version / 当前源码版本: **0.8.8** (stable release)
5
+ > Current source version / 当前源码版本: **0.8.10** (stable release)
6
6
 
7
- This release aligns the business UI with QCC blue: a database logo beside the home title, consistent entry/workbench icons, and searchable field groups retaining all 128 fields. Native DSH input and send behavior are preserved.
7
+ This release adds continuous resizing from the workbench's left edge, keeping the native composer clear. Arrow keys, Home/End, double-click reset, and width restoration on reopening are supported. Constrained layouts use a full-screen drawer. All 128 fields, QCC-blue menus, existing workflows, and native DSH input and send behavior are preserved.
8
8
 
9
9
  [![CI](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/workflows/ci.yml)
10
10
  [![npm](https://img.shields.io/npm/v/dsh-data-cleaning-agent)](https://www.npmjs.com/package/dsh-data-cleaning-agent)
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  > 在 DeepSeek Harness 中清洗、补全、画像企业名单数据的智能体插件:本地 CSV/XLSX/JSON 引擎 + 可选企查查(Qichacha/QCC)MCP 企业数据补全,由企查查(Qichacha/QCC)团队发起并维护。
4
4
  >
5
- > 当前源码版本 / Current source version: **0.8.8**(正式版本)
5
+ > 当前源码版本 / Current source version: **0.8.10**(正式版本)
6
6
 
7
- 本版统一企查查蓝业务界面:数据库 LOGO 与首页名称同排,入口/工作台图标一致;字段支持按维度、名称或工具搜索,保留全部 128 个字段。仅调整本业务界面,不替换 DSH 原生输入和发送机制。
7
+ 本版支持拖动右侧工作台左边缘连续调宽,中央输入区同步让位;支持方向键微调、Home/End 边界、双击恢复默认和关闭重开保留宽度。空间不足时全屏降级。保留企查查蓝菜单、全部 128 个字段和原有工作流,不替换 DSH 原生输入和发送机制。
8
8
 
9
9
  [![CI](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/workflows/ci.yml/badge.svg)](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/workflows/ci.yml)
10
10
  [![npm](https://img.shields.io/npm/v/dsh-data-cleaning-agent)](https://www.npmjs.com/package/dsh-data-cleaning-agent)
package/lib/client.js CHANGED
@@ -63,6 +63,137 @@ window.__ModuleLoader__.load({
63
63
  h('path', { d: DATABASE_PATH }));
64
64
  }
65
65
 
66
+ // 阶段只展示线性图标和短标题;说明、状态与操作保留在内容区。
67
+ const STAGE_ICON_PATHS = {
68
+ upload: 'M12 16V3m-4 4 4-4 4 4M4 14v6h16v-6',
69
+ check: 'M9 6h11M9 12h11M9 18h11M3 6l1 1 2-2M3 12l1 1 2-2M3 18l1 1 2-2',
70
+ search: 'M16 16l5 5M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0',
71
+ database: DATABASE_PATH,
72
+ table: 'M4 3h16v18H4zM4 8h16M4 13h16M4 17h16M10 8v13',
73
+ };
74
+ function StageIcon({ kind }) {
75
+ return h('svg', { width: 20, height: 20, viewBox: '0 0 24 24', fill: 'none',
76
+ stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round', strokeLinejoin: 'round',
77
+ 'aria-hidden': true, focusable: false }, h('path', { d: STAGE_ICON_PATHS[kind] }));
78
+ }
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
+ let requested = expanded ? 980 : preferredWidth;
93
+ let current = 0;
94
+ let drag = null;
95
+ const boundsForView = () => workbenchWidthBounds(view.innerWidth,
96
+ doc.querySelector('[data-conversation-scroll]')?.getBoundingClientRect().width || view.innerWidth);
97
+ const reserve = () => {
98
+ const width = boundsForView().mobile ? 0 : drawer.getBoundingClientRect().width;
99
+ for (const el of doc.querySelectorAll('[data-conversation-scroll]')) {
100
+ if (!reservations.has(el)) {
101
+ reservations.set(el, {
102
+ value: el.style.getPropertyValue('--dc-agent-workbench-reserve'),
103
+ priority: el.style.getPropertyPriority('--dc-agent-workbench-reserve'),
104
+ });
105
+ sizeObserver?.observe(el, { box: 'border-box' });
106
+ }
107
+ if (el.style.getPropertyValue('--dc-agent-workbench-reserve') !== `${width}px`) {
108
+ el.style.setProperty('--dc-agent-workbench-reserve', `${width}px`);
109
+ }
110
+ }
111
+ };
112
+ const apply = (value) => {
113
+ const bounds = boundsForView();
114
+ const fallback = view.innerWidth <= 1040 ? Math.min(420, view.innerWidth * .48) : Math.min(460, view.innerWidth * .42);
115
+ current = Math.round(Math.min(bounds.max, Math.max(bounds.min, Number.isFinite(value) ? value : fallback)));
116
+ drawer.dataset.narrow = String(bounds.mobile);
117
+ drawer.style.setProperty('--dc-agent-panel-width', `${current}px`);
118
+ handle.setAttribute('aria-valuemin', String(bounds.min));
119
+ handle.setAttribute('aria-valuemax', String(bounds.max));
120
+ handle.setAttribute('aria-valuenow', String(current));
121
+ handle.setAttribute('aria-valuetext', `${current} 像素`);
122
+ reserve();
123
+ };
124
+ const end = (cancel = false) => {
125
+ if (!drag) return;
126
+ const previous = drag;
127
+ drag = null;
128
+ drawer.classList.remove('is-resizing');
129
+ if (handle.hasPointerCapture?.(previous.id)) handle.releasePointerCapture(previous.id);
130
+ if (cancel) apply(requested);
131
+ else { requested = current; onWidthChange(current); }
132
+ };
133
+ const down = (event) => {
134
+ if (event.button !== 0 || drag || boundsForView().mobile) return;
135
+ event.preventDefault();
136
+ handle.focus();
137
+ drag = { id: event.pointerId, startX: event.clientX, startWidth: current };
138
+ handle.setPointerCapture(event.pointerId);
139
+ drawer.classList.add('is-resizing');
140
+ };
141
+ const move = (event) => {
142
+ if (drag?.id !== event.pointerId) return;
143
+ apply(drag.startWidth + drag.startX - event.clientX);
144
+ };
145
+ const up = (event) => { if (drag?.id === event.pointerId) end(); };
146
+ const cancel = () => end(true);
147
+ const keydown = (event) => {
148
+ if (event.key === 'Escape' && drag) {
149
+ event.preventDefault(); event.stopPropagation(); cancel(); return;
150
+ }
151
+ if (drag || boundsForView().mobile) return;
152
+ const bounds = boundsForView();
153
+ const delta = event.shiftKey ? 48 : 16;
154
+ const value = { ArrowLeft: current + delta, ArrowRight: current - delta, Home: bounds.min, End: bounds.max }[event.key];
155
+ if (value === undefined) return;
156
+ event.preventDefault(); event.stopPropagation();
157
+ requested = value; apply(value); onWidthChange(current);
158
+ };
159
+ const reset = () => { cancel(); requested = null; apply(null); onWidthChange(null); };
160
+ const resize = () => { cancel(); apply(requested); };
161
+ const sizeObserver = typeof view.ResizeObserver === 'function' ? new view.ResizeObserver(resize) : null;
162
+ handle.addEventListener('pointerdown', down);
163
+ handle.addEventListener('pointermove', move);
164
+ handle.addEventListener('pointerup', up);
165
+ handle.addEventListener('pointercancel', cancel);
166
+ handle.addEventListener('lostpointercapture', cancel);
167
+ handle.addEventListener('keydown', keydown);
168
+ handle.addEventListener('dblclick', reset);
169
+ view.addEventListener('resize', resize);
170
+ view.addEventListener('blur', cancel);
171
+ // 会话挂载点可被宿主替换;只对当前存在的滚动区设置自有让位变量。
172
+ const observer = typeof view.MutationObserver === 'function' ? new view.MutationObserver(reserve) : null;
173
+ observer?.observe(doc.body, { childList: true, subtree: true });
174
+ apply(requested);
175
+ return () => {
176
+ cancel();
177
+ observer?.disconnect();
178
+ sizeObserver?.disconnect();
179
+ handle.removeEventListener('pointerdown', down);
180
+ handle.removeEventListener('pointermove', move);
181
+ handle.removeEventListener('pointerup', up);
182
+ handle.removeEventListener('pointercancel', cancel);
183
+ handle.removeEventListener('lostpointercapture', cancel);
184
+ handle.removeEventListener('keydown', keydown);
185
+ handle.removeEventListener('dblclick', reset);
186
+ view.removeEventListener('resize', resize);
187
+ view.removeEventListener('blur', cancel);
188
+ drawer.style.removeProperty('--dc-agent-panel-width');
189
+ delete drawer.dataset.narrow;
190
+ for (const [el, previous] of reservations) {
191
+ if (previous.value) el.style.setProperty('--dc-agent-workbench-reserve', previous.value, previous.priority);
192
+ else el.style.removeProperty('--dc-agent-workbench-reserve');
193
+ }
194
+ };
195
+ }
196
+
66
197
  // 搜索仅改变可见字段,绝不清除已选字段或改变 Host 的字段目录。
67
198
  function visibleCatalogFields(group, query) {
68
199
  const needle = String(query || '').trim().toLowerCase();
@@ -152,12 +283,13 @@ window.__ModuleLoader__.load({
152
283
  }
153
284
 
154
285
  .dcAgentWorkbench {
286
+ position: relative;
155
287
  box-sizing: border-box;
156
288
  display: flex;
157
289
  flex-direction: column;
158
- width: min(460px, 42vw);
290
+ width: var(--dc-agent-panel-width, min(460px, 42vw));
159
291
  max-width: calc(100vw - 420px);
160
- min-width: 360px;
292
+ min-width: 320px;
161
293
  height: 100vh;
162
294
  border: 1px solid var(--dc-border);
163
295
  border-radius: 12px 0 0 12px;
@@ -167,7 +299,14 @@ window.__ModuleLoader__.load({
167
299
  overflow: hidden;
168
300
  pointer-events: auto;
169
301
  }
170
- .dcAgentWorkbench.is-expanded { width: min(980px, calc(100vw - 72px)); }
302
+ .dcAgentWorkbench.is-expanded { width: var(--dc-agent-panel-width, min(980px, calc(100vw - 420px))); }
303
+ .dcAgentResizeHandle { position: absolute; left: 0; top: 0; bottom: 0; width: 8px; z-index: 5; cursor: col-resize; touch-action: none; outline: none; }
304
+ .dcAgentResizeHandle::after { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: transparent; }
305
+ .dcAgentResizeHandle:hover::after, .dcAgentResizeHandle:focus-visible::after, .is-resizing .dcAgentResizeHandle::after { background: var(--dc-action); }
306
+ .dcAgentWorkbench.is-resizing { user-select: none; }
307
+ .dcAgentWorkbench[data-narrow="true"] { width: 100vw; max-width: none; min-width: 0; border-radius: 0; }
308
+ .dcAgentWorkbench[data-narrow="true"] .dcAgentResizeHandle { display: none; }
309
+ .dcAgentOverlay:has(.dcAgentWorkbench[data-narrow="true"]) { background: rgba(8, 10, 14, 0.22); pointer-events: auto; }
171
310
 
172
311
  .dcAgentCapabilities {
173
312
  display: flex;
@@ -525,32 +664,65 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
525
664
 
526
665
  .dcAgentStepper {
527
666
  display: grid;
528
- grid-template-columns: repeat(4, minmax(0, 1fr));
529
- gap: 8px;
530
- padding: 12px 18px;
667
+ grid-template-columns: repeat(var(--dc-stage-count, 5), minmax(0, 1fr));
668
+ min-width: 0;
531
669
  border-bottom: 1px solid var(--dc-border);
532
- background: var(--dc-page);
670
+ background: var(--dc-surface);
533
671
  }
534
672
 
535
673
  .dcAgentStep {
674
+ position: relative;
675
+ min-width: 0;
536
676
  display: flex;
677
+ flex-direction: column;
537
678
  align-items: center;
538
679
  justify-content: center;
539
- gap: 6px;
540
- padding: 9px 6px;
541
- border: 1px solid var(--dc-border);
542
- border-radius: 6px;
680
+ gap: 8px;
681
+ padding: 12px 4px;
682
+ border: 0;
683
+ border-right: 1px solid var(--dc-border);
684
+ border-radius: 0;
543
685
  background: transparent;
544
686
  color: var(--dc-muted);
545
687
  font-size: 12px;
688
+ font-weight: 600;
546
689
  cursor: pointer;
547
- white-space: nowrap;
548
690
  }
691
+ .dcAgentStep:last-child { border-right: 0; }
692
+ .dcAgentStep:hover { background: var(--dc-page); }
549
693
  .dcAgentStep.is-active {
550
694
  color: var(--dc-action);
551
- border-color: var(--dc-action);
552
695
  background: var(--dc-selected);
553
696
  }
697
+ .dcAgentStep.is-active::after {
698
+ content: '';
699
+ position: absolute;
700
+ bottom: 0;
701
+ left: 8px;
702
+ right: 8px;
703
+ height: 3px;
704
+ border-radius: 3px 3px 0 0;
705
+ background: var(--dc-action);
706
+ }
707
+ .dcAgentStepIcon {
708
+ display: grid;
709
+ place-items: center;
710
+ width: 30px;
711
+ height: 30px;
712
+ flex: none;
713
+ border: 1px solid var(--dc-border);
714
+ border-radius: 8px;
715
+ }
716
+ .dcAgentStepIcon svg { display: block; }
717
+ .dcAgentStepLabel {
718
+ display: block;
719
+ width: 100%;
720
+ min-width: 0;
721
+ line-height: 18px;
722
+ white-space: nowrap;
723
+ overflow: hidden;
724
+ text-overflow: ellipsis;
725
+ }
554
726
 
555
727
  .dcAgentWbBody {
556
728
  flex: 1 1 auto;
@@ -800,25 +972,25 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
800
972
  @media (min-width: 1041px) {
801
973
  body:has(.dcAgentWorkbench) [data-conversation-scroll] {
802
974
  box-sizing: border-box;
803
- padding-right: min(460px, 42vw);
975
+ padding-right: var(--dc-agent-workbench-reserve, min(460px, 42vw));
804
976
  }
805
977
  }
806
978
  @media (min-width: 761px) and (max-width: 1040px) {
807
979
  body:has(.dcAgentWorkbench) [data-conversation-scroll] {
808
980
  box-sizing: border-box;
809
- padding-right: min(420px, 48vw);
981
+ padding-right: var(--dc-agent-workbench-reserve, min(420px, 48vw));
810
982
  }
811
983
  }
812
984
  @media (min-width: 761px) {
813
985
  body:has(.dcAgentWorkbench.is-expanded) [data-conversation-scroll] {
814
986
  box-sizing: border-box;
815
- padding-right: min(980px, calc(100vw - 420px));
987
+ padding-right: var(--dc-agent-workbench-reserve, min(980px, calc(100vw - 420px)));
816
988
  }
817
989
  }
818
990
  @media (max-width: 760px) {
819
991
  .dcAgentWorkbench, .dcAgentWorkbench.is-expanded { width: 100vw; max-width: none; min-width: 0; border-radius: 0; }
820
992
  .dcAgentOverlay { background: rgba(8, 10, 14, 0.22); pointer-events: auto; }
821
- .dcAgentStepper { grid-template-columns: repeat(2, minmax(0, 1fr)); }
993
+ .dcAgentResizeHandle { display: none; }
822
994
  .dcAgentQccBadge, .dcAgentJobsPill { display: none; }
823
995
  .dcAgentCandidate { grid-template-columns: 1fr; }
824
996
  .dcAgentCapabilities { justify-content: flex-start; padding-inline: 12px; }
@@ -831,18 +1003,20 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
831
1003
  .dcAgentMappingRow > b { display: none; }
832
1004
  }
833
1005
  @media (min-width: 761px) and (max-width: 1040px) {
834
- .dcAgentWorkbench { width: 420px; max-width: 48vw; min-width: 360px; }
1006
+ .dcAgentWorkbench { width: var(--dc-agent-panel-width, 420px); }
835
1007
  .dcAgentQccBadge, .dcAgentJobsPill { display: none; }
836
1008
  }
1009
+ @media (max-width: 360px) {
1010
+ .dcAgentStep { font-size: 11px; padding: 10px 3px; gap: 6px; }
1011
+ .dcAgentStepIcon { width: 26px; height: 26px; }
1012
+ .dcAgentStepIcon svg { width: 18px; height: 18px; }
1013
+ }
837
1014
  /* Distinct levels: management tabs → compact task steps → working content. */
838
1015
  .dcAgentWorkbench, .dcAgentPromptPanel { font-size: 14px; line-height: 1.5; }
839
1016
  .dcAgentWbHeader, .dcAgentManagement, .dcAgentStepper { flex: none; }
840
1017
  .dcAgentManagement { display: flex; gap: 24px; padding: 0 18px; border-bottom: 1px solid var(--dc-border); }
841
1018
  .dcAgentManagement .dcAgentButton { border: 0; border-bottom: 3px solid transparent; border-radius: 0; padding: 10px 0; background: transparent; }
842
1019
  .dcAgentManagement .dcAgentButton[aria-pressed="true"] { border-bottom-color: var(--dc-action); color: var(--dc-action); font-weight: 600; }
843
- .dcAgentStepper { display: flex; overflow-x: auto; gap: 4px; padding: 10px 12px; }
844
- .dcAgentStep { flex: 0 0 auto; border-color: transparent; padding: 7px 8px; }
845
- .dcAgentStep.is-active { border-color: transparent; }
846
1020
  .dcAgentCapability { border-radius: 8px; }
847
1021
  .dcAgentWbBody { min-height: 0; }
848
1022
  .dcAgentTable th { background: var(--dc-table-head); color: var(--dc-text); }
@@ -913,11 +1087,11 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
913
1087
  }
914
1088
 
915
1089
  const STEPS = [
916
- { key: 'upload', label: '导入与核验', icon: '1' },
917
- { key: 'rules', label: '规则与体检', icon: '2' },
918
- { key: 'match', label: '主体匹配', icon: '3' },
919
- { key: 'enrich', label: '字段补全', icon: '4' },
920
- { key: 'download', label: '结果下载', icon: '5' },
1090
+ { key: 'upload', label: '导入与核验', icon: 'upload' },
1091
+ { key: 'rules', label: '规则与体检', icon: 'check' },
1092
+ { key: 'match', label: '主体匹配', icon: 'search' },
1093
+ { key: 'enrich', label: '字段补全', icon: 'database' },
1094
+ { key: 'download', label: '结果下载', icon: 'table' },
921
1095
  ];
922
1096
 
923
1097
  const CAPABILITIES = [
@@ -1439,6 +1613,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1439
1613
  init: () => ({
1440
1614
  open: false,
1441
1615
  expanded: false,
1616
+ preferredWidth: null,
1442
1617
  step: 'upload',
1443
1618
  busy: false,
1444
1619
  error: null,
@@ -1473,6 +1648,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
1473
1648
  },
1474
1649
  close: (draft) => { draft.open = false; },
1475
1650
  toggleExpanded: (draft) => { draft.expanded = !draft.expanded; },
1651
+ setPreferredWidth: (draft, width) => {
1652
+ draft.preferredWidth = Number.isFinite(width) ? width : null;
1653
+ draft.expanded = false;
1654
+ },
1476
1655
  setStep: (draft, step) => { draft.step = step; },
1477
1656
  setActiveSession: (draft, sessionId) => { draft.activeSessionId = sessionId ?? null; },
1478
1657
  setBusy: (draft, busy) => { draft.busy = busy; },
@@ -2920,6 +3099,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2920
3099
  const open = useStore((state) => state.open);
2921
3100
  const step = useStore((state) => state.step);
2922
3101
  const expanded = useStore((state) => state.expanded);
3102
+ const preferredWidth = useStore((state) => state.preferredWidth);
2923
3103
  const busy = useStore((state) => state.busy);
2924
3104
  const error = useStore((state) => state.error);
2925
3105
  const input = useStore((state) => state.input);
@@ -2945,6 +3125,13 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
2945
3125
  const activeSessionId = useStore((state) => state.activeSessionId);
2946
3126
  const [fieldQuery, setFieldQuery] = react.useState('');
2947
3127
 
3128
+ react.useEffect(() => {
3129
+ if (!open || typeof document === 'undefined') return undefined;
3130
+ const drawer = document.querySelector('.dcAgentWorkbench');
3131
+ if (!drawer) return undefined;
3132
+ return installWorkbenchSizing(drawer, { preferredWidth, expanded, onWidthChange: actions.setPreferredWidth });
3133
+ }, [open, expanded, preferredWidth, actions]);
3134
+
2948
3135
  react.useEffect(() => {
2949
3136
  rootWorkbenchActions = actions;
2950
3137
  const handleOpen = (event) => {
@@ -3056,7 +3243,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3056
3243
  view?.removeEventListener?.('resize', align);
3057
3244
  reset();
3058
3245
  };
3059
- }, [open, expanded]);
3246
+ }, [open, expanded, preferredWidth]);
3060
3247
 
3061
3248
  // DSH 的 useStore 基于 React hooks;所有订阅必须在每次渲染时以相同顺序调用。
3062
3249
  // 把关闭态 guard 放在全部 useStore 之后,避免首次关闭、随后打开时触发
@@ -3931,6 +4118,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3931
4118
  'aria-modal': 'false',
3932
4119
  'aria-label': '数据清洗补全工作台',
3933
4120
  'data-session-id': activeSessionId ?? undefined,
4121
+ id: 'dc-agent-workbench',
3934
4122
  onKeyDown: (event) => {
3935
4123
  if (event.key === 'Escape') {
3936
4124
  event.preventDefault();
@@ -3939,6 +4127,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3939
4127
  }
3940
4128
  },
3941
4129
  },
4130
+ h('div', { className: 'dcAgentResizeHandle', role: 'separator', tabIndex: 0,
4131
+ 'aria-label': '调整工作台宽度', 'aria-orientation': 'vertical', 'aria-controls': 'dc-agent-workbench',
4132
+ title: '拖动调整宽度;左右方向键微调,Home/End 最小/最大,双击恢复默认' }),
3942
4133
  h('header', { className: 'dcAgentWbHeader' },
3943
4134
  h('div', { className: 'dcAgentWbTitle' },
3944
4135
  h('span', { className: 'dcAgentWbIcon', 'aria-hidden': 'true' }, h(DatabaseLogo, { size: 22 })),
@@ -3977,15 +4168,18 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
3977
4168
  h('button', { type: 'button', className: 'dcAgentButton', 'aria-pressed': step === 'history',
3978
4169
  onClick: () => actions.setStep('history') }, '任务历史'),
3979
4170
  ),
3980
- step !== 'history' ? h('nav', { className: 'dcAgentStepper', 'aria-label': '清洗流程' },
4171
+ step !== 'history' ? h('nav', { className: 'dcAgentStepper', 'aria-label': '清洗流程',
4172
+ style: { '--dc-stage-count': STEPS.length } },
3981
4173
  STEPS.map((st) => h('button', {
3982
4174
  key: st.key,
3983
4175
  type: 'button',
3984
4176
  className: `dcAgentStep${(step === 'profile' ? 'rules' : step) === st.key ? ' is-active' : ''}`,
3985
4177
  'aria-label': st.label,
4178
+ title: st.label,
3986
4179
  'aria-current': (step === 'profile' ? 'rules' : step) === st.key ? 'step' : undefined,
3987
4180
  onClick: () => actions.setStep(st.key),
3988
- }, `${st.icon} ${st.label}`)),
4181
+ }, h('span', { className: 'dcAgentStepIcon' }, h(StageIcon, { kind: st.icon })),
4182
+ h('span', { className: 'dcAgentStepLabel' }, st.label))),
3989
4183
  ) : null,
3990
4184
  h('div', { className: 'dcAgentWbBody' },
3991
4185
  error ? h('div', { className: 'dcAgentError', role: 'alert' }, error) : null,
@@ -4102,6 +4296,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
4102
4296
  exports.inject = inject;
4103
4297
  // 测试用纯函数,不构成 Host / DSH 稳定 API。
4104
4298
  exports.__testing = {
4299
+ workbenchWidthBounds,
4105
4300
  DatabaseLogo,
4106
4301
  visibleCatalogFields,
4107
4302
  installCapabilityMount,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-data-cleaning-agent",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
4
4
  "description": "Clean, complete, and profile enterprise name lists in DeepSeek Harness — a data cleaning & completion agent plugin with local CSV/XLSX/JSON engine and optional Qichacha (QCC) MCP enrichment. Maintained by Qichacha/QCC.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",