dsh-data-cleaning-agent 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -0
- package/README.en.md +48 -7
- package/README.md +44 -6
- package/docs/COMPATIBILITY.md +54 -2
- package/docs/G5-E2E-RUNBOOK.md +110 -0
- package/docs/G5-HOST-BRIDGE.md +136 -0
- package/docs/PHASE2-ACCEPTANCE.md +133 -0
- package/docs/PHASE3-ACCEPTANCE.md +129 -0
- package/docs/QCC-ENRICHMENT-DESIGN.md +5 -2
- package/docs/QCC-PHASES-ROADMAP.md +33 -6
- package/docs/RELEASE-0.4.0.md +66 -0
- package/docs/RELEASE-0.5.0.md +84 -0
- package/docs/USER-GUIDE.md +85 -6
- package/lib/client.js +1132 -16
- package/lib/index.js +2 -0
- package/lib/qcc-phase2-acceptance.js +191 -0
- package/lib/qcc-phase2.js +99 -0
- package/lib/qcc-phase3-batch.js +437 -0
- package/lib/qcc-phase3.js +224 -0
- package/lib/qcc-runs.js +322 -0
- package/lib/qcc-safety.js +77 -0
- package/lib/qcc.js +735 -0
- package/lib/skill-enrich.js +54 -9
- package/lib/web.js +351 -2
- package/package.json +23 -4
package/lib/client.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Client 半区(
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Client 半区(M1 入口 + M2 工作台)。
|
|
3
|
+
*
|
|
4
|
+
* 通过 `window.__ModuleLoader__.load({id, factory})` 注册为惰性 CJS 工厂,
|
|
5
|
+
* 与 dsh-mcp-connector@0.2.32(生产 profile 中已实测的「MCP连接器」)同构:
|
|
6
|
+
* - `react` / `@deepseek-ai/dsh-client-ui-primitives` 由 web shell 静态模块表提供,
|
|
7
|
+
* 第三方 bundle 无需打包 React、无需构建步骤。
|
|
8
|
+
* - `defineStore` 首选 `@deepseek-ai/dsh-client-store`,回退 `@deepseek-ai/dsh-client-runtime/client`。
|
|
9
|
+
* - 入口注册到 `sidebar.footer.action`(order 10,排在 MCP连接器的 order 0 下方),
|
|
10
|
+
* 工作台注册到 `shell.overlay`(order 200)。
|
|
11
|
+
*
|
|
12
|
+
* 0.5.0 工作台复用本地 `/mvp/*` 与三域 `/phase3/*` 后端,完整覆盖上传映射、
|
|
13
|
+
* 数据体检、匹配核验、补全导出;计费调用仍由 Host Bridge 的确认/幂等/上限门约束。
|
|
6
14
|
*/
|
|
7
15
|
window.__ModuleLoader__.load({
|
|
8
16
|
id: 'dsh-data-cleaning-agent',
|
|
@@ -11,24 +19,1132 @@ window.__ModuleLoader__.load({
|
|
|
11
19
|
var exports = module.exports;
|
|
12
20
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
13
21
|
|
|
14
|
-
const
|
|
22
|
+
const react = require('react');
|
|
23
|
+
const { Button } = require('@deepseek-ai/dsh-client-ui-primitives');
|
|
24
|
+
|
|
25
|
+
let defineStore;
|
|
26
|
+
try {
|
|
27
|
+
({ defineStore } = require('@deepseek-ai/dsh-client-store'));
|
|
28
|
+
} catch (storeError) {
|
|
29
|
+
try {
|
|
30
|
+
({ defineStore } = require('@deepseek-ai/dsh-client-runtime/client'));
|
|
31
|
+
} catch (runtimeError) {
|
|
32
|
+
throw new AggregateError(
|
|
33
|
+
[storeError, runtimeError],
|
|
34
|
+
'data-cleaning-agent: DSH client store is unavailable'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const h = react.createElement;
|
|
40
|
+
|
|
41
|
+
/** 客户端所需服务:与 mcp-connector 对齐(槽位 + 会话/工作区/输入机)。 */
|
|
42
|
+
const inject = ['slots', 'sessions', 'workspaces', 'conversation'];
|
|
43
|
+
|
|
44
|
+
const FOOTER_STYLE_ID = 'dsh-data-cleaning-agent-sidebar';
|
|
45
|
+
const stylesCss = `
|
|
46
|
+
[data-slot="sidebar.footer.action"] {
|
|
47
|
+
display: flex !important;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
min-width: 0;
|
|
50
|
+
width: 100%;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.dcAgentLauncher {
|
|
54
|
+
flex: none;
|
|
55
|
+
box-sizing: border-box;
|
|
56
|
+
width: 100%;
|
|
57
|
+
min-width: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.dcAgentOverlay {
|
|
61
|
+
position: fixed;
|
|
62
|
+
inset: 0;
|
|
63
|
+
z-index: 1000;
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: stretch;
|
|
66
|
+
justify-content: flex-end;
|
|
67
|
+
background: rgba(8, 10, 14, 0.22);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.dcAgentWorkbench {
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
width: min(980px, calc(100vw - 72px));
|
|
74
|
+
height: 100vh;
|
|
75
|
+
border: 1px solid light-dark(#d5dae4, #2a3038);
|
|
76
|
+
border-radius: 14px 0 0 14px;
|
|
77
|
+
background: light-dark(#ffffff, #161b23);
|
|
78
|
+
color: light-dark(#172033, #edf2fa);
|
|
79
|
+
box-shadow: light-dark(0 18px 50px rgba(33, 55, 88, .14), 0 20px 60px rgba(0, 0, 0, .42));
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
}
|
|
82
|
+
.dcAgentWorkbench.is-expanded { width: calc(100vw - 72px); }
|
|
83
|
+
|
|
84
|
+
.dcAgentWbHeader {
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 12px;
|
|
88
|
+
padding: 14px 18px;
|
|
89
|
+
border-bottom: 1px solid light-dark(#e4e8f0, #303947);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.dcAgentWbTitle {
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
gap: 10px;
|
|
96
|
+
min-width: 0;
|
|
97
|
+
flex: 1 1 auto;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.dcAgentWbIcon {
|
|
101
|
+
width: 36px;
|
|
102
|
+
height: 36px;
|
|
103
|
+
display: grid;
|
|
104
|
+
place-items: center;
|
|
105
|
+
flex: 0 0 auto;
|
|
106
|
+
border-radius: 11px;
|
|
107
|
+
font-size: 18px;
|
|
108
|
+
background: light-dark(#edf4ff, #172841);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.dcAgentWbTitle b { display: block; font-size: 15px; }
|
|
112
|
+
.dcAgentWbTitle small { display: block; color: light-dark(#697386, #aab5c7); font-size: 11px; }
|
|
113
|
+
|
|
114
|
+
.dcAgentQccBadge {
|
|
115
|
+
flex: 0 0 auto;
|
|
116
|
+
padding: 4px 10px;
|
|
117
|
+
border: 1px solid light-dark(#e4e8f0, #303947);
|
|
118
|
+
border-radius: 999px;
|
|
119
|
+
color: light-dark(#697386, #aab5c7);
|
|
120
|
+
font-size: 11px;
|
|
121
|
+
white-space: nowrap;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.dcAgentWbClose {
|
|
125
|
+
flex: 0 0 auto;
|
|
126
|
+
width: 30px;
|
|
127
|
+
height: 30px;
|
|
128
|
+
display: grid;
|
|
129
|
+
place-items: center;
|
|
130
|
+
border: 1px solid light-dark(#e4e8f0, #303947);
|
|
131
|
+
border-radius: 8px;
|
|
132
|
+
background: transparent;
|
|
133
|
+
color: inherit;
|
|
134
|
+
font-size: 15px;
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
}
|
|
137
|
+
.dcAgentWbClose:hover, .dcAgentWbClose:focus-visible { background: light-dark(#f6f8fb, #202733); }
|
|
138
|
+
|
|
139
|
+
.dcAgentStepper {
|
|
140
|
+
display: grid;
|
|
141
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
142
|
+
gap: 8px;
|
|
143
|
+
padding: 12px 18px;
|
|
144
|
+
border-bottom: 1px solid light-dark(#e4e8f0, #303947);
|
|
145
|
+
background: light-dark(#fbfcfe, #12171e);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.dcAgentStep {
|
|
149
|
+
display: flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
justify-content: center;
|
|
152
|
+
gap: 6px;
|
|
153
|
+
padding: 9px 6px;
|
|
154
|
+
border: 1px solid light-dark(#e4e8f0, #303947);
|
|
155
|
+
border-radius: 9px;
|
|
156
|
+
background: transparent;
|
|
157
|
+
color: light-dark(#697386, #aab5c7);
|
|
158
|
+
font-size: 12px;
|
|
159
|
+
cursor: pointer;
|
|
160
|
+
white-space: nowrap;
|
|
161
|
+
}
|
|
162
|
+
.dcAgentStep.is-active {
|
|
163
|
+
color: light-dark(#1556cf, #8cb3ff);
|
|
164
|
+
border-color: light-dark(#2869e6, #6d9eff);
|
|
165
|
+
background: light-dark(#edf4ff, #172841);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.dcAgentWbBody {
|
|
169
|
+
flex: 1 1 auto;
|
|
170
|
+
overflow: auto;
|
|
171
|
+
padding: 18px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.dcAgentPane { display: flex; flex-direction: column; gap: 14px; }
|
|
175
|
+
.dcAgentHint { margin: 0; color: light-dark(#697386, #aab5c7); font-size: 13px; line-height: 1.6; }
|
|
176
|
+
|
|
177
|
+
.dcAgentTextarea {
|
|
178
|
+
width: 100%;
|
|
179
|
+
box-sizing: border-box;
|
|
180
|
+
min-height: 132px;
|
|
181
|
+
padding: 12px;
|
|
182
|
+
border: 1px solid light-dark(#cfd6e2, #485364);
|
|
183
|
+
border-radius: 10px;
|
|
184
|
+
background: light-dark(#ffffff, #191f28);
|
|
185
|
+
color: inherit;
|
|
186
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
187
|
+
font-size: 12px;
|
|
188
|
+
resize: vertical;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.dcAgentRow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
192
|
+
.dcAgentField {
|
|
193
|
+
flex: 1 1 auto;
|
|
194
|
+
box-sizing: border-box;
|
|
195
|
+
min-width: 0;
|
|
196
|
+
padding: 8px 12px;
|
|
197
|
+
border: 1px solid light-dark(#cfd6e2, #485364);
|
|
198
|
+
border-radius: 9px;
|
|
199
|
+
font-size: 12px;
|
|
200
|
+
background: light-dark(#ffffff, #191f28);
|
|
201
|
+
color: inherit;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.dcAgentButton {
|
|
205
|
+
padding: 9px 16px;
|
|
206
|
+
border: 1px solid light-dark(#cfd6e2, #485364);
|
|
207
|
+
border-radius: 9px;
|
|
208
|
+
background: light-dark(#ffffff, #191f28);
|
|
209
|
+
color: inherit;
|
|
210
|
+
font-size: 13px;
|
|
211
|
+
cursor: pointer;
|
|
212
|
+
}
|
|
213
|
+
.dcAgentButton:hover, .dcAgentButton:focus-visible { border-color: light-dark(#2869e6, #6d9eff); }
|
|
214
|
+
.dcAgentButton.is-primary {
|
|
215
|
+
border-color: transparent;
|
|
216
|
+
color: #ffffff;
|
|
217
|
+
background: light-dark(#2869e6, #3d7bf0);
|
|
218
|
+
}
|
|
219
|
+
.dcAgentButton.is-primary:hover, .dcAgentButton.is-primary:focus-visible { background: light-dark(#1556cf, #6d9eff); }
|
|
220
|
+
.dcAgentButton:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
221
|
+
|
|
222
|
+
.dcAgentGrid {
|
|
223
|
+
display: grid;
|
|
224
|
+
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
225
|
+
gap: 10px;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.dcAgentCard {
|
|
229
|
+
padding: 12px 14px;
|
|
230
|
+
border: 1px solid light-dark(#e4e8f0, #303947);
|
|
231
|
+
border-radius: 11px;
|
|
232
|
+
background: light-dark(#fbfcfe, #191f28);
|
|
233
|
+
}
|
|
234
|
+
.dcAgentCard span { display: block; color: light-dark(#697386, #aab5c7); font-size: 11px; }
|
|
235
|
+
.dcAgentCard b { display: block; margin-top: 3px; font-size: 18px; }
|
|
236
|
+
.dcAgentCard b.is-good { color: light-dark(#118a5b, #56d39b); }
|
|
237
|
+
.dcAgentCard b.is-warn { color: light-dark(#c17412, #f4b85d); }
|
|
238
|
+
.dcAgentCard b.is-bad { color: light-dark(#d2454f, #ff838b); }
|
|
239
|
+
|
|
240
|
+
.dcAgentTable {
|
|
241
|
+
width: 100%;
|
|
242
|
+
border-collapse: collapse;
|
|
243
|
+
font-size: 12px;
|
|
244
|
+
}
|
|
245
|
+
.dcAgentTable th, .dcAgentTable td {
|
|
246
|
+
padding: 7px 9px;
|
|
247
|
+
text-align: left;
|
|
248
|
+
border-bottom: 1px solid light-dark(#eef1f6, #242b36);
|
|
249
|
+
}
|
|
250
|
+
.dcAgentTable th { color: light-dark(#697386, #aab5c7); font-weight: 600; }
|
|
251
|
+
.dcAgentTable td.num { font-variant-numeric: tabular-nums; }
|
|
252
|
+
|
|
253
|
+
.dcAgentError {
|
|
254
|
+
padding: 10px 12px;
|
|
255
|
+
border: 1px solid light-dark(#ffd6d8, #5b2f33);
|
|
256
|
+
border-radius: 9px;
|
|
257
|
+
background: light-dark(#fff0f0, #351d21);
|
|
258
|
+
color: light-dark(#d2454f, #ff838b);
|
|
259
|
+
font-size: 12px;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.dcAgentChips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
263
|
+
.dcAgentChip {
|
|
264
|
+
padding: 4px 10px;
|
|
265
|
+
border: 1px solid light-dark(#e4e8f0, #303947);
|
|
266
|
+
border-radius: 999px;
|
|
267
|
+
font-size: 11px;
|
|
268
|
+
color: light-dark(#697386, #aab5c7);
|
|
269
|
+
}
|
|
270
|
+
.dcAgentChip.is-selected {
|
|
271
|
+
color: light-dark(#1556cf, #8cb3ff);
|
|
272
|
+
border-color: light-dark(#2869e6, #6d9eff);
|
|
273
|
+
background: light-dark(#edf4ff, #172841);
|
|
274
|
+
}
|
|
275
|
+
.dcAgentCheck { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; }
|
|
276
|
+
.dcAgentSection {
|
|
277
|
+
padding: 14px;
|
|
278
|
+
border: 1px solid light-dark(#e4e8f0, #303947);
|
|
279
|
+
border-radius: 11px;
|
|
280
|
+
background: light-dark(#fbfcfe, #191f28);
|
|
281
|
+
}
|
|
282
|
+
.dcAgentSection h3 { margin: 0 0 8px; font-size: 13px; }
|
|
283
|
+
.dcAgentCandidate {
|
|
284
|
+
display: grid;
|
|
285
|
+
grid-template-columns: minmax(0, 1.5fr) minmax(120px, 1fr) auto;
|
|
286
|
+
gap: 10px;
|
|
287
|
+
align-items: center;
|
|
288
|
+
padding: 10px 0;
|
|
289
|
+
border-bottom: 1px solid light-dark(#eef1f6, #242b36);
|
|
290
|
+
}
|
|
291
|
+
.dcAgentCandidate:last-child { border-bottom: 0; }
|
|
292
|
+
.dcAgentCandidate small { color: light-dark(#697386, #aab5c7); }
|
|
293
|
+
.dcAgentProgress { height: 8px; overflow: hidden; border-radius: 99px; background: light-dark(#e8edf5, #293240); }
|
|
294
|
+
.dcAgentProgress > span { display: block; height: 100%; background: #2869e6; }
|
|
295
|
+
|
|
296
|
+
/* M3 · overlay 头部 jobs 状态 pill(轮询 /mvp/jobs,不接计费遥测)。 */
|
|
297
|
+
.dcAgentJobsPill {
|
|
298
|
+
flex: 0 0 auto;
|
|
299
|
+
padding: 4px 10px;
|
|
300
|
+
border: 1px solid light-dark(#e4e8f0, #303947);
|
|
301
|
+
border-radius: 999px;
|
|
302
|
+
font-size: 11px;
|
|
303
|
+
white-space: nowrap;
|
|
304
|
+
color: light-dark(#697386, #aab5c7);
|
|
305
|
+
}
|
|
306
|
+
.dcAgentJobsPill[data-state='running'] {
|
|
307
|
+
color: light-dark(#1556cf, #8cb3ff);
|
|
308
|
+
border-color: light-dark(#2869e6, #6d9eff);
|
|
309
|
+
background: light-dark(#edf4ff, #172841);
|
|
310
|
+
}
|
|
311
|
+
.dcAgentJobsPill[data-state='completed'] {
|
|
312
|
+
color: light-dark(#118a5b, #56d39b);
|
|
313
|
+
border-color: light-dark(#118a5b, #2f7d5e);
|
|
314
|
+
background: light-dark(#eefaf3, #12261d);
|
|
315
|
+
}
|
|
316
|
+
.dcAgentJobsPill[data-state='failed'] {
|
|
317
|
+
color: light-dark(#d2454f, #ff838b);
|
|
318
|
+
border-color: light-dark(#d2454f, #7a3a40);
|
|
319
|
+
background: light-dark(#fff0f0, #351d21);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* M3 · tool.call.toolview 富化卡片(三工具摘要,替代裸 JSON)。 */
|
|
323
|
+
.dcAgentToolCard {
|
|
324
|
+
display: flex;
|
|
325
|
+
flex-direction: column;
|
|
326
|
+
gap: 8px;
|
|
327
|
+
padding: 12px 14px;
|
|
328
|
+
border: 1px solid light-dark(#e4e8f0, #303947);
|
|
329
|
+
border-radius: 11px;
|
|
330
|
+
background: light-dark(#fbfcfe, #191f28);
|
|
331
|
+
margin: 4px 0;
|
|
332
|
+
}
|
|
333
|
+
.dcAgentToolCardHead {
|
|
334
|
+
display: flex;
|
|
335
|
+
align-items: center;
|
|
336
|
+
gap: 8px;
|
|
337
|
+
min-width: 0;
|
|
338
|
+
}
|
|
339
|
+
.dcAgentToolCardIcon { flex: 0 0 auto; font-size: 15px; }
|
|
340
|
+
.dcAgentToolCardTitle {
|
|
341
|
+
flex: 1 1 auto;
|
|
342
|
+
min-width: 0;
|
|
343
|
+
font-size: 13px;
|
|
344
|
+
font-weight: 600;
|
|
345
|
+
}
|
|
346
|
+
.dcAgentToolCardState {
|
|
347
|
+
flex: 0 0 auto;
|
|
348
|
+
padding: 1px 8px;
|
|
349
|
+
border-radius: 999px;
|
|
350
|
+
font-size: 11px;
|
|
351
|
+
color: light-dark(#697386, #aab5c7);
|
|
352
|
+
border: 1px solid light-dark(#e4e8f0, #303947);
|
|
353
|
+
}
|
|
354
|
+
.dcAgentToolCardState.is-ok { color: light-dark(#118a5b, #56d39b); }
|
|
355
|
+
.dcAgentToolCardState.is-error { color: light-dark(#d2454f, #ff838b); }
|
|
356
|
+
.dcAgentToolCardState.is-running { color: light-dark(#1556cf, #8cb3ff); }
|
|
357
|
+
.dcAgentToolCardState.is-stopped { color: light-dark(#697386, #aab5c7); }
|
|
358
|
+
.dcAgentToolCardBody {
|
|
359
|
+
margin: 0;
|
|
360
|
+
white-space: pre-wrap;
|
|
361
|
+
word-break: break-word;
|
|
362
|
+
font-size: 12px;
|
|
363
|
+
line-height: 1.6;
|
|
364
|
+
color: light-dark(#33415a, #c9d4e5);
|
|
365
|
+
}
|
|
366
|
+
@media (max-width: 760px) {
|
|
367
|
+
.dcAgentWorkbench, .dcAgentWorkbench.is-expanded { width: 100vw; border-radius: 0; }
|
|
368
|
+
.dcAgentStepper { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
369
|
+
.dcAgentQccBadge { display: none; }
|
|
370
|
+
.dcAgentCandidate { grid-template-columns: 1fr; }
|
|
371
|
+
}
|
|
372
|
+
`;
|
|
373
|
+
|
|
374
|
+
/** 让 footer 多个入口纵向堆叠(与 mcp-connector 注入的列式 CSS 幂等并存)。 */
|
|
375
|
+
function installSidebarStyles() {
|
|
376
|
+
if (document.querySelector(`style[data-plugin="${FOOTER_STYLE_ID}"]`) !== null) return () => {};
|
|
377
|
+
const style = document.createElement('style');
|
|
378
|
+
style.dataset.plugin = FOOTER_STYLE_ID;
|
|
379
|
+
style.textContent = stylesCss;
|
|
380
|
+
document.head.append(style);
|
|
381
|
+
return () => { style.remove(); };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 会话级数据(不进 store,避免大快照膨胀)。原始行仅用于后端往返,不进模型上下文。
|
|
385
|
+
let session = { rows: [], headers: [] };
|
|
386
|
+
let lastCsv = { clean: null, complete: null, qcc: null, review: null };
|
|
387
|
+
|
|
388
|
+
const STEPS = [
|
|
389
|
+
{ key: 'upload', label: '上传与映射', icon: '📄' },
|
|
390
|
+
{ key: 'profile', label: '数据体检', icon: '🩺' },
|
|
391
|
+
{ key: 'review', label: '匹配核验', icon: '🔎' },
|
|
392
|
+
{ key: 'enrich', label: '补全与导出', icon: '⬇️' },
|
|
393
|
+
];
|
|
394
|
+
|
|
395
|
+
/** 后端往返:POST JSON,返回解析后的对象。 */
|
|
396
|
+
async function api(path, body) {
|
|
397
|
+
const res = await fetch(path, {
|
|
398
|
+
method: body ? 'POST' : 'GET',
|
|
399
|
+
headers: body ? { 'content-type': 'application/json' } : undefined,
|
|
400
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
401
|
+
});
|
|
402
|
+
return res.json();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** 工作台 store:入口按钮与工作台共享 open,另存四步流程 UI 状态。 */
|
|
406
|
+
function createWorkbenchStore() {
|
|
407
|
+
return defineStore({
|
|
408
|
+
init: () => ({
|
|
409
|
+
open: false,
|
|
410
|
+
expanded: false,
|
|
411
|
+
step: 'upload',
|
|
412
|
+
busy: false,
|
|
413
|
+
error: null,
|
|
414
|
+
input: '',
|
|
415
|
+
dataset: null, // { fmt, headers, rowCount, preview }
|
|
416
|
+
profile: null, // summary
|
|
417
|
+
clean: null, // summary
|
|
418
|
+
complete: null, // summary
|
|
419
|
+
nameField: 'name',
|
|
420
|
+
selectedDomains: [],
|
|
421
|
+
qccCapabilities: null,
|
|
422
|
+
qccEstimate: null,
|
|
423
|
+
qccRun: null,
|
|
424
|
+
paidConfirmed: false,
|
|
425
|
+
jobs: [], // 后台任务列表(/mvp/jobs 轮询),仅用于状态 pill,不接计费遥测
|
|
426
|
+
}),
|
|
427
|
+
actions: {
|
|
428
|
+
open: (draft) => { draft.open = true; },
|
|
429
|
+
close: (draft) => { draft.open = false; },
|
|
430
|
+
toggleExpanded: (draft) => { draft.expanded = !draft.expanded; },
|
|
431
|
+
setStep: (draft, step) => { draft.step = step; },
|
|
432
|
+
setBusy: (draft, busy) => { draft.busy = busy; },
|
|
433
|
+
setError: (draft, error) => { draft.error = error; },
|
|
434
|
+
setInput: (draft, input) => { draft.input = input; },
|
|
435
|
+
setDataset: (draft, dataset) => { draft.dataset = dataset; draft.error = null; },
|
|
436
|
+
setProfile: (draft, profile) => { draft.profile = profile; draft.error = null; },
|
|
437
|
+
setClean: (draft, clean) => { draft.clean = clean; draft.error = null; },
|
|
438
|
+
setComplete: (draft, complete) => { draft.complete = complete; draft.error = null; },
|
|
439
|
+
setNameField: (draft, nameField) => { draft.nameField = nameField; draft.qccEstimate = null; },
|
|
440
|
+
toggleDomain: (draft, domain) => {
|
|
441
|
+
const selected = new Set(draft.selectedDomains);
|
|
442
|
+
if (selected.has(domain)) selected.delete(domain); else selected.add(domain);
|
|
443
|
+
draft.selectedDomains = [...selected];
|
|
444
|
+
draft.qccEstimate = null;
|
|
445
|
+
draft.paidConfirmed = false;
|
|
446
|
+
},
|
|
447
|
+
setQccCapabilities: (draft, value) => { draft.qccCapabilities = value; draft.error = null; },
|
|
448
|
+
setQccEstimate: (draft, value) => { draft.qccEstimate = value; draft.error = null; draft.paidConfirmed = false; },
|
|
449
|
+
setQccRun: (draft, value) => { draft.qccRun = value; draft.error = null; },
|
|
450
|
+
setPaidConfirmed: (draft, value) => { draft.paidConfirmed = Boolean(value); },
|
|
451
|
+
setJobs: (draft, jobs) => { draft.jobs = Array.isArray(jobs) ? jobs : []; },
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** 左栏入口按钮:使用 DSH Button,与 MCP连接器一致。 */
|
|
457
|
+
function SidebarEntry(props) {
|
|
458
|
+
const { wide, useStore, actions } = props;
|
|
459
|
+
const open = useStore((state) => state.open);
|
|
460
|
+
return react.createElement(Button, {
|
|
461
|
+
variant: 'ghost',
|
|
462
|
+
className: 'dcAgentLauncher',
|
|
463
|
+
'data-wide': wide,
|
|
464
|
+
'aria-label': '数据清洗',
|
|
465
|
+
'aria-haspopup': 'dialog',
|
|
466
|
+
'aria-expanded': open,
|
|
467
|
+
onClick: () => {
|
|
468
|
+
try {
|
|
469
|
+
actions.open();
|
|
470
|
+
startJobsPolling(actions);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.error('[dc-agent] open failed:', error);
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
children: wide ? '🧹 数据清洗' : '🧹',
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** M3 · 三工具 tool.call.toolview 卡片元数据(wire 名 → 展示文案)。 */
|
|
480
|
+
const TOOL_VIEW_META = {
|
|
481
|
+
data_clean_rows: { icon: '🧹', label: '数据清洗', hint: '去重 · 剔除非法金额 · 缺失补 0' },
|
|
482
|
+
data_complete_rows: { icon: '🧩', label: '数据补全', hint: '名称 / 金额 / 手机号归一' },
|
|
483
|
+
data_profile: { icon: '🩺', label: '数据体检', hint: '缺失率 · 去重值 · 金额分布' },
|
|
484
|
+
};
|
|
485
|
+
const TOOL_VIEW_STATE = { running: '运行中', stopped: '已停止', error: '失败', ok: '完成' };
|
|
486
|
+
|
|
487
|
+
/** 从 settled 结果节点的 text 块提取可读摘要(对齐 tool 包 resultText 的降级口径)。 */
|
|
488
|
+
function flattenResultText(block) {
|
|
489
|
+
if (!block || typeof block !== 'object') return '';
|
|
490
|
+
const parts = [];
|
|
491
|
+
for (const b of (Array.isArray(block.content) ? block.content : [])) {
|
|
492
|
+
if (b && b.type === 'text' && typeof b.text === 'string') parts.push(b.text);
|
|
493
|
+
}
|
|
494
|
+
const text = parts.join('');
|
|
495
|
+
if (text) return text;
|
|
496
|
+
if (block.error && block.error.name) return `${block.error.name}: ${block.error.code ?? ''}`;
|
|
497
|
+
return '';
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** tool.call.toolview 富化卡片:把三工具摘要渲染为可读卡片,替代裸 JSON。 */
|
|
501
|
+
function DataToolCard(props) {
|
|
502
|
+
const { toolName, block } = props;
|
|
503
|
+
const meta = TOOL_VIEW_META[toolName] ?? { icon: '🧹', label: toolName, hint: '' };
|
|
504
|
+
const done = block !== null && typeof block === 'object' && 'kind' in block;
|
|
505
|
+
let state = 'running';
|
|
506
|
+
if (done) {
|
|
507
|
+
state = block.error?.code === 'interrupted' ? 'stopped' : (block.isError ? 'error' : 'ok');
|
|
508
|
+
}
|
|
509
|
+
const summary = done ? flattenResultText(block) : '';
|
|
510
|
+
return h('div', { className: 'dcAgentToolCard', 'data-tool': toolName, 'data-state': state },
|
|
511
|
+
h('div', { className: 'dcAgentToolCardHead' },
|
|
512
|
+
h('span', { className: 'dcAgentToolCardIcon', 'aria-hidden': 'true' }, meta.icon),
|
|
513
|
+
h('span', { className: 'dcAgentToolCardTitle' }, meta.label),
|
|
514
|
+
h('span', { className: `dcAgentToolCardState is-${state}` }, TOOL_VIEW_STATE[state] ?? state),
|
|
515
|
+
),
|
|
516
|
+
meta.hint ? h('div', { className: 'dcAgentToolCardHint' }, meta.hint) : null,
|
|
517
|
+
summary ? h('div', { className: 'dcAgentToolCardBody' }, summary) : null,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** jobs 状态 pill:由 /mvp/jobs 列表派生(服务端按 createdAt 降序,取队首)。 */
|
|
522
|
+
const JOB_STATE_LABEL = { queued: '排队中', running: '运行中', completed: '已完成', failed: '失败', killed: '已终止' };
|
|
523
|
+
function jobsPill(jobs) {
|
|
524
|
+
const list = Array.isArray(jobs) ? jobs : [];
|
|
525
|
+
if (!list.length) return { state: 'idle', label: '无后台任务' };
|
|
526
|
+
const state = list[0] && list[0].state ? list[0].state : 'idle';
|
|
527
|
+
return { state, label: JOB_STATE_LABEL[state] ?? state };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// jobs 轮询器(模块级单例):打开工作台时启动,关闭即停;任何失败静默降级为「无后台任务」。
|
|
531
|
+
let jobsTimer = null;
|
|
532
|
+
async function pollJobsOnce(actions) {
|
|
533
|
+
try {
|
|
534
|
+
const r = await api('/data-cleaning/api/mvp/jobs');
|
|
535
|
+
if (r && r.ok !== false) actions.setJobs(Array.isArray(r.jobs) ? r.jobs : []);
|
|
536
|
+
} catch (error) {
|
|
537
|
+
// 静默:jobs 不可用不影响工作台主流程。
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function startJobsPolling(actions) {
|
|
541
|
+
if (jobsTimer !== null) return;
|
|
542
|
+
pollJobsOnce(actions);
|
|
543
|
+
jobsTimer = setInterval(() => pollJobsOnce(actions), 2000);
|
|
544
|
+
if (jobsTimer && typeof jobsTimer.unref === 'function') jobsTimer.unref();
|
|
545
|
+
}
|
|
546
|
+
function stopJobsPolling() {
|
|
547
|
+
if (jobsTimer !== null) { clearInterval(jobsTimer); jobsTimer = null; }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** 上传 pane 的处理:文件 → 文本/base64 → parse。 */
|
|
551
|
+
async function parseFile(file) {
|
|
552
|
+
const isXlsx = /\.(xlsx|xls)$/i.test(file && file.name ? file.name : '');
|
|
553
|
+
let content;
|
|
554
|
+
if (isXlsx) {
|
|
555
|
+
const buf = await file.arrayBuffer();
|
|
556
|
+
const bytes = new Uint8Array(buf);
|
|
557
|
+
let bin = '';
|
|
558
|
+
for (let i = 0; i < bytes.length; i += 1) bin += String.fromCharCode(bytes[i]);
|
|
559
|
+
content = btoa(bin);
|
|
560
|
+
} else {
|
|
561
|
+
content = await file.text();
|
|
562
|
+
}
|
|
563
|
+
return api('/data-cleaning/api/mvp/parse', { filename: file.name, content });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** 文本/JSON 直通 parse(与 web.js 内联页同构,含 JSON 数组直通)。 */
|
|
567
|
+
async function parseText(text, actions) {
|
|
568
|
+
const trimmed = (text ?? '').trim();
|
|
569
|
+
if (!trimmed) {
|
|
570
|
+
actions.setError('请粘贴或上传数据后再解析。');
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (trimmed.startsWith('[')) {
|
|
574
|
+
try {
|
|
575
|
+
const rows = JSON.parse(trimmed);
|
|
576
|
+
session.rows = Array.isArray(rows) ? rows : [];
|
|
577
|
+
session.headers = session.rows.length ? Object.keys(session.rows[0]) : [];
|
|
578
|
+
actions.setDataset({
|
|
579
|
+
fmt: 'json',
|
|
580
|
+
headers: session.headers,
|
|
581
|
+
rowCount: session.rows.length,
|
|
582
|
+
preview: session.rows.slice(0, 5),
|
|
583
|
+
});
|
|
584
|
+
} catch (error) {
|
|
585
|
+
actions.setError(`JSON 解析失败:${error instanceof Error ? error.message : String(error)}`);
|
|
586
|
+
}
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
return api('/data-cleaning/api/mvp/parse', { filename: 'data.csv', content: trimmed });
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** 解析成功落库 + 前进到「数据体检」。 */
|
|
593
|
+
function applyParsed(result, actions) {
|
|
594
|
+
session.rows = Array.isArray(result.rows) ? result.rows : [];
|
|
595
|
+
session.headers = Array.isArray(result.headers) ? result.headers : [];
|
|
596
|
+
const guessedNameField = session.headers.find((name) => /^(name|company|company_name|企业名称|公司名称)$/i.test(name))
|
|
597
|
+
?? session.headers.find((name) => /企业|公司|名称|name/i.test(name))
|
|
598
|
+
?? session.headers[0]
|
|
599
|
+
?? 'name';
|
|
600
|
+
actions.setNameField(guessedNameField);
|
|
601
|
+
actions.setDataset({
|
|
602
|
+
fmt: result.fmt ?? 'csv',
|
|
603
|
+
headers: session.headers,
|
|
604
|
+
rowCount: typeof result.rowCount === 'number' ? result.rowCount : session.rows.length,
|
|
605
|
+
preview: Array.isArray(result.preview) ? result.preview : session.rows.slice(0, 5),
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** 工作台主视图:header + stepper + 当前 pane。 */
|
|
610
|
+
function WorkbenchOverlay(props) {
|
|
611
|
+
const { useStore, actions } = props;
|
|
612
|
+
const open = useStore((state) => state.open);
|
|
613
|
+
const step = useStore((state) => state.step);
|
|
614
|
+
const expanded = useStore((state) => state.expanded);
|
|
615
|
+
const busy = useStore((state) => state.busy);
|
|
616
|
+
const error = useStore((state) => state.error);
|
|
617
|
+
const input = useStore((state) => state.input);
|
|
618
|
+
const dataset = useStore((state) => state.dataset);
|
|
619
|
+
const profile = useStore((state) => state.profile);
|
|
620
|
+
const clean = useStore((state) => state.clean);
|
|
621
|
+
const complete = useStore((state) => state.complete);
|
|
622
|
+
const nameField = useStore((state) => state.nameField);
|
|
623
|
+
const selectedDomains = useStore((state) => state.selectedDomains);
|
|
624
|
+
const qccCapabilities = useStore((state) => state.qccCapabilities);
|
|
625
|
+
const qccEstimate = useStore((state) => state.qccEstimate);
|
|
626
|
+
const qccRun = useStore((state) => state.qccRun);
|
|
627
|
+
const paidConfirmed = useStore((state) => state.paidConfirmed);
|
|
628
|
+
const jobs = useStore((state) => state.jobs);
|
|
629
|
+
|
|
630
|
+
// DSH 的 useStore 基于 React hooks;所有订阅必须在每次渲染时以相同顺序调用。
|
|
631
|
+
// 把关闭态 guard 放在全部 useStore 之后,避免首次关闭、随后打开时触发
|
|
632
|
+
// React #310(Rendered more hooks than during the previous render)。
|
|
633
|
+
if (!open) return null;
|
|
634
|
+
|
|
635
|
+
const hasData = dataset !== null && dataset.rowCount > 0;
|
|
636
|
+
const fieldByPattern = (pattern) => session.headers.find((field) => pattern.test(field)) ?? null;
|
|
637
|
+
const phoneField = fieldByPattern(/^(phone|mobile|tel|telephone|联系电话|手机号码|手机号)$/i);
|
|
638
|
+
const amountField = fieldByPattern(/^(amount|price|金额|注册资本)$/i);
|
|
639
|
+
const localCleanOptions = {
|
|
640
|
+
required: nameField ? [nameField] : [],
|
|
641
|
+
dedupeOn: nameField || null,
|
|
642
|
+
phoneField,
|
|
643
|
+
amountField,
|
|
644
|
+
};
|
|
645
|
+
const localCompleteOptions = {
|
|
646
|
+
phoneField,
|
|
647
|
+
amountField,
|
|
648
|
+
// 企业名称不能由占位符补全;缺失值交给人工/QCC 匹配队列处理。
|
|
649
|
+
fillableName: false,
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const handleParse = async () => {
|
|
653
|
+
if (busy) return;
|
|
654
|
+
actions.setBusy(true);
|
|
655
|
+
actions.setError(null);
|
|
656
|
+
try {
|
|
657
|
+
const result = await parseText(input, actions);
|
|
658
|
+
if (result) applyParsed(result, actions);
|
|
659
|
+
} catch (err) {
|
|
660
|
+
actions.setError(err instanceof Error ? err.message : String(err));
|
|
661
|
+
} finally {
|
|
662
|
+
actions.setBusy(false);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const handleFile = async (event) => {
|
|
667
|
+
const file = event.target && event.target.files && event.target.files[0];
|
|
668
|
+
if (!file) return;
|
|
669
|
+
actions.setBusy(true);
|
|
670
|
+
actions.setError(null);
|
|
671
|
+
try {
|
|
672
|
+
const result = await parseFile(file);
|
|
673
|
+
if (result && result.ok !== false) applyParsed(result, actions);
|
|
674
|
+
else actions.setError((result && (result.message || result.error)) || '解析失败');
|
|
675
|
+
} catch (err) {
|
|
676
|
+
actions.setError(err instanceof Error ? err.message : String(err));
|
|
677
|
+
} finally {
|
|
678
|
+
actions.setBusy(false);
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
const runProfile = async () => {
|
|
683
|
+
if (busy || !session.rows.length) {
|
|
684
|
+
if (!session.rows.length) actions.setError('请先上传并解析数据。');
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
actions.setBusy(true);
|
|
688
|
+
actions.setError(null);
|
|
689
|
+
try {
|
|
690
|
+
const r = await api('/data-cleaning/api/mvp/profile', {
|
|
691
|
+
rows: session.rows,
|
|
692
|
+
headers: session.headers,
|
|
693
|
+
options: { amountField },
|
|
694
|
+
});
|
|
695
|
+
if (r && r.ok !== false) {
|
|
696
|
+
actions.setProfile(r.summary ?? r);
|
|
697
|
+
actions.setStep('review');
|
|
698
|
+
} else {
|
|
699
|
+
actions.setError((r && (r.message || r.error)) || '体检失败');
|
|
700
|
+
}
|
|
701
|
+
} catch (err) {
|
|
702
|
+
actions.setError(err instanceof Error ? err.message : String(err));
|
|
703
|
+
} finally {
|
|
704
|
+
actions.setBusy(false);
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const runClean = async () => {
|
|
709
|
+
if (busy || !session.rows.length) {
|
|
710
|
+
if (!session.rows.length) actions.setError('请先上传并解析数据。');
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
actions.setBusy(true);
|
|
714
|
+
actions.setError(null);
|
|
715
|
+
try {
|
|
716
|
+
const r = await api('/data-cleaning/api/mvp/clean', {
|
|
717
|
+
rows: session.rows,
|
|
718
|
+
headers: session.headers,
|
|
719
|
+
options: localCleanOptions,
|
|
720
|
+
});
|
|
721
|
+
if (r && r.ok !== false) {
|
|
722
|
+
actions.setClean(r.summary ?? r);
|
|
723
|
+
lastCsv.clean = { csv: r.csv ?? '', name: r.downloadName ?? 'cleaned.csv' };
|
|
724
|
+
} else {
|
|
725
|
+
actions.setError((r && (r.message || r.error)) || '清洗失败');
|
|
726
|
+
}
|
|
727
|
+
} catch (err) {
|
|
728
|
+
actions.setError(err instanceof Error ? err.message : String(err));
|
|
729
|
+
} finally {
|
|
730
|
+
actions.setBusy(false);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
const runComplete = async () => {
|
|
735
|
+
if (busy || !session.rows.length) {
|
|
736
|
+
if (!session.rows.length) actions.setError('请先上传并解析数据。');
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
actions.setBusy(true);
|
|
740
|
+
actions.setError(null);
|
|
741
|
+
try {
|
|
742
|
+
const r = await api('/data-cleaning/api/mvp/complete', {
|
|
743
|
+
rows: session.rows,
|
|
744
|
+
headers: session.headers,
|
|
745
|
+
options: localCompleteOptions,
|
|
746
|
+
});
|
|
747
|
+
if (r && r.ok !== false) {
|
|
748
|
+
actions.setComplete(r.summary ?? r);
|
|
749
|
+
lastCsv.complete = { csv: r.csv ?? '', name: r.downloadName ?? 'completed.csv' };
|
|
750
|
+
} else {
|
|
751
|
+
actions.setError((r && (r.message || r.error)) || '补全失败');
|
|
752
|
+
}
|
|
753
|
+
} catch (err) {
|
|
754
|
+
actions.setError(err instanceof Error ? err.message : String(err));
|
|
755
|
+
} finally {
|
|
756
|
+
actions.setBusy(false);
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
const loadQccCapabilities = async () => {
|
|
761
|
+
if (busy) return;
|
|
762
|
+
actions.setBusy(true);
|
|
763
|
+
actions.setError(null);
|
|
764
|
+
try {
|
|
765
|
+
const r = await api('/data-cleaning/api/phase3/capabilities');
|
|
766
|
+
if (r && r.ok !== false) actions.setQccCapabilities(r);
|
|
767
|
+
else actions.setError((r && (r.message || r.error)) || '企查查能力检测失败');
|
|
768
|
+
} catch (err) {
|
|
769
|
+
actions.setError(err instanceof Error ? err.message : String(err));
|
|
770
|
+
} finally {
|
|
771
|
+
actions.setBusy(false);
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
const estimateQcc = async () => {
|
|
776
|
+
if (busy || !selectedDomains.length) {
|
|
777
|
+
if (!selectedDomains.length) actions.setError('请至少选择一个补全域。');
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
actions.setBusy(true);
|
|
781
|
+
actions.setError(null);
|
|
782
|
+
try {
|
|
783
|
+
const r = await api('/data-cleaning/api/phase3/estimate', {
|
|
784
|
+
rows: session.rows, nameField, domains: selectedDomains, maxCalls: 500,
|
|
785
|
+
});
|
|
786
|
+
if (r && r.ok !== false) actions.setQccEstimate(r.estimate);
|
|
787
|
+
else actions.setError((r && (r.message || r.error)) || '调用估算失败');
|
|
788
|
+
} catch (err) {
|
|
789
|
+
actions.setError(err instanceof Error ? err.message : String(err));
|
|
790
|
+
} finally {
|
|
791
|
+
actions.setBusy(false);
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
const applyQccRun = (run) => {
|
|
796
|
+
actions.setQccRun(run);
|
|
797
|
+
session.rows = Array.isArray(run.rows) ? run.rows : session.rows;
|
|
798
|
+
lastCsv.qcc = { csv: run.csv ?? '', name: run.downloadName ?? 'qcc-phase3-enriched.csv' };
|
|
799
|
+
lastCsv.review = { csv: run.reviewCsv ?? '', name: run.reviewDownloadName ?? 'qcc-phase3-review.csv' };
|
|
800
|
+
if (!Array.isArray(run.reviewQueue) || run.reviewQueue.length === 0) actions.setStep('enrich');
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
const runQcc = async () => {
|
|
804
|
+
if (busy || !qccEstimate || !paidConfirmed || !qccEstimate.withinLimit) return;
|
|
805
|
+
actions.setBusy(true);
|
|
806
|
+
actions.setError(null);
|
|
807
|
+
try {
|
|
808
|
+
const key = `phase3-ui-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
809
|
+
const r = await api('/data-cleaning/api/phase3/enrich', {
|
|
810
|
+
rows: session.rows,
|
|
811
|
+
headers: session.headers,
|
|
812
|
+
nameField,
|
|
813
|
+
domains: selectedDomains,
|
|
814
|
+
maxCalls: qccEstimate.maxCalls,
|
|
815
|
+
concurrency: 2,
|
|
816
|
+
confirmPaidCalls: true,
|
|
817
|
+
idempotencyKey: key,
|
|
818
|
+
});
|
|
819
|
+
if (r && r.ok !== false) applyQccRun(r);
|
|
820
|
+
else actions.setError((r && (r.message || r.error)) || '三域补全失败');
|
|
821
|
+
} catch (err) {
|
|
822
|
+
actions.setError(err instanceof Error ? err.message : String(err));
|
|
823
|
+
} finally {
|
|
824
|
+
actions.setBusy(false);
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
const resolveCandidate = async (item, candidate) => {
|
|
829
|
+
if (busy || !paidConfirmed || !qccRun) return;
|
|
830
|
+
actions.setBusy(true);
|
|
831
|
+
actions.setError(null);
|
|
832
|
+
try {
|
|
833
|
+
const r = await api('/data-cleaning/api/phase3/resolve', {
|
|
834
|
+
runId: qccRun.runId,
|
|
835
|
+
companyName: item.companyName,
|
|
836
|
+
selectedCreditNo: candidate.creditNo,
|
|
837
|
+
confirmPaidCalls: true,
|
|
838
|
+
idempotencyKey: `phase3-resolve-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
839
|
+
});
|
|
840
|
+
if (r && r.ok !== false) applyQccRun(r);
|
|
841
|
+
else actions.setError((r && (r.message || r.error)) || '候选确认失败');
|
|
842
|
+
} catch (err) {
|
|
843
|
+
actions.setError(err instanceof Error ? err.message : String(err));
|
|
844
|
+
} finally {
|
|
845
|
+
actions.setBusy(false);
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
const retryFailures = async () => {
|
|
850
|
+
if (busy || !paidConfirmed || !qccRun) return;
|
|
851
|
+
const names = [...new Set((qccRun.errors || []).filter((item) => item.error && item.error.retryable).map((item) => item.companyName))];
|
|
852
|
+
if (!names.length) return;
|
|
853
|
+
actions.setBusy(true);
|
|
854
|
+
actions.setError(null);
|
|
855
|
+
try {
|
|
856
|
+
const r = await api('/data-cleaning/api/phase3/retry', {
|
|
857
|
+
runId: qccRun.runId,
|
|
858
|
+
companyNames: names,
|
|
859
|
+
confirmPaidCalls: true,
|
|
860
|
+
idempotencyKey: `phase3-retry-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
861
|
+
});
|
|
862
|
+
if (r && r.ok !== false) applyQccRun(r);
|
|
863
|
+
else actions.setError((r && (r.message || r.error)) || '失败项重试失败');
|
|
864
|
+
} catch (err) {
|
|
865
|
+
actions.setError(err instanceof Error ? err.message : String(err));
|
|
866
|
+
} finally {
|
|
867
|
+
actions.setBusy(false);
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
const download = (slot) => {
|
|
872
|
+
const item = lastCsv[slot];
|
|
873
|
+
if (!item || !item.csv) return;
|
|
874
|
+
const blob = new Blob([item.csv], { type: 'text/csv;charset=utf-8' });
|
|
875
|
+
const a = document.createElement('a');
|
|
876
|
+
a.href = URL.createObjectURL(blob);
|
|
877
|
+
a.download = item.name;
|
|
878
|
+
a.click();
|
|
879
|
+
URL.revokeObjectURL(a.href);
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
const stat = (label, value, tone) => h('div', { className: 'dcAgentCard' },
|
|
883
|
+
h('span', null, label),
|
|
884
|
+
h('b', { className: tone ? `is-${tone}` : null }, value)
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
let pane;
|
|
888
|
+
if (step === 'profile') {
|
|
889
|
+
pane = h('div', { className: 'dcAgentPane' },
|
|
890
|
+
h('p', { className: 'dcAgentHint' }, '基于已解析数据生成本地质量画像(缺失率、去重值、金额分布)。本步骤不发起企查查调用。'),
|
|
891
|
+
profile ? h('div', null,
|
|
892
|
+
h('div', { className: 'dcAgentGrid' },
|
|
893
|
+
stat('行数', profile.rowCount ?? '—'),
|
|
894
|
+
stat('列数', profile.columnCount ?? '—'),
|
|
895
|
+
profile.amountStats ? stat('金额 最小', profile.amountStats.min) : null,
|
|
896
|
+
profile.amountStats ? stat('金额 最大', profile.amountStats.max) : null,
|
|
897
|
+
profile.amountStats ? stat('金额 总和', profile.amountStats.sum) : null,
|
|
898
|
+
profile.amountStats ? stat('金额 均值', typeof profile.amountStats.mean === 'number' ? profile.amountStats.mean.toFixed(2) : profile.amountStats.mean) : null,
|
|
899
|
+
),
|
|
900
|
+
Array.isArray(profile.columns) && profile.columns.length ? h('table', { className: 'dcAgentTable' },
|
|
901
|
+
h('thead', null, h('tr', null,
|
|
902
|
+
h('th', null, '字段'),
|
|
903
|
+
h('th', null, '非空'),
|
|
904
|
+
h('th', null, '缺失'),
|
|
905
|
+
h('th', null, '去重值'),
|
|
906
|
+
)),
|
|
907
|
+
h('tbody', null, profile.columns.map((col) => h('tr', { key: col.name },
|
|
908
|
+
h('td', null, col.name),
|
|
909
|
+
h('td', { className: 'num' }, String(col.present)),
|
|
910
|
+
h('td', { className: 'num' }, String(col.missing)),
|
|
911
|
+
h('td', { className: 'num' }, String(col.distinct)),
|
|
912
|
+
))),
|
|
913
|
+
) : null,
|
|
914
|
+
h('div', { className: 'dcAgentRow' },
|
|
915
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy, onClick: () => actions.setStep('review') }, '下一步:匹配核验'),
|
|
916
|
+
),
|
|
917
|
+
) : h('div', { className: 'dcAgentRow' },
|
|
918
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !hasData, 'aria-label': '生成体检报告', onClick: runProfile }, busy ? '体检中…' : '生成体检报告'),
|
|
919
|
+
),
|
|
920
|
+
);
|
|
921
|
+
} else if (step === 'review') {
|
|
922
|
+
pane = h('div', { className: 'dcAgentPane' },
|
|
923
|
+
h('p', { className: 'dcAgentHint' }, '先执行本地确定性清洗,再按真实 ToolRuntime 状态进行主体匹配。多候选始终由人工选择;界面不生成虚构置信度。'),
|
|
924
|
+
h('section', { className: 'dcAgentSection' },
|
|
925
|
+
h('h3', null, '本地清洗预处理'),
|
|
926
|
+
h('div', { className: 'dcAgentRow' },
|
|
927
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy, 'aria-label': '执行清洗', onClick: runClean }, busy ? '处理中…' : '执行清洗'),
|
|
928
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy, 'aria-label': '执行补全', onClick: runComplete }, busy ? '处理中…' : '本地规则补全'),
|
|
929
|
+
),
|
|
930
|
+
clean ? h('div', { className: 'dcAgentGrid' },
|
|
931
|
+
stat('总数', clean.total), stat('保留', clean.kept, 'good'), stat('剔除', clean.dropped, 'bad'),
|
|
932
|
+
stat('缺失关键字段', clean.badMissing, clean.badMissing > 0 ? 'bad' : null),
|
|
933
|
+
stat('非法金额', clean.badAmount, clean.badAmount > 0 ? 'warn' : null),
|
|
934
|
+
stat('重复', clean.badDuplicate, clean.badDuplicate > 0 ? 'warn' : null),
|
|
935
|
+
) : null,
|
|
936
|
+
),
|
|
937
|
+
h('section', { className: 'dcAgentSection' },
|
|
938
|
+
h('h3', null, '企查查三域能力与调用范围'),
|
|
939
|
+
h('div', { className: 'dcAgentRow' },
|
|
940
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy, onClick: loadQccCapabilities }, busy ? '检测中…' : '检测企查查连接'),
|
|
941
|
+
qccCapabilities ? h('span', { className: 'dcAgentHint' }, qccCapabilities.capabilities?.ready ? '91 个三域工具已就绪' : `${qccCapabilities.capabilities?.totalRegistered ?? 0}/${qccCapabilities.capabilities?.total ?? 91} 工具可用`) : null,
|
|
942
|
+
),
|
|
943
|
+
h('div', { className: 'dcAgentChips' },
|
|
944
|
+
[['risk', '风险信息 · 38'], ['ipr', '知识产权 · 18'], ['operation', '经营信息 · 35']].map(([domain, label]) => h('label', {
|
|
945
|
+
key: domain,
|
|
946
|
+
className: `dcAgentChip${selectedDomains.includes(domain) ? ' is-selected' : ''}`,
|
|
947
|
+
},
|
|
948
|
+
h('input', { type: 'checkbox', checked: selectedDomains.includes(domain), 'aria-label': label, onChange: () => actions.toggleDomain(domain) }),
|
|
949
|
+
` ${label}`,
|
|
950
|
+
)),
|
|
951
|
+
),
|
|
952
|
+
h('div', { className: 'dcAgentRow' },
|
|
953
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !selectedDomains.length, onClick: estimateQcc }, '估算调用量'),
|
|
954
|
+
),
|
|
955
|
+
qccEstimate ? h('div', null,
|
|
956
|
+
h('div', { className: 'dcAgentGrid' },
|
|
957
|
+
stat('唯一企业', qccEstimate.uniqueCompanies),
|
|
958
|
+
stat('所选工具', qccEstimate.tools.length),
|
|
959
|
+
stat('调用上界', qccEstimate.estimatedCalls, qccEstimate.withinLimit ? 'good' : 'bad'),
|
|
960
|
+
stat('调用上限', qccEstimate.maxCalls),
|
|
961
|
+
),
|
|
962
|
+
h('label', { className: 'dcAgentCheck' },
|
|
963
|
+
h('input', { type: 'checkbox', checked: paidConfirmed, 'aria-label': '确认使用当前用户的企查查账号额度', onChange: (event) => actions.setPaidConfirmed(event.target.checked) }),
|
|
964
|
+
'我已核对企业数量、所选域及调用上界,并确认使用自己连接的企查查 MCP 账号;额度或费用由该账号自行承担',
|
|
965
|
+
),
|
|
966
|
+
h('div', { className: 'dcAgentRow' },
|
|
967
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy || !paidConfirmed || !qccEstimate.withinLimit, onClick: runQcc }, busy ? '执行中…' : '开始匹配与补全'),
|
|
968
|
+
),
|
|
969
|
+
) : null,
|
|
970
|
+
),
|
|
971
|
+
qccRun ? h('section', { className: 'dcAgentSection' },
|
|
972
|
+
h('h3', null, `任务 ${qccRun.runId} · ${qccRun.state}`),
|
|
973
|
+
h('div', { className: 'dcAgentGrid' },
|
|
974
|
+
stat('已补全', qccRun.summary?.enriched ?? 0, 'good'),
|
|
975
|
+
stat('部分成功', qccRun.summary?.partial ?? 0, (qccRun.summary?.partial ?? 0) > 0 ? 'warn' : null),
|
|
976
|
+
stat('待核验', qccRun.summary?.ambiguous ?? 0, (qccRun.summary?.ambiguous ?? 0) > 0 ? 'warn' : null),
|
|
977
|
+
stat('失败', qccRun.summary?.failed ?? 0, (qccRun.summary?.failed ?? 0) > 0 ? 'bad' : null),
|
|
978
|
+
stat('实际调用', qccRun.summary?.actualCalls ?? 0),
|
|
979
|
+
),
|
|
980
|
+
(qccRun.reviewQueue || []).map((item) => h('div', { key: item.companyName, className: 'dcAgentSection' },
|
|
981
|
+
h('h3', null, `待核验:${item.companyName}`),
|
|
982
|
+
item.candidates.map((candidate) => h('div', { key: candidate.creditNo, className: 'dcAgentCandidate' },
|
|
983
|
+
h('div', null, h('b', null, candidate.companyName || '未命名候选'), h('small', null, candidate.creditNo)),
|
|
984
|
+
h('small', null, `${candidate.status || '状态未知'} · ${(candidate.legalRep || []).join('、') || '法人未知'}`),
|
|
985
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !paidConfirmed, onClick: () => resolveCandidate(item, candidate) }, '确认此主体'),
|
|
986
|
+
)),
|
|
987
|
+
)),
|
|
988
|
+
(qccRun.errors || []).some((item) => item.error?.retryable) ? h('button', { type: 'button', className: 'dcAgentButton', disabled: busy || !paidConfirmed, onClick: retryFailures }, '重试可恢复失败项') : null,
|
|
989
|
+
h('div', { className: 'dcAgentRow' },
|
|
990
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', onClick: () => actions.setStep('enrich') }, '进入补全与导出'),
|
|
991
|
+
),
|
|
992
|
+
) : h('div', { className: 'dcAgentRow' },
|
|
993
|
+
h('button', { type: 'button', className: 'dcAgentButton', onClick: () => actions.setStep('enrich') }, '仅使用本地结果并导出'),
|
|
994
|
+
),
|
|
995
|
+
);
|
|
996
|
+
} else if (step === 'enrich') {
|
|
997
|
+
pane = h('div', { className: 'dcAgentPane' },
|
|
998
|
+
h('p', { className: 'dcAgentHint' }, '导出本地清洗结果、企查查三域补全结果和待核验清单。每个三域值均保留 sourceTool 与上游原值。'),
|
|
999
|
+
h('div', { className: 'dcAgentGrid' },
|
|
1000
|
+
stat('输入行数', dataset ? dataset.rowCount : '—'),
|
|
1001
|
+
stat('清洗保留', clean ? clean.kept : '—', clean && clean.kept > 0 ? 'good' : null),
|
|
1002
|
+
stat('本地补全', complete ? complete.completed : '—', complete && complete.completed > 0 ? 'good' : null),
|
|
1003
|
+
stat('QCC 已补全', qccRun ? qccRun.summary?.enriched ?? 0 : '—', qccRun && qccRun.summary?.enriched > 0 ? 'good' : null),
|
|
1004
|
+
stat('待核验', qccRun ? qccRun.summary?.ambiguous ?? 0 : '—', qccRun && qccRun.summary?.ambiguous > 0 ? 'warn' : null),
|
|
1005
|
+
),
|
|
1006
|
+
h('div', { className: 'dcAgentRow' },
|
|
1007
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: !lastCsv.clean, 'aria-label': '下载清洗结果', onClick: () => download('clean') }, '下载清洗结果 CSV'),
|
|
1008
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: !lastCsv.complete, 'aria-label': '下载补全结果', onClick: () => download('complete') }, '下载补全结果 CSV'),
|
|
1009
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: !lastCsv.qcc, 'aria-label': '下载 QCC 补全结果', onClick: () => download('qcc') }, '下载 QCC 补全结果 CSV'),
|
|
1010
|
+
h('button', { type: 'button', className: 'dcAgentButton', disabled: !lastCsv.review, 'aria-label': '下载待核验清单', onClick: () => download('review') }, '下载待核验清单 CSV'),
|
|
1011
|
+
),
|
|
1012
|
+
);
|
|
1013
|
+
} else {
|
|
1014
|
+
pane = h('div', { className: 'dcAgentPane' },
|
|
1015
|
+
h('p', { className: 'dcAgentHint' }, '上传 CSV / XLSX / XLS / JSON,或粘贴数据。原始明细仅在本机 Host 与同源工作台处理,不进入模型上下文。'),
|
|
1016
|
+
h('input', {
|
|
1017
|
+
type: 'file',
|
|
1018
|
+
accept: '.csv,.json,.xlsx,.xls',
|
|
1019
|
+
className: 'dcAgentField',
|
|
1020
|
+
'aria-label': '选择数据文件',
|
|
1021
|
+
onChange: handleFile,
|
|
1022
|
+
}),
|
|
1023
|
+
h('textarea', {
|
|
1024
|
+
className: 'dcAgentTextarea',
|
|
1025
|
+
placeholder: '粘贴 CSV 文本,或 JSON 数组(例如 [{"name":"某公司","amount":"100"}])…',
|
|
1026
|
+
'aria-label': '粘贴数据',
|
|
1027
|
+
value: input,
|
|
1028
|
+
onInput: (event) => actions.setInput(event.target.value),
|
|
1029
|
+
}),
|
|
1030
|
+
dataset ? h('div', { className: 'dcAgentChips' },
|
|
1031
|
+
h('span', { className: 'dcAgentChip' }, `格式 ${dataset.fmt}`),
|
|
1032
|
+
h('span', { className: 'dcAgentChip' }, `${dataset.rowCount} 行`),
|
|
1033
|
+
(dataset.headers || []).slice(0, 12).map((name) => h('span', { key: name, className: 'dcAgentChip' }, name)),
|
|
1034
|
+
(dataset.headers || []).length > 12 ? h('span', { className: 'dcAgentChip' }, `+${dataset.headers.length - 12} 列`) : null,
|
|
1035
|
+
) : null,
|
|
1036
|
+
dataset ? h('label', { className: 'dcAgentRow' },
|
|
1037
|
+
h('span', { className: 'dcAgentHint' }, '企业名称字段'),
|
|
1038
|
+
h('select', { className: 'dcAgentField', value: nameField, 'aria-label': '企业名称字段映射', onChange: (event) => actions.setNameField(event.target.value) },
|
|
1039
|
+
(dataset.headers || []).map((name) => h('option', { key: name, value: name }, name)),
|
|
1040
|
+
),
|
|
1041
|
+
) : null,
|
|
1042
|
+
h('div', { className: 'dcAgentRow' },
|
|
1043
|
+
h('button', { type: 'button', className: 'dcAgentButton is-primary', disabled: busy, 'aria-label': '解析数据', onClick: handleParse }, busy ? '解析中…' : '解析数据'),
|
|
1044
|
+
hasData ? h('button', { type: 'button', className: 'dcAgentButton', onClick: () => actions.setStep('profile') }, '继续到数据体检') : null,
|
|
1045
|
+
),
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
return h('div', {
|
|
1050
|
+
className: 'dcAgentOverlay',
|
|
1051
|
+
role: 'dialog',
|
|
1052
|
+
'aria-modal': 'true',
|
|
1053
|
+
'aria-label': '数据清洗',
|
|
1054
|
+
onClick: (event) => {
|
|
1055
|
+
if (event.target === event.currentTarget) actions.close();
|
|
1056
|
+
},
|
|
1057
|
+
},
|
|
1058
|
+
h('div', { className: `dcAgentWorkbench${expanded ? ' is-expanded' : ''}` },
|
|
1059
|
+
h('header', { className: 'dcAgentWbHeader' },
|
|
1060
|
+
h('div', { className: 'dcAgentWbTitle' },
|
|
1061
|
+
h('span', { className: 'dcAgentWbIcon', 'aria-hidden': 'true' }, '🧹'),
|
|
1062
|
+
h('div', null,
|
|
1063
|
+
h('b', null, '数据清洗补全'),
|
|
1064
|
+
h('small', null, '本地确定性清洗 · 企查查三域 Host Bridge'),
|
|
1065
|
+
),
|
|
1066
|
+
),
|
|
1067
|
+
h('span', {
|
|
1068
|
+
className: 'dcAgentQccBadge',
|
|
1069
|
+
title: qccRun ? `任务 ${qccRun.runId}` : '仅在当前用户确认使用自己的企查查账号后调用',
|
|
1070
|
+
}, qccRun ? `QCC · ${qccRun.state}` : qccCapabilities ? (qccCapabilities.capabilities?.ready ? 'QCC · 已连接' : 'QCC · 能力不完整') : 'QCC · 待检测'),
|
|
1071
|
+
h('span', { className: 'dcAgentJobsPill', 'data-state': jobsPill(jobs).state, title: '后台任务状态' }, jobsPill(jobs).label),
|
|
1072
|
+
h('button', {
|
|
1073
|
+
className: 'dcAgentWbClose',
|
|
1074
|
+
type: 'button',
|
|
1075
|
+
'aria-label': expanded ? '收起工作台' : '展开工作台',
|
|
1076
|
+
onClick: () => actions.toggleExpanded(),
|
|
1077
|
+
}, expanded ? '↘' : '↖'),
|
|
1078
|
+
h('button', {
|
|
1079
|
+
className: 'dcAgentWbClose',
|
|
1080
|
+
type: 'button',
|
|
1081
|
+
'aria-label': '关闭',
|
|
1082
|
+
onClick: () => {
|
|
1083
|
+
stopJobsPolling();
|
|
1084
|
+
actions.close();
|
|
1085
|
+
},
|
|
1086
|
+
}, '✕'),
|
|
1087
|
+
),
|
|
1088
|
+
h('nav', { className: 'dcAgentStepper', 'aria-label': '清洗流程' },
|
|
1089
|
+
STEPS.map((st) => h('button', {
|
|
1090
|
+
key: st.key,
|
|
1091
|
+
type: 'button',
|
|
1092
|
+
className: `dcAgentStep${step === st.key ? ' is-active' : ''}`,
|
|
1093
|
+
'aria-label': st.label,
|
|
1094
|
+
'aria-current': step === st.key ? 'step' : undefined,
|
|
1095
|
+
onClick: () => actions.setStep(st.key),
|
|
1096
|
+
}, `${st.icon} ${st.label}`)),
|
|
1097
|
+
),
|
|
1098
|
+
h('div', { className: 'dcAgentWbBody' },
|
|
1099
|
+
error ? h('div', { className: 'dcAgentError', role: 'alert' }, error) : null,
|
|
1100
|
+
pane,
|
|
1101
|
+
),
|
|
1102
|
+
),
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
15
1105
|
|
|
16
1106
|
function apply(ctx) {
|
|
17
1107
|
// eslint-disable-next-line no-console
|
|
18
1108
|
console.log('[dc-agent] client apply() ran');
|
|
19
|
-
const state = { applied: true,
|
|
1109
|
+
const state = { applied: true, entry: 'sidebar.footer.action', overlay: 'shell.overlay', error: null };
|
|
20
1110
|
window.__DC_MVP__ = state;
|
|
1111
|
+
try {
|
|
1112
|
+
const workbenchStore = createWorkbenchStore();
|
|
1113
|
+
ctx.effect(() => installSidebarStyles(), 'data-cleaning-agent: sidebar styles');
|
|
1114
|
+
|
|
1115
|
+
// 工作台:注册到 shell.overlay(与 mcp-connector 同源)。
|
|
1116
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
1117
|
+
name: 'shell.overlay',
|
|
1118
|
+
id: 'data-cleaning-agent',
|
|
1119
|
+
order: 200,
|
|
1120
|
+
store: workbenchStore,
|
|
1121
|
+
}, WorkbenchOverlay));
|
|
1122
|
+
|
|
1123
|
+
// 左栏:注册到 sidebar.footer.action,order 10 → 排在 MCP连接器(order 0)下方。
|
|
1124
|
+
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
|
|
1125
|
+
name: 'sidebar.footer.action',
|
|
1126
|
+
id: 'data-cleaning-agent',
|
|
1127
|
+
order: 10,
|
|
1128
|
+
store: workbenchStore,
|
|
1129
|
+
}, SidebarEntry));
|
|
1130
|
+
|
|
1131
|
+
// M3:三个工具的 tool.call.toolview 富化卡片(keyed by wire name,替代裸 JSON 摘要)。
|
|
1132
|
+
// 说明:分三条独立 inject(而非 generator)——测试 shim 对 inject 回调仅执行一次并 push 其返回值。
|
|
1133
|
+
const toolviewKeys = ['data_clean_rows', 'data_complete_rows', 'data_profile'];
|
|
1134
|
+
for (const key of toolviewKeys) {
|
|
1135
|
+
ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
|
|
1136
|
+
name: 'tool.call.toolview',
|
|
1137
|
+
key,
|
|
1138
|
+
locale: 'conversation',
|
|
1139
|
+
}, DataToolCard));
|
|
1140
|
+
}
|
|
21
1141
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.catch((error) => {
|
|
29
|
-
state.error = error instanceof Error ? error.message : String(error);
|
|
30
|
-
console.warn('[dc-agent] client seam fetch failed:', state.error);
|
|
31
|
-
});
|
|
1142
|
+
console.log('[dc-agent] client apply() completed');
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
state.applied = false;
|
|
1145
|
+
state.error = error instanceof Error ? error.message : String(error);
|
|
1146
|
+
console.error('[dc-agent] client apply() failed:', error);
|
|
1147
|
+
}
|
|
32
1148
|
}
|
|
33
1149
|
|
|
34
1150
|
exports.apply = apply;
|