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

116 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 导出会话聊天为 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`(不经拦截器):
```typescript
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,含日期选择器/搜索)加:
```vue
<el-button v-if="hasActive" size="small" type="primary" plain @click="exportCurrent">导出</el-button>
```
```typescript
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(与云客一致,无)。