Commit Graph

43 Commits

Author SHA1 Message Date
浅念 ddeda3fa22 feat(management): client补员工详情/分段接口+时间预设补今日昨日本周上周
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 11:07:25 +08:00
浅念 7b0e1601b7 feat(management): 前端API client——管理端点typed契约(批次B冻结面)
对批次B as-built逐键核对后相对任务稿的修正(以真机响应为准):
- DashboardResp 补 caliber_notes(总述口径说明)
- DrilldownResp 补 page/page_size/scope(各分支均返回)
- FactDrillRow 补 chat_hint(owner+talker 直达聊天记录)
- LifecycleDrillRow.nickname 收窄为非空(后端 friend_id 兜底)
- EmployeeRow.metrics 注明 user_id 键为字符串透传字段

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 09:57:44 +08:00
浅念 b0ebe8a15a feat(group-stats): 微信群列表对齐云客「微信群统计」——getChatroomList 数据源+窗内统计
用户报微信群列表数据/内容不对,应对齐云客「沟通→微信号管理→微信群统计」
(/cms/wechat/wechatgroup)。真机探明(表单登录+页面导航抓 network):

- 数据源=POST /wechatweb/chatroom/getChatroomList(form:departmentId+
  startTime/endTime 窗+分页;信封 data.overviews[]/totalCount 1,854 行)
- 云客窗是任意时间窗实时算:沟通消息数实测同群全史 417/7天 36/单日 6
- **修正存量映射错误**:member_count 原存 friendNum(员工好友数),真群成员
  数是 friendCount/getChatroomList.memberCount
- 新增/退出成员数云客按窗实时算,本地无事件流 → 按天积累

实现:
- ChatGroup 补 group_remark 列(备注仅 friend_list/es 携带,两归一化均
  「有值才写」防互清);迁移含存量 raw_json 回填(须先于基线抓取执行)
- 新表 chat_group_member_stats(渠道×账号×群×天一行 added/lost)
- 新实体 chatroom-stats:全窗刷档案列(真成员数/群备注/员工信息,owner
  经 wechat_account alias 解析)+ --day 单日窗落进出日行;full_sync 5c 步
- /api/groups 扩展:msg_start/msg_end 窗内聚合(window_msg_count=
  msg_daily_stats 按天和、window_added/lost=日表和)、last_start/end
  最新消息窗、group_remark 子串;行补员工微信/员工信息关联列
- GroupList 微信 tab:统计时段(默认近7天)/最新消息时间段/群名称/群备注/
  员工下拉/查询 + 9 列(群信息|群备注|员工微信|员工信息|群成员数量|
  新增成员数|退出成员数|沟通消息数|操作);WA tab 不动

700 测试绿(新增窗聚合/筛选/员工列 3 例)。本地实测:基线 1,854 群=云客
total 一字不差,今日日窗 added 407/lost 166 落表,页面 9 列/员工列渲染正常。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-09 18:09:20 +08:00
浅念 d42895337d feat(link): customer_account_link 账号↔客户多对多——共享好友员工维度对齐云客
用户报「Lily 好友数 36,点进员工客户页只有 12」。真机探明根因:那 24 个
是共享客户(同一人加多个员工),云客 per-employee 各算一个;而 customer
表 PK=friend_id 全局单行、归属 last-wins,其他员工名下必然「消失」。
抓取器本身每员工都拿全了(日志 declared=36 rows=36),丢在入库模型层。

- 新表 customer_account_link(渠道×账号×客户一行,per-relation 快照:
  昵称/备注/头像/手机号/收发计数/最近沟通/add_time/fromType/isDel)
- 写:repos.upsert_customer 随档案同事务维护(所有好友抓取路径一处挂全);
  新增 prune_customer_links 对账
- 读:/api/customers?owner_account_id= 改读 link(与云客 contacts_num 口径
  一致);该账号 link 为空时回退档案路径(过渡安全);与 dept/user 等
  过滤组合时维持旧行为;CustomerOut 透传 from_type
