Merge pull request #256 from StevenJack666/main

修改AI工作流后端逻辑
This commit is contained in:
ageerle
2026-02-11 17:03:31 +08:00
committed by GitHub
9 changed files with 289 additions and 44 deletions

View File

@@ -0,0 +1,42 @@
## 接口信息
**接口路径**: `POST /resource/oss/upload`
**请求类型**: `multipart/form-data`
**权限要求**: `system:oss:upload`
**业务类型**: [INSERT]
### 接口描述
上传OSS对象存储接口用于将文件上传到对象存储服务。
### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
| ---- | ------------- | ---- | ------ |
| file | MultipartFile | 是 | 要上传的文件 |
### 请求头
- `Content-Type`: `multipart/form-data`
### 返回值
返回 `R<SysOssUploadVo>` 类型,包含以下字段:
| 字段名 | 类型 | 说明 |
| -------- | ------ | ------- |
| url | String | 文件访问URL |
| fileName | String | 原始文件名 |
| ossId | String | 文件ID |
### 响应示例
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"url": "fileid://xxx",
"fileName": "example.jpg",
"ossId": "123"
}
}
```
### 异常情况
- 当上传文件为空时,返回错误信息:"上传文件不能为空"

View File

@@ -22,6 +22,12 @@
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
<dependency>
<groupId>com.openai</groupId>
<artifactId>openai-java</artifactId>
<version>4.8.0</version>
</dependency>
</dependencies>
</project>

View File

@@ -8,16 +8,16 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import okhttp3.*;
import org.ruoyi.common.core.constant.CacheNames;
import org.ruoyi.common.core.exception.ServiceException;
import org.ruoyi.common.core.service.ConfigService;
import org.ruoyi.common.core.service.OssService;
import org.ruoyi.common.core.utils.MapstructUtils;
import org.ruoyi.common.core.utils.SpringUtils;
import org.ruoyi.common.core.utils.StreamUtils;
import org.ruoyi.common.core.utils.StringUtils;
import org.ruoyi.common.core.utils.file.FileUtils;
import org.ruoyi.common.oss.core.OssClient;
import org.ruoyi.common.oss.entity.UploadResult;
import org.ruoyi.common.oss.enumd.AccessPolicyType;
import org.ruoyi.common.oss.factory.OssFactory;
import org.ruoyi.core.page.PageQuery;
@@ -27,12 +27,15 @@ import org.ruoyi.system.domain.bo.SysOssBo;
import org.ruoyi.system.domain.vo.SysOssVo;
import org.ruoyi.system.mapper.SysOssMapper;
import org.ruoyi.system.service.ISysOssService;
import org.ruoyi.system.utils.QwenFileUploadUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
@@ -48,6 +51,29 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
private final SysOssMapper baseMapper;
private final ConfigService configService;
// 文档解析判断前缀
private static final String FILE_ID_PREFIX = "fileid://";
// 服务名称
private static final String DASH_SCOPE = "Qwen";
// 默认服务
private static final String CATEGORY = "file";
// apiKey 配置名称
private static final String CONFIG_NAME_KEY = "apiKey";
// apiHost 配置名称
private static final String CONFIG_NAME_URL = "apiHost";
// 默认密钥 todo请在系统配置中设置正确的密钥
private static String API_KEY = "";
// 默认api路径地址
private static String API_HOST = "https://dashscope.aliyuncs.com/compatible-mode/v1/files";
@Override
public TableDataInfo<SysOssVo> queryPageList(SysOssBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<SysOss> lqw = buildQueryWrapper(bo);
@@ -161,26 +187,41 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
@Override
public SysOssVo upload(MultipartFile file) {
String originalfileName = file.getOriginalFilename();
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."),
originalfileName.length());
OssClient storage = OssFactory.instance();
UploadResult uploadResult;
String originalName = file.getOriginalFilename();
int lastDotIndex = originalName != null ? originalName.lastIndexOf(".") : -1;
String prefix = lastDotIndex > 0 ? "" : originalName.substring(0, lastDotIndex);
String suffix = lastDotIndex > 0 ? "" : originalName.substring(lastDotIndex);
File tempFile = null;
try {
uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType());
// 创建临时文件来处理MultipartFile
tempFile = File.createTempFile("upload_", suffix);
file.transferTo(tempFile);
// 获取配置
initConfig();
// 使用工具类上传文件到阿里云
String fileId = QwenFileUploadUtils.uploadFile(tempFile, API_HOST, API_KEY);
if (StringUtils.isEmpty(fileId)) {
throw new ServiceException("文件上传失败未获取到fileId");
}
// 保存文件信息到数据库
SysOss oss = new SysOss();
oss.setUrl(FILE_ID_PREFIX + fileId);
oss.setFileSuffix(suffix);
oss.setFileName(prefix);
oss.setOriginalName(originalName);
oss.setService(DASH_SCOPE);
baseMapper.insert(oss);
SysOssVo sysOssVo = new SysOssVo();
BeanUtils.copyProperties(oss, sysOssVo);
return sysOssVo;
} catch (IOException e) {
throw new ServiceException(e.getMessage());
throw new ServiceException("文件上传失败: " + e.getMessage());
} finally {
// 删除临时文件
if (tempFile != null) {
tempFile.delete();
}
}
// 保存文件信息
SysOss oss = new SysOss();
oss.setUrl(uploadResult.getUrl());
oss.setFileSuffix(suffix);
oss.setFileName(uploadResult.getFilename());
oss.setOriginalName(originalfileName);
oss.setService(storage.getConfigKey());
baseMapper.insert(oss);
SysOssVo sysOssVo = MapstructUtils.convert(oss, SysOssVo.class);
return this.matchingUrl(sysOssVo);
}
@Override
@@ -256,4 +297,20 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
throw new ServiceException("删除文件失败: " + e.getMessage());
}
}
/**
* 初始化配置并返回API密钥和主机
*/
private void initConfig() {
String apiKey = configService.getConfigValue(CATEGORY, CONFIG_NAME_KEY);
if (StringUtils.isEmpty(apiKey)) {
throw new ServiceException("请先配置Qwen上传文件相关API_KEY");
}
API_KEY = apiKey;
String apiHost = configService.getConfigValue(CATEGORY, CONFIG_NAME_URL);
if (StringUtils.isEmpty(apiHost)) {
throw new ServiceException("请先配置Qwen上传文件相关API_HOST");
}
API_HOST = apiHost;
}
}

View File

@@ -0,0 +1,53 @@
package org.ruoyi.system.utils;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import com.alibaba.fastjson.JSONObject;
import org.ruoyi.common.core.utils.StringUtils;
import java.io.File;
import java.io.IOException;
import java.rmi.ServerException;
/***
* 千问上传文件工具类
*/
public class QwenFileUploadUtils {
// 上传本地文件
public static String uploadFile(File file, String apiHost, String apiKey) throws IOException {
OkHttpClient client = new OkHttpClient();
// 构建 multipart/form-data 请求体(千问要求的格式)
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", file.getName(), // 参数名必须为 file
RequestBody.create(MediaType.parse("application/octet-stream"), file))
.addFormDataPart("purpose", "file-extract") // 必须为 file-extract文档解析专用
.build();
// 构建请求(必须为 POST 方法)
Request request = new Request.Builder()
.url(apiHost)
.post(requestBody)
.addHeader("Authorization", apiKey) // 认证头格式正确
.build();
// 发送请求并解析 fileId
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new ServerException("上传失败:" + response.code() + " " + response.message());
}
// 解析响应体,获取 fileId
String responseBody = response.body().string();
if (StringUtils.isEmpty(responseBody)){
throw new ServerException("上传失败:响应体为空");
}
JSONObject jsonObject = JSONObject.parseObject(responseBody);
return jsonObject.getString("id"); // 千问返回的 fileId
}
}
}

View File

@@ -98,7 +98,7 @@ public class WorkflowComponentService extends ServiceImpl<WorkflowComponentMappe
return baseMapper.selectPage(new Page<>(currentPage, pageSize), wrapper);
}
@Cacheable(cacheNames = WORKFLOW_COMPONENTS)
// @Cacheable(cacheNames = WORKFLOW_COMPONENTS)
public List<WorkflowComponent> getAllEnable() {
return ChainWrappers.lambdaQueryChain(baseMapper)
.eq(WorkflowComponent::getIsEnable, true)

View File

@@ -6,6 +6,7 @@ import org.ruoyi.workflow.workflow.node.AbstractWfNode;
import org.ruoyi.workflow.workflow.node.EndNode;
import org.ruoyi.workflow.workflow.node.answer.LLMAnswerNode;
import org.ruoyi.workflow.workflow.node.httpRequest.HttpRequestNode;
import org.ruoyi.workflow.workflow.node.humanFeedBack.HumanFeedbackNode;
import org.ruoyi.workflow.workflow.node.keywordExtractor.KeywordExtractorNode;
import org.ruoyi.workflow.workflow.node.knowledgeRetrieval.KnowledgeRetrievalNode;
import org.ruoyi.workflow.workflow.node.mailSend.MailSendNode;
@@ -25,6 +26,7 @@ public class WfNodeFactory {
case MAIL_SEND -> wfNode = new MailSendNode(wfComponent, nodeDefinition, wfState, nodeState);
case HTTP_REQUEST -> wfNode = new HttpRequestNode(wfComponent, nodeDefinition, wfState, nodeState);
case SWITCHER -> wfNode = new SwitcherNode(wfComponent, nodeDefinition, wfState, nodeState);
case HUMAN_FEEDBACK -> wfNode = new HumanFeedbackNode(wfComponent, nodeDefinition, wfState, nodeState);
default -> {
}
}

View File

@@ -35,6 +35,12 @@ public class WorkflowUtil {
@Resource
private ChatServiceFactory chatServiceFactory;
// 添加默认名称的成员变量
private static final String DEFAULT_NODE_NAME = "input";
// 添加文档解析的前缀字段
private static final String UPLOAD_FILE_API_PREFIX = "fileid";
public static String renderTemplate(String template, List<NodeIOData> values) {
// 🔒 关键修复:如果 template 为 null直接返回 null 或空字符串
if (template == null) {
@@ -125,9 +131,9 @@ public class WorkflowUtil {
// 构建 ruoyi-ai 的 ChatRequest
List<Message> messages = new ArrayList<>();
addUserMessage(node, state.getInputs(), messages);
addSystemMessage(systemMessage, messages);
List<NodeIOData> inputs = state.getInputs();
addUserMessage(node, inputs, messages);
addSystemMessage(systemMessage, inputs, messages);
ChatRequest chatRequest = new ChatRequest();
chatRequest.setModel(modelName);
@@ -150,20 +156,44 @@ public class WorkflowUtil {
}
WfNodeInputConfig nodeInputConfig = NodeInputConfigTypeHandler.fillNodeInputConfig(node.getInputConfig());
List<WfNodeParamRef> refInputs = nodeInputConfig.getRefInputs();
Set<String> nameSet = CollStreamUtil.toSet(refInputs, WfNodeParamRef::getName);
userMessage.stream().filter(item -> nameSet.contains(item.getName()))
.map(item -> getMessage("user", item.getContent().getValue())).forEach(messages::add);
if (CollUtil.isNotEmpty(messages)) {
return;
// 检查是否存在包含fileId的NodeIOData对象
boolean hasFileIdData = hasFileIdData(userMessage);
// 构建消息列表
List<Message> messageList = buildMessageList(userMessage, nameSet, hasFileIdData, DEFAULT_NODE_NAME);
// 如果没有找到匹配的消息尝试使用input字段
if (CollUtil.isEmpty(messageList)) {
messageList = buildMessageList(userMessage, Set.of("input"), hasFileIdData, DEFAULT_NODE_NAME);
}
messages.addAll(messageList);
}
userMessage.stream().filter(item -> "input".equals(item.getName()))
.map(item -> getMessage("user", item.getContent().getValue())).forEach(messages::add);
/**
* 检查是否包含fileId数据
*/
private boolean hasFileIdData(List<NodeIOData> userMessage) {
return userMessage.stream().anyMatch(item ->
item != null &&
item.getContent() != null &&
item.getContent().getValue() != null &&
String.valueOf(item.getContent().getValue()).toLowerCase().contains(UPLOAD_FILE_API_PREFIX)
);
}
/**
* 构建消息列表
*/
private List<Message> buildMessageList(List<NodeIOData> userMessage, Set<String> nameSet, boolean hasFileIdData, String defaultName) {
String role = hasFileIdData ? "system" : "user";
return userMessage.stream()
.filter(item -> item != null && item.getName() != null)
.filter(item -> nameSet.contains(item.getName()) || defaultName.equals(item.getName()))
.map(item -> getMessage(role, item.getContent().getValue()))
.toList();
}
/**
@@ -187,14 +217,22 @@ public class WorkflowUtil {
* @param systemMessage
* @param messages
*/
private void addSystemMessage(List<UserMessage> systemMessage, List<Message> messages) {
log.info("addSystemMessage received: {}", systemMessage); // 🔥 加这一行
private void addSystemMessage(List<UserMessage> systemMessage, List<NodeIOData> userMessage, List<Message> messages) {
log.info("addSystemMessage received: {}", systemMessage);
if (CollUtil.isEmpty(systemMessage)) {
return;
}
// 检查是否存在包含fileId的NodeIOData对象
boolean hasFileIdData = hasFileIdData(userMessage);
// 根据是否有fileId数据确定消息角色
String role = hasFileIdData ? "user" : "system";
// 添加消息
systemMessage.stream()
.map(userMsg -> getMessage("system", userMsg.singleText()))
.map(userMsg -> getMessage(role, userMsg.singleText()))
.forEach(messages::add);
}
}

View File

@@ -6,7 +6,6 @@ import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.commons.lang3.StringUtils;
import org.ruoyi.common.core.exception.base.BaseException;
import org.ruoyi.workflow.base.NodeInputConfigTypeHandler;
import org.ruoyi.workflow.entity.WorkflowComponent;
@@ -124,14 +123,6 @@ public abstract class AbstractWfNode {
log.info("↓↓↓↓↓ node process start,name:{},uuid:{}", node.getTitle(), node.getUuid());
state.setProcessStatus(NODE_PROCESS_STATUS_DOING);
initInput();
//HumanFeedback的情况
Object humanFeedbackState = state.data().get(HUMAN_FEEDBACK_KEY);
if (null != humanFeedbackState) {
String userInput = humanFeedbackState.toString();
if (StringUtils.isNotBlank(userInput)) {
state.getInputs().add(NodeIOData.createByText(HUMAN_FEEDBACK_KEY, "default", userInput));
}
}
if (null != inputConsumer) {
inputConsumer.accept(state);
}

View File

@@ -0,0 +1,56 @@
package org.ruoyi.workflow.workflow.node.humanFeedBack;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.ruoyi.workflow.entity.WorkflowComponent;
import org.ruoyi.workflow.entity.WorkflowNode;
import org.ruoyi.workflow.workflow.NodeProcessResult;
import org.ruoyi.workflow.workflow.WfNodeState;
import org.ruoyi.workflow.workflow.WfState;
import org.ruoyi.workflow.workflow.data.NodeIOData;
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.*;
/**
* 人机交互节点实现类
*/
@Slf4j
public class HumanFeedbackNode extends AbstractWfNode {
public HumanFeedbackNode(WorkflowComponent component, WorkflowNode nodeDefinition, WfState wfState, WfNodeState nodeState) {
super(component, nodeDefinition, wfState, nodeState);
}
// 人机交互节点的处理逻辑
@Override
public NodeProcessResult onProcess() {
log.info("Processing HumanFeedback node: {}", node.getTitle());
// 从状态中获取用户输入数据
Object humanFeedbackState = state.data().get(HUMAN_FEEDBACK_KEY);
if (null != humanFeedbackState) {
String userInput = humanFeedbackState.toString();
if (StringUtils.isNotBlank(userInput)) {
// 用户已提供输入,将用户输入添加到节点输入和输出中
NodeIOData feedbackData = NodeIOData.createByText("output", "default", userInput);
// 添加到输入列表,这样当前节点处理时可以使用
state.getInputs().add(feedbackData);
// 添加到输出列表,这样后续节点可以使用
state.getOutputs().add(feedbackData);
// 设置为成功状态
state.setProcessStatus(NODE_PROCESS_STATUS_SUCCESS);
log.info("Human feedback processed for node: {}, content: {}", node.getTitle(), userInput);
} else {
// 用户输入为空,设置等待状态
state.setProcessStatus(NODE_PROCESS_STATUS_DOING);
log.info("Human feedback is empty for node: {}", node.getTitle());
}
} else {
// 没有用户输入,这可能是正常情况(等待用户输入)
// 但为了确保流程可以继续,我们仍然标记为成功
state.setProcessStatus(NODE_PROCESS_STATUS_SUCCESS);
log.info("No human feedback found for node: {}, continuing workflow", node.getTitle());
}
return new NodeProcessResult();
}
}