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

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 (74) hide show
  1. package/dist/lib/domain/contract-number.js +2 -2
  2. package/dist/lib/domain/contract-number.js.map +1 -1
  3. package/dist/lib/remote-components/trade-compliance-workbench/app.css +1 -1
  4. package/dist/lib/remote-components/trade-compliance-workbench/app.js +5 -5
  5. package/dist/lib/remote-ui/app-source.d.ts.map +1 -1
  6. package/dist/lib/remote-ui/app-source.js +271 -48
  7. package/dist/lib/remote-ui/app-source.js.map +1 -1
  8. package/dist/lib/remote-ui/components/forms.js +1 -1
  9. package/dist/lib/remote-ui/components/forms.js.map +1 -1
  10. package/dist/lib/remote-ui/components/layout.d.ts +3 -2
  11. package/dist/lib/remote-ui/components/layout.d.ts.map +1 -1
  12. package/dist/lib/remote-ui/components/layout.js +4 -4
  13. package/dist/lib/remote-ui/components/layout.js.map +1 -1
  14. package/dist/lib/remote-ui/components/task-status.d.ts +20 -0
  15. package/dist/lib/remote-ui/components/task-status.d.ts.map +1 -0
  16. package/dist/lib/remote-ui/components/task-status.js +30 -0
  17. package/dist/lib/remote-ui/components/task-status.js.map +1 -0
  18. package/dist/lib/remote-ui/contract-import.d.ts.map +1 -1
  19. package/dist/lib/remote-ui/contract-import.js +6 -2
  20. package/dist/lib/remote-ui/contract-import.js.map +1 -1
  21. package/dist/lib/remote-ui/import-task-status.d.ts +10 -0
  22. package/dist/lib/remote-ui/import-task-status.d.ts.map +1 -0
  23. package/dist/lib/remote-ui/import-task-status.js +236 -0
  24. package/dist/lib/remote-ui/import-task-status.js.map +1 -0
  25. package/dist/lib/remote-ui/model.d.ts.map +1 -1
  26. package/dist/lib/remote-ui/model.js +2 -1
  27. package/dist/lib/remote-ui/model.js.map +1 -1
  28. package/dist/lib/remote-ui/pages/analytics-prototype.css +307 -0
  29. package/dist/lib/remote-ui/pages/catalogs.css +681 -0
  30. package/dist/lib/remote-ui/pages/catalogs.d.ts +5 -3
  31. package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
  32. package/dist/lib/remote-ui/pages/catalogs.js +6 -6
  33. package/dist/lib/remote-ui/pages/catalogs.js.map +1 -1
  34. package/dist/lib/remote-ui/pages/contracts.css +259 -0
  35. package/dist/lib/remote-ui/pages/contracts.d.ts.map +1 -1
  36. package/dist/lib/remote-ui/pages/contracts.js +36 -13
  37. package/dist/lib/remote-ui/pages/contracts.js.map +1 -1
  38. package/dist/lib/remote-ui/pages/files-prototype.css +547 -0
  39. package/dist/lib/remote-ui/pages/files.d.ts +3 -2
  40. package/dist/lib/remote-ui/pages/files.d.ts.map +1 -1
  41. package/dist/lib/remote-ui/pages/files.js +15 -15
  42. package/dist/lib/remote-ui/pages/files.js.map +1 -1
  43. package/dist/lib/remote-ui/pages/procurement-contracts.css +939 -0
  44. package/dist/lib/remote-ui/pages/procurement.js +1 -1
  45. package/dist/lib/remote-ui/pages/procurement.js.map +1 -1
  46. package/dist/lib/remote-ui/pages/sales-files.css +512 -0
  47. package/dist/lib/remote-ui/pages/sales.js +2 -2
  48. package/dist/lib/remote-ui/pages/sales.js.map +1 -1
  49. package/dist/lib/remote-ui/pages/settings.css +178 -0
  50. package/dist/lib/remote-ui/pages/settings.js +1 -1
  51. package/dist/lib/remote-ui/pages/settings.js.map +1 -1
  52. package/dist/lib/remote-ui/polling.d.ts +1 -1
  53. package/dist/lib/remote-ui/polling.d.ts.map +1 -1
  54. package/dist/lib/remote-ui/polling.js +4 -2
  55. package/dist/lib/remote-ui/polling.js.map +1 -1
  56. package/dist/lib/remote-ui/purchase-review-adapter.d.ts +4 -0
  57. package/dist/lib/remote-ui/purchase-review-adapter.d.ts.map +1 -0
  58. package/dist/lib/remote-ui/purchase-review-adapter.js +78 -0
  59. package/dist/lib/remote-ui/purchase-review-adapter.js.map +1 -0
  60. package/dist/lib/remote-ui/styles.css +588 -0
  61. package/dist/lib/sales-file.service.d.ts.map +1 -1
  62. package/dist/lib/sales-file.service.js +19 -7
  63. package/dist/lib/sales-file.service.js.map +1 -1
  64. package/dist/lib/workbench-view.provider.d.ts +24 -0
  65. package/dist/lib/workbench-view.provider.d.ts.map +1 -1
  66. package/dist/lib/workbench-view.provider.js +19 -4
  67. package/dist/lib/workbench-view.provider.js.map +1 -1
  68. package/dist/lib/workbench.plugin.d.ts.map +1 -1
  69. package/dist/lib/workbench.plugin.js +1 -2
  70. package/dist/lib/workbench.plugin.js.map +1 -1
  71. package/dist/lib/workbench.service.d.ts.map +1 -1
  72. package/dist/lib/workbench.service.js +36 -10
  73. package/dist/lib/workbench.service.js.map +1 -1
  74. 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":"AA2BA,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"}
