# 插件开发指南

**版本**: v1.0  
**日期**: 2025-01-13  
**目标**: 指导开发者快速上手节点插件开发

---

## 一、快速开始（5分钟创建你的第一个插件）

### 1.1 前提条件

- JDK 17+
- Maven 3.6+
- 基本的Java和Spring知识
- 已有Flowable DevOps项目运行环境

### 1.2 创建插件类

在 `src/main/java/com/flowable/devops/workflow/node/` 目录下创建新类：

```java
package com.flowable.devops.workflow.node;

import com.flowable.devops.entity.NodeType;
import com.flowable.devops.workflow.model.*;
import com.flowable.devops.workflow.plugin.NodePlugin;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

/**
 * 示例插件：文本处理节点
 * 
 * 功能：
 * - 转大写
 * - 转小写
 * - 反转字符串
 */
@Slf4j
@NodePlugin(
    id = "text_processor",
    name = "textProcessor",
    displayName = "Text Processor",
    category = NodeType.NodeCategory.TRANSFORM,
    version = "1.0.0",
    author = "Your Name",
    description = "文本处理工具，支持大小写转换和字符串反转",
    icon = "font-size",
    enabled = true
)
@Component
public class TextProcessorNode implements WorkflowNode {
    
    private final ObjectMapper objectMapper = new ObjectMapper();
    
    @Override
    public NodeType getMetadata() {
        NodeType nodeType = new NodeType();
        nodeType.setId("text_processor");
        nodeType.setName("textProcessor");
        nodeType.setDisplayName("Text Processor");
        nodeType.setCategory(NodeType.NodeCategory.TRANSFORM);
        nodeType.setIcon("font-size");
        nodeType.setDescription("文本处理工具");
        nodeType.setImplementationClass(this.getClass().getName());
        nodeType.setEnabled(true);
        
        // 字段定义
        nodeType.setFields(createFieldsJson());
        
        // 输出结构
        nodeType.setOutputSchema(createOutputSchemaJson());
        
        return nodeType;
    }
    
    /**
     * 创建字段定义
     */
    private com.fasterxml.jackson.databind.JsonNode createFieldsJson() {
        ArrayNode fields = objectMapper.createArrayNode();
        
        // 输入文本
        ObjectNode inputField = objectMapper.createObjectNode();
        inputField.put("name", "text");
        inputField.put("label", "Input Text");
        inputField.put("type", "text");
        inputField.put("required", true);
        inputField.put("supportsExpression", true);
        inputField.put("placeholder", "Enter text to process...");
        fields.add(inputField);
        
        // 操作类型
        ObjectNode operationField = objectMapper.createObjectNode();
        operationField.put("name", "operation");
        operationField.put("label", "Operation");
        operationField.put("type", "select");
        operationField.put("required", true);
        operationField.put("defaultValue", "uppercase");
        ArrayNode operations = objectMapper.createArrayNode();
        operations.add("uppercase");
        operations.add("lowercase");
        operations.add("reverse");
        operationField.set("options", operations);
        fields.add(operationField);
        
        return fields;
    }
    
    /**
     * 创建输出结构
     */
    private com.fasterxml.jackson.databind.JsonNode createOutputSchemaJson() {
        ObjectNode schema = objectMapper.createObjectNode();
        schema.put("type", "object");
        
        ObjectNode properties = objectMapper.createObjectNode();
        
        ObjectNode result = objectMapper.createObjectNode();
        result.put("type", "string");
        result.put("description", "处理后的文本");
        properties.set("result", result);
        
        ObjectNode operation = objectMapper.createObjectNode();
        operation.put("type", "string");
        operation.put("description", "执行的操作");
        properties.set("operation", operation);
        
        schema.set("properties", properties);
        return schema;
    }
    
    @Override
    public NodeExecutionResult execute(NodeInput input, NodeExecutionContext context) {
        LocalDateTime startTime = LocalDateTime.now();
        
        try {
            // 1. 获取输入参数
            String text = input.getStringRequired("text");
            String operation = input.getString("operation", "uppercase");
            
            log.info("执行文本处理: operation={}, text={}", operation, text);
            
            // 2. 执行操作
            String result;
            switch (operation) {
                case "uppercase":
                    result = text.toUpperCase();
                    break;
                case "lowercase":
                    result = text.toLowerCase();
                    break;
                case "reverse":
                    result = new StringBuilder(text).reverse().toString();
                    break;
                default:
                    throw new IllegalArgumentException("Unknown operation: " + operation);
            }
            
            // 3. 构建输出
            Map<String, Object> output = new HashMap<>();
            output.put("result", result);
            output.put("operation", operation);
            
            LocalDateTime endTime = LocalDateTime.now();
            log.info("文本处理完成: {} -> {}", text, result);
            
            return NodeExecutionResult.success(output, startTime, endTime);
            
        } catch (Exception e) {
            LocalDateTime endTime = LocalDateTime.now();
            log.error("文本处理失败: {}", e.getMessage(), e);
            return NodeExecutionResult.failed(e.getMessage(), e.getClass().getSimpleName(), startTime, endTime);
        }
    }
    
    @Override
    public void validate(NodeInput input) {
        // 验证必需参数
        input.getStringRequired("text");
        
        String operation = input.getString("operation");
        if (operation != null && 
            !operation.equals("uppercase") && 
            !operation.equals("lowercase") && 
            !operation.equals("reverse")) {
            throw new IllegalArgumentException("Invalid operation: " + operation);
        }
    }
}
```

