From 7d4ac67fc2ceb425ea26a54a4fe2c085dfe9fc8d Mon Sep 17 00:00:00 2001
From: 2977094657 <104895427+2977094657@users.noreply.github.com>
Date: Tue, 20 Jan 2026 17:21:13 +0800
Subject: [PATCH 001/103] Add downloads badge to README
Add a downloads badge to the README.
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 392cc83..ff46608 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@
特别致谢:echotrace(本项目大量功能参考其实现,提供了重要技术支持)
+
From 93ad7b7a1cd1d2c47f89d1e74185b52c7e43566e Mon Sep 17 00:00:00 2001
From: 2977094657 <2977094657@qq.com>
Date: Sat, 24 Jan 2026 18:47:06 +0800
Subject: [PATCH 002/103] =?UTF-8?q?improvement(chat):=20realtime=20?=
=?UTF-8?q?=E7=9B=B4=E8=AF=BB=20WCDB=20=E5=B9=B6=E5=AE=8C=E5=96=84?=
=?UTF-8?q?=E8=BF=BD=E8=B8=AA=E6=97=A5=E5=BF=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- SSE 变更扫描改用 asyncio.to_thread,避免阻塞事件循环
- sessions/messages 支持 source=realtime;realtime 下会话预览改用 session 信息避免缓存陈旧
- realtime sync/sync_all 增加 trace_id 与关键步骤日志,便于定位卡顿/锁竞争
- 支持通过 WECHAT_TOOL_LOG_LEVEL 环境变量覆盖日志级别
---
src/wechat_decrypt_tool/logging_config.py | 23 +-
src/wechat_decrypt_tool/routers/chat.py | 313 ++++++++++++++++++++--
2 files changed, 306 insertions(+), 30 deletions(-)
diff --git a/src/wechat_decrypt_tool/logging_config.py b/src/wechat_decrypt_tool/logging_config.py
index 4635353..35836b6 100644
--- a/src/wechat_decrypt_tool/logging_config.py
+++ b/src/wechat_decrypt_tool/logging_config.py
@@ -3,6 +3,7 @@
"""
import logging
+import os
import sys
from datetime import datetime
from pathlib import Path
@@ -58,6 +59,11 @@ def __init__(self):
def setup_logging(self, log_level: str = "INFO"):
"""设置日志配置"""
+ # Allow overriding via env var for easier debugging (e.g. WECHAT_TOOL_LOG_LEVEL=DEBUG)
+ env_level = str(os.environ.get("WECHAT_TOOL_LOG_LEVEL", "") or "").strip()
+ if env_level:
+ log_level = env_level
+
# 创建日志目录
now = datetime.now()
log_dir = Path("output/logs") / str(now.year) / f"{now.month:02d}" / f"{now.day:02d}"
@@ -88,46 +94,47 @@ def setup_logging(self, log_level: str = "INFO"):
# 文件处理器
file_handler = logging.FileHandler(self.log_file, encoding='utf-8')
file_handler.setFormatter(file_formatter)
- file_handler.setLevel(getattr(logging, log_level.upper()))
+ level = getattr(logging, str(log_level or "INFO").upper(), logging.INFO)
+ file_handler.setLevel(level)
# 控制台处理器
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(console_formatter)
- console_handler.setLevel(getattr(logging, log_level.upper()))
+ console_handler.setLevel(level)
# 配置根日志器
- root_logger.setLevel(getattr(logging, log_level.upper()))
+ root_logger.setLevel(level)
root_logger.addHandler(file_handler)
root_logger.addHandler(console_handler)
# 只为uvicorn日志器添加文件处理器,保持其原有的控制台处理器(带颜色)
uvicorn_logger = logging.getLogger("uvicorn")
uvicorn_logger.addHandler(file_handler)
- uvicorn_logger.setLevel(getattr(logging, log_level.upper()))
+ uvicorn_logger.setLevel(level)
# 只为uvicorn.access日志器添加文件处理器
uvicorn_access_logger = logging.getLogger("uvicorn.access")
uvicorn_access_logger.addHandler(file_handler)
- uvicorn_access_logger.setLevel(getattr(logging, log_level.upper()))
+ uvicorn_access_logger.setLevel(level)
# 只为uvicorn.error日志器添加文件处理器
uvicorn_error_logger = logging.getLogger("uvicorn.error")
uvicorn_error_logger.addHandler(file_handler)
- uvicorn_error_logger.setLevel(getattr(logging, log_level.upper()))
+ uvicorn_error_logger.setLevel(level)
# 配置FastAPI日志器
fastapi_logger = logging.getLogger("fastapi")
fastapi_logger.handlers = []
fastapi_logger.addHandler(file_handler)
fastapi_logger.addHandler(console_handler)
- fastapi_logger.setLevel(getattr(logging, log_level.upper()))
+ fastapi_logger.setLevel(level)
# 记录初始化信息
logger = logging.getLogger(__name__)
logger.info("=" * 60)
logger.info("微信解密工具日志系统初始化完成")
logger.info(f"日志文件: {self.log_file}")
- logger.info(f"日志级别: {log_level}")
+ logger.info(f"日志级别: {logging.getLevelName(level)}")
logger.info("=" * 60)
return self.log_file
diff --git a/src/wechat_decrypt_tool/routers/chat.py b/src/wechat_decrypt_tool/routers/chat.py
index 4597bf1..9455ad0 100644
--- a/src/wechat_decrypt_tool/routers/chat.py
+++ b/src/wechat_decrypt_tool/routers/chat.py
@@ -213,6 +213,13 @@ async def stream_chat_realtime_events(
if not db_storage_dir.exists() or not db_storage_dir.is_dir():
raise HTTPException(status_code=400, detail="db_storage directory not found for this account.")
+ logger.info(
+ "[realtime] SSE stream open account=%s interval_ms=%s db_storage=%s",
+ account_dir.name,
+ int(interval_ms),
+ str(db_storage_dir),
+ )
+
async def gen():
last_mtime_ns = 0
last_heartbeat = 0.0
@@ -226,27 +233,40 @@ async def gen():
}
yield f"data: {json.dumps(initial, ensure_ascii=False)}\n\n"
- while True:
- if await request.is_disconnected():
- break
+ try:
+ while True:
+ if await request.is_disconnected():
+ break
- mtime_ns = _scan_db_storage_mtime_ns(db_storage_dir)
- if mtime_ns and mtime_ns != last_mtime_ns:
- last_mtime_ns = mtime_ns
- payload = {
- "type": "change",
- "account": account_dir.name,
- "mtimeNs": int(mtime_ns),
- "ts": int(time.time() * 1000),
- }
- yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
+ # Avoid blocking the event loop on a potentially large directory walk.
+ scan_t0 = time.perf_counter()
+ try:
+ mtime_ns = await asyncio.to_thread(_scan_db_storage_mtime_ns, db_storage_dir)
+ except Exception:
+ mtime_ns = 0
+ scan_ms = (time.perf_counter() - scan_t0) * 1000.0
+ if scan_ms > 1000:
+ logger.warning("[realtime] SSE scan slow account=%s ms=%.1f", account_dir.name, scan_ms)
+
+ if mtime_ns and mtime_ns != last_mtime_ns:
+ last_mtime_ns = mtime_ns
+ payload = {
+ "type": "change",
+ "account": account_dir.name,
+ "mtimeNs": int(mtime_ns),
+ "ts": int(time.time() * 1000),
+ }
+ logger.info("[realtime] SSE change account=%s mtime_ns=%s", account_dir.name, int(mtime_ns))
+ yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
- now = time.time()
- if now - last_heartbeat > 15:
- last_heartbeat = now
- yield ": ping\n\n"
+ now = time.time()
+ if now - last_heartbeat > 15:
+ last_heartbeat = now
+ yield ": ping\n\n"
- await asyncio.sleep(interval_ms / 1000.0)
+ await asyncio.sleep(interval_ms / 1000.0)
+ finally:
+ logger.info("[realtime] SSE stream closed account=%s", account_dir.name)
headers = {"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"}
return StreamingResponse(gen(), media_type="text/event-stream", headers=headers)
@@ -337,7 +357,7 @@ def _ensure_session_last_message_table(conn: sqlite3.Connection) -> None:
@router.post("/api/chat/realtime/sync", summary="实时消息同步到解密库(按会话增量)")
-async def sync_chat_realtime_messages(
+def sync_chat_realtime_messages(
request: Request,
username: str,
account: Optional[str] = None,
@@ -357,11 +377,23 @@ async def sync_chat_realtime_messages(
max_scan = 5000
account_dir = _resolve_account_dir(account)
+ trace_id = f"rt-sync-{int(time.time() * 1000)}-{threading.get_ident()}"
+ logger.info(
+ "[%s] realtime sync start account=%s username=%s max_scan=%s",
+ trace_id,
+ account_dir.name,
+ username,
+ int(max_scan),
+ )
# Lock per (account, username) to avoid concurrent writes to the same sqlite tables.
+ logger.info("[%s] acquiring per-session lock account=%s username=%s", trace_id, account_dir.name, username)
with _realtime_sync_lock(account_dir.name, username):
+ logger.info("[%s] per-session lock acquired account=%s username=%s", trace_id, account_dir.name, username)
try:
+ logger.info("[%s] ensure wcdb connected account=%s", trace_id, account_dir.name)
rt_conn = WCDB_REALTIME.ensure_connected(account_dir)
+ logger.info("[%s] wcdb connected account=%s handle=%s", trace_id, account_dir.name, int(rt_conn.handle))
except WCDBRealtimeError as e:
raise HTTPException(status_code=400, detail=str(e))
@@ -369,6 +401,14 @@ async def sync_chat_realtime_messages(
if not resolved:
raise HTTPException(status_code=404, detail="Conversation table not found in decrypted databases.")
msg_db_path, table_name = resolved
+ logger.info(
+ "[%s] resolved decrypted table account=%s username=%s db=%s table=%s",
+ trace_id,
+ account_dir.name,
+ username,
+ str(msg_db_path),
+ table_name,
+ )
msg_conn = sqlite3.connect(str(msg_db_path))
msg_conn.row_factory = sqlite3.Row
@@ -457,8 +497,34 @@ def normalize(item: dict[str, Any]) -> dict[str, Any]:
while scanned < int(max_scan):
take = min(batch_size, int(max_scan) - scanned)
+ logger.info(
+ "[%s] wcdb_get_messages account=%s username=%s take=%s offset=%s",
+ trace_id,
+ account_dir.name,
+ username,
+ int(take),
+ int(offset),
+ )
+ wcdb_t0 = time.perf_counter()
with rt_conn.lock:
raw_rows = _wcdb_get_messages(rt_conn.handle, username, limit=take, offset=offset)
+ wcdb_ms = (time.perf_counter() - wcdb_t0) * 1000.0
+ logger.info(
+ "[%s] wcdb_get_messages done account=%s username=%s rows=%s ms=%.1f",
+ trace_id,
+ account_dir.name,
+ username,
+ len(raw_rows or []),
+ wcdb_ms,
+ )
+ if wcdb_ms > 2000:
+ logger.warning(
+ "[%s] wcdb_get_messages slow account=%s username=%s ms=%.1f",
+ trace_id,
+ account_dir.name,
+ username,
+ wcdb_ms,
+ )
if not raw_rows:
break
@@ -526,9 +592,27 @@ def normalize(item: dict[str, Any]) -> dict[str, Any]:
# Insert older -> newer to keep sqlite btree locality similar to existing data.
values = [tuple(r.get(c) for c in insert_cols) for r in reversed(new_rows)]
+ insert_t0 = time.perf_counter()
msg_conn.executemany(insert_sql, values)
msg_conn.commit()
+ insert_ms = (time.perf_counter() - insert_t0) * 1000.0
inserted = len(new_rows)
+ logger.info(
+ "[%s] sqlite insert done account=%s username=%s inserted=%s ms=%.1f",
+ trace_id,
+ account_dir.name,
+ username,
+ int(inserted),
+ insert_ms,
+ )
+ if insert_ms > 1000:
+ logger.warning(
+ "[%s] sqlite insert slow account=%s username=%s ms=%.1f",
+ trace_id,
+ account_dir.name,
+ username,
+ insert_ms,
+ )
if ("packed_info_data" in insert_cols) and backfill_rows:
update_values = []
@@ -539,12 +623,30 @@ def normalize(item: dict[str, Any]) -> dict[str, Any]:
update_values.append((pdata, int(r.get("local_id") or 0)))
if update_values:
before_changes = msg_conn.total_changes
+ update_t0 = time.perf_counter()
msg_conn.executemany(
f"UPDATE {quoted_table} SET packed_info_data = ? WHERE local_id = ? AND (packed_info_data IS NULL OR length(packed_info_data) = 0)",
update_values,
)
msg_conn.commit()
+ update_ms = (time.perf_counter() - update_t0) * 1000.0
backfilled = int(msg_conn.total_changes - before_changes)
+ logger.info(
+ "[%s] sqlite backfill done account=%s username=%s rows=%s ms=%.1f",
+ trace_id,
+ account_dir.name,
+ username,
+ int(backfilled),
+ update_ms,
+ )
+ if update_ms > 1000:
+ logger.warning(
+ "[%s] sqlite backfill slow account=%s username=%s ms=%.1f",
+ trace_id,
+ account_dir.name,
+ username,
+ update_ms,
+ )
# Update session.db so left sidebar ordering/time can follow new messages.
newest = new_rows[0] if new_rows else None
@@ -636,6 +738,16 @@ def normalize(item: dict[str, Any]) -> dict[str, Any]:
finally:
sconn.close()
+ logger.info(
+ "[%s] realtime sync done account=%s username=%s scanned=%s inserted=%s backfilled=%s maxLocalIdBefore=%s",
+ trace_id,
+ account_dir.name,
+ username,
+ int(scanned),
+ int(inserted),
+ int(backfilled),
+ int(max_local_id),
+ )
return {
"status": "success",
"account": account_dir.name,
@@ -750,8 +862,31 @@ def normalize(item: dict[str, Any]) -> dict[str, Any]:
while scanned < int(max_scan):
take = min(batch_size, int(max_scan) - scanned)
+ logger.info(
+ "[realtime] wcdb_get_messages account=%s username=%s take=%s offset=%s",
+ account_dir.name,
+ username,
+ int(take),
+ int(offset),
+ )
+ wcdb_t0 = time.perf_counter()
with rt_conn.lock:
raw_rows = _wcdb_get_messages(rt_conn.handle, username, limit=take, offset=offset)
+ wcdb_ms = (time.perf_counter() - wcdb_t0) * 1000.0
+ logger.info(
+ "[realtime] wcdb_get_messages done account=%s username=%s rows=%s ms=%.1f",
+ account_dir.name,
+ username,
+ len(raw_rows or []),
+ wcdb_ms,
+ )
+ if wcdb_ms > 2000:
+ logger.warning(
+ "[realtime] wcdb_get_messages slow account=%s username=%s ms=%.1f",
+ account_dir.name,
+ username,
+ wcdb_ms,
+ )
if not raw_rows:
break
@@ -816,9 +951,25 @@ def normalize(item: dict[str, Any]) -> dict[str, Any]:
continue
values = [tuple(r.get(c) for c in insert_cols) for r in reversed(new_rows)]
+ insert_t0 = time.perf_counter()
msg_conn.executemany(insert_sql, values)
msg_conn.commit()
+ insert_ms = (time.perf_counter() - insert_t0) * 1000.0
inserted = len(new_rows)
+ logger.info(
+ "[realtime] sqlite insert done account=%s username=%s inserted=%s ms=%.1f",
+ account_dir.name,
+ username,
+ int(inserted),
+ insert_ms,
+ )
+ if insert_ms > 1000:
+ logger.warning(
+ "[realtime] sqlite insert slow account=%s username=%s ms=%.1f",
+ account_dir.name,
+ username,
+ insert_ms,
+ )
if ("packed_info_data" in insert_cols) and backfill_rows:
update_values = []
@@ -829,12 +980,28 @@ def normalize(item: dict[str, Any]) -> dict[str, Any]:
update_values.append((pdata, int(r.get("local_id") or 0)))
if update_values:
before_changes = msg_conn.total_changes
+ update_t0 = time.perf_counter()
msg_conn.executemany(
f"UPDATE {quoted_table} SET packed_info_data = ? WHERE local_id = ? AND (packed_info_data IS NULL OR length(packed_info_data) = 0)",
update_values,
)
msg_conn.commit()
+ update_ms = (time.perf_counter() - update_t0) * 1000.0
backfilled = int(msg_conn.total_changes - before_changes)
+ logger.info(
+ "[realtime] sqlite backfill done account=%s username=%s rows=%s ms=%.1f",
+ account_dir.name,
+ username,
+ int(backfilled),
+ update_ms,
+ )
+ if update_ms > 1000:
+ logger.warning(
+ "[realtime] sqlite backfill slow account=%s username=%s ms=%.1f",
+ account_dir.name,
+ username,
+ update_ms,
+ )
newest = new_rows[0] if new_rows else None
preview = ""
@@ -938,7 +1105,7 @@ def normalize(item: dict[str, Any]) -> dict[str, Any]:
@router.post("/api/chat/realtime/sync_all", summary="实时消息同步到解密库(全会话增量)")
-async def sync_chat_realtime_messages_all(
+def sync_chat_realtime_messages_all(
request: Request,
account: Optional[str] = None,
max_scan: int = 200,
@@ -953,6 +1120,16 @@ async def sync_chat_realtime_messages_all(
说明:这是增量同步,不会每次全表扫描;priority_username 会优先同步并可设置更大的 priority_max_scan。
"""
account_dir = _resolve_account_dir(account)
+ trace_id = f"rt-syncall-{int(time.time() * 1000)}-{threading.get_ident()}"
+ logger.info(
+ "[%s] realtime sync_all start account=%s max_scan=%s priority=%s include_hidden=%s include_official=%s",
+ trace_id,
+ account_dir.name,
+ int(max_scan),
+ str(priority_username or "").strip(),
+ bool(include_hidden),
+ bool(include_official),
+ )
if max_scan < 20:
max_scan = 20
@@ -966,15 +1143,29 @@ async def sync_chat_realtime_messages_all(
priority = str(priority_username or "").strip()
started = time.time()
+ logger.info("[%s] acquiring global sync lock account=%s", trace_id, account_dir.name)
with _realtime_sync_all_lock(account_dir.name):
+ logger.info("[%s] global sync lock acquired account=%s", trace_id, account_dir.name)
try:
+ logger.info("[%s] ensure wcdb connected account=%s", trace_id, account_dir.name)
rt_conn = WCDB_REALTIME.ensure_connected(account_dir)
+ logger.info("[%s] wcdb connected account=%s handle=%s", trace_id, account_dir.name, int(rt_conn.handle))
except WCDBRealtimeError as e:
raise HTTPException(status_code=400, detail=str(e))
try:
+ logger.info("[%s] wcdb_get_sessions account=%s", trace_id, account_dir.name)
+ wcdb_t0 = time.perf_counter()
with rt_conn.lock:
raw_sessions = _wcdb_get_sessions(rt_conn.handle)
+ wcdb_ms = (time.perf_counter() - wcdb_t0) * 1000.0
+ logger.info(
+ "[%s] wcdb_get_sessions done account=%s sessions=%s ms=%.1f",
+ trace_id,
+ account_dir.name,
+ len(raw_sessions or []),
+ wcdb_ms,
+ )
except Exception:
raw_sessions = []
@@ -1018,6 +1209,13 @@ def _dedupe(items: list[tuple[int, str]]) -> list[tuple[int, str]]:
sessions = _dedupe(sessions)
sessions.sort(key=lambda x: int(x[0] or 0), reverse=True)
all_usernames = [u for _, u in sessions if u]
+ logger.info(
+ "[%s] sessions prepared account=%s raw=%s filtered=%s",
+ trace_id,
+ account_dir.name,
+ len(raw_sessions or []),
+ len(all_usernames),
+ )
# Skip sessions whose decrypted session.db already has a newer/equal sort_timestamp.
decrypted_ts_by_user: dict[str, int] = {}
@@ -1080,10 +1278,25 @@ def _dedupe(items: list[tuple[int, str]]) -> list[tuple[int, str]]:
continue
sync_usernames.append(u)
+ logger.info(
+ "[%s] sessions need_sync account=%s need_sync=%s skipped_up_to_date=%s",
+ trace_id,
+ account_dir.name,
+ len(sync_usernames),
+ int(skipped_up_to_date),
+ )
+
if priority and priority in sync_usernames:
sync_usernames = [priority] + [u for u in sync_usernames if u != priority]
table_map = _resolve_decrypted_message_tables(account_dir, sync_usernames)
+ logger.info(
+ "[%s] resolved decrypted tables account=%s resolved=%s need_sync=%s",
+ trace_id,
+ account_dir.name,
+ len(table_map),
+ len(sync_usernames),
+ )
scanned_total = 0
inserted_total = 0
@@ -1116,17 +1329,50 @@ def _dedupe(items: list[tuple[int, str]]) -> list[tuple[int, str]]:
inserted_total += ins
if ins:
updated_sessions += 1
+ logger.info(
+ "[%s] synced session account=%s username=%s inserted=%s scanned=%s",
+ trace_id,
+ account_dir.name,
+ uname,
+ ins,
+ int(result.get("scanned") or 0),
+ )
except HTTPException as e:
errors.append(f"{uname}: {str(e.detail or '')}".strip())
+ logger.warning(
+ "[%s] sync session failed account=%s username=%s err=%s",
+ trace_id,
+ account_dir.name,
+ uname,
+ str(e.detail or "").strip(),
+ )
continue
except Exception as e:
errors.append(f"{uname}: {str(e)}".strip())
+ logger.exception(
+ "[%s] sync session crashed account=%s username=%s",
+ trace_id,
+ account_dir.name,
+ uname,
+ )
continue
elapsed_ms = int((time.time() - started) * 1000)
if len(errors) > 20:
errors = errors[:20] + [f"... and {len(errors) - 20} more"]
+ logger.info(
+ "[%s] realtime sync_all done account=%s sessions_total=%s need_sync=%s synced=%s updated=%s inserted_total=%s elapsed_ms=%s errors=%s",
+ trace_id,
+ account_dir.name,
+ len(all_usernames),
+ len(sync_usernames),
+ int(synced),
+ int(updated_sessions),
+ int(inserted_total),
+ int(elapsed_ms),
+ len(errors),
+ )
return {
"status": "success",
"account": account_dir.name,
@@ -2134,7 +2380,7 @@ async def list_chat_accounts():
@router.get("/api/chat/sessions", summary="获取会话列表(聊天左侧列表)")
-async def list_chat_sessions(
+def list_chat_sessions(
request: Request,
account: Optional[str] = None,
limit: int = 400,
@@ -2157,10 +2403,32 @@ async def list_chat_sessions(
rows: list[Any]
if source_norm == "realtime":
+ trace_id = f"rt-sessions-{int(time.time() * 1000)}-{threading.get_ident()}"
+ logger.info(
+ "[%s] list_sessions realtime start account=%s limit=%s include_hidden=%s include_official=%s preview=%s",
+ trace_id,
+ account_dir.name,
+ int(limit),
+ bool(include_hidden),
+ bool(include_official),
+ str(preview or ""),
+ )
try:
+ logger.info("[%s] ensure wcdb connected account=%s", trace_id, account_dir.name)
conn = WCDB_REALTIME.ensure_connected(account_dir)
+ logger.info("[%s] wcdb connected account=%s handle=%s", trace_id, account_dir.name, int(conn.handle))
+ logger.info("[%s] wcdb_get_sessions account=%s", trace_id, account_dir.name)
+ wcdb_t0 = time.perf_counter()
with conn.lock:
raw = _wcdb_get_sessions(conn.handle)
+ wcdb_ms = (time.perf_counter() - wcdb_t0) * 1000.0
+ logger.info(
+ "[%s] wcdb_get_sessions done account=%s sessions=%s ms=%.1f",
+ trace_id,
+ account_dir.name,
+ len(raw or []),
+ wcdb_ms,
+ )
except WCDBRealtimeError as e:
raise HTTPException(status_code=400, detail=str(e))
@@ -2193,6 +2461,7 @@ def _ts(v: Any) -> int:
norm.sort(key=lambda r: _ts(r.get("sort_timestamp")), reverse=True)
rows = norm
+ logger.info("[%s] list_sessions realtime normalized account=%s rows=%s", trace_id, account_dir.name, len(rows))
else:
session_db_path = account_dir / "session.db"
sconn = sqlite3.connect(str(session_db_path))
@@ -2873,7 +3142,7 @@ def _collect_chat_messages(
@router.get("/api/chat/messages", summary="获取会话消息列表")
-async def list_chat_messages(
+def list_chat_messages(
request: Request,
username: str,
account: Optional[str] = None,
From ae2d7f128d3b1927aeb456266f886f8d6c50f762 Mon Sep 17 00:00:00 2001
From: 2977094657 <2977094657@qq.com>
Date: Sat, 24 Jan 2026 18:47:29 +0800
Subject: [PATCH 003/103] =?UTF-8?q?improvement(chat):=20realtime=20?=
=?UTF-8?q?=E5=88=B7=E6=96=B0=E5=8E=BB=E6=8A=96=E5=B9=B6=E7=BB=95=E8=BF=87?=
=?UTF-8?q?=E5=90=8E=E5=8F=B0=E5=85=A8=E9=87=8F=E5=90=8C=E6=AD=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- realtime 模式拉取消息时传 source=realtime,直接从 WCDB 读取
- SSE change 事件增加 500ms debounce,减少频繁刷新/请求抖动
- 停止 realtime 时清理 debounce timer
---
frontend/pages/chat/[[username]].vue | 32 ++++++++++++++++++++++------
1 file changed, 25 insertions(+), 7 deletions(-)
diff --git a/frontend/pages/chat/[[username]].vue b/frontend/pages/chat/[[username]].vue
index 48ed479..b2348ab 100644
--- a/frontend/pages/chat/[[username]].vue
+++ b/frontend/pages/chat/[[username]].vue
@@ -1894,6 +1894,7 @@ let realtimeSessionsRefreshQueued = false
let realtimeFullSyncFuture = null
let realtimeFullSyncQueued = false
let realtimeFullSyncPriority = ''
+let realtimeChangeDebounceTimer = null
const allMessages = ref({})
@@ -4644,9 +4645,9 @@ const loadMessages = async ({ username, reset }) => {
if (messageTypeFilter.value && messageTypeFilter.value !== 'all') {
params.render_types = messageTypeFilter.value
}
-
- if (reset) {
- await queueRealtimeFullSync(username)
+ if (realtimeEnabled.value) {
+ // In realtime mode, read directly from WCDB to avoid blocking on background sync.
+ params.source = 'realtime'
}
const resp = await api.listChatMessages(params)
@@ -4747,6 +4748,12 @@ const stopRealtimeStream = () => {
} catch {}
realtimeEventSource = null
}
+ if (realtimeChangeDebounceTimer) {
+ try {
+ clearTimeout(realtimeChangeDebounceTimer)
+ } catch {}
+ realtimeChangeDebounceTimer = null
+ }
}
const refreshRealtimeIncremental = async () => {
@@ -4774,8 +4781,8 @@ const refreshRealtimeIncremental = async () => {
if (messageTypeFilter.value && messageTypeFilter.value !== 'all') {
params.render_types = messageTypeFilter.value
}
+ params.source = 'realtime'
- await queueRealtimeFullSync(username)
const resp = await api.listChatMessages(params)
if (selectedContact.value?.username !== username) return
@@ -4820,6 +4827,19 @@ const queueRealtimeRefresh = () => {
})
}
+const queueRealtimeChange = () => {
+ if (!process.client || typeof window === 'undefined') return
+ if (!realtimeEnabled.value) return
+ if (realtimeChangeDebounceTimer) return
+
+ // Debounce noisy db_storage change events to avoid hammering the backend.
+ realtimeChangeDebounceTimer = setTimeout(() => {
+ realtimeChangeDebounceTimer = null
+ queueRealtimeRefresh()
+ queueRealtimeSessionsRefresh()
+ }, 500)
+}
+
const startRealtimeStream = () => {
stopRealtimeStream()
if (!process.client || typeof window === 'undefined') return
@@ -4840,9 +4860,7 @@ const startRealtimeStream = () => {
try {
const data = JSON.parse(String(ev.data || '{}'))
if (String(data?.type || '') === 'change') {
- queueRealtimeFullSync(selectedContact.value?.username || '')
- queueRealtimeRefresh()
- queueRealtimeSessionsRefresh()
+ queueRealtimeChange()
}
} catch {}
}
From d0d518aed9ba62d071d6416b51a30ced3e133387 Mon Sep 17 00:00:00 2001
From: 2977094657 <2977094657@qq.com>
Date: Tue, 27 Jan 2026 16:26:53 +0800
Subject: [PATCH 004/103] =?UTF-8?q?fix(chat):=20proxy=5Fimage=20=E5=85=BC?=
=?UTF-8?q?=E5=AE=B9=20tc.qq.com=20=E5=B9=B6=E5=A2=9E=E5=BC=BA=E9=98=B2?=
=?UTF-8?q?=E7=9B=97=E9=93=BE=20Referer?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- proxy_image 放开 .tc.qq.com 白名单,兼容朋友圈/CDN 图片
- 下载时按多组 Referer/Origin 轮询,提高成功率
- 保持 host 校验与 10MB 限制
---
src/wechat_decrypt_tool/routers/chat_media.py | 67 ++++++++++++-------
1 file changed, 43 insertions(+), 24 deletions(-)
diff --git a/src/wechat_decrypt_tool/routers/chat_media.py b/src/wechat_decrypt_tool/routers/chat_media.py
index 655f0bc..0943374 100644
--- a/src/wechat_decrypt_tool/routers/chat_media.py
+++ b/src/wechat_decrypt_tool/routers/chat_media.py
@@ -414,7 +414,7 @@ def _is_allowed_proxy_image_host(host: str) -> bool:
if not h:
return False
# WeChat public account/article thumbnails and avatars commonly live on these CDNs.
- return h.endswith(".qpic.cn") or h.endswith(".qlogo.cn")
+ return h.endswith(".qpic.cn") or h.endswith(".qlogo.cn") or h.endswith(".tc.qq.com")
@router.get("/api/chat/media/proxy_image", summary="代理获取远程图片(解决微信公众号图片防盗链)")
@@ -435,33 +435,52 @@ async def proxy_image(url: str):
raise HTTPException(status_code=400, detail="Unsupported url host for proxy_image.")
def _download_bytes() -> tuple[bytes, str]:
- headers = {
+ base_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36",
"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
- # qpic/qlogo often require a mp.weixin.qq.com referer (anti-hotlink)
- "Referer": "https://mp.weixin.qq.com/",
- "Origin": "https://mp.weixin.qq.com",
}
- r = requests.get(u, headers=headers, timeout=20, stream=True)
- try:
- r.raise_for_status()
- content_type = str(r.headers.get("Content-Type") or "").strip()
- max_bytes = 10 * 1024 * 1024
- chunks: list[bytes] = []
- total = 0
- for ch in r.iter_content(chunk_size=64 * 1024):
- if not ch:
- continue
- chunks.append(ch)
- total += len(ch)
- if total > max_bytes:
- raise HTTPException(status_code=400, detail="Proxy image too large (>10MB).")
- return b"".join(chunks), content_type
- finally:
+
+ # Different Tencent CDNs enforce different anti-hotlink rules.
+ # Try a couple of safe referers so Moments(qpic) and MP(qpic) both work.
+ header_variants = [
+ {"Referer": "https://wx.qq.com/", "Origin": "https://wx.qq.com"},
+ {"Referer": "https://mp.weixin.qq.com/", "Origin": "https://mp.weixin.qq.com"},
+ {"Referer": "https://www.baidu.com/", "Origin": "https://www.baidu.com"},
+ {},
+ ]
+
+ last_err: Exception | None = None
+ for extra in header_variants:
+ headers = dict(base_headers)
+ headers.update(extra)
+ r = requests.get(u, headers=headers, timeout=20, stream=True)
try:
- r.close()
- except Exception:
- pass
+ r.raise_for_status()
+ content_type = str(r.headers.get("Content-Type") or "").strip()
+ max_bytes = 10 * 1024 * 1024
+ chunks: list[bytes] = []
+ total = 0
+ for ch in r.iter_content(chunk_size=64 * 1024):
+ if not ch:
+ continue
+ chunks.append(ch)
+ total += len(ch)
+ if total > max_bytes:
+ raise HTTPException(status_code=400, detail="Proxy image too large (>10MB).")
+ return b"".join(chunks), content_type
+ except HTTPException:
+ # Hard failure, don't retry with another referer.
+ raise
+ except Exception as e:
+ last_err = e
+ finally:
+ try:
+ r.close()
+ except Exception:
+ pass
+
+ # All variants failed.
+ raise last_err or RuntimeError("proxy_image download failed")
try:
data, ct = await asyncio.to_thread(_download_bytes)
From ba9eb5e2676ab3738fd80fe7b608fe1af262bbc3 Mon Sep 17 00:00:00 2001
From: 2977094657 <2977094657@qq.com>
Date: Tue, 27 Jan 2026 16:27:19 +0800
Subject: [PATCH 005/103] =?UTF-8?q?feat(sns):=20=E5=A2=9E=E5=8A=A0?=
=?UTF-8?q?=E6=9C=8B=E5=8F=8B=E5=9C=88=E6=97=B6=E9=97=B4=E7=BA=BF=E4=B8=8E?=
=?UTF-8?q?=E5=9B=BE=E7=89=87=E6=9C=AC=E5=9C=B0=E7=BC=93=E5=AD=98=E6=8E=A5?=
=?UTF-8?q?=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 /api/sns/timeline:优先走 WCDB realtime 读取 sns.db,支持分页/用户过滤/关键字
- 新增 /api/sns/media:本地缓存(cache/.../Sns/Img)解密优先,支持手动 pick/避开重复
- 新增 /api/sns/media_candidates 与 /api/sns/media_picks:候选 key 列表与本机持久化匹配表
- wcdb_realtime 增加 exec_query/get_sns_timeline 封装,并在连接时 set_my_wxid 上下文
- 更新 wcdb_api.dll 并补齐 MSVC runtime 依赖
---
src/wechat_decrypt_tool/api.py | 2 +
src/wechat_decrypt_tool/native/msvcp140.dll | Bin 0 -> 553552 bytes
src/wechat_decrypt_tool/native/msvcp140_1.dll | Bin 0 -> 35488 bytes
.../native/vcruntime140.dll | Bin 0 -> 123472 bytes
.../native/vcruntime140_1.dll | Bin 0 -> 47264 bytes
src/wechat_decrypt_tool/native/wcdb_api.dll | Bin 251904 -> 689152 bytes
src/wechat_decrypt_tool/routers/sns.py | 1068 +++++++++++++++++
src/wechat_decrypt_tool/wcdb_realtime.py | 154 +++
8 files changed, 1224 insertions(+)
create mode 100644 src/wechat_decrypt_tool/native/msvcp140.dll
create mode 100644 src/wechat_decrypt_tool/native/msvcp140_1.dll
create mode 100644 src/wechat_decrypt_tool/native/vcruntime140.dll
create mode 100644 src/wechat_decrypt_tool/native/vcruntime140_1.dll
create mode 100644 src/wechat_decrypt_tool/routers/sns.py
diff --git a/src/wechat_decrypt_tool/api.py b/src/wechat_decrypt_tool/api.py
index df3773f..97ad48b 100644
--- a/src/wechat_decrypt_tool/api.py
+++ b/src/wechat_decrypt_tool/api.py
@@ -18,6 +18,7 @@
from .routers.health import router as _health_router
from .routers.keys import router as _keys_router
from .routers.media import router as _media_router
+from .routers.sns import router as _sns_router
from .routers.wechat_detection import router as _wechat_detection_router
from .wcdb_realtime import WCDB_REALTIME, shutdown as _wcdb_shutdown
@@ -51,6 +52,7 @@
app.include_router(_chat_router)
app.include_router(_chat_export_router)
app.include_router(_chat_media_router)
+app.include_router(_sns_router)
class _SPAStaticFiles(StaticFiles):
diff --git a/src/wechat_decrypt_tool/native/msvcp140.dll b/src/wechat_decrypt_tool/native/msvcp140.dll
new file mode 100644
index 0000000000000000000000000000000000000000..554d2ffc5f5aa84a0598b2f167258187f1fa5842
GIT binary patch
literal 553552
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