@nocobase/flow-engine 2.0.0-alpha.45 → 2.0.0-alpha.46

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.
@@ -112,7 +112,9 @@ const FlowModelRendererCore = (0, import_reactive_react.observer)(
112
112
  }
113
113
  return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, rendered);
114
114
  }, "ContentOrError");
115
- const contentKey = (model == null ? void 0 : model.use) || ((_a = model == null ? void 0 : model.constructor) == null ? void 0 : _a.name) || model.uid;
115
+ const rawUse = model == null ? void 0 : model.use;
116
+ const resolvedName = ((_a = model == null ? void 0 : model.constructor) == null ? void 0 : _a.name) || model.uid;
117
+ const contentKey = typeof rawUse === "string" ? `${rawUse}:${model.uid}` : `${resolvedName}:${model.uid}`;
116
118
  if (!showFlowSettings) {
117
119
  return wrapWithErrorBoundary(
118
120
  /* @__PURE__ */ import_react.default.createElement("div", { key: contentKey }, /* @__PURE__ */ import_react.default.createElement(ContentOrError, null))
@@ -227,14 +227,27 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
227
227
  );
228
228
  const getModelConfigurableFlowsAndSteps = (0, import_react.useCallback)(
229
229
  async (targetModel, modelKey) => {
230
+ var _a;
230
231
  try {
231
- const flows = targetModel.constructor.globalFlowRegistry.getFlows();
232
+ const flowsMap = new Map(targetModel.constructor.globalFlowRegistry.getFlows());
233
+ const originUse = targetModel == null ? void 0 : targetModel.use;
234
+ if (typeof originUse === "string" && originUse !== targetModel.constructor.name) {
235
+ const originCls = (_a = targetModel.flowEngine) == null ? void 0 : _a.getModelClass(originUse);
236
+ if (originCls == null ? void 0 : originCls.globalFlowRegistry) {
237
+ for (const [k, v] of originCls.globalFlowRegistry.getFlows()) {
238
+ if (!flowsMap.has(k)) {
239
+ flowsMap.set(k, v);
240
+ }
241
+ }
242
+ }
243
+ }
244
+ const flows = flowsMap;
232
245
  const flowsArray = Array.from(flows.values());
233
246
  const flowsWithSteps = await Promise.all(
234
247
  flowsArray.map(async (flow) => {
235
248
  const configurableSteps = await Promise.all(
236
249
  Object.entries(flow.steps).map(async ([stepKey, stepDefinition]) => {
237
- var _a;
250
+ var _a2;
238
251
  const actionStep = stepDefinition;
239
252
  if (actionStep.hideInSettings) {
240
253
  return null;
@@ -244,7 +257,7 @@ const DefaultSettingsIcon = /* @__PURE__ */ __name(({
244
257
  let stepTitle = actionStep.title;
245
258
  if (actionStep.use) {
246
259
  try {
247
- const action = (_a = targetModel.getAction) == null ? void 0 : _a.call(targetModel, actionStep.use);
260
+ const action = (_a2 = targetModel.getAction) == null ? void 0 : _a2.call(targetModel, actionStep.use);
248
261
  hasActionUiSchema = action && action.uiSchema != null;
249
262
  stepTitle = stepTitle || (action == null ? void 0 : action.title);
250
263
  } catch (error) {
@@ -234,6 +234,11 @@ export declare class FlowEngine {
234
234
  delegateToParent?: boolean;
235
235
  delegate?: FlowContext;
236
236
  }): T;
237
+ /**
238
+ * 按类上的 resolveUse 链路解析最终用于实例化的模型类。
239
+ * 允许模型类根据上下文动态指定实际使用的类,支持多级 resolveUse。
240
+ */
241
+ private _resolveModelClass;
237
242
  /**
238
243
  * 尝试应用当前模型可用 flow 的 defaultParams(如果存在)到 model.stepParams。
239
244
  * 仅对尚未存在的步骤参数进行填充,不覆盖已有值。
package/lib/flowEngine.js CHANGED
@@ -392,7 +392,10 @@ const _FlowEngine = class _FlowEngine {
392
392
  */
393
393
  createModel(options, extra) {
394
394
  const { parentId, uid, use: modelClassName, subModels } = options;
395
- const ModelClass = typeof modelClassName === "string" ? this.getModelClass(modelClassName) : modelClassName;
395
+ const ModelClass = this._resolveModelClass(
396
+ typeof modelClassName === "string" ? this.getModelClass(modelClassName) : modelClassName,
397
+ options
398
+ );
396
399
  if (uid && this._modelInstances.has(uid)) {
397
400
  return this._modelInstances.get(uid);
398
401
  }
@@ -424,6 +427,41 @@ const _FlowEngine = class _FlowEngine {
424
427
  modelInstance._createSubModels(options.subModels);
425
428
  return modelInstance;
426
429
  }
430
+ /**
431
+ * 按类上的 resolveUse 链路解析最终用于实例化的模型类。
432
+ * 允许模型类根据上下文动态指定实际使用的类,支持多级 resolveUse。
433
+ */
434
+ _resolveModelClass(initial, options) {
435
+ let current = initial;
436
+ const visited = /* @__PURE__ */ new Set();
437
+ while (current) {
438
+ if (visited.has(current)) {
439
+ console.warn(`FlowEngine: resolveUse circular reference detected on '${current.name}'.`);
440
+ break;
441
+ }
442
+ visited.add(current);
443
+ const resolver = current == null ? void 0 : current.resolveUse;
444
+ if (typeof resolver !== "function") {
445
+ break;
446
+ }
447
+ const resolved = resolver(options, this);
448
+ if (!resolved || resolved === current) {
449
+ break;
450
+ }
451
+ let next;
452
+ if (typeof resolved === "string") {
453
+ next = this.getModelClass(resolved);
454
+ if (!next) {
455
+ console.warn(`FlowEngine: resolveUse returned '${resolved}' but no model is registered under that name.`);
456
+ return void 0;
457
+ }
458
+ } else {
459
+ next = resolved;
460
+ }
461
+ current = next;
462
+ }
463
+ return current;
464
+ }
427
465
  /**
428
466
  * 尝试应用当前模型可用 flow 的 defaultParams(如果存在)到 model.stepParams。
429
467
  * 仅对尚未存在的步骤参数进行填充,不覆盖已有值。
@@ -500,6 +538,7 @@ const _FlowEngine = class _FlowEngine {
500
538
  return false;
501
539
  }
502
540
  const modelInstance = this._modelInstances.get(uid);
541
+ modelInstance.invalidateFlowCache(void 0, true);
503
542
  modelInstance.clearForks();
504
543
  if ((_a = modelInstance.parent) == null ? void 0 : _a.subModels) {
505
544
  for (const subKey in modelInstance.parent.subModels) {
@@ -691,6 +691,8 @@ const _FlowSettings = class _FlowSettings {
691
691
  }
692
692
  }
693
693
  currentView.close();
694
+ model.invalidateFlowCache("beforeRender", true);
695
+ await model.rerender();
694
696
  try {
695
697
  await (onSaved == null ? void 0 : onSaved());
696
698
  } catch (cbErr) {
@@ -11,7 +11,7 @@ import { Emitter } from '../emitter';
11
11
  import { InstanceFlowRegistry } from '../flow-registry/InstanceFlowRegistry';
12
12
  import { FlowContext, FlowModelContext, FlowRuntimeContext } from '../flowContext';
13
13
  import { FlowEngine } from '../flowEngine';
14
- import type { ActionDefinition, ArrayElementType, CreateModelOptions, CreateSubModelOptions, DefaultStructure, FlowDefinitionOptions, FlowModelMeta, FlowModelOptions, ModelConstructor, ParamObject, ParentFlowModel, PersistOptions, StepParams } from '../types';
14
+ import type { ActionDefinition, ArrayElementType, CreateModelOptions, CreateSubModelOptions, DefaultStructure, FlowDefinitionOptions, FlowModelMeta, FlowModelOptions, ModelConstructor, ParamObject, ParentFlowModel, PersistOptions, RegisteredModelClassName, StepParams } from '../types';
15
15
  import { IModelComponentProps, ReadonlyModelProps } from '../types';
16
16
  import { ModelActionRegistry } from '../action-registry/ModelActionRegistry';
17
17
  import { ModelEventRegistry } from '../event-registry/ModelEventRegistry';
@@ -97,6 +97,11 @@ export declare class FlowModel<Structure extends DefaultStructure = DefaultStruc
97
97
  static get globalFlowRegistry(): GlobalFlowRegistry;
98
98
  protected static get actionRegistry(): ModelActionRegistry;
99
99
  protected static get eventRegistry(): ModelEventRegistry;
100
+ /**
101
+ * 动态解析实际要实例化的模型类;可在子类中覆盖。
102
+ * 返回注册名或构造器,支持在 FlowEngine 中继续沿链解析。
103
+ */
104
+ static resolveUse?(options: CreateModelOptions, engine: FlowEngine): RegisteredModelClassName | ModelConstructor | void;
100
105
  /**
101
106
  * 注册仅当前 FlowModel 类及其子类可用的 Action。
102
107
  * 该注册是类级别的,不会影响全局(FlowEngine)的 Action 注册。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.0.0-alpha.45",
3
+ "version": "2.0.0-alpha.46",
4
4
  "private": false,
5
5
  "description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
6
6
  "main": "lib/index.js",
@@ -8,8 +8,8 @@
8
8
  "dependencies": {
9
9
  "@formily/antd-v5": "1.x",
10
10
  "@formily/reactive": "2.x",
11
- "@nocobase/sdk": "2.0.0-alpha.45",
12
- "@nocobase/shared": "2.0.0-alpha.45",
11
+ "@nocobase/sdk": "2.0.0-alpha.46",
12
+ "@nocobase/shared": "2.0.0-alpha.46",
13
13
  "ahooks": "^3.7.2",
14
14
  "dayjs": "^1.11.9",
15
15
  "dompurify": "^3.0.2",
@@ -36,5 +36,5 @@
36
36
  ],
37
37
  "author": "NocoBase Team",
38
38
  "license": "AGPL-3.0",
39
- "gitHead": "ddb6d6b89f4f3dc49dc942d46901e9e6f159be58"
39
+ "gitHead": "ebf3cbeea4957a914e3157f3c6b570dba685c46f"
40
40
  }
@@ -222,8 +222,10 @@ const FlowModelRendererCore: React.FC<{
222
222
  };
223
223
 
224
224
  // 如果不显示流程设置,直接返回模型内容(可能包装 ErrorBoundary)
225
- // 当模型类发生变化(如 replaceModel),强制重挂载内容,规避部分子组件 hooks deps 形态不一致导致的报错
226
- const contentKey = (model as any)?.use || (model as any)?.constructor?.name || model.uid;
225
+ // 当模型类或 use 变化时重挂载内容,规避组件内部状态残留
226
+ const rawUse = (model as any)?.use;
227
+ const resolvedName = (model as any)?.constructor?.name || model.uid;
228
+ const contentKey = typeof rawUse === 'string' ? `${rawUse}:${model.uid}` : `${resolvedName}:${model.uid}`;
227
229
 
228
230
  if (!showFlowSettings) {
229
231
  return wrapWithErrorBoundary(
@@ -267,8 +267,23 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
267
267
  const getModelConfigurableFlowsAndSteps = useCallback(
268
268
  async (targetModel: FlowModel, modelKey?: string): Promise<FlowInfo[]> => {
269
269
  try {
270
- // 仅使用静态流(类级全局注册的 flows),排除实例动态流
271
- const flows = (targetModel.constructor as typeof FlowModel).globalFlowRegistry.getFlows();
270
+ // 仅使用静态流(类级全局注册的 flows
271
+ const flowsMap = new Map((targetModel.constructor as typeof FlowModel).globalFlowRegistry.getFlows());
272
+
273
+ // 如果有原始 use 且与当前类不同,合并原始模型类的静态 flows(用于入口模型 resolveUse 场景)
274
+ const originUse = targetModel?.use;
275
+ if (typeof originUse === 'string' && originUse !== targetModel.constructor.name) {
276
+ const originCls = targetModel.flowEngine?.getModelClass(originUse) as typeof FlowModel | undefined;
277
+ if (originCls?.globalFlowRegistry) {
278
+ for (const [k, v] of originCls.globalFlowRegistry.getFlows()) {
279
+ if (!flowsMap.has(k)) {
280
+ flowsMap.set(k, v);
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ const flows = flowsMap;
272
287
 
273
288
  const flowsArray = Array.from(flows.values());
274
289
 
@@ -422,4 +422,59 @@ describe('DefaultSettingsIcon - only static flows are shown', () => {
422
422
  menu.onClick?.({ key: 'copy-pop-uid:items[0]:popupSettings:stage' });
423
423
  expect((navigator as any).clipboard.writeText).toHaveBeenCalledWith('child-2');
424
424
  });
425
+
426
+ it('merges static flows from origin use when instance class differs', async () => {
427
+ class TargetModel extends FlowModel {
428
+ static setupFlows() {
429
+ this.registerFlow({
430
+ key: 'targetFlow',
431
+ title: 'Target Flow',
432
+ steps: {
433
+ targetStep: { title: 'Target Step', uiSchema: { x: { type: 'string', 'x-component': 'Input' } } },
434
+ },
435
+ });
436
+ }
437
+ }
438
+ class EntryModel extends FlowModel {
439
+ static resolveUse() {
440
+ return TargetModel;
441
+ }
442
+ static setupFlows() {
443
+ this.registerFlow({
444
+ key: 'originFlow',
445
+ title: 'Origin Flow',
446
+ steps: {
447
+ originStep: { title: 'Origin Step', uiSchema: { y: { type: 'string', 'x-component': 'Input' } } },
448
+ },
449
+ });
450
+ }
451
+ }
452
+
453
+ TargetModel.setupFlows();
454
+ EntryModel.setupFlows();
455
+
456
+ const engine = new FlowEngine();
457
+ engine.registerModels({ EntryModel, TargetModel });
458
+
459
+ const model = engine.createModel({ use: 'EntryModel', uid: 'merge-origin', flowEngine: engine });
460
+
461
+ render(
462
+ React.createElement(
463
+ ConfigProvider as any,
464
+ null,
465
+ React.createElement(App as any, null, React.createElement(DefaultSettingsIcon as any, { model })),
466
+ ),
467
+ );
468
+
469
+ await waitFor(() => {
470
+ const menu = (globalThis as any).__lastDropdownMenu;
471
+ expect(menu).toBeTruthy();
472
+ const items = (menu?.items || []) as any[];
473
+ const groups = items.filter((it) => it.type === 'group').map((it) => String(it.label));
474
+ expect(groups).toContain('Origin Flow');
475
+ expect(groups).toContain('Target Flow');
476
+ expect(items.some((it) => String(it.key || '').startsWith('originFlow:originStep'))).toBe(true);
477
+ expect(items.some((it) => String(it.key || '').startsWith('targetFlow:targetStep'))).toBe(true);
478
+ });
479
+ });
425
480
  });
package/src/flowEngine.ts CHANGED
@@ -30,6 +30,7 @@ import type {
30
30
  ModelConstructor,
31
31
  PersistOptions,
32
32
  ResourceType,
33
+ RegisteredModelClassName,
33
34
  } from './types';
34
35
  import { isInheritedFrom } from './utils';
35
36
 
@@ -458,7 +459,10 @@ export class FlowEngine {
458
459
  extra?: { delegateToParent?: boolean; delegate?: FlowContext },
459
460
  ): T {
460
461
  const { parentId, uid, use: modelClassName, subModels } = options;
461
- const ModelClass = typeof modelClassName === 'string' ? this.getModelClass(modelClassName) : modelClassName;
462
+ const ModelClass = this._resolveModelClass(
463
+ typeof modelClassName === 'string' ? this.getModelClass(modelClassName) : modelClassName,
464
+ options,
465
+ );
462
466
 
463
467
  if (uid && this._modelInstances.has(uid)) {
464
468
  return this._modelInstances.get(uid) as T;
@@ -505,6 +509,53 @@ export class FlowEngine {
505
509
  return modelInstance as T;
506
510
  }
507
511
 
512
+ /**
513
+ * 按类上的 resolveUse 链路解析最终用于实例化的模型类。
514
+ * 允许模型类根据上下文动态指定实际使用的类,支持多级 resolveUse。
515
+ */
516
+ private _resolveModelClass(
517
+ initial: ModelConstructor | undefined,
518
+ options: CreateModelOptions,
519
+ ): ModelConstructor | undefined {
520
+ let current = initial;
521
+ const visited = new Set<ModelConstructor>();
522
+
523
+ while (current) {
524
+ if (visited.has(current)) {
525
+ console.warn(`FlowEngine: resolveUse circular reference detected on '${current.name}'.`);
526
+ break;
527
+ }
528
+ visited.add(current);
529
+
530
+ const resolver = (current as any)?.resolveUse as
531
+ | ((opts: CreateModelOptions, engine: FlowEngine) => RegisteredModelClassName | ModelConstructor | void)
532
+ | undefined;
533
+ if (typeof resolver !== 'function') {
534
+ break;
535
+ }
536
+
537
+ const resolved = resolver(options, this);
538
+ if (!resolved || resolved === current) {
539
+ break;
540
+ }
541
+
542
+ let next: ModelConstructor | undefined;
543
+ if (typeof resolved === 'string') {
544
+ next = this.getModelClass(resolved);
545
+ if (!next) {
546
+ console.warn(`FlowEngine: resolveUse returned '${resolved}' but no model is registered under that name.`);
547
+ return undefined;
548
+ }
549
+ } else {
550
+ next = resolved;
551
+ }
552
+
553
+ current = next;
554
+ }
555
+
556
+ return current;
557
+ }
558
+
508
559
  /**
509
560
  * 尝试应用当前模型可用 flow 的 defaultParams(如果存在)到 model.stepParams。
510
561
  * 仅对尚未存在的步骤参数进行填充,不覆盖已有值。
@@ -591,6 +642,8 @@ export class FlowEngine {
591
642
  return false;
592
643
  }
593
644
  const modelInstance = this._modelInstances.get(uid) as FlowModel;
645
+ // Ensure any cached beforeRender results tied to this uid are cleared before removal.
646
+ modelInstance.invalidateFlowCache(undefined, true);
594
647
  modelInstance.clearForks();
595
648
  // 从父模型中移除当前模型的引用
596
649
  if (modelInstance.parent?.subModels) {
@@ -875,6 +875,10 @@ export class FlowSettings {
875
875
 
876
876
  currentView.close();
877
877
 
878
+ // 配置变更后立即刷新 beforeRender,避免命中旧缓存导致界面不更新
879
+ model.invalidateFlowCache('beforeRender', true);
880
+ await model.rerender();
881
+
878
882
  // 触发保存成功回调
879
883
  try {
880
884
  await onSaved?.();
@@ -0,0 +1,112 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest';
11
+ import { FlowEngine } from '../../flowEngine';
12
+ import { ErrorFlowModel, FlowModel } from '../flowModel';
13
+
14
+ describe('FlowEngine.createModel resolveUse hook', () => {
15
+ let engine: FlowEngine;
16
+ let warnSpy: ReturnType<typeof vi.spyOn>;
17
+
18
+ beforeEach(() => {
19
+ engine = new FlowEngine();
20
+ warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
21
+ });
22
+
23
+ afterEach(() => {
24
+ warnSpy.mockRestore();
25
+ });
26
+
27
+ test('should use resolveUse returning registered name', () => {
28
+ class DynamicTarget extends FlowModel {}
29
+ class DynamicEntry extends FlowModel {
30
+ static resolveUse() {
31
+ return 'DynamicTarget';
32
+ }
33
+ }
34
+
35
+ engine.registerModels({ DynamicEntry, DynamicTarget });
36
+
37
+ const model = engine.createModel({ use: 'DynamicEntry', uid: 'dynamic-entry', flowEngine: engine });
38
+
39
+ expect(model).toBeInstanceOf(DynamicTarget);
40
+ expect(warnSpy).not.toHaveBeenCalled();
41
+ });
42
+
43
+ test('should chain resolveUse until final constructor', () => {
44
+ class ChainLeaf extends FlowModel {}
45
+ class ChainMid extends FlowModel {
46
+ static resolveUse() {
47
+ return ChainLeaf;
48
+ }
49
+ }
50
+ class ChainStart extends FlowModel {
51
+ static resolveUse() {
52
+ return 'ChainMid';
53
+ }
54
+ }
55
+
56
+ engine.registerModels({ ChainLeaf, ChainMid, ChainStart });
57
+
58
+ const model = engine.createModel({ use: 'ChainStart', uid: 'chain-start', flowEngine: engine });
59
+
60
+ expect(model).toBeInstanceOf(ChainLeaf);
61
+ expect(warnSpy).not.toHaveBeenCalled();
62
+ });
63
+
64
+ test('should break resolveUse on circular reference and warn', () => {
65
+ class LoopModel extends FlowModel {
66
+ static resolveUse() {
67
+ return 'LoopModel';
68
+ }
69
+ }
70
+
71
+ engine.registerModels({ LoopModel });
72
+
73
+ const model = engine.createModel({ use: 'LoopModel', uid: 'loop-model', flowEngine: engine });
74
+
75
+ expect(model).toBeInstanceOf(LoopModel);
76
+ expect(warnSpy).toHaveBeenCalled();
77
+ });
78
+
79
+ test('should fall back to ErrorFlowModel when resolveUse returns unregistered name', () => {
80
+ class MissingTargetEntry extends FlowModel {
81
+ static resolveUse() {
82
+ return 'NoSuchModel';
83
+ }
84
+ }
85
+
86
+ engine.registerModels({ MissingTargetEntry });
87
+
88
+ const model = engine.createModel({ use: 'MissingTargetEntry', uid: 'missing-target', flowEngine: engine });
89
+
90
+ expect(model).toBeInstanceOf(ErrorFlowModel);
91
+ expect(warnSpy).toHaveBeenCalledWith(
92
+ expect.stringContaining("resolveUse returned 'NoSuchModel' but no model is registered under that name."),
93
+ );
94
+ });
95
+
96
+ test('should keep original use in serialized data after resolveUse', () => {
97
+ class DynamicTarget extends FlowModel {}
98
+ class DynamicEntry extends FlowModel {
99
+ static resolveUse() {
100
+ return DynamicTarget;
101
+ }
102
+ }
103
+
104
+ engine.registerModels({ DynamicEntry, DynamicTarget });
105
+
106
+ const model = engine.createModel({ use: 'DynamicEntry', uid: 'dynamic-entry', flowEngine: engine });
107
+ const serialized = model.serialize();
108
+
109
+ expect(model).toBeInstanceOf(DynamicTarget);
110
+ expect(serialized.use).toBe('DynamicEntry');
111
+ });
112
+ });
@@ -31,6 +31,7 @@ import type {
31
31
  ParamObject,
32
32
  ParentFlowModel,
33
33
  PersistOptions,
34
+ RegisteredModelClassName,
34
35
  StepDefinition,
35
36
  StepParams,
36
37
  } from '../types';
@@ -318,6 +319,15 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
318
319
  return registry;
319
320
  }
320
321
 
322
+ /**
323
+ * 动态解析实际要实例化的模型类;可在子类中覆盖。
324
+ * 返回注册名或构造器,支持在 FlowEngine 中继续沿链解析。
325
+ */
326
+ static resolveUse?(
327
+ options: CreateModelOptions,
328
+ engine: FlowEngine,
329
+ ): RegisteredModelClassName | ModelConstructor | void;
330
+
321
331
  /**
322
332
  * 注册仅当前 FlowModel 类及其子类可用的 Action。
323
333
  * 该注册是类级别的,不会影响全局(FlowEngine)的 Action 注册。