演示

工作流

实现自定义工作流节点(IWorkflowNode)、在模块中注册、从服务触发工作流,并用 @WorkflowEntry 将控制器 API 暴露为可发现步骤。

概览

工作流引擎执行节点图。插件可贡献节点,并在领域事件发生时启动工作流。请从 @quan-erp/shared-backend-core 导入工作流类型。

  • 自定义节点 — 实现 IWorkflowNode,在 @Module({ workflowNodes }) 注册
  • 触发 — isTriggerable: true + WorkflowService.findByInitId / executeWorkflow
  • API 入口 — 在控制器方法上使用 @WorkflowEntry
  • 前端 — 通过 @quan-erp/base-frontend 的 workflowNodes() 注册对应 UI 节点

节点数据模型

  • Props — 工作流编辑器 UI 提供的配置
  • Args — 节点运行时注入的值
  • ReturnType — 本节点产出、供下游使用的值
  • type — 插件节点始终为 `${metadata.name}/${NODE_ID}`

Props 与返回值 schema

用 PropsType 声明 JSON schema,并用 SchemaToType 推断 TypeScript 类型。Schema 同时服务 process() 类型与前端编辑器。

TSXschemas
1import { 2 PropsType, 3 SchemaToType, 4} from "@quan-erp/shared-backend-core"; 5 6const inputSchema = { 7 type: "number", 8} as const satisfies PropsType; 9type Props = SchemaToType<typeof inputSchema>; 10 11const returnSchema = { 12 type: "object", 13 properties: { 14 bookingId: { type: "number" }, 15 bookingSource: { type: "string" }, 16 }, 17} as const satisfies PropsType; 18type ReturnType = SchemaToType<typeof returnSchema>; 19 20export type Args = { 21 bookingId: number; 22 bookingSource: string; 23};

执行生命周期

  1. 01

    process()

    执行器调用 process(context, uid, props, args)。返回 NodeResult.single(...)。

  2. 02

    nextToExecute()

    process 之后询问下一节点 id。null 跟随 UI next 边;string[] 覆盖路由。

  3. 03

    路由

    null → 使用画布 next 数组。string[] → 执行这些节点 id。

ExecutionContext

每次运行都会创建共享的 ExecutionContext。使用编辑器映射的 props 前必须先 resolve。

  • context.pipeData.resolve(props) — 展开 ${{node_….output}};读取 props 前务必 resolve
  • context.execute(nodeId, args) — 立即执行另一节点并等待 NodeResultType(T[])
  • toolResult[0] — 子执行的原始载荷
  • context.findWorkflowNodeById(uid) — 自定义 nextToExecute 时查看边
TSXresolve props
1import type { 2 ExecutionContext, 3 NodeResultType, 4 PromisableReturn, 5} from "@quan-erp/shared-backend-core"; 6import { NodeResult } from "@quan-erp/shared-backend-core"; 7 8function process( 9 context: ExecutionContext, 10 uid: string, 11 props: { expression: string }, 12 args: unknown, 13): PromisableReturn<NodeResultType<boolean>> { 14 const resolvedProps = context.pipeData.resolve(props); 15 const evaluateResult = eval(resolvedProps.expression); 16 return NodeResult.single(evaluateResult as boolean); 17}
TSXsub-execute
1import type { ExecutionContext } from "@quan-erp/shared-backend-core"; 2 3async function callTool( 4 context: ExecutionContext, 5 toolNodeId: string, 6 toolArgs: Record<string, unknown>, 7) { 8 const toolResult = await context.execute(toolNodeId, toolArgs); 9 return toolResult[0]; 10}

自定义路由(nextToExecute)

为条件/循环覆盖默认边遍历。若要沿用画布边,返回 null。nextToExecute 收到的是 process 的 NodeResultType 数组(使用 args[0])。

TSXif-style routing
1import type { 2 ExecutionContext, 3 NodeResultType, 4 PromisableReturn, 5} from "@quan-erp/shared-backend-core"; 6 7function nextToExecute( 8 context: ExecutionContext, 9 uid: string, 10 args: NodeResultType<boolean>, 11): PromisableReturn<string[]> | PromisableReturn<null> { 12 const current = context.findWorkflowNodeById(uid); 13 const ifTrue = current?.next?.[0]; 14 const ifFalse = current?.next?.[1]; 15 if (args[0] && ifTrue?.id) return [ifTrue.id]; 16 if (!args[0] && ifFalse?.id) return [ifFalse.id]; 17 return null; 18}

完整触发节点

触发节点设置 isTriggerable: true,通常把 Args 作为返回值传给后续图。

