feat(affinity-flow): 通过标签扩展与提及分类增强兴趣匹配

- 实施扩展标签描述以实现更精确的语义匹配
- 增加强/弱提及分类,并附带独立的兴趣评分
- 重构机器人兴趣管理器,采用动态嵌入生成与缓存机制
- 通过增强的@提及处理功能优化消息处理
- 更新配置以支持回帖提升机制
- 将亲和力流量聊天重新组织为模块化结构,包含核心、规划器、主动响应和工具子模块
- 移除已弃用的规划器组件并整合功能
- 为napcat适配器插件添加数据库表初始化功能
- 修复元事件处理器中的心跳监控
This commit is contained in:
Windpicker-owo
2025-11-03 22:24:51 +08:00
parent f822bfb6ee
commit 5e6e56454d
28 changed files with 1226 additions and 164 deletions

View File

@@ -0,0 +1,10 @@
"""
AffinityFlow Chatter 核心模块
包含兴趣度计算器和核心对话处理逻辑
"""
from .affinity_chatter import AffinityChatter
from .affinity_interest_calculator import AffinityInterestCalculator
__all__ = ["AffinityChatter", "AffinityInterestCalculator"]

View File

@@ -15,7 +15,7 @@ from src.common.data_models.message_manager_data_model import StreamContext
from src.common.logger import get_logger
from src.plugin_system.base.base_chatter import BaseChatter
from src.plugin_system.base.component_types import ChatType
from src.plugins.built_in.affinity_flow_chatter.planner import ChatterActionPlanner
from src.plugins.built_in.affinity_flow_chatter.planner.planner import ChatterActionPlanner
logger = get_logger("affinity_chatter")

View File

