@nocobase/flow-engine 2.0.0-beta.9 → 2.0.1

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 (245) hide show
  1. package/lib/BlockScopedFlowEngine.js +0 -1
  2. package/lib/FlowDefinition.d.ts +2 -0
  3. package/lib/JSRunner.d.ts +6 -0
  4. package/lib/JSRunner.js +32 -2
  5. package/lib/ViewScopedFlowEngine.js +3 -0
  6. package/lib/acl/Acl.js +13 -3
  7. package/lib/components/FlowContextSelector.js +155 -10
  8. package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
  9. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
  10. package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
  11. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +5 -1
  12. package/lib/components/variables/VariableInput.js +9 -4
  13. package/lib/components/variables/VariableTag.js +46 -39
  14. package/lib/components/variables/utils.d.ts +7 -0
  15. package/lib/components/variables/utils.js +42 -2
  16. package/lib/data-source/index.d.ts +7 -27
  17. package/lib/data-source/index.js +81 -51
  18. package/lib/executor/FlowExecutor.d.ts +2 -1
  19. package/lib/executor/FlowExecutor.js +163 -22
  20. package/lib/flowContext.d.ts +230 -7
  21. package/lib/flowContext.js +2267 -148
  22. package/lib/flowEngine.d.ts +21 -0
  23. package/lib/flowEngine.js +56 -8
  24. package/lib/flowI18n.js +6 -4
  25. package/lib/flowSettings.js +17 -11
  26. package/lib/index.d.ts +7 -1
  27. package/lib/index.js +21 -0
  28. package/lib/locale/en-US.json +9 -2
  29. package/lib/locale/index.d.ts +14 -0
  30. package/lib/locale/zh-CN.json +8 -1
  31. package/lib/models/CollectionFieldModel.d.ts +1 -0
  32. package/lib/models/CollectionFieldModel.js +3 -2
  33. package/lib/models/flowModel.js +12 -1
  34. package/lib/provider.js +5 -5
  35. package/lib/resources/baseRecordResource.d.ts +5 -0
  36. package/lib/resources/baseRecordResource.js +24 -0
  37. package/lib/resources/multiRecordResource.d.ts +1 -0
  38. package/lib/resources/multiRecordResource.js +11 -4
  39. package/lib/resources/singleRecordResource.js +2 -0
  40. package/lib/resources/sqlResource.d.ts +4 -3
  41. package/lib/resources/sqlResource.js +8 -3
  42. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
  43. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
  44. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
  45. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
  46. package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
  47. package/lib/runjs-context/contexts/base.js +706 -41
  48. package/lib/runjs-context/contributions.d.ts +33 -0
  49. package/lib/runjs-context/contributions.js +88 -0
  50. package/lib/runjs-context/helpers.js +12 -1
  51. package/lib/runjs-context/setup.js +6 -0
  52. package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
  53. package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
  54. package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
  55. package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
  56. package/lib/runjs-context/snippets/index.d.ts +11 -1
  57. package/lib/runjs-context/snippets/index.js +61 -40
  58. package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
  59. package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
  60. package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
  61. package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
  62. package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
  63. package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
  64. package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
  65. package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
  66. package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
  67. package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
  68. package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
  69. package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
  70. package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
  71. package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
  72. package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
  73. package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
  74. package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
  75. package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
  76. package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
  77. package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
  78. package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
  79. package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
  80. package/lib/runjsLibs.d.ts +28 -0
  81. package/lib/runjsLibs.js +532 -0
  82. package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
  83. package/lib/scheduler/ModelOperationScheduler.js +25 -21
  84. package/lib/types.d.ts +27 -0
  85. package/lib/utils/associationObjectVariable.d.ts +2 -2
  86. package/lib/utils/createCollectionContextMeta.js +1 -0
  87. package/lib/utils/createEphemeralContext.js +2 -2
  88. package/lib/utils/dateVariable.d.ts +16 -0
  89. package/lib/utils/dateVariable.js +380 -0
  90. package/lib/utils/exceptions.d.ts +7 -0
  91. package/lib/utils/exceptions.js +10 -0
  92. package/lib/utils/index.d.ts +8 -3
  93. package/lib/utils/index.js +45 -0
  94. package/lib/utils/params-resolvers.js +16 -9
  95. package/lib/utils/resolveModuleUrl.d.ts +58 -0
  96. package/lib/utils/resolveModuleUrl.js +65 -0
  97. package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
  98. package/lib/utils/resolveRunJSObjectValues.js +61 -0
  99. package/lib/utils/runjsModuleLoader.d.ts +58 -0
  100. package/lib/utils/runjsModuleLoader.js +422 -0
  101. package/lib/utils/runjsTemplateCompat.d.ts +35 -0
  102. package/lib/utils/runjsTemplateCompat.js +743 -0
  103. package/lib/utils/runjsValue.d.ts +29 -0
  104. package/lib/utils/runjsValue.js +275 -0
  105. package/lib/utils/safeGlobals.d.ts +18 -8
  106. package/lib/utils/safeGlobals.js +164 -17
  107. package/lib/utils/schema-utils.d.ts +10 -0
  108. package/lib/utils/schema-utils.js +61 -0
  109. package/lib/views/createViewMeta.d.ts +0 -7
  110. package/lib/views/createViewMeta.js +19 -70
  111. package/lib/views/index.d.ts +1 -2
  112. package/lib/views/index.js +4 -3
  113. package/lib/views/useDialog.js +7 -2
  114. package/lib/views/useDrawer.js +7 -2
  115. package/lib/views/usePage.d.ts +4 -0
  116. package/lib/views/usePage.js +43 -6
  117. package/lib/views/usePopover.js +4 -1
  118. package/lib/views/viewEvents.d.ts +17 -0
  119. package/lib/views/viewEvents.js +90 -0
  120. package/package.json +4 -4
  121. package/src/BlockScopedFlowEngine.ts +2 -5
  122. package/src/JSRunner.ts +44 -2
  123. package/src/ViewScopedFlowEngine.ts +4 -0
  124. package/src/__tests__/JSRunner.test.ts +64 -0
  125. package/src/__tests__/createViewMeta.popup.test.ts +62 -1
  126. package/src/__tests__/flowContext.test.ts +693 -1
  127. package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
  128. package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
  129. package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
  130. package/src/__tests__/flowRuntimeContext.test.ts +2 -1
  131. package/src/__tests__/flowSettings.open.test.tsx +123 -19
  132. package/src/__tests__/runjsContext.test.ts +10 -7
  133. package/src/__tests__/runjsContextImplementations.test.ts +34 -3
  134. package/src/__tests__/runjsContextRuntime.test.ts +3 -3
  135. package/src/__tests__/runjsContributions.test.ts +89 -0
  136. package/src/__tests__/runjsExternalLibs.test.ts +242 -0
  137. package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
  138. package/src/__tests__/runjsLocales.test.ts +4 -1
  139. package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
  140. package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
  141. package/src/__tests__/runjsSnippets.test.ts +40 -3
  142. package/src/acl/Acl.tsx +3 -3
  143. package/src/components/FlowContextSelector.tsx +208 -12
  144. package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
  145. package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
  146. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
  147. package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
  148. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +13 -2
  149. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
  150. package/src/components/variables/VariableInput.tsx +12 -4
  151. package/src/components/variables/VariableTag.tsx +54 -45
  152. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
  153. package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
  154. package/src/components/variables/__tests__/utils.test.ts +81 -3
  155. package/src/components/variables/utils.ts +67 -6
  156. package/src/data-source/index.ts +85 -110
  157. package/src/executor/FlowExecutor.ts +200 -23
  158. package/src/executor/__tests__/flowExecutor.test.ts +66 -0
  159. package/src/flowContext.ts +2986 -211
  160. package/src/flowEngine.ts +59 -8
  161. package/src/flowI18n.ts +7 -5
  162. package/src/flowSettings.ts +18 -12
  163. package/src/index.ts +14 -1
  164. package/src/locale/en-US.json +9 -2
  165. package/src/locale/zh-CN.json +8 -1
  166. package/src/models/CollectionFieldModel.tsx +3 -1
  167. package/src/models/__tests__/dispatchEvent.when.test.ts +554 -0
  168. package/src/models/__tests__/flowModel.test.ts +20 -4
  169. package/src/models/flowModel.tsx +13 -1
  170. package/src/provider.tsx +7 -6
  171. package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
  172. package/src/resources/__tests__/sqlResource.test.ts +60 -0
  173. package/src/resources/baseRecordResource.ts +31 -0
  174. package/src/resources/multiRecordResource.ts +11 -4
  175. package/src/resources/singleRecordResource.ts +3 -0
  176. package/src/resources/sqlResource.ts +11 -6
  177. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
  178. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
  179. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
  180. package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
  181. package/src/runjs-context/contexts/base.ts +715 -44
  182. package/src/runjs-context/contributions.ts +88 -0
  183. package/src/runjs-context/helpers.ts +11 -1
  184. package/src/runjs-context/setup.ts +6 -0
  185. package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
  186. package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
  187. package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
  188. package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
  189. package/src/runjs-context/snippets/index.ts +75 -41
  190. package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
  191. package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
  192. package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
  193. package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
  194. package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
  195. package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
  196. package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
  197. package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
  198. package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
  199. package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
  200. package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
  201. package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
  202. package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
  203. package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
  204. package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
  205. package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
  206. package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
  207. package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
  208. package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
  209. package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
  210. package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
  211. package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
  212. package/src/runjsLibs.ts +622 -0
  213. package/src/scheduler/ModelOperationScheduler.ts +27 -21
  214. package/src/types.ts +38 -1
  215. package/src/utils/__tests__/dateVariable.test.ts +101 -0
  216. package/src/utils/__tests__/params-resolvers.test.ts +40 -0
  217. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
  218. package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
  219. package/src/utils/__tests__/runjsValue.test.ts +44 -0
  220. package/src/utils/__tests__/safeGlobals.test.ts +57 -2
  221. package/src/utils/__tests__/utils.test.ts +95 -0
  222. package/src/utils/associationObjectVariable.ts +2 -2
  223. package/src/utils/createCollectionContextMeta.ts +1 -0
  224. package/src/utils/createEphemeralContext.ts +5 -4
  225. package/src/utils/dateVariable.ts +397 -0
  226. package/src/utils/exceptions.ts +11 -0
  227. package/src/utils/index.ts +37 -3
  228. package/src/utils/params-resolvers.ts +23 -9
  229. package/src/utils/resolveModuleUrl.ts +91 -0
  230. package/src/utils/resolveRunJSObjectValues.ts +46 -0
  231. package/src/utils/runjsModuleLoader.ts +553 -0
  232. package/src/utils/runjsTemplateCompat.ts +828 -0
  233. package/src/utils/runjsValue.ts +287 -0
  234. package/src/utils/safeGlobals.ts +188 -17
  235. package/src/utils/schema-utils.ts +79 -0
  236. package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
  237. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
  238. package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
  239. package/src/views/createViewMeta.ts +22 -75
  240. package/src/views/index.tsx +1 -2
  241. package/src/views/useDialog.tsx +8 -1
  242. package/src/views/useDrawer.tsx +8 -1
  243. package/src/views/usePage.tsx +51 -5
  244. package/src/views/usePopover.tsx +4 -1
  245. 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';