@@ -7,17 +7,20 @@ import { clampPageAfterSingleDeletion, createBlobUrlOwner, createKeyedLatestRequ
7
7
  import { encodeViewFilters } from '../view-parameters.js';
8
8
  import { InfoNotice } from './components/data-grid.js';
9
9
  import { LoadingBlocker, ToastStack } from './components/forms.js';
10
+ import { TaskStatusMenu } from './components/task-status.js';
10
11
  import { addToast, createToastId, createToastTimerRegistry, removeToast } from './toast-queue.js';
11
12
  import { buildBreadcrumbs, WorkbenchShell } from './components/layout.js';
12
13
  import { CatalogCommands, CatalogImportError, ControlledGoodsPage, findActiveCatalogImportTasks, findLatestFailedCatalogImportTask, runCatalogImportOrchestration, SanctionedCompaniesPage } from './pages/catalogs.js';
13
14
  import { HomePage } from './pages/home.js';
14
15
  import { ProcurementPage } from './pages/procurement.js';
16
+ import { adaptPurchaseReview } from './purchase-review-adapter.js';
15
17
  import { CustomerContractsPage, adaptCustomerContractReview } from './pages/contracts.js';
16
18
  import { SalesPage } from './pages/sales.js';
17
19
  import { SalesFilesPage } from './pages/files.js';
18
20
  import { AnalyticsPage, AnalyticsPrototypeBoard } from './pages/analytics.js';
19
21
  import { SettingsPage } from './pages/settings.js';
20
22
  import { ContractImportError, findRecoverableContractImport, findResumableContractImports, runContractImportOrchestration, runCustomerContractReanalysis, taskTransition } from './contract-import.js';
23
+ import { importTaskFromStart, importTaskProgressPercent, isLocalUploadPlaceholder, mergeImportTaskItems } from './import-task-status.js';
21
24
  import { compareTaskBusinessOrder, createPendingResultQueue, createPollingTaskTracker, isAbortError, isPollingObservationTimeout, submitAndRefreshTaskState } from './polling.js';
22
25
  import { ACTION_NOTIFICATION_POLICIES, executeWorkbenchAction } from './action-execution.js';
23
26
  import './styles.css';
@@ -33,12 +36,14 @@ const pageMeta = {
33
36
  settings: { title: '系统设置', subtitle: '维护企业、银行、汇率和利润规则。' }
34
37
  };
35
38
  const procurementPageMeta = {
36
- suppliers: { title: '供应商列表', subtitle: '一级页面:管理供应商主档,并通过供应商代码进入该供应商的采购订单列表。' },
39
+ suppliers: { title: '供应商列表', subtitle: '管理供应商主档,并通过供应商代码进入该供应商的采购订单列表。' },
37
40
  orders: { title: '采购订单列表', subtitle: '查看当前供应商的采购订单、金额及合规状态。' },
38
41
  lines: { title: '采购商品列表', subtitle: '查看当前采购订单的商品明细及合规状态。' }
39
42
  };
40
43
  const bridge = createRemoteBridge();
41
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;
42
47
  function comparePendingReviewBusinessOrder(left, right) {
43
48
  const leftTask = left.kind === 'CONTRACT_IMPORT' ? left.completion.task : left.result.run;
44
49
  const rightTask = right.kind === 'CONTRACT_IMPORT' ? right.completion.task : right.result.run;
@@ -71,6 +76,7 @@ function App() {
71
76
  const [analyticsPrototypeVisible, setAnalyticsPrototypeVisible] = React.useState(false);
72
77
  const [salesFilesPrototypeState, setSalesFilesPrototypeState] = React.useState(undefined);
73
78
  const [data, setData] = React.useState(() => normalizeWorkbenchData(null));
79
+ const importTaskItemsRef = React.useRef([]);
74
80
  const [busy, setBusy] = React.useState(false);
75
81
  const [actionBusy, setActionBusy] = React.useState(false);
76
82
  const [catalogCommand, setCatalogCommand] = React.useState(null);
@@ -105,6 +111,42 @@ function App() {
105
111
  toastTimers.dismiss(id);
106
112
  setToasts((items) => removeToast(items, id));
107
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
+ }, []);
108
150
  const configureAnalyticsPrototypeCapability = React.useCallback((parameters) => {
109
151
  analyticsPrototypeListenerCleanupRef.current?.();
110
152
  analyticsPrototypeListenerCleanupRef.current = null;
@@ -187,11 +229,12 @@ function App() {
187
229
  const requestId = gate.begin();
188
230
  setBusy(true);
189
231
  try {
190
- const response = await bridge.requestData({ page: 1, pageSize: 100, parameters: active.initialQuery?.parameters ?? {} });
232
+ const response = await bridge.requestData(summaryRequestQuery(active.initialQuery?.parameters));
191
233
  if (gate.isLatest(requestId)) {
192
234
  const normalized = normalizeWorkbenchData(response);
235
+ const importTasks = await loadImportTasksFallback(normalized);
193
236
  bridge.configure({ uploadPolicy: normalized.uploadPolicy });
194
- setData(normalized);
237
+ setData(current => mergeWorkbenchDataWithLocalTasks(current, { ...normalized, importTasks }));
195
238
  await loadRoute(routeRef.current);
196
239
  }
197
240
  }
@@ -203,7 +246,7 @@ function App() {
203
246
  gate.finish(requestId);
204
247
  setBusy(gate.activeCount() + routeRequestGateRef.current.activeCount() + analyticsRequestsRef.current.activeCount() > 0);
205
248
  }
206
- }, [loadRoute]);
249
+ }, [loadImportTasksFallback, loadRoute]);
207
250
  const loadTaskState = React.useCallback(async () => {
208
251
  const active = contextRef.current;
209
252
  if (!active)
@@ -211,17 +254,22 @@ function App() {
211
254
  const gate = taskStateRequestGateRef.current;
212
255
  const requestId = gate.begin();
213
256
  try {
214
- const response = await bridge.requestData({ page: 1, pageSize: 100, parameters: active.initialQuery?.parameters ?? {} });
257
+ const response = await bridge.requestData(summaryRequestQuery(active.initialQuery?.parameters));
215
258
  if (!gate.commit(requestId))
216
259
  return false;
217
260
  const normalized = normalizeWorkbenchData(response);
261
+ const importTasks = await loadImportTasksFallback(normalized);
218
262
  setData(current => ({
219
263
  ...current,
220
264
  controlledGoods: normalized.controlledGoods,
221
265
  controlledBatches: normalized.controlledBatches,
222
266
  sanctionedCompanies: normalized.sanctionedCompanies,
223
267
  sanctionBatches: normalized.sanctionBatches,
224
- 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
+ },
225
273
  customerContractAnalyses: normalized.customerContractAnalyses
226
274
  }));
227
275
  return true;
@@ -234,7 +282,7 @@ function App() {
234
282
  finally {
235
283
  gate.finish(requestId);
236
284
  }
237
- }, []);
285
+ }, [loadImportTasksFallback]);
238
286
  const scheduleTaskStateRecovery = React.useCallback(() => {
239
287
  if (taskRecoveryTimerRef.current != null)
240
288
  return;
@@ -242,11 +290,16 @@ function App() {
242
290
  void loadTaskState().then(refreshed => {
243
291
  if (!refreshed || taskRecoveryTimerRef.current == null)
244
292
  return;
245
- window.clearInterval(taskRecoveryTimerRef.current);
246
- taskRecoveryTimerRef.current = null;
293
+ if (!hasLocalUploadPlaceholders(importTaskItemsRef.current)) {
294
+ window.clearInterval(taskRecoveryTimerRef.current);
295
+ taskRecoveryTimerRef.current = null;
296
+ }
247
297
  });
248
298
  }, 2000);
249
299
  }, [loadTaskState]);
