@chenchaolong/plugin-trade-compliance-workbench 1.0.18 → 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 (67) 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 +190 -64
  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/task-status.d.ts +2 -1
  11. package/dist/lib/remote-ui/components/task-status.d.ts.map +1 -1
  12. package/dist/lib/remote-ui/components/task-status.js +5 -4
  13. package/dist/lib/remote-ui/components/task-status.js.map +1 -1
  14. package/dist/lib/remote-ui/import-task-status.d.ts +10 -0
  15. package/dist/lib/remote-ui/import-task-status.d.ts.map +1 -0
  16. package/dist/lib/remote-ui/import-task-status.js +236 -0
  17. package/dist/lib/remote-ui/import-task-status.js.map +1 -0
  18. package/dist/lib/remote-ui/model.d.ts.map +1 -1
  19. package/dist/lib/remote-ui/model.js +2 -1
  20. package/dist/lib/remote-ui/model.js.map +1 -1
  21. package/dist/lib/remote-ui/pages/analytics-prototype.css +307 -0
  22. package/dist/lib/remote-ui/pages/catalogs.css +681 -0
  23. package/dist/lib/remote-ui/pages/catalogs.d.ts +5 -3
  24. package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
  25. package/dist/lib/remote-ui/pages/catalogs.js +6 -6
  26. package/dist/lib/remote-ui/pages/catalogs.js.map +1 -1
  27. package/dist/lib/remote-ui/pages/contracts.css +259 -0
  28. package/dist/lib/remote-ui/pages/contracts.d.ts.map +1 -1
  29. package/dist/lib/remote-ui/pages/contracts.js +36 -13
  30. package/dist/lib/remote-ui/pages/contracts.js.map +1 -1
  31. package/dist/lib/remote-ui/pages/files-prototype.css +547 -0
  32. package/dist/lib/remote-ui/pages/files.d.ts +3 -2
  33. package/dist/lib/remote-ui/pages/files.d.ts.map +1 -1
  34. package/dist/lib/remote-ui/pages/files.js +15 -15
  35. package/dist/lib/remote-ui/pages/files.js.map +1 -1
  36. package/dist/lib/remote-ui/pages/procurement-contracts.css +939 -0
  37. package/dist/lib/remote-ui/pages/procurement.js +1 -1
  38. package/dist/lib/remote-ui/pages/procurement.js.map +1 -1
  39. package/dist/lib/remote-ui/pages/sales-files.css +512 -0
  40. package/dist/lib/remote-ui/pages/sales.js +2 -2
  41. package/dist/lib/remote-ui/pages/sales.js.map +1 -1
  42. package/dist/lib/remote-ui/pages/settings.css +178 -0
  43. package/dist/lib/remote-ui/pages/settings.js +1 -1
  44. package/dist/lib/remote-ui/pages/settings.js.map +1 -1
  45. package/dist/lib/remote-ui/polling.d.ts +1 -1
  46. package/dist/lib/remote-ui/polling.d.ts.map +1 -1
  47. package/dist/lib/remote-ui/polling.js +4 -2
  48. package/dist/lib/remote-ui/polling.js.map +1 -1
  49. package/dist/lib/remote-ui/purchase-review-adapter.d.ts +4 -0
  50. package/dist/lib/remote-ui/purchase-review-adapter.d.ts.map +1 -0
  51. package/dist/lib/remote-ui/purchase-review-adapter.js +78 -0
  52. package/dist/lib/remote-ui/purchase-review-adapter.js.map +1 -0
  53. package/dist/lib/remote-ui/styles.css +588 -0
  54. package/dist/lib/sales-file.service.d.ts.map +1 -1
  55. package/dist/lib/sales-file.service.js +19 -7
  56. package/dist/lib/sales-file.service.js.map +1 -1
  57. package/dist/lib/workbench-view.provider.d.ts +24 -0
  58. package/dist/lib/workbench-view.provider.d.ts.map +1 -1
  59. package/dist/lib/workbench-view.provider.js +19 -4
  60. package/dist/lib/workbench-view.provider.js.map +1 -1
  61. package/dist/lib/workbench.plugin.d.ts.map +1 -1
  62. package/dist/lib/workbench.plugin.js +1 -2
  63. package/dist/lib/workbench.plugin.js.map +1 -1
  64. package/dist/lib/workbench.service.d.ts.map +1 -1
  65. package/dist/lib/workbench.service.js +36 -10
  66. package/dist/lib/workbench.service.js.map +1 -1
  67. 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 重新分析启动失败');
@@ -773,7 +890,7 @@ function App() {
773
890
  }, []);
