feat(sync): sync_call_recordings + ENTITY_FUNCS['call_recordings'] 独立下载 pass
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.models import Department, SyncRun
|
||||
from app.scraper.call import scrape_phone_calls, scrape_sms
|
||||
from app.scraper.media import download_message_media
|
||||
from app.scraper.media import download_call_recordings, download_message_media
|
||||
from app.scraper.wechat import (
|
||||
scrape_departments,
|
||||
scrape_wechat_accounts,
|
||||
@@ -75,8 +75,10 @@ from app.sync.watermarks import get_last_timestamp, set_watermark
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
"ENTITY_CALL_RECORDINGS",
|
||||
"SyncResult",
|
||||
"sync_all_wechat",
|
||||
"sync_call_recordings",
|
||||
"sync_departments",
|
||||
"sync_phone_calls",
|
||||
"sync_sms",
|
||||
@@ -106,6 +108,11 @@ ENTITY_MESSAGE = "message"
|
||||
# wechat-family entities; they track their own SyncRun / SyncState rows.
|
||||
ENTITY_PHONE_CALL = "phone_call"
|
||||
ENTITY_SMS = "sms"
|
||||
# Recording-localization pass: not a scrape — reads already-mirrored
|
||||
# ``phone_call`` rows and downloads their OSS ``record_file`` into the local
|
||||
# object store (records a SyncRun with this entity_type; no SyncState row
|
||||
# because the phone_call table itself is the cursor).
|
||||
ENTITY_CALL_RECORDINGS = "call_recordings"
|
||||
|
||||
CHANNEL_WECHAT = "wechat"
|
||||
|
||||
@@ -588,6 +595,57 @@ async def sync_wa_voice_calls(
|
||||
)
|
||||
|
||||
|
||||
async def sync_call_recordings(
|
||||
session: AsyncSession, client: YunkeClient, store: ObjectStore
|
||||
) -> SyncResult:
|
||||
"""Download localized copies of call recordings for already-mirrored calls.
|
||||
|
||||
Not a scrape: reads local ``phone_call`` rows whose ``record_file`` (OSS
|
||||
URL) has no ``record_media_hash`` yet, downloads each via the public OSS
|
||||
URL, and links it. Records a ``SyncRun`` (``entity_type='call_recordings'``);
|
||||
``fetched`` = calls needing download, ``upserted`` = calls localized.
|
||||
|
||||
Unlike the other ``sync_*`` functions (which defer the commit to the
|
||||
caller), this one commits its own ``SyncRun`` row: the underlying
|
||||
:func:`download_call_recordings` commits per batch as it advances, so the
|
||||
run bookkeeping must land in the same transaction boundary to stay
|
||||
consistent with what was actually persisted.
|
||||
|
||||
Failure policy mirrors every other ``sync_*``: a downloader exception is
|
||||
recorded as a ``status='failed'`` ``SyncRun`` and a failed
|
||||
:class:`SyncResult` is returned (no re-raise), so a broken recording pass
|
||||
does NOT abort a multi-entity batch.
|
||||
"""
|
||||
run = await _start_run(session, ENTITY_CALL_RECORDINGS)
|
||||
try:
|
||||
attempted, localized = await download_call_recordings(
|
||||
client, store, session
|
||||
)
|
||||
await _finish_run(
|
||||
session, run, status="success", fetched=attempted, upserted=localized
|
||||
)
|
||||
await session.commit()
|
||||
return SyncResult(
|
||||
entity=ENTITY_CALL_RECORDINGS,
|
||||
status="success",
|
||||
fetched=attempted,
|
||||
upserted=localized,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception("sync_call_recordings failed")
|
||||
await _finish_run(
|
||||
session, run, status="failed", fetched=0, upserted=0, error=str(exc)
|
||||
)
|
||||
await session.commit()
|
||||
return SyncResult(
|
||||
entity=ENTITY_CALL_RECORDINGS,
|
||||
status="failed",
|
||||
fetched=0,
|
||||
upserted=0,
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
|
||||
async def sync_phone_calls(
|
||||
session: AsyncSession, client: YunkeClient
|
||||
) -> SyncResult:
|
||||
|
||||
Reference in New Issue
Block a user