Activepieces 深度全解:AI 优先的工作流自动化平台

7 阅读15分钟

一句话定位:Activepieces 是一个 AI 优先的开源工作流自动化平台,是 Zapier / n8n 的开源替代品——但它比它们走得更远,每个集成插件同时也是一个 MCP Server,可以直接被 Claude、Cursor 等 AI 工具调用。

1. 它是什么?

工作流自动化平台是什么

想象一个场景:每当客户在网站提交表单,自动发邮件给销售,同时在 Slack 发通知,再把信息写进 Google Sheet。这就是工作流自动化要解决的问题。

传统做法是手写代码,Zapier 这类产品让非技术人员也能用可视化界面完成同样的事。Activepieces 走的是同一条路,但做了三件 Zapier 没做的事:

┌────────────────────────────────────────────────────────────┐
│               Activepieces 的三大差异化                      │
├──────────────────┬─────────────────────────────────────────┤
│  完全开源         │  MIT 许可证,可自托管,代码 100% 透明     │
│  开发者友好       │  Pieces 是 TypeScript npm 包,可热重载    │
│  AI 原生         │  每个 Piece = MCP Server,AI 可直接调用   │
└──────────────────┴─────────────────────────────────────────┘

与竞品对比

维度Zapiern8nActivepieces
开源✅(部分)✅ MIT
自托管
MCP 支持✅ 280+
插件开发语言无法扩展JS/TSTypeScript(类型安全)
AI Agent 原生集成有限
企业版闭源商业闭源商业独立授权但共存

2. 核心概念

理解 Activepieces,先搞懂四个词:

┌─────────────────────────────────────────────────────────────────┐
│                        一条 Flow(流程)                          │
│                                                                   │
│   ┌─────────┐       ┌──────────┐      ┌──────────┐              │
│   │ Trigger │──────▶│ Action 1 │─────▶│ Action 2 │─── ...       │
│   │(触发器)  │       │ (动作)   │      │ (动作)   │              │
│   └─────────┘       └──────────┘      └──────────┘              │
│                                                                   │
│   Trigger 和 Action 都来自 Piece(集成插件)                       │
│   每个 Piece = 一个 npm 包 = 一组 Triggers + Actions              │
└─────────────────────────────────────────────────────────────────┘

Flow(流程) :一个完整的自动化任务,由一个 Trigger 和若干 Actions 组成。

Piece(插件) :对某个第三方服务的集成封装,比如 airtableopenaislack。每个 Piece 是一个独立的 npm 包。

Trigger(触发器) :流程的起点。分两种:

  • POLLING:Activepieces 定时轮询检查是否有新事件(如:Airtable 有新记录)
  • APP_WEBHOOK:对方主动调用 Activepieces 的 Webhook(如:GitHub Push 事件)

Action(动作) :流程的执行步骤,例如"发送邮件"、"创建记录"、"调用 AI"。


3. 整体架构

系统组件全景

                    ┌─────────────────────────────────────┐
                    │          用户 / AI 工具               │
                    │  浏览器  |  Claude Desktop  |  Cursor │
                    └──────────────┬──────────────────────┘
                                   │ HTTP / MCP
                    ┌──────────────▼──────────────────────┐
                    │           API Server                  │
                    │      Fastify 5 + TypeORM              │
                    │   (packages/server/api)               │
                    └──────┬──────────────┬────────────────┘
                           │              │
               ┌───────────▼──┐   ┌───────▼──────────────┐
               │  PostgreSQL   │   │       Redis           │
               │  (主数据库)   │   │  (BullMQ 队列 + 缓存) │
               └───────────────┘   └──────┬───────────────┘
                                          │ 任务队列
                    ┌─────────────────────▼──────────────┐
                    │           Worker                    │
                    │  (packages/server/worker)           │
                    │  通过 Socket.IO 连接 API Server     │
                    └──────────────┬──────────────────────┘
                                   │ 生成沙箱进程
              ┌────────────────────▼───────────────────────┐
              │            Sandbox (隔离进程)                │
              │       packages/server/engine               │
              │  实际执行 Piece 代码的地方                   │
              │  每次 Action 执行都在独立进程中运行           │
              └────────────────────────────────────────────┘

一次 Flow 执行的完整链路

用户点击"发布流程"
       │
       ▼
API Server 收到 Webhook / 定时触发
       │
       ▼
将 Job 推送到 BullMQ 队列 (Redis)
       │
       ▼
Worker 通过 Socket.IO 长连接轮询到 Job
       │
       ▼
Worker 创建 Sandbox(隔离进程)
       │
       ▼
Engine 在 Sandbox 内逐步执行每个 Action
  - 加载对应的 Piece npm 包
  - 传入 context(auth、propsValue、store...)
  - 调用 action.run(context)
  - 收集结果,传递给下一个 Action
       │
       ▼
执行结束,Worker 通知 API Server
       │
       ▼
API Server 更新 FlowRun 状态,触发 Side Effects
(发通知、更新统计、触发事件总线...)

多租户数据模型

Platform(平台/品牌)
    └── Project(项目/工作空间)
            ├── Flow(流程)
            ├── Connection(连接/凭据)
            └── User(用户)

⚠️ 关键规则:所有数据库查询必须带 projectId 过滤,这是防止数据越权的硬性要求。


4. 目录结构

Monorepo 全局视图

