dsh-data-cleaning-agent 0.8.9 → 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 +11 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/lib/client.js +152 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
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
|
+
|
|
5
16
|
## [0.8.9] - 2026-09-07
|
|
6
17
|
|
|
7
18
|
### 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.
|
|
5
|
+
> Current source version / 当前源码版本: **0.8.10** (stable release)
|
|
6
6
|
|
|
7
|
-
This release
|
|
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
|
[](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/workflows/ci.yml)
|
|
10
10
|
[](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.
|
|
5
|
+
> 当前源码版本 / Current source version: **0.8.10**(正式版本)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
本版支持拖动右侧工作台左边缘连续调宽,中央输入区同步让位;支持方向键微调、Home/End 边界、双击恢复默认和关闭重开保留宽度。空间不足时全屏降级。保留企查查蓝菜单、全部 128 个字段和原有工作流,不替换 DSH 原生输入和发送机制。
|
|
8
8
|
|
|
9
9
|
[](https://github.com/duhu2000/dsh-data-cleaning-agent/actions/workflows/ci.yml)
|
|
10
10
|
[](https://www.npmjs.com/package/dsh-data-cleaning-agent)
|
package/lib/client.js
CHANGED
|
@@ -77,6 +77,123 @@ 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
|
+
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
|
+
|
|
80
197
|
// 搜索仅改变可见字段,绝不清除已选字段或改变 Host 的字段目录。
|
|
81
198
|
function visibleCatalogFields(group, query) {
|
|
82
199
|
const needle = String(query || '').trim().toLowerCase();
|
|
@@ -166,12 +283,13 @@ window.__ModuleLoader__.load({
|
|
|
166
283
|
}
|
|
167
284
|
|
|
168
285
|
.dcAgentWorkbench {
|
|
286
|
+
position: relative;
|
|
169
287
|
box-sizing: border-box;
|
|
170
288
|
display: flex;
|
|
171
289
|
flex-direction: column;
|
|
172
|
-
width: min(460px, 42vw);
|
|
290
|
+
width: var(--dc-agent-panel-width, min(460px, 42vw));
|
|
173
291
|
max-width: calc(100vw - 420px);
|
|
174
|
-
min-width:
|
|
292
|
+
min-width: 320px;
|
|
175
293
|
height: 100vh;
|
|
176
294
|
border: 1px solid var(--dc-border);
|
|
177
295
|
border-radius: 12px 0 0 12px;
|
|
@@ -181,7 +299,14 @@ window.__ModuleLoader__.load({
|
|
|
181
299
|
overflow: hidden;
|
|
182
300
|
pointer-events: auto;
|
|
183
301
|
}
|
|
184
|
-
.dcAgentWorkbench.is-expanded { width: min(980px, calc(100vw -
|
|
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; }
|
|
185
310
|
|
|
186
311
|
.dcAgentCapabilities {
|
|
187
312
|
display: flex;
|
|
@@ -847,24 +972,25 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
847
972
|
@media (min-width: 1041px) {
|
|
848
973
|
body:has(.dcAgentWorkbench) [data-conversation-scroll] {
|
|
849
974
|
box-sizing: border-box;
|
|
850
|
-
padding-right: min(460px, 42vw);
|
|
975
|
+
padding-right: var(--dc-agent-workbench-reserve, min(460px, 42vw));
|
|
851
976
|
}
|
|
852
977
|
}
|
|
853
978
|
@media (min-width: 761px) and (max-width: 1040px) {
|
|
854
979
|
body:has(.dcAgentWorkbench) [data-conversation-scroll] {
|
|
855
980
|
box-sizing: border-box;
|
|
856
|
-
padding-right: min(420px, 48vw);
|
|
981
|
+
padding-right: var(--dc-agent-workbench-reserve, min(420px, 48vw));
|
|
857
982
|
}
|
|
858
983
|
}
|
|
859
984
|
@media (min-width: 761px) {
|
|
860
985
|
body:has(.dcAgentWorkbench.is-expanded) [data-conversation-scroll] {
|
|
861
986
|
box-sizing: border-box;
|
|
862
|
-
padding-right: min(980px, calc(100vw - 420px));
|
|
987
|
+
padding-right: var(--dc-agent-workbench-reserve, min(980px, calc(100vw - 420px)));
|
|
863
988
|
}
|
|
864
989
|
}
|
|
865
990
|
@media (max-width: 760px) {
|
|
866
991
|
.dcAgentWorkbench, .dcAgentWorkbench.is-expanded { width: 100vw; max-width: none; min-width: 0; border-radius: 0; }
|
|
867
992
|
.dcAgentOverlay { background: rgba(8, 10, 14, 0.22); pointer-events: auto; }
|
|
993
|
+
.dcAgentResizeHandle { display: none; }
|
|
868
994
|
.dcAgentQccBadge, .dcAgentJobsPill { display: none; }
|
|
869
995
|
.dcAgentCandidate { grid-template-columns: 1fr; }
|
|
870
996
|
.dcAgentCapabilities { justify-content: flex-start; padding-inline: 12px; }
|
|
@@ -877,7 +1003,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
877
1003
|
.dcAgentMappingRow > b { display: none; }
|
|
878
1004
|
}
|
|
879
1005
|
@media (min-width: 761px) and (max-width: 1040px) {
|
|
880
|
-
.dcAgentWorkbench { width:
|
|
1006
|
+
.dcAgentWorkbench { width: var(--dc-agent-panel-width, 420px); }
|
|
881
1007
|
.dcAgentQccBadge, .dcAgentJobsPill { display: none; }
|
|
882
1008
|
}
|
|
883
1009
|
@media (max-width: 360px) {
|
|
@@ -1487,6 +1613,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1487
1613
|
init: () => ({
|
|
1488
1614
|
open: false,
|
|
1489
1615
|
expanded: false,
|
|
1616
|
+
preferredWidth: null,
|
|
1490
1617
|
step: 'upload',
|
|
1491
1618
|
busy: false,
|
|
1492
1619
|
error: null,
|
|
@@ -1521,6 +1648,10 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
1521
1648
|
},
|
|
1522
1649
|
close: (draft) => { draft.open = false; },
|
|
1523
1650
|
toggleExpanded: (draft) => { draft.expanded = !draft.expanded; },
|
|
1651
|
+
setPreferredWidth: (draft, width) => {
|
|
1652
|
+
draft.preferredWidth = Number.isFinite(width) ? width : null;
|
|
1653
|
+
draft.expanded = false;
|
|
1654
|
+
},
|
|
1524
1655
|
setStep: (draft, step) => { draft.step = step; },
|
|
1525
1656
|
setActiveSession: (draft, sessionId) => { draft.activeSessionId = sessionId ?? null; },
|
|
1526
1657
|
setBusy: (draft, busy) => { draft.busy = busy; },
|
|
@@ -2968,6 +3099,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2968
3099
|
const open = useStore((state) => state.open);
|
|
2969
3100
|
const step = useStore((state) => state.step);
|
|
2970
3101
|
const expanded = useStore((state) => state.expanded);
|
|
3102
|
+
const preferredWidth = useStore((state) => state.preferredWidth);
|
|
2971
3103
|
const busy = useStore((state) => state.busy);
|
|
2972
3104
|
const error = useStore((state) => state.error);
|
|
2973
3105
|
const input = useStore((state) => state.input);
|
|
@@ -2993,6 +3125,13 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
2993
3125
|
const activeSessionId = useStore((state) => state.activeSessionId);
|
|
2994
3126
|
const [fieldQuery, setFieldQuery] = react.useState('');
|
|
2995
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
|
+
|
|
2996
3135
|
react.useEffect(() => {
|
|
2997
3136
|
rootWorkbenchActions = actions;
|
|
2998
3137
|
const handleOpen = (event) => {
|
|
@@ -3104,7 +3243,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3104
3243
|
view?.removeEventListener?.('resize', align);
|
|
3105
3244
|
reset();
|
|
3106
3245
|
};
|
|
3107
|
-
}, [open, expanded]);
|
|
3246
|
+
}, [open, expanded, preferredWidth]);
|
|
3108
3247
|
|
|
3109
3248
|
// DSH 的 useStore 基于 React hooks;所有订阅必须在每次渲染时以相同顺序调用。
|
|
3110
3249
|
// 把关闭态 guard 放在全部 useStore 之后,避免首次关闭、随后打开时触发
|
|
@@ -3979,6 +4118,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3979
4118
|
'aria-modal': 'false',
|
|
3980
4119
|
'aria-label': '数据清洗补全工作台',
|
|
3981
4120
|
'data-session-id': activeSessionId ?? undefined,
|
|
4121
|
+
id: 'dc-agent-workbench',
|
|
3982
4122
|
onKeyDown: (event) => {
|
|
3983
4123
|
if (event.key === 'Escape') {
|
|
3984
4124
|
event.preventDefault();
|
|
@@ -3987,6 +4127,9 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
3987
4127
|
}
|
|
3988
4128
|
},
|
|
3989
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 最小/最大,双击恢复默认' }),
|
|
3990
4133
|
h('header', { className: 'dcAgentWbHeader' },
|
|
3991
4134
|
h('div', { className: 'dcAgentWbTitle' },
|
|
3992
4135
|
h('span', { className: 'dcAgentWbIcon', 'aria-hidden': 'true' }, h(DatabaseLogo, { size: 22 })),
|
|
@@ -4153,6 +4296,7 @@ body:has(.dcAgentPromptBackdrop) { overflow: hidden; }
|
|
|
4153
4296
|
exports.inject = inject;
|
|
4154
4297
|
// 测试用纯函数,不构成 Host / DSH 稳定 API。
|
|
4155
4298
|
exports.__testing = {
|
|
4299
|
+
workbenchWidthBounds,
|
|
4156
4300
|
DatabaseLogo,
|
|
4157
4301
|
visibleCatalogFields,
|
|
4158
4302
|
installCapabilityMount,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-data-cleaning-agent",
|
|
3
|
-
"version": "0.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",
|