@chenchaolong/plugin-trade-compliance-workbench 1.0.6 → 1.0.8

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 (196) hide show
  1. package/dist/index.d.ts +18 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +20 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/adapters/hsbianma.provider.d.ts +6 -8
  6. package/dist/lib/adapters/hsbianma.provider.d.ts.map +1 -1
  7. package/dist/lib/adapters/hsbianma.provider.js +159 -53
  8. package/dist/lib/adapters/hsbianma.provider.js.map +1 -1
  9. package/dist/lib/adapters/translation.provider.d.ts +7 -1
  10. package/dist/lib/adapters/translation.provider.d.ts.map +1 -1
  11. package/dist/lib/adapters/translation.provider.js +15 -5
  12. package/dist/lib/adapters/translation.provider.js.map +1 -1
  13. package/dist/lib/domain/catalog-localization.d.ts +14 -0
  14. package/dist/lib/domain/catalog-localization.d.ts.map +1 -0
  15. package/dist/lib/domain/catalog-localization.js +19 -0
  16. package/dist/lib/domain/catalog-localization.js.map +1 -0
  17. package/dist/lib/domain/file-name.d.ts +8 -0
  18. package/dist/lib/domain/file-name.d.ts.map +1 -0
  19. package/dist/lib/domain/file-name.js +111 -0
  20. package/dist/lib/domain/file-name.js.map +1 -0
  21. package/dist/lib/domain/import-issues.d.ts +4 -0
  22. package/dist/lib/domain/import-issues.d.ts.map +1 -1
  23. package/dist/lib/domain/import-issues.js +14 -3
  24. package/dist/lib/domain/import-issues.js.map +1 -1
  25. package/dist/lib/entities/catalog.entity.d.ts +5 -1
  26. package/dist/lib/entities/catalog.entity.d.ts.map +1 -1
  27. package/dist/lib/entities/catalog.entity.js +16 -0
  28. package/dist/lib/entities/catalog.entity.js.map +1 -1
  29. package/dist/lib/entities/customer.entity.d.ts +3 -0
  30. package/dist/lib/entities/customer.entity.d.ts.map +1 -1
  31. package/dist/lib/entities/customer.entity.js +12 -0
  32. package/dist/lib/entities/customer.entity.js.map +1 -1
  33. package/dist/lib/entities/shared.entity.d.ts +9 -0
  34. package/dist/lib/entities/shared.entity.d.ts.map +1 -1
  35. package/dist/lib/entities/shared.entity.js +33 -0
  36. package/dist/lib/entities/shared.entity.js.map +1 -1
  37. package/dist/lib/import-jobs/agent-handoff.dispatcher.d.ts +15 -0
  38. package/dist/lib/import-jobs/agent-handoff.dispatcher.d.ts.map +1 -0
  39. package/dist/lib/import-jobs/agent-handoff.dispatcher.js +114 -0
  40. package/dist/lib/import-jobs/agent-handoff.dispatcher.js.map +1 -0
  41. package/dist/lib/import-jobs/contracts.d.ts +76 -0
  42. package/dist/lib/import-jobs/contracts.d.ts.map +1 -0
  43. package/dist/lib/import-jobs/contracts.js +31 -0
  44. package/dist/lib/import-jobs/contracts.js.map +1 -0
  45. package/dist/lib/import-jobs/import-command.builder.d.ts +30 -0
  46. package/dist/lib/import-jobs/import-command.builder.d.ts.map +1 -0
  47. package/dist/lib/import-jobs/import-command.builder.js +97 -0
  48. package/dist/lib/import-jobs/import-command.builder.js.map +1 -0
  49. package/dist/lib/import-jobs/import-job-callback.processor.d.ts +15 -0
  50. package/dist/lib/import-jobs/import-job-callback.processor.d.ts.map +1 -0
  51. package/dist/lib/import-jobs/import-job-callback.processor.js +107 -0
  52. package/dist/lib/import-jobs/import-job-callback.processor.js.map +1 -0
  53. package/dist/lib/import-jobs/import-job.coordinator.d.ts +17 -0
  54. package/dist/lib/import-jobs/import-job.coordinator.d.ts.map +1 -0
  55. package/dist/lib/import-jobs/import-job.coordinator.js +256 -0
  56. package/dist/lib/import-jobs/import-job.coordinator.js.map +1 -0
  57. package/dist/lib/import-jobs/import-job.processor.d.ts +24 -0
  58. package/dist/lib/import-jobs/import-job.processor.d.ts.map +1 -0
  59. package/dist/lib/import-jobs/import-job.processor.js +213 -0
  60. package/dist/lib/import-jobs/import-job.processor.js.map +1 -0
  61. package/dist/lib/import-jobs/import-timeout.processor.d.ts +12 -0
  62. package/dist/lib/import-jobs/import-timeout.processor.d.ts.map +1 -0
  63. package/dist/lib/import-jobs/import-timeout.processor.js +37 -0
  64. package/dist/lib/import-jobs/import-timeout.processor.js.map +1 -0
  65. package/dist/lib/import-jobs/organization-lease.d.ts +39 -0
  66. package/dist/lib/import-jobs/organization-lease.d.ts.map +1 -0
  67. package/dist/lib/import-jobs/organization-lease.js +136 -0
  68. package/dist/lib/import-jobs/organization-lease.js.map +1 -0
  69. package/dist/lib/plugin-context.token.d.ts +4 -0
  70. package/dist/lib/plugin-context.token.d.ts.map +1 -0
  71. package/dist/lib/plugin-context.token.js +2 -0
  72. package/dist/lib/plugin-context.token.js.map +1 -0
  73. package/dist/lib/public-dto.d.ts +6 -0
  74. package/dist/lib/public-dto.d.ts.map +1 -0
  75. package/dist/lib/public-dto.js +33 -0
  76. package/dist/lib/public-dto.js.map +1 -0
  77. package/dist/lib/remote-components/trade-compliance-workbench/app.css +1 -1
  78. package/dist/lib/remote-components/trade-compliance-workbench/app.js +5 -5
  79. package/dist/lib/remote-ui/action-execution.d.ts +26 -0
  80. package/dist/lib/remote-ui/action-execution.d.ts.map +1 -0
  81. package/dist/lib/remote-ui/action-execution.js +85 -0
  82. package/dist/lib/remote-ui/action-execution.js.map +1 -0
  83. package/dist/lib/remote-ui/analytics-contracts.d.ts +3 -2
  84. package/dist/lib/remote-ui/analytics-contracts.d.ts.map +1 -1
  85. package/dist/lib/remote-ui/analytics-contracts.js.map +1 -1
  86. package/dist/lib/remote-ui/app-source.d.ts.map +1 -1
  87. package/dist/lib/remote-ui/app-source.js +462 -176
  88. package/dist/lib/remote-ui/app-source.js.map +1 -1
  89. package/dist/lib/remote-ui/company-logo-lifecycle.d.ts +14 -0
  90. package/dist/lib/remote-ui/company-logo-lifecycle.d.ts.map +1 -0
  91. package/dist/lib/remote-ui/company-logo-lifecycle.js +52 -0
  92. package/dist/lib/remote-ui/company-logo-lifecycle.js.map +1 -0
  93. package/dist/lib/remote-ui/components/data-grid.d.ts +9 -4
  94. package/dist/lib/remote-ui/components/data-grid.d.ts.map +1 -1
  95. package/dist/lib/remote-ui/components/data-grid.js +41 -9
  96. package/dist/lib/remote-ui/components/data-grid.js.map +1 -1
  97. package/dist/lib/remote-ui/components/forms.d.ts +6 -1
  98. package/dist/lib/remote-ui/components/forms.d.ts.map +1 -1
  99. package/dist/lib/remote-ui/components/forms.js +6 -1
  100. package/dist/lib/remote-ui/components/forms.js.map +1 -1
  101. package/dist/lib/remote-ui/components/layout.d.ts.map +1 -1
  102. package/dist/lib/remote-ui/components/layout.js +4 -3
  103. package/dist/lib/remote-ui/components/layout.js.map +1 -1
  104. package/dist/lib/remote-ui/contract-import.d.ts +37 -32
  105. package/dist/lib/remote-ui/contract-import.d.ts.map +1 -1
  106. package/dist/lib/remote-ui/contract-import.js +51 -59
  107. package/dist/lib/remote-ui/contract-import.js.map +1 -1
  108. package/dist/lib/remote-ui/contracts.d.ts +20 -27
  109. package/dist/lib/remote-ui/contracts.d.ts.map +1 -1
  110. package/dist/lib/remote-ui/contracts.js +8 -0
  111. package/dist/lib/remote-ui/contracts.js.map +1 -1
  112. package/dist/lib/remote-ui/customer-contract-contracts.d.ts +5 -11
  113. package/dist/lib/remote-ui/customer-contract-contracts.d.ts.map +1 -1
  114. package/dist/lib/remote-ui/customer-contract-contracts.js.map +1 -1
  115. package/dist/lib/remote-ui/date-time.d.ts +3 -0
  116. package/dist/lib/remote-ui/date-time.d.ts.map +1 -0
  117. package/dist/lib/remote-ui/date-time.js +58 -0
  118. package/dist/lib/remote-ui/date-time.js.map +1 -0
  119. package/dist/lib/remote-ui/model.d.ts +26 -1
  120. package/dist/lib/remote-ui/model.d.ts.map +1 -1
  121. package/dist/lib/remote-ui/model.js +151 -15
  122. package/dist/lib/remote-ui/model.js.map +1 -1
  123. package/dist/lib/remote-ui/pages/analytics.d.ts +6 -0
  124. package/dist/lib/remote-ui/pages/analytics.d.ts.map +1 -1
  125. package/dist/lib/remote-ui/pages/analytics.js +56 -6
  126. package/dist/lib/remote-ui/pages/analytics.js.map +1 -1
  127. package/dist/lib/remote-ui/pages/catalogs.d.ts +35 -14
  128. package/dist/lib/remote-ui/pages/catalogs.d.ts.map +1 -1
  129. package/dist/lib/remote-ui/pages/catalogs.js +139 -89
  130. package/dist/lib/remote-ui/pages/catalogs.js.map +1 -1
  131. package/dist/lib/remote-ui/pages/contracts.d.ts +10 -5
  132. package/dist/lib/remote-ui/pages/contracts.d.ts.map +1 -1
  133. package/dist/lib/remote-ui/pages/contracts.js +29 -26
  134. package/dist/lib/remote-ui/pages/contracts.js.map +1 -1
  135. package/dist/lib/remote-ui/pages/files.d.ts +55 -4
  136. package/dist/lib/remote-ui/pages/files.d.ts.map +1 -1
  137. package/dist/lib/remote-ui/pages/files.js +121 -14
  138. package/dist/lib/remote-ui/pages/files.js.map +1 -1
  139. package/dist/lib/remote-ui/pages/home.js +2 -2
  140. package/dist/lib/remote-ui/pages/home.js.map +1 -1
  141. package/dist/lib/remote-ui/pages/procurement.d.ts +6 -4
  142. package/dist/lib/remote-ui/pages/procurement.d.ts.map +1 -1
  143. package/dist/lib/remote-ui/pages/procurement.js +69 -66
  144. package/dist/lib/remote-ui/pages/procurement.js.map +1 -1
  145. package/dist/lib/remote-ui/pages/sales.d.ts +51 -2
  146. package/dist/lib/remote-ui/pages/sales.d.ts.map +1 -1
  147. package/dist/lib/remote-ui/pages/sales.js +106 -19
  148. package/dist/lib/remote-ui/pages/sales.js.map +1 -1
  149. package/dist/lib/remote-ui/pages/settings.d.ts +29 -14
  150. package/dist/lib/remote-ui/pages/settings.d.ts.map +1 -1
  151. package/dist/lib/remote-ui/pages/settings.js +76 -69
  152. package/dist/lib/remote-ui/pages/settings.js.map +1 -1
  153. package/dist/lib/remote-ui/polling.d.ts +41 -0
  154. package/dist/lib/remote-ui/polling.d.ts.map +1 -0
  155. package/dist/lib/remote-ui/polling.js +148 -0
  156. package/dist/lib/remote-ui/polling.js.map +1 -0
  157. package/dist/lib/remote-ui/procurement-contracts.d.ts +4 -9
  158. package/dist/lib/remote-ui/procurement-contracts.d.ts.map +1 -1
  159. package/dist/lib/remote-ui/sales-contracts.d.ts +5 -3
  160. package/dist/lib/remote-ui/sales-contracts.d.ts.map +1 -1
  161. package/dist/lib/remote-ui/settings-contracts.d.ts +9 -1
  162. package/dist/lib/remote-ui/settings-contracts.d.ts.map +1 -1
  163. package/dist/lib/remote-ui/settings-contracts.js.map +1 -1
  164. package/dist/lib/remote-ui/toast-queue.d.ts +29 -0
  165. package/dist/lib/remote-ui/toast-queue.d.ts.map +1 -0
  166. package/dist/lib/remote-ui/toast-queue.js +71 -0
  167. package/dist/lib/remote-ui/toast-queue.js.map +1 -0
  168. package/dist/lib/sales-file.service.d.ts +13 -35
  169. package/dist/lib/sales-file.service.d.ts.map +1 -1
  170. package/dist/lib/sales-file.service.js +39 -21
  171. package/dist/lib/sales-file.service.js.map +1 -1
  172. package/dist/lib/types.d.ts +22 -3
  173. package/dist/lib/types.d.ts.map +1 -1
  174. package/dist/lib/types.js +5 -0
  175. package/dist/lib/types.js.map +1 -1
  176. package/dist/lib/workbench-action-contract.d.ts +4 -0
  177. package/dist/lib/workbench-action-contract.d.ts.map +1 -0
  178. package/dist/lib/workbench-action-contract.js +43 -0
  179. package/dist/lib/workbench-action-contract.js.map +1 -0
  180. package/dist/lib/workbench-view.provider.d.ts +57 -78
  181. package/dist/lib/workbench-view.provider.d.ts.map +1 -1
  182. package/dist/lib/workbench-view.provider.js +104 -94
  183. package/dist/lib/workbench-view.provider.js.map +1 -1
  184. package/dist/lib/workbench.middleware.d.ts +129 -72
  185. package/dist/lib/workbench.middleware.d.ts.map +1 -1
  186. package/dist/lib/workbench.middleware.js +16 -3
  187. package/dist/lib/workbench.middleware.js.map +1 -1
  188. package/dist/lib/workbench.plugin.d.ts +3 -1
  189. package/dist/lib/workbench.plugin.d.ts.map +1 -1
  190. package/dist/lib/workbench.plugin.js +13 -4
  191. package/dist/lib/workbench.plugin.js.map +1 -1
  192. package/dist/lib/workbench.service.d.ts +136 -40
  193. package/dist/lib/workbench.service.d.ts.map +1 -1
  194. package/dist/lib/workbench.service.js +985 -71
  195. package/dist/lib/workbench.service.js.map +1 -1
  196. package/package.json +6 -5