@@ -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,31 @@ 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
+ // Enable by default; use `preprocessTemplates: false` to explicitly disable.
3039
+ const shouldPreprocessTemplates = preprocessTemplates !== false;
3040
+ const jsCode = await prepareRunJsCode(String(code ?? ''), { preprocessTemplates: shouldPreprocessTemplates });
3041
+ return runner.run(jsCode);
3042
+ },
3043
+ );
3044
+ }
942
3045
  }
943
3046
 
944
3047
  class BaseFlowModelContext extends BaseFlowEngineContext {
@@ -956,7 +3059,16 @@ class BaseFlowModelContext extends BaseFlowEngineContext {
956
3059
  EventDefinition<TModel, TCtx>
957
3060
  >;
958
3061
  declare runAction: (actionName: string, params?: Record<string, any>) => Promise<any> | any;
3062
+ /**
3063
+ * @deprecated use `makeResource` instead
3064
+ */
959
3065
  declare createResource: <T extends FlowResource = FlowResource>(resourceType: ResourceType<T>) => T;
3066
+ /**
3067
+ * Create a new resource instance without adding it to the context.
3068
+ * @param resourceType - The resource type.
3069
+ * @returns The resource instance.
3070
+ */
3071
+ declare makeResource: <T extends FlowResource = FlowResource>(resourceType: ResourceType<T>) => T;
960
3072
  }
961
3073
 
962
3074
  export class FlowEngineContext extends BaseFlowEngineContext {
@@ -976,25 +3088,30 @@ export class FlowEngineContext extends BaseFlowEngineContext {
976
3088
  dataSourceManager.addDataSource(mainDataSource);
977
3089
  this.defineProperty('engine', {
978
3090
  value: this.engine,
3091
+ info: {
3092
+ description: 'FlowEngine instance.',
3093
+ detail: 'FlowEngine',
3094
+ },
979
3095
  });
980
3096
  this.defineProperty('sql', {
981
- get: () => new FlowSQLRepository(this),
3097
+ get: (ctx) => new FlowSQLRepository(ctx),
3098
+ cache: false,
3099
+ info: {
3100
+ description: 'SQL helper (FlowSQLRepository).',
3101
+ detail: 'FlowSQLRepository',
3102
+ },
982
3103
  });
983
3104
  this.defineProperty('dataSourceManager', {
984
3105
  value: dataSourceManager,
3106
+ info: {
3107
+ description: 'DataSourceManager instance.',
3108
+ detail: 'DataSourceManager',
3109
+ },
985
3110
  });
986
3111
  const i18n = new FlowI18n(this);
987
3112
  this.defineMethod('t', (keyOrTemplate: string, options?: any) => {
988
3113
  return i18n.translate(keyOrTemplate, options);
989
3114
  });
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
3115
  this.defineMethod('renderJson', function (template: any) {
999
3116
  return this.resolveJsonTemplate(template);
1000
3117
  });
@@ -1030,6 +3147,22 @@ export class FlowEngineContext extends BaseFlowEngineContext {
1030
3147
  const needServer = Object.keys(serverVarPaths).length > 0;
1031
3148
  let serverResolved = template;
1032
3149
  if (needServer) {
3150
+ const inferRecordRefWithMeta = (ctx: any): RecordRef | undefined => {
3151
+ const ref = inferRecordRef(ctx as any);
3152
+ if (ref) return ref as RecordRef;
3153
+ try {
3154
+ const tk = ctx?.resource?.getMeta?.('currentFilterByTk');
3155
+ if (typeof tk === 'undefined' || tk === null) return undefined;
3156
+ const collection =
3157
+ ctx?.collection?.name || ctx?.resource?.getResourceName?.()?.split?.('.')?.slice?.(-1)?.[0];
3158
+ if (!collection) return undefined;
3159
+ const dataSourceKey = ctx?.collection?.dataSourceKey || ctx?.resource?.getDataSourceKey?.();
3160
+ return { collection, dataSourceKey, filterByTk: tk } as RecordRef;
3161
+ } catch (_) {
3162
+ return undefined;
3163
+ }
3164
+ };
3165
+
1033
3166
  const collectFromMeta = async (): Promise<Record<string, any>> => {
1034
3167
  const out: Record<string, any> = {};
1035
3168
  try {
@@ -1069,7 +3202,62 @@ export class FlowEngineContext extends BaseFlowEngineContext {
1069
3202
  };
1070
3203
 
1071
3204
  const inputFromMeta = await collectFromMeta();
1072
- const autoInput = { ...inputFromMeta };
3205
+ const autoInput = { ...inputFromMeta } as Record<string, any>;
3206
+
3207
+ // Special-case: formValues
3208
+ // If server needs to resolve some formValues paths but meta params only cover association anchors
3209
+ // (e.g. formValues.customer) and some top-level paths are missing (e.g. formValues.status),
3210
+ // inject a top-level record anchor (formValues -> { collection, filterByTk, fields/appends }) so server can fetch DB values.
3211
+ // This anchor MUST be selective (fields/appends derived from serverVarPaths['formValues']) to avoid server overriding
3212
+ // client-only values for configured form fields in the same template.
3213
+ try {
3214
+ const varName = 'formValues';
3215
+ const neededPaths = serverVarPaths[varName] || [];
3216
+ if (neededPaths.length) {
3217
+ const requiredTop = new Set<string>();
3218
+ for (const p of neededPaths) {
3219
+ const top = topLevelOf(p);
3220
+ if (top) requiredTop.add(top);
3221
+ }
3222
+ const metaOut = inputFromMeta?.[varName];
3223
+ const builtTop = new Set<string>();
3224
+ if (metaOut && typeof metaOut === 'object' && !Array.isArray(metaOut) && !isRecordRefLike(metaOut)) {
3225
+ Object.keys(metaOut).forEach((k) => builtTop.add(k));
3226
+ }
3227
+
3228
+ const missing = [...requiredTop].filter((k) => !builtTop.has(k));
3229
+ if (missing.length) {
3230
+ const ref = inferRecordRefWithMeta(this);
3231
+ if (ref) {
3232
+ const { generatedFields, generatedAppends } = inferSelectsFromUsage(neededPaths);
3233
+ const recordRef: RecordRef = {
3234
+ ...ref,
3235
+ fields: generatedFields,
3236
+ appends: generatedAppends,
3237
+ };
3238
+
3239
+ // Preserve existing association anchors by lifting them to dotted keys before overwriting formValues
3240
+ const existing = autoInput[varName];
3241
+ if (
3242
+ existing &&
3243
+ typeof existing === 'object' &&
3244
+ !Array.isArray(existing) &&
3245
+ !isRecordRefLike(existing)
3246
+ ) {
3247
+ for (const [k, v] of Object.entries(existing)) {
3248
+ autoInput[`${varName}.${k}`] = v;
3249
+ }
3250
+ delete autoInput[varName];
3251
+ }
3252
+
3253
+ autoInput[varName] = recordRef;
3254
+ }
3255
+ }
3256
+ }
3257
+ } catch (_) {
3258
+ // ignore
3259
+ }
3260
+
1073
3261
  const autoContextParams = Object.keys(autoInput).length
1074
3262
  ? _buildServerContextParams(this, autoInput)
1075
3263
  : undefined;
@@ -1100,6 +3288,23 @@ export class FlowEngineContext extends BaseFlowEngineContext {
1100
3288
 
1101
3289
  return resolveExpressions(serverResolved, this);
1102
3290
  });
3291
+
3292
+ // Helper: resolve a single ctx expression value via resolveJsonTemplate behavior.
3293
+ // Example: await ctx.getVar('ctx.record.id')
3294
+ this.defineMethod(
3295
+ 'getVar',
3296
+ async function (this: BaseFlowEngineContext, varPath: string) {
3297
+ const raw = typeof varPath === 'string' ? varPath : String(varPath ?? '');
3298
+ const s = raw.trim();
3299
+ if (!s) return undefined;
3300
+ // Preferred input: 'ctx.xxx.yyy' (expression), consistent with envs.getVar outputs.
3301
+ if (s !== 'ctx' && !s.startsWith('ctx.')) {
3302
+ throw new Error(`ctx.getVar(path) expects an expression starting with "ctx.", got: "${s}"`);
3303
+ }
3304
+ return this.resolveJsonTemplate(`{{ ${s} }}` as any);
3305
+ },
3306
+ 'Resolve a ctx expression value by path (expression starts with "ctx.").',
3307
+ );
1103
3308
  this.defineProperty('requirejs', {
1104
3309
  get: () => this.app?.requirejs?.requirejs,
1105
3310
  });
@@ -1181,71 +3386,79 @@ export class FlowEngineContext extends BaseFlowEngineContext {
1181
3386
  user: this.user,
1182
3387
  }),
1183
3388
  });
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
- }
3389
+ this.defineProperty('date', {
3390
+ get: () => {
3391
+ const createBranch = (prefix: string[]) => {
3392
+ return new Proxy(
3393
+ {},
3394
+ {
3395
+ get: (_target, prop) => {
3396
+ if (typeof prop !== 'string') return undefined;
3397
+ const nextPath = [...prefix, prop];
3398
+ if (!isCtxDatePathPrefix(nextPath)) {
3399
+ return undefined;
3400
+ }
3401
+ const resolved = resolveCtxDatePath(nextPath);
3402
+ if (typeof resolved !== 'undefined') {
3403
+ return resolved;
3404
+ }
3405
+ return createBranch(nextPath);
3406
+ },
3407
+ },
3408
+ );
3409
+ };
1192
3410
 
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
- });
3411
+ return createBranch(['date']);
3412
+ },
3413
+ cache: false,
1200
3414
  });
