AI接口支持流式调用
This commit is contained in:
@@ -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,37 +76,15 @@ 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";
|
||||||
@@ -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配置
|
||||||
|
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();
|
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");
|
||||||
|
|
||||||
// 构建请求体 - 使用正确的Coze API格式
|
// 构建完整的API URL
|
||||||
Map<String, Object> requestBody = buildCozeRequest(conversationId, userMessage, userId);
|
String cozeApiUrl = config.getApiBaseUrl() + getApiPath(config);
|
||||||
|
|
||||||
// 更新API调用记录的请求信息
|
// 更新API调用记录的请求信息
|
||||||
updateApiCallRequest(apiCall, cozeBaseUrl + chatPath, requestBody, headers);
|
updateApiCallRequest(apiCall, cozeApiUrl, requestBody, headers);
|
||||||
|
|
||||||
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
|
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
|
||||||
|
log.info("发送Coze普通请求到: {}, 请求体: {}", cozeApiUrl, requestBody);
|
||||||
// 构建完整的API URL
|
|
||||||
String cozeApiUrl = cozeBaseUrl + chatPath;
|
|
||||||
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) {
|
||||||
|
// 获取总结场景的AI配置
|
||||||
|
AiConfig config = getSummaryAiConfig();
|
||||||
|
|
||||||
Map<String, Object> cozeRequest = new HashMap<>();
|
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,则添加
|
// 如果有workflow_id,则添加
|
||||||
if (summaryWorkflowId != null && !summaryWorkflowId.trim().isEmpty()) {
|
if (config.getWorkflowId() != null && !config.getWorkflowId().trim().isEmpty()) {
|
||||||
cozeRequest.put("workflow_id", summaryWorkflowId);
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user