a2a83f7d9e
这三条路径在 workflow_finished 事件只 extractAnswer、从不读 status: 工作流失败时 outputs 为空、answer 可能被填成 workflow_run_id(UUID), 原样下发等于把内部标识符当正文推给端上(issue 03)。 - activityOutlineGen:失败先判定,推「生成失败,请稍后重试」(reset=true) 再正常收尾 - executeWorkflowForResult:同上;最终结果失败时只回 status + 可读文案, 不带 outputs / raw - manuscriptGen:自查发现该方法不走通用执行、自带一份缺口,一并补齐 - socket 发送器增加 reset 字段:端上收到后丢弃失败前已累积的半截内容 - 新增 AiWorkflowFailureStatusTest 10 例:锁定「失败判定先于取 answer」 的顺序契约与顶层/嵌套 status、大小写空白、成功不误判等边界 Refs: .scratch/ai-assistant/issues/03-other-links-raw-payload-fallback.md
1815 lines
84 KiB
Java
1815 lines
84 KiB
Java
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.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;
|
||
|
||
@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;
|
||
|
||
/**
|
||
* 创意生成工作流以失败收场时给用户看的文案。
|
||
*
|
||
* <p>上游的真实失败原因(如 {@code Model disabled})属于 Dify 侧配置问题,
|
||
* 既不面向用户也不该泄露;这里统一给一句可读提示。
|
||
*/
|
||
private static final String CREATIVE_FAILED_MESSAGE = "生成失败,请稍后重试";
|
||
|
||
@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.*}。
|
||
*
|
||
* <p>原先这些 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<String, ActiveAiTask> 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<String, Object> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, Object> inputs = buildManuscriptInputs(message);
|
||
Object files = normalizeFiles(message.getFiles());
|
||
Map<String, Object> 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;
|
||
|
||
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);
|
||
String answer = extractAnswer(data);
|
||
if (answer != null && ("text_chunk".equals(event) || "message".equals(event))) {
|
||
reply.append(answer);
|
||
HashMap<String, String> 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)) {
|
||
|
||
String loadingText = StrUtil.blankToDefault(answer, event);
|
||
HashMap<String, String> 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);
|
||
reply.setLength(0);
|
||
reply.append(CREATIVE_FAILED_MESSAGE);
|
||
HashMap<String, String> 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<String, String> 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;
|
||
}
|
||
if (answer == null) {
|
||
answer = extractWorkflowOutputs(data);
|
||
}
|
||
if (answer != null && (answer.length() == 0 || reply.indexOf(answer) < 0)) {
|
||
reply.append(answer);
|
||
HashMap<String, String> 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));
|
||
}
|
||
if (!endSent) {
|
||
endSent = true;
|
||
HashMap<String, String> 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<String, String> 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);
|
||
}
|
||
} catch (Exception e) {
|
||
System.out.println(e.getMessage());
|
||
for (StackTraceElement stackTraceElement : e.getStackTrace()) {
|
||
System.out.println(stackTraceElement);
|
||
}
|
||
HashMap<String, String> answerData = new HashMap<>();
|
||
answerData.put("answer", "出错了,请晚点再试~");
|
||
answerData.put("taskId", "0");
|
||
webSocketServer.sendMessage(getLoginUserId().toString(), JSONUtil.toJSONString(answerData));
|
||
return fail("出错了,请晚点再试~");
|
||
}
|
||
return success("Stream processing completed");
|
||
}
|
||
|
||
@PostMapping("/creativeAssistant")
|
||
public DeferredResult<ApiResult<?>> creativeAssistant(@RequestBody Map<String, Object> requestBody) {
|
||
final Integer loginUserId = getLoginUserId();
|
||
final DeferredResult<ApiResult<?>> 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<ApiResult<?>> competitionHistoryPreprocess(
|
||
@RequestParam("files") List<MultipartFile> files,
|
||
@RequestParam(value = "scoreFile", required = false) MultipartFile scoreFile,
|
||
@RequestParam(value = "manualText", required = false) String manualText) {
|
||
final Integer loginUserId = getLoginUserId();
|
||
final DeferredResult<ApiResult<?>> 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<String, Object> inputs = new LinkedHashMap<>();
|
||
List<Object> uploadedFiles = new ArrayList<>();
|
||
for (MultipartFile file : files) {
|
||
if (file != null && !file.isEmpty()) {
|
||
uploadedFiles.add(uploadMultipartToAi(file, appKeys().getCompetitionHistoryPreprocess(), loginUserId));
|
||
}
|
||
}
|
||
if (uploadedFiles.isEmpty()) {
|
||
result = fail("请至少上传一个有效的历史项目书文件");
|
||
} else {
|
||
inputs.put("file_upload", uploadedFiles);
|
||
if (scoreFile != null && !scoreFile.isEmpty()) {
|
||
inputs.put("score_file", uploadMultipartToAi(scoreFile, appKeys().getCompetitionHistoryPreprocess(), loginUserId));
|
||
}
|
||
if (StrUtil.isNotBlank(manualText)) {
|
||
inputs.put("manual_text", manualText.trim());
|
||
}
|
||
result = executeWorkflowForResult(inputs, appKeys().getCompetitionHistoryPreprocess(),
|
||
"competitionHistoryPreprocess", loginUserId, true);
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
logger.error("competition history preprocess failed", e);
|
||
result = fail("历史竞赛文件预处理失败: " + e.getMessage());
|
||
}
|
||
if (requestActive.get() && !deferredResult.isSetOrExpired()) {
|
||
deferredResult.setResult(result);
|
||
}
|
||
});
|
||
return deferredResult;
|
||
}
|
||
|
||
@PostMapping("/project-quality-review")
|
||
public ApiResult<?> projectQualityReview(
|
||
@RequestParam("newProject") MultipartFile newProject,
|
||
@RequestParam(value = "customRubric", required = false) MultipartFile customRubric) {
|
||
Integer loginUserId = getLoginUserId();
|
||
try {
|
||
if (newProject == null || newProject.isEmpty()) {
|
||
return fail("请上传待审项目申报书");
|
||
}
|
||
Map<String, Object> inputs = new LinkedHashMap<>();
|
||
inputs.put("new_project", uploadMultipartToAi(newProject, appKeys().getProjectQualityReview(), loginUserId));
|
||
if (customRubric != null && !customRubric.isEmpty()) {
|
||
inputs.put("custom_rubric", uploadMultipartToAi(customRubric, appKeys().getProjectQualityReview(), loginUserId));
|
||
}
|
||
return executeWorkflowForResult(inputs, appKeys().getProjectQualityReview(),
|
||
"projectQualityReview", loginUserId, true);
|
||
} catch (Exception e) {
|
||
logger.error("project quality review failed", e);
|
||
sendWorkflowSocketMessage("出错了,请晚点再试~", null, null, "projectQualityReview", false, loginUserId);
|
||
sendWorkflowSocketEnd(null, null, "projectQualityReview", loginUserId);
|
||
return fail("项目书质量初审失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@ApiOperation("申报书生成")
|
||
@PostMapping("/declarationGen")
|
||
public ApiResult<?> declarationGen(@RequestBody DeclarationInputs message) {
|
||
Integer loginUserId = getLoginUserId();
|
||
if (message == null) {
|
||
return fail("请求参数不能为空");
|
||
}
|
||
// 核心创意是必填项(内网工作流 core_idea);upload_file 为可选的已有初稿/参考资料
|
||
if (StrUtil.isBlank(message.getCoreIdea())) {
|
||
return fail("coreIdea不能为空");
|
||
}
|
||
try {
|
||
Map<String, Object> inputs = buildMappedInputs(message, message.getInputs(),
|
||
difyProperties.getWorkflows().getDeclaration());
|
||
return executeWorkflowForResult(inputs, appKeys().getDeclaration(),
|
||
"declaration", loginUserId, true);
|
||
} catch (Exception e) {
|
||
logger.error("declaration gen failed", e);
|
||
sendWorkflowSocketMessage("出错了,请晚点再试~", null, null, "declaration", false, loginUserId);
|
||
sendWorkflowSocketEnd(null, null, "declaration", loginUserId);
|
||
return fail("申报书生成失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@ApiOperation("智能文档处理")
|
||
@PostMapping("/documentProcess")
|
||
public ApiResult<?> documentProcess(@RequestBody DocumentProcessInputs message) {
|
||
Integer loginUserId = getLoginUserId();
|
||
if (message == null) {
|
||
return fail("请求参数不能为空");
|
||
}
|
||
// 网络地址与上传文件至少一个(内网工作流 pdf_url / pdf_file)
|
||
if (StrUtil.isBlank(message.getPdfUrl()) && !hasFileValue(message.getPdfFile())) {
|
||
return fail("pdfUrl与pdfFile至少需要一个");
|
||
}
|
||
try {
|
||
Map<String, Object> inputs = buildMappedInputs(message, message.getInputs(),
|
||
difyProperties.getWorkflows().getDocumentProcess());
|
||
return executeWorkflowForResult(inputs, appKeys().getDocumentProcess(),
|
||
"documentProcess", loginUserId, true);
|
||
} catch (Exception e) {
|
||
logger.error("document process failed", e);
|
||
sendWorkflowSocketMessage("出错了,请晚点再试~", null, null, "documentProcess", false, loginUserId);
|
||
sendWorkflowSocketEnd(null, null, "documentProcess", loginUserId);
|
||
return fail("文档处理失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@ApiOperation("智能文档处理(文件上传)")
|
||
@PostMapping("/documentProcess/upload")
|
||
public DeferredResult<ApiResult<?>> documentProcessUpload(
|
||
@RequestParam(value = "file", required = false) MultipartFile file,
|
||
@RequestParam(value = "pdfUrl", required = false) String pdfUrl,
|
||
@RequestParam(value = "query", required = false) String query) {
|
||
final Integer loginUserId = getLoginUserId();
|
||
final DeferredResult<ApiResult<?>> 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 ((file == null || file.isEmpty()) && StrUtil.isBlank(pdfUrl)) {
|
||
result = fail("请上传文件或提供pdfUrl");
|
||
} else {
|
||
// 键名直接用业务字段名,再交给 remapInputs 按配置映射成真实变量名,
|
||
// 避免在这里手写 "pdf_file" 之类的 Dify 变量名造成两处维护。
|
||
Map<String, Object> inputs = new LinkedHashMap<>();
|
||
if (file != null && !file.isEmpty()) {
|
||
inputs.put("pdfFile", uploadMultipartToAi(file, appKeys().getDocumentProcess(), loginUserId));
|
||
}
|
||
if (StrUtil.isNotBlank(pdfUrl)) {
|
||
inputs.put("pdfUrl", pdfUrl.trim());
|
||
}
|
||
if (StrUtil.isNotBlank(query)) {
|
||
inputs.put("query", query.trim());
|
||
}
|
||
result = executeWorkflowForResult(
|
||
remapInputs(inputs, difyProperties.getWorkflows().getDocumentProcess()),
|
||
appKeys().getDocumentProcess(), "documentProcess", loginUserId, true);
|
||
}
|
||
} catch (Exception e) {
|
||
logger.error("document process upload failed", e);
|
||
result = fail("文档处理失败: " + e.getMessage());
|
||
}
|
||
if (requestActive.get() && !deferredResult.isSetOrExpired()) {
|
||
deferredResult.setResult(result);
|
||
}
|
||
});
|
||
return deferredResult;
|
||
}
|
||
|
||
@PostMapping("/activityOutlineGen")
|
||
public ApiResult<?> activityOutlineGen(@RequestBody ActivityOutlineInputs message) {
|
||
if (message == null) {
|
||
return fail("请求参数不能为空");
|
||
}
|
||
if (StrUtil.isBlank(message.getEventTheme())) {
|
||
return fail("event_theme不能为空");
|
||
}
|
||
if (StrUtil.isBlank(message.getPeople())) {
|
||
return fail("people不能为空");
|
||
}
|
||
if (StrUtil.isBlank(message.getEventTime())) {
|
||
return fail("event_time不能为空");
|
||
}
|
||
if (StrUtil.isBlank(message.getEventFunding())) {
|
||
return fail("event_funding不能为空");
|
||
}
|
||
|
||
try {
|
||
URL url = new URL(aiUrl() + "/chat-messages");
|
||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||
connection.setRequestMethod("POST");
|
||
connection.setRequestProperty("Authorization", "Bearer " + appKeys().getActivityOutline());
|
||
connection.setRequestProperty("Content-Type", "application/json");
|
||
connection.setRequestProperty("Accept", "text/event-stream");
|
||
connection.setDoOutput(true);
|
||
connection.setConnectTimeout(aiTimeout());
|
||
connection.setReadTimeout(aiTimeout());
|
||
|
||
Map<String, Object> payload = new LinkedHashMap<>();
|
||
payload.put("query", "请根据已提供的活动信息生成完整的活动策划大纲");
|
||
payload.put("response_mode", "streaming");
|
||
payload.put("user", getLoginUserId() == null ? "0" : getLoginUserId().toString());
|
||
payload.put("conversation_id", "");
|
||
payload.put("inputs", buildActivityOutlineInputs(message));
|
||
logger.info("activity outline 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());
|
||
Map<String, Object> responseData = null;
|
||
try {
|
||
responseData = JSONUtil.parseObject(responseBody, Map.class);
|
||
} catch (Exception ignored) {
|
||
}
|
||
String messageText = responseData == null ? responseBody : JSONUtil.toJSONString(responseData);
|
||
return fail("活动策划大纲生成失败", messageText);
|
||
}
|
||
|
||
StringBuilder reply = new StringBuilder();
|
||
String conversationId = null;
|
||
String messageId = null;
|
||
boolean endSent = 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 = stringValue(data.get("event"));
|
||
if (StrUtil.isNotBlank(stringValue(data.get("conversation_id")))) {
|
||
conversationId = stringValue(data.get("conversation_id"));
|
||
}
|
||
if (StrUtil.isNotBlank(stringValue(data.get("message_id")))) {
|
||
messageId = stringValue(data.get("message_id"));
|
||
}
|
||
|
||
if ("message".equals(event)) {
|
||
String answer = stringValue(data.get("answer"));
|
||
if (answer != null) {
|
||
reply.append(answer);
|
||
sendActivityOutlineSocketMessage(answer, conversationId, messageId, false);
|
||
}
|
||
} else if ("agent_log".equals(event) || "agent_thought".equals(event)) {
|
||
String loadingText = stringValue(data.get("thought"));
|
||
if (loadingText == null) {
|
||
loadingText = stringValue(data.get("answer"));
|
||
}
|
||
if (loadingText != null) {
|
||
sendActivityOutlineSocketMessage(loadingText, conversationId, messageId, true);
|
||
}
|
||
} else if ("message_end".equals(event) || "workflow_finished".equals(event)) {
|
||
// 失败态必须先判定:此时 outputs 为空、answer 可能是
|
||
// workflow_run_id 这类内部标识符,直接下发等于把标识符
|
||
// 当正文推给端上(见 issue 03)。
|
||
if (isWorkflowFailed(workflowStatus(data))) {
|
||
logger.warn("activity outline workflow failed, conversationId={}, messageId={}",
|
||
conversationId, messageId);
|
||
reply.setLength(0);
|
||
reply.append(CREATIVE_FAILED_MESSAGE);
|
||
// reset=true:丢弃失败前可能已推给端的半截内容,
|
||
// 否则失败文案会追加在流式残片后面(见 issue 03)。
|
||
sendActivityOutlineSocketMessage(CREATIVE_FAILED_MESSAGE,
|
||
conversationId, messageId, false, true);
|
||
if (!endSent) {
|
||
endSent = true;
|
||
sendActivityOutlineSocketEnd(conversationId, messageId);
|
||
}
|
||
continue;
|
||
}
|
||
String answer = extractAnswer(data);
|
||
if (answer != null && (answer.length() == 0 || reply.indexOf(answer) < 0)) {
|
||
reply.append(answer);
|
||
sendActivityOutlineSocketMessage(answer, conversationId, messageId, false);
|
||
}
|
||
if (!endSent) {
|
||
endSent = true;
|
||
sendActivityOutlineSocketEnd(conversationId, messageId);
|
||
}
|
||
} else if ("error".equals(event)) {
|
||
throw new RuntimeException(json);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!endSent) {
|
||
sendActivityOutlineSocketEnd(conversationId, messageId);
|
||
}
|
||
return success("Stream processing completed");
|
||
} catch (Exception e) {
|
||
logger.error("activity outline request failed", e);
|
||
sendActivityOutlineSocketMessage("出错了,请晚点再试~", null, null, false);
|
||
sendActivityOutlineSocketEnd(null, null);
|
||
return fail("出错了,请晚点再试~");
|
||
}
|
||
}
|
||
|
||
private void sendActivityOutlineSocketMessage(String answer, String conversationId, String messageId, boolean loading) {
|
||
sendActivityOutlineSocketMessage(answer, conversationId, messageId, loading, false);
|
||
}
|
||
|
||
/**
|
||
* @param reset 为真时端上应丢弃已累积的流式片段,只用本条内容。
|
||
* 失败收场时用它覆盖失败前可能已推给端的半截内容
|
||
* (见 issue 03)。
|
||
*/
|
||
private void sendActivityOutlineSocketMessage(String answer, String conversationId, String messageId,
|
||
boolean loading, boolean reset) {
|
||
HashMap<String, String> data = new HashMap<>();
|
||
data.put("answer", answer == null ? "" : answer);
|
||
data.put("conversationId", StrUtil.blankToDefault(conversationId, "0"));
|
||
data.put("messageId", StrUtil.blankToDefault(messageId, "0"));
|
||
if (loading) {
|
||
data.put("loading", "true");
|
||
}
|
||
if (reset) {
|
||
data.put("reset", "true");
|
||
}
|
||
Integer loginUserId = getLoginUserId();
|
||
try {
|
||
webSocketServer.sendMessage(loginUserId == null ? "0" : loginUserId.toString(), JSONUtil.toJSONString(data));
|
||
} catch (IOException e) {
|
||
logger.error("send activity outline socket message failed", e);
|
||
}
|
||
}
|
||
|
||
private void sendActivityOutlineSocketEnd(String conversationId, String messageId) {
|
||
HashMap<String, String> data = new HashMap<>();
|
||
data.put("answer", "__END__");
|
||
data.put("conversationId", StrUtil.blankToDefault(conversationId, "0"));
|
||
data.put("messageId", StrUtil.blankToDefault(messageId, "0"));
|
||
Integer loginUserId = getLoginUserId();
|
||
try {
|
||
webSocketServer.sendMessage(loginUserId == null ? "0" : loginUserId.toString(), JSONUtil.toJSONString(data));
|
||
} catch (IOException e) {
|
||
logger.error("send activity outline socket end failed", e);
|
||
}
|
||
}
|
||
|
||
private ApiResult<?> executeCreativeAssistant(Map<String, Object> requestBody, Integer loginUserId) {
|
||
String userContent = stringValue(requestBody == null ? null : requestBody.get("user_content"));
|
||
if (StrUtil.isBlank(userContent)) {
|
||
return fail("user_content不能为空");
|
||
}
|
||
|
||
HttpURLConnection connection = null;
|
||
try {
|
||
URL url = new URL(aiUrl() + "/workflows/run");
|
||
connection = (HttpURLConnection) url.openConnection();
|
||
connection.setRequestMethod("POST");
|
||
connection.setRequestProperty("Authorization", "Bearer " + appKeys().getCreativeAssistant());
|
||
connection.setRequestProperty("Content-Type", "application/json");
|
||
connection.setRequestProperty("Accept", "text/event-stream");
|
||
connection.setDoOutput(true);
|
||
connection.setConnectTimeout(aiTimeout());
|
||
connection.setReadTimeout(aiTimeout());
|
||
|
||
Map<String, Object> payload = new LinkedHashMap<>();
|
||
payload.put("inputs", buildCreativeAssistantInputs(requestBody));
|
||
payload.put("response_mode", "streaming");
|
||
payload.put("user", loginUserId == null ? "0" : loginUserId.toString());
|
||
logger.info("creative assistant 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());
|
||
Map<String, Object> responseData = null;
|
||
try {
|
||
responseData = JSONUtil.parseObject(responseBody, Map.class);
|
||
} catch (Exception ignored) {
|
||
}
|
||
String message = responseData == null ? responseBody : JSONUtil.toJSONString(responseData);
|
||
return fail("AI创意助手调用失败", message);
|
||
}
|
||
|
||
StringBuilder reply = new StringBuilder();
|
||
String workflowRunId = null;
|
||
String taskId = null;
|
||
|
||
try (BufferedReader reader = new BufferedReader(
|
||
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
|
||
String line;
|
||
while ((line = reader.readLine()) != null) {
|
||
logger.info("creative assistant stream: {}", line);
|
||
if (StrUtil.isBlank(line) || !line.startsWith("data:")) {
|
||
continue;
|
||
}
|
||
String json = line.substring(5).trim();
|
||
if (StrUtil.isBlank(json)) {
|
||
continue;
|
||
}
|
||
|
||
Map<String, Object> data = JSONUtil.parseObject(json, Map.class);
|
||
if (data == null) {
|
||
continue;
|
||
}
|
||
|
||
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(loginUserId, "workflow", taskId, appKeys().getCreativeAssistant());
|
||
}
|
||
|
||
String event = stringValue(data.get("event"));
|
||
String answer = extractAnswer(data);
|
||
if (answer != null && ("text_chunk".equals(event) || "message".equals(event))) {
|
||
reply.append(answer);
|
||
}
|
||
if ("error".equals(event)) {
|
||
throw new RuntimeException(json);
|
||
}
|
||
if ("message_end".equals(event) || "workflow_finished".equals(event)) {
|
||
Map<String, Object> result = buildCreativeAssistantResult(data, json);
|
||
if (isWorkflowFailed(stringValue(result.get("status")))) {
|
||
// 失败态:buildCreativeAssistantResult 已给好可读文案,
|
||
// 这里不得再用流式片段或 taskId 覆盖,也不下发 raw。
|
||
clearActiveAiTask(loginUserId, taskId);
|
||
logger.warn("creative assistant workflow failed, workflowRunId={}, taskId={}",
|
||
workflowRunId, taskId);
|
||
return success(result);
|
||
}
|
||
if (StrUtil.isBlank(stringValue(result.get("answer"))) && reply.length() > 0) {
|
||
result.put("answer", reply.toString());
|
||
}
|
||
if (StrUtil.isBlank(stringValue(result.get("workflowRunId"))) && StrUtil.isNotBlank(workflowRunId)) {
|
||
result.put("workflowRunId", workflowRunId);
|
||
}
|
||
if (StrUtil.isBlank(stringValue(result.get("taskId"))) && StrUtil.isNotBlank(taskId)) {
|
||
result.put("taskId", taskId);
|
||
}
|
||
clearActiveAiTask(loginUserId, taskId);
|
||
return success(result);
|
||
}
|
||
}
|
||
}
|
||
|
||
Map<String, Object> result = new LinkedHashMap<>();
|
||
result.put("answer", reply.toString());
|
||
if (StrUtil.isNotBlank(workflowRunId)) {
|
||
result.put("workflowRunId", workflowRunId);
|
||
}
|
||
if (StrUtil.isNotBlank(taskId)) {
|
||
result.put("taskId", taskId);
|
||
}
|
||
clearActiveAiTask(loginUserId, taskId);
|
||
return success(result);
|
||
} catch (Exception e) {
|
||
logger.error("creative assistant execute failed", e);
|
||
return fail("出错了,请晚点再试~");
|
||
} finally {
|
||
if (connection != null) {
|
||
connection.disconnect();
|
||
}
|
||
}
|
||
}
|
||
|
||
private Map<String, Object> parseInputs(String inputs) {
|
||
if (StrUtil.isBlank(inputs)) {
|
||
return new HashMap<>();
|
||
}
|
||
try {
|
||
return JSONUtil.parseObject(inputs, Map.class);
|
||
} catch (Exception e) {
|
||
return Collections.emptyMap();
|
||
}
|
||
}
|
||
|
||
private String readStream(InputStream inputStream) throws IOException {
|
||
if (inputStream == null) {
|
||
return "";
|
||
}
|
||
try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||
StringBuilder sb = new StringBuilder();
|
||
String line;
|
||
boolean firstLine = true;
|
||
while ((line = br.readLine()) != null) {
|
||
if (!firstLine) {
|
||
sb.append('\n');
|
||
}
|
||
sb.append(line);
|
||
firstLine = false;
|
||
}
|
||
return sb.toString();
|
||
}
|
||
}
|
||
|
||
private Map<String, Object> buildManuscriptInputs(ManuscriptGenInputs message) {
|
||
Map<String, Object> inputs = new LinkedHashMap<>();
|
||
if (message == null) {
|
||
return inputs;
|
||
}
|
||
if (StrUtil.isNotBlank(message.getDocType())) {
|
||
inputs.put("doc_type", message.getDocType());
|
||
}
|
||
if (StrUtil.isNotBlank(message.getEventType())) {
|
||
inputs.put("event_type", message.getEventType());
|
||
}
|
||
if (StrUtil.isNotBlank(message.getTheme())) {
|
||
inputs.put("theme", message.getTheme());
|
||
}
|
||
if (StrUtil.isNotBlank(message.getTime())) {
|
||
inputs.put("time", message.getTime());
|
||
}
|
||
if (StrUtil.isNotBlank(message.getLocation())) {
|
||
inputs.put("location", message.getLocation());
|
||
}
|
||
if (StrUtil.isNotBlank(message.getParticipants())) {
|
||
inputs.put("participants", message.getParticipants());
|
||
}
|
||
if (StrUtil.isNotBlank(message.getHighlights())) {
|
||
inputs.put("highlights", message.getHighlights());
|
||
}
|
||
if (message.getNumber() != null) {
|
||
// Dify 工作流里该变量名首字母大写(Number),与其它小写变量不一致。
|
||
// 变量名大小写敏感,发 "number" 会因缺参报错,故按实际变量名发送。
|
||
inputs.put("Number", message.getNumber());
|
||
}
|
||
return inputs;
|
||
}
|
||
|
||
private Map<String, Object> buildActivityOutlineInputs(ActivityOutlineInputs message) {
|
||
Map<String, Object> inputs = new LinkedHashMap<>();
|
||
if (message == null) {
|
||
return inputs;
|
||
}
|
||
if (StrUtil.isNotBlank(message.getEventTheme())) {
|
||
inputs.put("event_theme", message.getEventTheme());
|
||
}
|
||
if (StrUtil.isNotBlank(message.getPeople())) {
|
||
inputs.put("people", message.getPeople());
|
||
}
|
||
if (StrUtil.isNotBlank(message.getEventTime())) {
|
||
inputs.put("event_time", message.getEventTime());
|
||
}
|
||
if (StrUtil.isNotBlank(message.getEventFunding())) {
|
||
inputs.put("event_funding", message.getEventFunding());
|
||
}
|
||
if (StrUtil.isNotBlank(message.getEventType())) {
|
||
inputs.put("event_type", message.getEventType());
|
||
}
|
||
if (StrUtil.isNotBlank(message.getEventLocation())) {
|
||
inputs.put("event_location", message.getEventLocation());
|
||
}
|
||
if (StrUtil.isNotBlank(message.getEventMeme())) {
|
||
inputs.put("event_meme", message.getEventMeme());
|
||
}
|
||
return inputs;
|
||
}
|
||
|
||
/**
|
||
* 把入参实体的业务字段转成 Dify 工作流的 inputs。
|
||
*
|
||
* <p>调用方显式传了 {@code inputs} 时以它为准(可覆盖映射配置);
|
||
* 否则按 {@code mapping} 把非空业务字段改名后透传,未登记的字段按原名发送。
|
||
* 这样即使内网工作流的变量名与业务字段不一致,改 yml 即可对齐,无需改代码。
|
||
*/
|
||
private Map<String, Object> buildMappedInputs(Object source,
|
||
Map<String, Object> explicitInputs,
|
||
DifyProperties.VariableMapping mapping) {
|
||
if (explicitInputs != null && !explicitInputs.isEmpty()) {
|
||
return new LinkedHashMap<>(explicitInputs);
|
||
}
|
||
Map<String, Object> raw = objectToMap(source);
|
||
return remapInputs(raw, mapping);
|
||
}
|
||
|
||
/**
|
||
* 按映射表改写 inputs 的键名。
|
||
*/
|
||
private Map<String, Object> remapInputs(Map<String, Object> raw, DifyProperties.VariableMapping mapping) {
|
||
Map<String, Object> result = new LinkedHashMap<>();
|
||
if (raw == null) {
|
||
return result;
|
||
}
|
||
for (Map.Entry<String, Object> entry : raw.entrySet()) {
|
||
Object value = entry.getValue();
|
||
if (value == null) {
|
||
continue;
|
||
}
|
||
if (value instanceof String && StrUtil.isBlank((String) value)) {
|
||
continue;
|
||
}
|
||
String variable = mapping == null ? entry.getKey() : mapping.variableOf(entry.getKey());
|
||
// 映射为空串表示该字段不发给 Dify
|
||
if (StrUtil.isBlank(variable)) {
|
||
continue;
|
||
}
|
||
result.put(variable, value);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 用 Jackson 把入参实体转成 Map,跳过 null 字段。
|
||
*
|
||
* <p>不用 JSONUtil:它对实体走的是 fastjson,字段命名策略与 Jackson 注解
|
||
* ({@code @JsonProperty}/{@code @JsonAlias})不一致。
|
||
*/
|
||
private Map<String, Object> objectToMap(Object source) {
|
||
if (source == null) {
|
||
return new LinkedHashMap<>();
|
||
}
|
||
try {
|
||
String json = objectMapper.writeValueAsString(source);
|
||
Map<String, Object> map = JSONUtil.parseObject(json, Map.class);
|
||
return map == null ? new LinkedHashMap<>() : map;
|
||
} catch (Exception e) {
|
||
logger.warn("入参实体转换失败: {}", source.getClass().getSimpleName(), e);
|
||
return new LinkedHashMap<>();
|
||
}
|
||
}
|
||
|
||
private Map<String, Object> buildCreativeAssistantInputs(Map<String, Object> requestBody) {
|
||
Map<String, Object> inputs = new LinkedHashMap<>();
|
||
if (requestBody == null) {
|
||
return inputs;
|
||
}
|
||
String userContent = stringValue(requestBody.get("user_content"));
|
||
if (StrUtil.isNotBlank(userContent)) {
|
||
inputs.put("user_content", userContent);
|
||
}
|
||
Object rawUserFiles = requestBody.get("user_files");
|
||
if (hasFileValue(rawUserFiles)) {
|
||
Object userFiles = normalizeFiles(rawUserFiles);
|
||
Object fileValue = userFiles != null ? userFiles : rawUserFiles;
|
||
if (hasFileValue(fileValue)) {
|
||
inputs.put("user_files", fileValue);
|
||
}
|
||
}
|
||
return inputs;
|
||
}
|
||
|
||
private ApiResult<?> executeWorkflowForResult(Map<String, Object> inputs,
|
||
String apiKey,
|
||
String stopType,
|
||
Integer loginUserId,
|
||
boolean streamToSocket) throws IOException {
|
||
HttpURLConnection connection = null;
|
||
try {
|
||
URL url = new URL(aiUrl() + "/workflows/run");
|
||
connection = (HttpURLConnection) url.openConnection();
|
||
connection.setRequestMethod("POST");
|
||
connection.setRequestProperty("Authorization", "Bearer " + apiKey);
|
||
connection.setRequestProperty("Content-Type", "application/json");
|
||
connection.setRequestProperty("Accept", "text/event-stream");
|
||
connection.setDoOutput(true);
|
||
connection.setConnectTimeout(aiTimeout());
|
||
connection.setReadTimeout(aiTimeout());
|
||
|
||
Map<String, Object> payload = new LinkedHashMap<>();
|
||
payload.put("inputs", inputs == null ? new LinkedHashMap<>() : inputs);
|
||
payload.put("response_mode", "streaming");
|
||
payload.put("user", loginUserId == null ? "0" : loginUserId.toString());
|
||
logger.info("{} workflow payload: {}", stopType, 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());
|
||
return fail("AI工作流调用失败", responseBody);
|
||
}
|
||
|
||
StringBuilder reply = new StringBuilder();
|
||
String workflowRunId = null;
|
||
String taskId = null;
|
||
Map<String, Object> lastData = null;
|
||
boolean endSent = false;
|
||
boolean workflowFailed = false;
|
||
|
||
try (BufferedReader reader = new BufferedReader(
|
||
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
|
||
String line;
|
||
while ((line = reader.readLine()) != null) {
|
||
if (StrUtil.isBlank(line) || !line.startsWith("data:")) {
|
||
continue;
|
||
}
|
||
String json = line.substring(5).trim();
|
||
if (StrUtil.isBlank(json)) {
|
||
continue;
|
||
}
|
||
|
||
Map<String, Object> data = JSONUtil.parseObject(json, Map.class);
|
||
if (data == null) {
|
||
continue;
|
||
}
|
||
lastData = data;
|
||
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(loginUserId, stopType, taskId, apiKey);
|
||
}
|
||
|
||
String event = stringValue(data.get("event"));
|
||
String answer = extractAnswer(data);
|
||
if (answer != null && ("text_chunk".equals(event) || "message".equals(event))) {
|
||
reply.append(answer);
|
||
if (streamToSocket) {
|
||
sendWorkflowSocketMessage(answer, taskId, workflowRunId, stopType, false, loginUserId);
|
||
}
|
||
} else if ("workflow_started".equals(event) || "node_started".equals(event)
|
||
|| "node_finished".equals(event) || "agent_log".equals(event)
|
||
|| "agent_thought".equals(event)) {
|
||
if (streamToSocket) {
|
||
sendWorkflowSocketMessage(StrUtil.blankToDefault(answer, event),
|
||
taskId, workflowRunId, stopType, true, loginUserId);
|
||
}
|
||
} else if ("workflow_finished".equals(event) || "message_end".equals(event)) {
|
||
// 失败态必须先判定:outputs 为空时 answer 可能是
|
||
// workflow_run_id 这类内部标识符,不能当正文推给端上(见 issue 03)。
|
||
String eventStatus = workflowStatus(data);
|
||
if (isWorkflowFailed(eventStatus)) {
|
||
logger.warn("workflow failed, stopType={}, taskId={}, workflowRunId={}",
|
||
stopType, taskId, workflowRunId);
|
||
workflowFailed = true;
|
||
reply.setLength(0);
|
||
reply.append(CREATIVE_FAILED_MESSAGE);
|
||
if (streamToSocket) {
|
||
// reset=true:丢弃失败前可能已推给端的半截内容
|
||
sendWorkflowSocketMessage(CREATIVE_FAILED_MESSAGE,
|
||
taskId, workflowRunId, stopType, false, loginUserId, true);
|
||
}
|
||
if (streamToSocket) {
|
||
sendWorkflowSocketEnd(taskId, workflowRunId, stopType, loginUserId);
|
||
endSent = true;
|
||
}
|
||
clearActiveAiTask(loginUserId, taskId);
|
||
continue;
|
||
}
|
||
String finalAnswer = answer == null ? extractWorkflowOutputs(data) : answer;
|
||
if (finalAnswer != null && (finalAnswer.length() == 0 || reply.indexOf(finalAnswer) < 0)) {
|
||
reply.append(finalAnswer);
|
||
if (streamToSocket) {
|
||
sendWorkflowSocketMessage(finalAnswer, taskId, workflowRunId, stopType, false, loginUserId);
|
||
}
|
||
}
|
||
if (streamToSocket) {
|
||
sendWorkflowSocketEnd(taskId, workflowRunId, stopType, loginUserId);
|
||
endSent = true;
|
||
}
|
||
clearActiveAiTask(loginUserId, taskId);
|
||
} else if ("error".equals(event)) {
|
||
throw new RuntimeException(json);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (streamToSocket && !endSent) {
|
||
sendWorkflowSocketEnd(taskId, workflowRunId, stopType, loginUserId);
|
||
}
|
||
clearActiveAiTask(loginUserId, taskId);
|
||
Map<String, Object> result = new LinkedHashMap<>();
|
||
result.put("answer", reply.toString());
|
||
if (StrUtil.isNotBlank(taskId)) {
|
||
result.put("taskId", taskId);
|
||
}
|
||
if (StrUtil.isNotBlank(workflowRunId)) {
|
||
result.put("workflowRunId", workflowRunId);
|
||
}
|
||
// 失败态只回状态与可读文案,不下发 outputs / raw / 上游错误栈(见 issue 03)。
|
||
if (workflowFailed) {
|
||
result.put("status", "failed");
|
||
result.put("answer", CREATIVE_FAILED_MESSAGE);
|
||
return success(result);
|
||
}
|
||
if (lastData != null) {
|
||
Object data = lastData.get("data");
|
||
if (data instanceof Map) {
|
||
Object outputs = ((Map) data).get("outputs");
|
||
if (outputs != null) {
|
||
result.put("outputs", outputs);
|
||
}
|
||
Object status = ((Map) data).get("status");
|
||
if (status != null) {
|
||
result.put("status", status);
|
||
}
|
||
}
|
||
result.put("raw", lastData);
|
||
}
|
||
return success(result);
|
||
} finally {
|
||
if (connection != null) {
|
||
connection.disconnect();
|
||
}
|
||
}
|
||
}
|
||
|
||
private void sendWorkflowSocketMessage(String answer,
|
||
String taskId,
|
||
String workflowRunId,
|
||
String stopType,
|
||
boolean loading,
|
||
Integer loginUserId) {
|
||
sendWorkflowSocketMessage(answer, taskId, workflowRunId, stopType, loading, loginUserId, false);
|
||
}
|
||
|
||
/**
|
||
* @param reset 为真时端上应丢弃已累积的流式片段,只用本条内容。
|
||
* 失败收场时用它覆盖失败前可能已推给端的半截内容(见 issue 03)。
|
||
*/
|
||
private void sendWorkflowSocketMessage(String answer,
|
||
String taskId,
|
||
String workflowRunId,
|
||
String stopType,
|
||
boolean loading,
|
||
Integer loginUserId,
|
||
boolean reset) {
|
||
HashMap<String, String> data = new HashMap<>();
|
||
data.put("answer", answer == null ? "" : answer);
|
||
data.put("taskId", StrUtil.blankToDefault(taskId, "0"));
|
||
data.put("workflowRunId", StrUtil.blankToDefault(workflowRunId, "0"));
|
||
data.put("stopType", StrUtil.blankToDefault(stopType, "workflow"));
|
||
if (loading) {
|
||
data.put("loading", "true");
|
||
}
|
||
if (reset) {
|
||
data.put("reset", "true");
|
||
}
|
||
try {
|
||
webSocketServer.sendMessage(loginUserId == null ? "0" : loginUserId.toString(), JSONUtil.toJSONString(data));
|
||
} catch (IOException e) {
|
||
logger.error("send workflow socket message failed", e);
|
||
}
|
||
}
|
||
|
||
private void sendWorkflowSocketEnd(String taskId, String workflowRunId, String stopType, Integer loginUserId) {
|
||
HashMap<String, String> data = new HashMap<>();
|
||
data.put("answer", "__END__");
|
||
data.put("taskId", StrUtil.blankToDefault(taskId, "0"));
|
||
data.put("workflowRunId", StrUtil.blankToDefault(workflowRunId, "0"));
|
||
data.put("stopType", StrUtil.blankToDefault(stopType, "workflow"));
|
||
try {
|
||
webSocketServer.sendMessage(loginUserId == null ? "0" : loginUserId.toString(), JSONUtil.toJSONString(data));
|
||
} catch (IOException e) {
|
||
logger.error("send workflow socket end failed", e);
|
||
}
|
||
}
|
||
|
||
private Map<String, Object> uploadMultipartToAi(MultipartFile file, String apiKey, Integer loginUserId) throws IOException {
|
||
File tempFile = null;
|
||
try {
|
||
String originalFilename = StrUtil.blankToDefault(file.getOriginalFilename(), "upload.bin");
|
||
String suffix = "";
|
||
int suffixIndex = originalFilename.lastIndexOf('.');
|
||
if (suffixIndex >= 0) {
|
||
suffix = originalFilename.substring(suffixIndex);
|
||
}
|
||
tempFile = File.createTempFile("ai-upload-", suffix);
|
||
file.transferTo(tempFile);
|
||
HttpResponse response = HttpRequest.post(aiUrl() + "/files/upload")
|
||
.header("Authorization", "Bearer " + apiKey)
|
||
.form("user", loginUserId == null ? "0" : loginUserId.toString())
|
||
.form("file", tempFile, originalFilename)
|
||
.timeout(aiTimeout())
|
||
.execute();
|
||
String responseBody = response.body();
|
||
Map<String, Object> result = JSONUtil.parseObject(responseBody, Map.class);
|
||
if (response.getStatus() < 200 || response.getStatus() >= 300) {
|
||
throw new IOException(result == null ? responseBody : JSONUtil.toJSONString(result));
|
||
}
|
||
if (result == null) {
|
||
throw new IOException("AI文件上传返回为空");
|
||
}
|
||
Map<String, Object> fileValue = new LinkedHashMap<>();
|
||
fileValue.put("transfer_method", "local_file");
|
||
fileValue.put("upload_file_id", result.get("id"));
|
||
fileValue.put("type", resolveAiFileType(originalFilename, stringValue(result.get("mime_type"))));
|
||
fileValue.put("url", "");
|
||
fileValue.put("name", result.get("name"));
|
||
fileValue.put("raw", result);
|
||
return fileValue;
|
||
} finally {
|
||
if (tempFile != null && tempFile.exists()) {
|
||
tempFile.delete();
|
||
}
|
||
}
|
||
}
|
||
|
||
private String resolveAiFileType(String fileName, String mimeType) {
|
||
String lowerName = StrUtil.blankToDefault(fileName, "").toLowerCase();
|
||
String lowerMime = StrUtil.blankToDefault(mimeType, "").toLowerCase();
|
||
if (lowerMime.startsWith("image/") || lowerName.matches(".*\\.(png|jpe?g|gif|webp|bmp)$")) {
|
||
return "image";
|
||
}
|
||
if (lowerMime.startsWith("audio/") || lowerName.matches(".*\\.(mp3|wav|m4a|aac|flac)$")) {
|
||
return "audio";
|
||
}
|
||
if (lowerMime.startsWith("video/") || lowerName.matches(".*\\.(mp4|mov|avi|mkv|webm)$")) {
|
||
return "video";
|
||
}
|
||
return "document";
|
||
}
|
||
|
||
private boolean hasFileValue(Object value) {
|
||
if (value == null) {
|
||
return false;
|
||
}
|
||
if (value instanceof String) {
|
||
return StrUtil.isNotBlank((String) value);
|
||
}
|
||
if (value instanceof List) {
|
||
return !((List) value).isEmpty();
|
||
}
|
||
if (value instanceof Map) {
|
||
return !((Map) value).isEmpty();
|
||
}
|
||
return true;
|
||
}
|
||
|
||
private Map<String, Object> buildCreativeAssistantResult(Map<String, Object> responseData, String responseBody) {
|
||
Map<String, Object> result = new LinkedHashMap<>();
|
||
|
||
String status = workflowStatus(responseData);
|
||
if (isWorkflowFailed(status)) {
|
||
// 工作流失败:只回可读文案与状态,不下发 outputs / raw / 上游错误栈。
|
||
// raw 里含 workflow_id / task_id / req_id 与上游异常文本,端上没有任何消费者,
|
||
// 一旦下发就会被前端兜底逻辑整段贴进聊天气泡(见 issue 01)。
|
||
result.put("status", status);
|
||
result.put("answer", CREATIVE_FAILED_MESSAGE);
|
||
return result;
|
||
}
|
||
|
||
if (responseData == null) {
|
||
result.put("answer", responseBody);
|
||
return result;
|
||
}
|
||
|
||
String answer = extractWorkflowOutputs(responseData);
|
||
if (answer == null) {
|
||
answer = extractAnswer(responseData);
|
||
}
|
||
if (answer != null) {
|
||
result.put("answer", answer);
|
||
}
|
||
|
||
String workflowRunId = stringValue(responseData.get("workflow_run_id"));
|
||
if (StrUtil.isNotBlank(workflowRunId)) {
|
||
result.put("workflowRunId", workflowRunId);
|
||
}
|
||
|
||
String taskId = stringValue(responseData.get("task_id"));
|
||
if (StrUtil.isNotBlank(taskId)) {
|
||
result.put("taskId", taskId);
|
||
}
|
||
|
||
Object data = responseData.get("data");
|
||
if (data instanceof Map) {
|
||
// status 已在方法开头经 workflowStatus 统一取出,
|
||
// 这里只负责下发(含顶层 status 的兜底情况)。
|
||
if (status != null) {
|
||
result.put("status", status);
|
||
}
|
||
Object outputs = ((Map) data).get("outputs");
|
||
if (outputs != null) {
|
||
result.put("outputs", outputs);
|
||
}
|
||
}
|
||
|
||
if (!result.containsKey("outputs") && responseData.get("outputs") != null) {
|
||
result.put("outputs", responseData.get("outputs"));
|
||
}
|
||
result.put("raw", responseData);
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 取 Dify 工作流事件里的执行状态。
|
||
*
|
||
* <p>{@code workflow_finished} 事件把状态放在 {@code data.status};
|
||
* 少数事件会放在顶层,两处都看,取到即返回。
|
||
*
|
||
* @return 状态字符串;取不到时为 {@code null}
|
||
*/
|
||
private String workflowStatus(Map<String, Object> responseData) {
|
||
if (responseData == null) {
|
||
return null;
|
||
}
|
||
Object data = responseData.get("data");
|
||
if (data instanceof Map) {
|
||
Object status = ((Map) data).get("status");
|
||
if (status != null && StrUtil.isNotBlank(status.toString())) {
|
||
return status.toString();
|
||
}
|
||
}
|
||
Object status = responseData.get("status");
|
||
if (status != null && StrUtil.isNotBlank(status.toString())) {
|
||
return status.toString();
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 判断工作流是否以失败收场。
|
||
*
|
||
* <p>只在拿到明确失败状态时为真;状态缺失视为未失败,避免误伤正常响应。
|
||
*/
|
||
private boolean isWorkflowFailed(String status) {
|
||
return "failed".equalsIgnoreCase(StrUtil.trimToEmpty(status));
|
||
}
|
||
|
||
private Map<String, Object> buildChatMessageResult(Map<String, Object> responseData, String responseBody) {
|
||
Map<String, Object> result = new LinkedHashMap<>();
|
||
if (responseData == null) {
|
||
result.put("answer", responseBody);
|
||
return result;
|
||
}
|
||
|
||
String answer = extractAnswer(responseData);
|
||
if (answer != null) {
|
||
result.put("answer", answer);
|
||
}
|
||
|
||
String conversationId = stringValue(responseData.get("conversation_id"));
|
||
if (StrUtil.isNotBlank(conversationId)) {
|
||
result.put("conversationId", conversationId);
|
||
}
|
||
|
||
String messageId = stringValue(responseData.get("message_id"));
|
||
if (StrUtil.isNotBlank(messageId)) {
|
||
result.put("messageId", messageId);
|
||
}
|
||
|
||
if (responseData.get("metadata") != null) {
|
||
result.put("metadata", responseData.get("metadata"));
|
||
}
|
||
result.put("raw", responseData);
|
||
return result;
|
||
}
|
||
|
||
private Object normalizeFiles(Object files) {
|
||
if (files == null) {
|
||
return null;
|
||
}
|
||
if (files instanceof List) {
|
||
return files;
|
||
}
|
||
if (files instanceof Map) {
|
||
List<Object> fileList = new ArrayList<>();
|
||
fileList.add(files);
|
||
return fileList;
|
||
}
|
||
if (files instanceof String && StrUtil.isNotBlank((String) files)) {
|
||
String fileStr = (String) files;
|
||
if (fileStr.trim().startsWith("[")) {
|
||
Map<String, Object> wrapper = JSONUtil.parseObject("{\"files\":" + fileStr + "}", Map.class);
|
||
return wrapper == null ? null : wrapper.get("files");
|
||
}
|
||
if (fileStr.trim().startsWith("{")) {
|
||
Map<String, Object> fileMap = JSONUtil.parseObject(fileStr, Map.class);
|
||
if (fileMap != null) {
|
||
List<Object> fileList = new ArrayList<>();
|
||
fileList.add(fileMap);
|
||
return fileList;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private String extractAnswer(Map data) {
|
||
if (data == null) {
|
||
return null;
|
||
}
|
||
String[] keys = new String[]{"answer", "text", "chunk", "thought"};
|
||
for (String key : keys) {
|
||
Object value = data.get(key);
|
||
if (value instanceof String) {
|
||
return (String) value;
|
||
}
|
||
}
|
||
Object dataObj = data.get("data");
|
||
if (dataObj instanceof Map) {
|
||
String nestedAnswer = findTextValue((Map) dataObj);
|
||
if (nestedAnswer != null) {
|
||
return nestedAnswer;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private String extractWorkflowOutputs(Map data) {
|
||
if (data == null) {
|
||
return null;
|
||
}
|
||
Object outputs = data.get("outputs");
|
||
if (outputs instanceof Map) {
|
||
return findTextValue((Map) outputs);
|
||
}
|
||
Object dataObj = data.get("data");
|
||
if (dataObj instanceof Map) {
|
||
Object nestedOutputs = ((Map) dataObj).get("outputs");
|
||
if (nestedOutputs instanceof Map) {
|
||
return findTextValue((Map) nestedOutputs);
|
||
}
|
||
return findTextValue((Map) dataObj);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private String findTextValue(Map source) {
|
||
if (source == null || source.isEmpty()) {
|
||
return null;
|
||
}
|
||
String[] preferredKeys = new String[]{"answer", "content", "text", "result", "output", "article", "manuscript"};
|
||
for (String key : preferredKeys) {
|
||
Object value = source.get(key);
|
||
if (value instanceof String) {
|
||
return (String) value;
|
||
}
|
||
}
|
||
for (Object value : source.values()) {
|
||
if (value instanceof String) {
|
||
return (String) value;
|
||
}
|
||
if (value instanceof Map) {
|
||
String nestedValue = findTextValue((Map) value);
|
||
if (nestedValue != null) {
|
||
return nestedValue;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private String stringValue(Object value) {
|
||
return value == null ? null : value.toString();
|
||
}
|
||
|
||
private void recordActiveAiTask(Integer userId, String mode, String taskId, String apiKey) {
|
||
if (userId == null || StrUtil.isBlank(taskId)) {
|
||
return;
|
||
}
|
||
String normalizedMode = normalizeStopMode(mode);
|
||
activeAiTaskMap.put(buildActiveTaskKey(userId, normalizedMode),
|
||
new ActiveAiTask(taskId, normalizedMode, apiKey));
|
||
}
|
||
|
||
private ActiveAiTask findActiveAiTask(Integer userId, String mode) {
|
||
if (userId == null) {
|
||
return null;
|
||
}
|
||
ActiveAiTask activeTask = activeAiTaskMap.get(buildActiveTaskKey(userId, normalizeStopMode(mode)));
|
||
if (activeTask != null) {
|
||
return activeTask;
|
||
}
|
||
if (!isChatStopMode(mode)) {
|
||
return activeAiTaskMap.get(buildActiveTaskKey(userId, "workflow"));
|
||
}
|
||
return activeAiTaskMap.get(buildActiveTaskKey(userId, "chat"));
|
||
}
|
||
|
||
private void clearActiveAiTask(Integer userId, String taskId) {
|
||
if (userId == null) {
|
||
return;
|
||
}
|
||
if (StrUtil.isBlank(taskId)) {
|
||
activeAiTaskMap.remove(buildActiveTaskKey(userId, "chat"));
|
||
activeAiTaskMap.remove(buildActiveTaskKey(userId, "workflow"));
|
||
return;
|
||
}
|
||
activeAiTaskMap.entrySet().removeIf(entry ->
|
||
entry.getKey().startsWith(userId + ":") && taskId.equals(entry.getValue().getTaskId()));
|
||
}
|
||
|
||
private String resolveStopApiKey(String mode, ActiveAiTask activeTask) {
|
||
if (activeTask != null && StrUtil.isNotBlank(activeTask.getApiKey())) {
|
||
return activeTask.getApiKey();
|
||
}
|
||
if ("image".equals(mode)) {
|
||
return appKeys().getCreativeAssistant();
|
||
}
|
||
if ("writing".equals(mode)) {
|
||
return appKeys().getManuscript();
|
||
}
|
||
if ("projectQualityReview".equals(mode)) {
|
||
return appKeys().getProjectQualityReview();
|
||
}
|
||
if ("competitionHistoryPreprocess".equals(mode)) {
|
||
return appKeys().getCompetitionHistoryPreprocess();
|
||
}
|
||
if ("documentProcess".equals(mode)) {
|
||
return appKeys().getDocumentProcess();
|
||
}
|
||
if ("declaration".equals(mode)) {
|
||
return appKeys().getDeclaration();
|
||
}
|
||
return appKeys().getChat();
|
||
}
|
||
|
||
private String normalizeStopMode(String mode) {
|
||
return isChatStopMode(mode) ? "chat" : "workflow";
|
||
}
|
||
|
||
private boolean isChatStopMode(String mode) {
|
||
return StrUtil.isBlank(mode)
|
||
|| "chat".equals(mode)
|
||
|| "message".equals(mode)
|
||
|| "activityOutline".equals(mode);
|
||
}
|
||
private String buildActiveTaskKey(Integer userId, String mode) {
|
||
return userId + ":" + mode;
|
||
}
|
||
|
||
private static class ActiveAiTask {
|
||
private final String taskId;
|
||
private final String apiKey;
|
||
|
||
ActiveAiTask(String taskId, String mode, String apiKey) {
|
||
this.taskId = taskId;
|
||
this.apiKey = apiKey;
|
||
}
|
||
|
||
String getTaskId() {
|
||
return taskId;
|
||
}
|
||
|
||
String getApiKey() {
|
||
return apiKey;
|
||
}
|
||
}
|
||
|
||
@PostMapping("/messageStop")
|
||
public ApiResult<?> stop(@RequestBody Map<String, Object> data) {
|
||
Integer loginUserId = getLoginUserId();
|
||
String mode = StrUtil.blankToDefault(stringValue(data == null ? null : data.get("mode")), "chat");
|
||
String taskId = stringValue(data == null ? null : data.get("taskId"));
|
||
ActiveAiTask activeTask = findActiveAiTask(loginUserId, mode);
|
||
if (StrUtil.isBlank(taskId) && activeTask != null) {
|
||
taskId = activeTask.getTaskId();
|
||
}
|
||
if (StrUtil.isBlank(taskId)) {
|
||
return success("当前没有可停止的AI任务");
|
||
}
|
||
|
||
String stopType = isChatStopMode(mode) ? "chat" : "workflow";
|
||
String apiKey = resolveStopApiKey(mode, activeTask);
|
||
String stopPath = "chat".equals(stopType)
|
||
? "/chat-messages/" + taskId + "/stop"
|
||
: "/workflows/tasks/" + taskId + "/stop";
|
||
Map<String, Object> postData = new HashMap<>();
|
||
postData.put("user", loginUserId == null ? "0" : loginUserId.toString());
|
||
logger.info("AI stop payload: mode={}, taskId={}, path={}", mode, taskId, stopPath);
|
||
HttpResponse response = HttpRequest.post(aiUrl() + stopPath)
|
||
.header("Authorization", "Bearer " + apiKey)
|
||
.header("Content-Type", "application/json")
|
||
.body(JSONObject.toJSONString(postData))
|
||
.timeout(aiTimeout())
|
||
.execute();
|
||
String responseBody = response.body();
|
||
if (response.getStatus() < 200 || response.getStatus() >= 300) {
|
||
logger.warn("AI stop failed: status={}, body={}", response.getStatus(), responseBody);
|
||
return fail("停止AI任务失败", responseBody);
|
||
}
|
||
clearActiveAiTask(loginUserId, taskId);
|
||
return success(responseBody);
|
||
}
|
||
|
||
@PostMapping("/file/upload")
|
||
public ApiResult<?> uploadFile(@RequestParam("file") MultipartFile file,
|
||
@RequestParam(value = "user", required = false) String user,
|
||
@RequestParam(value = "type", required = false) Integer type) {
|
||
if (file == null || file.isEmpty()) {
|
||
return fail("文件不能为空");
|
||
}
|
||
File tempFile = null;
|
||
try {
|
||
String token = appKeys().getManuscript();
|
||
if (type == null || type == 0) {
|
||
token = appKeys().getChat();
|
||
}
|
||
String originalFilename = StrUtil.blankToDefault(file.getOriginalFilename(), "upload.bin");
|
||
String suffix = "";
|
||
int suffixIndex = originalFilename.lastIndexOf('.');
|
||
if (suffixIndex >= 0) {
|
||
suffix = originalFilename.substring(suffixIndex);
|
||
}
|
||
tempFile = File.createTempFile("ai-upload-", suffix);
|
||
file.transferTo(tempFile);
|
||
|
||
HttpResponse response = HttpRequest.post(aiUrl() + "/files/upload")
|
||
.header("Authorization", "Bearer " + token)
|
||
.form("user", StrUtil.blankToDefault(user, getLoginUserId().toString()))
|
||
.form("file", tempFile, originalFilename)
|
||
.timeout(aiTimeout())
|
||
.execute();
|
||
|
||
int responseCode = response.getStatus();
|
||
String responseBody = response.body();
|
||
Map<String, Object> result = JSONUtil.parseObject(responseBody, Map.class);
|
||
if (responseCode < 200 || responseCode >= 300) {
|
||
String message = result == null ? responseBody : JSONUtil.toJSONString(result);
|
||
return fail("上传失败", message);
|
||
}
|
||
return success(result == null ? responseBody : result);
|
||
} catch (Exception e) {
|
||
return fail("上传失败", e.getMessage());
|
||
} finally {
|
||
if (tempFile != null && tempFile.exists()) {
|
||
tempFile.delete();
|
||
}
|
||
}
|
||
}
|
||
}
|