3415
+ this.defineMethod(
3416
+ 'loadCSS',
3417
+ async (href: string) => {
3418
+ const url = resolveModuleUrl(href);
3419
+ return new Promise((resolve, reject) => {
3420
+ // Check if CSS is already loaded
3421
+ const existingLink = document.querySelector(`link[href="${url}"]`);
3422
+ if (existingLink) {
3423
+ resolve(null);
3424
+ return;
3425
+ }
3426
+
3427
+ const link = document.createElement('link');
3428
+ link.rel = 'stylesheet';
3429
+ link.href = url;
3430
+ link.onload = () => resolve(null);
3431
+ link.onerror = () => reject(new Error(`Failed to load CSS: ${url}`));
3432
+ document.head.appendChild(link);
3433
+ });
3434
+ },
3435
+ {
3436
+ description: 'Load a CSS file by URL (browser only).',
3437
+ params: [{ name: 'href', type: 'string', description: 'CSS URL.' }],
3438
+ returns: { type: 'Promise<void>' },
3439
+ completion: { insertText: "await ctx.loadCSS('https://example.com/style.css')" },
3440
+ examples: ["await ctx.loadCSS('https://example.com/style.css');"],
3441
+ },
3442
+ );
1201
3443
  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
- });
3444
+ // 判断是否为 CSS 文件(支持 example.css?v=123 等形式)
3445
+ if (isCssFile(url)) {
3446
+ return this.loadCSS(url);
3447
+ }
3448
+ const u = resolveModuleUrl(url, { raw: true });
3449
+ return await runjsRequireAsync(this.requirejs, u);
1215
3450
  });
1216
3451
  // 动态按 URL 加载 ESM 模块
1217
3452
  // - 使用 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;
3453
+ // - 通常返回模块命名空间对象(包含 default 与命名导出);
3454
+ // 若模块只有 default 一个导出,则会直接返回 default 值以提升易用性(无需再访问 .default)
3455
+ this.defineMethod('importAsync', async function (this: any, url: string) {
3456
+ // 判断是否为 CSS 文件(支持 example.css?v=123 等形式)
3457
+ if (isCssFile(url)) {
3458
+ return this.loadCSS(url);
3459
+ }
3460
+
3461
+ return await runjsImportModule(this, url, { importer: runjsImportAsync });
1249
3462
  });
1250
3463
  this.defineMethod('createJSRunner', async function (options?: JSRunnerOptions) {
1251
3464
  try {
@@ -1254,17 +3467,24 @@ export class FlowEngineContext extends BaseFlowEngineContext {
1254
3467
  } catch (_) {
1255
3468
  // ignore if setup is not available
1256
3469
  }
1257
- const version = (options?.version as any) || 'v1';
3470
+ const version = options?.version || 'v1';
1258
3471
  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 || {}) };
3472
+ const Ctor: new (delegate: any) => any = RunJSContextRegistry.resolve(version, modelClass) || FlowRunJSContext;
3473
+ const runCtx = new Ctor(this);
3474
+ runCtx.defineMethod('t', (key: string, options?: any) => {
3475
+ return this.t(key, { ns: 'runjs', ...options });
3476
+ });
3477
+
3478
+ let doc: RunJSDocMeta = {};
3479
+ try {
3480
+ const locale = (this as any)?.api?.auth?.locale || (this as any)?.i18n?.language || (this as any)?.locale;
3481
+ if ((Ctor as any)?.getDoc?.length) doc = (Ctor as any).getDoc(locale) || {};
3482
+ else doc = (Ctor as any)?.getDoc?.() || {};
3483
+ } catch (_) {
3484
+ doc = {};
3485
+ }
3486
+ const deprecatedCtx = createRunJSDeprecationProxy(runCtx, { doc });
3487
+ const globals: Record<string, any> = { ctx: deprecatedCtx, ...(options?.globals || {}) };
1268
3488
  const { timeoutMs } = options || {};
1269
3489
  return new JSRunner({ globals, timeoutMs });
1270
3490
  });
@@ -1282,57 +3502,6 @@ export class FlowEngineContext extends BaseFlowEngineContext {
1282
3502
  return this.engine.getEvents();
1283
3503
  });
1284
3504
 
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
3505
  this.defineMethod(
1337
3506
  'runAction',
1338
3507
  async function (this: BaseFlowEngineContext, actionName: string, params?: Record<string, any>) {
@@ -1375,17 +3544,34 @@ export class FlowEngineContext extends BaseFlowEngineContext {
1375
3544
  context: this.createProxy(),
1376
3545
  });
1377
3546
  });
3547
+ this.defineMethod('makeResource', function (this: BaseFlowEngineContext, resourceType) {
3548
+ return this.engine.createResource(resourceType, {
3549
+ context: this.createProxy(),
3550
+ });
3551
+ });
1378
3552
  // Provide useResource in base engine context so RunJS can call it directly