- 抓取器:friends/wa-friends 逐员工整批 upsert 后按名单对账(取满声明
  total 才删,防半份名单误删)
- 前端:StaffFriends 数据来源列优先 link 的 from_type

611 测试绿(link 新 6 例);本地验证空表回退/造数接管/from_type 透传。
上线后需重跑 friends+wa-friends 回填 link(存量关系无法从档案反推)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-08 17:17:06 +08:00
浅念 2e526cd056 feat(staff-friends): 员工客户列表独立页——对齐云客员工详情,好友数/微信群数下钻
员工列表点「好友数」原先跳全局 /customers?owner_account_id=x,布局与全局
列表无异且计数/分页绑全局声明口径(同日 dfa4fdd 修的显示 bug),与云客
「员工名下客户列表」体验割裂。新页 /staff-friends/:ownerId/:tab:

- 微信客户/微信群两 tab(对齐云客截图);标题=员工显示名(入口直带,
  直链经 /api/accounts 解析),返回箭头回员工列表
- 搜索(昵称/微信号/备注名/手机号,服务端 q)+ 查询/导出 CSV
- 发送/接收条数=Customer.send_num/receive_num 累计值(CustomerOut 补
  两列,同步自云客 friend 记录,与截图同源)
- 勾选批量「导出聊天记录」:新端点 GET /api/export/chat-html-zip,复用
  chat-html 渲染器每会话一个 HTML 打 zip(文件名=备注>昵称>friend_id,
  非法字符清洗;单会话 5000 行截断告警;一次上限 50 会话防误选打爆)
- 行内:查看聊天记录→staff-chat;导出HTML聊天记录(时间范围弹窗)
- WA 行保持原入口(该页暂仅微信)

后端 605 测试绿(zip 新 4 例);本地浏览器端到端验证:过滤 9 条/勾选
激活/群 tab/返回箭头;zip 产物 application/zip 双会话双气泡。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-08 16:14:44 +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
浅念 085a9b4fd3 feat(analytics): 管理层指标呈现——首响回复率上墙/客户资产总览/自定义日期/排行五新列
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-07 14:41:08 +08:00
浅念 bbfad520f2 feat(analytics): 员工排行条形图对比+近30天范围——幅度一眼可读/本地停更窗口也可用
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-07 13:43:23 +08:00
浅念 1aadc80d6b feat(analytics): 看板v2全平台口径——默认双渠道合计/拆分小字/双线趋势/跳转带query预选
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-07 12:20:18 +08:00
浅念 487f45b074 feat(analytics): 看板管理化补强——员工活跃排行表+预警行动入口卡片
管理者反馈「纯汇总数字看不到有用信息」:overview 载荷扩
staff_ranking(全员行:msg_total rollup 优先/空表回退 msg_daily_stats
聚合,active_* 两路径统一走 msg_daily_stats 窗内 distinct peer——
stat_daily_staff 的 active_* 是当日快照,跨日 SUM/MAX 语义皆错;
call_cnt 按 user_id 落窗直查;环比同期对齐)+ actions(该回未回 2h/
真流失 30d/敏感词 pending,复用已测试查询路径各取 total)。

前端看板:toolbar 下三张可点行动卡(0 灰显仍可点,直达处置页,
随 channel 切换刷新)+ 趋势卡下员工排行表(max-height 420 滚动,
未映射小 tag,环比色同卡片惯例 None 显 '-')。

测试 574→577 全绿(rollup 聚合/回退/行动计数三新);build 过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-07 11:48:24 +08:00
浅念 5fb19dd3ac feat(analytics): 前端板块骨架——echarts按需/useChart/路由/侧栏组/API模块
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-04 21:25:48 +08:00
浅念 6880ff1b97 feat(auth): 登录页(用户名/密码换 API token)+ 前端全链路登录态
后端:
- POST /api/auth/login:admin 账密(Settings 默认 admin/admin123,
  ADMIN_USER/ADMIN_PASSWORD 可覆盖)换取共享 api_token,免 token 中间件
