mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-02-19 14:20:51 +08:00
feat(chat): 合并转发聊天记录支持预览与弹窗查看
- appmsg(type=19) 解析为 renderType=chatHistory,并透传 recordItem(recorditem 原文) - 修复 recorditem CDATA 内包含 <refermsg> 时误判为引用消息的问题 - 列表/导出路径统一带上 recordItem,并避免已解析的 appmsg 被二次 XML 解析覆盖 - 前端聊天页新增聊天记录卡片 + 弹窗,支持按条展示及图片/视频/引用内容预览 - 会话列表与摘要统一显示为 [聊天记录]
This commit is contained in:
@@ -890,6 +890,7 @@ def _parse_message_for_export(
|
||||
content_text = raw_text
|
||||
title = ""
|
||||
url = ""
|
||||
record_item = ""
|
||||
image_md5 = ""
|
||||
image_file_id = ""
|
||||
emoji_md5 = ""
|
||||
@@ -929,6 +930,7 @@ def _parse_message_for_export(
|
||||
content_text = str(parsed.get("content") or "")
|
||||
title = str(parsed.get("title") or "")
|
||||
url = str(parsed.get("url") or "")
|
||||
record_item = str(parsed.get("recordItem") or "")
|
||||
quote_title = str(parsed.get("quoteTitle") or "")
|
||||
quote_content = str(parsed.get("quoteContent") or "")
|
||||
amount = str(parsed.get("amount") or "")
|
||||
@@ -1089,14 +1091,17 @@ def _parse_message_for_export(
|
||||
content_text = _infer_message_brief_by_local_type(local_type)
|
||||
else:
|
||||
if content_text.startswith("<") or content_text.startswith('"<'):
|
||||
parsed_special = False
|
||||
if "<appmsg" in content_text.lower():
|
||||
parsed = _parse_app_message(content_text)
|
||||
rt = str(parsed.get("renderType") or "")
|
||||
if rt and rt != "text":
|
||||
parsed_special = True
|
||||
render_type = rt
|
||||
content_text = str(parsed.get("content") or content_text)
|
||||
title = str(parsed.get("title") or title)
|
||||
url = str(parsed.get("url") or url)
|
||||
record_item = str(parsed.get("recordItem") or record_item)
|
||||
quote_title = str(parsed.get("quoteTitle") or quote_title)
|
||||
quote_content = str(parsed.get("quoteContent") or quote_content)
|
||||
amount = str(parsed.get("amount") or amount)
|
||||
@@ -1121,9 +1126,11 @@ def _parse_message_for_export(
|
||||
)
|
||||
if not content_text:
|
||||
content_text = transfer_status or "转账"
|
||||
t = _extract_xml_tag_text(content_text, "title")
|
||||
d = _extract_xml_tag_text(content_text, "des")
|
||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
||||
|
||||
if not parsed_special:
|
||||
t = _extract_xml_tag_text(content_text, "title")
|
||||
d = _extract_xml_tag_text(content_text, "des")
|
||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
||||
|
||||
if not content_text:
|
||||
content_text = _infer_message_brief_by_local_type(local_type)
|
||||
@@ -1151,6 +1158,7 @@ def _parse_message_for_export(
|
||||
"content": content_text,
|
||||
"title": title,
|
||||
"url": url,
|
||||
"recordItem": record_item,
|
||||
"thumbUrl": thumb_url,
|
||||
"imageMd5": image_md5,
|
||||
"imageFileId": image_file_id,
|
||||
|
||||
@@ -171,7 +171,7 @@ def _infer_last_message_brief(msg_type: Optional[int], sub_type: Optional[int])
|
||||
if s == 2003:
|
||||
return "[Red Packet]"
|
||||
if s == 19:
|
||||
return "[Chat History]"
|
||||
return "[聊天记录]"
|
||||
return "[App Message]"
|
||||
if t == 10000:
|
||||
return "[System]"
|
||||
@@ -209,7 +209,7 @@ def _infer_message_brief_by_local_type(local_type: Optional[int]) -> str:
|
||||
if t == 8594229559345:
|
||||
return "[Red Packet]"
|
||||
if t == 81604378673:
|
||||
return "[Chat History]"
|
||||
return "[聊天记录]"
|
||||
if t == 266287972401:
|
||||
return "[Pat]"
|
||||
if t == 8589934592049:
|
||||
@@ -698,6 +698,22 @@ def _parse_app_message(text: str) -> dict[str, Any]:
|
||||
|
||||
lower = text.lower()
|
||||
|
||||
if app_type == 19:
|
||||
# 合并转发聊天记录(Chat History)
|
||||
# 注意:recorditem 的 CDATA 内部可能包含 <refermsg> 等标签,不能据此把整条消息误判为引用消息。
|
||||
record_item = _extract_xml_tag_text(text, "recorditem")
|
||||
preview = (des or "").strip()
|
||||
if not preview:
|
||||
if record_item:
|
||||
preview = str(_extract_xml_tag_text(record_item, "desc") or "").strip()
|
||||
|
||||
return {
|
||||
"renderType": "chatHistory",
|
||||
"content": preview or "[聊天记录]",
|
||||
"title": (title or "").strip() or "聊天记录",
|
||||
"recordItem": record_item or "",
|
||||
}
|
||||
|
||||
if app_type in (5, 68) and url:
|
||||
thumb_url = _extract_xml_tag_text(text, "thumburl")
|
||||
return {
|
||||
@@ -724,7 +740,21 @@ def _parse_app_message(text: str) -> dict[str, Any]:
|
||||
"fileMd5": file_md5 or "",
|
||||
}
|
||||
|
||||
if app_type == 57 or "<refermsg" in lower:
|
||||
refermsg_probe = lower
|
||||
if "<recorditem" in lower and "<refermsg" in lower:
|
||||
# 合并转发聊天记录/其它 appmsg 里可能在 recorditem CDATA 内包含 refermsg,
|
||||
# 需要先剔除 recorditem 再判断是否为真正的引用消息。
|
||||
try:
|
||||
refermsg_probe = re.sub(
|
||||
r"(<recorditem[^>]*>.*?</recorditem>)",
|
||||
"",
|
||||
text,
|
||||
flags=re.IGNORECASE | re.DOTALL,
|
||||
).lower()
|
||||
except Exception:
|
||||
refermsg_probe = lower
|
||||
|
||||
if app_type == 57 or "<refermsg" in refermsg_probe:
|
||||
refer_block = _extract_refermsg_block(text)
|
||||
|
||||
try:
|
||||
@@ -944,6 +974,8 @@ def _build_latest_message_preview(
|
||||
rt = str(parsed.get("renderType") or "")
|
||||
content_text = str(parsed.get("content") or "")
|
||||
title_text = str(parsed.get("title") or "").strip()
|
||||
if rt == "chatHistory":
|
||||
content_text = "[聊天记录]"
|
||||
if rt == "file" and title_text:
|
||||
content_text = title_text
|
||||
if (not content_text) and rt == "transfer":
|
||||
|
||||
@@ -373,6 +373,7 @@ def _append_full_messages_from_rows(
|
||||
content_text = raw_text
|
||||
title = ""
|
||||
url = ""
|
||||
record_item = ""
|
||||
image_md5 = ""
|
||||
emoji_md5 = ""
|
||||
emoji_url = ""
|
||||
@@ -414,6 +415,7 @@ def _append_full_messages_from_rows(
|
||||
content_text = str(parsed.get("content") or "")
|
||||
title = str(parsed.get("title") or "")
|
||||
url = str(parsed.get("url") or "")
|
||||
record_item = str(parsed.get("recordItem") or "")
|
||||
quote_title = str(parsed.get("quoteTitle") or "")
|
||||
quote_content = str(parsed.get("quoteContent") or "")
|
||||
quote_username = str(parsed.get("quoteUsername") or "")
|
||||
@@ -606,14 +608,17 @@ def _append_full_messages_from_rows(
|
||||
content_text = _infer_message_brief_by_local_type(local_type)
|
||||
else:
|
||||
if content_text.startswith("<") or content_text.startswith('"<'):
|
||||
parsed_special = False
|
||||
if "<appmsg" in content_text.lower():
|
||||
parsed = _parse_app_message(content_text)
|
||||
rt = str(parsed.get("renderType") or "")
|
||||
if rt and rt != "text":
|
||||
parsed_special = True
|
||||
render_type = rt
|
||||
content_text = str(parsed.get("content") or content_text)
|
||||
title = str(parsed.get("title") or title)
|
||||
url = str(parsed.get("url") or url)
|
||||
record_item = str(parsed.get("recordItem") or record_item)
|
||||
quote_title = str(parsed.get("quoteTitle") or quote_title)
|
||||
quote_content = str(parsed.get("quoteContent") or quote_content)
|
||||
amount = str(parsed.get("amount") or amount)
|
||||
@@ -639,9 +644,11 @@ def _append_full_messages_from_rows(
|
||||
)
|
||||
if not content_text:
|
||||
content_text = transfer_status or "转账"
|
||||
t = _extract_xml_tag_text(content_text, "title")
|
||||
d = _extract_xml_tag_text(content_text, "des")
|
||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
||||
|
||||
if not parsed_special:
|
||||
t = _extract_xml_tag_text(content_text, "title")
|
||||
d = _extract_xml_tag_text(content_text, "des")
|
||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
||||
|
||||
if not content_text:
|
||||
content_text = _infer_message_brief_by_local_type(local_type)
|
||||
@@ -664,6 +671,7 @@ def _append_full_messages_from_rows(
|
||||
"content": content_text,
|
||||
"title": title,
|
||||
"url": url,
|
||||
"recordItem": record_item,
|
||||
"imageMd5": image_md5,
|
||||
"imageFileId": image_file_id,
|
||||
"emojiMd5": emoji_md5,
|
||||
@@ -1080,6 +1088,19 @@ async def list_chat_sessions(
|
||||
else:
|
||||
last_message = _infer_last_message_brief(r["last_msg_type"], r["last_msg_sub_type"])
|
||||
|
||||
# 合并转发聊天记录:左侧会话列表统一显示为 [聊天记录]
|
||||
if preview_mode != "none" and not str(last_message or "").startswith("[草稿]"):
|
||||
try:
|
||||
last_msg_type = int(r["last_msg_type"] or 0)
|
||||
except Exception:
|
||||
last_msg_type = 0
|
||||
try:
|
||||
last_msg_sub_type = int(r["last_msg_sub_type"] or 0)
|
||||
except Exception:
|
||||
last_msg_sub_type = 0
|
||||
if last_msg_type == 81604378673 or (last_msg_type == 49 and last_msg_sub_type == 19):
|
||||
last_message = "[聊天记录]"
|
||||
|
||||
last_time = _format_session_time(r["sort_timestamp"] or r["last_timestamp"])
|
||||
|
||||
sessions.append(
|
||||
@@ -1214,6 +1235,7 @@ def _collect_chat_messages(
|
||||
content_text = raw_text
|
||||
title = ""
|
||||
url = ""
|
||||
record_item = ""
|
||||
image_md5 = ""
|
||||
emoji_md5 = ""
|
||||
emoji_url = ""
|
||||
@@ -1257,6 +1279,7 @@ def _collect_chat_messages(
|
||||
content_text = str(parsed.get("content") or "")
|
||||
title = str(parsed.get("title") or "")
|
||||
url = str(parsed.get("url") or "")
|
||||
record_item = str(parsed.get("recordItem") or "")
|
||||
quote_title = str(parsed.get("quoteTitle") or "")
|
||||
quote_content = str(parsed.get("quoteContent") or "")
|
||||
quote_username = str(parsed.get("quoteUsername") or "")
|
||||
@@ -1444,14 +1467,17 @@ def _collect_chat_messages(
|
||||
content_text = _infer_message_brief_by_local_type(local_type)
|
||||
else:
|
||||
if content_text.startswith("<") or content_text.startswith('"<'):
|
||||
parsed_special = False
|
||||
if "<appmsg" in content_text.lower():
|
||||
parsed = _parse_app_message(content_text)
|
||||
rt = str(parsed.get("renderType") or "")
|
||||
if rt and rt != "text":
|
||||
parsed_special = True
|
||||
render_type = rt
|
||||
content_text = str(parsed.get("content") or content_text)
|
||||
title = str(parsed.get("title") or title)
|
||||
url = str(parsed.get("url") or url)
|
||||
record_item = str(parsed.get("recordItem") or record_item)
|
||||
quote_title = str(parsed.get("quoteTitle") or quote_title)
|
||||
quote_content = str(parsed.get("quoteContent") or quote_content)
|
||||
amount = str(parsed.get("amount") or amount)
|
||||
@@ -1477,9 +1503,11 @@ def _collect_chat_messages(
|
||||
)
|
||||
if not content_text:
|
||||
content_text = transfer_status or "转账"
|
||||
t = _extract_xml_tag_text(content_text, "title")
|
||||
d = _extract_xml_tag_text(content_text, "des")
|
||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
||||
|
||||
if not parsed_special:
|
||||
t = _extract_xml_tag_text(content_text, "title")
|
||||
d = _extract_xml_tag_text(content_text, "des")
|
||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
||||
|
||||
if not content_text:
|
||||
content_text = _infer_message_brief_by_local_type(local_type)
|
||||
@@ -1509,6 +1537,7 @@ def _collect_chat_messages(
|
||||
"content": content_text,
|
||||
"title": title,
|
||||
"url": url,
|
||||
"recordItem": record_item,
|
||||
"imageMd5": image_md5,
|
||||
"imageFileId": image_file_id,
|
||||
"emojiMd5": emoji_md5,
|
||||
@@ -1746,6 +1775,7 @@ async def list_chat_messages(
|
||||
content_text = raw_text
|
||||
title = ""
|
||||
url = ""
|
||||
record_item = ""
|
||||
image_md5 = ""
|
||||
emoji_md5 = ""
|
||||
emoji_url = ""
|
||||
@@ -1789,6 +1819,7 @@ async def list_chat_messages(
|
||||
content_text = str(parsed.get("content") or "")
|
||||
title = str(parsed.get("title") or "")
|
||||
url = str(parsed.get("url") or "")
|
||||
record_item = str(parsed.get("recordItem") or "")
|
||||
quote_title = str(parsed.get("quoteTitle") or "")
|
||||
quote_content = str(parsed.get("quoteContent") or "")
|
||||
quote_username = str(parsed.get("quoteUsername") or "")
|
||||
@@ -1976,14 +2007,17 @@ async def list_chat_messages(
|
||||
content_text = _infer_message_brief_by_local_type(local_type)
|
||||
else:
|
||||
if content_text.startswith("<") or content_text.startswith('"<'):
|
||||
parsed_special = False
|
||||
if "<appmsg" in content_text.lower():
|
||||
parsed = _parse_app_message(content_text)
|
||||
rt = str(parsed.get("renderType") or "")
|
||||
if rt and rt != "text":
|
||||
parsed_special = True
|
||||
render_type = rt
|
||||
content_text = str(parsed.get("content") or content_text)
|
||||
title = str(parsed.get("title") or title)
|
||||
url = str(parsed.get("url") or url)
|
||||
record_item = str(parsed.get("recordItem") or record_item)
|
||||
quote_title = str(parsed.get("quoteTitle") or quote_title)
|
||||
quote_content = str(parsed.get("quoteContent") or quote_content)
|
||||
amount = str(parsed.get("amount") or amount)
|
||||
@@ -2009,9 +2043,11 @@ async def list_chat_messages(
|
||||
)
|
||||
if not content_text:
|
||||
content_text = transfer_status or "转账"
|
||||
t = _extract_xml_tag_text(content_text, "title")
|
||||
d = _extract_xml_tag_text(content_text, "des")
|
||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
||||
|
||||
if not parsed_special:
|
||||
t = _extract_xml_tag_text(content_text, "title")
|
||||
d = _extract_xml_tag_text(content_text, "des")
|
||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
||||
|
||||
if not content_text:
|
||||
content_text = _infer_message_brief_by_local_type(local_type)
|
||||
@@ -2034,6 +2070,7 @@ async def list_chat_messages(
|
||||
"content": content_text,
|
||||
"title": title,
|
||||
"url": url,
|
||||
"recordItem": record_item,
|
||||
"imageMd5": image_md5,
|
||||
"imageFileId": image_file_id,
|
||||
"emojiMd5": emoji_md5,
|
||||
|
||||
Reference in New Issue
Block a user