TSXOnBookingWorkflowNode
1import { 2 ExecutionContext, 3 IWorkflowNode, 4 NodeResult, 5 NodeResultType, 6 PromisableReturn, 7 PropsType, 8 SchemaToType, 9 WorkflowNodeDefination, 10} from "@quan-erp/shared-backend-core"; 11import metadata from "../../../module.metadata.json" with { type: "json" }; 12 13const inputSchema = { 14 type: "number", 15} as const satisfies PropsType; 16type Props = SchemaToType<typeof inputSchema>; 17 18const returnSchema = { 19 type: "object", 20 properties: { 21 bookingId: { type: "number" }, 22 bookingSource: { type: "string" }, 23 }, 24} as const satisfies PropsType; 25type ReturnType = SchemaToType<typeof returnSchema>; 26 27export type Args = { 28 bookingId: number; 29 bookingSource: string; 30}; 31 32export class OnBookingWorkflowNode 33 implements IWorkflowNode<Props, ReturnType, Args> 34{ 35 static NODE_ID = "bookings"; 36 37 process( 38 context: ExecutionContext, 39 id: string, 40 props: Props, 41 args: Args, 42 ): PromisableReturn<NodeResultType<ReturnType>> { 43 return NodeResult.single(args); 44 } 45 46 nextToExecute( 47 context: ExecutionContext, 48 uid: string, 49 args: NodeResultType<ReturnType>, 50 ): PromisableReturn<string[]> | PromisableReturn<null> { 51 return null; 52 } 53 54 getDefination(): WorkflowNodeDefination { 55 return { 56 type: `${metadata.name}/${OnBookingWorkflowNode.NODE_ID}`, 57 group: ["any"], 58 displayName: "On Hotel Booked", 59 description: "Triggers when a hotel is booked", 60 props: inputSchema, 61 isTriggerable: true, 62 returnType: { 63 type: "array", 64 items: returnSchema, 65 }, 66 }; 67 } 68}

在模块中注册

把节点类加入 workflowNodes,后端才会挂载并向编辑器暴露定义。

TSXmodule
1import { Module } from "@quan-erp/shared-backend-core"; 2import metadata from "../../module.metadata.json" with { type: "json" }; 3import { OnBookingWorkflowNode } from "./workflow/on-booking.workflow.js"; 4 5@Module({ 6 name: metadata.name, 7 providers: [], 8 controllers: [], 9 entities: [], 10 workflowNodes: [OnBookingWorkflowNode], 11}) 12export class MyPluginModule {}

以编程方式触发

对 isTriggerable 节点,注入 builtin 的 WorkflowService,按 `${metadata.name}/${NODE_ID}` 查找活跃工作流,并用 Args 执行。

TSXexecuteWorkflow
1import { 2 ContainerRegistryManager, 3 Controller, 4 Inject, 5 Post, 6 ResponseDto, 7 WorkflowService, 8} from "@quan-erp/shared-backend-core"; 9import metadata from "../../module.metadata.json" with { type: "json" }; 10import { OnBookingWorkflowNode } from "../workflow/on-booking.workflow.js"; 11 12@Controller("/bookings") 13export class BookingController { 14 @Inject(WorkflowService, ContainerRegistryManager.BUILTIN_PLUGIN) 15 workflowService: WorkflowService; 16 17 @Post("/") 18 async createBooking() { 19 // … create booking … 20 const workflows = await this.workflowService.findByInitId( 21 `${metadata.name}/${OnBookingWorkflowNode.NODE_ID}`, 22 { active: true }, 23 ); 24 25 await Promise.all( 26 workflows.map((w) => 27 this.workflowService.executeWorkflow({ 28 workflow: w, 29 args: { 30 bookingId: 1, 31 bookingSource: "web", 32 }, 33 }), 34 ), 35 ); 36 37 return ResponseDto.okWithEmpty(); 38 } 39}

@WorkflowEntry(HTTP 步骤)

标记控制器方法,使工作流引擎可发现并调用该 API。提供 name、description,以及与响应形状匹配的 returnType schema。

TSX@WorkflowEntry
1import { 2 Controller, 3 Get, 4 WorkflowEntry, 5} from "@quan-erp/shared-backend-core"; 6 7@Controller("/branch") 8export class BranchController { 9 @Get("/") 10 @WorkflowEntry({ 11 name: "get-branch", 12 description: "get all branches", 13 returnType: { 14 type: "object", 15 properties: { 16 payload: { 17 type: "array", 18 items: { 19 type: "object", 20 properties: { 21 id: { type: "number" }, 22 name: { type: "string" }, 23 }, 24 }, 25 }, 26 }, 27 }, 28 }) 29 async list() { 30 /* … */ 31 } 32}

检查清单

  • Schemas:props 与 returnType 使用 PropsType + SchemaToType
  • 在 IWorkflowNode 上实现 process、nextToExecute、getDefination
  • type: `${metadata.name}/${NODE_ID}` — 不可省略插件前缀
  • 在 @Module({ workflowNodes }) 注册
  • 可触发节点:在领域事件上用 WorkflowService 触发
  • 使用编辑器 props 前先 context.pipeData.resolve(props)
  • 用 NodeResult.single(...) 返回结果