修复V4来源附件URL展示脱敏

This commit is contained in:
andy
2026-07-19 14:20:06 +07:00
parent cae169cefd
commit 145511620c
5 changed files with 53 additions and 6 deletions

View File

@@ -175,8 +175,8 @@ function normalizeAttachments(value: unknown): AttachmentSummary[] {
function toAttachmentSummary(value: unknown, index: number): AttachmentSummary {
const record = readRecord(value)
const name = record
? readFirstString(record, ['name', 'file_name', 'filename', 'display_name', 'title']) ?? t('conversation.unnamedAttachment')
: String(value)
? safeAttachmentName(readFirstString(record, ['name', 'file_name', 'filename', 'display_name', 'title']))
: safeAttachmentName(String(value))
const type = record ? readFirstString(record, ['content_type', 'mime_type', 'media_type', 'file_type']) : null
const size = record ? readNumber(record, 'size_bytes') ?? readNumber(record, 'size') : null
return {
@@ -249,6 +249,18 @@ function formatFileSize(size: number | null): string {
}
return `${(size / 1024 / 1024).toFixed(1)} MB`
}
function safeAttachmentName(value: string | null): string {
const trimmedValue = value?.trim()
if (!trimmedValue || isUnsafeAttachmentText(trimmedValue)) {
return t('conversation.unnamedAttachment')
}
return trimmedValue
}
function isUnsafeAttachmentText(value: string): boolean {
return /^(https?:\/\/|oss:\/\/|s3:\/\/)/i.test(value.trim())
}
</script>
<style scoped>

View File

@@ -75,6 +75,36 @@ describe('reservation V4 pages', () => {
})
})
it('does not render direct attachment URLs from V4 source message payload', async () => {
const detail = createOrderTaskDetail({
sourceDisplayPayload: {
subject: 'Booking Request',
sender_summary: 'guest@example.test',
relevant_message_excerpt: 'Please book one twin room.',
uploaded_media: [
'https://oss.example/private/booking.pdf',
'oss://bucket/private-voucher.jpg',
{
file_name: 'safe-booking.pdf',
content_type: 'application/pdf',
},
],
},
})
vi.mocked(service.fetchReservationV4OrderTaskDetail).mockResolvedValue(detail)
const wrapper = await mountWithPlugins(
ReservationV4OrderTaskDetailView,
'/reservation/order-tasks/9001',
)
await flushPromises()
expect(wrapper.text()).toContain('safe-booking.pdf')
expect(wrapper.text()).toContain(zhCN.conversation.unnamedAttachment)
expect(wrapper.text()).not.toContain('https://oss.example')
expect(wrapper.text()).not.toContain('oss://bucket')
})
it('submits V4 review resolution with field_pointer overrides and confirmed order id', async () => {
const detail = createOrderTaskDetail({
businessCardStatus: 'REVIEW_REQUIRED',
@@ -250,10 +280,11 @@ async function mountWithPlugins(component: object, initialPath: string) {
function createOrderTaskDetail(options: {
businessCardStatus?: string
businessCardAvailability?: Partial<ReservationV4TaskCardResult['availability']>
sourceDisplayPayload?: ReservationV4TaskCardResult['display_payload']
} = {}): ReservationV4OrderTaskDetailResult {
const sourceCard = createCard('card-source', 'SOURCE_MESSAGE_DISPLAY', 'READONLY', {
fields: [],
display_payload: {
display_payload: options.sourceDisplayPayload ?? {
subject: 'Booking Request',
sender_summary: 'guest@example.test',
relevant_message_excerpt: 'Please book one twin room.',

View File

@@ -474,6 +474,7 @@ RESERVATION_ROOMING_LIST_GENERATE
- `fields[].validation_errors` 应展示在对应字段旁边;接口返回 `V4_FIELD_VALIDATION_FAILED``details[]` 带嵌套路径时,前端应尝试定位到对应 field定位不到则在当前卡片动作错误区展示。
- `options_source` 第一版仍按前端固定种子渲染 Account / Room Type / Rate Code不调用真实目录 lookup API。
- 前端不展示 `ai_payload_json`、邮件完整正文、附件 URL、raw evidence 或 SuperAgent 原始 payload来源邮件详情仍从邮件会话页面查看卡片内仅展示后端普通接口返回的安全摘要、邮件片段和附件名称。
- V4 来源消息卡读取 `attachments``uploaded_media``file_references` 时,只允许展示附件名称、类型和大小等安全摘要;如果后端 payload 中异常出现 `https://``oss://``s3://` 等直接 URL 字符串,前端必须替换为“未命名附件”或隐藏,不得把 URL 渲染到普通业务页面。
- V4 主流程不调用旧 V2/V3 草稿、旧任务确认、旧同卡复核接口,也不展示 OPERA 模拟操作入口。
## 6. 不给前端直接调用的接口
@@ -491,7 +492,7 @@ RESERVATION_ROOMING_LIST_GENERATE
- 普通任务切换订单接口继续后置。
- M002 V4 入站解析与数据模型基线已完成第一版:后端可接收 `source_message + order_contexts[] + message_events[]`,识别 `NEW_BOOKING``UPDATE_BOOKING``CANCEL_BOOKING``TRACE_RESERVATION_NOTES``ROOMING_LIST``PAYMENT`,并保存 V4 原始 payload、`route_code`、系统处理分类和 `field_contract_version=20260718-v4`。前端暂不需要直接调用 V4 回调接口。
- M002 V4 CP5 已完成查询接口:普通 V4 业务包可通过 `/api/reservation/workbench-items``/api/reservation/order-tasks``/api/reservation/order-tasks/{orderTaskId}` 查看V4 S10/S99 来源通知可通过 `/api/reservation/source-notifications/{notificationId}` 查看。M002 V4 CP6 已开放普通卡片确认和 S10/S99 ack 写接口M002 V4 CP7 已开放 `POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution` 复核解阻接口M002 V4 CP8 已开放 V4 卡片 `fields[]` 白名单和第一版固定目录校验。
- V4 任务卡的 `display_payload_json` 只保留后端白名单展示字段;`ai_payload_json` 才包含完整 SuperAgent 原始 event。后续 V4 查询接口不得把 `ai_payload_json`、附件 URL 或 raw evidence 直接给普通页面渲染。
- V4 任务卡的 `display_payload_json` 只保留后端白名单展示字段;`ai_payload_json` 才包含完整 SuperAgent 原始 event。后续 V4 查询接口不得把 `ai_payload_json`、附件 URL 或 raw evidence 直接给普通页面渲染;前端对来源消息卡附件字段仍做 URL-like 文本兜底脱敏
- V4 可映射 event 现阶段仍保留现有任务详情结构作为过渡兼容;任务详情中若出现 `field_contract_version=20260718-v4` 或 AI payload 内的 `v4_source_message``v4_order_context``v4_message_event`,前端第一版只读展示即可,不要据此假定完整 V4 多卡页面已经完成。
- V4 `PAYMENT.attachment_ids[]` 不匹配、`UPDATE_BOOKING` 携带 `rate_code` 等问题会出现在任务详情同批次的 `adapter_contract_errors[]` 只读诊断块中,不展示保存、确认、执行或重试按钮。该字段只返回白名单诊断字段,不返回完整 AI payload、邮件正文、附件 URL 或 raw evidence。
- V4 包级契约错误只会保存在 AI transition 中不会出现在普通任务列表V4 event 级契约错误如果同批次存在其它业务任务,前端仍按任务详情里的 `adapter_contract_errors[]` 只读展示诊断信息。

View File

@@ -560,6 +560,8 @@ GET /api/reservation/order-tasks/{orderTaskId}
来源邮件摘要必须按当前查询酒店过滤。如果 V4 订单任务因脏引用指向了其它酒店的 SourceMessage详情接口只能返回该 `source_message_id` 的空摘要占位,不得透出对方酒店的主题、外部消息 ID、发件人或会话信息。
前端 V4 来源消息卡只展示来源邮件安全摘要、邮件片段和附件名称 / 类型 / 大小等非 URL 摘要;如果 `source_message_card.display_payload` 中的 `attachments``uploaded_media``file_references` 异常包含直接 URL 字符串,前端必须兜底显示为未命名附件或隐藏,不能在普通业务页面渲染具体 URL。
### 12.4 S10/S99 来源通知详情
```text
@@ -584,6 +586,7 @@ GET /api/reservation/source-notifications/{notificationId}
- 只用于 S10/S99 来源通知详情页。
- 不返回 `order_task``bound_order``basic_information_card``business_cards`
- 邮件正文、附件 URL 和会话原文读取仍按 SourceMessage 权限和原文读取审计规则处理。
- 来源通知详情页复用 V4 来源消息卡展示规则:普通页面只显示安全摘要和附件名称;附件 URL、完整正文和 HTML 只允许通过 SourceMessage 会话详情权限链路查看。
### 12.5 订单详情时间线

View File

@@ -49,8 +49,8 @@
| `GET /api/reservation/tasks/{taskId}` | `FRONTEND_USER` | 已强制 Bearer 登录 + `RESERVATION_TASK_READ`;按任务实际所属酒店校验访问权 | 保持登录 + `RESERVATION_TASK_READ` + 任务所属酒店访问权 | 只读查询默认不写业务审计 |
| `GET /api/reservation/workbench-items` | `FRONTEND_USER` | 已实现 M002 V4 CP5强制 Bearer 登录 + `RESERVATION_TASK_READ` + 酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 酒店访问权;统一返回 V4 业务订单任务和 S10/S99 来源通知摘要 | 只读查询默认不写业务审计;不得返回邮件正文、附件 URL、AI 原始 payload 或来源通知原始 payload同来源时间下使用 `updated_at` / `created_at` / 数字 ID 稳定排序 |
| `GET /api/reservation/order-tasks` | `FRONTEND_USER` | 已实现 M002 V4 CP5强制 Bearer 登录 + `RESERVATION_TASK_READ` + 酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 酒店访问权;只返回 V4 业务订单任务,不返回 S10/S99 来源通知 | 只读查询默认不写业务审计;不得返回 AI 原始 payload`card_status` 只匹配业务 / 可处理卡,固定来源邮件展示卡不参与筛选 |
| `GET /api/reservation/order-tasks/{orderTaskId}` | `FRONTEND_USER` | 已实现 M002 V4 CP5强制 Bearer 登录 + `RESERVATION_TASK_READ` + 订单任务所属酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 订单任务所属酒店访问权 | 只读查询默认不写业务审计;邮件正文和附件读取仍走 SourceMessage 原文权限;不得返回 `ai_payload_json`;同批次 `adapter_contract_errors[]` 只返回白名单诊断字段 |
| `GET /api/reservation/source-notifications/{notificationId}` | `FRONTEND_USER` | 已实现 M002 V4 CP5强制 Bearer 登录 + `RESERVATION_TASK_READ` + 来源通知所属酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 来源通知所属酒店访问权 | 只读查询默认不写业务审计;邮件正文和附件读取仍走 SourceMessage 原文权限;不得返回来源通知原始 payload |
| `GET /api/reservation/order-tasks/{orderTaskId}` | `FRONTEND_USER` | 已实现 M002 V4 CP5强制 Bearer 登录 + `RESERVATION_TASK_READ` + 订单任务所属酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 订单任务所属酒店访问权 | 只读查询默认不写业务审计;邮件正文和附件读取仍走 SourceMessage 原文权限;不得返回 `ai_payload_json` 或附件 URL;同批次 `adapter_contract_errors[]` 只返回白名单诊断字段;前端普通业务卡如遇 URL-like 附件字符串必须二次脱敏 |
| `GET /api/reservation/source-notifications/{notificationId}` | `FRONTEND_USER` | 已实现 M002 V4 CP5强制 Bearer 登录 + `RESERVATION_TASK_READ` + 来源通知所属酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 来源通知所属酒店访问权 | 只读查询默认不写业务审计;邮件正文和附件读取仍走 SourceMessage 原文权限;不得返回来源通知原始 payload 或附件 URL前端普通通知卡如遇 URL-like 附件字符串必须二次脱敏 |
| `PUT /api/reservation/tasks/{taskId}/draft` | `FRONTEND_USER` | 第一版未全量强制登录actor 仍待迁移 | 登录 + `RESERVATION_TASK_EDIT` + 酒店访问权 | 写草稿审计可按业务需要记录 |
| `POST /api/reservation/tasks/{taskId}/confirm` | `FRONTEND_USER` | 第一版未全量强制登录actor 仍待迁移 | 登录 + `RESERVATION_TASK_CONFIRM` + 酒店访问权 | 必须写业务审计 |
| `POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm` | `FRONTEND_USER` | 已实现 M002 V4 CP6/CP8强制 Bearer 登录 + `RESERVATION_TASK_CONFIRM` + 订单任务所属酒店访问权 + version 并发校验 + 第一版目录校验 | 保持Basic Information 前置确认,确认后卡片锁定,不返回 AI 原始 payloadBasic Account、Room Type、Rate Code 目录错误返回 `V4_FIELD_VALIDATION_FAILED` | 必须写业务审计actor 使用当前登录用户 |