@@ -3,6 +3,7 @@
基于原有的 AffinityFlow 兴趣度评分系统提供标准化的兴趣值计算功能
"""
import asyncio
import time
from typing import TYPE_CHECKING
@@ -60,10 +61,18 @@ class AffinityInterestCalculator(BaseInterestCalculator):
# 用户关系数据缓存
self.user_relationships: dict[str, float] = {} # user_id -> relationship_score
# 回复后阈值降低机制
self.enable_post_reply_boost = affinity_config.enable_post_reply_boost
self.post_reply_boost_remaining = 0 # 剩余的回复后降低次数
self.post_reply_threshold_reduction = affinity_config.post_reply_threshold_reduction
self.post_reply_boost_max_count = affinity_config.post_reply_boost_max_count
self.post_reply_boost_decay_rate = affinity_config.post_reply_boost_decay_rate
logger.info("[Affinity兴趣计算器] 初始化完成:")
logger.info(f" - 权重配置: {self.score_weights}")
logger.info(f" - 回复阈值: {self.reply_threshold}")
logger.info(f" - 智能匹配: {self.use_smart_matching}")
logger.info(f" - 回复后连续对话: {self.enable_post_reply_boost}")
# 检查 bot_interest_manager 状态
try:
@@ -120,22 +129,23 @@ class AffinityInterestCalculator(BaseInterestCalculator):
f"{mentioned_score:.3f}*{self.score_weights['mentioned']} = {total_score:.3f}"
)
# 5. 考虑连续不回复的概率提升
adjusted_score = self._apply_no_reply_boost(total_score)
logger.debug(f"[Affinity兴趣计算] 应用不回复提升后: {total_score:.3f}{adjusted_score:.3f}")
# 5. 考虑连续不回复的阈值调整
adjusted_score = total_score
adjusted_reply_threshold, adjusted_action_threshold = self._apply_no_reply_threshold_adjustment()
logger.debug(
f"[Affinity兴趣计算] 连续不回复调整: 回复阈值 {self.reply_threshold:.3f}{adjusted_reply_threshold:.3f}, "
f"动作阈值 {global_config.affinity_flow.non_reply_action_interest_threshold:.3f}{adjusted_action_threshold:.3f}"
)
# 6. 决定是否回复和执行动作
reply_threshold = self.reply_threshold
action_threshold = global_config.affinity_flow.non_reply_action_interest_threshold
should_reply = adjusted_score >= reply_threshold
should_take_action = adjusted_score >= action_threshold
should_reply = adjusted_score >= adjusted_reply_threshold
should_take_action = adjusted_score >= adjusted_action_threshold
logger.debug(
f"[Affinity兴趣计算] 阈值判断: {adjusted_score:.3f} >= 回复阈值:{reply_threshold:.3f}? = {should_reply}"
f"[Affinity兴趣计算] 阈值判断: {adjusted_score:.3f} >= 回复阈值:{adjusted_reply_threshold:.3f}? = {should_reply}"
)
logger.debug(
f"[Affinity兴趣计算] 阈值判断: {adjusted_score:.3f} >= 动作阈值:{action_threshold:.3f}? = {should_take_action}"
f"[Affinity兴趣计算] 阈值判断: {adjusted_score:.3f} >= 动作阈值:{adjusted_action_threshold:.3f}? = {should_take_action}"
)
calculation_time = time.time() - start_time
@@ -162,7 +172,7 @@ class AffinityInterestCalculator(BaseInterestCalculator):
)
async def _calculate_interest_match_score(self, content: str, keywords: list[str] | None = None) -> float:
"""计算兴趣匹配度(使用智能兴趣匹配系统)"""
"""计算兴趣匹配度(使用智能兴趣匹配系统,带超时保护"""
# 调试日志:检查各个条件
if not content:
@@ -178,8 +188,11 @@ class AffinityInterestCalculator(BaseInterestCalculator):
logger.debug(f"开始兴趣匹配计算,内容: {content[:50]}...")
try:
# 使用机器人的兴趣标签系统进行智能匹配
match_result = await bot_interest_manager.calculate_interest_match(content, keywords or [])
# 使用机器人的兴趣标签系统进行智能匹配1.5秒超时保护)
match_result = await asyncio.wait_for(
bot_interest_manager.calculate_interest_match(content, keywords or []),
timeout=1.5
)
logger.debug(f"兴趣匹配结果: {match_result}")
if match_result:
@@ -195,6 +208,9 @@ class AffinityInterestCalculator(BaseInterestCalculator):
logger.debug("兴趣匹配返回0.0: match_result为None")
return 0.0
except asyncio.TimeoutError:
logger.warning(f"⏱️ 兴趣匹配计算超时(>1.5秒)返回默认分值0.5以保留其他分数")
return 0.5 # 超时时返回默认分值,避免丢失提及分和关系分
except Exception as e:
logger.warning(f"智能兴趣匹配失败: {e}")
return 0.0
@@ -226,29 +242,78 @@ class AffinityInterestCalculator(BaseInterestCalculator):
return global_config.affinity_flow.base_relationship_score
def _calculate_mentioned_score(self, message: "DatabaseMessages", bot_nickname: str) -> float:
"""计算提及分 - 统一使用配置值,不区分提及方式"""
is_mentioned = getattr(message, "is_mentioned", False)
processed_plain_text = getattr(message, "processed_plain_text", "")
# 判断是否为私聊 - 通过 group_info 对象判断
is_private_chat = not message.group_info # 如果没有group_info则是私聊
logger.debug(f"[提及分计算] is_mentioned={is_mentioned}, is_private_chat={is_private_chat}, group_info={message.group_info}")
# 检查是否被提及(包括文本匹配)
bot_aliases = [bot_nickname, *global_config.bot.alias_names]
is_text_mentioned = any(alias in processed_plain_text for alias in bot_aliases if alias)
# 统一判断:只要提及了机器人(包括@、文本提及、私聊)都返回配置的分值
if is_mentioned or is_text_mentioned or is_private_chat:
logger.debug("[提及分计算] 检测到机器人提及,返回配置分值")
return global_config.affinity_flow.mention_bot_interest_score
"""计算提及分 - 区分提及和弱提及
强提及@被回复私聊: 使用 strong_mention_interest_score
弱提及文本匹配名字/别名: 使用 weak_mention_interest_score
"""
from src.chat.utils.utils import is_mentioned_bot_in_message
# 使用统一的提及检测函数
is_mentioned, mention_type = is_mentioned_bot_in_message(message)
if not is_mentioned:
logger.debug("[提及分计算] 未提及机器人返回0.0")
return 0.0
# mention_type: 0=未提及, 1=弱提及, 2=强提及
if mention_type >= 2:
# 强提及:被@、被回复、私聊
score = global_config.affinity_flow.strong_mention_interest_score
logger.debug(f"[提及分计算] 检测到强提及(@/回复/私聊),返回分值: {score}")
return score
elif mention_type >= 1:
# 弱提及文本匹配bot名字或别名
score = global_config.affinity_flow.weak_mention_interest_score
logger.debug(f"[提及分计算] 检测到弱提及(文本匹配),返回分值: {score}")
return score
else:
logger.debug("[提及分计算] 未提及机器人返回0.0")
return 0.0 # 未提及机器人
return 0.0
def _apply_no_reply_threshold_adjustment(self) -> tuple[float, float]:
"""应用阈值调整(包括连续不回复和回复后降低机制)
Returns:
tuple[float, float]: (调整后的回复阈值, 调整后的动作阈值)
"""
# 基础阈值
base_reply_threshold = self.reply_threshold
base_action_threshold = global_config.affinity_flow.non_reply_action_interest_threshold
total_reduction = 0.0
# 1. 连续不回复的阈值降低
if self.no_reply_count > 0 and self.no_reply_count < self.max_no_reply_count:
no_reply_reduction = self.no_reply_count * self.probability_boost_per_no_reply
total_reduction += no_reply_reduction
logger.debug(f"[阈值调整] 连续不回复降低: {no_reply_reduction:.3f} (计数: {self.no_reply_count})")
# 2. 回复后的阈值降低使bot更容易连续对话
if self.enable_post_reply_boost and self.post_reply_boost_remaining > 0:
# 计算衰减后的降低值
decay_factor = self.post_reply_boost_decay_rate ** (
self.post_reply_boost_max_count - self.post_reply_boost_remaining
)
post_reply_reduction = self.post_reply_threshold_reduction * decay_factor
total_reduction += post_reply_reduction
logger.debug(
f"[阈值调整] 回复后降低: {post_reply_reduction:.3f} "
f"(剩余次数: {self.post_reply_boost_remaining}, 衰减: {decay_factor:.2f})"
)
# 应用总降低量
adjusted_reply_threshold = max(0.0, base_reply_threshold - total_reduction)
adjusted_action_threshold = max(0.0, base_action_threshold - total_reduction)
return adjusted_reply_threshold, adjusted_action_threshold
def _apply_no_reply_boost(self, base_score: float) -> float:
"""应用连续不回复的概率提升"""
"""【已弃用】应用连续不回复的概率提升
注意此方法已被 _apply_no_reply_threshold_adjustment 替代
保留用于向后兼容
"""
if self.no_reply_count > 0 and self.no_reply_count < self.max_no_reply_count:
boost = self.no_reply_count * self.probability_boost_per_no_reply
return min(1.0, base_score + boost)
@@ -315,3 +380,34 @@ class AffinityInterestCalculator(BaseInterestCalculator):
self.no_reply_count = 0
else:
self.no_reply_count = min(self.no_reply_count + 1, self.max_no_reply_count)
def on_reply_sent(self):
"""当机器人发送回复后调用,激活回复后阈值降低机制"""
if self.enable_post_reply_boost:
# 重置回复后降低计数器
self.post_reply_boost_remaining = self.post_reply_boost_max_count
logger.debug(
f"[回复后机制] 激活连续对话模式,阈值将在接下来 {self.post_reply_boost_max_count} 条消息中降低"
)
# 同时重置不回复计数
self.no_reply_count = 0
def on_message_processed(self, replied: bool):
"""消息处理完成后调用,更新各种计数器
Args:
replied: 是否回复了此消息
"""
# 更新不回复计数
self.update_no_reply_count(replied)
# 如果已回复,激活回复后降低机制
if replied:
self.on_reply_sent()
else:
# 如果没有回复,减少回复后降低剩余次数
if self.post_reply_boost_remaining > 0:
self.post_reply_boost_remaining -= 1
logger.debug(
f"[回复后机制] 未回复消息,剩余降低次数: {self.post_reply_boost_remaining}"
)

View File

@@ -0,0 +1,13 @@
"""
AffinityFlow Chatter 规划器模块
包含计划生成、过滤、执行等规划相关功能
"""
from .plan_executor import ChatterPlanExecutor
from .plan_filter import ChatterPlanFilter
from .plan_generator import ChatterPlanGenerator
from .planner import ChatterActionPlanner
from . import planner_prompts
__all__ = ["ChatterActionPlanner", "planner_prompts", "ChatterPlanGenerator", "ChatterPlanFilter", "ChatterPlanExecutor"]

View File

@@ -11,9 +11,9 @@ from src.common.logger import get_logger
from src.config.config import global_config
from src.mood.mood_manager import mood_manager
from src.plugin_system.base.component_types import ChatMode
from src.plugins.built_in.affinity_flow_chatter.plan_executor import ChatterPlanExecutor
from src.plugins.built_in.affinity_flow_chatter.plan_filter import ChatterPlanFilter
from src.plugins.built_in.affinity_flow_chatter.plan_generator import ChatterPlanGenerator
from src.plugins.built_in.affinity_flow_chatter.planner.plan_executor import ChatterPlanExecutor
from src.plugins.built_in.affinity_flow_chatter.planner.plan_filter import ChatterPlanFilter
from src.plugins.built_in.affinity_flow_chatter.planner.plan_generator import ChatterPlanGenerator
if TYPE_CHECKING:
from src.chat.planner_actions.action_manager import ChatterActionManager
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
from src.common.data_models.message_manager_data_model import StreamContext
# 导入提示词模块以确保其被初始化
from src.plugins.built_in.affinity_flow_chatter import planner_prompts # noqa
from src.plugins.built_in.affinity_flow_chatter.planner import planner_prompts
logger = get_logger("planner")

View File

@@ -39,48 +39,48 @@ class AffinityChatterPlugin(BasePlugin):
components: ClassVar = []
try:
# 延迟导入 AffinityChatter
from .affinity_chatter import AffinityChatter
# 延迟导入 AffinityChatter(从 core 子模块)
from .core.affinity_chatter import AffinityChatter
components.append((AffinityChatter.get_chatter_info(), AffinityChatter))
except Exception as e:
logger.error(f"加载 AffinityChatter 时出错: {e}")
try:
# 延迟导入 AffinityInterestCalculator
from .affinity_interest_calculator import AffinityInterestCalculator
# 延迟导入 AffinityInterestCalculator(从 core 子模块)
from .core.affinity_interest_calculator import AffinityInterestCalculator
components.append((AffinityInterestCalculator.get_interest_calculator_info(), AffinityInterestCalculator))
except Exception as e:
logger.error(f"加载 AffinityInterestCalculator 时出错: {e}")
try:
# 延迟导入 UserProfileTool
from .user_profile_tool import UserProfileTool
# 延迟导入 UserProfileTool(从 tools 子模块)
from .tools.user_profile_tool import UserProfileTool
components.append((UserProfileTool.get_tool_info(), UserProfileTool))
except Exception as e:
logger.error(f"加载 UserProfileTool 时出错: {e}")
try:
# 延迟导入 ChatStreamImpressionTool
from .chat_stream_impression_tool import ChatStreamImpressionTool
# 延迟导入 ChatStreamImpressionTool(从 tools 子模块)
from .tools.chat_stream_impression_tool import ChatStreamImpressionTool
components.append((ChatStreamImpressionTool.get_tool_info(), ChatStreamImpressionTool))
except Exception as e:
logger.error(f"加载 ChatStreamImpressionTool 时出错: {e}")
try:
# 延迟导入 ProactiveThinkingReplyHandler
from .proactive_thinking_event import ProactiveThinkingReplyHandler
# 延迟导入 ProactiveThinkingReplyHandler(从 proactive 子模块)
from .proactive.proactive_thinking_event import ProactiveThinkingReplyHandler
components.append((ProactiveThinkingReplyHandler.get_handler_info(), ProactiveThinkingReplyHandler))
except Exception as e:
logger.error(f"加载 ProactiveThinkingReplyHandler 时出错: {e}")
try:
# 延迟导入 ProactiveThinkingMessageHandler
from .proactive_thinking_event import ProactiveThinkingMessageHandler
# 延迟导入 ProactiveThinkingMessageHandler(从 proactive 子模块)
from .proactive.proactive_thinking_event import ProactiveThinkingMessageHandler
components.append((ProactiveThinkingMessageHandler.get_handler_info(), ProactiveThinkingMessageHandler))
except Exception as e:

View File

@@ -0,0 +1,17 @@
"""
AffinityFlow Chatter 主动思考模块
包含主动思考调度器、执行器和事件处理
"""
from .proactive_thinking_event import ProactiveThinkingMessageHandler, ProactiveThinkingReplyHandler
from .proactive_thinking_executor import execute_proactive_thinking
from .proactive_thinking_scheduler import ProactiveThinkingScheduler, proactive_thinking_scheduler
__all__ = [
"ProactiveThinkingReplyHandler",
"ProactiveThinkingMessageHandler",
"execute_proactive_thinking",
"ProactiveThinkingScheduler",
"proactive_thinking_scheduler",
]

View File

@@ -9,7 +9,7 @@ from typing import ClassVar
from src.common.logger import get_logger
from src.plugin_system import BaseEventHandler, EventType
from src.plugin_system.base.base_event import HandlerResult
from src.plugins.built_in.affinity_flow_chatter.proactive_thinking_scheduler import (
from src.plugins.built_in.affinity_flow_chatter.proactive.proactive_thinking_scheduler import (
proactive_thinking_scheduler,
)

View File

@@ -226,7 +226,7 @@ class ProactiveThinkingPlanner:
# 5. 获取上次决策
last_decision = None
try:
from src.plugins.built_in.affinity_flow_chatter.proactive_thinking_scheduler import (
from src.plugins.built_in.affinity_flow_chatter.proactive.proactive_thinking_scheduler import (
proactive_thinking_scheduler,
)
@@ -590,7 +590,7 @@ async def execute_proactive_thinking(stream_id: str):
stream_id: 聊天流ID
"""
from src.config.config import global_config
from src.plugins.built_in.affinity_flow_chatter.proactive_thinking_scheduler import (
from src.plugins.built_in.affinity_flow_chatter.proactive.proactive_thinking_scheduler import (
proactive_thinking_scheduler,
)

View File

@@ -256,7 +256,7 @@ class ProactiveThinkingScheduler:
logger.debug(f"[调度器] 触发间隔={interval_seconds}秒 ({interval_seconds / 60:.1f}分钟)")
# 导入回调函数(延迟导入避免循环依赖)
from src.plugins.built_in.affinity_flow_chatter.proactive_thinking_executor import (
from src.plugins.built_in.affinity_flow_chatter.proactive.proactive_thinking_executor import (
execute_proactive_thinking,
)

View File

@@ -0,0 +1,10 @@
"""
AffinityFlow Chatter 工具模块
包含各种辅助工具类
"""
from .chat_stream_impression_tool import ChatStreamImpressionTool
from .user_profile_tool import UserProfileTool
__all__ = ["ChatStreamImpressionTool", "UserProfileTool"]

View File

@@ -386,6 +386,9 @@ class NapcatAdapterPlugin(BasePlugin):
return components
async def on_plugin_loaded(self):
# 初始化数据库表
await self._init_database_tables()
# 设置插件配置
message_send_instance.set_plugin_config(self.config)
# 设置chunker的插件配置
@@ -410,3 +413,18 @@ class NapcatAdapterPlugin(BasePlugin):
stream_router.cleanup_interval = config_api.get_plugin_config(self.config, "stream_router.cleanup_interval", 60)
# 设置其他handler的插件配置现在由component_registry在注册时自动设置
async def _init_database_tables(self):
"""初始化插件所需的数据库表"""
try:
from src.common.database.core.engine import get_engine
from .src.database import NapcatBanRecord
engine = await get_engine()
async with engine.begin() as conn:
# 创建 napcat_ban_records 表
await conn.run_sync(NapcatBanRecord.metadata.create_all)
logger.info("Napcat 插件数据库表初始化成功")
except Exception as e:
logger.error(f"Napcat 插件数据库表初始化失败: {e}", exc_info=True)

View File

@@ -35,13 +35,17 @@ class MetaEventHandler:
self_id = message.get("self_id")
self.last_heart_beat = time.time()
logger.info(f"Bot {self_id} 连接成功")
asyncio.create_task(self.check_heartbeat(self_id))
# 不在连接时立即启动心跳检查,等第一个心跳包到达后再启动
elif event_type == MetaEventType.heartbeat:
if message["status"].get("online") and message["status"].get("good"):
if not self._interval_checking:
asyncio.create_task(self.check_heartbeat())
self_id = message.get("self_id")
if not self._interval_checking and self_id:
# 第一次收到心跳包时才启动心跳检查
asyncio.create_task(self.check_heartbeat(self_id))
self.last_heart_beat = time.time()
self.interval = message.get("interval") / 1000
interval = message.get("interval")
if interval:
self.interval = interval / 1000
else:
self_id = message.get("self_id")
logger.warning(f"Bot {self_id} Napcat 端异常!")