Commit Graph

157 Commits

Author SHA1 Message Date
浅念 05d1e41d31 fix(staff): 默认统计窗改近30天(覆盖smoke.db数据范围,数字不再全0) 2026-08-11 13:53:55 +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
浅念 8b19c299bb feat(calls): WA语音 单元格可点(客户→WA chat,员工→drawer)+ chatChannel helper
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 11:59:56 +08:00
浅念 2b2f863800 feat(calls): VoiceClipModal 录音异常检查(本地元数据标记+仅看异常筛选)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 10:50:49 +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
浅念 4ee19cb9d9 chore(ui): 删除冗余 AccountList 页,/accounts 重定向 /staff(总 spec 收尾)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 17:16:26 +08:00
浅念 bb328ac59a feat(chat): ChatTimeline 导出按钮 + 修复 downloadBlob 文件名丢失(Content-Disposition)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 17:07:58 +08:00
浅念 5bf5e25bad feat(calls): CallList 微信语音 客户/员工/姓名 单元格可点(复用聊天导航,对齐云客)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 16:19:21 +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
浅念 394bfa8a7c feat(chat): getSessions + ChatSidebar 会话列表组件
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 15:13:30 +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
浅念 7405ac5fc9 feat(staff): stat modal 列对齐云客(序号/员工信息/员工微信) + 客户微信可点带窗跳聊天
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-08 13:46:59 +08:00
浅念 5fd9878c38 fix(staff): on-demand modal 拉取仅在日期窗未变时回写缓存
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 18:58:30 +08:00
浅念 f51623d652 feat(staff): 微信 5 数字 modal 换本地数据源(metric 过滤 + 合成分页)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 18:52:07 +08:00
浅念 cab8ab95dc fix(staff): loadWechatStats 加载竞态守卫(token)+ 重查时清空旧窗数字
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 18:50:02 +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
浅念 81f5a31864 fix(calls): drawer callStatusLabel channel-aware + isRoom 按 是/否 渲染
drawer 本地 callStatusLabel 此前为 phone-only 极性,与 list(Task 5 已 channel-aware)
在 voice/wa 的 call_status=2 上矛盾。改为按 channel 分支并补 1=接通;isRoom 加入
BOOLEAN_KEYS 以 是/否 渲染。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 12:02:14 +08:00
浅念 7a5bb9927e feat(calls): callStatusLabel channel-aware + callTypeLabel WA + 补 WA/微信语音字段 label
call_status 极性随 channel 不同(phone 0未接/2接通;voice/WA 1接通/2未接通),
仅值 2 按 channel 分支。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 11:54:38 +08:00
浅念 28d8448f62 feat(calls): 电话 tab 保留通用 8 列(按 channel 分离列集)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 11:09:47 +08:00
浅念 f8705cb741 feat(calls): WA 语音 tab 对齐云客列(状态/客户/员工/通话时间和录音)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 11:03:22 +08:00
浅念 f4dc115129 feat(calls): 微信语音 tab 对齐云客列(通话信息/客户/员工微信/员工信息/录音/时长)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 10:57:17 +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
浅念 6dfed36c05 feat(groups): 微信群操作列加「导出HTML聊天记录」(补 Plan3 漏的操作)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 17:57:58 +08:00
浅念 d1475e8f42 feat(customers): 微信客户操作列加「导出HTML聊天记录」+ 时间选择
对齐云客:先弹「请选择时间」→ 确定后调 /api/export/chat-html 下载。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 17:55:10 +08:00
浅念 b63b71bf68 feat(staff): 微信员工明细 modal 内客户微信 → 聊天记录(二级操作)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 17:47:02 +08:00
浅念 6a30bef21b feat(staff): 微信员工 主badge→聊天 + 员工姓名→drawer(纠正手机号误判)
云客跳详情页的是员工姓名(button+history),非手机号;手机号不可点。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 17:43:45 +08:00
浅念 b9055b5714 feat(staff): 微信员工及时回复 2 数字 → 明细 modal(回复率不可点)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 17:40:24 +08:00
浅念 c00522fc07 feat(staff): 微信员工沟通数 3 数字 → 明细 modal(补 Plan1 误判漏做的操作)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 17:37:08 +08:00
浅念 1368e3a471 feat(modal): YkStaffDetailModal Col 支持可选 onClick(单元格可点)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 17:32:05 +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
浅念 bfdb5852bb feat(groups): align GroupList to 云客 — per-channel tabs, view-chat action
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 11:14:00 +08:00
浅念 5d4a0c5901 feat(customers): align CustomerList to 云客 — per-channel tabs, 7 cols each, view-chat action
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 10:28:39 +08:00
浅念 3507de55fa feat(customerFields): add fromTypeLabel helper
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 10:13:40 +08:00
浅念 20b01ab849 fix(staff): review fixes — CSS ellipsis, owner_account_id scoping, viewChat guard
1. .yk-staffinfo__nick display:block (ellipsis now applies to WA inline span). 2. openAdded/openTalked modal use owner_account_id (always present) instead of user_id! (null would leak whole customer table). 3. viewChat guards null talker+room + awaits router.push. 4. WA 添加人数 title → 客户列表 (honest: WA has no add_time, shows all).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 19:25:24 +08:00
浅念 5bf43fc67b feat(staff): wechat stat-number/nickname → navigation
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 19:15:05 +08:00
浅念 16149fd499 feat(staff): WhatsApp nickname → chat timeline
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 19:09:24 +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
浅念 02a709ba93 fix(staff): format time columns in detail modal
YkStaffDetailModal Col gains optional formatter; baseCustomerCols time cols render via formatTime instead of raw epoch.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 18:58:06 +08:00
浅念 55578cf299 feat(staff): WhatsApp stat-number click → detail modal
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 18:56:27 +08:00
浅念 8e79f80925 feat(ui): YkStaffDetailModal shared detail-modal component
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 18:51:35 +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
浅念 5336431025 feat(staff): align StaffList columns to 云客 backend
WeChat tab: 6 compound columns mirroring wechatlist (员工信息/数据更新·最新消息时间/好友数/消息数/沟通数/及时回复). WhatsApp tab: 10 columns incl 操作 mirroring waBusiness/userlist. Fixes string-time bug (lastUpdate/uploadTime parsed as number showed --; cellTimeText renders strings).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 18:30:12 +08:00
浅念 62dc663294 feat: employee page with channel tabs (WeChat/WhatsApp account lists)
Replace the sparse staff-record table on /staff with a channel-tabbed
employee account view mirroring the 云客 backend's 员工微信列表 +
WhatsApp 员工列表. Each tab loads /api/accounts?channel=<channel> with
its own pagination state; rows open the existing account all-fields
drawer (reused from AccountList via accountFields.ts).

