package com.gxwebsoft.ai.controller;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gxwebsoft.ai.config.DifyProperties;
import com.gxwebsoft.ai.entity.ActivityOutlineInputs;
import com.gxwebsoft.ai.entity.AiChatHistory;
import com.gxwebsoft.ai.entity.AiChatList;
import com.gxwebsoft.ai.entity.DeclarationInputs;
import com.gxwebsoft.ai.entity.DocumentProcessInputs;
import com.gxwebsoft.ai.entity.ManuscriptGenInputs;
import com.gxwebsoft.ai.service.AiChatHistoryService;
import com.gxwebsoft.ai.service.AiChatListService;
import com.gxwebsoft.common.core.utils.JSONUtil;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.ai.config.WebSocketServer;
import com.gxwebsoft.ai.entity.ChatMessage;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.*;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Api(tags = "AI")
@RestController
@RequestMapping("/api/ai")
public class AiController extends BaseController {
private static final Logger logger = LoggerFactory.getLogger(AiController.class);
private static final long AI_REQUEST_TIMEOUT = 10 * 60 * 1000L;
/**
* 创意生成工作流以失败收场时给用户看的文案。
*
*
上游的真实失败原因(如 {@code Model disabled})属于 Dify 侧配置问题,
* 既不面向用户也不该泄露;这里统一给一句可读提示。
*/
private static final String CREATIVE_FAILED_MESSAGE = "生成失败,请稍后重试";
/**
* 内部标识符形状:纯 UUID({@code workflow_run_id} / 节点 id 等)。
* 与前端 {@code ai-result.js} 的 {@code isDisplayableAnswer} 同一口径。
*/
private static final Pattern UUID_PATTERN = Pattern.compile(
"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}");
/**
* 无空格的长 token({@code task_id} / {@code req_id} 等)。
*/
private static final Pattern LONG_TOKEN_PATTERN = Pattern.compile("[0-9a-zA-Z_-]{32,}");
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s");
/**
* loading 气泡文案的长度上限。
*
*
「加载中」气泡位置只够显示一句短提示;{@code agent_thought} 的思维链
* 实测可达数 KB,整段塞进去等于把思维链推给端上,故超限即弃用。
*/
private static final int LOADING_TEXT_MAX_LENGTH = 120;
/**
* 从「活动资金」的自由文本里抽金额。
*
*
两个捕获组:数值(容忍千分位逗号与小数)+ 可选的中文倍率单位。
* 单位前用 {@code \s*} 容忍「1 万元」这类写法 —— 只认紧邻的「万」会把 1 万元抽成 1 元。
*/
private static final Pattern EVENT_FUNDING_AMOUNT_PATTERN =
Pattern.compile("(\\d[\\d,]*(?:\\.\\d+)?)\\s*([亿万千百]?)");
/**
* 日期/时间表达式,用于在抽金额前剔除。
*
*
{@code "2026年4月办活动,预算5000元"} 里 2026 会被金额正则当成第一处数字,
* 静默发出错误金额 —— 比留空更糟,故先整体剔除。
*/
private static final Pattern EVENT_FUNDING_DATE_PATTERN = Pattern.compile(
"\\d[\\d,]*\\s*(?:年|月|日|号|时|点|分|秒)"
+ "|\\d{4}\\s*[-/.]\\s*\\d{1,2}\\s*[-/.]\\s*\\d{1,2}"
+ "|\\d{1,2}\\s*[-/.]\\s*\\d{1,2}\\s*[-/.]\\s*\\d{2,4}");
@Resource
private WebSocketServer webSocketServer;
@Resource
private AiChatHistoryService aiChatHistoryService;
@Resource
private AiChatListService aiChatListService;
@Resource
private DifyProperties difyProperties;
/**
* 用于把入参实体序列化成 Map,尊重 Jackson 注解
*/
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* Dify 接口地址前缀,取自 {@code dify.base-url}
*/
private String aiUrl() {
return difyProperties.getBaseUrl();
}
/**
* 各应用 API Key,取自 {@code dify.apps.*}。
*
*
原先这些 Key 硬编码在字段上,现统一外置到配置;取 Key 一律走这里,
* 便于换环境时只动 yml。
*/
private DifyProperties.AppKeys appKeys() {
return difyProperties.getApps();
}
/**
* 单次请求超时(毫秒)
*/
private int aiTimeout() {
Integer timeout = difyProperties.getTimeoutMs();
return timeout == null || timeout <= 0 ? 600000 : timeout;
}
private final Map activeAiTaskMap = new ConcurrentHashMap<>();
@PostMapping("/chat/message")
public ApiResult> message(@RequestBody ChatMessage message) throws IOException {
try {
URL url = new URL(aiUrl() + "/chat-messages");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Authorization", "Bearer " + appKeys().getChat());
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Accept", "text/event-stream");
connection.setDoOutput(true);
connection.setConnectTimeout(aiTimeout());
connection.setReadTimeout(aiTimeout());
Integer chatId = message.getChatId();
if (chatId == null || chatId == 0) {
AiChatList aiChatList = new AiChatList();
aiChatList.setUserId(getLoginUserId());
aiChatList.setTitle(message.getQuery());
aiChatListService.save(aiChatList);
chatId = aiChatList.getId();
}
Map payload = new HashMap<>();
payload.put("query", message.getQuery());
payload.put("response_mode", StrUtil.blankToDefault(message.getResponseMode(), "streaming"));
payload.put("user", getLoginUserId() == null ? "0" : getLoginUserId().toString());
payload.put("conversation_id", StrUtil.blankToDefault(message.getConversationId(), ""));
payload.put("inputs", parseInputs(message.getInputs()));
System.out.println("payload:" + payload);
try (OutputStream os = connection.getOutputStream()) {
os.write(JSONUtil.toJSONString(payload).getBytes(StandardCharsets.UTF_8));
os.flush();
}
int responseCode = connection.getResponseCode();
if (responseCode < 200 || responseCode >= 300) {
String responseBody = readStream(connection.getErrorStream());
JSONObject result = JSONObject.parseObject(responseBody);
throw new RuntimeException(result != null ? result.toJSONString() : responseBody);
}
AiChatHistory aiChatHistory = new AiChatHistory();
aiChatHistory.setUserId(getLoginUserId());
aiChatHistory.setChatId(chatId);
aiChatHistory.setContent(message.getQuery());
StringBuilder reply = new StringBuilder();
String conversationId = null;
String messageId = null;
String taskId = null;
boolean conversationSaved = false;
boolean historySaved = false;
try (BufferedReader br = new BufferedReader(
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
if (StrUtil.isBlank(line) || !line.startsWith("data:")) {
continue;
}
String json = line.substring(5).trim();
if (StrUtil.isBlank(json)) {
continue;
}
Map data = JSONUtil.parseObject(json, Map.class);
if (data == null) {
continue;
}
Object eventObj = data.get("event");
String event = eventObj == null ? "" : eventObj.toString();
if (StrUtil.isNotBlank(stringValue(data.get("task_id")))) {
taskId = stringValue(data.get("task_id"));
recordActiveAiTask(getLoginUserId(), "chat", taskId, appKeys().getChat());
}
String currentConversationId = data.get("conversation_id") == null
? conversationId
: data.get("conversation_id").toString();
String currentMessageId = data.get("message_id") == null
? messageId
: data.get("message_id").toString();
if (StrUtil.isNotBlank(currentConversationId)) {
conversationId = currentConversationId;
if (!conversationSaved) {
aiChatListService.updateConversationId(chatId, conversationId);
conversationSaved = true;
}
}
if (StrUtil.isNotBlank(currentMessageId)) {
messageId = currentMessageId;
}
if ("message".equals(event)) {
String answer = data.get("answer") == null ? null : data.get("answer").toString();
if (answer != null) {
reply.append(answer);
HashMap answerData = new HashMap<>();
answerData.put("answer", answer);
answerData.put("conversationId", StrUtil.blankToDefault(conversationId, "0"));
answerData.put("messageId", StrUtil.blankToDefault(messageId, "0"));
answerData.put("taskId", StrUtil.blankToDefault(taskId, "0"));
answerData.put("stopType", "chat");
webSocketServer.sendMessage(getLoginUserId().toString(), JSONUtil.toJSONString(answerData));
}
} else if ("agent_log".equals(event) || "agent_thought".equals(event)) {
String answer = data.get("thought") == null
? (data.get("answer") == null ? null : data.get("answer").toString())
: data.get("thought").toString();
if (answer != null) {
System.out.println(answer);
HashMap answerData = new HashMap<>();
answerData.put("answer", answer);
answerData.put("loading", "true");
answerData.put("conversationId", StrUtil.blankToDefault(conversationId, "0"));
answerData.put("messageId", StrUtil.blankToDefault(messageId, "0"));
answerData.put("taskId", StrUtil.blankToDefault(taskId, "0"));
answerData.put("stopType", "chat");
webSocketServer.sendMessage(getLoginUserId().toString(), JSONUtil.toJSONString(answerData));
}
} else if ("message_end".equals(event) || "workflow_finished".equals(event)) {
if (!historySaved) {
historySaved = true;
aiChatHistory.setConversationId(conversationId);
aiChatHistory.setReply(reply.toString());
aiChatHistoryService.save(aiChatHistory);
}
HashMap endData = new HashMap<>();
endData.put("answer", "__END__");
endData.put("conversationId", StrUtil.blankToDefault(conversationId, "0"));
endData.put("messageId", StrUtil.blankToDefault(messageId, "0"));
endData.put("taskId", StrUtil.blankToDefault(taskId, "0"));
endData.put("stopType", "chat");
webSocketServer.sendMessage(getLoginUserId().toString(), JSONUtil.toJSONString(endData));
clearActiveAiTask(getLoginUserId(), taskId);
} else if ("error".equals(event)) {
throw new RuntimeException(json);
}
}
}
if (!historySaved) {
aiChatHistory.setConversationId(conversationId);
aiChatHistory.setReply(reply.toString());
aiChatHistoryService.save(aiChatHistory);
HashMap endData = new HashMap<>();
endData.put("answer", "__END__");
endData.put("conversationId", StrUtil.blankToDefault(conversationId, "0"));
endData.put("messageId", StrUtil.blankToDefault(messageId, "0"));
endData.put("taskId", StrUtil.blankToDefault(taskId, "0"));
endData.put("stopType", "chat");
webSocketServer.sendMessage(getLoginUserId().toString(), JSONUtil.toJSONString(endData));
clearActiveAiTask(getLoginUserId(), taskId);
}
} catch (Exception e) {
System.out.println(e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
System.out.println(stackTraceElement);
}
HashMap answerData = new HashMap<>();
answerData.put("answer", "出错了,请晚点再来提问吧~");
answerData.put("taskId", "0");
webSocketServer.sendMessage(getLoginUserId().toString(), JSONUtil.toJSONString(answerData));
// webSocketServer.sendMessage(getLoginUserId().toString(), "出错了,请晚点再来提问吧~");
clearActiveAiTask(getLoginUserId(), null);
return fail("出错了,请晚点再来提问吧~");
}
// 返回成功响应
return success("Stream processing completed");
}
@PostMapping("/manuscriptGen")
public ApiResult> manuscriptGen(@RequestBody ManuscriptGenInputs message) throws IOException {
try {
URL url = new URL(aiUrl() + "/workflows/run");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Authorization", "Bearer " + appKeys().getManuscript());
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Accept", "text/event-stream");
connection.setDoOutput(true);
connection.setConnectTimeout(aiTimeout());
connection.setReadTimeout(aiTimeout());
Map inputs = buildManuscriptInputs(message);
// 附件当前不转发给 Dify:该应用没有文件类输入变量(file_upload.enabled=false),
// 上游也不认这个字段。原先这里调了 normalizeFiles 却从不使用其返回值,
// 反而让人以为附件已生效(见 issue 07)。要真正支持附件需先在 Dify
// 开始节点加文件变量,再按文件类变量的契约(transfer_method / upload_file_id)改造。
Map payload = new HashMap<>();
payload.put("response_mode", "streaming");
payload.put("user", getLoginUserId() == null ? "0" : getLoginUserId().toString());
payload.put("inputs", inputs);
System.out.println("manuscript payload:" + payload);
try (OutputStream os = connection.getOutputStream()) {
os.write(JSONUtil.toJSONString(payload).getBytes(StandardCharsets.UTF_8));
os.flush();
}
int responseCode = connection.getResponseCode();
if (responseCode < 200 || responseCode >= 300) {
String responseBody = readStream(connection.getErrorStream());
JSONObject result = JSONObject.parseObject(responseBody);
throw new RuntimeException(result != null ? result.toJSONString() : responseBody);
}
StringBuilder reply = new StringBuilder();
String workflowRunId = null;
String taskId = null;
boolean endSent = false;
// 上游以 failed 收场时置真,用于给 HTTP 返回体带上 status(见 issue 10)。
// 不用局部布尔直接在收尾赋值是因为失败分支里 `continue` 了,
// 必须在外层作用域里记录。
boolean workflowFailed = false;
try (BufferedReader br = new BufferedReader(
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
if (StrUtil.isBlank(line) || !line.startsWith("data:")) {
continue;
}
String json = line.substring(5).trim();
if (StrUtil.isBlank(json)) {
continue;
}
Map data = JSONUtil.parseObject(json, Map.class);
if (data == null) {
continue;
}
String event = data.get("event") == null ? "" : data.get("event").toString();
if (StrUtil.isNotBlank(stringValue(data.get("workflow_run_id")))) {
workflowRunId = stringValue(data.get("workflow_run_id"));
}
if (StrUtil.isNotBlank(stringValue(data.get("task_id")))) {
taskId = stringValue(data.get("task_id"));
} else if (StrUtil.isNotBlank(stringValue(data.get("id")))) {
taskId = stringValue(data.get("id"));
}
if (StrUtil.isNotBlank(taskId)) {
recordActiveAiTask(getLoginUserId(), "workflow", taskId, appKeys().getManuscript());
}
System.out.println(data);
// 正文只认正文事件(text_chunk / message);其余事件的
// extractAnswer 结果可能是 data.id 这类内部标识符,不得进 reply(见 issue 06)。
String answer = extractAnswer(data);
if (answer != null && ("text_chunk".equals(event) || "message".equals(event))) {
reply.append(answer);
HashMap answerData = new HashMap<>();
answerData.put("answer", answer);
answerData.put("taskId", StrUtil.blankToDefault(taskId, "0"));
answerData.put("workflowRunId", StrUtil.blankToDefault(workflowRunId, "0"));
webSocketServer.sendMessage(getLoginUserId().toString(), JSONUtil.toJSONString(answerData));
} else if ("workflow_started".equals(event) || "node_started".equals(event)
|| "node_finished".equals(event) || "agent_log".equals(event)
|| "agent_thought".equals(event)) {
// 这些事件只用于「加载中」提示,不下发正文。
// 不能用 extractAnswer 兜底:它可能在 outputs 里取到**整篇**稿件,
// 当成 loading 帧发出去等于把正文重复推送一遍(见 issue 06)。
String loadingText = buildLoadingText(event, data);
if (loadingText != null) {
HashMap answerData = new HashMap<>();
answerData.put("answer", loadingText);
answerData.put("loading", "true");
answerData.put("taskId", StrUtil.blankToDefault(taskId, "0"));
answerData.put("workflowRunId", StrUtil.blankToDefault(workflowRunId, "0"));
webSocketServer.sendMessage(getLoginUserId().toString(), JSONUtil.toJSONString(answerData));
}
} else if ("workflow_finished".equals(event) || "message_end".equals(event)) {
// 失败态先判定:outputs 为空时 answer 可能是 workflow_run_id
// 这类内部标识符,不能当正文推给端上(见 issue 03)。
if (isWorkflowFailed(workflowStatus(data))) {
logger.warn("manuscript workflow failed, taskId={}, workflowRunId={}",
taskId, workflowRunId);
workflowFailed = true;
reply.setLength(0);
reply.append(CREATIVE_FAILED_MESSAGE);
HashMap failureData = new HashMap<>();
failureData.put("answer", CREATIVE_FAILED_MESSAGE);
failureData.put("taskId", StrUtil.blankToDefault(taskId, "0"));
failureData.put("workflowRunId", StrUtil.blankToDefault(workflowRunId, "0"));
// reset=true:丢弃失败前可能已推给端的半截内容
failureData.put("reset", "true");
webSocketServer.sendMessage(getLoginUserId().toString(),
JSONUtil.toJSONString(failureData));
if (!endSent) {
endSent = true;
HashMap endData = new HashMap<>();
endData.put("answer", "__END__");
endData.put("taskId", StrUtil.blankToDefault(taskId, "0"));
endData.put("workflowRunId", StrUtil.blankToDefault(workflowRunId, "0"));
webSocketServer.sendMessage(getLoginUserId().toString(),
JSONUtil.toJSONString(endData));
}
clearActiveAiTask(getLoginUserId(), taskId);
continue;
}
// 收尾事件不产出正文:正文来自 text_chunk / message。
// 这里只在流式正文缺失时用 outputs 兜底,且兜底值同样要过
// 可展示性校验——否则会重演 issue 06(把 data.id 当正文)。
String finalAnswer = answer;
if (finalAnswer == null) {
finalAnswer = extractWorkflowOutputs(data);
}
if (isDisplayableText(finalAnswer)
&& reply.indexOf(finalAnswer) < 0) {
// 注:isDisplayableText 已排除空串,故无需再判 length()==0。
// 反过来若把空串放进来,reply.indexOf("") 恒为 0 会误判"已存在"
// 而短路掉正常内容,所以两道判断缺一不可。
reply.append(finalAnswer);
HashMap answerData = new HashMap<>();
answerData.put("answer", finalAnswer);
answerData.put("taskId", StrUtil.blankToDefault(taskId, "0"));
answerData.put("workflowRunId", StrUtil.blankToDefault(workflowRunId, "0"));
webSocketServer.sendMessage(getLoginUserId().toString(), JSONUtil.toJSONString(answerData));
}
if (!endSent) {
endSent = true;
HashMap endData = new HashMap<>();
endData.put("answer", "__END__");
endData.put("taskId", StrUtil.blankToDefault(taskId, "0"));
endData.put("workflowRunId", StrUtil.blankToDefault(workflowRunId, "0"));
webSocketServer.sendMessage(getLoginUserId().toString(), JSONUtil.toJSONString(endData));
}
clearActiveAiTask(getLoginUserId(), taskId);
} else if ("error".equals(event)) {
throw new RuntimeException(json);
}
}
}
if (!endSent) {
HashMap endData = new HashMap<>();
endData.put("answer", "__END__");
endData.put("taskId", StrUtil.blankToDefault(taskId, "0"));
endData.put("workflowRunId", StrUtil.blankToDefault(workflowRunId, "0"));
webSocketServer.sendMessage(getLoginUserId().toString(), JSONUtil.toJSONString(endData));
clearActiveAiTask(getLoginUserId(), taskId);
}
// 必须把累积的正文放进 data 一起回:只回一句状态文案时,
// 前端(uni-app 的 apiRequest 会回落 body.message)会把
// "Stream processing completed" 当成正文显示(见 issue 08)。
// 失败时同时带 status=failed,前端才能把失败文案与正文区分开(见 issue 10)。
return success("Stream processing completed",
buildManuscriptResult(reply, taskId, workflowRunId, workflowFailed));
} catch (Exception e) {
System.out.println(e.getMessage());
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
System.out.println(stackTraceElement);
}
HashMap answerData = new HashMap<>();
answerData.put("answer", "出错了,请晚点再试~");
answerData.put("taskId", "0");
webSocketServer.sendMessage(getLoginUserId().toString(), JSONUtil.toJSONString(answerData));
return fail("出错了,请晚点再试~");
}
}
/**
* 稿件生成的 HTTP 返回体。
*
* 本接口同时走 WebSocket 推流,但**不能**因此省略返回体:不是所有调用方
* 都连了 WebSocket(如小程序的问答助手页在稿件模式下只发 HTTP),
* 它们唯一的正文来源就是这里的 {@code answer}。
*
*
只回 {@code message}(如 "Stream processing completed")时,
* 前端会把它当正文渲染——uni-app 的 {@code apiRequest} 在 {@code data}
* 缺失时会回落 {@code body.message}(见 issue 08)。
*/
private Map buildManuscriptResult(StringBuilder reply, String taskId, String workflowRunId) {
return buildManuscriptResult(reply, taskId, workflowRunId, false);
}
/**
* 构造稿件生成的 HTTP 返回体。
*
* {@code answer} 键始终存在(空正文给空串):{@code ApiResult} 带
* {@code @JsonInclude(NON_NULL)},若 {@code data} 为 null 会被整个省略,
* 前端就会回落到 {@code message}("Stream processing completed")——见 issue 08。
*
*
失败时必须带 {@code status=failed}:否则前端拿到的是
* {@code answer="生成失败,请稍后重试"},会把失败文案当正文渲染
* (issue 10 实测:三个 admin 页面都会这样),且无从据此判定失败。
* uniapp 的 {@code isFailedPayload} 读的就是这个字段。
*
* @param failed 上游工作流是否以 failed 收场
*/
private Map buildManuscriptResult(StringBuilder reply, String taskId,
String workflowRunId, boolean failed) {
Map result = new LinkedHashMap<>();
result.put("answer", reply == null ? "" : reply.toString());
if (failed) {
result.put("status", "failed");
}
if (StrUtil.isNotBlank(taskId)) {
result.put("taskId", taskId);
}
if (StrUtil.isNotBlank(workflowRunId)) {
result.put("workflowRunId", workflowRunId);
}
return result;
}
@PostMapping("/creativeAssistant")
public DeferredResult> creativeAssistant(@RequestBody Map requestBody) {
final Integer loginUserId = getLoginUserId();
final DeferredResult> deferredResult = new DeferredResult<>(AI_REQUEST_TIMEOUT);
final AtomicBoolean requestActive = new AtomicBoolean(true);
deferredResult.onTimeout(() -> {
requestActive.set(false);
if (!deferredResult.isSetOrExpired()) {
deferredResult.setResult(fail("AI创意助手处理超时,请稍后重试"));
}
});
deferredResult.onError((throwable) -> requestActive.set(false));
deferredResult.onCompletion(() -> requestActive.set(false));
CompletableFuture.runAsync(() -> {
ApiResult> result;
try {
result = executeCreativeAssistant(requestBody, loginUserId);
} catch (Exception e) {
logger.error("creative assistant request failed", e);
result = fail("出错了,请晚点再试~");
}
if (requestActive.get() && !deferredResult.isSetOrExpired()) {
deferredResult.setResult(result);
}
});
return deferredResult;
}
@PostMapping("/competition-history/preprocess")
public DeferredResult> competitionHistoryPreprocess(
@RequestParam("files") List files,
@RequestParam(value = "scoreFile", required = false) MultipartFile scoreFile,
@RequestParam(value = "manualText", required = false) String manualText) {
final Integer loginUserId = getLoginUserId();
final DeferredResult> deferredResult = new DeferredResult<>(AI_REQUEST_TIMEOUT);
final AtomicBoolean requestActive = new AtomicBoolean(true);
deferredResult.onTimeout(() -> {
requestActive.set(false);
if (!deferredResult.isSetOrExpired()) {
deferredResult.setResult(fail("历史竞赛文件预处理超时,请稍后查看知识库列表"));
}
});
deferredResult.onError((throwable) -> requestActive.set(false));
deferredResult.onCompletion(() -> requestActive.set(false));
CompletableFuture.runAsync(() -> {
ApiResult> result;
try {
if (files == null || files.isEmpty()) {
result = fail("请至少上传一个历史项目书文件");
} else {
Map inputs = new LinkedHashMap<>();
List