dsh-data-cleaning-agent 0.9.6 → 0.9.8
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 +14 -0
- package/README.en.md +4 -2
- package/README.md +5 -3
- package/docs/RELEASE-0.9.8.md +68 -0
- package/lib/client.js +765 -174
- package/lib/image-workflow.js +1 -0
- package/lib/qcc-safety.js +1 -0
- package/lib/qcc.js +18 -2
- package/lib/web.js +4 -0
- package/lib/workflow-contract.js +13 -1
- package/lib/workflow-execution.js +1 -1
- package/lib/workflow.js +13 -0
- package/package.json +2 -1
package/lib/image-workflow.js
CHANGED
|
@@ -23,6 +23,7 @@ export function createImageWorkflow({ getWorkflow, execution, commands }) {
|
|
|
23
23
|
summary: { total: rows.length, valid: rows.length, missingAnchor: 0,
|
|
24
24
|
duplicates: rows.length - new Set(result.entries).size } });
|
|
25
25
|
const payload = await execution.prepare({ taskId: task.id, expectedRevision: task.revision,
|
|
26
|
+
originSessionId: task.originSessionId, originWorkspaceId: task.originWorkspaceId,
|
|
26
27
|
kind: 'enrich', rows, headers: ['主体标识'], nameField: '主体标识' });
|
|
27
28
|
const command = commands.prepare(payload, { artifactOrigin: record.workflow.artifactOrigin });
|
|
28
29
|
record.qccCommand = { commandId: command.commandId, taskId: task.id };
|
package/lib/qcc-safety.js
CHANGED
|
@@ -78,6 +78,7 @@ export function safeAuditEvent(event) {
|
|
|
78
78
|
upstreamCode: source.upstreamCode ? String(source.upstreamCode) : null,
|
|
79
79
|
durationMs: Math.max(0, Number(source.durationMs ?? 0)),
|
|
80
80
|
};
|
|
81
|
+
if (['success-data', 'success-empty', 'not-required', 'no-permission', 'failed', 'unknown'].includes(source.providerState)) safe.providerState = source.providerState;
|
|
81
82
|
if (source.catalogVersion || Array.isArray(source.missing) || Array.isArray(source.unknown)) {
|
|
82
83
|
safe.catalogVersion = source.catalogVersion ? String(source.catalogVersion).slice(0, 64) : null;
|
|
83
84
|
safe.missing = safeCatalogLabels(source.missing);
|
package/lib/qcc.js
CHANGED
|
@@ -281,12 +281,26 @@ function normalizedFailure(result, toolName, state) {
|
|
|
281
281
|
|
|
282
282
|
function emitAudit(options, event) {
|
|
283
283
|
try {
|
|
284
|
-
options?.onAudit?.(safeAuditEvent(event
|
|
284
|
+
options?.onAudit?.(safeAuditEvent({ ...event, providerState: event.providerState ?? providerResultState({
|
|
285
|
+
error: event.outcome === 'failed' || event.outcome === 'not-dispatched' ? { code: event.code, upstreamCode: event.upstreamCode } : null,
|
|
286
|
+
}) }));
|
|
285
287
|
} catch {
|
|
286
288
|
// 审计 sink 失败不能改变计费工具调用的业务结果。
|
|
287
289
|
}
|
|
288
290
|
}
|
|
289
291
|
|
|
292
|
+
/** Provider outcome is separate from execution completion and business review. */
|
|
293
|
+
export function providerResultState({ data, error, required = true } = {}) {
|
|
294
|
+
if (!required) return 'not-required';
|
|
295
|
+
if (error) return ['401', '403', 'QCC_AUTH_REQUIRED'].includes(String(error.upstreamCode || error.code)) ? 'no-permission' : 'failed';
|
|
296
|
+
if (Array.isArray(data)) return data.length ? 'success-data' : 'success-empty';
|
|
297
|
+
if (!data || typeof data !== 'object' || !Object.keys(data).length) return 'unknown';
|
|
298
|
+
const lists = ['实际控制人信息', '受益所有人信息', 'items', 'records', '企业列表'].map(k => data[k]).filter(Array.isArray);
|
|
299
|
+
if (lists.length && lists.every(v => !v.length)) return data.has_more === true ? 'unknown' : 'success-empty';
|
|
300
|
+
// No fabricated zero count for a missing/null/unrecognised transport payload.
|
|
301
|
+
return 'success-data';
|
|
302
|
+
}
|
|
303
|
+
|
|
290
304
|
function candidateView(candidate) {
|
|
291
305
|
const item = isRecord(candidate) ? candidate : {};
|
|
292
306
|
return {
|
|
@@ -835,15 +849,17 @@ export class QccHostBridge {
|
|
|
835
849
|
} : {}),
|
|
836
850
|
});
|
|
837
851
|
if (result?.isError !== true) {
|
|
852
|
+
const data = decodeQccToolValue(result?.value);
|
|
838
853
|
emitAudit(options, {
|
|
839
854
|
toolName: activeToolName,
|
|
840
855
|
callId,
|
|
841
856
|
attempt,
|
|
842
857
|
outcome: 'success',
|
|
858
|
+
providerState: providerResultState({ data }),
|
|
843
859
|
durationMs: Date.now() - attemptStarted,
|
|
844
860
|
});
|
|
845
861
|
attemptAudited = true;
|
|
846
|
-
return { callId, toolName: activeToolName, data
|
|
862
|
+
return { callId, toolName: activeToolName, data };
|
|
847
863
|
}
|
|
848
864
|
const upstreamCode = upstreamFailureCode(result);
|
|
849
865
|
if (upstreamCode === 'UNKNOWN_TOOL' && attempt === 1 && !state.signal.aborted) {
|
package/lib/web.js
CHANGED
|
@@ -424,6 +424,7 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
424
424
|
const payload = JSON.parse((await readBody(req)).toString('utf8'));
|
|
425
425
|
const record = imageIntake.require(decodeURIComponent(rest[0]));
|
|
426
426
|
const task = await (await getWorkflow()).require(payload.taskId);
|
|
427
|
+
await (await getWorkflow()).assertRequestOrigin(task.id, payload);
|
|
427
428
|
if (payload.executeOnSend !== true || record.state !== 'prepared' || task.state !== 'draft'
|
|
428
429
|
|| task.revision !== payload.expectedRevision || task.source?.type !== 'image'
|
|
429
430
|
|| task.source.fileName !== record.fileName || !task.fieldSelection.length) {
|
|
@@ -516,6 +517,7 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
516
517
|
if (req.method === 'PATCH') {
|
|
517
518
|
const body = await readBody(req);
|
|
518
519
|
const payload = body.length ? JSON.parse(body.toString('utf8')) : {};
|
|
520
|
+
await workflow.assertRequestOrigin(taskId, payload);
|
|
519
521
|
return writeJson(res, 200, {
|
|
520
522
|
ok: true,
|
|
521
523
|
marker: 'data-cleaning-workflow-v2',
|
|
@@ -545,6 +547,7 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
545
547
|
if (rest.length === 2 && req.method === 'POST') {
|
|
546
548
|
const body = await readBody(req);
|
|
547
549
|
const payload = body.length ? JSON.parse(body.toString('utf8')) : {};
|
|
550
|
+
await workflow.assertRequestOrigin(taskId, payload);
|
|
548
551
|
if (Number(payload.expectedRevision) !== current.revision) {
|
|
549
552
|
throw new WorkflowError('DC_WORKFLOW_REVISION_CONFLICT', 'Workflow task was updated by another session.', 409);
|
|
550
553
|
}
|
|
@@ -612,6 +615,7 @@ export function mountWebRoutes(wctx, { logger, report, TOOL_NAME, SKILL_NAME })
|
|
|
612
615
|
const body = await readBody(req);
|
|
613
616
|
const payload = body.length ? JSON.parse(body.toString('utf8')) : {};
|
|
614
617
|
const action = rest[2];
|
|
618
|
+
await workflow.assertRequestOrigin(taskId, payload);
|
|
615
619
|
const actions = {
|
|
616
620
|
upload: () => workflow.recordUpload(taskId, payload),
|
|
617
621
|
rules: () => workflow.confirmRules(taskId, payload),
|
package/lib/workflow-contract.js
CHANGED
|
@@ -215,5 +215,17 @@ export function assertWorkflowRecordShape(record) {
|
|
|
215
215
|
if (!STAGE_IDS.has(record.stage) || !STATE_IDS.has(record.state)) {
|
|
216
216
|
throw new WorkflowContractError('DC_WORKFLOW_SCHEMA', 'Workflow record contains an invalid stage or state.');
|
|
217
217
|
}
|
|
218
|
-
|
|
218
|
+
// Additive v2 migration: never infer ownership from whichever Session reads history.
|
|
219
|
+
return { ...record, ...normalizeWorkflowOrigin(record) };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function normalizeWorkflowOrigin(value = {}) {
|
|
223
|
+
const id = value => typeof value === 'string' && /^[\w:.-]{1,200}$/.test(value) ? value : null;
|
|
224
|
+
const label = value => typeof value === 'string' ? value.replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, 120) || null : null;
|
|
225
|
+
return {
|
|
226
|
+
originSessionId: id(value.originSessionId),
|
|
227
|
+
originWorkspaceId: id(value.originWorkspaceId),
|
|
228
|
+
originSessionName: label(value.originSessionName),
|
|
229
|
+
originWorkspaceName: label(value.originWorkspaceName),
|
|
230
|
+
};
|
|
219
231
|
}
|
|
@@ -10,7 +10,7 @@ export function createWorkflowExecution({ getWorkflow, artifacts }) {
|
|
|
10
10
|
return {
|
|
11
11
|
async prepare(payload) {
|
|
12
12
|
const store = await requireStore();
|
|
13
|
-
const task = await store.
|
|
13
|
+
const task = await store.assertRequestOrigin(payload.taskId, payload);
|
|
14
14
|
if (!['rules_confirmed', 'diagnosed', 'review_required', 'partial'].includes(task.state)) {
|
|
15
15
|
throw new WorkflowError('DC_EXECUTION_STATE', '请先在工作台确认规则;进行中或已完成任务不能重复启动。', 409);
|
|
16
16
|
}
|
package/lib/workflow.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
assertWorkflowTransition,
|
|
13
13
|
normalizeFieldSelection,
|
|
14
14
|
normalizeWorkflowDraft,
|
|
15
|
+
normalizeWorkflowOrigin,
|
|
15
16
|
validateMappings,
|
|
16
17
|
} from './workflow-contract.js';
|
|
17
18
|
|
|
@@ -140,6 +141,7 @@ export class DataCleaningWorkflowStore {
|
|
|
140
141
|
const id = this.idFactory();
|
|
141
142
|
const record = {
|
|
142
143
|
id,
|
|
144
|
+
...normalizeWorkflowOrigin(input),
|
|
143
145
|
schemaVersion: WORKFLOW_SCHEMA_VERSION,
|
|
144
146
|
revision: 1,
|
|
145
147
|
title: draft.title,
|
|
@@ -180,6 +182,16 @@ export class DataCleaningWorkflowStore {
|
|
|
180
182
|
return record;
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
async assertRequestOrigin(id, input = {}) {
|
|
186
|
+
const task = await this.require(id);
|
|
187
|
+
// Origin-less legacy/headless records remain readable; UI never adopts them.
|
|
188
|
+
if (task.originSessionId && (input.originSessionId !== task.originSessionId
|
|
189
|
+
|| (task.originWorkspaceId && input.originWorkspaceId !== task.originWorkspaceId))) {
|
|
190
|
+
throw new WorkflowError('DC_WORKFLOW_SCOPE', '任务不属于此来源会话;历史任务仅可只读查看。', 409);
|
|
191
|
+
}
|
|
192
|
+
return task;
|
|
193
|
+
}
|
|
194
|
+
|
|
183
195
|
async mutate(id, expectedRevision, updater) {
|
|
184
196
|
const key = String(id);
|
|
185
197
|
const current = await this.require(key);
|
|
@@ -197,6 +209,7 @@ export class DataCleaningWorkflowStore {
|
|
|
197
209
|
...latest,
|
|
198
210
|
...patch,
|
|
199
211
|
id: latest.id,
|
|
212
|
+
...normalizeWorkflowOrigin(latest),
|
|
200
213
|
schemaVersion: WORKFLOW_SCHEMA_VERSION,
|
|
201
214
|
revision: latest.revision + 1,
|
|
202
215
|
updatedAt: this.nowFn(),
|
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.8",
|
|
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",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"install.sh",
|
|
21
21
|
"marketing",
|
|
22
22
|
"docs/USER-GUIDE.md",
|
|
23
|
+
"docs/RELEASE-0.9.8.md",
|
|
23
24
|
"docs/FIRST-CONTRIBUTION.md",
|
|
24
25
|
"docs/COMPATIBILITY.md",
|
|
25
26
|
"docs/UI-V1.5.0-ADOPTION.md",
|