- 防爆破:同 IP 15min 窗口内 5 次失败锁 15min(429),成功登录清零
- 错误信息统一「用户名或密码错误」(防用户名枚举)
- 测试 7 个新增(TDD),全量 500 绿

前端:
- LoginView + 路由守卫(未登录跳 /login,回跳 redirect)
- axios 请求拦截器自动带 Bearer;401 清登录态回登录页
- 媒体直链统一 mediaUrl() 带 ?token=(img/audio 标签发不了 header),
  替换 4 处裸拼;downloadBlob 手动带头
- isLoggedIn 按 key 存在判定 — 后端鉴权关闭时空 token 不锁死登录页
- MainLayout 侧栏底部退出登录

compose:两份编排透传 ADMIN_USER/ADMIN_PASSWORD(默认 admin/admin123)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-03 15:03:20 +08:00
浅念 4c3bc3ee93 feat(staffchat): 统计分析7tab(分型明细列+计数徽标+近7天窗+msgid定位,对齐云客)
- StatsPane 占位替换: 7 tab(label 带 (N) 计数)+daterange 默认近7天(清空后端兜底)
- 明细列配置驱动(KIND_COLS);link 列跳 single/group 聊天;红包/转账行带 ?msgid 定位
- 语音通话列对齐 CallList(通话人/类型/呼入呼出/接通状态/时长/录音播放 dialog/时间/操作)
- 敏感词 tab 空态卡(词库未配置,待 C1 接入,零请求)
- 员工微信列用资料卡信息(明细行无 staff 字段,数据本属该员工)
- counts 与明细解耦;双 seq 竞态守卫(兄弟组件惯例)
- api: getStaffStatsDetail kind 收紧为 StaffStatsKind 联合(后端 Literal 6 值)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 19:32:14 +08:00
浅念 0371484dad feat(staffchat): 通讯录(客户表+群表列对齐云客+批量勾选导出+分页)+朋友圈翻页失败内联重试
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 18:56:02 +08:00
浅念 f2bcac0a4d feat(staffchat): 路由+API client+类型+relativeChatTime(还原旧页WIP+占位StaffChat)
- 还原 ChatTimeline.vue/api/index.ts 未提交 WIP(新页自建,旧文件回 HEAD)
- /staff-chat/:ownerId/:tab?/:talkerId? 路由 + 占位 StaffChat.vue(Task 9 替换)
- types: MomentsPost/MomentsList/StaffStatsCounts/StaffStatsDetail +
  ChatSession.pinned + Account.avatar
- api: getMoments/getStaffStatsCounts/getStaffStatsDetail + sessions
  is_del + messages msg_type/q 过滤
- format: relativeChatTime(今天HH:mm/昨天/M月D日/YYYY年M月D日) + 4 例
  vitest spec(全过,vitest 未入 devDependencies,npx 拉取运行)
- tsconfig.app.json exclude __tests__(vitest 未装,保持 vue-tsc 通过)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 16:45:30 +08:00
浅念 b6eea3070a feat(staff): 弹框客户微信/员工微信列改富单元格(头像+昵称+ID+真实姓名,对齐云客)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 10:24:07 +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
浅念 a290e39523 feat(customers): 发送/接收列改今日口径(对齐云客;本地消息表Asia/Shanghai今日聚合)
- CustomerOut 加 send_today/receive_today(路由赋值,非ORM列)
- /api/customers 每页一次 GROUP BY(owner IN 走复合索引前缀)
- 前端列优先今日值,无消息数据回退原raw字段;列名对齐云客「发送条数/接收条数」
- TDD:今日3发2收+昨日不计入+无消息客户0

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-22 14:39:17 +08:00
浅念 edca9b4c77 fix(customers): WA客户破1万上限(逐员工uid过滤,8585→11931全量)+双渠道页脚云客口径+消息同步刷新最近沟通时间
- wa-friends 逐员工抓取:全局查询~1万行硬上限(云客11931只翻到10000),
  uid=员工WA号过滤(真机验证 declared 1145→1149),total-aware翻页
