Commit Graph

83 Commits

Author SHA1 Message Date
浅念 f8a6acd009 docs(spec): 录音本地化 spec 修订——media_type='audio' 新label + store/session 接缝澄清
spec review(Approved)3 条 advisory:media_type 约定、ObjectStore 来源、
session 而非 engine。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 15:22:08 +08:00
浅念 cd4ac5feb3 docs(spec): 录音批量本地化设计——复用现有 media 管道接线
phone_call.record_media_hash + download_call_recordings + sync_call_recordings
独立 pass + PhoneCallOut 字段 + CallList 本地优先/OSS 回退。范围仅通话录音;
语音消息/voice_clip/转写后续。路线图第 1 个 plan(4c 长语音只读已搁置)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 15:13:34 +08:00
浅念 334adcb712 docs(spec): 4c 设计修订——补 user_real_name API 参数 + 员工姓名筛选语义/播放/scaper 文件定稿
spec review 反馈:API↔modal 员工姓名筛选对齐 + 打开语义澄清 +
channel 移出 body + scraper 文件/播放方式 pin。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 14:36:50 +08:00
浅念 fd33e82d62 docs(spec): Plan 4c 设计——长语音列表(voice_clip)本地化 + 只读展示
数据来源真机确认(POST /wechatweb/call/list 请求/响应);voice_clip 表 +
scraper/sync + /api/voice-clips + 只读 VoiceClipModal + CallList 接入。
匹配/转写/统计卡片等留 4d+。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 14:32:23 +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
浅念 21c555a7e4 test: orchestrator voice sync skip/passthrough 路径覆盖
照 sync_wechat_accounts 的现有测试模式给 WA + 微信 voice 各加 2 个测试:
skip-when-no-root-department(断言 status=skipped + scraper 未调用 + 无 SyncRun)
+ passes-resolved-root-to-scraper(断言 resolved department_id 透传到 scraper)。
顺手补 SyncResult.status 的 'skipped' 第三态文档。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 09:57:34 +08:00
浅念 79b89bc919 fix(scraper): voice body 加 departmentId/isRoom/时间窗 + sync 解析 root department_id
实测 queryWeChatVoiceList 必须带 callSource+departmentId+isRoom+startTime+endTime,
否则 200 但 data 空(0 数据)。WA callSource=4 / 微信 callSource=0,宽窗抓全量。
sync_*_voice_calls 照 sync_wechat_accounts 模式解析 _find_root_department_id。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 09:49:31 +08:00
浅念 18620870c6 feat(sync): sync_wa_voice_calls + scheduler ENTITY_FUNCS wa_voice_calls
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 09:36:14 +08:00
浅念 169a64ee29 feat(scraper): WA 语音通话抓取 scrape_wa_voice_calls(callSource=4)
复用云客统一语音接口 queryWeChatVoiceList + callSource=4;channel=wa_voice_call
区分;WA 特有字段落 raw_json;复用 PhoneCall 表 + upsert_voice_call。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 09:26:16 +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
浅念 d85bf64dde feat(api): GET /api/export/chat-html — 导出 1:1/群聊天为 HTML
支持 DM(talker_id) 与 群(room_id),可选 start/end 时间范围;
is_send 决定气泡左右。对齐云客微信客户/群列表「导出HTML聊天记录」。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 17:19:46 +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
浅念 de08bd09c7 docs: Plan 3 — GroupList 对齐云客(采集 + TDD plan)
phase0 采集云客「沟通」板块群列表(微信 wechatdetail/2 + WA friendlist 群列表 tab),
写 Plan 3(分 channel tab 各自列 + viewChat),经 plan-reviewer 评审 Approved。
已知 gap:smoke.db 0 群(空表验证)+ WA 群抓取缺失(字段推断)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 11:11:37 +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
浅念 c31d79f1e2 docs: Plan 2 — CustomerList 对齐云客(采集 + TDD plan)
phase0 采集云客「沟通」板块客户列表(wechat wechatdetail/1 + waBusiness/friendlist),
dump raw_json 字段覆盖,产出列+交互+字段映射;写 Plan 2(分 channel tab 各 7 列 +
查看聊天记录→ChatTimeline),经 plan-reviewer 评审 Approved。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 10:12:36 +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
浅念 fef176bd90 feat(api): customer filters user_id / add_time_since / last_chat_since
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 18:48:59 +08:00
浅念 8d7774b9f5 feat(scripts): backfill Customer.add_time from raw_json
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 18:45:55 +08:00
浅念 8494d564ef feat(scraper): map customer add_time from raw_json
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 18:44:04 +08:00
浅念 b5733f15d2 feat(db): add Customer.add_time column + migration
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 18:39:17 +08:00
浅念 9499110427 docs(plan): record wechat staff-list probe + adjust Task 8/9
WeChat interactions are navigation (not modal): 好友数/群数→customer/group list, 昵称→chat. Task 8 redone as navigation; Task 9 uses owner_account_id + row.channel.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 18:35:18 +08:00
浅念 1d815d84b9 docs: UI alignment spec + StaffList interaction plan
Spec: align all list/detail pages to 云客 (columns + interactions); covers interaction taxonomy, backend extensions (customer add_time/filters, /api/sessions), phase-0 capture. Plan 1: StaffList interaction closure + customer backend + YkStaffDetailModal.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 18:30:12 +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
浅念 c8311bb496 fix: sanitize lone surrogate chars in stored data + backfill script
Upstream API records may contain lone surrogate chars (emoji fragments)
that break SQLite/MySQL utf-8 encoding. Now sanitized to U+FFFD before
storage. Includes backfill_customers.py for full customer backfill.

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