# 查询接口后端开发契约：给开发

本文档给后端开发和开发 AI 使用，用于把 `Skill查询接口需求梳理_给开发.md` 落成可开发的只读查询接口。

本契约只定义 **Skill script 可调用的只读查询接口**。  
这些接口用于给 Skill 提供判断上下文，不负责创建任务卡、不写 Opera、不改业务数据库、不确认付款、不导入名单。

字段级细节以 `查询接口返回字段字典.xlsx` 为准；本文档负责定义接口路径、请求方式、统一响应、错误码和各接口的 JSON 结构。

## 1. 接口总原则

### 1.1 只读边界

所有接口必须是只读查询：

- 不创建任务卡。
- 不写 Opera。
- 不更新预订。
- 不确认 payment。
- 不生成 receipt。
- 不导入 rooming list。
- 不维护 TA Recorder。
- 不写 Trace / Reservation Notes。
- 不把 `manual_review` 自动升级为 `normal_task`。

接口只能返回事实、候选、状态和证据。  
最终业务判断仍由 Skill 根据项目规则输出 `ai_task_result`。

### 1.2 Current / History 边界

业务动作必须来自 `body_current`。  
`body_thread` 只能作为 target binding evidence。

如果查询 key 来自历史邮件，调用方应传：

```json
{
  "target_key_source": "body_thread_evidence",
  "body_thread_used_only_as_evidence": true
}
```

后端接口可以使用该 key 查询对象，但不得因为历史邮件里出现旧动作而创建或建议创建任务。

### 1.3 统一路径建议

第一版建议路径如下：

| 逻辑接口名 | HTTP 方法 | 路径 |
|---|---|---|
| `query_case_context` | `POST` | `/api/ai-query/v1/case-context` |
| `query_object_detail` | `POST` | `/api/ai-query/v1/object-detail` |
| `query_file_parse_context` | `POST` | `/api/ai-query/v1/file-parse-context` |
| `query_parent_task_context` | `POST` | `/api/ai-query/v1/parent-task-context` |

使用 `POST` 是为了传复杂查询条件，不代表有写入动作。

## 2. 通用请求头

建议支持：

| Header | 必填 | 说明 |
|---|---|---|
| `Content-Type: application/json` | 是 | JSON 请求 |
| `X-Request-Id` | 是 | 调用方生成的请求 ID，用于排查 |
| `X-Source-Message-Id` | 建议 | 邮件 ID，方便日志串联 |
| `X-AI-Trace-Id` | 建议 | AI 运行链路 ID |

鉴权方式由信息系统统一确定，本文不限定。

## 3. 通用响应包

所有接口统一返回：

```json
{
  "success": true,
  "request_id": "",
  "trace_id": "",
  "data": {},
  "warnings": [],
  "error": null
}
```

失败时：

```json
{
  "success": false,
  "request_id": "",
  "trace_id": "",
  "data": null,
  "warnings": [],
  "error": {
    "code": "",
    "message": "",
    "details": {}
  }
}
```

### 3.1 通用错误码

| code | HTTP 状态 | 说明 |
|---|---:|---|
| `BAD_REQUEST` | 400 | 请求 JSON 格式错误或字段类型错误 |
| `MISSING_REQUIRED_FIELD` | 400 | 缺少必填字段 |
| `UNAUTHORIZED` | 401 | 未鉴权 |
| `FORBIDDEN` | 403 | 无权限访问该数据 |
| `NOT_FOUND` | 404 | 查询对象不存在；仅用于按明确 ID 查询的接口 |
| `FILE_NOT_ACCESSIBLE` | 422 | 文件无权限、下载失败或不可访问 |
| `PARSE_FAILED` | 422 | 文件解析失败 |
| `UPSTREAM_TIMEOUT` | 504 | 上游系统超时 |
| `INTERNAL_ERROR` | 500 | 未知后端错误 |

注意：`query_case_context` 中查不到对象时，通常不应返回 404，而应返回 `success=true` 且 `matched_order_records=[]`。因为 Skill 还需要继续判断是否有 pending task、workflow 或终止记录。

## 4. 接口一：query_case_context

### 4.1 用途

通过 `group_code`、`confirmation_number`、`reservation_no` 查询业务对象、待处理任务、工作流、终止记录和 key 关系。

适用 Skill：

```text
S01/S02/S03/S04/S05/S06/S07/S08
```

### 4.2 Request

```json
{
  "source_message_id": "MSG-001",
  "source_event_index": 1,
  "group_code": "LT260417VIPA",
  "confirmation_number": null,
  "reservation_no": null,
  "object_type_hint": "group_block",
  "target_key_source": "body_current",
  "body_thread_used_only_as_evidence": false
}
```