WeChat tab columns: 头像/微信昵称/微信号/手机号/员工姓名/好友数/群数/
发送/接收/数据库大小/在职/在线状态/最后更新. WhatsApp tab columns:
头像(el-avatar initials)/昵称/员工姓名/手机/销售手机/好友数/群数/
联系人数/新增好友/在职/最后更新. Reads raw_json via the structured-first
readField helper; tolerates boolean and 1/0 forms of isOnJob/onlineStatus.

Also fix the avatar field key in accountFields.ts (headImageUrl, with
headImgUrl as a legacy alias) so the WeChat drawer avatar renders — the
upstream wxUser record uses headImageUrl.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 14:13:21 +08:00
浅念 df29dd027a feat: scheduler + CLI + docker-compose + README
M6 — the scheduling/deployment layer. Makes the system runnable as a
scheduled service + deployable via Docker.

Part A — SyncScheduler (app/sync/scheduler.py):
- APScheduler (AsyncIOScheduler) wraps the orchestrator's sync_* functions.
- Daily full sync at FULL_SYNC_HOUR (default 02:00 local) + incremental
  message sync every MESSAGE_INTERVAL_HOURS (default 4).
- ENTITY_FUNCS registry dispatches run_once("<entity>") to the right runner
  (departments/accounts/friends/groups/voice_calls/phone_calls/sms/messages/
  all_wechat/all).
- Conversations for the message run are discovered from the local customer
  mirror (no extra upstream call).
- Failure policy: every run is wrapped — a blown entity is logged + swallowed
  so the scheduler keeps its next appointment.
- Per-run JSON-line log (level=info/warning/error, totals, failed entities).

Part B — CLI entry point (app/sync/cli.py):
- `python -m app.sync.cli sync [--entity <name>] [--quiet]` — one-shot sync.
- `python -m app.sync.cli scheduler [--full-sync-hour H] [--message-interval-hours N]`
  — long-running scheduler (blocks until SIGINT/SIGTERM).
- Uses _push_settings_into_environ + build_client_from_env (session-cookie
  mode preferred; pcLogin deferred).
- Base.metadata.create_all on every command (fresh-checkout friendly).
- Exit codes: 0 success, 1 generic failure, 2 bad CLI / auth misconfig.

Part C — Docker stack:
- yunke-server/Dockerfile: python:3.12-slim, installs deps + app, single
  image serves both API (uvicorn) + scheduler (CLI).
- yunkes/Dockerfile: multi-stage node:20 build → nginx serves dist/ +
  reverse-proxies /api → backend:8000.
- yunkes/nginx.conf: /api proxy with proxy_buffering off (streaming media),
  SPA fallback, asset caching.
- docker-compose.yml: mysql (8.0, healthcheck, volume) + redis (7-alpine) +
  backend + scheduler + frontend (port 5173 → 80). Env-driven creds + cadence.

Part D — README.md at repo root: dev mode + docker mode quick-start, config
table, architecture overview, current limitations (pcLogin deferred, WA voice/
media deferred, conservative pacing, conversation discovery is local-only).

Verification:
- pytest: 323 passed (was 294; +29 new for scheduler + CLI).
- npm run build: clean.
- python -m app.sync.cli --help: prints usage.
- docker compose config: VALID.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 12:11:29 +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