3553
+ this.defineMethod(
3554
+ 'initResource',
3555
+ function (
3556
+ this: BaseFlowEngineContext,
3557
+ className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource',
3558
+ ) {
3559
+ if (!this.has('resource')) {
3560
+ this.defineProperty('resource', {
3561
+ get: () => this.createResource(className),
3562
+ });
3563
+ }
3564
+ return this.resource;
3565
+ },
3566
+ );
3567
+ // @deprecated use `initResource` instead
1379
3568
  this.defineMethod(
1380
3569
  'useResource',
1381
3570
  function (
1382
3571
  this: BaseFlowEngineContext,
1383
3572
  className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource',
1384
3573
  ) {
1385
- if (this.has('resource')) return;
1386
- this.defineProperty('resource', {
1387
- get: () => this.createResource(className),
1388
- });
3574
+ return this.initResource(className);
1389
3575
  },
1390
3576
  );
1391
3577
  }
@@ -1401,15 +3587,12 @@ export class FlowModelContext extends BaseFlowModelContext {
1401
3587
  this.defineMethod('onRefReady', (ref, cb, timeout) => {
1402
3588
  this.engine.reactView.onRefReady(ref, cb, timeout);
1403
3589
  });
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
3590
  this.defineProperty('model', {
1412
3591
  value: model,
3592
+ info: {
3593
+ description: 'Current FlowModel instance.',
3594
+ detail: 'FlowModel',
3595
+ },
1413
3596
  });
1414
3597
  // 提供稳定的 ref 实例,确保渲染端与运行时上下文使用同一对象
1415
3598
  const stableRef = createRef<HTMLDivElement>();
@@ -1418,6 +3601,10 @@ export class FlowModelContext extends BaseFlowModelContext {
1418
3601
  this.model['_refCreated'] = true;
1419
3602
  return stableRef;
1420
3603
  },
3604
+ info: {
3605
+ description: 'Stable React ref for the view container.',
3606
+ detail: 'React.RefObject<HTMLDivElement>',
3607
+ },
1421
3608
  });
1422
3609
  this.defineMethod('openView', async function (uid: string, options) {
1423
3610
  const opts = { ...options };
@@ -1478,8 +3665,17 @@ export class FlowModelContext extends BaseFlowModelContext {
1478
3665
  engineCtx: this.engine.context,
1479
3666
  };
1480
3667
  model.context.defineProperty('view', { value: pendingView });
3668
+ // 默认按 click 打开,但兼容 popupSettings 绑定到其他事件(例如 DuplicateActionModel 监听 openDuplicatePopup)。
3669
+ const popupFlow = model.getFlow?.('popupSettings');
3670
+ const on = (popupFlow as any)?.on;
3671
+ let openEventName = 'click';
3672
+ if (typeof on === 'string' && on) {
3673
+ openEventName = on;
3674
+ } else if (on && typeof on === 'object' && typeof (on as any).eventName === 'string' && (on as any).eventName) {
3675
+ openEventName = (on as any).eventName;
3676
+ }
1481
3677
  await model.dispatchEvent(
1482
- 'click',
3678
+ openEventName,
1483
3679
  {
1484
3680
  // navigation: false, // TODO: 路由模式有bug,不支持多层同样viewId的弹窗,因此这里默认先用false
1485
3681
  // ...this.model?.['getInputArgs']?.(), // 避免部分关系字段信息丢失, 仿照 ClickableCollectionField 做法
@@ -1538,12 +3734,16 @@ export class FlowForkModelContext extends BaseFlowModelContext {
1538
3734
  throw new Error('Invalid FlowModel instance');
1539
3735
  }
1540
3736
  super();
1541
- this.addDelegate((this.master as any).context);
3737
+ this.addDelegate(this.master.context);
1542
3738
  this.defineMethod('onRefReady', (ref, cb, timeout) => {
1543
3739
  this.engine.reactView.onRefReady(ref, cb, timeout);
1544
3740
  });
1545
3741
  this.defineProperty('model', {
1546
3742
  get: () => this.fork,
3743
+ info: {
3744
+ description: 'Current ForkFlowModel instance (as model).',
3745
+ detail: 'ForkFlowModel',
3746
+ },
1547
3747
  });
1548
3748
  // 提供稳定的 ref 实例,确保渲染端与运行时上下文使用同一对象
1549
3749
  const stableRef = createRef<HTMLDivElement>();
@@ -1552,13 +3752,10 @@ export class FlowForkModelContext extends BaseFlowModelContext {
1552
3752
  this.fork['_refCreated'] = true;
1553
3753
  return stableRef;
1554
3754
  },
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);
3755
+ info: {
3756
+ description: 'Stable React ref for the view container.',
3757
+ detail: 'React.RefObject<HTMLDivElement>',
3758
+ },
1562
3759
  });
1563
3760
  }
1564
3761
  }
@@ -1569,7 +3766,19 @@ export class FlowRuntimeContext<
1569
3766
  > extends BaseFlowModelContext {
1570
3767
  declare steps: Record<string, { params: Record<string, any>; uiSchema?: any; result?: any }>;
1571
3768
  stepResults: Record<string, any> = {};
1572
- declare useResource: (className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource') => void;
3769
+ /**
3770
+ * @deprecated use `initResource` instead
3771
+ */
3772
+ declare useResource: (
3773
+ className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource',
3774
+ ) => void;
3775
+ /**
3776
+ * Initialize a resource instance without adding it to the context.
3777
+ * @param className - The resource class name.
3778
+ */
3779
+ declare initResource: (
3780
+ className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource',
3781
+ ) => void;
1573
3782
  declare getStepParams: (stepKey: string) => Record<string, any>;
1574
3783
  declare setStepParams: (stepKey: string, params?: any) => void;
1575
3784
  declare getStepResults: (stepKey: string) => any;
@@ -1591,15 +3800,15 @@ export class FlowRuntimeContext<
1591
3800
  return _.get(this.steps, [stepKey, 'result']);
1592
3801
  });
1593
3802
  this.defineMethod(
1594
- 'useResource',
3803
+ 'initResource',
1595
3804
  (className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource') => {
1596
3805
  if (model.context.has('resource')) {
1597
- console.warn(`[FlowRuntimeContext] useResource - resource already defined in context: ${className}`);
3806
+ console.log(`[FlowRuntimeContext] useResource - resource already defined in context: ${className}`);
1598
3807
  return;
1599
3808
  }
1600
3809
  model.context.defineProperty('resource', {
1601
3810
  get: () => {
1602
- return this.createResource(className);
3811
+ return this.makeResource(className);
1603
3812
  },
1604
3813
  });
1605
3814
  if (!model['resource']) {
@@ -1607,6 +3816,13 @@ export class FlowRuntimeContext<
1607
3816
  }
1608
3817
  },
1609
3818
  );
