[v4.7.0] (dev) 多平台支持,目前是 Onebot 和 Discord

This commit is contained in:
SXP-Simon
2026-02-08 23:57:36 +08:00
parent 88093c4695
commit 50f601432f
5 changed files with 135 additions and 81 deletions
+6 -1
View File
@@ -1,9 +1,14 @@
# 更新日志 (CHANGELOG)
## [v4.7.0] - fix(AutoScheduler): 修复多平台环境下群列表获取异常,优化 Discord 权限说明
* **🛠️ 健壮性增强**: 优化 `AutoScheduler` 启动逻辑,强制进行 Bot 实例自动发现,确保冷启动时能正确获取群列表。
* **🔌 平台优化**: 修复多平台(如 Discord)下适配器识别不稳定的问题,优先使用 `BotManager` 缓存的适配器。
* **📕 文档更新**: 在 `README.md` 中补充了 Discord 平台必需的 “查看消息历史记录” (Read Message History) 权限说明。
## [v4.6.9] - fix(AutoScheduler): 增加友好提示,增强性处理,优化用户体验
* **🛠️ 健壮性增强**: 增加友好提示,增强性处理,优化用户体验
---
+3 -1
View File
@@ -108,7 +108,9 @@ retro_futurism 模板效果图
> [!CAUTION]
> **必需条件**
> - 已配置 LLM 提供商(用于智能分析)
> - QQ 平台适配器
> - 平台适配器OneBot, Discord, Telegram 等)
> - **Discord 平台特别说明**:机器人需要在频道中拥有 **“读取消息历史记录” (Read Message History)** 权限,否则分析将失败。
## 注意事项
+1 -1
View File
@@ -14,6 +14,6 @@ help: | # 插件的帮助信息
命令:
/群分析 [天数] - 分析群聊活动
/分析设置 [操作] - 管理设置(enable/disable/status/test
version: v4.6.9 # 插件版本号。格式:v1.1.1 或者 v1.1
version: v4.7.0 # 插件版本号。格式:v1.1.1 或者 v1.1
author: SXP-Simon # 作者
repo: https://github.com/SXP-Simon/astrbot_plugin_qq_group_daily_analysis # 插件的仓库地址
@@ -23,18 +23,24 @@ from ..base import PlatformAdapter
class OneBotAdapter(PlatformAdapter):
platform_name = "onebot"
"""OneBot v11 协议适配器"""
# QQ 头像 URL 模板
USER_AVATAR_TEMPLATE = "https://q1.qlogo.cn/g?b=qq&nk={user_id}&s={size}"
USER_AVATAR_HD_TEMPLATE = "https://q.qlogo.cn/headimg_dl?dst_uin={user_id}&spec={size}&img_type=jpg"
USER_AVATAR_HD_TEMPLATE = (
"https://q.qlogo.cn/headimg_dl?dst_uin={user_id}&spec={size}&img_type=jpg"
)
GROUP_AVATAR_TEMPLATE = "https://p.qlogo.cn/gh/{group_id}/{group_id}/{size}/"
AVAILABLE_SIZES = [40, 100, 140, 160, 640]
def __init__(self, bot_instance: Any, config: dict = None):
super().__init__(bot_instance, config)
self.bot_self_ids = [str(id) for id in config.get("bot_qq_ids", [])] if config else []
self.bot_self_ids = (
[str(id) for id in config.get("bot_qq_ids", [])] if config else []
)
def _init_capabilities(self) -> PlatformCapabilities:
return ONEBOT_V11_CAPABILITIES
@@ -90,7 +96,9 @@ class OneBotAdapter(PlatformAdapter):
except Exception:
return []
def _convert_message(self, raw_msg: dict, group_id: str) -> Optional[UnifiedMessage]:
def _convert_message(
self, raw_msg: dict, group_id: str
) -> Optional[UnifiedMessage]:
"""将 OneBot 消息转换为统一格式"""
try:
sender = raw_msg.get("sender", {})
@@ -109,56 +117,70 @@ class OneBotAdapter(PlatformAdapter):
if seg_type == "text":
text = seg_data.get("text", "")
text_parts.append(text)
contents.append(MessageContent(type=MessageContentType.TEXT, text=text))
contents.append(
MessageContent(type=MessageContentType.TEXT, text=text)
)
elif seg_type == "image":
contents.append(MessageContent(
type=MessageContentType.IMAGE,
url=seg_data.get("url", seg_data.get("file", ""))
))
contents.append(
MessageContent(
type=MessageContentType.IMAGE,
url=seg_data.get("url", seg_data.get("file", "")),
)
)
elif seg_type == "at":
contents.append(MessageContent(
type=MessageContentType.AT,
at_user_id=str(seg_data.get("qq", ""))
))
contents.append(
MessageContent(
type=MessageContentType.AT,
at_user_id=str(seg_data.get("qq", "")),
)
)
elif seg_type in ("face", "mface", "bface", "sface"):
contents.append(MessageContent(
type=MessageContentType.EMOJI,
emoji_id=str(seg_data.get("id", "")),
raw_data={"face_type": seg_type}
))
contents.append(
MessageContent(
type=MessageContentType.EMOJI,
emoji_id=str(seg_data.get("id", "")),
raw_data={"face_type": seg_type},
)
)
elif seg_type == "reply":
contents.append(MessageContent(
type=MessageContentType.REPLY,
raw_data={"reply_id": seg_data.get("id", "")}
))
contents.append(
MessageContent(
type=MessageContentType.REPLY,
raw_data={"reply_id": seg_data.get("id", "")},
)
)
elif seg_type == "forward":
contents.append(MessageContent(
type=MessageContentType.FORWARD,
raw_data=seg_data
))
contents.append(
MessageContent(
type=MessageContentType.FORWARD, raw_data=seg_data
)
)
elif seg_type == "record":
contents.append(MessageContent(
type=MessageContentType.VOICE,
url=seg_data.get("url", seg_data.get("file", ""))
))
contents.append(
MessageContent(
type=MessageContentType.VOICE,
url=seg_data.get("url", seg_data.get("file", "")),
)
)
elif seg_type == "video":
contents.append(MessageContent(
type=MessageContentType.VIDEO,
url=seg_data.get("url", seg_data.get("file", ""))
))
contents.append(
MessageContent(
type=MessageContentType.VIDEO,
url=seg_data.get("url", seg_data.get("file", "")),
)
)
else:
contents.append(MessageContent(
type=MessageContentType.UNKNOWN,
raw_data=seg
))
contents.append(
MessageContent(type=MessageContentType.UNKNOWN, raw_data=seg)
)
reply_to = None
for c in contents:
@@ -185,7 +207,7 @@ class OneBotAdapter(PlatformAdapter):
def convert_to_raw_format(self, messages: List[UnifiedMessage]) -> List[dict]:
"""
将统一消息格式转换为 OneBot 原生格式。
用于与现有分析器的向后兼容。
"""
raw_messages = []
@@ -194,26 +216,46 @@ class OneBotAdapter(PlatformAdapter):
message_chain = []
for content in msg.contents:
if content.type == MessageContentType.TEXT:
message_chain.append({"type": "text", "data": {"text": content.text or ""}})
message_chain.append(
{"type": "text", "data": {"text": content.text or ""}}
)
elif content.type == MessageContentType.IMAGE:
message_chain.append({"type": "image", "data": {"url": content.url or ""}})
message_chain.append(
{"type": "image", "data": {"url": content.url or ""}}
)
elif content.type == MessageContentType.AT:
message_chain.append({"type": "at", "data": {"qq": content.at_user_id or ""}})
message_chain.append(
{"type": "at", "data": {"qq": content.at_user_id or ""}}
)
elif content.type == MessageContentType.EMOJI:
face_type = content.raw_data.get("face_type", "face") if content.raw_data else "face"
message_chain.append({"type": face_type, "data": {"id": content.emoji_id or ""}})
face_type = (
content.raw_data.get("face_type", "face")
if content.raw_data
else "face"
)
message_chain.append(
{"type": face_type, "data": {"id": content.emoji_id or ""}}
)
elif content.type == MessageContentType.REPLY:
reply_id = content.raw_data.get("reply_id", "") if content.raw_data else ""
reply_id = (
content.raw_data.get("reply_id", "") if content.raw_data else ""
)
message_chain.append({"type": "reply", "data": {"id": reply_id}})
elif content.type == MessageContentType.FORWARD:
message_chain.append({"type": "forward", "data": content.raw_data or {}})
message_chain.append(
{"type": "forward", "data": content.raw_data or {}}
)
elif content.type == MessageContentType.VOICE:
message_chain.append({"type": "record", "data": {"url": content.url or ""}})
message_chain.append(
{"type": "record", "data": {"url": content.url or ""}}
)
elif content.type == MessageContentType.VIDEO:
message_chain.append({"type": "video", "data": {"url": content.url or ""}})
message_chain.append(
{"type": "video", "data": {"url": content.url or ""}}
)
elif content.type == MessageContentType.UNKNOWN and content.raw_data:
message_chain.append(content.raw_data)
raw_msg = {
"message_id": msg.message_id,
"time": msg.timestamp,
@@ -224,11 +266,11 @@ class OneBotAdapter(PlatformAdapter):
},
"message": message_chain,
"group_id": msg.group_id,
"raw_message": msg.text_content, # 添加 raw_message 兼容字段
"user_id": msg.sender_id, # 添加 user_id 兼容字段
"raw_message": msg.text_content, # 添加 raw_message 兼容字段
"user_id": msg.sender_id, # 添加 user_id 兼容字段
}
raw_messages.append(raw_msg)
return raw_messages
# ==================== IMessageSender ====================
@@ -344,13 +386,15 @@ class OneBotAdapter(PlatformAdapter):
members = []
for m in result or []:
members.append(UnifiedMember(
user_id=str(m.get("user_id", "")),
nickname=m.get("nickname", ""),
card=m.get("card", "") or None,
role=m.get("role", "member"),
join_time=m.get("join_time"),
))
members.append(
UnifiedMember(
user_id=str(m.get("user_id", "")),
nickname=m.get("nickname", ""),
card=m.get("card", "") or None,
role=m.get("role", "member"),
join_time=m.get("join_time"),
)
)
return members
except Exception:
return []
@@ -406,11 +450,13 @@ class OneBotAdapter(PlatformAdapter):
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp:
async with session.get(
url, timeout=aiohttp.ClientTimeout(total=5)
) as resp:
if resp.status == 200:
data = await resp.read()
b64 = base64.b64encode(data).decode('utf-8')
content_type = resp.headers.get('Content-Type', 'image/png')
b64 = base64.b64encode(data).decode("utf-8")
content_type = resp.headers.get("Content-Type", "image/png")
return f"data:{content_type};base64,{b64}"
except Exception:
pass
+17 -16
View File
@@ -8,6 +8,7 @@ import weakref
from apscheduler.triggers.cron import CronTrigger
from ..infrastructure.platform.factory import PlatformAdapterFactory
from ..utils.logger import logger
from ..core.message_sender import MessageSender
@@ -520,27 +521,21 @@ class AutoScheduler:
from ..infrastructure.platform.factory import PlatformAdapterFactory
# 强制刷新一次 Bot 实例,确保最新的 Bot 被发现
# 这对于 AutoScheduler 这种定时任务很重要,因为 BotManager 可能懒加载
if hasattr(self.bot_manager, "auto_discover_bot_instances"):
try:
# 注意:这里需要在 async 上下文中调用,且 auto_discover_bot_instances 是 async 的
# 但我们不想在这里阻塞太久或引发循环调用问题。
# 鉴于 _get_all_groups 是 async 的,直接 await 是安全的。
await self.bot_manager.auto_discover_bot_instances()
except Exception as e:
logger.warning(f"[AutoScheduler] Auto-discovery failed: {e}")
logger.info(
f"[AutoScheduler] Bot instances: {list(self.bot_manager._bot_instances.keys())}"
)
logger.info(
f"[AutoScheduler] Adapters: {list(self.bot_manager._adapters.keys())}"
bot_ids = list(self.bot_manager._bot_instances.keys())
adapter_ids = list(self.bot_manager._adapters.keys())
# 调试模式下记录详细信息,INFO级别仅显示概览
logger.debug(
f"[AutoScheduler] Bot instances: {bot_ids}, Adapters: {adapter_ids}"
)
if (
not hasattr(self.bot_manager, "_bot_instances")
or not self.bot_manager._bot_instances
):
if not bot_ids:
logger.warning("[AutoScheduler] No bot instances found after discovery.")
return []
@@ -576,13 +571,19 @@ class AutoScheduler:
for group_id in groups:
all_groups.add((platform_id, str(group_id)))
p_name = getattr(
adapter, "platform_name", platform_name or "unknown"
)
# 获取可读的平台名称
p_name = getattr(adapter, "platform_name", None)
if not p_name:
p_name = (
self.bot_manager._detect_platform_name(bot_instance)
or "unknown"
)
logger.info(
f"平台 {platform_id} ({p_name}) 成功获取 {len(groups)} 个群组"
)
continue
except Exception as e:
logger.warning(f"适配器 {platform_id} 获取群列表失败: {e}")