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>
This commit is contained in:
浅念
2026-08-07 09:49:31 +08:00
parent 18620870c6
commit 79b89bc919
5 changed files with 210 additions and 21 deletions
+61 -5
View File
@@ -488,21 +488,54 @@ async def sync_wechat_groups(
async def sync_wechat_voice_calls(
session: AsyncSession, client: YunkeClient
session: AsyncSession,
client: YunkeClient,
*,
department_id: str | None = None,
) -> SyncResult:
"""Full re-scrape of all WeChat voice-call records and idempotent upsert."""
"""Full re-scrape of all WeChat voice-call records and idempotent upsert.
``department_id`` is the company-root dept id REQUIRED by the
``queryWeChatVoiceList`` form body — without it (and the accompanying
``callSource`` / ``isRoom`` / ``startTime`` / ``endTime`` filters) the
endpoint returns 200 with an EMPTY ``data.data`` array (0 rows; live
diagnosis 2026-08-07). When omitted, the orchestrator resolves it from
the mirrored ``Department`` table via :func:`_find_root_department_id`
— which is why :func:`sync_departments` must run first. If no root can
be resolved, the scrape is SKIPPED with a logged warning (returns a
``status='skipped'`` :class:`SyncResult` and records no ``SyncRun``) so
a missing org tree does not abort a multi-entity batch.
"""
resolved = department_id
if resolved is None:
resolved = await _find_root_department_id(session)
if not resolved:
logger.warning(
"sync_wechat_voice_calls: no root department_id resolved"
" (sync departments first?); skipping queryWeChatVoiceList scrape"
)
return SyncResult(
entity=ENTITY_WECHAT_VOICE_CALL,
status="skipped",
fetched=0,
upserted=0,
error="no root department_id available",
)
return await _sync_full_rescrape(
session,
entity_type=ENTITY_WECHAT_VOICE_CALL,
channel=None,
scope_id=None,
scrape=lambda: scrape_wechat_voice_calls(client),
scrape=lambda: scrape_wechat_voice_calls(client, department_id=resolved),
upsert=upsert_voice_call,
)
async def sync_wa_voice_calls(
session: AsyncSession, client: YunkeClient
session: AsyncSession,
client: YunkeClient,
*,
department_id: str | None = None,
) -> SyncResult:
"""Full re-scrape of all WhatsApp voice-call records and idempotent upsert.
@@ -513,13 +546,36 @@ async def sync_wa_voice_calls(
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'``.
``department_id`` resolution mirrors :func:`sync_wechat_accounts`: the
``queryWeChatVoiceList`` form body REQUIRES the company-root
``departmentId`` (live diagnosis 2026-08-07 — without it the endpoint
returns 200 + empty ``data.data``, 0 rows). When omitted, the
orchestrator resolves it from the mirrored ``Department`` table; if no
root can be resolved, the scrape is SKIPPED with a logged warning and a
``status='skipped'`` :class:`SyncResult`.
"""
resolved = department_id
if resolved is None:
resolved = await _find_root_department_id(session)
if not resolved:
logger.warning(
"sync_wa_voice_calls: no root department_id resolved"
" (sync departments first?); skipping queryWeChatVoiceList scrape"
)
return SyncResult(
entity=ENTITY_WA_VOICE_CALL,
status="skipped",
fetched=0,
upserted=0,
error="no root department_id available",
)
return await _sync_full_rescrape(
session,
entity_type=ENTITY_WA_VOICE_CALL,
channel=None,
scope_id=None,
scrape=lambda: scrape_wa_voice_calls(client),
scrape=lambda: scrape_wa_voice_calls(client, department_id=resolved),
upsert=upsert_voice_call,
)