@@ -2,20 +2,24 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import React from 'react';
3
3
  import { createRoot } from 'react-dom/client';
4
4
  import { createRemoteBridge } from './bridge.js';
5
- import { createBlobUrlOwner, createLatestCommittedRequest, createLatestRequestGate, normalizeWorkbenchData, routeDataRequest, unwrapResponse } from './model.js';
5
+ import { resolvePaginationPageSize } from './contracts.js';
6
+ import { clampPageAfterSingleDeletion, createBlobUrlOwner, createKeyedLatestRequestGate, createLatestCommittedRequest, createLatestRequestGate, generatedFileHistoryKey, normalizeWorkbenchData, routeDataRequest, runKeyedLatestRequest, unwrapResponse } from './model.js';
6
7
  import { encodeViewFilters } from '../view-parameters.js';
7
8
  import { InfoNotice } from './components/data-grid.js';
8
- import { LoadingBlocker, Toast } from './components/forms.js';
9
+ import { LoadingBlocker, ToastStack } from './components/forms.js';
10
+ import { addToast, createToastId, createToastTimerRegistry, removeToast } from './toast-queue.js';
9
11
  import { buildBreadcrumbs, WorkbenchShell } from './components/layout.js';
10
- import { CatalogCommands, CatalogImportError, ControlledGoodsPage, findActiveCatalogImportTask, findLatestFailedCatalogImportTask, runCatalogImportOrchestration, SanctionedCompaniesPage } from './pages/catalogs.js';
12
+ import { CatalogCommands, CatalogImportError, ControlledGoodsPage, findActiveCatalogImportTasks, findLatestFailedCatalogImportTask, runCatalogImportOrchestration, SanctionedCompaniesPage } from './pages/catalogs.js';
11
13
  import { HomePage } from './pages/home.js';
12
14
  import { ProcurementPage } from './pages/procurement.js';
13
15
  import { CustomerContractsPage, adaptCustomerContractReview } from './pages/contracts.js';
14
16
  import { SalesPage } from './pages/sales.js';
15
17
  import { SalesFilesPage } from './pages/files.js';
16
- import { AnalyticsPage } from './pages/analytics.js';
18
+ import { AnalyticsPage, AnalyticsPrototypeBoard } from './pages/analytics.js';
17
19
  import { SettingsPage } from './pages/settings.js';
18
- import { ContractImportError, findRecoverableContractImport, findResumableContractImport, runContractImportOrchestration, runCustomerContractReanalysis } from './contract-import.js';
20
+ import { ContractImportError, findRecoverableContractImport, findResumableContractImports, runContractImportOrchestration, runCustomerContractReanalysis, taskTransition } from './contract-import.js';
21
+ import { compareTaskBusinessOrder, createPendingResultQueue, createPollingTaskTracker, isAbortError, isPollingObservationTimeout, submitAndRefreshTaskState } from './polling.js';
22
+ import { executeWorkbenchAction } from './action-execution.js';
19
23
  import './styles.css';
