@nocobase/flow-engine 2.1.0-alpha.1 → 2.1.0-alpha.10

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 (283) hide show
  1. package/LICENSE +201 -661
  2. package/README.md +79 -10
  3. package/lib/BlockScopedFlowEngine.js +0 -1
  4. package/lib/FlowDefinition.d.ts +2 -0
  5. package/lib/JSRunner.d.ts +15 -0
  6. package/lib/JSRunner.js +82 -7
  7. package/lib/ViewScopedFlowEngine.js +8 -1
  8. package/lib/acl/Acl.js +13 -3
  9. package/lib/components/FlowContextSelector.js +155 -10
  10. package/lib/components/MobilePopup.js +6 -5
  11. package/lib/components/dnd/gridDragPlanner.d.ts +1 -0
  12. package/lib/components/dnd/gridDragPlanner.js +59 -3
  13. package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
  14. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
  15. package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
  16. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +21 -3
  17. package/lib/components/subModel/AddSubModelButton.js +16 -1
  18. package/lib/components/subModel/utils.js +2 -2
  19. package/lib/components/variables/VariableInput.js +9 -4
  20. package/lib/components/variables/VariableTag.js +46 -39
  21. package/lib/components/variables/utils.d.ts +7 -0
  22. package/lib/components/variables/utils.js +42 -2
  23. package/lib/data-source/index.d.ts +7 -27
  24. package/lib/data-source/index.js +84 -51
  25. package/lib/executor/FlowExecutor.d.ts +2 -1
  26. package/lib/executor/FlowExecutor.js +190 -26
  27. package/lib/flowContext.d.ts +230 -7
  28. package/lib/flowContext.js +2270 -148
  29. package/lib/flowEngine.d.ts +160 -1
  30. package/lib/flowEngine.js +383 -26
  31. package/lib/flowI18n.js +6 -4
  32. package/lib/flowSettings.d.ts +14 -6
  33. package/lib/flowSettings.js +51 -17
  34. package/lib/index.d.ts +7 -1
  35. package/lib/index.js +21 -0
  36. package/lib/lazy-helper.d.ts +14 -0
  37. package/lib/lazy-helper.js +71 -0
  38. package/lib/locale/en-US.json +9 -2
  39. package/lib/locale/index.d.ts +14 -0
  40. package/lib/locale/zh-CN.json +8 -1
  41. package/lib/models/CollectionFieldModel.d.ts +1 -0
  42. package/lib/models/CollectionFieldModel.js +3 -2
  43. package/lib/models/flowModel.d.ts +7 -0
  44. package/lib/models/flowModel.js +83 -8
  45. package/lib/provider.js +7 -6
  46. package/lib/resources/baseRecordResource.d.ts +5 -0
  47. package/lib/resources/baseRecordResource.js +24 -0
  48. package/lib/resources/multiRecordResource.d.ts +1 -0
  49. package/lib/resources/multiRecordResource.js +11 -4
  50. package/lib/resources/singleRecordResource.js +2 -0
  51. package/lib/resources/sqlResource.d.ts +4 -3
  52. package/lib/resources/sqlResource.js +8 -3
  53. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
  54. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
  55. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
  56. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
  57. package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
  58. package/lib/runjs-context/contexts/base.js +706 -41
  59. package/lib/runjs-context/contributions.d.ts +33 -0
  60. package/lib/runjs-context/contributions.js +88 -0
  61. package/lib/runjs-context/helpers.js +12 -1
  62. package/lib/runjs-context/registry.d.ts +1 -1
  63. package/lib/runjs-context/setup.js +22 -9
  64. package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
  65. package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
  66. package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
  67. package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
  68. package/lib/runjs-context/snippets/index.d.ts +11 -1
  69. package/lib/runjs-context/snippets/index.js +61 -40
  70. package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
  71. package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
  72. package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
  73. package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
  74. package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
  75. package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
  76. package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
  77. package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
  78. package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
  79. package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
  80. package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
  81. package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
  82. package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
  83. package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
  84. package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
  85. package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
  86. package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
  87. package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
  88. package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
  89. package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
  90. package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
  91. package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
  92. package/lib/runjsLibs.d.ts +28 -0
  93. package/lib/runjsLibs.js +532 -0
  94. package/lib/scheduler/ModelOperationScheduler.d.ts +7 -1
  95. package/lib/scheduler/ModelOperationScheduler.js +28 -23
  96. package/lib/types.d.ts +63 -1
  97. package/lib/utils/associationObjectVariable.d.ts +2 -2
  98. package/lib/utils/createCollectionContextMeta.js +1 -0
  99. package/lib/utils/createEphemeralContext.js +2 -2
  100. package/lib/utils/dateVariable.d.ts +16 -0
  101. package/lib/utils/dateVariable.js +380 -0
  102. package/lib/utils/exceptions.d.ts +7 -0
  103. package/lib/utils/exceptions.js +10 -0
  104. package/lib/utils/index.d.ts +8 -3
  105. package/lib/utils/index.js +49 -0
  106. package/lib/utils/params-resolvers.js +16 -9
  107. package/lib/utils/parsePathnameToViewParams.js +1 -1
  108. package/lib/utils/resolveModuleUrl.d.ts +58 -0
  109. package/lib/utils/resolveModuleUrl.js +65 -0
  110. package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
  111. package/lib/utils/resolveRunJSObjectValues.js +61 -0
  112. package/lib/utils/runjsModuleLoader.d.ts +58 -0
  113. package/lib/utils/runjsModuleLoader.js +422 -0
  114. package/lib/utils/runjsTemplateCompat.d.ts +35 -0
  115. package/lib/utils/runjsTemplateCompat.js +743 -0
  116. package/lib/utils/runjsValue.d.ts +29 -0
  117. package/lib/utils/runjsValue.js +275 -0
  118. package/lib/utils/safeGlobals.d.ts +18 -8
  119. package/lib/utils/safeGlobals.js +164 -17
  120. package/lib/utils/schema-utils.d.ts +17 -1
  121. package/lib/utils/schema-utils.js +80 -0
  122. package/lib/views/FlowView.d.ts +7 -1
  123. package/lib/views/createViewMeta.d.ts +0 -7
  124. package/lib/views/createViewMeta.js +19 -70
  125. package/lib/views/index.d.ts +1 -2
  126. package/lib/views/index.js +4 -3
  127. package/lib/views/runViewBeforeClose.d.ts +10 -0
  128. package/lib/views/runViewBeforeClose.js +45 -0
  129. package/lib/views/useDialog.d.ts +2 -1
  130. package/lib/views/useDialog.js +28 -6
  131. package/lib/views/useDrawer.d.ts +2 -1
  132. package/lib/views/useDrawer.js +27 -5
  133. package/lib/views/usePage.d.ts +6 -1
  134. package/lib/views/usePage.js +53 -9
  135. package/lib/views/usePopover.js +4 -1
  136. package/lib/views/viewEvents.d.ts +17 -0
  137. package/lib/views/viewEvents.js +90 -0
  138. package/package.json +5 -5
  139. package/src/BlockScopedFlowEngine.ts +2 -5
  140. package/src/JSRunner.ts +111 -5
  141. package/src/ViewScopedFlowEngine.ts +8 -0
  142. package/src/__tests__/JSRunner.test.ts +91 -1
  143. package/src/__tests__/createViewMeta.popup.test.ts +62 -1
  144. package/src/__tests__/flowContext.test.ts +693 -1
  145. package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
  146. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  147. package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
  148. package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
  149. package/src/__tests__/flowRuntimeContext.test.ts +2 -1
  150. package/src/__tests__/flowSettings.open.test.tsx +123 -19
  151. package/src/__tests__/flowSettings.test.ts +94 -15
  152. package/src/__tests__/provider.test.tsx +0 -5
  153. package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
  154. package/src/__tests__/runjsContext.test.ts +23 -7
  155. package/src/__tests__/runjsContextImplementations.test.ts +34 -3
  156. package/src/__tests__/runjsContextRuntime.test.ts +3 -3
  157. package/src/__tests__/runjsContributions.test.ts +89 -0
  158. package/src/__tests__/runjsExternalLibs.test.ts +242 -0
  159. package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
  160. package/src/__tests__/runjsLocales.test.ts +4 -1
  161. package/src/__tests__/runjsPreprocessDefault.test.ts +72 -0
  162. package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
  163. package/src/__tests__/runjsSnippets.test.ts +40 -3
  164. package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
  165. package/src/acl/Acl.tsx +3 -3
  166. package/src/components/FlowContextSelector.tsx +208 -12
  167. package/src/components/MobilePopup.tsx +4 -2
  168. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +3 -3
  169. package/src/components/__tests__/gridDragPlanner.test.ts +229 -1
  170. package/src/components/dnd/gridDragPlanner.ts +68 -2
  171. package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
  172. package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
  173. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
  174. package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
  175. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +31 -4
  176. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
  177. package/src/components/subModel/AddSubModelButton.tsx +17 -1
  178. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +142 -32
  179. package/src/components/subModel/utils.ts +1 -1
  180. package/src/components/variables/VariableInput.tsx +12 -4
  181. package/src/components/variables/VariableTag.tsx +54 -45
  182. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
  183. package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
  184. package/src/components/variables/__tests__/utils.test.ts +81 -3
  185. package/src/components/variables/utils.ts +67 -6
  186. package/src/data-source/index.ts +88 -110
  187. package/src/executor/FlowExecutor.ts +230 -28
  188. package/src/executor/__tests__/flowExecutor.test.ts +123 -0
  189. package/src/flowContext.ts +2989 -212
  190. package/src/flowEngine.ts +427 -22
  191. package/src/flowI18n.ts +7 -5
  192. package/src/flowSettings.ts +58 -18
  193. package/src/index.ts +14 -1
  194. package/src/lazy-helper.tsx +57 -0
  195. package/src/locale/en-US.json +9 -2
  196. package/src/locale/zh-CN.json +8 -1
  197. package/src/models/CollectionFieldModel.tsx +3 -1
  198. package/src/models/__tests__/dispatchEvent.when.test.ts +768 -0
  199. package/src/models/__tests__/flowModel.clone.test.ts +416 -0
  200. package/src/models/__tests__/flowModel.test.ts +20 -4
  201. package/src/models/flowModel.tsx +112 -7
  202. package/src/provider.tsx +9 -7
  203. package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
  204. package/src/resources/__tests__/sqlResource.test.ts +60 -0
  205. package/src/resources/baseRecordResource.ts +31 -0
  206. package/src/resources/multiRecordResource.ts +11 -4
  207. package/src/resources/singleRecordResource.ts +3 -0
  208. package/src/resources/sqlResource.ts +11 -6
  209. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
  210. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
  211. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
  212. package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
  213. package/src/runjs-context/contexts/base.ts +715 -44
  214. package/src/runjs-context/contributions.ts +88 -0
  215. package/src/runjs-context/helpers.ts +11 -1
  216. package/src/runjs-context/registry.ts +1 -1
  217. package/src/runjs-context/setup.ts +24 -9
  218. package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
  219. package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
  220. package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
  221. package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
  222. package/src/runjs-context/snippets/index.ts +75 -41
  223. package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
  224. package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
  225. package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
  226. package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
  227. package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
  228. package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
  229. package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
  230. package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
  231. package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
  232. package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
  233. package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
  234. package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
  235. package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
  236. package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
  237. package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
  238. package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
  239. package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
  240. package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
  241. package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
  242. package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
  243. package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
  244. package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
  245. package/src/runjsLibs.ts +622 -0
  246. package/src/scheduler/ModelOperationScheduler.ts +41 -24
  247. package/src/types.ts +86 -1
  248. package/src/utils/__tests__/dateVariable.test.ts +101 -0
  249. package/src/utils/__tests__/params-resolvers.test.ts +40 -0
  250. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
  251. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
  252. package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
  253. package/src/utils/__tests__/runjsValue.test.ts +44 -0
  254. package/src/utils/__tests__/safeGlobals.test.ts +57 -2
  255. package/src/utils/__tests__/utils.test.ts +157 -0
  256. package/src/utils/associationObjectVariable.ts +2 -2
  257. package/src/utils/createCollectionContextMeta.ts +1 -0
  258. package/src/utils/createEphemeralContext.ts +5 -4
  259. package/src/utils/dateVariable.ts +397 -0
  260. package/src/utils/exceptions.ts +11 -0
  261. package/src/utils/index.ts +38 -3
  262. package/src/utils/params-resolvers.ts +23 -9
  263. package/src/utils/parsePathnameToViewParams.ts +2 -2
  264. package/src/utils/resolveModuleUrl.ts +91 -0
  265. package/src/utils/resolveRunJSObjectValues.ts +46 -0
  266. package/src/utils/runjsModuleLoader.ts +553 -0
  267. package/src/utils/runjsTemplateCompat.ts +828 -0
  268. package/src/utils/runjsValue.ts +287 -0
  269. package/src/utils/safeGlobals.ts +188 -17
  270. package/src/utils/schema-utils.ts +109 -1
  271. package/src/views/FlowView.tsx +11 -1
  272. package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
  273. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  274. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +44 -16
  275. package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
  276. package/src/views/createViewMeta.ts +22 -75
  277. package/src/views/index.tsx +1 -2
  278. package/src/views/runViewBeforeClose.ts +19 -0
  279. package/src/views/useDialog.tsx +34 -5
  280. package/src/views/useDrawer.tsx +33 -4
  281. package/src/views/usePage.tsx +63 -8
  282. package/src/views/usePopover.tsx +4 -1
  283. package/src/views/viewEvents.ts +55 -0
@@ -9,11 +9,10 @@
9
9
 
10
10
  import { ISchema } from '@formily/json-schema';
11
11
  import { observable } from '@formily/reactive';
12
- import { APIClient } from '@nocobase/sdk';
12
+ import { APIClient, RequestOptions } from '@nocobase/sdk';
13
13
  import type { Router } from '@remix-run/router';
14
14
  import { MessageInstance } from 'antd/es/message/interface';
15
15
  import * as antd from 'antd';
16
- import * as antdIcons from '@ant-design/icons';
17
16
  import type { HookAPI } from 'antd/es/modal/useModal';
18
17
  import { NotificationInstance } from 'antd/es/notification/interface';
19
18
  import _ from 'lodash';
@@ -28,7 +27,7 @@ import { ContextPathProxy } from './ContextPathProxy';
28
27
  import { DataSource, DataSourceManager } from './data-source';
29
28
  import { FlowEngine } from './flowEngine';
30
29
  import { FlowI18n } from './flowI18n';
31
- import { JSRunner, JSRunnerOptions } from './JSRunner';
30
+ import { JSRunner, JSRunnerOptions, shouldPreprocessRunJSTemplates } from './JSRunner';
32
31
  import type { FlowModel } from './models/flowModel';
33
32
  import type { ForkFlowModel } from './models/forkFlowModel';
34
33
  import { FlowResource, FlowSQLRepository } from './resources';
@@ -38,17 +37,26 @@ import {
38
37
  extractPropertyPath,
39
38
  extractUsedVariablePaths,
40
39
  FlowExitException,
40
+ FLOW_ENGINE_NAMESPACE,
41
+ isCtxDatePathPrefix,
42
+ isCssFile,
43
+ prepareRunJsCode,
44
+ resolveCtxDatePath,
41
45
  resolveDefaultParams,
42
46
  resolveExpressions,
47
+ resolveModuleUrl,
43
48
  } from './utils';
44
49
  import { FlowExitAllException } from './utils/exceptions';
45
50
  import { enqueueVariablesResolve, JSONValue } from './utils/params-resolvers';
46
51
  import type { RecordRef } from './utils/serverContextParams';
47
52
  import { buildServerContextParams as _buildServerContextParams } from './utils/serverContextParams';
53
+ import { inferRecordRef } from './utils/variablesParams';
48
54
  import { FlowView, FlowViewer } from './views/FlowView';
49
- import { RunJSContextRegistry, getModelClassName } from './runjs-context/registry';
55
+ import { RunJSContextRegistry, getModelClassName, type RunJSVersion } from './runjs-context/registry';
50
56
  import { createEphemeralContext } from './utils/createEphemeralContext';
51
57
  import dayjs from 'dayjs';
58
+ import { externalReactRender, setupRunJSLibs } from './runjsLibs';
59
+ import { runjsImportAsync, runjsImportModule, runjsRequireAsync } from './utils/runjsModuleLoader';
52
60
 
53
61
  // Helper: detect a RecordRef-like object