- declared-total 分渠道:Σ wechat/whatsapp_account.contacts_num(8590/11915)
- 消息增量同步顺带刷新 customer.last_chat_time(云客列表按最近沟通实时
  排序,快照每日一次导致顺序滞后;微信1:1+WA都加)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 14:04:06 +08:00
浅念 fd3cf0003b feat(customers): 微信客户页脚改云客口径(Σ员工好友数声明值8590,用户选B方案)
- GET /api/customers/declared-total: Σ wechat_account.contacts_num
- 页脚优先显示云客口径(回退真实total);列表内容保持去重真实行
- 云客列表声明值含其ES索引重复doc(连行id都相同无法物理区分),仅对齐计数

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 13:44:39 +08:00
浅念 4aeed003a4 feat(calls): 微信语音列表加今日统计卡片+通话时间范围筛选+客户跳转带通话时间定位(对齐云客)
- GET /api/calls/stats:今日(Asia/Shanghai)好友呼入/外呼量+群量+时长聚合(@chatroom判群)
- CallList:5张统计卡片(仅微信语音tab)+datetimerange筛选(服务端start/end)+viewCallCustomer带?start定位
- TDD新增stats聚合测试,全量绿

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-20 11:08:13 +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
浅念 ab5a918a3b fix(calls+scraper): 语音群scope服务端过滤+对话发现缺口补全+抓取韧性三连修
- /api/calls 加 scope=friend|group(@chatroom后缀判群,与isRoom实测等价120=120),CallList去掉「全部」tab改服务端查
- 对话发现:全部customer pair ∪ 语音pair(weChatId,talker),未扫过的排最前——修复纯语音关系客户聊天记录永远空(35%语音客户pair从未扫过)
- voice-calls改浏览器直发(isRoom=0/1双趟+WA callSource=4,拦截方式已两次失效)
- 断连恢复重试30×2min(云客约3h一次断连,已实战扛过多次)
- WA messages upsert改整列表直调(_upsert_all逐条传dict被迭代成key字符串→TypeError)
- 405测试绿含scope新增3个

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 16:48:22 +08:00
浅念 661ecfc8c9 feat(chat): 聊天标题显示昵称(客户→昵称,群→群名,替代原始ID)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-15 10:54:21 +08:00
浅念 5ce33232c1 feat(sync): 聊天图片本地化(message media_hash+download_message_images+local-first rendering)
- Message 模型新增 media_hash 列 (sha256 FK -> Media.media_hash)
- Alembic 迁移 e5f6a7b8c9d0_message_media_hash (batch_alter_table, SQLite/MySQL 通用)
- app/scraper/media.py:download_message_images 批量下载器,镜像 download_call_recordings
  - 候选: media_hash IS NULL AND channel IN ('wechat','whatsapp') AND type IN ('image','video','file') AND 任一 file_*_url LIKE 'http%'
  - image 类型优先 file_th_url (缩略图,小快省),video/file 优先 file_url
  - per-row try/except + per-run tried set,失败不挂循环
- sync_message_images orchestrator 入口 + ENTITY_MESSAGE_IMAGES 常量 (提交自身 SyncRun,镜像 call_recordings)
- scheduler: _run_message_images + ENTITY_FUNCS['message_images'] + 加入 daily all job (尾部)
- MessageOut schema 暴露 media_hash 字段
- MessageBubble.vue local-first: media_hash 优先 /api/media/{hash},否则回退 OSS file_url/file_th_url/file_hd_url
- types.ts: Message 接口添加 media_hash 字段
- 新增 test_message_images_downloader.py (6 tests, 镜像 test_recording_downloader)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 18:24:11 +08:00
浅念 538a0dc635 feat(sync): 长语音音频本地化(voice_clip media_hash+download+local-first playback)
Mirror download_call_recordings pattern for voice_clips:
- VoiceClip.media_hash column + Alembic migration (c4d5e6f7a8b9)
- VoiceClipOut.media_hash + frontend VoiceClip.media_hash type
- download_voice_clip_audio in app/scraper/media.py (resilient, sha256-dedup,
  same download_url/upsert_media helpers as call recordings)
