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

5.8 KiB

导出会话聊天为 HTML(③)Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: superpowers:subagent-driven-development or superpowers:executing-plans. Steps use checkbox (- [ ]).

Goal: ChatTimeline 聊天窗加「导出」按钮(触发已有 /api/export/chat-html)+ 修 downloadBlob 文件名丢失 bug(惠及 3 处导出按钮)。

Architecture: 纯前端 2 文件。client.ts 的 downloadBlob 改用裸 axios(不经解包拦截器)保 Content-Disposition → 正确文件名;ChatTimeline.vue 头部加「导出」el-button → downloadBlob('/export/chat-html', {channel,owner,talker|room,start,end}) 用当前 dateRange。后端 export.py 不动;CustomerList/GroupList 已有导出按钮不动。

Tech Stack: Vue3+TS+Element Plus(yunkes/);门禁 vue-tsc -b。无后端。

Spec: docs/superpowers/specs/2026-08-08-yunke-export-chat-html-design.md(已过审 + 已纠正)。


关键事实(plan 核实)

  • 后端 GET /api/export/chat-html(export.py)已就绪:参数 channel/owner_account_id/talker_id?/room_id?/start?/end?,返回 text/html,Content-Disposition: attachment; filename="chat-{channel}-{owner}-{peer}.html"。不改。
  • client.ts 现状:http = axios.create({baseURL:'/api'}) + 响应拦截器 (r)=>r.data(解包 body,丢头)。downloadBlob(46-66)用 http.get(url,{params,responseType:'blob'}) → res 已是 Blob(头丢)→ 文件名硬编码 'chat-export.html'。
  • CustomerList(:201 downloadBlob('/export/chat-html', params) + 时间 modal)、GroupList(:166)已有导出按钮(wechat tab)。不动。
  • ChatTimeline(step 5/Task4 后):defineProps<{channel,ownerId,talkerId?,roomId?}>、channelTyped(44)、isGroup(45)、winStart/winEnd(103-104)、hasActive(56)、ElMessage 已 import、.yk-chat__filters 区已存在(含日期选择器/搜索)。缺「导出」按钮。
  • axios 已在 client.ts import。

文件清单

  • 改 yunkes/src/api/client.ts(downloadBlob 文件名修复)。
  • 改 yunkes/src/views/ChatTimeline.vue(头部「导出」按钮 + exportCurrent)。
  • 无后端、无其它文件。

Task 1: downloadBlob 文件名修复 + ChatTimeline 导出按钮

Files: yunkes/src/api/client.ts、yunkes/src/views/ChatTimeline.vue。

  • Step 1: client.ts — downloadBlob 改裸 axios 保头

把现有 downloadBlob(46-66)替换为:

/** 以 blob 下载(HTML 导出等附件)。从 Content-Disposition 取文件名(绕开 http 的 (r)=>r.data 解包拦截器)。 */
export async function downloadBlob(
  url: string,
  params?: Record<string, unknown>,
): Promise<string> {
  // 裸 axios(补 /api 前缀)拿全响应(含 headers),不经 http 拦截器
  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/GroupList 现有调用一致);函数内补 /api。错误处理:裸 axios 抛原始错,调用方 try/catch 兜底(CustomerList 现状亦如此)。

  • Step 2: ChatTimeline.vue — import + exportCurrent + 按钮

import(ElMessage 已在;加 downloadBlob):

import { downloadBlob } from '@/api/client'

若 @/api 已 re-export downloadBlob 则从 @/api 导;否则从 @/api/client。plan 核实(grep export.*downloadBlob in api/index.ts)。

加 handler(放 onSelectSession/loadInitial 附近):

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}`)
  }
}

模板 .yk-chat__filters 区加(在日期选择器/搜索旁):

<el-button v-if="hasActive" size="small" type="primary" plain @click="exportCurrent">导出</el-button>
  • Step 3: vue-tsc
cd /d/refactor/yunke/yunkes && npx vue-tsc -b

Expected: exit 0。

  • Step 4: 浏览器实测

启动后端(smoke.db)+前端。/chat/wechat/{owner}/{talker}(有消息的会话)→「导出」→ 下载文件名 chat-wechat-{owner}-{talker}.html(非 chat-export.html);打开含该 dateRange 内消息气泡。改 dateRange 再导出 → 内容随窗变。 (回归)/customers → 操作列「导出HTML聊天记录」→ 文件名也正确(chat-...html)。

  • Step 5: Commit
git add yunkes/src/api/client.ts yunkes/src/views/ChatTimeline.vue
git commit -m "feat(chat): ChatTimeline 导出按钮 + 修复 downloadBlob 文件名丢失(Content-Disposition)"

End with blank line + Co-Authored-By: Claude <noreply@anthropic.com>。


验收(spec §6)

  • vue-tsc -b exit 0。
  • 浏览器(本地 smoke.db):ChatTimeline「导出」→ 文件名正确 + 内容随 dateRange;CustomerList/GroupList 导出文件名也修复(回归);network 仅 /api/export/chat-html。

风险

  • downloadBlob import 路径:@/api 是否 re-export downloadBlob?plan grep 核实;否则从 @/api/client。
  • 裸 axios 错误:无拦截器包装,调用方 try/catch + ElMessage(已加)。
  • filename 正则:后端双引号包裹(export.py 确认);fallback 'chat-export.html'。