20
24
  const pageMeta = {
21
25
  home: { title: '首页', subtitle: '查看系统风险操作提示与外贸合规业务流程。' },
@@ -30,24 +34,50 @@ const pageMeta = {
30
34
  };
31
35
  const bridge = createRemoteBridge();
32
36
  const initialAnalyticsQuery = { mode: 'PURCHASE', dimension: 'SUPPLIER', filters: {}, page: 1, pageSize: 20 };
37
+ function comparePendingReviewBusinessOrder(left, right) {
38
+ const leftTask = left.kind === 'CONTRACT_IMPORT' ? left.completion.task : left.result.run;
39
+ const rightTask = right.kind === 'CONTRACT_IMPORT' ? right.completion.task : right.result.run;
40
+ return compareTaskBusinessOrder(leftTask, rightTask);
41
+ }
42
+ class ActionResponseError extends Error {
43
+ constructor(message, code) {
44
+ super(message);
45
+ this.code = code;
46
+ this.name = 'ActionResponseError';
47
+ }
48
+ }
33
49
  let excelEditorRuntimePromise = null;
34
50
  function App() {
35
51
  const [route, setRoute] = React.useState({ type: 'page', page: 'home' });
36
52
  const routeRef = React.useRef(route);
37
53
  const contextRef = React.useRef(null);
38
54
  const requestGateRef = React.useRef(createLatestRequestGate());
55
+ const taskStateRequestGateRef = React.useRef(createLatestRequestGate());
56
+ const taskRecoveryTimerRef = React.useRef(null);
39
57
  const routeRequestGateRef = React.useRef(createLatestRequestGate());
58
+ const settingsRatesRequestGateRef = React.useRef(createLatestRequestGate());
59
+ const generatedFilesRequestGateRef = React.useRef(createKeyedLatestRequestGate());
60
+ const activeGeneratedFilesKeyRef = React.useRef(null);
61
+ const settingsRatesQueryRef = React.useRef({ page: 1, pageSize: 20 });
40
62
  const analyticsRequestsRef = React.useRef(createLatestCommittedRequest(initialAnalyticsQuery));
41
63
  const [analyticsCommittedQuery, setAnalyticsCommittedQuery] = React.useState(initialAnalyticsQuery);
64
+ const analyticsPrototypeListenerCleanupRef = React.useRef(null);
65
+ const [analyticsPrototypeVisible, setAnalyticsPrototypeVisible] = React.useState(false);
42
66
  const [data, setData] = React.useState(() => normalizeWorkbenchData(null));
43
67
  const [busy, setBusy] = React.useState(false);
44
68
  const [actionBusy, setActionBusy] = React.useState(false);
45
69
  const [catalogCommand, setCatalogCommand] = React.useState(null);
46
- const [importCompletion, setImportCompletion] = React.useState(null);
70
+ const [importCompletions, setImportCompletions] = React.useState({ CONTROLLED_CATALOG: null, SANCTION_CATALOG: null });
47
71
  const [importFailures, setImportFailures] = React.useState({ CONTROLLED_CATALOG: null, SANCTION_CATALOG: null });
48
- const runningImportTaskRef = React.useRef(null);
49
- const runningContractTasksRef = React.useRef(new Set());
50
- const handledContractTasksRef = React.useRef({ PURCHASE_CONTRACT: null, CUSTOMER_CONTRACT: null });
72
+ const catalogTaskTrackerRef = React.useRef(createPollingTaskTracker());
73
+ const contractTaskTrackerRef = React.useRef(createPollingTaskTracker());
74
+ const reanalysisTaskTrackerRef = React.useRef(createPollingTaskTracker());
75
+ const handledContractTasksRef = React.useRef(new Set());
76
+ const handledReanalysisResultsRef = React.useRef(new Set());
77
+ const pendingReviewResultsRef = React.useRef(createPendingResultQueue(item => item.kind === 'CONTRACT_IMPORT' ? item.completion.task.id : item.result.run.id, comparePendingReviewBusinessOrder));
78
+ const activeReviewResultRef = React.useRef(null);
79
+ const presentNextReviewRef = React.useRef(() => undefined);
80
+ const reviewPresentationTimerRef = React.useRef(null);
51
81
  const blobUrlOwnerRef = React.useRef(createBlobUrlOwner());
52
82
  const changeRouteRef = React.useRef(() => undefined);
53
83
  const [procurementContext, setProcurementContext] = React.useState({ supplier: null, order: null, metrics: null });
@@ -57,15 +87,34 @@ function App() {
57
87
  const [customerReanalysisState, setCustomerReanalysisState] = React.useState({ status: 'IDLE' });
58
88
  const [activeSalesFileOrder, setActiveSalesFileOrder] = React.useState(null);
59
89
  const [editingSalesFile, setEditingSalesFile] = React.useState(null);
60
- const [toast, setToast] = React.useState(null);
90
+ const [toasts, setToasts] = React.useState([]);
91
+ const [toastTimers] = React.useState(() => createToastTimerRegistry((id) => setToasts((items) => removeToast(items, id))));
61
92
  const [analyticsError, setAnalyticsError] = React.useState(null);
93
+ const setToast = React.useCallback((toast) => {
94
+ setToasts((items) => addToast(items, toast, createToastId));
95
+ }, []);
96
+ const closeToast = React.useCallback((id) => {
97
+ toastTimers.dismiss(id);
98
+ setToasts((items) => removeToast(items, id));
99
+ }, [toastTimers]);
100
+ const configureAnalyticsPrototypeCapability = React.useCallback((parameters) => {
101
+ analyticsPrototypeListenerCleanupRef.current?.();
102
+ analyticsPrototypeListenerCleanupRef.current = null;
103
+ setAnalyticsPrototypeVisible(false);
104
+ if (parameters.visualCheck !== true)
105
+ return;
106
+ const showPrototypeBoard = () => setAnalyticsPrototypeVisible(true);
107
+ window.addEventListener('analytics-prototype-board', showPrototypeBoard);
108
+ analyticsPrototypeListenerCleanupRef.current = () => window.removeEventListener('analytics-prototype-board', showPrototypeBoard);
109
+ }, []);
62
110
  const loadRoute = React.useCallback(async (targetRoute, page = 1, pageSize = 20, filters = {}) => {
63
- const request = routeDataRequest(targetRoute, page, pageSize, filters);
111
+ const requestedPageSize = resolvePaginationPageSize(pageSize);
112
+ const request = routeDataRequest(targetRoute, page, requestedPageSize, filters);
64
113
  if (!request)
65
114
  return;
66
115
  const collection = String(asRecord(request.parameters).collection);
67
116
  const requests = collection === 'analytics' ? analyticsRequestsRef.current : routeRequestGateRef.current;
68
- const analyticsCandidate = collection === 'analytics' ? analyticsQueryFromRoute(page, pageSize, filters) : null;
117
+ const analyticsCandidate = collection === 'analytics' ? analyticsQueryFromRoute(page, requestedPageSize, filters) : null;
69
118
  const requestId = analyticsCandidate ? analyticsRequestsRef.current.begin(analyticsCandidate) : routeRequestGateRef.current.begin();
70
119
  setBusy(true);
71
120
  try {
@@ -78,15 +127,18 @@ function App() {
78
127
  setAnalyticsCommittedQuery(analyticsRequestsRef.current.committed());
79
128
  setData(current => ({ ...current, analytics: normalizeWorkbenchData({ summary: { analytics: payload } }).analytics }));
80
129
  }
81
- else if (collection === 'settings')
82
- setData(current => ({ ...current, settings: normalizeWorkbenchData({ summary: { settings: payload } }).settings }));
130
+ else if (collection === 'settings') {
131
+ const settings = normalizeWorkbenchData({ summary: { settings: payload } }).settings;
132
+ settingsRatesQueryRef.current = { page: settings.rates.page, pageSize: settings.rates.pageSize };
133
+ setData(current => ({ ...current, settings }));
134
+ }
83
135
  else
84
136
  setData(current => ({
85
137
  ...current,
86
138
  [collection]: {
87
139
  ...payload,
88
140
  items: Array.isArray(payload.items) ? payload.items : [],
89
- total: numberValue(payload.total, 0), page: numberValue(payload.page, page), pageSize: numberValue(payload.pageSize, pageSize)
141
+ total: numberValue(payload.total, 0), page: numberValue(payload.page, page), pageSize: requestedPageSize
90
142
  }
91
143
  }));
92
144
  if (targetRoute.type === 'procurement')
@@ -116,7 +168,7 @@ function App() {
116
168
  const requestId = gate.begin();
117
169
  setBusy(true);
118
170
  try {
119
- const response = await bridge.requestData({ page: 1, pageSize: 200, parameters: active.initialQuery?.parameters ?? {} });
171
+ const response = await bridge.requestData({ page: 1, pageSize: 100, parameters: active.initialQuery?.parameters ?? {} });
120
172
  if (gate.isLatest(requestId)) {
121
173
  const normalized = normalizeWorkbenchData(response);
122
174
  bridge.configure({ uploadPolicy: normalized.uploadPolicy });
@@ -133,9 +185,68 @@ function App() {
133
185
  setBusy(gate.activeCount() + routeRequestGateRef.current.activeCount() + analyticsRequestsRef.current.activeCount() > 0);
134
186
  }
135
187
  }, [loadRoute]);
188
+ const loadTaskState = React.useCallback(async () => {
189
+ const active = contextRef.current;
190
+ if (!active)
191
+ return false;
192
+ const gate = taskStateRequestGateRef.current;
193
+ const requestId = gate.begin();
194
+ try {
195
+ const response = await bridge.requestData({ page: 1, pageSize: 100, parameters: active.initialQuery?.parameters ?? {} });
196
+ if (!gate.commit(requestId))
197
+ return false;
198
+ const normalized = normalizeWorkbenchData(response);
199
+ setData(current => ({
200
+ ...current,
201
+ controlledGoods: normalized.controlledGoods,
202
+ controlledBatches: normalized.controlledBatches,
203
+ sanctionedCompanies: normalized.sanctionedCompanies,
204
+ sanctionBatches: normalized.sanctionBatches,
205
+ importTasks: normalized.importTasks,
206
+ customerContractAnalyses: normalized.customerContractAnalyses
207
+ }));
208
+ return true;
209
+ }
210
+ catch (error) {
211
+ if (gate.isLatest(requestId))
212
+ setToast({ message: error instanceof Error ? `任务状态刷新失败:${error.message}` : '任务状态刷新失败', tone: 'error' });
213
+ return false;
214
+ }
215
+ finally {
216
+ gate.finish(requestId);
217
+ }
218
+ }, []);
219
+ const scheduleTaskStateRecovery = React.useCallback(() => {
220
+ if (taskRecoveryTimerRef.current != null)
221
+ return;
222
+ taskRecoveryTimerRef.current = window.setInterval(() => {
223
+ void loadTaskState().then(refreshed => {
224
+ if (!refreshed || taskRecoveryTimerRef.current == null)
225
+ return;
226
+ window.clearInterval(taskRecoveryTimerRef.current);
227
+ taskRecoveryTimerRef.current = null;
228
+ });
229
+ }, 2000);
230
+ }, [loadTaskState]);
231
+ React.useEffect(() => () => {
232
+ if (taskRecoveryTimerRef.current != null)
233
+ window.clearInterval(taskRecoveryTimerRef.current);
234
+ if (reviewPresentationTimerRef.current != null)
235
+ window.clearTimeout(reviewPresentationTimerRef.current);
236
+ }, []);
237
+ React.useEffect(() => {
238
+ return () => catalogTaskTrackerRef.current.abortAll();
239
+ }, []);
240
+ React.useEffect(() => {
241
+ return () => contractTaskTrackerRef.current.abortAll();
242
+ }, []);
243
+ React.useEffect(() => {
244
+ return () => reanalysisTaskTrackerRef.current.abortAll();
245
+ }, []);
136
246
  React.useEffect(() => {
137
247
  bridge.start({
138
248
  onInit(nextContext) {
249
+ configureAnalyticsPrototypeCapability(asRecord(nextContext.initialQuery?.parameters));
139
250
  contextRef.current = nextContext;
140
251
  const normalized = normalizeWorkbenchData(nextContext.payload);
141
252
  bridge.configure({ uploadPolicy: normalized.uploadPolicy });
@@ -146,86 +257,94 @@ function App() {
146
257
  void load();
147
258
  }
148
259
  });
149
- return () => { blobUrlOwnerRef.current.dispose(); bridge.destroy(); };
150
- }, [load]);
260
+ return () => {
261
+ analyticsPrototypeListenerCleanupRef.current?.();
262
+ analyticsPrototypeListenerCleanupRef.current = null;
263
+ blobUrlOwnerRef.current.dispose();
264
+ bridge.destroy();
265
+ };
266
+ }, [configureAnalyticsPrototypeCapability, load]);
267
+ React.useEffect(() => () => toastTimers.dispose(), [toastTimers]);
151
268
  React.useEffect(() => {
152
269
  const timer = window.setTimeout(() => bridge.reportResize(), 0);
153
270
  return () => window.clearTimeout(timer);
154
271
  }, [route, data, busy]);
155
272
  React.useEffect(() => {
156
- if (!toast)
157
- return;
158
- const timer = window.setTimeout(() => setToast(null), 4200);
159
- return () => window.clearTimeout(timer);
160
- }, [toast]);
273
+ toastTimers.sync(toasts);
274
+ }, [toastTimers, toasts]);
161
275
  const executeAction = React.useCallback(async (actionKey, input) => {
162
276
  setActionBusy(true);
163
277
  try {
164
- const result = asActionResult(unwrapResponse(await bridge.executeAction(actionKey, input)));
165
- if (result.success === false)
166
- throw new Error(resolveActionMessage(result) || '操作失败');
167
- if (actionKey === 'download_import_issues')
168
- downloadActionFile(result.data);
169
- if (result.refresh && shouldRefreshAfterAction(actionKey))
170
- await load();
171
- setToast({ message: resolveActionMessage(result) || '操作成功', tone: 'success' });
172
- return result;
173
- }
174
- catch (error) {
175
- setToast({ message: error instanceof Error ? error.message : '操作失败', tone: 'error' });
176
- throw error;
278
+ return await executeWorkbenchAction({
279
+ actionKey,
280
+ mutate: async () => asActionResult(unwrapResponse(await bridge.executeAction(actionKey, input))),
281
+ refresh: load,
282
+ notify: setToast,
283
+ resolveMessage: resolveActionMessage,
284
+ onResult: result => { if (actionKey === 'download_import_issues' || actionKey === 'download_sales_file')
285
+ downloadActionFile(result.data); }
286
+ });
177
287
  }
178
288
  finally {
179
289
  setActionBusy(false);
180
290
  }
181
291
  }, [load]);
182
292
  const executeSalesFileAction = React.useCallback((actionKey, input) => executeAction(actionKey, input), [executeAction]);
183
- const orchestrateImport = React.useCallback(async (start) => {
293
+ const schedulePendingReviewPresentation = React.useCallback(() => {
294
+ if (reviewPresentationTimerRef.current != null)
295
+ return;
296
+ reviewPresentationTimerRef.current = window.setTimeout(() => {
297
+ reviewPresentationTimerRef.current = null;
298
+ presentNextReviewRef.current();
299
+ }, 0);
300
+ }, []);
301
+ const enqueuePendingReview = React.useCallback((result) => {
302
+ pendingReviewResultsRef.current.enqueue(result);
303
+ schedulePendingReviewPresentation();
304
+ }, [schedulePendingReviewPresentation]);
305
+ const orchestrateImport = React.useCallback(async (start, signal) => {
184
306
  const taskId = start.data?.task.id;
185
307
  if (!taskId)
186
308
  throw new Error('解析响应缺少任务标识');
187
- if (runningImportTaskRef.current === taskId)
188
- throw new Error('该目录任务正在解析中');
189
- runningImportTaskRef.current = taskId;
190
- setActionBusy(true);
191
309
  const documentType = start.data?.task.documentType;
192
310
  if (documentType)
193
311
  setImportFailures((current) => ({ ...current, [documentType]: null }));
194
312
  try {
195
313
  const completion = await runCatalogImportOrchestration({
196
314
  start,
197
- invokeClientCommand: (commandKey, payload) => bridge.invokeClientCommand(commandKey, payload),
198
- markCommandDispatched: async (taskId) => {
199
- const result = asActionResult(unwrapResponse(await bridge.executeAction('mark_import_command_dispatched', { taskId })));
200
- if (result.success === false)
201
- throw new Error(resolveActionMessage(result) || '记录解析命令状态失败');
202
- },
203
315
  pollTask: async (taskId) => asImportCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId } }))),
204
- refresh: load
316
+ refresh: loadTaskState,
317
+ signal,
318
+ onTransition: (previous, next) => {
319
+ const notice = taskTransition(previous, next);
320
+ if (notice)
321
+ setToast(notice);
322
+ },
323
+ onPollError: error => setToast({ message: `任务状态查询失败:${error.message}`, tone: 'error' })
205
324
  });
206
- setImportCompletion(completion);
207
- setToast({ message: '目录导入完成', tone: 'success' });
325
+ setImportCompletions(current => ({ ...current, [completion.task.documentType]: completion }));
208
326
  return completion;
209
327
  }
210
328
  catch (error) {
211
- if (error instanceof CatalogImportError)
329
+ if (isAbortError(error))
330
+ throw error;
331
+ if (isPollingObservationTimeout(error)) {
332
+ setToast({ message: '任务仍在后台处理中,将继续跟踪', tone: 'info' });
333
+ scheduleTaskStateRecovery();
334
+ throw error;
335
+ }
336
+ if (error instanceof CatalogImportError) {
212
337
  setImportFailures((current) => ({ ...current, [error.task.documentType]: error.task }));
213
- setToast({ message: error instanceof Error ? error.message : '目录解析失败', tone: 'error' });
338
+ }
339
+ else
340
+ setToast({ message: error instanceof Error ? error.message : '目录解析失败', tone: 'error' });
214
341
  throw error;
215
342
  }
216
- finally {
217
- runningImportTaskRef.current = null;
218
- setActionBusy(false);
219
- }
220
- }, [load]);
221
- const orchestrateContractImport = React.useCallback(async (start) => {
343
+ }, [loadTaskState, scheduleTaskStateRecovery]);
344
+ const orchestrateContractImport = React.useCallback(async (start, signal) => {
222
345
  const task = start.data?.task;
223
346
  if (!task)
224
347
  throw new Error('解析响应缺少合同任务标识');
225
- if (runningContractTasksRef.current.has(task.id))
226
- throw new Error('该合同任务正在解析中');
227
- runningContractTasksRef.current.add(task.id);
228
- setActionBusy(true);
229
348
  if (task.documentType === 'PURCHASE_CONTRACT') {
230
349
  blobUrlOwnerRef.current.clear('purchase');
231
350
  setPurchaseImportTask(task);
@@ -238,29 +357,28 @@ function App() {
238
357
  try {
239
358
  const completion = await runContractImportOrchestration({
240
359
  start,
241
- invokeClientCommand: (commandKey, payload) => bridge.invokeClientCommand(commandKey, payload),
242
- markCommandDispatched: async (taskId) => {
243
- const result = asActionResult(unwrapResponse(await bridge.executeAction('mark_import_command_dispatched', { taskId })));
244
- if (result.success === false)
245
- throw new Error(resolveActionMessage(result) || '记录解析命令状态失败');
360
+ signal,
361
+ pollTask: async (taskId) => asContractCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId } }))),
362
+ onTransition: (previous, next) => {
363
+ const notice = taskTransition(previous, next);
364
+ if (notice)
365
+ setToast(notice);
246
366
  },
247
- pollTask: async (taskId) => asContractCompletion(unwrapResponse(await bridge.requestData({ parameters: { collection: 'importTask', taskId } })))
367
+ onPollError: error => setToast({ message: `任务状态查询失败:${error.message}`, tone: 'error' })
248
368
  });
249
- handledContractTasksRef.current[task.documentType] = task.id;
250
369
  if (completion.task.status === 'AWAITING_REVIEW') {
251
- if (completion.task.documentType === 'PURCHASE_CONTRACT') {
252
- const draft = adaptPurchaseReview(completion.task);
253
- setPurchaseImportTask(completion.task);
254
- setPurchaseReviewDraft(await attachPurchaseSource(draft, completion.task, blobUrlOwnerRef.current));
255
- }
256
- else {
257
- const review = adaptCustomerContractReview(completion.task.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: completion.task.id, sourceFileId: completion.task.sourceFileId ?? '', sourceFileName: completion.task.sourceFileName ?? '' });
258
- setCustomerImportState({ status: 'AWAITING_REVIEW', importTaskId: completion.task.id, review: await attachCustomerSource(review, blobUrlOwnerRef.current) });
259
- }
370
+ enqueuePendingReview({ kind: 'CONTRACT_IMPORT', completion });
260
371
  }
261
372
  return completion;
262
373
  }
