reap_orphan_reports.py 4.99 KB
#!/data/data/com.termux/files/usr/bin/python3
"""Orphan report reaper — import report_result_*.md files whose callback never fired.

Root cause: Flask restart kills daemon threads → _on_report_done never calls
report_add(). The result file remains in /tmp but never makes it to daily_reports.

This reaper runs via watchdog every 5 minutes.
"""
import os, re, sys, glob

TMP_DIR = "/data/data/com.termux/files/home/tmp"
APP_DIR = "/data/data/com.termux/files/home/wechat-viewer"
sys.path.insert(0, APP_DIR)
from local_db import report_add, report_list


def main():
    pattern = os.path.join(TMP_DIR, "report_result_*.md")
    result_files = glob.glob(pattern)

    if not result_files:
        return  # nothing to do, silent

    # Load existing reports once
    existing = report_list()
    existing_keys = set()
    for r in existing:
        existing_keys.add((r.get("chatroom_id", ""), r.get("report_date", "")))

    imported = 0

    for rf in result_files:
        basename = os.path.basename(rf)
        # Extract task_id: report_result_<12-char hex>.md
        m = re.match(r"report_result_([a-f0-9]{12})\.md", basename)
        if not m:
            print(f"  Skip (bad name): {basename}")
            continue

        task_id = m.group(1)
        task_file = os.path.join(TMP_DIR, f"report_task_{task_id}.json")

        if not os.path.exists(task_file):
            print(f"  Skip (no task file): {basename}")
            # Can't determine chatroom_id without task file — leave for manual
            continue

        # Read task metadata
        import json
        try:
            with open(task_file) as f:
                task = json.load(f)
        except Exception as e:
            print(f"  Skip (task file corrupt): {basename} — {e}")
            continue

        chatroom_id = task.get("chatroom_id", "")
        date_str = task.get("date", "")

        if not chatroom_id or not date_str:
            print(f"  Skip (missing fields): {basename}")
            continue

        # Check if already imported
        if (chatroom_id, date_str) in existing_keys:
            print(f"  Already imported: {basename}")
            # Clean up orphan files
            try:
                os.remove(rf)
                os.remove(task_file)
            except OSError:
                pass
            continue

        # Read report content
        try:
            with open(rf, encoding="utf-8", errors="replace") as f:
                content = f.read()
        except Exception as e:
            print(f"  Skip (unreadable): {basename} — {e}")
            continue

        if len(content.strip()) < 100:
            print(f"  Skip (too short): {basename} ({len(content)} bytes)")
            continue

        # Resolve chatroom name
        chatroom_name = resolve_chatroom_name(chatroom_id)

        # Parse counts from report content
        msg_count = 0
        image_count = 0
        article_count = 0
        mc = re.search(r'消息总数[::]?\s*(\d+)', content)
        ic = re.search(r'图片\s*(\d+)', content)
        ac = re.search(r'文章\s*(\d+)', content)
        if mc:
            msg_count = int(mc.group(1))
        if ic:
            image_count = int(ic.group(1))
        if ac:
            article_count = int(ac.group(1))

        # Import into daily_reports
        try:
            rid = report_add(chatroom_id, chatroom_name, date_str, content,
                             msg_count, image_count, article_count)
            print(f"  Imported: {chatroom_name} ({date_str}) → id={rid}")
            imported += 1
        except Exception as e:
            print(f"  Import failed: {basename} — {e}")
            continue

        # Clean up on success
        try:
            os.remove(rf)
            os.remove(task_file)
        except OSError:
            pass

        # Update cache
        existing_keys.add((chatroom_id, date_str))

    gate_trace = (
        f"[node=reaper,attempt=1,gate=ORPHAN_REPORTS,"
        f"action={'CONTINUE' if imported > 0 else 'PASS'}] "
        f"scanned {len(result_files)} files, imported {imported}"
    )
    if imported:
        print(f"Done: imported {imported} orphan reports.")
    print(gate_trace)


def resolve_chatroom_name(chatroom_id: str) -> str:
    """Resolve chatroom_id to display name, fallback to chatroom_id."""
    try:
        # Try reading from rcontact via WeChat DB
        import subprocess
        result = subprocess.run(
            ["su", "-c",
             f"sqlcipher /data/data/com.tencent.mm/MicroMsg/*/EnMicroMsg.db "
             f"\"PRAGMA key='x'; SELECT nickname FROM rcontact WHERE username='{chatroom_id}';\" "
             f"2>/dev/null"],
            capture_output=True, text=True, timeout=10)
        # sqlcipher output format: ok\n<NICKNAME>
        output = result.stdout.strip()
        if output and output != "ok":
            lines = output.split("\n")
            if len(lines) > 1 and lines[1].strip():
                return lines[1].strip()
    except Exception:
        pass
    return chatroom_id


if __name__ == "__main__":
    main()