Commit f225d483 authored by alex yao's avatar alex yao

refactor(ai-software-copyright): 重构dify调用代码

  1. 配置管理优化
   - 创建了 DifyConfigProperties 类来集中管理所有Dify相关的配置
   - 将硬编码的API密钥迁移到 config.properties 配置文件中
   - 为不同用途的API密钥建立了映射关系

  2. 代码复用优化
   - 创建了 DifyChatService 通用聊天服务类,封装与Dify API交互的逻辑
   - 消除了重复的聊天客户端创建和消息发送代码

  3. 架构模式优化
   - 实现了策略模式来处理不同类型的文档生成
   - 创建了 DocGenerationStrategy 接口和对应的实现类
   - 创建了 DocGenerationStrategyFactory 来管理文档生成策略

  4. 异常处理优化
   - 增强了异常处理机制,提供更详细的错误日志
   - 确保错误情况下能提供有用的调试信息

  5. 代码质量提升
   - 更新了 SoftwareCopyRightAgent 类,使其更加简洁和可维护
   - 改进了 SoftwareCopyRightUtils 类,增加了必要的辅助方法
   - 更新了单元测试以适应新的架构

  6. 安全性提升
   - 消除了硬编码的API密钥,提高了安全性
   - 所有API密钥现在都从配置文件中读取
