UNPKG

@nocobase/flow-engine

Version:

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

1,432 lines (1,315 loc) 61.2 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 { ISchema } from '@formily/json-schema'; import { observable } from '@formily/reactive'; import { APIClient } from '@nocobase/sdk'; import type { Router } from '@remix-run/router'; import * as antd from 'antd'; import { MessageInstance } from 'antd/es/message/interface'; import type { HookAPI } from 'antd/es/modal/useModal'; import { NotificationInstance } from 'antd/es/notification/interface'; import _ from 'lodash'; import pino from 'pino'; import qs from 'qs'; import React, { createRef } from 'react'; import type { Location } from 'react-router-dom'; import { ACL } from './acl/Acl'; import { ContextPathProxy } from './ContextPathProxy'; import { DataSource, DataSourceManager } from './data-source'; import { FlowEngine } from './flowEngine'; import { FlowI18n } from './flowI18n'; import { JSRunner, JSRunnerOptions } from './JSRunner'; import { createJSRunnerWithVersion } from './runjs-context'; import { FlowModel, ForkFlowModel } from './models'; import { APIResource, BaseRecordResource, FlowResource, FlowSQLRepository, MultiRecordResource, SingleRecordResource, SQLResource, } from './resources'; import type { ActionDefinition, EventDefinition, ResourceType } from './types'; import { createSafeDocument, createSafeWindow, escapeT, extractPropertyPath, extractUsedVariablePaths, FlowExitException, resolveDefaultParams, resolveExpressions, } from './utils'; import { FlowExitAllException } from './utils/exceptions'; import { enqueueVariablesResolve, JSONValue } from './utils/params-resolvers'; import type { RecordRef } from './utils/serverContextParams'; import { buildServerContextParams as _buildServerContextParams } from './utils/serverContextParams'; import { FlowView, FlowViewer } from './views/FlowView'; // Helper: detect a RecordRef-like object function isRecordRefLike(val: any): boolean { return !!(val && typeof val === 'object' && 'collection' in val && 'filterByTk' in val); } // Helper: Filter builder output by subpaths that need server resolution // - built can be RecordRef (top-level var) or an object mapping subKey -> RecordRef (e.g., { record: ref }) function filterBuilderOutputByPaths(built: any, neededPaths: string[]): any { if (!neededPaths || neededPaths.length === 0) return undefined; if (isRecordRefLike(built)) return built; if (built && typeof built === 'object' && !Array.isArray(built)) { const out: Record<string, any> = {}; for (const [k, v] of Object.entries(built)) { const hit = neededPaths.some((p) => p === k || p.startsWith(`${k}.`) || p.startsWith(`${k}[`)); if (hit) out[k] = v; } return out; } return undefined; } type Getter<T = any> = (ctx: FlowContext) => T | Promise<T>; export interface MetaTreeNode { name: string; title: string; type: string; interface?: string; uiSchema?: ISchema; render?: (props: any) => JSX.Element; // display?: 'default' | 'flatten' | 'none'; // 显示模式:默认、平铺子菜单、完全隐藏, 用于简化meta树显示层级 paths: string[]; parentTitles?: string[]; // 父级标题数组,不包含自身title,第一层可省略 // 变量禁用状态与原因(用于变量选择器 UI 展示) disabled?: boolean | (() => boolean); disabledReason?: string | (() => string | undefined); children?: MetaTreeNode[] | (() => Promise<MetaTreeNode[]>); } export interface PropertyMeta { type: string; title: string; interface?: string; uiSchema?: ISchema; // TODO: 这个是不是压根没必要啊? render?: (props: any) => JSX.Element; // 自定义渲染函数 // 用于 VariableInput 的排序:数值越大,显示越靠前;相同值保持稳定顺序 sort?: number; // display?: 'default' | 'flatten' | 'none'; // 显示模式:默认、平铺子菜单、完全隐藏, 用于简化meta树显示层级 properties?: Record<string, PropertyMeta> | (() => Promise<Record<string, PropertyMeta>>); // 变量禁用控制:若 disabled 为真(或函数返回真)则禁用 disabled?: boolean | (() => boolean); // 禁用原因(用于 UI 小问号提示),可为函数 disabledReason?: string | (() => string | undefined); // 变量解析参数构造器(用于 variables:resolve 的 contextParams,按属性名归位)。 // 支持返回 RecordRef 或任意嵌套对象(将被 buildServerContextParams 扁平化,例如 { record: RecordRef } -> 'view.record')。 buildVariablesParams?: ( ctx: FlowContext, ) => RecordRef | Record<string, any> | Promise<RecordRef | Record<string, any> | undefined> | undefined; } // A factory function that lazily produces PropertyMeta, and may carry // hint fields like `title` and `sort` for UI building before resolution. export type PropertyMetaFactory = { (): PropertyMeta | Promise<PropertyMeta | null> | null; /** * 仅作为“是否可能存在子节点”的提示,不影响 meta 工厂本身的惰性特性。 * - true(默认):视为可能有 children,节点会提供 children 懒加载器(用于级联展开加载子级)。 * - false:视为没有 children,不渲染展开箭头,且不提供 children 懒加载器; * 但节点本身的 meta 工厂仍保持惰性(在需要时仍可解析出 title/type 等信息)。 */ hasChildren?: boolean; title?: string; sort?: number; }; export type PropertyMetaOrFactory = PropertyMeta | PropertyMetaFactory; export interface PropertyOptions { value?: any; once?: boolean; // 是否只定义一次 get?: Getter; cache?: boolean; observable?: boolean; // 是否为 observable 属性 meta?: PropertyMetaOrFactory; // 支持静态、函数和异步函数(工厂函数可带 title/sort) // 标记该属性是否在服务端解析: // - boolean: true 表示整个顶层变量交给服务端;false 表示仅前端解析 // - function: 根据子路径决定是否交给服务端(子路径示例:'record.roles[0].name'、'id'、'') resolveOnServer?: boolean | ((subPath: string) => boolean); } type RouteOptions = { name?: string; // 路由唯一标识 path?: string; // 路由模板 params?: Record<string, any>; // 路由参数 pathname?: string; // 路由的完整路径 }; export class FlowContext { _props: Record<string, PropertyOptions> = {}; _methods: Record<string, (...args: any[]) => any> = {}; protected _cache: Record<string, any> = {}; protected _observableCache: Record<string, any> = observable.shallow({}); protected _delegates: FlowContext[] = []; protected _pending: Record<string, Promise<any>> = {}; [key: string]: any; #proxy: FlowContext | null = null; private _metaNodeCache: WeakMap<PropertyMetaOrFactory, MetaTreeNode> = new WeakMap(); createProxy() { if (this.#proxy) { return this.#proxy; } this.#proxy = new Proxy(this, { get: (target, key, receiver) => { if (typeof key === 'string') { // 1. 检查是否为直接属性或方法,如果是则跳过委托链查找 if (Reflect.has(target, key)) { const val = Reflect.get(target, key, receiver); if (typeof val === 'function') return val.bind(target); return val; } // 2. 优先查找自身 _props if (Object.prototype.hasOwnProperty.call(target._props, key)) { return target._getOwnProperty(key, this.createProxy()); } // 3. 优先查找自身 _methods if (Object.prototype.hasOwnProperty.call(target._methods, key)) { return target._getOwnMethod(key, this.createProxy()); } // 4. 只有在自身没有该属性时才查找委托链 const found = this._findInDelegates(target._delegates, key); if (found !== undefined) return found.result; return undefined; } return Reflect.get(target, key, receiver); }, has: (target, key) => { if (typeof key === 'string') { // 1. 检查直接属性 if (Reflect.has(target, key)) return true; // 2. 检查 _props 和 _methods if (Object.prototype.hasOwnProperty.call(target._props, key)) return true; if (Object.prototype.hasOwnProperty.call(target._methods, key)) return true; // 3. 检查委托链 if (this._hasInDelegates(target._delegates, key)) return true; } return Reflect.has(target, key); }, }); return this.#proxy; } constructor() { return this.createProxy(); } defineProperty(key: string, options: PropertyOptions) { if (this._props[key] && this._props[key]?.once) { return; } // 清除旧属性对应的缓存 const oldOptions = this._props[key]; if (oldOptions?.meta) { this._clearMetaNodeCacheFor(oldOptions.meta); } this._props[key] = options; delete this._observableCache[key]; // 清除旧的 observable 缓存 delete this._cache[key]; // 用 Object.defineProperty 挂载到实例上,便于 ctx.foo 直接访问 Object.defineProperty(this, key, { configurable: true, enumerable: true, get: () => this._getOwnProperty(key, this.createProxy()), }); } defineMethod(name: string, fn: (...args: any[]) => any, des?: string) { this._methods[name] = fn; Object.defineProperty(this, name, { configurable: true, enumerable: false, writable: false, value: fn.bind(this.createProxy()), }); } removeCache(key: string) { if (key in this._observableCache) { delete this._observableCache[key]; return true; } if (key in this._cache) { delete this._cache[key]; return true; } if (key in this._pending) { delete this._pending[key]; return true; } // 递归清理委托链 for (const delegate of this._delegates) { if (delegate.removeCache(key)) { return true; } } } delegate(ctx: FlowContext) { if (!(ctx instanceof FlowContext)) { throw new Error('Delegate must be an instance of FlowContext'); } // 防止重复委托同一个 context if (this._delegates.includes(ctx)) { console.warn(`[FlowContext] delegate - skip duplicate delegate: ${this._delegates.length}`); return; } this._delegates.unshift(ctx); } addDelegate(ctx: FlowContext) { if (!(ctx instanceof FlowContext)) { throw new Error('Delegate must be an instance of FlowContext'); } if (!this._delegates.includes(ctx)) { this._delegates.unshift(ctx); } } clearDelegates() { this._delegates = []; this._metaNodeCache = new WeakMap(); // 清除缓存 } removeDelegate(ctx: FlowContext) { if (!(ctx instanceof FlowContext)) { throw new Error('Delegate must be an instance of FlowContext'); } const index = this._delegates.indexOf(ctx); if (index !== -1) { this._delegates.splice(index, 1); // 不需要清除缓存:委托链变化不影响基于 meta 内容的缓存 } } /** * 清除特定 meta 对象的缓存 */ private _clearMetaNodeCacheFor(meta: PropertyMetaOrFactory): void { this._metaNodeCache.delete(meta); } has(key: string) { return !!this._props[key]; } /** * 获取属性元数据树 * 返回的 MetaTreeNode 中可能包含异步的延迟加载逻辑 * @param value 可选参数,指定要获取的属性路径,格式: "{{ ctx.propertyName }}" * @returns MetaTreeNode[] 根级属性的元数据树,或指定路径的子树 * * @example * // 同步调用,获取完整 meta tree * const metaTree = flowContext.getPropertyMetaTree(); * * // 获取指定属性的子树 * const subTree = flowContext.getPropertyMetaTree("{{ ctx.user }}"); * * // 获取多层级属性的子树 * const profileTree = flowContext.getPropertyMetaTree("{{ ctx.user.profile }}"); */ getPropertyMetaTree(value?: string, options?: { flatten?: boolean }): MetaTreeNode[] { const metaMap = this._getPropertiesMeta(); // 如果有 value 参数,尝试返回对应属性的子树 if (value) { const propertyPath = extractPropertyPath(value); if (propertyPath && propertyPath.length > 0) { const loadChildrenFrom = async ( metaOrFactory: PropertyMeta | (() => PropertyMeta | Promise<PropertyMeta>), fullPath: string[], finalKey: string, ): Promise<MetaTreeNode[]> => { try { const meta: PropertyMeta = typeof metaOrFactory === 'function' ? await (metaOrFactory as any)() : (metaOrFactory as PropertyMeta); if (!meta?.properties) return []; let props = meta.properties; if (typeof props === 'function') { const resolved = await props(); meta.properties = resolved; props = resolved; } const childNodes = this.#createChildNodes(props as Record<string, PropertyMeta>, fullPath, [], meta); return Array.isArray(childNodes) ? childNodes : await childNodes(); } catch (error) { console.warn(`Failed to load meta for ${finalKey}:`, error); return []; } }; const targetMeta = this.#findMetaByPath(propertyPath); if (targetMeta) { const [finalKey, metaOrFactory, fullPath] = targetMeta; const depth = propertyPath.length; if (depth === 1) { if (typeof metaOrFactory === 'function') { return (() => loadChildrenFrom(metaOrFactory, fullPath, finalKey)) as unknown as MetaTreeNode[]; } if (metaOrFactory.properties) { if (typeof metaOrFactory.properties === 'function') { return (() => loadChildrenFrom(metaOrFactory, fullPath, finalKey)) as unknown as MetaTreeNode[]; } const childNodes = this.#createChildNodes(metaOrFactory.properties, fullPath, [], metaOrFactory); return Array.isArray(childNodes) ? childNodes : []; } return []; } if (typeof metaOrFactory === 'function') { if (options?.flatten) { // 统一语义:当请求子层路径且 flatten=true 时,直接返回其 children 列表 return (() => loadChildrenFrom(metaOrFactory, fullPath, finalKey)) as unknown as MetaTreeNode[]; } const parentTitles = this.#buildParentTitles(fullPath); return [this.#toTreeNode(finalKey, metaOrFactory, fullPath, parentTitles)]; } if (metaOrFactory.properties) { const parentTitles = [...this.#buildParentTitles(fullPath), metaOrFactory.title]; const childNodes = this.#createChildNodes(metaOrFactory.properties, fullPath, parentTitles, metaOrFactory); return Array.isArray(childNodes) ? childNodes : []; } return []; } // 未找到目标路径,返回空数组 return []; } else if (propertyPath === null) { console.warn( `[FlowContext] getPropertyMetaTree - unsupported value format: "${value}". Only "{{ ctx.propertyName }}" format is supported. Returning empty meta tree.`, ); return []; } } // 根级节点按 meta.sort 降序排列(未设置默认为 0) const sorted = (Object.entries(metaMap) as [string, PropertyMetaOrFactory][]).sort(([, a], [, b]) => { const sa = (typeof a === 'function' ? a.sort : a?.sort) ?? 0; const sb = (typeof b === 'function' ? b.sort : b?.sort) ?? 0; return sb - sa; }); return sorted.map(([key, metaOrFactory]) => this.#toTreeNode(key, metaOrFactory, [key], [])); } #createChildNodes( properties: Record<string, PropertyMeta> | (() => Promise<Record<string, PropertyMeta>>), parentPaths: string[] = [], parentTitles: string[] = [], parentMeta?: PropertyMeta, // 传入父级 meta 以便缓存结果 ): MetaTreeNode[] | (() => Promise<MetaTreeNode[]>) { return typeof properties === 'function' ? async () => { const resolved = await properties(); // 缓存解析结果,避免下次重复调用 if (parentMeta) { parentMeta.properties = resolved; } const entries = Object.entries(resolved) as [string, PropertyMeta][]; entries.sort(([, a], [, b]) => (b?.sort ?? 0) - (a?.sort ?? 0)); return entries.map(([name, meta]) => this.#toTreeNode(name, meta, [...parentPaths, name], parentTitles)); } : (Object.entries(properties) as [string, PropertyMeta][]) .sort(([, a], [, b]) => (b?.sort ?? 0) - (a?.sort ?? 0)) .map(([name, meta]) => this.#toTreeNode(name, meta, [...parentPaths, name], parentTitles)); } /** * 根据属性路径查找对应的 meta * @param propertyPath 属性路径数组,例如 ["aaa", "bbb"] * @returns [finalKey, metaOrFactory, fullPath] 或 null */ #findMetaByPath(propertyPath: string[]): [string, PropertyMetaOrFactory, string[]] | null { if (propertyPath.length === 0) return null; const [firstKey, ...remainingPath] = propertyPath; // 首先查找第一个属性,这里利用委托链机制 // 1. 查找自身的属性 const ownProperty = this._props[firstKey]; if (ownProperty?.meta) { return this.#findMetaInProperty(firstKey, ownProperty.meta, remainingPath, [firstKey]); } // 2 进一步递归查找更深层委托链(_getPropertiesMeta 会递归收集,但此处原来仅检查了一层,导致不一致) const deepMeta = this.#findMetaInDelegatesDeep(this._delegates, firstKey); if (deepMeta) { return this.#findMetaInProperty(firstKey, deepMeta, remainingPath, [firstKey]); } return null; } /** * 递归在委托链中查找指定 key 的 meta(只返回 metaOrFactory,不解析路径)。 */ #findMetaInDelegatesDeep(delegates: FlowContext[], key: string): PropertyMetaOrFactory | null { for (const delegate of delegates) { const prop = delegate._props[key]; if (prop?.meta) return prop.meta; const deeper = this.#findMetaInDelegatesDeep(delegate._delegates, key); if (deeper) return deeper; } return null; } /** * 在给定属性的 meta 中查找剩余路径 */ #findMetaInProperty( currentKey: string, metaOrFactory: PropertyMetaOrFactory, remainingPath: string[], currentPath: string[], ): [string, PropertyMetaOrFactory, string[]] | null { // 如果已经到了最后一层,直接返回当前的 meta if (remainingPath.length === 0) { return [currentKey, metaOrFactory as any, currentPath]; } // 如果还有剩余路径,但当前是函数类型,构建一个新的异步函数继续解析剩余路径 if (typeof metaOrFactory === 'function') { const finalKey = remainingPath[remainingPath.length - 1]; const finalPath = [...currentPath, ...remainingPath]; const wrappedFactory = async (): Promise<PropertyMeta> => { const resolvedMeta = await metaOrFactory(); const result = await this.#resolvePathInMetaAsync(resolvedMeta, remainingPath); return result; }; return [finalKey, wrappedFactory, finalPath]; } // 如果还有剩余路径,且是同步 meta,尝试继续查找下一层 if (metaOrFactory.properties) { const [nextKey, ...restPath] = remainingPath; const nextPath = [...currentPath, nextKey]; // properties 是异步的,构建新的异步函数继续解析 if (typeof metaOrFactory.properties === 'function') { const finalKey = remainingPath[remainingPath.length - 1]; const finalPath = [...currentPath, ...remainingPath]; const wrappedFactory = async (): Promise<PropertyMeta> => { const propertiesFactory = metaOrFactory.properties as () => Promise<Record<string, PropertyMeta>>; const resolvedProperties = await propertiesFactory(); // 缓存解析结果,避免下次重复调用 metaOrFactory.properties = resolvedProperties; const startMeta = resolvedProperties[nextKey]; if (!startMeta) { throw new Error(`Property ${nextKey} not found in resolved properties`); } const result = await this.#resolvePathInMetaAsync(startMeta, restPath); return result; }; return [finalKey, wrappedFactory, finalPath]; } // properties 是同步的,继续查找 const nextMeta = metaOrFactory.properties[nextKey]; if (nextMeta) { return this.#findMetaInProperty(nextKey, nextMeta, restPath, nextPath); } } return null; } /** * 在给定的 meta 中递归解析路径 */ #resolvePathInMeta(meta: PropertyMeta, path: string[]): PropertyMeta | null { if (path.length === 0) { return meta; } let current = meta; for (const key of path) { const properties = _.get(current, 'properties'); if (!properties || typeof properties === 'function') { return null; // 无法同步解析异步 properties } current = _.get(properties, key); if (!current) { return null; } } return current; } /** * 支持异步 properties 的路径解析: * - 遇到 properties 为函数时会 await 并缓存其结果 * - 持续向下解析直到到达最终的 meta * 若解析失败则抛出异常,由调用方自行处理 */ async #resolvePathInMetaAsync(meta: PropertyMeta, path: string[]): Promise<PropertyMeta> { if (path.length === 0) return meta; let current: PropertyMeta = meta; for (const key of path) { let properties = _.get(current, 'properties'); if (!properties) { throw new Error(`Property path not found: ${path.join('.')}`); } if (typeof properties === 'function') { const resolved = await properties(); current.properties = resolved; properties = resolved; } const next = (properties as Record<string, PropertyMeta>)[key]; if (!next) { throw new Error(`Property ${key} not found while resolving path: ${path.join('.')}`); } current = next; } return current; } /** * 构建 parentTitles 数组,通过递归查找每个路径层级对应的 meta title * @param propertyPath 属性路径数组,例如 ['aaa', 'bbb', 'ccc'] * @param excludeLastLevel 是否排除最后一层,默认为 true(parentTitles 不包含当前节点) * @returns string[] 父级标题数组 */ #buildParentTitles(propertyPath: string[], excludeLastLevel = true): string[] { if (propertyPath.length === 0) return []; const pathToProcess = excludeLastLevel ? propertyPath.slice(0, -1) : propertyPath; if (pathToProcess.length === 0) return []; const parentTitles: string[] = []; // 从根级开始逐层查找 meta title let currentMetas = this._getPropertiesMeta(); for (let i = 0; i < pathToProcess.length; i++) { const currentKey = pathToProcess[i]; const currentMeta = currentMetas[currentKey]; if (!currentMeta || typeof currentMeta === 'function') { parentTitles.push(currentKey); break; } // 同步 meta,使用 title parentTitles.push(currentMeta.title || currentKey); // 为下一层级准备 meta 映射 if (i < pathToProcess.length - 1 && currentMeta.properties && typeof currentMeta.properties !== 'function') { currentMetas = currentMeta.properties as Record<string, PropertyMeta>; } else if (i < pathToProcess.length - 1) { // 如果下一层是异步的或者不存在,无法继续,使用路径名填充剩余部分 for (let j = i + 1; j < pathToProcess.length; j++) { parentTitles.push(pathToProcess[j]); } break; } } return parentTitles; } #toTreeNode( name: string, metaOrFactory: PropertyMetaOrFactory, paths: string[] = [name], parentTitles: string[] = [], ): MetaTreeNode { // 检查缓存 const cached = this._metaNodeCache.get(metaOrFactory); if (cached) { // 更新路径信息(因为同一个 meta 可能在不同路径下使用) cached.paths = paths; cached.parentTitles = parentTitles.length > 0 ? parentTitles : undefined; return cached; } let node: MetaTreeNode; // 计算禁用状态与原因的帮助函数 const computeDisabledFromMeta = (m: PropertyMeta): { disabled: boolean; reason?: string } => { if (!m) return { disabled: false }; const disabledVal = typeof m.disabled === 'function' ? m.disabled() : m.disabled; const reason = typeof m.disabledReason === 'function' ? m.disabledReason() : m.disabledReason; return { disabled: !!disabledVal, reason }; }; if (typeof metaOrFactory === 'function') { const initialTitle = name; const hasChildrenHint = (metaOrFactory as PropertyMetaFactory).hasChildren; node = { name, title: metaOrFactory.title || initialTitle, // 初始使用 name 作为 title type: 'object', // 初始类型 interface: undefined, uiSchema: undefined, paths, parentTitles: parentTitles.length > 0 ? parentTitles : undefined, disabled: () => { const maybe = metaOrFactory(); if (maybe && typeof maybe['then'] === 'function') return false; return computeDisabledFromMeta(maybe as PropertyMeta).disabled; }, disabledReason: () => { const maybe = metaOrFactory(); if (maybe && typeof maybe['then'] === 'function') return undefined; return computeDisabledFromMeta(maybe as PropertyMeta).reason; }, // 注意:即便 hasChildren === false,也只是“没有子节点”的 UI 提示; // 节点自身依然通过 meta 工厂保持惰性特性(需要时可解析出 title/type 等)。 // 这里仅在 hasChildren !== false 时,提供子节点的懒加载逻辑。 children: hasChildrenHint === false ? undefined : async () => { try { const meta = await metaOrFactory(); const finalTitle = meta?.title || name; node.title = finalTitle; node.type = meta?.type; node.interface = meta?.interface; node.uiSchema = meta?.uiSchema; // parentTitles 保持不变,因为它不包含自身 title if (!meta?.properties) return []; const childNodes = this.#createChildNodes( meta.properties, paths, [...parentTitles, finalTitle], meta, ); const resolvedChildren = Array.isArray(childNodes) ? childNodes : await childNodes(); // 更新 children 为解析后的结果 node.children = resolvedChildren; return resolvedChildren; } catch (error) { console.warn(`Failed to load meta for ${name}:`, error); return []; } }, }; } else { // 同步 meta:直接创建节点 const nodeTitle = metaOrFactory.title; const { disabled, reason } = computeDisabledFromMeta(metaOrFactory); node = { name, title: nodeTitle, type: metaOrFactory.type, interface: metaOrFactory.interface, uiSchema: metaOrFactory.uiSchema, paths, parentTitles: parentTitles.length > 0 ? parentTitles : undefined, disabled, disabledReason: reason, children: metaOrFactory.properties ? this.#createChildNodes(metaOrFactory.properties, paths, [...parentTitles, nodeTitle], metaOrFactory) : undefined, }; } // 缓存节点 this._metaNodeCache.set(metaOrFactory, node); return node; } _getPropertiesMeta(): Record<string, PropertyMetaOrFactory> { const metaMap: Record<string, PropertyMetaOrFactory> = {}; // 先处理委托链(委托链的 meta 优先级较低) for (const delegate of this._delegates) { Object.assign(metaMap, delegate._getPropertiesMeta()); } // 处理自身属性(自身属性优先级较高) for (const [key, options] of Object.entries(this._props)) { if (options.meta) { metaMap[key] = typeof options.meta === 'function' ? (options.meta as PropertyMetaFactory) : options.meta; } } return metaMap; } // 只查找自身 _props protected _getOwnProperty(key: string, currentContext): any { const options = this._props[key]; if (!options) return undefined; // 静态值 if ('value' in options) { return options.value; } // get 方法 if (options.get) { if (options.cache === false) { return options.get(currentContext); } const cacheKey = options.observable ? '_observableCache' : '_cache'; if (key in this[cacheKey]) { return this[cacheKey][key]; } if (this._pending[key]) return this._pending[key]; // 支持 async getter 并发排队 const result = options.get(this.createProxy()); // 判断是否为 Promise/thenable const isPromise = (typeof result === 'object' && result !== null && typeof (result as any).then === 'function') || (typeof result === 'function' && typeof (result as any).then === 'function'); if (isPromise) { this._pending[key] = (result as Promise<any>).then( (v) => { this[cacheKey][key] = v; delete this._pending[key]; return v; }, (err) => { delete this._pending[key]; throw err; }, ); return this._pending[key]; } // sync 直接缓存 this[cacheKey][key] = result; return result; } return undefined; } // 只查找自身 _methods protected _getOwnMethod(key: string, flowContext?: FlowContext): any { const fn = this._methods[key]; if (typeof fn === 'function') { return fn.bind(flowContext); } return fn; } _findPropertyInDelegates(delegates: FlowContext[], key: string): PropertyOptions | undefined { for (const delegate of delegates) { // 1. 查找委托的 _props if (Object.prototype.hasOwnProperty.call(delegate._props, key)) { return delegate._props[key]; } // 2. 递归查找更深层的委托链 const found = this._findPropertyInDelegates(delegate._delegates, key); if (found !== undefined) return found; } return undefined; } _findInDelegates(delegates: FlowContext[], key: string): any { for (const delegate of delegates) { // 1. 查找委托的 _props if (Object.prototype.hasOwnProperty.call(delegate._props, key)) { return { result: delegate._getOwnProperty(key, this.createProxy()), }; } // 2. 查找委托的 _methods if (Object.prototype.hasOwnProperty.call(delegate._methods, key)) { return { result: delegate._getOwnMethod(key, this.createProxy()), }; } // 3. 递归查找更深层的委托链 const found = this._findInDelegates(delegate._delegates, key); if (found !== undefined) return found; } return undefined; } // 递归查找委托链 _hasInDelegates(delegates: FlowContext[], key: string): boolean { for (const delegate of delegates) { if (Object.prototype.hasOwnProperty.call(delegate._props, key)) return true; if (Object.prototype.hasOwnProperty.call(delegate._methods, key)) return true; if (this._hasInDelegates(delegate._delegates, key)) return true; } return false; } /** * 获取属性定义选项(包含代理链)。 * * - 优先查找当前上下文自身通过 defineProperty 注册的属性定义 * - 若自身不存在,则沿委托链(delegates)向上查找第一个命中的定义 * * @param key 顶层属性名(例如 'user'、'view') * @returns 属性定义选项,或 undefined(未定义) */ getPropertyOptions(key: string): PropertyOptions | undefined { if (Object.prototype.hasOwnProperty.call(this._props, key)) { return this._props[key]; } return this._findPropertyInDelegates(this._delegates, key); } } export class FlowRunjsContext extends FlowContext { constructor(delegate: FlowContext) { super(); this.addDelegate(delegate); // Expose React and antd only within runjs context // This keeps the scope minimal while enabling React/AntD rendering in scripts this.defineProperty('React', { value: React }); this.defineProperty('antd', { value: antd }); this.defineMethod( 'dispatchModelEvent', async (modelOrUid: FlowModel | string, eventName: string, inputArgs?: Record<string, any>) => { let model: FlowModel | null = null; if (typeof modelOrUid === 'string') { model = await this.engine.loadModel({ uid: modelOrUid }); } else if (modelOrUid instanceof FlowModel) { model = modelOrUid; } if (model) { model.context.addDelegate(this); model.dispatchEvent(eventName, { navigation: false, ...this.model?.['getInputArgs']?.(), ...inputArgs }); } else { this.message.error(this.t('Model with ID {{uid}} not found', { uid: modelOrUid })); } }, ); } } class BaseFlowEngineContext extends FlowContext { declare router: Router; declare dataSourceManager: DataSourceManager; declare requireAsync: (url: string) => Promise<any>; declare createJSRunner: (options?: JSRunnerOptions) => JSRunner; /** * @deprecated use `resolveJsonTemplate` instead */ declare renderJson: (template: JSONValue) => Promise<any>; declare resolveJsonTemplate: (template: JSONValue) => Promise<any>; declare runjs: (code: string, variables?: Record<string, any>, options?: JSRunnerOptions) => Promise<any>; declare copyToClipboard: (text: string) => Promise<void>; declare getAction: <TModel extends FlowModel = FlowModel, TCtx extends FlowContext = FlowContext>( name: string, ) => ActionDefinition<TModel, TCtx> | undefined; declare getActions: <TModel extends FlowModel = FlowModel, TCtx extends FlowContext = FlowContext>() => Map< string, ActionDefinition<TModel, TCtx> >; declare getEvents: <TModel extends FlowModel = FlowModel, TCtx extends FlowContext = FlowContext>() => Map< string, EventDefinition<TModel, TCtx> >; declare runAction: (actionName: string, params?: Record<string, any>) => Promise<any> | any; declare engine: FlowEngine; declare api: APIClient; declare viewer: FlowViewer; declare view: FlowView; declare modal: HookAPI; declare message: MessageInstance; declare notification: NotificationInstance; declare route: RouteOptions; declare location: Location; declare sql: FlowSQLRepository; declare logger: pino.Logger; } class BaseFlowModelContext extends BaseFlowEngineContext { declare model: FlowModel; declare ref: React.RefObject<HTMLDivElement>; declare getAction: <TModel extends FlowModel = FlowModel, TCtx extends FlowContext = FlowContext>( name: string, ) => ActionDefinition<TModel, TCtx> | undefined; declare getActions: <TModel extends FlowModel = FlowModel, TCtx extends FlowContext = FlowContext>() => Map< string, ActionDefinition<TModel, TCtx> >; declare getEvents: <TModel extends FlowModel = FlowModel, TCtx extends FlowContext = FlowContext>() => Map< string, EventDefinition<TModel, TCtx> >; declare runAction: (actionName: string, params?: Record<string, any>) => Promise<any> | any; declare createResource: <T extends FlowResource = FlowResource>(resourceType: ResourceType<T>) => T; } export class FlowEngineContext extends BaseFlowEngineContext { // public dataSourceManager: DataSourceManager; constructor(public engine: FlowEngine) { if (!(engine instanceof FlowEngine)) { throw new Error('Invalid FlowEngine instance'); } super(); this.engine = engine; const dataSourceManager = new DataSourceManager(); dataSourceManager.setFlowEngine(this.engine); const mainDataSource = new DataSource({ key: 'main', displayName: 'Main', }); dataSourceManager.addDataSource(mainDataSource); this.defineProperty('engine', { value: this.engine, }); this.defineProperty('sql', { get: () => new FlowSQLRepository(this), }); this.defineProperty('dataSourceManager', { value: dataSourceManager, }); const i18n = new FlowI18n(this); this.defineMethod('t', (keyOrTemplate: string, options?: any) => { return i18n.translate(keyOrTemplate, options); }); this.defineMethod('runjs', async (code, variables, options?: JSRunnerOptions) => { const mergedGlobals = { ...(options?.globals || {}), ...(variables || {}) }; const runner = this.createJSRunner({ ...(options || {}), globals: mergedGlobals, }); return runner.run(code); }); this.defineMethod('renderJson', function (template: any) { return this.resolveJsonTemplate(template); }); this.defineMethod('resolveJsonTemplate', async function (this: BaseFlowEngineContext, template: any) { // 提取模板使用到的变量及其子路径 const used = extractUsedVariablePaths(template); const usedVarNames = Object.keys(used || {}); if (!usedVarNames.length) { // 模板未包含任何 ctx.* 变量,直接前端解析 return resolveExpressions(template, this); } // 分流:根据 resolveOnServer 标记与子路径判断哪些交给后端 const serverVarPaths: Record<string, string[]> = {}; for (const varName of usedVarNames) { const paths = used[varName] || []; const opt = this.getPropertyOptions(varName); const mark = opt?.resolveOnServer; if (mark === true) { serverVarPaths[varName] = paths; } else if (typeof mark === 'function') { const filtered = paths.filter((p) => { try { return !!mark(p); } catch (_) { return false; } }); if (filtered.length) serverVarPaths[varName] = filtered; } } const needServer = Object.keys(serverVarPaths).length > 0; let serverResolved = template; if (needServer) { const collectFromMeta = async (): Promise<Record<string, any>> => { const out: Record<string, any> = {}; try { const metas = this._getPropertiesMeta?.() as Record< string, PropertyMeta | (() => Promise<PropertyMeta | null> | PropertyMeta | null) >; if (!metas || typeof metas !== 'object') return out; for (const [key, metaOrFactory] of Object.entries(metas)) { if (!serverVarPaths[key]) continue; // 仅处理需要后端解析的变量 try { let meta: PropertyMeta | null; if (typeof metaOrFactory === 'function') { const fn = metaOrFactory as () => Promise<PropertyMeta | null>; meta = await fn(); } else { meta = metaOrFactory as PropertyMeta; } if (!meta || typeof meta !== 'object') continue; const builder = meta.buildVariablesParams; if (typeof builder !== 'function') continue; const built = await builder(this); if (!built) continue; const neededPaths = serverVarPaths[key] || []; const filtered = filterBuilderOutputByPaths(built, neededPaths); if (filtered && (typeof filtered !== 'object' || Object.keys(filtered).length)) { out[key] = filtered; } } catch (_) { // 忽略单个属性的错误 } } } catch (_) { // ignore } return out; }; const inputFromMeta = await collectFromMeta(); const autoInput = { ...inputFromMeta }; const autoContextParams = Object.keys(autoInput).length ? _buildServerContextParams(this, autoInput) : undefined; if (this.api) { try { serverResolved = await enqueueVariablesResolve(this as FlowRuntimeContext<FlowModel>, { template, contextParams: autoContextParams || {}, }); } catch (e) { this.logger?.warn?.({ err: e }, 'variables:resolve failed, fallback to client-only'); serverResolved = template; } } } return resolveExpressions(serverResolved, this); }); this.defineProperty('requirejs', { get: () => this.app?.requirejs?.requirejs, }); // Expose API token and current role as top-level variables for VariableInput. // Front-end only: no resolveOnServer flag. Mark cache: false to reflect runtime changes. this.defineProperty('token', { get: () => this.api?.auth?.token, cache: false, // 注意:使用惰性 meta 工厂,避免在 i18n 尚未注入时提前求值导致无法翻译 meta: Object.assign(() => ({ type: 'string', title: this.t('API Token'), sort: 980 }), { title: 'API Token', sort: 980, hasChildren: false, }), }); this.defineProperty('role', { get: () => this.api?.auth?.role, cache: false, // 注意:使用惰性 meta 工厂,避免在 i18n 尚未注入时提前求值导致无法翻译 meta: Object.assign(() => ({ type: 'string', title: this.t('Current role'), sort: 990 }), { title: escapeT('Current role'), sort: 990, hasChildren: false, }), }); // URL 查询参数(等价于 1.0 的 `$nURLSearchParams`) this.defineProperty('urlSearchParams', { // 不缓存,确保随 URL 变化实时生效 cache: false, get: () => { const search = this.location?.search || ''; const str = search.startsWith('?') ? search.slice(1) : search; return (qs.parse(str) as Record<string, any>) || {}; }, // 变量选择器中的元信息与动态子项 meta: Object.assign( () => ({ type: 'object', title: this.t('URL search params'), sort: 970, disabled: () => { const search = this.location?.search || ''; const str = search.startsWith('?') ? search.slice(1) : search; const params = (qs.parse(str) as Record<string, any>) || {}; return Object.keys(params).length === 0; }, disabledReason: () => this.t( 'The value of this variable is derived from the query string of the page URL. This variable can only be used normally when the page has a query string.', ), properties: async () => { const search = this.location?.search || ''; const str = search.startsWith('?') ? search.slice(1) : search; const params = (qs.parse(str) as Record<string, any>) || {}; const props: Record<string, any> = {}; for (const key of Object.keys(params)) { props[key] = { type: 'string', title: key }; } return props; }, }), { title: escapeT('URL search params'), sort: 970, hasChildren: true, }, ), }); this.defineProperty('logger', { get: () => { return this.engine.logger.child({ module: 'flow-engine' }); }, }); this.defineProperty('auth', { get: () => ({ roleName: this.api.auth.role, locale: this.api.auth.locale, token: this.api.auth.token, user: this.user, }), }); this.defineMethod('loadCSS', async (url: string) => { return new Promise((resolve, reject) => { // Check if CSS is already loaded const existingLink = document.querySelector(`link[href="${url}"]`); if (existingLink) { resolve(null); return; } const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = url; link.onload = () => resolve(null); link.onerror = () => reject(new Error(`Failed to load CSS: ${url}`)); document.head.appendChild(link); }); }); this.defineMethod('requireAsync', async (url: string) => { return new Promise((resolve, reject) => { if (!this.requirejs) { reject(new Error('requirejs is not available')); return; } this.requirejs( [url], (...args: any[]) => { resolve(args[0]); }, reject, ); }); }); this.defineMethod('createJSRunner', function (options?: JSRunnerOptions) { // return createJSRunnerWithVersion.call(this, options as any); const runCtx = new FlowRunjsContext(this.createProxy()); return new JSRunner({ ...options, globals: { ctx: runCtx, window: createSafeWindow(), document: createSafeDocument(), ...options?.globals, }, }); }); // 复制文本到剪贴板(优先使用 Clipboard API,降级到 execCommand) this.defineMethod('copyToClipboard', async (text: string) => { const content = String(text ?? ''); try { if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(content); return; } } catch (e) { // 忽略,尝试降级方案 } // 降级方案:创建临时 textarea + execCommand('copy') return new Promise<void>((resolve, reject) => { try { const ta = document.createElement('textarea'); ta.value = content; ta.setAttribute('readonly', ''); ta.style.position = 'fixed'; ta.style.top = '-9999px'; document.body.appendChild(ta); ta.focus(); ta.select(); const ok = document.execCommand('copy'); document.body.removeChild(ta); if (ok) resolve(); else reject(new Error('execCommand copy failed')); } catch (err) { reject(err); } }); }); // Helper: build server contextParams for variables:resolve this.defineMethod('buildServerContextParams', function (this: BaseFlowEngineContext, input?: any) { return _buildServerContextParams(this, input); }); this.defineMethod('getAction', function (this: BaseFlowEngineContext, name: string) { return this.engine.getAction(name); }); this.defineMethod('getActions', function (this: BaseFlowEngineContext) { return this.engine.getActions(); }); this.defineMethod('getEvents', function (this: BaseFlowEngineContext) { return this.engine.getEvents(); }); // // Date variables (for variable selector meta tree) // this.defineProperty('date', { // get: () => { // const vars = getDateVars() as Record<string, any>; // // align with client options: add dayBeforeYesterday // vars.dayBeforeYesterday = toUnit('day', -2); // const now = new Date().toISOString(); // const out: Record<string, any> = {}; // for (const [k, v] of Object.entries(vars)) { // try { // out[k] = typeof v === 'function' ? v({ now }) : v; // } catch (e) { // // ignore // } // } // return out; // }, // meta: () => { // const title = this.t('Date variables'); // const mk = (t: string) => ({ type: 'any', title: this.t(t) }); // return { // type: 'object', // title, // properties: { // now: mk('Current time'), // dayBeforeYesterday: mk('Day before yesterday'), // yesterday: mk('Yesterday'), // today: mk('Today'), // tomorrow: mk('Tomorrow'), // lastIsoWeek: mk('Last week'), // thisIsoWeek: mk('This week'), // nextIsoWeek: mk('Next week'), // lastMonth: mk('Last month'), // thisMonth: mk('This month'), // nextMonth: mk('Next month'), // lastQuarter: mk('Last quarter'), // thisQuarter: mk('This quarter'), // nextQuarter: mk('Next quarter'), // lastYear: mk('Last year'), // thisYear: mk('This year'), // nextYear: mk('Next year'), // last7Days: mk('Last 7 days'), // next7Days: mk('Next 7 days'), // last30Days: mk('Last 30 days'), // next30Days: mk('Next 30 days'), // last90Days: mk('Last 90 days'), // next90Days: mk('Next 90 days'), // }, // } as PropertyMeta; // }, // }); this.defineMethod( 'runAction', async function (this: BaseFlowEngineContext, actionName: string, params?: Record<string, any>) { const def = this.engine.getAction<FlowModel, FlowEngineContext>(actionName); const ctx = this.createProxy() as unknown as FlowEngineContext; if (!def) { throw new Error(`Action '${actionName}' not found.`); } const defaultParams = await resolveDefaultParams(def.defaultParams, ctx); let combinedParams: Record<string, any> = { ...(defaultParams || {}), ...(params || {}) }; let useRawParams = def.useRawParams; if (typeof useRawParams === 'function') { useRawParams = await useRawParams(ctx); } if (!useRawParams) { // 先服务端解析,再前端补齐 combinedParams = await (ctx as any).resolveJsonTemplate(combinedParams); } if (!def.handler) { throw new Error(`Action '${actionName}' has no handler.`); } return def.handler(ctx, combinedParams); }, ); this.defineProperty('acl', { get: () => { const acl = new ACL(this.engine); return acl; }, }); this.defineMethod('aclCheck', function (params) { return this.acl.aclCheck(params); }); this.defineMethod('createResource', function (this: BaseFlowEngineContext, resourceType) { return this.engine.createResource(resourceType, { context: this.createProxy(), }); }); } } export class FlowModelContext extends BaseFlowModelContext { constructor(model: FlowModel) { if (!(model instanceof FlowModel)) { throw new Error('Invalid FlowModel instance'); } super(); this.addDelegate(model.flowEngine.context); this.defineMethod('onRefReady', (ref, cb, timeout) => { this.engine.reactView.onRefReady(ref, cb, timeout); }); this.defineMethod('runjs', async (code, variables, options?: { version?: string }) => { const runner = this.createJSRunner({ globals: variables, version: options?.version, }); return runner.run(code); }); this.defineProperty('model', { value: model, }); this.defineProperty('ref', { get: () => { this.model['_refCreated'] = true; return createRef<HTMLDivElement>(); }, }); this.defineMethod('openView', async function (uid: string, options) { const opts = { ...options }; if (opts.defineProperties || opts.defineMethod) { opts.navigation = false; // 强制不使用路由导航, 避免刷新页面时丢失上下文 } let model: FlowModel | null = null; model = await this.engine.loadModel({ uid }); if (!model) { model = this.engine.createModel({ uid, // 注意: 新建的 model 应该使用 ${parentModel.uid}-xxx 形式的 uid use: 'PopupActionModel', parentId: this.model.uid, subType: 'object', subKey: uid, stepParams: { popupSettings: { openView: { ..._.pick(opts, ['dataSourceKey', 'collectionName', 'associationName']), }, }, }, }); await model.save(); } if (model.getStepParams('popupSettings')?.openView?.dataSourceKey) { model.setStepParams('popupSettings', { openView: { ...model.getStepParams('popupSettings')?.openView, ..._.pick(opts, ['dataSourceKey', 'collectionName', 'associationName']), }, }); await model.save(