263
374
  catch (error) {
375
+ if (isAbortError(error))
376
+ throw error;
377
+ if (isPollingObservationTimeout(error)) {
378
+ setToast({ message: '任务仍在后台处理中,将继续跟踪', tone: 'info' });
379
+ scheduleTaskStateRecovery();
380
+ throw error;
381
+ }
264
382
  const failed = error instanceof ContractImportError ? error.task : task;
265
383
  if (task.documentType === 'PURCHASE_CONTRACT') {
266
384
  setPurchaseImportTask({ ...failed, status: failed.status === 'TIMED_OUT' ? 'TIMED_OUT' : 'FAILED', errorMessage: error instanceof Error ? error.message : '采购合同解析失败' });
@@ -268,62 +386,58 @@ function App() {
268
386
  }
269
387
  else
270
388
  setCustomerImportState({ status: 'FAILED', importTaskId: failed.id, message: error instanceof Error ? error.message : '客户合同解析失败' });
271
- setToast({ message: error instanceof Error ? error.message : '合同解析失败', tone: 'error' });
389
+ if (!(error instanceof ContractImportError))
390
+ setToast({ message: error instanceof Error ? error.message : '合同解析失败', tone: 'error' });
272
391
  throw error;
273
392
  }
274
- finally {
275
- runningContractTasksRef.current.delete(task.id);
276
- setActionBusy(false);
277
- }
278
- }, []);
279
- const executeFileAction = React.useCallback(async (actionKey, file, input) => {
393
+ }, [enqueuePendingReview, scheduleTaskStateRecovery]);
394
+ const executeFileAction = React.useCallback(async (actionKey, file, input, notifyError = true) => {
280
395
  try {
281
- const start = asActionResult(unwrapResponse(await bridge.executeFileAction(actionKey, file, input)));
396
+ const raw = await submitAndRefreshTaskState(() => bridge.executeFileAction(actionKey, file, input), loadTaskState);
397
+ const start = asActionResult(unwrapResponse(raw));
282
398
  if (start.success === false)
283
- throw new Error(resolveActionMessage(start) || '上传失败');
284
- if (actionKey === 'upload_purchase_contract' || actionKey === 'upload_customer_contract') {
285
- const completion = await orchestrateContractImport(start);
286
- return { success: true, data: completion, message: '合同解析完成,等待审核', refresh: false };
287
- }
288
- const completion = await orchestrateImport(start);
289
- return { success: true, data: completion, message: '目录导入完成', refresh: true };
399
+ throw actionResponseError(start, '上传失败');
400
+ setToast({ message: resolveActionMessage(start) || '解析任务已提交', tone: 'info' });
401
+ return start;
290
402
  }
291
403
  catch (error) {
292
- if (!(error instanceof CatalogImportError))
404
+ if (notifyError && !(error instanceof CatalogImportError))
293
405
  setToast({ message: error instanceof Error ? error.message : '上传失败', tone: 'error' });
294
406
  throw error;
295
407
  }
296
- }, [orchestrateContractImport, orchestrateImport]);
408
+ }, [loadTaskState]);
409
+ const executeCatalogFileAction = React.useCallback((actionKey, file, input) => executeFileAction(actionKey, file, input, false), [executeFileAction]);
297
410
  React.useEffect(() => {
298
411
  const tasks = data.importTasks.items;
299
412
  setImportFailures({
300
413
  CONTROLLED_CATALOG: findLatestFailedCatalogImportTask(tasks, 'CONTROLLED_CATALOG'),
301
414
  SANCTION_CATALOG: findLatestFailedCatalogImportTask(tasks, 'SANCTION_CATALOG')
302
415
  });
303
- const active = findActiveCatalogImportTask(tasks);
304
- if (active && runningImportTaskRef.current !== active.id && active.executionCommand) {
305
- void orchestrateImport({ success: true, data: { task: active, commands: [active.executionCommand], polling: { intervalMs: 2000, timeoutMs: 1200000 } } }).catch(() => undefined);
306
- return;
416
+ for (const active of findActiveCatalogImportTasks(tasks)) {
417
+ const running = catalogTaskTrackerRef.current.run(active.id, signal => orchestrateImport({ success: true, data: { task: active } }, signal));
418
+ if (running)
419
+ void running.catch(() => undefined);
307
420
  }
308
421
  }, [data.importTasks.items, orchestrateImport]);
309
422
  React.useEffect(() => {
310
423
  const tasks = data.importTasks.items;
424
+ for (const resumable of findResumableContractImports(tasks)) {
425
+ if (handledContractTasksRef.current.has(resumable.id))
426
+ continue;
427
+ const running = contractTaskTrackerRef.current.run(resumable.id, signal => orchestrateContractImport({ success: true, data: { task: resumable } }, signal));
428
+ if (running)
429
+ void running.catch(() => undefined);
430
+ }
311
431
  for (const documentType of ['PURCHASE_CONTRACT', 'CUSTOMER_CONTRACT']) {
312
- const resumable = findResumableContractImport(tasks, documentType);
313
432
  const failed = findRecoverableContractImport(tasks, documentType);
314
- if (resumable && handledContractTasksRef.current[documentType] !== resumable.id && !runningContractTasksRef.current.has(resumable.id)) {
315
- if (resumable.status === 'AWAITING_REVIEW' || resumable.executionCommand) {
316
- void orchestrateContractImport({ success: true, data: { task: resumable, commands: resumable.executionCommand ? [resumable.executionCommand] : [], polling: { intervalMs: 2000, timeoutMs: 1200000 } } }).catch(() => undefined);
317
- }
318
- continue;
319
- }
320
- if (!resumable && failed) {
433
+ const hasResumable = tasks.some(task => task.documentType === documentType && (task.status === 'AWAITING_REVIEW' || ['QUEUED', 'EXTRACTING', 'AI_PARSING', 'VALIDATING', 'PERSISTING'].includes(task.status)));
434
+ if (!hasResumable && failed) {
321
435
  if (documentType === 'PURCHASE_CONTRACT')
322
436
  setPurchaseImportTask(failed);
323
437
  else
324
438
  setCustomerImportState({ status: 'FAILED', importTaskId: failed.id, message: failed.errorMessage || '客户合同解析失败,请重新解析' });
325
439
  }
326
- if (!resumable && !failed && tasks.some(item => item.documentType === documentType && item.status === 'SUCCEEDED')) {
440
+ if (!hasResumable && !failed && tasks.some(item => item.documentType === documentType && item.status === 'SUCCEEDED')) {
327
441
  if (documentType === 'PURCHASE_CONTRACT') {
328
442
  setPurchaseImportTask(null);
329
443
  setPurchaseReviewDraft(null);
@@ -335,65 +449,173 @@ function App() {
335
449
  }, [data.importTasks.items, orchestrateContractImport]);
336
450
  const retryImport = React.useCallback(async (taskId) => {
337
451
  try {
338
- const start = asActionResult(unwrapResponse(await bridge.executeAction('retry_import_task', { taskId })));
452
+ const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState);
453
+ const start = asActionResult(unwrapResponse(raw));
339
454
  if (start.success === false)
340
- throw new Error(resolveActionMessage(start) || '重新解析失败');
341
- await orchestrateImport(start);
455
+ throw actionResponseError(start, '重新解析失败');
456
+ setToast({ message: resolveActionMessage(start) || '重新解析任务已提交', tone: 'info' });
342
457
  }
343
458
  catch (error) {
344
459
  if (!(error instanceof CatalogImportError))
345
460
  setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
346
461
  throw error;
347
462
  }
348
- }, [orchestrateImport]);
463
+ }, [loadTaskState]);
349
464
  const retryContractImport = React.useCallback(async (taskId) => {
350
- const start = asActionResult(unwrapResponse(await bridge.executeAction('retry_import_task', { taskId })));
351
- if (start.success === false)
352
- throw new Error(resolveActionMessage(start) || '重新解析失败');
353
- const documentType = start.data?.task.documentType;
354
- if (documentType === 'PURCHASE_CONTRACT' || documentType === 'CUSTOMER_CONTRACT')
355
- handledContractTasksRef.current[documentType] = null;
356
- await orchestrateContractImport(start);
357
- }, [orchestrateContractImport]);
465
+ try {
466
+ const raw = await submitAndRefreshTaskState(() => bridge.executeAction('retry_import_task', { taskId }), loadTaskState);
467
+ const start = asActionResult(unwrapResponse(raw));
468
+ if (start.success === false)
469
+ throw actionResponseError(start, '重新解析失败');
470
+ setToast({ message: resolveActionMessage(start) || '重新解析任务已提交', tone: 'info' });
471
+ }
472
+ catch (error) {
473
+ setToast({ message: error instanceof Error ? error.message : '重新解析失败', tone: 'error' });
474
+ throw error;
475
+ }
476
+ }, [loadTaskState]);
477
+ const applyCustomerContractReanalysisResult = React.useCallback(async (result) => {
478
+ let review;
479
+ if (result.review) {
480
+ review = adaptCustomerContractReview(result.review, { reviewSource: 'EXISTING_CONTRACT' });
481
+ }
482
+ else if (result.task) {
483
+ review = adaptCustomerContractReview(result.task.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: result.task.id, sourceFileId: result.task.sourceFileId ?? '', sourceFileName: result.task.sourceFileName ?? '' });
484
+ }
485
+ else {
486
+ throw new Error('重新分析结果缺少合同审核数据');
487
+ }
488
+ const attached = await attachCustomerSource(review, blobUrlOwnerRef.current);
489
+ setCustomerImportState({ status: 'AWAITING_REVIEW', importTaskId: result.task?.id ?? result.run.importTaskId ?? '', review: attached });
490
+ changeRouteRef.current({ type: 'page', page: 'customer-contract-review' });
491
+ return attached;
492
+ }, []);
493
+ const consumeCustomerContractReanalysisResult = React.useCallback(async (runId) => {
494
+ const consumed = asActionResult(unwrapResponse(await bridge.executeAction('consume_customer_contract_reanalysis_result', { runId })));
495
+ if (consumed.success === false)
496
+ throw new Error(resolveActionMessage(consumed) || '确认重新分析结果失败');
497
+ }, []);
358
498
  const reanalyzeCustomerContract = React.useCallback(async (input) => {
359
499
  setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析中' });
360
500
  try {
361
- const response = asActionResult(unwrapResponse(await bridge.executeAction('reanalyze_customer_contract', { reviewSource: input.reviewSource, ...(input.customerContractId ? { id: input.customerContractId } : {}), ...(input.importTaskId ? { importTaskId: input.importTaskId } : {}) })));
501
+ const raw = await submitAndRefreshTaskState(() => bridge.executeAction('reanalyze_customer_contract', { reviewSource: input.reviewSource, ...(input.customerContractId ? { id: input.customerContractId } : {}), ...(input.importTaskId ? { importTaskId: input.importTaskId } : {}) }), loadTaskState);
502
+ const response = asActionResult(unwrapResponse(raw));
362
503
  if (response.success === false || !response.data)
363
- throw new Error(resolveActionMessage(response) || 'AI 重新分析启动失败');
364
- const result = await runCustomerContractReanalysis({
504
+ throw actionResponseError(response, 'AI 重新分析启动失败');
505
+ setToast({ message: resolveActionMessage(response) || 'AI 重新分析任务已提交', tone: 'info' });
506
+ const running = reanalysisTaskTrackerRef.current.run(response.data.run.id, signal => runCustomerContractReanalysis({
365
507
  start: response.data,
366
- invokeClientCommand: (commandKey, payload) => bridge.invokeClientCommand(commandKey, payload),
367
- markCommandDispatched: async (runId) => {
368
- const marked = asActionResult(unwrapResponse(await bridge.executeAction('mark_customer_contract_reanalysis_dispatched', { runId })));
369
- if (marked.success === false)
370
- throw new Error(resolveActionMessage(marked) || '记录重新分析命令状态失败');
371
- },
372
- markFailed: async (runId, failure) => {
373
- const marked = asActionResult(unwrapResponse(await bridge.executeAction('mark_customer_contract_reanalysis_failed', { runId, ...failure })));
374
- if (marked.success === false)
375
- throw new Error(resolveActionMessage(marked) || '记录重新分析失败状态失败');
376
- },
377
- poll: async (runId) => asCustomerContractReanalysisResult(unwrapResponse(await bridge.requestData({ parameters: { collection: 'customerContractAnalysis', runId } })))
378
- });
379
- let review;
380
- if (result.review) {
381
- review = adaptCustomerContractReview(result.review, { reviewSource: 'EXISTING_CONTRACT' });
382
- }
383
- else if (result.task) {
384
- review = adaptCustomerContractReview(result.task.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: result.task.id, sourceFileId: result.task.sourceFileId ?? '', sourceFileName: result.task.sourceFileName ?? '' });
385
- }
386
- else {
387
- throw new Error('重新分析结果缺少合同审核数据');
388
- }
508
+ signal,
509
+ poll: async (runId) => asCustomerContractReanalysisResult(unwrapResponse(await bridge.requestData({ parameters: { collection: 'customerContractAnalysis', runId } }))),
510
+ onPollError: error => setToast({ message: `任务状态查询失败:${error.message}`, tone: 'error' })
511
+ }));
512
+ if (!running)
513
+ throw new Error('该 AI 重新分析任务正在跟踪中');
514
+ const result = await running;
515
+ const review = await applyCustomerContractReanalysisResult(result);
516
+ await consumeCustomerContractReanalysisResult(result.run.id);
517
+ handledReanalysisResultsRef.current.add(result.run.id);
389
518
  setCustomerReanalysisState({ status: 'IDLE' });
519
+ setToast({ message: 'AI 重新分析完成', tone: 'success' });
520
+ await loadTaskState();
390
521
  return review;
391
522
  }
392
523
  catch (error) {
524
+ if (isAbortError(error))
525
+ throw error;
526
+ if (error instanceof ActionResponseError && error.code === 'IMPORT_QUEUE_UNAVAILABLE') {
527
+ setCustomerReanalysisState({ status: 'RUNNING', message: '任务已保存,等待队列服务恢复' });
528
+ setToast({ message: error.message, tone: 'info' });
529
+ scheduleTaskStateRecovery();
530
+ throw error;
531
+ }
532
+ if (isPollingObservationTimeout(error)) {
533
+ setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析仍在后台处理中' });
534
+ setToast({ message: '任务仍在后台处理中,将继续跟踪', tone: 'info' });
535
+ scheduleTaskStateRecovery();
536
+ throw error;
537
+ }
393
538
  setCustomerReanalysisState({ status: 'FAILED', message: error instanceof Error ? error.message : 'AI 重新分析失败' });
539
+ setToast({ message: error instanceof Error ? error.message : 'AI 重新分析失败', tone: 'error' });
394
540
  throw error;
395
541
  }
396
- }, []);
542
+ }, [applyCustomerContractReanalysisResult, consumeCustomerContractReanalysisResult, loadTaskState, scheduleTaskStateRecovery]);
543
+ React.useEffect(() => {
544
+ for (const recovery of data.customerContractAnalyses.items) {
545
+ const recoverable = recovery.recoveryType === 'ACTIVE_POLL' || recovery.recoveryType === 'SUCCEEDED_RESULT';
546
+ if (!recoverable)
547
+ continue;
548
+ if (handledReanalysisResultsRef.current.has(recovery.run.id))
549
+ continue;
550
+ const running = reanalysisTaskTrackerRef.current.run(recovery.run.id, signal => runCustomerContractReanalysis({
551
+ start: { run: recovery.run },
552
+ signal,
553
+ poll: async (runId) => asCustomerContractReanalysisResult(unwrapResponse(await bridge.requestData({ parameters: { collection: 'customerContractAnalysis', runId } }))),
554
+ onPollError: error => setToast({ message: `任务状态查询失败:${error.message}`, tone: 'error' })
555
+ }));
556
+ if (!running)
557
+ continue;
558
+ setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析中' });
559
+ setToast({ message: '已恢复 AI 重新分析任务', tone: 'info' });
560
+ void running.then(result => {
561
+ enqueuePendingReview({ kind: 'REANALYSIS', result });
562
+ }).catch(error => {
563
+ if (isAbortError(error))
564
+ return;
565
+ if (isPollingObservationTimeout(error)) {
566
+ setCustomerReanalysisState({ status: 'RUNNING', message: 'AI 重新分析仍在后台处理中' });
567
+ setToast({ message: '任务仍在后台处理中,将继续跟踪', tone: 'info' });
568
+ scheduleTaskStateRecovery();
569
+ return;
570
+ }
571
+ setCustomerReanalysisState({ status: 'FAILED', message: error instanceof Error ? error.message : 'AI 重新分析失败' });
572
+ setToast({ message: error instanceof Error ? error.message : 'AI 重新分析失败', tone: 'error' });
573
+ });
574
+ }
575
+ }, [data.customerContractAnalyses.items, enqueuePendingReview, scheduleTaskStateRecovery]);
576
+ const presentNextPendingReview = React.useCallback(async () => {
577
+ if (activeReviewResultRef.current)
578
+ return;
579
+ const pending = pendingReviewResultsRef.current.peek();
580
+ if (!pending)
581
+ return;
582
+ const resultId = pending.kind === 'CONTRACT_IMPORT' ? pending.completion.task.id : pending.result.run.id;
583
+ activeReviewResultRef.current = resultId;
584
+ try {
585
+ if (pending.kind === 'CONTRACT_IMPORT') {
586
+ const task = pending.completion.task;
587
+ if (task.documentType === 'PURCHASE_CONTRACT') {
588
+ blobUrlOwnerRef.current.clear('purchase');
589
+ const draft = adaptPurchaseReview(task);
590
+ setPurchaseImportTask(task);
591
+ setPurchaseReviewDraft(await attachPurchaseSource(draft, task, blobUrlOwnerRef.current));
592
+ changeRouteRef.current({ type: 'procurement', level: 'suppliers' });
593
+ }
594
+ else {
595
+ blobUrlOwnerRef.current.clear('customer');
596
+ const review = adaptCustomerContractReview(task.resultPayload, { reviewSource: 'IMPORT_DRAFT', importTaskId: task.id, sourceFileId: task.sourceFileId ?? '', sourceFileName: task.sourceFileName ?? '' });
597
+ setCustomerImportState({ status: 'AWAITING_REVIEW', importTaskId: task.id, review: await attachCustomerSource(review, blobUrlOwnerRef.current) });
598
+ changeRouteRef.current({ type: 'page', page: 'customer-contract-review' });
599
+ }
600
+ pendingReviewResultsRef.current.ack(resultId);
601
+ handledContractTasksRef.current.add(resultId);
602
+ return;
603
+ }
604
+ await applyCustomerContractReanalysisResult(pending.result);
605
+ await consumeCustomerContractReanalysisResult(pending.result.run.id);
606
+ pendingReviewResultsRef.current.ack(resultId);
607
+ handledReanalysisResultsRef.current.add(pending.result.run.id);
608
+ setCustomerReanalysisState({ status: 'IDLE' });
609
+ setToast({ message: 'AI 重新分析完成', tone: 'success' });
610
+ await loadTaskState();
611
+ }
612
+ catch (error) {
613
+ activeReviewResultRef.current = null;
614
+ setToast({ message: error instanceof Error ? error.message : '审核结果恢复失败', tone: 'error' });
615
+ scheduleTaskStateRecovery();
616
+ }
617
+ }, [applyCustomerContractReanalysisResult, consumeCustomerContractReanalysisResult, loadTaskState, scheduleTaskStateRecovery]);
618
+ presentNextReviewRef.current = () => { void presentNextPendingReview(); };
397
619
  const loadCustomerContractReview = React.useCallback(async (customerContractId) => {
398
620
  const result = asActionResult(unwrapResponse(await bridge.executeAction('get_customer_contract_review', { id: customerContractId })));
399
621
  if (result.success === false || !result.data)
@@ -405,21 +627,27 @@ function App() {
405
627
  const closePurchaseReview = React.useCallback(() => {
406
628
  blobUrlOwnerRef.current.clear('purchase');
407
629
  setPurchaseReviewDraft(null);
408
- }, []);
630
+ activeReviewResultRef.current = null;
631
+ schedulePendingReviewPresentation();
632
+ }, [schedulePendingReviewPresentation]);
409
633
  const closeCustomerReview = React.useCallback(() => {
410
634
  blobUrlOwnerRef.current.clear('customer');
411
635
  setCustomerImportState({ status: 'IDLE' });
412
- }, []);
636
+ activeReviewResultRef.current = null;
637
+ schedulePendingReviewPresentation();
638
+ }, [schedulePendingReviewPresentation]);
413
639
  const queryImportIssues = React.useCallback(async (query) => {
414
- const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: query.pageSize, parameters: { collection: 'importIssues', documentType: query.documentType, batchId: query.batchId, issueType: query.issueType } })));
415
- return { items: Array.isArray(payload.items) ? payload.items : [], total: numberValue(payload.total, 0), page: numberValue(payload.page, query.page), pageSize: numberValue(payload.pageSize, query.pageSize) };
640
+ const requestedPageSize = resolvePaginationPageSize(query.pageSize);
641
+ const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection: 'importIssues', documentType: query.documentType, batchId: query.batchId, issueType: query.issueType } })));
642
+ return { items: Array.isArray(payload.items) ? payload.items : [], total: numberValue(payload.total, 0), page: numberValue(payload.page, query.page), pageSize: requestedPageSize };
416
643
  }, []);
