mirror of
https://github.com/mfish-qf/mfish-nocode.git
synced 2026-09-19 02:21:43 +08:00
feat: 增加文件上传相关逻辑
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package cn.com.mfish.ai.agent;
|
||||
|
||||
import cn.com.mfish.ai.service.FileParseService;
|
||||
import cn.com.mfish.ai.service.LlmModelRouter;
|
||||
import cn.com.mfish.common.ai.agent.EventBus;
|
||||
import cn.com.mfish.common.ai.agent.TenantContext;
|
||||
@@ -10,6 +11,7 @@ import cn.com.mfish.common.ai.entity.EventType;
|
||||
import cn.com.mfish.common.ai.entity.PlanStep;
|
||||
import cn.com.mfish.common.core.utils.AuthInfoUtils;
|
||||
import cn.com.mfish.common.core.utils.ServletUtils;
|
||||
import cn.com.mfish.common.core.utils.StringUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
@@ -48,11 +50,14 @@ public class AgentRuntime {
|
||||
private final Planner planner;
|
||||
private final Executor executor;
|
||||
private final LlmModelRouter llmModelRouter;
|
||||
private final FileParseService fileParseService;
|
||||
|
||||
public AgentRuntime(Planner planner, Executor executor, LlmModelRouter llmModelRouter) {
|
||||
public AgentRuntime(Planner planner, Executor executor, LlmModelRouter llmModelRouter,
|
||||
FileParseService fileParseService) {
|
||||
this.planner = planner;
|
||||
this.executor = executor;
|
||||
this.llmModelRouter = llmModelRouter;
|
||||
this.fileParseService = fileParseService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,11 +72,16 @@ public class AgentRuntime {
|
||||
* 需要用快照构建 ToolContext 和 ChatClient。
|
||||
* </p>
|
||||
* <p>
|
||||
* 文件解析:若请求携带 fileIds,在请求线程同步加载文件内容(Feign 调用 mf-storage),
|
||||
* 拼接到用户提示词前。必须在请求线程执行,因为 Feign 的 BearerTokenInterceptor 依赖
|
||||
* RequestContextHolder 中继令牌,异步线程拿不到。
|
||||
* </p>
|
||||
* <p>
|
||||
* 所有返回的 {@link ChatResponseVo} 的 id 字段填充为 {@link AiRequest#getId()},
|
||||
* 与普通聊天返回结构保持一致,前端可按 id 关联请求与响应。
|
||||
* </p>
|
||||
*
|
||||
* @param aiRequest AI请求参数(含 id/sessionId/message)
|
||||
* @param aiRequest AI请求参数(含 id/sessionId/message/fileIds)
|
||||
* @return 聊天响应流(与普通聊天统一结构)
|
||||
*/
|
||||
public Flux<ChatResponseVo> run(AiRequest aiRequest) {
|
||||
@@ -84,6 +94,9 @@ public class AgentRuntime {
|
||||
// 在请求线程捕获租户上下文快照,供异步编排使用
|
||||
TenantContext tenantContext = captureTenantContext();
|
||||
|
||||
// 文件解析必须在请求线程执行:Feign BearerTokenInterceptor 依赖 RequestContextHolder
|
||||
prompt = resolveFileContents(prompt, aiRequest.getFileIds());
|
||||
|
||||
// 后台启动编排流程
|
||||
runOrchestration(sessionId, prompt, eventBus, tenantContext);
|
||||
|
||||
@@ -91,6 +104,31 @@ public class AgentRuntime {
|
||||
return eventBus.asFlux();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载文件内容并拼接到提示词前
|
||||
* <p>
|
||||
* 若 fileIds 为空或全部加载失败,返回原始 prompt。
|
||||
* 文件内容加载失败时记录日志但不阻断流程,降级为纯文本对话。
|
||||
* </p>
|
||||
*
|
||||
* @param prompt 原始用户提示词
|
||||
* @param fileIds 文件fileKey列表
|
||||
* @return 拼接后的提示词
|
||||
*/
|
||||
private String resolveFileContents(String prompt, List<String> fileIds) {
|
||||
if (fileIds == null || fileIds.isEmpty()) {
|
||||
return prompt;
|
||||
}
|
||||
String fileContents = fileParseService.loadFileContents(fileIds);
|
||||
if (StringUtils.isEmpty(fileContents)) {
|
||||
log.warn("[AgentRuntime] 文件内容加载为空 fileIds={}", fileIds);
|
||||
return prompt;
|
||||
}
|
||||
return "以下是用户上传的文件内容,请基于文件内容进行分析:\n\n"
|
||||
+ fileContents
|
||||
+ "\n用户需求:" + prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在请求线程捕获租户上下文快照
|
||||
* <p>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.com.mfish.ai.agent;
|
||||
|
||||
import cn.com.mfish.ai.service.FileParseService;
|
||||
import cn.com.mfish.ai.service.LlmModelRouter;
|
||||
import cn.com.mfish.common.ai.client.IClientAssistant;
|
||||
import cn.com.mfish.common.ai.engine.ApiToolEngine;
|
||||
@@ -17,6 +18,7 @@ import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
|
||||
import org.springframework.ai.chat.memory.ChatMemory;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
@@ -26,6 +28,7 @@ import reactor.core.scheduler.Schedulers;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
@@ -51,6 +54,13 @@ public abstract class BaseAssistant implements IClientAssistant, ToolCapable {
|
||||
private final LlmModelRouter llmModelRouter;
|
||||
protected final ApiToolEngine apiToolEngine;
|
||||
|
||||
/**
|
||||
* 文件解析服务:用于根据AiRequest.fileIds获取文件内容并注入提示词
|
||||
* 采用字段注入避免修改所有子类构造函数
|
||||
*/
|
||||
@Autowired
|
||||
protected FileParseService fileParseService;
|
||||
|
||||
public BaseAssistant(ChatMemory chatMemory, LlmModelRouter llmModelRouter, ApiToolEngine apiToolEngine) {
|
||||
this.llmModelRouter = llmModelRouter;
|
||||
this.chatMemory = chatMemory;
|
||||
@@ -268,12 +278,26 @@ public abstract class BaseAssistant implements IClientAssistant, ToolCapable {
|
||||
|
||||
/**
|
||||
* 聊天返回id
|
||||
* <p>
|
||||
* 若请求携带fileIds,会先通过FileParseService从文件服务获取文件内容,
|
||||
* 将其拼接到用户提示词前,再交由子类chat(sessionId, prompt)处理。
|
||||
*
|
||||
* @return 聊天信息
|
||||
*/
|
||||
@Override
|
||||
public Flux<ChatResponseVo> chat(AiRequest aiRequest) {
|
||||
return chat(aiRequest.getSessionId(), aiRequest.getMessage().getContent())
|
||||
String prompt = aiRequest.getMessage().getContent();
|
||||
List<String> fileIds = aiRequest.getFileIds();
|
||||
if (fileIds != null && !fileIds.isEmpty()) {
|
||||
String fileContents = fileParseService.loadFileContents(fileIds);
|
||||
if (StringUtils.isNotEmpty(fileContents)) {
|
||||
prompt = "以下是用户上传的文件内容,请基于文件内容进行分析:\n\n"
|
||||
+ fileContents
|
||||
+ "\n用户问题:" + prompt;
|
||||
}
|
||||
}
|
||||
final String finalPrompt = prompt;
|
||||
return chat(aiRequest.getSessionId(), finalPrompt)
|
||||
.filter(resp -> "STOP".equals(Objects.requireNonNull(resp.getResult()).getMetadata().getFinishReason())
|
||||
|| StringUtils.isNotEmpty(resp.getResult().getOutput().getText()))
|
||||
.map(resp -> new ChatResponseVo().setId(aiRequest.getId())
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
package cn.com.mfish.ai.service;
|
||||
|
||||
import cn.com.mfish.common.core.constants.RPCConstants;
|
||||
import cn.com.mfish.common.core.utils.StringUtils;
|
||||
import cn.com.mfish.common.storage.api.entity.StorageInfo;
|
||||
import cn.com.mfish.common.storage.api.remote.RemoteStorageService;
|
||||
import cn.com.mfish.common.core.web.Result;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.extractor.ExtractorFactory;
|
||||
import org.apache.poi.extractor.POITextExtractor;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 文件解析服务
|
||||
* <p>
|
||||
* 根据文件fileKey列表,通过远程存储服务获取文件元数据与内容,
|
||||
* 将文件内容提取为LLM可理解的提示词片段。
|
||||
* <p>
|
||||
* 支持的文件类型:
|
||||
* <ul>
|
||||
* <li>文本类:txt/log/md/json/xml/yml/csv/sql/源代码等(直接UTF-8读取)</li>
|
||||
* <li>Office文档(仅OOXML新格式):docx/xlsx/pptx(通过Apache POI提取文本)</li>
|
||||
* <li>其他二进制:图片/压缩包/旧版Office(.doc/.xls/.ppt)等(返回提示信息,不解析内容)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 注意:旧版OLE2格式(.doc/.xls/.ppt)需要 poi-scratchpad 依赖,当前未引入,
|
||||
* 前端应限制只允许上传 .docx/.xlsx/.pptx 格式。
|
||||
* <p>
|
||||
* 调用链:FileParseService → RemoteStorageService(Feign) → mf-storage 服务
|
||||
* BearerTokenInterceptor 自动中继用户令牌,保证私有文件可访问。
|
||||
*
|
||||
* @author: mfish
|
||||
* @date: 2026/07/20
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class FileParseService {
|
||||
/**
|
||||
* 单个文件读取的最大字符数,超过则截断,避免撑爆LLM上下文
|
||||
*/
|
||||
private static final int MAX_CONTENT_CHARS = 51200;
|
||||
/**
|
||||
* 文本类contentType前缀集合,命中时按文本读取
|
||||
*/
|
||||
private static final Set<String> TEXT_CONTENT_TYPE_PREFIXES = Set.of("text/");
|
||||
/**
|
||||
* 文本类contentType精确匹配集合
|
||||
*/
|
||||
private static final Set<String> TEXT_CONTENT_TYPES = Set.of(
|
||||
"application/json",
|
||||
"application/xml",
|
||||
"application/javascript",
|
||||
"application/x-yaml",
|
||||
"application/yaml",
|
||||
"application/x-sh",
|
||||
"application/sql",
|
||||
"application/csv",
|
||||
"application/x-java",
|
||||
"application/x-tex",
|
||||
"application/manifest+json",
|
||||
"application/graphql-response+json"
|
||||
);
|
||||
/**
|
||||
* 文本类文件后缀集合(按扩展名兜底识别)
|
||||
*/
|
||||
private static final Set<String> TEXT_SUFFIXES = Set.of(
|
||||
".txt", ".log", ".md", ".markdown", ".json", ".xml", ".yaml", ".yml",
|
||||
".csv", ".sql", ".js", ".ts", ".java", ".py", ".go", ".rs", ".c",
|
||||
".cpp", ".h", ".hpp", ".cs", ".rb", ".php", ".swift", ".kt", ".scala",
|
||||
".sh", ".bat", ".ps1", ".properties", ".conf", ".ini", ".toml",
|
||||
".html", ".htm", ".css", ".scss", ".less", ".vue", ".tsx", ".jsx",
|
||||
".gradle", ".gitignore", ".dockerfile", ".env", ".proto"
|
||||
);
|
||||
/**
|
||||
* Office文档后缀集合(仅OOXML新格式,通过Apache POI解析)
|
||||
* <p>
|
||||
* 旧版OLE2格式(.doc/.xls/.ppt)需要 poi-scratchpad 依赖,当前未引入,
|
||||
* 前端应限制只允许上传新格式。
|
||||
*/
|
||||
private static final Set<String> OFFICE_SUFFIXES = Set.of(
|
||||
".docx", ".xlsx", ".pptx", ".docm", ".dotm", ".xlsm", ".xltm", ".pptm", ".potm"
|
||||
);
|
||||
/**
|
||||
* Office文档contentType精确匹配集合(仅OOXML新格式)
|
||||
*/
|
||||
private static final Set<String> OFFICE_CONTENT_TYPES = Set.of(
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.template",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.template"
|
||||
);
|
||||
|
||||
private final RemoteStorageService remoteStorageService;
|
||||
|
||||
public FileParseService(RemoteStorageService remoteStorageService) {
|
||||
this.remoteStorageService = remoteStorageService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载文件内容并拼接为提示词片段
|
||||
* <p>
|
||||
* 遍历fileKey列表,逐个获取文件元数据与内容,将文本/Office文档内容拼接到StringBuilder。
|
||||
* 单个文件读取失败不影响其他文件,异常会被记录日志并跳过。
|
||||
*
|
||||
* @param fileIds 文件fileKey列表
|
||||
* @return 拼接好的文件内容提示词片段;列表为空或全部失败时返回空字符串
|
||||
*/
|
||||
public String loadFileContents(List<String> fileIds) {
|
||||
if (fileIds == null || fileIds.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int successCount = 0;
|
||||
for (String fileKey : fileIds) {
|
||||
if (StringUtils.isEmpty(fileKey)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
String segment = loadOneFile(fileKey);
|
||||
if (segment != null && !segment.isEmpty()) {
|
||||
sb.append(segment).append("\n\n");
|
||||
successCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("加载文件内容失败 fileKey={} reason={}", fileKey, e.getMessage());
|
||||
}
|
||||
}
|
||||
if (successCount == 0) {
|
||||
return "";
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载单个文件并构建内容片段
|
||||
* <p>
|
||||
* 按文件类型分发到不同解析路径:
|
||||
* <ol>
|
||||
* <li>文本类:直接UTF-8读取</li>
|
||||
* <li>Office文档:通过POI提取文本</li>
|
||||
* <li>其他二进制:返回提示信息</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param fileKey 文件key
|
||||
* @return 文件内容片段;文件不存在返回null
|
||||
*/
|
||||
private String loadOneFile(String fileKey) {
|
||||
Result<StorageInfo> infoResult = remoteStorageService.queryByKey(RPCConstants.INNER, fileKey);
|
||||
if (infoResult == null || !infoResult.isSuccess() || infoResult.getData() == null) {
|
||||
log.warn("文件信息查询失败 fileKey={} msg={}", fileKey,
|
||||
infoResult == null ? "result is null" : infoResult.getMsg());
|
||||
return null;
|
||||
}
|
||||
StorageInfo storageInfo = infoResult.getData();
|
||||
String fileName = StringUtils.isEmpty(storageInfo.getFileName()) ? fileKey : storageInfo.getFileName();
|
||||
String fileType = storageInfo.getFileType();
|
||||
|
||||
if (isTextFile(fileType, fileName)) {
|
||||
String content = readFileText(fileKey);
|
||||
return content == null ? buildReadFailureHint(fileName) : buildTextFileSegment(fileName, truncate(content));
|
||||
}
|
||||
if (isOfficeDocument(fileType, fileName)) {
|
||||
String content = extractOfficeText(fileKey, fileName);
|
||||
return content == null ? buildReadFailureHint(fileName) : buildTextFileSegment(fileName, truncate(content));
|
||||
}
|
||||
return buildBinaryFileHint(fileName, fileType, storageInfo.getFileSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过Feign获取文件资源并读取为UTF-8文本
|
||||
*
|
||||
* @param fileKey 文件key
|
||||
* @return 文件文本内容;读取失败返回null
|
||||
*/
|
||||
private String readFileText(String fileKey) {
|
||||
ResponseEntity<Resource> resp = remoteStorageService.fetch(RPCConstants.INNER, fileKey);
|
||||
if (resp == null || !resp.getStatusCode().is2xxSuccessful() || resp.getBody() == null) {
|
||||
log.warn("文件资源获取失败 fileKey={} status={}", fileKey,
|
||||
resp == null ? "null" : resp.getStatusCode());
|
||||
return null;
|
||||
}
|
||||
try (InputStream is = resp.getBody().getInputStream()) {
|
||||
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
log.warn("文件内容读取失败 fileKey={} reason={}", fileKey, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过Apache POI提取Office文档文本
|
||||
* <p>
|
||||
* 使用 {@link ExtractorFactory} 自动识别文档类型(Word/Excel/PowerPoint,旧版/新版格式),
|
||||
* 无需根据后缀手动分发,POI内部根据文件魔数选择合适的Extractor。
|
||||
* <p>
|
||||
* 注意:POI要求InputStream支持mark/reset,Feign返回的InputStream需先缓存为字节数组再包装。
|
||||
*
|
||||
* @param fileKey 文件key
|
||||
* @param fileName 文件名(仅用于日志)
|
||||
* @return 提取的文本内容;提取失败返回null
|
||||
*/
|
||||
private String extractOfficeText(String fileKey, String fileName) {
|
||||
ResponseEntity<Resource> resp = remoteStorageService.fetch(RPCConstants.INNER, fileKey);
|
||||
if (resp == null || !resp.getStatusCode().is2xxSuccessful() || resp.getBody() == null) {
|
||||
log.warn("Office文件资源获取失败 fileKey={} status={}", fileKey,
|
||||
resp == null ? "null" : resp.getStatusCode());
|
||||
return null;
|
||||
}
|
||||
// POI的ExtractorFactory.createExtractor需要File或mark/reset支持的InputStream
|
||||
// ByteArrayInputStream支持mark/reset,避免POI内部reset失败
|
||||
try (InputStream is = resp.getBody().getInputStream()) {
|
||||
byte[] bytes = is.readAllBytes();
|
||||
try (InputStream poiStream = new java.io.ByteArrayInputStream(bytes);
|
||||
POITextExtractor extractor = ExtractorFactory.createExtractor(poiStream)) {
|
||||
String text = extractor.getText();
|
||||
log.info("Office文档解析成功 fileName={} chars={}", fileName,
|
||||
text == null ? 0 : text.length());
|
||||
return text;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Office文档解析失败 fileKey={} fileName={} reason={}", fileKey, fileName, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断过长的内容,避免撑爆LLM上下文
|
||||
*/
|
||||
private String truncate(String content) {
|
||||
if (content == null) {
|
||||
return "";
|
||||
}
|
||||
if (content.length() > MAX_CONTENT_CHARS) {
|
||||
return content.substring(0, MAX_CONTENT_CHARS)
|
||||
+ "\n...[文件内容过长,已截断,仅显示前" + MAX_CONTENT_CHARS + "字符]";
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断文件是否为文本类文件
|
||||
* <p>
|
||||
* 优先按contentType判断,再按文件名后缀兜底
|
||||
*
|
||||
* @param contentType 文件MIME类型
|
||||
* @param fileName 文件名
|
||||
* @return true表示可按文本读取
|
||||
*/
|
||||
private boolean isTextFile(String contentType, String fileName) {
|
||||
if (StringUtils.isNotEmpty(contentType)) {
|
||||
String lower = contentType.toLowerCase(Locale.ROOT);
|
||||
if (TEXT_CONTENT_TYPE_PREFIXES.stream().anyMatch(lower::startsWith)) {
|
||||
return true;
|
||||
}
|
||||
if (TEXT_CONTENT_TYPES.contains(lower)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotEmpty(fileName)) {
|
||||
String lowerName = fileName.toLowerCase(Locale.ROOT);
|
||||
return TEXT_SUFFIXES.stream().anyMatch(lowerName::endsWith);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断文件是否为Office文档
|
||||
* <p>
|
||||
* 优先按contentType判断,再按文件名后缀兜底
|
||||
*
|
||||
* @param contentType 文件MIME类型
|
||||
* @param fileName 文件名
|
||||
* @return true表示为Office文档(Word/Excel/PowerPoint)
|
||||
*/
|
||||
private boolean isOfficeDocument(String contentType, String fileName) {
|
||||
if (StringUtils.isNotEmpty(contentType)) {
|
||||
String lower = contentType.toLowerCase(Locale.ROOT);
|
||||
if (OFFICE_CONTENT_TYPES.contains(lower)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotEmpty(fileName)) {
|
||||
String lowerName = fileName.toLowerCase(Locale.ROOT);
|
||||
return OFFICE_SUFFIXES.stream().anyMatch(lowerName::endsWith);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String buildTextFileSegment(String fileName, String content) {
|
||||
return "=== 文件: " + fileName + " ===\n" + content + "\n=== 文件结束: " + fileName + " ===";
|
||||
}
|
||||
|
||||
private String buildBinaryFileHint(String fileName, String fileType, Integer fileSize) {
|
||||
return "=== 文件: " + fileName + " ===\n" +
|
||||
"[二进制文件,无法直接作为文本解析] 类型: " + (StringUtils.isEmpty(fileType) ? "未知" : fileType) +
|
||||
" 大小: " + (fileSize == null ? "未知" : fileSize + " bytes") +
|
||||
"\n=== 文件结束: " + fileName + " ===";
|
||||
}
|
||||
|
||||
private String buildReadFailureHint(String fileName) {
|
||||
return "=== 文件: " + fileName + " ===\n[文件内容读取失败]\n=== 文件结束: " + fileName + " ===";
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package cn.com.mfish.common.ai.entity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @description: Ai请求
|
||||
* @author: mfish
|
||||
@@ -17,4 +19,6 @@ public class AiRequest {
|
||||
private String sessionId;
|
||||
@Schema(description = "消息")
|
||||
private AiMessage message;
|
||||
@Schema(description = "文件ID列表(文件fileKey集合,AI将获取文件内容并解析)")
|
||||
private List<String> fileIds;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user