- sync_voice_clip_audio orchestrator wrapper (commits own SyncRun, mirrors
  sync_call_recordings semantics)
- scheduler: _run_voice_clip_audio + ENTITY_FUNCS['voice_clip_audio'] +
  appended to daily _run_all job (after voice_clips)
- VoiceClipModal playClip local-first: /api/media/{media_hash} with OSS
  oss_file_name fallback (mirrors CallList.playRecord)

Tested: 376 pytest pass; vue-tsc clean; migration applied; 1523/1533 clips
localized (10 pending are genuine upstream HTTP 404s — resilient skip).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 12:06:26 +08:00
浅念 9da272b992 feat(calls): voice_clip 手动匹配(voice_clip↔customer 本地关联写操作)
- VoiceClip: 新增 matched_friend_id 列 (String(128), nullable) + alembic 迁移 b3c4d5e6f7a8
- PATCH /api/voice-clips/{id}/match: friend_id 关联 / null 取消 (本地写, 不回写云客)
- GET /api/voice-clips: 批量解析 matched_friend_name (remark>nickname>real>id)
- VoiceClipModal: 已匹配客户列 + 匹配/取消匹配按钮 + 嵌套客户选择 picker (默认按员工列客户, 名搜索走 /search)
- 测试: 17 passed (新增 7 个 match 用例覆盖 set/clear/name 解析/404)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 10:43:34 +08:00
浅念 f1b5861b35 feat(calls): VoiceClipModal 只读长语音列表+播放,接入 CallList(微信+WA语音)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-10 18:37:29 +08:00
浅念 bb328ac59a feat(chat): ChatTimeline 导出按钮 + 修复 downloadBlob 文件名丢失(Content-Disposition)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 17:07:58 +08:00
浅念 394bfa8a7c feat(chat): getSessions + ChatSidebar 会话列表组件
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 15:13:30 +08:00
浅念 876767cd68 feat(staff): StaffList 微信 5 数字改读本地 summary + 日期选择器(Phase A 接线)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 18:42:19 +08:00
浅念 05cc1dfcc4 feat(calls): 前端录音播放本地优先(/api/media/{hash}) + OSS 回退 + drawer
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 16:55:49 +08:00
浅念 ca3a2a2d3d feat(calls): CallList 改 el-tabs(微信语音/WA 语音/电话)+ CallChannel 加 wa_voice_call
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 10:50:38 +08:00
浅念 a66c3be00a chore: final Minor polish — export filename/test断言, downloadBlob 类型/清理, plan docs
address review Minors: drop redundant filename replace, assert 暂无消息,
tighten downloadBlob return type + try/finally, fix plan group time-picker wording.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 18:06:17 +08:00
浅念 ebac26822b feat(api): add downloadBlob helper for blob attachment download
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 17:27:16 +08:00
浅念 dfdd0595d2 feat(staff): action buttons → filtered customer/group list
Adds groups.user_id filter; viewCustomers/viewGroups push owner_account_id query; CustomerList/GroupList read it.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 19:06:27 +08:00
浅念 635e1145b4 feat(api): customer query params user_id/add_time_since/last_chat_since
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 18:50:14 +08:00
浅念 a92fa2927c feat: full-text search (messages + customers)
Backend (yunke-server):
- GET /api/search?q=&type=messages|customers|all&channel=&start=&end=&page=&page_size=
  LIKE '%q%' scan over message.text + customer friend_nickname/real_name/
  phone/wechat_phone. type=all returns both sections; each is its own Page[T].
- type=messages orders by timestamp desc; type=customers orders by
  last_chat_time desc (nulls last).
- q required: missing/empty/whitespace -> HTTP 400.
- 14 new tests in tests/test_api_search.py (TDD red -> green).

