@chenchaolong/plugin-trade-compliance-workbench 1.0.63 → 1.0.65

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.
Files changed (45) hide show
  1. package/README.md +22 -3
  2. package/dist/index.d.ts +52 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/lib/adapters/translation.provider.d.ts +7 -0
  5. package/dist/lib/adapters/translation.provider.d.ts.map +1 -1
  6. package/dist/lib/adapters/translation.provider.js +127 -0
  7. package/dist/lib/adapters/translation.provider.js.map +1 -1
  8. package/dist/lib/catalog-import/catalog-import.pipeline.d.ts +1 -1
  9. package/dist/lib/catalog-import/catalog-import.pipeline.d.ts.map +1 -1
  10. package/dist/lib/catalog-import/catalog-import.pipeline.js +8 -5
  11. package/dist/lib/catalog-import/catalog-import.pipeline.js.map +1 -1
  12. package/dist/lib/catalog-import/confidence.js +8 -1
  13. package/dist/lib/catalog-import/confidence.js.map +1 -1
  14. package/dist/lib/catalog-import/docx.parser.d.ts.map +1 -1
  15. package/dist/lib/catalog-import/docx.parser.js +15 -1
  16. package/dist/lib/catalog-import/docx.parser.js.map +1 -1
  17. package/dist/lib/constants.d.ts +1 -0
  18. package/dist/lib/constants.d.ts.map +1 -1
  19. package/dist/lib/constants.js +1 -0
  20. package/dist/lib/constants.js.map +1 -1
  21. package/dist/lib/domain/catalog-localization.d.ts +1 -1
  22. package/dist/lib/domain/catalog-localization.d.ts.map +1 -1
  23. package/dist/lib/domain/catalog-localization.js +3 -1
  24. package/dist/lib/domain/catalog-localization.js.map +1 -1
  25. package/dist/lib/import-jobs/agent-handoff.dispatcher.d.ts.map +1 -1
  26. package/dist/lib/import-jobs/agent-handoff.dispatcher.js +11 -3
  27. package/dist/lib/import-jobs/agent-handoff.dispatcher.js.map +1 -1
  28. package/dist/lib/import-jobs/import-command.builder.js +1 -1
  29. package/dist/lib/import-jobs/import-command.builder.js.map +1 -1
  30. package/dist/lib/trade-compliance.config.d.ts +52 -0
  31. package/dist/lib/trade-compliance.config.d.ts.map +1 -1
  32. package/dist/lib/trade-compliance.config.js +13 -3
  33. package/dist/lib/trade-compliance.config.js.map +1 -1
  34. package/dist/lib/workbench.middleware.d.ts +34 -1
  35. package/dist/lib/workbench.middleware.d.ts.map +1 -1
  36. package/dist/lib/workbench.middleware.js +10 -2
  37. package/dist/lib/workbench.middleware.js.map +1 -1
  38. package/dist/lib/workbench.service.d.ts +8 -0
  39. package/dist/lib/workbench.service.d.ts.map +1 -1
  40. package/dist/lib/workbench.service.js +127 -21
  41. package/dist/lib/workbench.service.js.map +1 -1
  42. package/package.json +6 -1
  43. package/scripts/verify-catalog-e2e.mjs +250 -0
  44. package/scripts/verify-catalog-samples.mjs +61 -0
  45. package/scripts/verify-qwen-translation.mjs +46 -0
@@ -577,7 +577,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
577
577
  batch.riskRecalculatedAt = result.riskRecalculatedAt ?? new Date();
578
578
  return batch;
579
579
  }
580
- async localizeControlledCatalogRows(rows, organizationId, deadline = Date.now() + CONTROLLED_CATALOG_TRANSLATION_TIMEOUT_MS, ownerSignal) {
580
+ async localizeControlledCatalogRows(rows, organizationId, deadline = Date.now() + CONTROLLED_CATALOG_TRANSLATION_TIMEOUT_MS, ownerSignal, allowServiceTranslation = false) {
581
581
  const localized = new Array(rows.length);
582
582
  const controller = new AbortController();
583
583
  const abortFromOwner = () => controller.abort(ownerSignal?.reason);
@@ -591,7 +591,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
591
591
  while (nextIndex < rows.length) {
592
592
  const index = nextIndex;
593
593
  nextIndex += 1;
594
- localized[index] = await this.localizeControlledCatalogRow(rows[index], organizationId, deadline, controller.signal);
594
+ localized[index] = await this.localizeControlledCatalogRow(rows[index], organizationId, deadline, controller.signal, allowServiceTranslation);
595
595
  }
596
596
  };
597
597
  try {
@@ -603,14 +603,14 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
603
603
  ownerSignal?.removeEventListener('abort', abortFromOwner);
604
604
  }
605
605
  }
