@chenchaolong/plugin-trade-compliance-workbench 1.0.18 → 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.
Files changed (96) hide show
  1. package/dist/lib/catalog-import/catalog-import.pipeline.d.ts.map +1 -1
  2. package/dist/lib/catalog-import/catalog-import.pipeline.js +2 -1
  3. package/dist/lib/catalog-import/catalog-import.pipeline.js.map +1 -1
  4. package/dist/lib/catalog-import/confidence.js +2 -2
  5. package/dist/lib/catalog-import/confidence.js.map +1 -1
  6. package/dist/lib/catalog-import/contracts.d.ts +5 -2
  7. package/dist/lib/catalog-import/contracts.d.ts.map +1 -1
  8. package/dist/lib/catalog-import/outcome.d.ts +19 -0
  9. package/dist/lib/catalog-import/outcome.d.ts.map +1 -0
  10. package/dist/lib/catalog-import/outcome.js +18 -0
  11. package/dist/lib/catalog-import/outcome.js.map +1 -0
  12. package/dist/lib/catalog-import/spreadsheet-structure.d.ts +15 -0
  13. package/dist/lib/catalog-import/spreadsheet-structure.d.ts.map +1 -0
  14. package/dist/lib/catalog-import/spreadsheet-structure.js +44 -0
  15. package/dist/lib/catalog-import/spreadsheet-structure.js.map +1 -0
  16. package/dist/lib/catalog-import/spreadsheet.parser.d.ts.map +1 -1
  17. package/dist/lib/catalog-import/spreadsheet.parser.js +71 -0
  18. package/dist/lib/catalog-import/spreadsheet.parser.js.map +1 -1
  19. package/dist/lib/domain/contract-number.js +2 -2
  20. package/dist/lib/domain/contract-number.js.map +1 -1
  21. package/dist/lib/entities/catalog.entity.d.ts +5 -1
  22. package/dist/lib/entities/catalog.entity.d.ts.map +1 -1
  23. package/dist/lib/entities/catalog.entity.js +16 -0
  24. package/dist/lib/entities/catalog.entity.js.map +1 -1
  25. package/dist/lib/remote-components/trade-compliance-workbench/app.css +1 -1
  26. package/dist/lib/remote-components/trade-compliance-workbench/app.js +5 -5
  27. package/dist/lib/remote-ui/app-source.d.ts.map +1 -1
  28. package/dist/lib/remote-ui/app-source.js +208 -67
  29. package/dist/lib/remote-ui/app-source.js.map +1 -1
  30. package/dist/lib/remote-ui/components/forms.js +1 -1
  31. package/dist/lib/remote-ui/components/forms.js.map +1 -1
  32. package/dist/lib/remote-ui/components/task-status.d.ts +2 -1
  33. package/dist/lib/remote-ui/components/task-status.d.ts.map +1 -1
  34. package/dist/lib/remote-ui/components/task-status.js +5 -4
  35. package/dist/lib/remote-ui/components/task-status.js.map +1 -1
  36. package/dist/lib/remote-ui/contracts.d.ts +3 -0
  37. package/dist/lib/remote-ui/contracts.d.ts.map +1 -1
  38. package/dist/lib/remote-ui/import-task-status.d.ts +10 -0
  39. package/dist/lib/remote-ui/import-task-status.d.ts.map +1 -0
  40. package/dist/lib/remote-ui/import-task-status.js +236 -0
  41. package/dist/lib/remote-ui/import-task-status.js.map +1 -0
  42. package/dist/lib/remote-ui/model.d.ts.map +1 -1
  43. package/dist/lib/remote-ui/model.js +2 -1
  44. package/dist/lib/remote-ui/model.js.map +1 -1
  45. package/dist/lib/remote-ui/pages/analytics-prototype.css +307 -0
  46. package/dist/lib/remote-ui/pages/catalogs.css +681 -0
  47. package/dist/lib/remote-ui/pages/catalogs.d.ts +9 -3
  48. package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
  49. package/dist/lib/remote-ui/pages/catalogs.js +36 -7
  50. package/dist/lib/remote-ui/pages/catalogs.js.map +1 -1
  51. package/dist/lib/remote-ui/pages/contracts.css +259 -0
  52. package/dist/lib/remote-ui/pages/contracts.d.ts.map +1 -1
  53. package/dist/lib/remote-ui/pages/contracts.js +36 -13
  54. package/dist/lib/remote-ui/pages/contracts.js.map +1 -1
  55. package/dist/lib/remote-ui/pages/files-prototype.css +547 -0
  56. package/dist/lib/remote-ui/pages/files.d.ts +3 -2
  57. package/dist/lib/remote-ui/pages/files.d.ts.map +1 -1
  58. package/dist/lib/remote-ui/pages/files.js +15 -15
  59. package/dist/lib/remote-ui/pages/files.js.map +1 -1
  60. package/dist/lib/remote-ui/pages/procurement-contracts.css +939 -0
  61. package/dist/lib/remote-ui/pages/procurement.js +1 -1
  62. package/dist/lib/remote-ui/pages/procurement.js.map +1 -1
  63. package/dist/lib/remote-ui/pages/sales-files.css +512 -0
  64. package/dist/lib/remote-ui/pages/sales.js +2 -2
  65. package/dist/lib/remote-ui/pages/sales.js.map +1 -1
  66. package/dist/lib/remote-ui/pages/settings.css +178 -0
  67. package/dist/lib/remote-ui/pages/settings.js +1 -1
  68. package/dist/lib/remote-ui/pages/settings.js.map +1 -1
  69. package/dist/lib/remote-ui/polling.d.ts +1 -1
  70. package/dist/lib/remote-ui/polling.d.ts.map +1 -1
  71. package/dist/lib/remote-ui/polling.js +4 -2
  72. package/dist/lib/remote-ui/polling.js.map +1 -1
  73. package/dist/lib/remote-ui/purchase-review-adapter.d.ts +4 -0
  74. package/dist/lib/remote-ui/purchase-review-adapter.d.ts.map +1 -0
  75. package/dist/lib/remote-ui/purchase-review-adapter.js +78 -0
  76. package/dist/lib/remote-ui/purchase-review-adapter.js.map +1 -0
  77. package/dist/lib/remote-ui/styles.css +588 -0
  78. package/dist/lib/sales-file.service.d.ts.map +1 -1
  79. package/dist/lib/sales-file.service.js +19 -7
  80. package/dist/lib/sales-file.service.js.map +1 -1
  81. package/dist/lib/types.d.ts +12 -1
  82. package/dist/lib/types.d.ts.map +1 -1
  83. package/dist/lib/types.js +1 -1
  84. package/dist/lib/types.js.map +1 -1
  85. package/dist/lib/workbench-view.provider.d.ts +24 -0
  86. package/dist/lib/workbench-view.provider.d.ts.map +1 -1
  87. package/dist/lib/workbench-view.provider.js +19 -4
  88. package/dist/lib/workbench-view.provider.js.map +1 -1
  89. package/dist/lib/workbench.plugin.d.ts.map +1 -1
  90. package/dist/lib/workbench.plugin.js +1 -2
  91. package/dist/lib/workbench.plugin.js.map +1 -1
  92. package/dist/lib/workbench.service.d.ts +7 -1
  93. package/dist/lib/workbench.service.d.ts.map +1 -1
  94. package/dist/lib/workbench.service.js +276 -75
  95. package/dist/lib/workbench.service.js.map +1 -1
  96. package/package.json +4 -1
