mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
feat: 使用 Adapter 进行规范抽象,优先使用 Adapter 接口,方便后期扩展
This commit is contained in:
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user