activepieces/                     ← 根目录
├── packages/                     ← 所有核心包(核心)
│   ├── server/                   ← 后端
│   │   ├── api/                  ← HTTP 接口层
│   │   ├── engine/               ← 流程执行引擎
│   │   ├── sandbox/              ← 沙箱管理
│   │   ├── worker/               ← 任务消费Worker
│   │   └── utils/                ← 服务端工具
│   ├── pieces/                   ← 所有集成插件
│   │   ├── framework/            ← Piece SDK(开发框架)
│   │   ├── community/            ← 280+ 社区插件
│   │   ├── common/               ← 共享HTTP工具等
│   │   └── core/                 ← 内置核心插件
│   ├── shared/                   ← 前后端共享类型
│   ├── react-ui/                 ← 前端(React,新)
│   ├── web/                      ← 前端(Angular,旧)
│   ├── ee/                       ← 企业版功能
│   └── cli/                      ← 开发者命令行工具
├── .agents/features/             ← 每功能模块 AI 辅助文档
├── deploy/                       ← Docker 部署配置
├── AGENTS.md                     ← 全局架构规范(必读)
├── docker-compose.yml            ← 生产部署
└── docker-compose.dev.yml        ← 本地开发

构建工具链

Bun(包管理 + 运行时)
  └── Turborepo(Monorepo 任务编排)
        ├── 并行构建所有包
        ├── 处理依赖关系(^build 表示先构建依赖)
        └── 缓存构建结果(提速)

5. 后端解析

API Server 的请求处理模式

后端用 Fastify 5 + fastify-type-provider-zod 构建,每个路由都有完整的类型安全保障。

// 典型的 Controller 结构(packages/server/api/src/app/flows/flow/flow.controller.ts)

export const flowController: FastifyPluginAsyncZod = async (app) => {

  // 创建 Flow —— 注意:用 POST,不是 PUT
  app.post('/', {
    config: {
      // 安全配置:声明哪些角色、哪些权限可以访问
      security: securityAccess.project(
        [PrincipalType.USER, PrincipalType.SERVICE],
        Permission.WRITE_FLOW,
        { type: ProjectResourceType.BODY }
      )
    },
    schema: {
      body: CreateFlowRequest,           // Zod 自动验证请求体
      response: { 201: PopulatedFlow }   // 自动序列化响应
    }
  }, async (request, reply) => {

    const newFlow = await flowService(request.log).create({
      projectId: request.projectId,  // ← 多租户:从请求上下文取项目ID
      request: request.body,
    })

    // 副作用:发应用事件(与主逻辑解耦)
    applicationEvents(request.log).sendUserEvent(request, {
      action: ApplicationEventName.FLOW_CREATED,
      data: { flow: newFlow }
    })

    return reply.status(201).send(newFlow)
  })
}

关键设计模式

┌────────────────────────────────────────────────────┐
│              Controller(路由层)                    │
│  职责:验证入参、鉴权、调用 Service、返回响应         │
└───────────────────────┬────────────────────────────┘
                        │ 调用
┌───────────────────────▼────────────────────────────┐
│              Service(业务逻辑层)                    │
│  职责:核心业务逻辑、数据库操作、调用其他 Service     │
└───────────────────────┬────────────────────────────┘
                        │ 调用
┌───────────────────────▼────────────────────────────┐
│          Side Effects(副作用层)                    │
│  职责:发通知、事件总线、清理缓存、记录日志           │
│  特点:在 mutation 之后显式调用,与主流程解耦         │
└────────────────────────────────────────────────────┘

Worker 的工作原理

Worker 通过 Socket.IO 与 API Server 保持长连接,主动"拉取"任务(而不是被动接收推送),这样可以精细控制并发:

// 简化的 worker 核心循环(packages/server/worker/src/lib/worker.ts)

// Worker 启动时
socket = io(socketUrl, { auth: { token, workerId } })

// 连接成功后,开始多并发轮询
const concurrency = system.get(WorkerSystemProp.WORKER_CONCURRENCY) // 默认 1
sandboxManagers = Array.from({ length: concurrency }, createSandboxManager)

// 每个并发槽独立轮询
while (polling) {
  const job = await apiClient.poll(machineInfo) // 长轮询,有任务才返回

  if (job) {
    // 在 Sandbox 中执行,隔离不同租户/流程的代码
    const result = await executeJob(apiClient, job, sbManager)
    await apiClient.completeJob({ jobId, status: result.status })
  }
}

Side Effects 的分离设计

// packages/server/api/src/app/flows/flow-run/flow-run-side-effects.ts
// 副作用与业务逻辑彻底分离,可测试、可替换

export const flowRunSideEffects = (log: FastifyBaseLogger) => ({

  async onFinish(flowRun: FlowRun): Promise<void> {
    await waitpointService(log).deleteByFlowRunId(flowRun.id) // 清理等待点
    await flowRunHooks(log).onFinish(flowRun)                 // 调用钩子
    applicationEvents(log).sendWorkerEvent(flowRun.projectId, {
      action: ApplicationEventName.FLOW_RUN_FINISHED,
      data: { flowRun }
    })
  },

  async onStart(flowRun: FlowRun): Promise<void> { ... },
  async onResume(flowRun: FlowRun): Promise<void> { ... },
})

6. 执行引擎

Sandbox 隔离机制

