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

- 新增通用 DifyWorkflow 任务处理器(difyWorkflowJobHandler)
- 实现了对 Dify 工作流的调用和结果处理- 更新了官方文档,增加了 DifyWorkflow 任务的使用说明- 在示例项目中添加了 Dify 相关的配置和调用代码
3.1.0-release
xuxueli 5 months ago
parent 07b58ac5c5
commit ec421fad37

@ -1172,7 +1172,7 @@ public void demoJobHandler() throws Exception {
为方便用户参考与快速使用,提供 “通用执行器” 并内置多个Bean模式任务Handler可以直接配置使用如下 为方便用户参考与快速使用,提供 “通用执行器” 并内置多个Bean模式任务Handler可以直接配置使用如下
**通用执行器说明:** **通用执行器说明:**
- 执行器xxl-job-executor-sample - AppNamexxl-job-executor-sample
- 执行器代码: - 执行器代码:
- xxl-job-executor-sample-springbootspringboot版本 - xxl-job-executor-sample-springbootspringboot版本
- xxl-job-executor-sample-frameless无框架版本 - xxl-job-executor-sample-frameless无框架版本
@ -1194,7 +1194,7 @@ public void demoJobHandler() throws Exception {
为方便用户参考与快速使用,提供 “AI执行器” 并内置多个Bean模式任务Handler与spring-ai集成打通支持快速开发AI类任务如下 为方便用户参考与快速使用,提供 “AI执行器” 并内置多个Bean模式任务Handler与spring-ai集成打通支持快速开发AI类任务如下
**AI执行器说明** **AI执行器说明**
- 执行器xxl-job-executor-sample-ai - AppNamexxl-job-executor-sample-ai
- 执行器代码xxl-job-executor-sample-springboot-ai - 执行器代码xxl-job-executor-sample-springboot-ai
**执行器内置任务列表:** **执行器内置任务列表:**
@ -1205,8 +1205,19 @@ public void demoJobHandler() throws Exception {
"prompt": "{模型prompt可选信息}" "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”模型,也可自定选择其他模型版本。 - b、difyWorkflowJobHandlerDifyWorkflow 任务支持自定义inputs、user等输入信息示例参数如下
依赖2启动示例 “AI执行器” 相关配置文件说明如下: ```
{
"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 配置 ### ollama 配置
spring.ai.ollama.base-url=http://localhost:11434 spring.ai.ollama.base-url=http://localhost:11434
@ -1214,6 +1225,12 @@ spring.ai.ollama.chat.enabled=true
### Model模型配置注意此处配置模型版本、必须本地先通过ollama进行安装运行。 ### Model模型配置注意此处配置模型版本、必须本地先通过ollama进行安装运行。
spring.ai.ollama.chat.options.model=qwen2.5:1.5b spring.ai.ollama.chat.options.model=qwen2.5:1.5b
spring.ai.ollama.chat.options.temperature=0.8 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,7 +2508,12 @@ public void execute() {
- b、版本3.x开始要求Jdk17版本2.x及以下支持Jdk1.8。如对Jdk版本有诉求可选择接入不同版本; - b、版本3.x开始要求Jdk17版本2.x及以下支持Jdk1.8。如对Jdk版本有诉求可选择接入不同版本;
### 7.38 版本 v3.0.1 Release Notes[规划中] ### 7.38 版本 v3.0.1 Release Notes[规划中]
- 1、【新增】新增“AI执行器”示例并与spring-ai集成打通内置一系列AIXxlJob支持快速开发AI类任务xxl-job-executor-sample-springboot-ai - 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等输入信息示例参数如下 - 2、【新增】新增通用 OllamaChat 任务ollamaJobHandler支持自定义prompt、input等输入信息示例参数如下
``` ```
{ {
@ -2499,16 +2521,22 @@ public void execute() {
"prompt": "{模型prompt可选信息}" "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、【修复】任务操作逻辑优化修复边界情况下逻辑中断问题(ISSUE-2081)。
- 4、【修复】调度中心Cron前端组件优化解决week配置与后端兼容性问题(ISSUE-2220)。 - 5、【修复】调度中心Cron前端组件优化解决week配置与后端兼容性问题(ISSUE-2220)。
- 5、【优化】Glue IDE调整版本回溯支持查看修改时间 - 6、【修复】合并PR-3708、PR-3704修复固定速度模式计算下次执行时间计算不准问题间隔秒数超过Int时触发
- 6、【优化】任务RollingLog调整XSS过滤支持白名单排出提升日志易读性 - 7、【优化】Glue IDE调整版本回溯支持查看修改时间
- 7、【升级】多个项目依赖升级至较新稳定版本涉及 gson、groovy、spring/springboot 等; - 8、【优化】任务RollingLog调整XSS过滤支持白名单排出提升日志易读性
- 9、【升级】多个项目依赖升级至较新稳定版本涉及 gson、groovy、spring/springboot、mysql 等;
### 7.39 版本 v3.0.2 Release Notes[规划中] ### 7.39 版本 v3.0.2 Release Notes[规划中]

@ -30,17 +30,17 @@
<maven-gpg-plugin.version>3.2.7</maven-gpg-plugin.version> <maven-gpg-plugin.version>3.2.7</maven-gpg-plugin.version>
<!-- base --> <!-- base -->
<slf4j-api.version>2.0.17</slf4j-api.version> <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> <jakarta.annotation-api.version>3.0.0</jakarta.annotation-api.version>
<!-- net --> <!-- net -->
<netty.version>4.2.0.Final</netty.version> <netty.version>4.2.0.Final</netty.version>
<gson.version>2.12.1</gson.version> <gson.version>2.13.1</gson.version>
<!-- spring --> <!-- spring -->
<spring-boot.version>3.4.4</spring-boot.version> <spring-boot.version>3.4.5</spring-boot.version>
<spring.version>6.2.5</spring.version> <spring.version>6.2.6</spring.version>
<!-- db --> <!-- db -->
<mybatis-spring-boot-starter.version>3.0.4</mybatis-spring-boot-starter.version> <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 --> <!-- dynamic language -->
<groovy.version>4.0.26</groovy.version> <groovy.version>4.0.26</groovy.version>
</properties> </properties>

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

@ -1,11 +1,20 @@
package com.xxl.job.executor.controller;//package com.xxl.job.executor.mvc.controller; 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.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping; 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.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import reactor.core.publisher.Flux; 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 @Controller
@EnableAutoConfiguration @EnableAutoConfiguration
@ -23,15 +38,16 @@ public class IndexController {
@RequestMapping("/") @RequestMapping("/")
@ResponseBody @ResponseBody
String index() { String index() {
return "xxl job executor running."; return "xxl job ai executor running.";
} }
// --------------------------------- ollama chat ---------------------------------
@Resource @Resource
private ChatClient chatClient; private ChatClient chatClient;
private static String prompt = "你好,你是一个研发工程师,擅长解决技术类问题。"; private static String prompt = "你好,你是一个研发工程师,擅长解决技术类问题。";
/** /**
* ChatClient * ChatClient
*/ */
@ -60,4 +76,103 @@ public class IndexController {
.content(); .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);
}
}
});
}
} }

@ -3,10 +3,17 @@ package com.xxl.job.executor.jobhandler;
import com.xxl.job.core.context.XxlJobHelper; import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob; import com.xxl.job.core.handler.annotation.XxlJob;
import com.xxl.job.core.util.GsonTool; 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 jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
@ -23,7 +30,7 @@ public class AIXxlJob {
/** /**
* 1ollama Chat * 1ollama Chat
* *
* * OllamaParam
* <pre> * <pre>
* { * {
* "input": "{输入信息,必填信息}", * "input": "{输入信息,必填信息}",
@ -43,16 +50,14 @@ public class AIXxlJob {
return; return;
} }
// param parse // ollama param
String prompt = "你是一个研发工程师,擅长解决技术类问题。"; OllamaParam ollamaParam = null;
String input;
try { try {
Map<String, String> paramMap =GsonTool.fromJson(param, Map.class); ollamaParam = GsonTool.fromJson(param, OllamaParam.class);
if (paramMap.containsKey("prompt")) { if (ollamaParam.getPrompt() == null) {
prompt = paramMap.get("prompt"); ollamaParam.setPrompt("你是一个研发工程师,擅长解决技术类问题。");
} }
input = paramMap.get("input"); if (ollamaParam.getInput() == null || ollamaParam.getInput().trim().isEmpty()) {
if (input == null || input.trim().isEmpty()) {
XxlJobHelper.log("input is empty."); XxlJobHelper.log("input is empty.");
XxlJobHelper.handleFail(); XxlJobHelper.handleFail();
@ -65,15 +70,133 @@ public class AIXxlJob {
} }
// input // input
XxlJobHelper.log("<br><br><b>【Input】: " + input + "</b><br><br>"); XxlJobHelper.log("<br><br><b>【Input】: " + ollamaParam.getInput()+ "</b><br><br>");
// invoke // invoke
String result = chatClient String result = chatClient
.prompt(prompt) .prompt(ollamaParam.getPrompt())
.user(input) .user(ollamaParam.getInput())
.call() .call()
.content(); .content();
XxlJobHelper.log("<br><br><b>【Output】: " + result+ "</b><br><br>"); 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;
/**
* 2dify 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;
}
}
} }

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

Loading…
Cancel
Save