feat(sync): WA messages 自动同步(per-conversation 增量水位,镜像 sync_wechat_messages)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,7 @@ from app.scraper.whatsapp import (
|
||||
scrape_wa_voice_calls,
|
||||
scrape_whatsapp_accounts,
|
||||
scrape_whatsapp_friends,
|
||||
scrape_whatsapp_messages,
|
||||
)
|
||||
from app.session.yunke_client import YunkeClient
|
||||
from app.storage.object_store import ObjectStore
|
||||
@@ -98,6 +99,7 @@ __all__ = [
|
||||
"sync_wa_voice_calls",
|
||||
"sync_whatsapp_accounts",
|
||||
"sync_whatsapp_friends",
|
||||
"sync_whatsapp_messages",
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -683,6 +685,107 @@ async def sync_whatsapp_friends(
|
||||
)
|
||||
|
||||
|
||||
async def sync_whatsapp_messages(
|
||||
session: AsyncSession,
|
||||
client: YunkeClient,
|
||||
uid: str,
|
||||
friend_uid: str,
|
||||
) -> SyncResult:
|
||||
"""Incremental sync of one WhatsApp conversation's messages.
|
||||
|
||||
Mirrors :func:`sync_wechat_messages` for the WA channel: reads
|
||||
``last_timestamp`` for ``(message, whatsapp, "<uid>:<friend_uid>")``
|
||||
(default ``0`` on first sync), queries ``[last_timestamp, now]`` via
|
||||
:func:`scrape_whatsapp_messages`, upserts every yielded message into the
|
||||
shared ``message`` table (``channel='whatsapp'`` — stamped by the scraper's
|
||||
normalizer), and advances the watermark to ``max(timestamp)``. A re-run
|
||||
with no new messages re-queries the empty window and fetches nothing —
|
||||
the watermark advanced past the previously-seen rows.
|
||||
|
||||
Per Phase 1 this entrypoint does NOT do best-effort media download (unlike
|
||||
:func:`sync_wechat_messages`); WA media localization is deferred to
|
||||
Phase 2. The message record itself is still upserted (so the conversation
|
||||
history is mirrored) — only the media fetch is skipped.
|
||||
|
||||
Returns a :class:`SyncResult`. On a scraper failure, records a
|
||||
``status='failed'`` SyncRun with the error and returns a failed result
|
||||
(does NOT re-raise), so a single broken conversation does NOT abort a
|
||||
multi-conversation batch.
|
||||
"""
|
||||
scope_id = f"{uid}:{friend_uid}"
|
||||
last_ts = await get_last_timestamp(
|
||||
session,
|
||||
ENTITY_MESSAGE,
|
||||
channel=CHANNEL_WHATSAPP,
|
||||
scope_id=scope_id,
|
||||
default=0,
|
||||
)
|
||||
end = _now_ms()
|
||||
|
||||
run = await _start_run(
|
||||
session,
|
||||
ENTITY_MESSAGE,
|
||||
channel=CHANNEL_WHATSAPP,
|
||||
scope_id=scope_id,
|
||||
)
|
||||
|
||||
# --- Scrape (may raise) ---
|
||||
try:
|
||||
msgs: list[dict[str, Any]] = [
|
||||
msg
|
||||
async for msg in scrape_whatsapp_messages(
|
||||
client, uid, friend_uid, last_ts, end
|
||||
)
|
||||
]
|
||||
except Exception as exc:
|
||||
logger.exception(
|
||||
"scrape_whatsapp_messages failed for %s", scope_id
|
||||
)
|
||||
error = repr(exc)
|
||||
await _finish_run(
|
||||
session, run, status="failed", fetched=0, upserted=0, error=error
|
||||
)
|
||||
return SyncResult(
|
||||
entity=ENTITY_MESSAGE,
|
||||
status="failed",
|
||||
fetched=0,
|
||||
upserted=0,
|
||||
channel=CHANNEL_WHATSAPP,
|
||||
scope_id=scope_id,
|
||||
error=error,
|
||||
)
|
||||
|
||||
# --- Upsert (idempotent; same msg_id twice -> one refreshed row) ---
|
||||
upserted = await upsert_messages(session, msgs) if msgs else 0
|
||||
|
||||
# NOTE: no media download for WA (Phase 2; records-only for now).
|
||||
|
||||
# --- Advance watermark to max(timestamp) seen in this batch ---
|
||||
timestamps = [
|
||||
m.get("timestamp") for m in msgs if m.get("timestamp") is not None
|
||||
]
|
||||
if timestamps:
|
||||
await set_watermark(
|
||||
session,
|
||||
ENTITY_MESSAGE,
|
||||
channel=CHANNEL_WHATSAPP,
|
||||
scope_id=scope_id,
|
||||
last_timestamp=max(timestamps),
|
||||
)
|
||||
|
||||
await _finish_run(
|
||||
session, run, status="success", fetched=len(msgs), upserted=upserted
|
||||
)
|
||||
return SyncResult(
|
||||
entity=ENTITY_MESSAGE,
|
||||
status="success",
|
||||
fetched=len(msgs),
|
||||
upserted=upserted,
|
||||
channel=CHANNEL_WHATSAPP,
|
||||
scope_id=scope_id,
|
||||
)
|
||||
|
||||
|
||||
async def sync_call_recordings(
|
||||
session: AsyncSession, client: YunkeClient, store: ObjectStore
|
||||
) -> SyncResult:
|
||||
|
||||
Reference in New Issue
Block a user