Worker 进程(长期运行)
    │
    ├── 收到 Job
    │
    ├── 创建 Sandbox(子进程,packages/server/engine)
    │      ├── 挂载只读文件系统
    │      ├── 限制内存和 CPU
    │      ├── 网络出口通过 Egress Proxy 过滤(防 SSRF)
    │      └── 通过 Socket.IO (IPC) 与 Worker 通信
    │
    ├── Engine 在 Sandbox 内执行
    │      ├── 动态 require() 对应的 Piece 包
    │      ├── 构建 context 对象(含 auth、store 等)
    │      └── 调用 action.run(context)
    │
    └── 执行完毕,Sandbox 进程退出(或被复用)

Engine 入口(packages/server/engine/src/main.ts)极其简洁:

// 这就是 Sandbox 进程的入口
ssrfGuard.install()         // 安装网络防护

const SANDBOX_ID = process.env.SANDBOX_ID
if (!isNil(SANDBOX_ID)) {
  workerSocket.init(SANDBOX_ID)   // 连接父进程
  runProgressService.init()       // 初始化进度上报
}

// 未捕获错误 → 告知父进程 → 退出
process.on('uncaughtException', (error) => {
  workerSocket.sendError(error)
  process.exit(3)
})

7. Piece 开发

这是整个项目最有学习价值的部分,也是贡献门槛最低的入口。

Piece 的解剖图

community/my-service/
├── src/
│   ├── index.ts              ← 📌 入口:createPiece() 把所有东西组装起来
│   └── lib/
│       ├── auth.ts           ← 认证配置(API Key / OAuth2 / 自定义)
│       ├── actions/          ← 动作(用户主动触发的操作)
│       │   ├── create-item.ts
│       │   └── delete-item.ts
│       ├── trigger/          ← 触发器(监听外部事件)
│       │   └── new-item.trigger.ts
│       └── common/           ← 公共的 API 封装、常量等
│           └── index.ts
└── src/i18n/translation.json ← 国际化文本

【简单示例】一个最小的 Piece

// src/index.ts —— 组装入口
import { createPiece, PieceAuth } from '@activepieces/pieces-framework'
import { PieceCategory } from '@activepieces/shared'
import { sayHelloAction } from './lib/actions/say-hello'

export const myService = createPiece({
  displayName: 'My Service',
  description: '这是一个示例 Piece',
  logoUrl: 'https://cdn.activepieces.com/pieces/my-service.png',
  auth: PieceAuth.SecretText({
    displayName: 'API Key',
    required: true,
    validate: async ({ auth }) => {
      // 验证 API Key 是否有效
      if (auth === 'valid-key') return { valid: true }
      return { valid: false, error: 'API Key 无效' }
    }
  }),
  categories: [PieceCategory.PRODUCTIVITY],
  actions: [sayHelloAction],
  triggers: [],
  authors: ['your-github-username']
})
// src/lib/actions/say-hello.ts —— 最简单的 Action
import { createAction, Property } from '@activepieces/pieces-framework'

export const sayHelloAction = createAction({
  name: 'say_hello',
  displayName: '打个招呼',
  description: '向指定用户发送问候',
  props: {
    // Property 会自动生成对应的 UI 表单控件
    name: Property.ShortText({
      displayName: '名字',
      required: true,
    }),
    language: Property.StaticDropdown({
      displayName: '语言',
      required: true,
      options: {
        options: [
          { label: '中文', value: 'zh' },
          { label: 'English', value: 'en' }
        ]
      }
    })
  },
  async run(context) {
    const { name, language } = context.propsValue
    return language === 'zh'
      ? `你好,${name}!`
      : `Hello, ${name}!`
  }
})

【进阶示例】带 HTTP 请求的 Action

这是真实生产代码的结构,参考 Airtable 的实现:

// src/lib/actions/create-record.ts
import { createAction, Property } from '@activepieces/pieces-framework'
import { httpClient, HttpMethod, AuthenticationType } from '@activepieces/pieces-common'
import { myServiceAuth } from '../auth'

export const createRecordAction = createAction({
  auth: myServiceAuth,
  name: 'create_record',
  displayName: '创建记录',
  description: '在数据库中创建一条新记录',
  props: {
    // 动态下拉框:选项从 API 动态加载,依赖 auth 变化
    tableId: Property.Dropdown({
      displayName: '表格',
      required: true,
      refreshers: ['auth'],  // ← auth 变化时重新加载
      options: async ({ auth }) => {
        if (!auth) return { options: [], disabled: true, placeholder: '请先填写 API Key' }

        const response = await httpClient.sendRequest({
          method: HttpMethod.GET,
          url: 'https://api.myservice.com/tables',
          authentication: {
            type: AuthenticationType.BEARER_TOKEN,
            token: auth as string
          }
        })

        return {
          options: response.body.tables.map(t => ({ label: t.name, value: t.id }))
        }
      }
    }),

    // 动态属性:根据选择的表格,动态生成不同的表单字段
    fields: Property.DynamicProperties({
      displayName: '字段',
      required: true,
      refreshers: ['auth', 'tableId'],  // ← 依赖这两个值
      props: async ({ auth, tableId }) => {
        const schema = await fetchTableSchema(auth, tableId)
        return schema.fields.reduce((acc, field) => {
          acc[field.id] = Property.ShortText({ displayName: field.name, required: false })
          return acc
        }, {})
      }
    })
  },

  async run(context) {
    const { tableId, fields } = context.propsValue
    const apiKey = context.auth  // ← 已认证的凭据

    // 使用 context.store 实现跨执行的状态持久化
    const runCount = (await context.store.get<number>('run_count')) ?? 0
    await context.store.put('run_count', runCount + 1)

    const response = await httpClient.sendRequest({
      method: HttpMethod.POST,
      url: `https://api.myservice.com/tables/${tableId}/records`,
      authentication: {
        type: AuthenticationType.BEARER_TOKEN,
        token: apiKey as string
      },
      body: { fields }
    })

    return response.body
  }
})

