Flowise 的 Monorepo 架构:pnpm + Turbo + 模块化节点池
学习目标
- 理解 Flowise 的 Monorepo 结构:6 个子包的职责与依赖关系
- 掌握 pnpm workspace + Turbo 的任务编排机制
- 了解 NodesPool 如何在启动时动态发现和加载节点
前置知识
本章为项目特定的工程实践,无外部前置知识要求。
项目实践
Monorepo 结构
Flowise 使用 pnpm workspaces 管理 6 个子包:
packages/├── server/ # Express.js 后端,API 逻辑、数据库、队列├── ui/ # React 前端,管理界面├── components/ # 节点库:LLM、工具、记忆、向量存储等├── agentflow/ # React Flow 画布编辑器├── observe/ # 可观测性模块└── api-documentation/ # Swagger API 文档依赖关系:
server → components (flowise-components)server → ui (flowise-ui)ui → agentflow (React Flow 组件)pnpm Workspace 配置
pnpm-workspace.yaml 定义:
packages: - 'packages/*'所有子包通过 workspace:* 引用彼此:
{ "dependencies": { "flowise-components": "workspace:^", "flowise-ui": "workspace:^" }}Turbo 任务编排
turbo.json 定义构建和开发任务:
| 任务 | 依赖 | 说明 |
|---|---|---|
build | ^build | 先构建依赖,再构建自己 |
dev | 无 | 开发模式,热重载 |
start | ^build | 先构建再启动 |
NodesPool 动态加载
NodesPool 类是 Flowise 节点系统的核心:
class NodesPool { componentNodes: IComponentNodes = {} // 所有已加载的节点 componentCredentials: IComponentCredentials = {} // 所有认证信息}节点加载流程:
关键代码:
private async initializeNodes() { const packagePath = getNodeModulesPackagePath('flowise-components') const nodesPath = path.join(packagePath, 'dist', 'nodes') const nodes = await this.loadNodesFromDir(nodesPath) Object.assign(this.componentNodes, nodes)}
async loadNodesFromDir(dir: string): Promise<IComponentNodes> { const disabled_nodes = process.env.DISABLED_NODES?.split(',') ?? [] const nodeFiles = await this.getFiles(dir)
await Promise.all(nodeFiles.map(async (file) => { if (file.endsWith('.js')) { const nodeModule = await require(file) if (nodeModule.nodeClass) { const instance = new nodeModule.nodeClass() instance.filePath = file // 图标路径处理... if (!disabled_nodes.includes(instance.name)) { nodes[instance.name] = instance } } } }))}Feature Flag:节点禁用机制
Flowise 支持通过环境变量禁用特定节点:
DISABLED_NODES=nodeA,nodeB,nodeC同时支持 appConfig.showCommunityNodes 控制是否显示社区贡献的节点(通过节点是否有 author 字段判断)。
问题与规避
启动时间 vs 运行时性能
问题:NodesPool 在启动时全量加载所有节点(数百个 .js 文件),启动时间较长。
对策:
- 使用
DISABLED_NODES环境变量禁用不需要的节点,减少加载数量 - 节点加载是 I/O 密集型的
require()操作,可通过 SSD 提升速度 - 生产环境建议预热启动一次后保持进程常驻
动态 require 的安全风险
问题:从文件系统直接 require() 用户可编辑的 .js 文件,存在代码注入风险。
对策:
- 节点文件来自
flowise-components包(npm 安装),非用户上传 - 社区节点通过
author字段标识,管理员可选择是否加载 - 用户自定义代码通过
vm2 NodeVM沙箱执行(见”沙箱安全”章节)
设计取舍
为什么选 pnpm 而非 npm/yarn?
| 方案 | 优势 | 代价 |
|---|---|---|
| pnpm | 节省磁盘空间(硬链接)、严格依赖隔离 | 与某些工具链兼容性差 |
| npm | 生态最广、开箱即用 | 重复安装依赖、phantom dependencies |
| yarn | 介于两者之间 | 社区分流 |
Flowise 选择 pnpm 的核心原因:
- Monorepo 中多个子包共享相同依赖(如
@langchain/core),pnpm 的 content-addressable store 节省大量磁盘空间 - 严格依赖隔离避免了 phantom dependencies(npm 可以 require 未声明的依赖)
文件扫描 vs 注册表
| 方案 | 优势 | 代价 |
|---|---|---|
| 文件扫描 + require | 零配置,新节点只需放在目录下 | 启动时全量扫描 |
| 注册表 | 精确控制加载哪些节点 | 需要手动维护注册表 |
Flowise 选择文件扫描,因为:
- 降低新节点开发门槛:只需导出
nodeClass即可自动注册 - 社区贡献友好:不需要修改核心代码
- 代价是启动时间,但可以通过禁用不需要的节点缓解