parent e8104954
package cn.com.poc.ai_software_copyright.factory;
import cn.com.poc.ai_software_copyright.contant.AiSoftWareCopyRightEnum;
import cn.com.poc.ai_software_copyright.strategy.DocGenerationStrategy;
import cn.com.poc.ai_software_copyright.strategy.impl.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.EnumMap;
import java.util.Map;
/**
* 文档生成策略工厂
*/
@Component
public class DocGenerationStrategyFactory {
@Autowired
private SourceCodeGenerationStrategy sourceCodeGenerationStrategy;
@Autowired
private PhOperatingManualGenerationStrategy phOperatingManualGenerationStrategy;
@Autowired
private PcOperatingManualGenerationStrategy pcOperatingManualGenerationStrategy;
@Autowired
private InformationGenerationStrategy informationGenerationStrategy;
private final Map<AiSoftWareCopyRightEnum.DOC_TYPE, DocGenerationStrategy> strategies = new EnumMap<>(AiSoftWareCopyRightEnum.DOC_TYPE.class);
@PostConstruct
public void init() {
strategies.put(AiSoftWareCopyRightEnum.DOC_TYPE.source_code, sourceCodeGenerationStrategy);
strategies.put(AiSoftWareCopyRightEnum.DOC_TYPE.ph_operating_manual, phOperatingManualGenerationStrategy);
strategies.put(AiSoftWareCopyRightEnum.DOC_TYPE.pc_operating_manual, pcOperatingManualGenerationStrategy);
strategies.put(AiSoftWareCopyRightEnum.DOC_TYPE.information, informationGenerationStrategy);
}
public DocGenerationStrategy getStrategy(AiSoftWareCopyRightEnum.DOC_TYPE docType) {
DocGenerationStrategy strategy = strategies.get(docType);
if (strategy == null) {
throw new IllegalArgumentException("不支持的文档类型:" + docType);
}
return strategy;
}
}
\ No newline at end of file
package cn.com.poc.ai_software_copyright.strategy;
import cn.com.poc.ai_software_copyright.domain.GeneratedDoc;
import cn.com.yict.framemax.core.config.Config;
/**
* 软著文档生成策略接口
*/
public abstract class DocGenerationStrategy {
public abstract void generate(GeneratedDoc doc);
public String getCallbackURL() {
return Config.get("dify.software_copyright.callball.url");
}
}
\ No newline at end of file
package cn.com.poc.ai_software_copyright.strategy.impl;
import cn.com.poc.ai_software_copyright.domain.GeneratedDoc;
import cn.com.poc.ai_software_copyright.strategy.DocGenerationStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* 信息采集表生成策略实现
*/
@Component
public class InformationGenerationStrategy extends DocGenerationStrategy {
private static final Logger logger = LoggerFactory.getLogger(InformationGenerationStrategy.class);
@Override
public void generate(GeneratedDoc doc) {
// 信息采集表生成逻辑 - 目前为空实现,可根据需要添加具体逻辑
logger.info(this.getCallbackURL());
logger.info("生成信息采集表 - 当前为空实现");
}
}
\ No newline at end of file
package cn.com.poc.ai_software_copyright.strategy.impl;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import cn.com.poc.ai_software_copyright.contant.AiSoftWareCopyRightRedisKey;
import cn.com.poc.ai_software_copyright.domain.GeneratedDoc;
import cn.com.poc.ai_software_copyright.strategy.DocGenerationStrategy;
import cn.com.poc.ai_software_copyright.utils.SoftwareCopyRightUtils;
import cn.com.poc.common.pool.CommonThreadPoolExecutor;
import cn.com.poc.common.utils.DateUtils;
import cn.com.poc.common.utils.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* PC端使用说明书生成策略实现
*/
@Component
public class PcOperatingManualGenerationStrategy extends DocGenerationStrategy {
private static final Logger logger = LoggerFactory.getLogger(PcOperatingManualGenerationStrategy.class);
@Resource
private SoftwareCopyRightUtils softwareCopyRightUtils;
@Override
public void generate(GeneratedDoc doc) {
String q = "生成电脑端使用说明书";
List<String> fileUrls = softwareCopyRightUtils.downloadAndUnzipPlugin(doc.getFileURLs());
Map<String, Object> data = new HashMap<>();
data.put("ui", doc.getUi());
data.put("structure", doc.getStructure());
data.put("demand", doc.getDemand());
data.put("code", "");
data.put("baseinfo", JsonUtils.serialize(doc.getBaseinfo()));
data.put("technical_content", JsonUtils.serialize(doc.getTechnical_content()));
data.put("callback_url", this.getCallbackURL()); // 获取回调URL
data.put("task_id", AiSoftWareCopyRightRedisKey.CALL_BACK + doc.getId());
String user = "pcOperatingManual" + DateUtils.getCurrTime();
CommonThreadPoolExecutor.addTask(() -> {
try {
softwareCopyRightUtils.executeBlockingChat(fileUrls, q, data, user, "pc-operating-manual");
} catch (Exception e) {
logger.error("生成电脑端使用说明书失败: {}", e.getMessage(), e);
}
});
}
}
\ No newline at end of file
package cn.com.poc.ai_software_copyright.strategy.impl;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import cn.com.poc.ai_software_copyright.contant.AiSoftWareCopyRightRedisKey;
import cn.com.poc.ai_software_copyright.domain.GeneratedDoc;
import cn.com.poc.ai_software_copyright.strategy.DocGenerationStrategy;
import cn.com.poc.ai_software_copyright.utils.SoftwareCopyRightUtils;
import cn.com.poc.common.pool.CommonThreadPoolExecutor;
import cn.com.poc.common.utils.DateUtils;
import cn.com.poc.common.utils.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* 手机端使用说明书生成策略实现
*/
@Component
public class PhOperatingManualGenerationStrategy extends DocGenerationStrategy {
private static final Logger logger = LoggerFactory.getLogger(PhOperatingManualGenerationStrategy.class);
@Resource
private SoftwareCopyRightUtils softwareCopyRightUtils;
@Override
public void generate(GeneratedDoc doc) {
String q = "生成手机端使用说明书";
List<String> fileUrls = softwareCopyRightUtils.downloadAndUnzipPlugin(doc.getFileURLs());
Map<String, Object> data = new HashMap<>();
data.put("ui", doc.getUi());
data.put("structure", doc.getStructure());
data.put("demand", doc.getDemand());
data.put("code", "");
data.put("baseinfo", JsonUtils.serialize(doc.getBaseinfo()));
data.put("technical_content", JsonUtils.serialize(doc.getTechnical_content()));
data.put("callback_url", this.getCallbackURL()); // 获取回调URL
data.put("task_id", AiSoftWareCopyRightRedisKey.CALL_BACK + doc.getId());
String user = "phOperatingManual" + DateUtils.getCurrTime();
CommonThreadPoolExecutor.addTask(() -> {
try {
softwareCopyRightUtils.executeBlockingChat(fileUrls, q, data, user, "ph-operating-manual");
} catch (Exception e) {
logger.error("生成手机端使用说明书失败: {}", e.getMessage(), e);
}
});
}
}
\ No newline at end of file
package cn.com.poc.ai_software_copyright.strategy.impl;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import cn.com.poc.ai_software_copyright.contant.AiSoftWareCopyRightRedisKey;
import cn.com.poc.ai_software_copyright.domain.GeneratedDoc;
import cn.com.poc.ai_software_copyright.strategy.DocGenerationStrategy;
import cn.com.poc.ai_software_copyright.utils.SoftwareCopyRightUtils;
import cn.com.poc.common.pool.CommonThreadPoolExecutor;
import cn.com.poc.common.utils.DateUtils;
import cn.com.poc.common.utils.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* 源代码生成策略实现
*/
@Component
public class SourceCodeGenerationStrategy extends DocGenerationStrategy {
private static final Logger logger = LoggerFactory.getLogger(SourceCodeGenerationStrategy.class);
@Resource
private SoftwareCopyRightUtils softwareCopyRightUtils;
@Override
public void generate(GeneratedDoc doc) {
String q = "生成源代码";
List<String> fileUrls = softwareCopyRightUtils.downloadAndUnzipPlugin(doc.getFileURLs());
Map<String, Object> data = new HashMap<>();
data.put("ui", doc.getUi());
data.put("structure", doc.getStructure());
data.put("demand", doc.getDemand());
data.put("code", "");
data.put("baseinfo", JsonUtils.serialize(doc.getBaseinfo()));
data.put("technical_content", JsonUtils.serialize(doc.getTechnical_content()));
data.put("callback_url", this.getCallbackURL()); // 获取回调URL
data.put("task_id", AiSoftWareCopyRightRedisKey.CALL_BACK + doc.getId());
String user = "sourceCode" + DateUtils.getCurrTime();
CommonThreadPoolExecutor.addTask(() -> {
try {
softwareCopyRightUtils.executeBlockingChat(fileUrls, q, data, user, "source-code");
} catch (Exception e) {
logger.error("生成源代码失败: {}", e.getMessage(), e);
}
});
}
}
\ No newline at end of file
......@@ -6,9 +6,12 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import cn.com.poc.common.config.DifyConfigProperties;
import cn.com.poc.common.service.impl.DifyChatServiceImpl;
import cn.com.poc.common.service.BizFileUploadRecordService;
import cn.com.poc.common.service.BosConfigService;
import cn.com.poc.common.utils.DocumentLoad;
......@@ -34,6 +37,12 @@ public class SoftwareCopyRightUtils {
@Resource
private BizFileUploadRecordService bizFileUploadRecordService;
@Resource
private DifyConfigProperties difyConfigProperties;
@Resource
private DifyChatServiceImpl difyChatService;
/**
* 插件是否存在压缩文件,存在则下载并解压文件
*/
......@@ -134,5 +143,19 @@ public class SoftwareCopyRightUtils {
}
}
}
/**
* 执行阻塞式聊天
*/
public void executeBlockingChat(List<String> fileUrls, String question,
Map<String, Object> data, String user, String apiKeyType) {
String apiKey = difyConfigProperties.getApiKeys().get(apiKeyType);
try {
difyChatService.blockingChat(fileUrls, question, data, user, apiKey);
} catch (Exception e) {
logger.error("执行{}生成异常: {}", user, e.getMessage(), e);
throw new BusinessException("执行" + user + "生成异常", e);
}
}
}
package cn.com.poc.common.config;
import java.util.Map;
import cn.com.yict.framemax.core.config.Config;
import org.springframework.stereotype.Component;
/**
* Dify配置属性类,用于管理所有Dify相关的配置
*/
@Component
public class DifyConfigProperties {
public Map<String, String> getApiKeys() {
return Config.getStartsWithAndTrim("dify.config.apiKeys.");
}
public String getBaseUrl() {
return Config.get("dify.base.url");
}
}
\ No newline at end of file
package cn.com.poc.common.service;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import cn.com.gsst.dify_client.exception.DifyApiException;
import cn.com.gsst.dify_client.model.chat.ChatMessageResponse;
/**
* @author alex.yao
* @date 2026/1/13
*/
public interface DifyChatService {
ChatMessageResponse blockingChat(List<String> fileUrls,
String question,
Map<String, Object> data,
String user,
String apiKey);
void streamChat(List<String> fileUrls,
String question,
Map<String, Object> data,
String user,
String apiKey) throws IOException, DifyApiException, InterruptedException;
}
package cn.com.poc.common.service.impl;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import cn.com.gsst.dify_client.DifyChatClient;
import cn.com.gsst.dify_client.DifyClientFactory;
import cn.com.gsst.dify_client.callback.ChatStreamCallback;
import cn.com.gsst.dify_client.enums.FileTransferMethod;
import cn.com.gsst.dify_client.enums.FileType;
import cn.com.gsst.dify_client.enums.ResponseMode;
import cn.com.gsst.dify_client.event.MessageEndEvent;
import cn.com.gsst.dify_client.event.MessageEvent;
import cn.com.gsst.dify_client.exception.DifyApiException;
import cn.com.gsst.dify_client.model.DifyConfig;
import cn.com.gsst.dify_client.model.chat.ChatMessage;
import cn.com.gsst.dify_client.model.chat.ChatMessageResponse;
import cn.com.gsst.dify_client.model.file.FileInfo;
import cn.com.poc.common.config.DifyConfigProperties;
import cn.com.poc.common.service.DifyChatService;
import cn.com.poc.common.utils.JsonUtils;
import cn.com.poc.common.utils.SSEUtil;
import cn.com.poc.common.utils.StringUtils;
import cn.com.poc.thirdparty.resource.demand.ai.entity.largemodel.LargeModelDemandResult;
import cn.com.yict.framemax.core.exception.BusinessException;
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 通用Dify聊天服务,封装与Dify API交互的逻辑
*/
@Service
public class DifyChatServiceImpl implements DifyChatService {
private final Logger logger = LoggerFactory.getLogger(DifyChatServiceImpl.class);
@Autowired
private DifyConfigProperties difyConfigProperties;
/**
* 阻塞式聊天请求
*/
public ChatMessageResponse blockingChat(List<String> fileUrls,
String question,
Map<String, Object> data,
String user,
String apiKey) {
// 从配置中获取API密钥,如果传入的apiKey为null或空,则从配置中查找
String actualApiKey = apiKey;
if (StringUtils.isEmpty(actualApiKey)) {
actualApiKey = difyConfigProperties.getApiKeys().get(user);
}
DifyConfig difyConfig = DifyConfig.builder()
.apiKey(actualApiKey)
.readTimeout(200 * 1000)
.baseUrl(difyConfigProperties.getBaseUrl())
.build();
DifyChatClient chatClient = DifyClientFactory.createChatClient(difyConfig);
List<FileInfo> files = new ArrayList<>();
if (CollectionUtils.isNotEmpty(fileUrls)) {
for (String fileUrl : fileUrls) {
files.add(FileInfo.builder()
.type(FileType.DOCUMENT)
.url(fileUrl)
.transferMethod(FileTransferMethod.REMOTE_URL)
.build());
}
}
// 创建聊天消息
ChatMessage message = ChatMessage.builder()
.query(question)
.user(user)
.files(files)
.inputs(data)
.responseMode(ResponseMode.BLOCKING)
.build();
// 发送消息并获取响应
try {
ChatMessageResponse chatMessageResponse = chatClient.sendChatMessage(message);
return chatMessageResponse;
} catch (IOException | DifyApiException e) {
logger.error("执行{}生成异常: {}", user, e.getMessage(), e);
throw new BusinessException("执行" + user + "生成异常");
}
}
/**
* 流式聊天请求
*/
public void streamChat(List<String> fileUrls,
String question,
Map<String, Object> data,
String user,
String apiKey) throws IOException, DifyApiException, InterruptedException {
// 从配置中获取API密钥,如果传入的apiKey为null或空,则从配置中查找
String actualApiKey = apiKey;
if (StringUtils.isEmpty(actualApiKey)) {
actualApiKey = difyConfigProperties.getApiKeys().get(user);
}
// 创建聊天客户端
DifyChatClient chatClient =
DifyClientFactory.createChatWorkflowClient(difyConfigProperties.getBaseUrl(), actualApiKey);
List<FileInfo> files = new ArrayList<>();
if (CollectionUtils.isNotEmpty(fileUrls)) {
for (String fileUrl : fileUrls) {
files.add(FileInfo.builder()
.type(FileType.DOCUMENT)
.url(fileUrl)
.transferMethod(FileTransferMethod.REMOTE_URL)
.build());
}
}
// 创建聊天消息
ChatMessage message = ChatMessage.builder()
.query(question)
.user(user)
.files(files)
.inputs(data)
.responseMode(ResponseMode.STREAMING)
.build();
CountDownLatch countDownLatch = new CountDownLatch(1);
ServletRequestAttributes servletRequestAttributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = servletRequestAttributes.getResponse();
// 发送消息并获取响应
chatClient.sendChatMessageStream(message, new ChatStreamCallback() {
SSEUtil sseUtil = new SSEUtil(response);
@Override
public void onMessage(MessageEvent event) {
try {
if (StringUtils.isNotEmpty(event.getAnswer())) {
LargeModelDemandResult largeModelDemandResult = new LargeModelDemandResult();
largeModelDemandResult.setCode("0");
largeModelDemandResult.setMessage(event.getAnswer());
sseUtil.send(JsonUtils.serialize(largeModelDemandResult));
}
} catch (Exception e) {
logger.error("发送SSE消息时出错: {}", e.getMessage(), e);
sseUtil.completeByError(e.getMessage());
throw new RuntimeException(e);
}
}
@Override
public void onMessageEnd(MessageEndEvent event) {
try {
sseUtil.send("[DONE]");
} catch (IOException e) {
logger.error("发送SSE结束消息时出错: {}", e.getMessage(), e);
sseUtil.completeByError(e.getMessage());
throw new RuntimeException(e);
} finally {
sseUtil.complete();
}
countDownLatch.countDown();
}
});
countDownLatch.await(120, TimeUnit.SECONDS);
}
}
\ No newline at end of file
......@@ -71,7 +71,4 @@ i18n.disable=false
framemax-frame.job.disable=false
#pay
pay.config.acctId=16653311814572
pay.config.wxAppId=wxea3470b5d2d97eca
#dify
dify.base.url=https://dify.gsstcloud.com/v1
pay.config.wxAppId=wxea3470b5d2d97eca
\ No newline at end of file
#dify
dify.base.url=https://dify.gsstcloud.com/v1
# API keys mapping
dify.config.apiKeys.ai-assistant-development-purpose=app-S3y8vmyX95cTa0sb9V9oBfSX
dify.config.apiKeys.generated-base-doc=app-VgynqQ7MDYW6CjUbtDu4osBf
dify.config.apiKeys.technical-content=app-VuwhZGr7UqFb2gkg7jOmibGM
dify.config.apiKeys.source-code=app-c0PWYmcythg7bIVxvt4d4kDM
dify.config.apiKeys.ph-operating-manual=app-c0PWYmcythg7bIVxvt4d4kDM
dify.config.apiKeys.pc-operating-manual=app-c0PWYmcythg7bIVxvt4d4kDM
......@@ -3,10 +3,12 @@ package cn.com.poc.softwareCopyRight;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import java.util.Map;
import cn.com.gsst.dify_client.exception.DifyApiException;
import cn.com.poc.ai_software_copyright.agent.SoftwareCopyRightAgent;
import cn.com.poc.ai_software_copyright.strategy.impl.InformationGenerationStrategy;
import cn.com.poc.common.config.DifyConfigProperties;
import cn.com.poc.ai_software_copyright.contant.AiSoftWareCopyRightRedisKey;
import cn.com.poc.ai_software_copyright.convert.BizSoftwareCopyrightConvert;
import cn.com.poc.ai_software_copyright.dto.SoftwareCopyRightDto;
......@@ -14,7 +16,6 @@ import cn.com.poc.ai_software_copyright.entity.BizSoftwareCopyrightEntity;
import cn.com.poc.ai_software_copyright.service.BizSoftwareCopyrightService;
import cn.com.poc.common.service.RedisService;
import cn.com.yict.framemax.core.spring.SingleContextInitializer;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
......@@ -40,6 +41,23 @@ public class AgentTest {
@Resource
private SoftwareCopyRightAgent softwareCopyRightAgent;
@Resource
private DifyConfigProperties difyConfigProperties;
@Resource
private InformationGenerationStrategy informationGenerationStrategy;
@Test
public void test_difyConfigProperties() {
Map<String, String> apiKeys = difyConfigProperties.getApiKeys();
System.out.println(apiKeys);
System.out.println(apiKeys.get("technical-content"));
System.out.println(informationGenerationStrategy.getCallbackURL());
}
@Test
public void test_agent() throws DifyApiException, IOException, InterruptedException {
BizSoftwareCopyrightEntity bizSoftwareCopyrightEntity = bizSoftwareCopyrightService.get(6L);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment