debug和superagent提交的数据保持一致

This commit is contained in:
andy
2026-07-13 17:03:27 +08:00
parent c55090bc2e
commit 9a2812fe74
11 changed files with 191 additions and 86 deletions

View File

@@ -14,7 +14,7 @@ import java.time.LocalDateTime;
* @param originalFileName 原始 EML 安全文件名
* @param originalEmlOssUrl 原始 EML OSS URL
* @param originalEmlSha256 原始 EML SHA-256
* @param payloadJson 发送给 SuperAgent 的 AgentBus-like payload
* @param payloadJson 发送给 SuperAgent 的 AgentBus Outlook-like payload
* @param superagentSessionId SuperAgent session ID
* @param superagentRunId SuperAgent run ID
* @param superagentProfileId SuperAgent profile ID

View File

@@ -12,7 +12,7 @@ import java.time.LocalDateTime;
* @param originalFileName 原始上传文件安全文件名
* @param originalEmlOssUrl 原始 .eml OSS URL
* @param originalEmlSha256 原始 .eml SHA-256
* @param payloadJson AgentBus-like payload JSON
* @param payloadJson AgentBus Outlook-like payload JSON
* @param superagentSessionId SuperAgent session ID
* @param superagentRunId SuperAgent run ID
* @param superagentProfileId SuperAgent profile ID

View File

@@ -21,7 +21,7 @@ import java.util.Map;
* @param htmlBodySanitized 后端清洗后的 HTML前端调试展示应优先使用
* @param htmlSanitizeRequired 是否要求前端按安全 HTML 规则展示
* @param htmlRenderMode HTML 渲染建议模式
* @param agentbusLikePayload 发送给 SuperAgent 的结构化 payload
* @param agentbusLikePayload 发送给 SuperAgent 的 AgentBus Outlook-like 主 payload
* @param superagentSessionId SuperAgent session ID
* @param superagentRunId SuperAgent run ID
* @param superagentRawAnswer SuperAgent 原始最终回答

View File