@@ -1 +1 @@
1
- {"version":3,"file":"app-source.d.ts","sourceRoot":"","sources":["../../../src/lib/remote-ui/app-source.tsx"],"names":[],"mappings":"AA4BA,OAAO,cAAc,CAAA"}
1
+ {"version":3,"file":"app-source.d.ts","sourceRoot":"","sources":["../../../src/lib/remote-ui/app-source.tsx"],"names":[],"mappings":"AA8BA,OAAO,cAAc,CAAA"}
@@ -13,12 +13,14 @@ import { buildBreadcrumbs, WorkbenchShell } from './components/layout.js';
13
13
  import { CatalogCommands, CatalogImportError, ControlledGoodsPage, findActiveCatalogImportTasks, findLatestFailedCatalogImportTask, runCatalogImportOrchestration, SanctionedCompaniesPage } from './pages/catalogs.js';
14
14
  import { HomePage } from './pages/home.js';
15
15
  import { ProcurementPage } from './pages/procurement.js';
16
+ import { adaptPurchaseReview } from './purchase-review-adapter.js';
16
17
  import { CustomerContractsPage, adaptCustomerContractReview } from './pages/contracts.js';
17
18
  import { SalesPage } from './pages/sales.js';
18
19
  import { SalesFilesPage } from './pages/files.js';
19
20
  import { AnalyticsPage, AnalyticsPrototypeBoard } from './pages/analytics.js';
20
21
  import { SettingsPage } from './pages/settings.js';
21
22
  import { ContractImportError, findRecoverableContractImport, findResumableContractImports, runContractImportOrchestration, runCustomerContractReanalysis, taskTransition } from './contract-import.js';
23
+ import { importTaskFromStart, importTaskProgressPercent, isLocalUploadPlaceholder, mergeImportTaskItems } from './import-task-status.js';
22
24
  import { compareTaskBusinessOrder, createPendingResultQueue, createPollingTaskTracker, isAbortError, isPollingObservationTimeout, submitAndRefreshTaskState } from './polling.js';
23
25
  import { ACTION_NOTIFICATION_POLICIES, executeWorkbenchAction } from './action-execution.js';
24
26
  import './styles.css';
@@ -34,12 +36,14 @@ const pageMeta = {
34
36
  settings: { title: '系统设置', subtitle: '维护企业、银行、汇率和利润规则。' }
35
37
  };
36
38
  const procurementPageMeta = {
37
- suppliers: { title: '供应商列表', subtitle: '一级页面:管理供应商主档,并通过供应商代码进入该供应商的采购订单列表。' },
39
+ suppliers: { title: '供应商列表', subtitle: '管理供应商主档,并通过供应商代码进入该供应商的采购订单列表。' },
38
40
  orders: { title: '采购订单列表', subtitle: '查看当前供应商的采购订单、金额及合规状态。' },
39
41
  lines: { title: '采购商品列表', subtitle: '查看当前采购订单的商品明细及合规状态。' }
40
42
  };
41
43
  const bridge = createRemoteBridge();
42
44
  const initialAnalyticsQuery = { mode: 'PURCHASE', dimension: 'SUPPLIER', filters: {}, page: 1, pageSize: 20 };
