兼容Debug EML SSE断流兜底
This commit is contained in:
@@ -93,16 +93,16 @@ public class SuperAgentOpenApiSseParser {
|
||||
* 汇总解析状态并生成结果对象。
|
||||
*/
|
||||
private SuperAgentOpenApiResult buildResult(String sessionId, Set<String> eventTypes, ParsedState state) {
|
||||
if (!state.endSeen) {
|
||||
throw new SuperAgentOpenApiException("SuperAgent SSE 未收到结束事件。");
|
||||
}
|
||||
if ((state.rawAnswer == null || state.rawAnswer.isBlank()) && state.fallbackRawAnswer != null) {
|
||||
if ((state.rawAnswer == null || state.rawAnswer.isBlank()) && state.endSeen && state.fallbackRawAnswer != null) {
|
||||
state.rawAnswer = state.fallbackRawAnswer;
|
||||
state.modelName = state.fallbackModelName;
|
||||
state.inputTokens = state.fallbackInputTokens;
|
||||
state.outputTokens = state.fallbackOutputTokens;
|
||||
state.totalTokens = state.fallbackTotalTokens;
|
||||
}
|
||||
if (!state.endSeen && (state.rawAnswer == null || state.rawAnswer.isBlank())) {
|
||||
throw new SuperAgentOpenApiException("SuperAgent SSE 未收到结束事件。");
|
||||
}
|
||||
if (state.rawAnswer == null || state.rawAnswer.isBlank()) {
|
||||
throw new SuperAgentOpenApiException("SuperAgent SSE 未找到最终 AI 回答。");
|
||||
}
|
||||
|
||||
@@ -9,13 +9,49 @@ import java.time.LocalDateTime;
|
||||
* @param hotelId 酒店或业务上下文 ID
|
||||
* @param runStatus 当前运行状态
|
||||
* @param sourceMessageId 关联的内部 SourceMessage ID
|
||||
* @param externalMessageId Debug 外部邮件 ID
|
||||
* @param externalConversationId Debug 外部会话 ID
|
||||
* @param originalFileName 原始 EML 安全文件名
|
||||
* @param originalEmlOssUrl 原始 EML OSS URL
|
||||
* @param originalEmlSha256 原始 EML SHA-256
|
||||
* @param payloadJson 发送给 SuperAgent 的 AgentBus-like payload
|
||||
* @param superagentSessionId SuperAgent session ID
|
||||
* @param superagentRunId SuperAgent run ID
|
||||
* @param superagentProfileId SuperAgent profile ID
|
||||
* @param superagentProfileVersionId SuperAgent profile version ID
|
||||
* @param superagentModelName SuperAgent 模型名称
|
||||
* @param superagentRawAnswer SuperAgent 最终原始回答
|
||||
* @param superagentParsedJson 后端解析出的 SuperAgent JSON 文本
|
||||
* @param superagentInputTokens SuperAgent 输入 token 数
|
||||
* @param superagentOutputTokens SuperAgent 输出 token 数
|
||||
* @param superagentTotalTokens SuperAgent 总 token 数
|
||||
* @param safeErrorSummary 安全错误摘要
|
||||
* @param createdAt 创建 UTC 时间
|
||||
* @param updatedAt 更新 UTC 时间
|
||||
*/
|
||||
public record DebugEmlSuperAgentRunSnapshot(
|
||||
Long id,
|
||||
String hotelId,
|
||||
String runStatus,
|
||||
Long sourceMessageId,
|
||||
LocalDateTime createdAt
|
||||
String externalMessageId,
|
||||
String externalConversationId,
|
||||
String originalFileName,
|
||||
String originalEmlOssUrl,
|
||||
String originalEmlSha256,
|
||||
String payloadJson,
|
||||
String superagentSessionId,
|
||||
String superagentRunId,
|
||||
String superagentProfileId,
|
||||
String superagentProfileVersionId,
|
||||
String superagentModelName,
|
||||
String superagentRawAnswer,
|
||||
String superagentParsedJson,
|
||||
Integer superagentInputTokens,
|
||||
Integer superagentOutputTokens,
|
||||
Integer superagentTotalTokens,
|
||||
String safeErrorSummary,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import java.util.Map;
|
||||
* @param superagentTraceEvents SuperAgent 公开 Trace 事件列表
|
||||
* @param warnings 可展示的安全警告
|
||||
* @param status Debug 运行状态
|
||||
* @param safeErrorSummary 安全错误摘要,成功时为空;不包含 Secret、邮件正文或附件签名 URL
|
||||
*/
|
||||
public record DebugEmlSuperAgentRunResult(
|
||||
@JsonProperty("debug_run_id")
|
||||
@@ -68,6 +69,8 @@ public record DebugEmlSuperAgentRunResult(
|
||||
@JsonProperty("superagent_trace_events")
|
||||
List<SuperAgentOpenApiTraceEvent> superagentTraceEvents,
|
||||
List<String> warnings,
|
||||
String status
|
||||
String status,
|
||||
@JsonProperty("safe_error_summary")
|
||||
String safeErrorSummary
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -44,6 +46,16 @@ public class DebugEmlSuperAgentController {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(runService.uploadAndRun(accessKey, file, hotelId, runLabel));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Debug EML 运行结果,用于流式响应提前结束后的前端兜底轮询。
|
||||
*/
|
||||
@GetMapping(path = "/{runId}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public DebugEmlSuperAgentRunResult getRun(
|
||||
@RequestHeader(name = "X-TH-Hotel-Debug-Upload-Key", required = false) String accessKey,
|
||||
@PathVariable String runId) {
|
||||
return runService.getRun(accessKey, runId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传单封 .eml 邮件并以 SSE 实时返回本系统阶段、SuperAgent 公开 Trace 和最终回答。
|
||||
*/
|
||||
|
||||
@@ -100,6 +100,24 @@ public class MybatisDebugEmlSuperAgentRunRepository implements DebugEmlSuperAgen
|
||||
entity.getHotelId(),
|
||||
entity.getRunStatus(),
|
||||
entity.getSourceMessageId(),
|
||||
entity.getCreatedAt());
|
||||
entity.getExternalMessageId(),
|
||||
entity.getExternalConversationId(),
|
||||
entity.getOriginalFileName(),
|
||||
entity.getOriginalEmlOssUrl(),
|
||||
entity.getOriginalEmlSha256(),
|
||||
entity.getPayloadJson(),
|
||||
entity.getSuperagentSessionId(),
|
||||
entity.getSuperagentRunId(),
|
||||
entity.getSuperagentProfileId(),
|
||||
entity.getSuperagentProfileVersionId(),
|
||||
entity.getSuperagentModelName(),
|
||||
entity.getSuperagentRawAnswer(),
|
||||
entity.getSuperagentParsedJson(),
|
||||
entity.getSuperagentInputTokens(),
|
||||
entity.getSuperagentOutputTokens(),
|
||||
entity.getSuperagentTotalTokens(),
|
||||
entity.getSafeErrorSummary(),
|
||||
entity.getCreatedAt(),
|
||||
entity.getUpdatedAt());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,11 @@ public interface DebugEmlSuperAgentRunService {
|
||||
String hotelId,
|
||||
String runLabel);
|
||||
|
||||
/**
|
||||
* 查询 Debug EML 运行安全结果,用于流式响应被中间链路提前关闭后的前端兜底轮询。
|
||||
*/
|
||||
DebugEmlSuperAgentRunResult getRun(String accessKey, String runId);
|
||||
|
||||
/**
|
||||
* 上传并处理单封 EML,按 text/event-stream 实时写出内部阶段、SuperAgent Trace 和最终结果。
|
||||
*/
|
||||
|
||||
@@ -12,6 +12,7 @@ import cn.nianxx.thhotel.integrations.storage.aliyunoss.service.impl.AliyunOssPr
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.service.impl.ObjectStorageException;
|
||||
import cn.nianxx.thhotel.platform.common.enums.SourceMessageOnlyResultCode;
|
||||
import cn.nianxx.thhotel.platform.debug.common.dto.DebugEmlSuperAgentRunDraft;
|
||||
import cn.nianxx.thhotel.platform.debug.common.dto.DebugEmlSuperAgentRunSnapshot;
|
||||
import cn.nianxx.thhotel.platform.debug.common.dto.DebugEmlSuperAgentRunStatusUpdate;
|
||||
import cn.nianxx.thhotel.platform.debug.common.dto.DebugEmlSuperAgentRunUpdate;
|
||||
import cn.nianxx.thhotel.platform.debug.common.enums.DebugEmlSuperAgentRunStatus;
|
||||
@@ -23,15 +24,19 @@ import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.ParsedEmlMediaItem;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.ParsedEmlMessage;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageOriginalContent;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageOriginalMediaItem;
|
||||
import cn.nianxx.thhotel.platform.message.common.enums.SourceMessageMediaType;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageCommand;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageMedia;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResult;
|
||||
import cn.nianxx.thhotel.platform.message.repository.SourceMessageInboxRepository;
|
||||
import cn.nianxx.thhotel.platform.message.service.EmlMessageParseService;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageHtmlSanitizerService;
|
||||
import cn.nianxx.thhotel.platform.message.service.impl.EmlMessageParseException;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
@@ -97,6 +102,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
private final EmlMessageParseService parseService;
|
||||
private final ObjectStorageService objectStorageService;
|
||||
private final SourceMessageCaptureService sourceMessageCaptureService;
|
||||
private final SourceMessageInboxRepository sourceMessageInboxRepository;
|
||||
private final SourceMessageHtmlSanitizerService htmlSanitizerService;
|
||||
private final SuperAgentOpenApiClient superAgentOpenApiClient;
|
||||
private final DebugEmlSuperAgentRunRepository runRepository;
|
||||
@@ -112,6 +118,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
EmlMessageParseService parseService,
|
||||
ObjectStorageService objectStorageService,
|
||||
SourceMessageCaptureService sourceMessageCaptureService,
|
||||
SourceMessageInboxRepository sourceMessageInboxRepository,
|
||||
SourceMessageHtmlSanitizerService htmlSanitizerService,
|
||||
SuperAgentOpenApiClient superAgentOpenApiClient,
|
||||
DebugEmlSuperAgentRunRepository runRepository,
|
||||
@@ -122,6 +129,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
this.parseService = parseService;
|
||||
this.objectStorageService = objectStorageService;
|
||||
this.sourceMessageCaptureService = sourceMessageCaptureService;
|
||||
this.sourceMessageInboxRepository = sourceMessageInboxRepository;
|
||||
this.htmlSanitizerService = htmlSanitizerService;
|
||||
this.superAgentOpenApiClient = superAgentOpenApiClient;
|
||||
this.runRepository = runRepository;
|
||||
@@ -199,6 +207,21 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Debug EML 运行安全结果。用于前端 SSE 被中间链路提前关闭后按 runId 兜底轮询。
|
||||
*/
|
||||
@Override
|
||||
public DebugEmlSuperAgentRunResult getRun(String accessKey, String runId) {
|
||||
validateAccessKey(accessKey);
|
||||
Long parsedRunId = parseRunId(runId);
|
||||
DebugEmlSuperAgentRunSnapshot snapshot = runRepository.findById(parsedRunId)
|
||||
.orElseThrow(() -> new DebugEmlSuperAgentException(
|
||||
HttpStatus.NOT_FOUND,
|
||||
"DEBUG_EML_RUN_NOT_FOUND",
|
||||
"Debug EML 运行记录不存在。"));
|
||||
return toRunResult(snapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单封 Debug EML 上传,并实时输出安全的调试 SSE 事件。
|
||||
*/
|
||||
@@ -571,7 +594,131 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
parsedJson,
|
||||
traceEvents,
|
||||
List.copyOf(warnings),
|
||||
status.name());
|
||||
status.name(),
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将持久化快照转换为 Debug 页面可安全展示的查询结果。
|
||||
*/
|
||||
private DebugEmlSuperAgentRunResult toRunResult(DebugEmlSuperAgentRunSnapshot snapshot) {
|
||||
SourceMessageOriginalContent originalContent = loadOriginalContent(snapshot.sourceMessageId());
|
||||
String htmlBodyWithOssUrls = originalContent == null ? null : originalContent.htmlBody();
|
||||
String htmlBodySanitized = htmlBodyWithOssUrls == null ? null : htmlSanitizerService.sanitizeHtml(htmlBodyWithOssUrls);
|
||||
return new DebugEmlSuperAgentRunResult(
|
||||
snapshot.id().toString(),
|
||||
snapshot.sourceMessageId() == null ? null : snapshot.sourceMessageId().toString(),
|
||||
snapshot.sourceMessageId() == null ? null : SOURCE_PROVIDER,
|
||||
snapshot.externalMessageId(),
|
||||
snapshot.externalConversationId(),
|
||||
snapshot.originalEmlOssUrl(),
|
||||
snapshot.originalEmlSha256(),
|
||||
originalContent == null ? List.of() : debugMediaResults(originalContent.mediaItems()),
|
||||
htmlBodyWithOssUrls,
|
||||
htmlBodySanitized,
|
||||
originalContent == null ? null : true,
|
||||
originalContent == null ? null : htmlSanitizerService.htmlRenderMode(htmlBodyWithOssUrls),
|
||||
parsePayloadJson(snapshot.payloadJson()),
|
||||
snapshot.superagentSessionId(),
|
||||
snapshot.superagentRunId(),
|
||||
snapshot.superagentRawAnswer(),
|
||||
parseJsonNode(snapshot.superagentParsedJson()),
|
||||
List.of(),
|
||||
List.of(),
|
||||
snapshot.runStatus(),
|
||||
snapshot.safeErrorSummary());
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 Debug run 关联 SourceMessage 的受控原文快照;缺失时返回 null,查询接口仍返回运行状态。
|
||||
*/
|
||||
private SourceMessageOriginalContent loadOriginalContent(Long sourceMessageId) {
|
||||
if (sourceMessageId == null) {
|
||||
return null;
|
||||
}
|
||||
return sourceMessageInboxRepository.findOriginalContent(sourceMessageId).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 SourceMessage 媒体快照转换为 Debug 响应媒体项。
|
||||
*/
|
||||
private List<DebugEmlUploadedMediaResult> debugMediaResults(List<SourceMessageOriginalMediaItem> mediaItems) {
|
||||
if (mediaItems == null || mediaItems.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return mediaItems.stream()
|
||||
.map(item -> new DebugEmlUploadedMediaResult(
|
||||
item.mediaType(),
|
||||
item.fileName(),
|
||||
item.contentType(),
|
||||
item.sizeBytes(),
|
||||
item.externalUrl(),
|
||||
item.externalMediaId(),
|
||||
objectKeyFromPublicUrl(item.externalUrl())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 尽量从公开 URL 还原 OSS object key;无法可靠还原时返回 null,不影响前端调试展示。
|
||||
*/
|
||||
private String objectKeyFromPublicUrl(String externalUrl) {
|
||||
String normalizedUrl = trimToNull(externalUrl);
|
||||
String publicBaseUrl = trimTrailingSlash(trimToNull(ossProperties.getPublicBaseUrl()));
|
||||
if (normalizedUrl == null || publicBaseUrl == null || !normalizedUrl.startsWith(publicBaseUrl + "/")) {
|
||||
return null;
|
||||
}
|
||||
String encodedObjectKey = normalizedUrl.substring(publicBaseUrl.length() + 1);
|
||||
return URLDecoder.decode(encodedObjectKey, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析查询路径中的 Debug runId。
|
||||
*/
|
||||
private Long parseRunId(String runId) {
|
||||
String normalizedRunId = trimToNull(runId);
|
||||
if (normalizedRunId == null) {
|
||||
throw new DebugEmlSuperAgentException(HttpStatus.BAD_REQUEST, "REQUEST_FIELD_REQUIRED", "debug_run_id 不能为空。");
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(normalizedRunId);
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new DebugEmlSuperAgentException(HttpStatus.BAD_REQUEST, "REQUEST_FIELD_INVALID", "debug_run_id 格式错误。");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析已入库的 payload JSON;解析失败返回 null,避免查询接口抛出内部异常。
|
||||
*/
|
||||
private Map<String, Object> parsePayloadJson(String payloadJson) {
|
||||
String normalizedPayload = trimToNull(payloadJson);
|
||||
if (normalizedPayload == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
JsonNode jsonNode = objectMapper.readTree(normalizedPayload);
|
||||
if (jsonNode == null || !jsonNode.isObject()) {
|
||||
return null;
|
||||
}
|
||||
return objectMapper.convertValue(jsonNode, new TypeReference<>() {
|
||||
});
|
||||
} catch (Exception exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析已入库的 SuperAgent JSON;解析失败返回 null,前端仍可查看 raw answer。
|
||||
*/
|
||||
private JsonNode parseJsonNode(String jsonText) {
|
||||
String normalizedJson = trimToNull(jsonText);
|
||||
if (normalizedJson == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readTree(normalizedJson);
|
||||
} catch (Exception exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1135,6 +1282,20 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 去掉 URL 末尾斜杠,便于用公开基础 URL 还原对象路径。
|
||||
*/
|
||||
private String trimTrailingSlash(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value;
|
||||
while (trimmed.endsWith("/")) {
|
||||
trimmed = trimmed.substring(0, trimmed.length() - 1);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断安全摘要,避免超过数据库限制。
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,10 @@ spring:
|
||||
# multipart 需要高于 Debug EML 业务文件上限,避免超限文件在进入 Controller 前被框架直接 413 拦截。
|
||||
max-file-size: ${DEBUG_EML_UPLOAD_MULTIPART_MAX_FILE_BYTES:20971520}
|
||||
max-request-size: ${DEBUG_EML_UPLOAD_MULTIPART_MAX_REQUEST_BYTES:${DEBUG_EML_UPLOAD_MAX_REQUEST_BYTES:25165824}}
|
||||
mvc:
|
||||
async:
|
||||
# Spring MVC async timeout 是应用级全局值;当前主要用于避免 Debug EML SSE 先于 SuperAgent 调试调用关闭。
|
||||
request-timeout: ${DEBUG_EML_UPLOAD_SSE_REQUEST_TIMEOUT:${SPRING_MVC_ASYNC_REQUEST_TIMEOUT:1800s}}
|
||||
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
|
||||
@@ -89,16 +89,30 @@ class SuperAgentOpenApiSseParserTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenEndEventMissingEvenIfAiContentExists() {
|
||||
void shouldFailWhenEndEventMissingAndOnlyPartialAiContentExists() {
|
||||
String sse = """
|
||||
event: messages
|
||||
data: {"type":"ai","content":"partial answer"}
|
||||
|
||||
""";
|
||||
|
||||
assertThatThrownBy(() -> parser.parse("session-debug-partial", sse))
|
||||
.isInstanceOf(SuperAgentOpenApiException.class)
|
||||
.hasMessageContaining("SuperAgent SSE 未收到结束事件。");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptMissingEndEventWhenFinalAiContentExists() {
|
||||
String sse = """
|
||||
event: messages
|
||||
data: {"type":"ai","content":"{\\"ai_task_results\\":[]}","response_metadata":{"finish_reason":"stop"}}
|
||||
|
||||
""";
|
||||
|
||||
assertThatThrownBy(() -> parser.parse("session-debug-004", sse))
|
||||
.isInstanceOf(SuperAgentOpenApiException.class)
|
||||
.hasMessageContaining("SuperAgent SSE 未收到结束事件。");
|
||||
SuperAgentOpenApiResult result = parser.parse("session-debug-004", sse);
|
||||
|
||||
assertThat(result.rawAnswer()).isEqualTo("{\"ai_task_results\":[]}");
|
||||
assertThat(result.eventTypes()).containsExactly("messages");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -8,6 +8,7 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
@@ -186,6 +187,44 @@ class DebugEmlSuperAgentControllerTest {
|
||||
org.assertj.core.api.Assertions.assertThat(debugRunCount).isEqualTo(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldQueryDebugRunResultByIdForStreamFallback() throws Exception {
|
||||
mockStorageAndSuperAgentSuccess();
|
||||
|
||||
mockMvc.perform(multipart(ENDPOINT)
|
||||
.file(emlFile())
|
||||
.param("hotel_id", "HOTEL-TEST")
|
||||
.param("run_label", "query-run-fallback")
|
||||
.header("X-TH-Hotel-Debug-Upload-Key", "test-debug-upload-key"))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
Long runId = jdbcTemplate.queryForObject("""
|
||||
SELECT id
|
||||
FROM platform_debug_eml_superagent_run
|
||||
WHERE run_label = 'query-run-fallback'
|
||||
""", Long.class);
|
||||
|
||||
mockMvc.perform(get(ENDPOINT + "/" + runId)
|
||||
.header("X-TH-Hotel-Debug-Upload-Key", "test-debug-upload-key"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.debug_run_id").value(runId.toString()))
|
||||
.andExpect(jsonPath("$.source_message_id").isNotEmpty())
|
||||
.andExpect(jsonPath("$.source_provider").value("DEBUG_EML_UPLOAD"))
|
||||
.andExpect(jsonPath("$.uploaded_media", hasSize(greaterThanOrEqualTo(3))))
|
||||
.andExpect(jsonPath("$.html_body_with_oss_urls", containsString("https://oss.example.test/")))
|
||||
.andExpect(jsonPath("$.html_body_with_oss_urls", not(containsString("cid:inline-001"))))
|
||||
.andExpect(jsonPath("$.html_body_sanitized", containsString("https://oss.example.test/")))
|
||||
.andExpect(jsonPath("$.html_sanitize_required").value(true))
|
||||
.andExpect(jsonPath("$.html_render_mode").value("SANITIZED_HTML"))
|
||||
.andExpect(jsonPath("$.superagent_session_id").value("session-debug-001"))
|
||||
.andExpect(jsonPath("$.superagent_run_id").value("run-debug-001"))
|
||||
.andExpect(jsonPath("$.superagent_raw_answer", containsString("ai_task_results")))
|
||||
.andExpect(jsonPath("$.superagent_parsed_json.ai_task_results[0].task_type").value("New Booking"))
|
||||
.andExpect(jsonPath("$.status").value("SUPERAGENT_SUCCEEDED"))
|
||||
.andExpect(jsonPath("$.safe_error_summary").doesNotExist())
|
||||
.andExpect(content().string(not(containsString("test-debug-upload-key"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStreamDebugStagesSuperAgentTraceAndFinalResult() throws Exception {
|
||||
when(objectStorageService.putObject(any())).thenAnswer(invocation -> {
|
||||
|
||||
Reference in New Issue
Block a user