mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
fix: 修复 Telegram 昵称解析与@消息入库
This commit is contained in:
@@ -188,12 +188,8 @@ class QQGroupDailyAnalysis(Star):
|
||||
raise ValueError(f"群 {group_id}: 无法获取发送者 ID,拒绝存储消息")
|
||||
sender_id = str(sender_id)
|
||||
|
||||
# 3. 获取发送者名称(必需)
|
||||
sender_name = event.get_sender_name()
|
||||
if not sender_name:
|
||||
raise ValueError(
|
||||
f"群 {group_id}: 无法获取发送者名称 (sender_id={sender_id}),拒绝存储消息"
|
||||
)
|
||||
# 3. 获取发送者名称(昵称优先,必要时回退)
|
||||
sender_name = self._resolve_sender_name(event, sender_id)
|
||||
|
||||
# 4. 获取平台 ID(必需)
|
||||
platform_id = event.get_platform_id()
|
||||
@@ -207,8 +203,55 @@ class QQGroupDailyAnalysis(Star):
|
||||
f"群 {group_id}: 消息内容为空 (sender={sender_name}),拒绝存储"
|
||||
)
|
||||
|
||||
# 6. 存储到数据库
|
||||
await self.context.message_history_manager.insert(
|
||||
# 6. 临时调试日志:打印入库前关键信息
|
||||
message_types = []
|
||||
for part in message_parts:
|
||||
if isinstance(part, dict):
|
||||
message_types.append(str(part.get("type", "unknown")))
|
||||
|
||||
preview_parts: list[str] = []
|
||||
for part in message_parts:
|
||||
if not isinstance(part, dict):
|
||||
continue
|
||||
|
||||
part_type = str(part.get("type", "unknown"))
|
||||
if part_type in ("plain", "text"):
|
||||
text = str(part.get("text", "")).strip()
|
||||
if text:
|
||||
preview_parts.append(text)
|
||||
elif part_type == "at":
|
||||
target = str(
|
||||
part.get("target_id")
|
||||
or part.get("qq")
|
||||
or part.get("at_user_id")
|
||||
or ""
|
||||
).strip()
|
||||
preview_parts.append(f"@{target}" if target else "@")
|
||||
elif part_type == "image":
|
||||
url = str(part.get("url", "")).strip()
|
||||
preview_parts.append(f"[image]{url}" if url else "[image]")
|
||||
else:
|
||||
preview_parts.append(f"[{part_type}]")
|
||||
|
||||
preview_text = " ".join(preview_parts).strip()
|
||||
if len(preview_text) > 300:
|
||||
preview_text = preview_text[:300] + "...(truncated)"
|
||||
|
||||
msg_obj = getattr(event, "message_obj", None)
|
||||
event_message_id = str(getattr(msg_obj, "message_id", "") or "")
|
||||
unified_msg_origin = str(getattr(event, "unified_msg_origin", "") or "")
|
||||
|
||||
logger.info(
|
||||
"[TEMP][HistoryStore][BeforeInsert] "
|
||||
f"platform_id={platform_id} group_id={group_id} "
|
||||
f"sender_id={sender_id} sender_name={sender_name} "
|
||||
f"event_message_id={event_message_id} unified_msg_origin={unified_msg_origin} "
|
||||
f"parts_count={len(message_parts)} part_types={message_types} "
|
||||
f"content_preview={preview_text}"
|
||||
)
|
||||
|
||||
# 7. 存储到数据库
|
||||
insert_result = await self.context.message_history_manager.insert(
|
||||
platform_id=platform_id,
|
||||
user_id=group_id,
|
||||
content={"type": "user", "message": message_parts},
|
||||
@@ -216,10 +259,103 @@ class QQGroupDailyAnalysis(Star):
|
||||
sender_name=sender_name,
|
||||
)
|
||||
|
||||
record_id = str(getattr(insert_result, "id", "") or "")
|
||||
created_at = getattr(insert_result, "created_at", None)
|
||||
logger.info(
|
||||
"[TEMP][HistoryStore][AfterInsert] "
|
||||
f"record_id={record_id} created_at={created_at} "
|
||||
f"platform_id={platform_id} group_id={group_id} "
|
||||
f"sender_id={sender_id} sender_name={sender_name} "
|
||||
f"parts_count={len(message_parts)}"
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"[{platform_id}] 已缓存群 {group_id} 的消息 (发送者: {sender_name})"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _is_placeholder_sender_name(name: str | None, sender_id: str) -> bool:
|
||||
"""判断 sender_name 是否为空或占位值。"""
|
||||
if not name:
|
||||
return True
|
||||
normalized = str(name).strip()
|
||||
if not normalized:
|
||||
return True
|
||||
if normalized.lower() in {"unknown", "none", "null", "nil", "undefined"}:
|
||||
return True
|
||||
return normalized == str(sender_id).strip()
|
||||
|
||||
def _resolve_sender_name(self, event: AstrMessageEvent, sender_id: str) -> str:
|
||||
"""
|
||||
解析发送者展示名。
|
||||
|
||||
优先级:
|
||||
- Telegram:
|
||||
1. raw_message.from_user.full_name
|
||||
2. raw_message.from_user.first_name
|
||||
3. event.get_sender_name() / message_obj.sender.nickname
|
||||
4. raw_message.from_user.username
|
||||
5. sender_id
|
||||
- 其他平台:
|
||||
1. event.get_sender_name()
|
||||
2. message_obj.sender.nickname
|
||||
3. raw_message.from_user.full_name / first_name / username
|
||||
4. sender_id(最终回退,避免消息丢失)
|
||||
"""
|
||||
platform_name = str(event.get_platform_name() or "").lower()
|
||||
candidates: list[str | None] = []
|
||||
|
||||
msg_obj = getattr(event, "message_obj", None)
|
||||
sender_obj = getattr(msg_obj, "sender", None)
|
||||
raw_message = getattr(msg_obj, "raw_message", None)
|
||||
raw_msg_obj = getattr(raw_message, "message", raw_message)
|
||||
from_user = getattr(raw_msg_obj, "from_user", None)
|
||||
|
||||
# Telegram 特殊策略:优先显示名,不优先 username
|
||||
if platform_name == "telegram":
|
||||
if from_user is not None:
|
||||
full_name = getattr(from_user, "full_name", None)
|
||||
first_name = getattr(from_user, "first_name", None)
|
||||
username = getattr(from_user, "username", None)
|
||||
logger.info(
|
||||
"[TEMP][SenderNameRaw] "
|
||||
f"sender_id={sender_id} full_name={full_name} "
|
||||
f"first_name={first_name} username={username} "
|
||||
f"event_sender_name={event.get_sender_name()}"
|
||||
)
|
||||
candidates.extend([full_name, first_name])
|
||||
|
||||
candidates.append(event.get_sender_name())
|
||||
if sender_obj is not None:
|
||||
candidates.append(getattr(sender_obj, "nickname", None))
|
||||
|
||||
if from_user is not None:
|
||||
candidates.append(getattr(from_user, "username", None))
|
||||
else:
|
||||
candidates.append(event.get_sender_name())
|
||||
if sender_obj is not None:
|
||||
candidates.append(getattr(sender_obj, "nickname", None))
|
||||
|
||||
if from_user is not None:
|
||||
candidates.extend(
|
||||
[
|
||||
getattr(from_user, "full_name", None),
|
||||
getattr(from_user, "first_name", None),
|
||||
getattr(from_user, "username", None),
|
||||
]
|
||||
)
|
||||
|
||||
for candidate in candidates:
|
||||
name = str(candidate or "").strip()
|
||||
if not self._is_placeholder_sender_name(name, sender_id):
|
||||
return name
|
||||
|
||||
logger.warning(
|
||||
f"[HistoryStore] 无法解析昵称,回退为 sender_id: {sender_id} "
|
||||
f"(platform={event.get_platform_id()})"
|
||||
)
|
||||
return sender_id
|
||||
|
||||
def _extract_message_parts(self, event: AstrMessageEvent) -> list[dict]:
|
||||
"""
|
||||
从事件中提取消息内容
|
||||
@@ -252,10 +388,18 @@ class QQGroupDailyAnalysis(Star):
|
||||
|
||||
elif seg_type in ("At", "at"):
|
||||
target = getattr(seg, "target", None)
|
||||
if target is None:
|
||||
target = getattr(seg, "qq", None)
|
||||
if target is None and hasattr(seg, "data"):
|
||||
target = seg.data.get("qq") or seg.data.get("target")
|
||||
if target:
|
||||
message_parts.append({"type": "at", "target_id": str(target)})
|
||||
message_parts.append(
|
||||
{
|
||||
"type": "at",
|
||||
"target_id": str(target),
|
||||
"name": str(getattr(seg, "name", "") or ""),
|
||||
}
|
||||
)
|
||||
|
||||
# 如果没有从消息链提取到内容,尝试使用 message_str
|
||||
if not message_parts and event.message_str:
|
||||
|
||||
@@ -5,6 +5,7 @@ Telegram 平台适配器
|
||||
通过 AstrBot 的 message_history_manager 存储和读取消息历史。
|
||||
"""
|
||||
|
||||
from dataclasses import replace
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from io import BytesIO
|
||||
from typing import TYPE_CHECKING, Any
|
||||
@@ -170,6 +171,16 @@ class TelegramAdapter(PlatformAdapter):
|
||||
cutoff_time = datetime.now(timezone.utc) - timedelta(days=days)
|
||||
|
||||
messages = []
|
||||
sender_name_cache: dict[str, str] = {}
|
||||
# 先用本批历史记录中已有的有效昵称预热缓存,减少额外 API 请求
|
||||
for record in history_records:
|
||||
sender_id = str(getattr(record, "sender_id", "") or "").strip()
|
||||
sender_name = str(getattr(record, "sender_name", "") or "").strip()
|
||||
if sender_id and not self._is_placeholder_sender_name(
|
||||
sender_name, sender_id
|
||||
):
|
||||
sender_name_cache[sender_id] = sender_name
|
||||
|
||||
for record in history_records:
|
||||
# before_id 过滤,仅保留更早的记录
|
||||
if before_id_int is not None:
|
||||
@@ -196,6 +207,9 @@ class TelegramAdapter(PlatformAdapter):
|
||||
continue
|
||||
if msg.sender_id in self.bot_self_ids:
|
||||
continue
|
||||
msg = await self._fix_sender_name_if_needed(
|
||||
group_id, msg, sender_name_cache
|
||||
)
|
||||
messages.append(msg)
|
||||
|
||||
messages.sort(key=lambda m: m.timestamp)
|
||||
@@ -230,6 +244,70 @@ class TelegramAdapter(PlatformAdapter):
|
||||
pass
|
||||
return "telegram"
|
||||
|
||||
@staticmethod
|
||||
def _is_placeholder_sender_name(name: str | None, sender_id: str | None) -> bool:
|
||||
"""判断 sender_name 是否属于占位值。"""
|
||||
if not name:
|
||||
return True
|
||||
normalized = str(name).strip()
|
||||
if not normalized:
|
||||
return True
|
||||
if normalized.lower() in {"unknown", "none", "null", "nil", "undefined"}:
|
||||
return True
|
||||
if sender_id and normalized == str(sender_id).strip():
|
||||
return True
|
||||
return False
|
||||
|
||||
async def _fix_sender_name_if_needed(
|
||||
self,
|
||||
group_id: str,
|
||||
msg: UnifiedMessage,
|
||||
sender_name_cache: dict[str, str],
|
||||
) -> UnifiedMessage:
|
||||
"""
|
||||
如果 sender_name 是占位值,尝试通过 get_member_info 修复。
|
||||
|
||||
说明:
|
||||
- 兼容历史脏数据(sender_name 写成 user_id / Unknown)
|
||||
- 使用 sender_id 级缓存,避免重复请求 Telegram API
|
||||
"""
|
||||
if not self._is_placeholder_sender_name(msg.sender_name, msg.sender_id):
|
||||
return msg
|
||||
|
||||
sender_id = str(msg.sender_id)
|
||||
if sender_id in sender_name_cache:
|
||||
cached_name = sender_name_cache[sender_id]
|
||||
if cached_name == msg.sender_name:
|
||||
return msg
|
||||
logger.info(
|
||||
"[TEMP][SenderNameFix][Cache] "
|
||||
f"group_id={group_id} sender_id={sender_id} "
|
||||
f"from={msg.sender_name} to={cached_name}"
|
||||
)
|
||||
return replace(msg, sender_name=cached_name)
|
||||
|
||||
resolved_name = msg.sender_name
|
||||
try:
|
||||
member = await self.get_member_info(group_id, sender_id)
|
||||
if member:
|
||||
candidate = str(member.nickname or "").strip()
|
||||
if self._is_placeholder_sender_name(candidate, sender_id):
|
||||
candidate = str(member.card or "").strip()
|
||||
if not self._is_placeholder_sender_name(candidate, sender_id):
|
||||
resolved_name = candidate
|
||||
except Exception as e:
|
||||
logger.debug(f"[Telegram] 修复 sender_name 失败 (uid={sender_id}): {e}")
|
||||
|
||||
sender_name_cache[sender_id] = resolved_name
|
||||
if resolved_name == msg.sender_name:
|
||||
return msg
|
||||
logger.info(
|
||||
"[TEMP][SenderNameFix][MemberInfo] "
|
||||
f"group_id={group_id} sender_id={sender_id} "
|
||||
f"from={msg.sender_name} to={resolved_name}"
|
||||
)
|
||||
return replace(msg, sender_name=resolved_name)
|
||||
|
||||
def _convert_history_record(
|
||||
self, record: Any, group_id: str
|
||||
) -> UnifiedMessage | None:
|
||||
@@ -287,10 +365,13 @@ class TelegramAdapter(PlatformAdapter):
|
||||
)
|
||||
)
|
||||
|
||||
sender_id = str(record.sender_id or "")
|
||||
sender_name = str(record.sender_name or "").strip() or "Unknown"
|
||||
|
||||
return UnifiedMessage(
|
||||
message_id=str(record.id),
|
||||
sender_id=record.sender_id or "",
|
||||
sender_name=record.sender_name or "Unknown",
|
||||
sender_id=sender_id,
|
||||
sender_name=sender_name,
|
||||
sender_card=None,
|
||||
group_id=group_id,
|
||||
text_content=text_content,
|
||||
@@ -561,7 +642,10 @@ class TelegramAdapter(PlatformAdapter):
|
||||
members.append(
|
||||
UnifiedMember(
|
||||
user_id=str(user.id),
|
||||
nickname=user.first_name or user.username or "Unknown",
|
||||
nickname=user.full_name
|
||||
or user.first_name
|
||||
or user.username
|
||||
or "Unknown",
|
||||
card=user.username,
|
||||
role="admin" if admin.status == "administrator" else "owner",
|
||||
)
|
||||
@@ -594,7 +678,10 @@ class TelegramAdapter(PlatformAdapter):
|
||||
|
||||
return UnifiedMember(
|
||||
user_id=str(user.id),
|
||||
nickname=user.first_name or user.username or "Unknown",
|
||||
nickname=user.full_name
|
||||
or user.first_name
|
||||
or user.username
|
||||
or "Unknown",
|
||||
card=user.username,
|
||||
role=role,
|
||||
)
|
||||
|
||||
@@ -380,11 +380,15 @@ class ReportGenerator(IReportGenerator):
|
||||
if user_analysis and uid in user_analysis:
|
||||
stats = user_analysis[uid]
|
||||
name = stats.get("nickname") or stats.get("name")
|
||||
if self._is_placeholder_display_name(name, uid):
|
||||
name = None
|
||||
|
||||
# 2. 尝试通过回调获取实时昵称
|
||||
if not name and nickname_getter:
|
||||
try:
|
||||
name = await nickname_getter(uid)
|
||||
if self._is_placeholder_display_name(name, uid):
|
||||
name = None
|
||||
except Exception as e:
|
||||
logger.warning(f"获取昵称失败 {uid}: {e}")
|
||||
|
||||
@@ -400,7 +404,7 @@ class ReportGenerator(IReportGenerator):
|
||||
# 3. 最终后备: 确保有头像和名称
|
||||
if not url:
|
||||
url = self._get_default_avatar_base64()
|
||||
if not name:
|
||||
if self._is_placeholder_display_name(name, uid):
|
||||
name = str(uid)
|
||||
|
||||
return (
|
||||
@@ -427,6 +431,18 @@ class ReportGenerator(IReportGenerator):
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _is_placeholder_display_name(name: str | None, user_id: str) -> bool:
|
||||
"""判断展示名称是否为占位值。"""
|
||||
if not name:
|
||||
return True
|
||||
normalized = str(name).strip()
|
||||
if not normalized:
|
||||
return True
|
||||
if normalized.lower() in {"unknown", "none", "null", "nil", "undefined"}:
|
||||
return True
|
||||
return normalized == str(user_id).strip()
|
||||
|
||||
def _render_html_template(self, template: str, data: dict) -> str:
|
||||
"""HTML模板渲染,使用 {{key}} 占位符格式
|
||||
|
||||
|
||||
Reference in New Issue
Block a user