diff --git a/docs/project/frontend-backend/backend-to-frontend-notes.md b/docs/project/frontend-backend/backend-to-frontend-notes.md index ce1f2ec..a69601b 100644 --- a/docs/project/frontend-backend/backend-to-frontend-notes.md +++ b/docs/project/frontend-backend/backend-to-frontend-notes.md @@ -283,9 +283,10 @@ run_label: 可选调试标签 - 接口会解析 `.eml`,上传原始邮件、内联图片和附件到本系统阿里云 OSS,替换 HTML 内 `cid:` 图片,再写入 SourceMessage Inbox。 - SourceMessage 来源 provider 固定为 `DEBUG_EML_UPLOAD`,用于和 AgentBus 入库邮件区分。 - AgentBus 实时收到邮件后自动推 SuperAgent 由 M007 单独建设;这个接口是人工 Debug 上传链路,不代表实时生产链路。 -- `external_message_id` 是后端生成的 Debug 独立 ID,格式类似 `debug-eml-run-{debugRunId}-{sha256前缀}`;原始邮件 `Message-ID` 保存在 `agentbus_like_payload.source.original_message_id`。 +- `external_message_id` 是后端生成的 Debug 独立 ID,格式类似 `debug-eml-run-{debugRunId}-{sha256前缀}`;原始邮件 `Message-ID` 不再放入 `agentbus_like_payload`,需要排查时看原始 EML OSS 文件和后端 Debug run / SuperAgent metadata。 - 邮件会话解析支持 `References`、`In-Reply-To` 和 `Thread-Index`,但 Debug EML 的 `external_message_id` 不使用原始 `Message-ID` 做幂等。 -- `agentbus_like_payload.schema_version` 固定为 `debug-eml-upload-v1`,前端可用于调试展示和版本判断。 +- `agentbus_like_payload` 是后端发送给 SuperAgent 的 AgentBus Outlook-like 主输入,前端只做只读展示;该对象会包含普通 `reply_policy.mode=manual`、`reply_policy.final_only=true`,但不再包含 `schema_version`、`source.provider=DEBUG_EML_UPLOAD`、`debug_context` 或旧的 Debug 专属 `reply_policy.mode=debug_only`。 +- Debug 来源版本仍由后端 SourceMessage payload 表 `schemaVersion=debug-eml-upload-v1` 记录,前端页面不要再依赖 `agentbus_like_payload.schema_version`。 - 第一版只返回 SuperAgent 结果,不创建订单、不创建任务、不调用任务结果通知接口。 - `X-TH-Hotel-Debug-Upload-Key` 只能由调试人员在受控环境手动提供,不能放入 `VITE_*`、源码、构建产物、URL query、localStorage、错误上报或普通日志。 - 返回的 `html_body_sanitized` 复用邮件会话详情的安全策略,前端展示 HTML 时优先使用;`html_body_with_oss_urls` 只作为调试原始处理结果,不建议直接渲染。 diff --git a/docs/project/frontend-backend/debug-eml-page-integration-guide.md b/docs/project/frontend-backend/debug-eml-page-integration-guide.md index 297d5ff..8e825a4 100644 --- a/docs/project/frontend-backend/debug-eml-page-integration-guide.md +++ b/docs/project/frontend-backend/debug-eml-page-integration-guide.md @@ -140,7 +140,7 @@ export async function uploadDebugEml(input: { | `html_body_sanitized` | string | 后端清洗后的 HTML,前端预览邮件正文时优先使用。 | | `html_sanitize_required` | boolean | 当前为 `true`,提醒前端不要直接信任原始 HTML。 | | `html_render_mode` | string | 当前可能为 `SANITIZED_HTML` 或 `TEXT_ONLY`。 | -| `agentbus_like_payload` | object | 后端发送给 SuperAgent 的结构化 payload。 | +| `agentbus_like_payload` | object | 后端发送给 SuperAgent 的 AgentBus Outlook-like 主 payload;只读展示。 | | `superagent_session_id` | string | SuperAgent session ID。 | | `superagent_run_id` | string | SuperAgent run ID。 | | `superagent_raw_answer` | string | SuperAgent 最终原始文本回答。 | @@ -164,7 +164,8 @@ export async function uploadDebugEml(input: { - 邮件正文 iframe / 富文本预览优先使用 `html_body_sanitized`。 - `html_body_with_oss_urls` 可以放在“原始处理 HTML”折叠面板中,不作为默认渲染内容。 -- `agentbus_like_payload.source.original_message_id` 是原始邮件 `Message-ID`。 +- `agentbus_like_payload` 不包含 Debug 专属字段,不再包含 `schema_version`、`source.provider=DEBUG_EML_UPLOAD`、`source.original_message_id`、`debug_context` 或旧的 Debug 专属 `reply_policy.mode=debug_only`;当前会包含普通 `reply_policy.mode=manual`、`reply_policy.final_only=true`。 +- 原始邮件 `Message-ID` 不作为 Debug 外部消息 ID,也不进入 SuperAgent 主 payload;需要排查时看原始 EML OSS 文件和后端 Debug run / SuperAgent metadata。 - `external_message_id` 是 Debug 链路生成的独立 ID,不等同于原始 `Message-ID`。 - `superagent_parsed_json` 有值时优先展示格式化 JSON;没有值时展示 `superagent_raw_answer`。 - 如果 SuperAgent 返回旧 `S000,source_message_id` 或 `S999,source_message_id`,后端会把它识别为特殊入口结果,不会作为 JSON 解析失败处理。前端可在结果区展示 `entry_result_code`、`entry_result_source_message_id`、`entry_result_meaning` 和 `entry_result_description`。 @@ -286,6 +287,7 @@ idle - Debug EML 页面是调试工具,不是 Message Notification 页面。 - Debug EML 页面写入的 SourceMessage `provider=DEBUG_EML_UPLOAD`,用于和 AgentBus 来源区分。 +- Debug 页面发给 SuperAgent 的 `agentbus_like_payload` 会尽量贴近实时 AgentBus Outlook payload;前端不要用 `DEBUG_EML_UPLOAD` 或 `schema_version` 判断该对象内容。 - Debug EML 页面第一版不创建订单和任务,所以上传成功后任务列表和订单列表不会因为这次上传自动新增业务数据。 - AgentBus 实时收到邮件后自动推 SuperAgent 已由 M007 后端 V1 实现,但默认关闭且不走 Debug 页面;不能用 Debug EML 页面代表生产实时链路。 diff --git a/docs/project/go-live-notes.md b/docs/project/go-live-notes.md index 6740f3b..9105bf7 100644 --- a/docs/project/go-live-notes.md +++ b/docs/project/go-live-notes.md @@ -192,7 +192,8 @@ - Debug EML 上传接口会接收原始邮件、上传 OSS 并调用 SuperAgent,风险和成本高于普通查询接口。 - Debug EML 上传 key、SuperAgent Open API Key、阿里云 OSS AccessKey 都不得进入前端源码、`VITE_*`、镜像、普通日志或文档真实值。 - Debug EML 写入 SourceMessage Inbox 时 `provider=DEBUG_EML_UPLOAD`,不能伪装为 AgentBus 来源。 -- Debug EML 写入 SourceMessage Inbox 时 `external_message_id` 是后端生成的 `debug-eml-run-{debugRunId}-{sha256前缀}`;原始邮件 `Message-ID` 只保存在 payload 的 `source.original_message_id`。 +- Debug EML 写入 SourceMessage Inbox 时 `external_message_id` 是后端生成的 `debug-eml-run-{debugRunId}-{sha256前缀}`;原始邮件 `Message-ID` 不进入发给 SuperAgent 的主 payload,需要排查时看原始 EML OSS 文件、Debug run 和 SuperAgent Open API metadata。 +- Debug EML 返回的 `agentbus_like_payload` 是 AgentBus Outlook-like 主输入,会包含普通 `reply_policy.mode=manual`、`reply_policy.final_only=true`;不再包含 `schema_version`、`source.provider=DEBUG_EML_UPLOAD`、`debug_context` 或旧的 Debug 专属 `reply_policy.mode=debug_only`。Debug 来源区分仍依赖 SourceMessage `provider=DEBUG_EML_UPLOAD` 和 payload 表 `schemaVersion=debug-eml-upload-v1`;拿到 AgentBus 真实 `reply_policy.mode` 枚举后需统一替换占位值。 - Debug EML 返回 `html_body_sanitized`、`html_sanitize_required`、`html_render_mode`,前端展示 HTML 时应优先使用清洗字段。 - Debug EML 当前会识别 SuperAgent Open API 返回的旧 `S000/S999,source_message_id`,并在 `superagent_parsed_json` 中返回结构化入口结果;这不是 JSON 解析失败。结构化 `S10/S99` 可通过任务结果通知接口入站,Debug EML 页面若要直接展示完整 V3 入口结构,前端展示仍需继续补齐。 - Debug EML 第一版只展示 SuperAgent 结果,不创建订单、不创建任务、不调用任务结果通知接口。 diff --git a/docs/project/requirements/M004-debug-eml-superagent-upload-v1.md b/docs/project/requirements/M004-debug-eml-superagent-upload-v1.md index 492e2f8..36432f0 100644 --- a/docs/project/requirements/M004-debug-eml-superagent-upload-v1.md +++ b/docs/project/requirements/M004-debug-eml-superagent-upload-v1.md @@ -42,10 +42,10 @@ Inbox 作为来源事实,但不创建订单、不创建任务、不写 AI 任 - `.eml` MIME 解析:邮件头、纯文本正文、HTML 正文、内联图片和附件。 - 阿里云 OSS 上传端口和适配器:原始 `.eml`、内联图片和附件。 - HTML `cid:` 图片替换为 OSS URL。 -- AgentBus-like payload 组装。 +- AgentBus Outlook-like payload 组装;发送给 SuperAgent 的主输入尽量贴近实时 AgentBus Outlook payload。 - SourceMessage Inbox 写入,`provider=DEBUG_EML_UPLOAD`,`schema_version=debug-eml-upload-v1`。 - Debug EML 的 `external_message_id` 由后端生成,格式为 `debug-eml-run-{debugRunId}-{sha256前缀}`,避免同一封 `.eml` 多次上传被 SourceMessage 幂等覆盖。 -- 原始邮件 `Message-ID` 不再作为 Debug EML 的 `external_message_id`,而是保存到 `agentbus_like_payload.source.original_message_id`。 +- 原始邮件 `Message-ID` 不再作为 Debug EML 的 `external_message_id`,也不混入发送给 SuperAgent 的主 payload;调试追溯信息放入 SuperAgent Open API `metadata.debug_context` 和 Debug run / 原始 EML OSS 记录中。 - EML 会话解析支持 `References` / `In-Reply-To` / `Thread-Index`;缺失时再回退到当前消息 ID 或 debug 会话 ID。 - Debug run 在解析、OSS 上传、构造 SourceMessage、写入 SourceMessage、调用 SuperAgent 前写入运行中阶段状态,便于测试环境定位卡点。 - SourceMessage 写入成功后先把 debug run 标记为 `SOURCE_CAPTURED`;即使后续 SuperAgent 调用失败,也保留 SourceMessage 和 OSS 原文追溯信息。 @@ -65,7 +65,7 @@ Inbox 作为来源事实,但不创建订单、不创建任务、不写 AI 任 - 后端可以把原始 `.eml`、内联图片和附件上传到阿里云 OSS。 - 后端可以把 HTML 正文中的 `cid:` 引用替换成 OSS URL。 - 后端可以返回清洗后的 HTML 字段,前端优先展示 `html_body_sanitized`。 -- 后端可以组装 AgentBus-like 邮件 payload,保持和当前 SourceMessage Inbox / SuperAgent 输入口径接近。 +- 后端可以组装 AgentBus Outlook-like 邮件 payload,保持和实时 AgentBus 自动分发给 SuperAgent 的主输入结构接近。 - 后端可以写入 SourceMessage Inbox,并明确标记来源为 Debug EML Upload。 - 后端可以调用 SuperAgent Open API 并解析最终返回。 - 前端可以看到 SourceMessage ID、上传媒体、处理后的 HTML、发送给 SuperAgent 的 payload、SuperAgent 原始回答和解析后的 JSON。 @@ -103,7 +103,7 @@ Debug 页面上传 .eml → 上传普通附件到阿里云 OSS → debug run 标记为 BUILDING_SOURCE_MESSAGE → 替换 HTML 正文中的 cid: 图片引用 -→ 组装 AgentBus-like payload +→ 组装 AgentBus Outlook-like payload → debug run 标记为 CAPTURING_SOURCE_MESSAGE → 调用 SourceMessageCaptureService 写入 SourceMessage Inbox → debug run 标记为 SOURCE_CAPTURED @@ -155,7 +155,7 @@ Header: X-TH-Hotel-Debug-Upload-Key: | `html_body_sanitized` | 复用邮件会话安全策略清洗后的 HTML 正文,前端调试展示优先使用 | | `html_sanitize_required` | 固定提示前端 HTML 需要按安全策略展示 | | `html_render_mode` | HTML 推荐渲染模式,当前优先返回 `SANITIZED_HTML` | -| `agentbus_like_payload` | 发送给 SuperAgent 的结构化邮件 payload,包含 `schema_version=debug-eml-upload-v1` | +| `agentbus_like_payload` | 发送给 SuperAgent 的 AgentBus Outlook-like 结构化邮件 payload;Debug 来源版本仍由 SourceMessage payload 表 `schemaVersion=debug-eml-upload-v1` 记录,不放入主 payload | | `superagent_session_id` | SuperAgent Open API session ID,失败时为空 | | `superagent_run_id` | SuperAgent 返回的 run ID,失败时为空 | | `superagent_raw_answer` | SuperAgent 最终文本回答 | @@ -194,7 +194,7 @@ Debug 链路调用现有 `SourceMessageCaptureService.capture`,建议映射如 | `subject` | 邮件 `Subject` | | `textBody` | 解析出的纯文本正文 | | `htmlBody` | 已替换 OSS URL 的 HTML 正文 | -| `payloadJson` | AgentBus-like payload JSON | +| `payloadJson` | AgentBus Outlook-like payload JSON | | `schemaVersion` | `debug-eml-upload-v1` | | `mediaItems` | 上传到 OSS 后的内联图片、附件和原始 `.eml` 引用 | @@ -210,42 +210,56 @@ Debug 链路调用现有 `SourceMessageCaptureService.capture`,建议映射如 补充说明: -- 原始邮件 `Message-ID` 保存到 `payloadJson.source.original_message_id`,不参与 Debug EML SourceMessage 幂等键。 -- 原始解析出的会话 ID 保存到 `payloadJson.source.original_conversation_id`,便于排查 Debug 会话和真实邮件头之间的关系。 +- 原始邮件 `Message-ID` 不参与 Debug EML SourceMessage 幂等键,也不进入发给 SuperAgent 的主 payload;需要排查时使用 Debug run、原始 EML OSS 文件和 Open API metadata。 +- 原始解析出的会话 ID 用于生成 `external_conversation_id`;缺失时使用 `debug-eml-thread-{externalMessageId}`。 - `Thread-Index` 仅作为 `References` / `In-Reply-To` 缺失时的会话 fallback,不覆盖明确的邮件回复链。 -## 8. AgentBus-like Payload 结构建议 +## 8. AgentBus Outlook-like Payload 结构建议 -第一版 payload 目标是让 SuperAgent 获得接近 AgentBus 邮件输入的结构,而不是完整复刻 AgentBus 协议。 +第一版 payload 目标是让 SuperAgent 获得尽量接近实时 AgentBus Outlook 邮件输入的主结构。Debug 专属字段不进入该主 payload,避免 SuperAgent 在 Debug 和实时链路上看到不同业务语义。 ```json { + "text": "plain text body", + "received_at": "2026-07-09T01:31:00Z", "source": { "channel": "EMAIL", - "provider": "DEBUG_EML_UPLOAD", "external_message_id": "debug-eml-run-1900000000000000001-4f2c9a8b7d6e", "external_conversation_id": "debug-eml-thread-debug-eml-run-1900000000000000001-4f2c9a8b7d6e", - "original_message_id": "message-id-from-eml@example.com", - "original_conversation_id": "thread-index-or-reference-from-eml", "sender": "sender@example.com", "subject": "Booking Request", - "sent_at": "2026-07-09T01:30:00Z" + "sent_at": "2026-07-09T01:30:00Z", + "received_at": "2026-07-09T01:31:00Z" }, "body": { - "content_type": "MIXED", + "content_type": "mixed", "text": "plain text body", "html": "..." }, - "inline_images": [], - "attachments": [], + "inline_images": [ + { + "id": "cid:inline-001", + "media_type": "INLINE_IMAGE", + "file_name": "inline.png", + "content_type": "image/png", + "size_bytes": 1200, + "external_url": "https://oss.example/debug/eml/inline.png", + "content_id": "inline-001" + } + ], + "attachments": [ + { + "id": "attachment:1", + "media_type": "ATTACHMENT", + "file_name": "booking.pdf", + "content_type": "application/pdf", + "size_bytes": 3400, + "external_url": "https://oss.example/debug/eml/booking.pdf" + } + ], "reply_policy": { - "mode": "debug_only", + "mode": "manual", "final_only": true - }, - "debug_context": { - "debug_run_id": "1900000000000000001", - "run_label": "frontend-debug-smoke", - "original_eml_oss_url": "https://oss.example/debug/eml/..." } } ``` @@ -253,8 +267,10 @@ Debug 链路调用现有 `SourceMessageCaptureService.capture`,建议映射如 中文说明: - `inline_images[]` 和 `attachments[]` 中的 URL 必须是本系统 OSS URL。 -- `reply_policy.mode=debug_only` 表示本链路只用于调试,不允许 SuperAgent 或本系统发送客户回复。 -- 如果后续 AgentBus 正式 payload 有字段变化,本 Debug 链路可以通过 schema version 单独升级。 +- 主 payload 包含 AgentBus Outlook 关键字段 `reply_policy.mode`、`reply_policy.final_only`;Debug EML 第一版使用 `mode=manual`、`final_only=true` 表示不由 Debug 链路自动回复客户,后续拿到 AgentBus 真实枚举后再统一对齐。 +- 主 payload 不包含 `schema_version`、`source.provider=DEBUG_EML_UPLOAD`、`debug_context`、`original_message_id`,也不再使用旧的 Debug 专属 `reply_policy.mode=debug_only`。 +- SourceMessage Inbox 仍以 `provider=DEBUG_EML_UPLOAD` 和 payload 表 `schemaVersion=debug-eml-upload-v1` 区分 Debug 来源,不能伪装成 AgentBus 入库。 +- Debug run ID、run_label、原始 Message-ID 和原始会话 ID 放入 SuperAgent Open API `metadata.debug_context`,只用于排查,不作为 SuperAgent 邮件业务主输入;原始 EML OSS URL 仅保存在本系统 Debug run / 受控响应中,不再透传给 SuperAgent metadata。 ## 9. SuperAgent Open API 调用口径 @@ -271,7 +287,7 @@ POST /api/open/agent-sessions/{sessionId}/messages/stream - 状态变更请求使用 CSRF double-submit:`X-CSRF-Token` 和 `Cookie: csrf_token=`;当前共享 Open API client 已自动生成临时随机 token 并同时写入 header / cookie。 - 创建 session 时使用 `SUPERAGENT_DEBUG_EML_EXTERNAL_SUBJECT_ID` 作为 `external_subject_id`。 - `idempotency_key` 使用 `debug_run_id` 派生,保证同一次 Debug 运行不会重复创建不可追溯请求。 -- 发送消息时把 AgentBus-like payload 序列化为 JSON 文本,并附加中文指令,要求 SuperAgent 输出结构化 JSON。 +- 发送消息时把 AgentBus Outlook-like payload 序列化为 JSON 文本,并附加中文指令,要求 SuperAgent 输出结构化 JSON 或 S10/S99 特殊结果。 - SSE 解析仍按项目现有经验,从后期 `values.messages[]` 中寻找 `type=ai` 且 `finish_reason=stop` 的最终回答。 当前未确认项: @@ -341,7 +357,7 @@ platform_debug_eml_superagent_run | `original_file_name` | 原始上传文件名安全摘要 | | `original_eml_oss_url` | 原始 `.eml` OSS URL | | `original_eml_sha256` | 原始 `.eml` SHA-256 | -| `payload_json` | AgentBus-like payload JSON | +| `payload_json` | AgentBus Outlook-like payload JSON | | `superagent_session_id` | SuperAgent session ID | | `superagent_run_id` | SuperAgent run ID | | `superagent_raw_answer` | SuperAgent 最终文本回答 | @@ -478,7 +494,7 @@ SUPERAGENT_DEBUG_EML_READ_TIMEOUT=180s 2. 支持上传 .eml,解析邮件头、text/html 正文、内联图片和附件。 3. 接入阿里云 OSS,上传原始 .eml、内联图片和附件。 4. 替换 HTML 正文中的 cid: 图片为 OSS URL。 -5. 组装 AgentBus-like payload。 +5. 组装 AgentBus Outlook-like payload。 6. 写入 SourceMessage Inbox,provider 使用 DEBUG_EML_UPLOAD,schema_version 使用 debug-eml-upload-v1。 7. 调用 SuperAgent Open API 创建 session 并发送 messages/stream。 8. 解析 SuperAgent SSE 最终回答,返回 raw answer 和 parsed json。 diff --git a/docs/project/requirements/M007-agentbus-superagent-auto-dispatch-v1.md b/docs/project/requirements/M007-agentbus-superagent-auto-dispatch-v1.md index e53f850..1db2b63 100644 --- a/docs/project/requirements/M007-agentbus-superagent-auto-dispatch-v1.md +++ b/docs/project/requirements/M007-agentbus-superagent-auto-dispatch-v1.md @@ -131,7 +131,7 @@ KEY idx_dispatch_external_message (hotel_id, external_message_id) ## 7. 消息组装 -AgentBus 自动分发发送给 SuperAgent 的 message 第一版应基于 SourceMessage 原始 payload 构造,保持和 Debug EML 的 AgentBus-like payload 语义一致。 +AgentBus 自动分发发送给 SuperAgent 的 message 第一版应基于 SourceMessage 原始 payload 构造。Debug EML 发送给 SuperAgent 的 `agentbus_like_payload` 也按 AgentBus Outlook-like 主结构组装,并包含 `reply_policy.mode`、`reply_policy.final_only`;Debug EML V1 暂用 `mode=manual`、`final_only=true`,不能再使用旧的 Debug 专属 `debug_only`。Debug 来源区分仍保留在 SourceMessage `provider=DEBUG_EML_UPLOAD`、payload 表 `schemaVersion=debug-eml-upload-v1` 和 Open API metadata 中,不混入主 payload。后续 AgentBus 明确真实 `reply_policy.mode` 枚举后,Debug EML 和实时链路需要一起对齐。 发送 metadata 建议包含: diff --git a/server/src/main/java/cn/nianxx/thhotel/platform/debug/common/dto/DebugEmlSuperAgentRunSnapshot.java b/server/src/main/java/cn/nianxx/thhotel/platform/debug/common/dto/DebugEmlSuperAgentRunSnapshot.java index f1927d9..67dd0ec 100644 --- a/server/src/main/java/cn/nianxx/thhotel/platform/debug/common/dto/DebugEmlSuperAgentRunSnapshot.java +++ b/server/src/main/java/cn/nianxx/thhotel/platform/debug/common/dto/DebugEmlSuperAgentRunSnapshot.java @@ -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 diff --git a/server/src/main/java/cn/nianxx/thhotel/platform/debug/common/dto/DebugEmlSuperAgentRunUpdate.java b/server/src/main/java/cn/nianxx/thhotel/platform/debug/common/dto/DebugEmlSuperAgentRunUpdate.java index 27a7ee6..546cb9d 100644 --- a/server/src/main/java/cn/nianxx/thhotel/platform/debug/common/dto/DebugEmlSuperAgentRunUpdate.java +++ b/server/src/main/java/cn/nianxx/thhotel/platform/debug/common/dto/DebugEmlSuperAgentRunUpdate.java @@ -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 diff --git a/server/src/main/java/cn/nianxx/thhotel/platform/debug/common/result/DebugEmlSuperAgentRunResult.java b/server/src/main/java/cn/nianxx/thhotel/platform/debug/common/result/DebugEmlSuperAgentRunResult.java index 1b21c94..544f08c 100644 --- a/server/src/main/java/cn/nianxx/thhotel/platform/debug/common/result/DebugEmlSuperAgentRunResult.java +++ b/server/src/main/java/cn/nianxx/thhotel/platform/debug/common/result/DebugEmlSuperAgentRunResult.java @@ -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 原始最终回答 diff --git a/server/src/main/java/cn/nianxx/thhotel/platform/debug/domain/DebugEmlSuperAgentRunEntity.java b/server/src/main/java/cn/nianxx/thhotel/platform/debug/domain/DebugEmlSuperAgentRunEntity.java index 7b3d2b0..caa63e7 100644 --- a/server/src/main/java/cn/nianxx/thhotel/platform/debug/domain/DebugEmlSuperAgentRunEntity.java +++ b/server/src/main/java/cn/nianxx/thhotel/platform/debug/domain/DebugEmlSuperAgentRunEntity.java @@ -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; diff --git a/server/src/main/java/cn/nianxx/thhotel/platform/debug/service/impl/DebugEmlSuperAgentRunServiceImpl.java b/server/src/main/java/cn/nianxx/thhotel/platform/debug/service/impl/DebugEmlSuperAgentRunServiceImpl.java index 3c4af3c..a6a25bb 100644 --- a/server/src/main/java/cn/nianxx/thhotel/platform/debug/service/impl/DebugEmlSuperAgentRunServiceImpl.java +++ b/server/src/main/java/cn/nianxx/thhotel/platform/debug/service/impl/DebugEmlSuperAgentRunServiceImpl.java @@ -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 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 streamedTraceEvents = new ArrayList<>(); Consumer 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 buildAgentBusLikePayload( - Long runId, - String runLabel, + LocalDateTime createdAt, String externalMessageId, String externalConversationId, - String originalMessageId, - String originalConversationId, ParsedEmlMessage parsed, String htmlWithOssUrls, List uploadedMedia) { Map 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 body = new LinkedHashMap<>(); body.put("content_type", contentType(parsed, htmlWithOssUrls)); @@ -950,26 +948,56 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe List> inlineImages = mediaPayload(uploadedMedia, SourceMessageMediaType.INLINE_IMAGE.code()); List> attachments = mediaPayload(uploadedMedia, SourceMessageMediaType.ATTACHMENT.code()); - Map replyPolicy = new LinkedHashMap<>(); - replyPolicy.put("mode", "debug_only"); - replyPolicy.put("final_only", true); - - Map 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 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 replyPolicy() { + Map replyPolicy = new LinkedHashMap<>(); + replyPolicy.put("mode", REPLY_POLICY_MODE); + replyPolicy.put("final_only", true); + return replyPolicy; + } + + /** + * 构造 Debug EML 调用 SuperAgent 的 metadata。这里保存调试来源和原始 EML 标识,不混入邮件主 payload。 + */ + private Map buildSuperAgentMetadata( + Long runId, + String runLabel, + Long sourceMessageId, + String hotelId, + String externalMessageId, + String externalConversationId, + String originalMessageId, + String originalConversationId) { + Map 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 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 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 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 还原对象路径。 */ diff --git a/server/src/test/java/cn/nianxx/thhotel/platform/debug/control/DebugEmlSuperAgentControllerTest.java b/server/src/test/java/cn/nianxx/thhotel/platform/debug/control/DebugEmlSuperAgentControllerTest.java index 511281c..dd03cb9 100644 --- a/server/src/test/java/cn/nianxx/thhotel/platform/debug/control/DebugEmlSuperAgentControllerTest.java +++ b/server/src/test/java/cn/nianxx/thhotel/platform/debug/control/DebugEmlSuperAgentControllerTest.java @@ -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 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); }