@chenchaolong/plugin-trade-compliance-workbench 1.0.19 → 1.0.21

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 (44) hide show
  1. package/dist/lib/adapters/translation.provider.d.ts +1 -0
  2. package/dist/lib/adapters/translation.provider.d.ts.map +1 -1
  3. package/dist/lib/adapters/translation.provider.js +1 -1
  4. package/dist/lib/adapters/translation.provider.js.map +1 -1
  5. package/dist/lib/catalog-import/catalog-import.pipeline.d.ts.map +1 -1
  6. package/dist/lib/catalog-import/catalog-import.pipeline.js +2 -1
  7. package/dist/lib/catalog-import/catalog-import.pipeline.js.map +1 -1
  8. package/dist/lib/catalog-import/confidence.js +2 -2
  9. package/dist/lib/catalog-import/confidence.js.map +1 -1
  10. package/dist/lib/catalog-import/contracts.d.ts +5 -2
  11. package/dist/lib/catalog-import/contracts.d.ts.map +1 -1
  12. package/dist/lib/catalog-import/outcome.d.ts +19 -0
  13. package/dist/lib/catalog-import/outcome.d.ts.map +1 -0
  14. package/dist/lib/catalog-import/outcome.js +18 -0
  15. package/dist/lib/catalog-import/outcome.js.map +1 -0
  16. package/dist/lib/catalog-import/spreadsheet-structure.d.ts +15 -0
  17. package/dist/lib/catalog-import/spreadsheet-structure.d.ts.map +1 -0
  18. package/dist/lib/catalog-import/spreadsheet-structure.js +44 -0
  19. package/dist/lib/catalog-import/spreadsheet-structure.js.map +1 -0
  20. package/dist/lib/catalog-import/spreadsheet.parser.d.ts.map +1 -1
  21. package/dist/lib/catalog-import/spreadsheet.parser.js +71 -0
  22. package/dist/lib/catalog-import/spreadsheet.parser.js.map +1 -1
  23. package/dist/lib/entities/catalog.entity.d.ts +5 -1
  24. package/dist/lib/entities/catalog.entity.d.ts.map +1 -1
  25. package/dist/lib/entities/catalog.entity.js +16 -0
  26. package/dist/lib/entities/catalog.entity.js.map +1 -1
  27. package/dist/lib/remote-components/trade-compliance-workbench/app.js +5 -5
  28. package/dist/lib/remote-ui/app-source.js +18 -3
  29. package/dist/lib/remote-ui/app-source.js.map +1 -1
  30. package/dist/lib/remote-ui/contracts.d.ts +3 -0
  31. package/dist/lib/remote-ui/contracts.d.ts.map +1 -1
  32. package/dist/lib/remote-ui/pages/catalogs.d.ts +4 -0
  33. package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
  34. package/dist/lib/remote-ui/pages/catalogs.js +31 -2
  35. package/dist/lib/remote-ui/pages/catalogs.js.map +1 -1
  36. package/dist/lib/types.d.ts +12 -1
  37. package/dist/lib/types.d.ts.map +1 -1
  38. package/dist/lib/types.js +1 -1
  39. package/dist/lib/types.js.map +1 -1
  40. package/dist/lib/workbench.service.d.ts +7 -1
  41. package/dist/lib/workbench.service.d.ts.map +1 -1
  42. package/dist/lib/workbench.service.js +250 -75
  43. package/dist/lib/workbench.service.js.map +1 -1
  44. package/package.json +1 -1
@@ -24,6 +24,7 @@ import { TranslationProvider } from './adapters/translation.provider.js';
24
24
  import { parseBankAccountType, parseExchangeRateInput, parseExpectedRevision, validateBankSettingsInput, validateProfitSettingsInput, validatePurchaseLineDiscriminators, validateTaxRates } from './runtime-validation.js';
25
25
  import { publicCustomerContractReanalysisDto, publicImportTaskDto } from './public-dto.js';
26
26
  import { classifyCandidate } from './catalog-import/confidence.js';
27
+ import { catalogTerminalOutcome } from './catalog-import/outcome.js';
27
28
  const DEFAULT_IMPORT_ISSUE_EXPORT_MAX_ROWS = 50_000;
28
29
  const HARD_IMPORT_ISSUE_EXPORT_MAX_ROWS = 100_000;
29
30
  const CUSTOMER_REANALYSIS_TIMEOUT_MS = 20 * 60 * 1000;
@@ -34,6 +35,12 @@ const CONTROLLED_CATALOG_PROCESSING_CLAIM_LEASE_MS = 90_000;
34
35
  const CONTROLLED_CATALOG_PROCESSING_HEARTBEAT_MS = 30_000;
35
36
  const CONTROLLED_CATALOG_PROCESSING_CLAIM_INITIAL_POLL_MS = 250;
36
37
  const CONTROLLED_CATALOG_PROCESSING_CLAIM_MAX_POLL_MS = 1_000;
38
+ const SKIPPED_REASON_MESSAGES = {
39
+ SECTION_OR_NOTE_SKIPPED: '章节或说明行已跳过',
40
+ INVALID_HS_CODE: 'HS 编码格式无效',
41
+ EMPTY_PRODUCT_DESCRIPTION: '商品描述为空',
42
+ STRUCTURAL_MAPPING_AMBIGUOUS: '表格列结构无法可靠判断'
43
+ };
37
44
  const RUNNING_IMPORT_TASK_STATUSES = ['EXTRACTING', 'AI_PARSING', 'VALIDATING', 'PERSISTING'];