【进阶示例】Polling 触发器(定时轮询)

// src/lib/trigger/new-record.trigger.ts
// 参考 Airtable 的真实实现

import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework'
import { DedupeStrategy, Polling, pollingHelper } from '@activepieces/pieces-common'
import { myServiceAuth } from '../auth'

// 轮询逻辑定义
const polling: Polling<typeof myServiceAuth, typeof props> = {
  strategy: DedupeStrategy.TIMEBASED,  // 基于时间去重(不重复触发相同记录)

  items: async ({ auth, propsValue }) => {
    // 获取所有记录,返回标准化格式
    const records = await fetchAllRecords(auth, propsValue.tableId)

    return records.map(record => ({
      epochMilliSeconds: Date.parse(record.createdAt), // 用于时间排序和去重
      data: record  // 这就是 Action 会收到的 trigger data
    }))
  }
}

export const newRecordTrigger = createTrigger({
  auth: myServiceAuth,
  name: 'new_record',
  displayName: '新记录',
  description: '当有新记录被创建时触发',
  props: {
    tableId: Property.ShortText({ displayName: '表格 ID', required: true })
  },
  sampleData: { id: 'rec001', name: '示例记录', createdAt: '2024-01-01T00:00:00Z' },
  type: TriggerStrategy.POLLING,

  // Activepieces 调用这三个生命周期钩子,框架负责调度
  async onEnable(context)  { await pollingHelper.onEnable(polling, context) },
  async onDisable(context) { await pollingHelper.onDisable(polling, context) },
  async run(context)       { return pollingHelper.poll(polling, context) },
  async test(context)      { return pollingHelper.test(polling, context) },
})

【高级示例】Human-in-the-Loop(人工审批暂停流程)

这是 Activepieces 最独特的功能之一:流程可以"暂停"等待人工介入。

// 一个需要人工审批的 Action
import { createAction, Property } from '@activepieces/pieces-framework'

export const waitForApprovalAction = createAction({
  name: 'wait_for_approval',
  displayName: '等待人工审批',
  props: {
    approverEmail: Property.ShortText({
      displayName: '审批人邮箱',
      required: true
    }),
    message: Property.LongText({
      displayName: '审批请求内容',
      required: true
    })
  },
  async run(context) {
    const { approverEmail, message } = context.propsValue

    // 生成一个唯一的恢复 URL
    // 当审批人访问这个 URL 时,流程从这里继续
    const resumeUrl = context.generateResumeUrl({
      queryParams: { decision: 'approved' }
    })

    const rejectUrl = context.generateResumeUrl({
      queryParams: { decision: 'rejected' }
    })

    // 发送审批邮件(实际使用时配合邮件 Piece)
    await sendEmail({
      to: approverEmail,
      subject: '需要您的审批',
      body: `
        ${message}

        ✅ 批准: ${resumeUrl}
        ❌ 拒绝: ${rejectUrl}
      `
    })

    // 🔑 关键:暂停流程,等待 Webhook 回调
    context.run.pause({
      pauseMetadata: {
        type: 'WEBHOOK',  // 等待 Webhook 触发
        response: {}      // 可以在这里定义暂停时的响应
      }
    })

    // ↑ 以上代码执行到 pause() 就停止,等审批人点击链接后继续
  }
})
流程暂停 & 恢复示意图:

[流程执行] ──▶ [等待审批 Action]
                    │
                    ├── 发邮件给审批人
                    │
                    ├── 调用 context.run.pause()
                    │
                    ▼
              [流程暂停 💤]
              (状态存入数据库)
                    │
                    │  ... 一天后 ...
                    │
                    ▼
         [审批人点击"批准"链接]
                    │
         HTTP GET /resume?decision=approved
                    │
                    ▼
              [流程恢复 ▶️]
                    │
                    ▼
              [后续 Actions 继续执行]

【高级示例】带记忆的 AI 对话 Action

参考 OpenAI Piece 的真实实现:

// 利用 context.store 实现跨执行的对话记忆
export const askOpenAI = createAction({
  name: 'ask_chatgpt',
  displayName: 'Ask ChatGPT',
  props: {
    model: Property.Dropdown({ /* 动态从 OpenAI API 加载模型列表 */ }),
    prompt: Property.LongText({ displayName: '问题', required: true }),
    memoryKey: Property.ShortText({
      displayName: '记忆 Key',
      description: '设置后,会记住跨次执行的对话历史。留空则每次独立对话。',
      required: false
    })
  },

  async run({ auth, propsValue, store }) {  // ← store 是 context 里的持久化存储
    const { model, prompt, memoryKey } = propsValue
    const openai = new OpenAI({ apiKey: auth.secret_text })

    // 从 store 读取历史对话(如果有 memoryKey 的话)
    let history = memoryKey
      ? await store.get<Message[]>(memoryKey, StoreScope.PROJECT) ?? []
      : []

    history.push({ role: 'user', content: prompt })

    const completion = await openai.chat.completions.create({
      model,
      messages: history,
    })

    const reply = completion.choices[0].message
    history.push(reply)

    // 将更新后的对话历史存回 store(跨执行持久化)
    if (memoryKey) {
      await store.put(memoryKey, history, StoreScope.PROJECT)
    }

    return reply.content
  }
})

8. 多版本架构

三个版本的关系

┌─────────────────────────────────────────────────────────────────┐
│                      代码层级关系                                 │
│                                                                   │
│  packages/server/api/src/app/           ← CE(社区版)核心功能   │
│  packages/server/api/src/app/ee/        ← EE(企业版)扩展功能   │
│                                                                   │
│  CE 代码 ──────────────────────────────────── 不能 import EE     │
│  EE 代码 ──── 通过 hooksFactory 钩子机制 ──── 注入 CE 功能       │
└─────────────────────────────────────────────────────────────────┘

hooksFactory 扩展机制

// CE 中定义钩子接口
// packages/server/api/src/app/flows/flow-run/flow-run-hooks.ts

export const flowRunHooks = (() => {
  // 默认实现(社区版行为)
  let hooks: FlowRunHooks = {
    async onFinish(flowRun) {
      // CE 默认:什么都不做
    }
  }

  return {
    setHooks: (newHooks: FlowRunHooks) => { hooks = newHooks },
    onFinish: (flowRun: FlowRun) => hooks.onFinish(flowRun)
  }
})()

// EE 中注入企业版行为(但 CE 不 import EE)
// packages/server/api/src/app/ee/flows/ee-flow-run-hooks.ts

flowRunHooks.setHooks({
  async onFinish(flowRun) {
    await billingService.recordUsage(flowRun)  // 企业版:计费
    await auditLogService.log(flowRun)          // 企业版:审计日志
  }
})

功能门控

// 后端:用中间件控制企业功能访问
// 返回 402 Payment Required(而不是 403)
await platformMustHaveFeatureEnabled((p) => p.plan.customBrandingEnabled)

// 前端:用组件包裹受限功能
<LockedFeatureGuard featureKey="customBranding">
  <BrandingSettings />
</LockedFeatureGuard>

9. 编码规范

这个项目规范极为严格,记住这几条能避免 90% 的 PR 被打回。

✅ 必须遵守

// 1. 命名参数(超过1个参数必须用对象解构)
// ❌ 错误
function createFlow(name: string, projectId: string, ownerId: string) {}

// ✅ 正确
function createFlow({ name, projectId, ownerId }: CreateFlowParams) {}

// 2. 错误处理用 tryCatch(Go 风格)
// ❌ 错误
try {
  const result = await doSomething()
} catch (e) { ... }

// ✅ 正确
const { data, error } = await tryCatch(() => doSomething())
if (error) { /* 处理错误 */ }

// 3. 导出顺序:逻辑在前,类型/常量在后
// ✅ 正确
function doSomething() { ... }  // 函数/逻辑在前
export const MY_CONST = 'value' // 导出的常量在后
export type MyType = { ... }    // 导出的类型在后

// 4. 绝对禁止 any 类型
// ❌ 错误
const data: any = response.body

// ✅ 正确
const data: unknown = response.body
if (isMyExpectedType(data)) { /* 使用 data */ }

// 5. i18n 字符串:错误消息必须用 key
// ❌ 错误
z.string().min(1, 'This field is required')

// ✅ 正确
z.string().min(1, formErrors.required)  // key 在 translation.json 里

常见"坑"速查

新建 TypeORM Entity 后忘记注册
    → 必须加到 database-connection.tsgetEntities()
    → 症状:TypeORM 完全忽略这个表

修改了 packages/shared 但没 bump 版本
    → 其他包可能用旧缓存
    → 改 patch:非破坏性修改;改 minor:新增导出

CE 代码里 import 了 EE 路径
    → CI lint 会报错
    → 原则:EE 单向依赖 CE,CE 不知道 EE 的存在

HTTP 方法用了 PUT / PATCH
    → 这个项目的规范:创建 + 更新都用 POST
    → DELETE 用于删除

Trigger run() 返回单对象而不是数组
    → 规范:Trigger 的 run() 必须返回数组
    → 每个元素代表一次触发事件

10. 本地开发

最快启动方式(5 分钟上手)

# 前提:安装 Docker、Bun (https://bun.sh)

# 1. 克隆仓库
git clone https://github.com/activepieces/activepieces.git
cd activepieces

# 2. 一键启动(自动安装依赖 + 启动所有服务)
npm start

# ☕ 喝杯咖啡,等待编译
# 启动后访问 http://localhost:4200

开发模式(热重载)

# 启动前端 + 后端,支持热重载
npm run dev

# 只启动后端
cd packages/server/api && npm run dev

# 创建一个新 Piece
npm run create-piece    # 根据提示填写信息

# 为已有 Piece 添加 Action
npm run create-action

# 为已有 Piece 添加 Trigger  
npm run create-trigger

开发一个新 Piece 的完整流程

# Step 1: 创建 Piece 骨架
npm run create-piece
# 输

开发一个新 Piece 的完整流程(续)

# Step 1: 创建 Piece 骨架
npm run create-piece
# 根据提示填写:
#   Piece name: my-crm
#   Display name: My CRM

# Step 2: 注册到 tsconfig(自动生成提示,手动加一行)
# 在 tsconfig.base.json 的 paths 里加:
# "@activepieces/piece-my-crm": ["packages/pieces/community/my-crm/src/index.ts"]

# Step 3: 开发模式启动,带热重载
AP_DEV_PIECES=my-crm npm run dev
# 只加载指定 piece,极大加速启动

# Step 4: 添加 Action
npm run create-action
# 提示选择所属 piece,输入 action 名称

# Step 5: 提交前必跑 lint
npm run lint-dev

开发环境架构示意

本地开发环境(docker-compose.dev.yml)

┌──────────────────────────────────────────────────────┐
  你的开发机                                            
                                                       
  ┌──────────────┐   ┌───────────────────────────┐    
    前端              后端 API + Worker             
    localhost:       localhost:3000                
    4200 (React)│     (Fastify)                     
  └──────────────┘   └────────────┬──────────────┘    
                                                      
└──────────────────────────────────┼────────────────────┘
                                   
              ┌────────────────────┼────────────────────┐
                Docker 容器                            
                ┌─────────────────▼──────────────┐    
                  PostgreSQL    Redis                
                  :5432         :6379                
                └─────────────────────────────────┘    
              └────────────────────────────────────────┘

11. 深度案例:OAuth2 认证的完整实现

Google Sheets 采用双认证模式(OAuth2 + 服务账号),是理解认证体系最好的教材:

// packages/pieces/community/google-sheets/src/lib/common/common.ts
// Google Sheets 支持两种认证方式,通过数组形式传入

export const googleSheetsAuth = [
  
  // 方式一:标准 OAuth2(推荐)
  PieceAuth.OAuth2({
    authUrl: 'https://accounts.google.com/o/oauth2/auth',
    tokenUrl: 'https://oauth2.googleapis.com/token',
    required: true,
    scope: [
      'https://www.googleapis.com/auth/spreadsheets',
      'https://www.googleapis.com/auth/drive',
    ],
  }),
  
  // 方式二:Service Account(高级,适合服务器间调用)
  PieceAuth.CustomAuth({
    displayName: 'Service Account(高级)',
    description: '通过服务账号 JSON 密钥认证,支持域委派...',
    required: true,
    props: {
      serviceAccount: Property.ShortText({
        displayName: 'Service Account JSON Key',
        required: true,
      }),
      userEmail: Property.ShortText({
        displayName: '用户邮箱(可选,用于域委派)',
        required: false,
      })
    },
    validate: async ({ auth }) => {
      try {
        await getAccessToken({ type: AppConnectionType.CUSTOM_AUTH, props: auth })
        return { valid: true }
      } catch (e) {
        return { valid: false, error: (e as Error).message }
      }
    }
  })
]

// 获取 Access Token,自动处理两种认证类型
export const getAccessToken = async (auth: GoogleSheetsAuthValue): Promise<string> => {
  if (auth.type === AppConnectionType.CUSTOM_AUTH) {
    // 服务账号:用 googleapis 库交换 token
    const googleClient = await createGoogleClient(auth)
    const response = await googleClient.getAccessToken()
    if (response.token) return response.token
    throw new Error('无法从服务账号获取 Access Token')
  }
  // OAuth2:直接使用 access_token
  return auth.access_token
}
┌──────────────────────────────────────────────────────────────┐
│                  认证类型总览                                   │
├────────────────┬─────────────────────────────────────────────┤
│ SecretText     │ API Key 类型,用户粘贴密钥,有 validate 回调   │
│                │ 示例:OpenAI、Airtable                        │
├────────────────┼─────────────────────────────────────────────┤
│ OAuth2         │ 标准 OAuth2 授权码流,框架自动处理 token 刷新  │
│                │ 示例:Google、GitHub、Slack                    │
├────────────────┼─────────────────────────────────────────────┤
│ CustomAuth     │ 自定义表单,灵活组合多个字段                   │
│                │ 示例:用户名+密码、多个 key、服务账号 JSON     │
├────────────────┼─────────────────────────────────────────────┤
│ 数组形式        │ 支持多种认证方式并存,用户自选                 │
│                │ 示例:Google Sheets(OAuth2 + 服务账号)       │
└────────────────┴─────────────────────────────────────────────┘

12. 深度案例:Webhook 触发器(Slack 的实现)

Slack 使用 APP_WEBHOOK 模式,不是轮询,而是 Slack 主动推事件过来。这需要实现 events 处理器:

// packages/pieces/community/slack/src/index.ts(关键部分)

export const slack = createPiece({
  // ...
  events: {
    
    // parseAndReply:解析 Slack 发来的 Webhook,返回事件类型 + 标识符
    // 同时处理 Slack 的 URL 验证挑战(challenge)
    parseAndReply: ({ payload }) => {
      const body = payload.body as EventPayloadBody
      
      // Slack 验证 Webhook URL 时发 challenge,需要原样返回
      if (body.challenge) {
        return { reply: { body: body.challenge, headers: {} } }
      }
      
      // 普通事件:返回事件类型和团队 ID(用于路由到对应的 Flow)
      return {
        event: body.event?.type,         // 如 "message"、"reaction_added"
        identifierValue: body.team_id,   // 关联到哪个 Slack 工作区
      }
    },
    
    // verify:验证 Webhook 签名,防伪造
    verify: ({ webhookSecret, payload }) => {
      const timestamp = payload.headers['x-slack-request-timestamp']
      const signature = payload.headers['x-slack-signature']
      
      // HMAC-SHA256 签名验证
      const signatureBase = `v0:${timestamp}:${payload.rawBody}`
      const hmac = crypto.createHmac('sha256', webhookSecret as string)
      hmac.update(signatureBase)
      const computed = `v0=${hmac.digest('hex')}`
      
      return signature === computed  // 签名匹配才处理
    }
  },
})
Webhook 触发器的完整流程:

                    Slack 服务器
                        │
                        │  POST /webhook/xxxxx
                        │  Headers: x-slack-signature: xxx
                        │  Body: { event: {...}, team_id: "T..." }
                        ▼
               Activepieces Webhook 入口
                        │
                        ├─ 调用 piece.events.verify()
                        │   └─ 验证签名 ✅ / ❌
                        │
                        ├─ 调用 piece.events.parseAndReply()
                        │   └─ 解析出 event 类型 + team_id
                        │
                        ├─ 根据 team_id 找到对应的 Flow
                        │
                        └─ 触发 Flow 执行

