完成M002 V3 CP4列表详情展示

This commit is contained in:
andy
2026-07-11 15:53:22 +08:00
parent 9bcbe8a3ac
commit 6416c1b78a
15 changed files with 647 additions and 30 deletions

View File

@@ -48,9 +48,9 @@
| `GET /api/auth/me` | 恢复当前登录态 | 前端启动后带 `Authorization: Bearer <access_token>` 调用401 时清理 token 并进入登录页。 |
| `POST /api/auth/logout` | 登出当前 session | 带 `Authorization: Bearer <access_token>`;成功后前端必须清理本地 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 接入继续后置。

View File

@@ -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。

View File

@@ -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_errormessage_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 全量回归。

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -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
) {
}

View File

@@ -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")

View File

@@ -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
) {

View File

@@ -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<ReservationTaskFieldResult> fields,
@JsonProperty("opera_operations")
List<ReservationOperaOperationResult> operaOperations
List<ReservationOperaOperationResult> operaOperations,
@JsonProperty("adapter_contract_errors")
List<ReservationAiTransitionDisplayResult> adapterContractErrors,
@JsonProperty("unhandled_intents")
List<ReservationAiTransitionDisplayResult> unhandledIntents
) {
}

View File

@@ -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")

View File

@@ -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<ReservationAiTransitionSnapshot> findTransitionById(String hotelId, Long transitionId) {
if (transitionId == null) {
return Optional.empty();
}
ReservationAiTransitionEntity entity = transitionMapper.selectOne(
Wrappers.<ReservationAiTransitionEntity>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<ReservationAiTransitionSnapshot> findTransitionsByBatchId(String hotelId, Long batchId) {
if (batchId == null) {
return List.of();
}
return transitionMapper.selectList(Wrappers.<ReservationAiTransitionEntity>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());
}
/**

View File

@@ -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<ReservationTaskCardSnapshot> findTaskCardByTaskId(String hotelId, Long taskId);
/**
* 按 AI transition ID 查询展示快照,用于任务详情补充 V3 路由和批次上下文。
*/
Optional<ReservationAiTransitionSnapshot> findTransitionById(String hotelId, Long transitionId);
/**
* 查询同一 AI 批次下的 transition 展示快照,用于任务详情展示未建任务的契约错误和未处理意图。
*/
List<ReservationAiTransitionSnapshot> findTransitionsByBatchId(String hotelId, Long batchId);
/**
* 查询同订单中排在当前任务之前且参与队列的任务。
*/

View File

@@ -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(),

View File

@@ -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<ReservationOperaOperationResult> operaOperations = findOperaOperationResults(task);
ReservationSourceMessageOnlyResult sourceMessageOnlyResult = buildSourceMessageOnlyResult(task, taskCard);
SourceMessageDetailContext sourceContext = findSourceMessageDetailContext(task);
ReservationAiTransitionSnapshot transition = findTaskTransition(task);
List<ReservationAiTransitionSnapshot> 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<ReservationAiTransitionSnapshot> findRelatedTransitions(
ReservationTaskSnapshot task,
ReservationAiTransitionSnapshot transition) {
if (transition == null) {
return List.of();
}
return workflowRepository.findTransitionsByBatchId(task.hotelId(), transition.batchId());
}
/**
* 按系统处理分类构造详情页展示块。
*/
private List<ReservationAiTransitionDisplayResult> buildTransitionDisplayResults(
List<ReservationAiTransitionSnapshot> 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 摘要,不保存完整客户字段。
*/

View File

@@ -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");