Skip to content

fix: ES2020 compatibility for automation engine build#750

Merged
hotlong merged 2 commits into
mainfrom
copilot/implement-automation-engine-architecture
Feb 20, 2026
Merged

fix: ES2020 compatibility for automation engine build#750
hotlong merged 2 commits into
mainfrom
copilot/implement-automation-engine-architecture

Conversation

Copilot AI commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

CI build fails because String.prototype.replaceAll() (ES2021) is used in the automation engine, but the project targets ES2020 via root tsconfig.json.

Changes

  • Replace replaceAll(pattern, replacement) with equivalent split(pattern).join(replacement) in:
    • engine.tsevaluateCondition() template variable substitution
    • logic-nodes-plugin.ts — decision node condition expression evaluation
// Before (ES2021)
resolved = resolved.replaceAll(`{${key}}`, String(value));

// After (ES2020)
resolved = resolved.split(`{${key}}`).join(String(value));
Original prompt

This section details on the original issue you should resolve

<issue_title>实现自动化引擎插件化架构,支持能力无限扩展</issue_title>
<issue_description>## 目标
设计并实现一个基础的自动化引擎,允许通过插件持续扩展节点能力。

方案概述

  1. 基于 MicroKernel 插件架构:引擎作为一个 Kernel 插件,只负责 DAG 解析和节点分发。各类自动化能力(节点类型/触发器/连接器/AI/审批等)全部通过独立插件扩展,按需加载。
  2. NodeExecutor 扩展点:每种节点类型都是一个实现 NodeExecutor 的插件,注册到核心引擎后即生效。
  3. Service Registry 提供依赖注入和插件发现:插件通过 PluginContext.registerService/getService方式互相调用。引擎暴露为 'automation' 服务。
  4. Hook/Event 总线:流程执行前后、节点执行时均可通过 hook 拦截与观察。第三方插件可注入自定义处理逻辑。
  5. 依赖拓扑排序保证初始化顺序:插件声明 dependencies,保证能力插件在引擎启动后注册。

技术任务拆分

MVP

  • 基础引擎实现(AutomationEngine 类)
    • 支持 FlowSchema/DAG 流程执行
    • NodeExecutor & Trigger 接口(插件扩展点)
    • Plugin 封装,暴露为 'automation' service
  • CRUD 节点插件(get/create/update/delete_record)
  • 核心逻辑节点插件(decision/assignment/loop)
  • HTTP/Connector 节点插件(http_request/connector_action)
  • 插件化注册与热插拔能力

二期

  • Script/Await/AI/Approval/Schedule/Event 节点插件
  • Studio 可视化能力插件

示例代码

// 插件注册API示例
kernel.use(createAutomationPlugin()); // 核心引擎
kernel.use(createCrudNodesPlugin());  // CRUD能力
kernel.use(createLogicNodesPlugin()); // 条件/赋值/循环
kernel.use(createHttpConnectorPlugin()); // HTTP/集成
kernel.use(createAINodePlugin()); // AI节点

验收标准

  • 任意新节点类型或自动化能力均可通过新插件独立扩展
  • 插件注册后无需重启即可新增/热插拔功能
  • 扩展流程符合对象协议与ROADMAP规划

备注:开发任务完成后需补充 test,并及时更新 roadmap。


🏗️ 整体架构:基于 ObjectStack MicroKernel 的插件化 Automation Engine

ObjectStack 已经有一套成熟的 MicroKernel + Plugin 架构,自动化引擎应该 完全复用 这套体系,而不是另起炉灶。

核心思路

┌─────────────────────────────────────────────────────────┐
│                    ObjectKernel (LiteKernel)              │
│  ┌─────────────────────────────────────────────────────┐ │
│  │              Service Registry (DI Container)         │ │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────��───────────┐ │ │
│  │  │   db     │ │  http    │ │  automation (核心引擎) │ │ │
│  │  └──────────┘ └──────────┘ └──────────────────────┘ │ │
│  └─────────────────────────────────────────────────────┘ │
│  ┌─────────────────────────────────────────────────────┐ │
│  │              Hook / Event Bus                        │ │
│  │   automation:beforeExecute  automation:afterExecute   │ │
│  │   automation:nodeExecute    automation:flowRegistered  │ │
│  └─────────────────────────────────────────────────────┘ │
│                                                           │
│  Plugins:                                                 │
│  ┌─────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│  │  core-nodes  │ │ connector-   │ │ ai-node-plugin    │ │
│  │  plugin      │ │ plugin       │ │                   │ │
│  └─────────────┘ └──────────────┘ └───────────────────┘ │
│  ┌─────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│  │ schedule-   │ │ approval-    │ │ custom-script-    │ │
│  │ trigger     │ │ plugin       │ │ plugin            │ │
│  └─────────────┘ └──────────────┘ └───────────────────┘ │
└─────────────────────────────────────────────────────────┘

第一步:创建核心自动化引擎插件

这是唯一的「硬核」部分——它只做 DAG 解析 + 节点分发 + 变量流转,不实现任何具体节点逻辑。

// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import type { FlowParsed, FlowNodeParsed, FlowEdgeParsed } from '@objectstack/spec/automation';
import type { AutomationContext, AutomationResult, IAutomationService } from '@objectstack/spec/contracts';
import type { Logger } from '@objectstack/spec/contracts';

// ─── Node Executor Interface (插件扩展点) ───────────────────────────

/**
 * 每种节点类型对应一个 NodeExecutor。
 * 第三方插件只需实现这个接口,注册到引擎即可扩展自动化能力。
 */
export interface NodeExecutor {
    /** 对应 FlowNodeAction 枚举值 */
    readonly type: string;