13. MCP Server 集成:Pieces 如何变成 AI 工具

这是 Activepieces 最前沿的特性。每个 Piece 的 Actions 可以直接暴露为 MCP (Model Context Protocol) 工具,供 Claude、Cursor 等 AI 工具调用。

MCP 服务端架构

                  AI 工具(Claude Desktop / Cursor)
                            │
                   MCP Protocol (SSE / Streamable HTTP)
                            │
              ┌─────────────▼───────────────────────────┐
              │   Activepieces MCP Server               │
              │   /v1/projects/:projectId/mcp-server    │
              │                                         │
              │   mcpServerController                   │
              │     ├── GET /       获取已启用的工具     │
              │     ├── POST /      更新启用工具列表     │
              │     └── POST /rotate  轮换访问 token    │
              └─────────────────────────────────────────┘
                            │
                     调用对应 PieceAction
                            │
              ┌─────────────▼───────────────────────────┐
              │   Piece Action(如 Airtable 创建记录)   │
              │   = 可以被 AI 直接调用的工具              │
              └─────────────────────────────────────────┘

配置示例(Claude Desktop)

// ~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "activepieces": {
      "transport": "sse",
      "url": "https://your-instance.com/v1/projects/YOUR_PROJECT_ID/mcp-server/sse",
      "headers": {
        "Authorization": "Bearer YOUR_MCP_TOKEN"
      }
    }
  }
}

配置完成后,Claude Desktop 就能直接调用你在 Activepieces 项目里启用的所有 Piece Actions:

用户对 Claude 说:"帮我在 Airtable 的 Sales 表里创建一条记录..."

Claude 识别到需要调用 airtable_create_record 工具
          │
          ▼
通过 MCP 协议发送工具调用请求到 Activepieces
          │
          ▼
Activepieces 执行 Action(使用已保存的认证凭据)
          │
          ▼
返回执行结果给 Claude
          │
          ▼
Claude 告知用户:"已成功创建记录..."

14. 架构级别的关键设计决策

为什么用 Socket.IO 而不是直接 BullMQ 消费?

方案 A(传统):Worker 直接消费 BullMQ
  Worker ──── BullMQ ────▶ 执行 Job
  问题:Worker 机器配置、并发数、能力无法动态感知

方案 B(Activepieces 实际):Socket.IO 长轮询
  Worker ──── Socket.IO ──── API Server ──── BullMQ
  
  优势:
  ✅ Worker 可以上报自己的 CPU/内存/磁盘状态
  ✅ API Server 根据机器状态智能分配任务
  ✅ 支持 Worker Groups(不同机器跑不同类型任务)
  ✅ Worker 可以动态扩缩容,API Server 感知实时在线数

为什么 Side Effects 要单独分离?

// ❌ 反例:副作用混在业务逻辑里
async function updateFlow(params) {
  const flow = await db.save(params)
  await sendNotification(flow)   // 耦合!
  await updateStats(flow)        // 耦合!
  await gitSync(flow)            // 耦合!
  return flow
}

// ✅ Activepieces 的方式:显式分离
async function updateFlow(params) {
  const flow = await db.save(params)  // 纯业务逻辑
  return flow
}

// 调用方显式触发副作用
const flow = await flowService.update(params)
await flowSideEffects.onUpdate(flow)   // 副作用独立调用

好处:测试时可以 mock 掉 side effects,业务逻辑单元测试更纯粹。

为什么所有 Props 都要用 Property 工厂而不是直接定义?

// 每种 Property 类型背后都是:
Property.ShortText(options)
  ├── 生成前端表单控件(单行文本输入框)
  ├── 携带类型信息(string)
  ├── 携带验证规则
  └── 支持变量插值(用 {{ }} 引用上一步输出)

Property.Dropdown(options)
  ├── 生成下拉选择框
  ├── options 可以是静态数组
  └── 也可以是异步函数(动态从 API 加载)

// 这套设计让 Piece 开发者只需关注业务逻辑
// UI、验证、类型推断都由框架自动处理

15. 测试体系

测试分层:

┌──────────────────────────────────────────────────────┐
│  E2E 测试(packages/tests-e2e)                       │
│  Playwright 驱动真实浏览器测试完整用户流程              │
└────────────────────────┬─────────────────────────────┘
                         │
┌────────────────────────▼─────────────────────────────┐
│  API 集成测试(packages/server/api/test)              │
│  npm run test-api / test-ee / test-cloud              │
│  真实 PostgreSQL + Redis,测试 HTTP 接口               │
│                                                       │
│  写法:                                               │
│  const app = await setupTestEnvironment()             │
│  const ctx = await createTestContext(app)             │
│  const res = await ctx.post('/v1/flows', body)        │
│  // DB 在每个测试之间自动清理                           │
└────────────────────────┬─────────────────────────────┘
                         │