606
- async localizeControlledCatalogRow(row, organizationId, deadline, signal) {
606
+ async localizeControlledCatalogRow(row, organizationId, deadline, signal, allowServiceTranslation) {
607
607
  const originalProductName = String(row.originalProductName ?? '').trim() || null;
608
608
  const originalControlDescription = String(row.originalControlDescription ?? '').trim() || null;
609
609
  let productName = String(row.productName ?? '').trim();
610
610
  let controlDescription = String(row.controlDescription ?? '').trim() || null;
611
611
  let translationSource = row.translationSource ?? null;
612
612
  let serviceTranslated = false;
613
- const inputIssue = catalogLocalizationInputIssues(row)[0];
613
+ const inputIssue = catalogLocalizationInputIssues(row, allowServiceTranslation)[0];
614
614
  if (inputIssue)
615
615
  return { productName, controlDescription, originalProductName, originalControlDescription, translationSource, error: inputIssue.message };
616
616
  if (!productName) {
@@ -1071,8 +1071,8 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1071
1071
  if (decision === 'PUBLISH_VALID_ROWS') {
1072
1072
  await this.appendStalledCatalogReviewIssues(manager, scope, task, batch, unresolved);
1073
1073
  }
1074
- if (unresolved.length > 0)
1075
- await reviewRepo.save(unresolved);
1074
+ for (const block of unresolved)
1075
+ await reviewRepo.save(block);
1076
1076
  await this.reconcileCatalogReviewPendingCount(reviewRepo, scope, task);
1077
1077
  if (progress.reviewPendingCount !== 0) {
1078
1078
  throw new DomainError(DomainErrorCodes.INVALID_INPUT, '目录恢复后仍存在待复核块');
@@ -1128,6 +1128,77 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1128
1128
  }
1129
1129
  return result;
1130
1130
  }
1131
+ async autoTranslateCatalogReviewBlocks(scope, taskId) {
1132
+ if (!this.translation)
1133
+ return { translated: 0, remaining: await this.countCatalogReviewBlocks(scope, taskId) };
1134
+ let translated = 0;
1135
+ while (true) {
1136
+ const leased = await this.leaseTranslationReviewBlocks(scope, taskId);
1137
+ if (!leased || leased.blocks.length === 0)
1138
+ break;
1139
+ const records = leased.blocks.map(block => ({
1140
+ recordId: block.id,
1141
+ fields: translationFields(block)
1142
+ }));
1143
+ const translatedRecords = await this.translation.translateBatch(records, { organizationId: scope.organizationId });
1144
+ const translatedById = new Map(translatedRecords.map(record => [record.recordId, record]));
1145
+ const accepted = leased.blocks.flatMap(block => {
1146
+ const result = translatedById.get(block.id);
1147
+ const fields = result?.fields;
1148
+ if (!fields)
1149
+ return [];
1150
+ const payload = autoTranslatedPayload(block, fields);
1151
+ return payload ? [{ blockId: block.id, decision: 'ACCEPT', payload }] : [];
1152
+ });
1153
+ const acceptedIds = new Set(accepted.map(item => item.blockId));
1154
+ await this.releaseTranslationReviewBlocks(scope, taskId, leased.leaseToken, leased.blocks.filter(block => !acceptedIds.has(block.id)).map(block => block.id));
1155
+ if (accepted.length === 0)
1156
+ break;
1157
+ const result = await this.submitCatalogReviewBatchInternal(scope, taskId, leased.leaseToken, accepted, true);
1158
+ translated += result.resolved;
1159
+ if (result.resolved === 0 || accepted.length < leased.blocks.length)
1160
+ break;
1161
+ }
1162
+ return { translated, remaining: await this.countCatalogReviewBlocks(scope, taskId) };
1163
+ }
1164
+ async leaseTranslationReviewBlocks(scope, taskId) {
1165
+ return this.dataSource.transaction(async (manager) => {
1166
+ const taskRepo = manager.getRepository(ImportTask);
1167
+ const task = await required(taskRepo.findOne({ where: { ...activeScope(scope), id: taskId }, lock: { mode: 'pessimistic_write' } }), '解析任务不存在');
1168
+ const repo = manager.getRepository(CatalogReviewBlock);
1169
+ const candidates = await repo.find({ where: { ...activeScope(scope), importTaskId: task.id, status: 'PENDING' }, order: { ordinal: 'ASC' }, lock: { mode: 'pessimistic_write' } });
1170
+ const blocks = candidates.filter(block => block.reasons.length === 1 && block.reasons[0] === 'TRANSLATION_REQUIRED').slice(0, CATALOG_REVIEW_BATCH_LIMIT);
1171
+ if (blocks.length === 0)
1172
+ return null;
1173
+ const leaseToken = randomUUID();
1174
+ const leaseExpiresAt = new Date(Date.now() + 10 * 60_000);
1175
+ for (const block of blocks) {
1176
+ block.status = 'LEASED';
1177
+ block.leaseToken = leaseToken;
1178
+ block.leaseExpiresAt = leaseExpiresAt;
1179
+ }
1180
+ await repo.update({ ...activeScope(scope), importTaskId: task.id, id: In(blocks.map(block => block.id)), status: 'PENDING' }, {
1181
+ status: 'LEASED', leaseToken, leaseExpiresAt
1182
+ });
1183
+ if (task.progress) {
1184
+ task.progress.stage = 'TRANSLATING';
1185
+ task.progress.progressVersion += 1;
1186
+ task.progress.lastProgressAt = new Date().toISOString();
1187
+ await taskRepo.save(task);
1188
+ }
1189
+ return { leaseToken, blocks };
1190
+ });
1191
+ }
1192
+ async releaseTranslationReviewBlocks(scope, taskId, leaseToken, blockIds) {
1193
+ if (blockIds.length === 0)
1194
+ return;
1195
+ await this.dataSource.getRepository(CatalogReviewBlock).update({ ...activeScope(scope), importTaskId: taskId, id: In(blockIds), status: 'LEASED', leaseToken }, {
1196
+ status: 'PENDING', leaseToken: null, leaseExpiresAt: null
1197
+ });
1198
+ }
1199
+ async countCatalogReviewBlocks(scope, taskId) {
1200
+ return this.dataSource.getRepository(CatalogReviewBlock).count({ where: { ...activeScope(scope), importTaskId: taskId, status: In(['PENDING', 'LEASED']) } });
1201
+ }
1131
1202
  async waitImportProgress(scope, taskId, afterProgressVersion = 0, waitMs = 10_000) {
1132
1203
  const deadline = Date.now() + Math.max(0, Math.min(waitMs, 10_000));
1133
1204
  while (true) {
@@ -1183,7 +1254,9 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1183
1254
  block.leaseToken = leaseToken;
1184
1255
  block.leaseExpiresAt = leaseExpiresAt;
1185
1256
  }
1186
- await repo.save(blocks);
1257
+ await repo.update({ ...activeScope(scope), importTaskId: task.id, id: In(blocks.map(block => block.id)), status: 'PENDING' }, {
1258
+ status: 'LEASED', leaseToken, leaseExpiresAt
1259
+ });
1187
1260
  return {
1188
1261
  leaseToken,
1189
1262
  leaseExpiresAt: leaseExpiresAt.toISOString(),
@@ -1196,6 +1269,9 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1196
1269
  });
1197
1270
  }
1198
1271
  async submitCatalogReviewBatch(scope, taskId, leaseToken, items) {
1272
+ return this.submitCatalogReviewBatchInternal(scope, taskId, leaseToken, items, false);
1273
+ }
1274
+ async submitCatalogReviewBatchInternal(scope, taskId, leaseToken, items, allowServiceTranslation) {
1199
1275
  if (!leaseToken.trim() || items.length === 0 || items.length > CATALOG_REVIEW_BATCH_LIMIT) {
1200
1276
  throw new DomainError(DomainErrorCodes.INVALID_INPUT, '复核提交参数无效');
1201
1277
  }
@@ -1254,7 +1330,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1254
1330
  block.resolutionFingerprint = catalogReviewResolutionFingerprint(item);
1255
1331
  }
1256
1332
  else {
1257
- const evaluation = evaluateCatalogReviewAccept(task.documentType, block, item.payload);
1333
+ const evaluation = evaluateCatalogReviewAccept(task.documentType, block, item.payload, allowServiceTranslation);
1258
1334
  if (!evaluation.candidate) {
1259
1335
  restorePendingReviewBlock(block);
1260
1336
  errors.push({ blockId: block.id, reasons: evaluation.reasons });
@@ -1283,7 +1359,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1283
1359
  const batchRepo = manager.getRepository(ControlledCatalogBatch);
1284
1360
  const batch = await required(batchRepo.findOne({ where: { ...activeScope(scope), id: task.resultEntityId } }), '管控目录批次不存在');
1285
1361
  const rows = acceptedCandidates.flatMap(candidateToControlledRows);
1286
- const localized = await this.localizeControlledCatalogRows(rows, scope.organizationId);
1362
+ const localized = await this.localizeControlledCatalogRows(rows, scope.organizationId, undefined, undefined, allowServiceTranslation);
1287
1363
  await this.appendControlledCatalogRows(manager, scope, batch, rows, localized);
1288
1364
  await batchRepo.save(batch);
1289
1365
  task.progress.successCount = batch.successCount;
@@ -1303,7 +1379,8 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1303
1379
  task.progress.invalidCount = rejectedBatch.invalidCount;
1304
1380
  task.progress.skippedCount = rejectedBatch.skippedCount;
1305
1381
  }
1306
- await reviewRepo.save(blocks);
1382
+ for (const block of blocks)
1383
+ await reviewRepo.save(block);
1307
1384
  task.progress.reviewResolvedCount += resolved;
1308
1385
  task.progress.reviewRejectedCount += rejected;
1309
1386
  await this.reconcileCatalogReviewPendingCount(reviewRepo, scope, task);
@@ -3607,12 +3684,10 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
3607
3684
  return { scanned: 0, matches: 0, riskRecalculatedAt: sanctionBatch.riskRecalculatedAt };
3608
3685
  }
3609
3686
  await repo.update({ ...activeScope(scope), deletedAt: IsNull() }, { deletedAt: now, invalidationReason: reason });
3610
- const [suppliers, customers, sanctions, aliases] = await Promise.all([
3611
- supplierRepo.find({ where: { ...activeScope(scope), deletedAt: IsNull() } }),
3612
- customerRepo.find({ where: { ...activeScope(scope), deletedAt: IsNull() } }),
3613
- manager.getRepository(SanctionedCompanyRecord).find({ where: { ...activeScope(scope), lifecycleStatus: 'ACTIVE', deletedAt: IsNull() } }),
3614
- manager.getRepository(SanctionedCompanyAlias).find({ where: { ...activeScope(scope), deletedAt: IsNull() } })
3615
- ]);
3687
+ const suppliers = await supplierRepo.find({ where: { ...activeScope(scope), deletedAt: IsNull() } });
3688
+ const customers = await customerRepo.find({ where: { ...activeScope(scope), deletedAt: IsNull() } });
3689
+ const sanctions = await manager.getRepository(SanctionedCompanyRecord).find({ where: { ...activeScope(scope), lifecycleStatus: 'ACTIVE', deletedAt: IsNull() } });
3690
+ const aliases = await manager.getRepository(SanctionedCompanyAlias).find({ where: { ...activeScope(scope), deletedAt: IsNull() } });
3616
3691
  const sanctionsById = new Map(sanctions.map(item => [item.id, item]));
3617
3692
  const sanctionsByNormalizedName = new Map();
3618
3693
  for (const item of sanctions)
@@ -4394,7 +4469,7 @@ function candidateToControlledRows(candidate) {
4394
4469
  const hsCodes = Array.isArray(candidate.values.hsCodes) && candidate.values.hsCodes.length
4395
4470
  ? candidate.values.hsCodes
4396
4471
  : [undefined];
4397
- const translationSource = candidate.values.translationSource === 'AGENT_TRANSLATED' || candidate.values.translationSource === 'SOURCE_CHINESE'
4472
+ const translationSource = candidate.values.translationSource === 'AGENT_TRANSLATED' || candidate.values.translationSource === 'SOURCE_CHINESE' || candidate.values.translationSource === 'SERVICE_TRANSLATED'
4398
4473
  ? candidate.values.translationSource
4399
4474
  : candidate.values.translationRequired || (!candidate.values.productName && candidate.values.originalProductName)
4400
4475
  ? 'AGENT_TRANSLATED'
@@ -4402,6 +4477,7 @@ function candidateToControlledRows(candidate) {
4402
4477
  return hsCodes.map(hsCode => ({
4403
4478
  productName: optionalString(candidate.values.productName) ?? undefined,
4404
4479
  originalProductName: optionalString(candidate.values.originalProductName) ?? undefined,
4480
+ originalControlDescription: optionalString(candidate.values.originalControlDescription) ?? undefined,
4405
4481
  hsCode: typeof hsCode === 'string' ? hsCode : undefined,
4406
4482
  controlDescription: optionalString(candidate.values.controlDescription) ?? undefined,
4407
4483
  controlCode: optionalString(candidate.values.controlCode) ?? undefined,
@@ -4498,11 +4574,11 @@ function canonicalJson(value) {
4498
4574
  }
4499
4575
  throw new TypeError('Resolution payload is not JSON serializable');
4500
4576
  }
4501
- function evaluateCatalogReviewAccept(documentType, block, patch) {
4577
+ function evaluateCatalogReviewAccept(documentType, block, patch, allowServiceTranslation = false) {
4502
4578
  try {
4503
4579
  const payload = mergeReviewPayload(block.payload, patch);
4504
4580
  let candidate = reviewCandidate(documentType, block, payload);
4505
- if (!validCatalogReviewCandidateValues(candidate))
4581
+ if (!validCatalogReviewCandidateValues(candidate, allowServiceTranslation))
4506
4582
  return { candidate: null, reasons: ['REVIEW_PAYLOAD_INVALID'] };
4507
4583
  candidate = normalizeCatalogCandidate(candidate);
4508
4584
  const decision = classifyCandidate(candidate);
@@ -4520,7 +4596,7 @@ function evaluateCatalogReviewAccept(documentType, block, patch) {
4520
4596
  const CATALOG_REVIEW_VARCHAR_MAX = 255;
4521
4597
  const CATALOG_REVIEW_TEXT_MAX = 100_000;
4522
4598
  const CATALOG_REVIEW_ARRAY_MAX = 100;
4523
- function validCatalogReviewCandidateValues(candidate) {
4599
+ function validCatalogReviewCandidateValues(candidate, allowServiceTranslation = false) {
4524
4600
  const values = candidate.values;
4525
4601
  const varcharFields = candidate.kind === 'CONTROLLED'
4526
4602
  ? ['productName', 'hsCode', 'controlCode', 'sourceSection', 'sourceOrdinal']
@@ -4534,7 +4610,7 @@ function validCatalogReviewCandidateValues(candidate) {
4534
4610
  return false;
4535
4611
  if (values.translationRequired !== undefined && typeof values.translationRequired !== 'boolean')
4536
4612
  return false;
4537
- if (values.translationSource !== undefined && values.translationSource !== 'SOURCE_CHINESE' && values.translationSource !== 'AGENT_TRANSLATED')
4613
+ if (values.translationSource !== undefined && values.translationSource !== 'SOURCE_CHINESE' && values.translationSource !== 'AGENT_TRANSLATED' && !(allowServiceTranslation && values.translationSource === 'SERVICE_TRANSLATED'))
4538
4614
  return false;
4539
4615
  if (values.hsCodes !== undefined && (!Array.isArray(values.hsCodes) || values.hsCodes.length > CATALOG_REVIEW_ARRAY_MAX ||
4540
4616
  values.hsCodes.some(value => typeof value !== 'string' || value.length > CATALOG_REVIEW_VARCHAR_MAX)))
@@ -4555,6 +4631,36 @@ function validCatalogReviewAlias(value) {
4555
4631
  return typeof alias === 'string' && alias.length > 0 && alias.length <= CATALOG_REVIEW_VARCHAR_MAX &&
4556
4632
  (aliasType === 'AKA' || aliasType === 'LOCAL_LANGUAGE');
4557
4633
  }
4634
+ function translationFields(block) {
4635
+ const payload = block.payload ?? {};
4636
+ const fields = block.documentType === 'CONTROLLED_CATALOG'
4637
+ ? [['productName', payload.originalProductName ?? payload.productName], ['controlDescription', payload.originalControlDescription ?? payload.controlDescription]]
4638
+ : [['sanctionReason', payload.sanctionReason], ['remarks', payload.remarks]];
4639
+ return Object.fromEntries(fields.filter(([, value]) => typeof value === 'string' && value.trim().length > 0));
4640
+ }
4641
+ function autoTranslatedPayload(block, fields) {
4642
+ const requiredFields = Object.keys(translationFields(block));
4643
+ if (requiredFields.length === 0 || requiredFields.some(field => typeof fields[field] !== 'string' || !fields[field]?.trim()))
4644
+ return null;
4645
+ const payload = { ...block.payload };
4646
+ for (const field of ['productName', 'controlDescription', 'sanctionReason', 'remarks']) {
4647
+ const value = fields[field];
4648
+ if (value !== undefined && value !== null && String(value).trim())
4649
+ payload[field] = String(value).trim();
4650
+ }
4651
+ if (block.documentType === 'CONTROLLED_CATALOG') {
4652
+ if (payload.originalProductName && !payload.productName)
4653
+ return null;
4654
+ if (payload.originalControlDescription && !payload.controlDescription)
4655
+ return null;
4656
+ payload.translationRequired = false;
4657
+ payload.translationSource = 'SERVICE_TRANSLATED';
4658
+ }
4659
+ else {
4660
+ payload.translationRequired = false;
4661
+ }
4662
+ return payload;
4663
+ }
4558
4664
  function isRealIsoDate(value) {
4559
4665
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
4560
4666
  if (!match)