Commit Graph

19 Commits

Author SHA1 Message Date
浅念 c56f13059d fix(group-stats): 群统计「查看详情」进纯聊天窗——无会话侧栏,操作列单按钮
用户反馈:微信群统计列表点查看聊天记录进入的页面带「好友列表/群列表」
侧栏不合适——云客该入口是专注的群详情聊天页(截图验证:无会话侧栏,
仅系统菜单+群名+工具栏+消息流)。

- ChatTimeline 增 bare 专注模式(?bare=1 隐藏会话侧栏);员工聊天窗口
  入口(不带 bare)侧栏常驻——昨天 WA 侧栏修复的场景不受影响
- GroupList 微信 tab 操作列精简为单按钮「查看详情」(=查看聊天记录,
  跳 bare 模式;导出HTML移除——聊天页头部本有导出),WA tab 不动

本地双路径验证:bare 无好友列表/有群聊标题;无 bare 侧栏在;列表点击
跳转 ?bare=1 正常。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 09:58:22 +08:00
浅念 5bfcebf20c fix(wa-chat): 群聊路由侧栏常驻——点群后「好友列表/群列表」不再消失
用户报 WA 聊天页点群列表里的群,跳到聊天框后侧栏(好友列表/群列表)
没了。根因:ChatTimeline 模板里 ChatSidebar 挂了 v-if="!isGroup"——
群路由(roomId 有值)下直接不渲染,是早期群聊独立页的设计遗留;
onSelectSession 本就支持好友/群双类型导航,侧栏组件自带好友/群过滤。

修:去掉 v-if 侧栏常驻,群聊中可随时切换好友/群会话(对齐云客体验)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-09 14:34:03 +08:00
浅念 9d3cc8e76e fix(messages): 聊天记录大对话超时——DESC首屏+keyset游标+群聊直达索引
用户报:聊天记录多时「聊天记录加载失败:请求失败 (ECONNABORTED)」(2026-09-07)。
服务器 EXPLAIN ANALYZE 实测两级根因:
- 单聊 1 万条:ASC 末页 OFFSET 10486 逐条回表 2515ms(DESC LIMIT 15ms,167 倍);
- 群聊 16,290 条:查询刻意不带 owner(chat_group 去重后 owner 错位),现有
  (channel,owner,room,timestamp) 索引被 owner 挡住,退化到 room 单列索引+
  filter+filesort——count 4.7s/首页 5.1s/末页 10.7s;真凶群 53132846289@chatroom
  40,863 条,三段串行 ~40s 必撞前端 axios 15s 超时。

修法(不动共享 MySQL 配置,沿用既往修复模式):
1. 迁移 e6f7a8b9c0d1:message 补 (channel, room_id, timestamp) 索引,群查询
   seek+排序直达+覆盖 count;模型侧双声明防 autogenerate 反 DROP。
2. /api/messages 加 order=asc|desc(默认 asc 不变)与 before_ts/before_id
   keyset 游标(成对,单传 422)——排序列 (timestamp,msg_id) 全序,平手按
   msg_id 决胜,游标页恒 O(page_size)。
3. 前端 ChatMain/ChatTimeline 首屏改「order=desc 取最新 50 反转」单请求
   (原 page1 拿 total+深 OFFSET 末页两请求),「加载更多」改游标上翻;
   hasMoreOlder=已加载<总数。SessionPane 搜索等 order=asc 默认路径不受影响。