字段要求：

| 字段 | 必填 | 说明 |
|---|---|---|
| `source_message_id` | 是 | 当前邮件 ID |
| `source_event_index` | 是 | 当前事件序号 |
| `group_code` | 条件 | Group / Allotment 优先 key |
| `confirmation_number` | 条件 | FIT 优先 key |
| `reservation_no` | 否 | Opera reservation no，辅助查询 |
| `object_type_hint` | 否 | `fit_reservation` / `group_block` / `allotment_control_block` / `booking_record` / `unknown` |
| `target_key_source` | 建议 | `body_current` / `body_thread_evidence` / `upstream_task_context` / `system_context` |
| `body_thread_used_only_as_evidence` | 建议 | key 来自历史证据时传 `true` |

`group_code`、`confirmation_number`、`reservation_no` 至少应有一个非空。  
如果三个 key 都为空，后端可返回 `MISSING_REQUIRED_FIELD`；调用方也可选择不调用该接口。

### 4.3 Response data

```json
{
  "matched_order_records": [
    {
      "object_id": "OBJ-001",
      "object_type": "group_block",
      "group_code": "LT260417VIPA",
      "confirmation_number": null,
      "reservation_no": null,
      "block_id": "BLK-10001",
      "block_name": "LT260417VIPA GROUP",
      "guest_name": null,
      "group_name": "LT260417VIPA GROUP",
      "arrival_date": "2026-04-17",
      "departure_date": "2026-04-20",
      "room_items": [],
      "rate_code": null,
      "rate_code_price": null,
      "reservation_type": null,
      "status": "active",
      "cancel_status": "not_cancelled",
      "source_table": "opera_group_block_sync",
      "last_updated_at": "2026-07-07T10:00:00+08:00"
    }
  ],
  "pending_or_open_tasks": [],
  "active_workflows": [],
  "terminated_records": [],
  "target_object_validation": {
    "status": "single",
    "matched_object_id": "OBJ-001",
    "matched_object_type": "group_block",
    "can_create_new_booking_task": false,
    "can_create_update_task": true,
    "can_create_cancel_task": true,
    "can_attach_voucher": true,
    "can_attach_rooming_list": true,
    "needs_manual_review_reason": null
  },
  "key_relationships": {
    "group_code_and_confirmation_same_object": null,
    "relationship_evidence": ""
  }
}
```

### 4.4 后端处理规则

- 查不到对象时返回空数组，不要直接报错。
- 必须同时查：已写入对象、pending/open task、active workflow、terminated record。
- 如果查到多个可能对象，`target_object_validation.status = "multiple"`。
- 接口不要返回“应该 normal_task / manual_review”的结论，只返回事实和校验摘要。
- 对 S01，若同 key 已有有效对象或 pending/open task，Skill 会倾向 `manual_review`。
- 对 S04/S05，如果无已写入对象但有可承接的 pending New Booking / 上游任务，仍可作为目标候选返回。

## 5. 接口二：query_object_detail

### 5.1 用途

按 `object_id` 查询更完整的对象快照，供 S02 建 before/after、S03 判断取消状态、S07 extra bed 取 `rate_code_price`、S04/S05/S08 做目标展示和硬校验。

适用 Skill：

```text
S02/S03/S04/S05/S07/S08
```

### 5.2 Request

```json
{
  "object_id": "OBJ-001",
  "object_type": "group_block"
}
```

字段要求：

| 字段 | 必填 | 说明 |
|---|---|---|
| `object_id` | 是 | `query_case_context` 返回的对象 ID |
| `object_type` | 建议 | 对象类型，可帮助后端选择查询表 |

### 5.3 Response data

```json
{
  "object_id": "OBJ-001",
  "object_type": "group_block",
  "group_code": "LT260417VIPA",
  "confirmation_number": null,
  "reservation_no": null,
  "block_id": "BLK-10001",
  "block_name": "LT260417VIPA GROUP",
  "status": "active",
  "arrival_date": "2026-04-17",
  "departure_date": "2026-04-20",
  "nights": 3,
  "guest_count": null,
  "room_items": [
    {
      "room_type": "TWN",
      "pms_room_type_code": "RM2",
      "room_quantity": 2,
      "rate_code": "WHO2",
      "rate_code_price": 2000
    }
  ],
  "rate_code": "WHO2",
  "rate_code_price": 2000,
  "reservation_type": "TA",
  "can_update": true,
  "can_cancel": true,
  "hard_validation_warnings": []
}
```