417
644
  const queryCatalog = React.useCallback(async (collection, query) => {
645
+ const requestedPageSize = resolvePaginationPageSize(query.pageSize);
418
646
  const gate = requestGateRef.current;
419
647
  const requestId = gate.begin();
420
648
  setBusy(true);
421
649
  try {
422
- const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: query.pageSize, parameters: { collection, filters: encodeViewFilters(query.filters) } })));
650
+ const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection, filters: encodeViewFilters(query.filters) } })));
423
651
  if (!gate.isLatest(requestId))
424
652
  return;
425
653
  setData((current) => ({
@@ -429,7 +657,7 @@ function App() {
429
657
  items: Array.isArray(payload.items) ? payload.items : [],
430
658
  total: numberValue(payload.total, 0),
431
659
  page: numberValue(payload.page, query.page),
432
- pageSize: numberValue(payload.pageSize, query.pageSize)
660
+ pageSize: requestedPageSize
433
661
  }
434
662
  }));
435
663
  }
@@ -442,6 +670,27 @@ function App() {
442
670
  setBusy(gate.activeCount() > 0);
443
671
  }
444
672
  }, []);
673
+ const queryMonthlyRates = React.useCallback(async (query) => {
674
+ const requestedPageSize = resolvePaginationPageSize(query.pageSize);
675
+ const gate = settingsRatesRequestGateRef.current;
676
+ const requestId = gate.begin();
677
+ setBusy(true);
678
+ try {
679
+ const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection: 'monthlyRates' } })));
680
+ if (!gate.commit(requestId))
681
+ return;
682
+ settingsRatesQueryRef.current = { page: numberValue(payload.page, query.page), pageSize: requestedPageSize };
683
+ setData(current => ({ ...current, settings: normalizeWorkbenchData({ summary: { settings: { ...current.settings, rates: { ...payload, pageSize: requestedPageSize } } } }).settings }));
684
+ }
685
+ catch (error) {
686
+ if (gate.isLatest(requestId))
687
+ setToast({ message: error instanceof Error ? `月度汇率加载失败:${error.message}` : '月度汇率加载失败', tone: 'error' });
688
+ }
689
+ finally {
690
+ gate.finish(requestId);
691
+ setBusy(gate.activeCount() + requestGateRef.current.activeCount() + routeRequestGateRef.current.activeCount() > 0);
692
+ }
693
+ }, []);
445
694
  const queryProcurement = React.useCallback(async (_collection, query) => {
446
695
  await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
447
696
  }, [loadRoute]);
