实现 Booking Excel 预处理接入 SuperAgent

This commit is contained in:
andy
2026-07-20 00:32:01 +07:00
parent b7b04cf1ac
commit d1955f5097
45 changed files with 2955 additions and 10 deletions

View File

@@ -1,6 +1,8 @@
package cn.nianxx.thhotel.platform.debug.common.result;
import cn.nianxx.thhotel.integrations.ai.superagent.common.result.SuperAgentOpenApiTraceEvent;
import cn.nianxx.thhotel.workflows.reservation.excelimport.common.result.BookingExcelAttachmentExtractionResult;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.List;
@@ -22,6 +24,7 @@ import java.util.Map;
* @param htmlSanitizeRequired 是否要求前端按安全 HTML 规则展示
* @param htmlRenderMode HTML 渲染建议模式
* @param agentbusLikePayload 发送给 SuperAgent 的 AgentBus Outlook-like 主 payload
* @param attachmentExtractions Booking Excel 附件预处理安全预览
* @param superagentSessionId SuperAgent session ID
* @param superagentRunId SuperAgent run ID
* @param superagentRawAnswer SuperAgent 原始最终回答
@@ -58,6 +61,9 @@ public record DebugEmlSuperAgentRunResult(
String htmlRenderMode,
@JsonProperty("agentbus_like_payload")
Map<String, Object> agentbusLikePayload,
@JsonProperty("attachment_extractions")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
List<BookingExcelAttachmentExtractionResult> attachmentExtractions,
@JsonProperty("superagent_session_id")
String superagentSessionId,
@JsonProperty("superagent_run_id")

View File

@@ -19,6 +19,8 @@ public class DebugEmlSuperAgentProperties {
private long maxFileBytes = 10 * 1024 * 1024L;
/** Debug SSE 心跳间隔,用于避免长时间调用 SuperAgent 时中间链路空闲断开。 */
private Duration sseHeartbeatInterval = Duration.ofSeconds(15);
/** 是否在 Debug EML 调用 SuperAgent 前追加 Booking Excel 附件预处理结果。 */
private boolean includeBookingExcelExtractions = false;
public boolean isEnabled() {
return enabled;
@@ -51,4 +53,12 @@ public class DebugEmlSuperAgentProperties {
public void setSseHeartbeatInterval(Duration sseHeartbeatInterval) {
this.sseHeartbeatInterval = sseHeartbeatInterval;
}
public boolean isIncludeBookingExcelExtractions() {
return includeBookingExcelExtractions;
}
public void setIncludeBookingExcelExtractions(boolean includeBookingExcelExtractions) {
this.includeBookingExcelExtractions = includeBookingExcelExtractions;
}
}

View File

@@ -35,6 +35,9 @@ 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 cn.nianxx.thhotel.workflows.reservation.excelimport.common.request.BookingExcelAttachmentExtractionRequest;
import cn.nianxx.thhotel.workflows.reservation.excelimport.common.result.BookingExcelAttachmentExtractionResult;
import cn.nianxx.thhotel.workflows.reservation.excelimport.service.ReservationBookingExcelAttachmentExtractionService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
@@ -49,6 +52,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
@@ -84,6 +88,9 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
private static final String SOURCE_CHANNEL = "EMAIL";
private static final String REPLY_POLICY_MODE = "manual";
private static final String SCHEMA_VERSION = "debug-eml-upload-v1";
private static final String PAYLOAD_FIELD_ATTACHMENT_EXTRACTIONS = "attachment_extractions";
private static final String BOOKING_EXCEL_EXTRACTION_DISABLED_REASON = "BOOKING_EXCEL_EXTRACTION_DISABLED";
private static final ZoneId DEFAULT_HOTEL_ZONE = ZoneId.of("Asia/Bangkok");
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\"'>]+)");
@@ -103,6 +110,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
private final EmlMessageParseService parseService;
private final ObjectStorageService objectStorageService;
private final SourceMessageCaptureService sourceMessageCaptureService;
private final ReservationBookingExcelAttachmentExtractionService bookingExcelExtractionService;
private final SourceMessageInboxRepository sourceMessageInboxRepository;
private final SourceMessageHtmlSanitizerService htmlSanitizerService;
private final SuperAgentOpenApiClient superAgentOpenApiClient;
@@ -119,6 +127,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
EmlMessageParseService parseService,
ObjectStorageService objectStorageService,
SourceMessageCaptureService sourceMessageCaptureService,
ReservationBookingExcelAttachmentExtractionService bookingExcelExtractionService,
SourceMessageInboxRepository sourceMessageInboxRepository,
SourceMessageHtmlSanitizerService htmlSanitizerService,
SuperAgentOpenApiClient superAgentOpenApiClient,
@@ -130,6 +139,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
this.parseService = parseService;
this.objectStorageService = objectStorageService;
this.sourceMessageCaptureService = sourceMessageCaptureService;
this.bookingExcelExtractionService = bookingExcelExtractionService;
this.sourceMessageInboxRepository = sourceMessageInboxRepository;
this.htmlSanitizerService = htmlSanitizerService;
this.superAgentOpenApiClient = superAgentOpenApiClient;
@@ -486,7 +496,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
parsed,
htmlWithOssUrls,
uploadedMedia);
String payloadJson = objectMapper.writeValueAsString(payload);
String sourcePayloadJson = objectMapper.writeValueAsString(payload);
markStage(
runId,
DebugEmlSuperAgentRunStatus.CAPTURING_SOURCE_MESSAGE,
@@ -505,9 +515,13 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
parsed.subject(),
parsed.textBody(),
htmlWithOssUrls,
payloadJson,
sourcePayloadJson,
SCHEMA_VERSION,
uploadedMedia.stream().map(UploadedMedia::captureMedia).toList()));
List<BookingExcelAttachmentExtractionResult> attachmentExtractions =
extractBookingExcelAttachments(runId, parsed.mediaItems(), createdAt, warnings);
Map<String, Object> superAgentPayload = payloadWithAttachmentExtractions(payload, attachmentExtractions);
String superAgentPayloadJson = objectMapper.writeValueAsString(superAgentPayload);
updateSourceCapturedRun(
runId,
captureResult.inboxId(),
@@ -516,7 +530,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
safeFileName,
uploadedMedia.get(0).result().externalUrl(),
sha256,
payloadJson);
superAgentPayloadJson);
markStage(
runId,
@@ -524,7 +538,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
"阶段:调用 SuperAgent Open APIsource_message_id=" + captureResult.inboxId() + "",
streamSink);
SuperAgentMailDebugRequest superAgentRequest = new SuperAgentMailDebugRequest(
buildSuperAgentMessage(payloadJson),
buildSuperAgentMessage(superAgentPayloadJson),
"debug-eml-" + runId,
buildSuperAgentMetadata(
runId,
@@ -561,7 +575,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
safeFileName,
uploadedMedia.get(0).result().externalUrl(),
sha256,
payloadJson,
superAgentPayloadJson,
superAgentResult.sessionId(),
superAgentResult.runId(),
superAgentResult.profileId(),
@@ -589,7 +603,8 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
htmlBodySanitized,
true,
htmlRenderMode,
payload,
superAgentPayload,
attachmentExtractions,
superAgentResult.sessionId(),
superAgentResult.runId(),
superAgentResult.rawAnswer(),
@@ -607,6 +622,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
SourceMessageOriginalContent originalContent = loadOriginalContent(snapshot.sourceMessageId());
String htmlBodyWithOssUrls = originalContent == null ? null : originalContent.htmlBody();
String htmlBodySanitized = htmlBodyWithOssUrls == null ? null : htmlSanitizerService.sanitizeHtml(htmlBodyWithOssUrls);
Map<String, Object> payload = parsePayloadJson(snapshot.payloadJson());
return new DebugEmlSuperAgentRunResult(
snapshot.id().toString(),
snapshot.sourceMessageId() == null ? null : snapshot.sourceMessageId().toString(),
@@ -620,7 +636,8 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
htmlBodySanitized,
originalContent == null ? null : true,
originalContent == null ? null : htmlSanitizerService.htmlRenderMode(htmlBodyWithOssUrls),
parsePayloadJson(snapshot.payloadJson()),
payload,
attachmentExtractionsFromPayload(payload),
snapshot.superagentSessionId(),
snapshot.superagentRunId(),
snapshot.superagentRawAnswer(),
@@ -708,6 +725,99 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
}
}
/**
* 从 Debug EML 附件字节中抽取 Booking Excel 高亮行;该结果只追加给 Debug SuperAgent 输入和调试响应。
*/
private List<BookingExcelAttachmentExtractionResult> extractBookingExcelAttachments(
Long runId,
List<ParsedEmlMediaItem> mediaItems,
LocalDateTime createdAt,
List<String> warnings) {
if (!properties.isIncludeBookingExcelExtractions() || mediaItems == null || mediaItems.isEmpty()) {
return List.of();
}
List<BookingExcelAttachmentExtractionResult> results = new ArrayList<>();
for (ParsedEmlMediaItem mediaItem : mediaItems) {
if (!SourceMessageMediaType.ATTACHMENT.code().equals(mediaItem.mediaType())) {
continue;
}
String attachmentName = safeFileName(mediaItem.fileName(), "attachment-" + (results.size() + 1));
if (!isExcelAttachmentName(attachmentName)) {
continue;
}
try {
byte[] content = mediaItem.bytes();
Long sizeBytes = mediaItem.sizeBytes() == null && content != null
? (long) content.length
: mediaItem.sizeBytes();
BookingExcelAttachmentExtractionResult extractionResult =
bookingExcelExtractionService.extract(new BookingExcelAttachmentExtractionRequest(
attachmentName,
mediaItem.contentType(),
sizeBytes,
content,
createdAt.atOffset(ZoneOffset.UTC).toInstant(),
DEFAULT_HOTEL_ZONE,
null,
null));
if (!BOOKING_EXCEL_EXTRACTION_DISABLED_REASON.equals(extractionResult.skippedReason())) {
results.add(extractionResult);
}
} catch (RuntimeException exception) {
warnings.add("Booking Excel 附件预处理失败:" + attachmentName);
log.warn(
"Debug EML Booking Excel extraction failed. debug_run_id={}, attachment_name={}, exception_chain={}",
runId,
attachmentName,
safeExceptionChain(exception));
}
}
return List.copyOf(results);
}
/**
* 将附件预处理结果并入发给 SuperAgent 的 payload无结果时沿用原始 payload。
*/
private Map<String, Object> payloadWithAttachmentExtractions(
Map<String, Object> payload,
List<BookingExcelAttachmentExtractionResult> attachmentExtractions) {
if (attachmentExtractions == null || attachmentExtractions.isEmpty()) {
return payload;
}
Map<String, Object> enrichedPayload = new LinkedHashMap<>(payload);
enrichedPayload.put(PAYLOAD_FIELD_ATTACHMENT_EXTRACTIONS, attachmentExtractions);
return enrichedPayload;
}
/**
* 查询历史 Debug run 时,从已保存的 payload 中还原附件预处理结果。
*/
private List<BookingExcelAttachmentExtractionResult> attachmentExtractionsFromPayload(Map<String, Object> payload) {
if (payload == null || !payload.containsKey(PAYLOAD_FIELD_ATTACHMENT_EXTRACTIONS)) {
return List.of();
}
Object value = payload.get(PAYLOAD_FIELD_ATTACHMENT_EXTRACTIONS);
if (!(value instanceof List<?>)) {
return List.of();
}
try {
return objectMapper.convertValue(
value,
new TypeReference<List<BookingExcelAttachmentExtractionResult>>() {
});
} catch (IllegalArgumentException exception) {
return List.of();
}
}
/**
* 判断附件名是否为可预处理的 Excel 文件。
*/
private boolean isExcelAttachmentName(String attachmentName) {
String lowerName = attachmentName == null ? "" : attachmentName.toLowerCase(Locale.ROOT);
return lowerName.endsWith(".xlsx") || lowerName.endsWith(".xls");
}
/**
* 解析已入库的 SuperAgent JSON解析失败返回 null前端仍可查看 raw answer。
*/