ソースを参照

feat(AI): 新增 DifyWorkflow 任务支持

- 新增通用 DifyWorkflow 任务处理器(difyWorkflowJobHandler)
- 实现了对 Dify 工作流的调用和结果处理- 更新了官方文档,增加了 DifyWorkflow 任务的使用说明- 在示例项目中添加了 Dify 相关的配置和调用代码
xuxueli 6 ヶ月 前
コミット
ec421fad37

+ 42 - 14
doc/XXL-JOB官方文档.md

@@ -1172,7 +1172,7 @@ public void demoJobHandler() throws Exception {
 为方便用户参考与快速使用,提供 “通用执行器” 并内置多个Bean模式任务Handler,可以直接配置使用,如下:
 
 **通用执行器说明:**
-- 执行器:xxl-job-executor-sample
+- AppName:xxl-job-executor-sample
 - 执行器代码:
   - xxl-job-executor-sample-springboot:springboot版本
   - xxl-job-executor-sample-frameless:无框架版本
@@ -1194,7 +1194,7 @@ public void demoJobHandler() throws Exception {
 为方便用户参考与快速使用,提供 “AI执行器” 并内置多个Bean模式任务Handler,与spring-ai集成打通,支持快速开发AI类任务,如下:
 
 **AI执行器说明:**
-- 执行器:xxl-job-executor-sample-ai
+- AppName:xxl-job-executor-sample-ai
 - 执行器代码:xxl-job-executor-sample-springboot-ai
 
 **执行器内置任务列表:**
@@ -1205,8 +1205,19 @@ public void demoJobHandler() throws Exception {
     "prompt": "{模型prompt,可选信息}"
 }
 ```
-依赖1:参考 [Ollama本地化部署大模型](https://www.xuxueli.com/blog/?blog=./notebook/13-AI/%E4%BD%BF%E7%94%A8Ollama%E6%9C%AC%E5%9C%B0%E5%8C%96%E9%83%A8%E7%BD%B2DeepSeek.md) ,本文示例部署“qwen2.5:1.5b”模型,也可自定选择其他模型版本。 
-依赖2:启动示例 “AI执行器” 相关配置文件说明如下:
+- b、difyWorkflowJobHandler:DifyWorkflow 任务,支持自定义inputs、user等输入信息,示例参数如下;
+```
+{
+    "inputs":{                      // inputs 为dify工作流任务参数;参数不固定,结合各自 workflow 自行定义。
+        "input":"{用户输入信息}"      // 该参数为示例变量,需要 workflow 的“开始”节点 自定义参数 “input”,可自行调整或删除。
+    },
+    "user": "{用户标识,选填}"
+}
+```
+
+依赖1:参考 [Ollama本地化部署大模型](https://www.xuxueli.com/blog/?blog=./notebook/13-AI/%E4%BD%BF%E7%94%A8Ollama%E6%9C%AC%E5%9C%B0%E5%8C%96%E9%83%A8%E7%BD%B2DeepSeek.md) ,执行器示例部署“qwen2.5:1.5b”模型,也可自定选择其他模型版本。
+依赖2:参考 [使用DeepSeek与Dify搭建AI助手](https://www.xuxueli.com/blog/?blog=./notebook/13-AI/%E4%BD%BF%E7%94%A8DeepSeek%E4%B8%8EDify%E6%90%AD%E5%BB%BAAI%E5%8A%A9%E6%89%8B.md),执行器示例新建Dify DifyWork应用,并在开始节点添加“input”参数,可结合实际情况调整。
+依赖3:启动示例 “AI执行器” 相关配置文件说明如下:
 ```
 ### ollama 配置
 spring.ai.ollama.base-url=http://localhost:11434
@@ -1214,6 +1225,12 @@ spring.ai.ollama.chat.enabled=true
 ### Model模型配置;注意,此处配置模型版本、必须本地先通过ollama进行安装运行。
 spring.ai.ollama.chat.options.model=qwen2.5:1.5b
 spring.ai.ollama.chat.options.temperature=0.8
+
+### dify 配置;选择相关 workflow 应用,切换 “访问API” 页面获取 url 地址信息.
+dify.base-url=http://localhost/v1
+### dify api-key;选择相关 workflow 应用并进入 “访问API” 页面,右上角 “API 密钥” 入口获取 api-key。
+dify.api-key={自行获取并修改}
+
 ```
 
 
@@ -2491,24 +2508,35 @@ public void execute() {
 - b、版本3.x开始要求Jdk17;版本2.x及以下支持Jdk1.8。如对Jdk版本有诉求,可选择接入不同版本;
 
 ### 7.38 版本 v3.0.1 Release Notes[规划中]
-- 1、【新增】新增“AI执行器”示例,并与spring-ai集成打通;内置一系列AIXxlJob,支持快速开发AI类任务(xxl-job-executor-sample-springboot-ai)。
-- 2、【新增】新增通用OllamaChat任务(ollamaJobHandler),支持自定义prompt、input等输入信息,示例参数如下;
+- 1、【新增】新增“AI执行器”示例,并与spring-ai、Ollama、Dify等集成打通;内置一系列AIXxlJob,支持快速开发AI类任务(xxl-job-executor-sample-springboot-ai)。
+```
+// 说明:ollamaJobHandler 内置在“AI执行器(AppName = xxl-job-executor-sample-ai)”中,需要先新建执行器;可参考如下SQL或自行创建:
+INSERT INTO `xxl_job_group`(`app_name`, `title`, `address_type`, `address_list`, `update_time`)
+    VALUES ('xxl-job-executor-sample-ai', 'AI执行器Sample', 0, NULL, now());
+```
+- 2、【新增】新增通用 OllamaChat 任务(ollamaJobHandler),支持自定义prompt、input等输入信息,示例参数如下;
 ```
 {
     "input": "{输入信息,必填信息}",
     "prompt": "{模型prompt,可选信息}"
 }
 ```
-说明:ollamaJobHandler 内置在“AI执行器(AppName = xxl-job-executor-sample-ai)”中,需要先新建执行器;可参考如下SQL或自行创建:
+- 3、【新增】新增通用 DifyWorkflow 任务(difyWorkflowJobHandler),支持自定义inputs、user等输入信息,示例参数如下;
 ```
-INSERT INTO `xxl_job_group`(`app_name`, `title`, `address_type`, `address_list`, `update_time`)
-    VALUES ('xxl-job-executor-sample-ai', 'AI执行器Sample', 0, NULL, now());
+{
+    "inputs":{                      // inputs 为dify工作流任务参数;参数不固定,结合各自 workflow 自行定义。
+        "input":"{用户输入信息}"      // 该参数为示例变量,需要 workflow 的“开始”节点 自定义参数 “input”,可自行调整或删除。
+    },
+    "user": "{用户标识,选填}"
+}
 ```
-- 3、【修复】任务操作逻辑优化,修复边界情况下逻辑中断问题(ISSUE-2081)。
-- 4、【修复】调度中心Cron前端组件优化,解决week配置与后端兼容性问题(ISSUE-2220)。
-- 5、【优化】Glue IDE调整,版本回溯支持查看修改时间;
-- 6、【优化】任务RollingLog调整,XSS过滤支持白名单排出,提升日志易读性;
-- 7、【升级】多个项目依赖升级至较新稳定版本,涉及 gson、groovy、spring/springboot 等;
+- 4、【修复】任务操作逻辑优化,修复边界情况下逻辑中断问题(ISSUE-2081)。
+- 5、【修复】调度中心Cron前端组件优化,解决week配置与后端兼容性问题(ISSUE-2220)。
+- 6、【修复】合并PR-3708、PR-3704:修复固定速度模式计算,下次执行时间计算不准问题(间隔秒数超过Int时触发)。
+- 7、【优化】Glue IDE调整,版本回溯支持查看修改时间;
+- 8、【优化】任务RollingLog调整,XSS过滤支持白名单排出,提升日志易读性;
+- 9、【升级】多个项目依赖升级至较新稳定版本,涉及 gson、groovy、spring/springboot、mysql 等;
+
 
 
 ### 7.39 版本 v3.0.2 Release Notes[规划中]

+ 5 - 5
pom.xml

@@ -30,17 +30,17 @@
 		<maven-gpg-plugin.version>3.2.7</maven-gpg-plugin.version>
 		<!-- base -->
 		<slf4j-api.version>2.0.17</slf4j-api.version>
-		<junit-jupiter.version>5.12.1</junit-jupiter.version>
+		<junit-jupiter.version>5.12.2</junit-jupiter.version>
 		<jakarta.annotation-api.version>3.0.0</jakarta.annotation-api.version>
 		<!-- net -->
 		<netty.version>4.2.0.Final</netty.version>
-		<gson.version>2.12.1</gson.version>
+		<gson.version>2.13.1</gson.version>
 		<!-- spring -->
-		<spring-boot.version>3.4.4</spring-boot.version>
-		<spring.version>6.2.5</spring.version>
+		<spring-boot.version>3.4.5</spring-boot.version>
+		<spring.version>6.2.6</spring.version>
 		<!-- db -->
 		<mybatis-spring-boot-starter.version>3.0.4</mybatis-spring-boot-starter.version>
-		<mysql-connector-j.version>9.2.0</mysql-connector-j.version>
+		<mysql-connector-j.version>9.3.0</mysql-connector-j.version>
 		<!-- dynamic language -->
 		<groovy.version>4.0.26</groovy.version>
 	</properties>

+ 8 - 0
xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/pom.xml

@@ -17,6 +17,7 @@
 
     <properties>
         <spring-ai.version>1.0.0-M6</spring-ai.version>
+        <dify-java-client.version>1.0.7</dify-java-client.version>
     </properties>
 
     <dependencyManagement>
@@ -57,6 +58,13 @@
             <version>${spring-ai.version}</version>
         </dependency>
 
+        <!-- dify -->
+        <dependency>
+            <groupId>io.github.imfangs</groupId>
+            <artifactId>dify-java-client</artifactId>
+            <version>${dify-java-client.version}</version>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 118 - 3
xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/controller/IndexController.java

@@ -1,11 +1,20 @@
 package com.xxl.job.executor.controller;//package com.xxl.job.executor.mvc.controller;
 
-import com.xxl.job.executor.config.XxlJobConfig;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.github.imfangs.dify.client.DifyClientFactory;
+import io.github.imfangs.dify.client.DifyWorkflowClient;
+import io.github.imfangs.dify.client.callback.WorkflowStreamCallback;
+import io.github.imfangs.dify.client.enums.ResponseMode;
+import io.github.imfangs.dify.client.event.*;
+import io.github.imfangs.dify.client.model.workflow.WorkflowRunRequest;
+import io.github.imfangs.dify.client.model.workflow.WorkflowRunResponse;
 import jakarta.annotation.Resource;
 import jakarta.servlet.http.HttpServletResponse;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -13,6 +22,12 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.ResponseBody;
 import reactor.core.publisher.Flux;
+import reactor.core.publisher.FluxSink;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
 
 @Controller
 @EnableAutoConfiguration
@@ -23,15 +38,16 @@ public class IndexController {
     @RequestMapping("/")
     @ResponseBody
     String index() {
-        return "xxl job executor running.";
+        return "xxl job ai executor running.";
     }
 
 
+    // --------------------------------- ollama chat ---------------------------------
+
     @Resource
     private ChatClient chatClient;
     private static String prompt = "你好,你是一个研发工程师,擅长解决技术类问题。";
 
-
     /**
      * ChatClient 简单调用
      */
@@ -60,4 +76,103 @@ public class IndexController {
                 .content();
     }
 
+
+    // --------------------------------- dify workflow ---------------------------------
+
+    @Value("${dify.api-key}")
+    private String apiKey;
+    @Value("${dify.base-url}")
+    private String baseUrl;
+
+    @GetMapping("/dify/simple")
+    @ResponseBody
+    public String difySimple(@RequestParam(required = false, value = "input") String input) throws IOException {
+
+        Map<String, Object> inputs = new HashMap<>();
+        inputs.put("input", input);
+
+        // request
+        WorkflowRunRequest request = WorkflowRunRequest.builder()
+                .inputs(inputs)
+                .responseMode(ResponseMode.BLOCKING)
+                .user("user-123")
+                .build();
+
+        // invoke
+        DifyWorkflowClient workflowClient = DifyClientFactory.createWorkflowClient(baseUrl, apiKey);
+        WorkflowRunResponse response = workflowClient.runWorkflow(request);
+
+        // response
+        return write2Json(response.getData().getOutputs());
+    }
+
+    private String write2Json(Object obj) {
+        if (obj == null) {
+            return "null";
+        }
+        try {
+            return new ObjectMapper().writeValueAsString(obj);
+        } catch (JsonProcessingException e) {
+            return obj.toString();
+        }
+    }
+
+    @GetMapping( "/dify/stream")
+    public Flux<String> difyStream(@RequestParam(required = false, value = "input") String input) {
+
+        Map<String, Object> inputs = new HashMap<>();
+        inputs.put("input", input);
+
+        // request
+        WorkflowRunRequest request = WorkflowRunRequest.builder()
+                .inputs(inputs)
+                .responseMode(ResponseMode.STREAMING)
+                .user("user-123")
+                .build();
+
+        // invoke
+        DifyWorkflowClient workflowClient = DifyClientFactory.createWorkflowClient(baseUrl, apiKey);
+        return Flux.create(new Consumer<FluxSink<String>>() {
+            @Override
+            public void accept(FluxSink<String> sink) {
+                try {
+                    workflowClient.runWorkflowStream(request, new WorkflowStreamCallback() {
+                        @Override
+                        public void onWorkflowStarted(WorkflowStartedEvent event) {
+                            sink.next("工作流开始: " + write2Json(event.getData()));
+                        }
+
+                        @Override
+                        public void onNodeStarted(NodeStartedEvent event) {
+                            sink.next("节点开始: " + write2Json(event.getData()));
+                        }
+
+                        @Override
+                        public void onNodeFinished(NodeFinishedEvent event) {
+                            sink.next("节点完成: " + write2Json(event.getData().getOutputs()));
+                        }
+
+                        @Override
+                        public void onWorkflowFinished(WorkflowFinishedEvent event) {
+                            sink.next("工作流完成: " + write2Json(event.getData().getOutputs()));
+                            sink.complete();
+                        }
+
+                        @Override
+                        public void onError(ErrorEvent event) {
+                            sink.error(new RuntimeException(event.getMessage()));
+                        }
+
+                        @Override
+                        public void onException(Throwable throwable) {
+                            sink.error(throwable);
+                        }
+                    });
+                } catch (IOException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        });
+    }
+
 }

+ 135 - 12
xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/jobhandler/AIXxlJob.java

@@ -3,10 +3,17 @@ package com.xxl.job.executor.jobhandler;
 import com.xxl.job.core.context.XxlJobHelper;
 import com.xxl.job.core.handler.annotation.XxlJob;
 import com.xxl.job.core.util.GsonTool;
+import io.github.imfangs.dify.client.DifyClientFactory;
+import io.github.imfangs.dify.client.DifyWorkflowClient;
+import io.github.imfangs.dify.client.enums.ResponseMode;
+import io.github.imfangs.dify.client.model.workflow.WorkflowRunRequest;
+import io.github.imfangs.dify.client.model.workflow.WorkflowRunResponse;
 import jakarta.annotation.Resource;
 import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
+import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -23,7 +30,7 @@ public class AIXxlJob {
     /**
      * 1、ollama Chat任务
      *
-     *  参数示例:
+     *  参数示例:格式见 OllamaParam
      *  <pre>
      *      {
      *          "input": "{输入信息,必填信息}",
@@ -43,16 +50,14 @@ public class AIXxlJob {
             return;
         }
 
-        // param parse
-        String prompt = "你是一个研发工程师,擅长解决技术类问题。";
-        String input;
+        // ollama param
+        OllamaParam ollamaParam = null;
         try {
-            Map<String, String> paramMap =GsonTool.fromJson(param, Map.class);
-            if (paramMap.containsKey("prompt")) {
-                prompt = paramMap.get("prompt");
+            ollamaParam = GsonTool.fromJson(param, OllamaParam.class);
+            if (ollamaParam.getPrompt() == null) {
+                ollamaParam.setPrompt("你是一个研发工程师,擅长解决技术类问题。");
             }
-            input = paramMap.get("input");
-            if (input == null || input.trim().isEmpty()) {
+            if (ollamaParam.getInput() == null || ollamaParam.getInput().trim().isEmpty()) {
                 XxlJobHelper.log("input is empty.");
 
                 XxlJobHelper.handleFail();
@@ -65,15 +70,133 @@ public class AIXxlJob {
         }
 
         // input
-        XxlJobHelper.log("<br><br><b>【Input】: " + input + "</b><br><br>");
+        XxlJobHelper.log("<br><br><b>【Input】: " + ollamaParam.getInput()+ "</b><br><br>");
 
         // invoke
         String result = chatClient
-                .prompt(prompt)
-                .user(input)
+                .prompt(ollamaParam.getPrompt())
+                .user(ollamaParam.getInput())
                 .call()
                 .content();
         XxlJobHelper.log("<br><br><b>【Output】: " + result+ "</b><br><br>");
     }
 
+    private static class OllamaParam{
+        private String input;
+        private String prompt;
+
+        public String getInput() {
+            return input;
+        }
+
+        public void setInput(String input) {
+            this.input = input;
+        }
+
+        public String getPrompt() {
+            return prompt;
+        }
+
+        public void setPrompt(String prompt) {
+            this.prompt = prompt;
+        }
+    }
+
+
+
+    @Value("${dify.api-key}")
+    private String apiKey;
+    @Value("${dify.base-url}")
+    private String baseUrl;
+
+    /**
+     * 2、dify Workflow任务
+     *
+     *  参数示例:格式见 DifyParam
+     *  <pre>
+     *      {
+     *          "inputs":{                      // inputs 为dify工作流任务参数;参数不固定,结合各自 workflow 自行定义。
+     *              "input":"{用户输入信息}"      // 该参数为示例变量,需要 workflow 的“开始”节点 自定义参数 “input”,可自行调整或删除。
+     *          },
+     *          "user": "{用户标识,选填}"
+     *      }
+     *  </pre>
+     */
+    @XxlJob("difyWorkflowJobHandler")
+    public void difyWorkflowJobHandler() throws Exception {
+
+        // param
+        String param = XxlJobHelper.getJobParam();
+        if (param==null || param.trim().isEmpty()) {
+            XxlJobHelper.log("param is empty.");
+            XxlJobHelper.handleFail();
+            return;
+        }
+
+        // param parse
+        DifyParam difyParam;
+        try {
+            difyParam =GsonTool.fromJson(param, DifyParam.class);
+            if (difyParam.getInputs() == null) {
+                difyParam.setInputs(new HashMap<>());
+            }
+            if (difyParam.getUser() == null) {
+                difyParam.setUser("xxl-job");
+            }
+        } catch (Exception e) {
+            XxlJobHelper.log(e);
+            XxlJobHelper.handleFail();
+            return;
+        }
+
+
+        // dify param
+        XxlJobHelper.log("<br><br><b>【inputs】: " + difyParam.getInputs() + "</b><br><br>");
+
+        // dify request
+        WorkflowRunRequest request = WorkflowRunRequest.builder()
+                .inputs(difyParam.getInputs())
+                .responseMode(ResponseMode.BLOCKING)
+                .user(difyParam.getUser())
+                .build();
+
+        // dify invoke
+        DifyWorkflowClient workflowClient = DifyClientFactory.createWorkflowClient(baseUrl, apiKey);
+        WorkflowRunResponse response = workflowClient.runWorkflow(request);
+
+        // response
+        XxlJobHelper.log("<br><br><b>【Output】: " + response.getData().getOutputs()+ "</b><br><br>");
+    }
+
+    private static class DifyParam{
+
+        /**
+         * 输入参数,允许传入 App 定义的各变量值
+         */
+        private Map<String, Object> inputs;
+
+        /**
+         * 用户标识
+         */
+        private String user;
+
+        public Map<String, Object> getInputs() {
+            return inputs;
+        }
+
+        public void setInputs(Map<String, Object> inputs) {
+            this.inputs = inputs;
+        }
+
+        public String getUser() {
+            return user;
+        }
+
+        public void setUser(String user) {
+            this.user = user;
+        }
+
+    }
+
+
 }

+ 9 - 0
xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/resources/application.properties

@@ -3,6 +3,9 @@ server.port=8082
 # no web
 #spring.main.web-environment=false
 
+server.servlet.encoding.force=true
+server.servlet.encoding.charset=UTF-8
+
 # log config
 logging.config=classpath:logback.xml
 
@@ -34,3 +37,9 @@ spring.ai.ollama.chat.enabled=true
 ### chat model,must install it locally through ollama
 spring.ai.ollama.chat.options.model=qwen2.5:1.5b
 spring.ai.ollama.chat.options.temperature=0.8
+
+### dify url
+dify.base-url=http://localhost/v1
+### dify api-key
+dify.api-key=app-OUVgNUOQRIMokfmuJvBJoUTN
+