54
62
  function isRecordRefLike(val: any): boolean {
@@ -71,13 +79,109 @@ function filterBuilderOutputByPaths(built: any, neededPaths: string[]): any {
71
79
  return undefined;
72
80
  }
73
81
 
82
+ // Helper: extract top-level segment of a subpath (e.g. 'a.b' -> 'a', 'tags[0].name' -> 'tags')
83
+ function topLevelOf(subPath: string): string | undefined {
84
+ if (!subPath) return undefined;
85
+ const m = String(subPath).match(/^([^.[]+)/);
86
+ return m?.[1];
87
+ }
88
+
89
+ // Helper: infer selects (fields/appends) from usage paths (mirrors server-side inferSelectsFromUsage)
90
+ function inferSelectsFromUsage(paths: string[] = []): { generatedAppends?: string[]; generatedFields?: string[] } {
91
+ if (!Array.isArray(paths) || paths.length === 0) {
92
+ return { generatedAppends: undefined, generatedFields: undefined };
93
+ }
94
+
95
+ const appendSet = new Set<string>();
96
+ const fieldSet = new Set<string>();
97
+
98
+ const normalizePath = (raw: string): string => {
99
+ if (!raw) return '';
100
+ let s = String(raw);
101
+ // remove numeric indexes like [0]
102
+ s = s.replace(/\[(?:\d+)\]/g, '');
103
+ // normalize string indexes like ["name"] / ['name'] into .name
104
+ s = s.replace(/\[(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)')\]/g, (_m, g1, g2) => `.${(g1 || g2) as string}`);
105
+ s = s.replace(/\.\.+/g, '.');
106
+ s = s.replace(/^\./, '').replace(/\.$/, '');
107
+ return s;
108
+ };
109
+
110
+ for (let path of paths) {
111
+ if (!path) continue;
112
+ // drop leading numeric index like [0].name
113
+ while (/^\[(\d+)\](\.|$)/.test(path)) {
114
+ path = path.replace(/^\[(\d+)\]\.?/, '');
115
+ }
116
+ const norm = normalizePath(path);
117
+ if (!norm) continue;
118
+ const segments = norm.split('.').filter(Boolean);
119
+ if (segments.length === 0) continue;
120
+
121
+ if (segments.length === 1) {
122
+ fieldSet.add(segments[0]);
123
+ continue;
124
+ }
125
+
126
+ for (let i = 0; i < segments.length - 1; i++) {
127
+ appendSet.add(segments.slice(0, i + 1).join('.'));
128
+ }
129
+ fieldSet.add(segments.join('.'));
130
+ }
131
+
132
+ const generatedAppends = appendSet.size ? Array.from(appendSet) : undefined;
133
+ const generatedFields = fieldSet.size ? Array.from(fieldSet) : undefined;
134
+ return { generatedAppends, generatedFields };
135
+ }
136
+
74
137
  type Getter<T = any> = (ctx: FlowContext) => T | Promise<T>;
75
138
 
139
+ export type FlowContextDocRef = string | { url: string; title?: string };
140
+
141
+ export type FlowDeprecationDoc =
142
+ | boolean
143
+ | {
144
+ /**
145
+ * 废弃说明(面向人/大模型)。
146
+ */
147
+ message?: string;
148
+ /**
149
+ * 推荐替代 API(例如 'ctx.resolveJsonTemplate')。
150
+ */
151
+ replacedBy?: string | string[];
152
+ /**
153
+ * 开始废弃的版本号(可选)。
154
+ */
155
+ since?: string;
156
+ /**
157
+ * 预计移除的版本号(可选)。
158
+ */
159
+ removedIn?: string;
160
+ /**
161
+ * 参考链接(可选)。
162
+ */
163
+ ref?: FlowContextDocRef;
164
+ };
165
+
166
+ export type FlowContextDocParam = {
167
+ name: string;
168
+ description?: string;
169
+ type?: string;
170
+ optional?: boolean;
171
+ default?: JSONValue;
172
+ };
173
+
174
+ export type FlowContextDocReturn = {
175
+ description?: string;
176
+ type?: string;
177
+ };
178
+
76
179
  export interface MetaTreeNode {
77
180
  name: string;
78
181
  title: string;
79
182
  type: string;
80
183
  interface?: string;
184
+ options?: any;
81
185
  uiSchema?: ISchema;
82
186
  render?: (props: any) => JSX.Element;
83
187
  // display?: 'default' | 'flatten' | 'none'; // 显示模式:默认、平铺子菜单、完全隐藏, 用于简化meta树显示层级
@@ -95,6 +199,7 @@ export interface PropertyMeta {
95
199
  type: string;
96
200
  title: string;
97
201
  interface?: string;
202
+ options?: any;
98
203
  uiSchema?: ISchema; // TODO: 这个是不是压根没必要啊?
99
204
  render?: (props: any) => JSX.Element; // 自定义渲染函数
100
205
  // 用于 VariableInput 的排序:数值越大,显示越靠前;相同值保持稳定顺序
@@ -102,11 +207,11 @@ export interface PropertyMeta {
102
207
  // display?: 'default' | 'flatten' | 'none'; // 显示模式:默认、平铺子菜单、完全隐藏, 用于简化meta树显示层级
103
208
  properties?: Record<string, PropertyMeta> | (() => Promise<Record<string, PropertyMeta>>);
104
209
  // 变量禁用控制:若 disabled 为真(或函数返回真)则禁用
105
- disabled?: boolean | (() => boolean);
210
+ disabled?: boolean | (() => boolean | Promise<boolean>);
106
211
  // 禁用原因(用于 UI 小问号提示),可为函数
107
- disabledReason?: string | (() => string | undefined);
212
+ disabledReason?: string | (() => string | undefined | Promise<string | undefined>);
108
213
  // 显示控制:当 hidden 为 true(或函数返回 true)时,不在变量选择器中展示该节点
109
- hidden?: boolean | (() => boolean);
214
+ hidden?: boolean | (() => boolean | Promise<boolean>);
110
215
  // 变量解析参数构造器(用于 variables:resolve 的 contextParams,按属性名归位)。
111
216
  // 支持返回 RecordRef 或任意嵌套对象(将被 buildServerContextParams 扁平化,例如 { record: RecordRef } -> 'view.record')。
112
217
  buildVariablesParams?: (
@@ -138,16 +243,154 @@ export interface PropertyOptions {
138
243
  cache?: boolean;
139
244
  observable?: boolean; // 是否为 observable 属性
140
245
  meta?: PropertyMetaOrFactory; // 支持静态、函数和异步函数(工厂函数可带 title/sort)
246
+ /**
247
+ * 面向工具/大模型的静态文档信息(不影响变量选择器 UI)。
248
+ * - `getApiInfos()` 仅使用 RunJS doc + 这里的 `info`(不会读取/展开 `meta`)
249
+ * - 变量结构信息请使用 `getVarInfos()`(来源于 `meta`)
250
+ */
251
+ info?: FlowContextPropertyInfoOrFactory;
141
252
  // 标记该属性是否在服务端解析:
142
253
  // - boolean: true 表示整个顶层变量交给服务端;false 表示仅前端解析
143
254
  // - function: 根据子路径决定是否交给服务端(子路径示例:'record.roles[0].name'、'id'、'')
144
255
  resolveOnServer?: boolean | ((subPath: string) => boolean);
145
256
  // 优化:当需要服务端解析但本属性在 buildVariablesParams 返回空时,是否跳过调用服务端。
146
- // - 典型场景:formValues / currentObject 仅在“已选关联值”存在时才需要服务端;否则没有必要请求。
257
+ // - 典型场景:formValues / item 仅在“已选关联值”存在时才需要服务端;否则没有必要请求。
147
258
  // - 默认 false:保持兼容,其他变量即使没有 contextParams 也可选择调用服务端。
148
259
  serverOnlyWhenContextParams?: boolean;
149
260
  }
150
261
 
262
+ export type FlowContextMethodInfoInput = {
263
+ description?: string;
264
+ detail?: string;
265
+ examples?: string[];
266
+ completion?: RunJSDocCompletionDoc;
267
+ ref?: FlowContextDocRef;
268
+ deprecated?: FlowDeprecationDoc;
269
+ params?: FlowContextDocParam[];
270
+ returns?: FlowContextDocReturn;
271
+ hidden?: boolean | ((ctx: any) => boolean | Promise<boolean>);
272
+ disabled?: boolean | ((ctx: any) => boolean | Promise<boolean>);
273
+ disabledReason?: string | ((ctx: any) => string | undefined | Promise<string | undefined>);
274
+ };
275
+
276
+ export type FlowContextMethodInfo = {
277
+ description?: string;
278
+ detail?: string;
279
+ examples?: string[];
280
+ completion?: RunJSDocCompletionDoc;
281
+ ref?: FlowContextDocRef;
282
+ deprecated?: FlowDeprecationDoc;
283
+ params?: FlowContextDocParam[];
284
+ returns?: FlowContextDocReturn;
285
+ disabled?: boolean;
286
+ disabledReason?: string;
287
+ };
288
+
289
+ export type FlowContextPropertyInfoObjectInput = Omit<
290
+ FlowContextPropertyInfo,
291
+ 'disabled' | 'disabledReason' | 'properties'
292
+ > & {
293
+ properties?:
294
+ | Record<string, FlowContextPropertyInfoInput>
295
+ | (() => Promise<Record<string, FlowContextPropertyInfoInput>>);
296
+ hidden?: RunJSDocHiddenOrPathsDoc;
297
+ disabled?: boolean | ((ctx: any) => boolean | Promise<boolean>);
298
+ disabledReason?: string | ((ctx: any) => string | undefined | Promise<string | undefined>);
299
+ };
300
+
301
+ export type FlowContextPropertyInfoInput = string | FlowContextPropertyInfoObjectInput;
302
+
303
+ export type FlowContextPropertyInfoFactory = {
304
+ (): FlowContextPropertyInfoInput | Promise<FlowContextPropertyInfoInput | null> | null;
305
+ };
306
+
307
+ export type FlowContextPropertyInfoOrFactory = FlowContextPropertyInfoInput | FlowContextPropertyInfoFactory;
308
+
309
+ export type FlowContextPropertyInfo = {
310
+ title?: string;
311
+ type?: string;
312
+ interface?: string;
313
+ description?: string;
314
+ detail?: string;
315
+ examples?: string[];
316
+ completion?: RunJSDocCompletionDoc;
317
+ ref?: FlowContextDocRef;
318
+ deprecated?: FlowDeprecationDoc;
319
+ params?: FlowContextDocParam[];
320
+ returns?: FlowContextDocReturn;
321
+ disabled?: boolean;
322
+ disabledReason?: string;
323
+ properties?: Record<string, FlowContextPropertyInfo>;
324
+ };
325
+
326
+ export type FlowContextApiInfo = {
327
+ title?: string;
328
+ type?: string;
329
+ interface?: string;
330
+ description?: string;
331
+ examples?: string[];
332
+ completion?: RunJSDocCompletionDoc;
333
+ ref?: FlowContextDocRef;
334
+ deprecated?: FlowDeprecationDoc;
335
+ params?: FlowContextDocParam[];
336
+ returns?: FlowContextDocReturn;
337
+ disabled?: boolean;
338
+ disabledReason?: string;
339
+ properties?: Record<string, FlowContextApiInfo>;
340
+ };
341
+
342
+ export type FlowContextInfosEnvNode = {
343
+ /**
344
+ * 说明(面向人/大模型)。建议为一句话。
345
+ */
346
+ description?: string;
347
+ /**
348
+ * 可用于 `await ctx.getVar(getVar)` 的表达式字符串,推荐以 `ctx.` 开头。
349
+ * 例如:'ctx.popup'、'ctx.resource.collectionName'
350
+ */
351
+ getVar?: string;
352
+ /**
353
+ * 已解析/可序列化的静态值(用于 prompt 直接使用)。
354
+ * 注意:应保持小体积,避免放入 record 等大对象。
355
+ */
356
+ value?: JSONValue;
357
+ /**
358
+ * 子节点(用于表达 popup.resource.xxx 等层级结构)。
359
+ */
360
+ properties?: Record<string, FlowContextInfosEnvNode>;
361
+ };
362
+
363
+ export type FlowContextInfosEnvs = {
364
+ popup?: FlowContextInfosEnvNode;
365
+ block?: FlowContextInfosEnvNode;
366
+ currentViewBlocks?: FlowContextInfosEnvNode;
367
+ flowModel?: FlowContextInfosEnvNode;
368
+ resource?: FlowContextInfosEnvNode;
369
+ record?: FlowContextInfosEnvNode;
370
+ };
371
+
372
+ export type FlowContextGetApiInfosOptions = {
373
+ /**
374
+ * RunJS 文档版本(默认 v1)。
375
+ */
376
+ version?: RunJSVersion;
377
+ };
378
+
379
+ export type FlowContextGetVarInfosOptions = {
380
+ /**
381
+ * 最大展开层级(默认 3)。
382
+ * - 当不传 path 时,top-level property depth=1。
383
+ * - 当传 path 时,path 对应节点 depth=1。
384
+ */
385
+ maxDepth?: number;
386
+ /**
387
+ * 剪裁:仅收集指定 path 下的变量结构信息。
388
+ * - string 形式支持:'record'、'record.id'、'ctx.record'、'{{ ctx.record }}'
389
+ * - string[] 表示多个剪裁路径合并
390
+ */
391
+ path?: string | string[];
392
+ };
393
+
151
394
  type RouteOptions = {
152
395
  name?: string; // 路由唯一标识
153
396
  path?: string; // 路由模板
@@ -158,6 +401,7 @@ type RouteOptions = {
158
401
  export class FlowContext {
159
402
  _props: Record<string, PropertyOptions> = {};
160
403
  _methods: Record<string, (...args: any[]) => any> = {};
404
+ _methodInfos: Record<string, FlowContextMethodInfoInput> = {};
161
405
  protected _cache: Record<string, any> = {};
162
406
  protected _observableCache: Record<string, any> = observable.shallow({});
163
407
  protected _delegates: FlowContext[] = [];
@@ -242,8 +486,15 @@ export class FlowContext {
242
486
  });
243
487
  }
244
488
 
245
- defineMethod(name: string, fn: (...args: any[]) => any, des?: string) {
489
+ defineMethod(name: string, fn: (...args: any[]) => any, info?: string | FlowContextMethodInfoInput) {
246
490
  this._methods[name] = fn;
491
+ if (typeof info === 'string') {
492
+ this._methodInfos[name] = { description: info };
493
+ } else if (info && typeof info === 'object') {
494
+ this._methodInfos[name] = info;
495
+ } else {
496
+ delete this._methodInfos[name];
497
+ }
247
498
  Object.defineProperty(this, name, {
248
499
  configurable: true,
249
500
  enumerable: false,
@@ -421,6 +672,1821 @@ export class FlowContext {
421
672
  return sorted.map(([key, metaOrFactory]) => this.#toTreeNode(key, metaOrFactory, [key], []));
422
673
  }
423
674
 
675
+ /**
676
+ * 获取静态 API 文档信息(仅顶层一层)。
677
+ *
678
+ * - 输出仅来自 RunJS doc 与 defineProperty/defineMethod 的 info
679
+ * - 不读取/展开 PropertyMeta(变量结构)
680
+ * - 不自动展开深层 properties
681
+ * - 不返回自动补全字段(例如 completion)
682
+ */
683
+ async getApiInfos(options: FlowContextGetApiInfosOptions = {}): Promise<Record<string, FlowContextApiInfo>> {
684
+ const version = (options.version as RunJSVersion) || ('v1' as RunJSVersion);
685
+ const evalCtx = this.createProxy();
686
+
687
+ const isPrivateKey = (key: string) => typeof key === 'string' && key.startsWith('_');
688
+ // NOTE: These are variable-like roots documented in RunJS context doc, but should be served by `getVarInfos()`.
689
+ // `getApiInfos()` intentionally excludes them to keep static docs and variable meta separated.
690
+ const isVarRootKey = (key: string) => key === 'record' || key === 'formValues' || key === 'popup';
691
+
692
+ const isPromiseLike = (v: any): v is Promise<any> =>
693
+ !!v && (typeof v === 'object' || typeof v === 'function') && typeof (v as any).then === 'function';
694
+
695
+ const getRunJSDoc = (): any => {
696
+ const modelClass = getModelClassName(this);
697
+ const Ctor = RunJSContextRegistry.resolve(version, modelClass) || RunJSContextRegistry.resolve(version, '*');
698
+ if (!Ctor) return {};
699
+ const locale = (this as any)?.api?.auth?.locale || (this as any)?.i18n?.language || (this as any)?.locale;
700
+ try {
701
+ if ((Ctor as any)?.getDoc?.length) {
702
+ return (Ctor as any).getDoc(locale) || {};
703
+ }
704
+ return (Ctor as any)?.getDoc?.() || {};
705
+ } catch (_) {
706
+ return {};
707
+ }
708
+ };
709
+
710
+ const doc = getRunJSDoc();
711
+ const docMethods = __isPlainObject(doc?.methods) ? (doc.methods as Record<string, any>) : {};
712
+ const docProps = __isPlainObject(doc?.properties) ? (doc.properties as Record<string, any>) : {};
713
+
714
+ const toDocObject = (node: any): any | undefined => {
715
+ if (typeof node === 'string') return { description: node };
716
+ if (__isPlainObject(node)) return node;
717
+ return undefined;
718
+ };
719
+
720
+ const mapDocKeyToApiKey = (key: string, docNode: any): string => {
721
+ // Some libs are exposed as both `ctx.React` and `ctx.libs.React`. Prefer documenting them under `libs.*`.
722
+ const desc =
723
+ typeof docNode === 'string'
724
+ ? docNode
725
+ : __isPlainObject(docNode) && typeof (docNode as any).description === 'string'
726
+ ? String((docNode as any).description)
727
+ : undefined;
728
+ if (desc && desc.includes(`ctx.libs.${key}`)) return `libs.${key}`;
729
+ return key;
730
+ };
731
+
732
+ const pickMethodInfo = (obj: any): Partial<FlowContextApiInfo> => {
733
+ const src = toDocObject(obj);
734
+ if (!src) return {};
735
+ const out: any = {};
736
+ for (const k of ['description', 'examples', 'ref', 'params', 'returns']) {
737
+ const v = (src as any)[k];
738
+ if (typeof v !== 'undefined') out[k] = v;
739
+ }
740
+ if (Array.isArray(out.examples)) {
741
+ out.examples = out.examples.filter((x: any) => typeof x === 'string' && x.trim());
742
+ }
743
+ return out;
744
+ };
745
+
746
+ const pickPropertyInfo = (obj: any): Partial<FlowContextApiInfo> => {
747
+ const src = toDocObject(obj);
748
+ if (!src) return {};
749
+ const out: any = {};
750
+ for (const k of ['title', 'type', 'interface', 'description', 'examples', 'ref', 'params', 'returns']) {
751
+ const v = (src as any)[k];
752
+ if (typeof v !== 'undefined') out[k] = v;
753
+ }
754
+ if (Array.isArray(out.examples)) {
755
+ out.examples = out.examples.filter((x: any) => typeof x === 'string' && x.trim());
756
+ }
757
+ return out;
758
+ };
759
+
760
+ const getMethodInfoFromChain = (name: string): FlowContextMethodInfoInput | undefined => {
761
+ const visited = new WeakSet<any>();
762
+ const walk = (ctx: FlowContext): FlowContextMethodInfoInput | undefined => {
763
+ if (!ctx || typeof ctx !== 'object') return undefined;
764
+ if (visited.has(ctx as any)) return undefined;
765
+ visited.add(ctx as any);
766
+ if (Object.prototype.hasOwnProperty.call((ctx as any)._methodInfos || {}, name)) {
767
+ return (ctx as any)._methodInfos?.[name] as FlowContextMethodInfoInput;
768
+ }
769
+ const delegates = (ctx as any)._delegates;
770
+ if (Array.isArray(delegates)) {
771
+ for (const d of delegates) {
772
+ const found = walk(d);
773
+ if (found) return found;
774
+ }
775
+ }
776
+ return undefined;
777
+ };
778
+ return walk(this);
779
+ };
780
+
781
+ const resolvePropertyInfo = async (key: string): Promise<FlowContextPropertyInfoInput | undefined> => {
782
+ const opt = this.getPropertyOptions(key);
783
+ if (!opt?.info) return undefined;
784
+ try {
785
+ const v = typeof opt.info === 'function' ? (opt.info as any).call(evalCtx, evalCtx) : opt.info;
786
+ const resolved = isPromiseLike(v) ? await v : v;
787
+ return (resolved ?? undefined) as any;
788
+ } catch (_) {
789
+ return undefined;
790
+ }
791
+ };
792
+
793
+ const propKeys = new Set<string>();
794
+ const methodKeys = new Set<string>();
795
+ for (const k of Object.keys(docProps)) propKeys.add(k);
796
+ for (const k of Object.keys(docMethods)) methodKeys.add(k);
797
+
798
+ const collectInfoKeysDeep = (ctx: FlowContext, visited: WeakSet<any>) => {
799
+ if (!ctx || typeof ctx !== 'object') return;
800
+ if (visited.has(ctx as any)) return;
801
+ visited.add(ctx as any);
802
+
803
+ try {
804
+ const props = (ctx as any)._props;
805
+ if (props && typeof props === 'object') {
806
+ for (const [k, v] of Object.entries(props)) {
807
+ if ((v as any)?.info) propKeys.add(k);
808
+ }
809
+ }
810
+ } catch (_) {
811
+ // ignore
812
+ }
813
+
814
+ try {
815
+ const mi = (ctx as any)._methodInfos;
816
+ if (mi && typeof mi === 'object') {
817
+ for (const k of Object.keys(mi)) methodKeys.add(k);
818
+ }
819
+ } catch (_) {
820
+ // ignore
821
+ }
822
+
823
+ try {
824
+ const delegates = (ctx as any)._delegates;
825
+ if (Array.isArray(delegates)) {
826
+ for (const d of delegates) collectInfoKeysDeep(d, visited);
827
+ }
828
+ } catch (_) {
829
+ // ignore
830
+ }
831
+ };
832
+ collectInfoKeysDeep(this, new WeakSet<any>());
833
+
834
+ const out: Record<string, FlowContextApiInfo> = {};
835
+
836
+ for (const key of propKeys) {
837
+ if (isPrivateKey(key)) continue;
838
+ if (isVarRootKey(key)) continue;
839
+ const docNode = docProps[key];
840
+ const infoNode = await resolvePropertyInfo(key);
841
+ if (typeof docNode === 'undefined' && typeof infoNode === 'undefined') continue;
842
+
843
+ const docObj = toDocObject(docNode);
844
+ const infoObj = toDocObject(infoNode);
845
+ let node: FlowContextApiInfo = {};
846
+ node = { ...node, ...pickPropertyInfo(docObj) };
847
+ node = { ...node, ...pickPropertyInfo(infoObj) };
848
+ delete (node as any).properties;
849
+ delete (node as any).completion;
850
+ if (!Object.keys(node).length) continue;
851
+ const outKey = mapDocKeyToApiKey(key, docNode);
852
+ // Avoid exposing ctx.React/ctx.ReactDOM/ctx.antd in api docs when mapping to ctx.libs.*.
853
+ out[outKey] = out[outKey] ? { ...(out[outKey] || {}), ...(node || {}) } : node;
854
+ }
855
+
856
+ for (const key of methodKeys) {
857
+ if (isPrivateKey(key)) continue;
858
+ const docNode = docMethods[key];
859
+ const info = getMethodInfoFromChain(key);
860
+ if (typeof docNode === 'undefined' && typeof info === 'undefined') continue;
861
+
862
+ const docObj = toDocObject(docNode);
863
+ let node: FlowContextApiInfo = {};
864
+ node = { ...node, ...pickMethodInfo(docObj) };
865
+ node = { ...node, ...pickMethodInfo(info) };
866
+ delete (node as any).properties;
867
+ delete (node as any).completion;
868
+ if (!Object.keys(node).length) continue;
869
+ node.type = 'function';
870
+
871
+ if (!out[key]) out[key] = node;
872
+ else out[key] = { ...(out[key] || {}), ...(node || {}) };
873
+ }
874
+
875
+ // Flatten libs children (one-layer output, but allow `libs.xxx` keys).
876
+ // Prefer richer doc from root aliases (e.g. `React` mapped to `libs.React`) when available.
877
+ const libsDocObj = toDocObject(docProps.libs);
878
+ const libsChildren = __isPlainObject((libsDocObj as any)?.properties)
879
+ ? ((libsDocObj as any).properties as any as Record<string, any>)
880
+ : undefined;
881
+ if (libsChildren) {
882
+ for (const [k, v] of Object.entries(libsChildren)) {
883
+ if (isPrivateKey(k)) continue;
884
+ const outKey = `libs.${k}`;
885
+ if (out[outKey]) continue;
886
+ const childObj = toDocObject(v);
887
+ let node: FlowContextApiInfo = {};
888
+ node = { ...node, ...pickPropertyInfo(childObj) };
889
+ delete (node as any).properties;
890
+ delete (node as any).completion;
891
+ if (!node.description || !String(node.description).trim()) continue;
892
+ out[outKey] = node;
893
+ }
894
+ }
895
+
896
+ return out;
897
+ }
898
+
899
+ /**
900
+ * 获取运行时环境快照信息(小体积、可序列化)。
901
+ */
902
+ async getEnvInfos(): Promise<FlowContextInfosEnvs> {
903
+ const evalCtx = this.createProxy();
904
+
905
+ const isPromiseLike = (v: any): v is Promise<any> =>
906
+ !!v && (typeof v === 'object' || typeof v === 'function') && typeof (v as any).then === 'function';
907
+
908
+ const envs: FlowContextInfosEnvs = {};
909
+
910
+ type ResourceSnapshotKey = 'dataSourceKey' | 'collectionName' | 'associationName' | 'filterByTk' | 'sourceId';
911
+ type ResourceSnapshot = Partial<Record<ResourceSnapshotKey, JSONValue>>;
912
+ type ResourceLike = ResourceSnapshot & {
913
+ getDataSourceKey?: () => JSONValue;
914
+ getFilterByTk?: () => JSONValue;
915
+ getSourceId?: () => JSONValue;
916
+ getResourceName?: () => string;
917
+ getMeta?: (key: string) => unknown;
918
+ };
919
+ type ModelCtorLike = { name?: string; meta?: { label?: string } };
920
+ type ModelLike = { uid?: string; title?: string; resource?: unknown; constructor?: ModelCtorLike };
921
+ type PopupLike = { uid?: string; resource?: unknown; record?: unknown; sourceRecord?: unknown; parent?: unknown };
922
+
923
+ const getMaybe = <T = any>(fn: () => T): T | undefined => {
924
+ try {
925
+ return fn();
926
+ } catch (_) {
927
+ return undefined;
928
+ }
929
+ };
930
+
931
+ const hasSnapshotValue = <T>(v: T): v is Exclude<T, undefined | null> => {
932
+ if (typeof v === 'undefined' || v === null) return false;
933
+ if (typeof v === 'string') return v.trim().length > 0;
934
+ if (Array.isArray(v)) return v.length > 0;
935
+ return true;
936
+ };
937
+
938
+ const getResourceSnapshot = (res: unknown): ResourceSnapshot => {
939
+ const out: ResourceSnapshot = {};
940
+ if (!res) return out;
941
+ const r = res as ResourceLike;
942
+
943
+ // Direct fields (popup/view inputArgs style)
944
+ for (const k of [
945
+ 'dataSourceKey',
946
+ 'collectionName',
947
+ 'associationName',
948
+ 'filterByTk',
949
+ 'sourceId',
950
+ ] as ResourceSnapshotKey[]) {
951
+ const v = r?.[k];
952
+ if (hasSnapshotValue(v)) out[k] = v;
953
+ }
954
+
955
+ // FlowResource-like methods (BaseRecordResource/SQLResource)
956
+ if (!('dataSourceKey' in out)) {
957
+ const v = r.getDataSourceKey?.();
958
+ if (hasSnapshotValue(v)) out.dataSourceKey = v;
959
+ }
960
+ if (!('filterByTk' in out)) {
961
+ const v = r.getFilterByTk?.();
962
+ if (hasSnapshotValue(v)) out.filterByTk = v;
963
+ }
964
+ if (!('filterByTk' in out)) {
965
+ const v = r.getMeta?.('currentFilterByTk') as JSONValue | undefined;
966
+ if (hasSnapshotValue(v)) out.filterByTk = v;
967
+ }
968
+ if (!('sourceId' in out)) {
969
+ const v = r.getSourceId?.();
970
+ if (hasSnapshotValue(v)) out.sourceId = v;
971
+ }
972
+
973
+ // Infer collection/association from resourceName when not provided
974
+ if (!('collectionName' in out) || !('associationName' in out)) {
975
+ const rn = r.getResourceName?.();
976
+ const resourceName = typeof rn === 'string' ? rn.trim() : '';
977
+ if (resourceName) {
978
+ const parts = resourceName
979
+ .split('.')
980
+ .map((x) => x.trim())
981
+ .filter(Boolean);
982
+ if (parts.length === 1) {
983
+ if (!('collectionName' in out)) out.collectionName = parts[0];
984
+ } else if (parts.length >= 2) {
985
+ if (!('collectionName' in out)) out.collectionName = parts[0];
986
+ if (!('associationName' in out)) out.associationName = parts.slice(1).join('.');
987
+ }
988
+ }
989
+ }
990
+
991
+ return out;
992
+ };
993
+
994
+ // Resolve popup (may be Promise)
995
+ const popup = await (async () => {
996
+ try {
997
+ const raw = (evalCtx as any).popup;
998
+ return isPromiseLike(raw) ? await raw : raw;
999
+ } catch (_) {
1000
+ return undefined;
1001
+ }
1002
+ })();
1003
+
1004
+ const popupLike = popup as PopupLike | undefined;
1005
+ const model = getMaybe(() => (evalCtx as any).model) as ModelLike | undefined;
1006
+ const blockModel = getMaybe(() => (evalCtx as any).blockModel) as ModelLike | undefined;
1007
+ const inputArgs = getMaybe(() => (evalCtx as any).view?.inputArgs) as
1008
+ | (ResourceLike & { viewUid?: string })
1009
+ | undefined;
1010
+ const ctxResource = getMaybe(() => (evalCtx as any).resource) as ResourceLike | undefined;
1011
+
1012
+ const popupResource = popupLike?.resource;
1013
+ const popupResourceSnap = getResourceSnapshot(popupResource);
1014
+
1015
+ const blockOwner = blockModel;
1016
+ const blockOwnerExpr = blockModel ? 'ctx.blockModel' : undefined;
1017
+ const blockResourceBaseExpr = blockOwnerExpr ? `${blockOwnerExpr}.resource` : undefined;
1018
+ const blockResource = blockOwner?.resource;
1019
+ const blockResourceSnap = getResourceSnapshot(blockResource);
1020
+ const inputArgsSnap = getResourceSnapshot(inputArgs);
1021
+ const ctxResourceSnap = getResourceSnapshot(ctxResource);
1022
+
1023
+ // Resource snapshot (for prompt)
1024
+ const pickWithGetVar = <T>(
1025
+ pairs: Array<{
1026
+ value: T | undefined;
1027
+ getVar: string;
1028
+ }>,
1029
+ ): { value: T; getVar: string } | undefined => {
1030
+ for (const p of pairs) {
1031
+ if (hasSnapshotValue(p.value)) return { value: p.value, getVar: p.getVar };
1032
+ }
1033
+ return undefined;
1034
+ };
1035
+
1036
+ const hasAnyResourceValuesIn = (snap: ResourceSnapshot): boolean =>
1037
+ hasSnapshotValue(snap.collectionName) ||
1038
+ hasSnapshotValue(snap.dataSourceKey) ||
1039
+ hasSnapshotValue(snap.associationName);
1040
+
1041
+ const resourceBaseExpr: string | undefined = hasAnyResourceValuesIn(popupResourceSnap)
1042
+ ? 'ctx.popup.resource'
1043
+ : hasAnyResourceValuesIn(blockResourceSnap)
1044
+ ? blockResourceBaseExpr
1045
+ : hasAnyResourceValuesIn(inputArgsSnap)
1046
+ ? 'ctx.view.inputArgs'
1047
+ : hasAnyResourceValuesIn(ctxResourceSnap)
1048
+ ? 'ctx.resource'
1049
+ : undefined;
1050
+
1051
+ const collectionNamePick = pickWithGetVar([
1052
+ { value: popupResourceSnap?.collectionName, getVar: 'ctx.popup.resource.collectionName' },
1053
+ { value: blockResourceSnap?.collectionName, getVar: `${blockResourceBaseExpr}.collectionName` },
1054
+ { value: inputArgsSnap?.collectionName, getVar: 'ctx.view.inputArgs.collectionName' },
1055
+ { value: ctxResourceSnap?.collectionName, getVar: 'ctx.resource.collectionName' },
1056
+ ]);
1057
+ const dataSourceKeyPick = pickWithGetVar([
1058
+ { value: popupResourceSnap?.dataSourceKey, getVar: 'ctx.popup.resource.dataSourceKey' },
1059
+ { value: blockResourceSnap?.dataSourceKey, getVar: `${blockResourceBaseExpr}.dataSourceKey` },
1060
+ { value: inputArgsSnap?.dataSourceKey, getVar: 'ctx.view.inputArgs.dataSourceKey' },
1061
+ { value: ctxResourceSnap?.dataSourceKey, getVar: 'ctx.resource.dataSourceKey' },
1062
+ ]);
1063
+ const associationNamePick = pickWithGetVar([
1064
+ { value: popupResourceSnap?.associationName, getVar: 'ctx.popup.resource.associationName' },
1065
+ { value: blockResourceSnap?.associationName, getVar: `${blockResourceBaseExpr}.associationName` },
1066
+ { value: inputArgsSnap?.associationName, getVar: 'ctx.view.inputArgs.associationName' },
1067
+ { value: ctxResourceSnap?.associationName, getVar: 'ctx.resource.associationName' },
1068
+ ]);
1069
+ const filterByTkPick = pickWithGetVar([
1070
+ { value: popupResourceSnap?.filterByTk, getVar: 'ctx.popup.resource.filterByTk' },
1071
+ { value: blockResourceSnap?.filterByTk, getVar: `${blockResourceBaseExpr}.filterByTk` },
1072
+ { value: inputArgsSnap?.filterByTk, getVar: 'ctx.view.inputArgs.filterByTk' },
1073
+ { value: ctxResourceSnap?.filterByTk, getVar: 'ctx.resource.filterByTk' },
1074
+ ]);
1075
+ const sourceIdPick = pickWithGetVar([
1076
+ { value: popupResourceSnap?.sourceId, getVar: 'ctx.popup.resource.sourceId' },
1077
+ { value: blockResourceSnap?.sourceId, getVar: `${blockResourceBaseExpr}.sourceId` },
1078
+ { value: inputArgsSnap?.sourceId, getVar: 'ctx.view.inputArgs.sourceId' },
1079
+ { value: ctxResourceSnap?.sourceId, getVar: 'ctx.resource.sourceId' },
1080
+ ]);
1081
+
1082
+ const resourceProps: Record<string, FlowContextInfosEnvNode> = {};
1083
+ let hasResourceValues = false;
1084
+ const collectionNameValue = collectionNamePick?.value;
1085
+ if (hasSnapshotValue(collectionNameValue)) {
1086
+ resourceProps.collectionName = {
1087
+ description: 'Collection name',
1088
+ getVar: collectionNamePick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.collectionName` : undefined),
1089
+ value: collectionNameValue,
1090
+ };
1091
+ hasResourceValues = true;
1092
+ }
1093
+ const dataSourceKeyValue = dataSourceKeyPick?.value;
1094
+ if (hasSnapshotValue(dataSourceKeyValue)) {
1095
+ resourceProps.dataSourceKey = {
1096
+ description: 'Data source key',
1097
+ getVar: dataSourceKeyPick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.dataSourceKey` : undefined),
1098
+ value: dataSourceKeyValue,
1099
+ };
1100
+ hasResourceValues = true;
1101
+ }
1102
+ const associationNameValue = associationNamePick?.value;
1103
+ if (hasSnapshotValue(associationNameValue)) {
1104
+ resourceProps.associationName = {
1105
+ description: 'Association name',
1106
+ getVar: associationNamePick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.associationName` : undefined),
1107
+ value: associationNameValue,
1108
+ };
1109
+ hasResourceValues = true;
1110
+ }
1111
+
1112
+ // Only include envs.resource when snapshot contains at least one resource value.
1113
+ // Optional fields like filterByTk/sourceId are included (without value) only when envs.resource exists.
1114
+ if (hasResourceValues) {
1115
+ if (hasSnapshotValue(filterByTkPick?.value)) {
1116
+ resourceProps.filterByTk = {
1117
+ description: 'Record filterByTk',
1118
+ getVar: filterByTkPick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.filterByTk` : undefined),
1119
+ };
1120
+ }
1121
+ if (hasSnapshotValue(sourceIdPick?.value)) {
1122
+ resourceProps.sourceId = {
1123
+ description: 'Source record ID (sourceId)',
1124
+ getVar: sourceIdPick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.sourceId` : undefined),
1125
+ };
1126
+ }
1127
+
1128
+ envs.resource = {
1129
+ description: 'Resource information',
1130
+ getVar: resourceBaseExpr,
1131
+ properties: resourceProps,
1132
+ };
1133
+ }
1134
+
1135
+ // Record (only when filterByTk is available)
1136
+ if (hasSnapshotValue(filterByTkPick?.value)) {
1137
+ envs.record = {
1138
+ description: 'Current record',
1139
+ getVar: 'ctx.record',
1140
+ };
1141
+ }
1142
+
1143
+ const pickLabel = (obj: ModelLike | null | undefined): string | undefined => {
1144
+ try {
1145
+ const t = obj?.title;
1146
+ if (typeof t === 'string' && t.trim()) return t;
1147
+ } catch (_) {
1148
+ // ignore
1149
+ }
1150
+ try {
1151
+ const label = obj?.constructor?.meta?.label;
1152
+ if (typeof label === 'string' && label.trim()) return label;
1153
+ } catch (_) {
1154
+ // ignore
1155
+ }
1156
+ return undefined;
1157
+ };
1158
+
1159
+ // FlowModel (when ctx.model exists)
1160
+ if (model) {
1161
+ const modelLabel = pickLabel(model);
1162
+ const modelUid = model.uid;
1163
+ const modelClassName = model.constructor?.name;
1164
+ const modelResourceSnap = getResourceSnapshot(model.resource);
1165
+ const modelResourceProps: Record<string, FlowContextInfosEnvNode> = {};
1166
+ let hasModelResourceValues = false;
1167
+ const modelCollectionName = modelResourceSnap.collectionName;
1168
+ if (hasSnapshotValue(modelCollectionName)) {
1169
+ modelResourceProps.collectionName = {
1170
+ description: 'Collection name',
1171
+ getVar: 'ctx.model.resource.collectionName',
1172
+ value: modelCollectionName,
1173
+ };
1174
+ hasModelResourceValues = true;
1175
+ }
1176
+ const modelDataSourceKey = modelResourceSnap.dataSourceKey;
1177
+ if (hasSnapshotValue(modelDataSourceKey)) {
1178
+ modelResourceProps.dataSourceKey = {
1179
+ description: 'Data source key',
1180
+ getVar: 'ctx.model.resource.dataSourceKey',
1181
+ value: modelDataSourceKey,
1182
+ };
1183
+ hasModelResourceValues = true;
1184
+ }
1185
+ const modelAssociationName = modelResourceSnap.associationName;
1186
+ if (hasSnapshotValue(modelAssociationName)) {
1187
+ modelResourceProps.associationName = {
1188
+ description: 'Association name',
1189
+ getVar: 'ctx.model.resource.associationName',
1190
+ value: modelAssociationName,
1191
+ };
1192
+ hasModelResourceValues = true;
1193
+ }
1194
+
1195
+ envs.flowModel = {
1196
+ description: 'Current FlowModel information',
1197
+ getVar: 'ctx.model',
1198
+ properties: {
1199
+ ...(hasSnapshotValue(modelLabel) ? { label: { description: 'Flow model label', value: modelLabel } } : {}),
1200
+ ...(hasSnapshotValue(modelClassName)
1201
+ ? {
1202
+ modelClass: {
1203
+ description: 'Flow model class name',
1204
+ value: modelClassName,
1205
+ },
1206
+ }
1207
+ : {}),
1208
+ ...(hasSnapshotValue(modelUid)
1209
+ ? { uid: { description: 'Flow model uid', getVar: 'ctx.model.uid', value: modelUid } }
1210
+ : {}),
1211
+ ...(hasModelResourceValues
1212
+ ? {
1213
+ resource: {
1214
+ description: 'Flow model resource',
1215
+ getVar: 'ctx.model.resource',
1216
+ properties: modelResourceProps,
1217
+ },
1218
+ }
1219
+ : {}),
1220
+ },
1221
+ };
1222
+ }
1223
+
1224
+ // popup info (when ctx.popup exists)
1225
+ if (popupLike?.uid) {
1226
+ envs.popup = {
1227
+ description: 'Current popup information',
1228
+ getVar: 'ctx.popup',
1229
+ properties: {
1230
+ uid: { description: 'Popup uid', getVar: 'ctx.popup.uid', value: popupLike.uid },
1231
+ ...(popupLike?.record ? { record: { description: 'Popup record', getVar: 'ctx.popup.record' } } : {}),
1232
+ ...(popupLike?.sourceRecord
1233
+ ? { sourceRecord: { description: 'Popup source record', getVar: 'ctx.popup.sourceRecord' } }
1234
+ : {}),
1235
+ },
1236
+ };
1237
+ }
1238
+
1239
+ // block (when ctx.blockModel exists)
1240
+ if (blockOwner) {
1241
+ const blockLabel = pickLabel(blockOwner);
1242
+ const blockUid = blockOwner.uid;
1243
+ const blockModelClass = blockOwner.constructor?.name;
1244
+ const blockResourceProps: Record<string, FlowContextInfosEnvNode> = {};
1245
+ let hasBlockResourceValues = false;
1246
+ const blockCollectionName = blockResourceSnap.collectionName;
1247
+ if (hasSnapshotValue(blockCollectionName)) {
1248
+ blockResourceProps.collectionName = {
1249
+ description: 'Collection name',
1250
+ getVar: `${blockResourceBaseExpr}.collectionName`,
1251
+ value: blockCollectionName,
1252
+ };
1253
+ hasBlockResourceValues = true;
1254
+ }
1255
+ const blockDataSourceKey = blockResourceSnap.dataSourceKey;
1256
+ if (hasSnapshotValue(blockDataSourceKey)) {
1257
+ blockResourceProps.dataSourceKey = {
1258
+ description: 'Data source key',
1259
+ getVar: `${blockResourceBaseExpr}.dataSourceKey`,
1260
+ value: blockDataSourceKey,
1261
+ };
1262
+ hasBlockResourceValues = true;
1263
+ }
1264
+ const blockAssociationName = blockResourceSnap.associationName;
1265
+ if (hasSnapshotValue(blockAssociationName)) {
1266
+ blockResourceProps.associationName = {
1267
+ description: 'Association name',
1268
+ getVar: `${blockResourceBaseExpr}.associationName`,
1269
+ value: blockAssociationName,
1270
+ };
1271
+ hasBlockResourceValues = true;
1272
+ }
1273
+
1274
+ envs.block = {
1275
+ description: 'Current block information',
1276
+ getVar: blockOwnerExpr,
1277
+ properties: {
1278
+ ...(hasSnapshotValue(blockLabel) ? { label: { description: 'Block label', value: blockLabel } } : {}),
1279
+ ...(hasSnapshotValue(blockModelClass)
1280
+ ? { modelClass: { description: 'Block model class name', value: blockModelClass } }
1281
+ : {}),
1282
+ ...(hasSnapshotValue(blockUid)
1283
+ ? { uid: { description: 'Block uid', getVar: `${blockOwnerExpr}.uid`, value: blockUid } }
1284
+ : {}),
1285
+ ...(hasBlockResourceValues
1286
+ ? {
1287
+ resource: {
1288
+ description: 'Block resource',
1289
+ getVar: blockResourceBaseExpr,
1290
+ properties: blockResourceProps,
1291
+ },
1292
+ }
1293
+ : {}),
1294
+ },
1295
+ };
1296
+ }
1297
+
1298
+ // Current view blocks snapshot (page view or current popup view)
1299
+ const viewUid = (() => {
1300
+ const popupUid = popupLike?.uid;
1301
+ if (hasSnapshotValue(popupUid)) return String(popupUid).trim();
1302
+ const v = (inputArgs as any)?.viewUid;
1303
+ if (hasSnapshotValue(v)) return String(v).trim();
1304
+ return undefined;
1305
+ })();
1306
+
1307
+ const engine = getMaybe(() => (evalCtx as any).engine) as FlowEngine | undefined;
1308
+ const viewModel = viewUid ? engine?.getModel(viewUid, true) : undefined;
1309
+
1310
+ type ViewTreeNode = {
1311
+ uid: string;
1312
+ subModels?: Record<string, unknown>;
1313
+ context?: { blockModel?: unknown };
1314
+ resource?: unknown;
1315
+ constructor?: { name?: string };
1316
+ };
1317
+
1318
+ const isBlockModelInstance = (m: ViewTreeNode): boolean => m.context?.blockModel === m;
1319
+
1320
+ if (viewModel) {
1321
+ const queue: ViewTreeNode[] = [viewModel as unknown as ViewTreeNode];
1322
+ const blocks: Array<Record<string, JSONValue>> = [];
1323
+
1324
+ for (let i = 0; i < queue.length; i++) {
1325
+ const m = queue[i];
1326
+
1327
+ if (isBlockModelInstance(m)) {
1328
+ const modelClass = m.constructor?.name || m.uid;
1329
+ const label = pickLabel(m as any) || modelClass || m.uid;
1330
+
1331
+ const resSnap = getResourceSnapshot(m.resource);
1332
+ const resource: Record<string, JSONValue> = {};
1333
+ if (hasSnapshotValue(resSnap.dataSourceKey)) resource.dataSourceKey = resSnap.dataSourceKey;
1334
+ if (hasSnapshotValue(resSnap.collectionName)) resource.collectionName = resSnap.collectionName;
1335
+ if (hasSnapshotValue(resSnap.associationName)) resource.associationName = resSnap.associationName;
1336
+
1337
+ const block: Record<string, JSONValue> = {
1338
+ uid: m.uid,
1339
+ label,
1340
+ modelClass,
1341
+ ...(Object.keys(resource).length > 0 ? { resource } : {}),
1342
+ };
1343
+ blocks.push(block);
1344
+ }
1345
+
1346
+ const subModels = m.subModels;
1347
+ if (subModels && typeof subModels === 'object') {
1348
+ for (const v of Object.values(subModels)) {
1349
+ if (!v) continue;
1350
+ if (Array.isArray(v)) queue.push(...(v as ViewTreeNode[]));
1351
+ else queue.push(v as ViewTreeNode);
1352
+ }
1353
+ }
1354
+ }
1355
+
1356
+ if (blocks.length) {
1357
+ envs.currentViewBlocks = {
1358
+ description: 'Current view blocks',
1359
+ value: blocks,
1360
+ };
1361
+ }
1362
+ }
1363
+
1364
+ return envs;
1365
+ }
1366
+
1367
+ /**
1368
+ * 获取变量结构信息(来源于 PropertyMeta)。
1369
+ *
1370
+ * - 返回静态 plain object(不包含函数)
1371
+ * - 支持 maxDepth(默认 3)与 path 剪裁
1372
+ */
1373
+ async getVarInfos(options: FlowContextGetVarInfosOptions = {}): Promise<Record<string, FlowContextApiInfo>> {
1374
+ const maxDepthRaw = options.maxDepth ?? 3;
1375
+ const maxDepth = Number.isFinite(maxDepthRaw) ? Math.max(1, Math.floor(maxDepthRaw)) : 3;
1376
+ const version = 'v1' as RunJSVersion;
1377
+ const evalCtx = this.createProxy();
1378
+
1379
+ const isPrivateKey = (key: string) => typeof key === 'string' && key.startsWith('_');
1380
+
1381
+ const isPromiseLike = (v: any): v is Promise<any> =>
1382
+ !!v && (typeof v === 'object' || typeof v === 'function') && typeof (v as any).then === 'function';
1383
+
1384
+ // Per-call cache for resolved PropertyMetaFactory nodes to avoid repeated async calls.
1385
+ const metaFactoryCache = new WeakMap<Function, Promise<PropertyMeta | null>>();
1386
+ const resolveMetaOrFactory = async (meta?: PropertyMetaOrFactory): Promise<PropertyMeta | undefined> => {
1387
+ if (!meta) return undefined;
1388
+ if (typeof meta !== 'function') return meta;
1389
+ let pending = metaFactoryCache.get(meta);
1390
+ if (!pending) {
1391
+ pending = (async () => {
1392
+ const v = (meta as any).call(evalCtx, evalCtx);
1393
+ const resolved = isPromiseLike(v) ? await v : v;
1394
+ return resolved || null;
1395
+ })();
1396
+ metaFactoryCache.set(meta, pending);
1397
+ }
1398
+ const resolved = await pending;
1399
+ return resolved || undefined;
1400
+ };
1401
+
1402
+ const buildEnvs = async (): Promise<FlowContextInfosEnvs> => {
1403
+ const envs: FlowContextInfosEnvs = {};
1404
+
1405
+ type ResourceSnapshotKey = 'dataSourceKey' | 'collectionName' | 'associationName' | 'filterByTk' | 'sourceId';
1406
+ type ResourceSnapshot = Partial<Record<ResourceSnapshotKey, JSONValue>>;
1407
+ type ResourceLike = ResourceSnapshot & {
1408
+ getDataSourceKey?: () => JSONValue;
1409
+ getFilterByTk?: () => JSONValue;
1410
+ getSourceId?: () => JSONValue;
1411
+ getResourceName?: () => string;
1412
+ getMeta?: (key: string) => unknown;
1413
+ };
1414
+ type ModelCtorLike = { name?: string; meta?: { label?: string } };
1415
+ type ModelLike = { uid?: string; title?: string; resource?: unknown; constructor?: ModelCtorLike };
1416
+ type PopupLike = { uid?: string; resource?: unknown; record?: unknown; sourceRecord?: unknown; parent?: unknown };
1417
+
1418
+ const getMaybe = <T = any>(fn: () => T): T | undefined => {
1419
+ try {
1420
+ return fn();
1421
+ } catch (_) {
1422
+ return undefined;
1423
+ }
1424
+ };
1425
+
1426
+ const hasSnapshotValue = <T>(v: T): v is Exclude<T, undefined | null> => {
1427
+ if (typeof v === 'undefined' || v === null) return false;
1428
+ if (typeof v === 'string') return v.trim().length > 0;
1429
+ if (Array.isArray(v)) return v.length > 0;
1430
+ return true;
1431
+ };
1432
+
1433
+ const getResourceSnapshot = (res: unknown): ResourceSnapshot => {
1434
+ const out: ResourceSnapshot = {};
1435
+ if (!res) return out;
1436
+ const r = res as ResourceLike;
1437
+
1438
+ // Direct fields (popup/view inputArgs style)
1439
+ for (const k of [
1440
+ 'dataSourceKey',
1441
+ 'collectionName',
1442
+ 'associationName',
1443
+ 'filterByTk',
1444
+ 'sourceId',
1445
+ ] as ResourceSnapshotKey[]) {
1446
+ const v = r?.[k];
1447
+ if (hasSnapshotValue(v)) out[k] = v;
1448
+ }
1449
+
1450
+ // FlowResource-like methods (BaseRecordResource/SQLResource)
1451
+ if (!('dataSourceKey' in out)) {
1452
+ const v = r.getDataSourceKey?.();
1453
+ if (hasSnapshotValue(v)) out.dataSourceKey = v;
1454
+ }
1455
+ if (!('filterByTk' in out)) {
1456
+ const v = r.getFilterByTk?.();
1457
+ if (hasSnapshotValue(v)) out.filterByTk = v;
1458
+ }
1459
+ if (!('filterByTk' in out)) {
1460
+ const v = r.getMeta?.('currentFilterByTk') as JSONValue | undefined;
1461
+ if (hasSnapshotValue(v)) out.filterByTk = v;
1462
+ }
1463
+ if (!('sourceId' in out)) {
1464
+ const v = r.getSourceId?.();
1465
+ if (hasSnapshotValue(v)) out.sourceId = v;
1466
+ }
1467
+
1468
+ // Infer collection/association from resourceName when not provided
1469
+ if (!('collectionName' in out) || !('associationName' in out)) {
1470
+ const rn = r.getResourceName?.();
1471
+ const resourceName = typeof rn === 'string' ? rn.trim() : '';
1472
+ if (resourceName) {
1473
+ const parts = resourceName
1474
+ .split('.')
1475
+ .map((x) => x.trim())
1476
+ .filter(Boolean);
1477
+ if (parts.length === 1) {
1478
+ if (!('collectionName' in out)) out.collectionName = parts[0];
1479
+ } else if (parts.length >= 2) {
1480
+ if (!('collectionName' in out)) out.collectionName = parts[0];
1481
+ if (!('associationName' in out)) out.associationName = parts.slice(1).join('.');
1482
+ }
1483
+ }
1484
+ }
1485
+
1486
+ return out;
1487
+ };
1488
+
1489
+ // Resolve popup (may be Promise)
1490
+ const popup = await (async () => {
1491
+ try {
1492
+ const raw = (evalCtx as any).popup;
1493
+ return isPromiseLike(raw) ? await raw : raw;
1494
+ } catch (_) {
1495
+ return undefined;
1496
+ }
1497
+ })();
1498
+
1499
+ const popupLike = popup as PopupLike | undefined;
1500
+ const model = getMaybe(() => (evalCtx as any).model) as ModelLike | undefined;
1501
+ const blockModel = getMaybe(() => (evalCtx as any).blockModel) as ModelLike | undefined;
1502
+ const inputArgs = getMaybe(() => (evalCtx as any).view?.inputArgs) as
1503
+ | (ResourceLike & { viewUid?: string })
1504
+ | undefined;
1505
+ const ctxResource = getMaybe(() => (evalCtx as any).resource) as ResourceLike | undefined;
1506
+
1507
+ const popupResource = popupLike?.resource;
1508
+ const popupResourceSnap = getResourceSnapshot(popupResource);
1509
+
1510
+ const blockOwner = blockModel;
1511
+ const blockOwnerExpr = blockModel ? 'ctx.blockModel' : undefined;
1512
+ const blockResourceBaseExpr = blockOwnerExpr ? `${blockOwnerExpr}.resource` : undefined;
1513
+ const blockResource = blockOwner?.resource;
1514
+ const blockResourceSnap = getResourceSnapshot(blockResource);
1515
+ const inputArgsSnap = getResourceSnapshot(inputArgs);
1516
+ const ctxResourceSnap = getResourceSnapshot(ctxResource);
1517
+
1518
+ // Resource snapshot (for prompt)
1519
+ const pickWithGetVar = <T>(
1520
+ pairs: Array<{
1521
+ value: T | undefined;
1522
+ getVar: string;
1523
+ }>,
1524
+ ): { value: T; getVar: string } | undefined => {
1525
+ for (const p of pairs) {
1526
+ if (hasSnapshotValue(p.value)) return { value: p.value, getVar: p.getVar };
1527
+ }
1528
+ return undefined;
1529
+ };
1530
+
1531
+ const hasAnyResourceValuesIn = (snap: ResourceSnapshot): boolean =>
1532
+ hasSnapshotValue(snap.collectionName) ||
1533
+ hasSnapshotValue(snap.dataSourceKey) ||
1534
+ hasSnapshotValue(snap.associationName);
1535
+
1536
+ const resourceBaseExpr: string | undefined = hasAnyResourceValuesIn(popupResourceSnap)
1537
+ ? 'ctx.popup.resource'
1538
+ : hasAnyResourceValuesIn(blockResourceSnap)
1539
+ ? blockResourceBaseExpr
1540
+ : hasAnyResourceValuesIn(inputArgsSnap)
1541
+ ? 'ctx.view.inputArgs'
1542
+ : hasAnyResourceValuesIn(ctxResourceSnap)
1543
+ ? 'ctx.resource'
1544
+ : undefined;
1545
+
1546
+ const collectionNamePick = pickWithGetVar([
1547
+ { value: popupResourceSnap?.collectionName, getVar: 'ctx.popup.resource.collectionName' },
1548
+ { value: blockResourceSnap?.collectionName, getVar: `${blockResourceBaseExpr}.collectionName` },
1549
+ { value: inputArgsSnap?.collectionName, getVar: 'ctx.view.inputArgs.collectionName' },
1550
+ { value: ctxResourceSnap?.collectionName, getVar: 'ctx.resource.collectionName' },
1551
+ ]);
1552
+ const dataSourceKeyPick = pickWithGetVar([
1553
+ { value: popupResourceSnap?.dataSourceKey, getVar: 'ctx.popup.resource.dataSourceKey' },
1554
+ { value: blockResourceSnap?.dataSourceKey, getVar: `${blockResourceBaseExpr}.dataSourceKey` },
1555
+ { value: inputArgsSnap?.dataSourceKey, getVar: 'ctx.view.inputArgs.dataSourceKey' },
1556
+ { value: ctxResourceSnap?.dataSourceKey, getVar: 'ctx.resource.dataSourceKey' },
1557
+ ]);
1558
+ const associationNamePick = pickWithGetVar([
1559
+ { value: popupResourceSnap?.associationName, getVar: 'ctx.popup.resource.associationName' },
1560
+ { value: blockResourceSnap?.associationName, getVar: `${blockResourceBaseExpr}.associationName` },
1561
+ { value: inputArgsSnap?.associationName, getVar: 'ctx.view.inputArgs.associationName' },
1562
+ { value: ctxResourceSnap?.associationName, getVar: 'ctx.resource.associationName' },
1563
+ ]);
1564
+ const filterByTkPick = pickWithGetVar([
1565
+ { value: popupResourceSnap?.filterByTk, getVar: 'ctx.popup.resource.filterByTk' },
1566
+ { value: blockResourceSnap?.filterByTk, getVar: `${blockResourceBaseExpr}.filterByTk` },
1567
+ { value: inputArgsSnap?.filterByTk, getVar: 'ctx.view.inputArgs.filterByTk' },
1568
+ { value: ctxResourceSnap?.filterByTk, getVar: 'ctx.resource.filterByTk' },
1569
+ ]);
1570
+ const sourceIdPick = pickWithGetVar([
1571
+ { value: popupResourceSnap?.sourceId, getVar: 'ctx.popup.resource.sourceId' },
1572
+ { value: blockResourceSnap?.sourceId, getVar: `${blockResourceBaseExpr}.sourceId` },
1573
+ { value: inputArgsSnap?.sourceId, getVar: 'ctx.view.inputArgs.sourceId' },
1574
+ { value: ctxResourceSnap?.sourceId, getVar: 'ctx.resource.sourceId' },
1575
+ ]);
1576
+
1577
+ const resourceProps: Record<string, FlowContextInfosEnvNode> = {};
1578
+ let hasResourceValues = false;
1579
+ const collectionNameValue = collectionNamePick?.value;
1580
+ if (hasSnapshotValue(collectionNameValue)) {
1581
+ resourceProps.collectionName = {
1582
+ description: 'Collection name',
1583
+ getVar: collectionNamePick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.collectionName` : undefined),
1584
+ value: collectionNameValue,
1585
+ };
1586
+ hasResourceValues = true;
1587
+ }
1588
+ const dataSourceKeyValue = dataSourceKeyPick?.value;
1589
+ if (hasSnapshotValue(dataSourceKeyValue)) {
1590
+ resourceProps.dataSourceKey = {
1591
+ description: 'Data source key',
1592
+ getVar: dataSourceKeyPick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.dataSourceKey` : undefined),
1593
+ value: dataSourceKeyValue,
1594
+ };
1595
+ hasResourceValues = true;
1596
+ }
1597
+ const associationNameValue = associationNamePick?.value;
1598
+ if (hasSnapshotValue(associationNameValue)) {
1599
+ resourceProps.associationName = {
1600
+ description: 'Association name',
1601
+ getVar: associationNamePick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.associationName` : undefined),
1602
+ value: associationNameValue,
1603
+ };
1604
+ hasResourceValues = true;
1605
+ }
1606
+
1607
+ // Only include envs.resource when snapshot contains at least one resource value.
1608
+ // Optional fields like filterByTk/sourceId are included (without value) only when envs.resource exists.
1609
+ if (hasResourceValues) {
1610
+ if (hasSnapshotValue(filterByTkPick?.value)) {
1611
+ resourceProps.filterByTk = {
1612
+ description: 'Record filterByTk',
1613
+ getVar: filterByTkPick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.filterByTk` : undefined),
1614
+ };
1615
+ }
1616
+ if (hasSnapshotValue(sourceIdPick?.value)) {
1617
+ resourceProps.sourceId = {
1618
+ description: 'Source record ID (sourceId)',
1619
+ getVar: sourceIdPick?.getVar || (resourceBaseExpr ? `${resourceBaseExpr}.sourceId` : undefined),
1620
+ };
1621
+ }
1622
+
1623
+ envs.resource = {
1624
+ description: 'Resource information',
1625
+ getVar: resourceBaseExpr,
1626
+ properties: resourceProps,
1627
+ };
1628
+ }
1629
+
1630
+ // Record (only when filterByTk is available)
1631
+ if (hasSnapshotValue(filterByTkPick?.value)) {
1632
+ envs.record = {
1633
+ description: 'Current record',
1634
+ getVar: 'ctx.record',
1635
+ };
1636
+ }
1637
+
1638
+ const pickLabel = (obj: ModelLike | null | undefined): string | undefined => {
1639
+ try {
1640
+ const t = obj?.title;
1641
+ if (typeof t === 'string' && t.trim()) return t;
1642
+ } catch (_) {
1643
+ // ignore
1644
+ }
1645
+ try {
1646
+ const label = obj?.constructor?.meta?.label;
1647
+ if (typeof label === 'string' && label.trim()) return label;
1648
+ } catch (_) {
1649
+ // ignore
1650
+ }
1651
+ return undefined;
1652
+ };
1653
+
1654
+ // FlowModel (when ctx.model exists)
1655
+ if (model) {
1656
+ const modelLabel = pickLabel(model);
1657
+ const modelUid = model.uid;
1658
+ const modelClassName = model.constructor?.name;
1659
+ const modelResourceSnap = getResourceSnapshot(model.resource);
1660
+ const modelResourceProps: Record<string, FlowContextInfosEnvNode> = {};
1661
+ let hasModelResourceValues = false;
1662
+ const modelCollectionName = modelResourceSnap.collectionName;
1663
+ if (hasSnapshotValue(modelCollectionName)) {
1664
+ modelResourceProps.collectionName = {
1665
+ description: 'Collection name',
1666
+ getVar: 'ctx.model.resource.collectionName',
1667
+ value: modelCollectionName,
1668
+ };
1669
+ hasModelResourceValues = true;
1670
+ }
1671
+ const modelDataSourceKey = modelResourceSnap.dataSourceKey;
1672
+ if (hasSnapshotValue(modelDataSourceKey)) {
1673
+ modelResourceProps.dataSourceKey = {
1674
+ description: 'Data source key',
1675
+ getVar: 'ctx.model.resource.dataSourceKey',
1676
+ value: modelDataSourceKey,
1677
+ };
1678
+ hasModelResourceValues = true;
1679
+ }
1680
+ const modelAssociationName = modelResourceSnap.associationName;
1681
+ if (hasSnapshotValue(modelAssociationName)) {
1682
+ modelResourceProps.associationName = {
1683
+ description: 'Association name',
1684
+ getVar: 'ctx.model.resource.associationName',
1685
+ value: modelAssociationName,
1686
+ };
1687
+ hasModelResourceValues = true;
1688
+ }
1689
+
1690
+ envs.flowModel = {
1691
+ description: 'Current FlowModel information',
1692
+ getVar: 'ctx.model',
1693
+ properties: {
1694
+ ...(hasSnapshotValue(modelLabel) ? { label: { description: 'Flow model label', value: modelLabel } } : {}),
1695
+ ...(hasSnapshotValue(modelClassName)
1696
+ ? {
1697
+ modelClass: {
1698
+ description: 'Flow model class name',
1699
+ value: modelClassName,
1700
+ },
1701
+ }
1702
+ : {}),
1703
+ ...(hasSnapshotValue(modelUid)
1704
+ ? { uid: { description: 'Flow model uid', getVar: 'ctx.model.uid', value: modelUid } }
1705
+ : {}),
1706
+ ...(hasModelResourceValues
1707
+ ? {
1708
+ resource: {
1709
+ description: 'Resource information',
1710
+ getVar: 'ctx.model.resource',
1711
+ properties: {
1712
+ ...modelResourceProps,
1713
+ ...(hasSnapshotValue(modelResourceSnap?.filterByTk)
1714
+ ? {
1715
+ filterByTk: {
1716
+ description: 'Record filterByTk',
1717
+ getVar: 'ctx.model.resource.filterByTk',
1718
+ },
1719
+ }
1720
+ : {}),
1721
+ ...(hasSnapshotValue(modelResourceSnap?.sourceId)
1722
+ ? {
1723
+ sourceId: {
1724
+ description: 'Source record ID (sourceId)',
1725
+ getVar: 'ctx.model.resource.sourceId',
1726
+ },
1727
+ }
1728
+ : {}),
1729
+ },
1730
+ },
1731
+ }
1732
+ : {}),
1733
+ },
1734
+ };
1735
+ }
1736
+
1737
+ // Block (when ctx.blockModel exists)
1738
+ if (blockOwner && blockOwnerExpr) {
1739
+ const blockLabel = pickLabel(blockOwner);
1740
+ const blockUid = blockOwner.uid;
1741
+ const blockModelClass = blockOwner.constructor?.name;
1742
+
1743
+ const blockResourceProps: Record<string, FlowContextInfosEnvNode> = {};
1744
+ let hasBlockResourceValues = false;
1745
+ const blockCollectionName = blockResourceSnap.collectionName;
1746
+ if (hasSnapshotValue(blockCollectionName)) {
1747
+ blockResourceProps.collectionName = {
1748
+ description: 'Collection name',
1749
+ getVar: `${blockResourceBaseExpr}.collectionName`,
1750
+ value: blockCollectionName,
1751
+ };
1752
+ hasBlockResourceValues = true;
1753
+ }
1754
+ const blockDataSourceKey = blockResourceSnap.dataSourceKey;
1755
+ if (hasSnapshotValue(blockDataSourceKey)) {
1756
+ blockResourceProps.dataSourceKey = {
1757
+ description: 'Data source key',
1758
+ getVar: `${blockResourceBaseExpr}.dataSourceKey`,
1759
+ value: blockDataSourceKey,
1760
+ };
1761
+ hasBlockResourceValues = true;
1762
+ }
1763
+ const blockAssociationName = blockResourceSnap.associationName;
1764
+ if (hasSnapshotValue(blockAssociationName)) {
1765
+ blockResourceProps.associationName = {
1766
+ description: 'Association name',
1767
+ getVar: `${blockResourceBaseExpr}.associationName`,
1768
+ value: blockAssociationName,
1769
+ };
1770
+ hasBlockResourceValues = true;
1771
+ }
1772
+
1773
+ envs.block = {
1774
+ description: 'Current block information',
1775
+ getVar: 'ctx.blockModel',
1776
+ properties: {
1777
+ ...(hasSnapshotValue(blockLabel) ? { label: { description: 'Block label', value: blockLabel } } : {}),
1778
+ ...(hasSnapshotValue(blockModelClass)
1779
+ ? {
1780
+ modelClass: {
1781
+ description: 'Block model class name',
1782
+ value: blockModelClass,
1783
+ },
1784
+ }
1785
+ : {}),
1786
+ ...(hasSnapshotValue(blockUid)
1787
+ ? {
1788
+ uid: {
1789
+ description: 'Block uid',
1790
+ getVar: 'ctx.blockModel.uid',
1791
+ value: blockUid,
1792
+ },
1793
+ }
1794
+ : {}),
1795
+ ...(hasBlockResourceValues
1796
+ ? {
1797
+ resource: {
1798
+ description: 'Resource information',
1799
+ getVar: 'ctx.blockModel.resource',
1800
+ properties: {
1801
+ ...blockResourceProps,
1802
+ ...(hasSnapshotValue(blockResourceSnap?.filterByTk)
1803
+ ? {
1804
+ filterByTk: {
1805
+ description: 'Record filterByTk',
1806
+ getVar: 'ctx.blockModel.resource.filterByTk',
1807
+ },
1808
+ }
1809
+ : {}),
1810
+ ...(hasSnapshotValue(blockResourceSnap?.sourceId)
1811
+ ? {
1812
+ sourceId: {
1813
+ description: 'Source record ID (sourceId)',
1814
+ getVar: 'ctx.blockModel.resource.sourceId',
1815
+ },
1816
+ }
1817
+ : {}),
1818
+ },
1819
+ },
1820
+ }
1821
+ : {}),
1822
+ },
1823
+ };
1824
+ }
1825
+
1826
+ // Popup (only when popup exists)
1827
+ if (popupLike?.uid) {
1828
+ const popupUid = popupLike.uid;
1829
+ const popupResourceProps: Record<string, FlowContextInfosEnvNode> = {};
1830
+ let hasPopupResourceValues = false;
1831
+ const popupCollectionName = popupResourceSnap.collectionName;
1832
+ if (hasSnapshotValue(popupCollectionName)) {
1833
+ popupResourceProps.collectionName = {
1834
+ description: 'Collection name',
1835
+ getVar: 'ctx.popup.resource.collectionName',
1836
+ value: popupCollectionName,
1837
+ };
1838
+ hasPopupResourceValues = true;
1839
+ }
1840
+ const popupDataSourceKey = popupResourceSnap.dataSourceKey;
1841
+ if (hasSnapshotValue(popupDataSourceKey)) {
1842
+ popupResourceProps.dataSourceKey = {
1843
+ description: 'Data source key',
1844
+ getVar: 'ctx.popup.resource.dataSourceKey',
1845
+ value: popupDataSourceKey,
1846
+ };
1847
+ hasPopupResourceValues = true;
1848
+ }
1849
+ const popupAssociationName = popupResourceSnap.associationName;
1850
+ if (hasSnapshotValue(popupAssociationName)) {
1851
+ popupResourceProps.associationName = {
1852
+ description: 'Association name',
1853
+ getVar: 'ctx.popup.resource.associationName',
1854
+ value: popupAssociationName,
1855
+ };
1856
+ hasPopupResourceValues = true;
1857
+ }
1858
+
1859
+ envs.popup = {
1860
+ description: 'Current popup information',
1861
+ getVar: 'ctx.popup',
1862
+ properties: {
1863
+ uid: { description: 'Popup uid', getVar: 'ctx.popup.uid', value: popupUid },
1864
+ record: {
1865
+ description: 'Current popup record (object).',
1866
+ getVar: 'ctx.popup.record',
1867
+ },
1868
+ sourceRecord: {
1869
+ description: 'Current popup sourceRecord (object).',
1870
+ getVar: 'ctx.popup.sourceRecord',
1871
+ },
1872
+ parent: {
1873
+ description: 'Parent popup info (object).',
1874
+ getVar: 'ctx.popup.parent',
1875
+ },
1876
+ ...(hasPopupResourceValues
1877
+ ? {
1878
+ resource: {
1879
+ description: 'Resource information',
1880
+ getVar: 'ctx.popup.resource',
1881
+ properties: {
1882
+ ...popupResourceProps,
1883
+ ...(hasSnapshotValue(popupResourceSnap?.filterByTk)
1884
+ ? {
1885
+ filterByTk: {
1886
+ description: 'Record filterByTk',
1887
+ getVar: 'ctx.popup.resource.filterByTk',
1888
+ },
1889
+ }
1890
+ : {}),
1891
+ ...(hasSnapshotValue(popupResourceSnap?.sourceId)
1892
+ ? {
1893
+ sourceId: {
1894
+ description: 'Source record ID (sourceId)',
1895
+ getVar: 'ctx.popup.resource.sourceId',
1896
+ },
1897
+ }
1898
+ : {}),
1899
+ },
1900
+ },
1901
+ }
1902
+ : {}),
1903
+ },
1904
+ };
1905
+ }
1906
+
1907
+ // Current view blocks snapshot (page or current popup)
1908
+ const viewUid = (() => {
1909
+ const popupUid = popupLike?.uid;
1910
+ if (hasSnapshotValue(popupUid)) return popupUid.trim();
1911
+ const v = inputArgs?.viewUid;
1912
+ if (hasSnapshotValue(v)) return v.trim();
1913
+ return undefined;
1914
+ })();
1915
+
1916
+ const engine = getMaybe(() => (evalCtx as any).engine) as FlowEngine | undefined;
1917
+ const viewModel = viewUid ? engine?.getModel(viewUid, true) : undefined;
1918
+
1919
+ type ViewTreeNode = {
1920
+ uid: string;
1921
+ subModels?: Record<string, unknown>;
1922
+ context?: { blockModel?: unknown };
1923
+ resource?: unknown;
1924
+ constructor?: { name?: string };
1925
+ };
1926
+
1927
+ const isBlockModelInstance = (m: ViewTreeNode): boolean => m.context?.blockModel === m;
1928
+
1929
+ if (viewModel) {
1930
+ const queue: ViewTreeNode[] = [viewModel as unknown as ViewTreeNode];
1931
+ const blocks: Array<Record<string, JSONValue>> = [];
1932
+
1933
+ for (let i = 0; i < queue.length; i++) {
1934
+ const m = queue[i];
1935
+
1936
+ if (isBlockModelInstance(m)) {
1937
+ const modelClass = m.constructor?.name || m.uid;
1938
+ const label = pickLabel(m) || modelClass || m.uid;
1939
+
1940
+ const resSnap = getResourceSnapshot(m.resource);
1941
+ const resource: Record<string, JSONValue> = {};
1942
+ if (hasSnapshotValue(resSnap.dataSourceKey)) resource.dataSourceKey = resSnap.dataSourceKey;
1943
+ if (hasSnapshotValue(resSnap.collectionName)) resource.collectionName = resSnap.collectionName;
1944
+ if (hasSnapshotValue(resSnap.associationName)) resource.associationName = resSnap.associationName;
1945
+
1946
+ const block: Record<string, JSONValue> = {
1947
+ uid: m.uid,
1948
+ label,
1949
+ modelClass,
1950
+ ...(Object.keys(resource).length > 0 ? { resource } : {}),
1951
+ };
1952
+ blocks.push(block);
1953
+ }
1954
+
1955
+ const subModels = m.subModels;
1956
+ if (subModels && typeof subModels === 'object') {
1957
+ for (const v of Object.values(subModels)) {
1958
+ if (!v) continue;
1959
+ if (Array.isArray(v)) queue.push(...(v as ViewTreeNode[]));
1960
+ else queue.push(v as ViewTreeNode);
1961
+ }
1962
+ }
1963
+ }
1964
+
1965
+ envs.currentViewBlocks = {
1966
+ description: 'Current view blocks',
1967
+ value: blocks,
1968
+ };
1969
+ }
1970
+
1971
+ return envs;
1972
+ };
1973
+
1974
+ const normalizePath = (raw: string): string | undefined => {
1975
+ if (typeof raw !== 'string') return undefined;
1976
+ const s = raw.trim();
1977
+ if (!s) return undefined;
1978
+ const extracted = extractPropertyPath(s);
1979
+ if (Array.isArray(extracted) && extracted.length > 0) {
1980
+ return extracted.join('.');
1981
+ }
1982
+ if (s === 'ctx') return '';
1983
+ if (s.startsWith('ctx.')) return s.slice(4).trim();
1984
+ return s;
1985
+ };
1986
+
1987
+ const paths = (() => {
1988
+ const p = options.path;
1989
+ const list = typeof p === 'string' ? [p] : Array.isArray(p) ? p : [];
1990
+ return list
1991
+ .map((x) => normalizePath(String(x)))
1992
+ .filter((x): x is string => typeof x === 'string' && x.length > 0);
1993
+ })();
1994
+
1995
+ const hasRootPath = (() => {
1996
+ const p = options.path;
1997
+ if (typeof p === 'string') return normalizePath(p) === '';
1998
+ if (Array.isArray(p)) return p.some((x) => normalizePath(String(x)) === '');
1999
+ return false;
2000
+ })();
2001
+
2002
+ const collectKeysDeep = (ctx: FlowContext, out: Set<string>, key: '_props' | '_methods', visited: WeakSet<any>) => {
2003
+ if (!ctx || typeof ctx !== 'object') return;
2004
+ if (visited.has(ctx as any)) return;
2005
+ visited.add(ctx as any);
2006
+ try {
2007
+ const bag = (ctx as any)[key];
2008
+ if (bag && typeof bag === 'object') {
2009
+ for (const k of Object.keys(bag)) out.add(k);
2010
+ }
2011
+ } catch (_) {
2012
+ // ignore
2013
+ }
2014
+ try {
2015
+ const delegates = (ctx as any)._delegates;
2016
+ if (Array.isArray(delegates)) {
2017
+ for (const d of delegates) collectKeysDeep(d, out, key, visited);
2018
+ }
2019
+ } catch (_) {
2020
+ // ignore
2021
+ }
2022
+ };
2023
+
2024
+ const getRunJSDoc = (): any => {
2025
+ const modelClass = getModelClassName(this);
2026
+ const Ctor = RunJSContextRegistry.resolve(version, modelClass) || RunJSContextRegistry.resolve(version, '*');
2027
+ if (!Ctor) return {};
2028
+ const locale = (this as any)?.api?.auth?.locale || (this as any)?.i18n?.language || (this as any)?.locale;
2029
+ try {
2030
+ if ((Ctor as any)?.getDoc?.length) {
2031
+ return (Ctor as any).getDoc(locale) || {};
2032
+ }
2033
+ return (Ctor as any)?.getDoc?.() || {};
2034
+ } catch (_) {
2035
+ return {};
2036
+ }
2037
+ };
2038
+
2039
+ const doc = getRunJSDoc();
2040
+ const docMethods = __isPlainObject(doc?.methods) ? (doc.methods as Record<string, any>) : {};
2041
+ const docProps = __isPlainObject(doc?.properties) ? (doc.properties as Record<string, any>) : {};
2042
+
2043
+ const toDocObject = (node: any): any | undefined => {
2044
+ if (typeof node === 'string') return { description: node };
2045
+ if (__isPlainObject(node)) return node;
2046
+ return undefined;
2047
+ };
2048
+
2049
+ const evalBool = async (raw: any, call: (fn: Function) => any): Promise<boolean | undefined> => {
2050
+ if (typeof raw === 'undefined') return undefined;
2051
+ if (typeof raw === 'boolean') return raw;
2052
+ if (typeof raw === 'function') {
2053
+ try {
2054
+ const v = call(raw);
2055
+ return isPromiseLike(v) ? !!(await v) : !!v;
2056
+ } catch (_) {
2057
+ return false;
2058
+ }
2059
+ }
2060
+ return !!raw;
2061
+ };
2062
+
2063
+ const evalString = async (raw: any, call: (fn: Function) => any): Promise<string | undefined> => {
2064
+ if (typeof raw === 'undefined' || raw === null) return undefined;
2065
+ if (typeof raw === 'string') return raw;
2066
+ if (typeof raw === 'function') {
2067
+ try {
2068
+ const v = call(raw);
2069
+ const resolved = isPromiseLike(v) ? await v : v;
2070
+ if (typeof resolved === 'string') return resolved;
2071
+ return typeof resolved === 'undefined' || resolved === null ? undefined : String(resolved);
2072
+ } catch (_) {
2073
+ return undefined;
2074
+ }
2075
+ }
2076
+ return String(raw);
2077
+ };
2078
+
2079
+ const evalRunJSHidden = async (
2080
+ raw: any,
2081
+ ): Promise<{
2082
+ hideSelf: boolean;
2083
+ hideSubpaths: string[];
2084
+ }> => {
2085
+ let hideSelf = false;
2086
+ let list: any = [];
2087
+ try {
2088
+ if (typeof raw === 'boolean') hideSelf = raw;
2089
+ else if (Array.isArray(raw)) list = raw;
2090
+ else if (typeof raw === 'function') {
2091
+ const v = raw(evalCtx);
2092
+ const resolved = isPromiseLike(v) ? await v : v;
2093
+ if (typeof resolved === 'boolean') hideSelf = resolved;
2094
+ else if (Array.isArray(resolved)) list = resolved;
2095
+ }
2096
+ } catch (_) {
2097
+ hideSelf = false;
2098
+ list = [];
2099
+ }
2100
+
2101
+ const hideSubpaths: string[] = [];
2102
+ if (Array.isArray(list)) {
2103
+ for (const p of list) {
2104
+ if (typeof p !== 'string') continue;
2105
+ const s = p.trim();
2106
+ if (!s) continue;
2107
+ // Only relative paths are supported. Ignore "ctx.xxx" absolute style to avoid ambiguity.
2108
+ if (s === 'ctx' || s.startsWith('ctx.')) continue;
2109
+ if (/\s/.test(s)) continue;
2110
+ hideSubpaths.push(s);
2111
+ }
2112
+ }
2113
+ return { hideSelf: !!hideSelf, hideSubpaths };
2114
+ };
2115
+
2116
+ const isHiddenByPrefixes = (path: string, hiddenPrefixes: Set<string>) => {
2117
+ if (!path) return false;
2118
+ const parts = path.split('.').filter(Boolean);
2119
+ while (parts.length) {
2120
+ if (hiddenPrefixes.has(parts.join('.'))) return true;
2121
+ parts.pop();
2122
+ }
2123
+ return false;
2124
+ };
2125
+
2126
+ const pickMethodInfo = (obj: any): Partial<FlowContextApiInfo> => {
2127
+ const src = toDocObject(obj);
2128
+ if (!src) return {};
2129
+ const out: any = {};
2130
+ for (const k of ['description', 'examples', 'completion', 'ref', 'params', 'returns']) {
2131
+ const v = (src as any)[k];
2132
+ if (typeof v !== 'undefined') out[k] = v;
2133
+ }
2134
+ if (Array.isArray(out.examples)) {
2135
+ out.examples = out.examples.filter((x: any) => typeof x === 'string' && x.trim());
2136
+ }
2137
+ return out;
2138
+ };
2139
+
2140
+ const pickPropertyInfo = (obj: any): Partial<FlowContextApiInfo> => {
2141
+ const src = toDocObject(obj);
2142
+ if (!src) return {};
2143
+ const out: any = {};
2144
+ for (const k of [
2145
+ 'title',
2146
+ 'type',
2147
+ 'interface',
2148
+ 'description',
2149
+ 'examples',
2150
+ 'completion',
2151
+ 'ref',
2152
+ 'params',
2153
+ 'returns',
2154
+ ]) {
2155
+ const v = (src as any)[k];
2156
+ if (typeof v !== 'undefined') out[k] = v;
2157
+ }
2158
+ if (Array.isArray(out.examples)) {
2159
+ out.examples = out.examples.filter((x: any) => typeof x === 'string' && x.trim());
2160
+ }
2161
+ return out;
2162
+ };
2163
+
2164
+ const getMethodInfoFromChain = (name: string): FlowContextMethodInfoInput | undefined => {
2165
+ const visited = new WeakSet<any>();
2166
+ const walk = (ctx: FlowContext): FlowContextMethodInfoInput | undefined => {
2167
+ if (!ctx || typeof ctx !== 'object') return undefined;
2168
+ if (visited.has(ctx as any)) return undefined;
2169
+ visited.add(ctx as any);
2170
+ if (Object.prototype.hasOwnProperty.call((ctx as any)._methodInfos || {}, name)) {
2171
+ return (ctx as any)._methodInfos?.[name] as FlowContextMethodInfoInput;
2172
+ }
2173
+ const delegates = (ctx as any)._delegates;
2174
+ if (Array.isArray(delegates)) {
2175
+ for (const d of delegates) {
2176
+ const found = walk(d);
2177
+ if (found) return found;
2178
+ }
2179
+ }
2180
+ return undefined;
2181
+ };
2182
+ return walk(this);
2183
+ };
2184
+
2185
+ const hasMethodInChain = (name: string): boolean => {
2186
+ const visited = new WeakSet<any>();
2187
+ const walk = (ctx: FlowContext): boolean => {
2188
+ if (!ctx || typeof ctx !== 'object') return false;
2189
+ if (visited.has(ctx as any)) return false;
2190
+ visited.add(ctx as any);
2191
+ if (Object.prototype.hasOwnProperty.call((ctx as any)._methods || {}, name)) return true;
2192
+ const delegates = (ctx as any)._delegates;
2193
+ if (Array.isArray(delegates)) {
2194
+ for (const d of delegates) {
2195
+ if (walk(d)) return true;
2196
+ }
2197
+ }
2198
+ return false;
2199
+ };
2200
+ return walk(this);
2201
+ };
2202
+
2203
+ const buildMethodInfo = async (name: string): Promise<FlowContextApiInfo | undefined> => {
2204
+ if (isPrivateKey(name)) return undefined;
2205
+ const docNode = docMethods[name];
2206
+ const info = getMethodInfoFromChain(name);
2207
+ const exists = typeof docNode !== 'undefined' || typeof info !== 'undefined' || hasMethodInChain(name);
2208
+ if (!exists) return undefined;
2209
+
2210
+ const docObj = toDocObject(docNode);
2211
+ const docHidden = await evalBool((docObj as any)?.hidden, (fn) => fn(evalCtx));
2212
+ const infoHidden = await evalBool(info?.hidden, (fn) => fn(evalCtx));
2213
+ if (!!docHidden || !!infoHidden) return undefined;
2214
+
2215
+ const docDisabled = await evalBool((docObj as any)?.disabled, (fn) => fn(evalCtx));
2216
+ const docDisabledReason = await evalString((docObj as any)?.disabledReason, (fn) => fn(evalCtx));
2217
+ const infoDisabled = await evalBool(info?.disabled, (fn) => fn(evalCtx));
2218
+ const infoDisabledReason = await evalString(info?.disabledReason, (fn) => fn(evalCtx));
2219
+ const disabled = typeof infoDisabled !== 'undefined' ? infoDisabled : docDisabled;
2220
+ const disabledReason = typeof infoDisabledReason !== 'undefined' ? infoDisabledReason : docDisabledReason;
2221
+
2222
+ let out: FlowContextApiInfo = {};
2223
+ out = { ...out, ...pickMethodInfo(docObj) };
2224
+ out = { ...out, ...pickMethodInfo(info) };
2225
+ if (typeof disabled !== 'undefined') out.disabled = !!disabled;
2226
+ if (typeof disabledReason !== 'undefined') out.disabledReason = disabledReason;
2227
+ if (!Object.keys(out).length) return undefined;
2228
+ // Mark as callable for tooling (e.g. code-editor completion).
2229
+ out.type = 'function';
2230
+ return out;
2231
+ };
2232
+
2233
+ const buildPropertyInfoFromNodes = async (args: {
2234
+ docNode?: any;
2235
+ metaNode?: PropertyMetaOrFactory;
2236
+ infoNode?: FlowContextPropertyInfoInput;
2237
+ depth: number;
2238
+ pathFromRoot: string[];
2239
+ hiddenPrefixes: Set<string>;
2240
+ }): Promise<FlowContextApiInfo | undefined> => {
2241
+ const { docNode, metaNode, infoNode, depth, pathFromRoot, hiddenPrefixes } = args;
2242
+ const relPath = pathFromRoot.join('.');
2243
+ if (isHiddenByPrefixes(relPath, hiddenPrefixes)) return undefined;
2244
+
2245
+ const docObj = toDocObject(docNode);
2246
+ const infoObj = toDocObject(infoNode);
2247
+
2248
+ const docHiddenDecision = await evalRunJSHidden((docObj as any)?.hidden);
2249
+ if (docHiddenDecision.hideSelf) return undefined;
2250
+ const infoHiddenDecision = await evalRunJSHidden((infoObj as any)?.hidden);
2251
+ if (infoHiddenDecision.hideSelf) return undefined;
2252
+
2253
+ const resolvedMetaNode = await resolveMetaOrFactory(metaNode);
2254
+ const metaHidden = await evalBool(resolvedMetaNode?.hidden, (fn) => (fn as any).call(resolvedMetaNode, evalCtx));
2255
+ if (metaHidden) return undefined;
2256
+
2257
+ const childHiddenPrefixes = new Set(hiddenPrefixes);
2258
+ for (const sub of [...docHiddenDecision.hideSubpaths, ...infoHiddenDecision.hideSubpaths]) {
2259
+ const normalized = sub.trim();
2260
+ if (!normalized) continue;
2261
+ const stripped = normalized === 'ctx' ? '' : normalized.startsWith('ctx.') ? normalized.slice(4) : normalized;
2262
+ if (!stripped) continue;
2263
+ const abs = relPath ? `${relPath}.${stripped}` : stripped;
2264
+ childHiddenPrefixes.add(abs);
2265
+ }
2266
+
2267
+ const docDisabled = await evalBool((docObj as any)?.disabled, (fn) => fn(evalCtx));
2268
+ const docDisabledReason = await evalString((docObj as any)?.disabledReason, (fn) => fn(evalCtx));
2269
+ const metaDisabled = await evalBool(resolvedMetaNode?.disabled, (fn) =>
2270
+ (fn as any).call(resolvedMetaNode, evalCtx),
2271
+ );
2272
+ const metaDisabledReason = await evalString(resolvedMetaNode?.disabledReason, (fn) =>
2273
+ (fn as any).call(resolvedMetaNode, evalCtx),
2274
+ );
2275
+ const infoDisabled = await evalBool((infoObj as any)?.disabled, (fn) => fn(evalCtx));
2276
+ const infoDisabledReason = await evalString((infoObj as any)?.disabledReason, (fn) => fn(evalCtx));
2277
+ const disabled =
2278
+ typeof infoDisabled !== 'undefined'
2279
+ ? infoDisabled
2280
+ : typeof metaDisabled !== 'undefined'
2281
+ ? metaDisabled
2282
+ : docDisabled;
2283
+ const disabledReason =
2284
+ typeof infoDisabledReason !== 'undefined'
2285
+ ? infoDisabledReason
2286
+ : typeof metaDisabledReason !== 'undefined'
2287
+ ? metaDisabledReason
2288
+ : docDisabledReason;
2289
+
2290
+ let out: FlowContextApiInfo = {};
2291
+ out = { ...out, ...pickPropertyInfo(docObj) };
2292
+ out = { ...out, ...pickPropertyInfo(resolvedMetaNode) };
2293
+ out = { ...out, ...pickPropertyInfo(infoObj) };
2294
+ if (typeof disabled !== 'undefined') out.disabled = !!disabled;
2295
+ if (typeof disabledReason !== 'undefined') out.disabledReason = disabledReason;
2296
+
2297
+ if (depth >= maxDepth) return Object.keys(out).length ? out : undefined;
2298
+
2299
+ const docChildren = __isPlainObject((docObj as any)?.properties)
2300
+ ? ((docObj as any).properties as any as Record<string, any>)
2301
+ : undefined;
2302
+
2303
+ let metaChildren: Record<string, PropertyMetaOrFactory> | undefined;
2304
+ if (resolvedMetaNode?.properties) {
2305
+ try {
2306
+ const props = resolvedMetaNode.properties;
2307
+ if (typeof props === 'function') {
2308
+ const resolved = await (props as any).call(resolvedMetaNode, evalCtx);
2309
+ resolvedMetaNode.properties = resolved;
2310
+ metaChildren = resolved as Record<string, PropertyMetaOrFactory>;
2311
+ } else if (__isPlainObject(props)) {
2312
+ metaChildren = props as Record<string, PropertyMetaOrFactory>;
2313
+ }
2314
+ } catch (_) {
2315
+ metaChildren = undefined;
2316
+ }
2317
+ }
2318
+
2319
+ let infoChildren: Record<string, FlowContextPropertyInfoInput> | undefined;
2320
+ if (__isPlainObject(infoObj) && (infoObj as any)?.properties) {
2321
+ try {
2322
+ const props = (infoObj as any).properties;
2323
+ if (typeof props === 'function') {
2324
+ const resolved = await (props as any).call(infoObj, evalCtx);
2325
+ (infoObj as any).properties = resolved;
2326
+ infoChildren = resolved as Record<string, FlowContextPropertyInfoInput>;
2327
+ } else if (__isPlainObject(props)) {
2328
+ infoChildren = props as Record<string, FlowContextPropertyInfoInput>;
2329
+ }
2330
+ } catch (_) {
2331
+ infoChildren = undefined;
2332
+ }
2333
+ }
2334
+
2335
+ const keys = new Set<string>();
2336
+ if (docChildren) for (const k of Object.keys(docChildren)) keys.add(k);
2337
+ if (metaChildren) for (const k of Object.keys(metaChildren)) keys.add(k);
2338
+ if (infoChildren) for (const k of Object.keys(infoChildren)) keys.add(k);
2339
+ if (!keys.size) return Object.keys(out).length ? out : undefined;
2340
+
2341
+ const childrenOut: Record<string, FlowContextApiInfo> = {};
2342
+ for (const k of keys) {
2343
+ if (isPrivateKey(k)) continue;
2344
+ const child = await buildPropertyInfoFromNodes({
2345
+ docNode: docChildren?.[k],
2346
+ metaNode: metaChildren?.[k],
2347
+ infoNode: infoChildren?.[k],
2348
+ depth: depth + 1,
2349
+ pathFromRoot: [...pathFromRoot, k],
2350
+ hiddenPrefixes: childHiddenPrefixes,
2351
+ });
2352
+ if (child) childrenOut[k] = child;
2353
+ }
2354
+ if (Object.keys(childrenOut).length) out.properties = childrenOut;
2355
+ if (!Object.keys(out).length) return undefined;
2356
+ return out;
2357
+ };
2358
+
2359
+ const resolvePropertyMetaAtPath = async (segments: string[]): Promise<PropertyMetaOrFactory | undefined> => {
2360
+ if (!segments.length) return undefined;
2361
+ const [first, ...rest] = segments;
2362
+ const opt = this.getPropertyOptions(first);
2363
+ if (!opt?.meta) return undefined;
2364
+ try {
2365
+ // Fast path: when querying the root key only, return the meta (may be a factory) and let
2366
+ // buildPropertyInfoFromNodes decide whether to resolve it based on maxDepth.
2367
+ if (!rest.length) return opt.meta as PropertyMetaOrFactory;
2368
+
2369
+ let current = await resolveMetaOrFactory(opt.meta as PropertyMetaOrFactory);
2370
+ if (!current) return undefined;
2371
+
2372
+ for (let i = 0; i < rest.length; i++) {
2373
+ const key = rest[i];
2374
+
2375
+ let props: any = (current as any)?.properties;
2376
+ if (!props) return undefined;
2377
+ if (typeof props === 'function') {
2378
+ const resolved = await props.call(current, evalCtx);
2379
+ (current as any).properties = resolved;
2380
+ props = resolved;
2381
+ }
2382
+ if (!props || typeof props !== 'object') return undefined;
2383
+
2384
+ const next = (props as any)?.[key] as PropertyMetaOrFactory | undefined;
2385
+ if (!next) return undefined;
2386
+
2387
+ // Return the node at the requested path (may still be a factory).
2388
+ if (i === rest.length - 1) return next;
2389
+
2390
+ const resolvedNext = await resolveMetaOrFactory(next);
2391
+ if (!resolvedNext) return undefined;
2392
+ current = resolvedNext;
2393
+ }
2394
+
2395
+ return undefined;
2396
+ } catch (_) {
2397
+ return undefined;
2398
+ }
2399
+ };
2400
+
2401
+ const resolvePropertyInfoAtPath = async (segments: string[]): Promise<FlowContextPropertyInfoInput | undefined> => {
2402
+ if (!segments.length) return undefined;
2403
+ const [first, ...rest] = segments;
2404
+ const opt = this.getPropertyOptions(first);
2405
+ if (!opt?.info) return undefined;
2406
+
2407
+ try {
2408
+ let cur: any = typeof opt.info === 'function' ? await (opt.info as any).call(evalCtx, evalCtx) : opt.info;
2409
+ if (!rest.length) return cur as FlowContextPropertyInfoInput;
2410
+
2411
+ for (const key of rest) {
2412
+ const obj = toDocObject(cur);
2413
+ if (!__isPlainObject(obj)) return undefined;
2414
+ let props: any = (obj as any)?.properties;
2415
+ if (!props) return undefined;
2416
+ if (typeof props === 'function') {
2417
+ const resolved = await props.call(obj, evalCtx);
2418
+ (obj as any).properties = resolved;
2419
+ props = resolved;
2420
+ }
2421
+ if (!__isPlainObject(props)) return undefined;
2422
+ cur = (props as any)[key];
2423
+ }
2424
+
2425
+ return cur as FlowContextPropertyInfoInput;
2426
+ } catch (_) {
2427
+ return undefined;
2428
+ }
2429
+ };
2430
+
2431
+ const resolveDocNodeAtPath = (segments: string[]): any => {
2432
+ if (!segments.length) return undefined;
2433
+ let cur: any = docProps[segments[0]];
2434
+ for (let i = 1; i < segments.length; i++) {
2435
+ const obj = toDocObject(cur);
2436
+ if (!__isPlainObject(obj)) return undefined;
2437
+ const props = (obj as any).properties;
2438
+ if (!__isPlainObject(props)) return undefined;
2439
+ cur = (props as any)[segments[i]];
2440
+ }
2441
+ return cur;
2442
+ };
2443
+
2444
+ // path 剪裁:每个 path 独立返回一个根节点(key 为 path 字符串)
2445
+ if (!hasRootPath && paths.length) {
2446
+ const out: Record<string, FlowContextApiInfo> = {};
2447
+
2448
+ for (const p of paths) {
2449
+ const segments = p
2450
+ .split('.')
2451
+ .map((x) => x.trim())
2452
+ .filter(Boolean);
2453
+ if (segments.some((s) => isPrivateKey(s))) continue;
2454
+ if (!segments.length) continue;
2455
+
2456
+ const metaNode = await resolvePropertyMetaAtPath(segments);
2457
+ const pi = await buildPropertyInfoFromNodes({
2458
+ docNode: undefined,
2459
+ metaNode,
2460
+ infoNode: undefined,
2461
+ depth: 1,
2462
+ pathFromRoot: [],
2463
+ hiddenPrefixes: new Set(),
2464
+ });
2465
+ if (pi) out[p] = pi;
2466
+ }
2467
+
2468
+ return out;
2469
+ }
2470
+
2471
+ // 全量输出:仅基于 property meta(含委托链)
2472
+ const metaMap = this._getPropertiesMeta();
2473
+ const out: Record<string, FlowContextApiInfo> = {};
2474
+ for (const [key, metaNode] of Object.entries(metaMap)) {
2475
+ if (isPrivateKey(key)) continue;
2476
+ const pi = await buildPropertyInfoFromNodes({
2477
+ docNode: undefined,
2478
+ metaNode,
2479
+ infoNode: undefined,
2480
+ depth: 1,
2481
+ pathFromRoot: [key],
2482
+ hiddenPrefixes: new Set(),
2483
+ });
2484
+ if (pi) out[key] = pi;
2485
+ }
2486
+
2487
+ return out;
2488
+ }
2489
+
424
2490
  #createChildNodes(
425
2491
  properties: Record<string, PropertyMeta> | (() => Promise<Record<string, PropertyMeta>>),
426
2492
  parentPaths: string[] = [],
@@ -666,9 +2732,16 @@ export class FlowContext {
666
2732
  const computeStateFromMeta = (m: PropertyMeta): { disabled: boolean; reason?: string; hidden: boolean } => {
667
2733
  if (!m) return { disabled: false, hidden: false };
668
2734
  const disabledVal = typeof m.disabled === 'function' ? m.disabled() : m.disabled;
669
- const reason = typeof m.disabledReason === 'function' ? m.disabledReason() : m.disabledReason;
2735
+ const reasonVal = typeof m.disabledReason === 'function' ? m.disabledReason() : m.disabledReason;
670
2736
  const hiddenVal = typeof m.hidden === 'function' ? m.hidden() : m.hidden;
671
- return { disabled: !!disabledVal, reason, hidden: !!hiddenVal };
2737
+ const disabledIsPromise = disabledVal && typeof (disabledVal as any).then === 'function';
2738
+ const reasonIsPromise = reasonVal && typeof (reasonVal as any).then === 'function';
2739
+ const hiddenIsPromise = hiddenVal && typeof (hiddenVal as any).then === 'function';
2740
+ // getPropertyMetaTree 为同步 API:遇到 Promise 时 fail-open(不隐藏/不禁用)
2741
+ const disabled = disabledIsPromise ? false : !!disabledVal;
2742
+ const reason = reasonIsPromise ? undefined : (reasonVal as any);
2743
+ const hidden = hiddenIsPromise ? false : !!hiddenVal;
2744
+ return { disabled, reason, hidden };
672
2745
  };
673
2746
 
674
2747
  if (typeof metaOrFactory === 'function') {
@@ -679,6 +2752,7 @@ export class FlowContext {
679
2752
  title: metaOrFactory.title || initialTitle, // 初始使用 name 作为 title
680
2753
  type: 'object', // 初始类型
681
2754
  interface: undefined,
2755
+ options: undefined,
682
2756
  uiSchema: undefined,
683
2757
  paths,
684
2758
  parentTitles: parentTitles.length > 0 ? parentTitles : undefined,
@@ -710,6 +2784,7 @@ export class FlowContext {
710
2784
  node.title = finalTitle;
711
2785
  node.type = meta?.type;
712
2786
  node.interface = meta?.interface;
2787
+ node.options = meta?.options;
713
2788
  node.uiSchema = meta?.uiSchema;
714
2789
  // parentTitles 保持不变,因为它不包含自身 title
715
2790
 
@@ -742,6 +2817,7 @@ export class FlowContext {
742
2817
  title: nodeTitle,
743
2818
  type: metaOrFactory.type,
744
2819
  interface: metaOrFactory.interface,
2820
+ options: metaOrFactory.options,
745
2821
  uiSchema: metaOrFactory.uiSchema,
746
2822
  paths,
747
2823
  parentTitles: parentTitles.length > 0 ? parentTitles : undefined,
@@ -908,13 +2984,15 @@ class BaseFlowEngineContext extends FlowContext {
908
2984
  declare dataSourceManager: DataSourceManager;
909
2985
  declare requireAsync: (url: string) => Promise<any>;
910
2986
  declare importAsync: (url: string) => Promise<any>;
911
- declare createJSRunner: (options?: JSRunnerOptions) => JSRunner;
2987
+ declare createJSRunner: (options?: JSRunnerOptions) => Promise<JSRunner>;
912
2988
  declare pageInfo: { version?: 'v1' | 'v2' };
913
2989
  /**
914
2990
  * @deprecated use `resolveJsonTemplate` instead
915
2991
  */
916
2992
  declare renderJson: (template: JSONValue) => Promise<any>;
917
2993
  declare resolveJsonTemplate: (template: JSONValue) => Promise<any>;
2994
+ declare getVar: (path: string) => Promise<any>;
2995
+ declare request: (options: RequestOptions) => Promise<any>;
918
2996
  declare runjs: (code: string, variables?: Record<string, any>, options?: JSRunnerOptions) => Promise<any>;
919
2997
  declare getAction: <TModel extends FlowModel = FlowModel, TCtx extends FlowContext = FlowContext>(
920
2998
  name: string,
@@ -939,6 +3017,33 @@ class BaseFlowEngineContext extends FlowContext {
939
3017
  declare location: Location;
940
3018
  declare sql: FlowSQLRepository;
941
3019
  declare logger: pino.Logger;
3020
+
3021
+ constructor() {
3022
+ super();
3023
+ this.defineMethod('getModel', (modelName: string, searchInPreviousEngines?: boolean) => {
3024
+ return this.engine.getModel(modelName, searchInPreviousEngines);
3025
+ });
3026
+ this.defineMethod('request', (options: RequestOptions) => {
3027
+ return this.api.request(options);
3028
+ });
3029
+ this.defineMethod(
3030
+ 'runjs',
3031
+ async function (code: string, variables?: Record<string, any>, options?: JSRunnerOptions) {
3032
+ const { preprocessTemplates, ...runnerOptions } = options || {};
3033
+ const mergedGlobals = { ...(runnerOptions?.globals || {}), ...(variables || {}) };
3034
+ const runner = await this.createJSRunner({
3035
+ ...(runnerOptions || {}),
3036
+ globals: mergedGlobals,
3037
+ });
3038
+ const shouldPreprocessTemplates = shouldPreprocessRunJSTemplates({
3039
+ version: runnerOptions?.version,
3040
+ preprocessTemplates,
3041
+ });
3042
+ const jsCode = await prepareRunJsCode(String(code ?? ''), { preprocessTemplates: shouldPreprocessTemplates });
3043
+ return runner.run(jsCode);
3044
+ },
3045
+ );
3046
+ }
942
3047
  }
943
3048
 
944
3049
  class BaseFlowModelContext extends BaseFlowEngineContext {
@@ -956,7 +3061,16 @@ class BaseFlowModelContext extends BaseFlowEngineContext {
956
3061
  EventDefinition<TModel, TCtx>
957
3062
  >;
958
3063
  declare runAction: (actionName: string, params?: Record<string, any>) => Promise<any> | any;
3064
+ /**
3065
+ * @deprecated use `makeResource` instead
3066
+ */
959
3067
  declare createResource: <T extends FlowResource = FlowResource>(resourceType: ResourceType<T>) => T;
3068
+ /**
3069
+ * Create a new resource instance without adding it to the context.
3070
+ * @param resourceType - The resource type.
3071
+ * @returns The resource instance.
3072
+ */
3073
+ declare makeResource: <T extends FlowResource = FlowResource>(resourceType: ResourceType<T>) => T;
960
3074
  }
961
3075
 
962
3076
  export class FlowEngineContext extends BaseFlowEngineContext {
@@ -976,25 +3090,30 @@ export class FlowEngineContext extends BaseFlowEngineContext {
976
3090
  dataSourceManager.addDataSource(mainDataSource);
977
3091
  this.defineProperty('engine', {
978
3092
  value: this.engine,
3093
+ info: {
3094
+ description: 'FlowEngine instance.',
3095
+ detail: 'FlowEngine',
3096
+ },
979
3097
  });
980
3098
  this.defineProperty('sql', {
981
- get: () => new FlowSQLRepository(this),
3099
+ get: (ctx) => new FlowSQLRepository(ctx),
3100
+ cache: false,
3101
+ info: {
3102
+ description: 'SQL helper (FlowSQLRepository).',
3103
+ detail: 'FlowSQLRepository',
3104
+ },
982
3105
  });
983
3106
  this.defineProperty('dataSourceManager', {
984
3107
  value: dataSourceManager,
3108
+ info: {
3109
+ description: 'DataSourceManager instance.',
3110
+ detail: 'DataSourceManager',
3111
+ },
985
3112
  });
986
3113
  const i18n = new FlowI18n(this);
987
3114
  this.defineMethod('t', (keyOrTemplate: string, options?: any) => {
988
3115
  return i18n.translate(keyOrTemplate, options);
989
3116
  });
990
- this.defineMethod('runjs', async (code, variables, options?: JSRunnerOptions) => {
991
- const mergedGlobals = { ...(options?.globals || {}), ...(variables || {}) };
992
- const runner = (await (this as any).createJSRunner({
993
- ...(options || {}),
994
- globals: mergedGlobals,
995
- })) as JSRunner;
996
- return runner.run(code);
997
- });
998
3117
  this.defineMethod('renderJson', function (template: any) {
999
3118
  return this.resolveJsonTemplate(template);
1000
3119
  });
@@ -1030,6 +3149,22 @@ export class FlowEngineContext extends BaseFlowEngineContext {
1030
3149
  const needServer = Object.keys(serverVarPaths).length > 0;
1031
3150
  let serverResolved = template;
1032
3151
  if (needServer) {
3152
+ const inferRecordRefWithMeta = (ctx: any): RecordRef | undefined => {
3153
+ const ref = inferRecordRef(ctx as any);
3154
+ if (ref) return ref as RecordRef;
3155
+ try {
3156
+ const tk = ctx?.resource?.getMeta?.('currentFilterByTk');
3157
+ if (typeof tk === 'undefined' || tk === null) return undefined;
3158
+ const collection =
3159
+ ctx?.collection?.name || ctx?.resource?.getResourceName?.()?.split?.('.')?.slice?.(-1)?.[0];
3160
+ if (!collection) return undefined;
3161
+ const dataSourceKey = ctx?.collection?.dataSourceKey || ctx?.resource?.getDataSourceKey?.();
3162
+ return { collection, dataSourceKey, filterByTk: tk } as RecordRef;
3163
+ } catch (_) {
3164
+ return undefined;
3165
+ }
3166
+ };
3167
+
1033
3168
  const collectFromMeta = async (): Promise<Record<string, any>> => {
1034
3169
  const out: Record<string, any> = {};
1035
3170
  try {
@@ -1069,7 +3204,62 @@ export class FlowEngineContext extends BaseFlowEngineContext {
1069
3204
  };
1070
3205
 
1071
3206
  const inputFromMeta = await collectFromMeta();
1072
- const autoInput = { ...inputFromMeta };
3207
+ const autoInput = { ...inputFromMeta } as Record<string, any>;
3208
+
3209
+ // Special-case: formValues
3210
+ // If server needs to resolve some formValues paths but meta params only cover association anchors
3211
+ // (e.g. formValues.customer) and some top-level paths are missing (e.g. formValues.status),
3212
+ // inject a top-level record anchor (formValues -> { collection, filterByTk, fields/appends }) so server can fetch DB values.
3213
+ // This anchor MUST be selective (fields/appends derived from serverVarPaths['formValues']) to avoid server overriding
3214
+ // client-only values for configured form fields in the same template.
3215
+ try {
3216
+ const varName = 'formValues';
3217
+ const neededPaths = serverVarPaths[varName] || [];
3218
+ if (neededPaths.length) {
3219
+ const requiredTop = new Set<string>();
3220
+ for (const p of neededPaths) {
3221
+ const top = topLevelOf(p);
3222
+ if (top) requiredTop.add(top);
3223
+ }
3224
+ const metaOut = inputFromMeta?.[varName];
3225
+ const builtTop = new Set<string>();
3226
+ if (metaOut && typeof metaOut === 'object' && !Array.isArray(metaOut) && !isRecordRefLike(metaOut)) {
3227
+ Object.keys(metaOut).forEach((k) => builtTop.add(k));
3228
+ }
3229
+
3230
+ const missing = [...requiredTop].filter((k) => !builtTop.has(k));
3231
+ if (missing.length) {
3232
+ const ref = inferRecordRefWithMeta(this);
3233
+ if (ref) {
3234
+ const { generatedFields, generatedAppends } = inferSelectsFromUsage(neededPaths);
3235
+ const recordRef: RecordRef = {
3236
+ ...ref,
3237
+ fields: generatedFields,
3238
+ appends: generatedAppends,
3239
+ };
3240
+
3241
+ // Preserve existing association anchors by lifting them to dotted keys before overwriting formValues
3242
+ const existing = autoInput[varName];
3243
+ if (
3244
+ existing &&
3245
+ typeof existing === 'object' &&
3246
+ !Array.isArray(existing) &&
3247
+ !isRecordRefLike(existing)
3248
+ ) {
3249
+ for (const [k, v] of Object.entries(existing)) {
3250
+ autoInput[`${varName}.${k}`] = v;
3251
+ }
3252
+ delete autoInput[varName];
3253
+ }
3254
+
3255
+ autoInput[varName] = recordRef;
3256
+ }
3257
+ }
3258
+ }
3259
+ } catch (_) {
3260
+ // ignore
3261
+ }
3262
+
1073
3263
  const autoContextParams = Object.keys(autoInput).length
1074
3264
  ? _buildServerContextParams(this, autoInput)
1075
3265
  : undefined;
@@ -1100,6 +3290,23 @@ export class FlowEngineContext extends BaseFlowEngineContext {
1100
3290
 
1101
3291
  return resolveExpressions(serverResolved, this);
1102
3292
  });
3293
+
3294
+ // Helper: resolve a single ctx expression value via resolveJsonTemplate behavior.
3295
+ // Example: await ctx.getVar('ctx.record.id')
3296
+ this.defineMethod(
3297
+ 'getVar',
3298
+ async function (this: BaseFlowEngineContext, varPath: string) {
3299
+ const raw = typeof varPath === 'string' ? varPath : String(varPath ?? '');
3300
+ const s = raw.trim();
3301
+ if (!s) return undefined;
3302
+ // Preferred input: 'ctx.xxx.yyy' (expression), consistent with envs.getVar outputs.
3303
+ if (s !== 'ctx' && !s.startsWith('ctx.')) {
3304
+ throw new Error(`ctx.getVar(path) expects an expression starting with "ctx.", got: "${s}"`);
3305
+ }
3306
+ return this.resolveJsonTemplate(`{{ ${s} }}` as any);
3307
+ },
3308
+ 'Resolve a ctx expression value by path (expression starts with "ctx.").',
3309
+ );
1103
3310
  this.defineProperty('requirejs', {
1104
3311
  get: () => this.app?.requirejs?.requirejs,
1105
3312
  });
@@ -1181,71 +3388,79 @@ export class FlowEngineContext extends BaseFlowEngineContext {
1181
3388
  user: this.user,
1182
3389
  }),
1183
3390
  });
1184
- this.defineMethod('loadCSS', async (url: string) => {
1185
- return new Promise((resolve, reject) => {
1186
- // Check if CSS is already loaded
1187
- const existingLink = document.querySelector(`link[href="${url}"]`);
1188
- if (existingLink) {
1189
- resolve(null);
1190
- return;
1191
- }
3391
+ this.defineProperty('date', {
3392
+ get: () => {
3393
+ const createBranch = (prefix: string[]) => {
3394
+ return new Proxy(
3395
+ {},
3396
+ {
3397
+ get: (_target, prop) => {
3398
+ if (typeof prop !== 'string') return undefined;
3399
+ const nextPath = [...prefix, prop];
3400
+ if (!isCtxDatePathPrefix(nextPath)) {
3401
+ return undefined;
3402
+ }
3403
+ const resolved = resolveCtxDatePath(nextPath);
3404
+ if (typeof resolved !== 'undefined') {
3405
+ return resolved;
3406
+ }
3407
+ return createBranch(nextPath);
3408
+ },
3409
+ },
3410
+ );
3411
+ };
1192
3412
 
1193
- const link = document.createElement('link');
1194
- link.rel = 'stylesheet';
1195
- link.href = url;
1196
- link.onload = () => resolve(null);
1197
- link.onerror = () => reject(new Error(`Failed to load CSS: ${url}`));
1198
- document.head.appendChild(link);
1199
- });
3413
+ return createBranch(['date']);
3414
+ },
3415
+ cache: false,
1200
3416
  });
3417
+ this.defineMethod(
3418
+ 'loadCSS',
3419
+ async (href: string) => {
3420
+ const url = resolveModuleUrl(href);
3421
+ return new Promise((resolve, reject) => {
3422
+ // Check if CSS is already loaded
3423
+ const existingLink = document.querySelector(`link[href="${url}"]`);
3424
+ if (existingLink) {
3425
+ resolve(null);
3426
+ return;
3427
+ }
3428
+
3429
+ const link = document.createElement('link');
3430
+ link.rel = 'stylesheet';
3431
+ link.href = url;
3432
+ link.onload = () => resolve(null);
3433
+ link.onerror = () => reject(new Error(`Failed to load CSS: ${url}`));
3434
+ document.head.appendChild(link);
3435
+ });
3436
+ },
3437
+ {
3438
+ description: 'Load a CSS file by URL (browser only).',
3439
+ params: [{ name: 'href', type: 'string', description: 'CSS URL.' }],
3440
+ returns: { type: 'Promise<void>' },
3441
+ completion: { insertText: "await ctx.loadCSS('https://example.com/style.css')" },
3442
+ examples: ["await ctx.loadCSS('https://example.com/style.css');"],
3443
+ },
3444
+ );
1201
3445
  this.defineMethod('requireAsync', async (url: string) => {
1202
- return new Promise((resolve, reject) => {
1203
- if (!this.requirejs) {
1204
- reject(new Error('requirejs is not available'));
1205
- return;
1206
- }
1207
- this.requirejs(
1208
- [url],
1209
- (...args: any[]) => {
1210
- resolve(args[0]);
1211
- },
1212
- reject,
1213
- );
1214
- });
3446
+ // 判断是否为 CSS 文件(支持 example.css?v=123 等形式)
3447
+ if (isCssFile(url)) {
3448
+ return this.loadCSS(url);
3449
+ }
3450
+ const u = resolveModuleUrl(url, { raw: true });
3451
+ return await runjsRequireAsync(this.requirejs, u);
1215
3452
  });
1216
3453
  // 动态按 URL 加载 ESM 模块
1217
3454
  // - 使用 Vite / Webpack ignore 注释,避免被预打包或重写
1218
- // - 返回模块命名空间对象(包含 default 与命名导出)
1219
- this.defineMethod('importAsync', async (url: string) => {
1220
- if (!url || typeof url !== 'string') {
1221
- throw new Error('invalid url');
1222
- }
1223
- const u = url.trim();
1224
- const g = globalThis as any;
1225
- g.__nocobaseImportAsyncCache = g.__nocobaseImportAsyncCache || new Map<string, Promise<any>>();
1226
- const cache: Map<string, Promise<any>> = g.__nocobaseImportAsyncCache;
1227
- if (cache.has(u)) return cache.get(u)!;
1228
- // 尝试使用原生 dynamic import(加上 vite/webpack 的 ignore 注释)
1229
- const nativeImport = () => import(/* @vite-ignore */ /* webpackIgnore: true */ u);
1230
- // 兜底方案:通过 eval 在运行时构造 import,避免被打包器接管
1231
- const evalImport = () => {
1232
- const importer = (0, eval)('u => import(u)');
1233
- return importer(u);
1234
- };
1235
- const p = (async () => {
1236
- try {
1237
- return await nativeImport();
1238
- } catch (err: any) {
1239
- // 常见于打包产物仍然拦截了 dynamic import 或开发态插件未识别 ignore 注释
1240
- try {
1241
- return await evalImport();
1242
- } catch (err2) {
1243
- throw err2 || err;
1244
- }
1245
- }
1246
- })();
1247
- cache.set(u, p);
1248
- return p;
3455
+ // - 通常返回模块命名空间对象(包含 default 与命名导出);
3456
+ // 若模块只有 default 一个导出,则会直接返回 default 值以提升易用性(无需再访问 .default)
3457
+ this.defineMethod('importAsync', async function (this: any, url: string) {
3458
+ // 判断是否为 CSS 文件(支持 example.css?v=123 等形式)
3459
+ if (isCssFile(url)) {
3460
+ return this.loadCSS(url);
3461
+ }
3462
+
3463
+ return await runjsImportModule(this, url, { importer: runjsImportAsync });
1249
3464
  });
1250
3465
  this.defineMethod('createJSRunner', async function (options?: JSRunnerOptions) {
1251
3466
  try {
@@ -1254,17 +3469,24 @@ export class FlowEngineContext extends BaseFlowEngineContext {
1254
3469
  } catch (_) {
1255
3470
  // ignore if setup is not available
1256
3471
  }
1257
- const version = (options?.version as any) || 'v1';
3472
+ const version = options?.version || 'v1';
1258
3473
  const modelClass = getModelClassName(this);
1259
- const Ctor =
1260
- (RunJSContextRegistry.resolve(version, modelClass) as any) ||
1261
- (RunJSContextRegistry.resolve(version, '*') as any) ||
1262
- FlowRunJSContext;
1263
- let runCtx: any;
1264
- if (Ctor) {
1265
- runCtx = new Ctor(this);
1266
- }
1267
- const globals: Record<string, any> = { ctx: runCtx, ...(options?.globals || {}) };
3474
+ const Ctor: new (delegate: any) => any = RunJSContextRegistry.resolve(version, modelClass) || FlowRunJSContext;
3475
+ const runCtx = new Ctor(this);
3476
+ runCtx.defineMethod('t', (key: string, options?: any) => {
3477
+ return this.t(key, { ns: 'runjs', ...options });
3478
+ });
3479
+
3480
+ let doc: RunJSDocMeta = {};
3481
+ try {
3482
+ const locale = (this as any)?.api?.auth?.locale || (this as any)?.i18n?.language || (this as any)?.locale;
3483
+ if ((Ctor as any)?.getDoc?.length) doc = (Ctor as any).getDoc(locale) || {};
3484
+ else doc = (Ctor as any)?.getDoc?.() || {};
3485
+ } catch (_) {
3486
+ doc = {};
3487
+ }
3488
+ const deprecatedCtx = createRunJSDeprecationProxy(runCtx, { doc });
3489
+ const globals: Record<string, any> = { ctx: deprecatedCtx, ...(options?.globals || {}) };
1268
3490
  const { timeoutMs } = options || {};
1269
3491
  return new JSRunner({ globals, timeoutMs });
1270
3492
  });
@@ -1282,57 +3504,6 @@ export class FlowEngineContext extends BaseFlowEngineContext {
1282
3504
  return this.engine.getEvents();
1283
3505
  });
1284
3506
 
1285
- // // Date variables (for variable selector meta tree)
1286
- // this.defineProperty('date', {
1287
- // get: () => {
1288
- // const vars = getDateVars() as Record<string, any>;
1289
- // // align with client options: add dayBeforeYesterday
1290
- // vars.dayBeforeYesterday = toUnit('day', -2);
1291
- // const now = new Date().toISOString();
1292
- // const out: Record<string, any> = {};
1293
- // for (const [k, v] of Object.entries(vars)) {
1294
- // try {
1295
- // out[k] = typeof v === 'function' ? v({ now }) : v;
1296
- // } catch (e) {
1297
- // // ignore
1298
- // }
1299
- // }
1300
- // return out;
1301
- // },
1302
- // meta: () => {
1303
- // const title = this.t('Date variables');
1304
- // const mk = (t: string) => ({ type: 'any', title: this.t(t) });
1305
- // return {
1306
- // type: 'object',
1307
- // title,
1308
- // properties: {
1309
- // now: mk('Current time'),
1310
- // dayBeforeYesterday: mk('Day before yesterday'),
1311
- // yesterday: mk('Yesterday'),
1312
- // today: mk('Today'),
1313
- // tomorrow: mk('Tomorrow'),
1314
- // lastIsoWeek: mk('Last week'),
1315
- // thisIsoWeek: mk('This week'),
1316
- // nextIsoWeek: mk('Next week'),
1317
- // lastMonth: mk('Last month'),
1318
- // thisMonth: mk('This month'),
1319
- // nextMonth: mk('Next month'),
1320
- // lastQuarter: mk('Last quarter'),
1321
- // thisQuarter: mk('This quarter'),
1322
- // nextQuarter: mk('Next quarter'),
1323
- // lastYear: mk('Last year'),
1324
- // thisYear: mk('This year'),
1325
- // nextYear: mk('Next year'),
1326
- // last7Days: mk('Last 7 days'),
1327
- // next7Days: mk('Next 7 days'),
1328
- // last30Days: mk('Last 30 days'),
1329
- // next30Days: mk('Next 30 days'),
1330
- // last90Days: mk('Last 90 days'),
1331
- // next90Days: mk('Next 90 days'),
1332
- // },
1333
- // } as PropertyMeta;
1334
- // },
1335
- // });
1336
3507
  this.defineMethod(
1337
3508
  'runAction',
1338
3509
  async function (this: BaseFlowEngineContext, actionName: string, params?: Record<string, any>) {
@@ -1375,17 +3546,34 @@ export class FlowEngineContext extends BaseFlowEngineContext {
1375
3546
  context: this.createProxy(),
1376
3547
  });
1377
3548
  });
3549
+ this.defineMethod('makeResource', function (this: BaseFlowEngineContext, resourceType) {
3550
+ return this.engine.createResource(resourceType, {
3551
+ context: this.createProxy(),
3552
+ });
3553
+ });
1378
3554
  // Provide useResource in base engine context so RunJS can call it directly
3555
+ this.defineMethod(
3556
+ 'initResource',
3557
+ function (
3558
+ this: BaseFlowEngineContext,
3559
+ className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource',
3560
+ ) {
3561
+ if (!this.has('resource')) {
3562
+ this.defineProperty('resource', {
3563
+ get: () => this.createResource(className),
3564
+ });
3565
+ }
3566
+ return this.resource;
3567
+ },
3568
+ );
3569
+ // @deprecated use `initResource` instead
1379
3570
  this.defineMethod(
1380
3571
  'useResource',
1381
3572
  function (
1382
3573
  this: BaseFlowEngineContext,
1383
3574
  className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource',
1384
3575
  ) {
1385
- if (this.has('resource')) return;
1386
- this.defineProperty('resource', {
1387
- get: () => this.createResource(className),
1388
- });
3576
+ return this.initResource(className);
1389
3577
  },
1390
3578
  );
1391
3579
  }
@@ -1401,15 +3589,12 @@ export class FlowModelContext extends BaseFlowModelContext {
1401
3589
  this.defineMethod('onRefReady', (ref, cb, timeout) => {
1402
3590
  this.engine.reactView.onRefReady(ref, cb, timeout);
1403
3591
  });
1404
- this.defineMethod('runjs', async (code, variables, options?: { version?: string }) => {
1405
- const runner = await this.createJSRunner({
1406
- globals: variables,
1407
- version: options?.version,
1408
- });
1409
- return runner.run(code);
1410
- });
1411
3592
  this.defineProperty('model', {
1412
3593
  value: model,
3594
+ info: {
3595
+ description: 'Current FlowModel instance.',
3596
+ detail: 'FlowModel',
3597
+ },
1413
3598
  });
1414
3599
  // 提供稳定的 ref 实例,确保渲染端与运行时上下文使用同一对象
1415
3600
  const stableRef = createRef<HTMLDivElement>();
@@ -1418,6 +3603,10 @@ export class FlowModelContext extends BaseFlowModelContext {
1418
3603
  this.model['_refCreated'] = true;
1419
3604
  return stableRef;
1420
3605
  },
3606
+ info: {
3607
+ description: 'Stable React ref for the view container.',
3608
+ detail: 'React.RefObject<HTMLDivElement>',
3609
+ },
1421
3610
  });
1422
3611
  this.defineMethod('openView', async function (uid: string, options) {
1423
3612
  const opts = { ...options };
@@ -1478,8 +3667,17 @@ export class FlowModelContext extends BaseFlowModelContext {
1478
3667
  engineCtx: this.engine.context,
1479
3668
  };
1480
3669
  model.context.defineProperty('view', { value: pendingView });
3670
+ // 默认按 click 打开,但兼容 popupSettings 绑定到其他事件(例如 DuplicateActionModel 监听 openDuplicatePopup)。
3671
+ const popupFlow = model.getFlow?.('popupSettings');
3672
+ const on = (popupFlow as any)?.on;
3673
+ let openEventName = 'click';
3674
+ if (typeof on === 'string' && on) {
3675
+ openEventName = on;
3676
+ } else if (on && typeof on === 'object' && typeof (on as any).eventName === 'string' && (on as any).eventName) {
3677
+ openEventName = (on as any).eventName;
3678
+ }
1481
3679
  await model.dispatchEvent(
1482
- 'click',
3680
+ openEventName,
1483
3681
  {
1484
3682
  // navigation: false, // TODO: 路由模式有bug,不支持多层同样viewId的弹窗,因此这里默认先用false
1485
3683
  // ...this.model?.['getInputArgs']?.(), // 避免部分关系字段信息丢失, 仿照 ClickableCollectionField 做法
@@ -1538,12 +3736,16 @@ export class FlowForkModelContext extends BaseFlowModelContext {
1538
3736
  throw new Error('Invalid FlowModel instance');
1539
3737
  }
1540
3738
  super();
1541
- this.addDelegate((this.master as any).context);
3739
+ this.addDelegate(this.master.context);
1542
3740
  this.defineMethod('onRefReady', (ref, cb, timeout) => {
1543
3741
  this.engine.reactView.onRefReady(ref, cb, timeout);
1544
3742
  });
1545
3743
  this.defineProperty('model', {
1546
3744
  get: () => this.fork,
3745
+ info: {
3746
+ description: 'Current ForkFlowModel instance (as model).',
3747
+ detail: 'ForkFlowModel',
3748
+ },
1547
3749
  });
1548
3750
  // 提供稳定的 ref 实例,确保渲染端与运行时上下文使用同一对象
1549
3751
  const stableRef = createRef<HTMLDivElement>();
@@ -1552,13 +3754,10 @@ export class FlowForkModelContext extends BaseFlowModelContext {
1552
3754
  this.fork['_refCreated'] = true;
1553
3755
  return stableRef;
1554
3756
  },
1555
- });
1556
- this.defineMethod('runjs', async (code, variables, options?: { version?: string }) => {
1557
- const runner = await this.createJSRunner({
1558
- globals: variables,
1559
- version: options?.version,
1560
- });
1561
- return runner.run(code);
3757
+ info: {
3758
+ description: 'Stable React ref for the view container.',
3759
+ detail: 'React.RefObject<HTMLDivElement>',
3760
+ },
1562
3761
  });
1563
3762
  }
1564
3763
  }
@@ -1569,7 +3768,19 @@ export class FlowRuntimeContext<
1569
3768
  > extends BaseFlowModelContext {
1570
3769
  declare steps: Record<string, { params: Record<string, any>; uiSchema?: any; result?: any }>;
1571
3770
  stepResults: Record<string, any> = {};
1572
- declare useResource: (className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource') => void;
3771
+ /**
3772
+ * @deprecated use `initResource` instead
3773
+ */
3774
+ declare useResource: (
3775
+ className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource',
3776
+ ) => void;
3777
+ /**
3778
+ * Initialize a resource instance without adding it to the context.
3779
+ * @param className - The resource class name.
3780
+ */
3781
+ declare initResource: (
3782
+ className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource',
3783
+ ) => void;
1573
3784
  declare getStepParams: (stepKey: string) => Record<string, any>;
1574
3785
  declare setStepParams: (stepKey: string, params?: any) => void;
1575
3786
  declare getStepResults: (stepKey: string) => any;
@@ -1591,15 +3802,15 @@ export class FlowRuntimeContext<
1591
3802
  return _.get(this.steps, [stepKey, 'result']);
1592
3803
  });
1593
3804
  this.defineMethod(
1594
- 'useResource',
3805
+ 'initResource',
1595
3806
  (className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource') => {
1596
3807
  if (model.context.has('resource')) {
1597
- console.warn(`[FlowRuntimeContext] useResource - resource already defined in context: ${className}`);
3808
+ console.log(`[FlowRuntimeContext] useResource - resource already defined in context: ${className}`);
1598
3809
  return;
1599
3810
  }
1600
3811
  model.context.defineProperty('resource', {
1601
3812
  get: () => {
1602
- return this.createResource(className);
3813
+ return this.makeResource(className);
1603
3814
  },
1604
3815
  });
1605
3816
  if (!model['resource']) {
@@ -1607,6 +3818,13 @@ export class FlowRuntimeContext<
1607
3818
  }
1608
3819
  },
1609
3820
  );
3821
+ // @deprecated use `initResource` instead
3822
+ this.defineMethod(
3823
+ 'useResource',
3824
+ (className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource') => {
3825
+ return this.initResource(className);
3826
+ },
3827
+ );
1610
3828
  this.defineProperty('resource', {
1611
3829
  get: () => model['resource'] || model.context['resource'],
1612
3830
  cache: false,
@@ -1614,13 +3832,6 @@ export class FlowRuntimeContext<
1614
3832
  this.defineMethod('onRefReady', (ref, cb, timeout) => {
1615
3833
  this.engine.reactView.onRefReady(ref, cb, timeout);
1616
3834
  });
1617
- this.defineMethod('runjs', async (code, variables, options?: { version?: string }) => {
1618
- const runner = await this.createJSRunner({
1619
- globals: variables,
1620
- version: options?.version,
1621
- });
1622
- return runner.run(code);
1623
- });
1624
3835
  }
1625
3836
 
1626
3837
  protected _getOwnProperty(key: string): any {
@@ -1654,7 +3865,7 @@ export class FlowRuntimeContext<
1654
3865
  }
1655
3866
 
1656
3867
  exit() {
1657
- throw new FlowExitException(this.flowKey, this.model.uid);
3868
+ throw new FlowExitAllException(this.flowKey, this.model.uid);
1658
3869
  }
1659
3870
 
1660
3871
  exitAll() {
@@ -1673,6 +3884,16 @@ export type RunJSDocCompletionDoc = {
1673
3884
  insertText?: string;
1674
3885
  };
1675
3886
 
3887
+ export type RunJSDocHiddenDoc = boolean | ((ctx: any) => boolean | Promise<boolean>);
3888
+
3889
+ // `hidden` is the single visibility entrypoint for RunJSDoc property docs:
3890
+ // - boolean: hide the whole node and its subtree
3891
+ // - string[]: hide specific subpaths under the node (relative dot-paths)
3892
+ export type RunJSDocHiddenOrPathsDoc =
3893
+ | boolean
3894
+ | string[]
3895
+ | ((ctx: any) => boolean | string[] | Promise<boolean | string[]>);
3896
+
1676
3897
  export type RunJSDocPropertyDoc =
1677
3898
  | string
1678
3899
  | {
@@ -1681,7 +3902,14 @@ export type RunJSDocPropertyDoc =
1681
3902
  type?: string;
1682
3903
  examples?: string[];
1683
3904
  completion?: RunJSDocCompletionDoc;
3905
+ ref?: FlowContextDocRef;
3906
+ deprecated?: FlowDeprecationDoc;
3907
+ params?: FlowContextDocParam[];
3908
+ returns?: FlowContextDocReturn;
1684
3909
  properties?: Record<string, RunJSDocPropertyDoc>;
3910
+ hidden?: RunJSDocHiddenOrPathsDoc;
3911
+ disabled?: boolean | ((ctx: any) => boolean | Promise<boolean>);
3912
+ disabledReason?: string | ((ctx: any) => string | undefined | Promise<string | undefined>);
1685
3913
  };
1686
3914
 
1687
3915
  export type RunJSDocMethodDoc =
@@ -1691,6 +3919,13 @@ export type RunJSDocMethodDoc =
1691
3919
  detail?: string;
1692
3920
  examples?: string[];
1693
3921
  completion?: RunJSDocCompletionDoc;
3922
+ ref?: FlowContextDocRef;
3923
+ deprecated?: FlowDeprecationDoc;
3924
+ params?: FlowContextDocParam[];
3925
+ returns?: FlowContextDocReturn;
3926
+ hidden?: RunJSDocHiddenDoc;
3927
+ disabled?: boolean | ((ctx: any) => boolean | Promise<boolean>);
3928
+ disabledReason?: string | ((ctx: any) => string | undefined | Promise<string | undefined>);
1694
3929
  };
1695
3930
 
1696
3931
  export type RunJSDocMeta = {
@@ -1717,13 +3952,518 @@ function __runjsDeepMerge(base: any, patch: any) {
1717
3952
  }
1718
3953
  return out;
1719
3954
  }
3955
+
3956
+ function __isPlainObject(val: any): val is Record<string, any> {
3957
+ return !!val && typeof val === 'object' && !Array.isArray(val);
3958
+ }
3959
+
3960
+ type RunJSDeprecatedTreeNode = {
3961
+ deprecated?: FlowDeprecationDoc;
3962
+ children?: Record<string, RunJSDeprecatedTreeNode>;
3963
+ };
3964
+
3965
+ function __isPromiseLike(v: any): v is Promise<any> {
3966
+ return !!v && (typeof v === 'object' || typeof v === 'function') && typeof (v as any).then === 'function';
3967
+ }
3968
+
3969
+ function __normalizeDeprecationDoc(v: any): FlowDeprecationDoc | undefined {
3970
+ if (v === true) return true;
3971
+ if (!v) return undefined;
3972
+ if (__isPlainObject(v)) return v as any;
3973
+ return undefined;
3974
+ }
3975
+
3976
+ function __addDeprecatedPath(root: RunJSDeprecatedTreeNode, path: string[], deprecated: FlowDeprecationDoc) {
3977
+ if (!Array.isArray(path) || !path.length) return;
3978
+ let cur = root;
3979
+ for (const seg of path) {
3980
+ if (!seg) return;
3981
+ cur.children = cur.children || {};
3982
+ cur.children[seg] = cur.children[seg] || {};
3983
+ cur = cur.children[seg];
3984
+ }
3985
+ cur.deprecated = deprecated;
3986
+ }
3987
+
3988
+ function __mergeDeprecatedTree(base: RunJSDeprecatedTreeNode, patch: RunJSDeprecatedTreeNode) {
3989
+ if (patch.deprecated !== undefined) base.deprecated = patch.deprecated;
3990
+ const pChildren = patch.children || {};
3991
+ const keys = Object.keys(pChildren);
3992
+ if (!keys.length) return;
3993
+ base.children = base.children || {};
3994
+ for (const k of keys) {
3995
+ base.children[k] = base.children[k] || {};
3996
+ __mergeDeprecatedTree(base.children[k], pChildren[k]);
3997
+ }
3998
+ }
3999
+
4000
+ function __buildDeprecatedTreeFromRunJSDoc(doc?: RunJSDocMeta): RunJSDeprecatedTreeNode {
4001
+ const root: RunJSDeprecatedTreeNode = {};
4002
+ if (!doc) return root;
4003
+
4004
+ const walkProps = (props: any, parentPath: string[]) => {
4005
+ if (!__isPlainObject(props)) return;
4006
+ for (const [key, raw] of Object.entries(props)) {
4007
+ if (!key) continue;
4008
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
4009
+ const node = raw as any;
4010
+ const dep = __normalizeDeprecationDoc(node.deprecated);
4011
+ if (dep) __addDeprecatedPath(root, [...parentPath, key], dep);
4012
+ if (__isPlainObject(node.properties)) {
4013
+ walkProps(node.properties, [...parentPath, key]);
4014
+ }
4015
+ }
4016
+ };
4017
+
4018
+ const walkMethods = (methods: any) => {
4019
+ if (!__isPlainObject(methods)) return;
4020
+ for (const [key, raw] of Object.entries(methods)) {
4021
+ if (!key) continue;
4022
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
4023
+ const node = raw as any;
4024
+ const dep = __normalizeDeprecationDoc(node.deprecated);
4025
+ if (dep) __addDeprecatedPath(root, [key], dep);
4026
+ }
4027
+ };
4028
+
4029
+ walkProps((doc as any).properties, []);
4030
+ walkMethods((doc as any).methods);
4031
+
4032
+ return root;
4033
+ }
4034
+
4035
+ function __buildDeprecatedTreeFromFlowContextInfos(ctx: any): RunJSDeprecatedTreeNode {
4036
+ const root: RunJSDeprecatedTreeNode = {};
4037
+ const visited = new WeakSet<any>();
4038
+
4039
+ const collectInfoProperties = (basePath: string[], props: any) => {
4040
+ if (!__isPlainObject(props)) return;
4041
+ for (const [key, raw] of Object.entries(props)) {
4042
+ if (!key) continue;
4043
+ if (typeof raw === 'string') continue;
4044
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
4045
+ const node = raw as any;
4046
+ const dep = __normalizeDeprecationDoc(node.deprecated);
4047
+ if (dep) __addDeprecatedPath(root, [...basePath, key], dep);
4048
+ if (__isPlainObject(node.properties)) {
4049
+ collectInfoProperties([...basePath, key], node.properties);
4050
+ }
4051
+ }
4052
+ };
4053
+
4054
+ const walk = (c: any) => {
4055
+ if (!c || (typeof c !== 'object' && typeof c !== 'function')) return;
4056
+ if (visited.has(c)) return;
4057
+ visited.add(c);
4058
+
4059
+ const methodInfos = (c as any)._methodInfos;
4060
+ if (__isPlainObject(methodInfos)) {
4061
+ for (const [name, info] of Object.entries(methodInfos)) {
4062
+ if (!name) continue;
4063
+ if (!info || typeof info !== 'object' || Array.isArray(info)) continue;
4064
+ const dep = __normalizeDeprecationDoc((info as any).deprecated);
4065
+ if (dep) __addDeprecatedPath(root, [name], dep);
4066
+ }
4067
+ }
4068
+
4069
+ const props = (c as any)._props;
4070
+ if (__isPlainObject(props)) {
4071
+ for (const [name, opt] of Object.entries(props)) {
4072
+ if (!name) continue;
4073
+ const info = (opt as any)?.info;
4074
+ if (!info || typeof info !== 'object' || Array.isArray(info)) continue;
4075
+ const dep = __normalizeDeprecationDoc((info as any).deprecated);
4076
+ if (dep) __addDeprecatedPath(root, [name], dep);
4077
+ if (__isPlainObject((info as any).properties)) {
4078
+ collectInfoProperties([name], (info as any).properties);
4079
+ }
4080
+ }
4081
+ }
4082
+
4083
+ const delegates = (c as any)._delegates;
4084
+ if (Array.isArray(delegates)) {
4085
+ for (const d of delegates) walk(d);
4086
+ }
4087
+ };
4088
+
4089
+ walk(ctx);
4090
+ return root;
4091
+ }
4092
+
4093
+ export function createRunJSDeprecationProxy(
4094
+ ctx: any,
4095
+ options: {
4096
+ doc?: RunJSDocMeta;
4097
+ } = {},
4098
+ ) {
4099
+ const fromDoc = __buildDeprecatedTreeFromRunJSDoc(options.doc);
4100
+ const fromInfo = __buildDeprecatedTreeFromFlowContextInfos(ctx);
4101
+ __mergeDeprecatedTree(fromDoc, fromInfo);
4102
+
4103
+ const warned = new Set<string>();
4104
+ const proxyToTarget = new WeakMap<object, object>();
4105
+ const objectProxyCache = new WeakMap<object, Map<string, any>>();
4106
+ const functionProxyCache = new WeakMap<Function, Map<string, any>>();
4107
+
4108
+ const extractRunJSLocation = (
4109
+ stack?: string,
4110
+ ): { line?: number; column?: number; rawLine?: number; rawColumn?: number } => {
4111
+ if (!stack || typeof stack !== 'string') return {};
4112
+ const WRAPPER_PREFIX_LINES = 2; // JSRunner.run wraps user code with 2 lines before `${code}`
4113
+ const lines = stack.split('\n');
4114
+ for (const l of lines) {
4115
+ if (!l) continue;
4116
+ const m = l.match(/<anonymous>:(\d+):(\d+)/);
4117
+ if (!m) continue;
4118
+ const rawLine = Number(m[1]);
4119
+ const rawColumn = Number(m[2]);
4120
+ const line =
4121
+ Number.isFinite(rawLine) && rawLine > WRAPPER_PREFIX_LINES ? rawLine - WRAPPER_PREFIX_LINES : rawLine;
4122
+ const column = Number.isFinite(rawColumn) ? rawColumn : undefined;
4123
+ return { line, column, rawLine, rawColumn };
4124
+ }
4125
+ return {};
4126
+ };
4127
+
4128
+ const collectInfoProperties = (basePath: string[], props: any) => {
4129
+ if (!__isPlainObject(props)) return;
4130
+ for (const [key, raw] of Object.entries(props)) {
4131
+ if (!key) continue;
4132
+ if (typeof raw === 'string') continue;
4133
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
4134
+ const node = raw as any;
4135
+ const dep = __normalizeDeprecationDoc(node.deprecated);
4136
+ if (dep) __addDeprecatedPath(fromDoc, [...basePath, key], dep);
4137
+ if (__isPlainObject(node.properties)) {
4138
+ collectInfoProperties([...basePath, key], node.properties);
4139
+ }
4140
+ }
4141
+ };
4142
+
4143
+ const updateTreeFromDefineProperty = (name: string, options: any) => {
4144
+ if (!name) return;
4145
+ const info = options?.info;
4146
+ if (!info || typeof info !== 'object' || Array.isArray(info)) return;
4147
+ const dep = __normalizeDeprecationDoc((info as any).deprecated);
4148
+ if (dep) __addDeprecatedPath(fromDoc, [name], dep);
4149
+ if (__isPlainObject((info as any).properties)) {
4150
+ collectInfoProperties([name], (info as any).properties);
4151
+ }
4152
+ };
4153
+
4154
+ const updateTreeFromDefineMethod = (name: string, info: any) => {
4155
+ if (!name) return;
4156
+ if (!info || typeof info !== 'object' || Array.isArray(info)) return;
4157
+ const dep = __normalizeDeprecationDoc((info as any).deprecated);
4158
+ if (dep) __addDeprecatedPath(fromDoc, [name], dep);
4159
+ };
4160
+
4161
+ const unwrapProxy = (val: any) => {
4162
+ let cur = val;
4163
+ while (cur && (typeof cur === 'object' || typeof cur === 'function')) {
4164
+ const mapped = proxyToTarget.get(cur as any);
4165
+ if (!mapped) break;
4166
+ cur = mapped;
4167
+ }
4168
+ return cur;
4169
+ };
4170
+
4171
+ const formatReplacedBy = (replacedBy: any): string | undefined => {
4172
+ if (!replacedBy) return undefined;
4173
+ if (typeof replacedBy === 'string') return replacedBy.trim() || undefined;
4174
+ if (Array.isArray(replacedBy)) {
4175
+ const parts = replacedBy.map((x) => (typeof x === 'string' ? x.trim() : '')).filter(Boolean);
4176
+ return parts.length ? parts.join(', ') : undefined;
4177
+ }
4178
+ return undefined;
4179
+ };
4180
+
4181
+ const warnOnce = (apiPath: string, deprecated: FlowDeprecationDoc, stack?: string) => {
4182
+ if (!apiPath) return;
4183
+ if (warned.has(apiPath)) return;
4184
+ warned.add(apiPath);
4185
+
4186
+ const logger = (ctx as any)?.logger;
4187
+ const t =
4188
+ typeof (ctx as any)?.t === 'function'
4189
+ ? (key: string, options?: any) =>
4190
+ (ctx as any).t(key, { ns: [FLOW_ENGINE_NAMESPACE, 'client'], nsMode: 'fallback', ...options })
4191
+ : (key: string, options?: any) => {
4192
+ const fallback = options?.defaultValue ?? key;
4193
+ if (typeof fallback !== 'string' || !options) return fallback;
4194
+ // lightweight interpolation for fallback strings (i18next-style: {{var}})
4195
+ return fallback.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_m, k) => {
4196
+ const v = options?.[k];
4197
+ return typeof v === 'string' || typeof v === 'number' ? String(v) : '';
4198
+ });
4199
+ };
4200
+ const meta = typeof deprecated === 'object' && deprecated ? deprecated : {};
4201
+ const replacedBy = formatReplacedBy((meta as any).replacedBy);
4202
+ const since = typeof (meta as any).since === 'string' ? String((meta as any).since) : undefined;
4203
+ const removedIn = typeof (meta as any).removedIn === 'string' ? String((meta as any).removedIn) : undefined;
4204
+ const message = typeof (meta as any).message === 'string' ? String((meta as any).message) : '';
4205
+
4206
+ const loc = extractRunJSLocation(stack);
4207
+
4208
+ const locText = loc.line ? `(line ${loc.line}${loc.column ? `:${loc.column}` : ''})` : '';
4209
+
4210
+ const msg = message.trim();
4211
+ const mainText = msg
4212
+ ? t('RunJS deprecated warning with message', {
4213
+ defaultValue: '[RunJS][Deprecated] {{api}} {{message}}{{location}}',
4214
+ api: apiPath,
4215
+ message: msg,
4216
+ location: locText,
4217
+ })
4218
+ : t('RunJS deprecated warning', {
4219
+ defaultValue: '[RunJS][Deprecated] {{api}} is deprecated{{location}}',
4220
+ api: apiPath,
4221
+ location: locText,
4222
+ });
4223
+
4224
+ const separator = t('RunJS deprecated separator', { defaultValue: '; ' });
4225
+ const textParts: string[] = [mainText];
4226
+ if (replacedBy)
4227
+ textParts.push(t('RunJS deprecated replacedBy', { defaultValue: 'Use {{replacedBy}} instead', replacedBy }));
4228
+ if (since) textParts.push(t('RunJS deprecated since', { defaultValue: 'since {{since}}', since }));
4229
+ if (removedIn)
4230
+ textParts.push(t('RunJS deprecated removedIn', { defaultValue: 'will be removed in {{removedIn}}', removedIn }));
4231
+ const text = textParts.filter(Boolean).join(separator);
4232
+
4233
+ try {
4234
+ if (logger && typeof logger.warn === 'function') {
4235
+ logger.warn(text);
4236
+ } else {
4237
+ // fail-open: avoid breaking runjs execution when logger is missing
4238
+ console.warn(text);
4239
+ }
4240
+ } catch (_) {
4241
+ // ignore logger failures
4242
+ }
4243
+ };
4244
+
4245
+ const createFunctionProxy = (fn: Function, node: RunJSDeprecatedTreeNode, path: string) => {
4246
+ const dep = node.deprecated;
4247
+ if (!dep) return fn;
4248
+
4249
+ const cacheByPath = functionProxyCache.get(fn) || new Map<string, any>();
4250
+ functionProxyCache.set(fn, cacheByPath);
4251
+ if (cacheByPath.has(path)) return cacheByPath.get(path);
4252
+
4253
+ const proxied = new Proxy(fn, {
4254
+ apply(target, thisArg, argArray) {
4255
+ const stack = warned.has(path) ? undefined : new Error().stack;
4256
+ warnOnce(path, dep, stack);
4257
+ const realThis = unwrapProxy(thisArg);
4258
+ return Reflect.apply(target, realThis, argArray);
4259
+ },
4260
+ get(target, key, receiver) {
4261
+ return Reflect.get(target, key, receiver);
4262
+ },
4263
+ });
4264
+
4265
+ cacheByPath.set(path, proxied);
4266
+ return proxied as any;
4267
+ };
4268
+
4269
+ const createObjectProxy = (target: any, node: RunJSDeprecatedTreeNode, path: string): any => {
4270
+ if (!target || (typeof target !== 'object' && typeof target !== 'function')) return target;
4271
+ if (__isPromiseLike(target)) return target;
4272
+ const hasChildren = !!node.children && Object.keys(node.children).length > 0;
4273
+ if (!hasChildren && path !== 'ctx') return target;
4274
+
4275
+ const cacheByPath = objectProxyCache.get(target) || new Map<string, any>();
4276
+ objectProxyCache.set(target, cacheByPath);
4277
+ if (cacheByPath.has(path)) return cacheByPath.get(path);
4278
+
4279
+ const proxied = new Proxy(target, {
4280
+ get(t, key, receiver) {
4281
+ if (typeof key === 'symbol') {
4282
+ return Reflect.get(t, key, unwrapProxy(receiver));
4283
+ }
4284
+ const prop = String(key);
4285
+ const value = Reflect.get(t, key, unwrapProxy(receiver));
4286
+
4287
+ // Support dynamic deprecation registration via ctx.defineProperty/defineMethod during RunJS execution.
4288
+ // - This is especially useful when the deprecated API is introduced after JSRunner is created.
4289
+ if (path === 'ctx' && prop === 'defineProperty' && typeof value === 'function') {
4290
+ return (...args: any[]) => {
4291
+ const result = value(...args);
4292
+ try {
4293
+ updateTreeFromDefineProperty(String(args?.[0] ?? ''), args?.[1]);
4294
+ } catch (_) {
4295
+ // ignore
4296
+ }
4297
+ return result;
4298
+ };
4299
+ }
4300
+ if (path === 'ctx' && prop === 'defineMethod' && typeof value === 'function') {
4301
+ return (...args: any[]) => {
4302
+ const result = value(...args);
4303
+ try {
4304
+ updateTreeFromDefineMethod(String(args?.[0] ?? ''), args?.[2]);
4305
+ } catch (_) {
4306
+ // ignore
4307
+ }
4308
+ return result;
4309
+ };
4310
+ }
4311
+
4312
+ const child = node.children?.[prop];
4313
+ if (!child) return value;
4314
+
4315
+ const childPath = `${path}.${prop}`;
4316
+ if (typeof value === 'function' && child.deprecated) {
4317
+ return createFunctionProxy(value, child, childPath);
4318
+ }
4319
+ if (child.deprecated) {
4320
+ // For non-callable APIs, "use" happens on access (there is no apply step).
4321
+ const stack = warned.has(childPath) ? undefined : new Error().stack;
4322
+ warnOnce(childPath, child.deprecated, stack);
4323
+ }
4324
+ if (value && (typeof value === 'object' || typeof value === 'function') && child.children) {
4325
+ return createObjectProxy(value, child, childPath);
4326
+ }
4327
+ return value;
4328
+ },
4329
+ has(t, key) {
4330
+ return Reflect.has(t, key);
4331
+ },
4332
+ });
4333
+
4334
+ proxyToTarget.set(proxied as any, target);
4335
+ cacheByPath.set(path, proxied);
4336
+ return proxied;
4337
+ };
4338
+
4339
+ return createObjectProxy(ctx, fromDoc, 'ctx');
4340
+ }
4341
+
4342
+ function __mergeRunJSDocDocRecord(base: any, patch: any, mergeDoc: (b: any, p: any) => any): any {
4343
+ if (!__isPlainObject(patch)) return base;
4344
+ // Important: preserve `null` markers when base is not an object (e.g. child class wants to delete parent keys).
4345
+ // If we eagerly delete them here, the deletion intent is lost for later merges in the inheritance chain.
4346
+ if (!__isPlainObject(base)) return patch;
4347
+ const out: any = { ...base };
4348
+ for (const k of Object.keys(patch)) {
4349
+ const pv = patch[k];
4350
+ if (pv === null) {
4351
+ delete out[k];
4352
+ continue;
4353
+ }
4354
+ const bv = __isPlainObject(base) ? base[k] : undefined;
4355
+ const merged = mergeDoc(bv, pv);
4356
+ if (typeof merged === 'undefined') delete out[k];
4357
+ else out[k] = merged;
4358
+ }
4359
+ return out;
4360
+ }
4361
+
4362
+ function __mergeRunJSDocPropertyDoc(base: any, patch: any): any {
4363
+ if (patch === null) return undefined;
4364
+
4365
+ const baseIsObj = __isPlainObject(base);
4366
+ const patchIsObj = __isPlainObject(patch);
4367
+ const baseIsStr = typeof base === 'string';
4368
+ const patchIsStr = typeof patch === 'string';
4369
+
4370
+ // Treat string docs as { description: string } when merging with object docs,
4371
+ // to avoid "whole replacement" that drops base hidden/properties/completion.
4372
+ if (patchIsStr) {
4373
+ if (baseIsObj) {
4374
+ return __mergeRunJSDocPropertyDoc(base, { description: patch });
4375
+ }
4376
+ return patch;
4377
+ }
4378
+
4379
+ if (patchIsObj) {
4380
+ const baseObj: any = baseIsObj ? base : baseIsStr ? { description: base } : undefined;
4381
+ const out: any = { ...(baseObj || {}) };
4382
+ for (const k of Object.keys(patch)) {
4383
+ if (k === 'properties') {
4384
+ const pv = (patch as any).properties;
4385
+ if (pv === null) {
4386
+ delete out.properties;
4387
+ continue;
4388
+ }
4389
+ const mergedProps = __mergeRunJSDocDocRecord(baseObj?.properties, pv, __mergeRunJSDocPropertyDoc);
4390
+ if (typeof mergedProps === 'undefined') delete out.properties;
4391
+ else out.properties = mergedProps;
4392
+ continue;
4393
+ }
4394
+ const mergedVal = __runjsDeepMerge(baseObj?.[k], (patch as any)[k]);
4395
+ if (typeof mergedVal === 'undefined') delete out[k];
4396
+ else out[k] = mergedVal;
4397
+ }
4398
+ return out;
4399
+ }
4400
+
4401
+ return patch ?? base;
4402
+ }
4403
+
4404
+ function __mergeRunJSDocMethodDoc(base: any, patch: any): any {
4405
+ if (patch === null) return undefined;
4406
+
4407
+ const baseIsObj = __isPlainObject(base);
4408
+ const patchIsObj = __isPlainObject(patch);
4409
+ const baseIsStr = typeof base === 'string';
4410
+ const patchIsStr = typeof patch === 'string';
4411
+
4412
+ if (patchIsStr) {
4413
+ if (baseIsObj) {
4414
+ return __mergeRunJSDocMethodDoc(base, { description: patch });
4415
+ }
4416
+ return patch;
4417
+ }
4418
+
4419
+ if (patchIsObj) {
4420
+ const baseObj: any = baseIsObj ? base : baseIsStr ? { description: base } : undefined;
4421
+ const out: any = { ...(baseObj || {}) };
4422
+ for (const k of Object.keys(patch)) {
4423
+ const mergedVal = __runjsDeepMerge(baseObj?.[k], (patch as any)[k]);
4424
+ if (typeof mergedVal === 'undefined') delete out[k];
4425
+ else out[k] = mergedVal;
4426
+ }
4427
+ return out;
4428
+ }
4429
+
4430
+ return patch ?? base;
4431
+ }
4432
+
4433
+ function __mergeRunJSDocMeta(base: any, patch: any): RunJSDocMeta {
4434
+ const baseObj: any = __isPlainObject(base) ? base : {};
4435
+ const patchObj: any = __isPlainObject(patch) ? patch : {};
4436
+ const out: any = { ...baseObj };
4437
+
4438
+ for (const k of Object.keys(patchObj)) {
4439
+ if (k === 'properties') {
4440
+ const mergedProps = __mergeRunJSDocDocRecord(baseObj.properties, patchObj.properties, __mergeRunJSDocPropertyDoc);
4441
+ if (typeof mergedProps === 'undefined') delete out.properties;
4442
+ else out.properties = mergedProps;
4443
+ continue;
4444
+ }
4445
+ if (k === 'methods') {
4446
+ const mergedMethods = __mergeRunJSDocDocRecord(baseObj.methods, patchObj.methods, __mergeRunJSDocMethodDoc);
4447
+ if (typeof mergedMethods === 'undefined') delete out.methods;
4448
+ else out.methods = mergedMethods;
4449
+ continue;
4450
+ }
4451
+ const mergedVal = __runjsDeepMerge(baseObj[k], patchObj[k]);
4452
+ if (typeof mergedVal === 'undefined') delete out[k];
4453
+ else out[k] = mergedVal;
4454
+ }
4455
+
4456
+ return out as RunJSDocMeta;
4457
+ }
1720
4458
  export class FlowRunJSContext extends FlowContext {
1721
4459
  constructor(delegate: FlowContext) {
1722
4460
  super();
1723
4461
  this.addDelegate(delegate);
1724
4462
  this.defineProperty('React', { value: React });
1725
4463
  this.defineProperty('antd', { value: antd });
1726
- this.defineProperty('dayjs', { value: dayjs });
4464
+ this.defineProperty('dayjs', {
4465
+ value: dayjs,
4466
+ });
1727
4467
  // 为 JS 运行时代码提供带有 antd/App/ConfigProvider 包裹的 React 根
1728
4468
  // 保持与 ReactDOMClient 接口一致,优先覆盖 createRoot,其余方法透传
1729
4469
  const ReactDOMShim: any = {
@@ -1735,19 +4475,10 @@ export class FlowRunJSContext extends FlowContext {
1735
4475
  return this.engine.reactView.createRoot(realContainer as HTMLElement, options);
1736
4476
  },
1737
4477
  };
4478
+ ReactDOMShim.__nbRunjsInternalShim = true;
1738
4479
  this.defineProperty('ReactDOM', { value: ReactDOMShim });
1739
4480
 
1740
- // 为第三方/通用库提供统一命名空间:ctx.libs
1741
- // - 新增库应优先挂载到 ctx.libs.xxx
1742
- // - 同时保留顶层别名(如 ctx.React / ctx.antd),以兼容历史代码
1743
- const libs = Object.freeze({
1744
- React,
1745
- ReactDOM: ReactDOMShim,
1746
- antd,
1747
- dayjs,
1748
- antdIcons,
1749
- });
1750
- this.defineProperty('libs', { value: libs });
4481
+ setupRunJSLibs(this);
1751
4482
 
1752
4483
  // Convenience: ctx.render(<App />[, container])
1753
4484
  // - container defaults to ctx.element if available
@@ -1767,16 +4498,37 @@ export class FlowRunJSContext extends FlowContext {
1767
4498
  globalRef.__nbRunjsRoots = globalRef.__nbRunjsRoots || new WeakMap<any, any>();
1768
4499
  const rootMap: WeakMap<any, any> = globalRef.__nbRunjsRoots;
1769
4500
 
1770
- // If vnode is string (HTML), unmount react root and set sanitized HTML
1771
- if (typeof vnode === 'string') {
1772
- const existingRoot = rootMap.get(containerEl);
1773
- if (existingRoot && typeof existingRoot.unmount === 'function') {
4501
+ const disposeEntry = (entry: any) => {
4502
+ if (!entry) return;
4503
+ if (entry.disposeTheme && typeof entry.disposeTheme === 'function') {
4504
+ try {
4505
+ entry.disposeTheme();
4506
+ } catch (_) {
4507
+ // ignore
4508
+ }
4509
+ entry.disposeTheme = undefined;
4510
+ }
4511
+ const root = entry.root || entry;
4512
+ if (root && typeof root.unmount === 'function') {
1774
4513
  try {
1775
- existingRoot.unmount();
1776
- } finally {
1777
- rootMap.delete(containerEl);
4514
+ root.unmount();
4515
+ } catch (_) {
4516
+ // ignore
1778
4517
  }
1779
4518
  }
4519
+ };
4520
+
4521
+ const unmountContainerRoot = () => {
4522
+ const existing = rootMap.get(containerEl);
4523
+ if (existing) {
4524
+ disposeEntry(existing);
4525
+ rootMap.delete(containerEl);
4526
+ }
4527
+ };
4528
+
4529
+ // If vnode is string (HTML), unmount react root and set sanitized HTML
4530
+ if (typeof vnode === 'string') {
4531
+ unmountContainerRoot();
1780
4532
  const proxy: any = new ElementProxy(containerEl);
1781
4533
  proxy.innerHTML = String(vnode ?? '');
1782
4534
  return null;
@@ -1788,39 +4540,64 @@ export class FlowRunJSContext extends FlowContext {
1788
4540
  (vnode as any).nodeType &&
1789
4541
  ((vnode as any).nodeType === 1 || (vnode as any).nodeType === 3 || (vnode as any).nodeType === 11)
1790
4542
  ) {
1791
- const existingRoot = rootMap.get(containerEl);
1792
- if (existingRoot && typeof existingRoot.unmount === 'function') {
1793
- try {
1794
- existingRoot.unmount();
1795
- } finally {
1796
- rootMap.delete(containerEl);
1797
- }
1798
- }
4543
+ unmountContainerRoot();
1799
4544
  while (containerEl.firstChild) containerEl.removeChild(containerEl.firstChild);
1800
4545
  containerEl.appendChild(vnode as any);
1801
4546
  return null;
1802
4547
  }
1803
4548
 
1804
- let root = rootMap.get(containerEl);
1805
- if (!root) {
1806
- root = this.ReactDOM.createRoot(containerEl);
1807
- rootMap.set(containerEl, root);
4549
+ // 注意:rootMap 是“全局按容器复用”的(key=containerEl)。
4550
+ // 若不同 RunJS ctx 复用同一个 containerEl,且 ReactDOM 实例引用也相同,
4551
+ // 则会复用到旧 entry,进而复用旧 ctx 创建的 autorun(闭包捕获旧 ctx),造成:
4552
+ // 1) 旧 ctx 的 reaction 继续驱动新渲染(跨 ctx 复用风险)
4553
+ // 2) 新 ctx 的主题变化不再触发 rerender
4554
+ // 3) 旧 ctx 被 entry/autorun 间接持有,无法被 GC(内存泄漏)
4555
+ // 因此这里把 ownerKey(当前 ctx)也纳入复用判断;owner 变化时必须重建 entry。
4556
+ const rendererKey = this.ReactDOM;
4557
+ const ownerKey = this;
4558
+ let entry = rootMap.get(containerEl);
4559
+ if (!entry || entry.rendererKey !== rendererKey || entry.ownerKey !== ownerKey) {
4560
+ if (entry) {
4561
+ disposeEntry(entry);
4562
+ rootMap.delete(containerEl);
4563
+ }
4564
+ const root = this.ReactDOM.createRoot(containerEl);
4565
+ entry = { rendererKey, ownerKey, root, disposeTheme: undefined, lastVnode: undefined };
4566
+ rootMap.set(containerEl, entry);
1808
4567
  }
1809
- root.render(vnode as any);
1810
- return root;
4568
+
4569
+ return externalReactRender({
4570
+ ctx: this,
4571
+ entry,
4572
+ vnode,
4573
+ containerEl,
4574
+ rootMap,
4575
+ unmountContainerRoot,
4576
+ internalReact: React,
4577
+ internalAntd: antd,
4578
+ });
1811
4579
  },
1812
4580
  );
1813
4581
  }
4582
+
4583
+ exit() {
4584
+ throw new FlowExitAllException(this.flowKey, this.model?.uid || 'runjs');
4585
+ }
4586
+
4587
+ exitAll() {
4588
+ throw new FlowExitAllException(this.flowKey, this.model?.uid || 'runjs');
4589
+ }
4590
+
1814
4591
  static define(meta: RunJSDocMeta, options?: { locale?: string }) {
1815
4592
  const locale = options?.locale;
1816
4593
  if (locale) {
1817
4594
  const map = __runjsClassLocaleMeta.get(this) || new Map<string, RunJSDocMeta>();
1818
4595
  const prev = map.get(locale) || {};
1819
- map.set(locale, __runjsDeepMerge(prev, meta));
4596
+ map.set(locale, __mergeRunJSDocMeta(prev, meta));
1820
4597
  __runjsClassLocaleMeta.set(this, map);
1821
4598
  } else {
1822
4599
  const prev = __runjsClassDefaultMeta.get(this) || {};
1823
- __runjsClassDefaultMeta.set(this, __runjsDeepMerge(prev, meta));
4600
+ __runjsClassDefaultMeta.set(this, __mergeRunJSDocMeta(prev, meta));
1824
4601
  }
1825
4602
  __runjsDocCache.delete(this);
1826
4603
  }
@@ -1828,7 +4605,7 @@ export class FlowRunJSContext extends FlowContext {
1828
4605
  const self = this as any as Function;
1829
4606
  let cacheForClass = __runjsDocCache.get(self);
1830
4607
  const cacheKey = String(locale || 'default');
1831
- if (cacheForClass && cacheForClass.has(cacheKey)) return cacheForClass.get(cacheKey)!;
4608
+ if (cacheForClass && cacheForClass.has(cacheKey)) return cacheForClass.get(cacheKey) as RunJSDocMeta;
1832
4609
  const chain: Function[] = [];
1833
4610
  let cur: any = self;
1834
4611
  while (cur && cur.prototype) {
@@ -1837,13 +4614,13 @@ export class FlowRunJSContext extends FlowContext {
1837
4614
  }
1838
4615
  let merged: RunJSDocMeta = {};
1839
4616
  for (const cls of chain) {
1840
- merged = __runjsDeepMerge(merged, __runjsClassDefaultMeta.get(cls) || {});
4617
+ merged = __mergeRunJSDocMeta(merged, __runjsClassDefaultMeta.get(cls) || {});
1841
4618
  }
1842
4619
  if (locale) {
1843
4620
  for (const cls of chain) {
1844
4621
  const lmap = __runjsClassLocaleMeta.get(cls);
1845
4622
  if (lmap && lmap.has(locale)) {
1846
- merged = __runjsDeepMerge(merged, lmap.get(locale));
4623
+ merged = __mergeRunJSDocMeta(merged, lmap.get(locale));
1847
4624
  }
1848
4625
  }
1849
4626
  }