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

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

View File

@@ -0,0 +1,654 @@
# Affinity Flow Chatter 插件优化总结
## 更新日期
2025年11月3日
## 优化概述
本次对 Affinity Flow Chatter 插件进行了全面的重构和优化主要包括目录结构优化、性能改进、bug修复和新功能添加。
## <20> 任务-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`:更新文档说明
---
## <20>🆔 任务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 数据
-**响应速度**:超时保护避免长时间等待
-**启动速度**:首次启动需要生成 embedding10-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 存储策略
- ✅ 修复连续不回复阈值调整
- ✅ 添加超时保护机制
- ✅ 实现回复后阈值降低

View File

@@ -70,6 +70,11 @@
- `mention_bot_adjustment_threshold` - `mention_bot_adjustment_threshold`
提及 Bot 后的调整阈值。当bot被提及后回复阈值会改变为这个值。 提及 Bot 后的调整阈值。当bot被提及后回复阈值会改变为这个值。
- `strong_mention_interest_score`
强提及的兴趣分。强提及包括:被@、被回复、私聊消息。这类提及表示用户明确想与bot交互。
- `weak_mention_interest_score`
弱提及的兴趣分。弱提及包括消息中包含bot的名字或别名文本匹配。这类提及可能只是在讨论中提到bot。
- `base_relationship_score` - `base_relationship_score`
--- ---
@@ -80,13 +85,16 @@
2. **Bot 太热情/回复太多** 2. **Bot 太热情/回复太多**
- 提高 `reply_action_interest_threshold`,或降低关键词相关倍率。 - 提高 `reply_action_interest_threshold`,或降低关键词相关倍率。
3. **希望 Bot 更关注被 @ 的消息** 3. **希望 Bot 更关注被 @ 或回复的消息**
- 提高 `mention_bot_interest_score``mention_bot_weight` - 提高 `strong_mention_interest_score``mention_bot_weight`
4. **希望 Bot 更看重关系好的用户** 4. **希望 Bot 对文本提及也积极回应**
- 提高 `weak_mention_interest_score`
5. **希望 Bot 更看重关系好的用户**
- 提高 `relationship_weight``base_relationship_score` - 提高 `relationship_weight``base_relationship_score`
5. **表情包行为过于频繁/稀少** 6. **表情包行为过于频繁/稀少**
- 调整 `non_reply_action_interest_threshold` - 调整 `non_reply_action_interest_threshold`
--- ---
@@ -121,7 +129,8 @@ keyword_match_weight = 0.4
mention_bot_weight = 0.3 mention_bot_weight = 0.3
relationship_weight = 0.3 relationship_weight = 0.3
mention_bot_adjustment_threshold = 0.5 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 base_relationship_score = 0.3
``` ```
@@ -134,7 +143,10 @@ MoFox-Bot 在收到每条消息时会通过一套“兴趣度评分afc
- 不同匹配度的关键词会乘以对应的倍率high/medium/low_match_keyword_multiplier并根据匹配数量叠加加成match_count_bonusmax_match_bonus - 不同匹配度的关键词会乘以对应的倍率high/medium/low_match_keyword_multiplier并根据匹配数量叠加加成match_count_bonusmax_match_bonus
### 2. 提及与关系加分 ### 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 计入总分。 - 与用户的关系分base_relationship_score 及动态关系分)也会按 relationship_weight 计入总分。
### 3. 综合评分计算 ### 3. 综合评分计算

View File

