@chenchaolong/plugin-trade-compliance-workbench 1.0.19 → 1.0.20
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/dist/lib/catalog-import/catalog-import.pipeline.d.ts.map +1 -1
- package/dist/lib/catalog-import/catalog-import.pipeline.js +2 -1
- package/dist/lib/catalog-import/catalog-import.pipeline.js.map +1 -1
- package/dist/lib/catalog-import/confidence.js +2 -2
- package/dist/lib/catalog-import/confidence.js.map +1 -1
- package/dist/lib/catalog-import/contracts.d.ts +5 -2
- package/dist/lib/catalog-import/contracts.d.ts.map +1 -1
- package/dist/lib/catalog-import/outcome.d.ts +19 -0
- package/dist/lib/catalog-import/outcome.d.ts.map +1 -0
- package/dist/lib/catalog-import/outcome.js +18 -0
- package/dist/lib/catalog-import/outcome.js.map +1 -0
- package/dist/lib/catalog-import/spreadsheet-structure.d.ts +15 -0
- package/dist/lib/catalog-import/spreadsheet-structure.d.ts.map +1 -0
- package/dist/lib/catalog-import/spreadsheet-structure.js +44 -0
- package/dist/lib/catalog-import/spreadsheet-structure.js.map +1 -0
- package/dist/lib/catalog-import/spreadsheet.parser.d.ts.map +1 -1
- package/dist/lib/catalog-import/spreadsheet.parser.js +71 -0
- package/dist/lib/catalog-import/spreadsheet.parser.js.map +1 -1
- package/dist/lib/entities/catalog.entity.d.ts +5 -1
- package/dist/lib/entities/catalog.entity.d.ts.map +1 -1
- package/dist/lib/entities/catalog.entity.js +16 -0
- package/dist/lib/entities/catalog.entity.js.map +1 -1
- package/dist/lib/remote-components/trade-compliance-workbench/app.js +5 -5
- package/dist/lib/remote-ui/app-source.js +18 -3
- package/dist/lib/remote-ui/app-source.js.map +1 -1
- package/dist/lib/remote-ui/contracts.d.ts +3 -0
- package/dist/lib/remote-ui/contracts.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/catalogs.d.ts +4 -0
- package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
- package/dist/lib/remote-ui/pages/catalogs.js +31 -2
- package/dist/lib/remote-ui/pages/catalogs.js.map +1 -1
- package/dist/lib/types.d.ts +12 -1
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +1 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/workbench.service.d.ts +7 -1
- package/dist/lib/workbench.service.d.ts.map +1 -1
- package/dist/lib/workbench.service.js +240 -65
- package/dist/lib/workbench.service.js.map +1 -1
- 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'];
|
|
@@ -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,
|
|
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
|
});
|
|
@@ -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
|
|
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 +=
|
|
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
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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.
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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 =
|
|
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);
|
|
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
|
-
|
|
971
|
-
|
|
972
|
-
task.progress.
|
|
973
|
-
task.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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) {
|