# 导出会话聊天为 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)替换为: ```typescript /** 以 blob 下载(HTML 导出等附件)。从 Content-Disposition 取文件名(绕开 http 的 (r)=>r.data 解包拦截器)。 */ export async function downloadBlob( url: string, params?: Record, ): Promise { // 裸 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`): ```typescript import { downloadBlob } from '@/api/client' ``` > 若 `@/api` 已 re-export `downloadBlob` 则从 `@/api` 导;否则从 `@/api/client`。plan 核实(grep `export.*downloadBlob` in api/index.ts)。 加 handler(放 `onSelectSession`/`loadInitial` 附近): ```typescript async function exportCurrent(): Promise { 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` 区加(在日期选择器/搜索旁): ```vue 导出 ``` - [ ] **Step 3: vue-tsc** ```bash 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** ```bash 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 `。 --- ## 验收(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'`。