@@ -452,20 +701,36 @@ function App() {
452
701
  await loadRoute(routeRef.current, query.page, query.pageSize, query.filters);
453
702
  }, [loadRoute]);
454
703
  const querySalesFiles = React.useCallback(async (collection, query) => {
704
+ const requestedPageSize = resolvePaginationPageSize(query.pageSize);
455
705
  if (collection === 'salesFileOrders') {
456
- await loadRoute({ type: 'page', page: 'sales-files' }, query.page, query.pageSize, query.filters);
706
+ await loadRoute({ type: 'page', page: 'sales-files' }, query.page, requestedPageSize, query.filters);
457
707
  return;
458
708
  }
459
- const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: query.pageSize, parameters: { collection, ...query.parameters, filters: encodeViewFilters(query.filters) } })));
460
- setData(current => ({ ...current, generatedFiles: { ...payload, items: Array.isArray(payload.items) ? payload.items : [], total: numberValue(payload.total, 0), page: numberValue(payload.page, query.page), pageSize: numberValue(payload.pageSize, query.pageSize) } }));
709
+ const key = generatedFileHistoryKey(query.parameters);
710
+ if (!key) {
711
+ setToast({ message: '历史生成文件查询缺少合同或采购订单标识', tone: 'error' });
712
+ return;
713
+ }
714
+ activeGeneratedFilesKeyRef.current = key;
715
+ await runKeyedLatestRequest({
716
+ gate: generatedFilesRequestGateRef.current,
717
+ key,
718
+ request: async () => asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: requestedPageSize, parameters: { collection, ...query.parameters, filters: encodeViewFilters(query.filters) } }))),
719
+ isCurrent: () => activeGeneratedFilesKeyRef.current === key,
720
+ commit: payload => setData(current => ({ ...current, generatedFiles: { ...payload, items: Array.isArray(payload.items) ? payload.items : [], total: numberValue(payload.total, 0), page: numberValue(payload.page, query.page), pageSize: requestedPageSize } })),
721
+ onError: error => setToast({ message: error instanceof Error ? `历史生成文件加载失败:${error.message}` : '历史生成文件加载失败', tone: 'error' }),
722
+ onStart: () => setBusy(true),
723
+ onFinish: () => setBusy(generatedFilesRequestGateRef.current.activeCount() + requestGateRef.current.activeCount() + routeRequestGateRef.current.activeCount() + analyticsRequestsRef.current.activeCount() > 0)
724
+ });
461
725
  }, [loadRoute]);