45
+ let summaryRequestSequence = 0;
46
+ const DOWNLOAD_URL_REVOKE_DELAY_MS = 30_000;
43
47
  function comparePendingReviewBusinessOrder(left, right) {
44
48
  const leftTask = left.kind === 'CONTRACT_IMPORT' ? left.completion.task : left.result.run;
45
49
  const rightTask = right.kind === 'CONTRACT_IMPORT' ? right.completion.task : right.result.run;
@@ -72,6 +76,7 @@ function App() {
72
76
  const [analyticsPrototypeVisible, setAnalyticsPrototypeVisible] = React.useState(false);
73
77
  const [salesFilesPrototypeState, setSalesFilesPrototypeState] = React.useState(undefined);
74
78
  const [data, setData] = React.useState(() => normalizeWorkbenchData(null));
79
+ const importTaskItemsRef = React.useRef([]);
75
80
  const [busy, setBusy] = React.useState(false);
76
81
  const [actionBusy, setActionBusy] = React.useState(false);
77
82
  const [catalogCommand, setCatalogCommand] = React.useState(null);
@@ -106,6 +111,42 @@ function App() {
106
111
  toastTimers.dismiss(id);
107
112
  setToasts((items) => removeToast(items, id));
108
113
  }, [toastTimers]);
114
+ const notifyTaskStatusActionError = React.useCallback((error) => {
115
+ setToast({ message: error instanceof Error ? error.message : '任务操作失败', tone: 'error' });
116
+ }, [setToast]);
117
+ const notifyTaskRecoveryRefreshError = React.useCallback((error) => {
118
+ setToast({ message: error instanceof Error ? `任务状态刷新失败:${error.message}` : '任务状态刷新失败', tone: 'error' });
119
+ }, [setToast]);
120
+ const handleBackgroundImportTrackingError = React.useCallback((error) => {
121
+ if (isAbortError(error) || isPollingObservationTimeout(error))
122
+ return;
123
+ setToast({ message: error instanceof Error ? `后台任务跟踪失败:${error.message}` : '后台任务跟踪失败', tone: 'error' });
124
+ }, [setToast]);
125
+ const upsertLocalImportTask = React.useCallback((task) => {
126
+ setData(current => {
127
+ const items = mergeImportTaskItems(current.importTasks.items, [task]);
128
+ return {
129
+ ...current,
130
+ importTasks: {
131
+ ...current.importTasks,
132
+ items,
133
+ total: Math.max(numberValue(current.importTasks.total, 0), items.length),
134
+ page: numberValue(current.importTasks.page, 1),
135
+ pageSize: current.importTasks.pageSize ?? 100
136
+ }
137
+ };
138
+ });
139
+ }, []);
140
+ const loadImportTasksFallback = React.useCallback(async (normalized) => {
141
+ try {
142
+ const payload = asRecord(unwrapResponse(await bridge.requestData(importTasksCollectionRequest())));
143
+ const importTasks = normalizeWorkbenchData({ summary: { importTasks: payload } }).importTasks;
144
+ return importTasks.items.length > 0 || numberValue(importTasks.total, 0) > 0 ? importTasks : normalized.importTasks;
145
+ }
146
+ catch {
147
+ return normalized.importTasks;
148
+ }
149
+ }, []);
109
150
  const configureAnalyticsPrototypeCapability = React.useCallback((parameters) => {
110
151
  analyticsPrototypeListenerCleanupRef.current?.();
111
152
  analyticsPrototypeListenerCleanupRef.current = null;
@@ -188,11 +229,12 @@ function App() {
188
229
  const requestId = gate.begin();
189
230
  setBusy(true);
190
231
  try {
191
- const response = await bridge.requestData({ page: 1, pageSize: 100, parameters: active.initialQuery?.parameters ?? {} });
232
+ const response = await bridge.requestData(summaryRequestQuery(active.initialQuery?.parameters));
192
233
  if (gate.isLatest(requestId)) {
193
234
  const normalized = normalizeWorkbenchData(response);
235
+ const importTasks = await loadImportTasksFallback(normalized);
194
236
  bridge.configure({ uploadPolicy: normalized.uploadPolicy });
195
- setData(normalized);
237
+ setData(current => mergeWorkbenchDataWithLocalTasks(current, { ...normalized, importTasks }));
196
238
  await loadRoute(routeRef.current);
197
239
  }
198
240
  }
@@ -204,7 +246,7 @@ function App() {
204
246
  gate.finish(requestId);
205
247
  setBusy(gate.activeCount() + routeRequestGateRef.current.activeCount() + analyticsRequestsRef.current.activeCount() > 0);
206
248
  }
207
- }, [loadRoute]);
249
+ }, [loadImportTasksFallback, loadRoute]);
208
250
  const loadTaskState = React.useCallback(async () => {
209
251
  const active = contextRef.current;
210
252
  if (!active)
@@ -212,17 +254,22 @@ function App() {
212
254
  const gate = taskStateRequestGateRef.current;
213
255
  const requestId = gate.begin();
214
256
  try {
215
- const response = await bridge.requestData({ page: 1, pageSize: 100, parameters: active.initialQuery?.parameters ?? {} });
257
+ const response = await bridge.requestData(summaryRequestQuery(active.initialQuery?.parameters));
216
258
  if (!gate.commit(requestId))
217
259
  return false;
218
260
  const normalized = normalizeWorkbenchData(response);
261
+ const importTasks = await loadImportTasksFallback(normalized);
219
262
  setData(current => ({
220
263
  ...current,
221
264
  controlledGoods: normalized.controlledGoods,
222
265
  controlledBatches: normalized.controlledBatches,
223
266
  sanctionedCompanies: normalized.sanctionedCompanies,
224
267
  sanctionBatches: normalized.sanctionBatches,
225
- importTasks: normalized.importTasks,
268
+ importTasks: {
269
+ ...importTasks,
270
+ items: mergeImportTaskItems(current.importTasks.items, importTasks.items),
271
+ total: Math.max(numberValue(importTasks.total, 0), importTasks.items.length, current.importTasks.items.length)
272
+ },
226
273
  customerContractAnalyses: normalized.customerContractAnalyses
227
274
  }));
228
275
  return true;
@@ -235,7 +282,7 @@ function App() {
235
282
  finally {
236
283
  gate.finish(requestId);
237
284
  }
238
- }, []);
285
+ }, [loadImportTasksFallback]);
239
286
  const scheduleTaskStateRecovery = React.useCallback(() => {
240
287
  if (taskRecoveryTimerRef.current != null)
241
288
  return;
@@ -243,11 +290,16 @@ function App() {
243
290
  void loadTaskState().then(refreshed => {
244
291
  if (!refreshed || taskRecoveryTimerRef.current == null)
245
292
  return;
246
- window.clearInterval(taskRecoveryTimerRef.current);
247
- taskRecoveryTimerRef.current = null;
293
+ if (!hasLocalUploadPlaceholders(importTaskItemsRef.current)) {
294
+ window.clearInterval(taskRecoveryTimerRef.current);
295
+ taskRecoveryTimerRef.current = null;
296
+ }
248
297
  });
249
298
  }, 2000);
250
299
  }, [loadTaskState]);
300
+ React.useEffect(() => {
301
+ importTaskItemsRef.current = data.importTasks.items;
302
+ }, [data.importTasks.items]);
251
303
  React.useEffect(() => () => {
252
304
  if (taskRecoveryTimerRef.current != null)
253
305
  window.clearInterval(taskRecoveryTimerRef.current);
@@ -270,7 +322,7 @@ function App() {
270
322
  contextRef.current = nextContext;
271
323
  const normalized = normalizeWorkbenchData(nextContext.payload);
272
324
  bridge.configure({ uploadPolicy: normalized.uploadPolicy });
273
- setData(normalized);
325
+ setData(current => mergeWorkbenchDataWithLocalTasks(current, normalized));
274
326
  void load(nextContext);
275
327
  },
276
328
  onHostEvent() {
@@ -385,6 +437,7 @@ function App() {
385
437
  signal,
386
438
  pollTask: async (taskId) => asContractCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId } }))),