38
45
  const RECOVERABLE_IMPORT_TASK_STATUSES = ['QUEUED', 'EXTRACTING', 'AI_PARSING', 'VALIDATING', 'PERSISTING'];
39
46
  const RECOVERABLE_REANALYSIS_STATUSES = ['PENDING', 'RUNNING'];
@@ -219,7 +226,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
219
226
  const pendingRows = await this.pendingControlledCatalogRows(scope, claim.batch, input.rows);
220
227
  if (!pendingRows.length)
221
228
  return this.completeControlledCatalogPostProcessing(scope, claim.batch);
222
- const localizedRows = await this.localizeControlledCatalogRows(pendingRows);
229
+ const localizedRows = await this.localizeControlledCatalogRows(pendingRows, scope.organizationId);
223
230
  const appended = await this.appendCompletedControlledCatalog(scope, { ...input, rows: pendingRows }, localizedRows, claim.batch.id);
224
231
  return this.completeControlledCatalogPostProcessing(scope, appended);
225
232
  }
@@ -231,7 +238,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
231
238
  try {
232
239
  let result;
233
240
  try {
234
- const localizedRows = await this.localizeControlledCatalogRows(input.rows, claim.deadline, heartbeat.signal);
241
+ const localizedRows = await this.localizeControlledCatalogRows(input.rows, scope.organizationId, claim.deadline, heartbeat.signal);
235
242
  heartbeat.assertOwned();
236
243
  result = await this.persistControlledCatalog(scope, input, localizedRows, claim.claimId);
237
244
  }
@@ -462,7 +469,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
462
469
  batch.riskRecalculatedAt = result.riskRecalculatedAt ?? new Date();
463
470
  return batch;
464
471
  }
465
- async localizeControlledCatalogRows(rows, deadline = Date.now() + CONTROLLED_CATALOG_TRANSLATION_TIMEOUT_MS, ownerSignal) {
472
+ async localizeControlledCatalogRows(rows, organizationId, deadline = Date.now() + CONTROLLED_CATALOG_TRANSLATION_TIMEOUT_MS, ownerSignal) {
466
473
  const localized = new Array(rows.length);
467
474
  const controller = new AbortController();
468
475
  const abortFromOwner = () => controller.abort(ownerSignal?.reason);
@@ -476,7 +483,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
476
483
  while (nextIndex < rows.length) {
477
484
  const index = nextIndex;
478
485
  nextIndex += 1;
479
- localized[index] = await this.localizeControlledCatalogRow(rows[index], deadline, controller.signal);
486
+ localized[index] = await this.localizeControlledCatalogRow(rows[index], organizationId, deadline, controller.signal);
480
487
  }
481
488
  };
482
489
  try {
@@ -488,7 +495,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
488
495
  ownerSignal?.removeEventListener('abort', abortFromOwner);
489
496
  }
490
497
  }
