feat(sync): sync_wa_voice_calls + scheduler ENTITY_FUNCS wa_voice_calls

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
浅念
2026-08-07 09:36:14 +08:00
parent 169a64ee29
commit 18620870c6
4 changed files with 48 additions and 9 deletions
+34 -6
View File
@@ -57,6 +57,7 @@ from app.scraper.wechat import (
scrape_wechat_messages,
scrape_wechat_voice_calls,
)
from app.scraper.whatsapp import scrape_wa_voice_calls
from app.session.yunke_client import YunkeClient
from app.storage.object_store import ObjectStore
from app.storage.repos import (
@@ -84,6 +85,7 @@ __all__ = [
"sync_wechat_groups",
"sync_wechat_messages",
"sync_wechat_voice_calls",
"sync_wa_voice_calls",
]
# ---------------------------------------------------------------------------
@@ -98,6 +100,7 @@ ENTITY_WECHAT_ACCOUNT = "wechat_account"
ENTITY_WECHAT_FRIEND = "wechat_friend"
ENTITY_WECHAT_GROUP = "wechat_group"
ENTITY_WECHAT_VOICE_CALL = "wechat_voice_call"
ENTITY_WA_VOICE_CALL = "wa_voice_call"
ENTITY_MESSAGE = "message"
# M4 (/pc/ CRM module) — phone calls + SMS share no channel column with the
# wechat-family entities; they track their own SyncRun / SyncState rows.
@@ -498,6 +501,29 @@ async def sync_wechat_voice_calls(
)
async def sync_wa_voice_calls(
session: AsyncSession, client: YunkeClient
) -> SyncResult:
"""Full re-scrape of all WhatsApp voice-call records and idempotent upsert.
Mirrors :func:`sync_wechat_voice_calls`: the WA variant of the unified
``queryWeChatVoiceList`` endpoint (``callSource=4``) has no ``since``
param, so "incremental" = re-scrape everything and rely on
:func:`upsert_voice_call`'s idempotent Core upsert (same ``call_id``
twice -> one refreshed row). The ``channel="wa_voice_call"`` value the
scraper stamps distinguishes WA rows from WeChat rows in the shared
``PhoneCall`` table; the SyncRun row records ``entity_type='wa_voice_call'``.
"""
return await _sync_full_rescrape(
session,
entity_type=ENTITY_WA_VOICE_CALL,
channel=None,
scope_id=None,
scrape=lambda: scrape_wa_voice_calls(client),
upsert=upsert_voice_call,
)
async def sync_phone_calls(
session: AsyncSession, client: YunkeClient
) -> SyncResult:
@@ -560,17 +586,18 @@ async def sync_all_wechat(
) -> list[SyncResult]:
"""Run the full WeChat sync sequence: org/entities first, then messages.
Order: departments → accounts → friends → groups → voice_calls, then the
per-conversation message sync for every ``(wechat_id, friend_id)`` in
``conversations``. Each step returns its own :class:`SyncResult` so a single
failed entity does NOT abort the batch (the failure is recorded and the
next entity proceeds). Returns the results in execution order.
Order: departments → accounts → friends → groups → voice_calls →
wa_voice_calls, then the per-conversation message sync for every
``(wechat_id, friend_id)`` in ``conversations``. Each step returns its own
:class:`SyncResult` so a single failed entity does NOT abort the batch
(the failure is recorded and the next entity proceeds). Returns the
results in execution order.
``conversations`` is the explicit "which direct / room chats to pull
messages for" list. Full auto-discovery from the talkers endpoint is a
LATER concern (Task 2.5 wires the scraper; wiring it into the coordinator
is intentionally deferred to keep this entrypoint bounded and testable).
When ``conversations`` is ``None`` / empty, only the five full-re-scrape
When ``conversations`` is ``None`` / empty, only the six full-re-scrape
entities run.
"""
results: list[SyncResult] = [
@@ -579,6 +606,7 @@ async def sync_all_wechat(
await sync_wechat_friends(session, client),
await sync_wechat_groups(session, client),
await sync_wechat_voice_calls(session, client),
await sync_wa_voice_calls(session, client),
]
for wechat_id, friend_id in conversations or []:
results.append(