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.
@@ -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: decodeQccToolValue(result?.value) };
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),
@@ -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
- return record;
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.require(payload.taskId);
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.6",
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",