462
726
  const queryAnalytics = React.useCallback(async (query) => {
727
+ const requestedQuery = { ...query, pageSize: resolvePaginationPageSize(query.pageSize) };
463
728
  const requests = analyticsRequestsRef.current;
464
- const requestId = requests.begin(query);
729
+ const requestId = requests.begin(requestedQuery);
465
730
  setBusy(true);
466
731
  setAnalyticsError(null);
467
732
  try {
468
- const payload = asRecord(unwrapResponse(await bridge.requestData({ page: query.page, pageSize: query.pageSize, parameters: { collection: 'analytics', mode: query.mode, dimension: query.dimension, filters: encodeViewFilters(query.filters) } })));
733
+ const payload = asRecord(unwrapResponse(await bridge.requestData({ page: requestedQuery.page, pageSize: requestedQuery.pageSize, parameters: { collection: 'analytics', mode: requestedQuery.mode, dimension: requestedQuery.dimension, filters: encodeViewFilters(requestedQuery.filters) } })));
469
734
  if (!requests.commit(requestId))
470
735
  return;
471
736
  setAnalyticsCommittedQuery(requests.committed());
@@ -509,25 +774,26 @@ function App() {
509
774
  const result = asActionResult(unwrapResponse(await bridge.executeAction('save_settings', request)));
510
775
  if (result.success === false)
511
776
  throw new Error(resolveActionMessage(result) || '设置保存失败');
512
- await loadRoute({ type: 'page', page: 'settings' });
777
+ await loadRoute({ type: 'page', page: 'settings' }, settingsRatesQueryRef.current.page, settingsRatesQueryRef.current.pageSize);
513
778
  }, [loadRoute]);
514
779
  const restoreSettings = React.useCallback(async (request) => {
515
780
  const result = asActionResult(unwrapResponse(await bridge.executeAction('restore_settings_default', request)));
516
781
  if (result.success === false)
517
782
  throw new Error(resolveActionMessage(result) || '恢复系统默认失败');
518
- await loadRoute({ type: 'page', page: 'settings' });
783
+ await loadRoute({ type: 'page', page: 'settings' }, settingsRatesQueryRef.current.page, settingsRatesQueryRef.current.pageSize);
519
784
  }, [loadRoute]);
520
785
  const deleteSettingsRate = React.useCallback(async (request) => {
521
786
  const result = asActionResult(unwrapResponse(await bridge.executeAction('delete_settings_rate', request)));
522
787
  if (result.success === false)
523
788
  throw new Error(resolveActionMessage(result) || '月度汇率删除失败');
524
- await loadRoute({ type: 'page', page: 'settings' });
525
- }, [loadRoute]);
789
+ const { page, pageSize } = settingsRatesQueryRef.current;
790
+ const clampedPage = clampPageAfterSingleDeletion(page, pageSize, data.settings.rates.total);
791
+ await loadRoute({ type: 'page', page: 'settings' }, clampedPage, pageSize);
792
+ }, [data.settings.rates.total, loadRoute]);
526
793
  const uploadCompanyLogo = React.useCallback(async (file) => {
527
794
  const result = asActionResult(unwrapResponse(await bridge.executeFileAction('upload_company_logo', file, {})));
528
795
  if (result.success === false || !result.data?.logoFileId)
529
796
  throw new Error(resolveActionMessage(result) || '公司 Logo 上传失败');
530
- setToast({ message: '公司 Logo 上传成功', tone: 'success' });
531
797
  return result.data.logoFileId;
532
798
  }, []);