300
+ React.useEffect(() => {
301
+ importTaskItemsRef.current = data.importTasks.items;
302
+ }, [data.importTasks.items]);
250
303
  React.useEffect(() => () => {
251
304
  if (taskRecoveryTimerRef.current != null)
252
305
  window.clearInterval(taskRecoveryTimerRef.current);
@@ -269,7 +322,7 @@ function App() {
269
322
  contextRef.current = nextContext;
270
323
  const normalized = normalizeWorkbenchData(nextContext.payload);
271
324
  bridge.configure({ uploadPolicy: normalized.uploadPolicy });
272
- setData(normalized);
325
+ setData(current => mergeWorkbenchDataWithLocalTasks(current, normalized));
273
326
  void load(nextContext);
274
327
  },
275
328
  onHostEvent() {
@@ -384,6 +437,7 @@ function App() {
384
437
  signal,
385
438
  pollTask: async (taskId) => asContractCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId } }))),
386
439
  onTransition: (previous, next) => {
440
+ upsertLocalImportTask(next);
387
441
  const notice = taskTransition(previous, next);
388
442
  if (notice)
389
443
  setToast(notice);
@@ -393,6 +447,7 @@ function App() {
393
447
  if (completion.task.status === 'AWAITING_REVIEW') {
394
448
  enqueuePendingReview({ kind: 'CONTRACT_IMPORT', completion });
395
449
  }
450
+ upsertLocalImportTask(completion.task);
396
451
  return completion;
397
452
  }
398
453
  catch (error) {
@@ -414,22 +469,85 @@ function App() {
414
469
  setToast({ message: error instanceof Error ? error.message : '合同解析失败', tone: 'error' });
415
470
  throw error;
416
471
  }
417
- }, [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]);
418
521
  const executeFileAction = React.useCallback(async (actionKey, file, input, notifyError = true) => {
419
522
  try {
420
- const raw = await submitAndRefreshTaskState(() => bridge.executeFileAction(actionKey, file, input), loadTaskState);
523
+ const raw = await bridge.executeFileAction(actionKey, file, input);
421
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
+ }
422
533
  if (start.success === false)
423
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);
424
539
  setToast({ message: resolveActionMessage(start) || '解析任务已提交', tone: 'info' });
425
540
  return start;
426
541
  }
