#!/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 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()