一句话定位: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 可直接调用 │
└──────────────────┴─────────────────────────────────────────┘
与竞品对比
| 维度 | Zapier | n8n | Activepieces |
|---|---|---|---|
| 开源 | ❌ | ✅(部分) | ✅ MIT |
| 自托管 | ❌ | ✅ | ✅ |
| MCP 支持 | ❌ | ❌ | ✅ 280+ |
| 插件开发语言 | 无法扩展 | JS/TS | TypeScript(类型安全) |
| AI Agent 原生集成 | ❌ | 有限 | ✅ |
| 企业版 | 闭源商业 | 闭源商业 | 独立授权但共存 |
2. 核心概念
理解 Activepieces,先搞懂四个词:
┌─────────────────────────────────────────────────────────────────┐
│ 一条 Flow(流程) │
│ │
│ ┌─────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Trigger │──────▶│ Action 1 │─────▶│ Action 2 │─── ... │
│ │(触发器) │ │ (动作) │ │ (动作) │ │
│ └─────────┘ └──────────┘ └──────────┘ │
│ │
│ Trigger 和 Action 都来自 Piece(集成插件) │
│ 每个 Piece = 一个 npm 包 = 一组 Triggers + Actions │
└─────────────────────────────────────────────────────────────────┘
Flow(流程) :一个完整的自动化任务,由一个 Trigger 和若干 Actions 组成。
Piece(插件) :对某个第三方服务的集成封装,比如 airtable、openai、slack。每个 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.ts 的 getEntities()
→ 症状: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 │
└─────────────────────────────────────────┘
│
调用对应 Piece 的 Action
│
┌─────────────▼───────────────────────────┐
│ 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.ts 的 getEntities() 里加上
❌ 永远不要直接修改现有 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(根目录) |
| 开发一个新 Piece | packages/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