├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── chat │ │ └── dify4j │ │ ├── ChatDify4jApplication.java │ │ ├── chatcomplete │ │ ├── event │ │ │ └── ChatCompleteEvent.java │ │ └── listener │ │ │ ├── ChatCompleteEventListener.java │ │ │ └── IChatCompleteEventListener.java │ │ ├── config │ │ └── ChatDify4jAutoConfiguration.java │ │ ├── controller │ │ └── SseController.java │ │ ├── customer │ │ └── rebuild │ │ │ ├── event │ │ │ └── CustomerRebuildAnswerEvent.java │ │ │ └── listener │ │ │ ├── CustomerRebuildAnswerListener.java │ │ │ └── ICustomerRebuildAnswerListener.java │ │ ├── enums │ │ └── ChatUrl.java │ │ ├── handler │ │ └── DifyApplicationChatHandler.java │ │ ├── model │ │ ├── DifyApplicationRequestPayload.java │ │ └── DifyApplicationResponse.java │ │ ├── processor │ │ ├── ResponseProcessor.java │ │ └── impl │ │ │ ├── ChatResponseProcessor.java │ │ │ └── WorkflowResponseProcessor.java │ │ └── properties │ │ ├── ChatUrlInitializer.java │ │ └── ChatUrlProperties.java └── resources │ └── application.properties └── test └── java └── com └── chat └── dify4j └── ChatDify4jApplicationTests.java /README.md: -------------------------------------------------------------------------------- 1 | # java对接dify应用的API 2 | 3 | # 项目简介 4 | [Dify](https://cloud.dify.ai/)是一款开源的大语言模型(LLM) 应用开发平台。它融合了后端即服务(Backend as Service)和 LLMOps 的理念,使开发者可以快速搭建生产级的生成式 AI 应用。 5 | 6 | 本项目旨在对接 Dify 大模型不同应用的 API,从而**对接自己业务系统**,实现与 Dify 应用的对话流处理,将对话结果流式返回给前端,并将对话结果分发给开发者自行处理。 7 | 8 | 由于是为对接本人所参与的业务系统,有些功能还不是很健壮和通用,后续有精力争取完善 9 | 目前是一个微服务的方式,有时间准备做一个`springboot starter`的方式 10 | 11 | ## 主要功能 12 | 13 | - **对话流处理**: 根据不同的对话类型:【对话型应用】和【Workflow应用】,通过 Dify 的 API 处理用户输入,并返回相应的响应 14 | - **多处理器支持**: 实现`ResponseProcessor`,处理不同的对话类型,方便日后系统拓展新的对话类型 15 | - **基于 SSE (Server-Sent Events)**: 处理和推送来自 Dify 平台的对话响应,确保流式数据处理和实时更新 16 | - **将每一轮对话结束的结果发送给开发者**: 单轮结束返回完整的回答内容以及对话源信息 17 | 18 | ## 使用说明 19 | 20 | > [!IMPORTANT] 21 | > 确保在请求时使用有效的 Dify 应用 `apiKey` 22 | 23 | ### 配置文件 24 | ```properties 25 | # 对话型应用URL 26 | dify.chat-url=https://api.dify.ai/v1/chat-messages 27 | # Workflow应用URL 28 | dify.flow-url=https://api.dify.ai/v1/workflows/run 29 | 30 | # 开启代理 本地代理或正向代理 31 | dify.proxy-enabled=false 32 | # 代理地址 33 | dify.proxy-host=127.0.0.1 34 | # 代理端口 35 | dify.proxy-port=7890 36 | ``` 37 | 38 | 39 | 40 | 41 | ### 示例代码 42 | - 方法调用 43 | 44 | ```java 45 | @Autowired 46 | private DifyApplicationChatHandler chatHandler; 47 | 48 | public void chat() throws JsonProcessingException { 49 | // 对话型应用:chat Workflow应用:flow 50 | String type = "chat"; 51 | 52 | // dify apikey 53 | String apiKey = "your-dify-api-key"; 54 | 55 | // 对话型应用的用户输入 56 | String query = "用户输入的查询"; 57 | 58 | // Dify App 定义的各变量值。 inputs 参数包含了多组键值对(Key/Value pairs),每组的键对应一个特定变量,每组的值则是该变量的具体值 59 | String inputs = "{\"key1\":\"value1\", \"key2\":\"key2\"}"; 60 | 61 | // 会话ID 62 | String conversationId = "会话ID"; 63 | 64 | // 用户标识 65 | String userId = "用户标识"; 66 | 67 | SseEmitter emitter = chatHandler.handle(type, apiKey, query, inputs, conversationId, userId); 68 | } 69 | ``` 70 | 71 | - controller demo 72 | 73 | ```java 74 | @Autowired 75 | private DifyApplicationChatHandler chatHandler; 76 | 77 | @GetMapping("/chat") 78 | public ResponseEntity chat(@RequestParam("type") String type, 79 | @RequestParam("apiKey") String apiKey, 80 | @RequestParam(value = "query", required = false) String query, 81 | @RequestParam(value = "inputs", required = false) String inputs, 82 | @RequestParam(value = "conversationId", required = false) String conversationId, 83 | @RequestParam("userId") String userId) throws JsonProcessingException { 84 | 85 | SseEmitter emitter = difyApplicationChatHandler.handle(type, apiKey, query, inputs, conversationId, userId); 86 | 87 | // 设置响应头 88 | HttpHeaders headers = new HttpHeaders(); 89 | headers.setContentType(new org.springframework.http.MediaType("text", "event-stream", StandardCharsets.UTF_8)); 90 | 91 | return new ResponseEntity<>(emitter, headers, HttpStatus.OK); 92 | } 93 | ``` 94 | ### 接收对话结束的结果 95 | 96 | 实现`IChatCompleteEventListener`接口 97 | 98 | ```java 99 | @Component 100 | public class ChatCompleteEventListener implements IChatCompleteEventListener{ 101 | 102 | public void onChatCompleteEvent(ChatCompleteEvent event) { 103 | // 完整的回答内容(对话型应用chat: 完整的回答内容 工作流flow: 工作流的结果) 104 | String fullAnswer = event.getFullAnswer(); 105 | DifyApplicationResponse fullResult = event.getFullResult(); 106 | System.out.println("Received chat completion fullAnswer: " + fullAnswer); 107 | System.out.println("Received chat completion fullResult: " + fullResult); 108 | } 109 | } 110 | ``` 111 | 其中,`DifyApplicationResponse`,由于我的业务不需要额外的字段,只接收了会话id和任务id,其他参数开发者可以自行填充,参考Dify官方的响应体 112 | 113 | ```java 114 | @Data 115 | @AllArgsConstructor 116 | @Builder 117 | @ToString 118 | public class DifyApplicationResponse { 119 | 120 | /** 121 | * 会话id(只有对话型应用有) 122 | */ 123 | @JsonProperty("conversation_id") 124 | private String conversationId; 125 | 126 | /** 127 | * 对话类型 和ChatUrl 枚举值一致 chat和flow 128 | */ 129 | private String type; 130 | 131 | /** 132 | * 任务id 133 | */ 134 | @JsonProperty("task_id") 135 | private String taskId; 136 | 137 | } 138 | ``` 139 | 140 | ### 自定义重构或修改dify返回的结果 141 | 有时,对话返回的结果,尤其是工作流,可能要稍作修改再返回给前端 142 | 实现`ICustomerRebuildAnswerListener`接口 143 | ```java 144 | @Component 145 | public class CustomerRebuildAnswerListener implements ICustomerRebuildAnswerListener{ 146 | @Override 147 | public void rebuildAnswer(CustomerRebuildAnswerEvent event) { 148 | StringBuilder stringBuilder = event.getBody().get(); 149 | // 清空或者自行修改 150 | stringBuilder.setLength(0); 151 | stringBuilder.append("this is customer rebuild answer"); 152 | } 153 | } 154 | ``` 155 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.5.8 9 | 10 | 11 | com.chat.dify4j 12 | chat-dify4j 13 | 0.0.1-SNAPSHOT 14 | chat-dify4j 15 | chat with dify for java 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 8 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-web 36 | 37 | 38 | 39 | org.projectlombok 40 | lombok 41 | true 42 | 43 | 44 | 45 | cn.hutool 46 | hutool-all 47 | 5.6.0 48 | 49 | 50 | 51 | com.squareup.okhttp3 52 | okhttp 53 | 54 | 55 | 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-maven-plugin 61 | 62 | 63 | 64 | org.projectlombok 65 | lombok 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/ChatDify4jApplication.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ChatDify4jApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ChatDify4jApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/chatcomplete/event/ChatCompleteEvent.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j.chatcomplete.event; 2 | 3 | import com.chat.dify4j.model.DifyApplicationResponse; 4 | import lombok.Getter; 5 | import org.springframework.context.ApplicationEvent; 6 | 7 | @Getter 8 | public class ChatCompleteEvent extends ApplicationEvent { 9 | 10 | /** 11 | * 完整的回答内容(对话型应用chat: 完整的回答内容 工作流flow: 工作流的结果) 12 | */ 13 | private final String fullAnswer; 14 | 15 | /** 16 | * 事件结束的任务信息 17 | */ 18 | private final DifyApplicationResponse fullResult; 19 | 20 | public ChatCompleteEvent(Object source, String fullAnswer, DifyApplicationResponse fullResult) { 21 | super(source); 22 | this.fullAnswer = fullAnswer; 23 | this.fullResult = fullResult; 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/chatcomplete/listener/ChatCompleteEventListener.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j.chatcomplete.listener; 2 | 3 | import com.chat.dify4j.chatcomplete.event.ChatCompleteEvent; 4 | import com.chat.dify4j.model.DifyApplicationResponse; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | public class ChatCompleteEventListener implements IChatCompleteEventListener{ 9 | 10 | public void onChatCompleteEvent(ChatCompleteEvent event) { 11 | // 处理对话结束后的结果 12 | String fullAnswer = event.getFullAnswer(); 13 | DifyApplicationResponse fullResult = event.getFullResult(); 14 | System.out.println("Received chat completion fullAnswer: " + fullAnswer); 15 | System.out.println("Received chat completion fullResult: " + fullResult); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/chatcomplete/listener/IChatCompleteEventListener.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j.chatcomplete.listener; 2 | 3 | import com.chat.dify4j.chatcomplete.event.ChatCompleteEvent; 4 | import org.springframework.context.event.EventListener; 5 | 6 | public interface IChatCompleteEventListener { 7 | 8 | /** 9 | * 对话结束发送对话结果事件监听 10 | * ChatCompleteEvent.fullAnswer 完整的回答内容(对话型应用chat: 完整的回答内容 工作流flow: 工作流的结果) 11 | * 12 | * ChatCompleteEvent.fullResult 事件结束的任务信息 13 | * 14 | * 结束事件的所有信息实例: 15 | * chat: 16 | * { 17 | * "event": "message_end", 18 | * "message_id": "5e52ce04-874b-4d27-9045-b3bc80def685", 19 | * "conversation_id": "45701982-8118-4bc5-8e9b-64562b4555f2", 20 | * "task_id" : "fc574300-aac4-49dc-aed3-ef4a855fe579" 21 | * } 22 | * flow: 23 | * { 24 | * "event": "workflow_finished", 25 | * "workflow_run_id": "4ca4c090-936d-494d-8baf-6c5202a2c962", 26 | * "task_id": "6e1a4299-2f6a-496f-a9c8-bac28c06c214", 27 | * "data": { 28 | * "id": "4ca4c090-936d-494d-8baf-6c5202a2c962", 29 | * "workflow_id": "ca0f593c-1d3d-41a6-94e2-219c64156aa8", 30 | * "sequence_number": 7, 31 | * "status": "succeeded", 32 | * "outputs": { 33 | * "body": "{\"message\":\"Hello! How can I assist you today?\"}" 34 | * }, 35 | * "error": null, 36 | * "elapsed_time": 1.0491352650569752, 37 | * "total_tokens": 18, 38 | * "total_steps": 4, 39 | * "created_by": { 40 | * "id": "7e1e8d4f-fafb-4168-94eb-b9bcc79cb65c", 41 | * "user": "abc-123" 42 | * }, 43 | * "created_at": 1723628026, 44 | * "finished_at": 1723628027, 45 | * "files": [] 46 | * } 47 | * } 48 | */ 49 | @EventListener 50 | void onChatCompleteEvent(ChatCompleteEvent event); 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/config/ChatDify4jAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j.config; 2 | 3 | import com.chat.dify4j.handler.DifyApplicationChatHandler; 4 | import com.chat.dify4j.processor.ResponseProcessor; 5 | import com.chat.dify4j.processor.impl.ChatResponseProcessor; 6 | import com.chat.dify4j.processor.impl.WorkflowResponseProcessor; 7 | import com.chat.dify4j.properties.ChatUrlInitializer; 8 | import com.chat.dify4j.properties.ChatUrlProperties; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import okhttp3.OkHttpClient; 11 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 12 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.Configuration; 15 | 16 | import java.net.InetSocketAddress; 17 | import java.net.Proxy; 18 | import java.util.Map; 19 | import java.util.Optional; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | @EnableConfigurationProperties(value = ChatUrlProperties.class) 23 | @Configuration 24 | public class ChatDify4jAutoConfiguration { 25 | 26 | 27 | @Bean 28 | public ObjectMapper objectMapper() { 29 | return new ObjectMapper(); 30 | } 31 | 32 | @ConditionalOnProperty(name = "dify.proxy-enabled", havingValue = "true") 33 | @Bean 34 | public Proxy proxy(ChatUrlProperties chatUrlProperties) { 35 | return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(chatUrlProperties.getProxyHost(), chatUrlProperties.getProxyPort())); 36 | } 37 | 38 | @Bean 39 | public OkHttpClient okHttpClient(Optional proxy) { 40 | OkHttpClient.Builder builder = new OkHttpClient.Builder() 41 | .readTimeout(0, TimeUnit.MILLISECONDS); 42 | proxy.ifPresent(builder::proxy); 43 | return builder.build(); 44 | } 45 | 46 | @Bean 47 | public DifyApplicationChatHandler difyApplicationChatHandler(Map responseProcessor, 48 | ObjectMapper objectMapper, 49 | OkHttpClient okHttpClient) { 50 | return new DifyApplicationChatHandler(responseProcessor, objectMapper, okHttpClient); 51 | } 52 | 53 | @Bean 54 | public ChatResponseProcessor chatProcessor() { 55 | return new ChatResponseProcessor(); 56 | } 57 | 58 | @Bean 59 | public WorkflowResponseProcessor flowProcessor() { 60 | return new WorkflowResponseProcessor(); 61 | } 62 | 63 | @Bean 64 | public ChatUrlInitializer chatUrlInitializer(ChatUrlProperties chatUrlProperties) { 65 | return new ChatUrlInitializer(chatUrlProperties); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/controller/SseController.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j.controller; 2 | 3 | import com.chat.dify4j.handler.DifyApplicationChatHandler; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import lombok.AllArgsConstructor; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.http.HttpHeaders; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestParam; 13 | import org.springframework.web.bind.annotation.RestController; 14 | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; 15 | 16 | import java.nio.charset.StandardCharsets; 17 | 18 | @RestController 19 | @RequestMapping("/api") 20 | @AllArgsConstructor 21 | @Slf4j 22 | public class SseController { 23 | 24 | private DifyApplicationChatHandler difyApplicationChatHandler; 25 | 26 | @GetMapping("/chat") 27 | public ResponseEntity chat(@RequestParam("type") String type, 28 | @RequestParam("apiKey") String apiKey, 29 | @RequestParam(value = "query", required = false) String query, 30 | @RequestParam(value = "inputs", required = false) String inputs, 31 | @RequestParam(value = "conversationId", required = false) String conversationId, 32 | @RequestParam("userId") String userId) throws JsonProcessingException { 33 | 34 | SseEmitter emitter = difyApplicationChatHandler.handle(type, apiKey, query, inputs, conversationId, userId); 35 | 36 | // 设置响应头 37 | HttpHeaders headers = new HttpHeaders(); 38 | headers.setContentType(new org.springframework.http.MediaType("text", "event-stream", StandardCharsets.UTF_8)); 39 | 40 | return new ResponseEntity<>(emitter, headers, HttpStatus.OK); 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/customer/rebuild/event/CustomerRebuildAnswerEvent.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j.customer.rebuild.event; 2 | 3 | import lombok.Getter; 4 | import org.springframework.context.ApplicationEvent; 5 | 6 | @Getter 7 | public class CustomerRebuildAnswerEvent extends ApplicationEvent { 8 | 9 | public ThreadLocal body; 10 | 11 | public CustomerRebuildAnswerEvent(Object source, ThreadLocal body) { 12 | super(source); 13 | this.body = body; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/customer/rebuild/listener/CustomerRebuildAnswerListener.java: -------------------------------------------------------------------------------- 1 | //package com.chat.dify4j.customer.rebuild.listener; 2 | // 3 | //import com.chat.dify4j.customer.rebuild.event.CustomerRebuildAnswerEvent; 4 | //import org.springframework.stereotype.Component; 5 | // 6 | //@Component 7 | //public class CustomerRebuildAnswerListener implements ICustomerRebuildAnswerListener{ 8 | // @Override 9 | // public void rebuildAnswer(CustomerRebuildAnswerEvent event) { 10 | // StringBuilder stringBuilder = event.getBody().get(); 11 | // stringBuilder.setLength(0); 12 | // stringBuilder.append("this is customer rebuild answer"); 13 | // } 14 | //} 15 | -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/customer/rebuild/listener/ICustomerRebuildAnswerListener.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j.customer.rebuild.listener; 2 | 3 | import com.chat.dify4j.customer.rebuild.event.CustomerRebuildAnswerEvent; 4 | import org.springframework.context.event.EventListener; 5 | 6 | /** 7 | * 在发送给前端应用之前, 用户可以修改输出, 重新构建或者修改 8 | */ 9 | public interface ICustomerRebuildAnswerListener { 10 | 11 | @EventListener 12 | void rebuildAnswer(CustomerRebuildAnswerEvent event); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/enums/ChatUrl.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j.enums; 2 | 3 | /** 4 | * dify 请求url 枚举 5 | */ 6 | public enum ChatUrl { 7 | 8 | // 对话型应用 9 | CHAT, 10 | 11 | // 工作流应用 12 | FLOW; 13 | 14 | private String url; 15 | 16 | public String getUrl() { 17 | return url; 18 | } 19 | 20 | public void setUrl(String url) { 21 | this.url = url; 22 | } 23 | 24 | /** 25 | * 根据类型名称获取枚举实例的方法 26 | * @param type 对话类型 和ChatUrl 枚举值一致 chat和flow 27 | * @return ChatUrl对象 28 | */ 29 | public static ChatUrl fromString(String type) { 30 | for (ChatUrl rt : ChatUrl.values()) { 31 | if (rt.name().equalsIgnoreCase(type)) { 32 | return rt; 33 | } 34 | } 35 | throw new IllegalArgumentException("Unknown type: " + type); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/handler/DifyApplicationChatHandler.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j.handler; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.chat.dify4j.enums.ChatUrl; 5 | import com.chat.dify4j.model.DifyApplicationRequestPayload; 6 | import com.chat.dify4j.processor.ResponseProcessor; 7 | import com.fasterxml.jackson.core.JsonProcessingException; 8 | import com.fasterxml.jackson.core.type.TypeReference; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import lombok.AllArgsConstructor; 11 | import lombok.extern.slf4j.Slf4j; 12 | import okhttp3.*; 13 | import okio.BufferedSource; 14 | import org.springframework.http.HttpHeaders; 15 | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; 16 | 17 | import java.io.IOException; 18 | import java.util.Map; 19 | 20 | /** 21 | * Dify应用对话处理器 22 | */ 23 | @Slf4j 24 | @AllArgsConstructor 25 | public class DifyApplicationChatHandler { 26 | 27 | /** 28 | * ResponseProcessor实现类Map key: bean名 value: bean实例 29 | */ 30 | private Map responseProcessor; 31 | 32 | /** 33 | * jackson ObjectMapper 34 | */ 35 | private final ObjectMapper objectMapper; 36 | 37 | /** 38 | * OkHttpClient 39 | */ 40 | private final OkHttpClient client; 41 | 42 | 43 | /** 44 | * 对话流失处理 45 | * @param type 对话类型 和ChatUrl 枚举值一致 chat和flow 46 | * @param apiKey Dify应用apiKey 47 | * @param query chat类型的用户输入 48 | * @param inputs 对应Dify应用中的提示词变量, json字符串格式 49 | * @param conversationId 会话id 50 | * @param userId 用户id 51 | */ 52 | public SseEmitter handle(String type, String apiKey, String query, String inputs, String conversationId, String userId) throws JsonProcessingException { 53 | SseEmitter emitter = new SseEmitter(); 54 | RequestBody requestBody; 55 | 56 | // 请求体 57 | DifyApplicationRequestPayload payload = DifyApplicationRequestPayload 58 | .builder() 59 | .inputs(objectMapper.readValue(inputs, new TypeReference>() {})) 60 | .conversationId(conversationId) 61 | .query(query) 62 | .user(userId) 63 | .build(); 64 | log.info("DifyApplicationRequestPayload >>> {}, type >>> {}, apiKey >>> {}", payload, type, apiKey); 65 | try { 66 | requestBody = RequestBody.create( 67 | MediaType.parse(org.springframework.http.MediaType.APPLICATION_JSON_VALUE), 68 | objectMapper.writeValueAsString(payload) 69 | ); 70 | } catch (Exception e) { 71 | throw new RuntimeException(e); 72 | } 73 | 74 | Request request = new Request.Builder() 75 | .url(ChatUrl.fromString(type).getUrl()) 76 | .post(requestBody) 77 | .addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) 78 | .addHeader(HttpHeaders.CONTENT_TYPE, org.springframework.http.MediaType.APPLICATION_JSON_VALUE) 79 | .build(); 80 | 81 | client.newCall(request).enqueue(new Callback() { 82 | @Override 83 | public void onFailure(Call call, IOException e) { 84 | emitter.completeWithError(e); 85 | } 86 | 87 | @Override 88 | public void onResponse(Call call, Response response) throws IOException { 89 | if (!response.isSuccessful()) { 90 | log.error("call fail : {}", new String(response.body().bytes())); 91 | emitter.completeWithError(new IOException("Unexpected code " + response)); 92 | return; 93 | } 94 | 95 | ResponseProcessor responseProcessor = chatResponseProcessor(type); 96 | try (ResponseBody responseBody = response.body()) { 97 | if (responseBody != null) { 98 | BufferedSource source = responseBody.source(); 99 | while (!source.exhausted()) { 100 | String line = source.readUtf8LineStrict(); 101 | if (StrUtil.isEmpty(line.trim())) { 102 | continue; 103 | } 104 | responseProcessor.processLine(line, emitter); 105 | } 106 | } 107 | } catch (Exception e) { 108 | e.printStackTrace(); 109 | responseProcessor.finishProcessing(emitter, null); 110 | } 111 | } 112 | }); 113 | return emitter; 114 | } 115 | 116 | private ResponseProcessor chatResponseProcessor(String type) { 117 | return responseProcessor.get(type + "Processor"); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/model/DifyApplicationRequestPayload.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | @Getter 11 | @ToString 12 | public class DifyApplicationRequestPayload { 13 | 14 | 15 | /** 16 | * 阻塞模式 常量 17 | */ 18 | private static final String DEFAULT_RESPONSE_MODE = "streaming"; 19 | 20 | /** 21 | * 流式模式 常量 22 | */ 23 | private static final String BLOCKING_RESPONSE_MODE = "blocking"; 24 | 25 | /** 26 | * 对话类型 和ChatUrl 枚举值一致 chat和flow 27 | */ 28 | private final Map inputs; 29 | 30 | /** 31 | * chat类型的用户输入 32 | */ 33 | private final String query; 34 | 35 | /** 36 | * 流式模式/阻塞模式 默认 streaming 37 | */ 38 | @JsonProperty("response_mode") 39 | private final String responseMode; 40 | 41 | /** 42 | * 会话id 43 | */ 44 | @JsonProperty("conversation_id") 45 | private final String conversationId; 46 | 47 | /** 48 | * userId 49 | */ 50 | private final String user; 51 | 52 | public static Builder builder() { 53 | return new Builder(); 54 | } 55 | 56 | private DifyApplicationRequestPayload(Builder builder) { 57 | this.inputs = builder.inputs; 58 | this.query = builder.query; 59 | this.responseMode = builder.responseMode; 60 | this.conversationId = builder.conversationId; 61 | this.user = builder.user; 62 | } 63 | 64 | 65 | /** 66 | * DifyApplicationRequestPayload构造类 67 | */ 68 | public static class Builder { 69 | 70 | /** 71 | * 对话类型 和ChatUrl 枚举值一致 chat和flow 72 | */ 73 | private Map inputs = new HashMap<>(); 74 | 75 | /** 76 | * chat类型的用户输入 77 | */ 78 | private String query; 79 | 80 | /** 81 | * 流式模式/阻塞模式 默认 streaming 82 | */ 83 | private String responseMode = DEFAULT_RESPONSE_MODE; 84 | 85 | /** 86 | * 会话id 87 | */ 88 | private String conversationId; 89 | 90 | /** 91 | * userId 92 | */ 93 | private String user; 94 | 95 | public Builder inputs(Map inputs) { 96 | this.inputs = inputs; 97 | return this; 98 | } 99 | 100 | public Builder query(String query) { 101 | this.query = query; 102 | return this; 103 | } 104 | 105 | /** 106 | * 切换为阻塞模式 107 | */ 108 | public Builder responseModeBlocking() { 109 | this.responseMode = BLOCKING_RESPONSE_MODE; 110 | return this; 111 | } 112 | 113 | public Builder conversationId(String conversationId) { 114 | this.conversationId = conversationId; 115 | return this; 116 | } 117 | 118 | public Builder user(String user) { 119 | this.user = user; 120 | return this; 121 | } 122 | 123 | public DifyApplicationRequestPayload build() { 124 | return new DifyApplicationRequestPayload(this); 125 | } 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/model/DifyApplicationResponse.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.ToString; 8 | 9 | @Data 10 | @AllArgsConstructor 11 | @Builder 12 | @ToString 13 | public class DifyApplicationResponse { 14 | 15 | /** 16 | * 会话id 17 | */ 18 | @JsonProperty("conversation_id") 19 | private String conversationId; 20 | 21 | /** 22 | * 对话类型 和ChatUrl 枚举值一致 chat和flow 23 | */ 24 | private String type; 25 | 26 | /** 27 | * 任务id 28 | */ 29 | @JsonProperty("task_id") 30 | private String taskId; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/processor/ResponseProcessor.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j.processor; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.chat.dify4j.chatcomplete.event.ChatCompleteEvent; 5 | import com.chat.dify4j.customer.rebuild.event.CustomerRebuildAnswerEvent; 6 | import com.chat.dify4j.model.DifyApplicationResponse; 7 | import com.fasterxml.jackson.databind.JsonNode; 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.context.ApplicationEventPublisher; 11 | import org.springframework.util.ObjectUtils; 12 | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; 13 | 14 | import java.io.IOException; 15 | import java.util.Optional; 16 | 17 | /** 18 | * Dify返回结果处理器抽象类 19 | */ 20 | public abstract class ResponseProcessor { 21 | 22 | 23 | /** 24 | * jackson ObjectMapper 25 | */ 26 | public ObjectMapper objectMapper = new ObjectMapper(); 27 | 28 | /** 29 | * 返回完整回答的StringBuilder 使用ThreadLocal确保线程安全 30 | */ 31 | public ThreadLocal threadLocalStringBuilder = ThreadLocal.withInitial(StringBuilder::new); 32 | 33 | 34 | /** 35 | * 对话结束发送对话结果事件发布者 36 | */ 37 | @Autowired 38 | protected ApplicationEventPublisher eventPublisher; 39 | 40 | 41 | /** 42 | * 处理Dify返回结果流式中的某一行 43 | * @param line 流失相应的一次数据 44 | * @param emitter SseEmitter对象 45 | */ 46 | abstract public void processLine(String line, SseEmitter emitter) throws IOException; 47 | 48 | /** 49 | * 向前端发送流 50 | * @param emitter SseEmitter对象 51 | * @param bodyNode 待发送的bodyNode对象 52 | */ 53 | public void send(SseEmitter emitter, JsonNode bodyNode) throws IOException { 54 | if (!bodyNode.isMissingNode() && !bodyNode.isNull()) { 55 | StringBuilder fullAnswer = threadLocalStringBuilder.get(); 56 | fullAnswer.append(bodyNode.asText()); 57 | // 使用者自定义修改 58 | eventPublisher.publishEvent(new CustomerRebuildAnswerEvent(this, threadLocalStringBuilder)); 59 | emitter.send(bodyNode.asText(), org.springframework.http.MediaType.TEXT_EVENT_STREAM); 60 | } 61 | } 62 | 63 | /** 64 | * 结束流输出, 并发送结果 65 | * 66 | * @param emitter SseEmitter对象 67 | */ 68 | public void finishProcessing(SseEmitter emitter, JsonNode fullResult) { 69 | if (!ObjectUtils.isEmpty(emitter)) { 70 | emitter.complete(); 71 | } 72 | DifyApplicationResponse difyApplicationResponse = DifyApplicationResponse.builder() 73 | .conversationId(Optional.ofNullable(fullResult) 74 | .map(result -> result.path("conversation_id").asText()) 75 | .orElse(StrUtil.EMPTY)) 76 | .taskId(Optional.ofNullable(fullResult) 77 | .map(result -> result.path("task_id").asText()) 78 | .orElse(StrUtil.EMPTY)) 79 | .type(this.chatType()) 80 | .build(); 81 | 82 | eventPublisher.publishEvent(new ChatCompleteEvent(this, threadLocalStringBuilder.get().toString(), difyApplicationResponse)); 83 | // 移除threadLocal 84 | threadLocalStringBuilder.remove(); 85 | } 86 | 87 | /** 88 | * 获取当前对话类型 89 | * @return 对象类型key 90 | */ 91 | abstract public String chatType(); 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/processor/impl/ChatResponseProcessor.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j.processor.impl; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.chat.dify4j.enums.ChatUrl; 5 | import com.chat.dify4j.processor.ResponseProcessor; 6 | import com.fasterxml.jackson.databind.JsonNode; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; 9 | 10 | import java.io.IOException; 11 | 12 | /** 13 | * Dify 普通chat方式返回结果处理器 14 | */ 15 | @Slf4j 16 | public class ChatResponseProcessor extends ResponseProcessor { 17 | 18 | /** 19 | * 普通chat方式响应的结束事件标识 20 | */ 21 | private static final String END_EVENT = "message_end"; 22 | 23 | @Override 24 | public void processLine(String line, SseEmitter emitter) throws IOException { 25 | JsonNode lineJsonResult; 26 | try { 27 | lineJsonResult = super.objectMapper.readTree(line.replaceAll("data: ", "")); 28 | } catch (Exception e) { 29 | return; 30 | } 31 | log.info("processLine >>> {}", lineJsonResult.toPrettyString()); 32 | String event = lineJsonResult.path("event").asText(); 33 | super.send(emitter,lineJsonResult.path("answer")); 34 | // 消息结束事件,收到此事件则代表流式返回结束 35 | if (StrUtil.equals(END_EVENT, event)) { 36 | finishProcessing(emitter, lineJsonResult); 37 | } 38 | } 39 | 40 | @Override 41 | public String chatType() { 42 | return ChatUrl.CHAT.name(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/processor/impl/WorkflowResponseProcessor.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j.processor.impl; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.chat.dify4j.enums.ChatUrl; 5 | import com.chat.dify4j.processor.ResponseProcessor; 6 | import com.fasterxml.jackson.databind.JsonNode; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; 9 | 10 | import java.io.IOException; 11 | 12 | /** 13 | * Dify flow工作流方式返回结果处理器 14 | */ 15 | @Slf4j 16 | public class WorkflowResponseProcessor extends ResponseProcessor { 17 | 18 | /** 19 | * 工作流方式响应的结束事件标识 20 | */ 21 | private static final String END_EVENT = "workflow_finished"; 22 | 23 | @Override 24 | public void processLine(String line, SseEmitter emitter) throws IOException { 25 | JsonNode lineJsonResult; 26 | try { 27 | lineJsonResult = super.objectMapper.readTree(line.replaceAll("data: ", "")); 28 | } catch (Exception e) { 29 | return; 30 | } 31 | log.info("processLine >>> {}", lineJsonResult.toPrettyString()); 32 | String event = lineJsonResult.path("event").asText(); 33 | // 消息结束事件,收到此事件则代表流式返回结束 34 | if (StrUtil.equals(END_EVENT, event)) { 35 | send(emitter, lineJsonResult.path("data").path("outputs").path("body")); 36 | finishProcessing(emitter, lineJsonResult); 37 | } 38 | } 39 | 40 | @Override 41 | public String chatType() { 42 | return ChatUrl.FLOW.name(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/properties/ChatUrlInitializer.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j.properties; 2 | 3 | import com.chat.dify4j.enums.ChatUrl; 4 | 5 | /** 6 | * 初始化enum ChatUrl的值 7 | */ 8 | public class ChatUrlInitializer { 9 | 10 | public ChatUrlInitializer(ChatUrlProperties chatUrlProperties) { 11 | ChatUrl.CHAT.setUrl(chatUrlProperties.getChatUrl()); 12 | ChatUrl.FLOW.setUrl(chatUrlProperties.getFlowUrl()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/chat/dify4j/properties/ChatUrlProperties.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j.properties; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | /** 7 | * dify 请求url 配置类 8 | */ 9 | @ConfigurationProperties(prefix = "dify") 10 | @Data 11 | public class ChatUrlProperties { 12 | 13 | /** 14 | * 对话型应用URL 15 | */ 16 | private String chatUrl; 17 | 18 | /** 19 | * Workflow应用URL 20 | */ 21 | private String flowUrl; 22 | 23 | /** 24 | * 是否启用代理 25 | */ 26 | private boolean proxyEnabled; 27 | 28 | /** 29 | * 代理地址 30 | */ 31 | private String proxyHost; 32 | 33 | /** 34 | * 代理端口 35 | */ 36 | private int proxyPort; 37 | } 38 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=chat-dify4j 2 | 3 | dify.chat-url=https://api.dify.ai/v1/chat-messages 4 | dify.flow-url=https://api.dify.ai/v1/workflows/run 5 | 6 | # ???? ????????? 7 | dify.proxy-enabled=false 8 | # ???? 9 | dify.proxy-host=127.0.0.1 10 | # ???? 11 | dify.proxy-port=7890 -------------------------------------------------------------------------------- /src/test/java/com/chat/dify4j/ChatDify4jApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.chat.dify4j; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ChatDify4jApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | --------------------------------------------------------------------------------