本地实测(37,532 条群):desc 首页 0.39s、游标上翻 0.24s,浏览器真机
渲染+上翻 150 条无错。测试 8 增(desc/游标/平手边界/422/窗口叠加/
前端流程不变式),全量 601 绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-07 16:59:04 +08:00
浅念 91639b28d8 fix(chat): loadInitial/loadTalkerName 加请求序号防会话切换竞态(ChatMain+ChatTimeline同修)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 18:20:14 +08:00
浅念 a7ef8632a0 feat(chat): 群聊隐藏左侧会话列表(对齐云客)+会话名WA去@s.whatsapp.net后缀显号码
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 18:29:27 +08:00
浅念 4f375c3d71 fix(chat): 群聊消息查询不带owner(chat_group按room去重owner被末位员工覆盖,与消息归属错位查空)
room 全局唯一且消息只在单一owner名下(多owner群=0),按room查即正确;
MessageQuery.owner_account_id 改可选。实测43304群6000条正常显示。
16个活跃群0条消息=增量未覆盖,4h调度自动补

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-22 16:50:56 +08:00
浅念 b9d250cdc1 fix(chat): 默认时间窗改全部时间(原近2周把老对话过滤成空——7-26的对话打开显示暂无聊天记录)
点「查看聊天记录」就该看到记录;需要过滤再手动选日期(已开clearable,
清除即回全部);stat-modal 带窗入口(?start&end)不受影响

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-22 16:16:01 +08:00
浅念 ce5cada898 feat(chat): 归属账号显示昵称(账号表查询,昵称+ID并列,回退原始ID)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 15:34:17 +08:00
浅念 6c9245a682 fix(groups): /api/groups加room_id精确直查(群聊页群名解析,修page_size=200超限422)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 10:58:39 +08:00
浅念 661ecfc8c9 feat(chat): 聊天标题显示昵称(客户→昵称,群→群名,替代原始ID)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-15 10:54:21 +08:00
浅念 bb328ac59a feat(chat): ChatTimeline 导出按钮 + 修复 downloadBlob 文件名丢失(Content-Disposition)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 17:07:58 +08:00
浅念 239f07e7a1 feat(chat): 页内日期选择器(跨会话持久)+ 关键词搜索(已加载页过滤)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 15:29:19 +08:00
浅念 8f166a9c18 feat(chat): ChatTimeline 双栏化(左会话列表)+ 路由可选参 + badge 账号级入口
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 15:23:26 +08:00
浅念 cdbfe3e5da fix(chat): 日期窗 start/end 成对透传(避免单边过滤)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 13:56:14 +08:00
浅念 765f80dd02 feat(chat): ChatTimeline 支持 start/end query 日期窗过滤(从 stat modal 带窗跳入)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 13:53:02 +08:00
浅念 8fde1d8597 fix: render chat media by URL (images/video/voice) instead of unknown-type placeholder
MessageBubble.vue was keys off the (incomplete) type code, so many messages
with real OSS media URLs (https://wx.oss.yunkecn.com/.../*.webp|*.mp4) showed
as '[未知消息]'. Rendering is now URL-driven: pick the best http URL from
file_url > file_hd_url > file_th_url, then render by extension
(image/video/audio/file). Type code is only used as a last-resort label.

- MessageBubble.vue: URL-driven cascade (URL by ext → duration →
  transcription → text → structured-type placeholder → localized tag).
  Image/video/audio/file children @click.stop so the all-fields drawer
  still opens on bubble click but not on media interactions.
- format.ts: wechatTypeLabel(type_raw) maps known codes (3→图片, 34→语音,
  43→图片/表情, 49→文件/名片, 42→名片, 9→表情, 20→红包/转账).
- ChatTimeline.vue drawer type tag uses wechatTypeLabel for WeChat msgs.
- style.css: compact .yk-msg__audio player for voice-with-URL messages.

Verified against smoke.db: 25 type-3 images + 3 type-43 videos that
previously showed '[未知消息]' now render as images/video. Build clean.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 10:23:01 +08:00
浅念 6ac47a1203 feat: full-field display for message/staff/accounts (raw_json + drawers)
Replicates the Customer all-fields pattern across the remaining three
entities. Backend now exposes `raw_json` on MessageOut / StaffOut /
AccountOut so the frontend can render every 云客 source field without a
second endpoint. Each entity gains a Chinese-label field utility + an
el-drawer surfacing all fields (structured-first, raw_json fallback,
unknown keys under 其他).

Backend:
- schemas.py: add `raw_json: dict | None` to MessageOut / StaffOut /
  AccountOut. MessageOut/StaffOut pick the column up via from_attributes;
  AccountOut is built explicitly in the router, which now threads
  `raw_json` through from both WechatAccount and WhatsappAccount rows.
- tests: new raw_json serialization tests for messages, staff, accounts
  (assert populated dict round-trips verbatim + NULL serializes as null).

Frontend (yunkes/):
- New utils: messageFields.ts, staffFields.ts, accountFields.ts — each
  with a 中文 label map, ordered groups, structured-column preference,
  and an 其他 fallback for unknown raw_json keys.
- Message: clicking a chat bubble opens an all-fields drawer (referMsg
  renders as pretty-printed JSON). Image preview + file download links
  stopPropagation so their own interactions still work.
- Staff: row click → drawer; columns relabeled (员工ID, 销售手机).
- Accounts: new AccountList.vue at /accounts with a 账号 nav item,
  unified WeChat/WhatsApp table (渠道/账号ID/昵称/微信号/归属员工/好友数/
  群数/收发/在职/最后更新), channel filter, pagination, row-click drawer.
- api: add Account type + getAccounts(); add raw_json to Message/Staff.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 17:47:50 +08:00
浅念 b822e87d4d feat: remaining frontend views (groups/calls/sms/sync) + messageout media fields
Backend (Part A):
- MessageOut: add file_th_url, file_hd_url, duration, transcription_text, md5
  (room_id + sender_head_url already existed) so the chat timeline can render
  image thumbnails, voice duration/transcription, and avatars from a single
  response.
- PhoneCallOut: add record_file so the Calls view can offer recording playback.
- SmsOut: add content (full body) so the SMS detail dialog shows the whole text.
- Tests: red->green for each new field (media fields default None + populated,
  record_file round-trip, content round-trip). Suite 267 -> 271.

Frontend (Part B):
- api/index.ts: wrappers getGroups/getGroupMembers/getCalls/getSms/getSyncStatus
  + Group/GroupMember/PhoneCall/Sms/SyncState/SyncRun/SyncStatus types in
  types.ts (mirror the *Out pydantic schemas).
- views/GroupList.vue (/groups): table + channel filter + pagination; row click
  opens the group timeline.
- views/CallList.vue (/calls): table + call-type filter (phone_call/voice_call/
  all); "播放录音" action opens an el-dialog with <audio controls>.
- views/SmsList.vue (/sms): table; row click opens a detail dialog with the full
  content.
- views/SyncStatus.vue (/sync): two tables (states + recent_runs) with a manual
  refresh; run status color-coded.
- ChatTimeline.vue: reused for both 1:1 DM (talkerId) and group (roomId) by
  branching the query; group title + per-member sender already handled by
  MessageBubble.
- router: add /groups /calls /sms /sync + /group/:channel/:ownerId/:roomId.
- layouts/MainLayout.vue: replace the single 客户列表 button with an el-menu
  (客户 / 群 / 通话 / 短信 / 同步); org tree kept on the left.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 16:38:47 +08:00
浅念 6c4cd92ae6 feat: vue3 frontend scaffold + org tree + customer list + chat timeline
M5b viewing frontend for the yunke data-mirror, under yunkes/.

- Scaffold: Vite + Vue 3 + TS, with element-plus, @element-plus/icons-vue,
  pinia, vue-router@4, axios. Dev server on :5173 with /api and /api/media
  proxied to the FastAPI backend on :8000.
- API client (src/api/): axios instance (baseURL '/api') whose response
  interceptor unwraps the axios envelope; typed wrappers (getCustomers,
  getMessages, getDepartments, getStaff) returning Page<T> / arrays; types
  mirroring the backend *Out schemas (Message typed as a permissive superset
  so file_th_url/duration/transcription_text degrade gracefully today and
  light up once MessageOut exposes them).
- Layout: el-header top bar (title + channel switch) + el-aside department
  tree (built from the flat parent_id list via useOrgStore, with search
  filter) + el-main router-view.
- CustomerList (/customers): el-table with channel filter, department filter
  driven by the tree selection (route query), pagination, and row-click into
  the chat timeline (channel + owner_account_id + talker_id).
- ChatTimeline (/chat/:channel/:ownerId/:talkerId): asc-ordered message
  bubbles keyed by is_send (direction) and type (text/image/video/voice/file/
  divider/system/unknown); lands on the most-recent page and prepends older
  pages via load-more with scroll anchoring.
- Home '/' redirects to '/customers'.

npm run build (vue-tsc -b + vite build) is clean.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 16:27:22 +08:00