diff --git a/docs/superpowers/plans/2026-08-08-yunke-export-chat-html.md b/docs/superpowers/plans/2026-08-08-yunke-export-chat-html.md new file mode 100644 index 0000000..85ffb7e --- /dev/null +++ b/docs/superpowers/plans/2026-08-08-yunke-export-chat-html.md @@ -0,0 +1,127 @@ +# 导出会话聊天为 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'`。