From 6416c1b78a25ad7addb38d60945945ec839c7259 Mon Sep 17 00:00:00 2001 From: andy Date: Sat, 11 Jul 2026 15:53:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90M002=20V3=20CP4=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E8=AF=A6=E6=83=85=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend-to-frontend-notes.md | 12 +- .../frontend-to-backend-api-requests.md | 114 +++++++++-- .../M002-order-task-workflow-v3.md | 14 +- .../dto/ReservationAiQueryTaskSnapshot.java | 10 +- .../dto/ReservationAiTransitionSnapshot.java | 55 ++++++ .../ReservationAiTransitionDisplayResult.java | 44 +++++ ...eservationOrderTaskTimelineItemResult.java | 12 ++ .../ReservationSourceMessageOnlyResult.java | 15 ++ .../result/ReservationTaskDetailResult.java | 23 ++- .../ReservationTaskWorkbenchItemResult.java | 12 ++ ...ybatisReservationAiWorkflowRepository.java | 70 ++++++- .../ReservationAiWorkflowRepository.java | 11 ++ .../ReservationFrontendQueryServiceImpl.java | 8 + .../ReservationTaskWorkflowServiceImpl.java | 95 ++++++++- .../SuperAgentTaskResultControllerTest.java | 182 ++++++++++++++++++ 15 files changed, 647 insertions(+), 30 deletions(-) create mode 100644 server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/dto/ReservationAiTransitionSnapshot.java create mode 100644 server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationAiTransitionDisplayResult.java diff --git a/docs/project/frontend-backend/backend-to-frontend-notes.md b/docs/project/frontend-backend/backend-to-frontend-notes.md index 1100952..ab5809a 100644 --- a/docs/project/frontend-backend/backend-to-frontend-notes.md +++ b/docs/project/frontend-backend/backend-to-frontend-notes.md @@ -48,9 +48,9 @@ | `GET /api/auth/me` | 恢复当前登录态 | 前端启动后带 `Authorization: Bearer ` 调用;401 时清理 token 并进入登录页。 | | `POST /api/auth/logout` | 登出当前 session | 带 `Authorization: Bearer `;成功后前端必须清理本地 token 和当前用户上下文。 | | `GET /api/reservation/orders` | 查询订单列表 | 默认返回全部订单状态;`open_task_count` 排除 `COMPLETED` 和 `FAILED`;隐藏技术订单不返回,因此 S10/S99 和旧 S000/S999 不会在订单列表形成订单。 | -| `GET /api/reservation/tasks` | 查询任务列表 / 工作台 | 用 `can_process` 和 `readonly_reason_code` 控制入口按钮;列表不返回 AI 原始 payload、邮件正文或附件 URL;已返回来源邮件会话摘要字段,并支持 `order_status` 按任务所属订单状态筛选;旧 S000/S999 和新 S10/S99 当前都会以 `task_type=SOURCE_MESSAGE_ONLY` 只读任务返回,任务结果回调响应已包含 `route_code/system_process_category`,列表接口完整透出 V3 路由字段后续补齐。 | -| `GET /api/reservation/orders/{orderId}` | 查询订单详情与任务时间线 | `include_tasks=false` 可只取订单摘要;时间线按后端队列顺序返回,前端不要自行按创建时间重排;`tasks[]` 已返回来源邮件会话摘要字段;隐藏技术订单详情不可作为普通订单页打开。 | -| `GET /api/reservation/tasks/{taskId}` | 查询任务详情 | 以返回的可处理状态和只读原因控制按钮,不只看任务状态;`fields[]` 已包含 P0 字段元数据;源邮件只读通知卡字段列表和 OPERA 操作列表为空,旧数据通过 `source_message_only_result` 返回 S000/S999 入口结果;结构化 S10/S99 已可入站并保存,详情接口完整透出 `agent_assessment/notification/manual_review` 后续补齐。 | +| `GET /api/reservation/tasks` | 查询任务列表 / 工作台 | 用 `can_process` 和 `readonly_reason_code` 控制入口按钮;列表不返回 AI 原始 payload、邮件正文或附件 URL;已返回来源邮件会话摘要字段,并支持 `order_status` 按任务所属订单状态筛选;旧 S000/S999 和新 S10/S99 都以 `task_type=SOURCE_MESSAGE_ONLY` 只读任务返回,列表已透出 `result_type`、`ai_task_type`、`route_code`、`system_process_category`。 | +| `GET /api/reservation/orders/{orderId}` | 查询订单详情与任务时间线 | `include_tasks=false` 可只取订单摘要;时间线按后端队列顺序返回,前端不要自行按创建时间重排;`tasks[]` 已返回来源邮件会话摘要字段和 V3 路由字段;隐藏技术订单详情不可作为普通订单页打开。 | +| `GET /api/reservation/tasks/{taskId}` | 查询任务详情 | 以返回的可处理状态和只读原因控制按钮,不只看任务状态;`fields[]` 已包含 P0 字段元数据;源邮件只读通知卡字段列表和 OPERA 操作列表为空;结构化 S10/S99 通过 `source_message_only_result.agent_assessment`、`notification`、`manual_review` 展示;普通业务任务可通过 `adapter_contract_errors[]` 和 `unhandled_intents[]` 查看同批次未建任务的诊断信息。 | | `PUT /api/reservation/tasks/{taskId}/draft` | 保存任务草稿 | 只保存草稿,不代表用户最终确认。 | | `POST /api/reservation/tasks/{taskId}/confirm` | 最终确认任务 | 后端会做第一版字段校验,通过后进入 `READY`。 | | `POST /api/reservation/tasks/{taskId}/manual-review-conversions` | Fallback 人工转换 | 只用于 manual_review / fallback,不用于普通任务切换订单。 | @@ -142,7 +142,9 @@ POST /api/auth/logout - 源邮件只读通知卡背后有隐藏技术订单用于满足后端任务外键,但订单列表不会返回该订单;任务列表中该类任务的 `display_order_key`、`temporary_order_no`、`group_code`、`confirmation_number` 可能为空,前端不要因此隐藏整条任务。 - 当前前端已按 `SOURCE_MESSAGE_ONLY` 展示旧 S000/S999;后端回调已支持结构化 `route_code=S10/S99`、`result_type=source_message_review_notification` 的新入口通知,并继续只在任务列表和任务详情提供只读查看入口;`INFORMATIONAL_MESSAGE` 仅作为历史 Message Notification 兼容路径保留。 - 任务列表里旧 `task_type=SOURCE_MESSAGE_ONLY`、`task_subtype=S000/S999` 或新 `task_subtype=S10/S99` 的记录只展示邮件来源和 SuperAgent 入口结果,不展示处理按钮。 -- 任务详情里 `source_message_only_result` 仅对 `SOURCE_MESSAGE_ONLY` 返回,包含 `entry_result_code`、`entry_result_meaning`、`entry_result_description`、`entry_result_source_message_id` 和 `raw_answer`;结构化 S10/S99 已在入站 payload 中保存 `route_code`,但详情接口完整透出 `agent_assessment`、`notification` 和入口 `manual_review` 仍待后续 checkpoint;普通业务任务该字段为空。 +- 任务详情里 `source_message_only_result` 仅对 `SOURCE_MESSAGE_ONLY` 返回,包含 `entry_result_code`、`entry_result_meaning`、`entry_result_description`、`entry_result_source_message_id`、`result_type`、`route_code`、`agent_assessment`、`notification`、`manual_review` 和 `raw_answer`;普通业务任务该字段为空。 +- 任务列表、订单任务时间线和任务详情顶层已透出 `result_type`、`ai_task_type`、`route_code`、`system_process_category`。前端展示任务卡标题和标签时优先用这些稳定 code,不要只靠旧 `task_type` 判断。 +- `adapter_contract_errors[]` 和 `unhandled_intents[]` 只在任务详情返回,表示同一 SuperAgent 入站批次中没有生成业务任务的诊断块;前端只读展示并提供来源邮件入口,不显示保存、确认、执行或重试按钮。 ### 5.6 前端联调演示数据 seed 接口 @@ -268,7 +270,7 @@ run_label: 可选调试标签 ## 7. 需要持续提醒的后置事项 - 普通任务切换订单接口继续后置。 -- M002 V3 的结构化 `S10/S99` 入站、42 路由枚举 / 稳定配置、`UNHANDLED_CURRENT_INTENT` 和 `adapter_contract_error` transition 最小落库已完成;type-known manual review 同卡解阻、复核场景订单归属确认、任务列表 / 详情完整透出 V3 路由字段仍需后续后端 checkpoint。前端新页面应预留字段和状态,不要继续只写死旧 `S000/S999`。 +- M002 V3 的结构化 `S10/S99` 入站、42 路由枚举 / 稳定配置、`UNHANDLED_CURRENT_INTENT`、`adapter_contract_error` transition 最小落库,以及任务列表 / 订单时间线 / 任务详情 V3 路由字段和只读诊断块透出已完成;type-known manual review 同卡解阻、复核场景订单归属确认仍需后续后端 checkpoint。 - 系统管理后台 V1 已完成;后续若要做用户搜索更多筛选、批量操作、密码策略增强、MFA、登录设备管理,应单独开需求。 - 现有 Reservation / SourceMessage 业务接口的强制登录、强制权限和业务审计 actor 全量迁移仍后置。 - 真实 OPERA / OHIP 接入继续后置。 diff --git a/docs/project/frontend-backend/frontend-to-backend-api-requests.md b/docs/project/frontend-backend/frontend-to-backend-api-requests.md index de0ca43..8e0d9b3 100644 --- a/docs/project/frontend-backend/frontend-to-backend-api-requests.md +++ b/docs/project/frontend-backend/frontend-to-backend-api-requests.md @@ -16,7 +16,7 @@ | P0 | 邮件会话详情接口 `GET /api/source-messages/{sourceMessageId}/conversation` | 邮件会话详情页 | 已完成第一版 | | 联调 | 演示数据 seed 接口 `POST /api/system/reservation/demo-data` | 本地 / test 前端页面看效果 | 已完成;仅 dev/test 受控使用 | | 联调 | Debug EML 上传接口 `POST /api/system/debug/eml-superagent-runs` | Debug 页面上传 `.eml` 看 SuperAgent 结果 | 已完成第一版;仅 dev/test 受控使用 | -| P1 | S000/S999 特殊只读任务展示 | 任务列表、任务详情来源邮件查看 | 已完成后端第一版;前端已按 `SOURCE_MESSAGE_ONLY` 完成列表筛选、S000/S999 展示和详情只读展示,不做独立 Message Notification 接口 | +| P1 | S10/S99 源邮件只读通知卡与旧 S000/S999 兼容 | 任务列表、任务详情来源邮件查看 | 已完成第一版:旧 S000/S999 兼容,新结构化 S10/S99 可入站并在任务列表 / 详情只读展示 | | P1 | 任务卡前端字段白名单元数据接口 | 字段白名单调试、版本对齐 | 未完成;若任务详情已透出完整元数据,可后置 | | 后置 | 普通任务切换订单接口 | 任务详情订单归属调整 | 未完成;已确认后置 | @@ -41,9 +41,9 @@ | `GET /api/reservation/orders` | 已完成第一版 | 可以 | 默认查询全部订单状态;`open_task_count` 排除 `COMPLETED` 和 `FAILED`。 | | `GET /api/source-messages/{sourceMessageId}/conversation` | 已完成第一版,已补 `html_body_sanitized` 和 `html_render_mode` | 可以 | 返回完整 text/html、后端清洗后的 HTML、媒体外链和关联订单 / 任务摘要;前端不传原文读取 key,页面展示优先使用 `html_body_sanitized`。 | | `POST /api/system/reservation/demo-data` | 已完成 | 仅本地 / test 联调可用 | 默认关闭,必须后端配置访问口令;不能作为生产页面接口。 | -| `POST /api/system/debug/eml-superagent-runs` | 已完成第一版 | 仅 dev/test Debug 页面可用 | 默认关闭,必须后端配置访问口令、阿里云 OSS 和 SuperAgent Open API;第一版只展示 SuperAgent 结果,不创建订单和任务。 | +| `POST /api/system/debug/eml-superagent-runs` | 已完成第一版 | 仅 dev/test Debug 页面可用 | 默认关闭,必须后端配置访问口令、阿里云 OSS 和 SuperAgent Open API;第一版只展示 SuperAgent 结果,不创建订单和任务;已能识别旧 S000/S999 和新结构化 S10/S99。 | | `GET /api/source-message-conversations/{externalConversationId}` | 未发现后端实现 | 不可以 | 历史讨论过的候选路径,当前不提供;前端统一使用 `GET /api/source-messages/{sourceMessageId}/conversation`。 | -| `GET /api/reservation/message-notifications` | 未发现后端实现 | 不可以 | 第一版不做独立接口;新入口 S000/S999 已通过 `SOURCE_MESSAGE_ONLY` 任务展示。 | +| `GET /api/reservation/message-notifications` | 未发现后端实现 | 不可以 | 第一版不做独立接口;旧 S000/S999 已通过 `SOURCE_MESSAGE_ONLY` 任务展示,0711 P0 新 S10/S99 也继续复用任务列表 / 任务详情只读展示。 | | `GET /api/reservation/task-card-field-whitelist` | 未发现后端实现 | 不可以 | 若任务详情 `fields[]` 已补齐 3.0 元数据,可后置。 | ## 3. 任务列表 / 工作台接口字段补齐 @@ -68,7 +68,11 @@ GET /api/reservation/tasks | `display_order_key` | 前端优先展示的业务号或临时订单号。 | | `temporary_order_no` | 临时订单号。 | | `task_type` | 系统主任务类型。 | +| `result_type` | AI 结果类型,例如 `normal_task`、`manual_review`、`source_message_review_notification`。 | +| `ai_task_type` | SuperAgent 原始任务类型,例如 `New Booking`、`S10`、`S99`。 | | `task_subtype` | 任务 subtype。 | +| `route_code` | M002 V3 路由码,例如 `R01_NEW_FIT_RESERVATION_NORMAL`、`S10`、`S99`。 | +| `system_process_category` | 系统处理分类,例如 `BUSINESS_TASK`、`SOURCE_MESSAGE_NOTIFICATION`。 | | `task_status` | 任务状态。 | | `card_name` | 任务卡展示名称。 | | `queue_sequence` | 同订单队列顺序。 | @@ -86,7 +90,7 @@ GET /api/reservation/tasks | --- | --- | --- | | `hotel_id` | 否 | 酒店 ID。单酒店阶段默认可为空,由后端按当前用户上下文或平台酒店表唯一 `ACTIVE` 酒店解析;显式传值时后端会校验访问权限。 | | `order_id` | 否 | 按订单过滤。 | -| `task_type` | 否 | `NEW_BOOKING`、`UPDATE_BOOKING`、`CANCEL_BOOKING`、`MANUAL_REVIEW`、`INFORMATIONAL_MESSAGE`、`SOURCE_MESSAGE_ONLY`。其中 `INFORMATIONAL_MESSAGE` 仅历史兼容,新入口 S000/S999 使用 `SOURCE_MESSAGE_ONLY`。 | +| `task_type` | 否 | `NEW_BOOKING`、`UPDATE_BOOKING`、`CANCEL_BOOKING`、`MANUAL_REVIEW`、`INFORMATIONAL_MESSAGE`、`SOURCE_MESSAGE_ONLY`。其中 `INFORMATIONAL_MESSAGE` 仅历史兼容;旧入口 S000/S999 和 0711 P0 结构化 S10/S99 均复用 `SOURCE_MESSAGE_ONLY` 只读源邮件通知卡。 | | `task_status` | 否 | 任务状态过滤。 | | `task_subtype` | 否 | 任务卡 subtype 过滤。 | | `order_status` | 否 | 按任务所属订单状态过滤,支持 `TEMPORARY`、`ACTIVE`、`ENDED`、`LOGIC_DELETED`;不传时保持当前行为。 | @@ -107,7 +111,11 @@ GET /api/reservation/tasks "display_order_key": "GRP-001", "temporary_order_no": "TMP-20260708-001", "task_type": "UPDATE_BOOKING", + "result_type": "normal_task", + "ai_task_type": "Update Booking", "task_subtype": "RATE_CHANGE", + "route_code": "R07_UPDATE_RATE_CODE_NORMAL", + "system_process_category": "BUSINESS_TASK", "task_status": "PENDING_CONFIRM", "card_name": "Rate Change", "queue_sequence": 2, @@ -140,6 +148,7 @@ GET /api/reservation/tasks | `source_received_at` | 邮件来源接收时间,优先取 AgentBus payload `received_at`,用于任务列表排序和展示。 | | `external_conversation_id` | 来源消息所属邮件会话 ID,用于打开完整邮件会话详情。 | | `conversation_message_count` | 会话内邮件数量,用于提示用户该入口是整段会话,不是单封邮件。 | +| `result_type` / `ai_task_type` / `route_code` / `system_process_category` | V3 路由展示字段,用于任务卡标签、筛选和 S10/S99 只读卡判断。 | ## 4. 订单详情与任务时间线接口字段补齐 @@ -183,7 +192,11 @@ GET /api/reservation/orders/{orderId} { "task_id": "10001", "task_type": "NEW_BOOKING", + "result_type": "normal_task", + "ai_task_type": "New Booking", "task_subtype": "NEW_BOOKING", + "route_code": "R01_NEW_FIT_RESERVATION_NORMAL", + "system_process_category": "BUSINESS_TASK", "task_status": "COMPLETED", "card_name": "New Booking", "queue_sequence": 1, @@ -213,6 +226,7 @@ GET /api/reservation/orders/{orderId} | `source_received_at` | 邮件来源接收时间,优先取 AgentBus payload `received_at`。 | | `external_conversation_id` | 来源消息所属邮件会话 ID。 | | `conversation_message_count` | 会话内邮件数量。 | +| `result_type` / `ai_task_type` / `route_code` / `system_process_category` | V3 路由展示字段,和任务列表字段语义一致。 | ## 5. 订单列表接口 @@ -299,7 +313,21 @@ POST /api/system/reservation/demo-data | `tasks[]` | 生成的任务 ID、任务类型、任务 subtype 和任务状态。 | | `entrypoints` | 可直接访问的任务列表、订单列表、订单详情、任务详情、邮件会话详情 URL。 | -第一版 seed 覆盖:队列阻塞、已完成 OPERA 模拟、OPERA 失败可重试、Fallback 人工复核、历史 Message Notification 只读任务、邮件会话完整 HTML / 附件 / 内联图片。S000/S999 特殊只读任务可通过 SuperAgent 回调或后续专用夹具补充。 +第一版 seed 覆盖:队列阻塞、已完成 OPERA 模拟、OPERA 失败可重试、Fallback 人工复核、历史 Message Notification 只读任务、邮件会话完整 HTML / 附件 / 内联图片。旧 S000/S999 特殊只读任务可通过 SuperAgent 回调或后续专用夹具补充;0711 P0 新入口的 S10/S99 需要后端后续 checkpoint 补充结构化 fixture。 + +## 6.1 M002 V3 当前状态和后续待补能力 + +本节记录 0711 P0 基线确认后,前端关心的 V3 能力状态。已完成项可以直接接入;后置项需要另开 checkpoint。 + +| 能力 | 页面 / 场景 | 状态 | 前端最小诉求 | +| --- | --- | --- | --- | +| 结构化 `S10/S99` 入站 | 任务列表、任务详情、Debug EML 结果展示 | 已完成第一版 | 后端接收 `result_type=source_message_review_notification + route_code=S10/S99`,创建只读源邮件通知卡;任务列表可见,订单列表不可见;返回 `route_code`、入口说明、`agent_assessment`、`notification` 和 S99 的入口 `manual_review`。 | +| 旧 `S000/S999` 兼容映射 | 任务列表、任务详情 | 已完成第一版 | 旧数据继续可见;前端可按 `S000→S10`、`S999→S99` 展示统一文案。 | +| 42 路由元数据 | 任务列表筛选、订单任务时间线、任务详情标题、字段展示 | 已完成第一版 | 后端保存并返回 AI 原始 `result_type/ai_task_type/task_subtype`、`route_code` 和系统处理分类;前端不要只依赖系统主任务类型判断卡片。 | +| `unhandled_current_intents[]` 展示块 | 任务详情 | 已完成第一版 | 后端保存并在任务详情 `unhandled_intents[]` 返回未覆盖业务意图,只用于展示和源邮件查看,不自动建业务任务卡。 | +| `adapter_contract_error` | 任务详情、错误提示 | 已完成第一版 | 命中 P1/P2 未闭合或路由冲突时,任务详情 `adapter_contract_errors[]` 返回稳定错误 code 和原始片段,不转成 Fallback。 | +| type-known manual review 同卡解阻 | 任务详情复核 | 后置 | `manual_review` 不再全部等同 Fallback;已知业务卡型应返回原业务卡信息、`review_status`、`review_resolution` 和可编辑 pointer 字段。 | +| 复核场景订单归属确认 | 任务详情复核 | 后置 | 后端提供复核确认时的订单归属确认 / 选择能力;这不等于普通任务任意切换订单。 | 前端注意:不要把访问口令写入前端仓库、浏览器环境变量或构建产物;该接口只能由本地联调人员手动调用或由受控测试脚本调用。 @@ -404,7 +432,7 @@ GET /api/source-message-conversations/{externalConversationId} GET /api/reservation/tasks/{taskId} ``` -当前状态:后端已有任务详情接口,前端任务详情页可以接入。该接口已返回 `fields[]`、草稿、确认 payload、可处理状态和 OPERA 模拟操作;本轮已透出来源邮件会话字段,以及 `docs/import/20260708/任务卡前端展示字段表 3.0.xlsx` 中 P0 需要的字段元数据。 +当前状态:后端已有任务详情接口,前端任务详情页可以接入。该接口已返回 `fields[]`、草稿、确认 payload、可处理状态和 OPERA 模拟操作;本轮已透出来源邮件会话字段,以及旧 `docs/import/20260708/任务卡前端展示字段表 3.0.xlsx` 中 P0 需要的字段元数据。0711 P0 后续开发应迁移到 `docs/import/20260711/开发交付_P0冻结基线_2026-07-11/01_Adapter_Frontend/任务卡前端字段变更说明_3.0_to_当前版_2026-07-10.xlsx` 和同目录路由说明。 任务详情页面相关已完成接口: @@ -474,7 +502,7 @@ GET /api/reservation/tasks/{taskId} "system_task_type": "NEW_BOOKING", "task_card_type": "NEW_BOOKING", "task_status": "PENDING_CONFIRM", - "field_contract_version": "20260708-3.0", + "field_contract_version": "20260711-p0", "fields": [ { "row_number": 2, @@ -501,19 +529,26 @@ GET /api/reservation/tasks/{taskId} - 如果后端已有更细的字段来源或适用场景元数据,可后续再扩展 `field_source`、`applicable_scenario`,不作为本轮 P0 阻塞项。 - 前端默认不需要为 `GET /api/reservation/orders`、`GET /api/reservation/tasks`、`GET /api/reservation/orders/{orderId}` 自动拼 `hotel_id`;如已接入酒店选择器,可以传当前选中酒店,后端会校验访问权限。当前 `GET /api/reservation/tasks/{taskId}` 以及任务写操作 Controller 不接收 `hotel_id`;第一版先按 ID 定位,后续多酒店隔离 / 权限方案统一补齐。 -## 9. S000/S999 特殊只读任务与历史 Message Notification +## 9. S10/S99 源邮件只读通知卡与历史兼容 -当前状态:后端不提供独立 Message Notification 列表 / 详情接口。SuperAgent 新入口返回 `S000,source_message_id` 或 `S999,source_message_id` 时,后端会创建 `SOURCE_MESSAGE_ONLY` 只读特殊任务。前端已通过 `GET /api/reservation/tasks?task_type=SOURCE_MESSAGE_ONLY` 和 `GET /api/reservation/tasks/{taskId}` 展示,并在任务列表筛选中支持 `SOURCE_MESSAGE_ONLY`、`S000`、`S999`。 +当前状态:后端不提供独立 Message Notification 列表 / 详情接口。旧 SuperAgent 入口返回 `S000,source_message_id` 或 `S999,source_message_id` 时,后端会创建 `SOURCE_MESSAGE_ONLY` 只读特殊任务;0711 P0 结构化 `S10/S99` 也复用同一只读任务模型。前端已通过 `GET /api/reservation/tasks?task_type=SOURCE_MESSAGE_ONLY` 和 `GET /api/reservation/tasks/{taskId}` 展示,并在任务列表筛选中支持 `SOURCE_MESSAGE_ONLY`、`S000`、`S999`、`S10`、`S99`。 + +0711 P0 新入口已迁移为结构化 `S10/S99`: + +- `S10`:`result_type=source_message_review_notification`,`route_code=S10`,表示未匹配当前支持的业务事件。 +- `S99`:`result_type=source_message_review_notification`,`route_code=S99`,表示输入不足或无法形成业务素材包。 +- 旧 `S000` 前端语义映射为 `S10`。 +- 旧 `S999` 前端语义映射为 `S99`。 历史 `INFORMATIONAL_MESSAGE` 仍可通过任务列表 / 任务详情兼容展示,但新数据不要依赖它。 展示规则: -- `task_type=SOURCE_MESSAGE_ONLY`、`task_subtype=S000`:纯信息类邮件。 -- `task_type=SOURCE_MESSAGE_ONLY`、`task_subtype=S999`:无法形成业务素材包。 +- 旧 `task_type=SOURCE_MESSAGE_ONLY`、`task_subtype=S000/S999`:按只读源邮件通知卡展示。 +- 新 `route_code=S10/S99`:按只读源邮件通知卡展示。 - 任务列表可见,订单列表不可见。 - 任务详情只允许查看来源邮件、会话、附件和 SuperAgent 原始返回。 -- 任务详情通过 `source_message_only_result` 返回 `entry_result_code`、`entry_result_meaning`、`entry_result_description`、`entry_result_source_message_id` 和 `raw_answer`。 +- 任务详情通过 `source_message_only_result` 返回 `entry_result_code`、`entry_result_meaning`、`entry_result_description`、`entry_result_source_message_id`、`result_type`、`route_code`、`agent_assessment`、`notification`、S99 的入口 `manual_review` 和 `raw_answer`。 - 不显示编辑、确认、人工转换、执行 OPERA 或重试 OPERA 按钮。 - 不参与订单任务执行顺序阻塞。 @@ -556,6 +591,55 @@ GET /api/reservation/message-notifications/{taskId} } ``` +V3 S10 结构化详情当前增量: + +```json +{ + "task_id": "10010", + "readonly": true, + "result_type": "source_message_review_notification", + "route_code": "S10", + "agent_assessment": { + "status": "no_booking_action_detected", + "reason_code": "no_booking_action_detected" + }, + "notification": { + "notification_type": "source_message_review", + "show_source_message": true, + "requires_user_decision": true + }, + "manual_review": null, + "source_message_id": "30010" +} +``` + +## 9.1 V3 诊断展示块 + +任务详情接口已新增两个只读数组: + +| 字段 | 说明 | +| --- | --- | +| `adapter_contract_errors[]` | 同一 SuperAgent 入站批次中未生成任务的 Adapter 契约错误。 | +| `unhandled_intents[]` | 同一 SuperAgent 入站批次中无法映射到业务任务卡的未处理意图。 | + +数组元素字段: + +| 字段 | 说明 | +| --- | --- | +| `transition_id` | AI transition ID。 | +| `source_event_index` | AI current 事件序号。 | +| `array_index` | AI 返回数组顺序。 | +| `result_type` | AI 结果类型。 | +| `ai_task_type` | AI 原始任务类型。 | +| `task_subtype` | 业务动作 subtype,可能为空。 | +| `route_code` | V3 路由码。 | +| `system_process_category` | 系统处理分类。 | +| `adapter_error_code` | 契约错误代码;未处理意图通常为空。 | +| `adapter_error_message` | 契约错误安全摘要。 | +| `payload_fragment` | 对前端展示安全的 AI item 片段。 | + +前端注意:这两个数组不是任务队列,不提供编辑、确认、执行 OPERA 或重试入口;只用于解释为什么同一封邮件中的某些 event 没有变成业务任务。 + ## 10. 任务卡前端字段白名单元数据接口 是否需要该接口待确认。如果任务详情接口 `fields[]` 已透出 3.0 所需元数据,则第一版可以不做独立白名单接口;如果后续需要字段矩阵调试页、版本对齐页或前端预加载全部任务卡配置,再补独立接口。 @@ -572,13 +656,13 @@ GET /api/reservation/task-card-field-whitelist | --- | --- | --- | | `task_type` | 否 | 按系统主任务类型过滤。 | | `task_subtype` | 否 | 按任务卡 subtype 过滤。 | -| `version` | 否 | 字段白名单版本,例如 `20260708-3.0`。 | +| `version` | 否 | 字段白名单版本,例如 `20260711-p0`。 | 建议返参: ```json { - "version": "20260708-3.0", + "version": "20260711-p0", "items": [ { "task_type": "UPDATE_BOOKING", @@ -628,5 +712,5 @@ POST /api/reservation/tasks/{taskId}/order-binding - 订单列表、任务列表当前统一使用 `items + page` 分页结构;邮件会话详情不分页,返回同一会话全部邮件。 - 邮件会话详情接口已优先使用 `GET /api/source-messages/{sourceMessageId}/conversation`。 - 任务详情 `fields[]` 已由后端直接透出 P0 需要的 3.0 元数据;独立字段白名单接口后置。 -- 独立 Message Notification 页面继续后置;新入口 S000/S999 第一版先在任务列表和任务详情展示。 +- 独立 Message Notification 页面继续后置;旧 S000/S999 和新 S10/S99 都先在任务列表和任务详情展示。 - 邮件会话全文读取的审计策略由后端内部处理;前端不保存原文读取 key。 diff --git a/docs/project/requirements/M002-order-task-workflow-v3.md b/docs/project/requirements/M002-order-task-workflow-v3.md index 5d791d6..073e055 100644 --- a/docs/project/requirements/M002-order-task-workflow-v3.md +++ b/docs/project/requirements/M002-order-task-workflow-v3.md @@ -6,7 +6,7 @@ | --- | --- | | 文档版本 | 0.1 | | 日期 | 2026-07-11 | -| 状态 | 0711 P0 基线确认版;后端已完成 M002 V3 CP1-CP2 入站解析与路由基线 | +| 状态 | 0711 P0 基线确认版;后端已完成 M002 V3 CP1-CP4 入站、路由持久化和列表 / 详情展示基线 | | 适用范围 | SourceMessage 之后的 SuperAgent 输出适配、任务路由、只读通知卡、人工复核同卡解阻、前后端协作边界 | | 主要读者 | 产品、后端、前端、测试、SuperAgent 对接方、后续协作 agent | @@ -310,7 +310,7 @@ Agent payload 不可变。本系统在同一张卡上维护复核状态: - 任务列表支持展示旧 `S000/S999` 和新 `S10/S99`,但新文案以 `S10/S99` 为主。 - `S10/S99` 只读卡只出现在任务列表和任务详情,不出现在订单列表。 -- 任务列表不应仅按旧 `SOURCE_MESSAGE_ONLY` 判断;应兼容后端后续返回的 `route_code=S10/S99`、`result_type=source_message_review_notification`。 +- 任务列表不应仅按旧 `SOURCE_MESSAGE_ONLY` 判断;当前后端已返回 `route_code=S10/S99`、`result_type=source_message_review_notification`。 - type-known manual review 不再统一展示成 Fallback,应展示原业务卡名称和 subtype,并显示复核状态。 - 复核解阻页需要能提交 `field_overrides[]`,并在复核场景下确认订单归属。 - `unhandled_current_intents[]` 只作为展示块,不提供执行按钮。 @@ -324,8 +324,8 @@ V3 建议拆成以下 checkpoint,避免一次性重构过大: | --- | --- | --- | | M002-V3-CP1 | 文档和枚举基线 | 已完成:建立 42 路由枚举 / 稳定配置,作为入站路由唯一代码源 | | M002-V3-CP2 | 入站解析兼容 | 已完成:正式回调支持结构化 S10/S99 和 V3 业务根,保留旧 S000/S999 兼容 | -| M002-V3-CP3 | 路由持久化 | 部分完成:已保存 AI 原始三元组、route_code、system_process_category、unhandled_current_intents 和 adapter_contract_error;message_events / unhandled_current_intents 的前端完整展示仍后置 | -| M002-V3-CP4 | 列表 / 详情展示 | 任务列表和详情支持 S10/S99、42 路由只读展示、type-known review 展示 | +| M002-V3-CP3 | 路由持久化 | 已完成第一版:已保存 AI 原始三元组、route_code、system_process_category、unhandled_current_intents 和 adapter_contract_error | +| M002-V3-CP4 | 列表 / 详情展示 | 已完成第一版:任务列表、订单任务时间线和任务详情透出 V3 路由字段;任务详情支持 S10/S99 入口通知结构、unhandled intent 展示块和 adapter contract error 展示块 | | M002-V3-CP5 | 同卡复核解阻 | 支持 review_status、field_overrides、复核场景订单归属确认和 READY 流转 | | M002-V3-CP6 | P0 fixtures 回归 | 引入 0711 P0 fixtures / validator 作为后端适配测试参考,补充项目级测试 | @@ -343,7 +343,7 @@ V3 P0 不做以下事项: ## 14. 当前代码现状提醒 -截至 M002 V3 CP1-CP2 落地后,当前后端已经实现: +截至 M002 V3 CP4 落地后,当前后端已经实现: - `S000/S999` 文本结果兼容处理。 - 结构化 `S10/S99` 入站处理,复用 `SOURCE_MESSAGE_ONLY` 只读特殊任务。 @@ -351,6 +351,9 @@ V3 P0 不做以下事项: - V3 业务根 `source_message + message_events[]` 基础解析;能派生到稳定路由的 event 创建业务任务,无法派生的 event、显式 `contract_errors`、根 `missing_fields`、不完整 `manual_review` 和不完整 parent split 候选只落 `adapter_contract_error` transition。 - `unhandled_current_intents[]` 只落 `UNHANDLED_CURRENT_INTENT` transition,不创建订单和任务,也不伪装成 adapter 契约错误。 - AI transition 最小保存 `route_code`、`system_process_category`、`adapter_error_code`、`adapter_error_message`。 +- 任务列表、订单任务时间线和任务详情顶层透出 `result_type`、`ai_task_type`、`task_subtype`、`route_code`、`system_process_category`。 +- `SOURCE_MESSAGE_ONLY` 任务详情透出 `source_message_only_result.result_type`、`route_code`、`agent_assessment`、`notification`、`manual_review` 和 `raw_answer`。 +- 业务任务详情按同一 AI 批次透出 `adapter_contract_errors[]` 与 `unhandled_intents[]` 只读展示块。 - 订单 / 任务列表、任务详情、草稿保存、最终确认、OPERA 模拟骨架和审计列表。 - SuperAgent 查询上下文接口 1、2,以及邮件会话相关查询。 @@ -360,5 +363,4 @@ V3 P0 不做以下事项: - 复核场景订单归属确认。 - `manual_review.missing_fields[]` 到任务卡可编辑字段白名单的完整映射校验。 - `source_message.source_message_id` 缺失时按 V3 typed `infrastructure_input_error` 结构响应。 -- 任务列表 / 详情完整透出 V3 `route_code`、入口通知结构、unhandled intent 展示块和 adapter contract error 展示。 - 真实 OPERA / OHIP、普通任务任意切换订单、P0 fixtures / validator 全量回归。 diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/dto/ReservationAiQueryTaskSnapshot.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/dto/ReservationAiQueryTaskSnapshot.java index b5ce5f0..fe2eae2 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/dto/ReservationAiQueryTaskSnapshot.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/dto/ReservationAiQueryTaskSnapshot.java @@ -29,6 +29,10 @@ import java.time.LocalDateTime; * @param transitionSourceEventIndex AI transition 来源事件序号 * @param catalogCode Skill 目录代码 * @param skillId Skill 标识 + * @param routeCode M002 V3 路由码 + * @param systemProcessCategory 系统处理分类 + * @param adapterErrorCode Adapter 契约错误代码 + * @param adapterErrorMessage Adapter 契约错误说明 */ public record ReservationAiQueryTaskSnapshot( Long id, @@ -54,6 +58,10 @@ public record ReservationAiQueryTaskSnapshot( LocalDateTime updatedAt, Integer transitionSourceEventIndex, String catalogCode, - String skillId + String skillId, + String routeCode, + String systemProcessCategory, + String adapterErrorCode, + String adapterErrorMessage ) { } diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/dto/ReservationAiTransitionSnapshot.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/dto/ReservationAiTransitionSnapshot.java new file mode 100644 index 0000000..73e0772 --- /dev/null +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/dto/ReservationAiTransitionSnapshot.java @@ -0,0 +1,55 @@ +package cn.nianxx.thhotel.workflows.reservation.common.dto; + +import java.time.LocalDateTime; + +/** + * AI transition 展示快照。用于任务详情展示同一批次的 V3 路由、契约错误和未处理意图,不直接暴露 Entity。 + * + * @param id AI transition 主键 + * @param hotelId 酒店上下文 ID + * @param batchId AI 入站批次 ID + * @param sourceMessageId 来源 SourceMessage Inbox ID + * @param sourceEventIndex AI current 事件序号 + * @param arrayIndex AI 返回数组顺序,从 1 开始 + * @param executionOrder 初始执行顺序 + * @param catalogCode Skill 目录代码 + * @param skillId Skill 标识 + * @param resultType AI 结果类型 + * @param aiTaskType AI 原始任务类型 + * @param routeCode M002 V3 路由码 + * @param systemProcessCategory 系统处理分类 + * @param systemTaskType 系统主任务类型 + * @param taskCardType 任务卡类型 + * @param taskSubtype 业务动作 subtype + * @param currentOrHistory 当前或历史标识 + * @param adapterErrorCode Adapter 契约错误代码 + * @param adapterErrorMessage Adapter 契约错误说明 + * @param aiPayloadJson AI 原始 item JSON + * @param createdAt 创建 UTC 时间 + * @param updatedAt 最近更新 UTC 时间 + */ +public record ReservationAiTransitionSnapshot( + Long id, + String hotelId, + Long batchId, + Long sourceMessageId, + Integer sourceEventIndex, + Integer arrayIndex, + Integer executionOrder, + String catalogCode, + String skillId, + String resultType, + String aiTaskType, + String routeCode, + String systemProcessCategory, + String systemTaskType, + String taskCardType, + String taskSubtype, + String currentOrHistory, + String adapterErrorCode, + String adapterErrorMessage, + String aiPayloadJson, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { +} diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationAiTransitionDisplayResult.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationAiTransitionDisplayResult.java new file mode 100644 index 0000000..619a447 --- /dev/null +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationAiTransitionDisplayResult.java @@ -0,0 +1,44 @@ +package cn.nianxx.thhotel.workflows.reservation.common.result; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * 任务详情中的 AI transition 展示块。用于展示未建任务的关联 V3 诊断信息。 + * + * @param transitionId AI transition ID,按字符串返回避免前端长整型精度问题 + * @param sourceEventIndex AI current 事件序号 + * @param arrayIndex AI 返回数组顺序 + * @param resultType AI 结果类型 + * @param aiTaskType AI 原始任务类型 + * @param taskSubtype 业务动作 subtype + * @param routeCode M002 V3 路由码 + * @param systemProcessCategory 系统处理分类 + * @param adapterErrorCode Adapter 契约错误代码 + * @param adapterErrorMessage Adapter 契约错误说明 + * @param payloadFragment 对前端展示安全的 AI item 片段,当前不包含邮件原文 + */ +public record ReservationAiTransitionDisplayResult( + @JsonProperty("transition_id") + String transitionId, + @JsonProperty("source_event_index") + Integer sourceEventIndex, + @JsonProperty("array_index") + Integer arrayIndex, + @JsonProperty("result_type") + String resultType, + @JsonProperty("ai_task_type") + String aiTaskType, + @JsonProperty("task_subtype") + String taskSubtype, + @JsonProperty("route_code") + String routeCode, + @JsonProperty("system_process_category") + String systemProcessCategory, + @JsonProperty("adapter_error_code") + String adapterErrorCode, + @JsonProperty("adapter_error_message") + String adapterErrorMessage, + @JsonProperty("payload_fragment") + Object payloadFragment +) { +} diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationOrderTaskTimelineItemResult.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationOrderTaskTimelineItemResult.java index fc88c18..0969b43 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationOrderTaskTimelineItemResult.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationOrderTaskTimelineItemResult.java @@ -8,7 +8,11 @@ import java.time.OffsetDateTime; * * @param taskId 任务 ID * @param taskType 系统主任务类型 + * @param resultType AI 结果类型 + * @param aiTaskType AI 原始任务类型 * @param taskSubtype 任务 subtype + * @param routeCode M002 V3 路由码 + * @param systemProcessCategory 系统处理分类 * @param taskStatus 任务状态 * @param cardName 任务卡展示名称 * @param queueSequence 同订单队列顺序 @@ -28,8 +32,16 @@ public record ReservationOrderTaskTimelineItemResult( String taskId, @JsonProperty("task_type") String taskType, + @JsonProperty("result_type") + String resultType, + @JsonProperty("ai_task_type") + String aiTaskType, @JsonProperty("task_subtype") String taskSubtype, + @JsonProperty("route_code") + String routeCode, + @JsonProperty("system_process_category") + String systemProcessCategory, @JsonProperty("task_status") String taskStatus, @JsonProperty("card_name") diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationSourceMessageOnlyResult.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationSourceMessageOnlyResult.java index 8fd8cbb..6fdfe44 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationSourceMessageOnlyResult.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationSourceMessageOnlyResult.java @@ -9,6 +9,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; * @param entryResultMeaning 入口结果语义 * @param entryResultDescription 入口结果中文说明 * @param entryResultSourceMessageId SuperAgent 回传的外部来源消息 ID + * @param resultType AI 结果类型 + * @param routeCode M002 V3 路由码 + * @param agentAssessment S10/S99 入口评估对象 + * @param notification S10/S99 入口通知对象 + * @param manualReview S99 人工复核对象;S10 通常为空 * @param rawAnswer SuperAgent 原始文本返回 */ public record ReservationSourceMessageOnlyResult( @@ -20,6 +25,16 @@ public record ReservationSourceMessageOnlyResult( String entryResultDescription, @JsonProperty("entry_result_source_message_id") String entryResultSourceMessageId, + @JsonProperty("result_type") + String resultType, + @JsonProperty("route_code") + String routeCode, + @JsonProperty("agent_assessment") + Object agentAssessment, + @JsonProperty("notification") + Object notification, + @JsonProperty("manual_review") + Object manualReview, @JsonProperty("raw_answer") String rawAnswer ) { diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationTaskDetailResult.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationTaskDetailResult.java index c8bb068..4b42590 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationTaskDetailResult.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationTaskDetailResult.java @@ -17,6 +17,11 @@ import java.util.List; * @param conversationMessageCount 同一外部邮件会话下的消息数量 * @param systemTaskType 系统主任务类型 * @param taskCardType 任务卡类型 + * @param resultType AI 结果类型 + * @param aiTaskType AI 原始任务类型 + * @param taskSubtype 业务动作 subtype + * @param routeCode M002 V3 路由码 + * @param systemProcessCategory 系统处理分类 * @param taskStatus 任务状态 * @param fieldContractVersion 字段矩阵契约版本 * @param draftPayload 草稿 payload @@ -25,6 +30,8 @@ import java.util.List; * @param availability 当前可处理状态 * @param fields 按字段矩阵生成的字段列表 * @param operaOperations OPERA 模拟操作列表 + * @param adapterContractErrors 同批次未建任务的 Adapter 契约错误展示块 + * @param unhandledIntents 同批次未建任务的未处理意图展示块 */ public record ReservationTaskDetailResult( @JsonProperty("task_id") @@ -47,6 +54,16 @@ public record ReservationTaskDetailResult( String systemTaskType, @JsonProperty("task_card_type") String taskCardType, + @JsonProperty("result_type") + String resultType, + @JsonProperty("ai_task_type") + String aiTaskType, + @JsonProperty("task_subtype") + String taskSubtype, + @JsonProperty("route_code") + String routeCode, + @JsonProperty("system_process_category") + String systemProcessCategory, @JsonProperty("task_status") String taskStatus, @JsonProperty("field_contract_version") @@ -60,6 +77,10 @@ public record ReservationTaskDetailResult( ReservationTaskAvailabilityResult availability, List fields, @JsonProperty("opera_operations") - List operaOperations + List operaOperations, + @JsonProperty("adapter_contract_errors") + List adapterContractErrors, + @JsonProperty("unhandled_intents") + List unhandledIntents ) { } diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationTaskWorkbenchItemResult.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationTaskWorkbenchItemResult.java index 3094bf3..968ce67 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationTaskWorkbenchItemResult.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/common/result/ReservationTaskWorkbenchItemResult.java @@ -12,7 +12,11 @@ import java.time.OffsetDateTime; * @param displayOrderKey 前端优先展示的订单业务号或临时订单号 * @param temporaryOrderNo 临时订单号 * @param taskType 系统主任务类型 + * @param resultType AI 结果类型 + * @param aiTaskType AI 原始任务类型 * @param taskSubtype 任务 subtype + * @param routeCode M002 V3 路由码 + * @param systemProcessCategory 系统处理分类 * @param taskStatus 任务状态 * @param cardName 任务卡展示名称,第一版使用任务卡类型 * @param queueSequence 同订单队列顺序 @@ -41,8 +45,16 @@ public record ReservationTaskWorkbenchItemResult( String temporaryOrderNo, @JsonProperty("task_type") String taskType, + @JsonProperty("result_type") + String resultType, + @JsonProperty("ai_task_type") + String aiTaskType, @JsonProperty("task_subtype") String taskSubtype, + @JsonProperty("route_code") + String routeCode, + @JsonProperty("system_process_category") + String systemProcessCategory, @JsonProperty("task_status") String taskStatus, @JsonProperty("card_name") diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/repository/MybatisReservationAiWorkflowRepository.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/repository/MybatisReservationAiWorkflowRepository.java index dcd5bd0..ae256d2 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/repository/MybatisReservationAiWorkflowRepository.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/repository/MybatisReservationAiWorkflowRepository.java @@ -5,6 +5,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiBatchSnap import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiQueryOrderSnapshot; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiQueryTaskSnapshot; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionDraft; +import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionSnapshot; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogDraft; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogSnapshot; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationAttemptDraft; @@ -288,6 +289,40 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork return Optional.ofNullable(entity).map(this::toTaskCardSnapshot); } + /** + * 按 AI transition ID 查询展示快照,用于任务详情补充 V3 入站路由上下文。 + */ + @Override + public Optional findTransitionById(String hotelId, Long transitionId) { + if (transitionId == null) { + return Optional.empty(); + } + ReservationAiTransitionEntity entity = transitionMapper.selectOne( + Wrappers.lambdaQuery() + .eq(ReservationAiTransitionEntity::getHotelId, hotelId) + .eq(ReservationAiTransitionEntity::getId, transitionId) + .last("LIMIT 1")); + return Optional.ofNullable(entity).map(this::toAiTransitionSnapshot); + } + + /** + * 按 AI 批次查询全部 transition 展示快照,按 AI 返回顺序稳定排序。 + */ + @Override + public List findTransitionsByBatchId(String hotelId, Long batchId) { + if (batchId == null) { + return List.of(); + } + return transitionMapper.selectList(Wrappers.lambdaQuery() + .eq(ReservationAiTransitionEntity::getHotelId, hotelId) + .eq(ReservationAiTransitionEntity::getBatchId, batchId) + .orderByAsc(ReservationAiTransitionEntity::getArrayIndex) + .orderByAsc(ReservationAiTransitionEntity::getId)) + .stream() + .map(this::toAiTransitionSnapshot) + .toList(); + } + /** * 查询同订单前置队列任务,Message Notification 等非队列任务不会进入结果。 */ @@ -1021,7 +1056,40 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork entity.getUpdatedAt(), transition == null ? null : transition.getSourceEventIndex(), transition == null ? null : transition.getCatalogCode(), - transition == null ? null : transition.getSkillId()); + transition == null ? null : transition.getSkillId(), + transition == null ? null : transition.getRouteCode(), + transition == null ? null : transition.getSystemProcessCategory(), + transition == null ? null : transition.getAdapterErrorCode(), + transition == null ? null : transition.getAdapterErrorMessage()); + } + + /** + * 转换 AI transition 实体为展示快照。 + */ + private ReservationAiTransitionSnapshot toAiTransitionSnapshot(ReservationAiTransitionEntity entity) { + return new ReservationAiTransitionSnapshot( + entity.getId(), + entity.getHotelId(), + entity.getBatchId(), + entity.getSourceMessageId(), + entity.getSourceEventIndex(), + entity.getArrayIndex(), + entity.getExecutionOrder(), + entity.getCatalogCode(), + entity.getSkillId(), + entity.getResultType(), + entity.getAiTaskType(), + entity.getRouteCode(), + entity.getSystemProcessCategory(), + entity.getSystemTaskType(), + entity.getTaskCardType(), + entity.getTaskSubtype(), + entity.getCurrentOrHistory(), + entity.getAdapterErrorCode(), + entity.getAdapterErrorMessage(), + entity.getAiPayloadJson(), + entity.getCreatedAt(), + entity.getUpdatedAt()); } /** diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/repository/ReservationAiWorkflowRepository.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/repository/ReservationAiWorkflowRepository.java index 4a2a6e8..a95873e 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/repository/ReservationAiWorkflowRepository.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/repository/ReservationAiWorkflowRepository.java @@ -4,6 +4,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiBatchDraf import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiBatchSnapshot; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiQueryOrderSnapshot; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiQueryTaskSnapshot; +import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionSnapshot; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionDraft; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogDraft; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogSnapshot; @@ -82,6 +83,16 @@ public interface ReservationAiWorkflowRepository { */ Optional findTaskCardByTaskId(String hotelId, Long taskId); + /** + * 按 AI transition ID 查询展示快照,用于任务详情补充 V3 路由和批次上下文。 + */ + Optional findTransitionById(String hotelId, Long transitionId); + + /** + * 查询同一 AI 批次下的 transition 展示快照,用于任务详情展示未建任务的契约错误和未处理意图。 + */ + List findTransitionsByBatchId(String hotelId, Long batchId); + /** * 查询同订单中排在当前任务之前且参与队列的任务。 */ diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationFrontendQueryServiceImpl.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationFrontendQueryServiceImpl.java index 7572a54..8342e1b 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationFrontendQueryServiceImpl.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationFrontendQueryServiceImpl.java @@ -329,7 +329,11 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ displayOrderKey(order), order == null || isHiddenSystemOrder(order) ? null : order.temporaryOrderCode(), task.systemTaskType(), + task.resultType(), + task.aiTaskType(), task.taskSubtype(), + task.routeCode(), + task.systemProcessCategory(), task.taskStatus(), task.taskCardType(), task.executionOrder(), @@ -356,7 +360,11 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ return new ReservationOrderTaskTimelineItemResult( task.id().toString(), task.systemTaskType(), + task.resultType(), + task.aiTaskType(), task.taskSubtype(), + task.routeCode(), + task.systemProcessCategory(), task.taskStatus(), task.taskCardType(), task.executionOrder(), diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskWorkflowServiceImpl.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskWorkflowServiceImpl.java index 7d39938..b951809 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskWorkflowServiceImpl.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationTaskWorkflowServiceImpl.java @@ -4,6 +4,7 @@ import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageSummaryRespons import cn.nianxx.thhotel.platform.message.service.SourceMessageQueryService; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogDraft; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogSnapshot; +import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionSnapshot; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationAttemptDraft; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationAttemptSnapshot; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationDraft; @@ -12,6 +13,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOrderSnapsh import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskCardFieldDefinition; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskCardSnapshot; import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskSnapshot; +import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationAiSystemProcessCategory; import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationBusinessKeySource; import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOperaAttemptStatus; import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOperaOperationStatus; @@ -24,6 +26,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.request.ManualReviewConver import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationOperaSimulationRequest; import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationTaskPayloadMutationRequest; import cn.nianxx.thhotel.workflows.reservation.common.result.ManualReviewConversionResult; +import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationAiTransitionDisplayResult; import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOperaOperationAttemptResult; import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOperaOperationResult; import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationSourceMessageOnlyResult; @@ -112,6 +115,8 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl List operaOperations = findOperaOperationResults(task); ReservationSourceMessageOnlyResult sourceMessageOnlyResult = buildSourceMessageOnlyResult(task, taskCard); SourceMessageDetailContext sourceContext = findSourceMessageDetailContext(task); + ReservationAiTransitionSnapshot transition = findTaskTransition(task); + List relatedTransitions = findRelatedTransitions(task, transition); return new ReservationTaskDetailResult( task.id().toString(), task.orderId().toString(), @@ -123,6 +128,11 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl sourceContext.conversationMessageCount(), task.systemTaskType(), task.taskCardType(), + task.resultType(), + task.aiTaskType(), + task.taskSubtype(), + transition == null ? null : transition.routeCode(), + transition == null ? null : transition.systemProcessCategory(), task.taskStatus(), taskCard.fieldContractVersion(), jsonPayloadToObject(taskCard.draftPayloadJson()), @@ -130,7 +140,13 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl sourceMessageOnlyResult, availability, fields, - operaOperations); + operaOperations, + buildTransitionDisplayResults( + relatedTransitions, + ReservationAiSystemProcessCategory.ADAPTER_CONTRACT_ERROR), + buildTransitionDisplayResults( + relatedTransitions, + ReservationAiSystemProcessCategory.UNHANDLED_CURRENT_INTENT)); } /** @@ -854,9 +870,86 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl textValue(payload, "entry_result_meaning"), textValue(payload, "entry_result_description"), textValue(payload, "source_message_id"), + textValue(payload, "result_type"), + textValue(payload, "route_code"), + rawAnswerField(payload, "agent_assessment"), + rawAnswerField(payload, "notification"), + rawAnswerField(payload, "manual_review"), textValue(payload, "raw_answer")); } + /** + * 查询当前任务对应的 AI transition,用于详情展示 V3 route 和系统处理分类。 + */ + private ReservationAiTransitionSnapshot findTaskTransition(ReservationTaskSnapshot task) { + return workflowRepository.findTransitionById(task.hotelId(), task.aiTransitionId()).orElse(null); + } + + /** + * 查询同一 AI 批次下的关联 transition,用于展示没有生成任务的诊断类结果。 + */ + private List findRelatedTransitions( + ReservationTaskSnapshot task, + ReservationAiTransitionSnapshot transition) { + if (transition == null) { + return List.of(); + } + return workflowRepository.findTransitionsByBatchId(task.hotelId(), transition.batchId()); + } + + /** + * 按系统处理分类构造详情页展示块。 + */ + private List buildTransitionDisplayResults( + List transitions, + ReservationAiSystemProcessCategory category) { + if (transitions == null || transitions.isEmpty() || category == null) { + return List.of(); + } + return transitions.stream() + .filter(transition -> category.name().equals(transition.systemProcessCategory())) + .map(this::toTransitionDisplayResult) + .toList(); + } + + /** + * 转换 AI transition 快照为前端详情展示块。 + */ + private ReservationAiTransitionDisplayResult toTransitionDisplayResult(ReservationAiTransitionSnapshot transition) { + return new ReservationAiTransitionDisplayResult( + transition.id().toString(), + transition.sourceEventIndex(), + transition.arrayIndex(), + transition.resultType(), + transition.aiTaskType(), + transition.taskSubtype(), + transition.routeCode(), + transition.systemProcessCategory(), + transition.adapterErrorCode(), + transition.adapterErrorMessage(), + jsonPayloadToObject(transition.aiPayloadJson())); + } + + /** + * 从 S10/S99 原始回答中提取指定对象字段;旧 S000/S999 文本无法解析时返回 null。 + */ + private Object rawAnswerField(JsonNode payload, String fieldName) { + String rawAnswer = textValue(payload, "raw_answer"); + if (trimToNull(rawAnswer) == null || trimToNull(fieldName) == null) { + return null; + } + try { + JsonNode root = objectMapper.readTree(rawAnswer); + JsonNode fieldNode = root.path(fieldName); + if (fieldNode.isMissingNode() || fieldNode.isNull()) { + return null; + } + return objectMapper.convertValue(fieldNode, Object.class); + } catch (Exception exception) { + return null; + } + } + /** * 写入草稿保存或最终确认审计。审计只记录 payload 摘要,不保存完整客户字段。 */ diff --git a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java index bb6a8d6..d2706a1 100644 --- a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java +++ b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java @@ -745,6 +745,78 @@ class SuperAgentTaskResultControllerTest { assertThat(taskCount).isEqualTo(1L); } + @Test + void shouldExposeStructuredS99NotificationInTaskListAndDetail() throws Exception { + captureSourceMessage("mail-v3-s99-frontend-visible-001"); + String body = """ + { + "source_message": { + "source_message_id": "mail-v3-s99-frontend-visible-001", + "subject": null, + "from": null, + "cc": [], + "received_at": null, + "source_channel": "Email" + }, + "route_code": "S99", + "handler_type": "main_agent_outcome", + "result_type": "source_message_review_notification", + "current_or_history": "current", + "agent_assessment": { + "status": "insufficient_business_material", + "reason_code": "cannot_form_business_material_package", + "automation_action": "none" + }, + "notification": { + "required": true, + "notification_type": "source_message_review", + "show_source_message": true, + "requires_user_decision": true + }, + "manual_review": { + "reason_code": "cannot_form_business_material_package", + "review_notes": "需要人工查看原邮件。" + } + } + """; + + MvcResult result = mockMvc.perform(signedPost(body, "nonce-v3-s99-frontend-visible-001")) + .andExpect(status().isCreated()) + .andReturn(); + String taskId = com.jayway.jsonpath.JsonPath.read( + result.getResponse().getContentAsString(), + "$.items[0].task_id" + ); + + mockMvc.perform(get("/api/reservation/tasks") + .param("hotel_id", "HOTEL-TEST") + .param("task_type", "SOURCE_MESSAGE_ONLY") + .param("keyword", "mail-v3-s99-frontend-visible-001")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.items[0].task_id").value(taskId)) + .andExpect(jsonPath("$.items[0].result_type").value("source_message_review_notification")) + .andExpect(jsonPath("$.items[0].ai_task_type").value("S99")) + .andExpect(jsonPath("$.items[0].route_code").value("S99")) + .andExpect(jsonPath("$.items[0].system_process_category").value("SOURCE_MESSAGE_NOTIFICATION")); + + mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.result_type").value("source_message_review_notification")) + .andExpect(jsonPath("$.ai_task_type").value("S99")) + .andExpect(jsonPath("$.task_subtype").value("S99")) + .andExpect(jsonPath("$.route_code").value("S99")) + .andExpect(jsonPath("$.system_process_category").value("SOURCE_MESSAGE_NOTIFICATION")) + .andExpect(jsonPath("$.source_message_only_result.result_type") + .value("source_message_review_notification")) + .andExpect(jsonPath("$.source_message_only_result.route_code").value("S99")) + .andExpect(jsonPath("$.source_message_only_result.agent_assessment.status") + .value("insufficient_business_material")) + .andExpect(jsonPath("$.source_message_only_result.notification.notification_type") + .value("source_message_review")) + .andExpect(jsonPath("$.source_message_only_result.manual_review.reason_code") + .value("cannot_form_business_material_package")); + } + @Test void shouldCreateBusinessTaskFromV3BusinessRootMessageEvent() throws Exception { SourceMessageCaptureResult source = captureSourceMessage("mail-v3-business-root-001"); @@ -968,6 +1040,116 @@ class SuperAgentTaskResultControllerTest { assertThat(taskCount).isEqualTo(1L); } + @Test + void shouldExposeV3RouteAndRelatedTransitionBlocksInFrontendQueries() throws Exception { + captureSourceMessage("mail-v3-frontend-route-blocks-001"); + String body = """ + { + "source_message": { + "source_message_id": "mail-v3-frontend-route-blocks-001", + "subject": "Mixed events", + "from": null, + "cc": [], + "received_at": null, + "source_channel": "Email" + }, + "message_events": [ + { + "event_type": "Trace", + "source_event_index": "E1", + "current_or_history": "current", + "case_keys": { + "group_code": "GRP-V3-FRONTEND-BLOCK-001", + "confirmation_number": null + }, + "extracted_fields": {}, + "contract_errors": [ + { + "path": "/extracted_fields", + "code": "unsupported_contract" + } + ], + "manual_review": null + }, + { + "event_type": "New Booking", + "source_event_index": "E2", + "current_or_history": "current", + "case_keys": { + "group_code": null, + "confirmation_number": "CNF-V3-FRONTEND-BLOCK-001" + }, + "extracted_fields": { + "booking_object_type": "FIT Reservation" + }, + "manual_review": null + } + ], + "case_candidates": [], + "extraction_warnings": [], + "unhandled_current_intents": [ + { + "intent_type": "unsupported_active_request", + "reason_code": "no_task_card_available" + } + ] + } + """; + + MvcResult result = mockMvc.perform(signedPost(body, "nonce-v3-frontend-route-blocks-001")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.accepted_count").value(3)) + .andReturn(); + String orderId = com.jayway.jsonpath.JsonPath.read( + result.getResponse().getContentAsString(), + "$.items[1].order_id" + ); + String taskId = com.jayway.jsonpath.JsonPath.read( + result.getResponse().getContentAsString(), + "$.items[1].task_id" + ); + + mockMvc.perform(get("/api/reservation/tasks") + .param("hotel_id", "HOTEL-TEST") + .param("keyword", "mail-v3-frontend-route-blocks-001")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.items[0].task_id").value(taskId)) + .andExpect(jsonPath("$.items[0].result_type").value("normal_task")) + .andExpect(jsonPath("$.items[0].ai_task_type").value("New Booking")) + .andExpect(jsonPath("$.items[0].route_code").value("R01_NEW_FIT_RESERVATION_NORMAL")) + .andExpect(jsonPath("$.items[0].system_process_category").value("BUSINESS_TASK")); + + mockMvc.perform(get("/api/reservation/orders/{orderId}", orderId) + .param("hotel_id", "HOTEL-TEST") + .param("include_tasks", "true")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.tasks[0].task_id").value(taskId)) + .andExpect(jsonPath("$.tasks[0].result_type").value("normal_task")) + .andExpect(jsonPath("$.tasks[0].ai_task_type").value("New Booking")) + .andExpect(jsonPath("$.tasks[0].route_code").value("R01_NEW_FIT_RESERVATION_NORMAL")) + .andExpect(jsonPath("$.tasks[0].system_process_category").value("BUSINESS_TASK")); + + mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.result_type").value("normal_task")) + .andExpect(jsonPath("$.ai_task_type").value("New Booking")) + .andExpect(jsonPath("$.task_subtype").value("new_fit_reservation")) + .andExpect(jsonPath("$.route_code").value("R01_NEW_FIT_RESERVATION_NORMAL")) + .andExpect(jsonPath("$.system_process_category").value("BUSINESS_TASK")) + .andExpect(jsonPath("$.adapter_contract_errors.length()").value(1)) + .andExpect(jsonPath("$.adapter_contract_errors[0].adapter_error_code") + .value("EVENT_CONTRACT_ERROR")) + .andExpect(jsonPath("$.adapter_contract_errors[0].payload_fragment.contract_errors[0].code") + .value("unsupported_contract")) + .andExpect(jsonPath("$.unhandled_intents.length()").value(1)) + .andExpect(jsonPath("$.unhandled_intents[0].route_code") + .value("R42_UNHANDLED_CURRENT_INTENT")) + .andExpect(jsonPath("$.unhandled_intents[0].system_process_category") + .value("UNHANDLED_CURRENT_INTENT")) + .andExpect(jsonPath("$.unhandled_intents[0].payload_fragment.intent_type") + .value("unsupported_active_request")); + } + @Test void shouldPersistAdapterContractErrorForIncompleteV3ManualReview() throws Exception { SourceMessageCaptureResult source = captureSourceMessage("mail-v3-manual-review-incomplete-001");