427
542
  catch (error) {
543
+ await loadTaskState().catch((refreshError) => {
544
+ notifyTaskRecoveryRefreshError(refreshError instanceof Error ? refreshError : new Error(String(refreshError)));
545
+ });
428
546
  if (notifyError && !(error instanceof CatalogImportError))
429
547
  setToast({ message: error instanceof Error ? error.message : '上传失败', tone: 'error' });
430
548
  throw error;
431
549
  }
432
- }, [loadTaskState]);
550
+ }, [loadTaskState, notifyTaskRecoveryRefreshError, presentImmediateContractUploadReview, scheduleTaskStateRecovery, startTrackingUploadedImportTask, upsertLocalImportTask]);
433
551
  const executeCatalogFileAction = React.useCallback((actionKey, file, input) => executeFileAction(actionKey, file, input, false), [executeFileAction]);
434
552
  React.useEffect(() => {
435
553
  const tasks = data.importTasks.items;
@@ -440,9 +558,9 @@ function App() {
440
558
  for (const active of findActiveCatalogImportTasks(tasks)) {
441
559
  const running = catalogTaskTrackerRef.current.run(active.id, signal => orchestrateImport({ success: true, data: { task: active } }, signal));
442
560
  if (running)
443
- void running.catch(() => undefined);
561
+ void running.catch(handleBackgroundImportTrackingError);
444
562
  }
445
- }, [data.importTasks.items, orchestrateImport]);
563
+ }, [data.importTasks.items, handleBackgroundImportTrackingError, orchestrateImport]);
446
564
  React.useEffect(() => {
447
565
  for (const task of data.importTasks.items) {
448
566
  const conversationId = task.observerConversationId;
@@ -461,7 +579,7 @@ function App() {
461
579
  continue;
462
580
  const running = contractTaskTrackerRef.current.run(resumable.id, signal => orchestrateContractImport({ success: true, data: { task: resumable } }, signal));
463
581
  if (running)
464
- void running.catch(() => undefined);
582
+ void running.catch(handleBackgroundImportTrackingError);
465
583
  }
466
584
  for (const documentType of ['PURCHASE_CONTRACT', 'CUSTOMER_CONTRACT']) {
467
585
  const failed = findRecoverableContractImport(tasks, documentType);
@@ -472,7 +590,7 @@ function App() {
472
590
  else
473
591
  setCustomerImportState({ status: 'FAILED', importTaskId: failed.id, message: failed.errorMessage || '客户合同解析失败,请重新解析' });
474
592
  }
475
- 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')) {
476
594
  if (documentType === 'PURCHASE_CONTRACT') {
477
595
  setPurchaseImportTask(null);
478
596
  setPurchaseReviewDraft(null);
@@ -481,10 +599,10 @@ function App() {
481
599
  setCustomerImportState({ status: 'IDLE' });
482
600
  }
483
601
  }
484
- }, [data.importTasks.items, orchestrateContractImport]);
602
+ }, [data.importTasks.items, handleBackgroundImportTrackingError, orchestrateContractImport]);
485
603
  const retryImport = React.useCallback(async (taskId) => {
486
604
  try {
487
- const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState);
605
+ const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState, notifyTaskRecoveryRefreshError);
488
606
  const start = asActionResult(unwrapResponse(raw));
489
607
  if (start.success === false)
490
608
  throw actionResponseError(start, '重新解析失败');
@@ -495,10 +613,10 @@ function App() {
495
613
  setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
496
614
  throw error;
497
615
  }
498
- }, [loadTaskState]);
616
+ }, [loadTaskState, notifyTaskRecoveryRefreshError]);
499
617
  const retryContractImport = React.useCallback(async (taskId) => {
500
618
  try {
501
- const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState);
619
+ const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState, notifyTaskRecoveryRefreshError);
502
620
  const start = asActionResult(unwrapResponse(raw));
503
621
  if (start.success === false)
504
622
  throw actionResponseError(start, '重新解析失败');
@@ -508,7 +626,38 @@ function App() {
508
626
  setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
509
627
  throw error;
510
628
  }
511
- }, [loadTaskState]);
629
+ }, [loadTaskState, notifyTaskRecoveryRefreshError]);
630
+ const openImportTaskFromStatus = React.useCallback(async (task) => {
631
+ if (task.status === 'FAILED' || task.status === 'TIMED_OUT') {
632
+ if (task.documentType === 'PURCHASE_CONTRACT' || task.documentType === 'CUSTOMER_CONTRACT')
633
+ await retryContractImport(task.id);
634
+ else
635
+ await retryImport(task.id);
636
+ return;
637
+ }
638
+ if (task.status !== 'AWAITING_REVIEW')
639
+ return;
640
+ if (task.documentType === 'CONTROLLED_CATALOG' || task.documentType === 'SANCTION_CATALOG') {
641
+ const completion = asImportCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId: task.id } })));
642
+ setImportCompletions(current => ({ ...current, [completion.task.documentType]: completion }));
643
+ changeRouteRef.current({ type: 'page', page: completion.task.documentType === 'CONTROLLED_CATALOG' ? 'controlled-goods' : 'sanctioned-companies' });
644
+ return;
645
+ }
646
+ const fullTask = asContractCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId: task.id } }))).task;
647
+ activeReviewResultRef.current = fullTask.id;
648
+ if (fullTask.documentType === 'PURCHASE_CONTRACT') {
649
+ blobUrlOwnerRef.current.clear('purchase');
650
+ const draft = adaptPurchaseReview(fullTask);
651
+ setPurchaseImportTask(fullTask);
652
+ setPurchaseReviewDraft(await attachPurchaseSource(draft, fullTask, blobUrlOwnerRef.current));
653
+ changeRouteRef.current({ type: 'procurement', level: 'suppliers' });
654
+ return;
655
+ }
656
+ blobUrlOwnerRef.current.clear('customer');
657
+ const review = adaptCustomerContractReview(fullTask.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: fullTask.id, sourceFileId: fullTask.sourceFileId ?? '', sourceFileName: fullTask.sourceFileName ?? '' });
658
+ setCustomerImportState({ status: 'AWAITING_REVIEW', importTaskId: fullTask.id, review: await attachCustomerSource(review, blobUrlOwnerRef.current) });
659
+ changeRouteRef.current({ type: 'page', page: 'customer-contract-review' });
660
+ }, [retryContractImport, retryImport]);
512
661
  const applyCustomerContractReanalysisResult = React.useCallback(async (result) => {
513
662
  let review;
514
663
  if (result.review) {
@@ -533,7 +682,7 @@ function App() {
533
682
  const reanalyzeCustomerContract = React.useCallback(async (input) => {
534
683
  setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析中' });
535
684
  try {
536
- 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);
537
686
  const response = asActionResult(unwrapResponse(raw));
538
687
  if (response.success === false || !response.data)
539
688
  throw actionResponseError(response, 'AI 重新分析启动失败');
@@ -741,7 +890,7 @@ function App() {
741
890
  }, []);
742
891
  const queryProcurement = React.useCallback(async (_collection, query) => {
743
892
  await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
744
- }, [loadRoute]);
893
+ }, [loadImportTasksFallback, loadRoute]);
745
894
  const queryCustomerContracts = React.useCallback(async (query) => {
746
895
  await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
747
896
  }, [loadRoute]);
@@ -792,7 +941,7 @@ function App() {
792
941
  requests.finish(requestId);
793
942
  setBusy(requests.activeCount() + requestGateRef.current.activeCount() + routeRequestGateRef.current.activeCount() > 0);
794
943
  }
795
- }, []);
944
+ }, [loadImportTasksFallback]);
796
945
  const exportAnalytics = React.useCallback(async (_request) => {
797
946
  setActionBusy(true);
798
947
  try {
@@ -872,16 +1021,17 @@ function App() {
872
1021
  changeRouteRef.current = changeRoute;
873
1022
  const page = routePage(route);
874
1023
  const meta = route.type === 'procurement' ? procurementPageMeta[route.level] : pageMeta[page];
1024
+ const taskStatusItems = React.useMemo(() => buildTaskStatusItems(data.importTasks.items, openImportTaskFromStatus), [data.importTasks.items, openImportTaskFromStatus]);
875
1025
  const contractCommands = page === 'procurement' && route.type === 'procurement'
876
1026
  ? route.level === 'suppliers'
877
- ? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u91C7\u8D2D\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_purchase_contract', file, { documentType: 'PURCHASE_CONTRACT' }) })
1027
+ ? _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' }) })
878
1028
  : undefined
879
1029
  : page === 'customer-contract-review'
880
- ? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u5BA2\u6237\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_customer_contract', file, { documentType: 'CUSTOMER_CONTRACT' }) })
1030
+ ? _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' }) })
881
1031
  : undefined;
882
1032
  if (analyticsPrototypeVisible)
883
- return (_jsx(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, onRouteChange: changeRoute, onRefresh: () => void load(), children: _jsx(AnalyticsPrototypeBoard, { onClose: () => setAnalyticsPrototypeVisible(false) }) }));
884
- return (_jsxs(_Fragment, { children: [_jsxs(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, subtitle: page === 'home' ? undefined : meta.subtitle, 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)
1033
+ 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) }) }));
1034
+ 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)
885
1035
  void querySalesFiles('generatedFiles', { page: 1, pageSize: 20, filters: {}, parameters: { customerContractId: activeSalesFileOrder.customerContractId, purchaseOrderId: activeSalesFileOrder.purchaseOrderId } }); } })] }));