@@ -26,6 +26,8 @@ class BotInterestManager:
def __init__(self): def __init__(self):
self.current_interests: BotPersonalityInterests | None = None self.current_interests: BotPersonalityInterests | None = None
self.embedding_cache: dict[str, list[float]] = {} # embedding缓存 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 self._initialized = False
# Embedding客户端配置 # Embedding客户端配置
@@ -169,22 +171,47 @@ class BotInterestManager:
1. 标签应该符合人设特点和性格 1. 标签应该符合人设特点和性格
2. 每个标签都有权重0.1-1.0),表示对该兴趣的喜好程度 2. 每个标签都有权重0.1-1.0),表示对该兴趣的喜好程度
3. 生成15-25个不等的标签 3. 生成15-25个不等的标签
4. 标签应该是具体的关键词,而不是抽象概念 4. 每个标签包含两个部分:
5. 每个标签的长度不超过10个字符 - name: 简短的标签名2-6个字符用于显示和管理"Python""追番""撸猫"
- expanded: 完整的描述性文本20-50个字符用于语义匹配描述这个兴趣的具体内容和场景
5. expanded 扩展描述要求:
- 必须是完整的句子或短语,包含丰富的语义信息
- 描述具体的对话场景、活动内容、相关话题
- 避免过于抽象,要有明确的语境
- 示例:
* "Python" -> "讨论Python编程语言、写Python代码、Python脚本开发、Python技术问题"
* "追番" -> "讨论正在播出的动漫番剧、追番进度、动漫剧情、番剧推荐、动漫角色"
* "撸猫" -> "讨论猫咪宠物、晒猫分享、萌宠日常、可爱猫猫、养猫心得"
* "社恐" -> "表达社交焦虑、不想见人、想躲起来、害怕社交的心情"
* "深夜码代码" -> "深夜写代码、熬夜编程、夜猫子程序员、深夜调试bug"
请以JSON格式返回格式如下 请以JSON格式返回格式如下
{{ {{
"interests": [ "interests": [
{{"name": "标签名", "weight": 0.8}}, {{
{{"name": "标签名", "weight": 0.6}}, "name": "Python",
{{"name": "标签名", "weight": 0.9}} "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生成兴趣标签 # 调用LLM生成兴趣标签
@@ -211,16 +238,22 @@ class BotInterestManager:
for i, tag_data in enumerate(interests_list): for i, tag_data in enumerate(interests_list):
tag_name = tag_data.get("name", f"标签_{i}") tag_name = tag_data.get("name", f"标签_{i}")
weight = tag_data.get("weight", 0.5) weight = tag_data.get("weight", 0.5)
expanded = tag_data.get("expanded") # 获取扩展描述
# 检查标签长度,如果过长则截断 # 检查标签长度,如果过长则截断
if len(tag_name) > 10: if len(tag_name) > 10:
logger.warning(f"⚠️ 标签 '{tag_name}' 过长将截断为10个字符") logger.warning(f"⚠️ 标签 '{tag_name}' 过长将截断为10个字符")
tag_name = 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 # 为所有标签生成embedding
logger.info("🧠 开始为兴趣标签生成embedding向量...") logger.info("🧠 开始为兴趣标签生成embedding向量...")
@@ -284,12 +317,12 @@ class BotInterestManager:
return None return None
async def _generate_embeddings_for_tags(self, interests: BotPersonalityInterests): async def _generate_embeddings_for_tags(self, interests: BotPersonalityInterests):
"""为所有兴趣标签生成embedding""" """为所有兴趣标签生成embedding(仅缓存在内存中)"""
if not hasattr(self, "embedding_request"): if not hasattr(self, "embedding_request"):
raise RuntimeError("❌ Embedding客户端未初始化无法生成embedding") raise RuntimeError("❌ Embedding客户端未初始化无法生成embedding")
total_tags = len(interests.interest_tags) total_tags = len(interests.interest_tags)
logger.info(f"🧠 开始为 {total_tags} 个兴趣标签生成embedding向量...") logger.info(f"🧠 开始为 {total_tags} 个兴趣标签生成embedding向量(动态生成,仅内存缓存)...")
cached_count = 0 cached_count = 0
generated_count = 0 generated_count = 0
@@ -297,22 +330,22 @@ class BotInterestManager:
for i, tag in enumerate(interests.interest_tags, 1): for i, tag in enumerate(interests.interest_tags, 1):
if tag.tag_name in self.embedding_cache: if tag.tag_name in self.embedding_cache:
# 使用缓存的embedding # 使用内存缓存的embedding
tag.embedding = self.embedding_cache[tag.tag_name] tag.embedding = self.embedding_cache[tag.tag_name]
cached_count += 1 cached_count += 1
logger.debug(f" [{i}/{total_tags}] 🏷️ '{tag.tag_name}' - 使用缓存") logger.debug(f" [{i}/{total_tags}] 🏷️ '{tag.tag_name}' - 使用内存缓存")
else: else:
# 生成新的embedding # 动态生成新的embedding
embedding_text = tag.tag_name 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) embedding = await self._get_embedding(embedding_text)
if embedding: if embedding:
tag.embedding = embedding tag.embedding = embedding # 设置到 tag 对象(内存中)
self.embedding_cache[tag.tag_name] = embedding self.embedding_cache[tag.tag_name] = embedding # 同时缓存
generated_count += 1 generated_count += 1
logger.debug(f"'{tag.tag_name}' embedding生成成功") logger.debug(f"'{tag.tag_name}' embedding动态生成成功并缓存到内存")
else: else:
failed_count += 1 failed_count += 1
logger.warning(f"'{tag.tag_name}' embedding生成失败") logger.warning(f"'{tag.tag_name}' embedding生成失败")
@@ -322,12 +355,12 @@ class BotInterestManager:
interests.last_updated = datetime.now() interests.last_updated = datetime.now()
logger.info("=" * 50) logger.info("=" * 50)
logger.info("✅ Embedding生成完成!") logger.info("✅ Embedding动态生成完成(仅存储在内存中)!")
logger.info(f"📊 总标签数: {total_tags}") logger.info(f"📊 总标签数: {total_tags}")
logger.info(f"💾 缓存命中: {cached_count}") logger.info(f"💾 内存缓存命中: {cached_count}")
logger.info(f"🆕 新生成: {generated_count}") logger.info(f"🆕 新生成: {generated_count}")
logger.info(f"❌ 失败: {failed_count}") logger.info(f"❌ 失败: {failed_count}")
logger.info(f"🗃️ 缓存大小: {len(self.embedding_cache)}") logger.info(f"🗃️ 内存缓存大小: {len(self.embedding_cache)}")
logger.info("=" * 50) logger.info("=" * 50)
async def _get_embedding(self, text: str) -> list[float]: async def _get_embedding(self, text: str) -> list[float]:
@@ -421,7 +454,19 @@ class BotInterestManager:
async def calculate_interest_match( async def calculate_interest_match(
self, message_text: str, keywords: list[str] | None = None self, message_text: str, keywords: list[str] | None = None
) -> InterestMatchResult: ) -> InterestMatchResult:
"""计算消息与机器人兴趣的匹配度""" """计算消息与机器人兴趣的匹配度(优化版 - 标签扩展策略)
核心优化:将短标签扩展为完整的描述性句子,解决语义粒度不匹配问题
原问题:
- 消息: "今天天气不错" (完整句子)
- 标签: "蹭人治愈" (2-4字短语)
- 结果: 误匹配,因为短标签的 embedding 过于抽象
解决方案:
- 标签扩展: "蹭人治愈" -> "表达亲近、寻求安慰、撒娇的内容"
- 现在是: 句子 vs 句子,匹配更准确
"""
if not self.current_interests or not self._initialized: if not self.current_interests or not self._initialized:
raise RuntimeError("❌ 兴趣标签系统未初始化") raise RuntimeError("❌ 兴趣标签系统未初始化")
@@ -442,13 +487,13 @@ class BotInterestManager:
message_embedding = await self._get_embedding(message_text) message_embedding = await self._get_embedding(message_text)
logger.debug(f"消息 embedding 生成成功, 维度: {len(message_embedding)}") logger.debug(f"消息 embedding 生成成功, 维度: {len(message_embedding)}")
# 计算与每个兴趣标签的相似度 # 计算与每个兴趣标签的相似度(使用扩展标签)
match_count = 0 match_count = 0
high_similarity_count = 0 high_similarity_count = 0
medium_similarity_count = 0 medium_similarity_count = 0
low_similarity_count = 0 low_similarity_count = 0
# 分级相似度阈值 # 分级相似度阈值 - 优化后可以提高阈值,因为匹配更准确了
affinity_config = global_config.affinity_flow affinity_config = global_config.affinity_flow
high_threshold = affinity_config.high_match_interest_threshold high_threshold = affinity_config.high_match_interest_threshold
medium_threshold = affinity_config.medium_match_interest_threshold medium_threshold = affinity_config.medium_match_interest_threshold
@@ -458,27 +503,45 @@ class BotInterestManager:
for tag in active_tags: for tag in active_tags:
if tag.embedding: 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 enhanced_score = weighted_score * affinity_config.high_match_keyword_multiplier
match_count += 1 match_count += 1
high_similarity_count += 1 high_similarity_count += 1
result.add_match(tag.tag_name, enhanced_score, [tag.tag_name]) 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 enhanced_score = weighted_score * affinity_config.medium_match_keyword_multiplier
match_count += 1 match_count += 1
medium_similarity_count += 1 medium_similarity_count += 1
result.add_match(tag.tag_name, enhanced_score, [tag.tag_name]) 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 enhanced_score = weighted_score * affinity_config.low_match_keyword_multiplier
match_count += 1 match_count += 1
@@ -520,6 +583,121 @@ class BotInterestManager:
) )
return result 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]: def _calculate_keyword_match_bonus(self, keywords: list[str], matched_tags: list[str]) -> dict[str, float]:
"""计算关键词直接匹配奖励""" """计算关键词直接匹配奖励"""
if not keywords or not matched_tags: if not keywords or not matched_tags:
@@ -668,11 +846,12 @@ class BotInterestManager:
last_updated=db_interests.last_updated, last_updated=db_interests.last_updated,
) )
# 解析兴趣标签 # 解析兴趣标签embedding 从数据库加载后会被忽略,因为我们不再存储它)
for tag_data in tags_data: for tag_data in tags_data:
tag = BotInterestTag( tag = BotInterestTag(
tag_name=tag_data.get("tag_name", ""), tag_name=tag_data.get("tag_name", ""),
weight=tag_data.get("weight", 0.5), weight=tag_data.get("weight", 0.5),
expanded=tag_data.get("expanded"), # 加载扩展描述
created_at=datetime.fromisoformat( created_at=datetime.fromisoformat(
tag_data.get("created_at", datetime.now().isoformat()) tag_data.get("created_at", datetime.now().isoformat())
), ),
@@ -680,11 +859,11 @@ class BotInterestManager:
tag_data.get("updated_at", datetime.now().isoformat()) tag_data.get("updated_at", datetime.now().isoformat())
), ),
is_active=tag_data.get("is_active", True), is_active=tag_data.get("is_active", True),
embedding=tag_data.get("embedding"), embedding=None, # 不再从数据库加载 embedding改为动态生成
) )
interests.interest_tags.append(tag) interests.interest_tags.append(tag)
logger.debug(f"成功解析 {len(interests.interest_tags)} 个兴趣标签") logger.debug(f"成功解析 {len(interests.interest_tags)} 个兴趣标签embedding 将在初始化时动态生成)")
return interests return interests
except (orjson.JSONDecodeError, Exception) as e: 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.compatibility import get_db_session
from src.common.database.core.models import BotPersonalityInterests as DBBotPersonalityInterests from src.common.database.core.models import BotPersonalityInterests as DBBotPersonalityInterests
# 将兴趣标签转换为JSON格式 # 将兴趣标签转换为JSON格式不再保存embedding启动时动态生成
tags_data = [] tags_data = []
for tag in interests.interest_tags: for tag in interests.interest_tags:
tag_dict = { tag_dict = {
"tag_name": tag.tag_name, "tag_name": tag.tag_name,
"weight": tag.weight, "weight": tag.weight,
"expanded": tag.expanded, # 保存扩展描述
"created_at": tag.created_at.isoformat(), "created_at": tag.created_at.isoformat(),
"updated_at": tag.updated_at.isoformat(), "updated_at": tag.updated_at.isoformat(),
"is_active": tag.is_active, "is_active": tag.is_active,
"embedding": tag.embedding, # embedding 不再存储到数据库,改为内存缓存
} }
tags_data.append(tag_dict) tags_data.append(tag_dict)

