fix: 修复 Telegram 适配器历史消息与平台ID映射问题

This commit is contained in:
clown145
2026-02-12 15:41:07 +08:00
committed by Helian Nuits
parent b460a41cf8
commit 8a2b1ad448
5 changed files with 80 additions and 44 deletions
@@ -165,12 +165,14 @@ ONEBOT_V11_CAPABILITIES = PlatformCapabilities(
TELEGRAM_CAPABILITIES = PlatformCapabilities(
platform_name="telegram",
platform_version="bot_api_7.x",
supports_message_history=False,
max_message_history_days=0,
max_message_count=0,
# 通过 PlatformMessageHistoryManager + 消息拦截器支持历史读取
supports_message_history=True,
max_message_history_days=7,
max_message_count=1000,
supports_group_list=False,
supports_group_info=True,
supports_member_list=True,
supports_member_info=True,
supports_text_message=True,
supports_image_message=True,
supports_file_message=True,
@@ -5,11 +5,12 @@ Telegram 平台适配器
通过 AstrBot 的 message_history_manager 存储和读取消息历史。
"""
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from io import BytesIO
from typing import TYPE_CHECKING, Any
from ....domain.value_objects.platform_capabilities import (
TELEGRAM_CAPABILITIES,
PlatformCapabilities,
)
from ....domain.value_objects.unified_group import UnifiedGroup, UnifiedMember
@@ -62,6 +63,7 @@ class TelegramAdapter(PlatformAdapter):
if config:
ids = config.get("bot_self_ids", [])
self.bot_self_ids = [str(i) for i in ids] if ids else []
self._platform_id = str(config.get("platform_id", "")).strip() if config else ""
def set_context(self, context: "Context") -> None:
"""
@@ -115,28 +117,7 @@ class TelegramAdapter(PlatformAdapter):
def _init_capabilities(self) -> PlatformCapabilities:
"""返回 Telegram 平台能力声明"""
# 通过数据库存储支持消息历史
return PlatformCapabilities(
platform_name="telegram",
platform_version="bot_api_7.x",
supports_message_history=True, # 通过数据库支持
max_message_history_days=7, # 取决于存储时长
max_message_count=1000,
supports_group_list=False,
supports_group_info=True,
supports_member_list=True,
supports_text_message=True,
supports_image_message=True,
supports_file_message=True,
supports_reply_message=True,
max_text_length=4096,
max_image_size_mb=50.0,
supports_edit=True,
supports_user_avatar=True,
supports_group_avatar=True,
avatar_needs_api_call=True,
avatar_sizes=(160, 320, 640),
)
return TELEGRAM_CAPABILITIES
# ==================== IMessageRepository ====================
@@ -163,6 +144,12 @@ class TelegramAdapter(PlatformAdapter):
# 获取平台 ID(从 bot 实例获取)
platform_id = self._get_platform_id()
before_id_int: int | None = None
if before_id:
try:
before_id_int = int(before_id)
except (TypeError, ValueError):
logger.warning(f"[Telegram] before_id invalid: {before_id}")
# 获取消息历史
history_records = await history_mgr.get(
@@ -179,13 +166,26 @@ class TelegramAdapter(PlatformAdapter):
)
return []
# 时间过滤
cutoff_time = datetime.now() - timedelta(days=days)
# 时间过滤(数据库时间为 UTC aware
cutoff_time = datetime.now(timezone.utc) - timedelta(days=days)
messages = []
for record in history_records:
# before_id 过滤,仅保留更早的记录
if before_id_int is not None:
try:
if int(record.id) >= before_id_int:
continue
except (TypeError, ValueError):
pass
# 检查时间
if record.created_at < cutoff_time:
record_time = getattr(record, "created_at", None)
if not record_time:
continue
if record_time.tzinfo is None:
record_time = record_time.replace(tzinfo=timezone.utc)
if record_time < cutoff_time:
continue
# 转换为 UnifiedMessage
@@ -198,6 +198,7 @@ class TelegramAdapter(PlatformAdapter):
continue
messages.append(msg)
messages.sort(key=lambda m: m.timestamp)
logger.info(
f"[Telegram] 从数据库获取群 {group_id} 的消息: "
f"{len(messages)}/{len(history_records)}"
@@ -211,11 +212,22 @@ class TelegramAdapter(PlatformAdapter):
def _get_platform_id(self) -> str:
"""获取平台 ID"""
if self._platform_id:
return self._platform_id
if isinstance(self.config, dict):
config_platform_id = str(self.config.get("platform_id", "")).strip()
if config_platform_id:
return config_platform_id
# 尝试从 bot 实例获取
if hasattr(self.bot, "meta"):
meta = self.bot.meta()
if hasattr(meta, "id"):
return meta.id
if hasattr(self.bot, "meta") and callable(self.bot.meta):
try:
meta = self.bot.meta()
if hasattr(meta, "id"):
return meta.id
except Exception:
pass
return "telegram"
def _convert_history_record(
@@ -255,10 +267,15 @@ class TelegramAdapter(PlatformAdapter):
)
)
elif part_type == "at":
target_id = (
part.get("target_id", "")
or part.get("qq", "")
or part.get("at_user_id", "")
)
contents.append(
MessageContent(
type=MessageContentType.AT,
target_id=part.get("target_id", ""),
at_user_id=str(target_id),
)
)
@@ -320,7 +337,7 @@ class TelegramAdapter(PlatformAdapter):
)
elif content.type == MessageContentType.AT:
raw["message"].append(
{"type": "at", "data": {"qq": content.target_id or ""}}
{"type": "at", "data": {"qq": content.at_user_id or ""}}
)
result.append(raw)
@@ -371,6 +388,7 @@ class TelegramAdapter(PlatformAdapter):
try:
chat_id, message_thread_id = self._parse_group_id(group_id)
photo_obj: Any = None
kwargs: dict[str, Any] = {"chat_id": chat_id}
if message_thread_id:
@@ -397,18 +415,22 @@ class TelegramAdapter(PlatformAdapter):
except Exception as e:
logger.warning(f"[Telegram] 下载图片失败,尝试直接发送: {e}")
kwargs["photo"] = image_path
photo_obj = kwargs["photo"]
await client.send_photo(**kwargs)
else:
# 本地文件
kwargs["photo"] = open(image_path, "rb")
with open(image_path, "rb") as f:
kwargs["photo"] = f
await client.send_photo(**kwargs)
await client.send_photo(**kwargs)
# 关闭文件句柄
if not isinstance(kwargs["photo"], (str, BytesIO)):
kwargs["photo"].close()
if isinstance(photo_obj, BytesIO):
photo_obj.close()
return True
except Exception as e:
if "photo_obj" in locals() and isinstance(photo_obj, BytesIO):
photo_obj.close()
logger.error(f"[Telegram] 发送图片失败: {e}")
return False
@@ -55,6 +55,7 @@ class BotManager:
if platform_name and PlatformAdapterFactory.is_supported(platform_name):
adapter_config = {
"bot_self_ids": self._bot_self_ids.copy(),
"platform_id": str(platform_id),
}
adapter = PlatformAdapterFactory.create(
platform_name, bot_instance, adapter_config
+13 -3
View File
@@ -5,6 +5,7 @@
import asyncio
import base64
import re
from datetime import datetime
from pathlib import Path
@@ -450,6 +451,14 @@ class ReportGenerator(IReportGenerator):
return result
@staticmethod
def _safe_url_for_log(url: str | None) -> str:
"""对日志中的 URL 进行脱敏,避免泄露 token。"""
if not url:
return ""
# Telegram file URL: .../file/bot<token>/<file_path>
return re.sub(r"/bot[^/]+/", "/bot<redacted>/", url)
async def _get_user_avatar(self, user_id: str, avatar_getter=None) -> str:
"""
获取用户头像的 Base64 Data URI。
@@ -509,6 +518,7 @@ class ReportGenerator(IReportGenerator):
return self._get_default_avatar_base64()
# 5. 下载并保存
safe_avatar_url = self._safe_url_for_log(avatar_url)
async with aiohttp.ClientSession() as client:
try:
async with client.get(avatar_url, timeout=5) as response:
@@ -538,14 +548,14 @@ class ReportGenerator(IReportGenerator):
file_content = content
else:
logger.warning(
f"下载的头像数据格式无效 ({avatar_url})"
f"下载的头像数据格式无效 ({safe_avatar_url})"
)
else:
logger.warning(
f"下载头像失败 {avatar_url}: {response.status}"
f"下载头像失败 {safe_avatar_url}: {response.status}"
)
except Exception as e:
logger.warning(f"下载头像网络错误 {avatar_url}: {e}")
logger.warning(f"下载头像网络错误 {safe_avatar_url}: {e}")
# 6. 转换为 Base64 Data URI
if file_content:
@@ -823,6 +823,7 @@ class AutoScheduler:
bot_instance,
config={
"bot_self_ids": self.config_manager.get_bot_self_ids(),
"platform_id": str(platform_id),
},
)