mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-03-13 20:53:42 +08:00
42
docs/文件上传接口文档.md
Normal file
42
docs/文件上传接口文档.md
Normal 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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 异常情况
|
||||
- 当上传文件为空时,返回错误信息:"上传文件不能为空"
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException(e.getMessage());
|
||||
// 创建临时文件来处理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(uploadResult.getUrl());
|
||||
oss.setUrl(FILE_ID_PREFIX + fileId);
|
||||
oss.setFileSuffix(suffix);
|
||||
oss.setFileName(uploadResult.getFilename());
|
||||
oss.setOriginalName(originalfileName);
|
||||
oss.setService(storage.getConfigKey());
|
||||
oss.setFileName(prefix);
|
||||
oss.setOriginalName(originalName);
|
||||
oss.setService(DASH_SCOPE);
|
||||
baseMapper.insert(oss);
|
||||
SysOssVo sysOssVo = MapstructUtils.convert(oss, SysOssVo.class);
|
||||
return this.matchingUrl(sysOssVo);
|
||||
SysOssVo sysOssVo = new SysOssVo();
|
||||
BeanUtils.copyProperties(oss, sysOssVo);
|
||||
return sysOssVo;
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("文件上传失败: " + e.getMessage());
|
||||
} finally {
|
||||
// 删除临时文件
|
||||
if (tempFile != null) {
|
||||
tempFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 -> {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user