mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 20:01:04 +00:00
[v4.7.0] (dev) 多平台支持,目前是 Onebot 和 Discord
This commit is contained in:
+6
-1
@@ -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): 增加友好提示,增强性处理,优化用户体验
|
||||
|
||||
* **🛠️ 健壮性增强**: 增加友好提示,增强性处理,优化用户体验
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -108,7 +108,9 @@ retro_futurism 模板效果图
|
||||
> [!CAUTION]
|
||||
> **必需条件**:
|
||||
> - 已配置 LLM 提供商(用于智能分析)
|
||||
> - QQ 平台适配器
|
||||
> - 平台适配器(OneBot, Discord, Telegram 等)
|
||||
> - **Discord 平台特别说明**:机器人需要在频道中拥有 **“读取消息历史记录” (Read Message History)** 权限,否则分析将失败。
|
||||
|
||||
|
||||
## 注意事项
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user