Files
yunke/docs/superpowers/specs/2026-08-08-yunke-export-chat-html-design.md
T

6.9 KiB
Raw Blame History

导出会话聊天为 HTML(③,ChatTimeline 接线 + 文件名修复)设计

日期:2026-08-08|分支:feat/yunke-data-mirror|路线图 ③ 前置(已就绪):后端 GET /api/export/chat-html(app/api/routers/export.py)已实现、已注册、有测试——按 (channel, owner_account_id, talker_id|room_id, start?, end?) 拉消息 → 渲染自包含 HTML 气泡页 → attachment 下载,文件名 chat-{channel}-{owner}-{peer}.html。 数据/交互:全本地。云客「导出聊天记录」2026-08-08 真机核实(异步任务→下载中心);用户定:按我们内部逻辑做,不复刻异步下载中心。

1. 目标与范围

现状(plan 核实,2026-08-08):

  • 后端 /api/export/chat-html 已就绪(export.py,不改)。
  • CustomerList、GroupList 操作列「导出HTML聊天记录」按钮已存在(wechat tab;用 downloadBlob('/export/chat-html', params),CustomerList 还带时间范围选择 modal)。本 plan 不动这两处。
  • ChatTimeline(聊天窗)缺「导出」按钮 —— 唯一缺失的入口。
  • downloadBlob(api/client.ts)有个 bug:因响应拦截器 (response)=>response.data 丢头,文件名硬编码 'chat-export.html',丢失后端 chat-{...}.html 命名(影响已存的 2 个按钮 + 即将加的 ChatTimeline 按钮)。

目标:补 ChatTimeline「导出」入口 + 修 downloadBlob 文件名丢失。

在本 plan(纯前端):

  1. ChatTimeline 头部加「导出」按钮(v-if="hasActive")→ downloadBlob('/export/chat-html', {channel, owner_account_id, talker_id/room_id, start: winStart, end: winEnd}),即导出当前会话在当前 dateRange 内的聊天 HTML。沿用现有 downloadBlob(与 CustomerList/GroupList 一致,不引入 window.open 第二套下载路径)。
  2. 修 downloadBlob 文件名:改用裸 axios.get(不经解包拦截器)拿全响应 → 从 Content-Disposition 解析文件名(chat-{channel}-{owner}-{peer}.html),fallback 'chat-export.html'。惠及 3 处按钮。

不在本 plan:

  • 云客异步任务/下载中心(不需要)。
  • CustomerList/GroupList 导出改造(已实现,不动;WA tab 无导出 = 与云客一致,保留)。
  • HTML 排版改版(沿用 export.py)。
  • ChatTimeline 的导出时间范围 modal:聊天窗已有 dateRange(step 5),直接用,不加 modal。

2. 后端契约(已就绪,引用,不改)

GET /api/export/chat-html:query channel/owner_account_id(必)/talker_id?/room_id?/start?/end?(ms) → text/html attachment,Content-Disposition: attachment; filename="chat-{channel}-{owner}-{peer}.html"。

3. 前端

3.1 downloadBlob 文件名修复(yunkes/src/api/client.ts)

当前用 http.get(url,{params,responseType:'blob'}) 但拦截器把响应解包成 body(丢头)。改为裸 axios(不经拦截器):

import axios from 'axios'  // 已 import

/** 以 blob 下载(HTML 导出等附件)。从 Content-Disposition 取文件名(绕开解包拦截器)。 */
export async function downloadBlob(
  url: string,
  params?: Record<string, unknown>,
): Promise<string> {
  // 裸 axios(baseURL '/api')拿全响应(含 headers),不经 http 的 (r)=>r.data 拦截器
  const res = await axios.get(`/api${url}`, { params, responseType: 'blob' })
  const cd = (res.headers['content-disposition'] as string) || ''
  const m = /filename="?([^";]+)"?/i.exec(cd)
  const filename = m ? m[1] : 'chat-export.html'
  const objUrl = URL.createObjectURL(res.data as Blob)
  const a = document.createElement('a')
  a.href = objUrl
  a.download = filename
  document.body.appendChild(a)
  try {
    a.click()
  } finally {
    document.body.removeChild(a)
    URL.revokeObjectURL(objUrl)
  }
  return filename
}

url 入参仍是相对 /export/chat-html(与 CustomerList 现有调用一致),函数内补 /api 前缀。

3.2 ChatTimeline 头部「导出」按钮(yunkes/src/views/ChatTimeline.vue)

  • import { downloadBlob } from '@/api/client'(或经 @/api 若已 re-export;plan 核实)。
  • .yk-chat__filters 区(step 5,含日期选择器/搜索)加:
<el-button v-if="hasActive" size="small" type="primary" plain @click="exportCurrent">导出</el-button>
async function exportCurrent(): Promise<void> {
  if (!hasActive.value) return
  try {
    await downloadBlob('/export/chat-html', {
      channel: channelTyped.value,
      owner_account_id: props.ownerId,
      talker_id: isGroup.value ? undefined : props.talkerId,
      room_id: isGroup.value ? props.roomId : undefined,
      start: winStart.value,
      end: winEnd.value,
    })
  } catch (e) {
    ElMessage.error(`导出失败:${(e as Error).message}`)
  }
}

ElMessage 已 import。channelTyped/isGroup/props/winStart/winEnd/hasActive 均存在(step 5/Task4)。

4. 关键决策(已与用户确认 + spec review 纠正 2026-08-08)

  1. ③ = 仅 ChatTimeline 加导出按钮 + 修 downloadBlob 文件名(CustomerList/GroupList 已实现,不动)。
  2. 沿用 downloadBlob(与现存 2 处一致),不引入 window.open/exportChatUrl 第二套路径(spec review 指出)。
  3. 修文件名 bug(惠及 3 处):裸 axios 保头 + 解析 Content-Disposition。
  4. 同步直接下载(无云客异步下载中心);HTML 渲染沿用 export.py。
  5. ChatTimeline 导出用现有 dateRange(不加 modal);CustomerList 的时间 modal 已存,不动。

5. 文件清单

  • 改 yunkes/src/api/client.ts(downloadBlob 文件名修复)。
  • 改 yunkes/src/views/ChatTimeline.vue(头部「导出」按钮 + exportCurrent)。
  • 无后端改动;CustomerList/GroupList/export.py 不动。

6. 验收

  • vue-tsc -b exit 0。
  • 浏览器实测(本地 smoke.db,无云客):
    • ChatTimeline:进一个会话 → 「导出」→ 下载文件名 chat-wechat-{owner}-{talker}.html(文件名正确,非 chat-export.html);打开含该 dateRange 内消息气泡(员工右/客户左、时间、文本);改 dateRange 再导出 → 内容随窗变。
    • (回归)CustomerList/GroupList「导出HTML聊天记录」→ 文件名也变正确(chat-...html),行为不变。
    • network:仅 /api/export/chat-html(无云客)。

7. 风险

  • 裸 axios 不带拦截器的错误处理:downloadBlob 失败时裸 axios 抛原始 axios 错(无统一 message 包装);exportCurrent 用 try/catch + ElMessage 兜底(CustomerList 现状亦如此)。
  • Content-Disposition 解析:正则 filename="...";后端用双引号包裹(export.py 确认);fallback 固定名。
  • WA/群导出:ChatTimeline 群会话(isGroup)传 room_id;WA 通道同理(channel=whatsapp)。WA tab 列表导出不在本 plan(与云客一致,无)。