┌────────────────────────▼─────────────────────────────┐
│  单元测试(Vitest)                                    │
│  npm run test-unit                                    │
│  测试 engine 执行逻辑 + shared 工具函数                │
└──────────────────────────────────────────────────────┘

API 测试示例

// 真实的测试写法(packages/server/api/test)

describe('Flow API', () => {
  let app: FastifyInstance
  let ctx: TestContext
  
  beforeAll(async () => {
    app = await setupTestEnvironment()  // 启动真实服务
  })
  
  beforeEach(async () => {
    ctx = await createTestContext(app)  // 每个测试独立的项目/用户上下文
  })
  
  it('应该成功创建一个流程', async () => {
    const response = await ctx.post('/v1/flows', {
      displayName: '测试流程',
      projectId: ctx.projectId
    })
    
    expect(response.statusCode).toBe(201)
    expect(response.json().displayName).toBe('测试流程')
  })
  
  it('应该拒绝跨项目访问', async () => {
    const otherCtx = await createTestContext(app)  // 另一个项目
    
    // 用 otherCtx 访问 ctx 的资源,应该 403
    const response = await otherCtx.get(`/v1/flows/${ctx.flowId}`)
    expect(response.statusCode).toBe(403)
  })
})

16. 数据库迁移规范

这是新手最容易出错的地方,规范极为严格:

数据库迁移的铁规则:

✅ 必须用 TypeORM CLI 生成迁移文件(不要手写)
   npm run db-migration -- --name=AddColumnXToFlows

✅ 迁移文件命名格式
   1700000000000-AddColumnXToFlows.ts

✅ 新增 Entity 必须同时注册
   在 database-connection.tsgetEntities() 里加上

❌ 永远不要直接修改现有 Entity 而不写迁移
❌ 永远不要在生产环境运行 synchronize: true
❌ 永远不要在迁移里使用 PostgreSQL 扩展(如 uuid-ossp)
   → 用 apId() 函数替代 uuid_generate_v4()

17. 完整的"实战场景"串联示例

以"新客户注册 → AI 分析 → Slack 通知 → 写入 CRM"为例,展示一个完整 Flow:

Flow 设计图:

[触发器] 新 Webhook 请求(客户注册表单)
    │
    │  data: { name, email, company, message }
    │
    ▼
[Action 1] OpenAI - Ask ChatGPT
    │  prompt: "分析以下客户信息,判断商机等级(高/中/低): {{trigger.data}}"
    │  memoryKey: "" (不需要记忆)
    │
    │  output: "商机等级:高。该客户来自500强企业..."
    │
    ▼
[Action 2] Airtable - Create Record
    │  base: Sales DB
    │  table: Leads
    │  fields:
    │    Name: {{ trigger.data.name }}
    │    Email: {{ trigger.data.email }}
    │    AIAnalysis: {{ action_1.output }}
    │
    │  output: { id: "recXXX", ... }
    │
    ▼
[Action 3] Slack - Send Message
    │  channel: #sales-alerts
    │  message: |
    │    🔥 新高价值客户!
    │    姓名: {{ trigger.data.name }}
    │    公司: {{ trigger.data.company }}
    │    AI 分析: {{ action_1.output }}
    │    CRM 记录: {{ action_2.output.id }}
    │
    ▼
[完成]

总结:学习路线图

┌─────────────────────────────────────────────────────────────────┐
│                    Activepieces 学习路线图                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  第一步(1-2天):用起来                                           │
│  ├── Docker Compose 本地部署                                      │
│  ├── 界面上点出一个简单 Flow(Webhook → Slack 通知)               │
│  └── 理解 Flow / Piece / Trigger / Action 四个核心概念            │
│                                                                   │
│  第二步(3-5天):写一个 Piece                                     │
│  ├── 跑 npm run create-piece                                      │
│  ├── 实现一个 SecretText Auth + HTTP Action                       │
│  ├── 实现一个 Polling Trigger                                     │
│  └── 读 airtable piece 源码(最佳参考实现)                       │
│                                                                   │
│  第三步(1周):理解后端                                           │
│  ├── 读 AGENTS.md(全局架构规范)                                  │
│  ├── 阅读 flows/flow/ 目录(Controller + Service 模式)           │
│  ├── 理解多租户 projectId 过滤原则                                │
│  └── 理解 CE/EE/Cloud 三版本边界                                  │
│                                                                   │
│  第四步(1-2周):深度贡献                                         │
│  ├── 阅读 .agents/features/ 下对应功能模块文档                    │
│  ├── 参与 Issues 讨论,认领 good-first-issue                      │
│  ├── 写 API 集成测试                                              │
│  └── 实现一个新功能(含迁移 + 测试 + 文档)                       │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

快速参考手册

我想做的事看哪里
了解整体架构约束AGENTS.md(根目录)
开发一个新 Piecepackages/pieces/CLAUDE.md
了解某个功能模块.agents/features/<name>.md
后端开发规范packages/server/AGENTS.md
添加新的 API 接口.agents/skills/add-endpoint.md
数据库迁移.agents/skills/ + TypeORM Migrations 文档
参与贡献CONTRIBUTING.md + Discord

最后一句话:Activepieces 是一个"思路密度"极高的项目。Monorepo 的组织方式、插件框架的类型安全设计、CE/EE 的代码隔离机制、Worker 的长轮询架构……每一个设计决策背后都有清晰的工程考量。对想深入学习现代 TypeScript 全栈工程实践的开发者来说,这是一个非常值得花时间精读的项目。

GitHub: github.com/activepiece… ⭐ 21.8k