This commit is contained in:
ran.shang
2026-01-08 14:33:33 +08:00
parent 2b4458dc1b
commit 5405012faa
45 changed files with 1716 additions and 209 deletions

View File

@@ -0,0 +1,23 @@
package com.senmesh.sdk.dify.config;
import lombok.Data;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* Dify配置
*
* @author sr
*/
@Data
@Configuration
@EnableConfigurationProperties(DifyProperties.class)
@ConditionalOnProperty(name = DifyConfig.DIFY_ENABLE, havingValue = "true")
public class DifyConfig {
static final String DIFY_ENABLE = "dify.enable";
}

View File

@@ -0,0 +1,32 @@
package com.senmesh.sdk.dify.config;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Dify配置属性
*
* @author sr
*/
@ConfigurationProperties(prefix = DifyProperties.PREFIX)
@Data
@Component
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DifyProperties implements Serializable {
private static final long serialVersionUID = 1L;
public static final String PREFIX = "dify";
/** Dify服务地址 */
private String host;
}

View File

@@ -0,0 +1,69 @@
package com.senmesh.sdk.dify.dto;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Dify对话请求
*
* @author sr
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DifyChatRequest implements Serializable {
private static final long serialVersionUID = 1L;
/** 输入参数 */
private Map<String, Object> inputs;
/** 查询内容 */
private String query;
/** 响应模式: blocking/streaming */
@Builder.Default
private String responseMode = "blocking";
/** 会话ID */
private String conversationId;
/** 用户标识 */
private String user;
/** 文件列表 */
private List<DifyFileInfo> files;
/**
* 文件信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class DifyFileInfo implements Serializable {
private static final long serialVersionUID = 1L;
/** 文件类型: image/document */
private String type;
/** 传输方式: remote_url/local_file */
@Builder.Default
private String transferMethod = "local_file";
/** 远程URL */
private String url;
/** 上传文件ID */
private String uploadFileId;
}
}

View File

@@ -0,0 +1,144 @@
package com.senmesh.sdk.dify.dto;
import java.io.Serializable;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Dify对话响应
*
* @author sr
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DifyChatResponse implements Serializable {
private static final long serialVersionUID = 1L;
/** 消息ID */
private String messageId;
/** 会话ID */
private String conversationId;
/** 模式 */
private String mode;
/** 回答内容 */
private String answer;
/** 元数据 */
private Metadata metadata;
/** 创建时间 */
private Long createdAt;
/**
* 元数据
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class Metadata implements Serializable {
private static final long serialVersionUID = 1L;
/** 使用量 */
private Usage usage;
/** 检索器资源 */
private List<RetrieverResource> retrieverResources;
}
/**
* 使用量
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class Usage implements Serializable {
private static final long serialVersionUID = 1L;
/** 提示词token数 */
private Integer promptTokens;
/** 提示词单位价格 */
private String promptUnitPrice;
/** 提示词价格单位 */
private String promptPriceUnit;
/** 提示词价格 */
private String promptPrice;
/** 完成token数 */
private Integer completionTokens;
/** 完成单位价格 */
private String completionUnitPrice;
/** 完成价格单位 */
private String completionPriceUnit;
/** 完成价格 */
private String completionPrice;
/** 总token数 */
private Integer totalTokens;
/** 总价格 */
private String totalPrice;
/** 货币 */
private String currency;
/** 延迟(毫秒) */
private Double latency;
}
/**
* 检索器资源
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class RetrieverResource implements Serializable {
private static final long serialVersionUID = 1L;
/** 位置 */
private Integer position;
/** 数据集ID */
private String datasetId;
/** 数据集名称 */
private String datasetName;
/** 文档ID */
private String documentId;
/** 文档名称 */
private String documentName;
/** 分段ID */
private String segmentId;
/** 分数 */
private Double score;
/** 内容 */
private String content;
}
}

View File

@@ -0,0 +1,36 @@
package com.senmesh.sdk.dify.dto;
import java.io.InputStream;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Dify文件上传请求
*
* @author sr
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DifyFileUploadRequest implements Serializable {
private static final long serialVersionUID = 1L;
/** 文件输入流 */
private InputStream inputStream;
/** 文件名 */
private String fileName;
/** 文件MIME类型 */
private String contentType;
/** 用户标识 */
private String user;
}

View File

@@ -0,0 +1,44 @@
package com.senmesh.sdk.dify.dto;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Dify文件上传响应
*
* @author sr
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DifyFileUploadResponse implements Serializable {
private static final long serialVersionUID = 1L;
/** 文件ID */
private String id;
/** 文件名 */
private String name;
/** 文件大小 */
private Long size;
/** 文件扩展名 */
private String extension;
/** MIME类型 */
private String mimeType;
/** 创建者 */
private String createdBy;
/** 创建时间 */
private Long createdAt;
}

View File