491
- async localizeControlledCatalogRow(row, deadline, signal) {
498
+ async localizeControlledCatalogRow(row, organizationId, deadline, signal) {
492
499
  const originalProductName = String(row.originalProductName ?? '').trim() || null;
493
500
  const originalControlDescription = String(row.originalControlDescription ?? '').trim() || null;
494
501
  let productName = String(row.productName ?? '').trim();
@@ -501,13 +508,13 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
501
508
  if (!productName) {
502
509
  if (!originalProductName)
503
510
  return { productName: '', controlDescription, originalProductName, originalControlDescription, translationSource, error: '商品名称为空' };
504
- productName = await this.translateCatalogValue(originalProductName, deadline, signal) ?? '';
511
+ productName = await this.translateCatalogValue(originalProductName, organizationId, deadline, signal) ?? '';
505
512
  if (!productName)
506
513
  return { productName: '', controlDescription, originalProductName, originalControlDescription, translationSource, error: '商品名称翻译失败' };
507
514
  serviceTranslated = true;
508
515
  }
509
516
  if (!controlDescription && originalControlDescription) {
510
- controlDescription = await this.translateCatalogValue(originalControlDescription, deadline, signal);
517
+ controlDescription = await this.translateCatalogValue(originalControlDescription, organizationId, deadline, signal);
511
518
  if (!controlDescription)
512
519
  return { productName, controlDescription: null, originalProductName, originalControlDescription, translationSource, error: '管控说明翻译失败' };
513
520
  serviceTranslated = true;
@@ -518,11 +525,11 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
518
525
  translationSource = 'SERVICE_TRANSLATED';
519
526
  return { productName, controlDescription, originalProductName, originalControlDescription, translationSource };
520
527
  }
521
- async translateCatalogValue(value, deadline, signal) {
528
+ async translateCatalogValue(value, organizationId, deadline, signal) {
522
529
  const remaining = deadline - Date.now();
523
530
  if (!this.translation || remaining <= 0 || signal.aborted)
524
531
  return null;
525
- const translated = Promise.resolve(this.translation.toChinese(value, { signal, deadline }))
532
+ const translated = Promise.resolve(this.translation.toChinese(value, { signal, deadline, organizationId }))
526
533
  .then(result => String(result ?? '').trim() || null, () => null);
527
534
  let onAbort;
528
535
  try {
@@ -716,7 +723,8 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
716
723
  const batch = await batchRepo.save(batchRepo.create({
717
724
  ...auditScope(scope), catalogType, sourceFileId: task.sourceFileId, sourceFileName: task.sourceFileName,
718
725
  importMode: input.replaceBatchId ? 'REPLACE_BATCH' : 'APPEND', replacedBatchId: input.replaceBatchId ?? null,
719
- totalCount: 0, successCount: 0, duplicateCount: 0, invalidCount: 0,
726
+ totalCount: 0, successCount: 0, duplicateCount: 0, invalidCount: 0, skippedCount: 0,
727
+ inferenceMetadata: input.inferenceMetadata ?? null,
720
728
  importTaskId: task.id, riskRecalculatedAt: null
721
729
  }));
722
730
  batchId = batch.id;
@@ -736,14 +744,15 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
736
744
  const batch = await batchRepo.save(batchRepo.create({
737
745
  ...auditScope(scope), sourceFileId: task.sourceFileId, sourceFileName: task.sourceFileName,
738
746
  importMode: input.replaceBatchId ? 'REPLACE_BATCH' : 'APPEND', replacedBatchId: input.replaceBatchId ?? null,
739
- totalCount: 0, successCount: 0, duplicateCount: 0, invalidCount: 0, importTaskId: task.id
747
+ totalCount: 0, successCount: 0, duplicateCount: 0, invalidCount: 0, skippedCount: 0,
748
+ inferenceMetadata: input.inferenceMetadata ?? null, importTaskId: task.id
740
749
  }));
741
750
  batchId = batch.id;
742
751
  }
743
752
  task.status = 'PERSISTING';
744
753
  task.resultEntityId = batchId;
745
754
  task.completedAt = null;
746
- task.progress = initialFastCatalogProgress(input.totalCandidates);
755
+ task.progress = initialFastCatalogProgress(input.totalCandidates, input.inferenceMetadata);
747
756
  await taskRepo.save(task);
748
757
  return { committedCursor: 0, batchId };
749
758
  });
@@ -752,7 +761,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
752
761
  const controlledRows = input.candidates.flatMap(candidate => candidate.kind === 'CONTROLLED'
753
762
  ? candidateToControlledRows(candidate)
754
763
  : []);
755
- const localizedRows = controlledRows.length ? await this.localizeControlledCatalogRows(controlledRows) : [];
764
+ const localizedRows = controlledRows.length ? await this.localizeControlledCatalogRows(controlledRows, scope.organizationId) : [];
756
765
  return this.dataSource.transaction(async (manager) => {
757
766
  const taskRepo = manager.getRepository(ImportTask);
758
767
  const task = await required(taskRepo.findOne({
@@ -771,23 +780,29 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
771
780
  const batchRepo = manager.getRepository(ControlledCatalogBatch);
772
781
  const batch = await required(batchRepo.findOne({ where: { ...activeScope(scope), id: task.resultEntityId } }), '管控目录批次不存在');
773
782
  await this.appendControlledCatalogRows(manager, scope, batch, controlledRows, localizedRows);
783
+ await this.appendCatalogSkippedSources(manager.getRepository(ControlledCatalogImportIssue), scope, batch, input.skipped);
774
784
  await batchRepo.save(batch);
775
785
  task.progress.successCount = batch.successCount;
776
786
  task.progress.duplicateCount = batch.duplicateCount;
777
787
  task.progress.invalidCount = batch.invalidCount;
788
+ task.progress.skippedCount = batch.skippedCount;
778
789
  }
779
790
  else {
780
791
  const batch = await this.appendFastSanctionCandidates(manager, scope, task.resultEntityId, input.candidates);
792
+ await this.appendCatalogSkippedSources(manager.getRepository(SanctionCatalogImportIssue), scope, batch, input.skipped);
793
+ await manager.getRepository(SanctionCatalogBatch).save(batch);
781
794
  task.progress.successCount = batch.successCount;
782
795
  task.progress.duplicateCount = batch.duplicateCount;
783
796
  task.progress.invalidCount = batch.invalidCount;
797
+ task.progress.skippedCount = batch.skippedCount;
784
798
  }
799
+ let insertedReviewCount = 0;
785
800
  if (input.reviewBlocks.length) {
786
801
  const reviewRepo = manager.getRepository(CatalogReviewBlock);
787
802
  for (const block of input.reviewBlocks) {
788
803
  const existing = await reviewRepo.findOne({ where: {
789
804
  ...activeScope(scope), importTaskId: task.id, ordinal: block.ordinal,
790
- sourceLocation: block.sourceLocation, status: 'PENDING'
805
+ sourceLocation: block.sourceLocation
791
806
  } });
792
807
  if (existing)
793
808
  continue;
@@ -798,11 +813,12 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
798
813
  payload: block.payload, reasons: block.reasons,
799
814
  leaseToken: null, leaseExpiresAt: null, resolvedAt: null
800
815
  }));
816
+ insertedReviewCount += 1;
801
817
  }
802
818
  }
803
819
  task.progress.committedCursor = input.endCursor;
804
820
  task.progress.processedCount = input.endCursor + input.reviewBlocks.length + input.skipped.length;
805
- task.progress.reviewPendingCount += input.reviewBlocks.length;
821
+ task.progress.reviewPendingCount += insertedReviewCount;
806
822
  task.progress.currentBatch = input.endCursor === 0 ? 0 : Math.ceil(input.endCursor / 100);
807
823
  task.progress.progressVersion += 1;
808
824
  task.progress.lastProgressAt = new Date().toISOString();
@@ -820,18 +836,42 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
820
836
  if (!current.progress || current.progress.mode !== 'FAST_PIPELINE') {
821
837
  throw new DomainError(DomainErrorCodes.INVALID_INPUT, '快速目录导入进度不存在');
822
838
  }
823
- current.status = current.progress.reviewPendingCount > 0 ? 'AWAITING_REVIEW' : 'SUCCEEDED';
824
- current.progress.stage = current.progress.reviewPendingCount > 0 ? 'REVIEWING' : 'SUCCEEDED';
825
- current.progress.observerStatus = current.progress.reviewPendingCount > 0 ? 'PENDING' : 'COMPLETED';
839
+ if (current.progress.reviewPendingCount > 0) {
840
+ current.status = 'AWAITING_REVIEW';
841
+ current.progress.stage = 'REVIEWING';
842
+ current.progress.observerStatus = 'PENDING';
843
+ current.completedAt = null;
844
+ }
845
+ else {
846
+ const counts = await this.catalogBatchCounts(manager, scope, current);
847
+ const outcome = catalogTerminalOutcome(counts);
848
+ current.progress.successCount = counts.successCount;
849
+ current.progress.duplicateCount = counts.duplicateCount;
850
+ current.progress.invalidCount = counts.invalidCount;
851
+ current.progress.skippedCount = counts.skippedCount;
852
+ current.progress.outcome = outcome.outcome;
853
+ if (outcome.status === 'FAILED') {
854
+ await this.applyImportTaskFailure(manager, scope, current, 'FAILED', outcome.errorCode, outcome.errorMessage);
855
+ }
856
+ else {
857
+ current.status = 'SUCCEEDED';
858
+ current.progress.stage = 'SUCCEEDED';
859
+ current.progress.observerStatus = 'COMPLETED';
860
+ current.errorCode = null;
861
+ current.errorMessage = null;
862
+ current.completedAt = new Date();
863
+ }
864
+ }
826
865
  current.progress.progressVersion += 1;
827
866
  current.progress.lastProgressAt = new Date().toISOString();
828
- current.completedAt = new Date();
829
867
  return repo.save(current);
830
868
  });
831
- if (task.documentType === 'CONTROLLED_CATALOG')
832
- await this.recalculateProductRisks(scope, 'CATALOG_CHANGED', task.resultEntityId ?? undefined);
833
- else
834
- await this.recalculateCompanyRisks(scope, 'CATALOG_CHANGED');
869
+ if (task.status === 'SUCCEEDED' && (task.progress?.successCount ?? 0) > 0) {
870
+ if (task.documentType === 'CONTROLLED_CATALOG')
871
+ await this.recalculateProductRisks(scope, 'CATALOG_CHANGED', task.resultEntityId ?? undefined);
872
+ else
873
+ await this.recalculateCompanyRisks(scope, 'CATALOG_CHANGED');
874
+ }
835
875
  return task;
836
876
  }