View File

@@ -196,10 +196,18 @@ async def _process_single_segment(segment: Seg, state: dict, message_info: BaseM
state["is_emoji"] = False state["is_emoji"] = False
state["is_video"] = False state["is_video"] = False
state["is_at"] = True state["is_at"] = True
# 处理at消息格式为"昵称:QQ号" # 处理at消息格式为"@<昵称:QQ号>"
if isinstance(segment.data, str) and ":" in segment.data: if isinstance(segment.data, str):
nickname, qq_id = segment.data.split(":", 1) if ":" in segment.data:
return f"@{nickname}" # 标准格式: "昵称: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 "@未知用户" return f"@{segment.data}" if isinstance(segment.data, str) else "@未知用户"
elif segment.type == "image": elif segment.type == "image":

View File

@@ -49,23 +49,22 @@ def is_mentioned_bot_in_message(message) -> tuple[bool, float]:
message: DatabaseMessages 消息对象 message: DatabaseMessages 消息对象
Returns: Returns:
tuple[bool, float]: (是否提及, 提及概率) tuple[bool, float]: (是否提及, 提及类型)
提及类型: 0=未提及, 1=弱提及(文本匹配), 2=强提及(@/回复/私聊)
""" """
keywords = [global_config.bot.nickname]
nicknames = global_config.bot.alias_names nicknames = global_config.bot.alias_names
reply_probability = 0.0 mention_type = 0 # 0=未提及, 1=弱提及, 2=强提及
is_at = False
is_mentioned = False
# 检查 is_mentioned 属性 # 检查 is_mentioned 属性(保持向后兼容)
mentioned_attr = getattr(message, "is_mentioned", None) mentioned_attr = getattr(message, "is_mentioned", None)
if mentioned_attr is not None: if mentioned_attr is not None:
try: 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): except (ValueError, TypeError):
pass pass
# 检查 additional_config # 检查 additional_config(保持向后兼容)
additional_config = None additional_config = None
# DatabaseMessages: additional_config 是 JSON 字符串 # 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: if additional_config and additional_config.get("is_mentioned") is not None:
try: try:
reply_probability = float(additional_config.get("is_mentioned")) # type: ignore mentioned_value = float(additional_config.get("is_mentioned")) # type: ignore
is_mentioned = True # 如果配置中有提及值,假设是强提及
return is_mentioned, reply_probability return True, 2.0 if mentioned_value > 0 else 0.0
except Exception as e: except Exception as e:
logger.warning(str(e)) logger.warning(str(e))
logger.warning( logger.warning(
f"消息中包含不合理的设置 is_mentioned: {additional_config.get('is_mentioned')}" f"消息中包含不合理的设置 is_mentioned: {additional_config.get('is_mentioned')}"
) )
# 检查消息文本内容
processed_text = message.processed_plain_text or "" processed_text = message.processed_plain_text or ""
if global_config.bot.nickname in processed_text:
is_mentioned = True # 1. 判断是否为私聊(强提及)
group_info = getattr(message, "group_info", None)
for alias_name in global_config.bot.alias_names: if not group_info or not getattr(group_info, "group_id", None):
if alias_name in processed_text: is_private = True
is_mentioned = True mention_type = 2
logger.debug("检测到私聊消息 - 强提及")
# 判断是否被@
if re.search(rf"@<(.+?):{global_config.bot.qq_account}>", message.processed_plain_text): # 2. 判断是否被@(强提及)
if re.search(rf"@<(.+?):{global_config.bot.qq_account}>", processed_text):
is_at = True is_at = True
is_mentioned = True mention_type = 2
logger.debug("检测到@提及 - 强提及")
# print(f"message.processed_plain_text: {message.processed_plain_text}")
# print(f"is_mentioned: {is_mentioned}") # 3. 判断是否被回复(强提及)
# print(f"is_at: {is_at}") if re.match(
rf"\[回复 (.+?)\({global_config.bot.qq_account!s}\)(.+?)\],说:", processed_text
if is_at and global_config.chat.at_bot_inevitable_reply: ) or re.match(
reply_probability = 1.0 rf"\[回复<(.+?)(?=:{global_config.bot.qq_account!s}>)\:{global_config.bot.qq_account!s}>(.+?)\],说:",
logger.debug("被@回复概率设置为100%") processed_text,
else: ):
if not is_mentioned: is_replied = True
# 判断是否被回复 mention_type = 2
if re.match( logger.debug("检测到回复消息 - 强提及")
rf"\[回复 (.+?)\({global_config.bot.qq_account!s}\)(.+?)\],说:", message.processed_plain_text
) or re.match( # 4. 判断文本中是否提及bot名字或别名弱提及
rf"\[回复<(.+?)(?=:{global_config.bot.qq_account!s}>)\:{global_config.bot.qq_account!s}>(.+?)\],说:", if mention_type == 0: # 只有在没有强提及时才检查弱提及
message.processed_plain_text, # 移除@和回复标记后再检查
): message_content = re.sub(r"@(.+?)(\d+)", "", processed_text)
is_mentioned = True message_content = re.sub(r"@<(.+?)(?=:(\d+))\:(\d+)>", "", message_content)
else: message_content = re.sub(r"\[回复 (.+?)\(((\d+)|未知id)\)(.+?)\],说:", "", message_content)
# 判断内容中是否被提及 message_content = re.sub(r"\[回复<(.+?)(?=:(\d+))\:(\d+)>(.+?)\],说:", "", message_content)
message_content = re.sub(r"@(.+?)(\d+)", "", message.processed_plain_text)
message_content = re.sub(r"@<(.+?)(?=:(\d+))\:(\d+)>", "", message_content) # 检查bot主名字
message_content = re.sub(r"\[回复 (.+?)\(((\d+)|未知id)\)(.+?)\],说:", "", message_content) if global_config.bot.nickname in message_content:
message_content = re.sub(r"\[回复<(.+?)(?=:(\d+))\:(\d+)>(.+?)\],说:", "", message_content) is_text_mentioned = True
for keyword in keywords: mention_type = 1
if keyword in message_content: logger.debug(f"检测到文本提及bot主名字 '{global_config.bot.nickname}' - 弱提及")
is_mentioned = True # 如果主名字没匹配,再检查别名
for nickname in nicknames: elif nicknames:
if nickname in message_content: for alias_name in nicknames:
is_mentioned = True if alias_name in message_content:
if is_mentioned and global_config.chat.mentioned_bot_inevitable_reply: is_text_mentioned = True
reply_probability = 1.0 mention_type = 1
logger.debug("被提及回复概率设置为100%") logger.debug(f"检测到文本提及bot别名 '{alias_name}' - 弱提及")
return is_mentioned, reply_probability break
# 返回结果
is_mentioned = mention_type > 0
return is_mentioned, float(mention_type)
async def get_embedding(text, request_type="embedding") -> list[float] | None: async def get_embedding(text, request_type="embedding") -> list[float] | None:
"""获取文本的embedding向量""" """获取文本的embedding向量"""

