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

128 lines
11 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.
# ChatTimeline 会话列表对齐云客(step 5)设计
> 日期:2026-08-08|分支:`feat/yunke-data-mirror`|总 spec 第 5 步(ChatTimeline 对齐)
> 前置:Phase A/B(StaffList 5 数字 modal + 客户微信→聊天,ChatTimeline 已支持 `start/end` query 过滤)。
> 数据/交互:全本地(运行时不碰云客)。云客「微信聊天窗口」**2026-08-08 Playwright 真机核实**(crm2.yunkecn.com `/cms/wechat/salewechat/message/wechat-window/1/{wxid}/...`)。
> 上游:`docs/superpowers/specs/2026-08-05-yunke-ui-alignment-design.md`(第 5 步:会话列表 + 消息流 + 查询 + `/api/sessions`)。
## 1. 目标与范围
**目标**:ChatTimeline 升级为云客式**双栏聊天窗口**——左侧会话列表(该账号的好友+群,带「今日消息数 + 最近联系时间」)+ 右侧消息流(现有 timeline)+ 页内日期选择器/关键词搜索。badge/昵称 → 账号级聊天窗口(取代当前「跳最近一条」降级)。
**在本 plan(核心范围,后端+UI 一个 plan)**:
1. **后端 `GET /api/sessions`**:按账号聚合会话(好友 customer + 群 group)→ 每条 `{type, id, name, avatar, today_count, last_chat_time}`,按 last_chat_time desc。
2. **新 `ChatSidebar.vue`**:会话列表(`/api/sessions`)+ 好友/群 筛选 + 搜索 + 每条头像/昵称/今日消息/最近联系;点会话 → 同组件换会话(侧栏常驻)。
3. **ChatTimeline 双栏化**:左 `ChatSidebar` + 右消息流;路由 `talkerId`/`roomId` 改**可选**;无会话 → 侧栏 + 空态。
4. **页内日期选择器 + 关键词搜索**(消息流过滤);兼容 stat modal 带 `start/end` query 入口。
**不在本 plan**:
- 朋友圈 / 通讯录 / 统计分析 / 已删除好友 / 删除聊天记录 —— **无数据来源或不适用**(只读镜像只持有 customer/group/message)。
- 导出聊天记录 —— 路线图 ③(另做)。
- 会话未读数 / 最后消息预览文本 —— 云客会话列表**无此字段**(只有今日消息数 + 最近联系时间),不做。
## 2. 云客真机核实(2026-08-08)
`wechat-window/1/{wxid}/{initialSession}` 三区结构(截图 + DOM 核实):
- **最左图标菜单**:单聊/群聊/朋友圈/通讯录/统计分析(完整聊天客户端;我们无后三者的数据,**不复制**)。
- **会话列表 sidebar**:顶部 `好友/群` 下拉筛选 + 搜索框(客户昵称/微信号);tab `聊天好友`/`已删除好友`;**每条会话 = 头像 + 昵称/备注(脱敏) + `今日消息N` + `最近联系时间:X月X日`**(**无**最后消息预览文本、无未读数、无在线状态)。
- **右消息面板**:会话名 + **开始/结束日期选择器**(默认约 2 周:`2026-07-25 00:00:00 ~ 2026-08-08 23:59:59`)+ 关键词搜索 + 类型筛选 + `导出聊天记录`/`删除聊天记录` + 消息气泡。
**关键结论**:
- 会话列表字段 = 头像/昵称/**今日消息数/最近联系时间**(全窗无关,全时段)。
- 日期选择器只过滤**右侧消息流**,不影响会话列表。
## 3. 后端 `GET /api/sessions`
镜像 `staff-chat-stats` 聚合模式(message 表算,无新表/无 Alembic,复用现有索引)。
- **query**:`owner_account_id`(必填)、`channel`(默认 `wechat`)。
- **好友会话**:`message WHERE channel=:ch AND owner_account_id=:owner AND (room_id IS NULL OR room_id='') AND timestamp IS NOT NULL` → `GROUP BY talker_id` →
- `last_chat_time = max(timestamp)`
- `today_count = count(timestamp >= :today_start)`(`today_start` = **Asia/Shanghai (UTC+8) 当日 0 点** ms-epoch —— 对齐云客中国时区;**不依赖服务器本地时区**,保证部署无关 + 测试确定性)
- **群会话**:`... AND room_id IS NOT NULL AND room_id != ''` → `GROUP BY room_id` → 同两字段。
- **取昵称/头像**:left-join `customer`(好友:`friend_remark`/`friend_nickname`/`real_name`/`head_image_url`,优先级同 Phase A)+ `group`/`chat_group`(群:`room_name` + 头像 if any)。
- **响应**:
```json
{
"owner_account_id": "...", "channel": "wechat",
"today_start": <ms>,
"sessions": [
{"type": "friend", "talker_id": "...", "name": "...", "avatar": "...", "today_count": N, "last_chat_time": <ms>},
{"type": "group", "room_id": "...", "name": "...", "avatar": null, "today_count": N, "last_chat_time": <ms>}
]
}
```
按 `last_chat_time` desc(NULL 最后)。
- **今日边界**:`today_start` = Asia/Shanghai(UTC+8) 当日 0 点的 ms-epoch(用 `zoneinfo.ZoneInfo("Asia/Shanghai")` 构造当日 0 点 → timestamp,**不用** `datetime.now()` 的服务器本地时区)。测试用 `time.time()` 播「当下」消息断言 `today_count>=1`、播「昨天(UTC+8)」断言排除——因 today_start 固定 UTC+8,测试确定性。
- schema:`SessionOut`(`type: Literal['friend','group']`、`talker_id|room_id` 二选一可空、`name/avatar` 可空、`today_count: int`、`last_chat_time: int|None`)。
## 4. 前端
### 4.1 新 `ChatSidebar.vue`
- props:`channel`、`ownerId`、`activeType`('friend'|'group'|null,当前选中会话类型,用于高亮)、`activeId`(当前 talker_id/room_id)。
- 调 `getSessions({owner_account_id, channel})` → 渲染会话列表:头像 + 昵称 + `今日消息{today_count}` + 最近联系(`formatTime(last_chat_time)`)。
- 顶部:`好友/群` 筛选(全部/好友/群)+ 搜索框(按 name/id 过滤,前端即可)。
- 点会话 → emit 或直接 `router.push`:
- friend → `{name:'chat', params:{channel, ownerId, talkerId}, query: 当前日期窗}`
- group → `{name:'group-chat', params:{channel, ownerId, roomId}, query: 当前日期窗}`
- loading/空态。
### 4.2 `ChatTimeline.vue` 双栏化
- 左 `<ChatSidebar :channel :ownerId :activeType :activeId />` + 右现有消息流。
- `activeType/activeId` 由路由派生:`/chat/.../:talkerId` → friend+talkerId;`/group/.../:roomId` → group+roomId;无 → null(空态)。
- 头部加 **页内日期选择器**(`el-date-picker daterange`,默认**近 2 周**)+ 关键词搜索框:
- 日期窗驱动消息流查询(取代 Task2 的纯 route-query 读法;**stat modal 带 query 入口 → 选择器初值取 query**,否则取近 2 周默认)。
- 关键词搜索:透传 `/api/messages` 的文本搜索能力(若后端 messages 已支持 `q`;否则前端过滤当前页——plan 核实)。
- `windowStart/windowEnd` 改为本地 `ref`(选择器驱动),初值从 `route.query` 或默认;`watch` 选择器 → 重查消息流。
- 向后兼容:CustomerList/GroupList「查看聊天记录」深链 `/chat/:channel/:ownerId/:talkerId` 照旧(侧栏显示该 owner 会话 + 该会话选中 + 消息流)。
### 4.3 路由
- `/chat/:channel/:ownerId/:talkerId?`(talkerId **可选**);`/group/:channel/:ownerId/:roomId?`(roomId 可选)。
- 新增/复用账号级入口:badge/昵称 → `/chat/:channel/:ownerId`(无 talkerId → 侧栏 + 「选择会话」空态)。
- StaffList `viewChat`(badge/昵称) 改推账号级 `/chat/:channel/:ownerId`(不再跳最近一条)。
### 4.4 类型/客户端
- `api/index.ts` + `types.ts`:`getSessions(query)` / `Session`/`SessionList` 类型;re-export。
## 5. 关键决策(已与用户确认 2026-08-08)
1. **核心范围**:会话列表(好友+群,今日消息数+最近联系时间)+消息流+日期选择器/搜索。排除朋友圈/通讯录/统计分析/已删除好友/删除记录(无数据/不适用);导出=③另做。
2. **后端+UI 一个 plan**(约 4 task)。
3. **架构 = ChatTimeline 双栏 + 新 ChatSidebar 子件**(侧栏常驻,同组件换会话,对齐云客;不做独立 ChatWindow 页以减少重构)。
4. **会话列表全时段**(today_count + last_chat_time,不受日期选择器影响);**日期选择器只过滤消息流**。
5. **日期选择器 = ChatTimeline 本地 ref(跨会话持久,默认近 2 周对齐云客聊天页)**:会话切换(同组件、路由 param 变)时选择器**不重置**,消息流按持久窗 + 新会话重查;**无需**在点会话时传 query。仅 stat-modal 带 query 入口时,选择器 mount 初值取 query;badge 账号级入口取默认(近 2 周)。
6. **不做未读数/最后消息预览**(云客会话列表无此字段)。
## 6. 文件清单(预期)
**后端**(`yunke-server/`):
- 新 `app/api/routers/sessions.py`(`/api/sessions` 聚合,镜像 staff_chat_stats)。
- 改 `app/api/schemas.py`(`SessionOut`/`SessionListOut`)、`app/api/main.py`(注册)。
- 新测试 `tests/test_api_sessions.py`(好友+群聚合、今日计数、最近时间排序、缺昵称 fallback、空集)。
**前端**(`yunkes/`):
- 新 `src/components/ChatSidebar.vue`。
- 改 `src/views/ChatTimeline.vue`(双栏 + 日期选择器 + 搜索 + 本地窗 ref)。
- 改 `src/router/index.ts`(talkerId/roomId 可选)。
- 改 `src/views/StaffList.vue`(`viewChat` → 账号级入口)。
- 改 `src/api/index.ts` + `types.ts`(`getSessions` + 类型)。
## 7. 验收
- 后端 `pytest -q` 全绿(含 `test_api_sessions.py`:好友/群聚合、今日计数、排序、fallback、空集)。
- `vue-tsc -b` exit 0。
- **浏览器实测**(本地 smoke.db,无云客):
- StaffList 点 badge/昵称 → `/chat/:channel/:ownerId`,左侧会话列表(头像/昵称/今日消息/最近联系,好友+群)+ 右空态。
- 点会话 → 右侧消息流加载该会话;侧栏常驻、当前会话高亮。
- 页内日期选择器改窗 → 消息流重查(会话列表不变);关键词搜索生效。
- stat modal 客户微信 → 仍跳 `/chat/.../:talkerId?start=&end=`,侧栏 + 该会话 + 选择器初值取 query。
- CustomerList/GroupList「查看聊天记录」深链照旧。
- network 仅 localhost。
## 8. 风险
- **今日计数确定性**:today_start 钉死 Asia/Shanghai(UTC+8) 当日 0 点(不依赖服务器时区);测试用 `time.time()` 播「当下(UTC+8 今日)」+「昨日(UTC+8)」消息对比,确定性。
- **群头像/群名缺失**:smoke.db 群数据稀疏(Plan 3 已知 gap);缺失显 `--`/默认头像,不阻塞。
- **ChatTimeline 重构面**:从「路由驱动单会话」→「双栏 + 本地窗 ref」,需保持向后兼容(深链/stat modal 入口)。plan 分小 task、每步 vue-tsc + 浏览器验。
- **关键词搜索后端能力**:`/api/messages` 是否支持 `q` 待 plan 核实;不支持则前端过滤当前页(声明近似)。
- **性能**:`/api/sessions` 每账号一次聚合(GROUP BY),现有索引覆盖;spec §8 类似 staff-chat-stats,可接受。