837
877
  async waitImportProgress(scope, taskId, afterProgressVersion = 0, waitMs = 10_000) {
@@ -914,12 +954,14 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
914
954
  throw new DomainError(DomainErrorCodes.INVALID_INPUT, '复核块租约无效或已过期');
915
955
  }
916
956
  const acceptedCandidates = [];
957
+ const rejectedBlocks = [];
917
958
  let resolved = 0;
918
959
  let rejected = 0;
919
960
  for (const item of items) {
920
961
  const block = blocks.find(value => value.id === item.blockId);
921
962
  if (item.decision === 'REJECT') {
922
963
  block.status = 'REJECTED';
964
+ rejectedBlocks.push(block);
923
965
  rejected += 1;
924
966
  }
925
967
  else {
@@ -942,12 +984,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
942
984
  const batchRepo = manager.getRepository(ControlledCatalogBatch);
943
985
  const batch = await required(batchRepo.findOne({ where: { ...activeScope(scope), id: task.resultEntityId } }), '管控目录批次不存在');
944
986
  const rows = acceptedCandidates.flatMap(candidateToControlledRows);
945
- const localized = rows.map(row => ({
946
- productName: String(row.productName ?? '').trim(),
947
- controlDescription: optionalString(row.controlDescription),
948
- originalProductName: null, originalControlDescription: null,
949
- translationSource: row.translationSource ?? null
950
- }));
987
+ const localized = await this.localizeControlledCatalogRows(rows, scope.organizationId);
951
988
  await this.appendControlledCatalogRows(manager, scope, batch, rows, localized);
952
989
  await batchRepo.save(batch);
953
990
  task.progress.successCount = batch.successCount;
@@ -960,6 +997,13 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
960
997
  task.progress.duplicateCount = batch.duplicateCount;
961
998
  task.progress.invalidCount = batch.invalidCount;
962
999
  }
1000
+ const rejectedBatch = await this.appendRejectedCatalogReviews(manager, scope, task, rejectedBlocks);
1001
+ if (rejectedBatch) {
1002
+ task.progress.successCount = rejectedBatch.successCount;
1003
+ task.progress.duplicateCount = rejectedBatch.duplicateCount;
1004
+ task.progress.invalidCount = rejectedBatch.invalidCount;
1005
+ task.progress.skippedCount = rejectedBatch.skippedCount;
1006
+ }
963
1007
  await reviewRepo.save(blocks);
964
1008
  task.progress.reviewResolvedCount += resolved;
965
1009
  task.progress.reviewRejectedCount += rejected;
@@ -967,10 +1011,24 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
967
1011
  task.progress.progressVersion += 1;
968
1012
  task.progress.lastProgressAt = new Date().toISOString();
969
1013
  if (task.progress.reviewPendingCount === 0) {
970
- task.status = 'SUCCEEDED';
971
- task.progress.stage = 'SUCCEEDED';
972
- task.progress.observerStatus = 'COMPLETED';
973
- task.completedAt = new Date();
1014
+ const counts = await this.catalogBatchCounts(manager, scope, task);
1015
+ const outcome = catalogTerminalOutcome(counts);
1016
+ task.progress.successCount = counts.successCount;
1017
+ task.progress.duplicateCount = counts.duplicateCount;
1018
+ task.progress.invalidCount = counts.invalidCount;
1019
+ task.progress.skippedCount = counts.skippedCount;
1020
+ task.progress.outcome = outcome.outcome;
1021
+ if (outcome.status === 'FAILED') {
1022
+ await this.applyImportTaskFailure(manager, scope, task, 'FAILED', outcome.errorCode, outcome.errorMessage);
1023
+ }
1024
+ else {
1025
+ task.status = 'SUCCEEDED';
1026
+ task.progress.stage = 'SUCCEEDED';
1027
+ task.progress.observerStatus = 'COMPLETED';
1028
+ task.errorCode = null;
1029
+ task.errorMessage = null;
1030
+ task.completedAt = new Date();
1031
+ }
974
1032
  }
975
1033
  else {
976
1034
  task.status = 'AWAITING_REVIEW';
@@ -1039,6 +1097,148 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1039
1097
  }
1040
1098
  return batchRepo.save(batch);
1041
1099
  }
