实现Debug EML实时Trace调试链路
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package cn.nianxx.thhotel.platform.debug.common.result;
|
||||
|
||||
import cn.nianxx.thhotel.integrations.ai.superagent.common.result.SuperAgentOpenApiTraceEvent;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import java.util.List;
|
||||
@@ -25,6 +26,7 @@ import java.util.Map;
|
||||
* @param superagentRunId SuperAgent run ID
|
||||
* @param superagentRawAnswer SuperAgent 原始最终回答
|
||||
* @param superagentParsedJson 后端解析出的 JSON
|
||||
* @param superagentTraceEvents SuperAgent 公开 Trace 事件列表
|
||||
* @param warnings 可展示的安全警告
|
||||
* @param status Debug 运行状态
|
||||
*/
|
||||
@@ -63,6 +65,8 @@ public record DebugEmlSuperAgentRunResult(
|
||||
String superagentRawAnswer,
|
||||
@JsonProperty("superagent_parsed_json")
|
||||
JsonNode superagentParsedJson,
|
||||
@JsonProperty("superagent_trace_events")
|
||||
List<SuperAgentOpenApiTraceEvent> superagentTraceEvents,
|
||||
List<String> warnings,
|
||||
String status
|
||||
) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package cn.nianxx.thhotel.platform.debug.control;
|
||||
import cn.nianxx.thhotel.platform.debug.common.result.DebugEmlSuperAgentRunResult;
|
||||
import cn.nianxx.thhotel.platform.debug.service.DebugEmlSuperAgentRunService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -12,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
|
||||
/**
|
||||
* Debug EML 上传 Controller。该入口只用于受控调试,不创建业务订单或任务。
|
||||
@@ -41,4 +43,23 @@ public class DebugEmlSuperAgentController {
|
||||
@RequestParam(name = "run_label", required = false) String runLabel) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(runService.uploadAndRun(accessKey, file, hotelId, runLabel));
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传单封 .eml 邮件并以 SSE 实时返回本系统阶段、SuperAgent 公开 Trace 和最终回答。
|
||||
*/
|
||||
@PostMapping(path = "/stream", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public ResponseEntity<StreamingResponseBody> uploadStream(
|
||||
@RequestHeader(name = "X-TH-Hotel-Debug-Upload-Key", required = false) String accessKey,
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(name = "hotel_id", required = false) String hotelId,
|
||||
@RequestParam(name = "run_label", required = false) String runLabel) {
|
||||
runService.validateUploadAccessKey(accessKey);
|
||||
StreamingResponseBody responseBody = outputStream ->
|
||||
runService.uploadAndRunStream(accessKey, file, hotelId, runLabel, outputStream);
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CACHE_CONTROL, "no-cache")
|
||||
.header("X-Accel-Buffering", "no")
|
||||
.contentType(MediaType.TEXT_EVENT_STREAM)
|
||||
.body(responseBody);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.nianxx.thhotel.platform.debug.service;
|
||||
|
||||
import cn.nianxx.thhotel.platform.debug.common.result.DebugEmlSuperAgentRunResult;
|
||||
import java.io.OutputStream;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
@@ -8,6 +9,11 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
*/
|
||||
public interface DebugEmlSuperAgentRunService {
|
||||
|
||||
/**
|
||||
* 校验 Debug EML 上传访问口令。流式入口需要在响应开始前完成校验,才能保留标准 HTTP 401。
|
||||
*/
|
||||
void validateUploadAccessKey(String accessKey);
|
||||
|
||||
/**
|
||||
* 上传并处理单封 EML,写入 SourceMessage 后调用 SuperAgent Open API。
|
||||
*/
|
||||
@@ -16,4 +22,14 @@ public interface DebugEmlSuperAgentRunService {
|
||||
MultipartFile file,
|
||||
String hotelId,
|
||||
String runLabel);
|
||||
|
||||
/**
|
||||
* 上传并处理单封 EML,按 text/event-stream 实时写出内部阶段、SuperAgent Trace 和最终结果。
|
||||
*/
|
||||
void uploadAndRunStream(
|
||||
String accessKey,
|
||||
MultipartFile file,
|
||||
String hotelId,
|
||||
String runLabel,
|
||||
OutputStream outputStream);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package cn.nianxx.thhotel.platform.debug.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.integrations.ai.superagent.common.request.SuperAgentMailDebugRequest;
|
||||
import cn.nianxx.thhotel.integrations.ai.superagent.common.result.SuperAgentOpenApiResult;
|
||||
import cn.nianxx.thhotel.integrations.ai.superagent.common.result.SuperAgentOpenApiTraceEvent;
|
||||
import cn.nianxx.thhotel.integrations.ai.superagent.service.SuperAgentOpenApiClient;
|
||||
import cn.nianxx.thhotel.integrations.ai.superagent.service.impl.SuperAgentOpenApiException;
|
||||
import cn.nianxx.thhotel.integrations.storage.aliyunoss.common.request.ObjectStoragePutRequest;
|
||||
@@ -34,6 +35,8 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -48,6 +51,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -106,6 +110,14 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
this.hotelContextService = hotelContextService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Debug EML 上传访问口令。供 Controller 在流式响应开始前复用,避免错误被写成 SSE 事件后丢失 HTTP 状态。
|
||||
*/
|
||||
@Override
|
||||
public void validateUploadAccessKey(String accessKey) {
|
||||
validateAccessKey(accessKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单封 Debug EML 上传;第一版只展示 SuperAgent 结果,不创建订单或任务。
|
||||
*/
|
||||
@@ -131,7 +143,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
DebugEmlSuperAgentRunStatus.CREATED.name(),
|
||||
now));
|
||||
try {
|
||||
return doUploadAndRun(runId, normalizedHotelId, trimToNull(runLabel), safeFileName, emlBytes, now);
|
||||
return doUploadAndRun(runId, normalizedHotelId, trimToNull(runLabel), safeFileName, emlBytes, now, null);
|
||||
} catch (DebugEmlSuperAgentException exception) {
|
||||
markFailed(runId, exception.getMessage(), statusForException(exception), nowUtc());
|
||||
throw exception;
|
||||
@@ -166,6 +178,80 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单封 Debug EML 上传,并实时输出安全的调试 SSE 事件。
|
||||
*/
|
||||
@Override
|
||||
public void uploadAndRunStream(
|
||||
String accessKey,
|
||||
MultipartFile file,
|
||||
String hotelId,
|
||||
String runLabel,
|
||||
OutputStream outputStream) {
|
||||
OutputStreamDebugEmlStreamSink streamSink = new OutputStreamDebugEmlStreamSink(outputStream, objectMapper);
|
||||
Long runId = null;
|
||||
try {
|
||||
validateAccessKey(accessKey);
|
||||
String normalizedHotelId = normalizeHotelId(hotelId);
|
||||
validateFile(file);
|
||||
byte[] emlBytes = readFileBytes(file);
|
||||
String safeFileName = safeFileName(file.getOriginalFilename(), "debug-email.eml");
|
||||
if (!safeFileName.toLowerCase(Locale.ROOT).endsWith(".eml")) {
|
||||
throw new DebugEmlSuperAgentException(HttpStatus.BAD_REQUEST, "INVALID_FILE_TYPE", "只支持上传 .eml 邮件文件。");
|
||||
}
|
||||
|
||||
LocalDateTime now = nowUtc();
|
||||
runId = runRepository.insert(new DebugEmlSuperAgentRunDraft(
|
||||
normalizedHotelId,
|
||||
trimToNull(runLabel),
|
||||
DebugEmlSuperAgentRunStatus.CREATED.name(),
|
||||
now));
|
||||
DebugEmlSuperAgentRunResult result = doUploadAndRun(
|
||||
runId,
|
||||
normalizedHotelId,
|
||||
trimToNull(runLabel),
|
||||
safeFileName,
|
||||
emlBytes,
|
||||
now,
|
||||
streamSink);
|
||||
streamSink.result(result);
|
||||
streamSink.done();
|
||||
} catch (DebugEmlSuperAgentException exception) {
|
||||
if (runId != null) {
|
||||
markFailed(runId, exception.getMessage(), statusForException(exception), nowUtc());
|
||||
}
|
||||
streamSink.error(runId, exception.getErrorCode(), exception.getMessage(), statusForException(exception).name());
|
||||
} catch (EmlMessageParseException exception) {
|
||||
if (runId != null) {
|
||||
markFailed(runId, "EML 邮件解析失败。", DebugEmlSuperAgentRunStatus.FAILED, nowUtc());
|
||||
}
|
||||
streamSink.error(runId, "EML_PARSE_FAILED", "EML 邮件解析失败。", DebugEmlSuperAgentRunStatus.FAILED.name());
|
||||
} catch (ObjectStorageException exception) {
|
||||
if (runId != null) {
|
||||
markFailed(runId, "OSS 上传失败。", DebugEmlSuperAgentRunStatus.FAILED, nowUtc());
|
||||
}
|
||||
streamSink.error(runId, "OSS_UPLOAD_FAILED", "OSS 上传失败。", DebugEmlSuperAgentRunStatus.FAILED.name());
|
||||
} catch (SuperAgentOpenApiException exception) {
|
||||
if (runId != null) {
|
||||
markFailed(runId, superAgentFailureSummary(exception), DebugEmlSuperAgentRunStatus.SUPERAGENT_FAILED, nowUtc());
|
||||
}
|
||||
streamSink.error(
|
||||
runId,
|
||||
"SUPERAGENT_OPEN_API_FAILED",
|
||||
"SuperAgent 调用失败。",
|
||||
DebugEmlSuperAgentRunStatus.SUPERAGENT_FAILED.name());
|
||||
} catch (Exception exception) {
|
||||
if (runId != null) {
|
||||
markFailed(runId, "Debug EML 上传处理失败。", DebugEmlSuperAgentRunStatus.FAILED, nowUtc());
|
||||
}
|
||||
streamSink.error(
|
||||
runId,
|
||||
"DEBUG_EML_RUN_FAILED",
|
||||
"Debug EML 上传处理失败。",
|
||||
DebugEmlSuperAgentRunStatus.FAILED.name());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Debug 上传酒店上下文。第一版可不传 hotel_id,由单酒店系统上下文兜底。
|
||||
*/
|
||||
@@ -250,12 +336,14 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
String runLabel,
|
||||
String safeFileName,
|
||||
byte[] emlBytes,
|
||||
LocalDateTime createdAt) throws Exception {
|
||||
LocalDateTime createdAt,
|
||||
DebugEmlSuperAgentStreamSink streamSink) throws Exception {
|
||||
String sha256 = sha256(emlBytes);
|
||||
markStage(
|
||||
runId,
|
||||
DebugEmlSuperAgentRunStatus.PARSING_EML,
|
||||
"阶段:解析 EML 邮件,文件名:" + safeFileName + ",大小:" + emlBytes.length + " bytes。");
|
||||
"阶段:解析 EML 邮件,文件名:" + safeFileName + ",大小:" + emlBytes.length + " bytes。",
|
||||
streamSink);
|
||||
ParsedEmlMessage parsed = parseService.parse(emlBytes, safeFileName);
|
||||
String originalMessageId = parsed.messageId();
|
||||
String originalConversationId = parsed.conversationId();
|
||||
@@ -267,7 +355,8 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
markStage(
|
||||
runId,
|
||||
DebugEmlSuperAgentRunStatus.UPLOADING_ORIGINAL_EML,
|
||||
"阶段:上传原始 EML 到 OSS,文件名:" + safeFileName + ",大小:" + emlBytes.length + " bytes。");
|
||||
"阶段:上传原始 EML 到 OSS,文件名:" + safeFileName + ",大小:" + emlBytes.length + " bytes。",
|
||||
streamSink);
|
||||
uploadedMedia.add(uploadOriginalEml(runId, safeFileName, emlBytes, createdAt));
|
||||
int inlineIndex = 1;
|
||||
int attachmentIndex = 1;
|
||||
@@ -276,13 +365,15 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
markStage(
|
||||
runId,
|
||||
DebugEmlSuperAgentRunStatus.UPLOADING_MEDIA,
|
||||
mediaUploadStageSummary(mediaItem, "inline", inlineIndex));
|
||||
mediaUploadStageSummary(mediaItem, "inline", inlineIndex),
|
||||
streamSink);
|
||||
uploadedMedia.add(uploadParsedMedia(runId, createdAt, mediaItem, "inline", inlineIndex++));
|
||||
} else {
|
||||
markStage(
|
||||
runId,
|
||||
DebugEmlSuperAgentRunStatus.UPLOADING_MEDIA,
|
||||
mediaUploadStageSummary(mediaItem, "attachments", attachmentIndex));
|
||||
mediaUploadStageSummary(mediaItem, "attachments", attachmentIndex),
|
||||
streamSink);
|
||||
uploadedMedia.add(uploadParsedMedia(runId, createdAt, mediaItem, "attachments", attachmentIndex++));
|
||||
}
|
||||
}
|
||||
@@ -290,7 +381,8 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
markStage(
|
||||
runId,
|
||||
DebugEmlSuperAgentRunStatus.BUILDING_SOURCE_MESSAGE,
|
||||
"阶段:构造 SourceMessage payload。");
|
||||
"阶段:构造 SourceMessage payload。",
|
||||
streamSink);
|
||||
String htmlWithOssUrls = replaceCidReferences(parsed.htmlBody(), uploadedMedia, warnings);
|
||||
String htmlBodySanitized = htmlSanitizerService.sanitizeHtml(htmlWithOssUrls);
|
||||
String htmlRenderMode = htmlSanitizerService.htmlRenderMode(htmlWithOssUrls);
|
||||
@@ -308,7 +400,8 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
markStage(
|
||||
runId,
|
||||
DebugEmlSuperAgentRunStatus.CAPTURING_SOURCE_MESSAGE,
|
||||
"阶段:写入 SourceMessage Inbox。");
|
||||
"阶段:写入 SourceMessage Inbox。",
|
||||
streamSink);
|
||||
SourceMessageCaptureResult captureResult = sourceMessageCaptureService.capture(new CaptureSourceMessageCommand(
|
||||
hotelId,
|
||||
SOURCE_PROVIDER,
|
||||
@@ -338,15 +431,29 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
markStage(
|
||||
runId,
|
||||
DebugEmlSuperAgentRunStatus.CALLING_SUPERAGENT,
|
||||
"阶段:调用 SuperAgent Open API,source_message_id=" + captureResult.inboxId() + "。");
|
||||
SuperAgentOpenApiResult superAgentResult = superAgentOpenApiClient.invokeMailDebug(new SuperAgentMailDebugRequest(
|
||||
"阶段:调用 SuperAgent Open API,source_message_id=" + captureResult.inboxId() + "。",
|
||||
streamSink);
|
||||
SuperAgentMailDebugRequest superAgentRequest = new SuperAgentMailDebugRequest(
|
||||
buildSuperAgentMessage(payloadJson),
|
||||
"debug-eml-" + runId,
|
||||
Map.of(
|
||||
"source", "th-hotel-debug-eml-upload",
|
||||
"debug_run_id", runId.toString(),
|
||||
"source_message_id", captureResult.inboxId().toString(),
|
||||
"hotel_id", hotelId)));
|
||||
"hotel_id", hotelId));
|
||||
List<SuperAgentOpenApiTraceEvent> streamedTraceEvents = new ArrayList<>();
|
||||
Consumer<SuperAgentOpenApiTraceEvent> traceConsumer = traceEvent -> {
|
||||
streamedTraceEvents.add(traceEvent);
|
||||
if (streamSink != null) {
|
||||
streamSink.trace(traceEvent);
|
||||
}
|
||||
};
|
||||
SuperAgentOpenApiResult superAgentResult = streamSink == null
|
||||
? superAgentOpenApiClient.invokeMailDebug(superAgentRequest)
|
||||
: superAgentOpenApiClient.invokeMailDebug(superAgentRequest, traceConsumer);
|
||||
List<SuperAgentOpenApiTraceEvent> traceEvents = streamedTraceEvents.isEmpty()
|
||||
? superAgentResult.traceEvents()
|
||||
: List.copyOf(streamedTraceEvents);
|
||||
JsonNode parsedJson = parseSuperAgentJson(superAgentResult.rawAnswer(), warnings);
|
||||
String parsedJsonText = parsedJson == null ? null : objectMapper.writeValueAsString(parsedJson);
|
||||
DebugEmlSuperAgentRunStatus status = DebugEmlSuperAgentRunStatus.SUPERAGENT_SUCCEEDED;
|
||||
@@ -391,6 +498,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
superAgentResult.runId(),
|
||||
superAgentResult.rawAnswer(),
|
||||
parsedJson,
|
||||
traceEvents,
|
||||
List.copyOf(warnings),
|
||||
status.name());
|
||||
}
|
||||
@@ -693,14 +801,29 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
Long runId,
|
||||
DebugEmlSuperAgentRunStatus status,
|
||||
String safeSummary) {
|
||||
markStage(runId, status, safeSummary, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记 Debug 运行中的当前阶段,并在流式调试入口实时输出阶段事件。
|
||||
*/
|
||||
private void markStage(
|
||||
Long runId,
|
||||
DebugEmlSuperAgentRunStatus status,
|
||||
String safeSummary,
|
||||
DebugEmlSuperAgentStreamSink streamSink) {
|
||||
if (runId == null) {
|
||||
return;
|
||||
}
|
||||
String truncatedSummary = truncate(safeSummary, 512);
|
||||
runRepository.updateStatus(new DebugEmlSuperAgentRunStatusUpdate(
|
||||
runId,
|
||||
status.name(),
|
||||
truncate(safeSummary, 512),
|
||||
truncatedSummary,
|
||||
nowUtc()));
|
||||
if (streamSink != null) {
|
||||
streamSink.stage(runId, status, truncatedSummary);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -912,6 +1035,106 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
return LocalDateTime.now(ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug EML 流式事件输出端口,避免主流程依赖 HTTP 细节。
|
||||
*/
|
||||
private interface DebugEmlSuperAgentStreamSink {
|
||||
|
||||
/**
|
||||
* 输出本系统处理阶段。
|
||||
*/
|
||||
void stage(Long runId, DebugEmlSuperAgentRunStatus status, String safeSummary);
|
||||
|
||||
/**
|
||||
* 输出 SuperAgent 公开 Trace 事件。
|
||||
*/
|
||||
void trace(SuperAgentOpenApiTraceEvent traceEvent);
|
||||
|
||||
/**
|
||||
* 输出最终 Debug 结果。
|
||||
*/
|
||||
void result(DebugEmlSuperAgentRunResult result);
|
||||
|
||||
/**
|
||||
* 输出安全错误事件。
|
||||
*/
|
||||
void error(Long runId, String errorCode, String message, String status);
|
||||
|
||||
/**
|
||||
* 输出流结束事件。
|
||||
*/
|
||||
void done();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Debug EML 事件写成 text/event-stream。
|
||||
*/
|
||||
private static final class OutputStreamDebugEmlStreamSink implements DebugEmlSuperAgentStreamSink {
|
||||
|
||||
private final OutputStream outputStream;
|
||||
private final ObjectMapper objectMapper;
|
||||
private boolean active = true;
|
||||
|
||||
private OutputStreamDebugEmlStreamSink(OutputStream outputStream, ObjectMapper objectMapper) {
|
||||
this.outputStream = outputStream;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stage(Long runId, DebugEmlSuperAgentRunStatus status, String safeSummary) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("debug_run_id", runId == null ? null : runId.toString());
|
||||
data.put("status", status.name());
|
||||
data.put("safe_summary", safeSummary);
|
||||
send("debug_stage", data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trace(SuperAgentOpenApiTraceEvent traceEvent) {
|
||||
send("superagent_trace", traceEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void result(DebugEmlSuperAgentRunResult result) {
|
||||
send("superagent_result", result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(Long runId, String errorCode, String message, String status) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("debug_run_id", runId == null ? null : runId.toString());
|
||||
data.put("error_code", errorCode);
|
||||
data.put("message", message);
|
||||
data.put("status", status);
|
||||
send("debug_error", data);
|
||||
done();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void done() {
|
||||
send("done", Map.of("status", "done"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 写出一个 SSE 事件块并立即 flush。写失败通常表示客户端断开,只关闭流输出,不影响后续业务落库。
|
||||
*/
|
||||
private void send(String eventName, Object data) {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String eventBlock = "event: " + eventName
|
||||
+ "\n"
|
||||
+ "data: " + objectMapper.writeValueAsString(data)
|
||||
+ "\n\n";
|
||||
outputStream.write(eventBlock.getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.flush();
|
||||
} catch (IOException exception) {
|
||||
active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传媒体内部组合对象。
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user