修复V4来源附件URL展示脱敏
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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[]` 只读展示诊断信息。
|
||||
|
||||
@@ -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 订单详情时间线
|
||||
|
||||
|
||||
@@ -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 原始 payload;Basic Account、Room Type、Rate Code 目录错误返回 `V4_FIELD_VALIDATION_FAILED` | 必须写业务审计,actor 使用当前登录用户 |
|
||||
|
||||
Reference in New Issue
Block a user