### 1.3 测试插件

**重启应用**：

```bash
mvn spring-boot:run
```

**查看日志**：

```
========================================
开始初始化插件系统...
========================================
✓ 加载内置插件: 2 个
  ✓ http_request: HTTP Request (1.0.0)
  ✓ text_processor: Text Processor (1.0.0)
========================================
插件清单:
========================================
【API接口】(1 个)
  ✓ 📦 HTTP Request v1.0.0 - Flowable Team (INTERNAL)
【数据转换】(1 个)
  ✓ 📦 Text Processor v1.0.0 - Your Name (INTERNAL)
========================================
```

**调用API测试**：

```bash
# 获取所有节点类型
curl http://localhost:8080/api/node-types

# 应该能看到新的text_processor节点
```

---

## 二、核心概念

### 2.1 插件注解 (@NodePlugin)

```java
@NodePlugin(
    id = "unique_id",              // 必需：全局唯一ID
    name = "nodeName",             // 必需：代码引用名
    displayName = "Display Name",  // 必需：UI显示名
    category = NodeCategory.API,   // 必需：分类
    version = "1.0.0",            // 版本号
    author = "Your Name",         // 作者
    description = "描述",          // 功能描述
    icon = "api",                 // 图标
    enabled = true                // 默认启用
)
```

**参数说明**：

| 参数 | 类型 | 必需 | 说明 | 示例 |
|------|------|------|------|------|
| id | String | ✅ | 节点唯一ID | "http_request" |
| name | String | ✅ | 代码引用名 | "httpRequest" |
| displayName | String | ✅ | UI显示名 | "HTTP Request" |
| category | NodeCategory | ✅ | 分类 | API, DATABASE, LOGIC, NOTIFICATION, TRANSFORM, OTHER |
| version | String | ❌ | 版本号 | "1.0.0" |
| author | String | ❌ | 作者 | "Your Name" |
| description | String | ❌ | 描述 | "发送HTTP请求" |
| icon | String | ❌ | 图标 | "api", "database", "mail" |
| enabled | boolean | ❌ | 默认启用 | true |

### 2.2 节点接口 (WorkflowNode)

所有插件必须实现 `WorkflowNode` 接口：

```java
public interface WorkflowNode {
    
    /**
     * 获取节点元数据
     * 包含字段定义、输出结构等
     */
    NodeType getMetadata();
    
    /**
     * 执行节点
     * @param input 输入参数（表达式已解析）
     * @param context 执行上下文
     * @return 执行结果
     */
    NodeExecutionResult execute(NodeInput input, NodeExecutionContext context);
    
    /**
     * 验证节点配置（可选）
     * @param input 输入参数
     */
    default void validate(NodeInput input) {
        // 默认不验证
    }
}
```

### 2.3 节点元数据 (NodeType)

定义节点的UI表现和数据结构：

```java
NodeType nodeType = new NodeType();
nodeType.setId("my_node");
nodeType.setDisplayName("My Node");
nodeType.setCategory(NodeCategory.API);
nodeType.setFields(fieldsJson);        // 字段定义
nodeType.setOutputSchema(schemaJson);  // 输出结构
```

**字段定义示例**：

```json
[
  {
    "name": "url",
    "label": "URL",
    "type": "text",
    "required": true,
    "supportsExpression": true,
    "placeholder": "https://api.example.com"
  },
  {
    "name": "method",
    "label": "Method",
    "type": "select",
    "options": ["GET", "POST", "PUT", "DELETE"],
    "defaultValue": "GET"
  }
]
```

**输出结构示例** (JSON Schema):

```json
{
  "type": "object",
  "properties": {
    "statusCode": {
      "type": "number",
      "description": "HTTP状态码"
    },
    "body": {
      "type": "object",
      "description": "响应体"
    }
  }
}
```

### 2.4 执行结果 (NodeExecutionResult)

返回节点执行结果：

```java
// 成功
return NodeExecutionResult.success(output, startTime, endTime);

// 失败
return NodeExecutionResult.failed(errorMessage, errorType, startTime, endTime);

// 跳过
return NodeExecutionResult.skipped(reason, startTime, endTime);
```

---

## 三、最佳实践

### 3.1 节点设计原则

✅ **无状态设计**
```java
// ✅ 推荐：所有状态通过参数传递
public NodeExecutionResult execute(NodeInput input, NodeExecutionContext context) {
    String url = input.getString("url");
    // ...
}

// ❌ 禁止：在节点中保存状态
private String lastUrl;  // 不要这样！
```

