fix: 修复 Telegram 昵称解析与@消息入库

This commit is contained in:
clown145
2026-02-12 15:41:07 +08:00
committed by Helian Nuits
parent 8a2b1ad448
commit ce65350d60
3 changed files with 261 additions and 14 deletions
+153 -9
View File
@@ -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,
)
+17 -1
View File
@@ -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}} 占位符格式