### 5.4 后端处理规则

- `object_id` 明确不存在时可返回 404 `NOT_FOUND`。
- `rate_code_price` 如果后端当前拿不到，应返回 `null`，并在 `hard_validation_warnings` 说明原因。
- S07 extra bed 不会自己计算最终房价；后端需提供能否取得 `rate_code_price` 的事实。
- `room_items[]` 可先返回摘要，第一版不要求完整 Opera 明细字段。

## 6. 接口三：query_file_parse_context

### 6.1 用途

查询附件、图片、PDF、OCR、Excel / workbook、QBD / LianTai 表格、voucher、rooming list 文件解析结果。

适用 Skill：

```text
S01/S02/S03/S04/S05/S06/S07/S08
```

### 6.2 Request

```json
{
  "source_message_id": "MSG-001",
  "source_event_index": 1,
  "file_reference": "file://attachment-001",
  "parse_need": "voucher"
}
```

`parse_need` 枚举建议：

| 值 | 用途 |
|---|---|
| `voucher` | S04 voucher / payment slip |
| `rooming_list` | S05 / S08 名单文件 |
| `qbd_liantai_table` | S01/S02/S03/S06/S07 表格行证据 |
| `ocr` | 图片/PDF OCR |
| `generic` | 通用解析 |

### 6.3 Response data

```json
{
  "file_reference": "file://attachment-001",
  "filename": "voucher.pdf",
  "download_status": "ok",
  "display_available": true,
  "file_type": "pdf",
  "ocr_text": "",
  "workbook_summary": {},
  "sheet_summaries": [],
  "highlighted_rows": [],
  "row_level_evidence": [],
  "voucher_evidence": {
    "voucher_subtype": "lian_tai_credit_voucher",
    "lian_tai_code_candidate": "LT260417VIPA",
    "code_field_read_status": "clear",
    "bank_slip_detected": false
  },
  "list_file_evidence": {},
  "parse_warnings": []
}
```

QBD / LianTai 表格场景可返回：

```json
{
  "row_level_evidence": [
    {
      "sheet_name": "BOOKING 06-2026",
      "row_number": 12,
      "row_label": "12",
      "highlight_status": "yellow",
      "action_status_cell_value": "NEW BOOKING",
      "raw_date_range": "17/04/26-20/04/26",
      "raw_room_type": "TWN",
      "raw_room_quantity": "2",
      "raw_rate": "2000THB+500",
      "before_after_evidence": null,
      "strikethrough_state": null,
      "cell_reference": "H12",
      "ocr_evidence": null
    }
  ]
}
```

Rooming List 场景可返回：

```json
{
  "list_file_evidence": {
    "list_evidence_type": "rooming_list",
    "file_type_validation": {
      "is_guest_level_list": true,
      "positive_headers": ["English Name", "Passport No", "Nationality"],
      "negative_booking_table_evidence": []
    },
    "target_key_candidates": [
      {
        "key_type": "group_code",
        "key_value": "HD260510A-1",
        "source": "sheet_name"
      }
    ],
    "shared_evidence_sheets": ["总名单"]
  }
}
```

### 6.4 后端处理规则

- 文件不可访问时返回 `success=false`、`FILE_NOT_ACCESSIBLE`，或 `success=true` 且 `download_status=failed`；项目第一版建议后者，方便 Skill 输出 structured `manual_review`。
- S04 不应从 bank slip OCR 的金额、memo、银行账号、付款人等字段自动生成 group_code。
- S05 不能因为文件是 Excel 就判断为 rooming list，必须返回正向名单证据或负向 booking table 证据。
- QBD / LianTai 多个高亮行应作为多个 `row_level_evidence[]` 返回，Skill 再按行拆分任务。
- 接口只返回证据，不负责决定路由到 S01/S02/S03/S05。

## 7. 接口四：query_parent_task_context

### 7.1 用途

查询 linked task 的父任务上下文。  
主要用于：

- S07 从 S01 / S02 / S06 派生 Trace / Extra Bed linked task。
- S08 从 S05 Rooming List 派生 TA Recorder linked task。

### 7.2 Request

```json
{
  "source_message_id": "MSG-001",
  "parent_source_event_index": 1,
  "linked_task_group_id": "LTK-001",
  "source_rooming_list_task_id": null
}
```

字段要求：

| 字段 | 必填 | 说明 |
|---|---|---|
| `source_message_id` | 建议 | 当前邮件 ID |
| `parent_source_event_index` | 条件 | 父事件序号 |
| `linked_task_group_id` | 条件 | 联动任务组 ID |
| `source_rooming_list_task_id` | S08条件 | S05 Rooming List 父任务 ID |