1100
+ async appendCatalogSkippedSources(repository, scope, batch, skippedSources) {
1101
+ for (const skipped of skippedSources) {
1102
+ const parsedPayload = { skipReason: skipped.reason, sourceLocation: skipped.sourceLocation };
1103
+ const existing = await repository.findOne({ where: {
1104
+ ...activeScope(scope), batchId: batch.id, issueType: 'INVALID', parsedPayload
1105
+ } });
1106
+ if (existing)
1107
+ continue;
1108
+ batch.totalCount += 1;
1109
+ batch.invalidCount += 1;
1110
+ batch.skippedCount += 1;
1111
+ await repository.save(repository.create({
1112
+ ...createdScope(scope), batchId: batch.id, issueType: 'INVALID', sourceSequence: batch.totalCount,
1113
+ sourceLocation: skipped.sourceLocation || null, sourceContent: skipped.sourceContent || null,
1114
+ parsedPayload, message: SKIPPED_REASON_MESSAGES[skipped.reason] ?? '无法入库的来源行已跳过'
1115
+ }));
1116
+ }
1117
+ }
1118
+ async appendRejectedCatalogReviews(manager, scope, task, blocks) {
1119
+ if (blocks.length === 0 || !task.resultEntityId)
1120
+ return null;
1121
+ const controlled = task.documentType === 'CONTROLLED_CATALOG';
1122
+ const batchRepo = controlled
1123
+ ? manager.getRepository(ControlledCatalogBatch)
1124
+ : manager.getRepository(SanctionCatalogBatch);
1125
+ const issueRepo = controlled
1126
+ ? manager.getRepository(ControlledCatalogImportIssue)
1127
+ : manager.getRepository(SanctionCatalogImportIssue);
1128
+ const batch = await required(batchRepo.findOne({ where: {
1129
+ ...activeScope(scope), id: task.resultEntityId
1130
+ } }), controlled ? '管控目录批次不存在' : '制裁目录批次不存在');
1131
+ for (const block of blocks) {
1132
+ const parsedPayload = { reviewBlockId: block.id, reviewDecision: 'REJECT' };
1133
+ const existing = await issueRepo.findOne({ where: {
1134
+ ...activeScope(scope), batchId: batch.id, issueType: 'INVALID', parsedPayload
1135
+ } });
1136
+ if (existing)
1137
+ continue;
1138
+ batch.totalCount += 1;
1139
+ batch.invalidCount += 1;
1140
+ batch.skippedCount += 1;
1141
+ await issueRepo.save(issueRepo.create({
1142
+ ...createdScope(scope), batchId: batch.id, issueType: 'INVALID', sourceSequence: batch.totalCount,
1143
+ sourceLocation: block.sourceLocation, sourceContent: block.sourceContent,
1144
+ parsedPayload, message: '审核未通过,来源行已跳过'
1145
+ }));
1146
+ }
1147
+ return batchRepo.save(batch);
1148
+ }
1149
+ async catalogBatchCounts(manager, scope, task) {
1150
+ if (!task.resultEntityId)
1151
+ throw new DomainError(DomainErrorCodes.INVALID_INPUT, '目录导入批次不存在');
1152
+ const batch = task.documentType === 'CONTROLLED_CATALOG'
1153
+ ? await required(manager.getRepository(ControlledCatalogBatch).findOne({
1154
+ where: { ...activeScope(scope), id: task.resultEntityId }
1155
+ }), '管控目录批次不存在')
1156
+ : await required(manager.getRepository(SanctionCatalogBatch).findOne({
1157
+ where: { ...activeScope(scope), id: task.resultEntityId }
1158
+ }), '制裁目录批次不存在');
1159
+ return {
1160
+ successCount: batch.successCount,
1161
+ duplicateCount: batch.duplicateCount,
1162
+ invalidCount: batch.invalidCount,
1163
+ skippedCount: batch.skippedCount ?? 0
1164
+ };
1165
+ }
1166
+ async rollbackFailedCatalogReplacement(manager, scope, task) {
1167
+ if (!task.resultEntityId || (task.documentType !== 'CONTROLLED_CATALOG' && task.documentType !== 'SANCTION_CATALOG'))
1168
+ return;
1169
+ const now = new Date();
1170
+ if (task.documentType === 'CONTROLLED_CATALOG') {
1171
+ const batchRepo = manager.getRepository(ControlledCatalogBatch);
1172
+ const recordRepo = manager.getRepository(ControlledGoodsRecord);
1173
+ const current = await batchRepo.findOne({ where: { ...activeScope(scope), id: task.resultEntityId } });
1174
+ if (!current?.replacedBatchId)
1175
+ return;
1176
+ const replaced = await batchRepo.findOne({ where: { ...activeScope(scope), id: current.replacedBatchId } });
1177
+ if (!replaced || replaced.deleteOperation !== 'CATALOG_REPLACE')
1178
+ return;
1179
+ current.deletedAt = now;
1180
+ current.deletedById = scope.userId ?? null;
1181
+ current.deleteOperation = 'IMPORT_FAILED';
1182
+ current.updatedById = scope.userId ?? null;
1183
+ await batchRepo.save(current);
1184
+ await recordRepo.update({
1185
+ ...activeScope(scope), batchId: current.id, deletedAt: IsNull()
1186
+ }, { deletedAt: now, deletedById: scope.userId ?? null, deleteOperation: 'IMPORT_FAILED', updatedById: scope.userId ?? null });
1187
+ replaced.deletedAt = null;
1188
+ replaced.deletedById = null;
1189
+ replaced.deleteOperation = null;
1190
+ replaced.updatedById = scope.userId ?? null;
1191
+ await batchRepo.save(replaced);
1192
+ await recordRepo.update({
1193
+ ...activeScope(scope), batchId: replaced.id, deleteOperation: 'CATALOG_REPLACE'
1194
+ }, { deletedAt: null, deletedById: null, deleteOperation: null, updatedById: scope.userId ?? null });
1195
+ return;
1196
+ }
1197
+ const batchRepo = manager.getRepository(SanctionCatalogBatch);
1198
+ const recordRepo = manager.getRepository(SanctionedCompanyRecord);
1199
+ const aliasRepo = manager.getRepository(SanctionedCompanyAlias);
1200
+ const current = await batchRepo.findOne({ where: { ...activeScope(scope), id: task.resultEntityId } });
1201
+ if (!current?.replacedBatchId)
1202
+ return;
1203
+ const replaced = await batchRepo.findOne({ where: { ...activeScope(scope), id: current.replacedBatchId } });
1204
+ if (!replaced || replaced.deleteOperation !== 'SANCTION_CATALOG_REPLACE')
1205
+ return;
1206
+ current.deletedAt = now;
1207
+ current.deletedById = scope.userId ?? null;
1208
+ current.deleteOperation = 'IMPORT_FAILED';
1209
+ current.updatedById = scope.userId ?? null;
1210
+ await batchRepo.save(current);
1211
+ const failedRecords = await recordRepo.find({
1212
+ where: { ...activeScope(scope), batchId: current.id, deletedAt: IsNull() }, select: { id: true }
1213
+ });
1214
+ if (failedRecords.length > 0) {
1215
+ await aliasRepo.update({
1216
+ ...activeScope(scope), sanctionedCompanyId: In(failedRecords.map(record => record.id)), deletedAt: IsNull()
1217
+ }, { deletedAt: now, deletedById: scope.userId ?? null, deleteOperation: 'IMPORT_FAILED', updatedById: scope.userId ?? null });
1218
+ }
1219
+ await recordRepo.update({
1220
+ ...activeScope(scope), batchId: current.id, deletedAt: IsNull()
1221
+ }, { deletedAt: now, deletedById: scope.userId ?? null, deleteOperation: 'IMPORT_FAILED', updatedById: scope.userId ?? null });
1222
+ replaced.deletedAt = null;
1223
+ replaced.deletedById = null;
1224
+ replaced.deleteOperation = null;
1225
+ replaced.updatedById = scope.userId ?? null;
1226
+ await batchRepo.save(replaced);
1227
+ await recordRepo.update({
1228
+ ...activeScope(scope), batchId: replaced.id, deleteOperation: 'SANCTION_CATALOG_REPLACE'
1229
+ }, { deletedAt: null, deletedById: null, deleteOperation: null, updatedById: scope.userId ?? null });
1230
+ }
1231
+ async applyImportTaskFailure(manager, scope, task, status, errorCode, errorMessage, completedAt = new Date()) {
1232
+ task.status = status;
1233
+ task.errorCode = errorCode;
1234
+ task.errorMessage = errorMessage;
1235
+ task.completedAt = completedAt;
1236
+ if (task.progress?.mode === 'FAST_PIPELINE') {
1237
+ task.progress.stage = 'FAILED';
1238
+ task.progress.observerStatus = 'FAILED';
1239
+ await this.rollbackFailedCatalogReplacement(manager, scope, task);
1240
+ }
1241
+ }
1042
1242
  async createImportTask(scope, input) {
1043
1243
  const repo = this.dataSource.getRepository(ImportTask);
1044
1244
  const targetAssistantId = requiredId(input.targetAssistantId, '目标智能体标识');
@@ -1075,10 +1275,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1075
1275
  if (!task)
1076
1276
  return null;
1077
1277
  if (!task.targetAssistantId) {
1078
- task.status = 'FAILED';
1079
- task.errorCode = 'TARGET_ASSISTANT_MISSING';
1080
- task.errorMessage = '历史解析任务缺少目标智能体,无法自动恢复';
1081
- task.completedAt = new Date();
1278
+ await this.applyImportTaskFailure(manager, scope, task, 'FAILED', 'TARGET_ASSISTANT_MISSING', '历史解析任务缺少目标智能体,无法自动恢复');
1082
1279
  await repo.save(task);
1083
1280
  return null;
1084
1281
  }
@@ -1102,10 +1299,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1102
1299
  if (!task)
1103
1300
  return null;
1104
1301
  if (!task.targetAssistantId) {
1105
- task.status = 'FAILED';
1106
- task.errorCode = 'TARGET_ASSISTANT_MISSING';
1107
- task.errorMessage = '历史解析任务缺少目标智能体,无法自动恢复';
1108
- task.completedAt = new Date();
1302
+ await this.applyImportTaskFailure(manager, scope, task, 'FAILED', 'TARGET_ASSISTANT_MISSING', '历史解析任务缺少目标智能体,无法自动恢复');
1109
1303
  await repo.save(task);
1110
1304
  return null;
1111
1305
  }
@@ -1181,10 +1375,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1181
1375
  const task = await repo.findOne({ where: { ...activeScope(snapshot), id: snapshot.id }, lock: { mode: 'pessimistic_write' } });
1182
1376
  if (!task || task.targetAssistantId || !RECOVERABLE_IMPORT_TASK_STATUSES.includes(task.status))
1183
1377
  return;
1184
- task.status = 'FAILED';
1185
- task.errorCode = 'TARGET_ASSISTANT_MISSING';
1186
- task.errorMessage = '历史解析任务缺少目标智能体,无法自动恢复';
1187
- task.completedAt = new Date();
1378
+ await this.applyImportTaskFailure(manager, snapshot, task, 'FAILED', 'TARGET_ASSISTANT_MISSING', '历史解析任务缺少目标智能体,无法自动恢复');
1188
1379
  await repo.save(task);
1189
1380
  });