3819
+ // @deprecated use `initResource` instead
3820
+ this.defineMethod(
3821
+ 'useResource',
3822
+ (className: 'APIResource' | 'SingleRecordResource' | 'MultiRecordResource' | 'SQLResource') => {
3823
+ return this.initResource(className);
3824
+ },
3825
+ );
1610
3826
  this.defineProperty('resource', {
1611
3827
  get: () => model['resource'] || model.context['resource'],
1612
3828
  cache: false,
@@ -1614,13 +3830,6 @@ export class FlowRuntimeContext<
1614
3830
  this.defineMethod('onRefReady', (ref, cb, timeout) => {
1615
3831
  this.engine.reactView.onRefReady(ref, cb, timeout);
1616
3832
  });
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
3833
  }
1625
3834
 
1626
3835
  protected _getOwnProperty(key: string): any {
@@ -1654,7 +3863,7 @@ export class FlowRuntimeContext<
1654
3863
  }
1655
3864
 
1656
3865
  exit() {
1657
- throw new FlowExitException(this.flowKey, this.model.uid);
3866
+ throw new FlowExitAllException(this.flowKey, this.model.uid);
1658
3867
  }
1659
3868
 
1660
3869
  exitAll() {
@@ -1673,6 +3882,16 @@ export type RunJSDocCompletionDoc = {
1673
3882
  insertText?: string;
1674
3883
  };
1675
3884
 
3885
+ export type RunJSDocHiddenDoc = boolean | ((ctx: any) => boolean | Promise<boolean>);
3886
+
3887
+ // `hidden` is the single visibility entrypoint for RunJSDoc property docs:
3888
+ // - boolean: hide the whole node and its subtree
3889
+ // - string[]: hide specific subpaths under the node (relative dot-paths)
3890
+ export type RunJSDocHiddenOrPathsDoc =
3891
+ | boolean
3892
+ | string[]
3893
+ | ((ctx: any) => boolean | string[] | Promise<boolean | string[]>);
3894
+
1676
3895
  export type RunJSDocPropertyDoc =
1677
3896
  | string
1678
3897
  | {
@@ -1681,7 +3900,14 @@ export type RunJSDocPropertyDoc =
1681
3900
  type?: string;
1682
3901
  examples?: string[];
1683
3902
  completion?: RunJSDocCompletionDoc;
3903
+ ref?: FlowContextDocRef;
3904
+ deprecated?: FlowDeprecationDoc;
3905
+ params?: FlowContextDocParam[];
3906
+ returns?: FlowContextDocReturn;
1684
3907
  properties?: Record<string, RunJSDocPropertyDoc>;
3908
+ hidden?: RunJSDocHiddenOrPathsDoc;
3909
+ disabled?: boolean | ((ctx: any) => boolean | Promise<boolean>);
3910
+ disabledReason?: string | ((ctx: any) => string | undefined | Promise<string | undefined>);
1685
3911
  };
1686
3912
 
1687
3913
  export type RunJSDocMethodDoc =
@@ -1691,6 +3917,13 @@ export type RunJSDocMethodDoc =
1691
3917
  detail?: string;
1692
3918
  examples?: string[];
1693
3919
  completion?: RunJSDocCompletionDoc;
3920
+ ref?: FlowContextDocRef;
3921
+ deprecated?: FlowDeprecationDoc;
3922
+ params?: FlowContextDocParam[];
3923
+ returns?: FlowContextDocReturn;
3924
+ hidden?: RunJSDocHiddenDoc;
3925
+ disabled?: boolean | ((ctx: any) => boolean | Promise<boolean>);
3926
+ disabledReason?: string | ((ctx: any) => string | undefined | Promise<string | undefined>);
1694
3927
  };
1695
3928
 
1696
3929
  export type RunJSDocMeta = {
@@ -1717,13 +3950,518 @@ function __runjsDeepMerge(base: any, patch: any) {
1717
3950
  }
1718
3951
  return out;
1719
3952
  }
3953
+
3954
+ function __isPlainObject(val: any): val is Record<string, any> {
3955
+ return !!val && typeof val === 'object' && !Array.isArray(val);
3956
+ }
3957
+
3958
+ type RunJSDeprecatedTreeNode = {
3959
+ deprecated?: FlowDeprecationDoc;
3960
+ children?: Record<string, RunJSDeprecatedTreeNode>;
3961
+ };
3962
+
3963
+ function __isPromiseLike(v: any): v is Promise<any> {
3964
+ return !!v && (typeof v === 'object' || typeof v === 'function') && typeof (v as any).then === 'function';
3965
+ }
3966
+
3967
+ function __normalizeDeprecationDoc(v: any): FlowDeprecationDoc | undefined {
3968
+ if (v === true) return true;
3969
+ if (!v) return undefined;
3970
+ if (__isPlainObject(v)) return v as any;
3971
+ return undefined;
3972
+ }
3973
+
3974
+ function __addDeprecatedPath(root: RunJSDeprecatedTreeNode, path: string[], deprecated: FlowDeprecationDoc) {
3975
+ if (!Array.isArray(path) || !path.length) return;
3976
+ let cur = root;
3977
+ for (const seg of path) {
3978
+ if (!seg) return;
3979
+ cur.children = cur.children || {};
3980
+ cur.children[seg] = cur.children[seg] || {};
3981
+ cur = cur.children[seg];
3982
+ }
3983
+ cur.deprecated = deprecated;
3984
+ }
3985
+
3986
+ function __mergeDeprecatedTree(base: RunJSDeprecatedTreeNode, patch: RunJSDeprecatedTreeNode) {
3987
+ if (patch.deprecated !== undefined) base.deprecated = patch.deprecated;
3988
+ const pChildren = patch.children || {};
3989
+ const keys = Object.keys(pChildren);
3990
+ if (!keys.length) return;
3991
+ base.children = base.children || {};
3992
+ for (const k of keys) {
3993
+ base.children[k] = base.children[k] || {};
3994
+ __mergeDeprecatedTree(base.children[k], pChildren[k]);
3995
+ }
3996
+ }
3997
+
3998
+ function __buildDeprecatedTreeFromRunJSDoc(doc?: RunJSDocMeta): RunJSDeprecatedTreeNode {
3999
+ const root: RunJSDeprecatedTreeNode = {};
4000
+ if (!doc) return root;
4001
+
4002
+ const walkProps = (props: any, parentPath: string[]) => {
4003
+ if (!__isPlainObject(props)) return;
4004
+ for (const [key, raw] of Object.entries(props)) {
4005
+ if (!key) continue;
4006
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
4007
+ const node = raw as any;
4008
+ const dep = __normalizeDeprecationDoc(node.deprecated);
4009
+ if (dep) __addDeprecatedPath(root, [...parentPath, key], dep);
4010
+ if (__isPlainObject(node.properties)) {
4011
+ walkProps(node.properties, [...parentPath, key]);
4012
+ }
4013
+ }
4014
+ };
4015
+
4016
+ const walkMethods = (methods: any) => {
4017
+ if (!__isPlainObject(methods)) return;
4018
+ for (const [key, raw] of Object.entries(methods)) {
4019
+ if (!key) continue;
4020
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
4021
+ const node = raw as any;
4022
+ const dep = __normalizeDeprecationDoc(node.deprecated);
4023
+ if (dep) __addDeprecatedPath(root, [key], dep);
4024
+ }
4025
+ };
4026
+
4027
+ walkProps((doc as any).properties, []);
4028
+ walkMethods((doc as any).methods);
4029
+
4030
+ return root;
4031
+ }
4032
+
4033
+ function __buildDeprecatedTreeFromFlowContextInfos(ctx: any): RunJSDeprecatedTreeNode {
4034
+ const root: RunJSDeprecatedTreeNode = {};
4035
+ const visited = new WeakSet<any>();
4036
+
4037
+ const collectInfoProperties = (basePath: string[], props: any) => {
4038
+ if (!__isPlainObject(props)) return;
4039
+ for (const [key, raw] of Object.entries(props)) {
4040
+ if (!key) continue;
4041
+ if (typeof raw === 'string') continue;
4042
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
4043
+ const node = raw as any;
4044
+ const dep = __normalizeDeprecationDoc(node.deprecated);
4045
+ if (dep) __addDeprecatedPath(root, [...basePath, key], dep);
4046
+ if (__isPlainObject(node.properties)) {
4047
+ collectInfoProperties([...basePath, key], node.properties);
4048
+ }
4049
+ }
4050
+ };
4051
+
4052
+ const walk = (c: any) => {
4053
+ if (!c || (typeof c !== 'object' && typeof c !== 'function')) return;
4054
+ if (visited.has(c)) return;
4055
+ visited.add(c);
4056
+
4057
+ const methodInfos = (c as any)._methodInfos;
4058
+ if (__isPlainObject(methodInfos)) {
4059
+ for (const [name, info] of Object.entries(methodInfos)) {
4060
+ if (!name) continue;
4061
+ if (!info || typeof info !== 'object' || Array.isArray(info)) continue;
4062
+ const dep = __normalizeDeprecationDoc((info as any).deprecated);
4063
+ if (dep) __addDeprecatedPath(root, [name], dep);
4064
+ }
4065
+ }
4066
+
4067
+ const props = (c as any)._props;
4068
+ if (__isPlainObject(props)) {
4069
+ for (const [name, opt] of Object.entries(props)) {
4070
+ if (!name) continue;
4071
+ const info = (opt as any)?.info;
4072
+ if (!info || typeof info !== 'object' || Array.isArray(info)) continue;
4073
+ const dep = __normalizeDeprecationDoc((info as any).deprecated);
4074
+ if (dep) __addDeprecatedPath(root, [name], dep);
4075
+ if (__isPlainObject((info as any).properties)) {
4076
+ collectInfoProperties([name], (info as any).properties);
4077
+ }
4078
+ }
4079
+ }
4080
+
4081
+ const delegates = (c as any)._delegates;
4082
+ if (Array.isArray(delegates)) {
4083
+ for (const d of delegates) walk(d);
4084
+ }
4085
+ };
4086
+
4087
+ walk(ctx);
4088
+ return root;
4089
+ }
4090
+
4091
+ export function createRunJSDeprecationProxy(
4092
+ ctx: any,
4093
+ options: {
4094
+ doc?: RunJSDocMeta;
4095
+ } = {},
4096
+ ) {
4097
+ const fromDoc = __buildDeprecatedTreeFromRunJSDoc(options.doc);
4098
+ const fromInfo = __buildDeprecatedTreeFromFlowContextInfos(ctx);
4099
+ __mergeDeprecatedTree(fromDoc, fromInfo);
4100
+
4101
+ const warned = new Set<string>();
4102
+ const proxyToTarget = new WeakMap<object, object>();
4103
+ const objectProxyCache = new WeakMap<object, Map<string, any>>();
4104
+ const functionProxyCache = new WeakMap<Function, Map<string, any>>();
4105
+
4106
+ const extractRunJSLocation = (
4107
+ stack?: string,
4108
+ ): { line?: number; column?: number; rawLine?: number; rawColumn?: number } => {
4109
+ if (!stack || typeof stack !== 'string') return {};
4110
+ const WRAPPER_PREFIX_LINES = 2; // JSRunner.run wraps user code with 2 lines before `${code}`
4111
+ const lines = stack.split('\n');
4112
+ for (const l of lines) {
4113
+ if (!l) continue;
4114
+ const m = l.match(/<anonymous>:(\d+):(\d+)/);
4115
+ if (!m) continue;
4116
+ const rawLine = Number(m[1]);
4117
+ const rawColumn = Number(m[2]);
4118
+ const line =
4119
+ Number.isFinite(rawLine) && rawLine > WRAPPER_PREFIX_LINES ? rawLine - WRAPPER_PREFIX_LINES : rawLine;
4120
+ const column = Number.isFinite(rawColumn) ? rawColumn : undefined;
4121
+ return { line, column, rawLine, rawColumn };
4122
+ }
4123
+ return {};
4124
+ };
4125
+
4126
+ const collectInfoProperties = (basePath: string[], props: any) => {
4127
+ if (!__isPlainObject(props)) return;
4128
+ for (const [key, raw] of Object.entries(props)) {
4129
+ if (!key) continue;
4130
+ if (typeof raw === 'string') continue;
4131
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue;
4132
+ const node = raw as any;
4133
+ const dep = __normalizeDeprecationDoc(node.deprecated);
4134
+ if (dep) __addDeprecatedPath(fromDoc, [...basePath, key], dep);
4135
+ if (__isPlainObject(node.properties)) {
4136
+ collectInfoProperties([...basePath, key], node.properties);
4137
+ }
4138
+ }
4139
+ };
4140
+
4141
+ const updateTreeFromDefineProperty = (name: string, options: any) => {
4142
+ if (!name) return;
4143
+ const info = options?.info;
4144
+ if (!info || typeof info !== 'object' || Array.isArray(info)) return;
4145
+ const dep = __normalizeDeprecationDoc((info as any).deprecated);
4146
+ if (dep) __addDeprecatedPath(fromDoc, [name], dep);
4147
+ if (__isPlainObject((info as any).properties)) {
4148
+ collectInfoProperties([name], (info as any).properties);
4149
+ }
4150
+ };
4151
+
4152
+ const updateTreeFromDefineMethod = (name: string, info: any) => {
4153
+ if (!name) return;
4154
+ if (!info || typeof info !== 'object' || Array.isArray(info)) return;
4155
+ const dep = __normalizeDeprecationDoc((info as any).deprecated);
4156
+ if (dep) __addDeprecatedPath(fromDoc, [name], dep);
4157
+ };
4158
+
4159
+ const unwrapProxy = (val: any) => {
4160
+ let cur = val;
4161
+ while (cur && (typeof cur === 'object' || typeof cur === 'function')) {
4162
+ const mapped = proxyToTarget.get(cur as any);
4163
+ if (!mapped) break;
4164
+ cur = mapped;
4165
+ }
4166
+ return cur;
4167
+ };
4168
+
4169
+ const formatReplacedBy = (replacedBy: any): string | undefined => {
4170
+ if (!replacedBy) return undefined;
4171
+ if (typeof replacedBy === 'string') return replacedBy.trim() || undefined;
4172
+ if (Array.isArray(replacedBy)) {
4173
+ const parts = replacedBy.map((x) => (typeof x === 'string' ? x.trim() : '')).filter(Boolean);
4174
+ return parts.length ? parts.join(', ') : undefined;
4175
+ }
4176
+ return undefined;
4177
+ };
4178
+
4179
+ const warnOnce = (apiPath: string, deprecated: FlowDeprecationDoc, stack?: string) => {
4180
+ if (!apiPath) return;
4181
+ if (warned.has(apiPath)) return;
4182
+ warned.add(apiPath);
4183
+
4184
+ const logger = (ctx as any)?.logger;
4185
+ const t =
4186
+ typeof (ctx as any)?.t === 'function'
4187
+ ? (key: string, options?: any) =>
4188
+ (ctx as any).t(key, { ns: [FLOW_ENGINE_NAMESPACE, 'client'], nsMode: 'fallback', ...options })
4189
+ : (key: string, options?: any) => {
4190
+ const fallback = options?.defaultValue ?? key;
4191
+ if (typeof fallback !== 'string' || !options) return fallback;
4192
+ // lightweight interpolation for fallback strings (i18next-style: {{var}})
4193
+ return fallback.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_m, k) => {
4194
+ const v = options?.[k];
4195
+ return typeof v === 'string' || typeof v === 'number' ? String(v) : '';
4196
+ });
4197
+ };
4198
+ const meta = typeof deprecated === 'object' && deprecated ? deprecated : {};
4199
+ const replacedBy = formatReplacedBy((meta as any).replacedBy);
4200
+ const since = typeof (meta as any).since === 'string' ? String((meta as any).since) : undefined;
4201
+ const removedIn = typeof (meta as any).removedIn === 'string' ? String((meta as any).removedIn) : undefined;
4202
+ const message = typeof (meta as any).message === 'string' ? String((meta as any).message) : '';
4203
+
4204
+ const loc = extractRunJSLocation(stack);
4205
+
4206
+ const locText = loc.line ? `(line ${loc.line}${loc.column ? `:${loc.column}` : ''})` : '';
4207
+
4208
+ const msg = message.trim();
4209
+ const mainText = msg
4210
+ ? t('RunJS deprecated warning with message', {
4211
+ defaultValue: '[RunJS][Deprecated] {{api}} {{message}}{{location}}',
4212
+ api: apiPath,
4213
+ message: msg,
4214
+ location: locText,
4215
+ })
4216
+ : t('RunJS deprecated warning', {
4217
+ defaultValue: '[RunJS][Deprecated] {{api}} is deprecated{{location}}',
4218
+ api: apiPath,
4219
+ location: locText,
4220
+ });
4221
+
4222
+ const separator = t('RunJS deprecated separator', { defaultValue: '; ' });
4223
+ const textParts: string[] = [mainText];
4224
+ if (replacedBy)
4225
+ textParts.push(t('RunJS deprecated replacedBy', { defaultValue: 'Use {{replacedBy}} instead', replacedBy }));
4226
+ if (since) textParts.push(t('RunJS deprecated since', { defaultValue: 'since {{since}}', since }));
4227
+ if (removedIn)
4228
+ textParts.push(t('RunJS deprecated removedIn', { defaultValue: 'will be removed in {{removedIn}}', removedIn }));
4229
+ const text = textParts.filter(Boolean).join(separator);
4230
+
4231
+ try {
4232
+ if (logger && typeof logger.warn === 'function') {
4233
+ logger.warn(text);
4234
+ } else {
4235
+ // fail-open: avoid breaking runjs execution when logger is missing
4236
+ console.warn(text);
4237
+ }
4238
+ } catch (_) {
4239
+ // ignore logger failures
4240
+ }
4241
+ };
4242
+
4243
+ const createFunctionProxy = (fn: Function, node: RunJSDeprecatedTreeNode, path: string) => {
4244
+ const dep = node.deprecated;
4245
+ if (!dep) return fn;
4246
+
4247
+ const cacheByPath = functionProxyCache.get(fn) || new Map<string, any>();
4248
+ functionProxyCache.set(fn, cacheByPath);
4249
+ if (cacheByPath.has(path)) return cacheByPath.get(path);
4250
+
4251
+ const proxied = new Proxy(fn, {
4252
+ apply(target, thisArg, argArray) {
4253
+ const stack = warned.has(path) ? undefined : new Error().stack;
4254
+ warnOnce(path, dep, stack);
4255
+ const realThis = unwrapProxy(thisArg);
4256
+ return Reflect.apply(target, realThis, argArray);
4257
+ },
4258
+ get(target, key, receiver) {
4259
+ return Reflect.get(target, key, receiver);
4260
+ },
4261
+ });
4262
+
4263
+ cacheByPath.set(path, proxied);
4264
+ return proxied as any;
4265
+ };
4266
+
4267
+ const createObjectProxy = (target: any, node: RunJSDeprecatedTreeNode, path: string): any => {
4268
+ if (!target || (typeof target !== 'object' && typeof target !== 'function')) return target;
4269
+ if (__isPromiseLike(target)) return target;
4270
+ const hasChildren = !!node.children && Object.keys(node.children).length > 0;
4271
+ if (!hasChildren && path !== 'ctx') return target;
4272
+
4273
+ const cacheByPath = objectProxyCache.get(target) || new Map<string, any>();
4274
+ objectProxyCache.set(target, cacheByPath);
4275
+ if (cacheByPath.has(path)) return cacheByPath.get(path);
4276
+
4277
+ const proxied = new Proxy(target, {
4278
+ get(t, key, receiver) {
4279
+ if (typeof key === 'symbol') {
4280
+ return Reflect.get(t, key, unwrapProxy(receiver));
4281
+ }
4282
+ const prop = String(key);
4283
+ const value = Reflect.get(t, key, unwrapProxy(receiver));
4284
+
4285
+ // Support dynamic deprecation registration via ctx.defineProperty/defineMethod during RunJS execution.
4286
+ // - This is especially useful when the deprecated API is introduced after JSRunner is created.
4287
+ if (path === 'ctx' && prop === 'defineProperty' && typeof value === 'function') {
4288
+ return (...args: any[]) => {
4289
+ const result = value(...args);
4290
+ try {
4291
+ updateTreeFromDefineProperty(String(args?.[0] ?? ''), args?.[1]);
4292
+ } catch (_) {
4293
+ // ignore
4294
+ }
4295
+ return result;
4296
+ };
4297
+ }
4298
+ if (path === 'ctx' && prop === 'defineMethod' && typeof value === 'function') {
4299
+ return (...args: any[]) => {
4300
+ const result = value(...args);
4301
+ try {
4302
+ updateTreeFromDefineMethod(String(args?.[0] ?? ''), args?.[2]);
4303
+ } catch (_) {
4304
+ // ignore
4305
+ }
4306
+ return result;
4307
+ };
4308
+ }
4309
+
4310
+ const child = node.children?.[prop];
4311
+ if (!child) return value;
4312
+
4313
+ const childPath = `${path}.${prop}`;
4314
+ if (typeof value === 'function' && child.deprecated) {
4315
+ return createFunctionProxy(value, child, childPath);
4316
+ }
4317
+ if (child.deprecated) {
4318
+ // For non-callable APIs, "use" happens on access (there is no apply step).
4319
+ const stack = warned.has(childPath) ? undefined : new Error().stack;
4320
+ warnOnce(childPath, child.deprecated, stack);
4321
+ }
4322
+ if (value && (typeof value === 'object' || typeof value === 'function') && child.children) {
4323
+ return createObjectProxy(value, child, childPath);
4324
+ }
4325
+ return value;
4326
+ },
4327
+ has(t, key) {
4328
+ return Reflect.has(t, key);
4329
+ },
4330
+ });
4331
+
4332
+ proxyToTarget.set(proxied as any, target);
4333
+ cacheByPath.set(path, proxied);
4334
+ return proxied;
4335
+ };
4336
+
4337
+ return createObjectProxy(ctx, fromDoc, 'ctx');
4338
+ }
4339
+
4340
+ function __mergeRunJSDocDocRecord(base: any, patch: any, mergeDoc: (b: any, p: any) => any): any {
4341
+ if (!__isPlainObject(patch)) return base;
4342
+ // Important: preserve `null` markers when base is not an object (e.g. child class wants to delete parent keys).
4343
+ // If we eagerly delete them here, the deletion intent is lost for later merges in the inheritance chain.
4344
+ if (!__isPlainObject(base)) return patch;
4345
+ const out: any = { ...base };
4346
+ for (const k of Object.keys(patch)) {
4347
+ const pv = patch[k];
4348
+ if (pv === null) {
4349
+ delete out[k];
4350
+ continue;
4351
+ }
4352
+ const bv = __isPlainObject(base) ? base[k] : undefined;
4353
+ const merged = mergeDoc(bv, pv);
4354
+ if (typeof merged === 'undefined') delete out[k];
4355
+ else out[k] = merged;
4356
+ }
4357
+ return out;
4358
+ }
4359
+
4360
+ function __mergeRunJSDocPropertyDoc(base: any, patch: any): any {
4361
+ if (patch === null) return undefined;
4362
+
4363
+ const baseIsObj = __isPlainObject(base);
4364
+ const patchIsObj = __isPlainObject(patch);
4365
+ const baseIsStr = typeof base === 'string';
4366
+ const patchIsStr = typeof patch === 'string';
4367
+
4368
+ // Treat string docs as { description: string } when merging with object docs,
4369
+ // to avoid "whole replacement" that drops base hidden/properties/completion.
4370
+ if (patchIsStr) {
4371
+ if (baseIsObj) {
4372
+ return __mergeRunJSDocPropertyDoc(base, { description: patch });
4373
+ }
4374
+ return patch;
4375
+ }
4376
+
4377
+ if (patchIsObj) {
4378
+ const baseObj: any = baseIsObj ? base : baseIsStr ? { description: base } : undefined;
4379
+ const out: any = { ...(baseObj || {}) };
4380
+ for (const k of Object.keys(patch)) {
4381
+ if (k === 'properties') {
4382
+ const pv = (patch as any).properties;
4383
+ if (pv === null) {
4384
+ delete out.properties;
4385
+ continue;
4386
+ }
4387
+ const mergedProps = __mergeRunJSDocDocRecord(baseObj?.properties, pv, __mergeRunJSDocPropertyDoc);
4388
+ if (typeof mergedProps === 'undefined') delete out.properties;
4389
+ else out.properties = mergedProps;
4390
+ continue;
4391
+ }
4392
+ const mergedVal = __runjsDeepMerge(baseObj?.[k], (patch as any)[k]);
4393
+ if (typeof mergedVal === 'undefined') delete out[k];
4394
+ else out[k] = mergedVal;
4395
+ }
4396
+ return out;
4397
+ }
4398
+
4399
+ return patch ?? base;
4400
+ }
4401
+
4402
+ function __mergeRunJSDocMethodDoc(base: any, patch: any): any {
4403
+ if (patch === null) return undefined;
4404
+
4405
+ const baseIsObj = __isPlainObject(base);
4406
+ const patchIsObj = __isPlainObject(patch);
4407
+ const baseIsStr = typeof base === 'string';
4408
+ const patchIsStr = typeof patch === 'string';
4409
+
4410
+ if (patchIsStr) {
4411
+ if (baseIsObj) {
4412
+ return __mergeRunJSDocMethodDoc(base, { description: patch });
4413
+ }
4414
+ return patch;
4415
+ }
4416
+
4417
+ if (patchIsObj) {
4418
+ const baseObj: any = baseIsObj ? base : baseIsStr ? { description: base } : undefined;
4419
+ const out: any = { ...(baseObj || {}) };
4420
+ for (const k of Object.keys(patch)) {
4421
+ const mergedVal = __runjsDeepMerge(baseObj?.[k], (patch as any)[k]);
4422
+ if (typeof mergedVal === 'undefined') delete out[k];
4423
+ else out[k] = mergedVal;
4424
+ }
4425
+ return out;
4426
+ }
4427
+
4428
+ return patch ?? base;
4429
+ }
4430
+
4431
+ function __mergeRunJSDocMeta(base: any, patch: any): RunJSDocMeta {
4432
+ const baseObj: any = __isPlainObject(base) ? base : {};
4433
+ const patchObj: any = __isPlainObject(patch) ? patch : {};
4434
+ const out: any = { ...baseObj };
4435
+
4436
+ for (const k of Object.keys(patchObj)) {
4437
+ if (k === 'properties') {
4438
+ const mergedProps = __mergeRunJSDocDocRecord(baseObj.properties, patchObj.properties, __mergeRunJSDocPropertyDoc);
4439
+ if (typeof mergedProps === 'undefined') delete out.properties;
4440
+ else out.properties = mergedProps;
4441
+ continue;
4442
+ }
4443
+ if (k === 'methods') {
4444
+ const mergedMethods = __mergeRunJSDocDocRecord(baseObj.methods, patchObj.methods, __mergeRunJSDocMethodDoc);
4445
+ if (typeof mergedMethods === 'undefined') delete out.methods;
4446
+ else out.methods = mergedMethods;
4447
+ continue;
4448
+ }
4449
+ const mergedVal = __runjsDeepMerge(baseObj[k], patchObj[k]);
4450
+ if (typeof mergedVal === 'undefined') delete out[k];
4451
+ else out[k] = mergedVal;
4452
+ }
4453
+
4454
+ return out as RunJSDocMeta;
4455
+ }
1720
4456
  export class FlowRunJSContext extends FlowContext {
1721
4457
  constructor(delegate: FlowContext) {
1722
4458
  super();
1723
4459
  this.addDelegate(delegate);
1724
4460
  this.defineProperty('React', { value: React });
1725
4461
  this.defineProperty('antd', { value: antd });
1726
- this.defineProperty('dayjs', { value: dayjs });
4462
+ this.defineProperty('dayjs', {
4463
+ value: dayjs,
4464
+ });
1727
4465
  // 为 JS 运行时代码提供带有 antd/App/ConfigProvider 包裹的 React 根
1728
4466
  // 保持与 ReactDOMClient 接口一致,优先覆盖 createRoot,其余方法透传
1729
4467
  const ReactDOMShim: any = {
@@ -1735,19 +4473,10 @@ export class FlowRunJSContext extends FlowContext {
1735
4473
  return this.engine.reactView.createRoot(realContainer as HTMLElement, options);
1736
4474
  },
1737
4475
  };
4476
+ ReactDOMShim.__nbRunjsInternalShim = true;
1738
4477
  this.defineProperty('ReactDOM', { value: ReactDOMShim });
1739
4478
 
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 });
4479
+ setupRunJSLibs(this);
1751
4480
 
