dsh-data-cleaning-agent 0.9.13 → 0.9.15
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 +15 -0
- package/README.en.md +2 -2
- package/README.md +3 -3
- package/docs/RELEASE-0.9.14.md +12 -0
- package/docs/RELEASE-0.9.15.md +13 -0
- package/lib/client.js +1006 -202
- package/lib/image-workflow.js +7 -1
- package/lib/qcc-command.js +8 -0
- package/lib/qcc-runs.js +9 -0
- package/lib/web.js +82 -42
- package/lib/workflow-contract.js +125 -7
- package/lib/workflow-execution.js +121 -20
- package/lib/workflow.js +170 -14
- package/package.json +3 -1
package/lib/workflow.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
normalizeFieldSelection,
|
|
14
14
|
normalizeWorkflowDraft,
|
|
15
15
|
normalizeWorkflowOrigin,
|
|
16
|
+
requiresQcc,
|
|
16
17
|
validateMappings,
|
|
17
18
|
} from './workflow-contract.js';
|
|
18
19
|
|
|
@@ -106,6 +107,30 @@ function sanitizeArtifact(value = {}, timestamp) {
|
|
|
106
107
|
};
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
function sanitizeArtifacts(input, timestamp) {
|
|
111
|
+
const incoming = Array.isArray(input.artifacts) ? input.artifacts : [input.artifact];
|
|
112
|
+
const artifacts = incoming.filter(Boolean).map((artifact) => sanitizeArtifact(artifact, timestamp));
|
|
113
|
+
if (!artifacts.length || artifacts.some((artifact) => !artifact.id)) {
|
|
114
|
+
throw new WorkflowError('DC_WORKFLOW_ARTIFACT_REQUIRED', '至少需要一个 Host 制品引用。', 400);
|
|
115
|
+
}
|
|
116
|
+
return artifacts;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function safeMatchSummary(value) {
|
|
120
|
+
const summary = safeSummary(value, ['total', 'exact', 'candidate', 'confirmed', 'unresolved', 'failed', 'reviewRequired']);
|
|
121
|
+
assertSummaryTotal(summary, ['exact', 'candidate', 'confirmed', 'unresolved', 'failed'], 'DC_WORKFLOW_MATCH_SUMMARY');
|
|
122
|
+
if (summary.reviewRequired > summary.candidate) {
|
|
123
|
+
throw new WorkflowError('DC_WORKFLOW_MATCH_SUMMARY', '待核验数量不能超过候选数量。', 400);
|
|
124
|
+
}
|
|
125
|
+
return summary;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function safeEnrichmentSummary(value) {
|
|
129
|
+
const summary = safeSummary(value, ['total', 'completed', 'unchanged', 'failed', 'reviewRequired', 'callsUsed']);
|
|
130
|
+
assertSummaryTotal(summary, ['completed', 'unchanged', 'failed', 'reviewRequired'], 'DC_WORKFLOW_ENRICH_SUMMARY');
|
|
131
|
+
return summary;
|
|
132
|
+
}
|
|
133
|
+
|
|
109
134
|
function nowIso() {
|
|
110
135
|
return new Date().toISOString();
|
|
111
136
|
}
|
|
@@ -271,7 +296,7 @@ export class DataCleaningWorkflowStore {
|
|
|
271
296
|
mappings,
|
|
272
297
|
matchRules: input.matchRules ?? record.matchRules,
|
|
273
298
|
});
|
|
274
|
-
return { ...draft, state: 'rules_confirmed', stage: '
|
|
299
|
+
return { ...draft, state: 'rules_confirmed', stage: 'rules', error: null };
|
|
275
300
|
});
|
|
276
301
|
}
|
|
277
302
|
|
|
@@ -283,7 +308,7 @@ export class DataCleaningWorkflowStore {
|
|
|
283
308
|
if (record.state !== 'diagnosed') contractCall(() => assertWorkflowTransition(record.state, 'diagnosed'));
|
|
284
309
|
return {
|
|
285
310
|
state: 'diagnosed',
|
|
286
|
-
stage: 'match',
|
|
311
|
+
stage: requiresQcc(record) ? 'match' : 'enrich',
|
|
287
312
|
error: null,
|
|
288
313
|
qualitySummary: safeSummary(input.summary, [
|
|
289
314
|
'total', 'valid', 'missingAnchor', 'duplicates', 'invalidCreditNo', 'invalidPhone', 'emptyFields',
|
|
@@ -304,11 +329,7 @@ export class DataCleaningWorkflowStore {
|
|
|
304
329
|
if (!['rules_confirmed', 'diagnosed', 'matching', 'review_required', 'partial'].includes(record.state)) {
|
|
305
330
|
throw new WorkflowError('DC_WORKFLOW_MATCH_STATE', 'Match summary cannot be recorded in the current state.', 409);
|
|
306
331
|
}
|
|
307
|
-
const summary =
|
|
308
|
-
assertSummaryTotal(summary, ['exact', 'candidate', 'confirmed', 'unresolved', 'failed'], 'DC_WORKFLOW_MATCH_SUMMARY');
|
|
309
|
-
if (summary.reviewRequired > summary.candidate) {
|
|
310
|
-
throw new WorkflowError('DC_WORKFLOW_MATCH_SUMMARY', 'Review-required count cannot exceed candidate count.', 400);
|
|
311
|
-
}
|
|
332
|
+
const summary = safeMatchSummary(input.summary);
|
|
312
333
|
const nextState = summary.reviewRequired > 0 ? 'review_required' : 'matched';
|
|
313
334
|
if (record.state !== nextState) contractCall(() => assertWorkflowTransition(record.state, nextState));
|
|
314
335
|
return {
|
|
@@ -343,9 +364,11 @@ export class DataCleaningWorkflowStore {
|
|
|
343
364
|
if (!['matched', 'enriching', 'partial', 'authorization_required'].includes(record.state)) {
|
|
344
365
|
throw new WorkflowError('DC_WORKFLOW_ENRICH_STATE', 'Enrichment summary cannot be recorded in the current state.', 409);
|
|
345
366
|
}
|
|
346
|
-
const summary =
|
|
347
|
-
assertSummaryTotal(summary, ['completed', 'unchanged', 'failed', 'reviewRequired'], 'DC_WORKFLOW_ENRICH_SUMMARY');
|
|
367
|
+
const summary = safeEnrichmentSummary(input.summary);
|
|
348
368
|
const nextState = summary.failed > 0 || summary.reviewRequired > 0 ? 'partial' : 'export_ready';
|
|
369
|
+
if (nextState === 'partial' && !record.artifacts.length) {
|
|
370
|
+
throw new WorkflowError('DC_WORKFLOW_PARTIAL_ARTIFACT_REQUIRED', '部分完成任务必须先生成可下载制品。', 409);
|
|
371
|
+
}
|
|
349
372
|
if (record.state !== nextState) contractCall(() => assertWorkflowTransition(record.state, nextState));
|
|
350
373
|
return {
|
|
351
374
|
state: nextState,
|
|
@@ -385,6 +408,143 @@ export class DataCleaningWorkflowStore {
|
|
|
385
408
|
});
|
|
386
409
|
}
|
|
387
410
|
|
|
411
|
+
async completeLocalDelivery(id, input = {}) {
|
|
412
|
+
return this.mutate(id, input.expectedRevision, (record) => {
|
|
413
|
+
if (record.state !== 'diagnosed' || requiresQcc(record)) {
|
|
414
|
+
throw new WorkflowError('DC_WORKFLOW_LOCAL_EXPORT_STATE', '只有已完成体检的本地任务可以生成结果。', 409);
|
|
415
|
+
}
|
|
416
|
+
contractCall(() => assertWorkflowTransition(record.state, 'completed'));
|
|
417
|
+
const total = integer(input.summary?.total ?? record.source?.rowCount);
|
|
418
|
+
const summary = {
|
|
419
|
+
total,
|
|
420
|
+
completed: integer(input.summary?.completed ?? total),
|
|
421
|
+
unchanged: integer(input.summary?.unchanged),
|
|
422
|
+
failed: integer(input.summary?.failed),
|
|
423
|
+
reviewRequired: 0,
|
|
424
|
+
callsUsed: 0,
|
|
425
|
+
};
|
|
426
|
+
assertSummaryTotal(summary, ['completed', 'unchanged', 'failed'], 'DC_WORKFLOW_ENRICH_SUMMARY');
|
|
427
|
+
const timestamp = this.nowFn();
|
|
428
|
+
const artifacts = sanitizeArtifacts(input, timestamp);
|
|
429
|
+
return {
|
|
430
|
+
state: 'completed',
|
|
431
|
+
stage: 'download',
|
|
432
|
+
enrichmentSummary: summary,
|
|
433
|
+
artifacts: [...record.artifacts, ...artifacts].slice(-20),
|
|
434
|
+
error: null,
|
|
435
|
+
completedAt: timestamp,
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async completeQccDelivery(id, input = {}) {
|
|
441
|
+
return this.mutate(id, input.expectedRevision, (record) => {
|
|
442
|
+
if (!['matching', 'review_required'].includes(record.state)) {
|
|
443
|
+
throw new WorkflowError('DC_WORKFLOW_DELIVERY_STATE', '当前任务不能提交企查查结果。', 409);
|
|
444
|
+
}
|
|
445
|
+
const matchSummary = safeMatchSummary(input.matchSummary);
|
|
446
|
+
if (matchSummary.reviewRequired > 0) {
|
|
447
|
+
throw new WorkflowError('DC_WORKFLOW_REVIEW_REQUIRED', '请先完成企业主体候选核验。', 409);
|
|
448
|
+
}
|
|
449
|
+
const enrichmentSummary = safeEnrichmentSummary(input.enrichmentSummary);
|
|
450
|
+
const nextState = enrichmentSummary.failed > 0 || enrichmentSummary.reviewRequired > 0 ? 'partial' : 'completed';
|
|
451
|
+
contractCall(() => assertWorkflowTransition(record.state, nextState));
|
|
452
|
+
const timestamp = this.nowFn();
|
|
453
|
+
const artifacts = sanitizeArtifacts(input, timestamp);
|
|
454
|
+
return {
|
|
455
|
+
state: nextState,
|
|
456
|
+
stage: 'download',
|
|
457
|
+
matchSummary,
|
|
458
|
+
enrichmentSummary,
|
|
459
|
+
qccRunId: safeText(input.qccRunId, 160) || record.qccRunId,
|
|
460
|
+
artifacts: [...record.artifacts, ...artifacts].slice(-20),
|
|
461
|
+
error: null,
|
|
462
|
+
completedAt: timestamp,
|
|
463
|
+
};
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async recordRetryReview(id, input = {}) {
|
|
468
|
+
return this.mutate(id, input.expectedRevision, (record) => {
|
|
469
|
+
if (record.state !== 'partial') {
|
|
470
|
+
throw new WorkflowError('DC_WORKFLOW_RETRY_STATE', '只有部分完成任务可以进入重试候选核验。', 409);
|
|
471
|
+
}
|
|
472
|
+
const summary = safeMatchSummary(input.summary);
|
|
473
|
+
if (summary.reviewRequired === 0) {
|
|
474
|
+
throw new WorkflowError('DC_WORKFLOW_REVIEW_REQUIRED', '重试结果没有待核验企业主体。', 400);
|
|
475
|
+
}
|
|
476
|
+
contractCall(() => assertWorkflowTransition(record.state, 'review_required'));
|
|
477
|
+
return {
|
|
478
|
+
state: 'review_required',
|
|
479
|
+
stage: 'match',
|
|
480
|
+
matchSummary: summary,
|
|
481
|
+
qccRunId: safeText(input.qccRunId, 160) || record.qccRunId,
|
|
482
|
+
error: null,
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async completeRetryDelivery(id, input = {}) {
|
|
488
|
+
return this.mutate(id, input.expectedRevision, (record) => {
|
|
489
|
+
const recoverableFailure = record.state === 'failed'
|
|
490
|
+
&& record.error?.code === 'DC_DELIVERY_FAILED'
|
|
491
|
+
&& record.qccRunId;
|
|
492
|
+
if (record.state !== 'partial' && record.state !== 'export_ready' && !recoverableFailure) {
|
|
493
|
+
throw new WorkflowError('DC_WORKFLOW_RETRY_STATE', '当前任务不能重试结果交付。', 409);
|
|
494
|
+
}
|
|
495
|
+
const matchSummary = safeMatchSummary(input.matchSummary ?? record.matchSummary);
|
|
496
|
+
if (matchSummary.reviewRequired > 0) {
|
|
497
|
+
throw new WorkflowError('DC_WORKFLOW_REVIEW_REQUIRED', '请先完成企业主体候选核验。', 409);
|
|
498
|
+
}
|
|
499
|
+
const enrichmentSummary = safeEnrichmentSummary(input.enrichmentSummary ?? record.enrichmentSummary);
|
|
500
|
+
const nextState = enrichmentSummary.failed > 0 || enrichmentSummary.reviewRequired > 0 ? 'partial' : 'completed';
|
|
501
|
+
if (record.state !== nextState) contractCall(() => assertWorkflowTransition(record.state, nextState));
|
|
502
|
+
const timestamp = this.nowFn();
|
|
503
|
+
const artifacts = sanitizeArtifacts(input, timestamp);
|
|
504
|
+
return {
|
|
505
|
+
state: nextState,
|
|
506
|
+
stage: 'download',
|
|
507
|
+
matchSummary,
|
|
508
|
+
enrichmentSummary,
|
|
509
|
+
qccRunId: safeText(input.qccRunId, 160) || record.qccRunId,
|
|
510
|
+
artifacts: [...record.artifacts, ...artifacts].slice(-20),
|
|
511
|
+
error: null,
|
|
512
|
+
completedAt: timestamp,
|
|
513
|
+
};
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async recordRetryFailure(id, input = {}) {
|
|
518
|
+
return this.mutate(id, input.expectedRevision, (record) => {
|
|
519
|
+
if (record.state !== 'partial') {
|
|
520
|
+
throw new WorkflowError('DC_WORKFLOW_RETRY_STATE', '只有部分完成任务可以记录重试错误。', 409);
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
state: 'partial',
|
|
524
|
+
stage: 'download',
|
|
525
|
+
error: { code: safeText(input.code, 80) || 'DC_RETRY_FAILED' },
|
|
526
|
+
};
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async recordDeliveryFailure(id, input = {}) {
|
|
531
|
+
return this.mutate(id, input.expectedRevision, (record) => {
|
|
532
|
+
if (!['matching', 'review_required', 'export_ready'].includes(record.state)) {
|
|
533
|
+
throw new WorkflowError('DC_WORKFLOW_DELIVERY_STATE', '当前任务不能记录结果交付失败。', 409);
|
|
534
|
+
}
|
|
535
|
+
const nextState = record.artifacts.length ? 'partial' : 'failed';
|
|
536
|
+
if (record.state !== nextState) contractCall(() => assertWorkflowTransition(record.state, nextState));
|
|
537
|
+
return {
|
|
538
|
+
state: nextState,
|
|
539
|
+
stage: 'download',
|
|
540
|
+
matchSummary: input.matchSummary ? safeMatchSummary(input.matchSummary) : record.matchSummary,
|
|
541
|
+
enrichmentSummary: input.enrichmentSummary ? safeEnrichmentSummary(input.enrichmentSummary) : record.enrichmentSummary,
|
|
542
|
+
qccRunId: safeText(input.qccRunId, 160) || record.qccRunId,
|
|
543
|
+
error: { code: 'DC_DELIVERY_FAILED' },
|
|
544
|
+
};
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
388
548
|
async recordExport(id, input = {}) {
|
|
389
549
|
return this.mutate(id, input.expectedRevision, (record) => {
|
|
390
550
|
if (!['export_ready', 'partial'].includes(record.state)) {
|
|
@@ -392,11 +552,7 @@ export class DataCleaningWorkflowStore {
|
|
|
392
552
|
}
|
|
393
553
|
contractCall(() => assertWorkflowTransition(record.state, 'completed'));
|
|
394
554
|
const timestamp = this.nowFn();
|
|
395
|
-
const
|
|
396
|
-
const artifacts = incoming.filter(Boolean).map((artifact) => sanitizeArtifact(artifact, timestamp));
|
|
397
|
-
if (!artifacts.length || artifacts.some((artifact) => !artifact.id)) {
|
|
398
|
-
throw new WorkflowError('DC_WORKFLOW_ARTIFACT_REQUIRED', 'At least one Host artifact reference is required.', 400);
|
|
399
|
-
}
|
|
555
|
+
const artifacts = sanitizeArtifacts(input, timestamp);
|
|
400
556
|
return {
|
|
401
557
|
state: input.preservePartial === true && record.state === 'partial' ? 'partial' : 'completed',
|
|
402
558
|
stage: 'download',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-data-cleaning-agent",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.15",
|
|
4
4
|
"description": "Data cleaning and data enrichment for CSV/XLSX/JSON enterprise lists in DeepSeek Harness, including spreadsheet cleaning, deduplication, profiling, optional Qichacha MCP and exports.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
"docs/USER-GUIDE.md",
|
|
23
23
|
"docs/RELEASE-0.9.11.md",
|
|
24
24
|
"docs/RELEASE-0.9.13.md",
|
|
25
|
+
"docs/RELEASE-0.9.14.md",
|
|
26
|
+
"docs/RELEASE-0.9.15.md",
|
|
25
27
|
"docs/UNIFIED-HOME-1.5.4-ADOPTION.md",
|
|
26
28
|
"docs/FIRST-CONTRIBUTION.md",
|
|
27
29
|
"docs/COMPATIBILITY.md",
|