55Runs on Windows Python (needs access to D:\ WeChat databases).
66"""
77
8- import os , sys , json , time , sqlite3 , tempfile , struct , hashlib , atexit , re
9- import hmac as hmac_mod
10- from datetime import datetime
11- from Crypto .Cipher import AES
12- from mcp .server .fastmcp import FastMCP
13- import zstandard as zstd
14- from decode_image import ImageResolver
15- from key_utils import get_key_info , key_path_variants , strip_key_metadata
8+ import os , sys , json , time , sqlite3 , tempfile , struct , hashlib , atexit , re
9+ import hmac as hmac_mod
10+ from datetime import datetime
11+ import xml .etree .ElementTree as ET
12+ from Crypto .Cipher import AES
13+ from mcp .server .fastmcp import FastMCP
14+ import zstandard as zstd
15+ from decode_image import ImageResolver
16+ from key_utils import get_key_info , key_path_variants , strip_key_metadata
1617
1718# ============ 加密常量 ============
1819PAGE_SZ = 4096
@@ -216,8 +217,9 @@ def cleanup(self):
216217
217218# ============ 联系人缓存 ============
218219
219- _contact_names = None # {username: display_name}
220- _contact_full = None # [{username, nick_name, remark}]
220+ _contact_names = None # {username: display_name}
221+ _contact_full = None # [{username, nick_name, remark}]
222+ _self_username = None
221223
222224
223225def _load_contacts_from (db_path ):
@@ -261,21 +263,32 @@ def get_contact_names():
261263 return {}
262264
263265
264- def get_contact_full ():
265- global _contact_full
266- if _contact_full is None :
267- get_contact_names ()
268- return _contact_full or []
266+ def get_contact_full ():
267+ global _contact_full
268+ if _contact_full is None :
269+ get_contact_names ()
270+ return _contact_full or []
269271
270272
271273# ============ 辅助函数 ============
272274
273- def format_msg_type (t ):
274- return {
275- 1 : '文本' , 3 : '图片' , 34 : '语音' , 42 : '名片' ,
276- 43 : '视频' , 47 : '表情' , 48 : '位置' , 49 : '链接/文件' ,
277- 50 : '通话' , 10000 : '系统' , 10002 : '撤回' ,
278- }.get (t , f'type={ t } ' )
275+ def format_msg_type (t ):
276+ base_type , _ = _split_msg_type (t )
277+ return {
278+ 1 : '文本' , 3 : '图片' , 34 : '语音' , 42 : '名片' ,
279+ 43 : '视频' , 47 : '表情' , 48 : '位置' , 49 : '链接/文件' ,
280+ 50 : '通话' , 10000 : '系统' , 10002 : '撤回' ,
281+ }.get (base_type , f'type={ t } ' )
282+
283+
284+ def _split_msg_type (t ):
285+ try :
286+ t = int (t )
287+ except (TypeError , ValueError ):
288+ return 0 , 0
289+ if t > 0xFFFFFFFF :
290+ return t & 0xFFFFFFFF , t >> 32
291+ return t , 0
279292
280293
281294def resolve_username (chat_name ):
@@ -316,10 +329,10 @@ def _decompress_content(content, ct):
316329 return content
317330
318331
319- def _parse_message_content (content , local_type , is_group ):
320- """解析消息内容,返回 (sender_id, text)"""
321- if content is None :
322- return '' , ''
332+ def _parse_message_content (content , local_type , is_group ):
333+ """解析消息内容,返回 (sender_id, text)"""
334+ if content is None :
335+ return '' , ''
323336 if isinstance (content , bytes ):
324337 return '' , '(二进制内容)'
325338
@@ -328,7 +341,163 @@ def _parse_message_content(content, local_type, is_group):
328341 if is_group and ':\n ' in content :
329342 sender , text = content .split (':\n ' , 1 )
330343
331- return sender , text
344+ return sender , text
345+
346+
347+ def _collapse_text (text ):
348+ if not text :
349+ return ''
350+ return re .sub (r'\s+' , ' ' , text ).strip ()
351+
352+
353+ def _get_self_username ():
354+ global _self_username
355+ if _self_username is not None :
356+ return _self_username
357+
358+ names = get_contact_names ()
359+ account_dir = os .path .basename (os .path .dirname (DB_DIR ))
360+ candidates = [account_dir ]
361+
362+ m = re .fullmatch (r'(.+)_([0-9a-fA-F]{4,})' , account_dir )
363+ if m :
364+ candidates .insert (0 , m .group (1 ))
365+
366+ for candidate in candidates :
367+ if candidate and candidate in names :
368+ _self_username = candidate
369+ return _self_username
370+
371+ _self_username = ''
372+ return _self_username
373+
374+
375+ def _load_name2id_maps (conn ):
376+ id_to_username = {}
377+ username_to_id = {}
378+ try :
379+ rows = conn .execute ("SELECT rowid, user_name FROM Name2Id" ).fetchall ()
380+ except sqlite3 .Error :
381+ return id_to_username , username_to_id
382+
383+ for rowid , user_name in rows :
384+ if not user_name :
385+ continue
386+ id_to_username [rowid ] = user_name
387+ username_to_id [user_name ] = rowid
388+ return id_to_username , username_to_id
389+
390+
391+ def _display_name_for_username (username , names ):
392+ if not username :
393+ return ''
394+ if username == _get_self_username ():
395+ return 'me'
396+ return names .get (username , username )
397+
398+
399+ def _resolve_sender_label (real_sender_id , sender_from_content , is_group , chat_username , chat_display_name , names , id_to_username ):
400+ sender_username = id_to_username .get (real_sender_id , '' )
401+
402+ if is_group :
403+ if sender_username and sender_username != chat_username :
404+ return _display_name_for_username (sender_username , names )
405+ if sender_from_content :
406+ return _display_name_for_username (sender_from_content , names )
407+ return ''
408+
409+ if sender_username == chat_username :
410+ return chat_display_name
411+ if sender_username :
412+ return _display_name_for_username (sender_username , names )
413+ return ''
414+
415+
416+ def _resolve_quote_sender_label (ref_user , ref_display_name , is_group , chat_username , chat_display_name , names ):
417+ if is_group :
418+ if ref_user :
419+ return _display_name_for_username (ref_user , names )
420+ return ref_display_name or ''
421+
422+ if ref_user :
423+ if ref_user == chat_username :
424+ return chat_display_name
425+ return 'me'
426+ if ref_display_name :
427+ if ref_display_name == chat_display_name :
428+ return chat_display_name
429+ return 'me'
430+ return ''
431+
432+
433+ def _format_app_message_text (content , local_type , is_group , chat_username , chat_display_name , names ):
434+ if not content or '<appmsg' not in content :
435+ return None
436+
437+ _ , sub_type = _split_msg_type (local_type )
438+
439+ try :
440+ root = ET .fromstring (content )
441+ except ET .ParseError :
442+ return None
443+
444+ appmsg = root .find ('.//appmsg' )
445+ if appmsg is None :
446+ return None
447+
448+ title = _collapse_text (appmsg .findtext ('title' ) or '' )
449+ app_type_text = (appmsg .findtext ('type' ) or '' ).strip ()
450+ app_type = int (app_type_text or sub_type or 0 )
451+
452+ if app_type == 57 :
453+ ref = appmsg .find ('.//refermsg' )
454+ ref_user = ''
455+ ref_display_name = ''
456+ ref_content = ''
457+ if ref is not None :
458+ ref_user = (ref .findtext ('fromusr' ) or '' ).strip ()
459+ ref_display_name = (ref .findtext ('displayname' ) or '' ).strip ()
460+ ref_content = _collapse_text (ref .findtext ('content' ) or '' )
461+ if len (ref_content ) > 160 :
462+ ref_content = ref_content [:160 ] + "..."
463+
464+ quote_text = title or "[引用消息]"
465+ if ref_content :
466+ ref_label = _resolve_quote_sender_label (
467+ ref_user , ref_display_name , is_group , chat_username , chat_display_name , names
468+ )
469+ prefix = f"回复 { ref_label } : " if ref_label else "回复: "
470+ quote_text += f"\n ↳ { prefix } { ref_content } "
471+ return quote_text
472+
473+ if app_type == 6 :
474+ return f"[文件] { title } " if title else "[文件]"
475+ if app_type == 5 :
476+ return f"[链接] { title } " if title else "[链接]"
477+ if app_type in (33 , 36 , 44 ):
478+ return f"[小程序] { title } " if title else "[小程序]"
479+ if title :
480+ return f"[链接/文件] { title } "
481+ return "[链接/文件]"
482+
483+
484+ def _format_message_text (local_id , local_type , content , is_group , chat_username , chat_display_name , names ):
485+ sender_from_content , text = _parse_message_content (content , local_type , is_group )
486+ base_type , _ = _split_msg_type (local_type )
487+
488+ if base_type == 3 :
489+ text = f"[图片] (local_id={ local_id } )"
490+ elif base_type == 47 :
491+ text = "[表情]"
492+ elif base_type == 49 :
493+ text = _format_app_message_text (
494+ text , local_type , is_group , chat_username , chat_display_name , names
495+ ) or "[链接/文件]"
496+ elif base_type != 1 :
497+ type_label = format_msg_type (local_type )
498+ text = f"[{ type_label } ] { text } " if text else f"[{ type_label } ]"
499+
500+ return sender_from_content , text
332501
333502
334503# 消息 DB 的 rel_keys
@@ -453,50 +622,47 @@ def get_chat_history(chat_name: str, limit: int = 50) -> str:
453622 if not db_path :
454623 return f"找不到 { display_name } 的消息记录(可能在未解密的DB中或无消息)"
455624
456- conn = sqlite3 .connect (db_path )
457- try :
458- rows = conn .execute (f"""
459- SELECT local_id, local_type, create_time, message_content,
460- WCDB_CT_message_content
461- FROM [{ table_name } ]
462- ORDER BY create_time DESC
463- LIMIT ?
464- """ , (limit ,)).fetchall ()
625+ conn = sqlite3 .connect (db_path )
626+ try :
627+ id_to_username , _ = _load_name2id_maps (conn )
628+ rows = conn .execute (f"""
629+ SELECT local_id, local_type, create_time, real_sender_id, message_content,
630+ WCDB_CT_message_content
631+ FROM [{ table_name } ]
632+ ORDER BY create_time DESC
633+ LIMIT ?
634+ """ , (limit ,)).fetchall ()
465635 except Exception as e :
466636 conn .close ()
467637 return f"查询失败: { e } "
468638 conn .close ()
469639
470640 if not rows :
471641 return f"{ display_name } 无消息记录"
472-
473- lines = []
474- for local_id , local_type , create_time , content , ct in reversed (rows ):
475- time_str = datetime .fromtimestamp (create_time ).strftime ('%m-%d %H:%M' )
476-
477- # zstd 解压
478- content = _decompress_content (content , ct )
479- if content is None :
480- content = '(无法解压)'
481-
482- sender , text = _parse_message_content (content , local_type , is_group )
483-
484- if local_type == 3 :
485- text = f"[图片] (local_id={ local_id } )"
486- elif local_type == 47 :
487- text = "[表情]"
488- elif local_type != 1 :
489- type_label = format_msg_type (local_type )
490- text = f"[{ type_label } ] { text } " if text else f"[{ type_label } ]"
491-
492- if text and len (text ) > 500 :
493- text = text [:500 ] + "..."
494-
495- if is_group and sender :
496- sender_name = names .get (sender , sender )
497- lines .append (f"[{ time_str } ] { sender_name } : { text } " )
498- else :
499- lines .append (f"[{ time_str } ] { text } " )
642+
643+ lines = []
644+ for local_id , local_type , create_time , real_sender_id , content , ct in reversed (rows ):
645+ time_str = datetime .fromtimestamp (create_time ).strftime ('%m-%d %H:%M' )
646+
647+ # zstd 解压
648+ content = _decompress_content (content , ct )
649+ if content is None :
650+ content = '(无法解压)'
651+
652+ sender , text = _format_message_text (
653+ local_id , local_type , content , is_group , username , display_name , names
654+ )
655+
656+ if text and len (text ) > 500 :
657+ text = text [:500 ] + "..."
658+
659+ sender_label = _resolve_sender_label (
660+ real_sender_id , sender , is_group , username , display_name , names , id_to_username
661+ )
662+ if sender_label :
663+ lines .append (f"[{ time_str } ] { sender_label } : { text } " )
664+ else :
665+ lines .append (f"[{ time_str } ] { text } " )
500666
501667 header = f"{ display_name } 的最近 { len (lines )} 条消息"
502668 if is_group :
0 commit comments