387
439
  onTransition: (previous, next) => {
440
+ upsertLocalImportTask(next);
388
441
  const notice = taskTransition(previous, next);
389
442
  if (notice)
390
443
  setToast(notice);
@@ -394,6 +447,7 @@ function App() {
394
447
  if (completion.task.status === 'AWAITING_REVIEW') {
395
448
  enqueuePendingReview({ kind: 'CONTRACT_IMPORT', completion });
396
449
  }
450
+ upsertLocalImportTask(completion.task);
397
451
  return completion;
398
452
  }
399
453
  catch (error) {
@@ -415,22 +469,85 @@ function App() {
415
469
  setToast({ message: error instanceof Error ? error.message : '合同解析失败', tone: 'error' });
416
470
  throw error;
417
471
  }
418
- }, [enqueuePendingReview, scheduleTaskStateRecovery]);
472
+ }, [enqueuePendingReview, scheduleTaskStateRecovery, upsertLocalImportTask]);
473
+ const startTrackingUploadedImportTask = React.useCallback((task) => {
474
+ if (task.documentType === 'PURCHASE_CONTRACT' || task.documentType === 'CUSTOMER_CONTRACT') {
475
+ const running = contractTaskTrackerRef.current.run(task.id, signal => orchestrateContractImport({ success: true, data: { task: task } }, signal));
476
+ if (running)
477
+ void running.catch(handleBackgroundImportTrackingError);
478
+ return;
479
+ }
480
+ if (task.documentType === 'CONTROLLED_CATALOG' || task.documentType === 'SANCTION_CATALOG') {
481
+ const running = catalogTaskTrackerRef.current.run(task.id, signal => orchestrateImport({ success: true, data: { task: task } }, signal));
482
+ if (running)
483
+ void running.catch(handleBackgroundImportTrackingError);
484
+ }
485
+ }, [handleBackgroundImportTrackingError, orchestrateContractImport, orchestrateImport]);
486
+ const presentImmediateContractUploadReview = React.useCallback(async (actionKey, start) => {
487
+ const data = asRecord(start.data);
488
+ const task = importTaskFromStart(start, actionKey);
489
+ if (actionKey === 'upload_customer_contract' && data.review) {
490
+ blobUrlOwnerRef.current.clear('customer');
491
+ const reviewTask = task ? { ...task, status: 'AWAITING_REVIEW' } : null;
492
+ if (reviewTask?.id) {
493
+ activeReviewResultRef.current = reviewTask.id;
494
+ handledContractTasksRef.current.add(reviewTask.id);
495
+ upsertLocalImportTask(reviewTask);
496
+ }
497
+ const review = adaptCustomerContractReview(data.review, {
498
+ reviewSource: 'IMPORT_DRAFT',
499
+ importTaskId: reviewTask?.id,
500
+ sourceFileId: reviewTask?.sourceFileId ?? '',
501
+ sourceFileName: reviewTask?.sourceFileName ?? ''
502
+ });
503
+ setCustomerImportState({ status: 'AWAITING_REVIEW', importTaskId: reviewTask?.id ?? '', review: await attachCustomerSource(review, blobUrlOwnerRef.current) });
504
+ changeRouteRef.current({ type: 'page', page: 'customer-contract-review' });
505
+ return true;
506
+ }
507
+ if (actionKey === 'upload_purchase_contract' && task?.documentType === 'PURCHASE_CONTRACT' && task.resultPayload) {
508
+ blobUrlOwnerRef.current.clear('purchase');
509
+ const reviewTask = { ...task, status: 'AWAITING_REVIEW' };
510
+ activeReviewResultRef.current = reviewTask.id;
511
+ handledContractTasksRef.current.add(reviewTask.id);
512
+ upsertLocalImportTask(reviewTask);
513
+ const draft = adaptPurchaseReview(reviewTask);
514
+ setPurchaseImportTask(reviewTask);
515
+ setPurchaseReviewDraft(await attachPurchaseSource(draft, reviewTask, blobUrlOwnerRef.current));
516
+ changeRouteRef.current({ type: 'procurement', level: 'suppliers' });
517
+ return true;
518
+ }
519
+ return false;
520
+ }, [upsertLocalImportTask]);
419
521
  const executeFileAction = React.useCallback(async (actionKey, file, input, notifyError = true) => {
420
522
  try {
421
- const raw = await submitAndRefreshTaskState(() => bridge.executeFileAction(actionKey, file, input), loadTaskState);
523
+ const raw = await bridge.executeFileAction(actionKey, file, input);
422
524
  const start = asActionResult(unwrapResponse(raw));
525
+ const task = importTaskFromStart(start, actionKey, input, file);
526
+ if (task) {
527
+ upsertLocalImportTask(task);
528
+ if (isLocalUploadPlaceholder(task))
529
+ scheduleTaskStateRecovery();
530
+ else
531
+ startTrackingUploadedImportTask(task);
532
+ }
423
533
  if (start.success === false)
424
534
  throw actionResponseError(start, '上传失败');
535
+ await loadTaskState().catch((refreshError) => {
536
+ notifyTaskRecoveryRefreshError(refreshError instanceof Error ? refreshError : new Error(String(refreshError)));
537
+ });
538
+ await presentImmediateContractUploadReview(actionKey, start);
425
539
  setToast({ message: resolveActionMessage(start) || '解析任务已提交', tone: 'info' });
426
540
  return start;
427
541
  }
428
542
  catch (error) {
543
+ await loadTaskState().catch((refreshError) => {
544
+ notifyTaskRecoveryRefreshError(refreshError instanceof Error ? refreshError : new Error(String(refreshError)));
545
+ });
429
546
  if (notifyError && !(error instanceof CatalogImportError))
430
547
  setToast({ message: error instanceof Error ? error.message : '上传失败', tone: 'error' });
431
548
  throw error;
432
549
  }
433
- }, [loadTaskState]);
550
+ }, [loadTaskState, notifyTaskRecoveryRefreshError, presentImmediateContractUploadReview, scheduleTaskStateRecovery, startTrackingUploadedImportTask, upsertLocalImportTask]);
434
551
  const executeCatalogFileAction = React.useCallback((actionKey, file, input) => executeFileAction(actionKey, file, input, false), [executeFileAction]);