View File

@@ -16,6 +16,7 @@ class BotInterestTag(BaseDataModel):
tag_name: str tag_name: str
weight: float = 1.0 # 权重,表示对这个兴趣的喜好程度 (0.0-1.0) weight: float = 1.0 # 权重,表示对这个兴趣的喜好程度 (0.0-1.0)
expanded: str | None = None # 标签的扩展描述,用于更精准的语义匹配
embedding: list[float] | None = None # 标签的embedding向量 embedding: list[float] | None = None # 标签的embedding向量
created_at: datetime = field(default_factory=datetime.now) created_at: datetime = field(default_factory=datetime.now)
updated_at: datetime = field(default_factory=datetime.now) updated_at: datetime = field(default_factory=datetime.now)
@@ -26,6 +27,7 @@ class BotInterestTag(BaseDataModel):
return { return {
"tag_name": self.tag_name, "tag_name": self.tag_name,
"weight": self.weight, "weight": self.weight,
"expanded": self.expanded,
"embedding": self.embedding, "embedding": self.embedding,
"created_at": self.created_at.isoformat(), "created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(), "updated_at": self.updated_at.isoformat(),
@@ -38,6 +40,7 @@ class BotInterestTag(BaseDataModel):
return cls( return cls(
tag_name=data["tag_name"], tag_name=data["tag_name"],
weight=data.get("weight", 1.0), weight=data.get("weight", 1.0),
expanded=data.get("expanded"),
embedding=data.get("embedding"), embedding=data.get("embedding"),
created_at=datetime.fromisoformat(data["created_at"]) if data.get("created_at") else datetime.now(), 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(), updated_at=datetime.fromisoformat(data["updated_at"]) if data.get("updated_at") else datetime.now(),

View File

@@ -703,6 +703,12 @@ class AffinityFlowConfig(ValidatedConfigBase):
reply_cooldown_reduction: int = Field(default=2, description="回复后减少的不回复计数") reply_cooldown_reduction: int = Field(default=2, description="回复后减少的不回复计数")
max_no_reply_count: int = Field(default=5, 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="兴趣关键词匹配度权重") keyword_match_weight: float = Field(default=0.4, description="兴趣关键词匹配度权重")
mention_bot_weight: float = Field(default=0.3, description="提及bot分数权重") mention_bot_weight: float = Field(default=0.3, description="提及bot分数权重")
@@ -710,7 +716,9 @@ class AffinityFlowConfig(ValidatedConfigBase):
# 提及bot相关参数 # 提及bot相关参数
mention_bot_adjustment_threshold: float = Field(default=0.3, description="提及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="基础人物关系分") base_relationship_score: float = Field(default=0.5, description="基础人物关系分")
# 关系追踪系统参数 # 关系追踪系统参数

View File

@@ -33,9 +33,14 @@ class Individuality:
personality_side = global_config.personality.personality_side personality_side = global_config.personality.personality_side
identity = global_config.personality.identity identity = global_config.personality.identity
person_info_manager = get_person_info_manager() # 基于人设文本生成 personality_id使用 MD5 hash
self.bot_person_id = person_info_manager.get_person_id("system", "bot_id") # 这样当人设发生变化时会自动生成新的 ID触发重新生成兴趣标签
personality_hash, _ = self._get_config_hash(bot_nickname, personality_core, personality_side, identity)
self.bot_person_id = personality_hash
self.name = bot_nickname 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( personality_changed, identity_changed = await self._check_config_and_clear_if_changed(
@@ -72,8 +77,8 @@ class Individuality:
if personality_changed or identity_changed: if personality_changed or identity_changed:
logger.info("将清空数据库中原有的关键词缓存") logger.info("将清空数据库中原有的关键词缓存")
update_data = { update_data = {
"platform": "system", "platform": "personality",
"user_id": "bot_id", "user_id": self.bot_person_id, # 使用基于人设生成的 ID
"person_name": self.name, "person_name": self.name,
"nickname": self.name, "nickname": self.name,
} }
@@ -171,8 +176,8 @@ class Individuality:
if personality_changed or identity_changed: if personality_changed or identity_changed:
logger.info("将清空原有的关键词缓存") logger.info("将清空原有的关键词缓存")
update_data = { update_data = {
"platform": "system", "platform": "personality",
"user_id": "bot_id", "user_id": current_personality_hash, # 使用 personality hash 作为 user_id
"person_name": self.name, "person_name": self.name,
"nickname": self.name, "nickname": self.name,
} }

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.common.logger import get_logger
from src.plugin_system.base.base_chatter import BaseChatter from src.plugin_system.base.base_chatter import BaseChatter
from src.plugin_system.base.component_types import ChatType 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") logger = get_logger("affinity_chatter")

View File

@@ -3,6 +3,7 @@
基于原有的 AffinityFlow 兴趣度评分系统提供标准化的兴趣值计算功能 基于原有的 AffinityFlow 兴趣度评分系统提供标准化的兴趣值计算功能
""" """
import asyncio
import time import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -60,10 +61,18 @@ class AffinityInterestCalculator(BaseInterestCalculator):
# 用户关系数据缓存 # 用户关系数据缓存
self.user_relationships: dict[str, float] = {} # user_id -> relationship_score 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("[Affinity兴趣计算器] 初始化完成:")
logger.info(f" - 权重配置: {self.score_weights}") logger.info(f" - 权重配置: {self.score_weights}")
logger.info(f" - 回复阈值: {self.reply_threshold}") logger.info(f" - 回复阈值: {self.reply_threshold}")
logger.info(f" - 智能匹配: {self.use_smart_matching}") logger.info(f" - 智能匹配: {self.use_smart_matching}")
logger.info(f" - 回复后连续对话: {self.enable_post_reply_boost}")
# 检查 bot_interest_manager 状态 # 检查 bot_interest_manager 状态
try: try:
@@ -120,22 +129,23 @@ class AffinityInterestCalculator(BaseInterestCalculator):
f"{mentioned_score:.3f}*{self.score_weights['mentioned']} = {total_score:.3f}" f"{mentioned_score:.3f}*{self.score_weights['mentioned']} = {total_score:.3f}"
) )
# 5. 考虑连续不回复的概率提升 # 5. 考虑连续不回复的阈值调整
adjusted_score = self._apply_no_reply_boost(total_score) adjusted_score = total_score
logger.debug(f"[Affinity兴趣计算] 应用不回复提升后: {total_score:.3f}{adjusted_score:.3f}") 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. 决定是否回复和执行动作 # 6. 决定是否回复和执行动作
reply_threshold = self.reply_threshold should_reply = adjusted_score >= adjusted_reply_threshold
action_threshold = global_config.affinity_flow.non_reply_action_interest_threshold should_take_action = adjusted_score >= adjusted_action_threshold
should_reply = adjusted_score >= reply_threshold
should_take_action = adjusted_score >= action_threshold
logger.debug( 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( 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 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: async def _calculate_interest_match_score(self, content: str, keywords: list[str] | None = None) -> float:
"""计算兴趣匹配度(使用智能兴趣匹配系统)""" """计算兴趣匹配度(使用智能兴趣匹配系统,带超时保护"""
# 调试日志:检查各个条件 # 调试日志:检查各个条件
if not content: if not content:
@@ -178,8 +188,11 @@ class AffinityInterestCalculator(BaseInterestCalculator):
logger.debug(f"开始兴趣匹配计算,内容: {content[:50]}...") logger.debug(f"开始兴趣匹配计算,内容: {content[:50]}...")
try: try:
# 使用机器人的兴趣标签系统进行智能匹配 # 使用机器人的兴趣标签系统进行智能匹配1.5秒超时保护)
match_result = await bot_interest_manager.calculate_interest_match(content, keywords or []) match_result = await asyncio.wait_for(
bot_interest_manager.calculate_interest_match(content, keywords or []),
timeout=1.5
)
logger.debug(f"兴趣匹配结果: {match_result}") logger.debug(f"兴趣匹配结果: {match_result}")
if match_result: if match_result:
@@ -195,6 +208,9 @@ class AffinityInterestCalculator(BaseInterestCalculator):
logger.debug("兴趣匹配返回0.0: match_result为None") logger.debug("兴趣匹配返回0.0: match_result为None")
return 0.0 return 0.0
except asyncio.TimeoutError:
logger.warning(f"⏱️ 兴趣匹配计算超时(>1.5秒)返回默认分值0.5以保留其他分数")
return 0.5 # 超时时返回默认分值,避免丢失提及分和关系分
except Exception as e: except Exception as e:
logger.warning(f"智能兴趣匹配失败: {e}") logger.warning(f"智能兴趣匹配失败: {e}")
return 0.0 return 0.0
@@ -226,29 +242,78 @@ class AffinityInterestCalculator(BaseInterestCalculator):
return global_config.affinity_flow.base_relationship_score return global_config.affinity_flow.base_relationship_score
def _calculate_mentioned_score(self, message: "DatabaseMessages", bot_nickname: str) -> float: 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", "") 强提及@被回复私聊: 使用 strong_mention_interest_score
弱提及文本匹配名字/别名: 使用 weak_mention_interest_score
# 判断是否为私聊 - 通过 group_info 对象判断 """
is_private_chat = not message.group_info # 如果没有group_info则是私聊 from src.chat.utils.utils import is_mentioned_bot_in_message
logger.debug(f"[提及分计算] is_mentioned={is_mentioned}, is_private_chat={is_private_chat}, group_info={message.group_info}") # 使用统一的提及检测函数
is_mentioned, mention_type = is_mentioned_bot_in_message(message)
# 检查是否被提及(包括文本匹配)
bot_aliases = [bot_nickname, *global_config.bot.alias_names] if not is_mentioned:
is_text_mentioned = any(alias in processed_plain_text for alias in bot_aliases if alias) logger.debug("[提及分计算] 未提及机器人返回0.0")
return 0.0
# 统一判断:只要提及了机器人(包括@、文本提及、私聊)都返回配置的分值
if is_mentioned or is_text_mentioned or is_private_chat: # mention_type: 0=未提及, 1=弱提及, 2=强提及
logger.debug("[提及分计算] 检测到机器人提及,返回配置分值") if mention_type >= 2:
return global_config.affinity_flow.mention_bot_interest_score # 强提及:被@、被回复、私聊
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: else:
logger.debug("[提及分计算] 未提及机器人返回0.0") 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: 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: 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 boost = self.no_reply_count * self.probability_boost_per_no_reply
return min(1.0, base_score + boost) return min(1.0, base_score + boost)
@@ -315,3 +380,34 @@ class AffinityInterestCalculator(BaseInterestCalculator):
self.no_reply_count = 0 self.no_reply_count = 0
else: else:
self.no_reply_count = min(self.no_reply_count + 1, self.max_no_reply_count) 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.config.config import global_config
from src.mood.mood_manager import mood_manager from src.mood.mood_manager import mood_manager
from src.plugin_system.base.component_types import ChatMode 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.planner.plan_executor import ChatterPlanExecutor
from src.plugins.built_in.affinity_flow_chatter.plan_filter import ChatterPlanFilter from src.plugins.built_in.affinity_flow_chatter.planner.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_generator import ChatterPlanGenerator
if TYPE_CHECKING: if TYPE_CHECKING:
from src.chat.planner_actions.action_manager import ChatterActionManager 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.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") logger = get_logger("planner")

View File

@@ -39,48 +39,48 @@ class AffinityChatterPlugin(BasePlugin):
components: ClassVar = [] components: ClassVar = []
try: try:
# 延迟导入 AffinityChatter # 延迟导入 AffinityChatter(从 core 子模块)
from .affinity_chatter import AffinityChatter from .core.affinity_chatter import AffinityChatter
components.append((AffinityChatter.get_chatter_info(), AffinityChatter)) components.append((AffinityChatter.get_chatter_info(), AffinityChatter))
except Exception as e: except Exception as e:
logger.error(f"加载 AffinityChatter 时出错: {e}") logger.error(f"加载 AffinityChatter 时出错: {e}")
try: try:
# 延迟导入 AffinityInterestCalculator # 延迟导入 AffinityInterestCalculator(从 core 子模块)
from .affinity_interest_calculator import AffinityInterestCalculator from .core.affinity_interest_calculator import AffinityInterestCalculator
components.append((AffinityInterestCalculator.get_interest_calculator_info(), AffinityInterestCalculator)) components.append((AffinityInterestCalculator.get_interest_calculator_info(), AffinityInterestCalculator))
except Exception as e: except Exception as e:
logger.error(f"加载 AffinityInterestCalculator 时出错: {e}") logger.error(f"加载 AffinityInterestCalculator 时出错: {e}")
try: try:
# 延迟导入 UserProfileTool # 延迟导入 UserProfileTool(从 tools 子模块)
from .user_profile_tool import UserProfileTool from .tools.user_profile_tool import UserProfileTool
components.append((UserProfileTool.get_tool_info(), UserProfileTool)) components.append((UserProfileTool.get_tool_info(), UserProfileTool))
except Exception as e: except Exception as e:
logger.error(f"加载 UserProfileTool 时出错: {e}") logger.error(f"加载 UserProfileTool 时出错: {e}")
try: try:
# 延迟导入 ChatStreamImpressionTool # 延迟导入 ChatStreamImpressionTool(从 tools 子模块)
from .chat_stream_impression_tool import ChatStreamImpressionTool from .tools.chat_stream_impression_tool import ChatStreamImpressionTool
components.append((ChatStreamImpressionTool.get_tool_info(), ChatStreamImpressionTool)) components.append((ChatStreamImpressionTool.get_tool_info(), ChatStreamImpressionTool))
except Exception as e: except Exception as e:
logger.error(f"加载 ChatStreamImpressionTool 时出错: {e}") logger.error(f"加载 ChatStreamImpressionTool 时出错: {e}")
try: try:
# 延迟导入 ProactiveThinkingReplyHandler # 延迟导入 ProactiveThinkingReplyHandler(从 proactive 子模块)
from .proactive_thinking_event import ProactiveThinkingReplyHandler from .proactive.proactive_thinking_event import ProactiveThinkingReplyHandler
components.append((ProactiveThinkingReplyHandler.get_handler_info(), ProactiveThinkingReplyHandler)) components.append((ProactiveThinkingReplyHandler.get_handler_info(), ProactiveThinkingReplyHandler))
except Exception as e: except Exception as e:
logger.error(f"加载 ProactiveThinkingReplyHandler 时出错: {e}") logger.error(f"加载 ProactiveThinkingReplyHandler 时出错: {e}")
try: try:
# 延迟导入 ProactiveThinkingMessageHandler # 延迟导入 ProactiveThinkingMessageHandler(从 proactive 子模块)
from .proactive_thinking_event import ProactiveThinkingMessageHandler from .proactive.proactive_thinking_event import ProactiveThinkingMessageHandler
components.append((ProactiveThinkingMessageHandler.get_handler_info(), ProactiveThinkingMessageHandler)) components.append((ProactiveThinkingMessageHandler.get_handler_info(), ProactiveThinkingMessageHandler))
except Exception as e: 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.common.logger import get_logger
from src.plugin_system import BaseEventHandler, EventType from src.plugin_system import BaseEventHandler, EventType
from src.plugin_system.base.base_event import HandlerResult 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, proactive_thinking_scheduler,
) )

View File

@@ -226,7 +226,7 @@ class ProactiveThinkingPlanner:
# 5. 获取上次决策 # 5. 获取上次决策
last_decision = None last_decision = None
try: 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, proactive_thinking_scheduler,
) )
@@ -520,7 +520,7 @@ async def execute_proactive_thinking(stream_id: str):
stream_id: 聊天流ID stream_id: 聊天流ID
""" """
from src.config.config import global_config 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, proactive_thinking_scheduler,
) )

View File

@@ -256,7 +256,7 @@ class ProactiveThinkingScheduler:
logger.debug(f"[调度器] 触发间隔={interval_seconds}秒 ({interval_seconds / 60:.1f}分钟)") 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, 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 return components
async def on_plugin_loaded(self): async def on_plugin_loaded(self):
# 初始化数据库表
await self._init_database_tables()
# 设置插件配置 # 设置插件配置
message_send_instance.set_plugin_config(self.config) message_send_instance.set_plugin_config(self.config)
# 设置chunker的插件配置 # 设置chunker的插件配置
@@ -410,3 +413,18 @@ class NapcatAdapterPlugin(BasePlugin):
stream_router.cleanup_interval = config_api.get_plugin_config(self.config, "stream_router.cleanup_interval", 60) stream_router.cleanup_interval = config_api.get_plugin_config(self.config, "stream_router.cleanup_interval", 60)
# 设置其他handler的插件配置现在由component_registry在注册时自动设置 # 设置其他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_id = message.get("self_id")
self.last_heart_beat = time.time() self.last_heart_beat = time.time()
logger.info(f"Bot {self_id} 连接成功") logger.info(f"Bot {self_id} 连接成功")
asyncio.create_task(self.check_heartbeat(self_id)) # 不在连接时立即启动心跳检查,等第一个心跳包到达后再启动
elif event_type == MetaEventType.heartbeat: elif event_type == MetaEventType.heartbeat:
if message["status"].get("online") and message["status"].get("good"): if message["status"].get("online") and message["status"].get("good"):
if not self._interval_checking: self_id = message.get("self_id")
asyncio.create_task(self.check_heartbeat()) if not self._interval_checking and self_id:
# 第一次收到心跳包时才启动心跳检查
asyncio.create_task(self.check_heartbeat(self_id))
self.last_heart_beat = time.time() self.last_heart_beat = time.time()
self.interval = message.get("interval") / 1000 interval = message.get("interval")
if interval:
self.interval = interval / 1000
else: else:
self_id = message.get("self_id") self_id = message.get("self_id")
logger.warning(f"Bot {self_id} Napcat 端异常!") logger.warning(f"Bot {self_id} Napcat 端异常!")

View File

@@ -1,5 +1,5 @@
[inner] [inner]
version = "7.5.6" version = "7.5.7"
#----以下是给开发人员阅读的如果你只是部署了MoFox-Bot不需要阅读---- #----以下是给开发人员阅读的如果你只是部署了MoFox-Bot不需要阅读----
#如果你想要修改配置文件请递增version的值 #如果你想要修改配置文件请递增version的值
@@ -558,6 +558,12 @@ no_reply_threshold_adjustment = 0.01 # 不回复兴趣阈值调整值
reply_cooldown_reduction = 5 # 回复后减少的不回复计数 reply_cooldown_reduction = 5 # 回复后减少的不回复计数
max_no_reply_count = 20 # 最大不回复计数次数 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 # 兴趣关键词匹配度权重 keyword_match_weight = 0.4 # 兴趣关键词匹配度权重
mention_bot_weight = 0.3 # 提及bot分数权重 mention_bot_weight = 0.3 # 提及bot分数权重
@@ -565,7 +571,9 @@ relationship_weight = 0.3 # 人物关系分数权重
# 提及bot相关参数 # 提及bot相关参数
mention_bot_adjustment_threshold = 0.5 # 提及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 # 基础人物关系分 base_relationship_score = 0.3 # 基础人物关系分
# 关系追踪系统参数 # 关系追踪系统参数