@@ -30,7 +30,7 @@ public class DebugEmlSuperAgentRunEntity {
private String originalEmlOssUrl;
/** 原始 .eml 文件 SHA-256。 */
private String originalEmlSha256;
/** AgentBus-like payload JSON。 */
/** 发送给 SuperAgent 的 AgentBus Outlook-like payload JSON。 */
private String payloadJson;
/** SuperAgent Open API session ID。 */
private String superagentSessionId;

View File

@@ -82,6 +82,7 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
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 REPLY_POLICY_MODE = "manual";
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(
@@ -479,12 +480,9 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
String htmlBodySanitized = htmlSanitizerService.sanitizeHtml(htmlWithOssUrls);
String htmlRenderMode = htmlSanitizerService.htmlRenderMode(htmlWithOssUrls);
Map<String, Object> payload = buildAgentBusLikePayload(
runId,
runLabel,
createdAt,
externalMessageId,
externalConversationId,
originalMessageId,
originalConversationId,
parsed,
htmlWithOssUrls,
uploadedMedia);
@@ -528,11 +526,15 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
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));
buildSuperAgentMetadata(
runId,
runLabel,
captureResult.inboxId(),
hotelId,
externalMessageId,
externalConversationId,
originalMessageId,
originalConversationId));
List<SuperAgentOpenApiTraceEvent> streamedTraceEvents = new ArrayList<>();
Consumer<SuperAgentOpenApiTraceEvent> traceConsumer = traceEvent -> {
streamedTraceEvents.add(traceEvent);
@@ -919,28 +921,24 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
}
/**
* 组装接近 AgentBus 邮件输入的 payload。
* 组装发给 SuperAgent 的 AgentBus Outlook 邮件 payload。调试来源信息放入 Open API metadata
* 避免 DEBUG_EML_UPLOAD 等调试语义影响 SuperAgent 对实时 AgentBus 邮件的判断。
*/
private Map<String, Object> buildAgentBusLikePayload(
Long runId,
String runLabel,
LocalDateTime createdAt,
String externalMessageId,
String externalConversationId,
String originalMessageId,
String originalConversationId,
ParsedEmlMessage parsed,
String htmlWithOssUrls,
List<UploadedMedia> uploadedMedia) {
Map<String, Object> source = new LinkedHashMap<>();
source.put("channel", SOURCE_CHANNEL);
source.put("provider", SOURCE_PROVIDER);
source.put("external_message_id", externalMessageId);
source.put("external_conversation_id", externalConversationId);
source.put("original_message_id", originalMessageId);
source.put("original_conversation_id", originalConversationId);
source.put("sender", parsed.sender());
source.put("subject", parsed.subject());
source.put("sent_at", parsed.sentAt() == null ? null : parsed.sentAt().toString());
source.put("received_at", utcInstantText(createdAt));
Map<String, Object> body = new LinkedHashMap<>();
body.put("content_type", contentType(parsed, htmlWithOssUrls));
@@ -950,26 +948,56 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
List<Map<String, Object>> inlineImages = mediaPayload(uploadedMedia, SourceMessageMediaType.INLINE_IMAGE.code());
List<Map<String, Object>> attachments = mediaPayload(uploadedMedia, SourceMessageMediaType.ATTACHMENT.code());
Map<String, Object> replyPolicy = new LinkedHashMap<>();
replyPolicy.put("mode", "debug_only");
replyPolicy.put("final_only", true);
Map<String, Object> debugContext = new LinkedHashMap<>();
debugContext.put("debug_run_id", runId.toString());
debugContext.put("run_label", runLabel);
debugContext.put("original_eml_oss_url", uploadedMedia.get(0).result().externalUrl());
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("schema_version", SCHEMA_VERSION);
payload.put("text", parsed.textBody());
payload.put("received_at", utcInstantText(createdAt));
payload.put("source", source);
payload.put("body", body);
payload.put("inline_images", inlineImages);
payload.put("attachments", attachments);
payload.put("reply_policy", replyPolicy);
payload.put("debug_context", debugContext);
payload.put("reply_policy", replyPolicy());
return payload;
}
/**
* 构造 AgentBus Outlook 关键字段中的回复策略。Debug 链路第一版不自动回复客户,使用非 Debug 专属的人工处理语义。
*/
private Map<String, Object> replyPolicy() {
Map<String, Object> replyPolicy = new LinkedHashMap<>();
replyPolicy.put("mode", REPLY_POLICY_MODE);
replyPolicy.put("final_only", true);
return replyPolicy;
}
/**
* 构造 Debug EML 调用 SuperAgent 的 metadata。这里保存调试来源和原始 EML 标识,不混入邮件主 payload。
*/
private Map<String, Object> buildSuperAgentMetadata(
Long runId,
String runLabel,
Long sourceMessageId,
String hotelId,
String externalMessageId,
String externalConversationId,
String originalMessageId,
String originalConversationId) {
Map<String, Object> debugContext = new LinkedHashMap<>();
putIfNotNull(debugContext, "debug_run_id", runId == null ? null : runId.toString());
putIfNotNull(debugContext, "run_label", runLabel);
putIfNotNull(debugContext, "original_message_id", originalMessageId);
putIfNotNull(debugContext, "original_conversation_id", originalConversationId);
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("source", "th-hotel-debug-eml-upload");
putIfNotNull(metadata, "debug_run_id", runId == null ? null : runId.toString());
putIfNotNull(metadata, "source_message_id", sourceMessageId == null ? null : sourceMessageId.toString());
putIfNotNull(metadata, "hotel_id", hotelId);
putIfNotNull(metadata, "external_message_id", externalMessageId);
putIfNotNull(metadata, "external_conversation_id", externalConversationId);
metadata.put("debug_context", debugContext);
return metadata;
}
/**
* 构造媒体 payload 数组。
*/
@@ -978,13 +1006,12 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
.filter(item -> mediaType.equals(item.result().mediaType()))
.map(item -> {
Map<String, Object> media = new LinkedHashMap<>();
media.put("id", item.result().externalMediaId());
media.put("media_type", item.result().mediaType());
media.put("file_name", item.result().fileName());
media.put("content_type", item.result().contentType());
media.put("size_bytes", item.result().sizeBytes());
media.put("external_url", item.result().externalUrl());
media.put("external_media_id", item.result().externalMediaId());
media.put("object_key", item.result().objectKey());
if (item.contentId() != null) {
media.put("content_id", item.contentId());
}
@@ -1000,16 +1027,21 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
boolean hasText = parsed.textBody() != null && !parsed.textBody().isBlank();
boolean hasHtml = htmlWithOssUrls != null && !htmlWithOssUrls.isBlank();
if (hasText && hasHtml) {
return "MIXED";
return "mixed";
}
return hasHtml ? "HTML" : "TEXT";
return hasHtml ? "html" : "text";
}
/**
* 构造发送给 SuperAgent 的消息文本。
*/
private String buildSuperAgentMessage(String payloadJson) {
return "请基于以下 Debug 邮件 JSON 输出结构化任务抽取结果,只返回 JSON不要创建订单或任务\n" + payloadJson;
return """
请基于以下 AgentBus Outlook 邮件 JSON 输出结构化任务抽取结果。
只返回 SuperAgent 当前约定的 JSON 或 S10/S99 特殊结果不要创建订单、任务、OPERA 操作或客户回复。
AgentBus Outlook 邮件 JSON
""" + payloadJson;
}
/**
@@ -1282,6 +1314,25 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
return trimmed.isEmpty() ? null : trimmed;
}
/**
* 向 metadata 写入非空字段,避免 Map.of 不能容纳 null也避免输出无意义空值。
*/
private void putIfNotNull(Map<String, Object> target, String key, Object value) {
if (value != null) {
target.put(key, value);
}
}
/**
* 将数据库 UTC LocalDateTime 转为带 Z 的 ISO-8601 时间点。
*/
private String utcInstantText(LocalDateTime value) {
if (value == null) {
return null;
}
return value.atOffset(ZoneOffset.UTC).toInstant().toString();
}
/**
* 去掉 URL 末尾斜杠,便于用公开基础 URL 还原对象路径。
*/

View File

@@ -6,6 +6,7 @@ import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.not;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
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;
@@ -16,6 +17,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import cn.nianxx.thhotel.ThHotelApplication;
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;
@@ -33,6 +35,7 @@ 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.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
@@ -139,11 +142,18 @@ class DebugEmlSuperAgentControllerTest {
.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("$.agentbus_like_payload.schema_version").value("debug-eml-upload-v1"))
.andExpect(jsonPath("$.agentbus_like_payload.source.provider").value("DEBUG_EML_UPLOAD"))
.andExpect(jsonPath("$.agentbus_like_payload.source.original_message_id")
.value("debug-controller-message-001@example.test"))
.andExpect(jsonPath("$.agentbus_like_payload.reply_policy.mode").value("debug_only"))
.andExpect(jsonPath("$.agentbus_like_payload.text",
containsString("Please create booking from debug email.")))
.andExpect(jsonPath("$.agentbus_like_payload.body.html", containsString("https://oss.example.test/")))
.andExpect(jsonPath("$.agentbus_like_payload.source.channel").value("EMAIL"))
.andExpect(jsonPath("$.agentbus_like_payload.source.external_message_id",
containsString("debug-eml-run-")))
.andExpect(jsonPath("$.agentbus_like_payload.schema_version").doesNotExist())
.andExpect(jsonPath("$.agentbus_like_payload.source.provider").doesNotExist())
.andExpect(jsonPath("$.agentbus_like_payload.source.original_message_id").doesNotExist())
.andExpect(jsonPath("$.agentbus_like_payload.reply_policy.mode").value("manual"))
.andExpect(jsonPath("$.agentbus_like_payload.reply_policy.final_only").value(true))
.andExpect(jsonPath("$.agentbus_like_payload.debug_context").doesNotExist())
.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")))
@@ -153,13 +163,15 @@ class DebugEmlSuperAgentControllerTest {
Long sourceCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM platform_source_message_inbox inbox
FROM platform_debug_eml_superagent_run run
JOIN platform_source_message_inbox inbox ON inbox.id = run.source_message_id
JOIN platform_source_message_payload payload ON payload.inbox_id = inbox.id
WHERE inbox.hotel_id = 'HOTEL-TEST'
AND inbox.provider = 'DEBUG_EML_UPLOAD'
AND inbox.channel = 'EMAIL'
AND payload.schema_version = 'debug-eml-upload-v1'
AND payload.payload_json LIKE '%controller-test%'
AND inbox.external_message_id LIKE 'debug-eml-run-%'
AND run.run_label = 'controller-test'
""", Long.class);
org.assertj.core.api.Assertions.assertThat(sourceCount).isEqualTo(1L);
@@ -167,13 +179,35 @@ class DebugEmlSuperAgentControllerTest {
SELECT COUNT(*)
FROM platform_source_message_media media
JOIN platform_source_message_inbox inbox ON inbox.id = media.inbox_id
JOIN platform_debug_eml_superagent_run run ON run.source_message_id = inbox.id
JOIN platform_source_message_payload payload ON payload.inbox_id = inbox.id
WHERE inbox.provider = 'DEBUG_EML_UPLOAD'
AND media.media_type = 'ORIGINAL_EMAIL'
AND payload.payload_json LIKE '%controller-test%'
AND inbox.external_message_id LIKE 'debug-eml-run-%'
AND run.run_label = 'controller-test'
""", Long.class);
org.assertj.core.api.Assertions.assertThat(originalEmailMediaCount).isEqualTo(1L);
ArgumentCaptor<SuperAgentMailDebugRequest> superAgentRequestCaptor =
ArgumentCaptor.forClass(SuperAgentMailDebugRequest.class);
verify(superAgentOpenApiClient).invokeMailDebug(superAgentRequestCaptor.capture());
SuperAgentMailDebugRequest superAgentRequest = superAgentRequestCaptor.getValue();
org.assertj.core.api.Assertions.assertThat(superAgentRequest.message())
.contains("AgentBus Outlook 邮件 JSON")
.contains("\"external_message_id\":\"debug-eml-run-")
.contains("\"body\"")
.doesNotContain("\"provider\":\"DEBUG_EML_UPLOAD\"")
.doesNotContain("\"schema_version\"")
.doesNotContain("\"debug_context\"")
.doesNotContain("\"original_message_id\"")
.doesNotContain("\"mode\":\"debug_only\"")
.contains("\"reply_policy\"")
.contains("\"mode\":\"manual\"")
.contains("\"final_only\":true");
org.assertj.core.api.Assertions.assertThat(superAgentRequest.metadata())
.containsEntry("source", "th-hotel-debug-eml-upload")
.containsKey("debug_context");
Long debugRunCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM platform_debug_eml_superagent_run
@@ -509,13 +543,13 @@ class DebugEmlSuperAgentControllerTest {
Long sourceCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM platform_source_message_inbox inbox
JOIN platform_source_message_payload payload ON payload.inbox_id = inbox.id
FROM platform_debug_eml_superagent_run run
JOIN platform_source_message_inbox inbox ON inbox.id = run.source_message_id
WHERE inbox.hotel_id = 'HOTEL-TEST'
AND inbox.provider = 'DEBUG_EML_UPLOAD'
AND inbox.channel = 'EMAIL'
AND inbox.external_message_id LIKE 'debug-eml-run-%'
AND payload.payload_json LIKE '%repeat-debug-upload%'
AND run.run_label = 'repeat-debug-upload'
""", Long.class);
org.assertj.core.api.Assertions.assertThat(sourceCount).isEqualTo(2L);
}
@@ -534,11 +568,11 @@ class DebugEmlSuperAgentControllerTest {
Long sourceCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM platform_source_message_inbox inbox
JOIN platform_source_message_payload payload ON payload.inbox_id = inbox.id
FROM platform_debug_eml_superagent_run run
JOIN platform_source_message_inbox inbox ON inbox.id = run.source_message_id
WHERE inbox.hotel_id = 'HOTEL-TEST'
AND inbox.provider = 'DEBUG_EML_UPLOAD'
AND payload.payload_json LIKE '%system-hotel-debug-upload%'
AND run.run_label = 'system-hotel-debug-upload'
""", Long.class);
org.assertj.core.api.Assertions.assertThat(sourceCount).isEqualTo(1L);
}