diff --git a/docs/superpowers/specs/2026-08-08-yunke-chat-sessions-design.md b/docs/superpowers/specs/2026-08-08-yunke-chat-sessions-design.md new file mode 100644 index 0000000..7c69f51 --- /dev/null +++ b/docs/superpowers/specs/2026-08-08-yunke-chat-sessions-design.md @@ -0,0 +1,127 @@ +# 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` = 本地 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": , + "sessions": [ + {"type": "friend", "talker_id": "...", "name": "...", "avatar": "...", "today_count": N, "last_chat_time": }, + {"type": "group", "room_id": "...", "name": "...", "avatar": null, "today_count": N, "last_chat_time": } + ] +} +``` + 按 `last_chat_time` desc(NULL 最后)。 +- **今日边界**:`today_start = int(datetime.now().replace(00:00:00).timestamp()*1000)`(本地 0 点)。测试用 `time.time()` 播「当下」消息断言 `today_count>=1`、播「昨天」断言排除。 +- 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` 双栏化 +- 左 `` + 右现有消息流。 +- `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. **日期选择器默认近 2 周**(对齐云客聊天页默认 ~2 周);stat modal 带 query 入口 → 初值取 query。 +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. 风险 + +- **今日计数确定性**:依赖「本地 0 点」+ 服务器时钟;测试用 `time.time()` 播当日 + 昨日消息对比(plan 落实)。 +- **群头像/群名缺失**: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,可接受。