1190
1381
  }
@@ -1213,10 +1404,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1213
1404
  return task;
1214
1405
  if (task.claimAttemptToken !== attemptToken)
1215
1406
  return task;
1216
- task.status = 'TIMED_OUT';
1217
- task.errorCode = 'AI_PARSING_TIMEOUT';
1218
- task.errorMessage = '解析任务超过 20 分钟未完成';
1219
- task.completedAt = new Date();
1407
+ await this.applyImportTaskFailure(manager, scope, task, 'TIMED_OUT', 'AI_PARSING_TIMEOUT', '解析任务超过 20 分钟未完成');
1220
1408
  return repo.save(task);
1221
1409
  });
1222
1410
  }
@@ -1328,10 +1516,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1328
1516
  return task;
1329
1517
  if (task.status === 'FAILED' || task.status === 'TIMED_OUT')
1330
1518
  return task;
1331
- task.status = 'FAILED';
1332
- task.errorCode = 'AGENT_HANDOFF_RESULT_MISSING';
1333
- task.errorMessage = '智能体已结束,但解析工具未保存业务结果';
1334
- task.completedAt = new Date();
1519
+ await this.applyImportTaskFailure(manager, scope, task, 'FAILED', 'AGENT_HANDOFF_RESULT_MISSING', '智能体已结束,但解析工具未保存业务结果');
1335
1520
  return repo.save(task);
