fix(sync): 空对话「已扫」记忆——根除 2,468 个无消息对话的每轮全量翻史
前项审计的深挖:带窗验证发现活跃仅 27 对话,但访问集仍有 2,493 个——
2,468 个「从未扫过」的对话(纯语音/从未聊过客户)水位恒 0,拉完 0 条
仍是 unscanned,下一轮又被全量翻史(~2.6s/个,每 4h×每晚都翻)——
这才是消息任务时长的真正黑洞(昨夜 53,682 条绝大部分源于此)。
修:复用 sync_state 表记扫描标记(entity_type={channel}_conv_empty)——
- 拉到 0 条有效消息即打标(微信/WA 1:1)
- 发现层排除「已扫空且不活跃」的;客户一旦发消息(last_chat_time 进
活跃窗)自动回到访问集——不会漏新消息
- 周日 full_history 强制全访问兜底(云客补历史场景)
- 空群(115 个,每轮 ~5min)本轮不动,量小
722 测试绿。预期效果:4h 序列访问集 2,493→~30,时长分钟级。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -514,8 +514,49 @@ async def paginate_chats(fetch_page, end_ms: int, max_pages: int = 200, sleep=No
|
||||
await sleep(1)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 空对话/空群「已扫」记忆(2026-09-10 同步审计):从未有消息的对话(纯语音/
|
||||
# 未聊过客户,WA 实测 2,468 个)水位恒 0,每轮都被当 unscanned 全量翻史
|
||||
# (~2.6s/个)——时长黑洞本体。拉到 0 条有效消息即记 sync_state 扫描标记;
|
||||
# 发现层排除「已扫空且不活跃」的——客户一旦开始发消息(last_chat_time 进
|
||||
# 活跃窗)自动回到访问集;周日 full_history 强制全访问兜底(云客补历史)。
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def _mark_scan_empty(kind: str, scope: str) -> None:
|
||||
"""记录「该对话/群已扫且无消息」。UPDATE 0 行则 INSERT(sync_state 无唯一键)。"""
|
||||
import time as _t
|
||||
|
||||
from app.db.base import async_engine
|
||||
from sqlalchemy import text as sa_text
|
||||
|
||||
now_ms = int(_t.time() * 1000)
|
||||
async with async_engine.begin() as conn:
|
||||
res = await conn.execute(sa_text(
|
||||
"UPDATE sync_state SET last_timestamp = :ts "
|
||||
"WHERE entity_type = :et AND scope_id = :sid"
|
||||
), {"et": kind, "sid": scope, "ts": now_ms})
|
||||
if not res.rowcount:
|
||||
await conn.execute(sa_text(
|
||||
"INSERT INTO sync_state (entity_type, channel, scope_id, last_timestamp) "
|
||||
"VALUES (:et, NULL, :sid, :ts)"
|
||||
), {"et": kind, "sid": scope, "ts": now_ms})
|
||||
|
||||
|
||||
async def _load_scan_empty(kind: str) -> set[str]:
|
||||
from app.db.base import async_engine
|
||||
from sqlalchemy import text as sa_text
|
||||
|
||||
async with async_engine.connect() as conn:
|
||||
rows = (await conn.execute(sa_text(
|
||||
"SELECT scope_id FROM sync_state WHERE entity_type = :et"
|
||||
), {"et": kind})).fetchall()
|
||||
return {r[0] for r in rows if r[0]}
|
||||
|
||||
|
||||
async def _discover_conversations(
|
||||
channel: str, voice_channel: str, active_since_ms: int | None = None
|
||||
channel: str, voice_channel: str, active_since_ms: int | None = None,
|
||||
*, exclude_scanned_empty: bool = True,
|
||||
) -> list[tuple[str, str]]:
|
||||
"""Conversation-pair discovery for the message scrapers (wechat + WA).
|
||||
|
||||
@@ -561,6 +602,9 @@ async def _discover_conversations(
|
||||
).fetchall()
|
||||
|
||||
have = {(r[0], r[1]) for r in with_msgs}
|
||||
scanned_empty = (
|
||||
await _load_scan_empty(f"{channel}_conv_empty") if exclude_scanned_empty else set()
|
||||
)
|
||||
seen: set[tuple[str, str]] = set()
|
||||
fresh: list[tuple[str, str]] = []
|
||||
known: list[tuple[str, str]] = []
|
||||
@@ -576,7 +620,18 @@ async def _discover_conversations(
|
||||
return
|
||||
seen.add(key)
|
||||
if key not in have:
|
||||
fresh.append(key) # 从未扫过:新覆盖优先,不受窗口限制
|
||||
# 从未扫过:新覆盖优先,不受窗口限制。但「已扫空且不活跃」的除外
|
||||
# (2026-09-10:WA 2,468 个无消息对话每轮全翻史的黑洞)——一旦
|
||||
# 活跃(last_chat_time 进窗)自动回到访问集;full_history 强制全访。
|
||||
scope = f"{key[0]}:{key[1]}"
|
||||
if (
|
||||
exclude_scanned_empty
|
||||
and scope in scanned_empty
|
||||
and active_since_ms is not None
|
||||
and not _active(last_ts)
|
||||
):
|
||||
return
|
||||
fresh.append(key)
|
||||
elif _active(last_ts):
|
||||
known.append(key)
|
||||
# else: 已扫过且陈旧 → 本轮跳过
|
||||
@@ -674,6 +729,8 @@ async def scrape_messages(cookie_str: str, max_conversations: int = 100, full_hi
|
||||
int((__import__("time").time() - active_window_hours * 3600) * 1000)
|
||||
if active_window_hours else None
|
||||
),
|
||||
# full_history(周日对齐)强制全访问:云客可能补出历史消息
|
||||
exclude_scanned_empty=not full_history,
|
||||
)
|
||||
watermarks = await _conversation_watermarks("wechat")
|
||||
cust_activity = await _customer_activity("wechat")
|
||||
@@ -815,6 +872,8 @@ async def scrape_messages(cookie_str: str, max_conversations: int = 100, full_hi
|
||||
continue
|
||||
|
||||
total_fetched += len(records)
|
||||
if not records:
|
||||
await _mark_scan_empty("wechat_conv_empty", f"{owner_id}:{friend_id}")
|
||||
total_pages += pages
|
||||
|
||||
if records:
|
||||
@@ -2201,6 +2260,7 @@ async def scrape_wa_messages(cookie_str: str, max_conversations: int = 100, full
|
||||
int((__import__("time").time() - active_window_hours * 3600) * 1000)
|
||||
if active_window_hours else None
|
||||
),
|
||||
exclude_scanned_empty=not full_history,
|
||||
)
|
||||
watermarks = await _conversation_watermarks("whatsapp")
|
||||
cust_activity = await _customer_activity("whatsapp")
|
||||
@@ -2285,6 +2345,8 @@ async def scrape_wa_messages(cookie_str: str, max_conversations: int = 100, full
|
||||
continue
|
||||
|
||||
total_fetched += len(records)
|
||||
if not records:
|
||||
await _mark_scan_empty("whatsapp_conv_empty", f"{uid}:{friend_uid}")
|
||||
if records:
|
||||
# upsert_messages 收整列表(逐条版 _upsert_all 会把 dict 迭代成
|
||||
# key 字符串 → TypeError, 2026-08-19 WA 阶段首条即崩的根因)
|
||||
|
||||
Reference in New Issue
Block a user