774
891
  const queryProcurement = React.useCallback(async (_collection, query) => {
775
892
  await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
776
- }, [loadRoute]);
893
+ }, [loadImportTasksFallback, loadRoute]);
777
894
  const queryCustomerContracts = React.useCallback(async (query) => {
778
895
  await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
779
896
  }, [loadRoute]);
@@ -824,7 +941,7 @@ function App() {
824
941
  requests.finish(requestId);
825
942
  setBusy(requests.activeCount() + requestGateRef.current.activeCount() + routeRequestGateRef.current.activeCount() > 0);
826
943
  }
827
- }, []);
944
+ }, [loadImportTasksFallback]);
828
945
  const exportAnalytics = React.useCallback(async (_request) => {
829
946
  setActionBusy(true);
830
947
  try {
@@ -907,14 +1024,14 @@ function App() {
907
1024
  const taskStatusItems = React.useMemo(() => buildTaskStatusItems(data.importTasks.items, openImportTaskFromStatus), [data.importTasks.items, openImportTaskFromStatus]);
908
1025
  const contractCommands = page === 'procurement' && route.type === 'procurement'
909
1026
  ? 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' }) })
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' }) })
911
1028
  : undefined
912
1029
  : 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' }) })
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' }) })
914
1031
  : undefined;
915
1032
  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)
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)
918
1035
  void querySalesFiles('generatedFiles', { page: 1, pageSize: 20, filters: {}, parameters: { customerContractId: activeSalesFileOrder.customerContractId, purchaseOrderId: activeSalesFileOrder.purchaseOrderId } }); } })] }));
919
1036
  }
920
1037
  function routePage(route) {
@@ -937,7 +1054,7 @@ function buildTaskStatusItems(tasks, onOpen) {
937
1054
  statusLabel: taskStatusLabel(task.status),
938
1055
  tone,
939
1056
  updatedAt: formatTaskTime(task.updatedAt ?? task.queuedAt ?? task.createdAt),
940
- progressPercent: taskProgressPercent(task),
1057
+ progressPercent: importTaskProgressPercent(task),
941
1058
  ...((task.status === 'AWAITING_REVIEW' || task.status === 'FAILED' || task.status === 'TIMED_OUT') ? { actionLabel: task.status === 'AWAITING_REVIEW' ? '审核' : '重试', onAction: () => onOpen(task) } : {})
942
1059
  };
943
1060
  });
@@ -965,21 +1082,6 @@ function documentTypeLabel(documentType) {
965
1082
  };
966
1083
  return labels[documentType] ?? documentType;
967
1084
  }
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
1085
  function formatTaskTime(value) {
984
1086
  if (typeof value !== 'string' || !value)
985
1087
  return null;
@@ -988,6 +1090,25 @@ function formatTaskTime(value) {
988
1090
  return value;
989
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, '/');
990
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
+ }
991
1112
  function analyticsQueryFromRoute(page, pageSize, values) {
992
1113
  const requestedPageSize = resolvePaginationPageSize(pageSize);
993
1114
  const dateValue = values.orderDate;
@@ -1028,6 +1149,26 @@ function actionResponseError(result, fallback) {
1028
1149
  function asRecord(value) {
1029
1150
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
1030
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
+ }
1031
1172
  function numberValue(value, fallback) {
1032
1173
  const result = Number(value);
1033
1174
  return Number.isFinite(result) ? result : fallback;
@@ -1049,26 +1190,11 @@ function downloadActionFile(value) {
1049
1190
  anchor.href = url;
1050
1191
  anchor.download = file.fileName;
1051
1192
  anchor.click();
1052
- setTimeout(() => URL.revokeObjectURL(url), 0);
1193
+ setTimeout(() => URL.revokeObjectURL(url), DOWNLOAD_URL_REVOKE_DELAY_MS);
1053
1194
  }
1054
- function ContractUploadCommand({ label, busy, onFile }) {
1195
+ function ContractUploadCommand({ label, busy, onFile, onError }) {
1055
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)
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
- };
1197
+ void onFile(file).catch(error => onError?.(error instanceof Error ? error.message : '合同上传失败')); } })] });
1072
1198
  }
1073
1199
  async function attachPurchaseSource(draft, task, owner) {
1074
1200
  if (draft.sourceFileUrl || !task.sourceFileId)
@@ -1107,7 +1233,7 @@ async function loadSourcePreview(sourceFileId, slot, owner) {
1107
1233
  throw new Error('源文件预览响应类型无效');
1108
1234
  }
1109
1235
  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>`;
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>`;
1111
1237
  return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
1112
1238
  }
1113
1239
  function escapeHtml(value) { return value.replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character] ?? character); }