1336
1521
  });
1337
1522
  }
@@ -1355,17 +1540,11 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1355
1540
  if (input.kind === 'complete' &&
1356
1541
  task.status !== 'AWAITING_REVIEW' && task.status !== 'SUCCEEDED' &&
1357
1542
  task.status !== 'FAILED' && task.status !== 'TIMED_OUT') {
1358
- task.status = 'FAILED';
1359
- task.errorCode = 'AGENT_HANDOFF_RESULT_MISSING';
1360
- task.errorMessage = '智能体已结束,但解析工具未保存业务结果';
1361
- task.completedAt = new Date();
1543
+ await this.applyImportTaskFailure(manager, scope, task, 'FAILED', 'AGENT_HANDOFF_RESULT_MISSING', '智能体已结束,但解析工具未保存业务结果');
1362
1544
  }
1363
1545
  else if (input.kind === 'error' &&
1364
1546
  !TERMINAL_IMPORT_TASK_STATUSES.includes(task.status)) {
1365
- task.status = 'FAILED';
1366
- task.errorCode = requiredId(input.errorCode, '错误代码');
1367
- task.errorMessage = requiredId(input.errorMessage, '错误信息');
1368
- task.completedAt = new Date();
1547
+ await this.applyImportTaskFailure(manager, scope, task, 'FAILED', requiredId(input.errorCode, '错误代码'), requiredId(input.errorMessage, '错误信息'));
1369
1548
  }