533
799
  const discardCompanyLogo = React.useCallback(async (logoFileId) => {
@@ -537,8 +803,22 @@ function App() {
537
803
  }, []);
538
804
  const openSalesFiles = React.useCallback((order) => {
539
805
  setActiveSalesFileOrder(order);
540
- if (order)
541
- changeRouteRef.current({ type: 'page', page: 'sales-files' });
806
+ if (!order)
807
+ return;
808
+ changeRouteRef.current({ type: 'page', page: 'sales-files' });
809
+ void querySalesFiles('generatedFiles', {
810
+ page: 1,
811
+ pageSize: 20,
812
+ filters: {},
813
+ parameters: { customerContractId: order.customerContractId, purchaseOrderId: order.purchaseOrderId }
814
+ });
815
+ }, [querySalesFiles]);
816
+ const selectSalesFileOrder = React.useCallback((order) => {
817
+ const previousKey = activeGeneratedFilesKeyRef.current;
818
+ if (previousKey)
819
+ generatedFilesRequestGateRef.current.invalidate(previousKey);
820
+ activeGeneratedFilesKeyRef.current = order ? generatedFileHistoryKey(order) : null;
821
+ setActiveSalesFileOrder(order);
542
822
  }, []);
543
823
  const changeRoute = React.useCallback((nextRoute) => {
544
824
  routeRef.current = nextRoute;
@@ -548,12 +828,16 @@ function App() {
548
828
  changeRouteRef.current = changeRoute;
549
829
  const page = routePage(route);
550
830
  const meta = pageMeta[page];
551
- const contractCommands = page === 'procurement'
552
- ? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u91C7\u8D2D\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_purchase_contract', file, { documentType: 'PURCHASE_CONTRACT' }) })
831
+ const contractCommands = page === 'procurement' && route.type === 'procurement'
832
+ ? route.level === 'orders'
833
+ ? _jsxs("button", { type: "button", className: "tcw-button tcw-button--primary", disabled: busy || actionBusy, onClick: () => changeRoute({ type: 'procurement', level: 'suppliers' }), children: [_jsx("i", { className: "ri-add-line", "aria-hidden": "true" }), "\u65B0\u5EFA\u91C7\u8D2D\u8BA2\u5355"] })
834
+ : _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u91C7\u8D2D\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_purchase_contract', file, { documentType: 'PURCHASE_CONTRACT' }) })
553
835
  : page === 'customer-contract-review'
554
836
  ? _jsx(ContractUploadCommand, { label: "\u4E0A\u4F20\u5BA2\u6237\u5408\u540C", busy: busy || actionBusy, onFile: file => executeFileAction('upload_customer_contract', file, { documentType: 'CUSTOMER_CONTRACT' }) })
555
837
  : undefined;
556
- return (_jsxs(_Fragment, { children: [_jsxs(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, subtitle: 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: executeFileAction, onRefresh: load, onQuery: (query) => queryCatalog('controlledGoods', query), onIssueQuery: queryImportIssues, importCompletion: importCompletion, importFailure: importFailures.CONTROLLED_CATALOG, onRetryImport: retryImport, command: catalogCommand, onCommandHandled: () => setCatalogCommand(null) }), page === 'sanctioned-companies' && _jsx(SanctionedCompaniesPage, { collection: data.sanctionedCompanies, batches: data.sanctionBatches.items, busy: busy || actionBusy, onAction: executeAction, onFileAction: executeFileAction, onRefresh: load, onQuery: (query) => queryCatalog('sanctionedCompanies', query), onIssueQuery: queryImportIssues, importCompletion: importCompletion, importFailure: importFailures.SANCTION_CATALOG, onRetryImport: retryImport, command: catalogCommand, onCommandHandled: () => setCatalogCommand(null) }), 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 }), 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 }), 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 }), page === 'sales-files' && _jsx(SalesFilesPage, { orders: data.salesFileOrders, history: data.generatedFiles, activeOrder: activeSalesFileOrder, banks: data.settings.banks, busy: busy || actionBusy, onQuery: querySalesFiles, onAction: executeSalesFileAction, onOpenSalesOrder: setActiveSalesFileOrder, onDownload: async (file) => { const result = await executeAction('download_sales_file', { id: file.id }); downloadActionFile(result.data); }, onEdit: async (file) => setEditingSalesFile(file) }), 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, onUploadLogo: uploadCompanyLogo, onDiscardLogo: discardCompanyLogo }), !['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 ? '操作处理中' : '数据加载中' }), toast && _jsx(Toast, { message: toast.message, tone: toast.tone, onClose: () => setToast(null) }), editingSalesFile && _jsx(SalesFileEditor, { file: editingSalesFile, onClose: () => setEditingSalesFile(null), onSaved: () => { setEditingSalesFile(null); if (activeSalesFileOrder)
838
+ if (analyticsPrototypeVisible)
839
+ return (_jsx(WorkbenchShell, { route: route, breadcrumbs: buildBreadcrumbs(route), title: meta.title, onRouteChange: changeRoute, onRefresh: () => void load(), children: _jsx(AnalyticsPrototypeBoard, { onClose: () => setAnalyticsPrototypeVisible(false) }) }));
840
+ 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, 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)
557
841
  void querySalesFiles('generatedFiles', { page: 1, pageSize: 20, filters: {}, parameters: { customerContractId: activeSalesFileOrder.customerContractId, purchaseOrderId: activeSalesFileOrder.purchaseOrderId } }); } })] }));
558
842
  }
559
843
  function routePage(route) {
@@ -564,6 +848,7 @@ function routePage(route) {
564
848
  return route.page;
565
849
  }
566
850
  function analyticsQueryFromRoute(page, pageSize, values) {
851
+ const requestedPageSize = resolvePaginationPageSize(pageSize);
567
852
  const dateValue = values.orderDate;
568
853
  const date = dateValue && typeof dateValue === 'object' && !Array.isArray(dateValue) ? dateValue : null;
569
854
  const orderDate = date ? { from: optionalText(date.from), to: optionalText(date.to) } : undefined;
@@ -574,11 +859,11 @@ function analyticsQueryFromRoute(page, pageSize, values) {
574
859
  };
575
860
  if (values.mode === 'SALES')
576
861
  return {
577
- mode: 'SALES', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'CUSTOMER', page, pageSize,
862
+ mode: 'SALES', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'CUSTOMER', page, pageSize: requestedPageSize,
578
863
  filters: { ...common, ...(optionalText(values.customer) ? { customer: optionalText(values.customer) } : {}), ...(enabled(values.unreceivedOnly) ? { unreceivedOnly: true } : {}) }
579
864
  };
580
865
  return {
581
- mode: 'PURCHASE', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'SUPPLIER', page, pageSize,
866
+ mode: 'PURCHASE', dimension: values.dimension === 'PRODUCT' ? 'PRODUCT' : 'SUPPLIER', page, pageSize: requestedPageSize,
582
867
  filters: { ...common, ...(optionalText(values.supplier) ? { supplier: optionalText(values.supplier) } : {}), ...(enabled(values.unpaidOnly) ? { unpaidOnly: true } : {}), ...(enabled(values.unrefundedOnly) ? { unrefundedOnly: true } : {}) }
583
868
  };
584
869
  }
@@ -596,6 +881,9 @@ function resolveActionMessage(result) {
596
881
  }
597
882
  return '';
598
883
  }
884
+ function actionResponseError(result, fallback) {
885
+ return new ActionResponseError(resolveActionMessage(result) || fallback, result.error?.code);
886
+ }
599
887
  function asRecord(value) {
600
888
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
601
889
  }
@@ -694,9 +982,6 @@ function asCustomerContractReanalysisResult(value) {
694
982
  throw new Error('轮询响应缺少重新分析任务');
695
983
  return payload;
696
984
  }
697
- function shouldRefreshAfterAction(actionKey) {
698
- return new Set(['save_controlled_goods', 'update_controlled_goods', 'save_sanctioned_company', 'update_sanctioned_company', 'save_purchase_review', 'save_customer_contract_review', 'update_supplier', 'update_purchase_order', 'add_purchase_line', 'update_purchase_line', 'update_customer_contract_review', 'delete_entity']).has(actionKey);
699
- }
700
985
  async function ensureExcelEditorRuntime() {
701
986
  const runtimeWindow = window;
702
987
  if (runtimeWindow.TradeComplianceExcelEditor?.open)
@@ -737,7 +1022,7 @@ async function ensureExcelEditorRuntime() {
737
1022
  });
738
1023
  return excelEditorRuntimePromise;
739
1024
  }
740
- function SalesFileEditor({ file, onClose, onSaved }) {
1025
+ function SalesFileEditor({ file, onClose, onSaved, onNotify }) {
741
1026
  const containerRef = React.useRef(null);
742
1027
  const editorRef = React.useRef(null);
743
1028
  const [error, setError] = React.useState('');
@@ -778,12 +1063,13 @@ function SalesFileEditor({ file, onClose, onSaved }) {
778
1063
  return () => { cancelled = true; editorRef.current?.dispose(); editorRef.current = null; };
779
1064
  }, [file.id]);
780
1065
  const save = async () => { if (!editorRef.current)
781
- return; setSaving(true); setError(''); try {
1066
+ return; setSaving(true); try {
782
1067
  await editorRef.current.save();
1068
+ onNotify({ message: '工作簿保存成功', tone: 'success' });
783
1069
  onSaved();
784
1070
  }
785
1071
  catch (cause) {
786
- setError(cause instanceof Error ? cause.message : '工作簿保存失败');
1072
+ onNotify({ message: cause instanceof Error ? cause.message : '工作簿保存失败', tone: 'error' });
787
1073
  }
788
1074
  finally {
789
1075
  setSaving(false);