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

11 KiB
Raw Blame History

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)。
  • 响应:
{
  "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,可接受。