Frontend (yunkes):
- New /search route + 搜索 nav item with Search icon.
- SearchView.vue: search bar, two stacked sections (消息结果 / 客户结果),
  counts + independent pagination per section, keyword highlighting (escaped),
  click message -> /chat/:channel/:ownerId/:talkerId,
  click customer -> all-fields drawer (CustomerList pattern reuse).
- search() wrapper in src/api/index.ts with SearchQuery/SearchPayload types.
- Empty state: "输入关键词搜索消息和客户".

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 11:46:21 +08:00
浅念 0d466c4e4a feat: full-field display for group/call/sms (raw_json + drawers)
Mirrors the Customer/Message/Staff/Accounts pattern: backend exposes the
verbatim upstream record on the response schema; the frontend list shows
source-matching columns and a row-click opens an all-fields el-drawer with
Chinese-labeled camelCase keys (unknown keys roll up under 其他).

Backend (yunke-server/app/api/schemas.py)
- GroupOut / PhoneCallOut / SmsOut: add raw_json: dict[str, Any] | None
  (Pydantic v2 from_attributes picks up the existing ORM column).
- Tests: extend tests/test_api_{groups,calls,sms}.py with a per-schema
  raw_json round-trip assertion (phone_call + voice_call payload shapes
  both covered for calls).

Frontend (yunkes)
- src/utils/{groupFields,callFields,smsFields}.ts: 中文 label map +
  grouped sections + readField (structured-column preference) +
  toDisplayField (image / time / audio / bool / status / direction).
- src/api/types.ts: add raw_json to Group / PhoneCall / Sms.
- src/views/GroupList.vue: richer columns (群名/群ID/成员数/群主/归属微信/
  消息数/最近聊天/渠道) + all-fields drawer (chatRoomId/chatRoomName,
  memberCount/memberIds, headImageUrl, wechatId/wechatNickName,
  userName/userPhone/userId, messageCount/ownMessageCount,
  lossMembers/addMembers, lastChatTime/updateTime).
- src/views/CallList.vue: keeps existing columns + adds all-fields drawer
  (id/startCallTime, callDuration/callSeconds/ringTime, callStatus/
  incomingCall, userId/userIdName/departmentId, callNumber/simPhone/
  simNumber/imei, recordFile/isUpload/downloadCount/aiAsr/qcStatus/
  planCustomerName/version + voice-call extras ossFileName/duration/
  startTime/endTime/remark). Drawer plays recording inline via audio
  render kind; existing play-record dialog retained.
- src/views/SmsList.vue: keeps existing columns + adds all-fields drawer
  (id/customerPhone, sms/smsSub, sender, direction, userId/
  senderDepartment, receiveName, createTime, simPhone/simNumber, source)
  with a full-body preview block at the top.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 10:12:49 +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
浅念 e5af1a2bc2 feat: customer full-field display (raw_json + cloud-matching list + all-fields drawer)
Backend:
- CustomerOut now exposes raw_json (verbatim upstream friend record) so the
  frontend can render every 云客 field without per-field structured columns.
- The verbatim blob round-trips via from_attributes; no model change needed.

Frontend:
- CustomerList table mirrors the 云客 客户列表 column set (头像/微信昵称/微信号/
  备注/真实姓名/手机/性别/地区/描述/销售员工/部门/来源/添加方式/添加时间/最近
  聊天/消息数/渠道). A readField helper reads the structured column first and
  falls back to raw_json.<camelCase>.
- Row click opens an all-fields drawer (right side) rendering EVERY field of
  the record with Chinese labels, grouped (基本信息/联系方式/归属员工与部门/时间
  与来源/消息统计/好友状态/其他). Timestamps format to local datetime, flags
  render 是/否, image URLs render as el-image thumbnails, empty values show —.
- New src/utils/customerFields.ts holds the camelCase→中文 label map, the
  structured-column alias map, group definitions, and value-type formatters;
  unknown raw_json keys auto-flow into an 其他 section so new upstream fields
  surface without code changes.
- Drawer 查看聊天记录 action navigates to /chat/:channel/:ownerId/:talkerId
  (owner_account_id + friend_id + channel), preserving the existing path.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 17:24:38 +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