1370
1549
  await repo.save(task);
1371
1550
  return { accepted: true, sequence: input.sequence };
@@ -1377,10 +1556,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1377
1556
  const task = await required(repo.findOne({ where: { ...activeScope(scope), id }, lock: { mode: 'pessimistic_write' } }), '解析任务不存在');
1378
1557
  if (TERMINAL_IMPORT_TASK_STATUSES.includes(task.status))
1379
1558
  return task;
1380
- task.status = 'FAILED';
1381
- task.errorCode = failure.errorCode;
1382
- task.errorMessage = failure.errorMessage;
1383
- task.completedAt = new Date();
1559
+ await this.applyImportTaskFailure(manager, scope, task, 'FAILED', failure.errorCode, failure.errorMessage);
1384
1560
  return repo.save(task);
1385
1561
  });
1386
1562
  }
@@ -1397,10 +1573,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
1397
1573
  return task;
1398
1574
  if (!snapshotMissingRecoveryMetadataStillMissing(snapshot, task))
1399
1575
  return task;
1400
- task.status = 'FAILED';
1401
- task.errorCode = failure.errorCode;
1402
- task.errorMessage = failure.errorMessage;
1403
- task.completedAt = new Date();
1576
+ await this.applyImportTaskFailure(manager, scope, task, 'FAILED', failure.errorCode, failure.errorMessage);
1404
1577
  return repo.save(task);
1405
1578
  });
1406
1579
  }
@@ -3108,10 +3281,7 @@ let TradeComplianceWorkbenchService = class TradeComplianceWorkbenchService {
3108
3281
  const lockedStartedAt = locked.startedAt ?? locked.queuedAt ?? locked.createdAt;
3109
3282
  if (!lockedStartedAt || !importTaskTimedOut(lockedStartedAt, now))
3110
3283
  return;
3111
- locked.status = 'TIMED_OUT';
3112
- locked.errorCode = DomainErrorCodes.IMPORT_TIMED_OUT;
3113
- locked.errorMessage = '文件解析超过 20 分钟,请重新上传';
3114
- locked.completedAt = now;
3284
+ await this.applyImportTaskFailure(manager, scope, locked, 'TIMED_OUT', DomainErrorCodes.IMPORT_TIMED_OUT, '文件解析超过 20 分钟,请重新上传', now);
3115
3285
  locked.machineElapsedMs = now.getTime() - lockedStartedAt.getTime();
3116
3286
  await lockedRepo.save(locked);
3117
3287
  });
@@ -3549,7 +3719,7 @@ function optionalString(value) {
3549
3719
  const normalized = String(value ?? '').trim();
3550
3720
  return normalized || null;
3551
3721
  }
3552
- function initialFastCatalogProgress(totalCandidates) {
3722
+ function initialFastCatalogProgress(totalCandidates, inferenceMetadata) {
3553
3723
  return {
3554
3724
  version: 1,
3555
3725
  mode: 'FAST_PIPELINE',
@@ -3559,6 +3729,8 @@ function initialFastCatalogProgress(totalCandidates) {
3559
3729
  successCount: 0,
3560
3730
  duplicateCount: 0,
3561
3731
  invalidCount: 0,
3732
+ skippedCount: 0,
3733
+ inferenceMetadata,
3562
3734
  currentBatch: 0,
3563
3735
  totalBatches: totalCandidates > 0 ? Math.ceil(totalCandidates / 100) : 0,
3564
3736
  committedCursor: 0,
@@ -3577,6 +3749,7 @@ function candidateToControlledRows(candidate) {
3577
3749
  : [undefined];
3578
3750
  return hsCodes.map(hsCode => ({
3579
3751
  productName: optionalString(candidate.values.productName) ?? undefined,
3752
+ originalProductName: optionalString(candidate.values.originalProductName) ?? undefined,
3580
3753
  hsCode: typeof hsCode === 'string' ? hsCode : undefined,
3581
3754
  controlDescription: optionalString(candidate.values.controlDescription) ?? undefined,
3582
3755
  controlCode: optionalString(candidate.values.controlCode) ?? undefined,
@@ -3584,7 +3757,9 @@ function candidateToControlledRows(candidate) {
3584
3757
  sourceOrdinal: optionalString(candidate.values.sourceOrdinal) ?? String(candidate.ordinal),
3585
3758
  sourceLocation: evidence?.sourceLocation,
3586
3759
  sourceContent: evidence?.sourceContent,
3587
- translationSource: 'SOURCE_CHINESE'
3760
+ translationSource: candidate.values.translationRequired || (!candidate.values.productName && candidate.values.originalProductName)
3761
+ ? 'AGENT_TRANSLATED'
3762
+ : 'SOURCE_CHINESE'
3588
3763
  }));
3589
3764
  }
3590
3765
  function candidateAliases(candidate) {