AI接口支持流式调用
This commit is contained in:
@@ -13,6 +13,8 @@ import com.emotion.service.ConversationService;
|
||||
import com.emotion.service.CozeApiCallService;
|
||||
import com.emotion.service.EmotionRecordService;
|
||||
import com.emotion.service.EmotionAnalysisService;
|
||||
import com.emotion.service.AiConfigService;
|
||||
import com.emotion.entity.AiConfig;
|
||||
import com.emotion.dto.request.*;
|
||||
import com.emotion.dto.response.*;
|
||||
import com.emotion.util.SnowflakeIdGenerator;
|
||||
@@ -41,6 +43,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* AI聊天服务实现类
|
||||
@@ -73,37 +76,15 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
@Autowired
|
||||
private SnowflakeIdGenerator snowflakeIdGenerator;
|
||||
|
||||
@Value("${emotion.coze.api.token:}")
|
||||
private String cozeApiToken;
|
||||
|
||||
@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;
|
||||
@Autowired
|
||||
private AiConfigService aiConfigService;
|
||||
|
||||
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 相关常量
|
||||
private static final String CONTENT_KEY = "content";
|
||||
@@ -494,13 +475,20 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
@Override
|
||||
public boolean healthCheck() {
|
||||
try {
|
||||
// 简化健康检查 - 检查必要配置是否存在
|
||||
boolean configValid = cozeApiToken != null && !cozeApiToken.trim().isEmpty() &&
|
||||
chatBotId != null && !chatBotId.trim().isEmpty() &&
|
||||
cozeBaseUrl != null && !cozeBaseUrl.trim().isEmpty();
|
||||
// 检查聊天场景的AI配置是否存在
|
||||
AiConfig chatConfig = aiConfigService.getBestConfig(USAGE_SCENARIO_CHAT, DEFAULT_ENVIRONMENT);
|
||||
if (chatConfig == null) {
|
||||
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) {
|
||||
log.warn("Coze API 配置不完整");
|
||||
log.warn("AI配置不完整: configId={}", chatConfig.getId());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -587,65 +575,89 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
CozeApiCall apiCall = createSummaryApiCallRecord(conversationId, null, userMessage, userId, "summary");
|
||||
|
||||
try {
|
||||
// 构建请求头
|
||||
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总结服务响应异常,请稍后再试。";
|
||||
}
|
||||
|
||||
return executeSummaryCozeApiCall(apiCall, conversationId, userMessage, userId);
|
||||
} catch (Exception e) {
|
||||
log.error("发送总结消息到Coze AI失败", e);
|
||||
updateApiCallError(apiCall, "REQUEST_FAILED", e.getMessage());
|
||||
log.error("发送总结消息失败", e);
|
||||
updateApiCallFailure(apiCall, e.getMessage());
|
||||
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
|
||||
public EmotionSummaryGenerateResponse generateEmotionSummaryWithResponse(String userId) {
|
||||
log.info("生成用户情绪记录总结: userId={}", userId);
|
||||
@@ -719,9 +731,11 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
|
||||
public boolean isServiceAvailable() {
|
||||
try {
|
||||
// 简单的健康检查
|
||||
return cozeApiToken != null && !cozeApiToken.isEmpty() &&
|
||||
chatBotId != null && !chatBotId.isEmpty();
|
||||
// 检查聊天场景的AI配置是否可用
|
||||
AiConfig chatConfig = aiConfigService.getBestConfig(USAGE_SCENARIO_CHAT, DEFAULT_ENVIRONMENT);
|
||||
return chatConfig != null &&
|
||||
chatConfig.getApiToken() != null && !chatConfig.getApiToken().isEmpty() &&
|
||||
chatConfig.getBotId() != null && !chatConfig.getBotId().isEmpty();
|
||||
} catch (Exception e) {
|
||||
log.error("检查AI服务可用性失败", e);
|
||||
return false;
|
||||
@@ -751,22 +765,40 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
* 执行Coze API调用的公共逻辑
|
||||
*/
|
||||
private String executeCozeApiCall(CozeApiCall apiCall, String conversationId, String userMessage, String userId) {
|
||||
// 获取聊天场景的AI配置
|
||||
AiConfig config = getChatAiConfig();
|
||||
|
||||
// 构建请求体 - 使用正确的Coze API格式
|
||||
Map<String, Object> requestBody = buildCozeRequestWithConfig(conversationId, userMessage, userId, config);
|
||||
|
||||
// 检查是否使用流式输出
|
||||
boolean useStream = (Boolean) requestBody.get("stream");
|
||||
|
||||
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 " + cozeApiToken);
|
||||
headers.set("Authorization", "Bearer " + config.getApiToken());
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
// 构建请求体 - 使用正确的Coze API格式
|
||||
Map<String, Object> requestBody = buildCozeRequest(conversationId, userMessage, userId);
|
||||
|
||||
// 构建完整的API URL
|
||||
String cozeApiUrl = config.getApiBaseUrl() + getApiPath(config);
|
||||
|
||||
// 更新API调用记录的请求信息
|
||||
updateApiCallRequest(apiCall, cozeBaseUrl + chatPath, requestBody, headers);
|
||||
updateApiCallRequest(apiCall, cozeApiUrl, requestBody, headers);
|
||||
|
||||
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
// 构建完整的API URL
|
||||
String cozeApiUrl = cozeBaseUrl + chatPath;
|
||||
log.info("发送Coze请求到: {}, 请求体: {}", cozeApiUrl, requestBody);
|
||||
log.info("发送Coze普通请求到: {}, 请求体: {}", cozeApiUrl, requestBody);
|
||||
|
||||
// 发送请求
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
@@ -791,7 +823,7 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
|
||||
// 轮询聊天状态直到完成并获取回复内容
|
||||
String aiReply = waitForChatCompletionWithTracking(chatId, cozeConversationId, apiCall);
|
||||
log.info("Coze AI响应成功: reply={}", aiReply);
|
||||
log.info("Coze AI普通响应成功: reply={}", aiReply);
|
||||
|
||||
// 更新API调用记录的最终结果
|
||||
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) {
|
||||
log.info("访客聊天: message={}, clientIp={}", message, clientIp);
|
||||
|
||||
@@ -883,16 +1074,28 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
* 构建Coze API请求 - 根据官方文档修正格式
|
||||
*/
|
||||
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<>();
|
||||
cozeRequest.put("bot_id", chatBotId);
|
||||
cozeRequest.put("bot_id", config.getBotId());
|
||||
|
||||
// 如果有workflow_id,则添加
|
||||
if (chatWorkflowId != null && !chatWorkflowId.trim().isEmpty()) {
|
||||
cozeRequest.put("workflow_id", chatWorkflowId);
|
||||
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("stream", false);
|
||||
|
||||
// 根据配置决定是否使用流式输出
|
||||
boolean useStream = config.getSupportStream() != null && config.getSupportStream() == 1;
|
||||
cozeRequest.put("stream", useStream);
|
||||
|
||||
// 构建消息列表 - 按照 Coze API 标准格式
|
||||
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) {
|
||||
try {
|
||||
// 获取聊天场景的AI配置
|
||||
AiConfig config = getChatAiConfig();
|
||||
|
||||
// 最多等待30秒,每2秒轮询一次
|
||||
int maxAttempts = 15;
|
||||
int attempt = 0;
|
||||
@@ -952,12 +1158,12 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
log.info("轮询聊天状态,第{}次尝试: chatId={}, conversationId={}", attempt + 1, chatId, conversationId);
|
||||
|
||||
// 构建状态查询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;
|
||||
|
||||
// 构建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", "Bearer " + cozeApiToken);
|
||||
headers.set("Authorization", "Bearer " + config.getApiToken());
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
HttpEntity<String> request = new HttpEntity<>(headers);
|
||||
@@ -1009,15 +1215,18 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
*/
|
||||
private String getChatMessages(String chatId, String conversationId) {
|
||||
try {
|
||||
// 获取聊天场景的AI配置
|
||||
AiConfig config = getChatAiConfig();
|
||||
|
||||
log.info("获取聊天消息: chatId={}, conversationId={}", chatId, conversationId);
|
||||
|
||||
// 构建消息查询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;
|
||||
|
||||
// 构建请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", "Bearer " + cozeApiToken);
|
||||
headers.set("Authorization", "Bearer " + config.getApiToken());
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
HttpEntity<String> request = new HttpEntity<>(headers);
|
||||
@@ -1067,16 +1276,22 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
* 构建总结请求 - 使用专门的总结bot和workflow
|
||||
*/
|
||||
private Map<String, Object> buildSummaryRequest(String conversationId, String userMessage, String userId) {
|
||||
// 获取总结场景的AI配置
|
||||
AiConfig config = getSummaryAiConfig();
|
||||
|
||||
Map<String, Object> cozeRequest = new HashMap<>();
|
||||
cozeRequest.put("bot_id", summaryBotId != null && !summaryBotId.trim().isEmpty() ? summaryBotId : chatBotId);
|
||||
cozeRequest.put("bot_id", config.getBotId());
|
||||
|
||||
// 如果有总结workflow_id,则添加
|
||||
if (summaryWorkflowId != null && !summaryWorkflowId.trim().isEmpty()) {
|
||||
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("stream", false);
|
||||
|
||||
// 根据配置决定是否使用流式输出
|
||||
boolean useStream = config.getSupportStream() != null && config.getSupportStream() == 1;
|
||||
cozeRequest.put("stream", useStream);
|
||||
cozeRequest.put("auto_save_history", true);
|
||||
|
||||
// 如果有会话ID,则添加
|
||||
@@ -1118,6 +1333,9 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
*/
|
||||
private CozeApiCall createApiCallRecord(String conversationId, String messageId, String userMessage, String userId,
|
||||
String requestType) {
|
||||
// 获取聊天场景的AI配置
|
||||
AiConfig config = getChatAiConfig();
|
||||
|
||||
CozeApiCall apiCall = CozeApiCall.builder()
|
||||
.conversationId(conversationId)
|
||||
.messageId(messageId) // 设置messageId
|
||||
@@ -1125,8 +1343,8 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
.requestType(requestType)
|
||||
.userMessage(userMessage)
|
||||
.userMessageType("text")
|
||||
.botId(chatBotId)
|
||||
.workflowId(chatWorkflowId)
|
||||
.botId(config.getBotId())
|
||||
.workflowId(config.getWorkflowId())
|
||||
.status("pending")
|
||||
.startTime(LocalDateTime.now())
|
||||
.traceId(UUID.randomUUID().toString())
|
||||
@@ -1158,6 +1376,9 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
*/
|
||||
private CozeApiCall createSummaryApiCallRecord(String conversationId, String messageId, String userMessage,
|
||||
String userId, String requestType) {
|
||||
// 获取总结场景的AI配置
|
||||
AiConfig config = getSummaryAiConfig();
|
||||
|
||||
CozeApiCall apiCall = CozeApiCall.builder()
|
||||
.conversationId(conversationId)
|
||||
.messageId(messageId) // 设置messageId
|
||||
@@ -1165,8 +1386,8 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
.requestType(requestType)
|
||||
.userMessage(userMessage)
|
||||
.userMessageType("text")
|
||||
.botId(summaryBotId)
|
||||
.workflowId(summaryWorkflowId)
|
||||
.botId(config.getBotId())
|
||||
.workflowId(config.getWorkflowId())
|
||||
.status("pending")
|
||||
.startTime(LocalDateTime.now())
|
||||
.traceId(UUID.randomUUID().toString())
|
||||
@@ -1505,4 +1726,36 @@ public class AiChatServiceImpl implements AiChatService {
|
||||
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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -549,6 +549,28 @@
|
||||
<el-input v-model="testRequest.url" readonly />
|
||||
</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-input
|
||||
v-model="testRequest.headers"
|
||||
@@ -562,14 +584,14 @@
|
||||
<el-input
|
||||
v-model="testRequest.body"
|
||||
type="textarea"
|
||||
:rows="12"
|
||||
:rows="10"
|
||||
placeholder="JSON格式的请求体"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleTestRequest" :loading="testLoading">
|
||||
发送测试请求
|
||||
{{ testOptions.useStream ? '发送流式测试' : '发送测试请求' }}
|
||||
</el-button>
|
||||
<el-button @click="handleFormatRequest">格式化请求</el-button>
|
||||
<el-button @click="handleResetTest">重置</el-button>
|
||||
@@ -625,6 +647,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { InfoFilled } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getAiConfigPage,
|
||||
createAiConfig,
|
||||
@@ -746,6 +769,11 @@ const testResponse = reactive({
|
||||
body: ''
|
||||
})
|
||||
|
||||
const testOptions = reactive({
|
||||
useStream: false,
|
||||
testMessage: '你好,这是一个测试消息,请回复确认接口正常工作。'
|
||||
})
|
||||
|
||||
// 获取配置类型标签类型
|
||||
const getConfigTypeTagType = (type: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
@@ -1078,11 +1106,11 @@ const initTestData = (config: AiConfig) => {
|
||||
const requestBody: any = {
|
||||
bot_id: config.botId || '',
|
||||
user_id: 'test_user_' + Date.now(),
|
||||
stream: false,
|
||||
stream: testOptions.useStream,
|
||||
additional_messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: '你好,这是一个测试消息,请回复确认接口正常工作。',
|
||||
content: testOptions.testMessage,
|
||||
content_type: 'text',
|
||||
type: 'question'
|
||||
}
|
||||
@@ -1113,6 +1141,20 @@ const initTestData = (config: AiConfig) => {
|
||||
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 () => {
|
||||
if (!testConfig.value) return
|
||||
@@ -1138,39 +1180,15 @@ const handleTestRequest = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
const response = await fetch(testRequest.url, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
// 检查是否为流式请求
|
||||
const isStreamRequest = body.stream === true
|
||||
|
||||
// 获取响应头
|
||||
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('测试请求发送成功')
|
||||
if (isStreamRequest) {
|
||||
// 处理流式请求
|
||||
await handleStreamRequest(headers, body)
|
||||
} else {
|
||||
ElMessage.warning(`请求返回状态码: ${response.status}`)
|
||||
// 处理普通请求
|
||||
await handleNormalRequest(headers, body)
|
||||
}
|
||||
|
||||
} 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 = () => {
|
||||
try {
|
||||
@@ -1260,6 +1437,8 @@ const handleTestDialogClose = () => {
|
||||
testResponse.status = null
|
||||
testResponse.headers = ''
|
||||
testResponse.body = ''
|
||||
testOptions.useStream = false
|
||||
testOptions.testMessage = '你好,这是一个测试消息,请回复确认接口正常工作。'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -1321,6 +1500,17 @@ onMounted(() => {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.test-options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.info-icon {
|
||||
color: #909399;
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
|
||||
.el-textarea {
|
||||
:deep(.el-textarea__inner) {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
|
||||
Reference in New Issue
Block a user