    /**
     * 执行节点
     * @param node - 当前节点定义
     * @param variables - 流程变量上下文(可读写)
     * @param context - 触发上下文
     * @returns 执行结果(可包含输出数据、分支条件等)
     */
    execute(
        node: FlowNodeParsed,
        variables: Map<string, unknown>,
        context: AutomationContext,
    ): Promise<NodeExecutionResult>;
}

export interface NodeExecutionResult {
    success: boolean;
    output?: Record<string, unknown>;
    error?: string;
    /** 用于 decision 节点,返回选中的分支标签 */
    branchLabel?: string;
}

// ─── Trigger Interface (插件扩展点) ─────────────────────────────────

/**
 * 触发器接口。Schedule/Event/API 等触发器通过插件注册。
 */
export interface FlowTrigger {
    readonly type: string;
    start(flowName: string, callback: (ctx: AutomationContext) => Promise<void>): void;
    stop(flowName: string): void;
}

// ─── Core Automation Engine ─────────────────────────────────────────

export class AutomationEngine implements IAutomationService {
    private flows = new Map<string, FlowParsed>();
    private nodeExecutors = new Map<string, NodeExecutor>();
    private triggers = new Map<string, FlowTrigger>();
    private logger: Logger;

    constructor(logger: Logger) {
        this.logger = logger;
    }

    // ── 插件扩展 API ─────────────────...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes objectstack-ai/spec#741

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

@vercel

vercel Bot commented Feb 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Feb 20, 2026 4:40am
spec Ready Ready Preview, Comment Feb 20, 2026 4:40am

Request Review

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement plugin architecture for automation engine fix: ES2020 compatibility for automation engine build Feb 20, 2026
Copilot AI requested a review from hotlong February 20, 2026 04:40
@hotlong
hotlong marked this pull request as ready for review February 20, 2026 05:42
Copilot AI review requested due to automatic review settings February 20, 2026 05:42
@hotlong
hotlong merged commit 1ed1e8d into main Feb 20, 2026
4 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes the service-automation build under the repo’s ES2020 TypeScript target by removing use of the ES2021-only String.prototype.replaceAll() API in the automation engine’s condition/template substitution logic.

Changes:

  • Replaced replaceAll() with ES2020-compatible split().join() in AutomationEngine.evaluateCondition().
  • Replaced replaceAll() with ES2020-compatible split().join() in the logic plugin’s decision-node expression evaluation.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
packages/services/service-automation/src/engine.ts Removes ES2021 replaceAll() usage in condition expression variable substitution.
packages/services/service-automation/src/plugins/logic-nodes-plugin.ts Removes ES2021 replaceAll() usage in decision-node condition variable substitution.
Comments suppressed due to low confidence (1)

packages/services/service-automation/src/plugins/logic-nodes-plugin.ts:37

  • There’s no test that exercises decision-node expression substitution (e.g. a flow with a decision condition that references {status} and branches accordingly). Since this logic is part of the plugin and impacts runtime behavior, adding an integration test in the existing service-automation test suite would help prevent future regressions.
                for (const cond of conditions) {
                    // MVP: Simple template replacement + expression evaluation.
                    // Flow definitions are authored by trusted developers/admins.
                    // TODO: Replace with safe expression evaluator (e.g., jexl) for production.
                    let expr = cond.expression;
                    for (const [k, v] of variables) {
                        expr = expr.split(`{${k}}`).join(String(v));
                    }
                    try {
                        if (new Function(`return (${expr})`)()) {
                            return { success: true, branchLabel: cond.label };

let expr = cond.expression;
for (const [k, v] of variables) {
expr = expr.replaceAll(`{${k}}`, String(v));
expr = expr.split(`{${k}}`).join(String(v));

Copilot AI Feb 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in engine.ts: split().join() inserts the replacement literally and does not interpret "$" sequences the way replaceAll() does. Please confirm literal insertion is intended for decision expressions (it avoids surprising "$1"/"$&" behavior in values); otherwise you’ll need an ES2020-compatible implementation that preserves replaceAll semantics.

Suggested change
expr = expr.split(`{${k}}`).join(String(v));
expr = expr.replaceAll(`{${k}}`, String(v));

Copilot uses AI. Check for mistakes.
Comment on lines 231 to 233
for (const [key, value] of variables) {
resolved = resolved.replaceAll(`{${key}}`, String(value));
resolved = resolved.split(`{${key}}`).join(String(value));
}

Copilot AI Feb 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using split().join() is not fully equivalent to replaceAll() when the replacement string contains "$" sequences (e.g. "$&", "$1"); replaceAll interprets those as replacement patterns, while join inserts them literally. Please confirm the intended semantics for template substitution (literal insertion is usually what you want here); if you need to preserve replaceAll’s behavior, you’ll need an ES2020-compatible polyfill that reproduces those "$" replacement patterns.

Copilot uses AI. Check for mistakes.
Comment on lines 226 to 233
private evaluateCondition(expression: string, variables: Map<string, unknown>): boolean {
// MVP: Simple template replacement + expression evaluation.
// Flow definitions are authored by trusted developers/admins.
// TODO: Replace with safe expression evaluator (e.g., jexl) for production.
let resolved = expression;
for (const [key, value] of variables) {
resolved = resolved.replaceAll(`{${key}}`, String(value));
resolved = resolved.split(`{${key}}`).join(String(value));
}

Copilot AI Feb 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The template substitution/evaluation path isn’t covered by current tests (existing coverage only checks constant conditions like "true"/"false"). Adding a small test that exercises {var} substitution—especially with values containing $—would lock in the intended behavior and prevent regressions when changing the ES target/polyfills later.

Copilot generated this review using guidance from organization custom instructions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants