feat: improve quoted message rendering in dashboard
This commit is contained in:
@@ -3,7 +3,7 @@ from .auth import login_required
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from utils.message_formatter import format_quote_message
|
from utils.message_formatter import format_quote_message, parse_quote_message
|
||||||
|
|
||||||
# 创建消息管理蓝图
|
# 创建消息管理蓝图
|
||||||
messages_bp = Blueprint('messages', __name__)
|
messages_bp = Blueprint('messages', __name__)
|
||||||
@@ -68,7 +68,11 @@ def get_messages():
|
|||||||
# 检查是否为引用消息
|
# 检查是否为引用消息
|
||||||
if '<refermsg>' in msg['content']:
|
if '<refermsg>' in msg['content']:
|
||||||
# 使用格式化工具处理引用消息
|
# 使用格式化工具处理引用消息
|
||||||
msg['content'] = format_quote_message(msg['content'])
|
quote_data = parse_quote_message(msg['content'])
|
||||||
|
msg['content'] = quote_data['formatted_message']
|
||||||
|
msg['quoted_type'] = quote_data['reference_type']
|
||||||
|
msg['quoted_preview_image'] = quote_data['preview_image']
|
||||||
|
msg['quoted_preview_video_thumb'] = quote_data['preview_video_thumb']
|
||||||
else:
|
else:
|
||||||
# 其他类型的应用消息,解析 XML 提取标题
|
# 其他类型的应用消息,解析 XML 提取标题
|
||||||
root = ET.fromstring(msg['content'])
|
root = ET.fromstring(msg['content'])
|
||||||
|
|||||||
@@ -94,6 +94,14 @@
|
|||||||
|
|
||||||
<div v-else class="message-text-preview is-muted">
|
<div v-else class="message-text-preview is-muted">
|
||||||
{% raw %}{{ scope.row.content || `【消息类型: ${scope.row.message_type}】` }}{% endraw %}
|
{% raw %}{{ scope.row.content || `【消息类型: ${scope.row.message_type}】` }}{% endraw %}
|
||||||
|
<div v-if="scope.row.quoted_type === 'image' && scope.row.quoted_preview_image" class="quoted-media-preview">
|
||||||
|
<div class="message-media-label">【引用图片】</div>
|
||||||
|
<img :src="scope.row.quoted_preview_image" class="message-thumb" @click="showQuotedImage(scope.row.quoted_preview_image)">
|
||||||
|
</div>
|
||||||
|
<div v-else-if="scope.row.quoted_type === 'video' && scope.row.quoted_preview_video_thumb" class="quoted-media-preview">
|
||||||
|
<div class="message-media-label">【引用视频】</div>
|
||||||
|
<img :src="scope.row.quoted_preview_video_thumb" class="message-thumb">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -198,7 +206,8 @@
|
|||||||
},
|
},
|
||||||
detailDialogVisible: false,
|
detailDialogVisible: false,
|
||||||
imageDialogVisible: false,
|
imageDialogVisible: false,
|
||||||
selectedMessage: null
|
selectedMessage: null,
|
||||||
|
quotedPreviewUrl: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -309,6 +318,10 @@
|
|||||||
this.selectedMessage = message;
|
this.selectedMessage = message;
|
||||||
this.imageDialogVisible = true;
|
this.imageDialogVisible = true;
|
||||||
},
|
},
|
||||||
|
showQuotedImage(url) {
|
||||||
|
this.selectedMessage = { image_path: '', message_thumb: url };
|
||||||
|
this.imageDialogVisible = true;
|
||||||
|
},
|
||||||
showVideo(message) {
|
showVideo(message) {
|
||||||
this.selectedMessage = message;
|
this.selectedMessage = message;
|
||||||
this.detailDialogVisible = true;
|
this.detailDialogVisible = true;
|
||||||
@@ -444,6 +457,13 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quoted-media-preview {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.message-media-label {
|
.message-media-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
|
|||||||
@@ -3,6 +3,93 @@ import html
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_text(value: str) -> str:
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
value = html.unescape(value)
|
||||||
|
value = re.sub(r"<br\s*/?>", "\n", value, flags=re.IGNORECASE)
|
||||||
|
value = re.sub(r"<[^>]+>", "", value)
|
||||||
|
return value.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_first(pattern: str, text: str, default: str = "") -> str:
|
||||||
|
match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
|
||||||
|
return match.group(1) if match else default
|
||||||
|
|
||||||
|
|
||||||
|
def _format_referenced_content(ref_type: str, quoted_content: str, xml_content: str) -> str:
|
||||||
|
cleaned = _clean_text(quoted_content)
|
||||||
|
lower_xml = (quoted_content or "") + (xml_content or "")
|
||||||
|
lower_xml = lower_xml.lower()
|
||||||
|
|
||||||
|
if ref_type in {"3"} or "<img" in lower_xml or "cdnthumburl" in lower_xml:
|
||||||
|
return "[图片]"
|
||||||
|
if ref_type in {"43", "62"} or "<videomsg" in lower_xml or "cdnvideourl" in lower_xml:
|
||||||
|
return "[视频]"
|
||||||
|
if ref_type in {"47", "1048625", "1090519089"} or "<emoji" in lower_xml or "<emoticonmd5>" in lower_xml:
|
||||||
|
return "[表情]"
|
||||||
|
if ref_type in {"34"} or "<voicemsg" in lower_xml:
|
||||||
|
return "[语音]"
|
||||||
|
if ref_type in {"48"} or "<location" in lower_xml:
|
||||||
|
return "[位置]"
|
||||||
|
if ref_type in {"49"}:
|
||||||
|
title = _extract_first(r"<title>(.*?)</title>", quoted_content) or _extract_first(r"<title>(.*?)</title>", xml_content)
|
||||||
|
title = _clean_text(title)
|
||||||
|
return f"[链接] {title}" if title else "[链接]"
|
||||||
|
|
||||||
|
if cleaned:
|
||||||
|
return cleaned
|
||||||
|
return "[消息]"
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_media_preview(ref_type: str, quoted_content: str) -> dict:
|
||||||
|
payload = html.unescape(quoted_content or "")
|
||||||
|
preview = {"reference_type": "text", "preview_image": "", "preview_video_thumb": ""}
|
||||||
|
|
||||||
|
if ref_type in {"3"} or "<img" in payload.lower():
|
||||||
|
preview["reference_type"] = "image"
|
||||||
|
preview["preview_image"] = (
|
||||||
|
_extract_first(r'cdnthumburl="(.*?)"', payload)
|
||||||
|
or _extract_first(r"<cdnthumburl><!\[CDATA\[(.*?)\]\]></cdnthumburl>", payload)
|
||||||
|
or _extract_first(r"<cdnmidimgurl><!\[CDATA\[(.*?)\]\]></cdnmidimgurl>", payload)
|
||||||
|
)
|
||||||
|
return preview
|
||||||
|
|
||||||
|
if ref_type in {"43", "62"} or "<videomsg" in payload.lower():
|
||||||
|
preview["reference_type"] = "video"
|
||||||
|
preview["preview_video_thumb"] = (
|
||||||
|
_extract_first(r'cdnthumburl="(.*?)"', payload)
|
||||||
|
or _extract_first(r"<cdnthumburl><!\[CDATA\[(.*?)\]\]></cdnthumburl>", payload)
|
||||||
|
)
|
||||||
|
return preview
|
||||||
|
|
||||||
|
if ref_type in {"47", "1048625", "1090519089"} or "<emoji" in payload.lower():
|
||||||
|
preview["reference_type"] = "emoji"
|
||||||
|
return preview
|
||||||
|
|
||||||
|
return preview
|
||||||
|
|
||||||
|
|
||||||
|
def parse_quote_message(xml_content: str) -> dict:
|
||||||
|
xml_content = xml_content.replace('<', '<').replace('>', '>')
|
||||||
|
main_content = _clean_text(_extract_first(r'<title>(.*?)</title>', xml_content, "[无标题]")) or "[无标题]"
|
||||||
|
display_name = _clean_text(_extract_first(r'<displayname>(.*?)</displayname>', xml_content, "未知用户")) or "未知用户"
|
||||||
|
quoted_content = _extract_first(r'<refermsg>.*?<content>(.*?)</content>', xml_content)
|
||||||
|
ref_type = _extract_first(r'<refermsg>.*?<type>(.*?)</type>', xml_content)
|
||||||
|
pretty_reference = _format_referenced_content(ref_type, quoted_content, xml_content)
|
||||||
|
media_preview = _extract_media_preview(ref_type, quoted_content)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"main_content": main_content,
|
||||||
|
"display_name": display_name,
|
||||||
|
"quoted_content": pretty_reference,
|
||||||
|
"reference_type": media_preview.get("reference_type", "text"),
|
||||||
|
"preview_image": media_preview.get("preview_image", ""),
|
||||||
|
"preview_video_thumb": media_preview.get("preview_video_thumb", ""),
|
||||||
|
"formatted_message": f"{main_content}\n引用 {display_name}:{pretty_reference}" if display_name and pretty_reference else main_content
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def format_quote_message(xml_content):
|
def format_quote_message(xml_content):
|
||||||
"""
|
"""
|
||||||
格式化引用消息
|
格式化引用消息
|
||||||
@@ -14,31 +101,7 @@ def format_quote_message(xml_content):
|
|||||||
格式化后的消息文本
|
格式化后的消息文本
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
return parse_quote_message(xml_content)["formatted_message"]
|
||||||
xml_content = xml_content.replace('<', '<').replace('>', '>')
|
|
||||||
# 使用正则表达式直接提取关键信息,避免XML解析问题
|
|
||||||
title_match = re.search(r'<title>(.*?)</title>', xml_content)
|
|
||||||
main_content = title_match.group(1) if title_match else "[无标题]"
|
|
||||||
|
|
||||||
# 提取引用消息的发送者和内容
|
|
||||||
display_name_match = re.search(r'<displayname>(.*?)</displayname>', xml_content)
|
|
||||||
display_name = display_name_match.group(1) if display_name_match else "未知用户"
|
|
||||||
|
|
||||||
quoted_content_match = re.search(r'<refermsg>.*?<content>(.*?)</content>', xml_content, re.DOTALL)
|
|
||||||
quoted_content = quoted_content_match.group(1) if quoted_content_match else ""
|
|
||||||
|
|
||||||
# 解码HTML实体
|
|
||||||
try:
|
|
||||||
quoted_content = html.unescape(quoted_content)
|
|
||||||
except:
|
|
||||||
pass # 如果解码失败,使用原始内容
|
|
||||||
|
|
||||||
# 构建格式化的引用消息
|
|
||||||
if display_name and quoted_content:
|
|
||||||
formatted_message = f"{main_content}\n引用 {display_name}:{quoted_content}"
|
|
||||||
return formatted_message
|
|
||||||
|
|
||||||
return main_content
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 如果解析失败,尝试提取title标签内容
|
# 如果解析失败,尝试提取title标签内容
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user