diff --git a/docs/affinity_flow_chatter_optimization_summary.md b/docs/affinity_flow_chatter_optimization_summary.md new file mode 100644 index 000000000..f1c3fb9d8 --- /dev/null +++ b/docs/affinity_flow_chatter_optimization_summary.md @@ -0,0 +1,654 @@ +# Affinity Flow Chatter 插件优化总结 + +## 更新日期 +2025年11月3日 + +## 优化概述 + +本次对 Affinity Flow Chatter 插件进行了全面的重构和优化,主要包括目录结构优化、性能改进、bug修复和新功能添加。 + +## � 任务-1: 细化提及分数机制(强提及 vs 弱提及) + +### 变更内容 +将原有的统一提及分数细化为**强提及**和**弱提及**两种类型,使用不同的分值。 + +### 原设计问题 +**旧逻辑**: +- ❌ 所有提及方式使用同一个分值(`mention_bot_interest_score`) +- ❌ 被@、私聊、文本提到名字都是相同的重要性 +- ❌ 无法区分用户的真实意图 + +### 新设计 + +#### 强提及(Strong Mention) +**定义**:用户**明确**想与bot交互 +- ✅ 被 @ 提及 +- ✅ 被回复 +- ✅ 私聊消息 + +**分值**:`strong_mention_interest_score = 2.5`(默认) + +#### 弱提及(Weak Mention) +**定义**:在讨论中**顺带**提到bot +- ✅ 消息中包含bot名字 +- ✅ 消息中包含bot别名 + +**分值**:`weak_mention_interest_score = 1.5`(默认) + +### 检测逻辑 + +```python +def is_mentioned_bot_in_message(message) -> tuple[bool, float]: + """ + Returns: + tuple[bool, float]: (是否提及, 提及类型) + 提及类型: 0=未提及, 1=弱提及, 2=强提及 + """ + # 1. 检查私聊 → 强提及 + if is_private_chat: + return True, 2.0 + + # 2. 检查 @ → 强提及 + if is_at: + return True, 2.0 + + # 3. 检查回复 → 强提及 + if is_replied: + return True, 2.0 + + # 4. 检查文本匹配 → 弱提及 + if text_contains_bot_name_or_alias: + return True, 1.0 + + return False, 0.0 +``` + +### 配置参数 + +**config/bot_config.toml**: +```toml +[affinity_flow] +# 提及bot相关参数 +strong_mention_interest_score = 2.5 # 强提及(@/回复/私聊) +weak_mention_interest_score = 1.5 # 弱提及(文本匹配) +``` + +### 实际效果对比 + +**场景1:被@** +``` +用户: "@小狐 你好呀" +旧逻辑: 提及分 = 2.5 +新逻辑: 提及分 = 2.5 (强提及) ✅ 保持不变 +``` + +**场景2:回复bot** +``` +用户: [回复 小狐:...] "是的" +旧逻辑: 提及分 = 2.5 +新逻辑: 提及分 = 2.5 (强提及) ✅ 保持不变 +``` + +**场景3:私聊** +``` +用户: "在吗" +旧逻辑: 提及分 = 2.5 +新逻辑: 提及分 = 2.5 (强提及) ✅ 保持不变 +``` + +**场景4:文本提及** +``` +用户: "小狐今天没来吗" +旧逻辑: 提及分 = 2.5 (可能过高) +新逻辑: 提及分 = 1.5 (弱提及) ✅ 更合理 +``` + +**场景5:讨论bot** +``` +用户A: "小狐这个bot挺有意思的" +旧逻辑: 提及分 = 2.5 (bot可能会插话) +新逻辑: 提及分 = 1.5 (弱提及,降低打断概率) ✅ 更自然 +``` + +### 优势 + +- ✅ **意图识别**:区分"想对话"和"在讨论" +- ✅ **减少误判**:降低在他人讨论中插话的概率 +- ✅ **灵活调节**:可以独立调整强弱提及的权重 +- ✅ **向后兼容**:保持原有强提及的行为不变 + +### 影响文件 + +- `config/bot_config.toml`:添加 `strong/weak_mention_interest_score` 配置 +- `template/bot_config_template.toml`:同步模板配置 +- `src/config/official_configs.py`:添加配置字段定义 +- `src/chat/utils/utils.py`:修改 `is_mentioned_bot_in_message()` 函数 +- `src/plugins/built_in/affinity_flow_chatter/core/affinity_interest_calculator.py`:使用新的强弱提及逻辑 +- `docs/affinity_flow_guide.md`:更新文档说明 + +--- + +## �🆔 任务0: 修改 Personality ID 生成逻辑 + +### 变更内容 +将 `bot_person_id` 从固定值改为基于人设文本的 hash 生成,实现人设变化时自动触发兴趣标签重新生成。 + +### 原设计问题 +**旧逻辑**: +```python +self.bot_person_id = person_info_manager.get_person_id("system", "bot_id") +# 结果:md5("system_bot_id") = 固定值 +``` +- ❌ personality_id 固定不变 +- ❌ 人设修改后不会重新生成兴趣标签 +- ❌ 需要手动清空数据库才能触发重新生成 + +### 新设计 +**新逻辑**: +```python +personality_hash, _ = self._get_config_hash(bot_nickname, personality_core, personality_side, identity) +self.bot_person_id = personality_hash +# 结果:md5(人设配置的JSON) = 动态值 +``` + +### Hash 生成规则 +```python +personality_config = { + "nickname": bot_nickname, + "personality_core": personality_core, + "personality_side": personality_side, + "compress_personality": global_config.personality.compress_personality, +} +personality_hash = md5(json_dumps(personality_config, sorted=True)) +``` + +### 工作原理 +1. **初始化时**:根据当前人设配置计算 hash 作为 personality_id +2. **配置变化检测**: + - 计算当前人设的 hash + - 与上次保存的 hash 对比 + - 如果不同,触发重新生成 +3. **兴趣标签生成**: + - `bot_interest_manager` 根据 personality_id 查询数据库 + - 如果 personality_id 不存在(人设变化了),自动生成新的兴趣标签 + - 保存时使用新的 personality_id + +### 优势 +- ✅ **自动检测**:人设改变后无需手动操作 +- ✅ **数据隔离**:不同人设的兴趣标签分开存储 +- ✅ **版本管理**:可以保留历史人设的兴趣标签(如果需要) +- ✅ **逻辑清晰**:personality_id 直接反映人设内容 + +### 示例 +``` +人设 A: + nickname: "小狐" + personality_core: "活泼开朗" + personality_side: "喜欢编程" + → personality_id: a1b2c3d4e5f6... + +人设 B (修改后): + nickname: "小狐" + personality_core: "冷静理性" ← 改变 + personality_side: "喜欢编程" + → personality_id: f6e5d4c3b2a1... ← 自动生成新ID + +结果: +- 数据库查询时找不到 f6e5d4c3b2a1 的兴趣标签 +- 自动触发重新生成 +- 新兴趣标签保存在 f6e5d4c3b2a1 下 +``` + +### 影响范围 +- `src/individuality/individuality.py`:personality_id 生成逻辑 +- `src/chat/interest_system/bot_interest_manager.py`:兴趣标签加载/保存(已支持) +- 数据库:`bot_personality_interests` 表通过 personality_id 字段关联 + +--- + +## 📁 任务1: 优化插件目录结构 + +### 变更内容 +将原本扁平的文件结构重组为分层目录,提高代码可维护性: + +``` +affinity_flow_chatter/ +├── core/ # 核心模块 +│ ├── __init__.py +│ ├── affinity_chatter.py # 主聊天处理器 +│ └── affinity_interest_calculator.py # 兴趣度计算器 +│ +├── planner/ # 规划器模块 +│ ├── __init__.py +│ ├── planner.py # 动作规划器 +│ ├── planner_prompts.py # 提示词模板 +│ ├── plan_generator.py # 计划生成器 +│ ├── plan_filter.py # 计划过滤器 +│ └── plan_executor.py # 计划执行器 +│ +├── proactive/ # 主动思考模块 +│ ├── __init__.py +│ ├── proactive_thinking_scheduler.py # 主动思考调度器 +│ ├── proactive_thinking_executor.py # 主动思考执行器 +│ └── proactive_thinking_event.py # 主动思考事件 +│ +├── tools/ # 工具模块 +│ ├── __init__.py +│ ├── chat_stream_impression_tool.py # 聊天印象工具 +│ └── user_profile_tool.py # 用户档案工具 +│ +├── plugin.py # 插件注册 +├── __init__.py # 插件元数据 +└── README.md # 文档 +``` + +### 优势 +- ✅ **逻辑清晰**:相关功能集中在同一目录 +- ✅ **易于维护**:模块职责明确,便于定位和修改 +- ✅ **可扩展性**:新功能可以轻松添加到对应目录 +- ✅ **团队协作**:多人开发时减少文件冲突 + +--- + +## 💾 任务2: 修改 Embedding 存储策略 + +### 问题分析 +**原设计**:兴趣标签的 embedding 向量(2560维度浮点数组)直接存储在数据库中 +- ❌ 数据库存储过长,可能导致写入失败 +- ❌ 每次加载需要反序列化大量数据 +- ❌ 数据库体积膨胀 + +### 解决方案 +**新设计**:Embedding 改为启动时动态生成并缓存在内存中 + +#### 实现细节 + +**1. 数据库存储**(不再包含 embedding): +```python +# 保存时 +tag_dict = { + "tag_name": tag.tag_name, + "weight": tag.weight, + "expanded": tag.expanded, # 扩展描述 + "created_at": tag.created_at.isoformat(), + "updated_at": tag.updated_at.isoformat(), + "is_active": tag.is_active, + # embedding 不再存储 +} +``` + +**2. 启动时动态生成**: +```python +async def _generate_embeddings_for_tags(self, interests: BotPersonalityInterests): + """为所有兴趣标签生成embedding(仅缓存在内存中)""" + for tag in interests.interest_tags: + if tag.tag_name in self.embedding_cache: + # 使用内存缓存 + tag.embedding = self.embedding_cache[tag.tag_name] + else: + # 动态生成新的embedding + embedding = await self._get_embedding(tag.tag_name) + tag.embedding = embedding # 设置到内存对象 + self.embedding_cache[tag.tag_name] = embedding # 缓存 +``` + +**3. 加载时处理**: +```python +tag = BotInterestTag( + tag_name=tag_data.get("tag_name", ""), + weight=tag_data.get("weight", 0.5), + expanded=tag_data.get("expanded"), + embedding=None, # 不从数据库加载,改为动态生成 + # ... +) +``` + +### 优势 +- ✅ **数据库轻量化**:数据库只存储标签名和权重等元数据 +- ✅ **避免写入失败**:不再因为数据过长导致数据库操作失败 +- ✅ **灵活性**:可以随时切换 embedding 模型而无需迁移数据 +- ✅ **性能**:内存缓存访问速度快 + +### 权衡 +- ⚠️ 启动时需要生成 embedding(首次启动稍慢,约10-20秒) +- ✅ 后续运行时使用内存缓存,性能与原来相当 + +--- + +## 🔧 任务3: 修复连续不回复阈值调整问题 + +### 问题描述 +原实现中,连续不回复调整只提升了分数,但阈值保持不变: +```python +# ❌ 错误的实现 +adjusted_score = self._apply_no_reply_boost(total_score) # 只提升分数 +should_reply = adjusted_score >= self.reply_threshold # 阈值不变 +``` + +**问题**:动作阈值(`non_reply_action_interest_threshold`)没有被调整,导致即使回复阈值满足,动作阈值可能仍然不满足。 + +### 解决方案 +改为**同时降低回复阈值和动作阈值**: + +```python +def _apply_no_reply_threshold_adjustment(self) -> 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 + + # 连续不回复的阈值降低 + if self.no_reply_count > 0: + no_reply_reduction = self.no_reply_count * self.probability_boost_per_no_reply + total_reduction += no_reply_reduction + + # 应用到两个阈值 + 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 +``` + +**使用**: +```python +# ✅ 正确的实现 +adjusted_reply_threshold, adjusted_action_threshold = self._apply_no_reply_threshold_adjustment() +should_reply = adjusted_score >= adjusted_reply_threshold +should_take_action = adjusted_score >= adjusted_action_threshold +``` + +### 优势 +- ✅ **逻辑一致**:回复阈值和动作阈值同步调整 +- ✅ **避免矛盾**:不会出现"满足回复但不满足动作"的情况 +- ✅ **更合理**:连续不回复时,bot更容易采取任何行动 + +--- + +## ⏱️ 任务4: 添加兴趣度计算超时机制 + +### 问题描述 +兴趣匹配计算调用 embedding API,可能因为网络问题或模型响应慢导致: +- ❌ 长时间等待(>5秒) +- ❌ 整体超时导致强制使用默认分值 +- ❌ **丢失了提及分和关系分**(因为整个计算被中断) + +### 解决方案 +为兴趣匹配计算添加**1.5秒超时保护**,超时时返回默认分值: + +```python +async def _calculate_interest_match_score(self, content: str, keywords: list[str] | None = None) -> float: + """计算兴趣匹配度(带超时保护)""" + try: + # 使用 asyncio.wait_for 添加1.5秒超时 + match_result = await asyncio.wait_for( + bot_interest_manager.calculate_interest_match(content, keywords or []), + timeout=1.5 + ) + + if match_result: + # 正常计算分数 + final_score = match_result.overall_score * 1.15 * match_result.confidence + match_count_bonus + return final_score + else: + return 0.0 + + except asyncio.TimeoutError: + # 超时时返回默认分值 0.5 + logger.warning("⏱️ 兴趣匹配计算超时(>1.5秒),返回默认分值0.5以保留其他分数") + return 0.5 # 避免丢失提及分和关系分 + + except Exception as e: + logger.warning(f"智能兴趣匹配失败: {e}") + return 0.0 +``` + +### 工作流程 +``` +正常情况(<1.5秒): + 兴趣匹配分: 0.8 + 关系分: 0.3 + 提及分: 2.5 = 3.6 ✅ + +超时情况(>1.5秒): + 兴趣匹配分: 0.5(默认)+ 关系分: 0.3 + 提及分: 2.5 = 3.3 ✅ + (保留了关系分和提及分) + +强制中断(无超时保护): + 整体计算失败 = 0.0(默认) ❌ + (丢失了所有分数) +``` + +### 优势 +- ✅ **防止阻塞**:不会因为一个API调用卡住整个流程 +- ✅ **保留分数**:即使兴趣匹配超时,提及分和关系分依然有效 +- ✅ **用户体验**:响应更快,不会长时间无反应 +- ✅ **降级优雅**:超时时仍能给出合理的默认值 + +--- + +## 🔄 任务5: 实现回复后阈值降低机制 + +### 需求背景 +**目标**:让bot在回复后更容易进行连续对话,提升对话的连贯性和自然性。 + +**场景示例**: +``` +用户: "你好呀" +Bot: "你好!今天过得怎么样?" ← 此时激活连续对话模式 + +用户: "还不错" +Bot: "那就好~有什么有趣的事情吗?" ← 阈值降低,更容易回复 + +用户: "没什么" +Bot: "嗯嗯,那要不要聊聊别的?" ← 仍然更容易回复 + +用户: "..." +(如果一直不回复,降低效果会逐渐衰减) +``` + +### 配置项 +在 `bot_config.toml` 中添加: + +```toml +# 回复后连续对话机制参数 +enable_post_reply_boost = true # 是否启用回复后阈值降低机制 +post_reply_threshold_reduction = 0.15 # 回复后初始阈值降低值 +post_reply_boost_max_count = 3 # 回复后阈值降低的最大持续次数 +post_reply_boost_decay_rate = 0.5 # 每次回复后阈值降低衰减率(0-1) +``` + +### 实现细节 + +**1. 初始化计数器**: +```python +def __init__(self): + # 回复后阈值降低机制 + 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 +``` + +**2. 阈值调整**: +```python +def _apply_no_reply_threshold_adjustment(self) -> tuple[float, float]: + """应用阈值调整""" + total_reduction = 0.0 + + # 1. 连续不回复的降低 + if self.no_reply_count > 0: + no_reply_reduction = self.no_reply_count * self.probability_boost_per_no_reply + total_reduction += no_reply_reduction + + # 2. 回复后的降低(带衰减) + 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 + + # 应用总降低量 + 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 +``` + +**3. 状态更新**: +```python +def on_reply_sent(self): + """当机器人发送回复后调用""" + if self.enable_post_reply_boost: + # 重置回复后降低计数器 + self.post_reply_boost_remaining = self.post_reply_boost_max_count + # 同时重置不回复计数 + self.no_reply_count = 0 + +def on_message_processed(self, replied: bool): + """消息处理完成后调用""" + # 更新不回复计数 + 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 +``` + +### 衰减机制说明 + +**衰减公式**: +``` +decay_factor = decay_rate ^ (max_count - remaining_count) +actual_reduction = base_reduction * decay_factor +``` + +**示例**(`base_reduction=0.15`, `decay_rate=0.5`, `max_count=3`): +``` +第1次回复后: decay_factor = 0.5^0 = 1.00, reduction = 0.15 * 1.00 = 0.15 +第2次回复后: decay_factor = 0.5^1 = 0.50, reduction = 0.15 * 0.50 = 0.075 +第3次回复后: decay_factor = 0.5^2 = 0.25, reduction = 0.15 * 0.25 = 0.0375 +``` + +### 实际效果 + +**配置示例**: +- 回复阈值: 0.7 +- 初始降低值: 0.15 +- 最大次数: 3 +- 衰减率: 0.5 + +**对话流程**: +``` +初始状态: + 回复阈值: 0.7 + +Bot发送回复 → 激活连续对话模式: + 剩余次数: 3 + +第1条消息: + 阈值降低: 0.15 + 实际阈值: 0.7 - 0.15 = 0.55 ✅ 更容易回复 + +第2条消息: + 阈值降低: 0.075 (衰减) + 实际阈值: 0.7 - 0.075 = 0.625 + +第3条消息: + 阈值降低: 0.0375 (继续衰减) + 实际阈值: 0.7 - 0.0375 = 0.6625 + +第4条消息: + 降低结束,恢复正常阈值: 0.7 +``` + +### 优势 +- ✅ **连贯对话**:bot回复后更容易继续对话 +- ✅ **自然衰减**:避免无限连续回复,逐渐恢复正常 +- ✅ **可配置**:可以根据需求调整降低值、次数和衰减率 +- ✅ **灵活控制**:可以随时启用/禁用此功能 + +--- + +## 📊 整体影响 + +### 性能优化 +- ✅ **内存优化**:不再在数据库中存储大量 embedding 数据 +- ✅ **响应速度**:超时保护避免长时间等待 +- ✅ **启动速度**:首次启动需要生成 embedding(10-20秒),后续运行使用缓存 + +### 功能增强 +- ✅ **阈值调整**:修复了回复和动作阈值不一致的问题 +- ✅ **连续对话**:新增回复后阈值降低机制,提升对话连贯性 +- ✅ **容错能力**:超时保护确保即使API失败也能保留其他分数 + +### 代码质量 +- ✅ **目录结构**:清晰的模块划分,易于维护 +- ✅ **可扩展性**:新功能可以轻松添加到对应目录 +- ✅ **可配置性**:关键参数可通过配置文件调整 + +--- + +## 🔧 使用说明 + +### 配置调整 + +在 `config/bot_config.toml` 中调整回复后连续对话参数: + +```toml +[affinity_flow] +# 回复后连续对话机制 +enable_post_reply_boost = true # 启用/禁用 +post_reply_threshold_reduction = 0.15 # 初始降低值(建议0.1-0.2) +post_reply_boost_max_count = 3 # 持续次数(建议2-5) +post_reply_boost_decay_rate = 0.5 # 衰减率(建议0.3-0.7) +``` + +### 调用方式 + +在 planner 或其他需要的地方调用: + +```python +# 计算兴趣值 +result = await interest_calculator.execute(message) + +# 消息处理完成后更新状态 +interest_calculator.on_message_processed(replied=result.should_reply) +``` + +--- + +## 🐛 已知问题 + +暂无 + +--- + +## 📝 后续优化建议 + +1. **监控日志**:观察实际使用中的阈值调整效果 +2. **A/B测试**:对比启用/禁用回复后降低机制的对话质量 +3. **参数调优**:根据实际使用情况调整默认配置值 +4. **性能监控**:监控 embedding 生成的时间和缓存命中率 + +--- + +## 👥 贡献者 + +- GitHub Copilot - 代码实现和文档编写 + +--- + +## 📅 更新历史 + +- 2025-11-03: 完成所有5个任务的实现 + - ✅ 优化插件目录结构 + - ✅ 修改 embedding 存储策略 + - ✅ 修复连续不回复阈值调整 + - ✅ 添加超时保护机制 + - ✅ 实现回复后阈值降低 diff --git a/docs/affinity_flow_guide.md b/docs/affinity_flow_guide.md index 31aa37cab..d2929572c 100644 --- a/docs/affinity_flow_guide.md +++ b/docs/affinity_flow_guide.md @@ -70,6 +70,11 @@ - `mention_bot_adjustment_threshold` 提及 Bot 后的调整阈值。当bot被提及后,回复阈值会改变为这个值。 +- `strong_mention_interest_score` + 强提及的兴趣分。强提及包括:被@、被回复、私聊消息。这类提及表示用户明确想与bot交互。 + +- `weak_mention_interest_score` + 弱提及的兴趣分。弱提及包括:消息中包含bot的名字或别名(文本匹配)。这类提及可能只是在讨论中提到bot。 - `base_relationship_score` --- @@ -80,13 +85,16 @@ 2. **Bot 太热情/回复太多** - 提高 `reply_action_interest_threshold`,或降低关键词相关倍率。 -3. **希望 Bot 更关注被 @ 的消息** - - 提高 `mention_bot_interest_score` 或 `mention_bot_weight`。 +3. **希望 Bot 更关注被 @ 或回复的消息** + - 提高 `strong_mention_interest_score` 或 `mention_bot_weight`。 -4. **希望 Bot 更看重关系好的用户** +4. **希望 Bot 对文本提及也积极回应** + - 提高 `weak_mention_interest_score`。 + +5. **希望 Bot 更看重关系好的用户** - 提高 `relationship_weight` 或 `base_relationship_score`。 -5. **表情包行为过于频繁/稀少** +6. **表情包行为过于频繁/稀少** - 调整 `non_reply_action_interest_threshold`。 --- @@ -121,7 +129,8 @@ keyword_match_weight = 0.4 mention_bot_weight = 0.3 relationship_weight = 0.3 mention_bot_adjustment_threshold = 0.5 -mention_bot_interest_score = 2.5 +strong_mention_interest_score = 2.5 # 强提及(@/回复/私聊) +weak_mention_interest_score = 1.5 # 弱提及(文本匹配) base_relationship_score = 0.3 ``` @@ -134,7 +143,10 @@ MoFox-Bot 在收到每条消息时,会通过一套“兴趣度评分(afc) - 不同匹配度的关键词会乘以对应的倍率(high/medium/low_match_keyword_multiplier),并根据匹配数量叠加加成(match_count_bonus,max_match_bonus)。 ### 2. 提及与关系加分 -- 如果消息中提及了 Bot(如被@),会直接获得一部分兴趣分(mention_bot_interest_score),并按权重(mention_bot_weight)计入总分。 +- 如果消息中提及了 Bot,会根据提及类型获得不同的兴趣分: + * **强提及**(被@、被回复、私聊): 获得 `strong_mention_interest_score` 分值,表示用户明确想与bot交互 + * **弱提及**(文本中包含bot名字或别名): 获得 `weak_mention_interest_score` 分值,表示在讨论中提到bot + * 提及分按权重(`mention_bot_weight`)计入总分 - 与用户的关系分(base_relationship_score 及动态关系分)也会按 relationship_weight 计入总分。 ### 3. 综合评分计算 diff --git a/src/chat/interest_system/bot_interest_manager.py b/src/chat/interest_system/bot_interest_manager.py index ca49d63de..aca3a6d2f 100644 --- a/src/chat/interest_system/bot_interest_manager.py +++ b/src/chat/interest_system/bot_interest_manager.py @@ -26,6 +26,8 @@ class BotInterestManager: def __init__(self): self.current_interests: BotPersonalityInterests | None = None self.embedding_cache: dict[str, list[float]] = {} # embedding缓存 + self.expanded_tag_cache: dict[str, str] = {} # 扩展标签缓存 + self.expanded_embedding_cache: dict[str, list[float]] = {} # 扩展标签的embedding缓存 self._initialized = False # Embedding客户端配置 @@ -169,22 +171,47 @@ class BotInterestManager: 1. 标签应该符合人设特点和性格 2. 每个标签都有权重(0.1-1.0),表示对该兴趣的喜好程度 3. 生成15-25个不等的标签 -4. 标签应该是具体的关键词,而不是抽象概念 -5. 每个标签的长度不超过10个字符 +4. 每个标签包含两个部分: + - name: 简短的标签名(2-6个字符),用于显示和管理,如"Python"、"追番"、"撸猫" + - expanded: 完整的描述性文本(20-50个字符),用于语义匹配,描述这个兴趣的具体内容和场景 +5. expanded 扩展描述要求: + - 必须是完整的句子或短语,包含丰富的语义信息 + - 描述具体的对话场景、活动内容、相关话题 + - 避免过于抽象,要有明确的语境 + - 示例: + * "Python" -> "讨论Python编程语言、写Python代码、Python脚本开发、Python技术问题" + * "追番" -> "讨论正在播出的动漫番剧、追番进度、动漫剧情、番剧推荐、动漫角色" + * "撸猫" -> "讨论猫咪宠物、晒猫分享、萌宠日常、可爱猫猫、养猫心得" + * "社恐" -> "表达社交焦虑、不想见人、想躲起来、害怕社交的心情" + * "深夜码代码" -> "深夜写代码、熬夜编程、夜猫子程序员、深夜调试bug" 请以JSON格式返回,格式如下: {{ "interests": [ - {{"name": "标签名", "weight": 0.8}}, - {{"name": "标签名", "weight": 0.6}}, - {{"name": "标签名", "weight": 0.9}} + {{ + "name": "Python", + "expanded": "讨论Python编程语言、写Python代码、Python脚本开发、Python技术问题", + "weight": 0.9 + }}, + {{ + "name": "追番", + "expanded": "讨论正在播出的动漫番剧、追番进度、动漫剧情、番剧推荐、动漫角色", + "weight": 0.85 + }}, + {{ + "name": "撸猫", + "expanded": "讨论猫咪宠物、晒猫分享、萌宠日常、可爱猫猫、养猫心得", + "weight": 0.95 + }} ] }} 注意: -- 权重范围0.1-1.0,权重越高表示越感兴趣 -- 标签要具体,如"编程"、"游戏"、"旅行"等 -- 根据人设生成个性化的标签 +- name: 简短标签名,2-6个字符,方便显示 +- expanded: 完整描述,20-50个字符,用于精准的语义匹配 +- weight: 权重范围0.1-1.0,权重越高表示越感兴趣 +- 根据人设生成个性化、具体的标签和描述 +- expanded 描述要有具体场景,避免泛化 """ # 调用LLM生成兴趣标签 @@ -211,16 +238,22 @@ class BotInterestManager: for i, tag_data in enumerate(interests_list): tag_name = tag_data.get("name", f"标签_{i}") weight = tag_data.get("weight", 0.5) + expanded = tag_data.get("expanded") # 获取扩展描述 # 检查标签长度,如果过长则截断 if len(tag_name) > 10: logger.warning(f"⚠️ 标签 '{tag_name}' 过长,将截断为10个字符") tag_name = tag_name[:10] - tag = BotInterestTag(tag_name=tag_name, weight=weight) - bot_interests.interest_tags.append(tag) + # 验证扩展描述 + if expanded: + logger.debug(f" 🏷️ {tag_name} (权重: {weight:.2f})") + logger.debug(f" 📝 扩展: {expanded}") + else: + logger.warning(f" ⚠️ 标签 '{tag_name}' 缺少扩展描述,将使用回退方案") - logger.debug(f" 🏷️ {tag_name} (权重: {weight:.2f})") + tag = BotInterestTag(tag_name=tag_name, weight=weight, expanded=expanded) + bot_interests.interest_tags.append(tag) # 为所有标签生成embedding logger.info("🧠 开始为兴趣标签生成embedding向量...") @@ -284,12 +317,12 @@ class BotInterestManager: return None async def _generate_embeddings_for_tags(self, interests: BotPersonalityInterests): - """为所有兴趣标签生成embedding""" + """为所有兴趣标签生成embedding(仅缓存在内存中)""" if not hasattr(self, "embedding_request"): raise RuntimeError("❌ Embedding客户端未初始化,无法生成embedding") total_tags = len(interests.interest_tags) - logger.info(f"🧠 开始为 {total_tags} 个兴趣标签生成embedding向量...") + logger.info(f"🧠 开始为 {total_tags} 个兴趣标签生成embedding向量(动态生成,仅内存缓存)...") cached_count = 0 generated_count = 0 @@ -297,22 +330,22 @@ class BotInterestManager: for i, tag in enumerate(interests.interest_tags, 1): if tag.tag_name in self.embedding_cache: - # 使用缓存的embedding + # 使用内存缓存的embedding tag.embedding = self.embedding_cache[tag.tag_name] cached_count += 1 - logger.debug(f" [{i}/{total_tags}] 🏷️ '{tag.tag_name}' - 使用缓存") + logger.debug(f" [{i}/{total_tags}] 🏷️ '{tag.tag_name}' - 使用内存缓存") else: - # 生成新的embedding + # 动态生成新的embedding embedding_text = tag.tag_name - logger.debug(f" [{i}/{total_tags}] 🔄 正在为 '{tag.tag_name}' 生成embedding...") + logger.debug(f" [{i}/{total_tags}] 🔄 正在为 '{tag.tag_name}' 动态生成embedding...") embedding = await self._get_embedding(embedding_text) if embedding: - tag.embedding = embedding - self.embedding_cache[tag.tag_name] = embedding + tag.embedding = embedding # 设置到 tag 对象(内存中) + self.embedding_cache[tag.tag_name] = embedding # 同时缓存 generated_count += 1 - logger.debug(f" ✅ '{tag.tag_name}' embedding生成成功") + logger.debug(f" ✅ '{tag.tag_name}' embedding动态生成成功并缓存到内存") else: failed_count += 1 logger.warning(f" ❌ '{tag.tag_name}' embedding生成失败") @@ -322,12 +355,12 @@ class BotInterestManager: interests.last_updated = datetime.now() logger.info("=" * 50) - logger.info("✅ Embedding生成完成!") + logger.info("✅ Embedding动态生成完成(仅存储在内存中)!") logger.info(f"📊 总标签数: {total_tags}") - logger.info(f"💾 缓存命中: {cached_count}") + logger.info(f"💾 内存缓存命中: {cached_count}") logger.info(f"🆕 新生成: {generated_count}") logger.info(f"❌ 失败: {failed_count}") - logger.info(f"🗃️ 总缓存大小: {len(self.embedding_cache)}") + logger.info(f"🗃️ 内存缓存总大小: {len(self.embedding_cache)}") logger.info("=" * 50) async def _get_embedding(self, text: str) -> list[float]: @@ -421,7 +454,19 @@ class BotInterestManager: async def calculate_interest_match( self, message_text: str, keywords: list[str] | None = None ) -> InterestMatchResult: - """计算消息与机器人兴趣的匹配度""" + """计算消息与机器人兴趣的匹配度(优化版 - 标签扩展策略) + + 核心优化:将短标签扩展为完整的描述性句子,解决语义粒度不匹配问题 + + 原问题: + - 消息: "今天天气不错" (完整句子) + - 标签: "蹭人治愈" (2-4字短语) + - 结果: 误匹配,因为短标签的 embedding 过于抽象 + + 解决方案: + - 标签扩展: "蹭人治愈" -> "表达亲近、寻求安慰、撒娇的内容" + - 现在是: 句子 vs 句子,匹配更准确 + """ if not self.current_interests or not self._initialized: raise RuntimeError("❌ 兴趣标签系统未初始化") @@ -442,13 +487,13 @@ class BotInterestManager: message_embedding = await self._get_embedding(message_text) logger.debug(f"消息 embedding 生成成功, 维度: {len(message_embedding)}") - # 计算与每个兴趣标签的相似度 + # 计算与每个兴趣标签的相似度(使用扩展标签) match_count = 0 high_similarity_count = 0 medium_similarity_count = 0 low_similarity_count = 0 - # 分级相似度阈值 + # 分级相似度阈值 - 优化后可以提高阈值,因为匹配更准确了 affinity_config = global_config.affinity_flow high_threshold = affinity_config.high_match_interest_threshold medium_threshold = affinity_config.medium_match_interest_threshold @@ -458,27 +503,45 @@ class BotInterestManager: for tag in active_tags: if tag.embedding: - similarity = self._calculate_cosine_similarity(message_embedding, tag.embedding) + # 🔧 优化:获取扩展标签的 embedding(带缓存) + expanded_embedding = await self._get_expanded_tag_embedding(tag.tag_name) + + if expanded_embedding: + # 使用扩展标签的 embedding 进行匹配 + similarity = self._calculate_cosine_similarity(message_embedding, expanded_embedding) + + # 同时计算原始标签的相似度作为参考 + original_similarity = self._calculate_cosine_similarity(message_embedding, tag.embedding) + + # 混合策略:扩展标签权重更高(70%),原始标签作为补充(30%) + # 这样可以兼顾准确性(扩展)和灵活性(原始) + final_similarity = similarity * 0.7 + original_similarity * 0.3 + + logger.debug(f"标签'{tag.tag_name}': 原始={original_similarity:.3f}, 扩展={similarity:.3f}, 最终={final_similarity:.3f}") + else: + # 如果扩展 embedding 获取失败,使用原始 embedding + final_similarity = self._calculate_cosine_similarity(message_embedding, tag.embedding) + logger.debug(f"标签'{tag.tag_name}': 使用原始相似度={final_similarity:.3f}") # 基础加权分数 - weighted_score = similarity * tag.weight + weighted_score = final_similarity * tag.weight # 根据相似度等级应用不同的加成 - if similarity > high_threshold: + if final_similarity > high_threshold: # 高相似度:强加成 enhanced_score = weighted_score * affinity_config.high_match_keyword_multiplier match_count += 1 high_similarity_count += 1 result.add_match(tag.tag_name, enhanced_score, [tag.tag_name]) - elif similarity > medium_threshold: + elif final_similarity > medium_threshold: # 中相似度:中等加成 enhanced_score = weighted_score * affinity_config.medium_match_keyword_multiplier match_count += 1 medium_similarity_count += 1 result.add_match(tag.tag_name, enhanced_score, [tag.tag_name]) - elif similarity > low_threshold: + elif final_similarity > low_threshold: # 低相似度:轻微加成 enhanced_score = weighted_score * affinity_config.low_match_keyword_multiplier match_count += 1 @@ -520,6 +583,121 @@ class BotInterestManager: ) return result + async def _get_expanded_tag_embedding(self, tag_name: str) -> list[float] | None: + """获取扩展标签的 embedding(带缓存) + + 优先使用缓存,如果没有则生成并缓存 + """ + # 检查缓存 + if tag_name in self.expanded_embedding_cache: + return self.expanded_embedding_cache[tag_name] + + # 扩展标签 + expanded_tag = self._expand_tag_for_matching(tag_name) + + # 生成 embedding + try: + embedding = await self._get_embedding(expanded_tag) + if embedding: + # 缓存结果 + self.expanded_tag_cache[tag_name] = expanded_tag + self.expanded_embedding_cache[tag_name] = embedding + logger.debug(f"✅ 为标签'{tag_name}'生成并缓存扩展embedding: {expanded_tag[:50]}...") + return embedding + except Exception as e: + logger.warning(f"为标签'{tag_name}'生成扩展embedding失败: {e}") + + return None + + def _expand_tag_for_matching(self, tag_name: str) -> str: + """将短标签扩展为完整的描述性句子 + + 这是解决"标签太短导致误匹配"的核心方法 + + 策略: + 1. 优先使用 LLM 生成的 expanded 字段(最准确) + 2. 如果没有,使用基于规则的回退方案 + 3. 最后使用通用模板 + + 示例: + - "Python" + expanded -> "讨论Python编程语言、写Python代码、Python脚本开发、Python技术问题" + - "蹭人治愈" + expanded -> "想要获得安慰、寻求温暖关怀、撒娇卖萌、表达亲昵、求抱抱求陪伴的对话" + """ + # 使用缓存 + if tag_name in self.expanded_tag_cache: + return self.expanded_tag_cache[tag_name] + + # 🎯 优先策略:使用 LLM 生成的 expanded 字段 + if self.current_interests: + for tag in self.current_interests.interest_tags: + if tag.tag_name == tag_name and tag.expanded: + logger.debug(f"✅ 使用LLM生成的扩展描述: {tag_name} -> {tag.expanded[:50]}...") + self.expanded_tag_cache[tag_name] = tag.expanded + return tag.expanded + + # 🔧 回退策略:基于规则的扩展(用于兼容旧数据或LLM未生成扩展的情况) + logger.debug(f"⚠️ 标签'{tag_name}'没有LLM扩展描述,使用规则回退方案") + tag_lower = tag_name.lower() + + # 技术编程类标签(具体化描述) + if any(word in tag_lower for word in ['python', 'java', 'code', '代码', '编程', '脚本', '算法', '开发']): + if 'python' in tag_lower: + return f"讨论Python编程语言、写Python代码、Python脚本开发、Python技术问题" + elif '算法' in tag_lower: + return f"讨论算法题目、数据结构、编程竞赛、刷LeetCode题目、代码优化" + elif '代码' in tag_lower or '被窝' in tag_lower: + return f"讨论写代码、编程开发、代码实现、技术方案、编程技巧" + else: + return f"讨论编程开发、软件技术、代码编写、技术实现" + + # 情感表达类标签(具体化为真实对话场景) + elif any(word in tag_lower for word in ['治愈', '撒娇', '安慰', '呼噜', '蹭', '卖萌']): + return f"想要获得安慰、寻求温暖关怀、撒娇卖萌、表达亲昵、求抱抱求陪伴的对话" + + # 游戏娱乐类标签(具体游戏场景) + elif any(word in tag_lower for word in ['游戏', '网游', 'mmo', '游', '玩']): + return f"讨论网络游戏、MMO游戏、游戏玩法、组队打副本、游戏攻略心得" + + # 动漫影视类标签(具体观看行为) + elif any(word in tag_lower for word in ['番', '动漫', '视频', 'b站', '弹幕', '追番', '云新番']): + # 特别处理"云新番" - 它的意思是在网上看新动漫,不是泛泛的"新东西" + if '云' in tag_lower or '新番' in tag_lower: + return f"讨论正在播出的新动漫、新番剧集、动漫剧情、追番心得、动漫角色" + else: + return f"讨论动漫番剧内容、B站视频、弹幕文化、追番体验" + + # 社交平台类标签(具体平台行为) + elif any(word in tag_lower for word in ['小红书', '贴吧', '论坛', '社区', '吃瓜', '八卦']): + if '吃瓜' in tag_lower: + return f"聊八卦爆料、吃瓜看热闹、网络热点事件、社交平台热议话题" + else: + return f"讨论社交平台内容、网络社区话题、论坛讨论、分享生活" + + # 生活日常类标签(具体萌宠场景) + elif any(word in tag_lower for word in ['猫', '宠物', '尾巴', '耳朵', '毛绒']): + return f"讨论猫咪宠物、晒猫分享、萌宠日常、可爱猫猫、养猫心得" + + # 状态心情类标签(具体情绪状态) + elif any(word in tag_lower for word in ['社恐', '隐身', '流浪', '深夜', '被窝']): + if '社恐' in tag_lower: + return f"表达社交焦虑、不想见人、想躲起来、害怕社交的心情" + elif '深夜' in tag_lower: + return f"深夜睡不着、熬夜、夜猫子、深夜思考人生的对话" + else: + return f"表达当前心情状态、个人感受、生活状态" + + # 物品装备类标签(具体使用场景) + elif any(word in tag_lower for word in ['键盘', '耳机', '装备', '设备']): + return f"讨论键盘耳机装备、数码产品、使用体验、装备推荐评测" + + # 互动关系类标签 + elif any(word in tag_lower for word in ['拾风', '互怼', '互动']): + return f"聊天互动、开玩笑、友好互怼、日常对话交流" + + # 默认:尽量具体化 + else: + return f"明确讨论{tag_name}这个特定主题的具体内容和相关话题" + def _calculate_keyword_match_bonus(self, keywords: list[str], matched_tags: list[str]) -> dict[str, float]: """计算关键词直接匹配奖励""" if not keywords or not matched_tags: @@ -668,11 +846,12 @@ class BotInterestManager: last_updated=db_interests.last_updated, ) - # 解析兴趣标签 + # 解析兴趣标签(embedding 从数据库加载后会被忽略,因为我们不再存储它) for tag_data in tags_data: tag = BotInterestTag( tag_name=tag_data.get("tag_name", ""), weight=tag_data.get("weight", 0.5), + expanded=tag_data.get("expanded"), # 加载扩展描述 created_at=datetime.fromisoformat( tag_data.get("created_at", datetime.now().isoformat()) ), @@ -680,11 +859,11 @@ class BotInterestManager: tag_data.get("updated_at", datetime.now().isoformat()) ), is_active=tag_data.get("is_active", True), - embedding=tag_data.get("embedding"), + embedding=None, # 不再从数据库加载 embedding,改为动态生成 ) interests.interest_tags.append(tag) - logger.debug(f"成功解析 {len(interests.interest_tags)} 个兴趣标签") + logger.debug(f"成功解析 {len(interests.interest_tags)} 个兴趣标签(embedding 将在初始化时动态生成)") return interests except (orjson.JSONDecodeError, Exception) as e: @@ -715,16 +894,17 @@ class BotInterestManager: from src.common.database.compatibility import get_db_session from src.common.database.core.models import BotPersonalityInterests as DBBotPersonalityInterests - # 将兴趣标签转换为JSON格式 + # 将兴趣标签转换为JSON格式(不再保存embedding,启动时动态生成) tags_data = [] for tag in interests.interest_tags: tag_dict = { "tag_name": tag.tag_name, "weight": tag.weight, + "expanded": tag.expanded, # 保存扩展描述 "created_at": tag.created_at.isoformat(), "updated_at": tag.updated_at.isoformat(), "is_active": tag.is_active, - "embedding": tag.embedding, + # embedding 不再存储到数据库,改为内存缓存 } tags_data.append(tag_dict) diff --git a/src/chat/message_receive/message_processor.py b/src/chat/message_receive/message_processor.py index 09b5aba7d..889a74f50 100644 --- a/src/chat/message_receive/message_processor.py +++ b/src/chat/message_receive/message_processor.py @@ -196,10 +196,18 @@ async def _process_single_segment(segment: Seg, state: dict, message_info: BaseM state["is_emoji"] = False state["is_video"] = False state["is_at"] = True - # 处理at消息,格式为"昵称:QQ号" - if isinstance(segment.data, str) and ":" in segment.data: - nickname, qq_id = segment.data.split(":", 1) - return f"@{nickname}" + # 处理at消息,格式为"@<昵称:QQ号>" + if isinstance(segment.data, str): + if ":" in segment.data: + # 标准格式: "昵称:QQ号" + nickname, qq_id = segment.data.split(":", 1) + result = f"@<{nickname}:{qq_id}>" + logger.info(f"[at处理] 标准格式 -> {result}") + return result + else: + logger.warning(f"[at处理] 无法解析格式: '{segment.data}'") + return f"@{segment.data}" + logger.warning(f"[at处理] 数据类型异常: {type(segment.data)}") return f"@{segment.data}" if isinstance(segment.data, str) else "@未知用户" elif segment.type == "image": diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index f4e6edac9..b7a32e329 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -49,23 +49,22 @@ def is_mentioned_bot_in_message(message) -> tuple[bool, float]: message: DatabaseMessages 消息对象 Returns: - tuple[bool, float]: (是否提及, 提及概率) + tuple[bool, float]: (是否提及, 提及类型) + 提及类型: 0=未提及, 1=弱提及(文本匹配), 2=强提及(@/回复/私聊) """ - keywords = [global_config.bot.nickname] nicknames = global_config.bot.alias_names - reply_probability = 0.0 - is_at = False - is_mentioned = False + mention_type = 0 # 0=未提及, 1=弱提及, 2=强提及 - # 检查 is_mentioned 属性 + # 检查 is_mentioned 属性(保持向后兼容) mentioned_attr = getattr(message, "is_mentioned", None) if mentioned_attr is not None: try: - return bool(mentioned_attr), float(mentioned_attr) + # 如果已有 is_mentioned,直接返回(假设是强提及) + return bool(mentioned_attr), 2.0 if mentioned_attr else 0.0 except (ValueError, TypeError): pass - # 检查 additional_config + # 检查 additional_config(保持向后兼容) additional_config = None # DatabaseMessages: additional_config 是 JSON 字符串 @@ -78,62 +77,66 @@ def is_mentioned_bot_in_message(message) -> tuple[bool, float]: if additional_config and additional_config.get("is_mentioned") is not None: try: - reply_probability = float(additional_config.get("is_mentioned")) # type: ignore - is_mentioned = True - return is_mentioned, reply_probability + mentioned_value = float(additional_config.get("is_mentioned")) # type: ignore + # 如果配置中有提及值,假设是强提及 + return True, 2.0 if mentioned_value > 0 else 0.0 except Exception as e: logger.warning(str(e)) logger.warning( f"消息中包含不合理的设置 is_mentioned: {additional_config.get('is_mentioned')}" ) - # 检查消息文本内容 processed_text = message.processed_plain_text or "" - if global_config.bot.nickname in processed_text: - is_mentioned = True - - for alias_name in global_config.bot.alias_names: - if alias_name in processed_text: - is_mentioned = True - - # 判断是否被@ - if re.search(rf"@<(.+?):{global_config.bot.qq_account}>", message.processed_plain_text): + + # 1. 判断是否为私聊(强提及) + group_info = getattr(message, "group_info", None) + if not group_info or not getattr(group_info, "group_id", None): + is_private = True + mention_type = 2 + logger.debug("检测到私聊消息 - 强提及") + + # 2. 判断是否被@(强提及) + if re.search(rf"@<(.+?):{global_config.bot.qq_account}>", processed_text): is_at = True - is_mentioned = True - - # print(f"message.processed_plain_text: {message.processed_plain_text}") - # print(f"is_mentioned: {is_mentioned}") - # print(f"is_at: {is_at}") - - if is_at and global_config.chat.at_bot_inevitable_reply: - reply_probability = 1.0 - logger.debug("被@,回复概率设置为100%") - else: - if not is_mentioned: - # 判断是否被回复 - if re.match( - rf"\[回复 (.+?)\({global_config.bot.qq_account!s}\):(.+?)\],说:", message.processed_plain_text - ) or re.match( - rf"\[回复<(.+?)(?=:{global_config.bot.qq_account!s}>)\:{global_config.bot.qq_account!s}>:(.+?)\],说:", - message.processed_plain_text, - ): - is_mentioned = True - else: - # 判断内容中是否被提及 - message_content = re.sub(r"@(.+?)((\d+))", "", message.processed_plain_text) - message_content = re.sub(r"@<(.+?)(?=:(\d+))\:(\d+)>", "", message_content) - message_content = re.sub(r"\[回复 (.+?)\(((\d+)|未知id)\):(.+?)\],说:", "", message_content) - message_content = re.sub(r"\[回复<(.+?)(?=:(\d+))\:(\d+)>:(.+?)\],说:", "", message_content) - for keyword in keywords: - if keyword in message_content: - is_mentioned = True - for nickname in nicknames: - if nickname in message_content: - is_mentioned = True - if is_mentioned and global_config.chat.mentioned_bot_inevitable_reply: - reply_probability = 1.0 - logger.debug("被提及,回复概率设置为100%") - return is_mentioned, reply_probability + mention_type = 2 + logger.debug("检测到@提及 - 强提及") + + # 3. 判断是否被回复(强提及) + if re.match( + rf"\[回复 (.+?)\({global_config.bot.qq_account!s}\):(.+?)\],说:", processed_text + ) or re.match( + rf"\[回复<(.+?)(?=:{global_config.bot.qq_account!s}>)\:{global_config.bot.qq_account!s}>:(.+?)\],说:", + processed_text, + ): + is_replied = True + mention_type = 2 + logger.debug("检测到回复消息 - 强提及") + + # 4. 判断文本中是否提及bot名字或别名(弱提及) + if mention_type == 0: # 只有在没有强提及时才检查弱提及 + # 移除@和回复标记后再检查 + message_content = re.sub(r"@(.+?)((\d+))", "", processed_text) + message_content = re.sub(r"@<(.+?)(?=:(\d+))\:(\d+)>", "", message_content) + message_content = re.sub(r"\[回复 (.+?)\(((\d+)|未知id)\):(.+?)\],说:", "", message_content) + message_content = re.sub(r"\[回复<(.+?)(?=:(\d+))\:(\d+)>:(.+?)\],说:", "", message_content) + + # 检查bot主名字 + if global_config.bot.nickname in message_content: + is_text_mentioned = True + mention_type = 1 + logger.debug(f"检测到文本提及bot主名字 '{global_config.bot.nickname}' - 弱提及") + # 如果主名字没匹配,再检查别名 + elif nicknames: + for alias_name in nicknames: + if alias_name in message_content: + is_text_mentioned = True + mention_type = 1 + logger.debug(f"检测到文本提及bot别名 '{alias_name}' - 弱提及") + break + + # 返回结果 + is_mentioned = mention_type > 0 + return is_mentioned, float(mention_type) async def get_embedding(text, request_type="embedding") -> list[float] | None: """获取文本的embedding向量""" diff --git a/src/common/data_models/bot_interest_data_model.py b/src/common/data_models/bot_interest_data_model.py index 769139058..8bd8fa5a0 100644 --- a/src/common/data_models/bot_interest_data_model.py +++ b/src/common/data_models/bot_interest_data_model.py @@ -16,6 +16,7 @@ class BotInterestTag(BaseDataModel): tag_name: str weight: float = 1.0 # 权重,表示对这个兴趣的喜好程度 (0.0-1.0) + expanded: str | None = None # 标签的扩展描述,用于更精准的语义匹配 embedding: list[float] | None = None # 标签的embedding向量 created_at: datetime = field(default_factory=datetime.now) updated_at: datetime = field(default_factory=datetime.now) @@ -26,6 +27,7 @@ class BotInterestTag(BaseDataModel): return { "tag_name": self.tag_name, "weight": self.weight, + "expanded": self.expanded, "embedding": self.embedding, "created_at": self.created_at.isoformat(), "updated_at": self.updated_at.isoformat(), @@ -38,6 +40,7 @@ class BotInterestTag(BaseDataModel): return cls( tag_name=data["tag_name"], weight=data.get("weight", 1.0), + expanded=data.get("expanded"), embedding=data.get("embedding"), created_at=datetime.fromisoformat(data["created_at"]) if data.get("created_at") else datetime.now(), updated_at=datetime.fromisoformat(data["updated_at"]) if data.get("updated_at") else datetime.now(), diff --git a/src/config/official_configs.py b/src/config/official_configs.py index c63df290d..fd02bcebb 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -703,6 +703,12 @@ class AffinityFlowConfig(ValidatedConfigBase): reply_cooldown_reduction: int = Field(default=2, description="回复后减少的不回复计数") max_no_reply_count: int = Field(default=5, description="最大不回复计数次数") + # 回复后连续对话机制参数 + enable_post_reply_boost: bool = Field(default=True, description="是否启用回复后阈值降低机制,使bot在回复后更容易进行连续对话") + post_reply_threshold_reduction: float = Field(default=0.15, description="回复后初始阈值降低值(建议0.1-0.2)") + post_reply_boost_max_count: int = Field(default=3, description="回复后阈值降低的最大持续次数(建议2-5)") + post_reply_boost_decay_rate: float = Field(default=0.5, description="每次回复后阈值降低衰减率(0-1,建议0.3-0.7)") + # 综合评分权重 keyword_match_weight: float = Field(default=0.4, description="兴趣关键词匹配度权重") mention_bot_weight: float = Field(default=0.3, description="提及bot分数权重") @@ -710,7 +716,9 @@ class AffinityFlowConfig(ValidatedConfigBase): # 提及bot相关参数 mention_bot_adjustment_threshold: float = Field(default=0.3, description="提及bot后的调整阈值") - mention_bot_interest_score: float = Field(default=0.6, description="提及bot的兴趣分") + mention_bot_interest_score: float = Field(default=0.6, description="提及bot的兴趣分(已弃用,改用strong/weak_mention)") + strong_mention_interest_score: float = Field(default=2.5, description="强提及的兴趣分(被@、被回复、私聊)") + weak_mention_interest_score: float = Field(default=1.5, description="弱提及的兴趣分(文本匹配bot名字或别名)") base_relationship_score: float = Field(default=0.5, description="基础人物关系分") # 关系追踪系统参数 diff --git a/src/individuality/individuality.py b/src/individuality/individuality.py index eebdad9ad..3ef490e57 100644 --- a/src/individuality/individuality.py +++ b/src/individuality/individuality.py @@ -33,9 +33,14 @@ class Individuality: personality_side = global_config.personality.personality_side identity = global_config.personality.identity - person_info_manager = get_person_info_manager() - self.bot_person_id = person_info_manager.get_person_id("system", "bot_id") + # 基于人设文本生成 personality_id(使用 MD5 hash) + # 这样当人设发生变化时会自动生成新的 ID,触发重新生成兴趣标签 + personality_hash, _ = self._get_config_hash(bot_nickname, personality_core, personality_side, identity) + self.bot_person_id = personality_hash self.name = bot_nickname + logger.info(f"生成的 personality_id: {self.bot_person_id[:16]}... (基于人设文本 hash)") + + person_info_manager = get_person_info_manager() # 检查配置变化,如果变化则清空 personality_changed, identity_changed = await self._check_config_and_clear_if_changed( @@ -72,8 +77,8 @@ class Individuality: if personality_changed or identity_changed: logger.info("将清空数据库中原有的关键词缓存") update_data = { - "platform": "system", - "user_id": "bot_id", + "platform": "personality", + "user_id": self.bot_person_id, # 使用基于人设生成的 ID "person_name": self.name, "nickname": self.name, } @@ -171,8 +176,8 @@ class Individuality: if personality_changed or identity_changed: logger.info("将清空原有的关键词缓存") update_data = { - "platform": "system", - "user_id": "bot_id", + "platform": "personality", + "user_id": current_personality_hash, # 使用 personality hash 作为 user_id "person_name": self.name, "nickname": self.name, } diff --git a/src/plugins/built_in/affinity_flow_chatter/core/__init__.py b/src/plugins/built_in/affinity_flow_chatter/core/__init__.py new file mode 100644 index 000000000..36909697d --- /dev/null +++ b/src/plugins/built_in/affinity_flow_chatter/core/__init__.py @@ -0,0 +1,10 @@ +""" +AffinityFlow Chatter 核心模块 + +包含兴趣度计算器和核心对话处理逻辑 +""" + +from .affinity_chatter import AffinityChatter +from .affinity_interest_calculator import AffinityInterestCalculator + +__all__ = ["AffinityChatter", "AffinityInterestCalculator"] diff --git a/src/plugins/built_in/affinity_flow_chatter/affinity_chatter.py b/src/plugins/built_in/affinity_flow_chatter/core/affinity_chatter.py similarity index 98% rename from src/plugins/built_in/affinity_flow_chatter/affinity_chatter.py rename to src/plugins/built_in/affinity_flow_chatter/core/affinity_chatter.py index 73fe374d5..aebe1254a 100644 --- a/src/plugins/built_in/affinity_flow_chatter/affinity_chatter.py +++ b/src/plugins/built_in/affinity_flow_chatter/core/affinity_chatter.py @@ -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") diff --git a/src/plugins/built_in/affinity_flow_chatter/affinity_interest_calculator.py b/src/plugins/built_in/affinity_flow_chatter/core/affinity_interest_calculator.py similarity index 65% rename from src/plugins/built_in/affinity_flow_chatter/affinity_interest_calculator.py rename to src/plugins/built_in/affinity_flow_chatter/core/affinity_interest_calculator.py index 059a1f762..91a9f14c7 100644 --- a/src/plugins/built_in/affinity_flow_chatter/affinity_interest_calculator.py +++ b/src/plugins/built_in/affinity_flow_chatter/core/affinity_interest_calculator.py @@ -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}" + ) diff --git a/src/plugins/built_in/affinity_flow_chatter/planner/__init__.py b/src/plugins/built_in/affinity_flow_chatter/planner/__init__.py new file mode 100644 index 000000000..95a7d90ff --- /dev/null +++ b/src/plugins/built_in/affinity_flow_chatter/planner/__init__.py @@ -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"] diff --git a/src/plugins/built_in/affinity_flow_chatter/plan_executor.py b/src/plugins/built_in/affinity_flow_chatter/planner/plan_executor.py similarity index 100% rename from src/plugins/built_in/affinity_flow_chatter/plan_executor.py rename to src/plugins/built_in/affinity_flow_chatter/planner/plan_executor.py diff --git a/src/plugins/built_in/affinity_flow_chatter/plan_filter.py b/src/plugins/built_in/affinity_flow_chatter/planner/plan_filter.py similarity index 100% rename from src/plugins/built_in/affinity_flow_chatter/plan_filter.py rename to src/plugins/built_in/affinity_flow_chatter/planner/plan_filter.py diff --git a/src/plugins/built_in/affinity_flow_chatter/plan_generator.py b/src/plugins/built_in/affinity_flow_chatter/planner/plan_generator.py similarity index 100% rename from src/plugins/built_in/affinity_flow_chatter/plan_generator.py rename to src/plugins/built_in/affinity_flow_chatter/planner/plan_generator.py diff --git a/src/plugins/built_in/affinity_flow_chatter/planner.py b/src/plugins/built_in/affinity_flow_chatter/planner/planner.py similarity index 98% rename from src/plugins/built_in/affinity_flow_chatter/planner.py rename to src/plugins/built_in/affinity_flow_chatter/planner/planner.py index d6a77576c..0975c8d12 100644 --- a/src/plugins/built_in/affinity_flow_chatter/planner.py +++ b/src/plugins/built_in/affinity_flow_chatter/planner/planner.py @@ -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") diff --git a/src/plugins/built_in/affinity_flow_chatter/planner_prompts.py b/src/plugins/built_in/affinity_flow_chatter/planner/planner_prompts.py similarity index 100% rename from src/plugins/built_in/affinity_flow_chatter/planner_prompts.py rename to src/plugins/built_in/affinity_flow_chatter/planner/planner_prompts.py diff --git a/src/plugins/built_in/affinity_flow_chatter/plugin.py b/src/plugins/built_in/affinity_flow_chatter/plugin.py index c66152a4d..a3bb5d60e 100644 --- a/src/plugins/built_in/affinity_flow_chatter/plugin.py +++ b/src/plugins/built_in/affinity_flow_chatter/plugin.py @@ -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: diff --git a/src/plugins/built_in/affinity_flow_chatter/proactive/__init__.py b/src/plugins/built_in/affinity_flow_chatter/proactive/__init__.py new file mode 100644 index 000000000..bfffdd2bf --- /dev/null +++ b/src/plugins/built_in/affinity_flow_chatter/proactive/__init__.py @@ -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", +] diff --git a/src/plugins/built_in/affinity_flow_chatter/proactive_thinking_event.py b/src/plugins/built_in/affinity_flow_chatter/proactive/proactive_thinking_event.py similarity index 98% rename from src/plugins/built_in/affinity_flow_chatter/proactive_thinking_event.py rename to src/plugins/built_in/affinity_flow_chatter/proactive/proactive_thinking_event.py index 1d33f3121..0d3f39aa8 100644 --- a/src/plugins/built_in/affinity_flow_chatter/proactive_thinking_event.py +++ b/src/plugins/built_in/affinity_flow_chatter/proactive/proactive_thinking_event.py @@ -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, ) diff --git a/src/plugins/built_in/affinity_flow_chatter/proactive_thinking_executor.py b/src/plugins/built_in/affinity_flow_chatter/proactive/proactive_thinking_executor.py similarity index 99% rename from src/plugins/built_in/affinity_flow_chatter/proactive_thinking_executor.py rename to src/plugins/built_in/affinity_flow_chatter/proactive/proactive_thinking_executor.py index 60e48b145..344c251ef 100644 --- a/src/plugins/built_in/affinity_flow_chatter/proactive_thinking_executor.py +++ b/src/plugins/built_in/affinity_flow_chatter/proactive/proactive_thinking_executor.py @@ -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, ) @@ -520,7 +520,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, ) diff --git a/src/plugins/built_in/affinity_flow_chatter/proactive_thinking_scheduler.py b/src/plugins/built_in/affinity_flow_chatter/proactive/proactive_thinking_scheduler.py similarity index 99% rename from src/plugins/built_in/affinity_flow_chatter/proactive_thinking_scheduler.py rename to src/plugins/built_in/affinity_flow_chatter/proactive/proactive_thinking_scheduler.py index e5171c721..23c003928 100644 --- a/src/plugins/built_in/affinity_flow_chatter/proactive_thinking_scheduler.py +++ b/src/plugins/built_in/affinity_flow_chatter/proactive/proactive_thinking_scheduler.py @@ -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, ) diff --git a/src/plugins/built_in/affinity_flow_chatter/tools/__init__.py b/src/plugins/built_in/affinity_flow_chatter/tools/__init__.py new file mode 100644 index 000000000..cce9d75ae --- /dev/null +++ b/src/plugins/built_in/affinity_flow_chatter/tools/__init__.py @@ -0,0 +1,10 @@ +""" +AffinityFlow Chatter 工具模块 + +包含各种辅助工具类 +""" + +from .chat_stream_impression_tool import ChatStreamImpressionTool +from .user_profile_tool import UserProfileTool + +__all__ = ["ChatStreamImpressionTool", "UserProfileTool"] diff --git a/src/plugins/built_in/affinity_flow_chatter/chat_stream_impression_tool.py b/src/plugins/built_in/affinity_flow_chatter/tools/chat_stream_impression_tool.py similarity index 100% rename from src/plugins/built_in/affinity_flow_chatter/chat_stream_impression_tool.py rename to src/plugins/built_in/affinity_flow_chatter/tools/chat_stream_impression_tool.py diff --git a/src/plugins/built_in/affinity_flow_chatter/user_profile_tool.py b/src/plugins/built_in/affinity_flow_chatter/tools/user_profile_tool.py similarity index 100% rename from src/plugins/built_in/affinity_flow_chatter/user_profile_tool.py rename to src/plugins/built_in/affinity_flow_chatter/tools/user_profile_tool.py diff --git a/src/plugins/built_in/napcat_adapter_plugin/plugin.py b/src/plugins/built_in/napcat_adapter_plugin/plugin.py index 10e7efe6f..92dc32608 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/plugin.py +++ b/src/plugins/built_in/napcat_adapter_plugin/plugin.py @@ -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) diff --git a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py index 2e9bbaf2f..12998239c 100644 --- a/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py +++ b/src/plugins/built_in/napcat_adapter_plugin/src/recv_handler/meta_event_handler.py @@ -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 端异常!") diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 37cd0b595..9775d7a8d 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "7.5.6" +version = "7.5.7" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -558,6 +558,12 @@ no_reply_threshold_adjustment = 0.01 # 不回复兴趣阈值调整值 reply_cooldown_reduction = 5 # 回复后减少的不回复计数 max_no_reply_count = 20 # 最大不回复计数次数 +# 回复后连续对话机制参数 +enable_post_reply_boost = true # 是否启用回复后阈值降低机制,使bot在回复后更容易进行连续对话 +post_reply_threshold_reduction = 0.15 # 回复后初始阈值降低值(建议0.1-0.2) +post_reply_boost_max_count = 3 # 回复后阈值降低的最大持续次数(建议2-5) +post_reply_boost_decay_rate = 0.5 # 每次回复后阈值降低衰减率(0-1,建议0.3-0.7) + # 综合评分权重 keyword_match_weight = 0.4 # 兴趣关键词匹配度权重 mention_bot_weight = 0.3 # 提及bot分数权重 @@ -565,7 +571,9 @@ relationship_weight = 0.3 # 人物关系分数权重 # 提及bot相关参数 mention_bot_adjustment_threshold = 0.5 # 提及bot后的调整阈值 -mention_bot_interest_score = 2.5 # 提及bot的兴趣分 +# 强提及(被@、被回复、私聊)和弱提及(文本匹配名字/别名)使用不同分值 +strong_mention_interest_score = 2.5 # 强提及的兴趣分(被@、被回复、私聊) +weak_mention_interest_score = 1.5 # 弱提及的兴趣分(文本匹配bot名字或别名) base_relationship_score = 0.3 # 基础人物关系分 # 关系追踪系统参数