AI接口支持流式调用

This commit is contained in:
2025-10-30 15:59:48 +08:00
parent 9ddc6887ff
commit 093d07ab76
2 changed files with 596 additions and 153 deletions
@@ -13,6 +13,8 @@ import com.emotion.service.ConversationService;
import com.emotion.service.CozeApiCallService; import com.emotion.service.CozeApiCallService;
import com.emotion.service.EmotionRecordService; import com.emotion.service.EmotionRecordService;
import com.emotion.service.EmotionAnalysisService; import com.emotion.service.EmotionAnalysisService;
import com.emotion.service.AiConfigService;
import com.emotion.entity.AiConfig;
import com.emotion.dto.request.*; import com.emotion.dto.request.*;
import com.emotion.dto.response.*; import com.emotion.dto.response.*;
import com.emotion.util.SnowflakeIdGenerator; import com.emotion.util.SnowflakeIdGenerator;
@@ -41,6 +43,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.Arrays; import java.util.Arrays;
import java.util.stream.Collectors;
/** /**
* AI聊天服务实现类 * AI聊天服务实现类
@@ -73,38 +76,16 @@ public class AiChatServiceImpl implements AiChatService {
@Autowired @Autowired
private SnowflakeIdGenerator snowflakeIdGenerator; private SnowflakeIdGenerator snowflakeIdGenerator;
@Value("${emotion.coze.api.token:}") @Autowired
private String cozeApiToken; private AiConfigService aiConfigService;
@Value("${emotion.coze.api.base-url:https://api.coze.cn}")
private String cozeBaseUrl;
@Value("${emotion.coze.api.chat.path:/v3/chat}")
private String chatPath;
@Value("${emotion.coze.api.chat.talk.bot-id:}")
private String chatBotId;
@Value("${emotion.coze.api.chat.talk.workflow-id:}")
private String chatWorkflowId;
@Value("${emotion.coze.api.chat.summary.bot-id:}")
private String summaryBotId;
@Value("${emotion.coze.api.chat.summary.workflow-id:}")
private String summaryWorkflowId;
@Value("${emotion.coze.api.timeout:30000}")
private int timeout;
@Value("${emotion.coze.api.retry-count:3}")
private int retryCount;
@Value("${emotion.coze.api.retry-delay:1000}")
private int retryDelay;
private static final String DEFAULT_USER_ID = "emotion-museum-user"; private static final String DEFAULT_USER_ID = "emotion-museum-user";
// 使用场景常量
private static final String USAGE_SCENARIO_CHAT = "chat";
private static final String USAGE_SCENARIO_SUMMARY = "summary";
private static final String DEFAULT_ENVIRONMENT = "production";
// API 相关常量 // API 相关常量
private static final String CONTENT_KEY = "content"; private static final String CONTENT_KEY = "content";
private static final String ROLE_KEY = "role"; private static final String ROLE_KEY = "role";
@@ -494,13 +475,20 @@ public class AiChatServiceImpl implements AiChatService {
@Override @Override
public boolean healthCheck() { public boolean healthCheck() {
try { try {
// 简化健康检查 - 检查必要配置是否存在 // 检查聊天场景的AI配置是否存在
boolean configValid = cozeApiToken != null && !cozeApiToken.trim().isEmpty() && AiConfig chatConfig = aiConfigService.getBestConfig(USAGE_SCENARIO_CHAT, DEFAULT_ENVIRONMENT);
chatBotId != null && !chatBotId.trim().isEmpty() && if (chatConfig == null) {
cozeBaseUrl != null && !cozeBaseUrl.trim().isEmpty(); log.warn("未找到聊天场景的AI配置");
return false;
}
// 检查必要配置是否完整
boolean configValid = chatConfig.getApiToken() != null && !chatConfig.getApiToken().trim().isEmpty() &&
chatConfig.getBotId() != null && !chatConfig.getBotId().trim().isEmpty() &&
chatConfig.getApiBaseUrl() != null && !chatConfig.getApiBaseUrl().trim().isEmpty();
if (!configValid) { if (!configValid) {
log.warn("Coze API 配置不完整"); log.warn("AI配置不完整: configId={}", chatConfig.getId());
return false; return false;
} }
@@ -587,65 +575,89 @@ public class AiChatServiceImpl implements AiChatService {
CozeApiCall apiCall = createSummaryApiCallRecord(conversationId, null, userMessage, userId, "summary"); CozeApiCall apiCall = createSummaryApiCallRecord(conversationId, null, userMessage, userId, "summary");
try { try {
// 构建请求头 return executeSummaryCozeApiCall(apiCall, conversationId, userMessage, userId);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + cozeApiToken);
headers.set("Content-Type", "application/json");
// 构建请求体 - 使用总结专用的bot和workflow
Map<String, Object> requestBody = buildSummaryRequest(conversationId, userMessage, userId);
// 更新API调用记录的请求信息
updateApiCallRequest(apiCall, cozeBaseUrl + chatPath, requestBody, headers);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
// 构建完整的API URL
String cozeApiUrl = cozeBaseUrl + chatPath;
log.info("发送Coze总结请求到: {}, 请求体: {}", cozeApiUrl, requestBody);
// 发送请求
ResponseEntity<String> response = restTemplate.exchange(
cozeApiUrl,
HttpMethod.POST,
request,
String.class);
log.info("收到Coze总结初始响应: {}", response.getBody());
// 更新API调用记录的响应信息
updateApiCallResponse(apiCall, response);
// 解析响应获取chat_id和conversation_id
JSONObject responseJson = JSON.parseObject(response.getBody());
String chatId = extractChatIdFromResponse(responseJson);
String cozeConversationId = extractConversationIdFromResponse(responseJson);
if (chatId != null && cozeConversationId != null) {
// 更新API调用记录的Coze ID信息
updateApiCallCozeIds(apiCall, chatId, cozeConversationId);
// 轮询聊天状态直到完成并获取回复内容
String aiReply = waitForChatCompletionWithTracking(chatId, cozeConversationId, apiCall);
log.info("Coze AI总结响应成功: reply={}", aiReply);
// 更新API调用记录的最终结果
updateApiCallSuccess(apiCall, aiReply);
return aiReply;
} else {
log.error("无法从Coze总结响应中获取chat_id或conversation_id");
updateApiCallError(apiCall, "INVALID_RESPONSE", "无法从Coze总结响应中获取chat_id或conversation_id");
return "抱歉,AI总结服务响应异常,请稍后再试。";
}
} catch (Exception e) { } catch (Exception e) {
log.error("发送总结消息到Coze AI失败", e); log.error("发送总结消息失败", e);
updateApiCallError(apiCall, "REQUEST_FAILED", e.getMessage()); updateApiCallFailure(apiCall, e.getMessage());
return "抱歉,AI总结服务暂时不可用,请稍后再试。"; return "抱歉,AI总结服务暂时不可用,请稍后再试。";
} }
} }
/**
* 执行总结Coze API调用的逻辑
*/
private String executeSummaryCozeApiCall(CozeApiCall apiCall, String conversationId, String userMessage, String userId) {
// 获取总结场景的AI配置
AiConfig config = getSummaryAiConfig();
// 构建请求体 - 使用总结专用的bot和workflow
Map<String, Object> requestBody = buildSummaryRequest(conversationId, userMessage, userId);
// 检查是否使用流式输出
boolean useStream = (Boolean) requestBody.get("stream");
if (useStream) {
return executeStreamCozeApiCall(apiCall, config, requestBody, conversationId, userMessage, userId);
} else {
return executeSummaryNormalCozeApiCall(apiCall, config, requestBody, conversationId, userMessage, userId);
}
}
/**
* 执行普通(非流式)总结Coze API调用
*/
private String executeSummaryNormalCozeApiCall(CozeApiCall apiCall, AiConfig config, Map<String, Object> requestBody,
String conversationId, String userMessage, String userId) {
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + config.getApiToken());
headers.set("Content-Type", "application/json");
// 构建完整的API URL
String cozeApiUrl = config.getApiBaseUrl() + getApiPath(config);
// 更新API调用记录的请求信息
updateApiCallRequest(apiCall, cozeApiUrl, requestBody, headers);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
log.info("发送Coze总结请求到: {}, 请求体: {}", cozeApiUrl, requestBody);
// 发送请求
ResponseEntity<String> response = restTemplate.exchange(
cozeApiUrl,
HttpMethod.POST,
request,
String.class);
log.info("收到Coze总结初始响应: {}", response.getBody());
// 更新API调用记录的响应信息
updateApiCallResponse(apiCall, response);
// 解析响应获取chat_id和conversation_id
JSONObject responseJson = JSON.parseObject(response.getBody());
String chatId = extractChatIdFromResponse(responseJson);
String cozeConversationId = extractConversationIdFromResponse(responseJson);
if (chatId != null && cozeConversationId != null) {
// 更新API调用记录的Coze ID信息
updateApiCallCozeIds(apiCall, chatId, cozeConversationId);
// 轮询聊天状态直到完成并获取回复内容
String aiReply = waitForChatCompletionWithTracking(chatId, cozeConversationId, apiCall);
log.info("Coze AI总结响应成功: reply={}", aiReply);
// 更新API调用记录的最终结果
updateApiCallSuccess(apiCall, aiReply);
return aiReply;
} else {
log.error("无法从Coze总结响应中获取chat_id或conversation_id");
updateApiCallError(apiCall, "INVALID_RESPONSE", "无法从Coze总结响应中获取chat_id或conversation_id");
return "抱歉,AI总结服务响应异常,请稍后再试。";
}
}
@Override @Override
public EmotionSummaryGenerateResponse generateEmotionSummaryWithResponse(String userId) { public EmotionSummaryGenerateResponse generateEmotionSummaryWithResponse(String userId) {
log.info("生成用户情绪记录总结: userId={}", userId); log.info("生成用户情绪记录总结: userId={}", userId);
@@ -719,9 +731,11 @@ public class AiChatServiceImpl implements AiChatService {
public boolean isServiceAvailable() { public boolean isServiceAvailable() {
try { try {
// 简单的健康检查 // 检查聊天场景的AI配置是否可用
return cozeApiToken != null && !cozeApiToken.isEmpty() && AiConfig chatConfig = aiConfigService.getBestConfig(USAGE_SCENARIO_CHAT, DEFAULT_ENVIRONMENT);
chatBotId != null && !chatBotId.isEmpty(); return chatConfig != null &&
chatConfig.getApiToken() != null && !chatConfig.getApiToken().isEmpty() &&
chatConfig.getBotId() != null && !chatConfig.getBotId().isEmpty();
} catch (Exception e) { } catch (Exception e) {
log.error("检查AI服务可用性失败", e); log.error("检查AI服务可用性失败", e);
return false; return false;
@@ -751,22 +765,40 @@ public class AiChatServiceImpl implements AiChatService {
* 执行Coze API调用的公共逻辑 * 执行Coze API调用的公共逻辑
*/ */
private String executeCozeApiCall(CozeApiCall apiCall, String conversationId, String userMessage, String userId) { private String executeCozeApiCall(CozeApiCall apiCall, String conversationId, String userMessage, String userId) {
// 构建请求头 // 获取聊天场景的AI配置
HttpHeaders headers = new HttpHeaders(); AiConfig config = getChatAiConfig();
headers.set("Authorization", "Bearer " + cozeApiToken);
headers.set("Content-Type", "application/json");
// 构建请求体 - 使用正确的Coze API格式 // 构建请求体 - 使用正确的Coze API格式
Map<String, Object> requestBody = buildCozeRequest(conversationId, userMessage, userId); Map<String, Object> requestBody = buildCozeRequestWithConfig(conversationId, userMessage, userId, config);
// 更新API调用记录的请求信息 // 检查是否使用流式输出
updateApiCallRequest(apiCall, cozeBaseUrl + chatPath, requestBody, headers); boolean useStream = (Boolean) requestBody.get("stream");
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers); if (useStream) {
return executeStreamCozeApiCall(apiCall, config, requestBody, conversationId, userMessage, userId);
} else {
return executeNormalCozeApiCall(apiCall, config, requestBody, conversationId, userMessage, userId);
}
}
/**
* 执行普通(非流式)Coze API调用
*/
private String executeNormalCozeApiCall(CozeApiCall apiCall, AiConfig config, Map<String, Object> requestBody,
String conversationId, String userMessage, String userId) {
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + config.getApiToken());
headers.set("Content-Type", "application/json");
// 构建完整的API URL // 构建完整的API URL
String cozeApiUrl = cozeBaseUrl + chatPath; String cozeApiUrl = config.getApiBaseUrl() + getApiPath(config);
log.info("发送Coze请求到: {}, 请求体: {}", cozeApiUrl, requestBody);
// 更新API调用记录的请求信息
updateApiCallRequest(apiCall, cozeApiUrl, requestBody, headers);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
log.info("发送Coze普通请求到: {}, 请求体: {}", cozeApiUrl, requestBody);
// 发送请求 // 发送请求
ResponseEntity<String> response = restTemplate.exchange( ResponseEntity<String> response = restTemplate.exchange(
@@ -791,7 +823,7 @@ public class AiChatServiceImpl implements AiChatService {
// 轮询聊天状态直到完成并获取回复内容 // 轮询聊天状态直到完成并获取回复内容
String aiReply = waitForChatCompletionWithTracking(chatId, cozeConversationId, apiCall); String aiReply = waitForChatCompletionWithTracking(chatId, cozeConversationId, apiCall);
log.info("Coze AI响应成功: reply={}", aiReply); log.info("Coze AI普通响应成功: reply={}", aiReply);
// 更新API调用记录的最终结果 // 更新API调用记录的最终结果
updateApiCallSuccess(apiCall, aiReply); updateApiCallSuccess(apiCall, aiReply);
@@ -804,6 +836,165 @@ public class AiChatServiceImpl implements AiChatService {
} }
} }
/**
* 执行流式Coze API调用
*/
private String executeStreamCozeApiCall(CozeApiCall apiCall, AiConfig config, Map<String, Object> requestBody,
String conversationId, String userMessage, String userId) {
try {
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + config.getApiToken());
headers.set("Content-Type", "application/json");
headers.set("Accept", "text/event-stream");
// 构建完整的API URL
String cozeApiUrl = config.getApiBaseUrl() + getApiPath(config);
// 更新API调用记录的请求信息
updateApiCallRequest(apiCall, cozeApiUrl, requestBody, headers);
log.info("发送Coze流式请求到: {}, 请求体: {}", cozeApiUrl, requestBody);
// 使用RestTemplate处理流式响应
String streamResponse = handleStreamResponse(cozeApiUrl, headers, requestBody, apiCall);
log.info("Coze AI流式响应完成: length={}", streamResponse != null ? streamResponse.length() : 0);
// 更新API调用记录的最终结果
updateApiCallSuccess(apiCall, streamResponse);
return streamResponse;
} catch (Exception e) {
log.error("流式API调用失败", e);
updateApiCallError(apiCall, "STREAM_ERROR", e.getMessage());
return "抱歉,AI流式服务暂时不可用,请稍后再试。";
}
}
/**
* 处理流式响应
*/
private String handleStreamResponse(String url, HttpHeaders headers, Map<String, Object> requestBody, CozeApiCall apiCall) {
try {
// 创建HTTP客户端
java.net.http.HttpClient client = java.net.http.HttpClient.newBuilder()
.connectTimeout(java.time.Duration.ofSeconds(30))
.build();
// 构建请求
java.net.http.HttpRequest.Builder requestBuilder = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create(url))
.timeout(java.time.Duration.ofMinutes(5))
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(JSON.toJSONString(requestBody)));
// 添加请求头
headers.forEach((key, values) -> {
if (values != null && !values.isEmpty()) {
requestBuilder.header(key, values.get(0));
}
});
java.net.http.HttpRequest request = requestBuilder.build();
// 发送请求并处理流式响应
StringBuilder responseBuilder = new StringBuilder();
StringBuilder fullStreamData = new StringBuilder();
java.net.http.HttpResponse<java.util.stream.Stream<String>> response = client.send(request,
java.net.http.HttpResponse.BodyHandlers.ofLines());
log.info("流式响应状态码: {}", response.statusCode());
if (response.statusCode() != 200) {
String errorBody = response.body().collect(java.util.stream.Collectors.joining("\n"));
log.error("流式请求失败,状态码: {}, 响应: {}", response.statusCode(), errorBody);
return "流式请求失败,状态码: " + response.statusCode();
}
// 处理流式数据
response.body().forEach(line -> {
fullStreamData.append(line).append("\n");
if (line.startsWith("data: ")) {
String data = line.substring(6).trim();
if ("[DONE]".equals(data)) {
log.info("流式响应完成");
return;
}
try {
JSONObject jsonData = JSON.parseObject(data);
// 提取消息内容
if (jsonData.containsKey("choices")) {
com.alibaba.fastjson2.JSONArray choices = jsonData.getJSONArray("choices");
if (choices != null && !choices.isEmpty()) {
JSONObject choice = choices.getJSONObject(0);
if (choice != null && choice.containsKey("delta")) {
JSONObject delta = choice.getJSONObject("delta");
if (delta != null && delta.containsKey("content")) {
String content = delta.getString("content");
if (content != null) {
responseBuilder.append(content);
}
}
}
}
}
// Coze特定格式处理
if (jsonData.containsKey("event")) {
String event = jsonData.getString("event");
if ("conversation.message.delta".equals(event) && jsonData.containsKey("data")) {
JSONObject eventData = jsonData.getJSONObject("data");
if (eventData != null && eventData.containsKey("content")) {
String content = eventData.getString("content");
if (content != null) {
responseBuilder.append(content);
}
}
}
}
} catch (Exception e) {
log.warn("解析流式数据失败: {}, 数据: {}", e.getMessage(), data);
}
}
});
// 记录完整的流式数据用于调试
updateApiCallStreamData(apiCall, fullStreamData.toString());
String finalResponse = responseBuilder.toString();
if (finalResponse.isEmpty()) {
log.warn("流式响应为空,返回默认消息");
return "收到了流式响应,但内容为空。";
}
return finalResponse;
} catch (Exception e) {
log.error("处理流式响应失败", e);
throw new RuntimeException("处理流式响应失败: " + e.getMessage(), e);
}
}
/**
* 更新API调用记录的流式数据
*/
private void updateApiCallStreamData(CozeApiCall apiCall, String streamData) {
try {
// 可以将流式数据存储到响应体字段中,用于调试和分析
apiCall.setResponseBody(streamData);
cozeApiCallService.updateById(apiCall);
} catch (Exception e) {
log.error("更新API调用记录流式数据失败: {}", e.getMessage(), e);
}
}
public Map<String, Object> guestChat(String message, String clientIp) { public Map<String, Object> guestChat(String message, String clientIp) {
log.info("访客聊天: message={}, clientIp={}", message, clientIp); log.info("访客聊天: message={}, clientIp={}", message, clientIp);
@@ -883,16 +1074,28 @@ public class AiChatServiceImpl implements AiChatService {
* 构建Coze API请求 - 根据官方文档修正格式 * 构建Coze API请求 - 根据官方文档修正格式
*/ */
private Map<String, Object> buildCozeRequest(String conversationId, String userMessage, String userId) { private Map<String, Object> buildCozeRequest(String conversationId, String userMessage, String userId) {
// 获取聊天场景的AI配置
AiConfig config = getChatAiConfig();
return buildCozeRequestWithConfig(conversationId, userMessage, userId, config);
}
/**
* 使用指定配置构建Coze API请求
*/
private Map<String, Object> buildCozeRequestWithConfig(String conversationId, String userMessage, String userId, AiConfig config) {
Map<String, Object> cozeRequest = new HashMap<>(); Map<String, Object> cozeRequest = new HashMap<>();
cozeRequest.put("bot_id", chatBotId); cozeRequest.put("bot_id", config.getBotId());
// 如果有workflow_id,则添加 // 如果有workflow_id,则添加
if (chatWorkflowId != null && !chatWorkflowId.trim().isEmpty()) { if (config.getWorkflowId() != null && !config.getWorkflowId().trim().isEmpty()) {
cozeRequest.put("workflow_id", chatWorkflowId); cozeRequest.put("workflow_id", config.getWorkflowId());
} }
cozeRequest.put("user_id", userId != null ? userId : DEFAULT_USER_ID); cozeRequest.put("user_id", userId != null ? userId : DEFAULT_USER_ID);
cozeRequest.put("stream", false);
// 根据配置决定是否使用流式输出
boolean useStream = config.getSupportStream() != null && config.getSupportStream() == 1;
cozeRequest.put("stream", useStream);
// 构建消息列表 - 按照 Coze API 标准格式 // 构建消息列表 - 按照 Coze API 标准格式
java.util.List<Map<String, Object>> messages = new java.util.ArrayList<>(); java.util.List<Map<String, Object>> messages = new java.util.ArrayList<>();
@@ -944,6 +1147,9 @@ public class AiChatServiceImpl implements AiChatService {
*/ */
private String waitForChatCompletion(String chatId, String conversationId) { private String waitForChatCompletion(String chatId, String conversationId) {
try { try {
// 获取聊天场景的AI配置
AiConfig config = getChatAiConfig();
// 最多等待30秒,每2秒轮询一次 // 最多等待30秒,每2秒轮询一次
int maxAttempts = 15; int maxAttempts = 15;
int attempt = 0; int attempt = 0;
@@ -952,12 +1158,12 @@ public class AiChatServiceImpl implements AiChatService {
log.info("轮询聊天状态,第{}次尝试: chatId={}, conversationId={}", attempt + 1, chatId, conversationId); log.info("轮询聊天状态,第{}次尝试: chatId={}, conversationId={}", attempt + 1, chatId, conversationId);
// 构建状态查询URL // 构建状态查询URL
String statusUrl = cozeBaseUrl + "/v3/chat/retrieve?chat_id=" + chatId + "&conversation_id=" String statusUrl = config.getApiBaseUrl() + "/v3/chat/retrieve?chat_id=" + chatId + "&conversation_id="
+ conversationId; + conversationId;
// 构建请求头 // 构建请求头
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + cozeApiToken); headers.set("Authorization", "Bearer " + config.getApiToken());
headers.set("Content-Type", "application/json"); headers.set("Content-Type", "application/json");
HttpEntity<String> request = new HttpEntity<>(headers); HttpEntity<String> request = new HttpEntity<>(headers);
@@ -1009,15 +1215,18 @@ public class AiChatServiceImpl implements AiChatService {
*/ */
private String getChatMessages(String chatId, String conversationId) { private String getChatMessages(String chatId, String conversationId) {
try { try {
// 获取聊天场景的AI配置
AiConfig config = getChatAiConfig();
log.info("获取聊天消息: chatId={}, conversationId={}", chatId, conversationId); log.info("获取聊天消息: chatId={}, conversationId={}", chatId, conversationId);
// 构建消息查询URL // 构建消息查询URL
String messagesUrl = cozeBaseUrl + "/v3/chat/message/list?chat_id=" + chatId + "&conversation_id=" String messagesUrl = config.getApiBaseUrl() + "/v3/chat/message/list?chat_id=" + chatId + "&conversation_id="
+ conversationId; + conversationId;
// 构建请求头 // 构建请求头
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + cozeApiToken); headers.set("Authorization", "Bearer " + config.getApiToken());
headers.set("Content-Type", "application/json"); headers.set("Content-Type", "application/json");
HttpEntity<String> request = new HttpEntity<>(headers); HttpEntity<String> request = new HttpEntity<>(headers);
@@ -1067,16 +1276,22 @@ public class AiChatServiceImpl implements AiChatService {
* 构建总结请求 - 使用专门的总结bot和workflow * 构建总结请求 - 使用专门的总结bot和workflow
*/ */
private Map<String, Object> buildSummaryRequest(String conversationId, String userMessage, String userId) { private Map<String, Object> buildSummaryRequest(String conversationId, String userMessage, String userId) {
Map<String, Object> cozeRequest = new HashMap<>(); // 获取总结场景的AI配置
cozeRequest.put("bot_id", summaryBotId != null && !summaryBotId.trim().isEmpty() ? summaryBotId : chatBotId); AiConfig config = getSummaryAiConfig();
// 如果有总结workflow_id,则添加 Map<String, Object> cozeRequest = new HashMap<>();
if (summaryWorkflowId != null && !summaryWorkflowId.trim().isEmpty()) { cozeRequest.put("bot_id", config.getBotId());
cozeRequest.put("workflow_id", summaryWorkflowId);
// 如果有workflow_id,则添加
if (config.getWorkflowId() != null && !config.getWorkflowId().trim().isEmpty()) {
cozeRequest.put("workflow_id", config.getWorkflowId());
} }
cozeRequest.put("user_id", userId != null ? userId : DEFAULT_USER_ID); cozeRequest.put("user_id", userId != null ? userId : DEFAULT_USER_ID);
cozeRequest.put("stream", false);
// 根据配置决定是否使用流式输出
boolean useStream = config.getSupportStream() != null && config.getSupportStream() == 1;
cozeRequest.put("stream", useStream);
cozeRequest.put("auto_save_history", true); cozeRequest.put("auto_save_history", true);
// 如果有会话ID,则添加 // 如果有会话ID,则添加
@@ -1118,6 +1333,9 @@ public class AiChatServiceImpl implements AiChatService {
*/ */
private CozeApiCall createApiCallRecord(String conversationId, String messageId, String userMessage, String userId, private CozeApiCall createApiCallRecord(String conversationId, String messageId, String userMessage, String userId,
String requestType) { String requestType) {
// 获取聊天场景的AI配置
AiConfig config = getChatAiConfig();
CozeApiCall apiCall = CozeApiCall.builder() CozeApiCall apiCall = CozeApiCall.builder()
.conversationId(conversationId) .conversationId(conversationId)
.messageId(messageId) // 设置messageId .messageId(messageId) // 设置messageId
@@ -1125,8 +1343,8 @@ public class AiChatServiceImpl implements AiChatService {
.requestType(requestType) .requestType(requestType)
.userMessage(userMessage) .userMessage(userMessage)
.userMessageType("text") .userMessageType("text")
.botId(chatBotId) .botId(config.getBotId())
.workflowId(chatWorkflowId) .workflowId(config.getWorkflowId())
.status("pending") .status("pending")
.startTime(LocalDateTime.now()) .startTime(LocalDateTime.now())
.traceId(UUID.randomUUID().toString()) .traceId(UUID.randomUUID().toString())
@@ -1158,6 +1376,9 @@ public class AiChatServiceImpl implements AiChatService {
*/ */
private CozeApiCall createSummaryApiCallRecord(String conversationId, String messageId, String userMessage, private CozeApiCall createSummaryApiCallRecord(String conversationId, String messageId, String userMessage,
String userId, String requestType) { String userId, String requestType) {
// 获取总结场景的AI配置
AiConfig config = getSummaryAiConfig();
CozeApiCall apiCall = CozeApiCall.builder() CozeApiCall apiCall = CozeApiCall.builder()
.conversationId(conversationId) .conversationId(conversationId)
.messageId(messageId) // 设置messageId .messageId(messageId) // 设置messageId
@@ -1165,8 +1386,8 @@ public class AiChatServiceImpl implements AiChatService {
.requestType(requestType) .requestType(requestType)
.userMessage(userMessage) .userMessage(userMessage)
.userMessageType("text") .userMessageType("text")
.botId(summaryBotId) .botId(config.getBotId())
.workflowId(summaryWorkflowId) .workflowId(config.getWorkflowId())
.status("pending") .status("pending")
.startTime(LocalDateTime.now()) .startTime(LocalDateTime.now())
.traceId(UUID.randomUUID().toString()) .traceId(UUID.randomUUID().toString())
@@ -1505,4 +1726,36 @@ public class AiChatServiceImpl implements AiChatService {
return record; return record;
} }
/**
* 获取聊天场景的AI配置
*/
private AiConfig getChatAiConfig() {
AiConfig config = aiConfigService.getBestConfig(USAGE_SCENARIO_CHAT, DEFAULT_ENVIRONMENT);
if (config == null) {
log.error("未找到聊天场景的AI配置");
throw new RuntimeException("未找到聊天场景的AI配置,请先在管理后台配置");
}
return config;
}
/**
* 获取总结场景的AI配置
*/
private AiConfig getSummaryAiConfig() {
AiConfig config = aiConfigService.getBestConfig(USAGE_SCENARIO_SUMMARY, DEFAULT_ENVIRONMENT);
if (config == null) {
log.warn("未找到总结场景的AI配置,使用聊天配置");
return getChatAiConfig();
}
return config;
}
/**
* 获取配置的API路径
*/
private String getApiPath(AiConfig config) {
// 默认使用 /v3/chat 路径
return "/v3/chat";
}
} }
+225 -35
View File
@@ -549,6 +549,28 @@
<el-input v-model="testRequest.url" readonly /> <el-input v-model="testRequest.url" readonly />
</el-form-item> </el-form-item>
<el-form-item label="测试选项">
<div class="test-options">
<el-checkbox
v-model="testOptions.useStream"
@change="updateTestRequestBody"
>
启用流式响应
</el-checkbox>
<el-tooltip content="启用后将测试流式返回,可以看到AI逐步生成的响应内容" placement="top">
<el-icon class="info-icon"><InfoFilled /></el-icon>
</el-tooltip>
</div>
</el-form-item>
<el-form-item label="测试消息">
<el-input
v-model="testOptions.testMessage"
placeholder="输入测试消息内容"
@input="updateTestRequestBody"
/>
</el-form-item>
<el-form-item label="请求头"> <el-form-item label="请求头">
<el-input <el-input
v-model="testRequest.headers" v-model="testRequest.headers"
@@ -562,14 +584,14 @@
<el-input <el-input
v-model="testRequest.body" v-model="testRequest.body"
type="textarea" type="textarea"
:rows="12" :rows="10"
placeholder="JSON格式的请求体" placeholder="JSON格式的请求体"
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="handleTestRequest" :loading="testLoading"> <el-button type="primary" @click="handleTestRequest" :loading="testLoading">
发送测试请求 {{ testOptions.useStream ? '发送流式测试' : '发送测试请求' }}
</el-button> </el-button>
<el-button @click="handleFormatRequest">格式化请求</el-button> <el-button @click="handleFormatRequest">格式化请求</el-button>
<el-button @click="handleResetTest">重置</el-button> <el-button @click="handleResetTest">重置</el-button>
@@ -625,6 +647,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus' import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { InfoFilled } from '@element-plus/icons-vue'
import { import {
getAiConfigPage, getAiConfigPage,
createAiConfig, createAiConfig,
@@ -746,6 +769,11 @@ const testResponse = reactive({
body: '' body: ''
}) })
const testOptions = reactive({
useStream: false,
testMessage: '你好,这是一个测试消息,请回复确认接口正常工作。'
})
// 获取配置类型标签类型 // 获取配置类型标签类型
const getConfigTypeTagType = (type: string) => { const getConfigTypeTagType = (type: string) => {
const typeMap: Record<string, string> = { const typeMap: Record<string, string> = {
@@ -1078,11 +1106,11 @@ const initTestData = (config: AiConfig) => {
const requestBody: any = { const requestBody: any = {
bot_id: config.botId || '', bot_id: config.botId || '',
user_id: 'test_user_' + Date.now(), user_id: 'test_user_' + Date.now(),
stream: false, stream: testOptions.useStream,
additional_messages: [ additional_messages: [
{ {
role: 'user', role: 'user',
content: '你好,这是一个测试消息,请回复确认接口正常工作。', content: testOptions.testMessage,
content_type: 'text', content_type: 'text',
type: 'question' type: 'question'
} }
@@ -1113,6 +1141,20 @@ const initTestData = (config: AiConfig) => {
testResponse.body = '' testResponse.body = ''
} }
// 更新测试请求体
const updateTestRequestBody = () => {
if (!testConfig.value) return
try {
const body = JSON.parse(testRequest.body)
body.stream = testOptions.useStream
body.additional_messages[0].content = testOptions.testMessage
testRequest.body = JSON.stringify(body, null, 2)
} catch (e) {
console.warn('更新请求体失败:', e)
}
}
// 发送测试请求 // 发送测试请求
const handleTestRequest = async () => { const handleTestRequest = async () => {
if (!testConfig.value) return if (!testConfig.value) return
@@ -1138,39 +1180,15 @@ const handleTestRequest = async () => {
return return
} }
// 发送请求 // 检查是否为流式请求
const response = await fetch(testRequest.url, { const isStreamRequest = body.stream === true
method: 'POST',
headers: headers,
body: JSON.stringify(body)
})
// 获取响应头 if (isStreamRequest) {
const responseHeaders: any = {} // 处理流式请求
response.headers.forEach((value, key) => { await handleStreamRequest(headers, body)
responseHeaders[key] = value
})
// 获取响应体
const responseBody = await response.text()
// 更新响应数据
testResponse.status = response.status
testResponse.headers = JSON.stringify(responseHeaders, null, 2)
testResponse.body = responseBody
// 尝试格式化响应体
try {
const jsonBody = JSON.parse(responseBody)
testResponse.body = JSON.stringify(jsonBody, null, 2)
} catch (e) {
// 如果不是JSON格式,保持原样
}
if (response.ok) {
ElMessage.success('测试请求发送成功')
} else { } else {
ElMessage.warning(`请求返回状态码: ${response.status}`) // 处理普通请求
await handleNormalRequest(headers, body)
} }
} catch (error: any) { } catch (error: any) {
@@ -1190,6 +1208,165 @@ const handleTestRequest = async () => {
} }
} }
// 处理普通请求
const handleNormalRequest = async (headers: any, body: any) => {
const response = await fetch(testRequest.url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
})
// 获取响应头
const responseHeaders: any = {}
response.headers.forEach((value, key) => {
responseHeaders[key] = value
})
// 获取响应体
const responseBody = await response.text()
// 更新响应数据
testResponse.status = response.status
testResponse.headers = JSON.stringify(responseHeaders, null, 2)
testResponse.body = responseBody
// 尝试格式化响应体
try {
const jsonBody = JSON.parse(responseBody)
testResponse.body = JSON.stringify(jsonBody, null, 2)
} catch (e) {
// 如果不是JSON格式,保持原样
}
if (response.ok) {
ElMessage.success('测试请求发送成功')
} else {
ElMessage.warning(`请求返回状态码: ${response.status}`)
}
}
// 处理流式请求
const handleStreamRequest = async (headers: any, body: any) => {
const response = await fetch(testRequest.url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
})
// 获取响应头
const responseHeaders: any = {}
response.headers.forEach((value, key) => {
responseHeaders[key] = value
})
testResponse.status = response.status
testResponse.headers = JSON.stringify(responseHeaders, null, 2)
if (!response.ok) {
const errorBody = await response.text()
testResponse.body = errorBody
ElMessage.warning(`请求返回状态码: ${response.status}`)
return
}
if (!response.body) {
testResponse.body = 'Error: 响应体为空'
ElMessage.error('响应体为空')
return
}
// 处理流式响应
const reader = response.body.getReader()
const decoder = new TextDecoder()
let streamContent = ''
let chunks: string[] = []
// 清空响应体,准备接收流式数据
testResponse.body = '正在接收流式数据...\n\n'
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
// 解码数据块
const chunk = decoder.decode(value, { stream: true })
streamContent += chunk
chunks.push(chunk)
// 实时更新响应体显示
testResponse.body = `=== 流式响应数据 ===\n\n` +
`接收到 ${chunks.length} 个数据块,总长度: ${streamContent.length} 字符\n\n` +
`=== 原始数据流 ===\n${streamContent}\n\n` +
`=== 解析后的数据 ===\n${parseStreamData(streamContent)}`
}
ElMessage.success(`流式请求完成,共接收 ${chunks.length} 个数据块`)
} catch (streamError: any) {
console.error('流式数据读取失败:', streamError)
testResponse.body += `\n\n=== 流式读取错误 ===\n${streamError.message || streamError}`
ElMessage.error('流式数据读取失败: ' + (streamError.message || streamError))
} finally {
reader.releaseLock()
}
}
// 解析流式数据
const parseStreamData = (streamContent: string): string => {
try {
const lines = streamContent.split('\n')
const parsedData: any[] = []
let currentEvent = ''
let currentData = ''
for (const line of lines) {
if (line.startsWith('event:')) {
currentEvent = line.substring(6).trim()
} else if (line.startsWith('data:')) {
currentData = line.substring(5).trim()
if (currentData === '[DONE]') {
parsedData.push({
event: currentEvent || 'done',
data: '[DONE]',
timestamp: new Date().toISOString()
})
} else if (currentData) {
try {
const jsonData = JSON.parse(currentData)
parsedData.push({
event: currentEvent || 'data',
data: jsonData,
timestamp: new Date().toISOString()
})
} catch (e) {
parsedData.push({
event: currentEvent || 'raw',
data: currentData,
timestamp: new Date().toISOString()
})
}
}
currentEvent = ''
currentData = ''
} else if (line.trim() === '') {
// 空行,重置状态
currentEvent = ''
currentData = ''
}
}
return JSON.stringify(parsedData, null, 2)
} catch (e) {
return `解析失败: ${e}\n\n原始内容:\n${streamContent}`
}
}
// 格式化请求 // 格式化请求
const handleFormatRequest = () => { const handleFormatRequest = () => {
try { try {
@@ -1260,6 +1437,8 @@ const handleTestDialogClose = () => {
testResponse.status = null testResponse.status = null
testResponse.headers = '' testResponse.headers = ''
testResponse.body = '' testResponse.body = ''
testOptions.useStream = false
testOptions.testMessage = '你好,这是一个测试消息,请回复确认接口正常工作。'
} }
onMounted(() => { onMounted(() => {
@@ -1321,6 +1500,17 @@ onMounted(() => {
padding-bottom: 8px; padding-bottom: 8px;
} }
.test-options {
display: flex;
align-items: center;
gap: 8px;
.info-icon {
color: #909399;
cursor: help;
}
}
.el-textarea { .el-textarea {
:deep(.el-textarea__inner) { :deep(.el-textarea__inner) {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;