至少应提供 `parent_source_event_index`、`linked_task_group_id`、`source_rooming_list_task_id` 中一个。

### 7.3 Response data

```json
{
  "parent_task": {
    "task_id": "TASK-001",
    "task_type": "Rooming List",
    "task_status": "confirmed",
    "case_keys": {
      "group_code": "HD260510A-1"
    },
    "source_event_index": 1
  },
  "linked_tasks": [],
  "execution_order": [
    {
      "task_id": "TASK-001",
      "execution_order": 1
    },
    {
      "task_id": "TASK-002",
      "execution_order": 2
    }
  ],
  "parent_confirmed_payload": {
    "target_object_id": "OBJ-001",
    "file_reference": "file://rooming-list.xlsx"
  },
  "parent_status": "confirmed",
  "attachment_binding": {
    "file_reference": "file://rooming-list.xlsx",
    "workbook_name": "HD260510A.xlsx",
    "sheet_name": "HD260510A-1"
  },
  "file_reference": "file://rooming-list.xlsx",
  "workbook_name": "HD260510A.xlsx",
  "sheet_name": "HD260510A-1"
}
```

### 7.4 后端处理规则

- 这个接口不能创建 linked task，只能返回父任务和已存在 linked task 上下文。
- S07 linked task 必须等父 S01/S02/S06 完成并确认目标对象后才能真正执行。
- S08 linked task 必须跟随 S05 已确认的目标 `group_code`，不得从 sheet 名自行猜 `group_code`。
- `parent_status` 未完成时，Skill 可输出 `blocked_until_parent_completed=true`。

## 8. 推荐调用时序

### 8.1 普通路由级查询

```text
Main Agent 从 body_current 提取 group_code / confirmation_number
  -> POST /api/ai-query/v1/case-context
  -> routing_context 传给 Skill
  -> Skill 判断 normal_task / manual_review
```

### 8.2 current 无 key，但 history 有唯一目标 key

```text
body_current 有明确业务动作
  -> body_thread 仅提取唯一 target key 作为 evidence
  -> POST /api/ai-query/v1/case-context
     target_key_source=body_thread_evidence
     body_thread_used_only_as_evidence=true
  -> Skill 输出中保留 target_key_source 和 hard validation 标记
```

### 8.3 文件类任务

```text
当前邮件有附件 / 图片 / PDF / Excel / link
  -> POST /api/ai-query/v1/file-parse-context
  -> 如解析出 key，再 POST /api/ai-query/v1/case-context
  -> Skill 判断任务
```

### 8.4 linked task

```text
S01/S02/S06 主任务 + S07 linked task
或 S05 Rooming List + S08 TA Recorder
  -> POST /api/ai-query/v1/parent-task-context
  -> 返回父任务状态、目标对象、附件绑定和执行顺序
```

## 9. 与两张字典表的关系

### 9.1 与 `查询接口返回字段字典.xlsx`

本文定义接口级契约。  
字段名、字段路径、数据类型、是否数组、是否必传/必返，以 `查询接口返回字段字典.xlsx` 为准。

### 9.2 与 `AI输出参数并集字典.xlsx`

`AI输出参数并集字典.xlsx` 只定义 Skill 最终输出到过渡表的字段。  
查询接口返回字段不要求全部进入 AI 输出 JSON。

如果某个查询字段只用于 Skill 判断，可只进入 `context_used` 摘要，不必作为 AI 输出字段展开。

当前建议重点关注这些查询字段：

- `reservation_no`
- `object_id`
- `object_type`
- `block_id`
- `block_name`
- `rate_code_price`

这些字段虽属于查询上下文，但后续任务卡展示、Opera API 定位、底表展示和硬校验都会用到，开发应在接口层明确返回。

## 10. 后端验收清单

后端完成后，至少应满足：

- 四个接口都可通过 POST JSON 调用。
- 所有接口都使用统一响应包。
- 查询不到对象时，`query_case_context` 返回空数组而不是直接报错。
- 每次查询都能带上 `source_message_id`、`source_event_index` 或请求追踪信息。
- 支持 target key 来自 `body_thread_evidence`，但不从 history 触发任务。
- `query_case_context` 同时返回已写入对象、pending/open task、active workflow、terminated record。
- `query_file_parse_context` 能区分 voucher、rooming list、QBD/LianTai table 证据。
- `query_parent_task_context` 能返回父任务状态和执行顺序。
- 所有接口不发生写入动作。
