feat: 使用 Adapter 进行规范抽象,优先使用 Adapter 接口,方便后期扩展

This commit is contained in:
SXP-Simon
2026-02-08 23:18:08 +08:00
parent bf09eab53d
commit b8ea7adf5a
5 changed files with 210 additions and 61 deletions
+1 -1
View File
@@ -174,7 +174,7 @@ class QQGroupDailyAnalysis(Star):
f" - 平台 {platform_id}: {type(bot_instance).__name__}"
)
# 预先创建编排器
self._get_orchestrator(platform_id, bot_instance)
self._get_orchestrator(platform_id, bot_instance=bot_instance)
# 启动调度器
self.auto_scheduler.schedule_jobs(self.context)
+21 -7
View File
@@ -117,11 +117,15 @@ class BotManager:
continue
bot_client = None
# 优先尝试 get_client()
if hasattr(platform, "get_client"):
bot_client = platform.get_client()
elif hasattr(platform, "bot"):
# 如果 get_client() 返回 None,尝试直接访问属性
if not bot_client and hasattr(platform, "bot"):
bot_client = platform.bot
elif hasattr(platform, "client"):
if not bot_client and hasattr(platform, "client"):
# AstrBot v4.14.4 DiscordPlatformAdapter uses 'client' attribute
bot_client = platform.client
if bot_client:
@@ -264,18 +268,16 @@ class BotManager:
logger.info(
f"auto_discover_bot_instances: 在管理器中发现 {len(platforms)} 个平台。"
)
for p in platforms:
p_id = p.metadata.id if hasattr(p, "metadata") else "unknown"
logger.info(f" - 正在检查平台: {p_id}, 类型: {type(p).__name__}")
for platform in platforms:
# 获取bot实例
bot_client = None
if hasattr(platform, "get_client"):
bot_client = platform.get_client()
elif hasattr(platform, "bot"):
if not bot_client and hasattr(platform, "bot"):
bot_client = platform.bot
elif hasattr(platform, "client"):
if not bot_client and hasattr(platform, "client"):
bot_client = platform.client
# 健壮地获取元数据
@@ -295,6 +297,11 @@ class BotManager:
platform_id = metadata.get("id")
if platform_id:
# KNOWLEDGE DISCOVERY: Log metadata for debugging custom IDs
logger.info(
f"[群分析插件 BotManager]: Log metadata for debugging custom IDs ,Platform: {platform_id}, Metadata Type: {getattr(metadata, 'type', 'N/A')}, Metadata Name: {getattr(metadata, 'name', 'N/A')}"
)
# 从元数据检测平台名称
platform_name = None
# 优先使用 type
@@ -307,6 +314,10 @@ class BotManager:
elif isinstance(metadata, dict) and "name" in metadata:
platform_name = metadata["name"]
logger.info(
f"[群分析插件 BotManager] Initial platform_name detection: {platform_name}"
)
# 验证此平台名称是否受支持,如果不支持,尝试从bot实例检测(如果可用)
if (
not platform_name
@@ -431,6 +442,9 @@ class BotManager:
return str(bot_instance.qq)
elif hasattr(bot_instance, "user_id") and bot_instance.user_id:
return str(bot_instance.user_id)
# Discord.py style: client.user.id
elif hasattr(bot_instance, "user") and hasattr(bot_instance.user, "id"):
return str(bot_instance.user.id)
return None
def _extract_bot_qq_id(self, bot_instance):
+150 -50
View File
@@ -32,14 +32,24 @@ class MessageSender:
logger.error(f"[{trace_id}] No available platforms for group {group_id}")
return False
for pid, bot in platforms:
for pid, adapter in platforms:
try:
logger.info(f"[{trace_id}] Trying platform {pid}...")
await bot.api.call_action(
"send_group_msg", group_id=group_id, message=text
)
logger.info(f"[{trace_id}] Successfully sent text via {pid}")
return True
# 优先使用 Adapter 接口
if hasattr(adapter, "send_text"):
if await adapter.send_text(group_id, text):
logger.info(f"[{trace_id}] Successfully sent text via {pid}")
return True
# Fallback to OneBot API (for backward compatibility or if adapter wrapping failed)
if hasattr(adapter, "api") and hasattr(adapter.api, "call_action"):
await adapter.api.call_action(
"send_group_msg", group_id=group_id, message=text
)
logger.info(f"[{trace_id}] Successfully sent text via {pid} (API)")
return True
except Exception as e:
self._log_send_error(pid, group_id, "text", e)
continue
@@ -62,19 +72,36 @@ class MessageSender:
if not platforms:
return False
message_chain = []
if text_prefix:
message_chain.append({"type": "text", "data": {"text": text_prefix}})
message_chain.append({"type": "image", "data": {"url": image_url}})
for pid, bot in platforms:
for pid, adapter in platforms:
try:
logger.info(f"[{trace_id}] Trying sending image (URL) via {pid}...")
await bot.api.call_action(
"send_group_msg", group_id=group_id, message=message_chain
)
logger.info(f"[{trace_id}] Successfully sent image (URL) via {pid}")
return True
# 优先使用 Adapter 接口
if hasattr(adapter, "send_image"):
if await adapter.send_image(
group_id, image_url, caption=text_prefix
):
logger.info(
f"[{trace_id}] Successfully sent image (URL) via {pid}"
)
return True
# Fallback to OneBot API
if hasattr(adapter, "api") and hasattr(adapter.api, "call_action"):
message_chain = []
if text_prefix:
message_chain.append(
{"type": "text", "data": {"text": text_prefix}}
)
message_chain.append({"type": "image", "data": {"url": image_url}})
await adapter.api.call_action(
"send_group_msg", group_id=group_id, message=message_chain
)
logger.info(
f"[{trace_id}] Successfully sent image (URL) via {pid} (API)"
)
return True
except Exception as e:
self._log_send_error(pid, group_id, "image_url", e)
continue
@@ -99,26 +126,51 @@ class MessageSender:
return False
image_b64 = base64.b64encode(image_bytes).decode()
# file URI for Base64 (OneBot style)
base64_uri = f"base64://{image_b64}"
platforms = self._get_available_platforms(group_id, platform_id)
if not platforms:
return False
message_chain = []
if text_prefix:
message_chain.append({"type": "text", "data": {"text": text_prefix}})
message_chain.append(
{"type": "image", "data": {"file": f"base64://{image_b64}"}}
)
for pid, bot in platforms:
for pid, adapter in platforms:
try:
logger.info(f"[{trace_id}] Trying sending image (Base64) via {pid}...")
await bot.api.call_action(
"send_group_msg", group_id=group_id, message=message_chain
)
logger.info(f"[{trace_id}] Successfully sent image (Base64) via {pid}")
return True
# 优先使用 Adapter 接口 (注意 Adapter 接口通常接受 path/url,这里我们传 base64 uri 它是支持的吗?)
# 大多数 Adapter 的 send_image 如果识别 base64:// 应该能处理
# 如果是 DiscordAdapter, 它需要特殊处理 local file.
# 但这里是 Base64 string.
# 为了稳妥,我们可以先尝试 Adapter,如果 Adapter 明确支持 base64://
if hasattr(adapter, "send_image"):
# 尝试发送 base64 URI
if await adapter.send_image(
group_id, base64_uri, caption=text_prefix
):
logger.info(
f"[{trace_id}] Successfully sent image (Base64) via {pid}"
)
return True
# Fallback to OneBot API
if hasattr(adapter, "api") and hasattr(adapter.api, "call_action"):
message_chain = []
if text_prefix:
message_chain.append(
{"type": "text", "data": {"text": text_prefix}}
)
message_chain.append(
{"type": "image", "data": {"file": base64_uri}}
)
await adapter.api.call_action(
"send_group_msg", group_id=group_id, message=message_chain
)
logger.info(
f"[{trace_id}] Successfully sent image (Base64) via {pid} (API)"
)
return True
except Exception as e:
self._log_send_error(pid, group_id, "image_base64", e)
continue
@@ -159,19 +211,30 @@ class MessageSender:
if not platforms:
return False
message_chain = []
if text_prefix:
message_chain.append({"type": "text", "data": {"text": text_prefix}})
message_chain.append({"type": "file", "data": {"file": pdf_path}})
for pid, bot in platforms:
for pid, adapter in platforms:
try:
logger.info(f"[{trace_id}] Trying sending PDF via {pid}...")
await bot.api.call_action(
"send_group_msg", group_id=group_id, message=message_chain
)
logger.info(f"[{trace_id}] Successfully sent PDF via {pid}")
return True
if hasattr(adapter, "send_file"):
if await adapter.send_file(group_id, pdf_path):
logger.info(f"[{trace_id}] Successfully sent PDF via {pid}")
return True
# Fallback to OneBot API
if hasattr(adapter, "api") and hasattr(adapter.api, "call_action"):
message_chain = []
if text_prefix:
message_chain.append(
{"type": "text", "data": {"text": text_prefix}}
)
message_chain.append({"type": "file", "data": {"file": pdf_path}})
await adapter.api.call_action(
"send_group_msg", group_id=group_id, message=message_chain
)
logger.info(f"[{trace_id}] Successfully sent PDF via {pid} (API)")
return True
except Exception as e:
self._log_send_error(pid, group_id, "pdf", e)
continue
@@ -181,21 +244,58 @@ class MessageSender:
self, group_id: str, specific_platform_id: str | None = None
) -> list[tuple]:
"""
获取可用的发送平台列表
获取可用的发送平台列表 (返回 Adapter 实例)
"""
from ..infrastructure.platform.factory import PlatformAdapterFactory
from ..infrastructure.platform.base import PlatformAdapter
instances = []
if specific_platform_id:
bot = self.bot_manager.get_bot_instance(specific_platform_id)
if bot:
return [(specific_platform_id, bot)]
logger.warning(f"Specified platform {specific_platform_id} not found")
instances.append((specific_platform_id, bot))
else:
logger.warning(f"Specified platform {specific_platform_id} not found")
else:
# 获取所有已发现的平台
all_instances = self.bot_manager.get_all_bot_instances()
if all_instances:
instances = list(all_instances.items())
# 获取所有已发现的平台
all_instances = self.bot_manager.get_all_bot_instances()
if all_instances:
# 这里可以加入逻辑判断哪些平台在该群中,目前简单返回所有
return list(all_instances.items())
# Wrap instances with Adapters if needed
adapters = []
for pid, bot in instances:
# Check if it's already an adapter
if isinstance(bot, PlatformAdapter):
adapters.append((pid, bot))
continue
return []
# If not, try to create an adapter
# We need to detect platform name first
platform_name = self.bot_manager._detect_platform_name(bot)
if not platform_name:
# If cannot detect, assume it's a OneBot raw object if it has api
if hasattr(bot, "api"):
adapters.append((pid, bot)) # Return raw bot for backward compat
continue
# Create adapter
try:
# We need config for adapter, here we use empty config or try to fetch from somewhere
# Ideally config_manager should provide it but it's complex.
# Passing empty config is fine for basic sending tasks as long as bot instance is valid.
adapter = PlatformAdapterFactory.create(platform_name, bot, config={})
if adapter:
adapters.append((pid, adapter))
else:
# Fallback: return raw bot
adapters.append((pid, bot))
except Exception as e:
logger.warning(f"Failed to create adapter for {pid}: {e}")
adapters.append((pid, bot))
return adapters
async def _download_image(self, url: str) -> bytes | None:
"""下载图片 helper"""
@@ -625,11 +625,20 @@ class DiscordAdapter(PlatformAdapter):
) -> Optional[str]:
"""获取 Discord 用户头像 URL"""
if not discord:
logger.warning("[群分析插件 DiscordAdapter] py-cord 未安装")
return None
try:
logger.debug(f"[群分析插件 DiscordAdapter] 正在获取用户头像 {user_id}")
if not self._discord_client:
logger.warning("[群分析插件 DiscordAdapter] Discord 客户端未准备就绪")
return None
user = self._discord_client.get_user(int(user_id))
if not user:
logger.debug(
f"[群分析插件 DiscordAdapter] 用户 {user_id} 不在缓存中,正在获取..."
)
user = await self._discord_client.fetch_user(int(user_id))
if user:
@@ -637,10 +646,18 @@ class DiscordAdapter(PlatformAdapter):
allowed_sizes = [16, 32, 64, 128, 256, 512, 1024, 2048, 4096]
target_size = min(allowed_sizes, key=lambda x: abs(x - size))
# display_avatar 自动处理默认头像
return user.display_avatar.with_size(target_size).url
url = user.display_avatar.with_size(target_size).url
logger.debug(
f"[群分析插件 DiscordAdapter] 获取用户头像 {user_id} 成功: {url}"
)
return url
logger.warning(f"[群分析插件 DiscordAdapter] 用户 {user_id} 未找到")
return None
except Exception:
except Exception as e:
logger.error(
f"[群分析插件 DiscordAdapter] 获取用户头像 {user_id} 失败: {e}"
)
return None
async def get_user_avatar_data(
+18
View File
@@ -2,6 +2,7 @@ import asyncio
import base64
import random
import time
import aiohttp
from collections.abc import Callable
from dataclasses import dataclass
@@ -158,6 +159,23 @@ class RetryManager:
image_options,
)
# Fix: html_render might return URL (str) even if return_url=False in some implementations
if isinstance(image_data, str) and image_data.startswith(
("http://", "https://")
):
logger.warning(
f"[RetryManager] html_render 返回了 URL 而不是 bytes,尝试下载: {image_data}"
)
async with aiohttp.ClientSession() as session:
async with session.get(image_data) as resp:
if resp.status == 200:
image_data = await resp.read()
else:
logger.error(
f"[RetryManager] 下载重试图片失败: {resp.status}"
)
image_data = None
if not image_data:
logger.warning(
f"[RetryManager] 重新渲染失败(返回空数据){task.group_id}"