@@ -0,0 +1,206 @@
package com.senmesh.sdk.dify.service;
import com.senmesh.common.AlitaResult;
import com.senmesh.common.util.JsonUtils;
import com.senmesh.sdk.common.constant.SdkConstants;
import com.senmesh.sdk.common.util.HttpUtils;
import com.senmesh.sdk.dify.config.DifyProperties;
import com.senmesh.sdk.dify.dto.DifyChatRequest;
import com.senmesh.sdk.dify.dto.DifyChatResponse;
import com.senmesh.sdk.dify.dto.DifyFileUploadRequest;
import com.senmesh.sdk.dify.dto.DifyFileUploadResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Dify客户端
*
* @author sr
*/
@Slf4j
@Component
public class DifyClient {
private static DifyProperties difyProperties;
private static final String FILE_UPLOAD_URI = "/v1/files/upload";
private static final String CHAT_MESSAGES_URI = "/v1/chat-messages";
/**
* 构造方法 - 用于Spring注入,同时设置静态变量
*/
@Autowired
public DifyClient(DifyProperties difyProperties) {
DifyClient.difyProperties = difyProperties;
}
/**
* 上传文件到Dify(静态方法 - 只传apiKey)
*
* @param apiKey API密钥
* @param request 文件上传请求
* @return 上传结果
*/
public static AlitaResult<DifyFileUploadResponse> uploadFile(String apiKey, DifyFileUploadRequest request) {
if (difyProperties == null) {
return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "DifyProperties未初始化");
}
return uploadFile(difyProperties.getHost(), apiKey, request);
}
/**
* 上传文件到Dify(静态方法)
*
* @param host Dify服务地址
* @param apiKey API密钥
* @param request 文件上传请求
* @return 上传结果
*/
public static AlitaResult<DifyFileUploadResponse> uploadFile(String host, String apiKey, DifyFileUploadRequest request) {
try {
// 参数校验
if (StringUtils.isBlank(host)) {
return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "Dify服务地址不能为空");
}
if (StringUtils.isBlank(apiKey)) {
return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "API密钥不能为空");
}
if (request == null || request.getInputStream() == null) {
return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "文件输入流不能为空");
}
if (StringUtils.isBlank(request.getFileName())) {
return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "文件名不能为空");
}
if (StringUtils.isBlank(request.getUser())) {
return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "用户标识不能为空");
}
String url = host + FILE_UPLOAD_URI;
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
headers.set("Authorization", "Bearer " + apiKey);
// 构建请求体
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
// 将InputStream转换为ByteArrayResource
byte[] fileBytes = org.springframework.util.StreamUtils.copyToByteArray(request.getInputStream());
ByteArrayResource fileResource = new ByteArrayResource(fileBytes) {
@Override
public String getFilename() {
return request.getFileName();
}
};
body.add("file", fileResource);
body.add("user", request.getUser());
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
log.info("dify uploadFile request url:{}, user:{}, fileName:{}",
url, request.getUser(), request.getFileName());
// 发送请求
ResponseEntity<DifyFileUploadResponse> response = HttpUtils.restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
DifyFileUploadResponse.class
);
DifyFileUploadResponse result = response.getBody();
log.info("dify uploadFile response:{}", JsonUtils.toJsonString(result));
return AlitaResult.success(result);
} catch (Exception e) {
log.error("dify uploadFile error:", e);
return AlitaResult.error(SdkConstants.SDK_HTTP_INVOKE_ERROR_CODE, e.getMessage());
}
}
/**
* 发送对话消息(静态方法 - 只传apiKey)
*
* @param apiKey API密钥
* @param request 对话请求
* @return 对话结果
*/
public static AlitaResult<DifyChatResponse> chatMessages(String apiKey, DifyChatRequest request) {
if (difyProperties == null) {
return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "DifyProperties未初始化");
}
return chatMessages(difyProperties.getHost(), apiKey, request);
}
/**
* 发送对话消息(静态方法)
*
* @param host Dify服务地址
* @param apiKey API密钥
* @param request 对话请求
* @return 对话结果
*/
public static AlitaResult<DifyChatResponse> chatMessages(String host, String apiKey, DifyChatRequest request) {
try {
// 参数校验
if (StringUtils.isBlank(host)) {
return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "Dify服务地址不能为空");
}
if (StringUtils.isBlank(apiKey)) {
return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "API密钥不能为空");
}
if (request == null) {
return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "请求参数不能为空");
}
if (StringUtils.isBlank(request.getQuery())) {
return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "查询内容不能为空");
}
if (StringUtils.isBlank(request.getUser())) {
return AlitaResult.error(SdkConstants.SDK_INVOKE_NOT_REACH_CODE, "用户标识不能为空");
}
String url = host + CHAT_MESSAGES_URI;
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + apiKey);
HttpEntity<DifyChatRequest> requestEntity = new HttpEntity<>(request, headers);
log.info("dify chatMessages request url:{}, body:{}", url, JsonUtils.toJsonString(request));
// 发送请求
ResponseEntity<DifyChatResponse> response = HttpUtils.restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
DifyChatResponse.class
);
DifyChatResponse result = response.getBody();
log.info("dify chatMessages response:{}", JsonUtils.toJsonString(result));
return AlitaResult.success(result);
} catch (Exception e) {
log.error("dify chatMessages error:", e);
return AlitaResult.error(SdkConstants.SDK_HTTP_INVOKE_ERROR_CODE, e.getMessage());
}
}
}