1752
4481
  // Convenience: ctx.render(<App />[, container])
1753
4482
  // - container defaults to ctx.element if available
@@ -1767,16 +4496,37 @@ export class FlowRunJSContext extends FlowContext {
1767
4496
  globalRef.__nbRunjsRoots = globalRef.__nbRunjsRoots || new WeakMap<any, any>();
1768
4497
  const rootMap: WeakMap<any, any> = globalRef.__nbRunjsRoots;
1769
4498
 
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') {
4499
+ const disposeEntry = (entry: any) => {
4500
+ if (!entry) return;
4501
+ if (entry.disposeTheme && typeof entry.disposeTheme === 'function') {
4502
+ try {
4503
+ entry.disposeTheme();
4504
+ } catch (_) {
4505
+ // ignore
4506
+ }
4507
+ entry.disposeTheme = undefined;
4508
+ }
4509
+ const root = entry.root || entry;
4510
+ if (root && typeof root.unmount === 'function') {
1774
4511
  try {
1775
- existingRoot.unmount();
1776
- } finally {
1777
- rootMap.delete(containerEl);
4512
+ root.unmount();
4513
+ } catch (_) {
4514
+ // ignore
1778
4515
  }
1779
4516
  }
4517
+ };
4518
+
4519
+ const unmountContainerRoot = () => {
4520
+ const existing = rootMap.get(containerEl);
4521
+ if (existing) {
4522
+ disposeEntry(existing);
4523
+ rootMap.delete(containerEl);
4524
+ }
4525
+ };
4526
+
4527
+ // If vnode is string (HTML), unmount react root and set sanitized HTML
4528
+ if (typeof vnode === 'string') {
4529
+ unmountContainerRoot();
1780
4530
  const proxy: any = new ElementProxy(containerEl);
1781
4531
  proxy.innerHTML = String(vnode ?? '');
1782
4532
  return null;
@@ -1788,39 +4538,64 @@ export class FlowRunJSContext extends FlowContext {
1788
4538
  (vnode as any).nodeType &&
1789
4539
  ((vnode as any).nodeType === 1 || (vnode as any).nodeType === 3 || (vnode as any).nodeType === 11)
1790
4540
  ) {
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
- }
4541
+ unmountContainerRoot();
1799
4542
  while (containerEl.firstChild) containerEl.removeChild(containerEl.firstChild);
1800
4543
  containerEl.appendChild(vnode as any);
1801
4544
  return null;
1802
4545
  }
1803
4546
 
1804
- let root = rootMap.get(containerEl);
1805
- if (!root) {
1806
- root = this.ReactDOM.createRoot(containerEl);
1807
- rootMap.set(containerEl, root);
4547
+ // 注意:rootMap 是“全局按容器复用”的(key=containerEl)。
4548
+ // 若不同 RunJS ctx 复用同一个 containerEl,且 ReactDOM 实例引用也相同,
4549
+ // 则会复用到旧 entry,进而复用旧 ctx 创建的 autorun(闭包捕获旧 ctx),造成:
4550
+ // 1) 旧 ctx 的 reaction 继续驱动新渲染(跨 ctx 复用风险)
4551
+ // 2) 新 ctx 的主题变化不再触发 rerender
4552
+ // 3) 旧 ctx 被 entry/autorun 间接持有,无法被 GC(内存泄漏)
4553
+ // 因此这里把 ownerKey(当前 ctx)也纳入复用判断;owner 变化时必须重建 entry。
4554
+ const rendererKey = this.ReactDOM;
4555
+ const ownerKey = this;
4556
+ let entry = rootMap.get(containerEl);
4557
+ if (!entry || entry.rendererKey !== rendererKey || entry.ownerKey !== ownerKey) {
4558
+ if (entry) {
4559
+ disposeEntry(entry);
4560
+ rootMap.delete(containerEl);
4561
+ }
4562
+ const root = this.ReactDOM.createRoot(containerEl);
4563
+ entry = { rendererKey, ownerKey, root, disposeTheme: undefined, lastVnode: undefined };
4564
+ rootMap.set(containerEl, entry);
1808
4565
  }
1809
- root.render(vnode as any);
1810
- return root;
4566
+
4567
+ return externalReactRender({
4568
+ ctx: this,
4569
+ entry,
4570
+ vnode,
4571
+ containerEl,
4572
+ rootMap,
4573
+ unmountContainerRoot,
4574
+ internalReact: React,
4575
+ internalAntd: antd,
4576
+ });
1811
4577
  },
1812
4578
  );
