fix: 完成多平台逻辑解耦优化

配置模式 (_conf_schema.json):
将所有 "QQ号" 描述更名为通用的 "ID" 或 "用户ID"。
更新了 话题分析、用户称号分析、金句分析 的默认 Prompt 模板,将示例 JSON 中的 qq 字段改为 user_id。
保留 bot_qq_ids 配置项名称以维持向后兼容性,但其描述已更新。
数据模型 (data_models.py):
UserTitle 和 GoldenQuote 类中的 qq 字段重命名为 user_id (类型 str)。
为了兼容旧代码,添加了 qq 属性(@property),自动将 user_id 转为 int 返回。
分析逻辑 (analyzers/*.py):
TopicAnalyzer: 通用化了 @ 消息的处理,支持读取 qq、id 或 user_id 字段。
UserTitleAnalyzer & GoldenQuoteAnalyzer: 更新了 Prompt 构建和结果提取逻辑,优先使用 user_id,不再强制依赖 QQ 号格式。
ReportGenerator: 更新了报告生成逻辑,使用 user_id 获取头像。
结果: 插件现在更具通用性,配置和提示词不再默认绑定 QQ 平台术语,能更好地适应 Discord 等其他平台。容器已重启,新配置生效。
This commit is contained in:
SXP-Simon
2026-02-08 20:09:56 +08:00
parent 94fdc42abb
commit e24449cac1
6 changed files with 96 additions and 67 deletions
+3 -3
View File
@@ -50,9 +50,9 @@
},
"bot_qq_ids": {
"type": "list",
"description": "群分析时屏蔽的QQ号列表",
"description": "群分析时屏蔽的用户ID列表",
"default": [],
"hint": "填写后可启用自动分析功能。可以填写用于自动分析的机器人QQ号、多消息平台QQ号、不希望出现于群分析中的其他人的机器人QQ号等,这种群聊中出现但是不希望分析的QQ号。",
"hint": "填写后可启用自动分析功能。可以填写用于自动分析的机器人ID、多消息平台ID、不希望出现于群分析中的其他人的机器人ID等,这种群聊中出现但是不希望分析的ID。",
"items": {
"type": "string"
}
@@ -218,7 +218,7 @@
"type": "text",
"editor_mode": true,
"editor_language": "markdown",
"default": "请为以下群友分配合适的称号和 MBTI 类型。\n\n## 规则:\n\n- 每个人只能有一个称号\n- 每个称号只能给一个人\n\n## 可选称号:\n\n- **龙王**: 发言频繁但内容轻松的人\n- **技术专家**: 经常讨论技术话题的人\n- **夜猫子**: 经常在深夜发言的人\n- **表情包军火库**: 经常发表情的人\n- **沉默终结者**: 经常开启话题的人\n- **评论家**: 平均发言长度很长的人\n- **阳角**: 在群里很有影响力的人\n- **互动达人**: 经常回复别人的人\n- *...(你可以自行进行拓展添加)*\n\n## 用户数据:\n\n{users_text}\n\n---\n\n### 返回格式示例:\n\n```json\n[\n {{\n \"name\": \"用户名\",\n \"qq\": 123456789,\n \"title\": \"称号\",\n \"mbti\": \"MBTI类型\",\n \"reason\": \"获得此称号的原因\"\n }}\n]\n```\n\n**注意**:请以纯 JSON 格式返回,不要包含 markdown 代码块标记。"
"default": "请为以下群友分配合适的称号和 MBTI 类型。\n\n## 规则:\n\n- 每个人只能有一个称号\n- 每个称号只能给一个人\n\n## 可选称号:\n\n- **龙王**: 发言频繁但内容轻松的人\n- **技术专家**: 经常讨论技术话题的人\n- **夜猫子**: 经常在深夜发言的人\n- **表情包军火库**: 经常发表情的人\n- **沉默终结者**: 经常开启话题的人\n- **评论家**: 平均发言长度很长的人\n- **阳角**: 在群里很有影响力的人\n- **互动达人**: 经常回复别人的人\n- *...(你可以自行进行拓展添加)*\n\n## 用户数据:\n\n{users_text}\n\n---\n\n### 返回格式示例:\n\n```json\n[\n {{\n \"name\": \"用户名\",\n \"user_id\": \"123456789\",\n \"title\": \"称号\",\n \"mbti\": \"MBTI类型\",\n \"reason\": \"获得此称号的原因\"\n }}\n]\n```\n\n**注意**:请以纯 JSON 格式返回,不要包含 markdown 代码块标记。"
}
}
},
+43 -44
View File
@@ -126,6 +126,48 @@ class GoldenQuoteAnalyzer(BaseAnalyzer):
logger.error(f"创建金句对象失败: {e}")
return []
async def analyze_golden_quotes(
self, messages: list[dict], umo: str = None
) -> tuple[list[GoldenQuote], TokenUsage]:
"""
分析群聊金句
Args:
messages: 群聊消息列表
umo: 模型唯一标识符
Returns:
(金句列表, Token使用统计)
"""
try:
# 提取圣经的文本消息
interesting_messages = self.extract_interesting_messages(messages)
if not interesting_messages:
logger.info("没有符合条件的圣经消息,返回空结果")
return [], TokenUsage()
logger.info(f"开始从 {len(interesting_messages)} 条圣经消息中提取金句")
quotes, usage = await self.analyze(interesting_messages, umo)
# 回填 User ID
for quote in quotes:
for msg in interesting_messages:
# 尝试匹配内容和发送者
# 注意:LLM 可能会微调内容,这里使用包含匹配或精确匹配
if (
quote.content in msg["content"]
or msg["content"] in quote.content
) and quote.sender == msg["sender"]:
quote.user_id = str(msg.get("user_id", ""))
break
return quotes, usage
except Exception as e:
logger.error(f"金句分析失败: {e}")
return [], TokenUsage()
def extract_interesting_messages(self, messages: list[dict]) -> list[dict]:
"""
提取圣经的文本消息
@@ -156,7 +198,7 @@ class GoldenQuoteAnalyzer(BaseAnalyzer):
"sender": nickname,
"time": msg_time,
"content": text,
"qq": sender.get("user_id", 0),
"user_id": str(sender.get("user_id", "")),
}
)
@@ -165,46 +207,3 @@ class GoldenQuoteAnalyzer(BaseAnalyzer):
except Exception as e:
logger.error(f"提取圣经消息失败: {e}")
return []
async def analyze_golden_quotes(
self, messages: list[dict], umo: str = None
) -> tuple[list[GoldenQuote], TokenUsage]:
"""
分析群聊金句
Args:
messages: 群聊消息列表
umo: 模型唯一标识符
Returns:
(金句列表, Token使用统计)
"""
try:
# 提取圣经的文本消息
interesting_messages = self.extract_interesting_messages(messages)
if not interesting_messages:
logger.info("没有符合条件的圣经消息,返回空结果")
return [], TokenUsage()
logger.info(f"开始从 {len(interesting_messages)} 条圣经消息中提取金句")
logger.info(f"开始从 {len(interesting_messages)} 条圣经消息中提取金句")
quotes, usage = await self.analyze(interesting_messages, umo)
# 回填QQ号
for quote in quotes:
for msg in interesting_messages:
# 尝试匹配内容和发送者
# 注意:LLM 可能会微调内容,这里使用包含匹配或精确匹配
if (
quote.content in msg["content"]
or msg["content"] in quote.content
) and quote.sender == msg["sender"]:
quote.qq = msg.get("qq", 0)
break
return quotes, usage
except Exception as e:
logger.error(f"金句分析失败: {e}")
return [], TokenUsage()
+9 -3
View File
@@ -100,9 +100,15 @@ class TopicAnalyzer(BaseAnalyzer):
text_parts.append(text)
elif content_type == "at":
# 处理 @ 消息,转换为文本
at_qq = content.get("data", {}).get("qq", "")
if at_qq:
at_text = f"@{at_qq}"
at_data = content.get("data", {})
# 兼容不同平台的 ID 字段
at_id = (
at_data.get("qq")
or at_data.get("id")
or at_data.get("user_id")
)
if at_id:
at_text = f"@{at_id}"
text_parts.append(at_text)
elif content_type == "reply":
# 处理回复消息,添加标记
+20 -12
View File
@@ -54,7 +54,7 @@ class UserTitleAnalyzer(BaseAnalyzer):
# 构建用户数据文本
users_text = "\n".join(
[
f"- {user['name']} (QQ:{user['qq']}): "
f"- {user['name']} (ID:{user['user_id']}): "
f"发言{user['message_count']}条, 平均{user['avg_chars']}字, "
f"表情比例{user['emoji_ratio']}, 夜间发言比例{user['night_ratio']}, "
f"回复比例{user['reply_ratio']}"
@@ -109,7 +109,8 @@ class UserTitleAnalyzer(BaseAnalyzer):
for title_data in titles_data[:max_titles]:
# 确保数据格式正确
name = title_data.get("name", "").strip()
qq = title_data.get("qq")
# 兼容 LLM 返回 qq 或 user_id 的情况
user_id = title_data.get("user_id") or title_data.get("qq")
title = title_data.get("title", "").strip()
mbti = title_data.get("mbti", "").strip()
reason = title_data.get("reason", "").strip()
@@ -119,15 +120,21 @@ class UserTitleAnalyzer(BaseAnalyzer):
logger.warning(f"用户称号数据格式不完整,跳过: {title_data}")
continue
# 验证QQ号格式(单个用户QQ号)
try:
qq = int(qq)
except (ValueError, TypeError):
logger.warning(f"QQ号格式无效,跳过: {qq}")
# 确保 user_id 是字符串
if user_id is not None:
user_id = str(user_id)
else:
logger.warning(f"未找到用户ID (user_id/qq),跳过: {title_data}")
continue
titles.append(
UserTitle(name=name, qq=qq, title=title, mbti=mbti, reason=reason)
UserTitle(
name=name,
user_id=user_id,
title=title,
mbti=mbti,
reason=reason,
)
)
return titles
@@ -172,13 +179,14 @@ class UserTitleAnalyzer(BaseAnalyzer):
}
for user_id, stats in user_analysis.items():
user_id_str = str(user_id)
# 过滤机器人自己的消息
if bot_qq_ids and str(user_id) in [str(qq) for qq in bot_qq_ids]:
logger.debug(f"过滤掉机器人QQ号: {user_id}")
if bot_qq_ids and user_id_str in [str(qq) for qq in bot_qq_ids]:
logger.debug(f"过滤掉机器人ID: {user_id}")
continue
# 只处理活跃用户
if user_id not in target_user_ids:
if user_id_str not in target_user_ids:
continue
# 分析用户特征
@@ -192,7 +200,7 @@ class UserTitleAnalyzer(BaseAnalyzer):
user_summaries.append(
{
"name": stats["nickname"],
"qq": int(user_id),
"user_id": user_id_str, # 使用 user_id
"message_count": stats["message_count"],
"avg_chars": round(avg_chars, 1),
"emoji_ratio": round(
+18 -2
View File
@@ -20,11 +20,19 @@ class UserTitle:
"""用户称号数据结构"""
name: str
qq: int
user_id: str # 原 qq 字段
title: str
mbti: str
reason: str
@property
def qq(self) -> int:
"""兼容旧字段"""
try:
return int(self.user_id) if self.user_id else 0
except ValueError:
return 0
@dataclass
class GoldenQuote:
@@ -33,7 +41,15 @@ class GoldenQuote:
content: str
sender: str
reason: str
qq: int = 0
user_id: str = "" # 原 qq 字段
@property
def qq(self) -> int:
"""兼容旧字段"""
try:
return int(self.user_id) if self.user_id else 0
except ValueError:
return 0
@dataclass
+3 -3
View File
@@ -256,7 +256,7 @@ class ReportGenerator:
titles_list = []
for title in user_titles[:max_user_titles]:
# 获取用户头像
avatar_data = await self._get_user_avatar(str(title.qq), avatar_getter)
avatar_data = await self._get_user_avatar(str(title.user_id), avatar_getter)
title_data = {
"name": title.name,
"title": title.title,
@@ -276,8 +276,8 @@ class ReportGenerator:
quotes_list = []
for quote in stats.golden_quotes[:max_golden_quotes]:
avatar_url = (
await self._get_user_avatar(str(quote.qq), avatar_getter)
if quote.qq
await self._get_user_avatar(str(quote.user_id), avatar_getter)
if quote.user_id
else None
)
quotes_list.append(