实现 Booking Excel 预处理接入 SuperAgent
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 API,source_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。
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user