chore: import upstream snapshot with attribution
CI / build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:09:51 +08:00
commit 0232b4e2bb
3528 changed files with 291404 additions and 0 deletions
+204
View File
@@ -0,0 +1,204 @@
---
description: 为代码添加单元测试(支持增量和存量代码测试补齐)
---
# 单元测试生成
## 用法
- `/add-tests [dir]` - 为指定模块或文件添加单元测试
- `[dir]` 可选参数:包名(以 @ 开头)、文件路径或目录路径
- 不指定 `[dir]` 则默认为全代码库(会提示用户确认)
- 执行后会询问用户选择:增量代码测试(基于 git diff)或存量代码测试补齐
## 命令说明
此命令用于自动生成和补充单元测试,确保代码质量。FlowGram 使用 **Vitest** 作为测试框架。
### 测试覆盖率目标
根据包的类型和重要性,测试覆盖率要求如下:
- **核心引擎层**canvas-engine、node-engine、variable-engine、runtime
- 覆盖率目标:≥ 85%
- 包括:@flowgram.ai/core、@flowgram.ai/form、@flowgram.ai/variable-core、@flowgram.ai/runtime-js 等
- **插件和客户端层**plugins、client
- 覆盖率目标:≥ 60%
- 包括:@flowgram.ai/editor、各类 plugin 包、@flowgram.ai/fixed-layout-editor 等
- **工具和示例**common、apps
- 覆盖率目标:尽可能覆盖关键逻辑
- 包括:@flowgram.ai/utils、demo 应用等
### 测试文件组织
- 测试文件位置:
- `__tests__/` 目录(推荐)
- 或与源文件同级的 `*.test.ts`/`*.test.tsx` 文件
- 命名规范:
- 对于 `src/core/utils.ts`,测试文件为 `__tests__/core/utils.test.ts``src/core/utils.test.ts`
## 测试生成流程
### 0. 命令执行和用户确认
1. **确认范围**
- 如果未指定 `[dir]`,询问用户是否要对全代码库操作,还是指定具体目录
- 全代码库操作工作量巨大,需要用户明确确认
2. **选择模式**
- 询问用户选择测试模式:
- **增量代码测试**:仅为 git diff 中的新增/修改代码添加测试
- **存量代码测试补齐**:扫描所有代码,补齐缺失或覆盖率不足的测试
### 1. 识别待测代码
**增量代码模式**
```bash
# 检查 git diff 获取所有修改的文件
git diff --name-only
git diff <file> # 查看具体变更
```
**存量代码模式**
- 扫描指定目录下所有源文件(排除已有完整测试的文件)
- 查找缺少测试或覆盖率不足的文件
- 优先处理核心引擎层的文件
### 2. 确定包信息和覆盖率目标
1. 从最近的 `package.json` 获取包名
2. 使用包名在 `rush.json` 中查找包的分类(projectFolder
3. 根据包所在目录确定覆盖率目标:
- `packages/canvas-engine/``packages/node-engine/``packages/variable-engine/``packages/runtime/` → 85%
- `packages/plugins/``packages/client/` → 60%
- `packages/common/``apps/` → 尽可能覆盖
### 3. 生成测试代码
**测试重点**
- 新增或修改的函数、方法、类
- 分支逻辑(if/else、switch/case
- 边界条件和异常处理
- 依赖注入容器(inversify)的模拟
- 响应式状态(ReactiveState)的行为验证
**Vitest 最佳实践**
```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen } from '@testing-library/react';
describe('ModuleName', () => {
beforeEach(() => {
// 初始化
});
it('should handle specific case', () => {
// 测试逻辑
expect(result).toBe(expected);
});
});
```
**React 组件测试**
- 使用 `@testing-library/react` 进行组件测试
- 为关键元素添加 `data-testid` 属性
- 测试用户交互和状态变化
**依赖注入测试**
- 使用 `vi.mock()` 模拟依赖
- 创建测试容器来验证服务注册
### 4. 执行测试验证
**单包测试**
```bash
cd packages/canvas-engine/core
rushx test # 运行测试
rushx test:cov # 生成覆盖率报告
```
**全局测试**
```bash
rush test # 运行所有包的测试
rush test:cov # 生成所有包的覆盖率报告
```
**每次添加测试后**
1. 立即运行测试确保通过
2. 检查覆盖率是否达到目标
3. 修复失败的测试或调整测试用例
4. 继续处理下一个文件
### 5. 输出测试文件
- 将测试文件保存到 `__tests__/` 目录(优先)或源文件同级
- 保持目录结构与源代码一致
- 添加必要的导入和类型声明
## 示例命令
```bash
# 为某个包添加测试(执行后会询问增量或存量)
/add_tests @flowgram.ai/core
# 为特定目录添加测试
/add_tests packages/node-engine/form
# 为单个文件添加测试
/add_tests packages/canvas-engine/core/src/core/utils.ts
# 全代码库测试(会先确认范围,再询问增量或存量)
/add_tests
```
### 典型使用场景
**场景 1:为新功能添加测试**
```bash
/add_tests packages/plugins/my-new-plugin
# 选择:增量代码测试
# 结果:仅为 git diff 中的新代码生成测试
```
**场景 2:提升现有包的测试覆盖率**
```bash
/add_tests @flowgram.ai/variable-core
# 选择:存量代码测试补齐
# 结果:扫描所有代码,补齐缺失的测试,目标 85% 覆盖率
```
**场景 3:全面测试检查**
```bash
/add_tests
# 确认:选择要处理的目录或全代码库
# 选择:存量代码测试补齐
# 结果:系统性地补齐整个项目的测试
```
## 注意事项
1. **优先级**:优先为核心引擎层包编写高质量测试
2. **隔离性**:每个测试应该独立,不依赖其他测试的执行顺序
3. **可读性**:测试用例命名应清晰描述测试场景(使用中文或英文皆可)
4. **Mock 策略**
- 外部依赖(网络请求、文件系统)必须 mock
- 内部复杂模块可以考虑 mock
- 简单工具函数可以直接使用
5. **快照测试**:谨慎使用快照测试,仅用于稳定的 UI 或数据结构
6. **异步测试**:使用 async/await 处理异步操作,确保 Promise 正确解决
## 工作流程总结
1. **接收命令**:用户执行 `/add_tests [dir]`
2. **确认范围**:如果未指定 dir,询问用户要处理全代码库还是指定目录
3. **选择模式**:询问用户选择增量代码测试或存量代码测试补齐
4. **分析代码**:根据选择的模式识别待测代码
5. **确定目标**:根据包的分类确定覆盖率目标
6. **生成测试**:逐文件生成测试用例
7. **运行验证**:每生成一批测试后立即运行验证
8. **修复问题**:修复失败的测试或调整测试用例
9. **检查覆盖率**:查看覆盖率报告是否达标
10. **继续迭代**:直到达到目标覆盖率或所有文件都有测试
开始生成测试吧!
+423
View File
@@ -0,0 +1,423 @@
---
skill_name: create-node
description: 用于在 FlowGram demo-free-layout 中创建新的自定义节点,支持简单节点(自动表单)和复杂节点(自定义 UI)
version: 1.0.0
tags: [flowgram, node, workflow, custom-node]
---
# FlowGram Custom Node Development
## 概述
本 SKILL 用于指导在 FlowGram 项目的 `apps/demo-free-layout/src/nodes` 目录下创建新的自定义工作流节点。
## 核心概念
### 节点数据结构
节点数据在保存时会存储到后端,基本结构如下:
```typescript
{
id: 'node_xxxxx', // 节点 ID
type: 'node_type', // 节点类型
data: {
title: 'Node Title', // 节点标题
inputsValues: { ... }, // 节点表单字段的初始值(实际的值)
inputs: { ... }, // 节点表单的 JSON Schema(定义表单结构)
outputs: { ... }, // 节点输出的 JSON Schema(工作流执行时的输出)
// ... 其他自定义字段
}
}
```
### 三个核心字段
#### 1. `data.inputsValues` - 节点表单字段的初始值
存储表单中各个字段的实际值,每个字段值包含 `type``content` 两个属性:
```typescript
inputsValues: {
url: {
type: 'constant', // 常量类型
content: 'https://...', // 实际的值
},
prompt: {
type: 'template', // 模板类型(支持变量引用)
content: 'Hello {var}', // 可以引用变量
},
}
```
**`type` 的可选值**
- `'constant'`:常量值,不支持变量引用
- `'template'`:模板值,支持 `{variableName}` 语法引用变量
- `'variable'`:变量引用
#### 2. `data.inputs` - 节点表单的 JSON Schema
使用 JSON Schema 定义表单的结构,系统会根据这个 Schema 自动生成表单界面:
```typescript
inputs: {
type: 'object',
required: ['url'], // 必填字段
properties: {
url: {
type: 'string',
},
timeout: {
type: 'number',
minimum: 0,
maximum: 60000,
},
prompt: {
type: 'string',
extra: {
formComponent: 'prompt-editor', // 指定自定义组件
},
},
},
}
```
#### 3. `data.outputs` - 节点输出的 JSON Schema
定义节点在工作流执行时的输出数据结构,供下游节点使用:
```typescript
outputs: {
type: 'object',
properties: {
body: { type: 'string' },
statusCode: { type: 'number' },
headers: { type: 'object' },
},
}
```
### 三者的关系
```
inputs (JSON Schema) → 定义表单结构
inputsValues (实际值) → 存储表单数据
[节点执行]
outputs (JSON Schema) → 定义输出结构
```
### 字段类型与自动组件映射
在简单节点中,字段类型会自动匹配对应的表单组件:
| 字段类型 | `extra.formComponent` | 默认组件 |
|---------|---------------------|---------|
| `string` | - | Input |
| `string` | `'prompt-editor'` | PromptEditorWithVariables |
| `number` | - | InputNumber |
| `boolean` | - | Switch |
| `object` | - | JsonCodeEditor |
| `array` | - | JsonCodeEditor |
## 节点开发模式
### 1. 简单节点(自动表单模式)
- **适用场景**:节点配置较为简单,不需要复杂的自定义 UI
- **特点**:根据 `inputs` Schema 自动生成表单
- **示例**LLM 节点
- **文件结构**:只需要 `index.ts` 文件
- **模板位置**`./templates/simple-node/index.ts`
### 2. 复杂节点(自定义 UI 模式)
- **适用场景**:需要自定义表单布局、特殊交互或复杂的 UI 组件
- **特点**:完全控制表单渲染和交互逻辑
- **示例**HTTP 节点
- **文件结构**
```
{节点名}/
├── index.tsx # 节点注册配置
├── form-meta.tsx # 自定义表单渲染
├── types.tsx # TypeScript 类型定义
└── components/ # 自定义组件
└── *.tsx
```
- **模板位置**`./templates/complex-node/`
## 开发流程
### Step 1: 规划节点
确定节点的核心信息:
- **节点类型 ID**:唯一标识,如 `database`、`webhook`
- **节点功能**:明确节点要做什么
- **输入参数**:节点需要哪些配置项
- **输出数据**:节点执行后返回什么数据
- **UI 复杂度**:是否需要自定义 UI
### Step 2: 选择开发模式
```
是否需要自定义 UI
├─ 否 → 使用简单节点模式(复制 templates/simple-node/
└─ 是 → 使用复杂节点模式(复制 templates/complex-node/
```
### Step 3: 复制模板并修改
#### 简单节点
```bash
# 复制模板
cp .claude/skills/create-node/templates/simple-node/index.ts \
apps/demo-free-layout/src/nodes/{节点名}/index.ts
# 修改模板中的 TODO 标记
# - {NODE_NAME} → 节点名(PascalCase
# - {NODE_TYPE} → 节点类型枚举值
# - {node_name} → 节点名(kebab-case
# - {node_type} → 节点类型(小写)
```
#### 复杂节点
```bash
# 复制模板目录
cp -r .claude/skills/create-node/templates/complex-node \
apps/demo-free-layout/src/nodes/{节点名}
# 修改所有文件中的 TODO 标记
```
### Step 4: 添加节点类型常量
编辑 `apps/demo-free-layout/src/nodes/constants.ts`
```typescript
export enum WorkflowNodeType {
// ... 现有节点
{节点类型} = '{节点类型}',
}
```
### Step 5: 注册节点
编辑 `apps/demo-free-layout/src/nodes/index.ts`
```typescript
// 导入节点
export { {节点名}NodeRegistry } from './{节点名}';
// 添加到注册列表
export const nodeRegistries: FlowNodeRegistry[] = [
// ... 现有节点
{节点名}NodeRegistry,
];
```
### Step 6: 准备节点图标
在 `apps/demo-free-layout/src/assets/` 目录下添加节点图标(SVG 或 JPG 格式):
```
apps/demo-free-layout/src/assets/icon-{节点名}.svg
```
### Step 7: 测试验证
```bash
# 启动开发服务器
rush dev:demo-free-layout
# 在浏览器中测试节点功能
```
## 常用组件和工具
### FlowGram 组件
从 `@flowgram.ai/form-materials` 导入:
```typescript
import {
PromptEditorWithVariables, // 带变量的提示词编辑器
VariableSelector, // 变量选择器
JsonCodeEditor, // JSON 代码编辑器
CodeEditor, // 代码编辑器
DisplayOutputs, // 输出字段展示
DynamicValueInput, // 动态值输入
createInferInputsPlugin, // 输入推断插件
} from '@flowgram.ai/form-materials';
```
### Semi UI 组件
从 `@douyinfe/semi-ui` 导入:
```typescript
import {
Input,
InputNumber,
Select,
Switch,
Button,
Divider,
} from '@douyinfe/semi-ui';
```
### 表单工具
```typescript
import { Field } from '@flowgram.ai/free-layout-editor';
import { FormItem, FormHeader, FormContent } from '../../form-components';
import { useNodeRenderContext } from '../../hooks';
```
## 最佳实践
### 1. 节点设计
- **单一职责**:一个节点只做一件事
- **清晰的 Schema**:明确定义 inputs 和 outputs
- **合理的默认值**:提供有意义的初始配置
- **友好的描述**:为节点和字段提供清晰的描述
### 2. Schema 设计
```typescript
// ✅ 好的做法:清晰的 Schema
inputs: {
type: 'object',
required: ['url', 'method'],
properties: {
url: {
type: 'string',
description: 'API endpoint URL',
},
method: {
type: 'string',
enum: ['GET', 'POST', 'PUT', 'DELETE'],
},
},
}
// ❌ 不好的做法:缺少约束
inputs: {
type: 'object',
properties: {
url: { type: 'string' },
method: { type: 'string' },
},
}
```
### 3. 表单组件使用
```typescript
// ✅ 好的做法:使用 Field 绑定表单状态
<Field<string> name="api.url">
{({ field }) => (
<Input
value={field.value}
onChange={(value) => field.onChange(value)}
/>
)}
</Field>
// ❌ 不好的做法:手动管理状态
const [url, setUrl] = useState('');
<Input value={url} onChange={setUrl} />
```
### 4. 只读状态处理
```typescript
export function CustomComponent() {
const { readonly } = useNodeRenderContext();
return (
<Input disabled={readonly} {...props} />
);
}
```
## 常见问题
### Q1: 如何选择简单节点还是复杂节点?
**判断标准**
- 字段简单 + 默认布局满足需求 → 简单节点
- 需要自定义布局/特殊交互 → 复杂节点
### Q2: 如何使用变量功能?
在 `inputs` Schema 中使用 `formComponent: 'prompt-editor'`,并在 `inputsValues` 中使用 `type: 'template'`。
### Q3: 如何定义必填字段?
在 `inputs` Schema 的 `required` 数组中列出必填字段名。
### Q4: `inputsValues` 和 `inputs` 必须一致吗?
是的。`inputsValues` 中的字段必须在 `inputs.properties` 中有对应的定义。
### Q5: 节点图标支持什么格式?
支持 SVG、JPG、PNG 格式,推荐使用 SVG。
### Q6: 如何调试节点?
1. 使用浏览器开发者工具查看 console.log
2. 在 FormRender 组件中添加 `console.log(form.getValues())`
3. 使用 React DevTools 查看组件状态
## 参考资源
### 代码示例
- **简单节点**: `apps/demo-free-layout/src/nodes/llm/`
- **复杂节点**: `apps/demo-free-layout/src/nodes/http/`
- **表单组件**: `apps/demo-free-layout/src/form-components/`
- **默认表单**: `apps/demo-free-layout/src/nodes/default-form-meta.tsx`
### 模板文件
- **简单节点模板**: `.claude/skills/create-node/templates/simple-node/`
- **复杂节点模板**: `.claude/skills/create-node/templates/complex-node/`
### 相关文档
- FlowGram 官方文档: https://flowgram.ai
- JSON Schema 规范: https://json-schema.org/
- Semi UI 组件库: https://semi.design/
### 开发命令
```bash
# 启动开发服务器
rush dev:demo-free-layout
# 构建项目
rush build
# 类型检查
rush ts-check
# 代码检查
rush lint
```
## 快速开始检查清单
创建新节点时,按照此检查清单执行:
- [ ] 规划节点功能和数据结构
- [ ] 选择开发模式(简单 vs 复杂)
- [ ] 复制对应的模板文件
- [ ] 修改模板中的 TODO 标记
- [ ] 在 `constants.ts` 中添加节点类型
- [ ] 在 `index.ts` 中注册节点
- [ ] 准备节点图标文件
- [ ] 启动开发服务器测试
- [ ] 验证节点功能正常
@@ -0,0 +1,69 @@
# Node Templates
这些是创建新节点的模板文件,使用时需要替换其中的占位符。
## 占位符说明
在使用模板时,需要将以下占位符替换为实际值:
| 占位符 | 说明 | 示例 |
|-------|------|------|
| `{NODE_NAME}` | 节点名称(PascalCase | `Database`, `Webhook`, `EmailSender` |
| `{NODE_TYPE}` | 节点类型枚举值(SCREAMING_SNAKE_CASE | `DATABASE`, `WEBHOOK`, `EMAIL_SENDER` |
| `{node_name}` | 节点名称(kebab-case,用于 ID 前缀) | `database`, `webhook`, `email_sender` |
| `{node_type}` | 节点类型(小写,用于 type 字段) | `database`, `webhook`, `email_sender` |
| `{节点功能描述}` | 节点的功能描述(中文) | `发送邮件`, `查询数据库`, `调用 Webhook` |
## 使用方法
### 简单节点
```bash
# 1. 复制模板
cp .claude/skills/create-node/templates/simple-node/index.ts \
apps/demo-free-layout/src/nodes/database/index.ts
# 2. 替换占位符
# {NODE_NAME} → Database
# {NODE_TYPE} → DATABASE
# {node_name} → database
# {node_type} → database
# {节点功能描述} → 查询数据库
```
### 复杂节点
```bash
# 1. 复制模板目录
cp -r .claude/skills/create-node/templates/complex-node \
apps/demo-free-layout/src/nodes/webhook
# 2. 替换所有文件中的占位符
# {NODE_NAME} → Webhook
# {NODE_TYPE} → WEBHOOK
# {node_name} → webhook
# {node_type} → webhook
# {节点功能描述} → 调用 Webhook
```
## 快速替换脚本(可选)
如果需要批量替换,可以使用以下命令(macOS/Linux):
```bash
# 设置变量
NODE_NAME="Database"
NODE_TYPE="DATABASE"
node_name="database"
node_type="database"
description="查询数据库"
# 批量替换
find apps/demo-free-layout/src/nodes/database -type f -name "*.ts*" -exec sed -i '' \
-e "s/{NODE_NAME}/$NODE_NAME/g" \
-e "s/{NODE_TYPE}/$NODE_TYPE/g" \
-e "s/{node_name}/$node_name/g" \
-e "s/{node_type}/$node_type/g" \
-e "s/{节点功能描述}/$description/g" \
{} +
```
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { Field } from '@flowgram.ai/free-layout-editor';
import { Input, Select } from '@douyinfe/semi-ui';
import { useNodeRenderContext } from '../../../hooks';
import { FormItem } from '../../../form-components';
/**
* 自定义表单组件
*/
export function CustomComponent() {
const { readonly } = useNodeRenderContext();
return (
<div>
<FormItem name="配置项名称" required vertical type="string">
<Field<string> name="customConfig.key" defaultValue="">
{({ field }) => (
<Input
value={field.value}
onChange={(value) => field.onChange(value)}
disabled={readonly}
placeholder="请输入..."
/>
)}
</Field>
</FormItem>
{/* TODO: 添加更多表单字段 */}
<FormItem name="选择器示例" vertical type="string">
<Field<string> name="customConfig.option" defaultValue="option1">
{({ field }) => (
<Select
value={field.value}
onChange={(value) => field.onChange(value as string)}
disabled={readonly}
style={{ width: '100%' }}
optionList={[
{ label: '选项 1', value: 'option1' },
{ label: '选项 2', value: 'option2' },
{ label: '选项 3', value: 'option3' },
]}
/>
)}
</Field>
</FormItem>
</div>
);
}
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { FormMeta, FormRenderProps } from '@flowgram.ai/free-layout-editor';
import { DisplayOutputs } from '@flowgram.ai/form-materials';
import { Divider } from '@douyinfe/semi-ui';
import { FormHeader, FormContent } from '../../form-components';
import { {NODE_NAME}NodeJSON } from './types';
import { CustomComponent } from './components/custom-component';
import { defaultFormMeta } from '../default-form-meta';
/**
* 表单渲染组件
*/
export const FormRender = ({ form }: FormRenderProps<{NODE_NAME}NodeJSON>) => (
<>
<FormHeader />
<FormContent>
{/* TODO: 添加自定义组件 */}
<CustomComponent />
<Divider />
{/* 显示节点输出 */}
<DisplayOutputs displayFromScope />
</FormContent>
</>
);
/**
* 表单配置
*/
export const formMeta: FormMeta = {
render: (props) => <FormRender {...props} />,
effect: defaultFormMeta.effect,
plugins: [
// TODO: 根据需要添加插件
// createInferInputsPlugin({ sourceKey: 'xxxValues', targetKey: 'xxx' }),
],
};
@@ -0,0 +1,51 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { nanoid } from 'nanoid';
import { WorkflowNodeType } from '../constants';
import { FlowNodeRegistry } from '../../typings';
import iconNode from '../../assets/icon-{NODE_NAME}.svg'; // TODO: 准备图标文件
import { formMeta } from './form-meta';
let index = 0;
export const {NODE_NAME}NodeRegistry: FlowNodeRegistry = {
type: WorkflowNodeType.{NODE_TYPE}, // TODO: 在 constants.ts 中定义
info: {
icon: iconNode,
description: '{节点功能描述}', // TODO: 修改描述
},
meta: {
size: {
width: 360,
height: 390,
},
},
onAdd() {
return {
id: `{node_name}_${nanoid(5)}`, // TODO: 修改前缀
type: '{node_type}', // TODO: 与 WorkflowNodeType 保持一致
data: {
title: `{NODE_NAME}_${++index}`, // TODO: 修改标题前缀
// TODO: 根据实际需求定义自定义字段
customConfig: {
key: 'value',
},
// 节点输出数据的 JSON Schema
outputs: {
type: 'object',
properties: {
// TODO: 定义节点执行后的输出结构
result: { type: 'string' },
status: { type: 'number' },
},
},
},
};
},
formMeta: formMeta, // 引入自定义表单
};
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { FlowNodeJSON } from '../../typings';
/**
* 节点数据类型定义
*/
export interface {NODE_NAME}NodeJSON extends FlowNodeJSON {
data: FlowNodeJSON['data'] & {
// TODO: 根据实际需求定义自定义字段类型
customConfig?: {
key: string;
// 其他自定义字段
};
};
}
@@ -0,0 +1,95 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { nanoid } from 'nanoid';
import { WorkflowNodeType } from '../constants';
import { FlowNodeRegistry } from '../../typings';
import iconNode from '../../assets/icon-{NODE_NAME}.svg'; // TODO: 准备图标文件
let index = 0;
export const {NODE_NAME}NodeRegistry: FlowNodeRegistry = {
type: WorkflowNodeType.{NODE_TYPE}, // TODO: 在 constants.ts 中定义
info: {
icon: iconNode,
description: '{节点功能描述}', // TODO: 修改描述
},
meta: {
size: {
width: 360,
height: 390,
},
},
onAdd() {
// 返回节点的初始数据,这些数据会被保存到后端
return {
id: `{node_name}_${nanoid(5)}`, // TODO: 修改前缀
type: '{node_type}', // TODO: 与 WorkflowNodeType 保持一致
data: {
title: `{NODE_NAME}_${++index}`, // TODO: 修改标题前缀
// 节点表单字段的初始值
inputsValues: {
// TODO: 根据实际需求定义字段初始值
field1: {
type: 'constant', // 常量类型
content: '默认值', // 字段的初始值
},
field2: {
type: 'constant',
content: 100,
},
promptField: {
type: 'template', // 支持变量的模板类型
content: '',
},
},
// 节点表单的 JSON Schema(定义表单结构)
inputs: {
type: 'object',
required: ['field1'], // TODO: 定义必填字段
properties: {
// TODO: 根据实际需求定义字段 Schema
field1: {
type: 'string',
// 使用默认 Input 组件
},
field2: {
type: 'number',
minimum: 0,
maximum: 100,
},
promptField: {
type: 'string',
// 使用 PromptEditorWithVariables 组件
extra: {
formComponent: 'prompt-editor',
},
},
booleanField: {
type: 'boolean',
// 使用 Switch 组件
},
objectField: {
type: 'object',
// 使用 JsonCodeEditor 组件
},
},
},
// 节点输出数据的 JSON Schema(工作流执行时的输出)
outputs: {
type: 'object',
properties: {
// TODO: 定义节点执行后的输出结构
result: { type: 'string' },
status: { type: 'number' },
},
},
},
};
},
};
@@ -0,0 +1,367 @@
---
skill_name: material-component-dev
description: FlowGram 物料组件开发指南 - 用于在 form-materials 包中创建新的物料组件
version: 1.0.0
tags: [flowgram, material, component, development]
---
# FlowGram Material Component Development
## 概述
本 SKILL 用于指导在 FlowGram 项目的 `@flowgram.ai/form-materials` 包中创建新的物料组件。
## 核心原则
### 1. 组件位置
-**在现有包中创建**:直接在 `packages/materials/form-materials/src/components/` 下创建组件目录
-**不要单独拆包**:不创建新的 npm 包,保持简洁
### 2. 代码质量
-**使用 named export**:所有导出使用 named export 提高 tree shake 性能
-**不写单元测试**:通过 Storybook 进行手动测试
-**通过类型检查**:必须通过 `yarn ts-check`
-**符合代码规范**:遵循项目 ESLint 规则
### 3. 物料设计
-**保持精简**:只保留必要的 props,不添加非核心功能配置项
-**功能单一**:一个物料只做一件事
-**使用内部依赖**:优先使用 FlowGram 内部的组件和类型
### 4. 技术栈
- **UI 组件库**`@douyinfe/semi-ui`
- **代码编辑器**`JsonCodeEditor`, `CodeEditor` 等来自 `../code-editor`
- **类型定义**`IJsonSchema` 来自 `@flowgram.ai/json-schema`(不使用外部的 `json-schema` 包)
- **React**:必须显式 `import React` 避免 UMD 全局引用错误
## 开发流程
### Step 1: 规划组件结构
确定组件的:
- **功能**:组件要解决什么问题
- **Props 接口**:只保留核心必需的 props
- **命名**:使用 PascalCase,清晰描述功能
### Step 2: 创建目录结构
```bash
mkdir -p packages/materials/form-materials/src/components/{组件名}/utils
```
典型结构:
```
packages/materials/form-materials/src/components/{组件名}/
├── index.tsx # 导出文件 (named export)
├── {组件名}.tsx # 主组件
├── {辅助组件}.tsx # 可选的辅助组件
└── utils/ # 可选的工具函数
└── *.ts
```
### Step 3: 实现组件
#### 3.1 工具函数(如需要)
```typescript
// utils/helper.ts
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
export function helperFunction(input: string): Output {
// 实现逻辑
}
```
#### 3.2 辅助组件(如需要)
```typescript
// modal.tsx
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React, { useState } from 'react';
import { Modal, Typography } from '@douyinfe/semi-ui';
interface ModalProps {
visible: boolean;
onClose: () => void;
onConfirm: (data: SomeType) => void;
}
export function MyModal({ visible, onClose, onConfirm }: ModalProps) {
// 实现
}
```
#### 3.3 主组件
```typescript
// my-component.tsx
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React, { useState } from 'react';
import { Button } from '@douyinfe/semi-ui';
import type { IJsonSchema } from '@flowgram.ai/json-schema';
export interface MyComponentProps {
/** 核心功能的回调 */
onSomething?: (data: SomeType) => void;
}
// 使用 named export,不使用 default export
export function MyComponent({ onSomething }: MyComponentProps) {
const [visible, setVisible] = useState(false);
return (
<>
<Button onClick={() => setVisible(true)}>
</Button>
{/* 其他组件 */}
</>
);
}
```
**关键点**
- ✅ 显式 `import React`
- ✅ 使用 Semi UI 组件
- ✅ 使用 function 声明而非 React.FC
- ✅ Props 精简,只保留核心功能
- ✅ Named export
#### 3.4 导出文件
```typescript
// index.tsx
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
export { MyComponent } from './my-component';
export type { MyComponentProps } from './my-component';
```
### Step 4: 在 form-materials 主入口导出
编辑 `packages/materials/form-materials/src/components/index.ts`
```typescript
export {
// ... 其他组件按字母序
MyComponent,
// ... 继续其他组件
type MyComponentProps,
// ... 继续其他类型
} from './components';
```
然后编辑 `packages/materials/form-materials/src/index.ts`,确保新组件在主导出列表中:
```typescript
export {
// ... 其他组件按字母序
MyComponent,
// ...
type MyComponentProps,
// ...
} from './components';
```
### Step 5: 创建 Storybook Story
`apps/demo-materials/src/stories/components/` 创建 Story
```typescript
// my-component.stories.tsx
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React, { useState } from 'react';
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
import { MyComponent } from '@flowgram.ai/form-materials';
import type { SomeType } from '@flowgram.ai/json-schema';
const MyComponentDemo: React.FC = () => {
const [result, setResult] = useState<SomeType | null>(null);
return (
<div style={{ padding: '20px' }}>
<h2>My Component Demo</h2>
<MyComponent
onSomething={(data) => {
console.log('Generated data:', data);
setResult(data);
}}
/>
{result && (
<div style={{ marginTop: '20px' }}>
<h3>:</h3>
<pre style={{
background: '#f5f5f5',
padding: '16px',
borderRadius: '4px',
overflow: 'auto'
}}>
{JSON.stringify(result, null, 2)}
</pre>
</div>
)}
</div>
);
};
const meta: Meta<typeof MyComponentDemo> = {
title: 'Form Components/MyComponent',
component: MyComponentDemo,
parameters: {
layout: 'centered',
docs: {
description: {
component: '组件功能描述',
},
},
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
```
### Step 6: 运行类型检查
```bash
cd packages/materials/form-materials
yarn ts-check
```
确保通过所有类型检查。
### Step 7: 启动开发环境测试
开启两个 Terminal
**Terminal 1 - 监听包编译:**
```bash
rush build:watch
```
**Terminal 2 - 启动 Storybook**
```bash
cd apps/demo-materials
yarn dev
```
访问 http://localhost:6006/,找到你的组件进行测试。
## 常见问题
### Q1: React 引用错误
**错误信息**
```
error TS2686: 'React' refers to a UMD global, but the current file is a module.
```
**解决方案**
在文件顶部添加:
```typescript
import React from 'react';
```
### Q2: 组件未导出
**错误信息**
```
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined.
```
**解决方案**
检查以下文件的导出:
1. `components/{组件名}/index.tsx`
2. `components/index.ts`
3. `src/index.ts`
### Q3: 类型找不到
**错误信息**
```
Cannot find module '@flowgram.ai/json-schema' or its corresponding type declarations.
```
**解决方案**
- 使用 `type IJsonSchema` 而非 `type JSONSchema7`
-`@flowgram.ai/json-schema` 导入而非 `json-schema`
### Q4: CodeEditor 没有 height 属性
**错误信息**
```
Property 'height' does not exist on type 'CodeEditorPropsType'.
```
**解决方案**
使用外层 div 设置高度:
```tsx
<div style={{ minHeight: 300 }}>
<JsonCodeEditor value={value} onChange={onChange} />
</div>
```
## 验收标准
- [ ] 组件在 `packages/materials/form-materials/src/components/` 下创建
- [ ] 使用 named export
- [ ] 通过 `yarn ts-check` 类型检查
- [ ] Props 精简,只保留核心功能
- [ ] 在 Storybook 中可以正常显示和使用
- [ ] 功能正常,无明显 bug
- [ ] 代码符合 FlowGram 代码规范
## 最佳实践
### 1. 组件设计
- **单一职责**:一个组件只做一件事
- **Props 精简**:避免过度配置
- **命名清晰**:组件名和 Props 名要清晰易懂
### 2. 代码风格
- **使用 TypeScript**:充分利用类型系统
- **显式导入**:明确导入所需的依赖
- **注释适度**:关键逻辑添加注释
### 3. UI 一致性
- **使用 Semi UI**:保持 UI 风格一致
- **响应式设计**:考虑不同屏幕尺寸
- **错误处理**:友好的错误提示
### 4. 性能优化
- **Named export**:支持 tree shaking
- **按需加载**:避免不必要的依赖
- **合理使用 memo**:必要时使用 React.memo
## 示例参考
完整示例请参考:
- `packages/materials/form-materials/src/components/json-schema-creator/`
- `apps/demo-materials/src/stories/components/json-schema-creator.stories.tsx`
@@ -0,0 +1,378 @@
---
name: material-component-doc
description: 用于 FlowGram 物料库组件文档撰写的专用技能,提供组件文档生成、Story 创建、翻译等功能的指导和自动化支持
metadata:
version: "1.1.0"
category: "documentation"
language: "zh-CN"
framework: "FlowGram"
---
# FlowGram 文档的组织结构
- **英文文档**: `apps/docs/src/en`
- **中文文档**: `apps/docs/src/zh`
- **Story 组件**: `apps/docs/components/form-materials/components`
- **物料源码**: `packages/materials/form-materials/src/components`
- **文档模板**: `./templates/material.mdx`
# 组件物料文档撰写流程
## 1. 源码定位
`packages/materials/form-materials/src/components` 目录下确认物料源代码地址。
**操作**
- 使用 Glob 工具搜索物料文件
- 确认目录结构(是否有 hooks.ts, context.tsx 等)
- 记录导出名称和文件路径
## 2. 需求收集
向用户询问物料使用实例和具体需求。
**收集信息**
- 主要使用场景
- 典型代码示例(1-2 个)
- 特殊配置或高级用法
- 是否需要配图
## 3. 功能分析
深入阅读源代码,理解物料功能。
**分析要点**
- Props 接口(类型、默认值、描述)
- 核心功能和实现方式
- 依赖关系(FlowGram API、其他物料、第三方库)
- Hooks 和 Context
- 特殊逻辑(条件渲染、副作用等)
## 4. Story 创建
`apps/docs/components/form-materials/components` 下创建 Story 组件(详见下方 Story 规范)。
## 5. 文档撰写
基于模板 `./templates/material.mdx` 撰写完整文档。
**文档位置**
- 中文:`apps/docs/src/zh/materials/components/{物料名称}.mdx`
- 英文:`apps/docs/src/en/materials/components/{物料名称}.mdx`(翻译后)
## 6. 质量检查
**检查清单**
- [ ] Story 组件能正常运行
- [ ] 代码示例准确无误
- [ ] API 表格完整
- [ ] 依赖链接正确可访问
- [ ] 图片路径正确
- [ ] Mermaid 流程图语法正确
- [ ] CLI 命令路径准确
**用户确认中文文档的撰写后,再执行翻译**
**用户确认中文文档的撰写后,再执行翻译**
**用户确认中文文档的撰写后,再执行翻译**
---
# Story 组件规范
> **参考示例**: `apps/docs/components/form-materials/components/variable-selector.tsx`
## 命名规范
**文件命名**: kebab-case,与物料名称一致
-`variable-selector.tsx`
-`dynamic-value-input.tsx`
-`VariableSelector.tsx`
**Story 导出命名**: PascalCase + "Story" 后缀
- `BasicStory` - 基础使用(必需)
- `WithSchemaStory` - 带 Schema 约束
- `DisabledStory` - 禁用状态
- `CustomFilterStory` - 自定义过滤
- 根据物料特性命名,见名知意
## 代码要求
### 1. 懒加载导入
```tsx
// ✅ 正确
const VariableSelector = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.VariableSelector,
}))
);
// ❌ 错误
import { VariableSelector } from '@flowgram.ai/form-materials';
```
### 2. 包装组件
```tsx
// ✅ 正确
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<string[]> name="variable_selector">
{({ field }) => (
<VariableSelector
value={field.value}
onChange={(value) => field.onChange(value)}
/>
)}
</Field>
</>
),
}}
/>
);
// ❌ 错误:缺少包装
export const BasicStory = () => (
<VariableSelector value={[]} onChange={() => {}} />
);
```
### 3. 类型标注
```tsx
// ✅ 正确
<Field<string[] | undefined> name="variable_selector">
// ❌ 错误
<Field<any> name="variable_selector">
```
### 4. 语言规范
代码和注释只使用英文,无中文。
## 完整示例
```tsx
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React from 'react';
import { Field } from '@flowgram.ai/free-layout-editor';
import { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';
const VariableSelector = React.lazy(() =>
import('@flowgram.ai/form-materials').then((module) => ({
default: module.VariableSelector,
}))
);
export const BasicStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<string[] | undefined> name="variable_selector">
{({ field }) => (
<VariableSelector
value={field.value}
onChange={(value) => field.onChange(value)}
/>
)}
</Field>
</>
),
}}
/>
);
export const FilterSchemaStory = () => (
<FreeFormMetaStoryBuilder
filterEndNode
formMeta={{
render: () => (
<>
<FormHeader />
<Field<string[] | undefined> name="variable_selector">
{({ field }) => (
<VariableSelector
value={field.value}
onChange={(value) => field.onChange(value)}
includeSchema={{ type: 'string' }}
/>
)}
</Field>
</>
),
}}
/>
);
```
---
# 物料文档格式
## 使用模板
**模板文件**: `./templates/material.mdx`
文档必须严格按照模板格式编写,包含以下章节:
1. Import 语句
2. 标题和简介(带可选配图)
3. 案例演示(基本使用 + 高级用法)
4. API 参考(Props 表格)
5. 源码导读(目录结构、核心实现、流程图、依赖梳理)
## 参考示例
- [`dynamic-value-input.mdx`](apps/docs/src/zh/materials/components/dynamic-value-input.mdx) - 完整的流程图和依赖说明
- [`variable-selector.mdx`](apps/docs/src/zh/materials/components/variable-selector.mdx) - 多个 API 表格和警告提示
## 关键注意事项
**API 表格要求**
- 必须包含所有公开的 Props
- 类型使用反引号(如 \`string\`
- 描述清晰简洁
- 多个相关类型分开列表
**源码导读要求**
- 目录结构:展示文件列表及说明
- 核心实现:用代码片段说明关键逻辑
- 整体流程:Mermaid 流程图(推荐)
- 依赖梳理:分类列出 FlowGram API、其他物料、第三方库
---
# 图片处理指南
## 截图要求
1. **时机**: Story 组件完成后,运行 docs 站点截图
2. **内容**: 捕获物料的典型使用状态,清晰可见
3. **格式**: PNG,适当压缩
## 命名和存储
- **命名**: `{物料名称}.png`kebab-case
- **存储**: `apps/docs/src/public/materials/{物料名称}.png`
- **引用**: `/materials/{物料名称}.png`
## 在文档中使用
```mdx
<br />
<div>
<img loading="lazy" src="/materials/{物料名称}.png" alt="{物料名称} 组件" style={{ width: '50%' }} />
</div>
```
---
# 翻译流程
## 翻译时机
- ✅ 用户明确要求翻译
- ✅ 中文文档已经用户审核确认
- ❌ 文档还在修改中
- ❌ 用户未确认最终版本
## 翻译原则
**术语一致性**
- ComponentName → ComponentName(组件名不翻译)
- Props、Hook、Schema 等术语保持原文
**代码不翻译**
- 所有代码块、命令、路径保持原样
**链接处理**
- 内部链接:`/zh/``/en/`
- 外部链接和 GitHub 链接:保持不变
**格式保持**
- Markdown 格式、缩进、空行完全一致
## 翻译检查清单
- [ ] 标题和描述已翻译
- [ ] 代码示例未被翻译
- [ ] 命令和路径保持原样
- [ ] 内部文档链接已更新
- [ ] API 表格描述列已翻译
- [ ] Mermaid 图中文节点已翻译
- [ ] 术语使用一致
---
# 最佳实践
## Props 提取技巧
1. 查找 `interface``type` 定义
2. 检查组件函数参数类型
3. 查找 `defaultProps` 确认默认值
4. 阅读 JSDoc 提取描述
## 依赖分析方法
1. 查看 import 语句(直接依赖)
2. 分析 Hook 调用(FlowGram API
3. 查找组件引用(其他物料)
4. 检查 package.json(第三方库)
## Mermaid 流程图建议
1. 简洁明了,关注核心流程
2. 使用时序图绘制
## 常见错误避免
❌ 直接导入物料而不使用 `React.lazy`
❌ API 表格遗漏 Props
❌ 依赖链接失效
❌ 中英文混用
❌ 路径格式错误
✅ 参考优秀示例
✅ 仔细阅读源码
✅ 验证所有链接
✅ 保持语言和格式一致
✅ 使用项目约定的路径格式
---
# 相关工具和资源
## 开发命令
```bash
# 启动文档站点
rush dev:docs
# 查看修改
git diff
git diff --cached
```
## 关键目录
| 目录 | 说明 |
|------|------|
| `packages/materials/form-materials/src/components` | 物料源码 |
| `apps/docs/src/zh/materials/components` | 中文文档 |
| `apps/docs/src/en/materials/components` | 英文文档 |
| `apps/docs/components/form-materials/components` | Story 组件 |
| `apps/docs/src/public/materials` | 图片资源 |
| `./templates` | 文档模板 |
@@ -0,0 +1,172 @@
import { SourceCode } from '@theme';
import { BasicStory, WithSchemaStory } from 'components/form-materials/components/{物料名称}';
# ComponentName
ComponentName 是一个用于...的组件,它支持...功能。[用 1-2 段文字描述物料的核心功能、使用场景和主要特性]
<br />
<div>
<img loading="lazy" src="/materials/{物料名称}.png" alt="ComponentName 组件" style={{ width: '50%' }} />
</div>
## 案例演示
### 基本使用
<BasicStory />
```tsx pure title="form-meta.tsx"
import { ComponentName } from '@flowgram.ai/form-materials';
const formMeta = {
render: () => (
<>
<FormHeader />
<Field<ValueType> name="field_name">
{({ field }) => (
<ComponentName
value={field.value}
onChange={(value) => field.onChange(value)}
/>
)}
</Field>
</>
),
}
```
### 高级用法示例(根据物料特性添加)
<WithSchemaStory />
```tsx pure title="form-meta.tsx"
import { ComponentName } from '@flowgram.ai/form-materials';
const formMeta = {
render: () => (
<>
<FormHeader />
<Field<ValueType> name="field_name">
{({ field }) => (
<ComponentName
value={field.value}
onChange={(value) => field.onChange(value)}
schema={{ type: 'string' }}
// 其他高级配置...
/>
)}
</Field>
</>
),
}
```
## API 参考
### ComponentName Props
| 属性名 | 类型 | 默认值 | 描述 |
|--------|------|--------|------|
| `value` | `ValueType` | - | 组件的值 |
| `onChange` | `(value: ValueType) => void` | - | 值变化时的回调函数 |
| `readonly` | `boolean` | `false` | 是否为只读模式 |
| `hasError` | `boolean` | `false` | 是否显示错误状态 |
| `style` | `React.CSSProperties` | - | 自定义样式 |
### RelatedConfigType(如果有相关的配置类型)
| 属性名 | 类型 | 默认值 | 描述 |
|--------|------|--------|------|
| `property1` | `string` | - | 属性说明 |
| `property2` | `boolean` | `false` | 属性说明 |
### RelatedProviderProps(如果有 Provider 组件)
| 属性名 | 类型 | 默认值 | 描述 |
|--------|------|--------|------|
| `children` | `React.ReactNode` | - | 子组件 |
| `config` | `ConfigType` | - | 配置对象 |
## 源码导读
<SourceCode
href="https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/{物料路径}"
/>
使用 CLI 命令可以复制源代码到本地:
```bash
npx @flowgram.ai/cli@latest materials components/{物料路径}
```
### 目录结构讲解
```
{物料名称}/
├── index.tsx # 主组件实现,包含 ComponentName 核心逻辑
├── hooks.ts # 自定义 Hooks,处理... [如果有]
├── context.tsx # Context Provider,提供... [如果有]
├── utils.ts # 工具函数,用于... [如果有]
└── styles.css # 样式文件
```
### 核心实现说明
#### 功能点1
[用简洁的文字描述实现原理]
```typescript
// 展示关键代码片段
const result = useHookName(props);
```
#### 功能点2
[描述另一个关键功能的实现方式]
```typescript
// 展示关键逻辑
if (condition) {
return <ComponentA />;
} else {
return <ComponentB />;
}
```
### 整体流程
```mermaid
graph TD
A[组件初始化] --> B{判断条件}
B -->|条件1| C[执行分支A]
B -->|条件2| D[执行分支B]
C --> E[处理用户交互]
D --> F[处理数据变化]
E --> G[触发 onChange 回调]
F --> G
```
### 使用到的 FlowGram API
[**@flowgram.ai/package-name**](https://github.com/bytedance/flowgram.ai/tree/main/packages/path)
- [`ApiName`](https://flowgram.ai/auto-docs/package/type/ApiName): API 的功能说明
- [`HookName`](https://flowgram.ai/auto-docs/package/functions/HookName): Hook 的功能说明
[**@flowgram.ai/another-package**](https://github.com/bytedance/flowgram.ai/tree/main/packages/another-path)
- [`TypeName`](https://flowgram.ai/auto-docs/package/interfaces/TypeName): 类型定义说明
### 依赖的其他物料
[**DependentMaterial**](./dependent-material) 物料的简要说明
- `ExportedComponent`: 导出组件的用途
- `ExportedHook`: 导出 Hook 的用途
[**AnotherMaterial**](./another-material) 物料的简要说明
### 使用的第三方库
[**library-name**](https://library-url.com) 库的说明
- `ImportedComponent`: 组件的用途
- `importedFunction`: 函数的用途
+14
View File
@@ -0,0 +1,14 @@
# Don't allow people to merge changes to these generated files, because the result
# may be invalid. You need to run "rush update" again.
pnpm-lock.yaml merge=ours
shrinkwrap.yaml merge=binary
npm-shrinkwrap.json merge=binary
yarn.lock merge=binary
# Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic
# syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor
# may also require a special configuration to allow comments in JSON.
#
# For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088
#
*.json linguist-language=JSON-with-Comments
+12
View File
@@ -0,0 +1,12 @@
# 文件路径与代码负责人分配
# 对整个仓库设置代码负责人
* @xiamidaxia @luics @dragooncjw @YuanHeDx @sanmaopep @louisyoungx
# 对特定目录设置代码负责人
/apps/docs/ @xiamidaxia @dragooncjw @YuanHeDx @sanmaopep @louisyoungx
/apps/demo-node-form/ @xiamidaxia @dragooncjw @YuanHeDx
/packages/node-engine/ @xiamidaxia @dragooncjw @YuanHeDx
/packages/variable-engine/ @xiamidaxia @dragooncjw @sanmaopep
/packages/plugins/variable-plugin/ @xiamidaxia @dragooncjw @sanmaopep
/packages/plugins/node-variable-plugin/ @xiamidaxia @dragooncjw @sanmaopep
/packages/plugins/node-core-plugin/ @xiamidaxia @dragooncjw @YuanHeDx
+19
View File
@@ -0,0 +1,19 @@
---
name: Bug Report
about: Report Bug
title: "[Bug] "
labels: [bug]
---
## 🙋 SDK Version
Please input version of SDK.
## 📌 Layout
Free layout or Fixed layout?
## 💻 Environment
- Operation System: (e.g. Windows 11 / macOs 14.3)
- Node.js:
- Other:
## 📝 Question Description
+19
View File
@@ -0,0 +1,19 @@
---
name: Question Report
about: Report Question
title: "[Question] "
labels: [question]
---
## 🙋 SDK Version
Please input version of SDK.
## 📌 Layout
Free layout or Fixed layout?
## 💻 Environment
- Operation System: (e.g. Windows 11 / macOs 14.3)
- Node.js:
- Other:
## 📝 Question Description
+38
View File
@@ -0,0 +1,38 @@
name: CI
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
merge_group:
branches: ["main"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Config Git User
run: |
git config --local user.name "dragooncjw"
git config --local user.email "289056872@qq.com"
- name: For Debug
run: |
echo "Listing files in the root directory:"
ls -alh
- uses: actions/setup-node@v3
with:
node-version: 18
# - name: Verify Change Logs
# run: node common/scripts/install-run-rush.js change --verify
- name: Rush Install
run: node common/scripts/install-run-rush.js install
- name: Rush build
run: node common/scripts/install-run-rush.js build
- name: Check Lint
run: node common/scripts/install-run-rush.js lint --verbose
- name: Check TS
run: node common/scripts/install-run-rush.js ts-check
- name: Test (coverage)
run: node common/scripts/install-run-rush.js test:cov
+39
View File
@@ -0,0 +1,39 @@
name: PR Common Checks
on:
pull_request:
types: [opened, edited, synchronize, reopened]
jobs:
common-checks:
name: PR Common Checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Config Git User
run: |
git config --local user.name "tecvan"
git config --local user.email "fanwenjie.fe@bytedance.com"
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Dependencies
run: node common/scripts/install-run-rush.js install
# PR Title Format Check
- name: Check PR Title Format
if: ${{ !contains(github.event.pull_request.title, 'WIP') && !contains(github.event.pull_request.title, 'wip') }}
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
node common/scripts/install-run-rush.js update-autoinstaller --name rush-commitlint && \
pushd common/autoinstallers/rush-commitlint && \
echo "$PR_TITLE" | npx commitlint --config commitlint.config.js && \
popd
# Add more common checks here
# For example: file size checks, specific file format validations, etc.
+75
View File
@@ -0,0 +1,75 @@
name: Deploy With Actions
on: workflow_dispatch
concurrency:
group: "main-deploy-branch-workflow"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
pages: write
id-token: write
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2
persist-credentials: true
- name: Config Git User
run: |
git config --local user.name "dragooncjw"
git config --local user.email "289056872@qq.com"
- uses: actions/setup-node@v3
with:
node-version: 18
registry-url: "https://registry.npmjs.org/"
- name: Rush Install
run: node common/scripts/install-run-rush.js install -t @flowgram.ai/docs
- name: Rush build
run: node common/scripts/install-run-rush.js build -t @flowgram.ai/docs
- name: Generate docs
run: |
cd apps/docs
npm run docs
- name: Copy auto-docs to en
run: cp -r apps/docs/src/zh/auto-docs apps/docs/src/en/auto-docs
- name: Build Doc site
run: |
cd apps/docs
npm run build
- name: Replace docs
run: |
rm -rf docs
mv apps/docs/doc_build docs
# 🔥 新增步骤:在 docs 目录下生成 vercel.json
- name: Create vercel.json
run: |
cat > docs/vercel.json <<EOF
{
"version": 2,
"buildCommand": "",
"outputDirectory": ".",
"cleanUrls": true,
"trailingSlash": false
}
EOF
# 🔥 推送到 gh-pages 分支
- name: Push to gh-pages branch
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
publish_branch: gh-pages
force: true
+39
View File
@@ -0,0 +1,39 @@
name: E2E Tests
on:
pull_request:
branches: ["main"]
merge_group:
branches: ["main"]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Rush Install
run: node common/scripts/install-run-rush.js install
- name: Rush build
run: node common/scripts/install-run-rush.js build
# 缓存 Playwright 浏览器
- name: Cache Playwright browsers
uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('e2e/fixed-layout/package.json') }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Install Playwright Browsers
run: pushd e2e/fixed-layout && npx playwright install --with-deps --only-shell chromium && popd
- name: Run E2E tests
run: node common/scripts/install-run-rush.js e2e:test --verbose
+196
View File
@@ -0,0 +1,196 @@
name: Publish
on:
workflow_dispatch:
inputs:
publish-type:
description: "发布类型"
required: true
type: choice
options:
- sdk-patch
- sdk-minor
- sdk-alpha
- sdk-to-version
- app-patch
- app-to-version
sdk-version:
description: "指定版本号(仅 sdk-to-version / app-to-version 时需要,e.g. 1.0.0"
required: false
default: ""
concurrency:
group: "publish-workflow"
cancel-in-progress: false
permissions:
id-token: write
contents: read
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Config Git User
run: |
git config --local user.name "dragooncjw"
git config --local user.email "289056872@qq.com"
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Upgrade npm for OIDC support
run: |
npm install -g npm@latest
npm --version
- name: Determine version and publish parameters
id: params
run: |
PUBLISH_TYPE="${{ github.event.inputs.publish-type }}"
INPUT_VERSION="${{ github.event.inputs.sdk-version }}"
case "$PUBLISH_TYPE" in
sdk-patch)
LATEST_VERSION=$(npm view @flowgram.ai/core version --tag=latest latest)
POLICY_NAME="publishPolicy"
NEXT_BUMP="patch"
NPM_TAG="latest"
USE_OVERRIDE="false"
;;
sdk-minor)
LATEST_VERSION=$(npm view @flowgram.ai/core version --tag=latest latest)
POLICY_NAME="publishPolicy"
NEXT_BUMP="minor"
NPM_TAG="latest"
USE_OVERRIDE="false"
;;
sdk-alpha)
LATEST_VERSION=$(npm view @flowgram.ai/core version --tag=alpha latest 2>/dev/null || true)
if [ -z "$LATEST_VERSION" ]; then
LATEST_VERSION="0.1.0-alpha.1"
echo "Version not found, using default: $LATEST_VERSION"
fi
POLICY_NAME="publishPolicy"
NEXT_BUMP="prerelease"
NPM_TAG="alpha"
USE_OVERRIDE="true"
;;
sdk-to-version)
if [ -z "$INPUT_VERSION" ]; then
echo "::error::sdk-version is required for sdk-to-version"
exit 1
fi
LATEST_VERSION="$INPUT_VERSION"
POLICY_NAME="publishPolicy"
NEXT_BUMP=""
NPM_TAG="latest"
USE_OVERRIDE="true"
;;
app-patch)
LATEST_VERSION=$(npm view @flowgram.ai/demo-fixed-layout version --tag=latest latest)
POLICY_NAME="appPolicy"
NEXT_BUMP="patch"
NPM_TAG="latest"
USE_OVERRIDE="false"
;;
app-to-version)
if [ -z "$INPUT_VERSION" ]; then
echo "::error::sdk-version is required for app-to-version"
exit 1
fi
LATEST_VERSION="$INPUT_VERSION"
POLICY_NAME="appPolicy"
NEXT_BUMP=""
NPM_TAG="latest"
USE_OVERRIDE="true"
;;
esac
echo "LATEST_VERSION=$LATEST_VERSION" >> $GITHUB_ENV
echo "POLICY_NAME=$POLICY_NAME" >> $GITHUB_ENV
echo "NEXT_BUMP=$NEXT_BUMP" >> $GITHUB_ENV
echo "NPM_TAG=$NPM_TAG" >> $GITHUB_ENV
echo "USE_OVERRIDE=$USE_OVERRIDE" >> $GITHUB_ENV
echo "PUBLISH_TYPE=$PUBLISH_TYPE" >> $GITHUB_ENV
- name: Echo version
run: |
echo "Publish type: $PUBLISH_TYPE"
echo "The package version is: $LATEST_VERSION"
echo "Policy: $POLICY_NAME, Next bump: $NEXT_BUMP, Tag: $NPM_TAG"
- name: Rush Install
run: node common/scripts/install-run-rush.js install
- name: Rush build
run: node common/scripts/install-run-rush.js build
- name: Sync versions
run: |
if [ -n "$NEXT_BUMP" ]; then
echo "[
{
\"policyName\": \"$POLICY_NAME\",
\"definitionName\": \"lockStepVersion\",
\"version\": \"$LATEST_VERSION\",
\"nextBump\": \"$NEXT_BUMP\"
}
]" > common/config/rush/version-policies.json
else
echo "[
{
\"policyName\": \"$POLICY_NAME\",
\"definitionName\": \"lockStepVersion\",
\"version\": \"$LATEST_VERSION\"
}
]" > common/config/rush/version-policies.json
fi
- name: Version Bump (override)
if: env.USE_OVERRIDE == 'true'
run: node common/scripts/install-run-rush.js version --ensure-version-policy --override-version=$LATEST_VERSION --version-policy=$POLICY_NAME
- name: Version Bump
if: env.USE_OVERRIDE == 'false'
run: node common/scripts/install-run-rush.js version --bump --version-policy $POLICY_NAME
# For alpha, we need both override and bump
- name: Version Bump (alpha additional bump)
if: env.PUBLISH_TYPE == 'sdk-alpha'
run: node common/scripts/install-run-rush.js version --bump --version-policy $POLICY_NAME
- name: Publish
run: node common/scripts/install-run-rush.js publish --include-all -p --tag $NPM_TAG
- name: Print npm debug logs on failure
if: failure()
run: |
echo "=== npm debug logs ==="
find "$GITHUB_WORKSPACE/common/temp/publish-home/.npm/_logs" -name '*.log' -exec echo "--- {} ---" \; -exec cat {} \; 2>/dev/null || echo "No npm debug logs found"
- name: Get new Version
id: get_new_version
if: env.PUBLISH_TYPE != 'app-patch'
run: |
if [ "$NPM_TAG" = "alpha" ]; then
NEW_VERSION=$(npm view @flowgram.ai/core version --tag=alpha latest)
else
NEW_VERSION=$(npm view @flowgram.ai/core version --tag=latest latest)
fi
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV
- name: Create tag
if: env.PUBLISH_TYPE != 'app-patch'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git tag "v$NEW_VERSION"
git push origin "v$NEW_VERSION"
+48
View File
@@ -0,0 +1,48 @@
name: Sync Screenshot
on: workflow_dispatch
concurrency:
group: "manual-sync-screenshot"
cancel-in-progress: false
jobs:
e2e:
# can not update screenshot run on main.
if: github.ref_name != 'main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Set Git user.name and user.email from trigger actor
run: |
git config --global user.name "${{ github.actor }}"
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
- name: Rush Install
run: node common/scripts/install-run-rush.js install
- name: Rush build
run: node common/scripts/install-run-rush.js build
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run E2E tests
run: node common/scripts/install-run-rush.js e2e:update-screenshot --verbose
- name: Commit and push changes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git add .
if git diff-index --quiet HEAD; then
echo "No changes to commit"
else
git commit -m "chore: sync screenshot"
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }} HEAD:${{ github.ref_name }}
fi
+132
View File
@@ -0,0 +1,132 @@
# e2e results
test-results/
doc_build/
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov/
# Coverage directory used by tools like istanbul
coverage/
# nyc test coverage
.nyc_output/
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt/
# Bower dependency directory (https://bower.io/)
bower_components/
# node-waf configuration
.lock-wscript/
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release/
# Dependency directories
node_modules/
jspm_packages/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm/
# Optional eslint cache
.eslintcache/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# next.js build output
.next/
# Docusaurus cache and generated files
.docusaurus/
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# yarn v2
.yarn/cache/
.yarn/unplugged/
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# OS X temporary files
.DS_Store
# IntelliJ IDEA project files; if you want to commit IntelliJ settings, this recipe may be helpful:
# https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
.idea/
*.iml
# Visual Studio Code
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
# Rush temporary files
common/deploy/
common/temp/
common/autoinstallers/*/.npmrc
**/.rush/temp/
*.lock
# Common toolchain intermediate files
temp/
lib/
lib-amd/
lib-es6/
lib-esnext/
lib-commonjs/
lib-shim/
dist/
dist-storybook/
*.tsbuildinfo
# Heft temporary files
.cache/
.heft/
# rush standard files
.eslintcache
# eslint cache for v9
.lintcache/
+15
View File
@@ -0,0 +1,15 @@
{
"recommendations": [
"styled-components.vscode-styled-components",
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"streetsidesoftware.code-spell-checker",
"codezombiech.gitignore",
"aaron-bond.better-comments"
],
"unwantedRecommendations": [
"nucllear.vscode-extension-auto-import",
"steoates.autoimport"
]
}
+148
View File
@@ -0,0 +1,148 @@
{
"eslint.nodePath": "config/eslint-config/node_modules/eslint",
"prettier.prettierPath": "config/eslint-config/node_modules/prettier",
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
"editor.formatOnType": false,
"editor.formatOnPaste": false,
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.fixAll.eslint": "explicit"
},
"search.followSymlinks": false,
"search.exclude": {
"**/node_modules": true,
"**/.nyc_output": true,
"**/.rush": true,
"**/pnpm-lock.yaml": true,
"**/CHANGELOG.json": true,
"**/CHANGELOG.md": true,
"common/changes": true,
"**/output": true,
"**/lib": true,
"**/dist": true,
"**/coverage": true,
"common/temp": true
},
"eslint.workingDirectories": [
{
"mode": "auto"
}
],
"files.defaultLanguage": "plaintext",
"files.associations": {
".code-workspace": "jsonc",
".babelrc": "json",
".eslintrc": "jsonc",
".eslintrc*.json": "jsonc",
".stylelintrc": "jsonc",
"stylelintrc": "jsonc",
"*.json": "jsonc",
"package.json": "json",
".htmlhintrc": "jsonc",
"htmlhintrc": "jsonc",
"Procfile*": "shellscript",
"README": "markdown",
"**/coverage/**/*.*": "plaintext",
"OWNERS": "yaml",
"**/pnpm-lock.yaml": "plaintext",
"**/dist/**": "plaintext",
"**/dist_*/**": "plaintext",
"*.map": "plaintext",
"*.log": "plaintext"
},
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true,
"**/.rush": true
},
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/*/**": true
},
"search.useIgnoreFiles": true,
//
"editor.rulers": [
80,
120
],
"files.eol": "\n",
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"cSpell.diagnosticLevel": "Warning",
"eslint.probe": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"eslint.format.enable": true,
"eslint.lintTask.enable": true,
"javascript.validate.enable": false,
"typescript.validate.enable": true,
"typescript.tsdk": "config/ts-config/node_modules/typescript/lib",
"typescript.tsserver.maxTsServerMemory": 8192,
// "typescript.tsserver.experimental.enableProjectDiagnostics": true,
"typescript.tsserver.watchOptions": {
"fallbackPolling": "dynamicPriorityPolling",
"synchronousWatchDirectory": false,
"watchDirectory": "dynamicPriorityPolling",
"watchFile": "useFsEventsOnParentDirectory"
},
"css.validate": false,
"scss.validate": false,
"less.validate": false,
"emmet.triggerExpansionOnTab": true,
"[yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[less]": {
"editor.defaultFormatter": "vscode.css-language-features"
},
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[javascriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[ignore]": {
"editor.defaultFormatter": "foxundermoon.shell-format"
},
"[shellscript]": {
"editor.defaultFormatter": "foxundermoon.shell-format"
},
"[dotenv]": {
"editor.defaultFormatter": "foxundermoon.shell-format"
},
"[svg]": {
"editor.defaultFormatter": "jock.svg"
},
"[mdx]": {
"editor.defaultFormatter": "unifiedjs.vscode-mdx"
},
"cSpell.words": [
"Twoway"
]
}
+16
View File
@@ -0,0 +1,16 @@
# Repository Guidelines
## Project Structure & Module Organization
FlowGram is a Rush-managed monorepo. Production-ready libraries live under `packages/*` (canvas engine, node engine, runtime, plugins). Demo UIs and docs sit inside `apps/*` (for example `apps/demo-free-layout`, `apps/docs`). Shared tooling, config, and scripts live under `common/` and `config/`. End-to-end Playwright suites are isolated in `e2e/<scenario>` so they can be installed and run independently. New code should land in the closest existing package; create additional Rush projects only when a module needs its own version and publish cycle.
## Build, Test, and Development Commands
Use Node.js 18 LTS with pnpm 10.6.5 (Rush enforces versions). Install dependencies via `rush install`. Run `rush build` to compile every registered project. Use `rush dev:docs` or `rush dev:demo-free-layout` for hot-reload docs and demos. `rush lint`, `rush lint:fix`, and `rush ts-check` keep lint/TS diagnostics consistent. `rush test` aggregates unit tests; `rush e2e:test` runs Playwright suites, while `rush e2e:update-screenshot` refreshes snapshots.
## Coding Style & Naming Conventions
We write TypeScript with React, sharing configs from `config/eslint-config`. ESLint enforces 2-space indentation, semicolons, and import order; run `rush lint:fix` before committing. Use `PascalCase` for React components and classes, `camelCase` for variables/functions, and `SCREAMING_SNAKE_CASE` only for constants exported from config files. File names follow kebab-case (e.g., `flow-node-form.tsx`). Keep public API surfaces documented via barrel files such as `packages/canvas-engine/core/src/index.ts`.
## Testing Guidelines
Unit tests use Vitest and live beside source in `__tests__` folders or `*.test.ts` files; prefer descriptive names like `node-service.test.ts`. Ensure new logic is covered by `rush test`, and include data fixtures where possible. Playwright specs under `e2e/*/tests` cover critical workflows—coordinate UI changes with updated snapshots and run `rush e2e:test --to <package>` to scope failures.
## Commit & Pull Request Guidelines
Follow conventional commits (`type(scope): subject`) as seen in history (`fix(auto-layout): ...`). Keep subjects imperative and ≤72 characters, with optional bodies for context. PRs must describe the change, link GitHub issues, and attach before/after screenshots for UI updates. Confirm CI status, note any follow-ups, and request reviewers from the owning package tags (see `rush.json`).
+29
View File
@@ -0,0 +1,29 @@
# Changelog
## [Unreleased]
### Added
- Add `flowing` field to `LineColor` interface for configuring flowing line colors
- Added `flowing: string` field to `LineColor` interface
- Added `LineColors.FLOWING` enum value with default color
- Updated `WorkflowLinesManager.getLineColor()` to support flowing state
- Updated demo configurations to include flowing color examples
- Added comprehensive test coverage for flowing line functionality
### Features
- Lines can now be colored differently when in flowing state (e.g., during workflow execution)
- Priority order: hidden > error > highlight > drawing > hovered > selected > flowing > default
- Backward compatible with existing line color configurations
### Demo Updates
- Updated `apps/demo-free-layout` to include flowing color configuration
- Added CSS variable support: `var(--g-workflow-line-color-flowing,#4d53e8)`
### Tests
- Added test cases for flowing line color functionality
- Verified priority ordering with other line states
- Ensured backward compatibility
+233
View File
@@ -0,0 +1,233 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository Overview
FlowGram is a composable, visual workflow development framework built as a Rush-managed monorepo. It provides tools for building AI workflow platforms, including a flow canvas, node configuration forms, variable scope chains, and pre-built materials (LLM, Condition, Code Editor, etc.).
## Build System & Package Management
This monorepo uses **Rush 5.150.0** with **pnpm 10.6.5** as the package manager. Node.js version must be >=18.20.3 <19.0.0 || >=20.14.0 <23.0.0.
### Essential Commands
```bash
# Install dependencies (required first step)
rush install
# Build all packages
rush build
# Build specific package and its dependencies
rush build --to @flowgram.ai/core
# Lint all packages
rush lint
# Fix lint issues
rush lint:fix
# TypeScript type checking
rush ts-check
# Run unit tests
rush test
# Run tests with coverage
rush test:cov
# Build packages in watch mode (for development)
rush build:watch
# E2E tests with Playwright
rush e2e:test
# Update E2E screenshots
rush e2e:update-screenshot
```
### Development Commands for Demos
```bash
# Run docs site with hot reload
rush dev:docs
# Run specific demo apps with hot reload
rush dev:demo-free-layout
rush dev:demo-fixed-layout
rush dev:demo-fixed-layout-simple
rush dev:demo-free-layout-simple
rush dev:demo-nextjs
rush dev:demo-nextjs-antd
```
These commands use `concurrently` to run `rush build:watch` for dependencies alongside the demo's dev server.
### Single Package Development
To work on a single package in isolation:
```bash
cd packages/canvas-engine/core
rushx build # Runs the build script for this package only
rushx test # Runs tests for this package only
rushx ts-check # Type checks this package only
```
## Monorepo Structure
### Core Organization
- **`packages/`** - Production libraries organized by functional area:
- `canvas-engine/` - Canvas rendering and layout systems (core, document, renderer, fixed-layout-core, free-layout-core)
- `node-engine/` - Node data and form management (node, form, form-core)
- `variable-engine/` - Variable scoping and type inference (variable-core, variable-layout, json-schema)
- `runtime/` - Workflow execution engines (interface, js-core, nodejs)
- `plugins/` - Extensibility modules (23+ plugins for features like history, drag, snap, minimap, etc.)
- `client/` - High-level React components (editor, fixed-layout-editor, free-layout-editor, playground-react)
- `materials/` - Pre-built node materials (form-materials, form-antd-materials, fixed-semi-materials, coze-editor, type-editor)
- `common/` - Shared utilities (utils, reactive, command, history, history-storage, i18n)
- **`apps/`** - Demos and documentation:
- `docs/` - Main documentation site
- `demo-*/` - Example applications (free-layout, fixed-layout, nextjs, vite, playground, etc.)
- `create-app/`, `cli/` - CLI tools for scaffolding
- **`e2e/`** - End-to-end test suites (fixed-layout, free-layout)
- **`config/`** - Shared configuration (eslint-config, ts-config)
- **`common/`** - Rush tooling and scripts
### Architectural Layers
FlowGram is architected in distinct layers:
1. **Canvas Engine Layer** (`@flowgram.ai/core`, `@flowgram.ai/document`, `@flowgram.ai/renderer`)
- Core abstractions for canvas rendering, document model, and viewport management
- Supports two layout modes: free-layout (drag-anywhere) and fixed-layout (structured positioning)
- Plugin-based architecture using dependency injection (inversify)
2. **Node Engine Layer** (`@flowgram.ai/node`, `@flowgram.ai/form`, `@flowgram.ai/form-core`)
- Manages node data structures and lifecycle
- Form engine with validation, side effects, linkage, and error capture
- Uses `FormModelV2` (exported as `FormModel` from `@flowgram.ai/editor`)
3. **Variable Engine Layer** (`@flowgram.ai/variable-core`, `@flowgram.ai/json-schema`)
- Provides variable scoping, structure inspection, and type inference
- Manages data flow constraints across workflow nodes
- Scope chain mechanism for variable resolution
4. **Runtime Layer** (`@flowgram.ai/runtime-js`, `@flowgram.ai/runtime-nodejs`, `@flowgram.ai/runtime-interface`)
- Executes workflows in JavaScript/Node.js environments
- Interface package defines runtime contracts
- Separate implementations for browser and server
5. **Client/Editor Layer** (`@flowgram.ai/editor`, `@flowgram.ai/fixed-layout-editor`, `@flowgram.ai/free-layout-editor`)
- High-level React components that integrate all subsystems
- `@flowgram.ai/editor` is the main barrel export for fixed-layout workflows
- Re-exports from core, form, variable, and plugin packages
6. **Plugin Ecosystem**
- 20+ plugins providing features like drag-and-drop, history/undo, snap-to-grid, minimap, auto-layout, etc.
- Plugins are registered via dependency injection containers
- Naming convention: `free-*-plugin` for free-layout, `fixed-*-plugin` for fixed-layout, or generic plugins
## Key Design Patterns
### Dependency Injection
The codebase heavily uses **inversify** for dependency injection. Services are decorated with `@injectable()` and injected via `@inject()`. Container modules organize related services.
### Reactive State Management
Uses a custom reactive system (`@flowgram.ai/reactive`) with React hooks:
- `ReactiveState` and `ReactiveBaseState` for observable state
- `useReactiveState`, `useReadonlyReactiveState`, `useObserve` hooks
- `Tracker` for dependency tracking
### Command Pattern
`@flowgram.ai/command` provides a command/command registry system for undo/redo operations. Re-exported by `@flowgram.ai/core`.
### Plugin Architecture
Plugins extend functionality via:
- `Plugin` interface from `@flowgram.ai/core`
- Registration through container modules
- Lifecycle hooks (`onInit`, `onDestroy`, etc.)
## Testing
- **Unit tests**: Use Vitest, located in `__tests__/` folders or `*.test.ts` files
- **E2E tests**: Use Playwright, located in `e2e/*/tests/` directories
- Run all tests: `rush test`
- Run E2E tests for specific package: `rush e2e:test --to @flowgram.ai/e2e-free-layout`
- Update Playwright snapshots: `rush e2e:update-screenshot`
## Code Quality
### Linting & Type Checking
- ESLint configuration in `config/eslint-config` enforces 2-space indentation, semicolons, and import order
- TypeScript config in `config/ts-config`
- Always run `rush lint:fix` before committing
- Run `rush ts-check` to validate TypeScript across all packages
### Naming Conventions
- **React components/classes**: PascalCase
- **Variables/functions**: camelCase
- **Constants**: SCREAMING_SNAKE_CASE (only for exported config)
- **File names**: kebab-case (e.g., `flow-node-form.tsx`)
### Pre-commit Hooks
- `rush lint-staged` - Runs linting on staged files
- `rush commitlint` - Validates commit message format (conventional commits)
### Dependency Checks
- `rush check-circular-dependency` - Detects circular dependencies
- `rush dep-check` - Validates dependency consistency
## Commit & Pull Request Standards
Follow **conventional commits** format: `type(scope): subject`
Examples from recent history:
- `fix(auto-layout): rankdir top to bottom`
- `feat(landing): hover logo node glowing`
- `docs(variable): optimize variable docs by codex`
Keep commit subjects imperative, ≤72 characters. Reference GitHub issues in PR descriptions.
## Working with Packages
### Adding a New Package
1. Create folder under appropriate category (`packages/<category>/<package-name>`)
2. Add entry to `rush.json` "projects" array with:
- `packageName`: `@flowgram.ai/<package-name>`
- `projectFolder`: relative path
- `versionPolicyName`: typically "publishPolicy" for libraries, "appPolicy" for apps
- `tags`: for categorization
3. Run `rush update` to link the package
### Publishing Workflow
Packages with `versionPolicyName: "publishPolicy"` are publishable to npm. Apps use `"appPolicy"`.
### Inter-package Dependencies
Use `workspace:^x.x.x` protocol in package.json for internal dependencies. Rush will link them locally during development.
## Common Issues
### Build Failures
- Ensure `rush install` was run after pulling changes
- Check Node.js version matches `nodeSupportedVersionRange` in rush.json
- Clear incremental build cache: delete `common/temp` and rebuild
### Type Errors in Editor Packages
The main editor packages (`@flowgram.ai/editor`, `@flowgram.ai/fixed-layout-editor`, `@flowgram.ai/free-layout-editor`) re-export many types. Look for type definitions in their upstream dependencies (@flowgram.ai/form, @flowgram.ai/core, @flowgram.ai/node).
### Plugin Registration
Plugins must be registered in a dependency injection container. Check demo apps for examples of container module setup.
### Rush Command Not Found
Ensure Rush is installed globally: `npm install -g @microsoft/rush`
Or use the install-run script: `node common/scripts/install-run-rush.js <command>`
## Additional Resources
- Documentation: https://flowgram.ai
- Issues: https://github.com/bytedance/flowgram.ai/issues
- Contributing: See CONTRIBUTING.md
+1
View File
@@ -0,0 +1 @@
flowgram.ai
+108
View File
@@ -0,0 +1,108 @@
# Contributing to flowgram.ai
## Quick Start
### Prerequisites
- Node.js 18+ (LTS/Hydrogen recommended)
- pnpm 10.6.5
- Rush 5.150.0
### Installation
1. **Install Node.js 18+**
``` bash
nvm install lts/hydrogen
nvm alias default lts/hydrogen # set default node version
nvm use lts/hydrogen
```
2. **Clone the repository**
``` bash
git clone git@github.com:bytedance/flowgram.ai.git
```
3. **Install required global dependencies**
``` bash
npm i -g pnpm@10.6.5 @microsoft/rush@5.150.0
```
4. **Install project dependencies**
``` bash
rush install
```
5. **Build the project**
``` bash
rush build
```
6. **Run docs or demo**
``` bash
rush dev:docs # docs
rush dev:demo-fixed-layout
rush dev:demo-free-layout
```
After that, you can start to develop projects inside this repository.
## Submitting Changes
1. Create a new branch from `main` using the format:
- `feat/description` for features
- `fix/description` for bug fixes
- `docs/description` for documentation
- `chore/description` for maintenance
2. Write code and tests
- Follow our coding standards
- Add/update tests for changes
- Update documentation if needed
3. Ensure quality
- Run `cd path/to/packageName && npm test` for all tests
- Run `rush lint` for code style
- Run `rush build` to verify build
4. Create Pull Request
- Use the PR template
- Link related issues
- Provide clear description of changes
5. Review Process
- Maintainers will review your PR
- Address review feedback if any
- Changes must pass CI checks
6. Commit Message Format
```
type(scope): subject
body
```
Types: feat, fix, docs, style, refactor, test, chore
## Reporting Bugs
Report bugs via [GitHub Issues](https://github.com/bytedance/flowgram.ai/issues/new/choose). Please include:
- Issue description
- Steps to reproduce
- Expected behavior
- Actual behavior
- Code examples (if applicable)
## Documentation
- Update API documentation for interface changes
- Update README.md if usage is affected
## License
This project is under the [MIT License](http://choosealicense.com/licenses/mit/). By submitting code, you agree to these terms.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+91
View File
@@ -0,0 +1,91 @@
![Image](https://github.com/user-attachments/assets/4f9dfa0e-e600-4d4e-9e73-c919184f7573)
<div align="center">
[![License](https://img.shields.io/github/license/bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/blob/main/LICENSE) [![@flowgram.ai/editor](https://img.shields.io/npm/dm/%40flowgram.ai%2Fcore)](https://www.npmjs.com/package/@flowgram.ai/editor) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bytedance/flowgram.ai) [![juejin](https://img.shields.io/badge/juejin-FFFFFF?logo=juejin&logoColor=%23007FFF)](https://juejin.cn/column/7479814468601315362)
[![](https://trendshift.io/api/badge/repositories/13877)](https://trendshift.io/repositories/13877)
</div>
# FlowGramWorkflow development framework
[English](README.md) | [中文](README_ZH.md) | [Español](README_ES.md) | [Русский](README_RU.md) | [Português](README_PT.md) | [Deutsch](README_DE.md) | [日本語](README_JA.md)
FlowGram is a composable, visual, easy-to-integrate, and extensible workflow development framework & toolkit.
Our goal is to help developers build AI workflow platforms **faster** and **simpler**.
FlowGram comes with a suite of built-in tools for workflow development: flow canvas, node configuration form, variable scope chain, and ready-to-use materials (LLM, Condition, Code Editor etc). Its not a ready-made workflow platform; its the framework and toolkit to build yours.
Learn more at [FlowGram.AI 🌐](https://flowgram.ai)
## 🎬 Demo
<https://github.com/user-attachments/assets/fee87890-ceec-4c07-b659-08afc4dedc26>
Open in [CodeSandbox 🌐](https://codesandbox.io/p/github/louisyoungx/flowgram-demo/main) or [StackBlitz 🌐](https://stackblitz.com/~/github.com/louisyoungx/flowgram-demo)
In this demo, we iterate through a list of cities, fetch real-time weather via HTTP, parse temperatures with a Code node, generate outfit suggestions with an LLM, gate by a Condition, aggregate results across the loop, and finally use an Advisor LLM to pick the most comfortable city before sending the result to the End node.
## 🚀 Quick Start
1. Create a new FlowGram project:
```sh
npx @flowgram.ai/create-app@latest
```
> We recommend choosing the `Free Layout Demo ⭐️` template.
2. Start the project:
```sh
cd demo-free-layout
npm install
npm start
```
3. Open [http://localhost:3000](http://localhost:3000) in your browser.
## ✨ Features
| Feature | Description | Demo |
| -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| [Free Layout Canvas](https://flowgram.ai/examples/free-layout/free-feature-overview.html) | Free layout canvas where nodes can be placed anywhere and connected using free-form lines. | ![Free Layout Demo](./apps/docs/src/public/free-layout/free-layout-demo.gif) |
| [Fixed Layout Canvas](https://flowgram.ai/examples/fixed-layout/fixed-feature-overview.html) | Fixed layout canvas where nodes can be dragged to specified positions, with support for compound nodes like branches and loops. | ![Fixed Layout Demo](./apps/docs/src/public/fixed-layout/fixed-layout-demo.gif) |
| [Form](https://flowgram.ai/examples/node-form/basic.html) | The form engine manages the CRUD operations of node data and provides rendering, validation, side effects, linkage, and error-capturing capabilities, simplifying the development of node configurations. | ![Form](https://github.com/user-attachments/assets/13e9b4cd-e993-4d21-901c-fb6cf106de78) |
| [Variable](https://flowgram.ai/guide/variable/basic.html) | The variable engine supports scope constraints, variable structure inspection, and type inference, making it easy to manage data flow within the workflow. | ![Variable](https://github.com/user-attachments/assets/442006db-25e3-4fb5-972c-7a0545638ff5) |
## 📖 Documentation
You can find the FlowGram documentation [on the website](https://flowgram.ai).
The documentation is divided into several sections:
- [Quick Start](https://flowgram.ai/guide/getting-started/introduction.html)
- [Canvas](https://flowgram.ai/guide/free-layout/load.html)
- [Form](https://flowgram.ai/guide/form/form.html)
- [Variable](https://flowgram.ai/guide/variable/basic.html)
- [Material](https://flowgram.ai/materials/introduction.html)
- [Runtime](https://flowgram.ai/guide/runtime/introduction.html)
- [Advanced Guides](https://flowgram.ai/guide/advanced/zoom-scroll.html)
- [API Reference](https://flowgram.ai/api/index.html)
- [Where to get Support](https://flowgram.ai/guide/contact-us.html)
- [Contributing Guide](https://flowgram.ai/guide/contributing.html)
## 🙌 Contributors
[![FlowGram.AI Contributors](https://contrib.rocks/image?repo=bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/graphs/contributors)
## 🌍 Adoption
- [Coze Studio](https://github.com/coze-dev/coze-studio) is an all-in-one AI agent development tool. Providing the latest large models and tools, various development modes and frameworks, Coze Studio offers the most convenient AI agent development environment, from development to deployment.
- [NNDeploy](https://github.com/NNDeploy/nndeploy) is a workflow-based multi-platform ai deployment tool.
- [Certimate](https://github.com/certimate-go/certimate) is an open-source SSL certificate management tool that helps you automatically apply for and deploy SSL certificates with a visual workflow. It is one of the ACME client options listed in the official documentation of Let's Encrypt.
## 📬 Contact us
- Issues: [Issues](https://github.com/bytedance/flowgram.ai/issues)
- Lark: Scan the QR code below with [Register Feishu](https://www.feishu.cn/en/) to join our FlowGram user group.
<img src="./apps/docs/src/public/lark-group.png" width="200"/>
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`bytedance/flowgram.ai`
- 原始仓库:https://github.com/bytedance/flowgram.ai
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+91
View File
@@ -0,0 +1,91 @@
![Image](https://github.com/user-attachments/assets/4f9dfa0e-e600-4d4e-9e73-c919184f7573)
<div align="center">
[![Lizenz](https://img.shields.io/github/license/bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/blob/main/LICENSE) [![@flowgram.ai/editor](https://img.shields.io/npm/dm/%40flowgram.ai%2Fcore)](https://www.npmjs.com/package/@flowgram.ai/editor) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bytedance/flowgram.ai) [![juejin](https://img.shields.io/badge/juejin-FFFFFF?logo=juejin&logoColor=%23007FFF)](https://juejin.cn/column/7479814468601315362)
[![](https://trendshift.io/api/badge/repositories/13877)](https://trendshift.io/repositories/13877)
</div>
# FlowGram | Workflow-Entwicklungs-Framework
[English](README.md) | [中文](README_ZH.md) | [Español](README_ES.md) | [Русский](README_RU.md) | [Português](README_PT.md) | [Deutsch](README_DE.md) | [日本語](README_JA.md)
FlowGram ist ein zusammensetzbares, visuelles, einfach zu integrierendes und erweiterbares Workflow-Entwicklungs-Framework & Toolkit.
Unser Ziel ist es, Entwicklern zu helfen, KI-Workflow-Plattformen **schneller** und **einfacher** zu erstellen.
FlowGram wird mit einer Reihe von integrierten Werkzeugen für die Workflow-Entwicklung geliefert: eine visuelle Flow-Canvas, Node-Konfigurationsformulare, eine Variablen-Scope-Chain und sofort einsatzbereite Materialien (LLM, Bedingung, Code-Editor usw.). Es ist keine fertige Workflow-Plattform; es ist das Framework und Toolkit, um Ihre zu erstellen.
Erfahren Sie mehr unter [FlowGram.AI 🌐](https://flowgram.ai)
## 🎬 Demo
<https://github.com/user-attachments/assets/fee87890-ceec-4c07-b659-08afc4dedc26>
Öffnen Sie in [CodeSandbox 🌐](https://codesandbox.io/p/github/louisyoungx/flowgram-demo/main) oder [StackBlitz 🌐](https://stackblitz.com/~/github.com/louisyoungx/flowgram-demo)
In dieser Demo durchlaufen wir eine Liste von Städten, rufen das Echtzeit-Wetter über HTTP ab, parsen die Temperaturen mit einem Code-Knoten, generieren Outfit-Vorschläge mit einem LLM, steuern durch eine Bedingung, aggregieren die Ergebnisse über die Schleife und verwenden schließlich einen Berater-LLM, um die komfortabelste Stadt auszuwählen, bevor das Ergebnis an den Endknoten gesendet wird.
## 🚀 Schnellstart
1. Erstellen Sie ein neues FlowGram-Projekt:
```sh
npx @flowgram.ai/create-app@latest
```
> Wir empfehlen, die Vorlage `Free Layout Demo ⭐️` zu wählen.
2. Starten Sie das Projekt:
```sh
cd demo-free-layout
npm install
npm start
```
3. Öffnen Sie [http://localhost:3000](http://localhost:3000) in Ihrem Browser.
## ✨ Funktionen
| Funktion | Beschreibung | Demo |
| -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| [Free Layout Canvas](https://flowgram.ai/examples/free-layout/free-feature-overview.html) | Freie Layout-Canvas, auf der Knoten beliebig platziert und mit Freiformlinien verbunden werden können. | ![Free Layout Demo](./apps/docs/src/public/free-layout/free-layout-demo.gif) |
| [Fixed Layout Canvas](https://flowgram.ai/examples/fixed-layout/fixed-feature-overview.html) | Feste Layout-Canvas, auf der Knoten an bestimmte Positionen gezogen werden können, mit Unterstützung für zusammengesetzte Knoten wie Verzweigungen und Schleifen. | ![Fixed Layout Demo](./apps/docs/src/public/fixed-layout/fixed-layout-demo.gif) |
| [Formular](https://flowgram.ai/examples/node-form/basic.html) | Die Formular-Engine verwaltet CRUD-Operationen von Knotendaten und bietet Rendering-, Validierungs-, Nebeneffekt-, Verknüpfungs- und Fehlererfassungsfunktionen, wodurch die Entwicklung von Knotenkonfigurationen vereinfacht wird. | ![Formular](https://github.com/user-attachments/assets/13e9b4cd-e993-4d21-901c-fb6cf106de78) |
| [Variable](https://flowgram.ai/guide/variable/basic.html) | Die Variablen-Engine unterstützt Bereichsbeschränkungen, Variablenstrukturinspektion und Typinferenz, wodurch der Datenfluss im Workflow einfach verwaltet werden kann. | ![Variable](https://github.com/user-attachments/assets/442006db-25e3-4fb5-972c-7a0545638ff5) |
## 📖 Dokumentation
Sie finden die FlowGram-Dokumentation [auf der Website](https://flowgram.ai).
Die Dokumentation ist in mehrere Abschnitte unterteilt:
- [Schnellstart](https://flowgram.ai/guide/getting-started/introduction.html)
- [Canvas](https://flowgram.ai/guide/free-layout/load.html)
- [Formular](https://flowgram.ai/guide/form/form.html)
- [Variable](https://flowgram.ai/guide/variable/basic.html)
- [Material](https://flowgram.ai/materials/introduction.html)
- [Laufzeit](https://flowgram.ai/guide/runtime/introduction.html)
- [Erweiterte Anleitungen](https://flowgram.ai/guide/advanced/zoom-scroll.html)
- [API-Referenz](https://flowgram.ai/api/index.html)
- [Wo Sie Unterstützung erhalten](https://flowgram.ai/guide/contact-us.html)
- [Leitfaden für Beiträge](https://flowgram.ai/guide/contributing.html)
## 🙌 Mitwirkende
[![FlowGram.AI-Mitwirkende](https://contrib.rocks/image?repo=bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/graphs/contributors)
## 🌍 Einführung
- [Coze Studio](https://github.com/coze-dev/coze-studio) ist ein All-in-One-KI-Agenten-Entwicklungstool. Coze Studio bietet die neuesten großen Modelle und Werkzeuge, verschiedene Entwicklungsmodi und Frameworks und bietet die bequemste KI-Agenten-Entwicklungsumgebung, von der Entwicklung bis zur Bereitstellung.
- [NNDeploy](https://github.com/NNDeploy/nndeploy) ist ein Workflow-basiertes Multi-Plattform-KI-Bereitstellungstool.
- [Certimate](https://github.com/certimate-go/certimate) ist ein Open-Source-SSL-Zertifikatsverwaltungstool, mit dem Sie SSL-Zertifikate automatisch mit einem visuellen Workflow beantragen und bereitstellen können. Es ist eine der ACME-Client-Optionen, die in der offiziellen Dokumentation von Let's Encrypt aufgeführt sind.
## 📬 Kontaktieren Sie uns
- Probleme: [Probleme](https://github.com/bytedance/flowgram.ai/issues)
- Lark: Scannen Sie den QR-Code unten mit [Register Feishu](https://www.feishu.cn/en/), um unserer FlowGram-Benutzergruppe beizutreten.
<img src="./apps/docs/src/public/lark-group.png" width="200"/>
+91
View File
@@ -0,0 +1,91 @@
![Imagen](https://github.com/user-attachments/assets/4f9dfa0e-e600-4d4e-9e73-c919184f7573)
<div align="center">
[![Licencia](https://img.shields.io/github/license/bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/blob/main/LICENSE) [![@flowgram.ai/editor](https://img.shields.io/npm/dm/%40flowgram.ai%2Fcore)](https://www.npmjs.com/package/@flowgram.ai/editor) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bytedance/flowgram.ai) [![juejin](https://img.shields.io/badge/juejin-FFFFFF?logo=juejin&logoColor=%23007FFF)](https://juejin.cn/column/7479814468601315362)
[![](https://trendshift.io/api/badge/repositories/13877)](https://trendshift.io/repositories/13877)
</div>
# FlowGram | Marco de desarrollo de flujos de trabajo
[English](README.md) | [中文](README_ZH.md) | [Español](README_ES.md) | [Русский](README_RU.md) | [Português](README_PT.md) | [Deutsch](README_DE.md) | [日本語](README_JA.md)
FlowGram es un marco y conjunto de herramientas de desarrollo de flujos de trabajo componible, visual, fácil de integrar y extensible.
Nuestro objetivo es ayudar a los desarrolladores a crear plataformas de flujo de trabajo de IA de forma **más rápida** y **sencilla**.
FlowGram viene con un conjunto de herramientas integradas para el desarrollo de flujos de trabajo: un lienzo de flujo visual, formularios de configuración de nodos, una cadena de alcance de variables y materiales listos para usar (LLM, Condición, Editor de código, etc.). No es una plataforma de flujo de trabajo ya hecha; es el marco y el conjunto de herramientas para crear la suya.
Obtenga más información en [FlowGram.AI 🌐](https://flowgram.ai)
## 🎬 Demostración
<https://github.com/user-attachments/assets/fee87890-ceec-4c07-b659-08afc4dedc26>
Abra en [CodeSandbox 🌐](https://codesandbox.io/p/github/louisyoungx/flowgram-demo/main) o [StackBlitz 🌐](https://stackblitz.com/~/github.com/louisyoungx/flowgram-demo)
En esta demostración, iteramos a través de una lista de ciudades, obtenemos el clima en tiempo real a través de HTTP, analizamos las temperaturas con un nodo de código, generamos sugerencias de atuendos con un LLM, controlamos mediante una condición, agregamos los resultados a lo largo del bucle y, finalmente, usamos un LLM asesor para elegir la ciudad más cómoda antes de enviar el resultado al nodo final.
## 🚀 Inicio rápido
1. Cree un nuevo proyecto de FlowGram:
```sh
npx @flowgram.ai/create-app@latest
```
> Le recomendamos que elija la plantilla `Free Layout Demo ⭐️`.
2. Inicie el proyecto:
```sh
cd demo-free-layout
npm install
npm start
```
3. Abra [http://localhost:3000](http://localhost:3000) en su navegador.
## ✨ Características
| Característica | Descripción | Demostración |
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
| [Lienzo de diseño libre](https://flowgram.ai/examples/free-layout/free-feature-overview.html) | Lienzo de diseño libre donde los nodos se pueden colocar en cualquier lugar y conectar mediante líneas de forma libre. | ![Demostración de diseño libre](./apps/docs/src/public/free-layout/free-layout-demo.gif) |
| [Lienzo de diseño fijo](https://flowgram.ai/examples/fixed-layout/fixed-feature-overview.html) | Lienzo de diseño fijo donde los nodos se pueden arrastrar a posiciones específicas, con soporte para nodos compuestos como ramas y bucles. | ![Demostración de diseño fijo](./apps/docs/src/public/fixed-layout/fixed-layout-demo.gif) |
| [Formulario](https://flowgram.ai/examples/node-form/basic.html) | El motor de formularios gestiona las operaciones CRUD de datos de nodos y proporciona capacidades de renderizado, validación, efectos secundarios, vinculación y captura de errores, simplificando el desarrollo de configuraciones de nodos. | ![Formulario](https://github.com/user-attachments/assets/13e9b4cd-e993-4d21-901c-fb6cf106de78) |
| [Variable](https://flowgram.ai/guide/variable/basic.html) | El motor de variables admite restricciones de ámbito, inspección de estructura de variables e inferencia de tipos, facilitando la gestión del flujo de datos dentro del flujo de trabajo. | ![Variable](https://github.com/user-attachments/assets/442006db-25e3-4fb5-972c-7a0545638ff5) |
## 📖 Documentación
Puede encontrar la documentación de FlowGram [en el sitio web](https://flowgram.ai).
La documentación se divide en varias secciones:
- [Inicio rápido](https://flowgram.ai/guide/getting-started/introduction.html)
- [Lienzo](https://flowgram.ai/guide/free-layout/load.html)
- [Formulario](https://flowgram.ai/guide/form/form.html)
- [Variable](https://flowgram.ai/guide/variable/basic.html)
- [Material](https://flowgram.ai/materials/introduction.html)
- [Tiempo de ejecución](https://flowgram.ai/guide/runtime/introduction.html)
- [Guías avanzadas](https://flowgram.ai/guide/advanced/zoom-scroll.html)
- [Referencia de la API](https://flowgram.ai/api/index.html)
- [Dónde obtener soporte](https://flowgram.ai/guide/contact-us.html)
- [Guía de contribución](https://flowgram.ai/guide/contributing.html)
## 🙌 Colaboradores
[![Colaboradores de FlowGram.AI](https://contrib.rocks/image?repo=bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/graphs/contributors)
## 🌍 Adopción
- [Coze Studio](https://github.com/coze-dev/coze-studio) es una herramienta de desarrollo de agentes de IA todo en uno. Coze Studio, que proporciona los últimos modelos y herramientas grandes, varios modos y marcos de desarrollo, ofrece el entorno de desarrollo de agentes de IA más conveniente, desde el desarrollo hasta la implementación.
- [NNDeploy](https://github.com/NNDeploy/nndeploy) es una herramienta de implementación de IA multiplataforma basada en flujos de trabajo.
- [Certimate](https://github.com/certimate-go/certimate) es una herramienta de gestión de certificados SSL de código abierto que le ayuda a solicitar e implementar automáticamente certificados SSL con un flujo de trabajo visual. Es una de las opciones de cliente ACME que se enumeran en la documentación oficial de Let's Encrypt.
## 📬 Contáctenos
- Problemas: [Problemas](https://github.com/bytedance/flowgram.ai/issues)
- Lark: Escanee el código QR a continuación con [Registrar Feishu](https://www.feishu.cn/en/) para unirse a nuestro grupo de usuarios de FlowGram.
<img src="./apps/docs/src/public/lark-group.png" width="200"/>
+91
View File
@@ -0,0 +1,91 @@
![画像](https://github.com/user-attachments/assets/4f9dfa0e-e600-4d4e-9e73-c919184f7573)
<div align="center">
[![ライセンス](https://img.shields.io/github/license/bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/blob/main/LICENSE) [![@flowgram.ai/editor](https://img.shields.io/npm/dm/%40flowgram.ai%2Fcore)](https://www.npmjs.com/package/@flowgram.ai/editor) [![DeepWikiに聞く](https://deepwiki.com/badge.svg)](https://deepwiki.com/bytedance/flowgram.ai) [![juejin](https://img.shields.io/badge/juejin-FFFFFF?logo=juejin&logoColor=%23007FFF)](https://juejin.cn/column/7479814468601315362)
[![](https://trendshift.io/api/badge/repositories/13877)](https://trendshift.io/repositories/13877)
</div>
# FlowGram|ワークフロー開発フレームワーク
[English](README.md) | [中文](README_ZH.md) | [Español](README_ES.md) | [Русский](README_RU.md) | [Português](README_PT.md) | [Deutsch](README_DE.md) | [日本語](README_JA.md)
FlowGramは、構成可能で、視覚的で、統合しやすく、拡張可能なワークフロー開発フレームワーク&ツールキットです。
私たちの目標は、開発者がAIワークフロープラットフォームを**より速く**、**よりシンプルに**構築できるよう支援することです。
FlowGramには、ワークフロー開発用の組み込みツール一式が付属しています。視覚的なフローキャンバス、ノード構成フォーム、変数スコープチェーン、すぐに使えるマテリアル(LLM、条件、コードエディターなど)です。これは既製のワークフロープラットフォームではありません。あなたのワークフロープラットフォームを構築するためのフレームワークとツールキットです。
詳細は[FlowGram.AI 🌐](https://flowgram.ai)をご覧ください。
## 🎬 デモ
<https://github.com/user-attachments/assets/fee87890-ceec-4c07-b659-08afc4dedc26>
[CodeSandbox 🌐](https://codesandbox.io/p/github/louisyoungx/flowgram-demo/main)または[StackBlitz 🌐](https://stackblitz.com/~/github.com/louisyoungx/flowgram-demo)で開く
このデモでは、都市のリストを反復処理し、HTTP経由でリアルタイムの天気を取得し、コードノードで気温を解析し、LLMで服装の提案を生成し、条件でゲートし、ループ全体で結果を集計し、最後にアドバイザーLLMを使用して最も快適な都市を選択してから、結果を終了ノードに送信します。
## 🚀 クイックスタート
1. 新しいFlowGramプロジェクトを作成します:
```sh
npx @flowgram.ai/create-app@latest
```
> `Free Layout Demo ⭐️` テンプレートを選択することをお勧めします。
2. プロジェクトを開始します:
```sh
cd demo-free-layout
npm install
npm start
```
3. ブラウザで[http://localhost:3000](http://localhost:3000)を開きます。
## ✨ 機能
| 機能 | 説明 | デモ |
| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| [フリーレイアウトキャンバス](https://flowgram.ai/examples/free-layout/free-feature-overview.html) | ノードをどこにでも配置し、自由形式の線で接続できるフリーレイアウトキャンバス。 | ![フリーレイアウトデモ](./apps/docs/src/public/free-layout/free-layout-demo.gif) |
| [固定レイアウトキャンバス](https://flowgram.ai/examples/fixed-layout/fixed-feature-overview.html) | 分岐やループなどの複合ノードをサポートし、ノードを指定した位置にドラッグできる固定レイアウトキャンバス。 | ![固定レイアウトデモ](./apps/docs/src/public/fixed-layout/fixed-layout-demo.gif) |
| [フォーム](https://flowgram.ai/examples/node-form/basic.html) | フォームエンジンはノードデータのCRUD操作を管理し、レンダリング、検証、副作用、連動、エラー処理機能を提供し、ノード設定の開発を簡素化します。 | ![フォーム](https://github.com/user-attachments/assets/13e9b4cd-e993-4d21-901c-fb6cf106de78) |
| [変数](https://flowgram.ai/guide/variable/basic.html) | 変数エンジンはスコープ制約、変数構造検査、型推論をサポートし、ワークフロー内のデータフローの管理を容易にします。 | ![変数](https://github.com/user-attachments/assets/442006db-25e3-4fb5-972c-7a0545638ff5) |
## 📖 ドキュメント
FlowGramのドキュメントは[ウェブサイト](https://flowgram.ai)でご覧いただけます。
ドキュメントはいくつかのセクションに分かれています。
- [クイックスタート](https://flowgram.ai/guide/getting-started/introduction.html)
- [キャンバス](https://flowgram.ai/guide/free-layout/load.html)
- [フォーム](https://flowgram.ai/guide/form/form.html)
- [変数](https://flowgram.ai/guide/variable/basic.html)
- [マテリアル](https://flowgram.ai/materials/introduction.html)
- [ランタイム](https://flowgram.ai/guide/runtime/introduction.html)
- [高度なガイド](https://flowgram.ai/guide/advanced/zoom-scroll.html)
- [APIリファレンス](https://flowgram.ai/api/index.html)
- [サポートの入手先](https://flowgram.ai/guide/contact-us.html)
- [貢献ガイド](https://flowgram.ai/guide/contributing.html)
## 🙌 貢献者
[![FlowGram.AI貢献者](https://contrib.rocks/image?repo=bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/graphs/contributors)
## 🌍 採用
- [Coze Studio](https://github.com/coze-dev/coze-studio)は、オールインワンのAIエージェント開発ツールです。最新の主要モデルとツール、さまざまな開発モードとフレームワークを提供するCoze Studioは、開発から展開まで、最も便利なAIエージェント開発環境を提供します。
- [NNDeploy](https://github.com/NNDeploy/nndeploy)は、ワークフローベースのマルチプラットフォームAI展開ツールです。
- [Certimate](https://github.com/certimate-go/certimate)は、視覚的なワークフローでSSL証明書を自動的に申請および展開するのに役立つオープンソースのSSL証明書管理ツールです。これは、Let's Encryptの公式ドキュメントに記載されているACMEクライアントオプションの1つです。
## 📬 お問い合わせ
- 問題:[問題](https://github.com/bytedance/flowgram.ai/issues)
- Lark[Register Feishu](https://www.feishu.cn/en/)で以下のQRコードをスキャンして、FlowGramユーザーグループに参加してください。
<img src="./apps/docs/src/public/lark-group.png" width="200"/>
+91
View File
@@ -0,0 +1,91 @@
![Imagem](https://github.com/user-attachments/assets/4f9dfa0e-e600-4d4e-9e73-c919184f7573)
<div align="center">
[![Licença](https://img.shields.io/github/license/bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/blob/main/LICENSE) [![@flowgram.ai/editor](https://img.shields.io/npm/dm/%40flowgram.ai%2Fcore)](https://www.npmjs.com/package/@flowgram.ai/editor) [![Pergunte ao DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bytedance/flowgram.ai) [![juejin](https://img.shields.io/badge/juejin-FFFFFF?logo=juejin&logoColor=%23007FFF)](https://juejin.cn/column/7479814468601315362)
[![](https://trendshift.io/api/badge/repositories/13877)](https://trendshift.io/repositories/13877)
</div>
# FlowGram | Estrutura de desenvolvimento de fluxo de trabalho
[English](README.md) | [中文](README_ZH.md) | [Español](README_ES.md) | [Русский](README_RU.md) | [Português](README_PT.md) | [Deutsch](README_DE.md) | [日本語](README_JA.md)
FlowGram é uma estrutura e kit de ferramentas de desenvolvimento de fluxo de trabalho componível, visual, fácil de integrar e extensível.
Nosso objetivo é ajudar os desenvolvedores a criar plataformas de fluxo de trabalho de IA de forma **mais rápida** e **simples**.
O FlowGram vem com um conjunto de ferramentas integradas para o desenvolvimento de fluxo de trabalho: uma tela de fluxo visual, formulários de configuração de nós, uma cadeia de escopo de variáveis e materiais prontos para uso (LLM, Condição, Editor de Código, etc.). Não é uma plataforma de fluxo de trabalho pronta; é a estrutura e o kit de ferramentas para construir a sua.
Saiba mais em [FlowGram.AI 🌐](https://flowgram.ai)
## 🎬 Demonstração
<https://github.com/user-attachments/assets/fee87890-ceec-4c07-b659-08afc4dedc26>
Abra no [CodeSandbox 🌐](https://codesandbox.io/p/github/louisyoungx/flowgram-demo/main) ou [StackBlitz 🌐](https://stackblitz.com/~/github.com/louisyoungx/flowgram-demo)
Nesta demonstração, iteramos por uma lista de cidades, buscamos o clima em tempo real via HTTP, analisamos as temperaturas com um nó de Código, geramos sugestões de roupas com um LLM, controlamos por uma Condição, agregamos os resultados ao longo do loop e, finalmente, usamos um LLM Conselheiro para escolher a cidade mais confortável antes de enviar o resultado para o nó Final.
## 🚀 Início rápido
1. Crie um novo projeto FlowGram:
```sh
npx @flowgram.ai/create-app@latest
```
> Recomendamos escolher o template `Free Layout Demo ⭐️`.
2. Inicie o projeto:
```sh
cd demo-free-layout
npm install
npm start
```
3. Abra [http://localhost:3000](http://localhost:3000) no seu navegador.
## ✨ Recursos
| Recurso | Descrição | Demonstração |
| -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
| [Tela de Layout Livre](https://flowgram.ai/examples/free-layout/free-feature-overview.html) | Tela de layout livre onde os nós podem ser colocados em qualquer lugar e conectados usando linhas de forma livre. | ![Demonstração de Layout Livre](./apps/docs/src/public/free-layout/free-layout-demo.gif) |
| [Tela de Layout Fixo](https://flowgram.ai/examples/fixed-layout/fixed-feature-overview.html) | Tela de layout fixo onde os nós podem ser arrastados para posições especificadas, com suporte para nós compostos como ramificações e loops. | ![Demonstração de Layout Fixo](./apps/docs/src/public/fixed-layout/fixed-layout-demo.gif) |
| [Formulário](https://flowgram.ai/examples/node-form/basic.html) | O mecanismo de formulário gerencia as operações CRUD de dados do nó e fornece recursos de renderização, validação, efeitos colaterais, vinculação e captura de erros, simplificando o desenvolvimento de configurações de nó. | ![Formulário](https://github.com/user-attachments/assets/13e9b4cd-e993-4d21-901c-fb6cf106de78) |
| [Variável](https://flowgram.ai/guide/variable/basic.html) | O mecanismo de variáveis suporta restrições de escopo, inspeção de estrutura de variáveis e inferência de tipos, facilitando o gerenciamento do fluxo de dados dentro do fluxo de trabalho. | ![Variável](https://github.com/user-attachments/assets/442006db-25e3-4fb5-972c-7a0545638ff5) |
## 📖 Documentação
Você pode encontrar a documentação do FlowGram [no site](https://flowgram.ai).
A documentação está dividida em várias seções:
- [Início Rápido](https://flowgram.ai/guide/getting-started/introduction.html)
- [Tela](https://flowgram.ai/guide/free-layout/load.html)
- [Formulário](https://flowgram.ai/guide/form/form.html)
- [Variável](https://flowgram.ai/guide/variable/basic.html)
- [Material](https://flowgram.ai/materials/introduction.html)
- [Tempo de Execução](https://flowgram.ai/guide/runtime/introduction.html)
- [Guias Avançados](https://flowgram.ai/guide/advanced/zoom-scroll.html)
- [Referência da API](https://flowgram.ai/api/index.html)
- [Onde obter suporte](https://flowgram.ai/guide/contact-us.html)
- [Guia de contribuição](https://flowgram.ai/guide/contributing.html)
## 🙌 Colaboradores
[![Colaboradores do FlowGram.AI](https://contrib.rocks/image?repo=bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/graphs/contributors)
## 🌍 Adoção
- [Coze Studio](https://github.com/coze-dev/coze-studio) é uma ferramenta de desenvolvimento de agente de IA tudo-em-um. Fornecendo os modelos e ferramentas mais recentes, vários modos e estruturas de desenvolvimento, o Coze Studio oferece o ambiente de desenvolvimento de agente de IA mais conveniente, do desenvolvimento à implantação.
- [NNDeploy](https://github.com/NNDeploy/nndeploy) é uma ferramenta de implantação de IA multiplataforma baseada em fluxo de trabalho.
- [Certimate](https://github.com/certimate-go/certimate) é uma ferramenta de gerenciamento de certificados SSL de código aberto que ajuda você a solicitar e implantar certificados SSL automaticamente com um fluxo de trabalho visual. É uma das opções de cliente ACME listadas na documentação oficial do Let's Encrypt.
## 📬 Contate-nos
- Problemas: [Problemas](https://github.com/bytedance/flowgram.ai/issues)
- Lark: Digitalize o código QR abaixo com [Registrar Feishu](https://www.feishu.cn/en/) para se juntar ao nosso grupo de usuários FlowGram.
<img src="./apps/docs/src/public/lark-group.png" width="200"/>
+91
View File
@@ -0,0 +1,91 @@
![Изображение](https://github.com/user-attachments/assets/4f9dfa0e-e600-4d4e-9e73-c919184f7573)
<div align="center">
[![Лицензия](https://img.shields.io/github/license/bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/blob/main/LICENSE) [![@flowgram.ai/editor](https://img.shields.io/npm/dm/%40flowgram.ai%2Fcore)](https://www.npmjs.com/package/@flowgram.ai/editor) [![Спросить DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bytedance/flowgram.ai) [![juejin](https://img.shields.io/badge/juejin-FFFFFF?logo=juejin&logoColor=%23007FFF)](https://juejin.cn/column/7479814468601315362)
[![](https://trendshift.io/api/badge/repositories/13877)](https://trendshift.io/repositories/13877)
</div>
# FlowGram | Фреймворк для разработки рабочих процессов
[English](README.md) | [中文](README_ZH.md) | [Español](README_ES.md) | [Русский](README_RU.md) | [Português](README_PT.md) | [Deutsch](README_DE.md) | [日本語](README_JA.md)
FlowGram — это компонуемый, визуальный, легко интегрируемый и расширяемый фреймворк и набор инструментов для разработки рабочих процессов.
Наша цель — помочь разработчикам создавать платформы для рабочих процессов ИИ **быстрее** и **проще**.
FlowGram поставляется со встроенным набором инструментов для разработки рабочих процессов: визуальным холстом потока, формами конфигурации узлов, цепочкой области видимости переменных и готовыми к использованию материалами (LLM, Условие, Редактор кода и т. д.). Это не готовая платформа для рабочих процессов; это фреймворк и набор инструментов для создания вашей собственной.
Узнайте больше на [FlowGram.AI 🌐](https://flowgram.ai)
## 🎬 Демо
<https://github.com/user-attachments/assets/fee87890-ceec-4c07-b659-08afc4dedc26>
Откройте в [CodeSandbox 🌐](https://codesandbox.io/p/github/louisyoungx/flowgram-demo/main) или [StackBlitz 🌐](https://stackblitz.com/~/github.com/louisyoungx/flowgram-demo)
В этом демо мы перебираем список городов, получаем погоду в реальном времени по HTTP, анализируем температуру с помощью узла «Код», генерируем предложения по одежде с помощью LLM, управляем с помощью «Условия», агрегируем результаты по циклу и, наконец, используем LLM-советника, чтобы выбрать самый комфортный город, прежде чем отправить результат в конечный узел.
## 🚀 Быстрый старт
1. Создайте новый проект FlowGram:
```sh
npx @flowgram.ai/create-app@latest
```
> Мы рекомендуем выбрать шаблон `Free Layout Demo ⭐️`.
2. Запустите проект:
```sh
cd demo-free-layout
npm install
npm start
```
3. Откройте [http://localhost:3000](http://localhost:3000) в вашем браузере.
## ✨ Особенности
| Особенность | Описание | Демо |
| ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| [Холст со свободной компоновкой](https://flowgram.ai/examples/free-layout/free-feature-overview.html) | Холст со свободной компоновкой, где узлы можно размещать где угодно и соединять линиями произвольной формы. | ![Демо со свободной компоновкой](./apps/docs/src/public/free-layout/free-layout-demo.gif) |
| [Холст с фиксированной компоновкой](https://flowgram.ai/examples/fixed-layout/fixed-feature-overview.html) | Холст с фиксированной компоновкой, где узлы можно перетаскивать в указанные позиции, с поддержкой составных узлов, таких как ветви и циклы. | ![Демо с фиксированной компоновкой](./apps/docs/src/public/fixed-layout/fixed-layout-demo.gif) |
| [Форма](https://flowgram.ai/examples/node-form/basic.html) | Движок форм управляет операциями CRUD данных узлов и предоставляет возможности рендеринга, валидации, побочных эффектов, связывания и обработки ошибок, упрощая разработку конфигураций узлов. | ![Форма](https://github.com/user-attachments/assets/13e9b4cd-e993-4d21-901c-fb6cf106de78) |
| [Переменная](https://flowgram.ai/guide/variable/basic.html) | Движок переменных поддерживает ограничения области видимости, инспекцию структуры переменных и вывод типов, облегчая управление потоком данных в рабочем процессе. | ![Переменная](https://github.com/user-attachments/assets/442006db-25e3-4fb5-972c-7a0545638ff5) |
## 📖 Документация
Вы можете найти документацию FlowGram [на веб-сайте](https://flowgram.ai).
Документация разделена на несколько разделов:
- [Быстрый старт](https://flowgram.ai/guide/getting-started/introduction.html)
- [Холст](https://flowgram.ai/guide/free-layout/load.html)
- [Форма](https://flowgram.ai/guide/form/form.html)
- [Переменная](https://flowgram.ai/guide/variable/basic.html)
- [Материал](https://flowgram.ai/materials/introduction.html)
- [Среда выполнения](https://flowgram.ai/guide/runtime/introduction.html)
- [Расширенные руководства](https://flowgram.ai/guide/advanced/zoom-scroll.html)
- [Справочник по API](https://flowgram.ai/api/index.html)
- [Где получить поддержку](https://flowgram.ai/guide/contact-us.html)
- [Руководство по вкладу](https://flowgram.ai/guide/contributing.html)
## 🙌 Участники
[![Участники FlowGram.AI](https://contrib.rocks/image?repo=bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/graphs/contributors)
## 🌍 Внедрение
- [Coze Studio](https://github.com/coze-dev/coze-studio) — это универсальный инструмент для разработки агентов ИИ. Предоставляя новейшие большие модели и инструменты, различные режимы и фреймворки разработки, Coze Studio предлагает наиболее удобную среду разработки агентов ИИ, от разработки до развертывания.
- [NNDeploy](https://github.com/NNDeploy/nndeploy) — это мультиплатформенный инструмент развертывания ИИ на основе рабочих процессов.
- [Certimate](https://github.com/certimate-go/certimate) — это инструмент управления SSL-сертификатами с открытым исходным кодом, который помогает автоматически подавать заявки и развертывать SSL-сертификаты с помощью визуального рабочего процесса. Это один из вариантов клиента ACME, перечисленных в официальной документации Let's Encrypt.
## 📬 Свяжитесь с нами
- Проблемы: [Проблемы](https://github.com/bytedance/flowgram.ai/issues)
- Lark: отсканируйте QR-код ниже с помощью [Register Feishu](https://www.feishu.cn/en/), чтобы присоединиться к нашей группе пользователей FlowGram.
<img src="./apps/docs/src/public/lark-group.png" width="200"/>
+91
View File
@@ -0,0 +1,91 @@
![Image](https://github.com/user-attachments/assets/4f9dfa0e-e600-4d4e-9e73-c919184f7573)
<div align="center">
[![License](https://img.shields.io/github/license/bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/blob/main/LICENSE) [![@flowgram.ai/editor](https://img.shields.io/npm/dm/%40flowgram.ai%2Fcore)](https://www.npmjs.com/package/@flowgram.ai/editor) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bytedance/flowgram.ai) [![juejin](https://img.shields.io/badge/juejin-FFFFFF?logo=juejin&logoColor=%23007FFF)](https://juejin.cn/column/7479814468601315362)
[![](https://trendshift.io/api/badge/repositories/13877)](https://trendshift.io/repositories/13877)
</div>
# FlowGram.AI|工作流开发框架
[English](README.md) | [中文](README_ZH.md) | [Español](README_ES.md) | [Русский](README_RU.md) | [Português](README_PT.md) | [Deutsch](README_DE.md) | [日本語](README_JA.md)
FlowGram 是一个可组合、可视化、易于集成且可扩展的工作流开发框架与工具集。
我们的目标是帮助开发者以更快、更简单的方式搭建 AI 工作流平台。
FlowGram 内置开箱开箱即用的工作流开发能力:可视化流程画布、节点配置表单、变量作用域链,以及开箱即用的物料(LLM、条件、代码编辑器等)。这并非一个现成的工作流平台,而是帮助你构建平台的框架与工具。
了解更多 [FlowGram.AI 🌐](https://flowgram.ai)
## 🎬 演示
<https://github.com/user-attachments/assets/fee87890-ceec-4c07-b659-08afc4dedc26>
在 [CodeSandbox 🌐](https://codesandbox.io/p/github/louisyoungx/flowgram-demo/main) 或 [StackBlitz 🌐](https://stackblitz.com/~/github.com/louisyoungx/flowgram-demo) 中打开
在该演示中,我们遍历一组城市,通过 HTTP 获取实时天气,用 Code 节点解析温度,借助 LLM 生成穿搭建议,经由 Condition 进行筛选,在循环中汇总结果,最后使用 Advisor LLM 选出最舒适的城市,并将结果发送至 End 节点。
## 🚀 快速上手
1. 创建一个新的 FlowGram 项目:
```sh
npx @flowgram.ai/create-app@latest
```
> 我们推荐选择 `Free Layout Demo ⭐️` 模板。
2. 启动项目:
```sh
cd demo-free-layout
npm install
npm start
```
3. 在浏览器中打开 [http://localhost:3000](http://localhost:3000)。
## ✨ 特性
| 特性 | 说明 | 演示 |
| -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| [Free Layout Canvas](https://flowgram.ai/examples/free-layout/free-feature-overview.html) | 自由布局画布,节点可任意摆放,可在节点间创建边进行链接。 | ![Free Layout Demo](./apps/docs/src/public/free-layout/free-layout-demo.gif) |
| [Fixed Layout Canvas](https://flowgram.ai/examples/fixed-layout/fixed-feature-overview.html) | 固定布局画布,节点可拖拽至指定位置,支持复合节点(如分支与循环)。 | ![Fixed Layout Demo](./apps/docs/src/public/fixed-layout/fixed-layout-demo.gif) |
| [Form](https://flowgram.ai/examples/node-form/basic.html) | 表单引擎管理节点数据的增删改查操作,并提供渲染、验证、副作用、联动和错误捕获等功能,简化节点配置的开发。 | ![Form](https://github.com/user-attachments/assets/13e9b4cd-e993-4d21-901c-fb6cf106de78) |
| [Variable](https://flowgram.ai/guide/variable/basic.html) | 变量引擎支持作用域约束、变量结构检查和类型推断等功能,便于管理工作流中的数据流。 | ![Variable](https://github.com/user-attachments/assets/442006db-25e3-4fb5-972c-7a0545638ff5) |
## 📖 文档
你可以在官网查阅完整文档:[FlowGram 文档](https://flowgram.ai)。
文档分为以下章节:
- [快速入门](https://flowgram.ai/guide/getting-started/introduction.html)
- [自由画布](https://flowgram.ai/guide/free-layout/load.html)
- [固定画布](https://flowgram.ai/guide/fixed-layout/load.html)
- [表单](https://flowgram.ai/guide/form/form.html)
- [变量](https://flowgram.ai/guide/variable/basic.html)
- [素材](https://flowgram.ai/materials/introduction.html)
- [运行时](https://flowgram.ai/guide/runtime/introduction.html)
- [进阶指南](https://flowgram.ai/guide/advanced/zoom-scroll.html)
- [API 参考](https://flowgram.ai/api/index.html)
- [获取支持](https://flowgram.ai/guide/contact-us.html)
- [贡献指南](https://flowgram.ai/guide/contributing.html)
## 🙌 贡献者
[![FlowGram.AI Contributors](https://contrib.rocks/image?repo=bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/graphs/contributors)
## 🌍 被这些项目采用
- [Coze Studio](https://github.com/coze-dev/coze-studio) 是一体化的 AI Agent 开发工具,提供最新的大模型与工具、多样的开发模式与框架,并在从开发到部署的全流程中,提供最便捷的 Agent 开发体验。
- [NNDeploy](https://github.com/NNDeploy/nndeploy) 是一个基于工作流的多平台 AI 部署工具。
- [Certimate](https://github.com/certimate-go/certimate) 是开源的 SSL 证书管理工具,借助可视化工作流帮助你自动申请与部署证书;它也是官方文档列出的 Let's Encrypt ACME 客户端选项之一。
## 📬 联系我们
- 问题反馈: [Issues](https://github.com/bytedance/flowgram.ai/issues)
- 飞书:使用 [Register Feishu](https://www.feishu.cn/en/) 扫码下方二维码,加入 FlowGram 用户群。
<img src="./apps/docs/src/public/lark-group.png" width="200"/>
+1
View File
@@ -0,0 +1 @@
.download
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env node
import '../dist/index.js';
+23
View File
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { defineFlatConfig } from '@flowgram.ai/eslint-config';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineFlatConfig({
preset: 'web',
packageRoot: __dirname,
rules: {
'no-console': 'off',
},
settings: {
react: {
version: '18',
},
},
});
+45
View File
@@ -0,0 +1,45 @@
{
"name": "@flowgram.ai/cli",
"version": "0.1.8",
"description": "A CLI tool to create demo projects or sync materials",
"repository": "https://github.com/bytedance/flowgram.ai",
"bin": {
"flowgram-cli": "./bin/index.js"
},
"type": "module",
"files": [
"bin",
"src",
"dist"
],
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts --out-dir dist",
"watch": "tsup src/index.ts --format esm,cjs --out-dir dist --watch",
"start": "node bin/index.js",
"lint": "eslint ./src --cache",
"lint:fix": "eslint ./src --fix"
},
"dependencies": {
"fs-extra": "^9.1.0",
"commander": "^11.0.0",
"chalk": "^5.3.0",
"tar": "7.4.3",
"inquirer": "^12.9.4",
"ignore": "~7.0.5"
},
"devDependencies": {
"@flowgram.ai/eslint-config": "workspace:*",
"@types/download": "8.0.5",
"@types/fs-extra": "11.0.4",
"@types/node": "^18",
"@types/inquirer": "^9.0.9",
"tsup": "^8.0.1",
"eslint": "^9.0.0",
"@typescript-eslint/parser": "^8.0.0",
"typescript": "^5.8.3"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}
+162
View File
@@ -0,0 +1,162 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import path from 'path';
import https from 'https';
import { execSync } from 'child_process';
import * as tar from 'tar';
import inquirer from 'inquirer';
import fs from 'fs-extra';
import chalk from 'chalk';
const updateFlowGramVersions = (dependencies: any[], latestVersion: string) => {
for (const packageName in dependencies) {
if (packageName.startsWith('@flowgram.ai')) {
dependencies[packageName] = latestVersion;
}
}
};
// 使用 https 下载文件
function downloadFile(url: string, dest: string): Promise<void> {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(dest);
https
.get(url, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`Download failed: ${response.statusCode}`));
return;
}
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
})
.on('error', (err) => {
fs.unlink(dest, () => reject(err));
});
file.on('error', (err) => {
fs.unlink(dest, () => reject(err));
});
});
}
export const createApp = async (projectName?: string) => {
console.log(chalk.green('Welcome to @flowgram.ai/create-app CLI!'));
const latest = execSync('npm view @flowgram.ai/demo-fixed-layout version --tag=latest latest')
.toString()
.trim();
let folderName = '';
if (!projectName) {
// 询问用户选择 demo 项目
const { repo } = await inquirer.prompt([
{
type: 'list',
name: 'repo',
message: 'Select a demo to create:',
choices: [
{ name: 'Fixed Layout Demo', value: 'demo-fixed-layout' },
{ name: 'Free Layout Demo', value: 'demo-free-layout' },
{ name: 'Fixed Layout Demo Simple', value: 'demo-fixed-layout-simple' },
{ name: 'Free Layout Demo Simple', value: 'demo-free-layout-simple' },
{ name: 'Free Layout Nextjs Demo', value: 'demo-nextjs' },
{ name: 'Free Layout Vite Demo Simple', value: 'demo-vite' },
{ name: 'Demo Playground for infinite canvas', value: 'demo-playground' },
],
},
]);
folderName = repo;
} else {
if (
[
'fixed-layout',
'free-layout',
'fixed-layout-simple',
'free-layout-simple',
'playground',
'nextjs',
].includes(projectName)
) {
folderName = `demo-${projectName}`;
} else {
console.error('Invalid projectName. Please run "npx create-app" to choose demo.');
return;
}
}
try {
const targetDir = path.join(process.cwd());
// 下载 npm 包的 tarball
const downloadPackage = async () => {
try {
const url = `https://registry.npmjs.org/@flowgram.ai/${folderName}/-/${folderName}-${latest}.tgz`;
const tempTarballPath = path.join(process.cwd(), `${folderName}.tgz`);
console.log(chalk.blue(`Downloading ${url} ...`));
await downloadFile(url, tempTarballPath);
fs.ensureDirSync(targetDir);
await tar.x({
file: tempTarballPath,
C: targetDir,
});
fs.renameSync(path.join(targetDir, 'package'), path.join(targetDir, folderName));
fs.unlinkSync(tempTarballPath);
return true;
} catch (error) {
console.error(`Error downloading or extracting package`);
console.error(error);
return false;
}
};
const res = await downloadPackage();
// 替换 package.json 内部的 @flowgram.ai 包版本为 latest
const pkgJsonPath = path.join(targetDir, folderName, 'package.json');
const data = fs.readFileSync(pkgJsonPath, 'utf-8');
const packageLatestVersion = execSync('npm view @flowgram.ai/core version --tag=latest latest')
.toString()
.trim();
const jsonData = JSON.parse(data);
if (jsonData.dependencies) {
updateFlowGramVersions(jsonData.dependencies, packageLatestVersion);
}
if (jsonData.devDependencies) {
updateFlowGramVersions(jsonData.devDependencies, packageLatestVersion);
}
fs.writeFileSync(pkgJsonPath, JSON.stringify(jsonData, null, 2), 'utf-8');
if (res) {
console.log(chalk.green(`${folderName} Demo project created successfully!`));
console.log(chalk.yellow('Run the following commands to start:'));
console.log(chalk.cyan(` cd ${folderName}`));
console.log(chalk.cyan(' npm install'));
console.log(chalk.cyan(' npm start'));
} else {
console.log(chalk.red('Download failed'));
}
} catch (error) {
console.error('Error downloading repo:', error);
return;
}
};
+75
View File
@@ -0,0 +1,75 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import chalk from 'chalk';
import { traverseRecursiveTsFiles } from '../utils/ts-file';
import { Project } from '../utils/project';
import { loadNpm } from '../utils/npm';
import { Material } from '../materials/material';
export async function findUsedMaterials() {
// materialName can be undefined
console.log(chalk.bold('🚀 Welcome to @flowgram.ai form-materials CLI!'));
const project = await Project.getSingleton();
project.printInfo();
const formMaterialPkg = await loadNpm('@flowgram.ai/form-materials');
const materials: Material[] = Material.listAll(formMaterialPkg);
const allUsedMaterials = new Set<Material>();
const exportName2Material = new Map<string, Material>();
for (const material of materials) {
if (!material.indexFile) {
console.warn(`Material ${material.name} not found`);
return;
}
console.log(`👀 The exports of ${material.name} is ${material.allExportNames.join(',')}`);
material.allExportNames.forEach((exportName) => {
exportName2Material.set(exportName, material);
});
}
for (const tsFile of traverseRecursiveTsFiles(project.srcPath)) {
const fileMaterials = new Set<Material>();
let fileImportPrinted = false;
for (const importDeclaration of tsFile.imports) {
if (
!importDeclaration.source.startsWith('@flowgram.ai/form-materials') ||
!importDeclaration.namedImports?.length
) {
continue;
}
if (!fileImportPrinted) {
fileImportPrinted = true;
console.log(chalk.bold(`\n👀 Searching ${tsFile.path}`));
}
console.log(`🔍 ${importDeclaration.statement}`);
if (importDeclaration.namedImports) {
importDeclaration.namedImports.forEach((namedImport) => {
const material = exportName2Material.get(namedImport.imported);
if (material) {
fileMaterials.add(material);
allUsedMaterials.add(material);
console.log(`import ${chalk.bold(material.fullName)} by ${namedImport.imported}`);
}
});
}
}
}
console.log(chalk.bold('\n📦 All used materials:'));
console.log([...allUsedMaterials].map((_material) => _material.fullName).join(','));
}
+63
View File
@@ -0,0 +1,63 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import path from 'path';
import { Command } from 'commander';
import { updateFlowgramVersion } from './update-version';
import { syncMaterial } from './materials';
import { findUsedMaterials } from './find-materials';
import { createApp } from './create-app';
const program = new Command();
program.name('flowgram-cli').version('1.0.0').description('Flowgram CLI');
program
.command('create-app')
.description('Create a new flowgram project')
.argument('[string]', 'Project name')
.action(async (projectName) => {
await createApp(projectName);
});
program
.command('materials')
.description('Sync materials to the project')
.argument(
'[string]',
'Material name or names\nExample 1: components/variable-selector \nExample2: components/variable-selector,effect/provideJsonSchemaOutputs'
)
.option('--refresh-project-imports', 'Refresh project imports to copied materials', false)
.option('--target-material-root-dir <string>', 'Target directory to copy materials')
.option('--select-multiple', 'Select multiple materials', false)
.action(async (materialName, options) => {
await syncMaterial({
materialName,
refreshProjectImports: options.refreshProjectImports,
targetMaterialRootDir: options.targetMaterialRootDir
? path.join(process.cwd(), options.targetMaterialRootDir)
: undefined,
selectMultiple: options.selectMultiple,
});
});
program
.command('find-used-materials')
.description('Find used materials in the project')
.action(async () => {
await findUsedMaterials();
});
program
.command('update-version')
.description('Update flowgram version in the project')
.argument('[string]', 'Flowgram version')
.action(async (version) => {
await updateFlowgramVersion(version);
});
program.parse(process.argv);
+60
View File
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import path from 'path';
import fs from 'fs';
import { traverseRecursiveTsFiles } from '../utils/ts-file';
import { SyncMaterialContext } from './types';
interface CopyMaterialReturn {
packagesToInstall: string[];
}
export const copyMaterials = (ctx: SyncMaterialContext): CopyMaterialReturn => {
const { selectedMaterials, project, formMaterialPkg, targetFormMaterialRoot } = ctx;
const formMaterialDependencies = formMaterialPkg.dependencies;
const packagesToInstall: Set<string> = new Set();
for (const material of selectedMaterials) {
const sourceDir: string = material.sourceDir;
const targetDir: string = path.join(targetFormMaterialRoot, material.type, material.name);
fs.cpSync(sourceDir, targetDir, { recursive: true });
for (const file of traverseRecursiveTsFiles(targetDir)) {
for (const importDeclaration of file.imports) {
const { source } = importDeclaration;
if (source.startsWith('@/')) {
// is inner import
console.log(`Replace Import from ${source} to @flowgram.ai/form-materials`);
file.replaceImport(
[importDeclaration],
[{ ...importDeclaration, source: '@flowgram.ai/form-materials' }]
);
packagesToInstall.add(`@flowgram.ai/form-materials@${project.flowgramVersion}`);
} else if (!source.startsWith('.') && !source.startsWith('react')) {
// check if is in form material dependencies
const [dep, version] =
Object.entries(formMaterialDependencies).find(([_key]) => source.startsWith(_key)) ||
[];
if (!dep) {
continue;
}
if (dep.startsWith('@flowgram.ai/')) {
packagesToInstall.add(`${dep}@${project.flowgramVersion}`);
} else {
packagesToInstall.add(`${dep}@${version}`);
}
}
}
}
}
return {
packagesToInstall: [...packagesToInstall],
};
};
+78
View File
@@ -0,0 +1,78 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import path from 'path';
import chalk from 'chalk';
import { Project } from '../utils/project';
import { loadNpm } from '../utils/npm';
import { MaterialCliOptions, SyncMaterialContext } from './types';
import { getSelectedMaterials } from './select';
import { executeRefreshProjectImport } from './refresh-project-import';
import { Material } from './material';
import { copyMaterials } from './copy';
export async function syncMaterial(cliOpts: MaterialCliOptions) {
const { refreshProjectImports, targetMaterialRootDir } = cliOpts;
// materialName can be undefined
console.log(chalk.bold('🚀 Welcome to @flowgram.ai form-materials CLI!'));
const project = await Project.getSingleton();
project.printInfo();
// where to place all material in target project
const targetFormMaterialRoot =
targetMaterialRootDir || path.join(project.projectPath, 'src', 'form-materials');
console.log(chalk.black(` - Target material root: ${targetFormMaterialRoot}`));
if (!project.flowgramVersion) {
throw new Error(
chalk.red(
'❌ Please install @flowgram.ai/fixed-layout-editor or @flowgram.ai/free-layout-editor'
)
);
}
const formMaterialPkg = await loadNpm('@flowgram.ai/form-materials');
let selectedMaterials: Material[] = await getSelectedMaterials(cliOpts, formMaterialPkg);
// Ensure material is defined before proceeding
if (!selectedMaterials.length) {
console.error(chalk.red('No material selected. Exiting.'));
process.exit(1);
}
const context: SyncMaterialContext = {
selectedMaterials: selectedMaterials,
project,
formMaterialPkg,
cliOpts,
targetFormMaterialRoot,
};
// Copy the materials to the project
console.log(chalk.bold('🚀 The following materials will be added to your project'));
console.log(selectedMaterials.map((material) => `📦 ${material.fullName}`).join('\n'));
console.log('\n');
let { packagesToInstall } = copyMaterials(context);
// Refresh project imports
if (refreshProjectImports) {
console.log(chalk.bold('🚀 Refresh imports in your project'));
executeRefreshProjectImport(context);
}
// Install the dependencies
await project.addDependencies(packagesToInstall);
console.log(chalk.bold('\n✅ These npm dependencies is added to your package.json'));
packagesToInstall.forEach((_package) => {
console.log(`- ${_package}`);
});
console.log(chalk.bold(chalk.bold('\n➡️ Please run npm install to install dependencies\n')));
}
+61
View File
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import path from 'path';
import { readdirSync } from 'fs';
import { getIndexTsFile } from '../utils/ts-file';
import { LoadedNpmPkg } from '../utils/npm';
export class Material {
protected static _all_materials_cache: Material[] = [];
static ALL_TYPES = [
'components',
'effects',
'plugins',
'shared',
'validate',
'form-plugins',
'hooks',
];
constructor(public type: string, public name: string, public formMaterialPkg: LoadedNpmPkg) {}
get fullName() {
return `${this.type}/${this.name}`;
}
get sourceDir() {
return path.join(this.formMaterialPkg.srcPath, this.type, this.name);
}
get indexFile() {
return getIndexTsFile(this.sourceDir);
}
get allExportNames() {
return this.indexFile?.allExportNames || [];
}
static listAll(formMaterialPkg: LoadedNpmPkg): Material[] {
if (!this._all_materials_cache.length) {
this._all_materials_cache = Material.ALL_TYPES.map((type) => {
const materialsPath: string = path.join(formMaterialPkg.srcPath, type);
return readdirSync(materialsPath)
.map((_path: string) => {
if (_path === 'index.ts') {
return null;
}
return new Material(type, _path, formMaterialPkg);
})
.filter((material): material is Material => material !== null);
}).flat();
}
return this._all_materials_cache;
}
}
@@ -0,0 +1,87 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import path from 'path';
import chalk from 'chalk';
import { traverseRecursiveTsFiles } from '../utils/ts-file';
import { ImportDeclaration, NamedImport } from '../utils/import';
import { SyncMaterialContext } from './types';
import { Material } from './material';
export function executeRefreshProjectImport(context: SyncMaterialContext) {
const { selectedMaterials, project, targetFormMaterialRoot } = context;
const exportName2Material = new Map<string, Material>();
const targetModule = `@/${path.relative(project.srcPath, targetFormMaterialRoot)}`;
for (const material of selectedMaterials) {
if (!material.indexFile) {
console.warn(`Material ${material.name} not found`);
return;
}
console.log(`👀 The exports of ${material.name} is ${material.allExportNames.join(',')}`);
material.allExportNames.forEach((exportName) => {
exportName2Material.set(exportName, material);
});
}
for (const tsFile of traverseRecursiveTsFiles(project.srcPath)) {
for (const importDeclaration of tsFile.imports) {
if (importDeclaration.source.startsWith('@flowgram.ai/form-materials')) {
// Import Module and its related named Imported
const restImports: NamedImport[] = [];
const importMap: Record<string, NamedImport[]> = {};
if (!importDeclaration.namedImports) {
continue;
}
for (const nameImport of importDeclaration.namedImports) {
const material = exportName2Material.get(nameImport.imported);
if (material) {
const importModule = `${targetModule}/${material.fullName}`;
importMap[importModule] = importMap[importModule] || [];
importMap[importModule].push(nameImport);
} else {
restImports.push(nameImport);
}
}
if (Object.keys(importMap).length === 0) {
continue;
}
const nextImports: ImportDeclaration[] = Object.entries(importMap).map(
([importModule, namedImports]) => ({
...importDeclaration,
namedImports,
source: importModule,
})
);
if (restImports?.length) {
nextImports.unshift({
...importDeclaration,
namedImports: restImports,
});
}
tsFile.replaceImport([importDeclaration], nextImports);
console.log(chalk.green(`🔄 Refresh Imports In: ${tsFile.path}`));
console.log(
`From:\n${importDeclaration.statement}\nTo:\n${nextImports
.map((item) => item.statement)
.join('\n')}`
);
}
}
}
}
+69
View File
@@ -0,0 +1,69 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import inquirer from 'inquirer';
import chalk from 'chalk';
import { LoadedNpmPkg } from '../utils/npm';
import { MaterialCliOptions } from './types';
import { Material } from './material';
export const getSelectedMaterials = async (
cliOpts: MaterialCliOptions,
formMaterialPkg: LoadedNpmPkg
) => {
const { materialName, selectMultiple } = cliOpts;
const materials: Material[] = Material.listAll(formMaterialPkg);
let selectedMaterials: Material[] = [];
// 1. Check if materialName is provided and exists in materials
if (materialName) {
selectedMaterials = materialName
.split(',')
.map((_name) => materials.find((_m) => _m.fullName === _name.trim())!)
.filter(Boolean);
}
// 2. If material not found or materialName not provided, prompt user to select
if (!selectedMaterials.length) {
console.log(chalk.yellow(`Material "${materialName}" not found. Please select from the list:`));
const choices = materials.map((_material) => ({
name: _material.fullName,
value: _material,
}));
if (selectMultiple) {
// User select one component
const result = await inquirer.prompt<{
material: Material[]; // Specify type for prompt result
}>([
{
type: 'checkbox',
name: 'material',
message: 'Select multiple materials to add:',
choices: choices,
},
]);
selectedMaterials = result.material;
} else {
// User select one component
const result = await inquirer.prompt<{
material: Material; // Specify type for prompt result
}>([
{
type: 'list',
name: 'material',
message: 'Select one material to add:',
choices: choices,
},
]);
selectedMaterials = [result.material];
}
}
return selectedMaterials;
};
+23
View File
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { Project } from '../utils/project';
import { LoadedNpmPkg } from '../utils/npm';
import { Material } from './material';
export interface MaterialCliOptions {
materialName?: string;
refreshProjectImports?: boolean;
targetMaterialRootDir?: string;
selectMultiple?: boolean;
}
export interface SyncMaterialContext {
selectedMaterials: Material[];
project: Project;
formMaterialPkg: LoadedNpmPkg;
cliOpts: MaterialCliOptions;
targetFormMaterialRoot: string;
}
+53
View File
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import chalk from 'chalk';
import { getLatestVersion } from '../utils/npm';
import { traverseRecursiveFiles } from '../utils/file';
export async function updateFlowgramVersion(inputVersion?: string) {
console.log(chalk.bold('🚀 Welcome to @flowgram.ai update-version helper'));
// Get latest version
const latestVersion = await getLatestVersion('@flowgram.ai/editor');
const currentPath = process.cwd();
console.log('- Latest flowgram version: ', latestVersion);
console.log('- Current Path: ', currentPath);
// User Input flowgram version, default is latestVersion
const flowgramVersion: string = inputVersion || latestVersion;
for (const file of traverseRecursiveFiles(currentPath)) {
if (file.path.endsWith('package.json')) {
console.log('👀 Find package.json: ', file.path);
let updated = false;
const json = JSON.parse(file.content);
if (json.dependencies) {
for (const key in json.dependencies) {
if (key.startsWith('@flowgram.ai/')) {
updated = true;
json.dependencies[key] = flowgramVersion;
console.log(`- Update ${key} to ${flowgramVersion}`);
}
}
}
if (json.devDependencies) {
for (const key in json.devDependencies) {
if (key.startsWith('@flowgram.ai/')) {
updated = true;
json.devDependencies[key] = flowgramVersion;
console.log(`- Update ${key} to ${flowgramVersion}`);
}
}
}
if (updated) {
file.write(JSON.stringify(json, null, 2));
console.log(`${file.path} Updated`);
}
}
}
}
+114
View File
@@ -0,0 +1,114 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
export function extractNamedExports(content: string) {
const valueExports = [];
const typeExports = [];
// Collect all type definition names
const typeDefinitions = new Set();
const typePatterns = [/\b(?:type|interface)\s+(\w+)/g, /\bexport\s+(?:type|interface)\s+(\w+)/g];
let match;
for (const pattern of typePatterns) {
while ((match = pattern.exec(content)) !== null) {
typeDefinitions.add(match[1]);
}
}
// Match various export patterns
const exportPatterns = [
// export const/var/let/function/class/type/interface
/\bexport\s+(const|var|let|function|class|type|interface)\s+(\w+)/g,
// export { name1, name2 }
/\bexport\s*\{([^}]+)\}/g,
// export { name as alias }
/\bexport\s*\{[^}]*\b(\w+)\s+as\s+(\w+)[^}]*\}/g,
// export default function name()
/\bexport\s+default\s+(?:function|class)\s+(\w+)/g,
// export type { Type1, Type2 }
/\bexport\s+type\s*\{([^}]+)\}/g,
// export type { Original as Alias }
/\bexport\s+type\s*\{[^}]*\b(\w+)\s+as\s+(\w+)[^}]*\}/g,
];
// Handle first pattern: export const/var/let/function/class/type/interface
exportPatterns[0].lastIndex = 0;
while ((match = exportPatterns[0].exec(content)) !== null) {
const [, kind, name] = match;
if (kind === 'type' || kind === 'interface' || typeDefinitions.has(name)) {
typeExports.push(name);
} else {
valueExports.push(name);
}
}
// Handle second pattern: export { name1, name2 }
exportPatterns[1].lastIndex = 0;
while ((match = exportPatterns[1].exec(content)) !== null) {
const exportsList = match[1]
.split(',')
.map((item) => item.trim())
.filter((item) => item && !item.includes(' as '));
for (const name of exportsList) {
if (name.startsWith('type ')) {
typeExports.push(name.replace('type ', '').trim());
} else if (typeDefinitions.has(name)) {
typeExports.push(name);
} else {
valueExports.push(name);
}
}
}
// Handle third pattern: export { name as alias }
exportPatterns[2].lastIndex = 0;
while ((match = exportPatterns[2].exec(content)) !== null) {
const [, original, alias] = match;
if (typeDefinitions.has(original)) {
typeExports.push(alias);
} else {
valueExports.push(alias);
}
}
// Handle fourth pattern: export default function name()
exportPatterns[3].lastIndex = 0;
while ((match = exportPatterns[3].exec(content)) !== null) {
const name = match[1];
if (typeDefinitions.has(name)) {
typeExports.push(name);
} else {
valueExports.push(name);
}
}
// Handle fifth pattern: export type { Type1, Type2 }
exportPatterns[4].lastIndex = 0;
while ((match = exportPatterns[4].exec(content)) !== null) {
const exportsList = match[1]
.split(',')
.map((item) => item.trim())
.filter((item) => item && !item.includes(' as '));
for (const name of exportsList) {
typeExports.push(name);
}
}
// Handle sixth pattern: export type { Original as Alias }
exportPatterns[5].lastIndex = 0;
while ((match = exportPatterns[5].exec(content)) !== null) {
const [, original, alias] = match;
typeExports.push(alias);
}
// Deduplicate and sort
return {
values: [...new Set(valueExports)].sort(),
types: [...new Set(typeExports)].sort(),
};
}
+88
View File
@@ -0,0 +1,88 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import path from 'path';
import fs from 'fs';
import ignore, { Ignore } from 'ignore';
export class File {
content: string;
isUtf8: boolean;
relativePath: string;
path: string;
suffix: string;
constructor(filePath: string, public root: string = '/') {
this.path = filePath;
this.relativePath = path.relative(this.root, this.path);
this.suffix = path.extname(this.path);
// Check if exists
if (!fs.existsSync(this.path)) {
throw Error(`File ${path} Not Exists`);
}
// If no utf-8, skip
try {
this.content = fs.readFileSync(this.path, 'utf-8');
this.isUtf8 = true;
} catch (e) {
this.isUtf8 = false;
return;
}
}
replace(updater: (content: string) => string) {
if (!this.isUtf8) {
console.warn('Not UTF-8 file skipped: ', this.path);
return;
}
this.content = updater(this.content);
fs.writeFileSync(this.path, this.content, 'utf-8');
}
write(nextContent: string) {
this.content = nextContent;
fs.writeFileSync(this.path, this.content, 'utf-8');
}
}
export function* traverseRecursiveFilePaths(
folder: string,
ig: Ignore = ignore().add('.git'),
root: string = folder
): Generator<string> {
const files = fs.readdirSync(folder);
// add .gitignore to ignore if exists
if (fs.existsSync(path.join(folder, '.gitignore'))) {
ig.add(fs.readFileSync(path.join(folder, '.gitignore'), 'utf-8'));
}
for (const file of files) {
const filePath = path.join(folder, file);
if (ig.ignores(path.relative(root, filePath))) {
continue;
}
if (fs.statSync(filePath).isDirectory()) {
yield* traverseRecursiveFilePaths(filePath, ig, root);
} else {
yield filePath;
}
}
}
export function* traverseRecursiveFiles(folder: string): Generator<File> {
for (const filePath of traverseRecursiveFilePaths(folder)) {
yield new File(filePath, folder);
}
}
+129
View File
@@ -0,0 +1,129 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
export interface NamedImport {
local?: string;
imported: string;
typeOnly?: boolean;
}
/**
* Cases
* import { A, B } from 'module';
* import A from 'module';
* import * as C from 'module';
* import D, { type E, F } from 'module';
* import A, { B as B1 } from 'module';
*/
export interface ImportDeclaration {
// origin statement
statement: string;
// import { A, B } from 'module';
namedImports?: NamedImport[];
// import A from 'module';
defaultImport?: string;
// import * as C from 'module';
namespaceImport?: string;
source: string;
}
export function assembleImport(declaration: ImportDeclaration): string {
const { namedImports, defaultImport, namespaceImport, source } = declaration;
const importClauses = [];
if (namedImports) {
importClauses.push(
`{ ${namedImports
.map(
({ local, imported, typeOnly }) =>
`${typeOnly ? 'type ' : ''}${imported}${local ? ` as ${local}` : ''}`
)
.join(', ')} }`
);
}
if (defaultImport) {
importClauses.push(defaultImport);
}
if (namespaceImport) {
importClauses.push(`* as ${namespaceImport}`);
}
return `import ${importClauses.join(', ')} from '${source}';`;
}
export function replaceImport(
fileContent: string,
origin: ImportDeclaration,
replaceTo: ImportDeclaration[]
): string {
const replaceImportStatements = replaceTo.map(assembleImport);
// replace origin statement
return fileContent.replace(origin.statement, replaceImportStatements.join('\n'));
}
export function* traverseFileImports(fileContent: string): Generator<ImportDeclaration> {
// 匹配所有 import 语句的正则表达式
const importRegex =
/import\s+([^{}*,]*?)?(?:\s*\*\s*as\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*,?)?(?:\s*\{([^}]*)\}\s*,?)?(?:\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*,?)?\s*from\s*['"`]([^'"`]+)['"`]\;?/g;
let match;
while ((match = importRegex.exec(fileContent)) !== null) {
const [fullMatch, defaultPart, namespacePart, namedPart, defaultPart2, source] = match;
const declaration: ImportDeclaration = {
statement: fullMatch,
source: source,
};
// 处理默认导入
const defaultImport = defaultPart?.trim() || defaultPart2?.trim();
if (defaultImport && !namespacePart && !namedPart) {
declaration.defaultImport = defaultImport;
} else if (defaultImport && (namespacePart || namedPart)) {
declaration.defaultImport = defaultImport;
}
// 处理命名空间导入 (* as)
if (namespacePart) {
declaration.namespaceImport = namespacePart.trim();
}
// 处理命名导入
if (namedPart) {
const namedImports = [];
const namedItems = namedPart
.split(',')
.map((item) => item.trim())
.filter(Boolean);
for (const item of namedItems) {
const typeOnly = item.startsWith('type ');
const cleanItem = typeOnly ? item.slice(5).trim() : item;
if (cleanItem.includes(' as ')) {
const [imported, local] = cleanItem.split(' as ').map((s) => s.trim());
namedImports.push({
imported,
local,
typeOnly,
});
} else {
namedImports.push({
imported: cleanItem,
typeOnly,
});
}
}
if (namedImports.length > 0) {
declaration.namedImports = namedImports;
}
}
yield declaration;
}
}
+117
View File
@@ -0,0 +1,117 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import path from 'path';
import https from 'https';
import { existsSync, readFileSync } from 'fs';
import { execSync } from 'child_process';
import * as tar from 'tar';
import fs from 'fs-extra';
export class LoadedNpmPkg {
constructor(public name: string, public version: string, public path: string) {}
get srcPath() {
return path.join(this.path, 'src');
}
get distPath() {
return path.join(this.path, 'dist');
}
protected _packageJson: Record<string, any>;
get packageJson() {
if (!this._packageJson) {
this._packageJson = JSON.parse(readFileSync(path.join(this.path, 'package.json'), 'utf8'));
}
return this._packageJson;
}
get dependencies(): Record<string, string> {
return this.packageJson.dependencies;
}
}
// 使用 https 下载文件
function downloadFile(url: string, dest: string): Promise<void> {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(dest);
https
.get(url, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`Download failed: ${response.statusCode}`));
return;
}
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
})
.on('error', (err) => {
fs.unlink(dest, () => reject(err));
});
file.on('error', (err) => {
fs.unlink(dest, () => reject(err));
});
});
}
export async function getLatestVersion(packageName: string): Promise<string> {
return execSync(`npm view ${packageName} version --tag=latest`).toString().trim();
}
export async function loadNpm(packageName: string): Promise<LoadedNpmPkg> {
const packageLatestVersion = await getLatestVersion(packageName);
const packagePath = path.join(__dirname, `./.download/${packageName}-${packageLatestVersion}`);
if (existsSync(packagePath)) {
return new LoadedNpmPkg(packageName, packageLatestVersion, packagePath);
}
try {
// 获取 tarball 地址
const tarballUrl = execSync(`npm view ${packageName}@${packageLatestVersion} dist.tarball`)
.toString()
.trim();
// 临时 tarball 路径
const tempTarballPath = path.join(
__dirname,
`./.download/${packageName}-${packageLatestVersion}.tgz`
);
// 确保目录存在
fs.ensureDirSync(path.dirname(tempTarballPath));
// 下载 tarball
await downloadFile(tarballUrl, tempTarballPath);
fs.ensureDirSync(packagePath);
// 解压到目标目录
await tar.x({
file: tempTarballPath,
cwd: packagePath,
strip: 1,
});
// 删除临时文件
fs.unlinkSync(tempTarballPath);
return new LoadedNpmPkg(packageName, packageLatestVersion, packagePath);
} catch (error) {
console.error(`Error downloading or extracting package`);
console.error(error);
throw error;
}
}
+103
View File
@@ -0,0 +1,103 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import path from 'path';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import chalk from 'chalk';
import { getLatestVersion } from './npm';
interface PackageJson {
dependencies: { [key: string]: string };
devDependencies?: { [key: string]: string };
peerDependencies?: { [key: string]: string };
[key: string]: any;
}
export class Project {
flowgramVersion?: string;
projectPath: string;
packageJsonPath: string;
packageJson: PackageJson;
srcPath: string;
protected constructor() {}
async init() {
// get nearest package.json
let projectPath: string = process.cwd();
while (projectPath !== '/' && !existsSync(path.join(projectPath, 'package.json'))) {
projectPath = path.join(projectPath, '..');
}
if (projectPath === '/') {
throw new Error('Please run this command in a valid project');
}
this.projectPath = projectPath;
this.srcPath = path.join(projectPath, 'src');
this.packageJsonPath = path.join(projectPath, 'package.json');
this.packageJson = JSON.parse(readFileSync(this.packageJsonPath, 'utf8'));
this.flowgramVersion =
this.packageJson.dependencies['@flowgram.ai/fixed-layout-editor'] ||
this.packageJson.dependencies['@flowgram.ai/free-layout-editor'] ||
this.packageJson.dependencies['@flowgram.ai/editor'];
}
async addDependency(dependency: string) {
let name: string;
let version: string;
// 处理作用域包(如 @types/react@1.0.0
const lastAtIndex = dependency.lastIndexOf('@');
if (lastAtIndex <= 0) {
// 没有@符号 或者@在开头(如 @types/react
name = dependency;
version = await getLatestVersion(name);
} else {
// 正常分割包名和版本
name = dependency.substring(0, lastAtIndex);
version = dependency.substring(lastAtIndex + 1);
// 如果版本部分为空,获取最新版本
if (!version.trim()) {
version = await getLatestVersion(name);
}
}
this.packageJson.dependencies[name] = version;
writeFileSync(this.packageJsonPath, JSON.stringify(this.packageJson, null, 2));
}
async addDependencies(dependencies: string[]) {
for (const dependency of dependencies) {
await this.addDependency(dependency);
}
}
writeToPackageJsonFile() {
writeFileSync(this.packageJsonPath, JSON.stringify(this.packageJson, null, 2));
}
printInfo() {
console.log(chalk.bold('Project Info:'));
console.log(chalk.black(` - Flowgram Version: ${this.flowgramVersion}`));
console.log(chalk.black(` - Project Path: ${this.projectPath}`));
}
static async getSingleton() {
const info = new Project();
await info.init();
return info;
}
}
+113
View File
@@ -0,0 +1,113 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import path, { join } from 'path';
import fs from 'fs';
import { assembleImport, ImportDeclaration, traverseFileImports } from './import';
import { File, traverseRecursiveFilePaths } from './file';
import { extractNamedExports } from './export';
class TsFile extends File {
exports: {
values: string[];
types: string[];
} = {
values: [],
types: [],
};
imports: ImportDeclaration[] = [];
get allExportNames() {
return [...this.exports.values, ...this.exports.types];
}
constructor(filePath: string, root?: string) {
super(filePath, root);
this.exports = extractNamedExports(fs.readFileSync(filePath, 'utf-8'));
this.imports = Array.from(traverseFileImports(fs.readFileSync(filePath, 'utf-8')));
}
addImport(importDeclarations: ImportDeclaration[]) {
importDeclarations.forEach((importDeclaration) => {
importDeclaration.statement = assembleImport(importDeclaration);
});
// add in last import statement
this.replace((content) => {
const lastImportStatement = this.imports[this.imports.length - 1];
return content.replace(
lastImportStatement.statement,
`${lastImportStatement?.statement}\n${importDeclarations.map((item) => item.statement)}\n`
);
});
this.imports.push(...importDeclarations);
}
removeImport(importDeclarations: ImportDeclaration[]) {
this.replace((content) =>
importDeclarations.reduce((prev, cur) => prev.replace(cur.statement, ''), content)
);
this.imports = this.imports.filter((item) => !importDeclarations.includes(item));
}
replaceImport(oldImports: ImportDeclaration[], newImports: ImportDeclaration[]) {
newImports.forEach((importDeclaration) => {
importDeclaration.statement = assembleImport(importDeclaration);
});
this.replace((content) => {
oldImports.forEach((oldImport, idx) => {
const replaceTo = newImports[idx];
if (replaceTo) {
content = content.replace(oldImport.statement, replaceTo.statement);
this.imports.map((_import) => {
if (_import.statement === oldImport.statement) {
_import = replaceTo;
}
});
} else {
content = content.replace(oldImport.statement, '');
this.imports = this.imports.filter(
(_import) => _import.statement !== oldImport.statement
);
}
});
const restNewImports = newImports.slice(oldImports.length);
if (restNewImports.length > 0) {
const lastImportStatement = newImports[oldImports.length - 1].statement;
content = content.replace(
lastImportStatement,
`${lastImportStatement}
${restNewImports.map((item) => item.statement).join('\n')}
`
);
}
this.imports.push(...restNewImports);
return content;
});
}
}
export function* traverseRecursiveTsFiles(folder: string): Generator<TsFile> {
for (const filePath of traverseRecursiveFilePaths(folder)) {
if (filePath.endsWith('.ts') || filePath.endsWith('.tsx')) {
yield new TsFile(filePath, folder);
}
}
}
export function getIndexTsFile(folder: string): TsFile | undefined {
// ts or tsx
const files = fs.readdirSync(folder);
for (const file of files) {
if (file === 'index.ts' || file === 'index.tsx') {
return new TsFile(path.join(folder, file), folder);
}
}
return undefined;
}
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"target": "es2020",
"module": "esnext",
"strictPropertyInitialization": false,
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"skipLibCheck": true,
"noUnusedLocals": true,
"noImplicitAny": true,
"allowJs": true,
"resolveJsonModule": true,
"types": ["node"],
"jsx": "react-jsx",
"lib": ["es6", "dom", "es2020", "es2019.Array"]
},
"include": ["./src"],
}
+10
View File
@@ -0,0 +1,10 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { defineConfig } from "tsup";
export default defineConfig({
shims: true,
})
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env node
import '../dist/index.js';
+64
View File
@@ -0,0 +1,64 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { defineFlatConfig } from '@flowgram.ai/eslint-config';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineFlatConfig({
preset: 'node',
packageRoot: __dirname,
ignore: [
'**/*.d.ts',
'**/__mocks__',
'**/node_modules',
'**/build',
'**/dist',
'**/es',
'**/lib',
'**/.codebase',
'**/.changeset',
'**/config',
'**/common/scripts',
'**/output',
'error-log-str.js',
'*.bundle.js',
'*.min.js',
'*.js.map',
'**/*.log',
'**/tsconfig.tsbuildinfo',
'**/vitest.config.ts',
'package.json',
'*.json',
],
rules: {
'no-console': 'off',
'import/prefer-default-export': 'off',
'lines-between-class-members': 'warn',
'no-unused-vars': 'off',
'no-redeclare': 'off',
'no-empty-function': 'off',
'prefer-destructuring': 'off',
'no-underscore-dangle': 'off',
'no-multi-assign': 'off',
'arrow-body-style': 'warn',
'no-useless-constructor': 'off',
'no-param-reassign': 'off',
'max-classes-per-file': 'off',
'grouped-accessor-pairs': 'off',
'no-plusplus': 'off',
'no-restricted-syntax': 'off',
'import/extensions': 'off',
'consistent-return': 'off',
'no-use-before-define': 'off',
'no-bitwise': 'off',
'no-case-declarations': 'off',
'no-dupe-class-members': 'off',
'class-methods-use-this': 'off',
'default-param-last': 'off',
},
});
+42
View File
@@ -0,0 +1,42 @@
{
"name": "@flowgram.ai/create-app",
"version": "0.1.8",
"description": "A CLI tool to create demo projects",
"repository": "https://github.com/bytedance/flowgram.ai",
"bin": {
"create-app": "./bin/index.js"
},
"type": "module",
"files": [
"bin",
"src",
"dist"
],
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts --out-dir dist",
"start": "node bin/index.js",
"lint": "eslint ./src --cache",
"lint:fix": "eslint ./src --fix"
},
"dependencies": {
"fs-extra": "^9.1.0",
"commander": "^11.0.0",
"chalk": "^5.3.0",
"tar": "7.4.3",
"inquirer": "^12.9.4"
},
"devDependencies": {
"@flowgram.ai/eslint-config": "workspace:*",
"@types/fs-extra": "11.0.4",
"@types/node": "^18",
"@types/inquirer": "^9.0.9",
"tsup": "^8.0.1",
"eslint": "^9.0.0",
"@typescript-eslint/parser": "^8.0.0",
"typescript": "^5.8.3"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}
+176
View File
@@ -0,0 +1,176 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import path from 'path';
import https from 'https';
import http from 'http';
import { execSync } from 'child_process';
import * as tar from 'tar';
import inquirer from 'inquirer';
import fs from 'fs-extra';
import { Command } from 'commander';
import chalk from 'chalk';
const program = new Command();
const args = process.argv.slice(2);
const updateFlowGramVersions = (dependencies: any[], latestVersion: string) => {
for (const packageName in dependencies) {
if (packageName.startsWith('@flowgram.ai')) {
dependencies[packageName] = latestVersion;
}
}
};
// 使用 http/https 下载文件
function downloadFile(url: string, dest: string): Promise<void> {
return new Promise((resolve, reject) => {
const lib = url.startsWith('https') ? https : http;
const file = fs.createWriteStream(dest);
const request = lib.get(url, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`Download failed: ${response.statusCode}`));
return;
}
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
});
request.on('error', (err) => {
fs.unlink(dest, () => reject(err));
});
file.on('error', (err) => {
fs.unlink(dest, () => reject(err));
});
});
}
const main = async () => {
console.log(chalk.green('Welcome to @flowgram.ai/create-app CLI!123123'));
const latest = execSync('npm view @flowgram.ai/demo-fixed-layout version --tag=latest latest')
.toString()
.trim();
let folderName = '';
if (!args?.length) {
const { repo } = await inquirer.prompt([
{
type: 'list',
name: 'repo',
message: 'Select a demo to create:',
choices: [
{ name: 'Fixed Layout Demo', value: 'demo-fixed-layout' },
{ name: 'Free Layout Demo', value: 'demo-free-layout' },
{ name: 'Fixed Layout Demo Simple', value: 'demo-fixed-layout-simple' },
{ name: 'Free Layout Demo Simple', value: 'demo-free-layout-simple' },
{ name: 'Free Layout Nextjs Demo', value: 'demo-nextjs' },
{ name: 'Free Layout Vite Demo Simple', value: 'demo-vite' },
{ name: 'Demo Playground for infinite canvas', value: 'demo-playground' },
],
},
]);
folderName = repo;
} else {
if (
[
'fixed-layout',
'free-layout',
'fixed-layout-simple',
'free-layout-simple',
'playground',
'nextjs',
].includes(args[0])
) {
folderName = `demo-${args[0]}`;
} else {
console.error('Invalid argument. Please run "npx create-app" to choose demo.');
return;
}
}
try {
const targetDir = path.join(process.cwd());
const downloadPackage = async () => {
try {
const tempTarballPath = path.join(process.cwd(), `${folderName}.tgz`);
const url = `https://registry.npmjs.org/@flowgram.ai/${folderName}/-/${folderName}-${latest}.tgz`;
console.log(chalk.blue(`Downloading ${url} ...`));
await downloadFile(url, tempTarballPath);
fs.ensureDirSync(targetDir);
await tar.x({
file: tempTarballPath,
C: targetDir,
});
fs.renameSync(path.join(targetDir, 'package'), path.join(targetDir, folderName));
fs.unlinkSync(tempTarballPath);
return true;
} catch (error) {
console.error(`Error downloading or extracting package`);
console.error(error);
return false;
}
};
const res = await downloadPackage();
const pkgJsonPath = path.join(targetDir, folderName, 'package.json');
const data = fs.readFileSync(pkgJsonPath, 'utf-8');
const packageLatestVersion = execSync('npm view @flowgram.ai/core version --tag=latest latest')
.toString()
.trim();
const jsonData = JSON.parse(data);
if (jsonData.dependencies) {
updateFlowGramVersions(jsonData.dependencies, packageLatestVersion);
}
if (jsonData.devDependencies) {
updateFlowGramVersions(jsonData.devDependencies, packageLatestVersion);
}
fs.writeFileSync(pkgJsonPath, JSON.stringify(jsonData, null, 2), 'utf-8');
if (res) {
console.log(chalk.green(`${folderName} Demo project created successfully!`));
console.log(chalk.yellow('Run the following commands to start:'));
console.log(chalk.cyan(` cd ${folderName}`));
console.log(chalk.cyan(' npm install'));
console.log(chalk.cyan(' npm start'));
} else {
console.log(chalk.red('Download failed'));
}
} catch (error) {
console.error('Error downloading repo:', error);
return;
}
};
program.version('1.0.0').description('Create a demo project');
program.parse(process.argv);
main();
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"target": "es2020",
"module": "esnext",
"strictPropertyInitialization": false,
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"skipLibCheck": true,
"noUnusedLocals": true,
"noImplicitAny": true,
"allowJs": true,
"resolveJsonModule": true,
"types": ["node"],
"jsx": "react-jsx",
"lib": ["es6", "dom", "es2020", "es2019.Array"]
},
"include": ["./src"],
}
@@ -0,0 +1,20 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
const { defineFlatConfig } = require('@flowgram.ai/eslint-config');
module.exports = defineFlatConfig({
preset: 'web',
packageRoot: __dirname,
rules: {
'no-console': 'off',
'react/prop-types': 'off',
},
settings: {
react: {
version: 'detect', // 自动检测 React 版本
},
},
});
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en" data-bundler="rspack">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flow FixedLayoutEditor Demo</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
@@ -0,0 +1,65 @@
{
"name": "@flowgram.ai/demo-fixed-layout-animation",
"version": "0.1.0",
"description": "",
"keywords": [],
"license": "MIT",
"main": "./src/app.ts",
"files": [
"src/",
"eslint.config.js",
".gitignore",
"index.html",
"package.json",
"rsbuild.config.ts",
"tsconfig.json"
],
"scripts": {
"build": "exit 0",
"build:fast": "exit 0",
"build:watch": "exit 0",
"build:prod": "cross-env MODE=app NODE_ENV=production rsbuild build",
"clean": "rimraf dist",
"dev": "cross-env MODE=app NODE_ENV=development rsbuild dev --open",
"lint": "eslint ./src --cache",
"lint:fix": "eslint ./src --fix",
"ts-check": "tsc --noEmit",
"start": "cross-env NODE_ENV=development rsbuild dev --open",
"test": "exit",
"test:cov": "exit",
"watch": "exit 0"
},
"dependencies": {
"@flowgram.ai/fixed-layout-editor": "workspace:*",
"@flowgram.ai/fixed-semi-materials": "workspace:*",
"@flowgram.ai/minimap-plugin": "workspace:*",
"lodash-es": "^4.17.21",
"nanoid": "^5.0.9",
"react": "^18",
"react-dom": "^18",
"classnames": "^2.5.1",
"styled-components": "^5"
},
"devDependencies": {
"@flowgram.ai/ts-config": "workspace:*",
"@flowgram.ai/eslint-config": "workspace:*",
"@rsbuild/core": "^1.2.16",
"@rsbuild/plugin-react": "^1.1.1",
"@rsbuild/plugin-less": "^1.1.1",
"@types/lodash-es": "^4.17.12",
"@types/node": "^18",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/styled-components": "^5",
"@typescript-eslint/parser": "^8.0.0",
"typescript": "^5.8.3",
"eslint": "^9.0.0",
"less": "^4.1.2",
"less-loader": "^6",
"cross-env": "~7.0.3"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}
@@ -0,0 +1,34 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginLess } from '@rsbuild/plugin-less';
import { defineConfig } from '@rsbuild/core';
export default defineConfig({
plugins: [pluginReact(), pluginLess()],
source: {
entry: {
index: './src/app.tsx',
},
/**
* support inversify @injectable() and @inject decorators
*/
decorators: {
version: 'legacy',
},
},
html: {
title: 'demo-fixed-layout-animation',
},
tools: {
rspack: {
/**
* ignore warnings from @coze-editor/editor/language-typescript
*/
ignoreWarnings: [/Critical dependency: the request of a dependency is an expression/],
},
},
});
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import '@flowgram.ai/fixed-layout-editor/index.css';
import { createRoot } from 'react-dom/client';
import { FixedLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/fixed-layout-editor';
import { useEditorProps } from '@/hooks/use-editor-props';
import { UpdateSchema } from '@/components/update-schema';
import { Tools } from '@/components/tools';
const FlowGramApp = () => {
const editorProps = useEditorProps();
return (
<FixedLayoutEditorProvider {...editorProps}>
<EditorRenderer />
<Tools />
<UpdateSchema />
</FixedLayoutEditorProvider>
);
};
const app = createRoot(document.getElementById('root')!);
app.render(<FlowGramApp />);
@@ -0,0 +1,14 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { TitleField } from '@/fields/title-field';
import { ContentField } from '@/fields/content-field';
export const FormRender = () => (
<>
<TitleField />
<ContentField />
</>
);
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
.loading-dots {
display: flex;
gap: 4px;
.dot {
width: 6px;
height: 6px;
background: #3b82f6;
border-radius: 50%;
animation: bounce 1.4s ease-in-out infinite both;
&:nth-child(1) {
animation-delay: -0.32s;
}
&:nth-child(2) {
animation-delay: -0.16s;
}
&:nth-child(3) {
animation-delay: 0s;
}
}
}
@@ -0,0 +1,14 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import './index.less';
export const LoadingDots = () => (
<div className="loading-dots">
<span className="dot"></span>
<span className="dot"></span>
<span className="dot"></span>
</div>
);
@@ -0,0 +1,118 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
.node-render {
background: #fff;
border: 1px solid rgba(6, 7, 9, 0.15);
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
cursor: pointer;
padding: 16px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
width: auto;
min-width: 200px;
// Activated state - when node is selected/focused
&-activated {
border-color: #82a7fc;
}
// Dragging state - when node is being dragged
&-dragging {
opacity: 0.3;
}
// Block icon states - when showing order or regular block icons
&-block-icon,
&-block-order-icon {
width: 260px;
}
// Hover effects for better UX
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
// Focus state for accessibility
&:focus-within {
outline: 2px solid #82a7fc;
outline-offset: 2px;
}
.node-form {
transition: opacity 1s ease-in-out;
}
}
.node-render-before-render {
max-height: 1px;
.node-form {
opacity: 0;
}
}
.node-render-rendered {
max-height: 1px;
animation: node-rendered-transition 1s ease-out forwards;
.node-form {
opacity: 1;
}
}
@keyframes node-rendered-transition {
0% {
max-height: 1px;
}
100% {
max-height: 500px;
}
}
.node-render-removed {
max-height: 500px;
animation: node-removed-transition 0.3s ease-out forwards;
overflow: hidden;
padding: 0 16px;
transition: opacity 0.3s ease-out;
opacity: 0;
}
@keyframes node-removed-transition {
0% {
max-height: 500px;
padding: 16px;
}
100% {
max-height: 1px;
padding: 0 16px;
}
}
.node-render-border-transition {
outline: 2px solid transparent;
animation: node-border-appear-hide 0.8s ease-in-out forwards;
}
@keyframes node-border-appear-hide {
0% {
outline: 2px solid transparent;
}
50% {
outline: 2px solid #82a7fc;
}
100% {
outline: 2px solid transparent;
}
}
@@ -0,0 +1,34 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import '@flowgram.ai/fixed-layout-editor/index.css';
import './index.less';
import classNames from 'classnames';
import { FlowNodeEntity, useNodeRender } from '@flowgram.ai/fixed-layout-editor';
import { useNodeStatus } from '@/hooks/use-node-loading';
export const NodeRender = ({ node }: { node: FlowNodeEntity }) => {
const { onMouseEnter, onMouseLeave, form, dragging, isBlockOrderIcon, isBlockIcon, activated } =
useNodeRender();
const { className } = useNodeStatus();
return (
<div
className={classNames('node-render', className, {
'node-render-activated': activated,
'node-render-dragging': dragging,
'node-render-block-order-icon': isBlockOrderIcon,
'node-render-block-icon': isBlockIcon,
})}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<div className="node-form">{form?.render()}</div>
</div>
);
};
@@ -0,0 +1,90 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
.thinking-node {
background: #fff;
border: 1px solid oklch(80.9% .105 251.813);
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
cursor: pointer;
padding: 16px;
background-color: oklch(97% .014 254.604);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
width: auto;
min-width: 200px;
.node-form {
transition: opacity 1s ease-in-out;
}
}
.thinking-node-loading {
&::before {
content: '';
position: absolute;
top: -3px;
left: -3px;
right: -3px;
bottom: -3px;
background: linear-gradient(90deg,
transparent,
transparent,
transparent,
transparent,
rgba(59, 130, 246, 0.6),
rgba(96, 165, 250, 0.6),
transparent,
transparent,
transparent,
transparent,
transparent,
transparent);
background-size: 400% 100%;
border-radius: 8px;
z-index: -1;
animation: flowing-border 5s linear infinite;
pointer-events: none;
}
&::after {
content: '';
position: absolute;
top: -3px;
left: -3px;
right: -3px;
bottom: -3px;
background: linear-gradient(90deg,
transparent,
transparent,
transparent,
transparent,
rgba(59, 130, 246, 0.08),
rgba(96, 165, 250, 0.08),
transparent,
transparent,
transparent,
transparent,
transparent,
transparent);
background-size: 400% 100%;
border-radius: 8px;
z-index: 1;
animation: flowing-border 5s linear infinite;
pointer-events: none;
}
}
@keyframes flowing-border {
0% {
background-position: 0% 0;
}
100% {
background-position: 400% 0;
}
}
@@ -0,0 +1,20 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import classNames from 'classnames';
import { useNodeRender } from '@flowgram.ai/fixed-layout-editor';
import './index.less';
import { useNodeStatus } from '@/hooks/use-node-loading';
export const ThinkingNode = () => {
const { form } = useNodeRender();
const { className } = useNodeStatus();
return (
<div className={classNames('thinking-node', 'thinking-node-loading', className)}>
<div className="node-form">{form?.render()}</div>
</div>
);
};
@@ -0,0 +1,90 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { CSSProperties, useEffect, useState } from 'react';
import { usePlaygroundTools, useClientContext } from '@flowgram.ai/fixed-layout-editor';
import { Minimap } from './minimap';
export const Tools = () => {
const { history } = useClientContext();
const tools = usePlaygroundTools();
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const buttonStyle: CSSProperties = {
border: '1px solid #e0e0e0',
borderRadius: '4px',
cursor: 'pointer',
padding: '4px',
color: '#141414',
background: '#e1e3e4',
};
useEffect(() => {
const disposable = history.undoRedoService.onChange(() => {
setCanUndo(history.canUndo());
setCanRedo(history.canRedo());
});
return () => disposable.dispose();
}, [history]);
return (
<>
<Minimap />
<div
style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 16, display: 'flex', gap: 8 }}
>
<button style={buttonStyle} onClick={() => tools.zoomin()}>
ZoomIn
</button>
<button style={buttonStyle} onClick={() => tools.zoomout()}>
ZoomOut
</button>
<span
style={{
...buttonStyle,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'default',
width: 40,
}}
>
{Math.floor(tools.zoom * 100)}%
</span>
<button style={buttonStyle} onClick={() => tools.fitView()}>
FitView
</button>
<button style={buttonStyle} onClick={() => tools.changeLayout()}>
ChangeLayout
</button>
<button
style={{
...buttonStyle,
cursor: canUndo ? 'pointer' : 'not-allowed',
color: canUndo ? '#141414' : '#b1b1b1',
}}
onClick={() => history.undo()}
disabled={!canUndo}
>
Undo
</button>
<button
style={{
...buttonStyle,
cursor: canRedo ? 'pointer' : 'not-allowed',
color: canRedo ? '#141414' : '#b1b1b1',
}}
onClick={() => history.redo()}
disabled={!canRedo}
>
Redo
</button>
</div>
</>
);
};
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { MinimapRender } from '@flowgram.ai/minimap-plugin';
export const Minimap = () => (
<div
style={{
position: 'absolute',
left: 16,
bottom: 58,
zIndex: 100,
width: 218,
}}
>
<MinimapRender
containerStyles={{
pointerEvents: 'auto',
position: 'relative',
top: 'unset',
right: 'unset',
bottom: 'unset',
left: 'unset',
}}
inactiveStyle={{
opacity: 1,
scale: 1,
translateX: 0,
translateY: 0,
}}
/>
</div>
);
@@ -0,0 +1,294 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';
const initSchema = {
nodes: [
{
id: 'start_0',
type: 'start',
data: {
title: '开始',
},
},
],
};
const processStartSchema = {
nodes: [
{
id: 'start_0',
type: 'start',
data: {
title: '开始',
content: '天气穿衣建议工作流',
},
},
{
id: 'thinking_0',
type: 'thinking',
data: {
text: '正在生成天气穿衣建议工作流...业务流程:1.进行输入处理 2.获取天气数据 3.生成穿衣建议 4.整理输出。我需要根据这些步骤来生成天气穿衣建议工作流核心节点...',
},
},
],
};
const addCoreNodesSchema = {
nodes: [
{
id: 'start_0',
type: 'start',
data: {
title: '开始',
content: '天气穿衣建议工作流',
},
},
{
id: 'validate_input_0',
type: 'custom',
data: {
title: '输入处理节点',
content: '验证并清理城市名称输入 - validate_city_input()',
},
},
{
id: 'thinking_1',
type: 'thinking',
data: {
text: '正在生成错误检查节点与天气检查节点...',
},
},
{
id: 'fetch_weather_0',
type: 'custom',
data: {
title: '天气数据获取',
content: '调用wttr.in API获取天气信息 - fetch_weather_data()',
},
},
{
id: 'generate_suggestion_0',
type: 'custom',
data: {
title: '穿衣建议生成',
content: '基于天气数据生成穿衣建议 - generate_clothing_suggestion()',
},
},
{
id: 'format_response_0',
type: 'custom',
data: {
title: '输出整理节点',
content: '格式化最终回答 - format_final_response()',
},
},
{
id: 'end_0',
type: 'end',
data: {
title: '结束',
content: '返回格式化的穿衣建议',
},
},
],
};
const completeWorkflowLoadingSchema = {
nodes: [
{
id: 'start_0',
type: 'start',
data: {
title: '开始',
content: '天气穿衣建议工作流',
},
},
{
id: 'validate_input_0',
type: 'custom',
data: {
title: '输入处理节点',
content: '验证并清理城市名称输入 - validate_city_input()',
},
},
{
id: 'condition_0',
type: 'condition',
data: {
title: '输入验证',
content: '检查输入验证是否有错误',
},
blocks: [
{
id: 'thinking_2',
type: 'thinking',
data: {
text: '天气数据获取节点生成中',
},
},
{
id: 'thinking_3',
type: 'thinking',
data: {
text: '格式化错误节点生成中',
},
},
],
},
{
id: 'condition_1',
type: 'condition',
data: {
title: '天气数据检查',
content: '检查天气数据获取是否成功',
},
blocks: [
{
id: 'thinking_4',
type: 'thinking',
data: {
text: '穿衣建议生成节点生成中',
},
},
{
id: 'thinking_5',
type: 'thinking',
data: {
text: '格式化错误节点生成中',
},
},
],
},
{
id: 'format_response_0',
type: 'custom',
data: {
title: '输出整理节点',
content: '格式化最终回答 - format_final_response()',
},
},
{
id: 'end_0',
type: 'end',
data: {
title: '结束',
content: '返回格式化的穿衣建议',
},
},
],
};
const completeWorkflowSchema = {
nodes: [
{
id: 'start_0',
type: 'start',
data: {
title: '开始',
content: '天气穿衣建议工作流',
},
},
{
id: 'validate_input_0',
type: 'custom',
data: {
title: '输入处理节点',
content: '验证并清理城市名称输入 - validate_city_input()',
},
},
{
id: 'condition_0',
type: 'condition',
data: {
title: '输入验证',
content: '检查输入验证是否有错误',
},
blocks: [
{
id: 'block_0',
type: 'block',
blocks: [
{
id: 'fetch_weather_0',
type: 'custom',
data: {
title: '天气数据获取',
content: '调用wttr.in API获取天气信息 - fetch_weather_data()',
},
},
{
id: 'format_data_0',
type: 'custom',
data: {
title: '格式化数据',
content: '天气数据提取并进行处理格式化',
},
},
],
},
{
id: 'format_error_0',
type: 'custom',
data: {
title: '格式化错误',
content: '直接跳转到输出格式化',
},
},
],
},
{
id: 'condition_1',
type: 'condition',
data: {
title: '天气数据检查',
content: '检查天气数据获取是否成功',
},
blocks: [
{
id: 'generate_suggestion_0',
type: 'custom',
data: {
title: '穿衣建议生成',
content: '基于天气数据生成穿衣建议 - generate_clothing_suggestion()',
},
},
{
id: 'format_error_1',
type: 'custom',
data: {
title: '格式化错误',
content: '跳转到输出格式化',
},
},
],
},
{
id: 'format_response_0',
type: 'custom',
data: {
title: '输出整理节点',
content: '格式化最终回答 - format_final_response()',
},
},
{
id: 'end_0',
type: 'end',
data: {
title: '结束',
content: '返回格式化的穿衣建议',
},
},
],
};
export const exampleSchemas: FlowDocumentJSON[] = [
initSchema,
processStartSchema,
addCoreNodesSchema,
completeWorkflowLoadingSchema,
completeWorkflowSchema,
];
@@ -0,0 +1,419 @@
#!/usr/bin/env python3
"""
Weather-based Clothing Advisor using LangGraph
A workflow that fetches weather data and provides clothing recommendations.
"""
import json
import re
import requests
from typing import Dict, Any, Optional, TypedDict
from dataclasses import dataclass
from langgraph.graph import Graph, StateGraph, END
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
import os
# State definition for the workflow
class WorkflowState(TypedDict):
"""State structure for the weather clothing advisor workflow"""
city_name: str
validated_city: str
weather_data: Dict[str, Any]
temperature: float
weather_condition: str
clothing_suggestion: str
final_response: str
error_message: Optional[str]
@dataclass
class WeatherInfo:
"""Weather information data structure"""
temperature: float
condition: str
humidity: int
wind_speed: float
description: str
class WeatherClothingAdvisor:
"""Main class for the weather-based clothing advisor workflow"""
def __init__(self, openai_api_key: Optional[str] = None):
"""
Initialize the advisor with OpenAI API key
Args:
openai_api_key: OpenAI API key for LLM calls
"""
self.openai_api_key = openai_api_key or os.getenv("OPENAI_API_KEY")
if self.openai_api_key:
self.llm = ChatOpenAI(
api_key=self.openai_api_key,
model="gpt-3.5-turbo",
temperature=0.7
)
else:
self.llm = None
print("Warning: No OpenAI API key provided. Using rule-based suggestions.")
def validate_city_input(self, state: WorkflowState) -> WorkflowState:
"""
Node 1: Input processing and validation
Validates and cleans the city name input
Args:
state: Current workflow state
Returns:
Updated state with validated city name
"""
city_name = state.get("city_name", "").strip()
# Basic validation
if not city_name:
state["error_message"] = "城市名称不能为空"
return state
# Remove special characters and normalize
validated_city = re.sub(r'[^\w\s-]', '', city_name)
validated_city = validated_city.strip()
if len(validated_city) < 2:
state["error_message"] = "请输入有效的城市名称"
return state
state["validated_city"] = validated_city
state["error_message"] = None
print(f"✓ 城市名称验证通过: {validated_city}")
return state
def fetch_weather_data(self, state: WorkflowState) -> WorkflowState:
"""
Node 2: Weather data retrieval
Fetches weather information from wttr.in API
Args:
state: Current workflow state
Returns:
Updated state with weather data
"""
if state.get("error_message"):
return state
validated_city = state["validated_city"]
try:
# Use wttr.in API for weather data
url = f"http://wttr.in/{validated_city}?format=j1"
headers = {
'User-Agent': 'WeatherClothingAdvisor/1.0'
}
print(f"🌤️ 正在获取 {validated_city} 的天气数据...")
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
weather_data = response.json()
# Extract current weather information
current_condition = weather_data["current_condition"][0]
temperature_c = float(current_condition["temp_C"])
weather_desc = current_condition["weatherDesc"][0]["value"]
humidity = int(current_condition["humidity"])
wind_speed = float(current_condition["windspeedKmph"])
state["weather_data"] = weather_data
state["temperature"] = temperature_c
state["weather_condition"] = weather_desc
weather_info = WeatherInfo(
temperature=temperature_c,
condition=weather_desc,
humidity=humidity,
wind_speed=wind_speed,
description=f"{temperature_c}°C, {weather_desc}, 湿度 {humidity}%, 风速 {wind_speed}km/h"
)
print(f"✓ 天气数据获取成功: {weather_info.description}")
except requests.exceptions.RequestException as e:
state["error_message"] = f"获取天气数据失败: {str(e)}"
print(f"❌ 天气数据获取失败: {str(e)}")
except (KeyError, ValueError, IndexError) as e:
state["error_message"] = f"天气数据解析失败: {str(e)}"
print(f"❌ 天气数据解析失败: {str(e)}")
return state
def generate_clothing_suggestion(self, state: WorkflowState) -> WorkflowState:
"""
Node 3: Clothing suggestion generation
Generates clothing recommendations based on weather data
Args:
state: Current workflow state
Returns:
Updated state with clothing suggestions
"""
if state.get("error_message"):
return state
temperature = state["temperature"]
weather_condition = state["weather_condition"]
city_name = state["validated_city"]
print(f"🧥 正在生成穿衣建议...")
if self.llm:
# Use LLM for intelligent suggestions
try:
prompt = f"""
作为一个专业的穿衣顾问,请根据以下天气信息为用户提供详细的穿衣建议:
城市:{city_name}
温度:{temperature}°C
天气状况:{weather_condition}
请提供:
1. 上身穿着建议
2. 下身穿着建议
3. 外套建议
4. 配饰建议(如帽子、围巾等)
5. 鞋子建议
6. 特别注意事项
请用简洁明了的中文回答,语气友好自然。
"""
messages = [
SystemMessage(content="你是一个专业的穿衣顾问,擅长根据天气情况提供实用的穿衣建议。"),
HumanMessage(content=prompt)
]
response = self.llm.invoke(messages)
state["clothing_suggestion"] = response.content
except Exception as e:
print(f"⚠️ LLM调用失败,使用规则建议: {str(e)}")
state["clothing_suggestion"] = self._get_rule_based_suggestion(temperature, weather_condition)
else:
# Use rule-based suggestions
state["clothing_suggestion"] = self._get_rule_based_suggestion(temperature, weather_condition)
print("✓ 穿衣建议生成完成")
return state
def _get_rule_based_suggestion(self, temperature: float, weather_condition: str) -> str:
"""
Generate rule-based clothing suggestions
Args:
temperature: Temperature in Celsius
weather_condition: Weather condition description
Returns:
Clothing suggestion string
"""
suggestions = []
# Temperature-based suggestions
if temperature < 0:
suggestions.append("🧥 上身:保暖内衣 + 毛衣 + 厚外套")
suggestions.append("👖 下身:保暖裤 + 厚裤子")
suggestions.append("🧤 配饰:帽子、围巾、手套必备")
elif temperature < 10:
suggestions.append("🧥 上身:长袖衬衫 + 毛衣 + 外套")
suggestions.append("👖 下身:长裤")
suggestions.append("🧣 配饰:围巾、帽子")
elif temperature < 20:
suggestions.append("👔 上身:长袖衬衫 + 薄外套")
suggestions.append("👖 下身:长裤或牛仔裤")
suggestions.append("🧢 配饰:可选择轻薄围巾")
elif temperature < 25:
suggestions.append("👕 上身:长袖T恤或薄衬衫")
suggestions.append("👖 下身:长裤或休闲裤")
else:
suggestions.append("👕 上身:短袖T恤或薄衬衫")
suggestions.append("🩳 下身:短裤或薄长裤")
suggestions.append("🧴 注意:防晒和补水")
# Weather condition adjustments
weather_lower = weather_condition.lower()
if any(word in weather_lower for word in ['rain', 'shower', '', '阵雨']):
suggestions.append("☔ 特别提醒:携带雨伞或穿防水外套")
elif any(word in weather_lower for word in ['snow', '']):
suggestions.append("❄️ 特别提醒:穿防滑鞋,注意保暖")
elif any(word in weather_lower for word in ['wind', '']):
suggestions.append("💨 特别提醒:选择防风外套")
# Shoe suggestions
if temperature < 5:
suggestions.append("👢 鞋子:保暖靴子或厚底鞋")
elif temperature > 25:
suggestions.append("👟 鞋子:透气运动鞋或凉鞋")
else:
suggestions.append("👟 鞋子:舒适的运动鞋或休闲鞋")
return "\n".join(suggestions)
def format_final_response(self, state: WorkflowState) -> WorkflowState:
"""
Node 4: Output formatting
Formats the final response with weather info and clothing suggestions
Args:
state: Current workflow state
Returns:
Updated state with formatted final response
"""
if state.get("error_message"):
state["final_response"] = f"❌ 错误:{state['error_message']}"
return state
city_name = state["validated_city"]
temperature = state["temperature"]
weather_condition = state["weather_condition"]
clothing_suggestion = state["clothing_suggestion"]
final_response = f"""
🌍 {city_name} 天气穿衣建议
📊 当前天气情况:
• 温度:{temperature}°C
• 天气:{weather_condition}
👔 穿衣建议:
{clothing_suggestion}
💡 温馨提示:
建议出门前再次确认天气变化,根据个人体感适当调整穿着。
""".strip()
state["final_response"] = final_response
print("✓ 最终回答格式化完成")
return state
def create_workflow(self) -> StateGraph:
"""
Create and configure the LangGraph workflow
Returns:
Configured StateGraph workflow
"""
# Create the graph
workflow = StateGraph(WorkflowState)
# Add nodes
workflow.add_node("validate_input", self.validate_city_input)
workflow.add_node("fetch_weather", self.fetch_weather_data)
workflow.add_node("generate_suggestion", self.generate_clothing_suggestion)
workflow.add_node("format_response", self.format_final_response)
# Define the flow
workflow.set_entry_point("validate_input")
# Add conditional edges
workflow.add_conditional_edges(
"validate_input",
lambda state: "fetch_weather" if not state.get("error_message") else "format_response"
)
workflow.add_conditional_edges(
"fetch_weather",
lambda state: "generate_suggestion" if not state.get("error_message") else "format_response"
)
workflow.add_conditional_edges(
"generate_suggestion",
lambda state: "format_response" if not state.get("error_message") else "format_response"
)
workflow.add_edge("format_response", END)
return workflow.compile()
def get_clothing_advice(self, city_name: str) -> str:
"""
Main method to get clothing advice for a city
Args:
city_name: Name of the city to get weather and clothing advice for
Returns:
Formatted clothing advice string
"""
print(f"🚀 开始为 '{city_name}' 生成穿衣建议...")
# Create and run the workflow
workflow = self.create_workflow()
# Initial state
initial_state = WorkflowState(
city_name=city_name,
validated_city="",
weather_data={},
temperature=0.0,
weather_condition="",
clothing_suggestion="",
final_response="",
error_message=None
)
# Execute the workflow
result = workflow.invoke(initial_state)
return result["final_response"]
def main():
"""Main function to demonstrate the weather clothing advisor"""
print("🌤️ 天气穿衣建议助手")
print("=" * 50)
# Initialize the advisor
advisor = WeatherClothingAdvisor()
# Example usage
cities = ["北京", "上海", "广州", "深圳"]
for city in cities:
print(f"\n{'='*20} {city} {'='*20}")
try:
advice = advisor.get_clothing_advice(city)
print(advice)
except Exception as e:
print(f"❌ 处理 {city} 时出错: {str(e)}")
print("\n" + "-" * 60)
# Interactive mode
print("\n🎯 交互模式 (输入 'quit' 退出)")
while True:
try:
city_input = input("\n请输入城市名称: ").strip()
if city_input.lower() in ['quit', 'exit', '退出', 'q']:
print("👋 再见!")
break
if city_input:
advice = advisor.get_clothing_advice(city_input)
print(f"\n{advice}")
else:
print("❌ 请输入有效的城市名称")
except KeyboardInterrupt:
print("\n👋 再见!")
break
except Exception as e:
print(f"❌ 出现错误: {str(e)}")
if __name__ == "__main__":
main()
@@ -0,0 +1,64 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
.update-schema-button-container {
// Position and layout
position: absolute;
top: 50px;
right: 50px;
z-index: 100;
display: flex;
flex-direction: column;
gap: 12px;
}
.update-schema-button {
// Size and spacing
width: auto;
min-width: 120px;
height: 44px;
padding: 12px 24px;
// Typography
font-size: 14px;
font-weight: 600;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #ffffff;
// Background and borders
background: #667eea;
border: none;
border-radius: 8px;
// Shadow and effects
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1);
// Interaction states
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
// Prevent text selection
user-select: none;
-webkit-user-select: none;
// Hover state - subtle enhancement
&:hover {
background: #5a6fd8;
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.5), 0 3px 6px rgba(0, 0, 0, 0.15);
}
// Active/Click state - gentle press effect
&:active {
background: #4c5bc4;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3), 0 1px 2px rgba(0, 0, 0, 0.1);
}
// Button content styling
.button-content {
display: flex;
align-items: center;
gap: 8px;
}
}
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { useState } from 'react';
import { FlowDocumentJSON, useService } from '@flowgram.ai/fixed-layout-editor';
import './index.less';
import { WorkflowLoadSchemaService } from '@/services';
import { exampleSchemas } from './example-schemas';
export const UpdateSchema = () => {
const loadSchemaService = useService(WorkflowLoadSchemaService);
const [currentSchemaIndex, setCurrentSchemaIndex] = useState<number>(0);
const handleUpdateSchema = (): void => {
const currentSchema: FlowDocumentJSON = exampleSchemas[currentSchemaIndex];
// Update the document with current schema
loadSchemaService.load(currentSchema);
// Move to next schema index, cycle back to 0 when reaching the end
setCurrentSchemaIndex((currentSchemaIndex + 1) % exampleSchemas.length);
};
const handleForceUpdateSchema = (): void => {
const currentSchema: FlowDocumentJSON = exampleSchemas[currentSchemaIndex];
// Update the document with current schema
loadSchemaService.forceLoad(currentSchema);
// Move to next schema index, cycle back to 0 when reaching the end
setCurrentSchemaIndex((currentSchemaIndex + 1) % exampleSchemas.length);
};
return (
<div className="update-schema-button-container">
<button onClick={handleUpdateSchema} className="update-schema-button">
<span className="button-content">{`更新 ${currentSchemaIndex}/${exampleSchemas.length}`}</span>
</button>
<button onClick={handleForceUpdateSchema} className="update-schema-button">
<span className="button-content">{`强制更新 ${currentSchemaIndex}/${exampleSchemas.length}`}</span>
</button>
</div>
);
};
@@ -0,0 +1,10 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
.form-render-content {
font-size: 14px;
line-height: 1.6;
color: #666666;
}
@@ -0,0 +1,13 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { Field } from '@flowgram.ai/fixed-layout-editor';
import './index.less';
export const ContentField = () => (
<Field<string> name="content">
{({ field }) => <div className="form-render-content">{field.value}</div>}
</Field>
);
@@ -0,0 +1,73 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
.thinking-title {
font-size: 14px;
font-weight: 500;
color: #8d8d8d;
}
.thinking-text {
display: flex;
align-items: flex-start;
flex-direction: column;
gap: 4px;
border-radius: 8px;
line-height: 1.5;
font-size: 14px;
.thinking-content {
flex: 1;
word-break: break-word;
color: #8d8d8d;
}
.cursor {
font-weight: bold;
animation: blink 1s infinite;
}
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
@keyframes bounce {
0%,
80%,
100% {
transform: scale(0);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
@keyframes blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: 0;
}
}
@@ -0,0 +1,59 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import React, { useState, useEffect } from 'react';
import { Field } from '@flowgram.ai/fixed-layout-editor';
import './index.less';
interface ThinkingTextProps {
thinking?: string;
}
// ThinkingText component with typewriter effect
const ThinkingText: React.FC<ThinkingTextProps> = ({ thinking }) => {
const [displayedText, setDisplayedText] = useState<string>('');
const [currentIndex, setCurrentIndex] = useState<number>(0);
// Reset animation when thinking text changes
useEffect(() => {
setDisplayedText('');
setCurrentIndex(0);
}, [thinking]);
// Typewriter effect for thinking text
useEffect(() => {
if (!thinking || currentIndex >= thinking.length) {
return;
}
const timer = setTimeout(() => {
setDisplayedText((prev: string) => prev + (thinking?.[currentIndex] || ''));
setCurrentIndex((prev: number) => prev + 1);
}, 50); // 50ms delay between each character
return () => clearTimeout(timer);
}, [thinking, currentIndex]);
if (!thinking) {
return null;
}
return (
<div className="thinking-text">
<div className="thinking-title">:</div>
<div>
<span className="thinking-content">{displayedText}</span>
<span className="cursor">
{currentIndex < (thinking?.length || 0) && <span className="cursor">|</span>}
</span>
</div>
</div>
);
};
export const ThinkingTextField = () => (
<Field<string> name="text">{({ field }) => <ThinkingText thinking={field.value} />}</Field>
);
@@ -0,0 +1,15 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
.form-render-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 12px;
color: #333333;
display: flex;
gap: 8px;
justify-content: flex-start;
align-items: center;
}
@@ -0,0 +1,28 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { Field } from '@flowgram.ai/fixed-layout-editor';
import { useNodeStatus } from '@/hooks/use-node-loading';
import { LoadingDots } from '@/components/loading-dots';
import './index.less';
export const TitleField = () => {
const { loading } = useNodeStatus();
return (
<Field<string> name="title">
{({ field }) => (
<div className="form-render-title">
<span>{field.value}</span>
{loading && (
<span>
<LoadingDots />
</span>
)}
</div>
)}
</Field>
);
};
@@ -0,0 +1,96 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import '@flowgram.ai/fixed-layout-editor/index.css';
import { useMemo } from 'react';
import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
import { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';
import { FixedLayoutProps, FlowRendererKey } from '@flowgram.ai/fixed-layout-editor';
import { WorkflowLoadSchemaService } from '@/services';
import { nodeRegistries } from '@/nodes';
import { ThinkingNode } from '@/components/thinking-node';
import { NodeRender } from '@/components/node-render';
import { FormRender } from '@/components/form-render';
export function useEditorProps(): FixedLayoutProps {
return useMemo<FixedLayoutProps>(
() => ({
plugins: () => [
createMinimapPlugin({
disableLayer: true,
enableDisplayAllNodes: true,
canvasStyle: {
canvasWidth: 200,
canvasHeight: 100,
canvasPadding: 50,
},
}),
],
nodeRegistries,
initialData: {
nodes: [],
},
materials: {
renderDefaultNode: NodeRender,
components: {
...defaultFixedSemiMaterials,
[FlowRendererKey.DRAG_NODE]: () => <></>,
[FlowRendererKey.BRANCH_ADDER]: () => <></>,
[FlowRendererKey.ADDER]: () => <></>,
},
renderNodes: {
ThinkingNode,
},
},
onAllLayersRendered: (ctx) => {
setTimeout(() => {
ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));
}, 10);
},
onBind: ({ bind }) => {
bind(WorkflowLoadSchemaService).toSelf().inSingletonScope();
},
/**
* Get the default node registry, which will be merged with the 'nodeRegistries'
* 提供默认的节点注册,这个会和 nodeRegistries 做合并
*/
getNodeDefaultRegistry(type) {
return {
type,
meta: {
defaultExpanded: true,
},
formMeta: {
/**
* Render form
*/
render: FormRender,
},
};
},
/**
* Redo/Undo enable
*/
history: {
enable: true,
enableChangeNode: true, // Listen Node engine data change
onApply: (ctx) => {
if (ctx.document.disposed) return;
// Listen change to trigger auto save
// console.log('auto save: ', ctx.document.toJSON());
},
},
/**
* Node engine enable, you can configure formMeta in the FlowNodeRegistry
*/ nodeEngine: {
enable: true,
},
}),
[]
);
}
@@ -0,0 +1,55 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { useEffect, useState } from 'react';
import { FlowNodeFormData, FormModelV2, useNodeRender } from '@flowgram.ai/fixed-layout-editor';
interface NodeStatus {
loading: boolean;
className: string;
}
const NodeStatusKey = 'status';
export const useNodeStatus = () => {
const { node } = useNodeRender();
const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();
const formStatus = formModel.getValueIn<NodeStatus>(NodeStatusKey);
const [loading, setLoading] = useState(formStatus?.loading ?? false);
const [className, setClassName] = useState(formStatus?.className ?? '');
// 初始化表单值
useEffect(() => {
const initSize = formModel.getValueIn<{ width: number; height: number }>(NodeStatusKey);
if (!initSize) {
formModel.setValueIn(NodeStatusKey, {
loading: false,
});
}
}, [formModel]);
// 同步表单外部值变化:初始化/undo/redo/协同
useEffect(() => {
const disposer = formModel.onFormValuesChange(({ name }) => {
if (name !== NodeStatusKey && name !== '') {
return;
}
const newStatus = formModel.getValueIn<NodeStatus>(NodeStatusKey);
if (!newStatus) {
return;
}
setLoading(newStatus.loading);
setClassName(newStatus.className);
});
return () => disposer.dispose();
}, [formModel]);
return {
loading,
className,
};
};
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import {
FlowNodeBaseType,
FlowNodeEntity,
FlowNodeJSON,
FlowNodeMeta,
FlowNodeRegistry,
FlowNodeSplitType,
} from '@flowgram.ai/fixed-layout-editor';
export const ConditionNodeRegistry: FlowNodeRegistry<FlowNodeMeta> = {
type: 'condition',
extend: FlowNodeSplitType.DYNAMIC_SPLIT,
onBlockChildCreate(
originParent: FlowNodeEntity,
blockData: FlowNodeJSON,
addedNodes: FlowNodeEntity[] = [] // 新创建的节点都要存在这里
) {
const { document } = originParent;
const parent = document.getNode(`$inlineBlocks$${originParent.id}`);
const blockNode = document.addNode(
{
id: `$block$${blockData.id}`,
type: FlowNodeBaseType.BLOCK,
parent,
},
addedNodes
);
const createdNode = document.addNode(
{
...blockData,
type: blockData.type || FlowNodeBaseType.BLOCK,
parent: blockNode,
},
addedNodes
);
addedNodes.push(blockNode, createdNode);
return createdNode;
},
};
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { FlowNodeMeta, FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';
export const CustomNodeRegistry: FlowNodeRegistry<FlowNodeMeta> = {
type: 'custom',
meta: {},
// onAdd() {
// return {
// id: `custom_${nanoid(5)}`,
// type: 'custom',
// data: {
// title: 'Custom',
// content: 'this is custom content',
// },
// };
// },
};
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
/**
* Copyright (c) 202 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import type { FlowNodeMeta, FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';
import { ThinkingNodeRegistry } from './thinking';
import { CustomNodeRegistry } from './custom';
import { ConditionNodeRegistry } from './condition';
export const nodeRegistries: FlowNodeRegistry<FlowNodeMeta>[] = [
ConditionNodeRegistry,
CustomNodeRegistry,
ThinkingNodeRegistry,
];
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { FlowNodeMeta, FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';
import { ThinkingTextField } from '@/fields/thinking-text-field';
import { LoadingDots } from '@/components/loading-dots';
export const ThinkingNodeRegistry: FlowNodeRegistry<FlowNodeMeta> = {
type: 'thinking',
meta: {
renderKey: 'ThinkingNode',
},
formMeta: {
render: () => (
<>
<div
style={{
marginBottom: 16,
}}
>
<LoadingDots />
</div>
<ThinkingTextField />
</>
),
},
};
@@ -0,0 +1,6 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
export { WorkflowLoadSchemaService } from './load-schema-service';
@@ -0,0 +1,170 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import {
delay,
EntityManager,
FlowDocument,
FlowDocumentJSON,
FlowNodeBaseType,
FlowNodeEntity,
FlowNodeFormData,
FlowOperationBaseService,
FormModelV2,
inject,
injectable,
Playground,
} from '@flowgram.ai/fixed-layout-editor';
import { WorkflowLoadSchemaUtils } from './utils';
import { SchemaPatch, SchemaPatchData } from './type';
@injectable()
export class WorkflowLoadSchemaService {
@inject(FlowDocument) private document: FlowDocument;
@inject(EntityManager) private entityManager: EntityManager;
@inject(FlowOperationBaseService) private operationService: FlowOperationBaseService;
@inject(Playground) private playground: Playground;
private currentSchema: FlowDocumentJSON = {
nodes: [],
};
// constructor() {
// (window as any).WorkflowLoadSchemaService = this;
// }
public async load(schema: FlowDocumentJSON): Promise<void> {
const schemaPatch: SchemaPatch = WorkflowLoadSchemaUtils.createSchemaPatch(
this.currentSchema,
schema
);
this.currentSchema = schema;
await this.applySchemaPatch(schemaPatch);
this.document.fromJSON(schema);
}
public forceLoad(schema: FlowDocumentJSON): void {
this.currentSchema = schema;
this.document.fromJSON(schema);
}
private async applySchemaPatch(schemaPatch: SchemaPatch): Promise<void> {
await this.applyRemovePatch(schemaPatch.remove);
await delay(300);
await this.applyCreatePatch(schemaPatch.create);
await this.playground.config.fitView(this.document.root.bounds.pad(30));
}
private async applyCreatePatch(createSchemaPatchData: SchemaPatchData[]): Promise<void> {
const skipNodeIDs: Set<string> = new Set();
for (const nodePatchData of createSchemaPatchData) {
// 跳过 block 节点
if (skipNodeIDs.has(nodePatchData.nodeID)) {
continue;
}
const parentNode = this.getNode(nodePatchData.parentID);
// 特殊处理 condition 节点
if (parentNode?.flowNodeType === 'condition') {
const blocksSchema = createSchemaPatchData
.filter((item) => item.parentID === parentNode.id)
.map((item) => {
skipNodeIDs.add(item.nodeID);
return item.schema;
});
const blocks = this.document.addInlineBlocks(parentNode, blocksSchema);
await Promise.all(blocks.map((block) => this.createNodeMotion(block)));
continue;
}
// 更新节点数据
const isExist = Boolean(this.getNode(nodePatchData.nodeID));
const node = this.createNode(nodePatchData);
if (!isExist) {
// 新增节点动画
await this.createNodeMotion(node);
}
}
}
private createNode(patchData: SchemaPatchData): FlowNodeEntity {
const parent = this.getNode(patchData.parentID) ?? this.document.root;
if (parent?.flowNodeType === 'condition') {
// 特殊处理 condition 节点
const blocks = this.document.addInlineBlocks(parent, [patchData.schema]);
return blocks.find((block) => block.flowNodeType === patchData.schema.type) ?? blocks[0];
} else if (patchData.fromNodeID) {
return this.operationService.addFromNode(patchData.fromNodeID, patchData.schema);
} else {
return this.document.addNode({
...patchData.schema,
parent,
});
}
}
private getNode(id?: string): FlowNodeEntity | undefined {
if (!id) {
return undefined;
}
return this.document.getNode(id);
}
private async createNodeMotion(node: FlowNodeEntity): Promise<void> {
// 隐藏节点
this.setNodeStatus(node, { loading: true, className: 'node-render-before-render' });
this.document.fireRender();
await delay(20);
// 展示节点动画
this.setNodeStatus(node, { loading: true, className: 'node-render-rendered' });
await delay(180);
// 滚动到节点位置
this.playground.scrollToView({
bounds: node.bounds,
scrollToCenter: true,
});
// 高亮节点边框
this.setNodeStatus(node, { loading: true, className: 'node-render-border-transition' });
await delay(800);
// 移除节点边框高亮
this.setNodeStatus(node, { loading: false, className: '' });
}
private async removeNodeMotion(node: FlowNodeEntity): Promise<void> {
// 隐藏节点
this.setNodeStatus(node, { loading: false, className: 'node-render-removed' });
this.document.fireRender();
await delay(300);
}
private async applyRemovePatch(removeNodeIDs: string[]): Promise<void> {
await Promise.all(
removeNodeIDs.map(async (nodeID) => {
const node = this.entityManager.getEntityById<FlowNodeEntity>(nodeID);
const parent = node?.parent;
if (node) {
await this.removeNodeMotion(node);
node.dispose();
}
if (parent?.flowNodeType === FlowNodeBaseType.BLOCK && !parent.blocks.length) {
parent.dispose();
}
})
);
}
private setNodeStatus(
node: FlowNodeEntity,
status: {
loading: boolean;
className: string;
}
): void {
const formModel = node.getData(FlowNodeFormData)?.getFormModel<FormModelV2>();
formModel?.setValueIn('status', status);
}
}
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { FlowNodeJSON } from '@flowgram.ai/fixed-layout-editor';
export interface SchemaPatchData {
nodeID: string;
schema: FlowNodeJSON;
parentID?: string;
index?: number;
fromNodeID?: string;
}
export interface SchemaPatch {
create: SchemaPatchData[];
remove: string[];
}
@@ -0,0 +1,92 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { FlowDocumentJSON, FlowNodeJSON } from '@flowgram.ai/fixed-layout-editor';
import { SchemaPatch, SchemaPatchData } from './type';
export namespace WorkflowLoadSchemaUtils {
const createSchemaPatchDataMap = (params: {
nodeSchemas: FlowNodeJSON[];
parentID?: string;
schemaPatchDataMap?: Map<string, SchemaPatchData>;
}): Map<string, SchemaPatchData> => {
const { nodeSchemas, parentID, schemaPatchDataMap = new Map() } = params;
nodeSchemas.forEach((nodeSchema: FlowNodeJSON, index: number) => {
const prevNodeSchema = nodeSchemas[index - 1];
const processedSchema: FlowNodeJSON = {
...nodeSchema,
blocks: [],
};
const schemaPatchData: SchemaPatchData = {
nodeID: nodeSchema.id,
schema: processedSchema,
parentID,
index,
fromNodeID: prevNodeSchema?.id,
};
schemaPatchDataMap.set(nodeSchema.id, schemaPatchData);
if (nodeSchema.blocks) {
createSchemaPatchDataMap({
nodeSchemas: nodeSchema.blocks,
parentID: nodeSchema.id,
schemaPatchDataMap,
});
}
});
return schemaPatchDataMap;
};
export const createSchemaPatch = (
prevSchema: FlowDocumentJSON,
schema: FlowDocumentJSON
): SchemaPatch => {
const prevSchemaPatchDataMap = createSchemaPatchDataMap({
nodeSchemas: prevSchema.nodes,
});
const currentSchemaPatchDataMap = createSchemaPatchDataMap({
nodeSchemas: schema.nodes,
});
const prevNodeIDs: string[] = Array.from(prevSchemaPatchDataMap.keys());
const currentNodeIDs: string[] = Array.from(currentSchemaPatchDataMap.keys());
const createNodeIDs: string[] = currentNodeIDs.filter((id) => {
if (!prevSchemaPatchDataMap.has(id)) {
return true;
}
const prevSchemaPatchData = prevSchemaPatchDataMap.get(id)!;
const currentSchemaPatchData = currentSchemaPatchDataMap.get(id)!;
return (
prevSchemaPatchData.parentID !== currentSchemaPatchData.parentID ||
prevSchemaPatchData.fromNodeID !== currentSchemaPatchData.fromNodeID
);
});
const removeNodeIDs: string[] = prevNodeIDs.filter((id) => {
if (!currentSchemaPatchDataMap.has(id)) {
return true;
}
const prevSchemaPatchData = prevSchemaPatchDataMap.get(id)!;
const currentSchemaPatchData = currentSchemaPatchDataMap.get(id)!;
return (
prevSchemaPatchData.parentID !== currentSchemaPatchData.parentID ||
prevSchemaPatchData.fromNodeID !== currentSchemaPatchData.fromNodeID
);
});
const createSchemaPatches: SchemaPatchData[] = createNodeIDs
.map((id) => currentSchemaPatchDataMap.get(id)!)
.filter(Boolean);
const schemaPatch: SchemaPatch = {
create: createSchemaPatches,
remove: removeNodeIDs,
};
console.log('@debug schemaPatch', schemaPatch);
return schemaPatch;
};
}
@@ -0,0 +1,38 @@
{
"extends": "@flowgram.ai/ts-config/tsconfig.flow.path.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"experimentalDecorators": true,
"target": "es2020",
"module": "esnext",
"strictPropertyInitialization": false,
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"skipLibCheck": true,
"noUnusedLocals": true,
"noImplicitAny": true,
"allowJs": true,
"resolveJsonModule": true,
"types": [
"node"
],
"jsx": "react-jsx",
"lib": [
"es6",
"dom",
"es2020",
"es2019.Array"
],
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
],
}
},
"include": [
"./src"
],
}

Some files were not shown because too many files have changed in this diff Show More