886
1036
  }
887
1037
  function routePage(route) {
@@ -891,6 +1041,74 @@ function routePage(route) {
891
1041
  return 'sales';
892
1042
  return route.page;
893
1043
  }
1044
+ function buildTaskStatusItems(tasks, onOpen) {
1045
+ return [...tasks]
1046
+ .sort((left, right) => String(right.updatedAt ?? right.queuedAt ?? right.createdAt ?? '').localeCompare(String(left.updatedAt ?? left.queuedAt ?? left.createdAt ?? '')))
1047
+ .map(task => {
1048
+ const tone = taskTone(task.status);
1049
+ return {
1050
+ id: task.id,
1051
+ title: task.sourceFileName || documentTypeLabel(task.documentType),
1052
+ subtitle: documentTypeLabel(task.documentType),
1053
+ status: task.status,
1054
+ statusLabel: taskStatusLabel(task.status),
1055
+ tone,
1056
+ updatedAt: formatTaskTime(task.updatedAt ?? task.queuedAt ?? task.createdAt),
1057
+ progressPercent: importTaskProgressPercent(task),
1058
+ ...((task.status === 'AWAITING_REVIEW' || task.status === 'FAILED' || task.status === 'TIMED_OUT') ? { actionLabel: task.status === 'AWAITING_REVIEW' ? '审核' : '重试', onAction: () => onOpen(task) } : {})
1059
+ };
1060
+ });
1061
+ }
1062
+ function taskTone(status) {
1063
+ if (status === 'AWAITING_REVIEW')
1064
+ return 'review';
1065
+ if (status === 'SUCCEEDED')
1066
+ return 'success';
1067
+ if (status === 'FAILED' || status === 'TIMED_OUT')
1068
+ return 'failed';
1069
+ if (status === 'QUEUED' || status === 'EXTRACTING' || status === 'AI_PARSING' || status === 'VALIDATING' || status === 'PERSISTING')
1070
+ return 'running';
1071
+ return 'idle';
1072
+ }
1073
+ function taskStatusLabel(status) {
1074
+ const labels = {
1075
+ QUEUED: '排队中', EXTRACTING: '提取中', AI_PARSING: 'AI 解析中', VALIDATING: '校验中', AWAITING_REVIEW: '待审核', PERSISTING: '入库中', SUCCEEDED: '已完成', FAILED: '失败', TIMED_OUT: '超时'
1076
+ };
1077
+ return labels[status] ?? status;
1078
+ }
1079
+ function documentTypeLabel(documentType) {
1080
+ const labels = {
1081
+ CONTROLLED_CATALOG: '管控商品目录', SANCTION_CATALOG: '公司制裁目录', PURCHASE_CONTRACT: '采购合同解析', CUSTOMER_CONTRACT: '客户合同解析'
1082
+ };
1083
+ return labels[documentType] ?? documentType;
1084
+ }
1085
+ function formatTaskTime(value) {
1086
+ if (typeof value !== 'string' || !value)
1087
+ return null;
1088
+ const date = new Date(value);
1089
+ if (!Number.isFinite(date.getTime()))
1090
+ return value;
1091
+ 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, '/');
1092
+ }
1093
+ function mergeWorkbenchDataWithLocalTasks(current, incoming) {
1094
+ const items = mergeImportTaskItems(current.importTasks.items, incoming.importTasks.items);
1095
+ return {
1096
+ ...incoming,
1097
+ importTasks: {
1098
+ ...incoming.importTasks,
1099
+ items,
1100
+ total: Math.max(numberValue(incoming.importTasks.total, 0), incoming.importTasks.items.length, current.importTasks.items.length, items.length),
1101
+ page: numberValue(incoming.importTasks.page, numberValue(current.importTasks.page, 1)),
1102
+ pageSize: incoming.importTasks.pageSize ?? current.importTasks.pageSize ?? 100
1103
+ }
1104
+ };
1105
+ }
1106
+ function hasLocalUploadPlaceholders(items) {
1107
+ return items.some(item => {
1108
+ const task = item && typeof item === 'object' && !Array.isArray(item) ? item : null;
1109
+ return task ? isLocalUploadPlaceholder(task) : false;
1110
+ });
1111
+ }
894
1112
  function analyticsQueryFromRoute(page, pageSize, values) {
895
1113
  const requestedPageSize = resolvePaginationPageSize(pageSize);
896
1114
  const dateValue = values.orderDate;
@@ -931,6 +1149,26 @@ function actionResponseError(result, fallback) {
931
1149
  function asRecord(value) {
932
1150
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
933
1151
  }
1152
+ function summaryRequestParameters(value) {
1153
+ const parameters = { ...asRecord(value) };
1154
+ delete parameters.collection;
1155
+ delete parameters.filters;
1156
+ delete parameters.taskId;
1157
+ delete parameters.runId;
1158
+ delete parameters.customerContractId;
1159
+ delete parameters.purchaseOrderId;
1160
+ return parameters;
1161
+ }
1162
+ function summaryRequestQuery(parameters) {
1163
+ return { page: 1, pageSize: 100, search: nextSummaryRequestNonce(), parameters: summaryRequestParameters(parameters) };
1164
+ }
1165
+ function importTasksCollectionRequest() {
1166
+ return { page: 1, pageSize: 100, parameters: { collection: 'importTasks', filters: '{}', refreshNonce: nextSummaryRequestNonce() } };
1167
+ }
1168
+ function nextSummaryRequestNonce() {
1169
+ summaryRequestSequence += 1;
1170
+ return `__tcw_summary_refresh__:${Date.now()}:${summaryRequestSequence}`;
1171
+ }
934
1172
  function numberValue(value, fallback) {
935
1173
  const result = Number(value);
936
1174
  return Number.isFinite(result) ? result : fallback;
@@ -952,26 +1190,11 @@ function downloadActionFile(value) {
952
1190
  anchor.href = url;
953
1191
  anchor.download = file.fileName;
954
1192
  anchor.click();
955
- setTimeout(() => URL.revokeObjectURL(url), 0);
1193
+ setTimeout(() => URL.revokeObjectURL(url), DOWNLOAD_URL_REVOKE_DELAY_MS);
956
1194
  }
957
- function ContractUploadCommand({ label, busy, onFile }) {
1195
+ function ContractUploadCommand({ label, busy, onFile, onError }) {
958
1196
  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)
959
- void onFile(file).catch(() => undefined); } })] });
960
- }
961
- function adaptPurchaseReview(task) {
962
- const payload = asRecord(task.resultPayload);
963
- const supplier = asRecord(payload.supplier);
964
- const order = asRecord(payload.order);
965
- const lines = Array.isArray(payload.lines) ? payload.lines.map(value => {
966
- const line = asRecord(value);
967
- return { ...line, sourceType: line.sourceType === 'MANUAL_ENTRY' ? 'MANUAL_ENTRY' : 'CONTRACT_EXTRACTED', hsCodeStatus: line.hsCodeStatus === 'AI_CONFIRMED' || line.hsCodeStatus === 'MANUALLY_CONFIRMED' ? line.hsCodeStatus : 'UNCONFIRMED' };
968
- }) : [];
969
- return {
970
- supplier: { ...supplier, name: String(supplier.name ?? '') },
971
- order: { ...order, supplierId: String(order.supplierId ?? ''), contractNo: String(order.contractNo ?? ''), orderDate: String(order.orderDate ?? ''), contractAmountCny: String(order.contractAmountCny ?? '0') },
972
- lines,
973
- sourceFileName: task.sourceFileName ?? null
974
- };
1197
+ void onFile(file).catch(error => onError?.(error instanceof Error ? error.message : '合同上传失败')); } })] });
975
1198
  }
976
1199
  async function attachPurchaseSource(draft, task, owner) {
977
1200
  if (draft.sourceFileUrl || !task.sourceFileId)
@@ -1010,7 +1233,7 @@ async function loadSourcePreview(sourceFileId, slot, owner) {
1010
1233
  throw new Error('源文件预览响应类型无效');
1011
1234
  }
1012
1235
  function textPreviewUrl(title, content) {
1013
- 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>`;
1236
+ 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>`;
1014
1237
  return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
1015
1238
  }
1016
1239
  function escapeHtml(value) { return value.replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character] ?? character); }