feat: wechat voice-call scraper + group message coverage
- scrape_wechat_voice_calls: wraps POST /wechatweb/callRecord/queryWeChatVoiceList with page-number pagination (reuses paginate_by_page); normalizes records to the PhoneCall column shape with channel='voice_call'. Field mapping per phase0-findings §4: id->call_id, duration(str s)->call_duration(int), startTime(YYYY-MM-DD HH:MM:SS)->start_call_time(ms epoch), callStatus-> call_status, talker->call_number, talkerNickName->plan_customer_name, ossFileName->record_file (may be null). isSend (0=in/1=out) is inverted to incoming_call (1=in/0=out) so the column's meaning matches the phone_call channel. - upsert_voice_call: idempotent Core upsert into PhoneCall by call_id (sqlite/mysql dialect-aware). Adds call_duration/start_call_time to the _INT_FIELDS coercion net (scraper already emits ints; defensive for hand-built records). - Group-message test (Task 2.3): locks in that scrape_wechat_messages already handles chatroom conversations (friendId=@chatroom, roomid set, talker=member wxid) — yields room_id populated and talker_id = the sending member. No scraper change required. Suite: 123 -> 130 passing. ruff clean. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ from app.db.models import (
|
||||
Department,
|
||||
GroupMember,
|
||||
Message,
|
||||
PhoneCall,
|
||||
Staff,
|
||||
WechatAccount,
|
||||
)
|
||||
@@ -41,6 +42,7 @@ from app.storage.repos import (
|
||||
upsert_messages,
|
||||
upsert_staff,
|
||||
upsert_staff_from_account,
|
||||
upsert_voice_call,
|
||||
upsert_wechat_account,
|
||||
)
|
||||
|
||||
@@ -885,3 +887,84 @@ async def test_upsert_chat_group_with_no_members_writes_group_only():
|
||||
assert members == []
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PhoneCall (voice_call) upsert (Task 2.4)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_upsert_voice_call_is_idempotent_and_updates_record_file():
|
||||
"""Same call_id twice with different record_file/version -> 1 row, refreshed."""
|
||||
engine = await _make_engine()
|
||||
try:
|
||||
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||
async with SessionLocal() as session:
|
||||
rec1 = {
|
||||
"call_id": "vc_dup",
|
||||
"channel": "voice_call",
|
||||
"record_file": "https://example.com/call_v1.mp3",
|
||||
"call_duration": 10,
|
||||
"version": "v1",
|
||||
}
|
||||
rec2 = {
|
||||
"call_id": "vc_dup",
|
||||
"channel": "voice_call",
|
||||
"record_file": "https://example.com/call_v2.mp3",
|
||||
"call_duration": 12,
|
||||
"version": "v2",
|
||||
}
|
||||
|
||||
await upsert_voice_call(session, rec1)
|
||||
await session.commit()
|
||||
await upsert_voice_call(session, rec2)
|
||||
await session.commit()
|
||||
|
||||
rows = (
|
||||
await session.execute(
|
||||
sa.select(PhoneCall)
|
||||
.where(PhoneCall.call_id == "vc_dup")
|
||||
.execution_options(populate_existing=True)
|
||||
)
|
||||
).scalars().all()
|
||||
assert len(rows) == 1
|
||||
assert rows[0].call_id == "vc_dup"
|
||||
assert rows[0].channel == "voice_call"
|
||||
assert rows[0].record_file == "https://example.com/call_v2.mp3"
|
||||
assert rows[0].call_duration == 12
|
||||
assert rows[0].version == "v2"
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
async def test_upsert_voice_call_null_record_file_and_coerces_ints():
|
||||
"""null record_file -> None; numeric-str call_duration/start_call_time/
|
||||
call_status/incoming_call coerced to int."""
|
||||
engine = await _make_engine()
|
||||
try:
|
||||
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||
async with SessionLocal() as session:
|
||||
rec = {
|
||||
"call_id": "vc_null",
|
||||
"channel": "voice_call",
|
||||
"record_file": None,
|
||||
"call_duration": "151", # str -> int
|
||||
"start_call_time": "1700000000123", # numeric str -> int (ms epoch)
|
||||
"call_status": "1", # str -> int
|
||||
"incoming_call": "1", # str -> int
|
||||
}
|
||||
await upsert_voice_call(session, rec)
|
||||
await session.commit()
|
||||
|
||||
fetched = (
|
||||
await session.execute(
|
||||
sa.select(PhoneCall).where(PhoneCall.call_id == "vc_null")
|
||||
)
|
||||
).scalar_one()
|
||||
assert fetched.record_file is None
|
||||
assert fetched.call_duration == 151
|
||||
assert fetched.start_call_time == 1700000000123
|
||||
assert fetched.call_status == 1
|
||||
assert fetched.incoming_call == 1
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
Reference in New Issue
Block a user