435
552
  React.useEffect(() => {
436
553
  const tasks = data.importTasks.items;
@@ -441,9 +558,9 @@ function App() {
441
558
  for (const active of findActiveCatalogImportTasks(tasks)) {
442
559
  const running = catalogTaskTrackerRef.current.run(active.id, signal => orchestrateImport({ success: true, data: { task: active } }, signal));
443
560
  if (running)
444
- void running.catch(() => undefined);
561
+ void running.catch(handleBackgroundImportTrackingError);
445
562
  }
446
- }, [data.importTasks.items, orchestrateImport]);
563
+ }, [data.importTasks.items, handleBackgroundImportTrackingError, orchestrateImport]);
447
564
  React.useEffect(() => {
448
565
  for (const task of data.importTasks.items) {
449
566
  const conversationId = task.observerConversationId;
@@ -462,7 +579,7 @@ function App() {
462
579
  continue;
463
580
  const running = contractTaskTrackerRef.current.run(resumable.id, signal => orchestrateContractImport({ success: true, data: { task: resumable } }, signal));
464
581
  if (running)
465
- void running.catch(() => undefined);
582
+ void running.catch(handleBackgroundImportTrackingError);
466
583
  }
467
584
  for (const documentType of ['PURCHASE_CONTRACT', 'CUSTOMER_CONTRACT']) {
468
585
  const failed = findRecoverableContractImport(tasks, documentType);
@@ -473,7 +590,7 @@ function App() {
473
590
  else
474
591
  setCustomerImportState({ status: 'FAILED', importTaskId: failed.id, message: failed.errorMessage || '客户合同解析失败,请重新解析' });
475
592
  }
476
- if (!hasResumable && !failed && tasks.some(item => item.documentType === documentType && item.status === 'SUCCEEDED')) {
593
+ if (!activeReviewResultRef.current && !hasResumable && !failed && tasks.some(item => item.documentType === documentType && item.status === 'SUCCEEDED')) {
477
594
  if (documentType === 'PURCHASE_CONTRACT') {
478
595
  setPurchaseImportTask(null);
479
596
  setPurchaseReviewDraft(null);
@@ -482,10 +599,10 @@ function App() {
482
599
  setCustomerImportState({ status: 'IDLE' });
483
600
  }
484
601
  }
485
- }, [data.importTasks.items, orchestrateContractImport]);
602
+ }, [data.importTasks.items, handleBackgroundImportTrackingError, orchestrateContractImport]);
486
603
  const retryImport = React.useCallback(async (taskId) => {
487
604
  try {
488
- const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState);
605
+ const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState, notifyTaskRecoveryRefreshError);
489
606
  const start = asActionResult(unwrapResponse(raw));
490
607
  if (start.success === false)
491
608
  throw actionResponseError(start, '重新解析失败');
@@ -496,10 +613,10 @@ function App() {
496
613
  setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
497
614
  throw error;
498
615
  }
499
- }, [loadTaskState]);
616
+ }, [loadTaskState, notifyTaskRecoveryRefreshError]);
500
617
  const retryContractImport = React.useCallback(async (taskId) => {
501
618
  try {
502
- const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState);
619
+ const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState, notifyTaskRecoveryRefreshError);
503
620
  const start = asActionResult(unwrapResponse(raw));
504
621
  if (start.success === false)
505
622
  throw actionResponseError(start, '重新解析失败');
@@ -509,7 +626,7 @@ function App() {
509
626
  setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
510
627
  throw error;
511
628
  }
512
- }, [loadTaskState]);
629
+ }, [loadTaskState, notifyTaskRecoveryRefreshError]);
513
630
  const openImportTaskFromStatus = React.useCallback(async (task) => {
514
631
  if (task.status === 'FAILED' || task.status === 'TIMED_OUT') {
515
632
  if (task.documentType === 'PURCHASE_CONTRACT' || task.documentType === 'CUSTOMER_CONTRACT')
@@ -565,7 +682,7 @@ function App() {
565
682
  const reanalyzeCustomerContract = React.useCallback(async (input) => {
566
683
  setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析中' });
567
684
  try {
568
- const raw = await submitAndRefreshTaskState(() => bridge.executeAction('reanalyze_customer_contract', { reviewSource: input.reviewSource, ...(input.customerContractId ? { id: input.customerContractId } : {}), ...(input.importTaskId ? { importTaskId: input.importTaskId } : {}) }), loadTaskState);
685
+ const raw = await submitAndRefreshTaskState(() => bridge.executeAction('reanalyze_customer_contract', { reviewSource: input.reviewSource, ...(input.customerContractId ? { id: input.customerContractId } : {}), ...(input.importTaskId ? { importTaskId: input.importTaskId } : {}) }), loadTaskState, notifyTaskRecoveryRefreshError);
569
686
  const response = asActionResult(unwrapResponse(raw));
570
687
  if (response.success === false || !response.data)
571
688
  throw actionResponseError(response, 'AI 重新分析启动失败');
@@ -727,7 +844,22 @@ function App() {
727
844
  const requestId = gate.begin();
728
845
  setBusy(true);
729
846
  try {
730
- const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection, filters: encodeViewFilters(query.filters) } })));
847
+ const requestCatalogPage = async (page) => asRecord(unwrapResponse(await bridge.requestData({
848
+ page,
849
+ pageSize: requestedPageSize,
850
+ parameters: { collection, filters: encodeViewFilters(query.filters), refreshNonce: nextSummaryRequestNonce() }
851
+ })));
852
+ let committedPage = query.page;
853
+ let payload = await requestCatalogPage(committedPage);
854
+ let total = numberValue(payload.total, 0);
855
+ const maxPage = Math.max(1, Math.ceil(total / requestedPageSize));
856
+ if (query.page > maxPage) {
857
+ if (!gate.isLatest(requestId))
858
+ return;
859
+ committedPage = maxPage;
860
+ payload = await requestCatalogPage(maxPage);
861
+ total = numberValue(payload.total, 0);
862
+ }
731
863
  if (!gate.isLatest(requestId))
732
864
  return;
733
865
  setData((current) => ({
@@ -735,8 +867,8 @@ function App() {
735
867
  [collection]: {
736
868
  ...current[collection],
737
869
  items: Array.isArray(payload.items) ? payload.items : [],
738
- total: numberValue(payload.total, 0),
739
- page: numberValue(payload.page, query.page),
870
+ total,
871
+ page: numberValue(payload.page, committedPage),
740
872
  pageSize: requestedPageSize
741
873
  }
742
874
  }));
@@ -773,7 +905,7 @@ function App() {
773
905
  }, []);
774
906
  const queryProcurement = React.useCallback(async (_collection, query) => {
775
907
  await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
776
- }, [loadRoute]);
908
+ }, [loadImportTasksFallback, loadRoute]);
777
909
  const queryCustomerContracts = React.useCallback(async (query) => {
778
910
  await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
779
911
  }, [loadRoute]);
@@ -824,7 +956,7 @@ function App() {
824
956
  requests.finish(requestId);
825
957
  setBusy(requests.activeCount() + requestGateRef.current.activeCount() + routeRequestGateRef.current.activeCount() > 0);
826
958
  }
827
- }, []);
959
+ }, [loadImportTasksFallback]);
828
960
  const exportAnalytics = React.useCallback(async (_request) => {
829
961
  setActionBusy(true);
830
962
  try {
@@ -907,14 +1039,14 @@ function App() {
907
1039
  const taskStatusItems = React.useMemo(() => buildTaskStatusItems(data.importTasks.items, openImportTaskFromStatus), [data.importTasks.items, openImportTaskFromStatus]);
908
1040
  const contractCommands = page === 'procurement' && route.type === 'procurement'
909
1041
  ? route.level === 'suppliers'
910
- ? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u91C7\u8D2D\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_purchase_contract', file, { documentType: 'PURCHASE_CONTRACT' }) })
1042
+ ? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u91C7\u8D2D\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_purchase_contract', file, { documentType: 'PURCHASE_CONTRACT' }), onError: message => setToast({ message, tone: 'error' }) })
911
1043
  : undefined
912
1044
  : page === 'customer-contract-review'
913
- ? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u5BA2\u6237\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_customer_contract', file, { documentType: 'CUSTOMER_CONTRACT' }) })
1045
+ ? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u5BA2\u6237\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_customer_contract', file, { documentType: 'CUSTOMER_CONTRACT' }), onError: message => setToast({ message, tone: 'error' }) })
914
1046
  : undefined;
915
1047
  if (analyticsPrototypeVisible)
916
- return (_jsx(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, topbarActions: _jsx(TaskStatusMenu, { items: taskStatusItems, busy: busy || actionBusy }), onRouteChange: changeRoute, onRefresh: () => void load(), children: _jsx(AnalyticsPrototypeBoard, { onClose: () => setAnalyticsPrototypeVisible(false) }) }));
917
- return (_jsxs(_Fragment, { children: [_jsxs(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, subtitle: page === 'home' ? undefined : meta.subtitle, topbarActions: _jsx(TaskStatusMenu, { items: taskStatusItems, busy: busy || actionBusy }), commands: (page === 'controlled-goods' || page === 'sanctioned-companies') ? _jsx(CatalogCommands, { busy: busy || actionBusy, onUpload: () => setCatalogCommand('upload'), onCreate: () => setCatalogCommand('create') }) : contractCommands, busy: busy || actionBusy, onRouteChange: changeRoute, onRefresh: () => void load(), children: [page === 'home' && _jsx(HomePage, {}), page === 'controlled-goods' && _jsx(ControlledGoodsPage, { collection: data.controlledGoods, batches: data.controlledBatches.items, busy: busy || actionBusy, onAction: executeAction, onFileAction: executeCatalogFileAction, onRefresh: load, onQuery: (query) => queryCatalog('controlledGoods', query), onIssueQuery: queryImportIssues, importCompletion: importCompletions.CONTROLLED_CATALOG, importFailure: importFailures.CONTROLLED_CATALOG, onRetryImport: retryImport, command: catalogCommand, onCommandHandled: () => setCatalogCommand(null), onNotify: setToast }), page === 'sanctioned-companies' && _jsx(SanctionedCompaniesPage, { collection: data.sanctionedCompanies, batches: data.sanctionBatches.items, busy: busy || actionBusy, onAction: executeAction, onFileAction: executeCatalogFileAction, onRefresh: load, onQuery: (query) => queryCatalog('sanctionedCompanies', query), onIssueQuery: queryImportIssues, importCompletion: importCompletions.SANCTION_CATALOG, importFailure: importFailures.SANCTION_CATALOG, onRetryImport: retryImport, command: catalogCommand, onCommandHandled: () => setCatalogCommand(null), onNotify: setToast }), page === 'procurement' && route.type === 'procurement' && _jsx(ProcurementPage, { route: route, suppliers: data.suppliers, purchaseOrders: data.purchaseOrders, purchaseLines: data.purchaseLines, supplier: procurementContext.supplier, order: procurementContext.order, metrics: procurementContext.metrics, importTask: purchaseImportTask, reviewDraft: purchaseReviewDraft, busy: busy || actionBusy, onQuery: queryProcurement, onAction: executeAction, onFileAction: executeFileAction, onRouteChange: changeRoute, onRetryImport: retryContractImport, onReviewClose: closePurchaseReview, onNotify: setToast }), page === 'customer-contract-review' && _jsx(CustomerContractsPage, { collection: data.customerContracts, busy: busy || actionBusy, importReviewState: customerImportState, onQuery: queryCustomerContracts, onAction: executeAction, onFileAction: executeFileAction, onReanalyze: reanalyzeCustomerContract, onLoadReview: loadCustomerContractReview, onReviewClose: closeCustomerReview, reanalysisState: customerReanalysisState, onRetryImport: retryContractImport, onNotify: setToast }), page === 'sales' && route.type === 'sales' && _jsx(SalesPage, { route: route, customers: data.salesCustomers, orders: data.salesOrders, lines: data.salesLines, busy: busy || actionBusy, onQuery: querySales, onAction: executeAction, onRouteChange: changeRoute, onOpenFiles: openSalesFiles, onNotify: setToast }), page === 'sales-files' && _jsx(SalesFilesPage, { orders: data.salesFileOrders, history: data.generatedFiles, activeOrder: activeSalesFileOrder, visualPrototypeState: salesFilesPrototypeState, banks: data.settings.banks, busy: busy || actionBusy, onQuery: querySalesFiles, onAction: executeSalesFileAction, onOpenSalesOrder: selectSalesFileOrder, onDownload: file => executeAction('download_sales_file', { id: file.id }).then(() => undefined), onEdit: async (file) => setEditingSalesFile(file), onNotify: setToast }), page === 'analytics' && _jsx(AnalyticsPage, { data: data.analytics, committedQuery: analyticsCommittedQuery, busy: busy || actionBusy, error: analyticsError, onQuery: queryAnalytics, onExportXlsx: exportAnalytics }), page === 'settings' && _jsx(SettingsPage, { data: data.settings, busy: busy || actionBusy, onSave: saveSettings, onRestoreDefault: restoreSettings, onDeleteRate: deleteSettingsRate, onRateQuery: queryMonthlyRates, onUploadLogo: uploadCompanyLogo, onDiscardLogo: discardCompanyLogo, onNotify: setToast }), !['home', 'controlled-goods', 'sanctioned-companies', 'procurement', 'customer-contract-review', 'sales', 'sales-files', 'analytics', 'settings'].includes(page) && _jsxs("section", { className: "tcw-page-placeholder", "data-page": page, children: [_jsx(InfoNotice, { children: "\u9875\u9762\u57FA\u7840\u7ED3\u6784\u5DF2\u5C31\u7EEA\uFF0C\u4E1A\u52A1\u5185\u5BB9\u5C06\u5728\u5BF9\u5E94\u9875\u9762\u6A21\u5757\u4E2D\u52A0\u8F7D\u3002" }), _jsx("div", { className: "tcw-placeholder-body", "aria-label": `${meta.title}页面内容` })] })] }), _jsx(LoadingBlocker, { visible: busy || actionBusy, label: actionBusy ? '操作处理中' : '数据加载中' }), _jsx(ToastStack, { items: toasts, onClose: closeToast }), editingSalesFile && _jsx(SalesFileEditor, { file: editingSalesFile, onNotify: setToast, onClose: () => setEditingSalesFile(null), onSaved: () => { setEditingSalesFile(null); if (activeSalesFileOrder)
1048
+ return (_jsx(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, topbarActions: _jsx(TaskStatusMenu, { items: taskStatusItems, busy: busy || actionBusy, onActionError: notifyTaskStatusActionError }), onRouteChange: changeRoute, onRefresh: () => void load(), children: _jsx(AnalyticsPrototypeBoard, { onClose: () => setAnalyticsPrototypeVisible(false) }) }));
1049
+ return (_jsxs(_Fragment, { children: [_jsxs(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, subtitle: page === 'home' ? undefined : meta.subtitle, topbarActions: _jsx(TaskStatusMenu, { items: taskStatusItems, busy: busy || actionBusy, onActionError: notifyTaskStatusActionError }), commands: (page === 'controlled-goods' || page === 'sanctioned-companies') ? _jsx(CatalogCommands, { busy: busy || actionBusy, onUpload: () => setCatalogCommand('upload'), onCreate: () => setCatalogCommand('create') }) : contractCommands, busy: busy || actionBusy, onRouteChange: changeRoute, onRefresh: () => void load(), children: [page === 'home' && _jsx(HomePage, {}), page === 'controlled-goods' && _jsx(ControlledGoodsPage, { collection: data.controlledGoods, batches: data.controlledBatches.items, busy: busy || actionBusy, onAction: executeAction, onFileAction: executeCatalogFileAction, onRefresh: load, onQuery: (query) => queryCatalog('controlledGoods', query), onIssueQuery: queryImportIssues, importCompletion: importCompletions.CONTROLLED_CATALOG, importFailure: importFailures.CONTROLLED_CATALOG, onRetryImport: retryImport, command: catalogCommand, onCommandHandled: () => setCatalogCommand(null), onNotify: setToast }), page === 'sanctioned-companies' && _jsx(SanctionedCompaniesPage, { collection: data.sanctionedCompanies, batches: data.sanctionBatches.items, busy: busy || actionBusy, onAction: executeAction, onFileAction: executeCatalogFileAction, onRefresh: load, onQuery: (query) => queryCatalog('sanctionedCompanies', query), onIssueQuery: queryImportIssues, importCompletion: importCompletions.SANCTION_CATALOG, importFailure: importFailures.SANCTION_CATALOG, onRetryImport: retryImport, command: catalogCommand, onCommandHandled: () => setCatalogCommand(null), onNotify: setToast }), page === 'procurement' && route.type === 'procurement' && _jsx(ProcurementPage, { route: route, suppliers: data.suppliers, purchaseOrders: data.purchaseOrders, purchaseLines: data.purchaseLines, supplier: procurementContext.supplier, order: procurementContext.order, metrics: procurementContext.metrics, importTask: purchaseImportTask, reviewDraft: purchaseReviewDraft, busy: busy || actionBusy, onQuery: queryProcurement, onAction: executeAction, onFileAction: executeFileAction, onRouteChange: changeRoute, onRetryImport: retryContractImport, onReviewClose: closePurchaseReview, onNotify: setToast }), page === 'customer-contract-review' && _jsx(CustomerContractsPage, { collection: data.customerContracts, busy: busy || actionBusy, importReviewState: customerImportState, onQuery: queryCustomerContracts, onAction: executeAction, onFileAction: executeFileAction, onReanalyze: reanalyzeCustomerContract, onLoadReview: loadCustomerContractReview, onReviewClose: closeCustomerReview, reanalysisState: customerReanalysisState, onRetryImport: retryContractImport, onNotify: setToast }), page === 'sales' && route.type === 'sales' && _jsx(SalesPage, { route: route, customers: data.salesCustomers, orders: data.salesOrders, lines: data.salesLines, busy: busy || actionBusy, onQuery: querySales, onAction: executeAction, onRouteChange: changeRoute, onOpenFiles: openSalesFiles, onNotify: setToast }), page === 'sales-files' && _jsx(SalesFilesPage, { orders: data.salesFileOrders, history: data.generatedFiles, activeOrder: activeSalesFileOrder, visualPrototypeState: salesFilesPrototypeState, onPrototypeStateClose: () => setSalesFilesPrototypeState(undefined), banks: data.settings.banks, busy: busy || actionBusy, onQuery: querySalesFiles, onAction: executeSalesFileAction, onOpenSalesOrder: selectSalesFileOrder, onDownload: file => executeAction('download_sales_file', { id: file.id }).then(() => undefined), onEdit: async (file) => setEditingSalesFile(file), onNotify: setToast }), page === 'analytics' && _jsx(AnalyticsPage, { data: data.analytics, committedQuery: analyticsCommittedQuery, busy: busy || actionBusy, error: analyticsError, onQuery: queryAnalytics, onExportXlsx: exportAnalytics }), page === 'settings' && _jsx(SettingsPage, { data: data.settings, busy: busy || actionBusy, onSave: saveSettings, onRestoreDefault: restoreSettings, onDeleteRate: deleteSettingsRate, onRateQuery: queryMonthlyRates, onUploadLogo: uploadCompanyLogo, onDiscardLogo: discardCompanyLogo, onNotify: setToast }), !['home', 'controlled-goods', 'sanctioned-companies', 'procurement', 'customer-contract-review', 'sales', 'sales-files', 'analytics', 'settings'].includes(page) && _jsxs("section", { className: "tcw-page-placeholder", "data-page": page, children: [_jsx(InfoNotice, { children: "\u9875\u9762\u57FA\u7840\u7ED3\u6784\u5DF2\u5C31\u7EEA\uFF0C\u4E1A\u52A1\u5185\u5BB9\u5C06\u5728\u5BF9\u5E94\u9875\u9762\u6A21\u5757\u4E2D\u52A0\u8F7D\u3002" }), _jsx("div", { className: "tcw-placeholder-body", "aria-label": `${meta.title}页面内容` })] })] }), _jsx(LoadingBlocker, { visible: busy || actionBusy, label: actionBusy ? '操作处理中' : '数据加载中' }), _jsx(ToastStack, { items: toasts, onClose: closeToast }), editingSalesFile && _jsx(SalesFileEditor, { file: editingSalesFile, onNotify: setToast, onClose: () => setEditingSalesFile(null), onSaved: () => { setEditingSalesFile(null); if (activeSalesFileOrder)
918
1050
  void querySalesFiles('generatedFiles', { page: 1, pageSize: 20, filters: {}, parameters: { customerContractId: activeSalesFileOrder.customerContractId, purchaseOrderId: activeSalesFileOrder.purchaseOrderId } }); } })] }));
919
1051
  }
920
1052
  function routePage(route) {
@@ -937,7 +1069,7 @@ function buildTaskStatusItems(tasks, onOpen) {
937
1069
  statusLabel: taskStatusLabel(task.status),
938
1070
  tone,
939
1071
  updatedAt: formatTaskTime(task.updatedAt ?? task.queuedAt ?? task.createdAt),
940
- progressPercent: taskProgressPercent(task),
1072
+ progressPercent: importTaskProgressPercent(task),
941
1073
  ...((task.status === 'AWAITING_REVIEW' || task.status === 'FAILED' || task.status === 'TIMED_OUT') ? { actionLabel: task.status === 'AWAITING_REVIEW' ? '审核' : '重试', onAction: () => onOpen(task) } : {})
942
1074
  };
943
1075
  });
@@ -965,21 +1097,6 @@ function documentTypeLabel(documentType) {
965
1097
  };
966
1098
  return labels[documentType] ?? documentType;
967
1099
  }
968
- function taskProgressPercent(task) {
969
- if (task.status === 'SUCCEEDED')
970
- return 100;
971
- if (task.status === 'AWAITING_REVIEW')
972
- return 92;
973
- if (task.status === 'FAILED' || task.status === 'TIMED_OUT')
974
- return null;
975
- const progress = 'progress' in task ? task.progress : null;
976
- const processed = Number(progress?.processedCount);
977
- const total = Number(progress?.successCount) + Number(progress?.duplicateCount) + Number(progress?.invalidCount) + Number(progress?.reviewPendingCount);
978
- if (Number.isFinite(processed) && Number.isFinite(total) && total > 0)
979
- return Math.min(90, Math.max(5, (processed / total) * 90));
980
- const fallback = { QUEUED: 8, EXTRACTING: 24, AI_PARSING: 54, VALIDATING: 74, PERSISTING: 88 };
981
- return fallback[task.status] ?? null;
982
- }
983
1100
  function formatTaskTime(value) {
984
1101
  if (typeof value !== 'string' || !value)
985
1102
  return null;
@@ -988,6 +1105,25 @@ function formatTaskTime(value) {
988
1105
  return value;
989
1106
  return new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }).format(date).replace(/\//g, '/');
990
1107
  }
1108
+ function mergeWorkbenchDataWithLocalTasks(current, incoming) {
1109
+ const items = mergeImportTaskItems(current.importTasks.items, incoming.importTasks.items);
1110
+ return {
1111
+ ...incoming,
1112
+ importTasks: {
1113
+ ...incoming.importTasks,
1114
+ items,
1115
+ total: Math.max(numberValue(incoming.importTasks.total, 0), incoming.importTasks.items.length, current.importTasks.items.length, items.length),
1116
+ page: numberValue(incoming.importTasks.page, numberValue(current.importTasks.page, 1)),
1117
+ pageSize: incoming.importTasks.pageSize ?? current.importTasks.pageSize ?? 100
1118
+ }
1119
+ };
1120
+ }
1121
+ function hasLocalUploadPlaceholders(items) {
1122
+ return items.some(item => {
1123
+ const task = item && typeof item === 'object' && !Array.isArray(item) ? item : null;
1124
+ return task ? isLocalUploadPlaceholder(task) : false;
1125
+ });
1126
+ }
991
1127
  function analyticsQueryFromRoute(page, pageSize, values) {
992
1128
  const requestedPageSize = resolvePaginationPageSize(pageSize);
993
1129
  const dateValue = values.orderDate;
@@ -1028,6 +1164,26 @@ function actionResponseError(result, fallback) {
1028
1164
  function asRecord(value) {
1029
1165
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
1030
1166
  }
1167
+ function summaryRequestParameters(value) {
1168
+ const parameters = { ...asRecord(value) };
1169
+ delete parameters.collection;
1170
+ delete parameters.filters;
1171
+ delete parameters.taskId;
1172
+ delete parameters.runId;
1173
+ delete parameters.customerContractId;
1174
+ delete parameters.purchaseOrderId;
1175
+ return parameters;
1176
+ }
1177
+ function summaryRequestQuery(parameters) {
1178
+ return { page: 1, pageSize: 100, search: nextSummaryRequestNonce(), parameters: summaryRequestParameters(parameters) };
1179
+ }
1180
+ function importTasksCollectionRequest() {
1181
+ return { page: 1, pageSize: 100, parameters: { collection: 'importTasks', filters: '{}', refreshNonce: nextSummaryRequestNonce() } };
1182
+ }
1183
+ function nextSummaryRequestNonce() {
1184
+ summaryRequestSequence += 1;
1185
+ return `__tcw_summary_refresh__:${Date.now()}:${summaryRequestSequence}`;
1186
+ }
1031
1187
  function numberValue(value, fallback) {
1032
1188
  const result = Number(value);
1033
1189
  return Number.isFinite(result) ? result : fallback;
@@ -1049,26 +1205,11 @@ function downloadActionFile(value) {
1049
1205
  anchor.href = url;
1050
1206
  anchor.download = file.fileName;
1051
1207
  anchor.click();
1052
- setTimeout(() => URL.revokeObjectURL(url), 0);
1208
+ setTimeout(() => URL.revokeObjectURL(url), DOWNLOAD_URL_REVOKE_DELAY_MS);
1053
1209
  }
1054
- function ContractUploadCommand({ label, busy, onFile }) {
1210
+ function ContractUploadCommand({ label, busy, onFile, onError }) {
1055
1211
  return _jsxs("label", { className: "tcw-button tcw-button--primary", children: [_jsx("i", { className: "ri-upload-cloud-2-line", "aria-hidden": "true" }), label, _jsx("input", { className: "tcw-procurement-file-input", type: "file", accept: ".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.webp", disabled: busy, onChange: event => { const file = event.target.files?.[0]; event.currentTarget.value = ''; if (file)
1056
- void onFile(file).catch(() => undefined); } })] });
1057
- }
1058
- function adaptPurchaseReview(task) {
1059
- const payload = asRecord(task.resultPayload);
1060
- const supplier = asRecord(payload.supplier);
1061
- const order = asRecord(payload.order);
1062
- const lines = Array.isArray(payload.lines) ? payload.lines.map(value => {
1063
- const line = asRecord(value);
1064
- return { ...line, sourceType: line.sourceType === 'MANUAL_ENTRY' ? 'MANUAL_ENTRY' : 'CONTRACT_EXTRACTED', hsCodeStatus: line.hsCodeStatus === 'AI_CONFIRMED' || line.hsCodeStatus === 'MANUALLY_CONFIRMED' ? line.hsCodeStatus : 'UNCONFIRMED' };
1065
- }) : [];
1066
- return {
1067
- supplier: { ...supplier, name: String(supplier.name ?? '') },
1068
- order: { ...order, supplierId: String(order.supplierId ?? ''), contractNo: String(order.contractNo ?? ''), orderDate: String(order.orderDate ?? ''), contractAmountCny: String(order.contractAmountCny ?? '0') },
1069
- lines,
1070
- sourceFileName: task.sourceFileName ?? null
1071
- };
1212
+ void onFile(file).catch(error => onError?.(error instanceof Error ? error.message : '合同上传失败')); } })] });
1072
1213
  }
1073
1214
  async function attachPurchaseSource(draft, task, owner) {
1074
1215
  if (draft.sourceFileUrl || !task.sourceFileId)
@@ -1107,7 +1248,7 @@ async function loadSourcePreview(sourceFileId, slot, owner) {
1107
1248
  throw new Error('源文件预览响应类型无效');
1108
1249
  }
1109
1250
  function textPreviewUrl(title, content) {
1110
- const html = `<!doctype html><html lang="zh-Hans"><head><meta charset="utf-8"><title>${escapeHtml(title)}</title><style>body{margin:0;padding:24px;font:14px/1.7 system-ui,sans-serif;color:#1f2937;background:#fff}pre{margin:0;white-space:pre-wrap;overflow-wrap:anywhere}</style></head><body><pre>${escapeHtml(content)}</pre></body></html>`;
1251
+ const html = `<!doctype html><html lang="zh-Hans"><head><meta charset="utf-8"><title>${escapeHtml(title)}</title><style>body{margin:0;padding:16px;font:14px/1.7 system-ui,sans-serif;color:#1f2937;background:#fff}pre{margin:0;white-space:pre-wrap;overflow-wrap:anywhere}</style></head><body><pre>${escapeHtml(content)}</pre></body></html>`;
1111
1252
  return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
1112
1253
  }
1113
1254
  function escapeHtml(value) { return value.replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character] ?? character); }