1813
4579
  }
4580
+
4581
+ exit() {
4582
+ throw new FlowExitAllException(this.flowKey, this.model?.uid || 'runjs');
4583
+ }
4584
+
4585
+ exitAll() {
4586
+ throw new FlowExitAllException(this.flowKey, this.model?.uid || 'runjs');
4587
+ }
4588
+
1814
4589
  static define(meta: RunJSDocMeta, options?: { locale?: string }) {
1815
4590
  const locale = options?.locale;
1816
4591
  if (locale) {
1817
4592
  const map = __runjsClassLocaleMeta.get(this) || new Map<string, RunJSDocMeta>();
1818
4593
  const prev = map.get(locale) || {};
1819
- map.set(locale, __runjsDeepMerge(prev, meta));
4594
+ map.set(locale, __mergeRunJSDocMeta(prev, meta));
1820
4595
  __runjsClassLocaleMeta.set(this, map);
1821
4596
  } else {
1822
4597
  const prev = __runjsClassDefaultMeta.get(this) || {};
1823
- __runjsClassDefaultMeta.set(this, __runjsDeepMerge(prev, meta));
4598
+ __runjsClassDefaultMeta.set(this, __mergeRunJSDocMeta(prev, meta));
1824
4599
  }
1825
4600
  __runjsDocCache.delete(this);
1826
4601
  }
