UNPKG

@nocobase/flow-engine

Version:

A standalone flow engine for NocoBase, managing workflows, models, and actions.

191 lines (176 loc) 6.49 kB
/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ import type { FlowContext } from '../../flowContext'; import * as antd from 'antd'; import React from 'react'; import * as ReactDOMClient from 'react-dom/client'; export type RunJSVersion = 'v1' | (string & {}); export type RunJSContextCtor = new (delegate: FlowContext) => FlowRunJSContext; export type RunJSDocMeta = { label?: string; properties?: Record<string, any>; methods?: Record<string, any>; snipastes?: Record<string, any>; }; const classMeta = new WeakMap<Function, RunJSDocMeta>(); const classDocCache = new WeakMap<Function, RunJSDocMeta>(); function deepMerge(base: any, patch: any) { if (patch === null) return undefined; if (Array.isArray(base) || Array.isArray(patch) || typeof base !== 'object' || typeof patch !== 'object') { return patch ?? base; } const out: any = { ...base }; for (const k of Object.keys(patch)) { const v = deepMerge(base?.[k], patch[k]); if (typeof v === 'undefined') delete out[k]; else out[k] = v; } return out; } export class FlowRunJSContext { protected _delegate: FlowContext; [key: string]: any; static allow?: { keys?: ReadonlyArray<string>; facades?: Record<string, ReadonlyArray<string>> }; constructor(delegate: FlowContext) { this._delegate = delegate; const self = this as any; // 常用前端依赖直接注入 self.React = React; self.react = React; self.antd = antd; self.ReactDOM = ReactDOMClient; // 公共方法:分发模型事件 self.dispatchModelEvent = async (modelOrUid: any, eventName: string, inputArgs?: Record<string, any>) => { let model: any = null; const engine = (this._delegate as any).engine; if (typeof modelOrUid === 'string') { model = await engine?.loadModel?.({ uid: modelOrUid }); } else if (modelOrUid && typeof modelOrUid === 'object' && typeof modelOrUid.dispatchEvent === 'function') { model = modelOrUid; } if (model) { model.context?.addDelegate?.(self); model.dispatchEvent(eventName, { navigation: false, ...(self.model?.['getInputArgs']?.() || {}), ...(inputArgs || {}), }); } else { self.message?.error?.( self.t?.('Model with ID {{uid}} not found', { uid: String(modelOrUid) }) || 'Model not found', ); } }; // 显式暴露:基础属性(只读 getter 映射到委托) this.#exposeProps([ 'message', 'notification', 'logger', 'resource', 'urlSearchParams', 'token', 'role', 'auth', 'api', 'ref', 'model', ]); // 显式暴露:基础方法(绑定到委托) this.#exposeMethods(['t', 'requireAsync', 'copyToClipboard', 'resolveJsonTemplate', 'runAction', 'onRefReady']); } // 供子类/外部一致使用的定义 API(与 FlowContext 的接口保持一致风格) defineProperty(key: string, options: { get?: (ctx: any) => any; value?: any }) { if (options && typeof options.get === 'function') { Object.defineProperty(this, key, { configurable: true, enumerable: true, get: () => options.get?.(this), }); return; } Object.defineProperty(this, key, { configurable: true, enumerable: true, writable: false, value: options?.value, }); } defineMethod(name: string, fn: (...args: any[]) => any) { Object.defineProperty(this, name, { configurable: true, enumerable: false, writable: false, value: fn.bind(this), }); } // 工具:将委托上的属性以 getter 暴露 #exposeProps(names: string[]) { for (const k of names) { if (Object.prototype.hasOwnProperty.call(this, k)) continue; this.defineProperty(k, { get: () => (this._delegate as any)[k] }); } } // 工具:将委托上的同名方法绑定暴露 #exposeMethods(names: string[]) { for (const k of names) { if (Object.prototype.hasOwnProperty.call(this, k)) continue; const src = (this._delegate as any)[k]; if (typeof src === 'function') { this.defineMethod(k, src.bind(this._delegate)); } } } static injectDefaultGlobals?(): { window?: any; document?: any } | void; static define(meta: RunJSDocMeta) { const prev = classMeta.get(this) || {}; classMeta.set(this, deepMerge(prev, meta)); classDocCache.delete(this); } static getDoc(): RunJSDocMeta { const self = this as any; if (classDocCache.has(self)) return classDocCache.get(self)!; const chain: Function[] = []; let cur: any = self; while (cur && cur.prototype) { chain.unshift(cur); cur = Object.getPrototypeOf(cur); } let merged: RunJSDocMeta = {}; for (const cls of chain) merged = deepMerge(merged, classMeta.get(cls) || {}); classDocCache.set(self, merged); return merged; } } // Define base doc on FlowRunJSContext itself FlowRunJSContext.define({ label: 'RunJS base', properties: { t: "国际化函数。示例:`ctx.t('Hello {name}', { name: 'World' })`", logger: "Pino logger 子实例。`ctx.logger.info({ foo: 1 }, 'msg')`", message: "AntD 全局消息。`ctx.message.success('done')`", notification: "AntD 通知。`ctx.notification.open({ message: 'Hi' })`", requireAsync: '异步加载外部库。`const x = await ctx.requireAsync(url)`', copyToClipboard: '复制文本到剪贴板。`await ctx.copyToClipboard(text)`', resolveJsonTemplate: '解析含 {{ }} 的模板/表达式', runAction: '运行当前模型动作。`await ctx.runAction(name, params)`', resource: '数据资源(按委托可见)', urlSearchParams: 'URL 查询参数对象', token: 'API Token', role: '当前角色', auth: '认证信息(locale/role/user/token)', api: 'APIClient 实例', React: 'React 命名空间(RunJS 环境可用)', react: 'React 别名(小写)', ReactDOM: 'ReactDOM 客户端(含 createRoot)', antd: 'AntD 组件库(RunJS 环境可用)', }, methods: { dispatchModelEvent: "触发模型事件:`await ctx.dispatchModelEvent(modelUid, 'click', { ... })`", }, });