Skip to content

Commit fa273b8

Browse files
committed
Improve chat history formatting
1 parent a5a347f commit fa273b8

1 file changed

Lines changed: 229 additions & 63 deletions

File tree

mcp_server.py

Lines changed: 229 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
Runs 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
# ============ 加密常量 ============
1819
PAGE_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

223225
def _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

281294
def 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

Comments
 (0)