mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
fix: 分析器和消息处理
This commit is contained in:
@@ -109,8 +109,6 @@ class QQGroupDailyAnalysis(Star):
|
|||||||
logger.info(
|
logger.info(
|
||||||
f" - 平台 {platform_id}: {type(bot_instance).__name__}"
|
f" - 平台 {platform_id}: {type(bot_instance).__name__}"
|
||||||
)
|
)
|
||||||
# 预先创建编排器
|
|
||||||
self._get_orchestrator(platform_id, bot_instance=bot_instance)
|
|
||||||
|
|
||||||
# 启动调度器
|
# 启动调度器
|
||||||
self.auto_scheduler.schedule_jobs(self.context)
|
self.auto_scheduler.schedule_jobs(self.context)
|
||||||
@@ -142,10 +140,8 @@ class QQGroupDailyAnalysis(Star):
|
|||||||
# 重置实例属性
|
# 重置实例属性
|
||||||
self.auto_scheduler = None
|
self.auto_scheduler = None
|
||||||
self.bot_manager = None
|
self.bot_manager = None
|
||||||
self.message_analyzer = None
|
|
||||||
self.report_generator = None
|
self.report_generator = None
|
||||||
self.config_manager = None
|
self.config_manager = None
|
||||||
self.orchestrators = {}
|
|
||||||
|
|
||||||
logger.info("QQ群日常分析插件资源清理完成")
|
logger.info("QQ群日常分析插件资源清理完成")
|
||||||
|
|
||||||
@@ -598,3 +594,25 @@ class QQGroupDailyAnalysis(Star):
|
|||||||
💡 可用命令: enable, disable, status, reload, test
|
💡 可用命令: enable, disable, status, reload, test
|
||||||
💡 支持的输出格式: image, text, pdf (图片和PDF包含活跃度可视化)
|
💡 支持的输出格式: image, text, pdf (图片和PDF包含活跃度可视化)
|
||||||
💡 其他命令: /设置格式, /安装PDF""")
|
💡 其他命令: /设置格式, /安装PDF""")
|
||||||
|
|
||||||
|
def _get_group_id_from_event(self, event: AstrMessageEvent) -> str | None:
|
||||||
|
"""从消息事件中安全获取群组 ID"""
|
||||||
|
try:
|
||||||
|
group_id = event.get_group_id()
|
||||||
|
return group_id if group_id else None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_platform_id_from_event(self, event: AstrMessageEvent) -> str:
|
||||||
|
"""从消息事件中获取平台唯一 ID"""
|
||||||
|
try:
|
||||||
|
return event.get_platform_id()
|
||||||
|
except Exception:
|
||||||
|
# 后备方案:从元数据获取
|
||||||
|
if (
|
||||||
|
hasattr(event, "platform_meta")
|
||||||
|
and event.platform_meta
|
||||||
|
and hasattr(event.platform_meta, "id")
|
||||||
|
):
|
||||||
|
return event.platform_meta.id
|
||||||
|
return "default"
|
||||||
|
|||||||
@@ -63,25 +63,34 @@ class AnalysisApplicationService:
|
|||||||
days = self.config_manager.get_analysis_days()
|
days = self.config_manager.get_analysis_days()
|
||||||
max_count = self.config_manager.get_max_messages()
|
max_count = self.config_manager.get_max_messages()
|
||||||
|
|
||||||
unified_messages = await adapter.fetch_messages(
|
raw_messages = await adapter.fetch_messages(
|
||||||
group_id=group_id, days=days, max_count=max_count
|
group_id=group_id, days=days, max_count=max_count
|
||||||
)
|
)
|
||||||
|
|
||||||
if not unified_messages:
|
if not raw_messages:
|
||||||
logger.warning(f"群 {group_id} 在最近 {days} 天内无消息或无法获取")
|
logger.warning(f"群 {group_id} 在最近 {days} 天内无消息或无法获取")
|
||||||
return {"success": False, "reason": "no_messages"}
|
return {"success": False, "reason": "no_messages"}
|
||||||
|
|
||||||
# 检查最小消息阈值
|
# 3. 清理消息 (Filter commands, bot messages, noise)
|
||||||
if (
|
from ...domain.services.message_cleaner_service import MessageCleanerService
|
||||||
len(unified_messages) < self.config_manager.get_min_messages_threshold()
|
|
||||||
and not manual
|
cleaner = MessageCleanerService()
|
||||||
):
|
bot_self_ids = self.config_manager.get_bot_self_ids()
|
||||||
|
|
||||||
|
# 对于自动任务,强制过滤指令;对于手动任务,也建议过滤以保持报告纯净
|
||||||
|
unified_messages = cleaner.clean_messages(
|
||||||
|
raw_messages, bot_self_ids=bot_self_ids, filter_commands=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. 检查最小消息阈值 (在清理后进行)
|
||||||
|
threshold = self.config_manager.get_min_messages_threshold()
|
||||||
|
if len(unified_messages) < threshold and not manual:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"群 {group_id} 消息数 ({len(unified_messages)}) 未达到自动分析阈值"
|
f"群 {group_id} 有效消息数 ({len(unified_messages)}) 未达到自动分析阈值 ({threshold})"
|
||||||
)
|
)
|
||||||
return {"success": False, "reason": "below_threshold"}
|
return {"success": False, "reason": "below_threshold"}
|
||||||
|
|
||||||
# 3. 基础统计 (Domain Service)
|
# 5. 基础统计 (Domain Service)
|
||||||
statistics = await asyncio.to_thread(
|
statistics = await asyncio.to_thread(
|
||||||
self.statistics_service.calculate_group_statistics, unified_messages
|
self.statistics_service.calculate_group_statistics, unified_messages
|
||||||
)
|
)
|
||||||
@@ -120,7 +129,7 @@ class AnalysisApplicationService:
|
|||||||
f"{platform_id}:GroupMessage:{group_id}" if platform_id else group_id
|
f"{platform_id}:GroupMessage:{group_id}" if platform_id else group_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if topic_enabled and user_title_enabled and golden_quote_enabled:
|
if topic_enabled or user_title_enabled or golden_quote_enabled:
|
||||||
(
|
(
|
||||||
topics,
|
topics,
|
||||||
user_titles,
|
user_titles,
|
||||||
@@ -131,10 +140,10 @@ class AnalysisApplicationService:
|
|||||||
user_activity,
|
user_activity,
|
||||||
umo=unified_msg_origin,
|
umo=unified_msg_origin,
|
||||||
top_users=top_users,
|
top_users=top_users,
|
||||||
|
topic_enabled=topic_enabled,
|
||||||
|
user_title_enabled=user_title_enabled,
|
||||||
|
golden_quote_enabled=golden_quote_enabled,
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
# 按需串行执行 (略,实际实现可补全或合并)
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 回填结果
|
# 回填结果
|
||||||
statistics.golden_quotes = golden_quotes
|
statistics.golden_quotes = golden_quotes
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
"""
|
||||||
|
消息清理服务 - 领域层
|
||||||
|
负责过滤掉机器人消息、指令、技术性内容(如原始表情代码)及敏感内容。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from dataclasses import replace
|
||||||
|
|
||||||
|
from ..value_objects.unified_message import (
|
||||||
|
MessageContent,
|
||||||
|
MessageContentType,
|
||||||
|
UnifiedMessage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MessageCleanerService:
|
||||||
|
"""消息清理服务"""
|
||||||
|
|
||||||
|
# Discord 自定义表情正则 <:name:id> 或 <a:name:id>
|
||||||
|
DISCORD_CUSTOM_EMOJI_PATTERN = re.compile(r"<a?:.+?:\d+>")
|
||||||
|
|
||||||
|
# 指令匹配正则:匹配以 / 开头,或者以 @某人 / 开头的消息
|
||||||
|
# 比如: "/group_analysis", "@bot /help", " /test"
|
||||||
|
COMMAND_PATTERN = re.compile(r"^\s*(?:<@\d+>\s+)?/")
|
||||||
|
|
||||||
|
def clean_messages(
|
||||||
|
self,
|
||||||
|
messages: list[UnifiedMessage],
|
||||||
|
bot_self_ids: list[str] = None,
|
||||||
|
filter_commands: bool = True,
|
||||||
|
) -> list[UnifiedMessage]:
|
||||||
|
"""
|
||||||
|
清理并过滤消息列表。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages: 原始统一格式消息列表
|
||||||
|
bot_self_ids: 机器人自身的 ID 列表
|
||||||
|
filter_commands: 是否过滤指令消息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
清理后的消息列表
|
||||||
|
"""
|
||||||
|
bot_ids = set(bot_self_ids or [])
|
||||||
|
cleaned_list = []
|
||||||
|
|
||||||
|
for msg in messages:
|
||||||
|
# 1. 过滤机器人发送的消息
|
||||||
|
if msg.sender_id in bot_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 2. 预检指令消息(首个内容块通常是文本)
|
||||||
|
is_command = False
|
||||||
|
first_text = msg.text_content
|
||||||
|
if (
|
||||||
|
filter_commands
|
||||||
|
and first_text
|
||||||
|
and self.COMMAND_PATTERN.match(first_text)
|
||||||
|
):
|
||||||
|
is_command = True
|
||||||
|
|
||||||
|
if is_command:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 3. 清理消息内容中的技术性噪音
|
||||||
|
cleaned_contents = []
|
||||||
|
has_meaningful_content = False
|
||||||
|
|
||||||
|
for content in msg.contents:
|
||||||
|
if content.type == MessageContentType.TEXT:
|
||||||
|
text = content.text or ""
|
||||||
|
|
||||||
|
# 移除 Discord 原始表情代码
|
||||||
|
text = self.DISCORD_CUSTOM_EMOJI_PATTERN.sub("", text)
|
||||||
|
|
||||||
|
# 移除 @mentions 文本 (e.g. <@123456>)
|
||||||
|
text = re.sub(r"<@\d+>", "", text)
|
||||||
|
|
||||||
|
# 清理多余空格
|
||||||
|
text = text.strip()
|
||||||
|
|
||||||
|
if text:
|
||||||
|
cleaned_contents.append(
|
||||||
|
MessageContent(type=MessageContentType.TEXT, text=text)
|
||||||
|
)
|
||||||
|
has_meaningful_content = True
|
||||||
|
else:
|
||||||
|
# 其他类型(图片、回复等)暂时保留,但由后续分析器决定是否使用
|
||||||
|
cleaned_contents.append(content)
|
||||||
|
if content.type != MessageContentType.REPLY:
|
||||||
|
has_meaningful_content = True
|
||||||
|
|
||||||
|
# 4. 如果清理后仍有内容,则保留消息
|
||||||
|
if has_meaningful_content:
|
||||||
|
# 重新合成 text_content 用于 LLM 分析
|
||||||
|
new_text_content = "".join(
|
||||||
|
[
|
||||||
|
c.text
|
||||||
|
for c in cleaned_contents
|
||||||
|
if c.type == MessageContentType.TEXT
|
||||||
|
]
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
# 使用 replace 创建新实例(Frozen dataclass 必须如此)
|
||||||
|
new_msg = replace(
|
||||||
|
msg, contents=tuple(cleaned_contents), text_content=new_text_content
|
||||||
|
)
|
||||||
|
cleaned_list.append(new_msg)
|
||||||
|
|
||||||
|
return cleaned_list
|
||||||
@@ -97,7 +97,11 @@ class StatisticsService:
|
|||||||
legacy_list.append(
|
legacy_list.append(
|
||||||
{
|
{
|
||||||
"time": msg.timestamp,
|
"time": msg.timestamp,
|
||||||
"sender": {"user_id": msg.sender_id},
|
"sender": {
|
||||||
|
"user_id": msg.sender_id,
|
||||||
|
"nickname": msg.sender_name,
|
||||||
|
"card": msg.sender_card or "",
|
||||||
|
},
|
||||||
"message": [
|
"message": [
|
||||||
{"type": "text", "data": {"text": msg.text_content or ""}}
|
{"type": "text", "data": {"text": msg.text_content or ""}}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -170,18 +170,18 @@ class GoldenQuoteAnalyzer(BaseAnalyzer):
|
|||||||
|
|
||||||
def extract_interesting_messages(self, messages: list[dict]) -> list[dict]:
|
def extract_interesting_messages(self, messages: list[dict]) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
提取圣经的文本消息
|
根据清理后的消息提取可能有意义的消息片段用于金句分析。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
messages: 群聊消息列表
|
messages: 已由 MessageCleaner 处理过的 legacy 消息列表
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
圣经的文本消息列表
|
提取的文本消息列表
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
interesting_messages = []
|
interesting_messages = []
|
||||||
|
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
|
# 获取发送者显示名
|
||||||
sender = msg.get("sender", {})
|
sender = msg.get("sender", {})
|
||||||
nickname = InfoUtils.get_user_nickname(self.config_manager, sender)
|
nickname = InfoUtils.get_user_nickname(self.config_manager, sender)
|
||||||
msg_time = datetime.fromtimestamp(msg.get("time", 0)).strftime("%H:%M")
|
msg_time = datetime.fromtimestamp(msg.get("time", 0)).strftime("%H:%M")
|
||||||
@@ -189,10 +189,8 @@ class GoldenQuoteAnalyzer(BaseAnalyzer):
|
|||||||
for content in msg.get("message", []):
|
for content in msg.get("message", []):
|
||||||
if content.get("type") == "text":
|
if content.get("type") == "text":
|
||||||
text = content.get("data", {}).get("text", "").strip()
|
text = content.get("data", {}).get("text", "").strip()
|
||||||
# 过滤长度适中、可能圣经的消息
|
# 过滤掉过短或过长的噪音(已经在 cleaner 处理过一遍基本垃圾)
|
||||||
if 5 <= len(text) <= 100 and not text.startswith(
|
if 2 <= len(text) <= 500:
|
||||||
("http", "www", "/")
|
|
||||||
):
|
|
||||||
interesting_messages.append(
|
interesting_messages.append(
|
||||||
{
|
{
|
||||||
"sender": nickname,
|
"sender": nickname,
|
||||||
@@ -203,7 +201,3 @@ class GoldenQuoteAnalyzer(BaseAnalyzer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return interesting_messages
|
return interesting_messages
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"提取圣经消息失败: {e}")
|
|
||||||
return []
|
|
||||||
|
|||||||
@@ -257,79 +257,38 @@ class TopicAnalyzer(BaseAnalyzer):
|
|||||||
|
|
||||||
def extract_text_messages(self, messages: list[dict]) -> list[dict]:
|
def extract_text_messages(self, messages: list[dict]) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
从群聊消息中提取文本消息
|
从已清理的消息中提取文本消息用于话题分析。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
messages: 群聊消息列表
|
messages: 已由 MessageCleaner 处理过的 legacy 消息列表
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
提取的文本消息列表
|
提取的文本消息列表
|
||||||
"""
|
"""
|
||||||
logger.debug(
|
|
||||||
f"extract_text_messages 开始处理,输入消息数量: {len(messages) if messages else 0}"
|
|
||||||
)
|
|
||||||
logger.debug(f"extract_text_messages 输入消息类型: {type(messages)}")
|
|
||||||
|
|
||||||
if not messages:
|
|
||||||
logger.warning("extract_text_messages 收到空消息列表")
|
|
||||||
return []
|
|
||||||
|
|
||||||
text_messages = []
|
text_messages = []
|
||||||
|
|
||||||
for i, msg in enumerate(messages):
|
for msg in messages:
|
||||||
logger.debug(f"处理第 {i + 1} 条消息,类型: {type(msg)}")
|
# 获取发送者显示名
|
||||||
# 确保msg是字典类型,避免'str' object has no attribute 'get'错误
|
|
||||||
if not isinstance(msg, dict):
|
|
||||||
logger.warning(f"跳过非字典类型的消息: {type(msg)} - {msg}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
sender = msg.get("sender", {})
|
sender = msg.get("sender", {})
|
||||||
# 确保sender是字典类型,避免'str' object has no attribute 'get'错误
|
|
||||||
if not isinstance(sender, dict):
|
|
||||||
logger.warning(
|
|
||||||
f"extract_text_messages 跳过sender非字典类型的消息: {type(sender)} - {sender}"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 获取发送者ID并过滤机器人消息
|
|
||||||
user_id = str(sender.get("user_id", ""))
|
|
||||||
bot_self_ids = self.config_manager.get_bot_self_ids()
|
|
||||||
|
|
||||||
# 跳过机器人自己的消息
|
|
||||||
if bot_self_ids and user_id in [str(uid) for uid in bot_self_ids]:
|
|
||||||
logger.debug(f"extract_text_messages 过滤掉机器人QQ号: {user_id}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
nickname = InfoUtils.get_user_nickname(self.config_manager, sender)
|
nickname = InfoUtils.get_user_nickname(self.config_manager, sender)
|
||||||
msg_time = datetime.fromtimestamp(msg.get("time", 0)).strftime("%H:%M")
|
msg_time = datetime.fromtimestamp(msg.get("time", 0)).strftime("%H:%M")
|
||||||
|
|
||||||
for content in msg.get("message", []):
|
for content in msg.get("message", []):
|
||||||
if content.get("type") == "text":
|
if content.get("type") == "text":
|
||||||
text = content.get("data", {}).get("text", "").strip()
|
text = content.get("data", {}).get("text", "").strip()
|
||||||
if text and len(text) > 2 and not text.startswith("/"):
|
# 已经在 MessageCleaner 中处理过基本的垃圾内容
|
||||||
# 清理消息内容
|
if text:
|
||||||
text = text.replace('""', '"').replace('""', '"')
|
# 简单的额外清理
|
||||||
text = text.replace(""", "'").replace(""", "'")
|
cleaned_text = text.replace("\n", " ").replace("\r", " ")
|
||||||
text = text.replace("\n", " ").replace("\r", " ")
|
cleaned_text = re.sub(r"[\x00-\x1f\x7f-\x9f]", "", cleaned_text)
|
||||||
text = text.replace("\t", " ")
|
|
||||||
text = re.sub(r"[\x00-\x1f\x7f-\x9f]", "", text)
|
|
||||||
text_messages.append(
|
text_messages.append(
|
||||||
{
|
{
|
||||||
"sender": nickname,
|
"sender": nickname,
|
||||||
"time": msg_time,
|
"time": msg_time,
|
||||||
"content": text.strip(),
|
"content": cleaned_text.strip(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"处理第 {i + 1} 条消息时出错: {e}", exc_info=True)
|
|
||||||
continue
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
f"extract_text_messages 完成,提取到 {len(text_messages)} 条文本消息"
|
|
||||||
)
|
|
||||||
if text_messages:
|
|
||||||
logger.debug(f"extract_text_messages 第一条文本消息: {text_messages[0]}")
|
|
||||||
return text_messages
|
return text_messages
|
||||||
|
|
||||||
async def analyze_topics(
|
async def analyze_topics(
|
||||||
|
|||||||
@@ -178,16 +178,15 @@ class UserTitleAnalyzer(BaseAnalyzer):
|
|||||||
|
|
||||||
for user_id, stats in user_analysis.items():
|
for user_id, stats in user_analysis.items():
|
||||||
user_id_str = str(user_id)
|
user_id_str = str(user_id)
|
||||||
# 过滤机器人自己的消息
|
# 过滤机器人由 MessageCleaner 已处理,此处仅作为二级防御
|
||||||
if bot_self_ids and user_id_str in [str(uid) for uid in bot_self_ids]:
|
if bot_self_ids and user_id_str in [str(uid) for uid in bot_self_ids]:
|
||||||
logger.debug(f"过滤掉机器人ID: {user_id}")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 只处理活跃用户
|
# 只处理活跃用户 (top_users 或 消息数>=5)
|
||||||
if user_id_str not in target_user_ids:
|
if user_id_str not in target_user_ids:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 分析用户特征
|
# 分析用户特征 (此处已基于已清理的 stats)
|
||||||
night_messages = sum(stats["hours"][h] for h in range(6))
|
night_messages = sum(stats["hours"][h] for h in range(6))
|
||||||
avg_chars = (
|
avg_chars = (
|
||||||
stats["char_count"] / stats["message_count"]
|
stats["char_count"] / stats["message_count"]
|
||||||
@@ -198,7 +197,7 @@ class UserTitleAnalyzer(BaseAnalyzer):
|
|||||||
user_summaries.append(
|
user_summaries.append(
|
||||||
{
|
{
|
||||||
"name": stats["nickname"],
|
"name": stats["nickname"],
|
||||||
"user_id": user_id_str, # 使用 user_id
|
"user_id": user_id_str,
|
||||||
"message_count": stats["message_count"],
|
"message_count": stats["message_count"],
|
||||||
"avg_chars": round(avg_chars, 1),
|
"avg_chars": round(avg_chars, 1),
|
||||||
"emoji_ratio": round(
|
"emoji_ratio": round(
|
||||||
|
|||||||
@@ -156,15 +156,21 @@ class LLMAnalyzer:
|
|||||||
user_analysis: dict,
|
user_analysis: dict,
|
||||||
umo: str = None,
|
umo: str = None,
|
||||||
top_users: list[dict] = None,
|
top_users: list[dict] = None,
|
||||||
|
topic_enabled: bool = True,
|
||||||
|
user_title_enabled: bool = True,
|
||||||
|
golden_quote_enabled: bool = True,
|
||||||
) -> tuple[list[SummaryTopic], list[UserTitle], list[GoldenQuote], TokenUsage]:
|
) -> tuple[list[SummaryTopic], list[UserTitle], list[GoldenQuote], TokenUsage]:
|
||||||
"""
|
"""
|
||||||
并发执行所有分析任务(话题、用户称号、金句)
|
并发执行所有分析任务(话题、用户称号、金句),支持按需启用。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
messages: 群聊消息列表
|
messages: 群聊消息列表
|
||||||
user_analysis: 用户分析统计
|
user_analysis: 用户分析统计
|
||||||
umo: 模型唯一标识符
|
umo: 模型唯一标识符
|
||||||
top_users: 活跃用户列表(可选)
|
top_users: 活跃用户列表(可选)
|
||||||
|
topic_enabled: 是否启用话题分析
|
||||||
|
user_title_enabled: 是否启用用户称号分析
|
||||||
|
golden_quote_enabled: 是否启用金句分析
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(话题列表, 用户称号列表, 金句列表, 总Token使用统计)
|
(话题列表, 用户称号列表, 金句列表, 总Token使用统计)
|
||||||
@@ -179,10 +185,13 @@ class LLMAnalyzer:
|
|||||||
else:
|
else:
|
||||||
session_id = timestamp
|
session_id = timestamp
|
||||||
|
|
||||||
logger.info(f"开始并发执行所有分析任务,会话ID: {session_id}")
|
logger.info(
|
||||||
|
f"开始并发执行分析任务 (话题:{topic_enabled}, 称号:{user_title_enabled}, 金句:{golden_quote_enabled}),会话ID: {session_id}"
|
||||||
|
)
|
||||||
|
|
||||||
# 保存原始消息数据 (Debug Mode)
|
# 保存原始消息数据 (Debug Mode)
|
||||||
if self.config_manager.get_debug_mode():
|
if self.config_manager.get_debug_mode():
|
||||||
|
# ... (保持原有的调试保存代码)
|
||||||
try:
|
try:
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -202,44 +211,57 @@ class LLMAnalyzer:
|
|||||||
msg_file_path = debug_dir / f"{session_id}_messages.json"
|
msg_file_path = debug_dir / f"{session_id}_messages.json"
|
||||||
with open(msg_file_path, "w", encoding="utf-8") as f:
|
with open(msg_file_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(messages, f, ensure_ascii=False, indent=2)
|
json.dump(messages, f, ensure_ascii=False, indent=2)
|
||||||
logger.info(f"已保存原始消息数据到: {msg_file_path}")
|
except Exception:
|
||||||
except Exception as e:
|
pass
|
||||||
logger.error(f"保存原始消息数据失败: {e}", exc_info=True)
|
|
||||||
|
|
||||||
# 并发执行三个分析任务
|
# 构建并发任务列表
|
||||||
results = await asyncio.gather(
|
tasks = []
|
||||||
self.topic_analyzer.analyze_topics(messages, umo, session_id),
|
task_names = []
|
||||||
|
|
||||||
|
if topic_enabled:
|
||||||
|
tasks.append(
|
||||||
|
self.topic_analyzer.analyze_topics(messages, umo, session_id)
|
||||||
|
)
|
||||||
|
task_names.append("topic")
|
||||||
|
|
||||||
|
if user_title_enabled:
|
||||||
|
tasks.append(
|
||||||
self.user_title_analyzer.analyze_user_titles(
|
self.user_title_analyzer.analyze_user_titles(
|
||||||
messages, user_analysis, umo, top_users, session_id
|
messages, user_analysis, umo, top_users, session_id
|
||||||
),
|
)
|
||||||
|
)
|
||||||
|
task_names.append("user_title")
|
||||||
|
|
||||||
|
if golden_quote_enabled:
|
||||||
|
tasks.append(
|
||||||
self.golden_quote_analyzer.analyze_golden_quotes(
|
self.golden_quote_analyzer.analyze_golden_quotes(
|
||||||
messages, umo, session_id
|
messages, umo, session_id
|
||||||
),
|
|
||||||
return_exceptions=True,
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
task_names.append("golden_quote")
|
||||||
|
|
||||||
|
if not tasks:
|
||||||
|
return [], [], [], TokenUsage()
|
||||||
|
|
||||||
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
# 处理结果
|
# 处理结果
|
||||||
topics, topic_usage = [], TokenUsage()
|
topics, topic_usage = [], TokenUsage()
|
||||||
user_titles, title_usage = [], TokenUsage()
|
user_titles, title_usage = [], TokenUsage()
|
||||||
golden_quotes, quote_usage = [], TokenUsage()
|
golden_quotes, quote_usage = [], TokenUsage()
|
||||||
|
|
||||||
# 话题分析结果
|
for i, result in enumerate(results):
|
||||||
if isinstance(results[0], Exception):
|
name = task_names[i]
|
||||||
logger.error(f"话题分析失败: {results[0]}")
|
if isinstance(result, Exception):
|
||||||
else:
|
logger.error(f"分析任务 {name} 失败: {result}")
|
||||||
topics, topic_usage = results[0]
|
continue
|
||||||
|
|
||||||
# 用户称号分析结果
|
if name == "topic":
|
||||||
if isinstance(results[1], Exception):
|
topics, topic_usage = result
|
||||||
logger.error(f"用户称号分析失败: {results[1]}")
|
elif name == "user_title":
|
||||||
else:
|
user_titles, title_usage = result
|
||||||
user_titles, title_usage = results[1]
|
elif name == "golden_quote":
|
||||||
|
golden_quotes, quote_usage = result
|
||||||
# 金句分析结果
|
|
||||||
if isinstance(results[2], Exception):
|
|
||||||
logger.error(f"金句分析失败: {results[2]}")
|
|
||||||
else:
|
|
||||||
golden_quotes, quote_usage = results[2]
|
|
||||||
|
|
||||||
# 合并Token使用统计
|
# 合并Token使用统计
|
||||||
total_usage = TokenUsage(
|
total_usage = TokenUsage(
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ class InfoUtils:
|
|||||||
"""
|
"""
|
||||||
enable_user_card = config_manager.get_enable_user_card()
|
enable_user_card = config_manager.get_enable_user_card()
|
||||||
if enable_user_card:
|
if enable_user_card:
|
||||||
return sender.get("card", "") or sender.get("nickname", "")
|
return (
|
||||||
|
sender.get("card", "")
|
||||||
|
or sender.get("nickname", "")
|
||||||
|
or str(sender.get("user_id", ""))
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return sender.get("nickname", "") or sender.get("card", "")
|
return (
|
||||||
|
sender.get("nickname", "")
|
||||||
|
or sender.get("card", "")
|
||||||
|
or str(sender.get("user_id", ""))
|
||||||
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ JSON处理工具模块
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from ...utils.logger import logger
|
from ....utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
def fix_json(text: str) -> str:
|
def fix_json(text: str) -> str:
|
||||||
|
|||||||
@@ -105,9 +105,6 @@ class BotManager:
|
|||||||
def _refresh_from_stored_platforms(self):
|
def _refresh_from_stored_platforms(self):
|
||||||
"""尝试从已存储的平台对象中刷新 bot 实例 (Lazy Load)"""
|
"""尝试从已存储的平台对象中刷新 bot 实例 (Lazy Load)"""
|
||||||
for platform_id, platform in self._platforms.items():
|
for platform_id, platform in self._platforms.items():
|
||||||
if platform_id in self._bot_instances:
|
|
||||||
continue
|
|
||||||
|
|
||||||
bot_client = None
|
bot_client = None
|
||||||
# 优先尝试 get_client()
|
# 优先尝试 get_client()
|
||||||
if hasattr(platform, "get_client"):
|
if hasattr(platform, "get_client"):
|
||||||
@@ -121,6 +118,13 @@ class BotManager:
|
|||||||
bot_client = platform.client
|
bot_client = platform.client
|
||||||
|
|
||||||
if bot_client:
|
if bot_client:
|
||||||
|
# 检查是否已存在且是否发生变化(防止重复创建适配器)
|
||||||
|
old_client = self._bot_instances.get(platform_id)
|
||||||
|
|
||||||
|
# 如果 client 对象没变且已经有适配器,跳过
|
||||||
|
if bot_client is old_client and platform_id in self._adapters:
|
||||||
|
continue
|
||||||
|
|
||||||
platform_name = None
|
platform_name = None
|
||||||
if hasattr(platform, "metadata"):
|
if hasattr(platform, "metadata"):
|
||||||
# 优先使用 type
|
# 优先使用 type
|
||||||
@@ -129,6 +133,16 @@ class BotManager:
|
|||||||
elif hasattr(platform.metadata, "name"):
|
elif hasattr(platform.metadata, "name"):
|
||||||
platform_name = platform.metadata.name
|
platform_name = platform.metadata.name
|
||||||
|
|
||||||
|
# 兼容不同版本的元数据获取
|
||||||
|
if not platform_name:
|
||||||
|
meta = getattr(platform, "meta", None)
|
||||||
|
if callable(meta):
|
||||||
|
try:
|
||||||
|
metadata = meta()
|
||||||
|
platform_name = getattr(metadata, "name", None)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# 后备检测:如果不支持名称
|
# 后备检测:如果不支持名称
|
||||||
if not platform_name or not PlatformAdapterFactory.is_supported(
|
if not platform_name or not PlatformAdapterFactory.is_supported(
|
||||||
str(platform_name)
|
str(platform_name)
|
||||||
@@ -138,7 +152,7 @@ class BotManager:
|
|||||||
platform_name = detected
|
platform_name = detected
|
||||||
|
|
||||||
self.set_bot_instance(bot_client, platform_id, platform_name)
|
self.set_bot_instance(bot_client, platform_id, platform_name)
|
||||||
logger.info(f"懒加载发现平台 {platform_id} 的 bot 实例")
|
logger.info(f"已刷新/发现平台 {platform_id} 的 bot 实例 (变动或懒加载)")
|
||||||
|
|
||||||
def get_all_bot_instances(self) -> dict:
|
def get_all_bot_instances(self) -> dict:
|
||||||
"""获取所有已加载的bot实例 {platform_id: bot_instance}"""
|
"""获取所有已加载的bot实例 {platform_id: bot_instance}"""
|
||||||
@@ -213,17 +227,29 @@ class BotManager:
|
|||||||
这是 DDD 架构操作的主要方法。
|
这是 DDD 架构操作的主要方法。
|
||||||
"""
|
"""
|
||||||
if platform_id:
|
if platform_id:
|
||||||
|
# 无论是否存在适配器,都尝试检测一次 client 是否有变(如重启后 session 变化)
|
||||||
|
if platform_id in self._platforms:
|
||||||
|
self._refresh_from_stored_platforms()
|
||||||
|
|
||||||
return self._adapters.get(platform_id)
|
return self._adapters.get(platform_id)
|
||||||
|
|
||||||
if self._adapters:
|
if self._adapters:
|
||||||
if len(self._adapters) == 1:
|
if len(self._adapters) == 1:
|
||||||
return list(self._adapters.values())[0]
|
return list(self._adapters.values())[0]
|
||||||
|
|
||||||
logger.error(
|
logger.warning(
|
||||||
f"存在多个适配器 {list(self._adapters.keys())},但未指定 platform_id。"
|
f"存在多个适配器 {list(self._adapters.keys())},但未指定 platform_id。"
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# 如果没有任何适配器,尝试全局刷新一次
|
||||||
|
self._refresh_from_stored_platforms()
|
||||||
|
if self._adapters:
|
||||||
|
if platform_id:
|
||||||
|
return self._adapters.get(platform_id)
|
||||||
|
if len(self._adapters) == 1:
|
||||||
|
return list(self._adapters.values())[0]
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_all_adapters(self) -> dict:
|
def get_all_adapters(self) -> dict:
|
||||||
@@ -373,17 +399,22 @@ class BotManager:
|
|||||||
|
|
||||||
def update_from_event(self, event):
|
def update_from_event(self, event):
|
||||||
"""从事件更新bot实例(用于手动命令)"""
|
"""从事件更新bot实例(用于手动命令)"""
|
||||||
if hasattr(event, "bot") and event.bot:
|
# 兼容不同平台的 bot 实例属性名 (OneBot 使用 bot, Discord 使用 client)
|
||||||
|
bot_instance = getattr(event, "bot", None) or getattr(event, "client", None)
|
||||||
|
|
||||||
|
if bot_instance:
|
||||||
# 从事件中获取平台ID
|
# 从事件中获取平台ID
|
||||||
platform_id = None
|
platform_id = None
|
||||||
if hasattr(event, "platform") and isinstance(event.platform, str):
|
if hasattr(event, "get_platform_id"):
|
||||||
|
platform_id = event.get_platform_id()
|
||||||
|
elif hasattr(event, "platform_meta") and hasattr(event.platform_meta, "id"):
|
||||||
|
platform_id = event.platform_meta.id
|
||||||
|
elif hasattr(event, "platform") and isinstance(event.platform, str):
|
||||||
platform_id = event.platform
|
platform_id = event.platform
|
||||||
elif hasattr(event, "metadata") and hasattr(event.metadata, "id"):
|
|
||||||
platform_id = event.metadata.id
|
|
||||||
|
|
||||||
self.set_bot_instance(event.bot, platform_id)
|
self.set_bot_instance(bot_instance, platform_id)
|
||||||
# 每次都尝试从bot实例提取ID
|
# 每次都尝试从bot实例提取ID
|
||||||
bot_self_id = self._extract_bot_self_id(event.bot)
|
bot_self_id = self._extract_bot_self_id(bot_instance)
|
||||||
if bot_self_id:
|
if bot_self_id:
|
||||||
# 将单个ID转换为列表,保持统一处理
|
# 将单个ID转换为列表,保持统一处理
|
||||||
self.set_bot_self_ids([bot_self_id])
|
self.set_bot_self_ids([bot_self_id])
|
||||||
|
|||||||
Reference in New Issue
Block a user