✅ **幂等性**
```java
// 多次执行相同输入应得到相同结果
// 对于非幂等操作（如发送邮件），应该有幂等性保护机制
```

✅ **错误处理**
```java
try {
    // 业务逻辑
    return NodeExecutionResult.success(output, startTime, endTime);
} catch (Exception e) {
    log.error("执行失败: {}", e.getMessage(), e);
    return NodeExecutionResult.failed(e.getMessage(), e.getClass().getSimpleName(), startTime, endTime);
}
```

✅ **日志记录**
```java
log.info("开始执行HTTP请求: {}", url);
log.debug("请求参数: {}", params);
log.error("请求失败: {}", e.getMessage(), e);
```

### 3.2 字段定义最佳实践

**支持表达式**：
```json
{
  "name": "url",
  "supportsExpression": true,  // 允许使用 ${nodes.xxx.output.yyy}
  "supportsFieldMapping": true  // 允许使用字段映射选择器
}
```

**提供默认值**：
```json
{
  "name": "timeout",
  "type": "number",
  "defaultValue": 30000
}
```

**完整的字段类型**：
- `text`: 单行文本
- `textarea`: 多行文本
- `number`: 数字
- `boolean`: 布尔值
- `select`: 下拉选择
- `code`: 代码编辑器
- `key_value`: 键值对列表

### 3.3 输出结构设计

**清晰的层次结构**：
```json
{
  "type": "object",
  "properties": {
    "data": {
      "type": "object",
      "description": "主数据"
    },
    "metadata": {
      "type": "object",
      "description": "元数据"
    }
  }
}
```

**提供详细的字段说明**：
```json
{
  "statusCode": {
    "type": "number",
    "description": "HTTP状态码 (200-599)"
  }
}
```

---

## 四、高级功能

### 4.1 访问上游节点数据

```java
@Override
public NodeExecutionResult execute(NodeInput input, NodeExecutionContext context) {
    // 获取上游节点的输出
    Map<String, Object> upstreamData = context.getNodeOutput("upstream_node_id");
    
    if (upstreamData != null) {
        Object someValue = upstreamData.get("output");
        // 使用上游数据...
    }
    
    // ...
}
```

### 4.2 访问环境变量

```java
String apiKey = context.getEnv("API_KEY");
```

### 4.3 访问工作流变量

```java
Map<String, Object> workflowVars = context.getVariables();
Object inputData = workflowVars.get("input");
```

### 4.4 异步执行（高级）

```java
// 注意：第一期不支持，使用同步执行
// 第二期可以使用CompletableFuture实现异步
```

---

## 五、测试

### 5.1 单元测试

```java
@SpringBootTest
public class TextProcessorNodeTest {
    
    @Autowired
    private TextProcessorNode node;
    
    @Test
    public void testUppercase() {
        // 准备输入
        Map<String, Object> config = Map.of(
            "text", "hello",
            "operation", "uppercase"
        );
        NodeInput input = new NodeInput(config);
        
        // 准备上下文
        NodeExecutionContext context = NodeExecutionContext.builder()
            .nodeId("test_node")
            .build();
        
        // 执行
        NodeExecutionResult result = node.execute(input, context);
        
        // 验证
        assertEquals(NodeExecutionResult.ExecutionStatus.SUCCESS, result.getStatus());
        assertEquals("HELLO", result.getOutput().get("result"));
    }
}
```

### 5.2 集成测试

创建完整的工作流进行测试：

```json
{
  "name": "测试文本处理",
  "nodes": [
    {
      "id": "node1",
      "type": "text_processor",
      "config": {
        "text": "Hello World",
        "operation": "uppercase"
      }
    }
  ],
  "edges": []
}
```

---

## 六、常见问题

### Q1: 如何添加新的字段类型？

A: 修改前端的字段渲染组件，后端只需要在 `fields` JSON中定义即可。

### Q2: 节点可以调用外部服务吗？

A: 可以，但建议使用 WebClient（异步）而不是 RestTemplate（同步）。

### Q3: 如何处理大文件？

A: 避免在节点输出中保存大文件，使用文件引用（URL或路径）。

### Q4: 节点可以访问数据库吗？

A: 可以通过 Spring 注入 Repository，但不推荐直接访问，建议通过 Service 层。

### Q5: 如何debug插件？

A: 
1. 增加日志输出
2. 使用IDE断点调试
3. 查看节点执行日志表

---

## 七、示例插件库

### 7.1 数据库查询节点

参考：`DatabaseQueryNode.java`（待实现）

### 7.2 发送邮件节点

参考：`SendEmailNode.java`（待实现）

### 7.3 Slack通知节点

参考：`SlackNotificationNode.java`（待实现）

---

## 八、发布插件

### 8.1 内置插件

直接提交代码到主仓库即可。

### 8.2 外部插件（第二期）

1. 打包为JAR
2. 上传到插件市场
3. 用户下载安装

---

**下一步**：查看 `06-插件化架构设计.md` 了解系统架构。
