增强Debug EML SSE诊断与心跳
This commit is contained in:
@@ -109,6 +109,9 @@ export async function uploadDebugEmlSuperAgentRunStream(
|
||||
handlers.onTrace?.(normalizeTraceEvent(payload))
|
||||
return
|
||||
}
|
||||
if (event.event === 'debug_heartbeat') {
|
||||
return
|
||||
}
|
||||
if (event.event === 'superagent_result') {
|
||||
finalResult = normalizeDebugEmlResult(payload)
|
||||
handlers.onResult?.(finalResult)
|
||||
|
||||
@@ -187,6 +187,8 @@ describe('debugEmlService', () => {
|
||||
'data: {"event":"reasoning.summary","run_id":"run-stream-1","text":"正在分析 Debug 邮件。","ts":"2026-07-11T10:00:01Z"}\n\n',
|
||||
'event: superagent_trace\n',
|
||||
'data: {"event":"tool.call.started","run_id":"run-stream-1","tool_name":"th_hotel_query_case_context","input_summary":"{\\"group_code\\":\\"G001\\"}"}\n\n',
|
||||
'event: debug_heartbeat\n',
|
||||
'data: {"debug_run_id":"90003","status":"CALLING_SUPERAGENT"}\n\n',
|
||||
'event: superagent_result\n',
|
||||
'data: {"debug_run_id":"90003","source_message_id":"30003","uploaded_media":[],"superagent_session_id":"session-stream-1","superagent_run_id":"run-stream-1","superagent_raw_answer":"{\\"route_code\\":\\"S10\\"}","superagent_parsed_json":{"route_code":"S10"},"superagent_trace_events":[{"event":"reasoning.summary","text":"正在分析 Debug 邮件。"}],"warnings":[],"status":"SUPERAGENT_SUCCEEDED"}\n\n',
|
||||
'event: done\n',
|
||||
|
||||
@@ -157,6 +157,7 @@
|
||||
| `DEBUG_EML_UPLOAD_TEST_ACCESS_KEY` | 是 | test Debug EML 上传访问口令,未配置时可兜底 `DEBUG_EML_UPLOAD_ACCESS_KEY`。 |
|
||||
| `DEBUG_EML_UPLOAD_PROD_ACCESS_KEY` | 是 | prod Debug EML 上传访问口令;生产通常不应启用该接口。 |
|
||||
| `DEBUG_EML_UPLOAD_MAX_FILE_BYTES` | 否 | `.eml` 上传大小上限,默认 `10485760`。 |
|
||||
| `DEBUG_EML_UPLOAD_SSE_HEARTBEAT_INTERVAL` | 否 | Debug EML 页面到后端的 SSE 心跳间隔,默认 `15s`;测试机如仍遇到空闲断流可调小到 `10s`。 |
|
||||
| `DEERFLOW_DEV_BASE_URL` / `DEERFLOW_TEST_BASE_URL` / `DEERFLOW_PROD_BASE_URL` | 否 | SuperAgent / DeerFlow Open API 基础地址,未配置时可兜底 `DEERFLOW_BASE_URL`。 |
|
||||
| `DEERFLOW_DEV_OPEN_API_KEY` / `DEERFLOW_TEST_OPEN_API_KEY` / `DEERFLOW_PROD_OPEN_API_KEY` | 是 | SuperAgent Open API Key,未配置时可兜底 `DEERFLOW_OPEN_API_KEY`。 |
|
||||
| `SUPERAGENT_DEV_OPEN_API_ENABLED` / `SUPERAGENT_TEST_OPEN_API_ENABLED` / `SUPERAGENT_PROD_OPEN_API_ENABLED` | 否 | 是否启用真实 SuperAgent Open API 调用;prod 默认关闭。 |
|
||||
|
||||
@@ -425,6 +425,7 @@ integrations.ai.superagent
|
||||
DEBUG_EML_UPLOAD_ENABLED=true
|
||||
DEBUG_EML_UPLOAD_ACCESS_KEY=
|
||||
DEBUG_EML_UPLOAD_MAX_FILE_BYTES=10485760
|
||||
DEBUG_EML_UPLOAD_SSE_HEARTBEAT_INTERVAL=15s
|
||||
|
||||
ALIYUN_OSS_ENDPOINT=
|
||||
ALIYUN_OSS_BUCKET=
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.nianxx.thhotel.platform.debug.service.impl;
|
||||
|
||||
import java.time.Duration;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -16,6 +17,8 @@ public class DebugEmlSuperAgentProperties {
|
||||
private String accessKey = "";
|
||||
/** 允许上传的最大文件字节数。 */
|
||||
private long maxFileBytes = 10 * 1024 * 1024L;
|
||||
/** Debug SSE 心跳间隔,用于避免长时间调用 SuperAgent 时中间链路空闲断开。 */
|
||||
private Duration sseHeartbeatInterval = Duration.ofSeconds(15);
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
@@ -40,4 +43,12 @@ public class DebugEmlSuperAgentProperties {
|
||||
public void setMaxFileBytes(long maxFileBytes) {
|
||||
this.maxFileBytes = maxFileBytes;
|
||||
}
|
||||
|
||||
public Duration getSseHeartbeatInterval() {
|
||||
return sseHeartbeatInterval;
|
||||
}
|
||||
|
||||
public void setSseHeartbeatInterval(Duration sseHeartbeatInterval) {
|
||||
this.sseHeartbeatInterval = sseHeartbeatInterval;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -51,9 +52,16 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -66,12 +74,23 @@ import org.springframework.web.client.RestClientResponseException;
|
||||
@Service
|
||||
public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DebugEmlSuperAgentRunServiceImpl.class);
|
||||
private static final String SOURCE_PROVIDER = "DEBUG_EML_UPLOAD";
|
||||
private static final String SOURCE_CHANNEL = "EMAIL";
|
||||
private static final String SCHEMA_VERSION = "debug-eml-upload-v1";
|
||||
private static final DateTimeFormatter DATE_FOLDER_FORMATTER = DateTimeFormatter.BASIC_ISO_DATE;
|
||||
private static final Pattern CID_REFERENCE_PATTERN = Pattern.compile(
|
||||
"(?i)cid:(?:<[^>]+>|%3c[^\\s\"'>]+%3e|[^\\s\"'>]+)");
|
||||
private static final Pattern JSON_SECRET_PATTERN = Pattern.compile(
|
||||
"(?i)(\"(?:api[_-]?key|token|secret|password|cookie|authorization)\"\\s*:\\s*\")[^\"]*(\")");
|
||||
private static final Pattern AUTHORIZATION_BEARER_SECRET_PATTERN = Pattern.compile(
|
||||
"(?i)(authorization\\s*[:=]\\s*bearer\\s+)[^\\s,;]+");
|
||||
private static final Pattern AUTHORIZATION_SECRET_PATTERN = Pattern.compile(
|
||||
"(?i)(authorization\\s*[:=]\\s*)(?!bearer\\s)[^\\s,;]+");
|
||||
private static final Pattern COOKIE_SECRET_PATTERN = Pattern.compile(
|
||||
"(?i)(cookie\\s*[:=]\\s*)[^\\s,;]+");
|
||||
private static final Pattern TEXT_SECRET_PATTERN = Pattern.compile(
|
||||
"(?i)((?:api[_-]?key|token|secret|password)\\s*[:=]\\s*)[^\\s,;}]+");
|
||||
|
||||
private final DebugEmlSuperAgentProperties properties;
|
||||
private final AliyunOssProperties ossProperties;
|
||||
@@ -162,7 +181,9 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
"OSS 上传失败。",
|
||||
exception);
|
||||
} catch (SuperAgentOpenApiException exception) {
|
||||
markFailed(runId, superAgentFailureSummary(exception), DebugEmlSuperAgentRunStatus.SUPERAGENT_FAILED, nowUtc());
|
||||
String safeSummary = superAgentFailureSummary(exception);
|
||||
logSuperAgentOpenApiFailure(runId, safeSummary, exception);
|
||||
markFailed(runId, safeSummary, DebugEmlSuperAgentRunStatus.SUPERAGENT_FAILED, nowUtc());
|
||||
throw new DebugEmlSuperAgentException(
|
||||
HttpStatus.BAD_GATEWAY,
|
||||
"SUPERAGENT_OPEN_API_FAILED",
|
||||
@@ -233,7 +254,9 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
streamSink.error(runId, "OSS_UPLOAD_FAILED", "OSS 上传失败。", DebugEmlSuperAgentRunStatus.FAILED.name());
|
||||
} catch (SuperAgentOpenApiException exception) {
|
||||
if (runId != null) {
|
||||
markFailed(runId, superAgentFailureSummary(exception), DebugEmlSuperAgentRunStatus.SUPERAGENT_FAILED, nowUtc());
|
||||
String safeSummary = superAgentFailureSummary(exception);
|
||||
logSuperAgentOpenApiFailure(runId, safeSummary, exception);
|
||||
markFailed(runId, safeSummary, DebugEmlSuperAgentRunStatus.SUPERAGENT_FAILED, nowUtc());
|
||||
}
|
||||
streamSink.error(
|
||||
runId,
|
||||
@@ -291,6 +314,52 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
return "SuperAgent Open API 调用失败。";
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 SuperAgent Debug 调用失败的安全诊断日志。只写异常类型和脱敏消息,不记录 API Key、Cookie、邮件正文或响应体。
|
||||
*/
|
||||
private void logSuperAgentOpenApiFailure(
|
||||
Long runId,
|
||||
String safeSummary,
|
||||
SuperAgentOpenApiException exception) {
|
||||
log.warn(
|
||||
"Debug EML SuperAgent Open API failed. debug_run_id={}, safe_error_summary={}, exception_chain={}",
|
||||
runId,
|
||||
safeSummary,
|
||||
safeExceptionChain(exception));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成异常链摘要,帮助区分 timeout、connection reset、EOF 等网络/流读取问题。
|
||||
*/
|
||||
private String safeExceptionChain(Throwable throwable) {
|
||||
List<String> chain = new ArrayList<>();
|
||||
Throwable current = throwable;
|
||||
int depth = 0;
|
||||
while (current != null && depth < 8) {
|
||||
String message = trimToNull(current.getMessage());
|
||||
String item = current.getClass().getSimpleName();
|
||||
if (message != null) {
|
||||
item += ": " + sanitizeLogMessage(message);
|
||||
}
|
||||
chain.add(item);
|
||||
current = current.getCause();
|
||||
depth++;
|
||||
}
|
||||
return String.join(" <- ", chain);
|
||||
}
|
||||
|
||||
/**
|
||||
* 日志消息脱敏并截断,避免外部异常把 Secret 或过长内容写入日志。
|
||||
*/
|
||||
private String sanitizeLogMessage(String message) {
|
||||
String sanitized = JSON_SECRET_PATTERN.matcher(message).replaceAll("$1[REDACTED]$2");
|
||||
sanitized = AUTHORIZATION_BEARER_SECRET_PATTERN.matcher(sanitized).replaceAll("$1[REDACTED]");
|
||||
sanitized = AUTHORIZATION_SECRET_PATTERN.matcher(sanitized).replaceAll("$1[REDACTED]");
|
||||
sanitized = COOKIE_SECRET_PATTERN.matcher(sanitized).replaceAll("$1[REDACTED]");
|
||||
sanitized = TEXT_SECRET_PATTERN.matcher(sanitized).replaceAll("$1[REDACTED]");
|
||||
return safeErrorSummary(sanitized);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取最底层异常,便于判断 HTTP、网络和解析失败类型。
|
||||
*/
|
||||
@@ -448,9 +517,11 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
streamSink.trace(traceEvent);
|
||||
}
|
||||
};
|
||||
SuperAgentOpenApiResult superAgentResult = streamSink == null
|
||||
? superAgentOpenApiClient.invokeMailDebug(superAgentRequest)
|
||||
: superAgentOpenApiClient.invokeMailDebug(superAgentRequest, traceConsumer);
|
||||
SuperAgentOpenApiResult superAgentResult = invokeSuperAgentWithHeartbeat(
|
||||
runId,
|
||||
superAgentRequest,
|
||||
traceConsumer,
|
||||
streamSink);
|
||||
List<SuperAgentOpenApiTraceEvent> traceEvents = streamedTraceEvents.isEmpty()
|
||||
? superAgentResult.traceEvents()
|
||||
: List.copyOf(streamedTraceEvents);
|
||||
@@ -503,6 +574,52 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
status.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 SuperAgent 期间维持 Debug SSE 心跳,避免无 trace 输出时浏览器或反向代理认为连接空闲。
|
||||
*/
|
||||
private SuperAgentOpenApiResult invokeSuperAgentWithHeartbeat(
|
||||
Long runId,
|
||||
SuperAgentMailDebugRequest request,
|
||||
Consumer<SuperAgentOpenApiTraceEvent> traceConsumer,
|
||||
DebugEmlSuperAgentStreamSink streamSink) {
|
||||
if (streamSink == null) {
|
||||
return superAgentOpenApiClient.invokeMailDebug(request);
|
||||
}
|
||||
ScheduledExecutorService heartbeatExecutor = null;
|
||||
ScheduledFuture<?> heartbeatTask = null;
|
||||
try {
|
||||
Duration interval = properties.getSseHeartbeatInterval();
|
||||
if (interval != null && !interval.isZero() && !interval.isNegative()) {
|
||||
heartbeatExecutor = Executors.newSingleThreadScheduledExecutor(heartbeatThreadFactory(runId));
|
||||
long intervalMillis = Math.max(1L, interval.toMillis());
|
||||
heartbeatTask = heartbeatExecutor.scheduleAtFixedRate(
|
||||
() -> streamSink.heartbeat(runId),
|
||||
intervalMillis,
|
||||
intervalMillis,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
return superAgentOpenApiClient.invokeMailDebug(request, traceConsumer);
|
||||
} finally {
|
||||
if (heartbeatTask != null) {
|
||||
heartbeatTask.cancel(false);
|
||||
}
|
||||
if (heartbeatExecutor != null) {
|
||||
heartbeatExecutor.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Debug SSE 心跳线程;使用 daemon 线程,避免调试请求异常退出时阻塞 JVM 关闭。
|
||||
*/
|
||||
private ThreadFactory heartbeatThreadFactory(Long runId) {
|
||||
return runnable -> {
|
||||
Thread thread = new Thread(runnable, "debug-eml-sse-heartbeat-" + runId);
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SourceMessage 已写入后先回填 Debug run,保证后续 SuperAgent 失败时仍可追踪入库消息和 OSS 原文。
|
||||
*/
|
||||
@@ -1050,6 +1167,11 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
*/
|
||||
void trace(SuperAgentOpenApiTraceEvent traceEvent);
|
||||
|
||||
/**
|
||||
* 输出安全心跳事件,维持 Debug SSE 长连接。
|
||||
*/
|
||||
void heartbeat(Long runId);
|
||||
|
||||
/**
|
||||
* 输出最终 Debug 结果。
|
||||
*/
|
||||
@@ -1094,6 +1216,14 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
send("superagent_trace", traceEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void heartbeat(Long runId) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("debug_run_id", runId == null ? null : runId.toString());
|
||||
data.put("status", DebugEmlSuperAgentRunStatus.CALLING_SUPERAGENT.name());
|
||||
send("debug_heartbeat", data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void result(DebugEmlSuperAgentRunResult result) {
|
||||
send("superagent_result", result);
|
||||
@@ -1118,7 +1248,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
/**
|
||||
* 写出一个 SSE 事件块并立即 flush。写失败通常表示客户端断开,只关闭流输出,不影响后续业务落库。
|
||||
*/
|
||||
private void send(String eventName, Object data) {
|
||||
private synchronized void send(String eventName, Object data) {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ debug:
|
||||
enabled: ${DEBUG_EML_UPLOAD_DEV_ENABLED:${DEBUG_EML_UPLOAD_ENABLED:false}}
|
||||
access-key: ${DEBUG_EML_UPLOAD_DEV_ACCESS_KEY:${DEBUG_EML_UPLOAD_ACCESS_KEY:}}
|
||||
max-file-bytes: ${DEBUG_EML_UPLOAD_DEV_MAX_FILE_BYTES:${DEBUG_EML_UPLOAD_MAX_FILE_BYTES:10485760}}
|
||||
sse-heartbeat-interval: ${DEBUG_EML_UPLOAD_DEV_SSE_HEARTBEAT_INTERVAL:${DEBUG_EML_UPLOAD_SSE_HEARTBEAT_INTERVAL:15s}}
|
||||
|
||||
auth:
|
||||
bootstrap:
|
||||
|
||||
@@ -57,6 +57,7 @@ debug:
|
||||
enabled: ${DEBUG_EML_UPLOAD_PROD_ENABLED:false}
|
||||
access-key: ${DEBUG_EML_UPLOAD_PROD_ACCESS_KEY:${DEBUG_EML_UPLOAD_ACCESS_KEY}}
|
||||
max-file-bytes: ${DEBUG_EML_UPLOAD_PROD_MAX_FILE_BYTES:${DEBUG_EML_UPLOAD_MAX_FILE_BYTES:10485760}}
|
||||
sse-heartbeat-interval: ${DEBUG_EML_UPLOAD_PROD_SSE_HEARTBEAT_INTERVAL:${DEBUG_EML_UPLOAD_SSE_HEARTBEAT_INTERVAL:15s}}
|
||||
|
||||
auth:
|
||||
bootstrap:
|
||||
|
||||
@@ -61,6 +61,7 @@ debug:
|
||||
enabled: ${DEBUG_EML_UPLOAD_TEST_ENABLED:${DEBUG_EML_UPLOAD_ENABLED:false}}
|
||||
access-key: ${DEBUG_EML_UPLOAD_TEST_ACCESS_KEY:${DEBUG_EML_UPLOAD_ACCESS_KEY:}}
|
||||
max-file-bytes: ${DEBUG_EML_UPLOAD_TEST_MAX_FILE_BYTES:${DEBUG_EML_UPLOAD_MAX_FILE_BYTES:10485760}}
|
||||
sse-heartbeat-interval: ${DEBUG_EML_UPLOAD_TEST_SSE_HEARTBEAT_INTERVAL:${DEBUG_EML_UPLOAD_SSE_HEARTBEAT_INTERVAL:15s}}
|
||||
|
||||
auth:
|
||||
bootstrap:
|
||||
|
||||
@@ -23,6 +23,7 @@ import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.request.ObjectSto
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.result.ObjectStoragePutResult;
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.service.ObjectStorageService;
|
||||
import cn.nianxx.thhotel.platform.debug.service.DebugEmlSuperAgentRunService;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -30,10 +31,13 @@ import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
@@ -48,12 +52,14 @@ import org.springframework.web.client.RestClientResponseException;
|
||||
"debug.eml-upload.enabled=true",
|
||||
"debug.eml-upload.access-key=test-debug-upload-key",
|
||||
"debug.eml-upload.max-file-bytes=1048576",
|
||||
"debug.eml-upload.sse-heartbeat-interval=25ms",
|
||||
"aliyun.oss.debug-eml-prefix=debug/eml/",
|
||||
"superagent.open-api.enabled=true",
|
||||
"superagent.open-api.external-subject-id=test-debug-eml"
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class DebugEmlSuperAgentControllerTest {
|
||||
|
||||
private static final String ENDPOINT = "/api/system/debug/eml-superagent-runs";
|
||||
@@ -254,6 +260,50 @@ class DebugEmlSuperAgentControllerTest {
|
||||
.andExpect(content().string(not(containsString("test-debug-upload-key"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSendHeartbeatWhileWaitingForSuperAgentStreamResult() throws Exception {
|
||||
when(objectStorageService.putObject(any())).thenAnswer(invocation -> {
|
||||
ObjectStoragePutRequest request = invocation.getArgument(0);
|
||||
return new ObjectStoragePutResult(
|
||||
request.objectKey(),
|
||||
"https://oss.example.test/" + request.objectKey(),
|
||||
request.contentType(),
|
||||
request.sizeBytes());
|
||||
});
|
||||
when(superAgentOpenApiClient.invokeMailDebug(any(), any())).thenAnswer(invocation -> {
|
||||
Thread.sleep(120);
|
||||
return new SuperAgentOpenApiResult(
|
||||
"session-heartbeat-001",
|
||||
"run-heartbeat-001",
|
||||
"profile-debug",
|
||||
"profile-version-debug",
|
||||
"debug-model",
|
||||
"{\"ai_task_results\":[{\"task_type\":\"New Booking\"}]}",
|
||||
11,
|
||||
7,
|
||||
18,
|
||||
List.of("metadata", "values", "end"),
|
||||
List.of());
|
||||
});
|
||||
|
||||
MvcResult mvcResult = mockMvc.perform(multipart(ENDPOINT + "/stream")
|
||||
.file(emlFile())
|
||||
.param("hotel_id", "HOTEL-TEST")
|
||||
.param("run_label", "stream-heartbeat")
|
||||
.header("X-TH-Hotel-Debug-Upload-Key", "test-debug-upload-key"))
|
||||
.andExpect(request().asyncStarted())
|
||||
.andReturn();
|
||||
|
||||
mockMvc.perform(asyncDispatch(mvcResult))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_EVENT_STREAM))
|
||||
.andExpect(content().string(containsString("event: debug_heartbeat")))
|
||||
.andExpect(content().string(containsString("\"status\":\"CALLING_SUPERAGENT\"")))
|
||||
.andExpect(content().string(containsString("event: superagent_result")))
|
||||
.andExpect(content().string(containsString("event: done")))
|
||||
.andExpect(content().string(not(containsString("test-debug-upload-key"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepBusinessRunStatusWhenSseClientDisconnects() {
|
||||
when(objectStorageService.putObject(any())).thenAnswer(invocation -> {
|
||||
@@ -313,6 +363,36 @@ class DebugEmlSuperAgentControllerTest {
|
||||
org.assertj.core.api.Assertions.assertThat(debugRunCount).isEqualTo(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLogSafeRootCauseWhenSuperAgentSseReadFails(CapturedOutput output) {
|
||||
when(objectStorageService.putObject(any())).thenAnswer(invocation -> {
|
||||
ObjectStoragePutRequest request = invocation.getArgument(0);
|
||||
return new ObjectStoragePutResult(
|
||||
request.objectKey(),
|
||||
"https://oss.example.test/" + request.objectKey(),
|
||||
request.contentType(),
|
||||
request.sizeBytes());
|
||||
});
|
||||
when(superAgentOpenApiClient.invokeMailDebug(any(), any()))
|
||||
.thenThrow(new SuperAgentOpenApiException(
|
||||
"SuperAgent SSE 读取失败。",
|
||||
new IOException("simulated stream reset Authorization: Bearer should-not-log")));
|
||||
|
||||
runService.uploadAndRunStream(
|
||||
"test-debug-upload-key",
|
||||
emlFile(),
|
||||
"HOTEL-TEST",
|
||||
"stream-read-failed-log",
|
||||
new ByteArrayOutputStream());
|
||||
|
||||
org.assertj.core.api.Assertions.assertThat(output.getOut())
|
||||
.contains("Debug EML SuperAgent Open API failed")
|
||||
.contains("SuperAgentOpenApiException: SuperAgent SSE 读取失败。")
|
||||
.contains("IOException: simulated stream reset")
|
||||
.doesNotContain("should-not-log")
|
||||
.doesNotContain("Bearer should-not-log");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRecordPhaseBeforeUploadingOriginalEmlToOss() throws Exception {
|
||||
AtomicInteger uploadIndex = new AtomicInteger();
|
||||
|
||||
Reference in New Issue
Block a user