@@ -1828,7 +4603,7 @@ export class FlowRunJSContext extends FlowContext {
1828
4603
  const self = this as any as Function;
1829
4604
  let cacheForClass = __runjsDocCache.get(self);
1830
4605
  const cacheKey = String(locale || 'default');
1831
- if (cacheForClass && cacheForClass.has(cacheKey)) return cacheForClass.get(cacheKey)!;
4606
+ if (cacheForClass && cacheForClass.has(cacheKey)) return cacheForClass.get(cacheKey) as RunJSDocMeta;
1832
4607
  const chain: Function[] = [];
1833
4608
  let cur: any = self;
1834
4609
  while (cur && cur.prototype) {
@@ -1837,13 +4612,13 @@ export class FlowRunJSContext extends FlowContext {
1837
4612
  }
1838
4613
  let merged: RunJSDocMeta = {};
1839
4614
  for (const cls of chain) {
1840
- merged = __runjsDeepMerge(merged, __runjsClassDefaultMeta.get(cls) || {});
4615
+ merged = __mergeRunJSDocMeta(merged, __runjsClassDefaultMeta.get(cls) || {});
1841
4616
  }
1842
4617
  if (locale) {
1843
4618
  for (const cls of chain) {
1844
4619
  const lmap = __runjsClassLocaleMeta.get(cls);
1845
4620
  if (lmap && lmap.has(locale)) {
1846
- merged = __runjsDeepMerge(merged, lmap.get(locale));
4621
+ merged = __mergeRunJSDocMeta(merged, lmap.get(locale));
1847
4622
  }
1848
4623
  }
1849
4624
  }