refc:重构插件api,补全文档,合并expressor和replyer,分离reply和sender,新log浏览器

This commit is contained in:
SengokuCola
2025-06-19 20:20:34 +08:00
parent 7e05ede846
commit ab28b94e33
63 changed files with 5285 additions and 8316 deletions

View File

@@ -1,60 +0,0 @@
# MaiBot 插件开发文档
## 📖 总体介绍
MaiBot 是一个基于大语言模型的智能聊天机器人,采用现代化的插件系统架构,支持灵活的功能扩展和定制。插件系统提供了统一的开发框架,让开发者可以轻松创建和管理各种功能组件。
### 🎯 插件系统特点
- **组件化架构**支持Action动作和Command命令两种主要组件类型
- **统一API接口**提供丰富的API功能包括消息发送、数据库操作、LLM调用等
- **配置驱动**支持TOML配置文件实现灵活的参数配置
- **热加载机制**:支持动态加载和卸载插件
- **智能依赖管理**自动检查和安装Python第三方包依赖
- **拦截控制**Command组件支持消息拦截控制
- **双目录支持**:区分用户插件和系统内置插件
### 📂 插件目录说明
> ⚠️ **重要**:请将你的自定义插件放在项目根目录的 `plugins/` 文件夹下!
MaiBot支持两个插件目录
- **`plugins/`** (项目根目录)**用户自定义插件目录**,这是你应该放置插件的位置
- **`src/plugins/builtin/`****系统内置插件目录**,包含核心功能插件,请勿修改
**优先级**:用户插件 > 系统内置插件(同名时用户插件会覆盖系统插件)
## 📚 文档导航
### 🚀 快速入门
- [🚀 快速开始指南](docs/plugins/quick-start.md) - 5分钟创建你的第一个插件
- [📋 开发规范](docs/plugins/development-standards.md) - 代码规范和最佳实践
### 📖 核心概念
- [⚡ Action组件详解](docs/plugins/action-components.md) - 智能动作组件开发指南
- [💻 Command组件详解](docs/plugins/command-components.md) - 命令组件开发指南
- [🔧 工具系统详解](docs/plugins/tool-system.md) - 扩展麦麦信息获取能力的工具组件
- [📦 依赖管理系统](docs/plugins/dependency-management.md) - Python包依赖管理详解
### 🔌 API参考
- [📡 消息API](docs/plugins/api/message-api.md) - 消息发送和处理接口
### 💡 示例和模板
- [📚 完整示例](docs/plugins/examples/complete-examples.md) - 各种类型的插件示例
## 🎉 快速开始
想立即开始开发?跳转到 [🚀 快速开始指南](docs/plugins/quick-start.md)5分钟内创建你的第一个MaiBot插件
## 💬 社区和支持
- 📖 **文档问题**如果发现文档错误或需要改进请提交Issue
- 🐛 **Bug报告**在GitHub上报告插件系统相关的问题
- 💡 **功能建议**:欢迎提出新功能建议和改进意见
- 🤝 **贡献代码**欢迎提交PR改进插件系统
---
**开始你的MaiBot插件开发之旅吧** 🚀

158
docs/plugins/README.md Normal file
View File

@@ -0,0 +1,158 @@
# MaiBot插件开发文档
> 欢迎来到MaiBot插件系统开发文档这里是你开始插件开发旅程的最佳起点。
## 🎯 快速导航
### 🌟 新手入门
- [📖 快速开始指南](quick-start.md) - 5分钟创建你的第一个插件
- [🧱 Action组件详解](action-components.md) - 掌握最核心的Action组件
- [💻 Command组件详解](command-components.md) - 学习直接响应命令的组件
- [⚙️ 配置管理指南](configuration-guide.md) - 学会使用配置驱动开发
### 📖 API参考
- [📡 消息API](api/message-api.md) - 消息发送接口
### 🔧 高级主题
- [📦 依赖管理系统](dependency-management.md) - Python包依赖管理
- [🔧 工具系统详解](tool-system.md) - 工具系统的使用和开发
## 🔥 最新更新 (v2.0 新API格式)
### 🎉 重大变更
1. **新API格式**
- 不再使用 `self.api`,改为直接方法调用
- `await self.send_text()` 替代旧的发送方式
- `await self.send_emoji()` 专门的表情发送方法
- `self.get_config()` 简化的配置访问
2. **replyer_1集成**
- 新增专用的 `generator_api` 模块
- 在Action中直接使用 `replyer_1` 生成个性化内容
- 支持多种生成风格和情感色彩
3. **更好的类型安全**
- 完整的类型注解支持
- 更清晰的返回值类型
- 更好的IDE支持
## 🚀 最佳学习路径
### 📚 初学者路径(推荐)
1. **基础入门**
```
快速开始指南 → Action组件详解 → Command组件详解
```
2. **API掌握**
```
消息API指南 → 配置管理指南
```
3. **高级功能**
```
依赖管理系统 → 工具系统详解
```
## 💡 核心概念速览
### 🧱 Action组件
- **用途**:增强麦麦的主动行为,让对话更自然
- **激活**关键词、LLM判断、随机等多种方式
- **新特性**支持replyer_1智能生成、更简洁的API
### 💻 Command组件
- **用途**:响应用户的明确指令,提供确定性功能
- **触发**:正则表达式匹配用户输入
- **特点**:即时响应、参数解析、拦截控制
### ⚙️ 配置系统
- **Schema驱动**使用ConfigField定义配置结构
- **类型安全**:强类型配置验证
- **嵌套访问**:支持 `section.key` 形式访问
### 🧠 replyer_1集成
- **智能生成**AI驱动的个性化内容生成
- **简单易用**:通过 `generator_api` 轻松调用
- **灵活配置**:支持多种生成风格和参数
## 📋 开发清单
在开始开发之前,确保你已经:
- [ ] 阅读了[快速开始指南](quick-start.md)
- [ ] 了解了Action组件或Command组件
- [ ] 熟悉了[Action组件](action-components.md)或[Command组件](command-components.md)
- [ ] 查看了[配置管理](configuration-guide.md)
开发完成后,请检查:
- [ ] 使用了新的API格式`self.send_text()`等)
- [ ] 正确配置了Schema和ConfigField
- [ ] 添加了适当的错误处理
- [ ] 测试了所有功能路径
## 🤝 获取帮助
### 📖 文档问题
如果你在文档中发现错误或需要补充,请:
1. 检查最新的文档版本
2. 查看相关示例代码
3. 参考其他类似插件
### 💻 开发问题
遇到开发问题时:
1. 查看现有插件示例
2. 检查配置是否正确
3. 参考API文档
### 🎯 最佳实践建议
为了创建高质量的插件:
1. 始终使用新的API格式
2. 充分利用replyer_1的智能生成能力
3. 设计配置驱动的功能
4. 实现完善的错误处理
5. 编写清晰的文档注释
---
## 🌟 推荐插件示例
### 🎯 新手友好
- **Hello World插件**展示基础API使用
- **简单计算器**Command组件入门
- **智能问候**Action组件和replyer_1集成
### 🔧 实用工具
- **智能聊天助手**完整的replyer_1集成示例
- **用户管理系统**:配置驱动的复杂功能
- **定时提醒插件**:状态管理和持久化
### 🚀 高级应用
- **多功能聊天助手**:综合功能展示
- **游戏管理插件**:复杂状态管理
- **数据分析插件**:外部服务集成
---
**🎉 准备好开始了吗?从[快速开始指南](quick-start.md)开始你的插件开发之旅!**
使用新的API格式你可以创建更强大、更智能、更易维护的插件。让我们一起构建更好的MaiBot生态系统

View File

@@ -24,9 +24,9 @@ Action采用**两层决策机制**来优化性能和决策质量:
#### 激活类型说明 #### 激活类型说明
| 激活类型 | 说明 | 使用场景 | | 激活类型 | 说明 | 使用场景 |
|---------|-----|---------| | ------------- | ------------------------------------------- | ------------------------ |
| `NEVER` | 从不激活Action对麦麦不可见 | 临时禁用某个Action | | `NEVER` | 从不激活Action对麦麦不可见 | 临时禁用某个Action |
| `ALWAYS` | 永远激活Action总是在麦麦的候选池中 | 核心功能,如回复、表情 | | `ALWAYS` | 永远激活Action总是在麦麦的候选池中 | 核心功能,如回复、不回复 |
| `LLM_JUDGE` | 通过LLM智能判断当前情境是否需要激活此Action | 需要智能判断的复杂场景 | | `LLM_JUDGE` | 通过LLM智能判断当前情境是否需要激活此Action | 需要智能判断的复杂场景 |
| `RANDOM` | 基于随机概率决定是否激活 | 增加行为随机性的功能 | | `RANDOM` | 基于随机概率决定是否激活 | 增加行为随机性的功能 |
| `KEYWORD` | 当检测到特定关键词时激活 | 明确触发条件的功能 | | `KEYWORD` | 当检测到特定关键词时激活 | 明确触发条件的功能 |
@@ -34,7 +34,7 @@ Action采用**两层决策机制**来优化性能和决策质量:
#### 聊天模式控制 #### 聊天模式控制
| 模式 | 说明 | | 模式 | 说明 |
|-----|-----| | ------------------- | ------------------------ |
| `ChatMode.FOCUS` | 仅在专注聊天模式下可激活 | | `ChatMode.FOCUS` | 仅在专注聊天模式下可激活 |
| `ChatMode.NORMAL` | 仅在普通聊天模式下可激活 | | `ChatMode.NORMAL` | 仅在普通聊天模式下可激活 |
| `ChatMode.ALL` | 所有模式下都可激活 | | `ChatMode.ALL` | 所有模式下都可激活 |
@@ -44,6 +44,7 @@ Action采用**两层决策机制**来优化性能和决策质量:
**在Action被激活后使用条件决定麦麦什么时候会"选择"使用这个Action** **在Action被激活后使用条件决定麦麦什么时候会"选择"使用这个Action**
这一层由以下因素综合决定: 这一层由以下因素综合决定:
- `action_require`使用场景描述帮助LLM判断何时选择 - `action_require`使用场景描述帮助LLM判断何时选择
- `action_parameters`所需参数影响Action的可执行性 - `action_parameters`所需参数影响Action的可执行性
- 当前聊天上下文和麦麦的决策逻辑 - 当前聊天上下文和麦麦的决策逻辑
@@ -68,11 +69,13 @@ class EmojiAction(BaseAction):
``` ```
**决策流程** **决策流程**
1. **第一层激活判断** 1. **第一层激活判断**
- 普通模式:只有当用户消息包含"表情"、"emoji"或"😊"时,麦麦才"知道"可以使用这个Action - 普通模式:只有当用户消息包含"表情"、"emoji"或"😊"时,麦麦才"知道"可以使用这个Action
- 专注模式:随机激活,有概率让麦麦"看到"这个Action - 专注模式:随机激活,有概率让麦麦"看到"这个Action
2. **第二层使用决策** 2. **第二层使用决策**
- 即使Action被激活麦麦还会根据 `action_require`中的条件判断是否真正选择使用 - 即使Action被激活麦麦还会根据 `action_require`中的条件判断是否真正选择使用
- 例如:如果刚刚已经发过表情,根据"不要连续发送多个表情"的要求麦麦可能不会选择这个Action - 例如:如果刚刚已经发过表情,根据"不要连续发送多个表情"的要求麦麦可能不会选择这个Action
@@ -125,38 +128,147 @@ action_require = [
associated_types = ["text", "emoji", "image"] associated_types = ["text", "emoji", "image"]
``` ```
### 4. 动作记录必须项 ### 4. 新API导入必须项
每个 Action 在执行完成后,**必须**使用 `store_action_info` 记录动作信息。这是非常重要的,因为 使用新插件系统时必须导入所需的API模块
1. **记忆连续性**:让麦麦记住自己执行过的动作,避免重复执行
2. **上下文理解**:帮助麦麦理解自己的行为历史,做出更合理的决策
3. **行为追踪**:便于后续查询和分析麦麦的行为模式
```python ```python
async def execute(self) -> Tuple[bool, Optional[str]]: # 导入新API模块
from src.plugin_system.apis import generator_api, send_api, emoji_api
# 如果需要使用其他API
from src.plugin_system.apis import llm_api, database_api, message_api
```
### 5. 动作记录必须项
每个 Action 在执行完成后,**必须**使用 `store_action_info` 记录动作信息:
```python
async def execute(self) -> Tuple[bool, str]:
# ... 执行动作的代码 ... # ... 执行动作的代码 ...
if success: if success:
# 存储动作信息 # 存储动作信息 - 使用新API格式
await self.api.store_action_info( await self.store_action_info(
action_build_into_prompt=True, # 让麦麦知道这个动作 action_build_into_prompt=True, # 让麦麦知道这个动作
action_prompt_display=f"执行了xxx动作参数{param}", # 动作描述 action_prompt_display=f"执行了xxx动作参数{param}", # 动作描述
action_done=True, # 动作是否完成 action_done=True, # 动作是否完成
thinking_id=self.thinking_id, # 关联的思考ID
action_data={ # 动作的详细数据
"param1": value1,
"param2": value2
}
) )
return True, "动作执行成功" return True, "动作执行成功"
``` ```
> ⚠️ **重要提示**如果不记录动作信息,可能会导致以下问题: > ⚠️ **重要提示**新API格式中不再需要手动传递 `thinking_id` 等参数BaseAction会自动处理。
> - 麦麦不知道自己执行过什么动作,可能会重复执行
> - 无法追踪动作历史,影响后续决策 ## 🚀 新API使用指南
> - 在长对话中失去上下文连续性
> - 无法进行行为分析和优化 ### 📨 消息发送API
新的消息发送API更加简洁自动处理群聊/私聊逻辑:
```python
class MessageAction(BaseAction):
async def execute(self) -> Tuple[bool, str]:
# 发送文本消息 - 自动判断群聊/私聊
await self.send_text("Hello World!")
# 发送表情包
emoji_base64 = await emoji_api.get_by_description("开心")
if emoji_base64:
await self.send_emoji(emoji_base64)
# 发送图片
await self.send_image(image_base64)
# 发送自定义类型消息
await self.send_custom("video", video_data, typing=True)
return True, "消息发送完成"
```
### 🤖 智能生成API (replyer_1)
使用replyer_1生成个性化内容
```python
class SmartReplyAction(BaseAction):
async def execute(self) -> Tuple[bool, str]:
# 构建生成参数
reply_data = {
"text": "请生成一个友好的回复",
"style": "casual",
"topic": "日常聊天",
"replyer_name": "replyer_1" # 指定使用replyer_1
}
# 使用generator_api生成回复
success, reply_set = await generator_api.generate_reply(
chat_stream=self.chat_stream,
action_data=reply_data,
platform=self.platform,
chat_id=self.chat_id,
is_group=self.is_group
)
if success and reply_set:
# 提取并发送文本回复
for reply_type, reply_content in reply_set:
if reply_type == "text":
await self.send_text(reply_content)
elif reply_type == "emoji":
await self.send_emoji(reply_content)
# 记录动作
await self.store_action_info(
action_build_into_prompt=True,
action_prompt_display=f"使用replyer_1生成了智能回复",
action_done=True
)
return True, "智能回复生成成功"
else:
return False, "回复生成失败"
```
### ⚙️ 配置访问API
使用便捷的配置访问方法:
```python
class ConfigurableAction(BaseAction):
async def execute(self) -> Tuple[bool, str]:
# 获取插件配置 - 支持嵌套键访问
enable_feature = self.get_config("features.enable_smart_mode", False)
max_length = self.get_config("limits.max_text_length", 200)
style = self.get_config("behavior.response_style", "friendly")
if enable_feature:
# 启用高级功能
pass
return True, "配置获取成功"
```
### 📊 数据库API
使用新的数据库API存储和查询数据
```python
class DataAction(BaseAction):
async def execute(self) -> Tuple[bool, str]:
# 使用database_api
from src.plugin_system.apis import database_api
# 存储数据
await database_api.store_action_info(
chat_stream=self.chat_stream,
action_name=self.action_name,
action_data=self.action_data,
# ... 其他参数
)
return True, "数据存储完成"
```
## 🔧 激活类型详解 ## 🔧 激活类型详解
@@ -172,6 +284,30 @@ class GreetingAction(BaseAction):
# 关键词配置 # 关键词配置
activation_keywords = ["你好", "hello", "hi", "嗨"] activation_keywords = ["你好", "hello", "hi", "嗨"]
keyword_case_sensitive = False # 不区分大小写 keyword_case_sensitive = False # 不区分大小写
async def execute(self) -> Tuple[bool, str]:
# 可选使用replyer_1生成个性化问候
if self.get_config("greeting.use_smart_reply", False):
greeting_data = {
"text": "生成一个友好的问候语",
"replyer_name": "replyer_1"
}
success, reply_set = await generator_api.generate_reply(
chat_stream=self.chat_stream,
action_data=greeting_data
)
if success:
for reply_type, content in reply_set:
if reply_type == "text":
await self.send_text(content)
break
return True, "发送智能问候"
# 传统问候方式
await self.send_text("你好!很高兴见到你!")
return True, "发送问候"
``` ```
### LLM_JUDGE激活 ### LLM_JUDGE激活
@@ -192,6 +328,28 @@ class HelpAction(BaseAction):
请回答"是"或"否"。 请回答"是"或"否"。
""" """
async def execute(self) -> Tuple[bool, str]:
# 使用replyer_1生成帮助内容
help_data = {
"text": "用户需要帮助,请提供适当的帮助信息",
"help_type": self.action_data.get("help_type", "general"),
"replyer_name": "replyer_1"
}
success, reply_set = await generator_api.generate_reply(
chat_stream=self.chat_stream,
action_data=help_data
)
if success:
for reply_type, content in reply_set:
if reply_type == "text":
await self.send_text(content)
return True, "提供了帮助"
else:
await self.send_text("我来帮助你!有什么问题吗?")
return True, "提供了默认帮助"
``` ```
### RANDOM激活 ### RANDOM激活
@@ -205,463 +363,207 @@ class SurpriseAction(BaseAction):
# 随机激活概率 # 随机激活概率
random_activation_probability = 0.1 # 10%概率激活 random_activation_probability = 0.1 # 10%概率激活
```
### ALWAYS/NEVER激活
```python
class CoreAction(BaseAction):
focus_activation_type = ActionActivationType.ALWAYS # 总是激活
normal_activation_type = ActionActivationType.NEVER # 在普通模式下禁用
```
## 🎨 完整Action示例
### 智能问候Action
```python
from src.plugin_system import BaseAction, ActionActivationType, ChatMode
class SmartGreetingAction(BaseAction):
"""智能问候Action - 展示关键词激活的完整示例"""
# ===== 激活控制必须项 =====
focus_activation_type = ActionActivationType.KEYWORD
normal_activation_type = ActionActivationType.KEYWORD
mode_enable = ChatMode.ALL
parallel_action = False
# ===== 基本信息必须项 =====
action_name = "smart_greeting"
action_description = "智能问候系统,基于关键词触发,支持个性化问候消息"
# 关键词配置
activation_keywords = ["你好", "hello", "hi", "嗨", "问候", "早上好", "晚上好"]
keyword_case_sensitive = False
# ===== 功能定义必须项 =====
action_parameters = {
"username": "要问候的用户名(可选)",
"greeting_style": "问候风格casual(随意)、formal(正式)、friendly(友好)"
}
action_require = [
"用户发送包含问候词汇的消息时使用",
"检测到新用户加入时使用",
"响应友好交流需求时使用",
"避免在短时间内重复问候同一用户"
]
associated_types = ["text", "emoji"]
async def execute(self) -> Tuple[bool, str]: async def execute(self) -> Tuple[bool, str]:
"""执行智能问候""" import random
# 获取参数
username = self.action_data.get("username", "")
greeting_style = self.action_data.get("greeting_style", "casual")
# 根据风格生成问候消息 surprises = ["🎉", "✨", "🌟", "💝", "🎈"]
if greeting_style == "formal": selected = random.choice(surprises)
message = f"您好{username}!很荣幸为您服务!"
emoji = "🙏"
elif greeting_style == "friendly":
message = f"你好{username}!欢迎来到这里,希望我们能成为好朋友!"
emoji = "😊"
else: # casual
message = f"嗨{username}!很开心见到你~"
emoji = "👋"
# 发送消息 await self.send_emoji(selected)
await self.send_text(message) return True, f"发送了惊喜表情: {selected}"
await self.send_type("emoji", emoji)
return True, f"向{username or '用户'}发送了{greeting_style}风格的问候"
``` ```
### 智能禁言Action ## 💡 完整示例
以下是一个真实的群管理禁言Action示例展示了LLM判断、参数验证、配置管理等高级功能 ### 智能聊天Action
```python ```python
from typing import Optional from src.plugin_system.apis import generator_api, emoji_api
import random
from src.plugin_system.base.base_action import BaseAction
from src.plugin_system.base.component_types import ActionActivationType, ChatMode
class MuteAction(BaseAction): class IntelligentChatAction(BaseAction):
"""智能禁言Action - 基于LLM智能判断是否需要禁言""" """智能聊天Action - 展示新API的完整用法"""
# ===== 激活控制必须项 ===== # 激活设置
focus_activation_type = ActionActivationType.LLM_JUDGE # Focus模式使用LLM判定 focus_activation_type = ActionActivationType.ALWAYS
normal_activation_type = ActionActivationType.KEYWORD # Normal模式使用关键词 normal_activation_type = ActionActivationType.LLM_JUDGE
mode_enable = ChatMode.ALL mode_enable = ChatMode.ALL
parallel_action = False parallel_action = False
# ===== 基本信息必须项 ===== # 基本信息
action_name = "mute" action_name = "intelligent_chat"
action_description = "智能禁言系统基于LLM判断是否需要禁言" action_description = "使用replyer_1进行智能聊天回复支持表情包和个性化回复"
# ===== 激活配置 =====
# 关键词设置用于Normal模式
activation_keywords = ["禁言", "mute", "ban", "silence"]
keyword_case_sensitive = False
# LLM判定提示词用于Focus模式
llm_judge_prompt = """
判定是否需要使用禁言动作的严格条件:
使用禁言的情况:
1. 用户发送明显违规内容(色情、暴力、政治敏感等)
2. 恶意刷屏或垃圾信息轰炸
3. 用户主动明确要求被禁言("禁言我"等)
4. 严重违反群规的行为
5. 恶意攻击他人或群组管理
绝对不要使用的情况:
1. 正常聊天和交流
2. 情绪化表达但无恶意
3. 开玩笑或调侃,除非过分
4. 单纯的意见分歧或争论
"""
# ===== 功能定义必须项 =====
action_parameters = {
"target": "禁言对象,必填,输入你要禁言的对象的名字,请仔细思考不要弄错禁言对象",
"duration": "禁言时长,必填,输入你要禁言的时长(秒),单位为秒,必须为数字",
"reason": "禁言理由,可选",
}
action_require = [
"当有人违反了公序良俗的内容",
"当有人刷屏时使用",
"当有人发了擦边,或者色情内容时使用",
"当有人要求禁言自己时使用",
"如果某人已经被禁言了,就不要再次禁言了,除非你想追加时间!!",
]
associated_types = ["text", "command"]
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行智能禁言判定"""
# 获取参数
target = self.action_data.get("target")
duration = self.action_data.get("duration")
reason = self.action_data.get("reason", "违反群规")
# 参数验证
if not target:
await self.send_text("没有指定禁言对象呢~")
return False, "禁言目标不能为空"
if not duration:
await self.send_text("没有指定禁言时长呢~")
return False, "禁言时长不能为空"
# 获取时长限制配置
min_duration = self.api.get_config("mute.min_duration", 60)
max_duration = self.api.get_config("mute.max_duration", 2592000)
# 验证时长格式并转换
try:
duration_int = int(duration)
if duration_int <= 0:
await self.send_text("禁言时长必须是正数哦~")
return False, "禁言时长必须大于0"
# 限制禁言时长范围
if duration_int < min_duration:
duration_int = min_duration
elif duration_int > max_duration:
duration_int = max_duration
except (ValueError, TypeError):
await self.send_text("禁言时长必须是数字哦~")
return False, f"禁言时长格式无效: {duration}"
# 获取用户ID
try:
platform, user_id = await self.api.get_user_id_by_person_name(target)
except Exception as e:
await self.send_text("查找用户信息时出现问题~")
return False, f"查找用户ID时出错: {e}"
if not user_id:
await self.send_text(f"找不到 {target} 这个人呢~")
return False, f"未找到用户 {target} 的ID"
# 格式化时长显示
time_str = self._format_duration(duration_int)
# 获取模板化消息
message = self._get_template_message(target, time_str, reason)
await self.send_message_by_expressor(message)
# 发送群聊禁言命令
success = await self.send_command(
command_name="GROUP_BAN",
args={"qq_id": str(user_id), "duration": str(duration_int)},
display_message=f"禁言了 {target} {time_str}",
)
if success:
return True, f"成功禁言 {target},时长 {time_str}"
else:
await self.send_text("执行禁言动作失败")
return False, "发送禁言命令失败"
def _get_template_message(self, target: str, duration_str: str, reason: str) -> str:
"""获取模板化的禁言消息"""
templates = self.api.get_config(
"mute.templates",
[
"好的,禁言 {target} {duration},理由:{reason}",
"收到,对 {target} 执行禁言 {duration},因为{reason}",
"明白了,禁言 {target} {duration},原因是{reason}",
"哇哈哈哈哈哈,已禁言 {target} {duration},理由:{reason}",
],
)
template = random.choice(templates)
return template.format(target=target, duration=duration_str, reason=reason)
def _format_duration(self, seconds: int) -> str:
"""将秒数格式化为可读的时间字符串"""
if seconds < 60:
return f"{seconds}秒"
elif seconds < 3600:
minutes = seconds // 60
remaining_seconds = seconds % 60
if remaining_seconds > 0:
return f"{minutes}{remaining_seconds}秒"
else:
return f"{minutes}分钟"
else:
hours = seconds // 3600
remaining_minutes = (seconds % 3600) // 60
if remaining_minutes > 0:
return f"{hours}小时{remaining_minutes}分钟"
else:
return f"{hours}小时"
```
**关键特性说明**
1. **🎯 双模式激活**Focus模式使用LLM_JUDGE更谨慎Normal模式使用KEYWORD快速响应
2. **🧠 严格的LLM判定**详细提示词指导LLM何时应该/不应该使用禁言,避免误判
3. **✅ 完善的参数验证**验证必需参数、数值转换、用户ID查找等多重验证
4. **⚙️ 配置驱动**:时长限制、消息模板等都可通过配置文件自定义
5. **😊 友好的用户反馈**:错误提示清晰、随机化消息模板、时长格式化显示
6. **🛡️ 安全措施**:严格权限控制、防误操作验证、完整错误处理
### 智能助手Action
```python
class IntelligentHelpAction(BaseAction):
"""智能助手Action - 展示LLM判断激活的完整示例"""
# ===== 激活控制必须项 =====
focus_activation_type = ActionActivationType.LLM_JUDGE
normal_activation_type = ActionActivationType.RANDOM
mode_enable = ChatMode.ALL
parallel_action = True
# ===== 基本信息必须项 =====
action_name = "intelligent_help"
action_description = "智能助手,主动提供帮助和建议"
# LLM判断提示词 # LLM判断提示词
llm_judge_prompt = """ llm_judge_prompt = """
是否需要提供智能帮助的条件 是否需要进行智能聊天回复
1. 用户表达了困惑或需要帮助 1. 用户提出了有趣的话题
2. 对话中出现了技术问题 2. 需要更加个性化的回复
3. 用户寻求解决方案或建议 3. 适合发送表情包的情况
4. 适合提供额外信息的场合
不要使用的情况:
1. 用户明确表示不需要帮助
2. 对话进行得很顺利
3. 刚刚已经提供过帮助
请回答"是"或"否"。 请回答"是"或"否"。
""" """
# 随机激活概率 # 功能定义
random_activation_probability = 0.15
# ===== 功能定义必须项 =====
action_parameters = { action_parameters = {
"help_type": "帮助类型explanation(解释)、suggestion(建议)、guidance(指导)", "topic": "聊天话题",
"topic": "帮助主题或用户关心的问题", "mood": "当前氛围happy/sad/excited/calm",
"urgency": "紧急程度low(低)、medium(中)、high(高)" "include_emoji": "是否包含表情包true/false"
} }
action_require = [ action_require = [
"用户表达困惑或寻求帮助时使用", "需要更个性化回复时使用",
"检测到用户遇到技术问题时使用", "聊天氛围适合发送表情时使用",
"对话中出现知识盲点时主动提供帮助", "避免在正式场合使用"
"避免过度频繁地提供帮助,要恰到好处"
] ]
associated_types = ["text", "emoji"] associated_types = ["text", "emoji"]
async def execute(self) -> Tuple[bool, str]: async def execute(self) -> Tuple[bool, str]:
"""执行智能帮助"""
# 获取参数 # 获取参数
help_type = self.action_data.get("help_type", "suggestion") topic = self.action_data.get("topic", "日常聊天")
topic = self.action_data.get("topic", "") mood = self.action_data.get("mood", "happy")
urgency = self.action_data.get("urgency", "medium") include_emoji = self.action_data.get("include_emoji", "true") == "true"
# 根据帮助类型和紧急程度生成消息 # 构建智能回复数据
if help_type == "explanation": chat_data = {
message = f"关于{topic},让我来为你解释一下..." "text": f"请针对{topic}话题进行回复,当前氛围是{mood}",
elif help_type == "guidance": "topic": topic,
message = f"在{topic}方面,我可以为你提供一些指导..." "mood": mood,
else: # suggestion "style": "conversational",
message = f"针对{topic},我建议你可以尝试以下方法..." "replyer_name": "replyer_1" # 使用replyer_1
}
# 根据紧急程度调整表情 # 生成智能回复
if urgency == "high": success, reply_set = await generator_api.generate_reply(
emoji = "🚨" chat_stream=self.chat_stream,
elif urgency == "low": action_data=chat_data,
emoji = "💡" platform=self.platform,
chat_id=self.chat_id,
is_group=self.is_group
)
reply_sent = False
if success and reply_set:
# 发送生成的回复
for reply_type, content in reply_set:
if reply_type == "text":
await self.send_text(content)
reply_sent = True
elif reply_type == "emoji":
await self.send_emoji(content)
# 如果配置允许且生成失败,发送表情包
if include_emoji and not reply_sent:
emoji_result = await emoji_api.get_by_description(mood)
if emoji_result:
emoji_base64, emoji_desc, matched_emotion = emoji_result
await self.send_emoji(emoji_base64)
reply_sent = True
# 记录动作执行
if reply_sent:
await self.store_action_info(
action_build_into_prompt=True,
action_prompt_display=f"进行了智能聊天回复,话题:{topic},氛围:{mood}",
action_done=True
)
return True, f"完成智能聊天回复:{topic}"
else: else:
emoji = "🤔" return False, "智能回复生成失败"
# 发送帮助消息
await self.send_text(message)
await self.send_type("emoji", emoji)
return True, f"提供了{help_type}类型的帮助,主题:{topic}"
``` ```
## 📊 性能优化建议 ## 🛠️ 调试技巧
### 1. 合理使用激活类型 ### 开发调试Action
- **ALWAYS**: 仅用于核心功能
- **LLM_JUDGE**: 适度使用避免过多LLM调用
- **KEYWORD**: 优选,性能最好
- **RANDOM**: 控制概率,避免过于频繁
### 2. 优化execute方法
```python ```python
async def execute(self) -> Tuple[bool, str]: class DebugAction(BaseAction):
try: """调试Action - 展示如何调试新API"""
# 快速参数验证
if not self._validate_parameters():
return False, "参数验证失败"
# 核心逻辑 focus_activation_type = ActionActivationType.KEYWORD
result = await self._core_logic() normal_activation_type = ActionActivationType.KEYWORD
activation_keywords = ["debug", "调试"]
mode_enable = ChatMode.ALL
parallel_action = True
# 成功返回 action_name = "debug_helper"
return True, "执行成功" action_description = "调试助手,显示当前状态信息"
except Exception as e: action_parameters = {}
logger.error(f"{self.log_prefix} 执行失败: {e}") action_require = ["需要调试信息时使用"]
return False, f"执行失败: {str(e)}" associated_types = ["text"]
```
### 3. 合理设置并行执行
```python
# 轻量级Action可以并行
parallel_action = True # 如:发送表情、记录日志
# 重要Action应该独占
parallel_action = False # 如:回复消息、状态切换
```
## 🐛 调试技巧
### 1. 日志记录
```python
from src.common.logger import get_logger
logger = get_logger("my_action")
async def execute(self) -> Tuple[bool, str]: async def execute(self) -> Tuple[bool, str]:
logger.info(f"{self.log_prefix} 开始执行: {self.reasoning}") # 收集调试信息
logger.debug(f"{self.log_prefix} 参数: {self.action_data}") debug_info = {
"聊天类型": "群聊" if self.is_group else "私聊",
"平台": self.platform,
"目标ID": self.target_id,
"用户ID": self.user_id,
"用户昵称": self.user_nickname,
"动作数据": self.action_data,
}
# 执行逻辑... if self.is_group:
debug_info.update({
"群ID": self.group_id,
"群名": self.group_name,
})
logger.info(f"{self.log_prefix} 执行完成") # 格式化调试信息
info_lines = ["🔍 调试信息:"]
for key, value in debug_info.items():
info_lines.append(f" • {key}: {value}")
debug_text = "\n".join(info_lines)
# 发送调试信息
await self.send_text(debug_text)
# 测试配置获取
test_config = self.get_config("debug.verbose", True)
if test_config:
await self.send_text(f"配置测试: debug.verbose = {test_config}")
return True, "调试信息已发送"
``` ```
### 2. 激活状态检查 ## 📚 最佳实践
1. **总是导入所需的API模块**
```python ```python
# 在execute方法中检查激活原因 from src.plugin_system.apis import generator_api, send_api, emoji_api
def _debug_activation(self):
logger.debug(f"激活类型: Focus={self.focus_activation_type}, Normal={self.normal_activation_type}")
logger.debug(f"当前模式: {self.api.get_chat_mode()}")
logger.debug(f"激活原因: {self.reasoning}")
``` ```
2. **在生成内容时指定replyer_1**
### 3. 参数验证
```python ```python
def _validate_parameters(self) -> bool: action_data = {
required_params = ["param1", "param2"] "text": "生成内容的请求",
for param in required_params: "replyer_name": "replyer_1"
if param not in self.action_data: }
logger.warning(f"{self.log_prefix} 缺少必需参数: {param}")
return False
return True
``` ```
3. **使用便捷发送方法**
## 🎯 最佳实践
### 1. 清晰的Action命名
- 使用描述性的类名:`SmartGreetingAction` 而不是 `Action1`
- action_name要简洁明确`"smart_greeting"` 而不是 `"action_1"`
### 2. 完整的文档字符串
```python ```python
class MyAction(BaseAction): await self.send_text("文本") # 自动处理群聊/私聊
""" await self.send_emoji(emoji_base64)
我的Action - 一句话描述功能
详细描述Action的用途、激活条件、执行逻辑等。
激活条件:
- Focus模式关键词激活
- Normal模式LLM判断激活
执行逻辑:
1. 验证参数
2. 生成响应
3. 发送消息
"""
``` ```
4. **合理使用配置**
### 3. 错误处理
```python ```python
async def execute(self) -> Tuple[bool, str]: enable_feature = self.get_config("section.key", default_value)
try:
# 主要逻辑
pass
except ValueError as e:
await self.send_text("参数错误,请检查输入")
return False, f"参数错误: {e}"
except Exception as e:
await self.send_text("操作失败,请稍后重试")
return False, f"执行失败: {e}"
``` ```
5. **总是记录动作信息**
### 4. 配置驱动
```python ```python
# 从配置文件读取设置 await self.store_action_info(
enable_feature = self.api.get_config("my_action.enable_feature", True) action_build_into_prompt=True,
max_retries = self.api.get_config("my_action.max_retries", 3) action_prompt_display="动作描述",
action_done=True
)
``` ```
--- 通过使用新的API格式Action的开发变得更加简洁和强大
🎉 **现在你已经掌握了Action组件开发的完整知识继续学习 [Command组件详解](command-components.md) 来了解命令开发。**

View File

@@ -2,14 +2,14 @@
## 📖 概述 ## 📖 概述
消息API提供了发送各种类型消息的接口支持文本、表情、图片等多种消息类型,以及向不同目标发送消息的功能 消息API提供了发送各种类型消息的接口支持文本、表情、图片等多种消息类型。新版API格式更加简洁直观自动处理群聊/私聊判断
## 🔄 基础消息发送 ## 🔄 基础消息发送
### 发送文本消息 ### 发送文本消息
```python ```python
# 发送普通文本消息 # 新API格式 - 自动判断群聊/私聊
await self.send_text("这是一条文本消息") await self.send_text("这是一条文本消息")
# 发送多行文本 # 发送多行文本
@@ -21,60 +21,73 @@ message = """
await self.send_text(message.strip()) await self.send_text(message.strip())
``` ```
### 发送表情消息
```python
# 新API格式 - 发送表情
await self.send_emoji("😊")
await self.send_emoji("🎉")
await self.send_emoji("👋")
```
### 发送特定类型消息 ### 发送特定类型消息
```python ```python
# 发送表情
await self.send_type("emoji", "😊")
# 发送图片 # 发送图片
await self.send_type("image", "https://example.com/image.jpg") await self.send_type("image", "https://example.com/image.jpg")
# 发送音频 # 发送音频
await self.send_type("audio", "audio_file_path") await self.send_type("audio", "audio_file_path")
# 发送视频
await self.send_type("video", "video_file_path")
# 发送文件
await self.send_type("file", "file_path")
``` ```
### 发送命令消息 ## 🎯 跨目标消息发送
### 使用send_api模块发送消息
```python ```python
# 发送命令类型的消息 # 导入send_api
await self.send_command("system_command", {"param": "value"}) from src.plugin_system.apis import send_api
```
## 🎯 目标消息发送
### 向指定群聊发送消息
```python
# 向指定群聊发送文本消息 # 向指定群聊发送文本消息
success = await self.api.send_text_to_group( success = await send_api.text_to_group(
text="这是发送到群聊的消息", text="这是发送到群聊的消息",
group_id="123456789", group_id="123456789",
platform="qq" platform="qq"
) )
if success:
print("消息发送成功")
else:
print("消息发送失败")
```
### 向指定用户发送私聊消息
```python
# 向指定用户发送私聊消息 # 向指定用户发送私聊消息
success = await self.api.send_text_to_user( success = await send_api.text_to_user(
text="这是私聊消息", text="这是私聊消息",
user_id="987654321", user_id="987654321",
platform="qq" platform="qq"
) )
# 向指定群聊发送表情
success = await send_api.emoji_to_group(
emoji="😊",
group_id="123456789",
platform="qq"
)
# 向指定用户发送表情
success = await send_api.emoji_to_user(
emoji="🎉",
user_id="987654321",
platform="qq"
)
``` ```
### 通用目标消息发送 ### 通用目标消息发送
```python ```python
# 向任意目标发送任意类型消息 # 向任意目标发送任意类型消息
success = await self.api.send_message_to_target( success = await send_api.message_to_target(
message_type="text", # 消息类型 message_type="text", # 消息类型
content="消息内容", # 消息内容 content="消息内容", # 消息内容
platform="qq", # 平台 platform="qq", # 平台
@@ -88,23 +101,25 @@ success = await self.api.send_message_to_target(
### 支持的消息类型 ### 支持的消息类型
| 类型 | 说明 | 示例 | | 类型 | 说明 | 新API方法 | send_api方法 |
|-----|------|------| |-----|------|----------|-------------|
| `text` | 普通文本消息 | "Hello World" | | `text` | 普通文本消息 | `await self.send_text()` | `await send_api.text_to_group()` |
| `emoji` | 表情消息 | "😊" | | `emoji` | 表情消息 | `await self.send_emoji()` | `await send_api.emoji_to_group()` |
| `image` | 图片消息 | 图片URL或路径 | | `image` | 图片消息 | `await self.send_type("image", url)` | `await send_api.message_to_target()` |
| `audio` | 音频消息 | 音频文件路径 | | `audio` | 音频消息 | `await self.send_type("audio", path)` | `await send_api.message_to_target()` |
| `video` | 视频消息 | 视频文件路径 | | `video` | 视频消息 | `await self.send_type("video", path)` | `await send_api.message_to_target()` |
| `file` | 文件消息 | 文件路径 | | `file` | 文件消息 | `await self.send_type("file", path)` | `await send_api.message_to_target()` |
### 消息类型示例 ### 新API格式示例
```python ```python
# 文本消息 class ExampleAction(BaseAction):
await self.send_type("text", "普通文本") async def execute(self) -> Tuple[bool, str]:
# 文本消息 - 最常用
await self.send_text("普通文本消息")
# 表情消息 # 表情消息 - 直接方法
await self.send_type("emoji", "🎉") await self.send_emoji("🎉")
# 图片消息 # 图片消息
await self.send_type("image", "/path/to/image.jpg") await self.send_type("image", "/path/to/image.jpg")
@@ -114,50 +129,55 @@ await self.send_type("audio", "/path/to/audio.mp3")
# 文件消息 # 文件消息
await self.send_type("file", "/path/to/document.pdf") await self.send_type("file", "/path/to/document.pdf")
return True, "发送了多种类型的消息"
``` ```
## 🔍 消息查询 ## 🔍 消息信息获取
### 获取聊天类型
```python
# 获取当前聊天类型
chat_type = self.api.get_chat_type()
if chat_type == "group":
print("当前是群聊")
elif chat_type == "private":
print("当前是私聊")
```
### 获取最近消息
```python
# 获取最近的5条消息
recent_messages = self.api.get_recent_messages(count=5)
for message in recent_messages:
print(f"用户: {message.user_nickname}")
print(f"内容: {message.processed_plain_text}")
print(f"时间: {message.timestamp}")
```
### 获取当前消息信息 ### 获取当前消息信息
```python ```python
# 在Action或Command中获取当前处理的消息 # 新API格式 - 直接属性访问
current_message = self.message class ExampleCommand(BaseCommand):
async def execute(self) -> Tuple[bool, str]:
# 用户信息
user_id = self.user_id
user_nickname = self.user_nickname
# 消息基本信息 # 聊天信息
user_id = current_message.message_info.user_info.user_id is_group_chat = self.is_group
user_nickname = current_message.message_info.user_info.user_nickname chat_id = self.chat_id
message_content = current_message.processed_plain_text platform = self.platform
timestamp = current_message.timestamp
# 群聊信息(如果是群聊) # 消息内容
if current_message.message_info.group_info: message_text = self.message.processed_plain_text
group_id = current_message.message_info.group_info.group_id
group_name = current_message.message_info.group_info.group_name # 构建信息显示
info = f"""
👤 用户: {user_nickname}({user_id})
💬 类型: {'群聊' if is_group_chat else '私聊'}
📱 平台: {platform}
📝 内容: {message_text}
""".strip()
await self.send_text(info)
return True, "显示了消息信息"
```
### 获取群聊信息(如果适用)
```python
# 在Action或Command中检查群聊信息
if self.is_group:
group_info = self.message.message_info.group_info
if group_info:
group_id = group_info.group_id
group_name = getattr(group_info, 'group_name', '未知群聊')
await self.send_text(f"当前群聊: {group_name}({group_id})")
else:
await self.send_text("当前是私聊对话")
``` ```
## 🌐 平台支持 ## 🌐 平台支持
@@ -170,242 +190,209 @@ if current_message.message_info.group_info:
| 微信 | `wechat` | 微信聊天平台 | | 微信 | `wechat` | 微信聊天平台 |
| Discord | `discord` | Discord聊天平台 | | Discord | `discord` | Discord聊天平台 |
### 平台特定功能 ### 平台适配示例
```python ```python
class PlatformAdaptiveAction(BaseAction):
async def execute(self) -> Tuple[bool, str]:
# 获取当前平台 # 获取当前平台
current_platform = self.api.get_current_platform() current_platform = self.platform
# 根据平台调整消息格式 # 根据平台调整消息格式
if current_platform == "qq": if current_platform == "qq":
# QQ平台特定处理 await self.send_text("[QQ] 这是QQ平台的消息")
await self.send_text("[QQ] 消息内容") await self.send_emoji("🐧") # QQ企鹅表情
elif current_platform == "wechat": elif current_platform == "wechat":
# 微信平台特定处理 await self.send_text("【微信】这是微信平台的消息")
await self.send_text("【微信】消息内容") await self.send_emoji("💬") # 微信气泡表情
elif current_platform == "discord":
await self.send_text("**Discord** 这是Discord平台的消息")
await self.send_emoji("🎮") # Discord游戏表情
else:
await self.send_text(f"未知平台: {current_platform}")
return True, f"发送了{current_platform}平台适配消息"
``` ```
## 🎨 消息格式化 ## 🎨 消息格式化和高级功能
### Markdown支持 ### 长消息分割
```python ```python
# 发送Markdown格式的消息如果平台支持 # 自动处理长消息分割
markdown_message = """ long_message = "这是一条很长的消息..." * 100
**粗体文本**
*斜体文本*
`代码块`
[链接](https://example.com)
"""
await self.send_text(markdown_message) # 新API会自动处理长消息分割
await self.send_text(long_message)
``` ```
### 消息模板 ### 消息模板和格式化
```python ```python
# 使用模板生成消息 class TemplateMessageAction(BaseAction):
def format_user_info(username: str, level: int, points: int) -> str: async def execute(self) -> Tuple[bool, str]:
return f""" # 使用配置中的消息模板
👤 用户信息 template = self.get_config("messages.greeting_template", "你好 {username}")
━━━━━━━━━━━━━━━━━━
📛 用户名: {username}
⭐ 等级: Lv.{level}
💰 积分: {points:,}
━━━━━━━━━━━━━━━━━━
""".strip()
# 使用模板 # 格式化消息
user_info = format_user_info("张三", 15, 12580) formatted_message = template.format(
await self.send_text(user_info) username=self.user_nickname,
``` time=datetime.now().strftime("%H:%M"),
platform=self.platform
### 表情和Unicode
```python
# 发送Unicode表情
await self.send_text("消息发送成功 ✅")
# 发送表情包
await self.send_type("emoji", "🎉")
# 组合文本和表情
await self.send_text("恭喜你完成任务!🎊🎉")
```
## 🔄 流式消息
### 获取聊天流信息
```python
# 获取当前聊天流
chat_stream = self.api.get_service("chat_stream")
if chat_stream:
# 流基本信息
stream_id = chat_stream.stream_id
platform = chat_stream.platform
# 群聊信息
if chat_stream.group_info:
group_id = chat_stream.group_info.group_id
group_name = chat_stream.group_info.group_name
print(f"当前群聊: {group_name} ({group_id})")
# 用户信息
user_id = chat_stream.user_info.user_id
user_name = chat_stream.user_info.user_nickname
print(f"当前用户: {user_name} ({user_id})")
```
## 🚨 错误处理
### 消息发送错误处理
```python
async def safe_send_message(self, content: str) -> bool:
"""安全发送消息,包含错误处理"""
try:
await self.send_text(content)
return True
except Exception as e:
logger.error(f"消息发送失败: {e}")
# 发送错误提示
try:
await self.send_text("❌ 消息发送失败,请稍后重试")
except:
pass # 避免循环错误
return False
```
### 目标消息发送错误处理
```python
async def send_to_group_safely(self, text: str, group_id: str) -> bool:
"""安全向群聊发送消息"""
try:
success = await self.api.send_text_to_group(
text=text,
group_id=group_id,
platform="qq"
) )
if not success: await self.send_text(formatted_message)
logger.warning(f"向群聊 {group_id} 发送消息失败")
return success # 根据配置决定是否发送表情
if self.get_config("messages.include_emoji", True):
await self.send_emoji("😊")
except Exception as e: return True, "发送了模板化消息"
logger.error(f"向群聊发送消息异常: {e}")
return False
``` ```
## 📊 最佳实践 ### 条件消息发送
### 1. 消息长度控制
```python ```python
async def send_long_message(self, content: str, max_length: int = 500): class ConditionalMessageAction(BaseAction):
"""发送长消息,自动分段""" async def execute(self) -> Tuple[bool, str]:
if len(content) <= max_length: # 根据用户类型发送不同消息
if self.is_group:
await self.send_text(f"群聊消息 - 当前群成员: @{self.user_nickname}")
else:
await self.send_text(f"私聊消息 - 你好 {self.user_nickname}")
# 根据时间发送不同表情
from datetime import datetime
hour = datetime.now().hour
if 6 <= hour < 12:
await self.send_emoji("🌅") # 早上
elif 12 <= hour < 18:
await self.send_emoji("☀️") # 下午
else:
await self.send_emoji("🌙") # 晚上
return True, "发送了条件化消息"
```
## 🛠️ 高级消息发送功能
### 批量消息发送
```python
class BatchMessageAction(BaseAction):
async def execute(self) -> Tuple[bool, str]:
messages = [
("text", "第一条消息"),
("emoji", "🎉"),
("text", "第二条消息"),
("emoji", "✨")
]
for msg_type, content in messages:
if msg_type == "text":
await self.send_text(content) await self.send_text(content)
else: elif msg_type == "emoji":
# 分段发送 await self.send_emoji(content)
parts = [content[i:i+max_length] for i in range(0, len(content), max_length)]
for i, part in enumerate(parts):
prefix = f"[{i+1}/{len(parts)}] " if len(parts) > 1 else ""
await self.send_text(f"{prefix}{part}")
# 避免发送过快 # 可选:添加延迟避免消息发送过快
if i < len(parts) - 1: import asyncio
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
return True, "发送了批量消息"
``` ```
### 2. 消息格式规范 ### 错误处理和重试
```python ```python
class MessageFormatter: class ReliableMessageAction(BaseAction):
"""消息格式化工具类""" async def execute(self) -> Tuple[bool, str]:
max_retries = 3
retry_count = 0
@staticmethod while retry_count < max_retries:
def success(message: str) -> str: try:
return f"✅ {message}" await self.send_text("重要消息")
return True, "消息发送成功"
except Exception as e:
retry_count += 1
logger.warning(f"消息发送失败 (尝试 {retry_count}/{max_retries}): {e}")
@staticmethod if retry_count < max_retries:
def error(message: str) -> str: import asyncio
return f"❌ {message}" await asyncio.sleep(1) # 等待1秒后重试
@staticmethod return False, "消息发送失败,已达到最大重试次数"
def warning(message: str) -> str:
return f"⚠️ {message}"
@staticmethod
def info(message: str) -> str:
return f" {message}"
# 使用示例
await self.send_text(MessageFormatter.success("操作成功完成"))
await self.send_text(MessageFormatter.error("操作失败,请重试"))
``` ```
### 3. 异步消息处理 ## 📝 最佳实践
### 1. 消息发送最佳实践
```python ```python
async def batch_send_messages(self, messages: List[str]): # ✅ 好的做法
"""批量发送消息""" class GoodMessageAction(BaseAction):
tasks = [] async def execute(self) -> Tuple[bool, str]:
# 1. 检查配置
if not self.get_config("features.enable_messages", True):
return True, "消息功能已禁用"
for message in messages: # 2. 简洁的消息发送
task = self.send_text(message) await self.send_text("简洁明了的消息")
tasks.append(task)
# 并发发送,但控制并发数 # 3. 适当的表情使用
semaphore = asyncio.Semaphore(3) # 最多3个并发 if self.get_config("features.enable_emoji", True):
await self.send_emoji("😊")
async def send_with_limit(message): return True, "消息发送完成"
async with semaphore:
# ❌ 避免的做法
class BadMessageAction(BaseAction):
async def execute(self) -> Tuple[bool, str]:
# 避免:过长的消息
await self.send_text("这是一条非常非常长的消息" * 50)
# 避免:过多的表情
for emoji in ["😊", "🎉", "✨", "🌟", "💫"]:
await self.send_emoji(emoji)
return True, "发送了糟糕的消息"
```
### 2. 错误处理
```python
# ✅ 推荐的错误处理
class SafeMessageAction(BaseAction):
async def execute(self) -> Tuple[bool, str]:
try:
message = self.get_config("messages.default", "默认消息")
await self.send_text(message) await self.send_text(message)
return True, "消息发送成功"
await asyncio.gather(*[send_with_limit(msg) for msg in messages]) except Exception as e:
logger.error(f"消息发送失败: {e}")
# 可选:发送备用消息
await self.send_text("消息发送遇到问题,请稍后再试")
return False, f"发送失败: {str(e)}"
``` ```
### 4. 消息缓存 ### 3. 性能优化
```python ```python
class MessageCache: # ✅ 性能友好的消息发送
"""消息缓存管理""" class OptimizedMessageAction(BaseAction):
async def execute(self) -> Tuple[bool, str]:
# 合并多个短消息为一条长消息
parts = [
"第一部分信息",
"第二部分信息",
"第三部分信息"
]
def __init__(self): combined_message = "\n".join(parts)
self._cache = {} await self.send_text(combined_message)
self._max_size = 100
def get_cached_message(self, key: str) -> Optional[str]: return True, "发送了优化的消息"
return self._cache.get(key)
def cache_message(self, key: str, message: str):
if len(self._cache) >= self._max_size:
# 删除最旧的缓存
oldest_key = next(iter(self._cache))
del self._cache[oldest_key]
self._cache[key] = message
# 使用缓存避免重复生成消息
cache = MessageCache()
async def send_user_info(self, user_id: str):
cache_key = f"user_info_{user_id}"
cached_message = cache.get_cached_message(cache_key)
if cached_message:
await self.send_text(cached_message)
else:
# 生成新消息
message = await self._generate_user_info(user_id)
cache.cache_message(cache_key, message)
await self.send_text(message)
``` ```
--- 通过新的API格式消息发送变得更加简洁高效
🎉 **现在你已经掌握了消息API的完整用法继续学习其他API接口。**

View File

@@ -73,7 +73,7 @@ class SimpleCommand(BaseCommand):
### 参数捕获 ### 参数捕获
使用命名组 `(?P<name>pattern)` 捕获参数: 使用命名组 `(?P<n>pattern)` 捕获参数:
```python ```python
class UserCommand(BaseCommand): class UserCommand(BaseCommand):
@@ -336,7 +336,7 @@ class SystemInfoCommand(BaseCommand):
async def _show_plugin_info(self): async def _show_plugin_info(self):
"""显示插件信息""" """显示插件信息"""
# 通过API获取插件信息 # 通过配置获取插件信息
plugins = await self._get_loaded_plugins() plugins = await self._get_loaded_plugins()
plugin_info = f""" plugin_info = f"""
@@ -349,7 +349,7 @@ class SystemInfoCommand(BaseCommand):
async def _get_loaded_plugins(self) -> list: async def _get_loaded_plugins(self) -> list:
"""获取已加载的插件列表""" """获取已加载的插件列表"""
# 这里可以通过self.api获取实际的插件信息 # 这里可以通过配置或API获取实际的插件信息
return [ return [
{"name": "core_actions", "active": True}, {"name": "core_actions", "active": True},
{"name": "example_plugin", "active": True}, {"name": "example_plugin", "active": True},
@@ -386,6 +386,55 @@ class CustomPrefixCommand(BaseCommand):
return True, f"投掷了{count}面骰子,结果{result}" return True, f"投掷了{count}面骰子,结果{result}"
``` ```
## 🔧 新API格式使用指南
### 消息发送
```python
# 新API格式 ✅
await self.send_text("消息内容")
await self.send_emoji("😊")
# 旧API格式 ❌
await self.api.send_text_to_group("消息内容", group_id, "qq")
```
### 配置访问
```python
# 新API格式 ✅
config_value = self.get_config("section.key", "default_value")
# 旧API格式 ❌
config_value = self.api.get_config("section.key", "default_value")
```
### 用户信息获取
```python
# 新API格式 ✅
user_id = self.user_id
user_nickname = self.user_nickname
is_group_chat = self.is_group
# 旧API格式 ❌
user_id = self.message.message_info.user_info.user_id
```
### 动作记录
```python
# 新API格式 ✅ (在Action中)
await self.store_action_info(
action_build_into_prompt=True,
action_prompt_display="执行了某操作",
action_done=True
)
# 旧API格式 ❌
await self.api.store_action_info(...)
```
## 📊 性能优化建议 ## 📊 性能优化建议
### 1. 正则表达式优化 ### 1. 正则表达式优化
@@ -398,83 +447,39 @@ command_pattern = r"^/ping$"
command_pattern = r"^/(?:ping|pong|test|check|status|info|help|...)" command_pattern = r"^/(?:ping|pong|test|check|status|info|help|...)"
# ✅ 好的做法 - 分离复杂逻辑 # ✅ 好的做法 - 分离复杂逻辑
class PingCommand(BaseCommand):
command_pattern = r"^/ping$"
class StatusCommand(BaseCommand):
command_pattern = r"^/status$"
``` ```
### 2. 参数验证 ### 2. 参数验证
```python ```python
# ✅ 好的做法 - 早期验证
async def execute(self) -> Tuple[bool, Optional[str]]: async def execute(self) -> Tuple[bool, Optional[str]]:
# 快速参数验证
username = self.matched_groups.get("username") username = self.matched_groups.get("username")
if not username or len(username) < 2: if not username:
await self.send_text("❌ 用户名不合法") await self.send_text("❌ 请提供用户名")
return False, "参数验证失败" return False, "缺少参数"
# 主要逻辑 # 继续处理...
...
``` ```
### 3. 异常处理 ### 3. 错误处理
```python ```python
# ✅ 好的做法 - 完整错误处理
async def execute(self) -> Tuple[bool, Optional[str]]: async def execute(self) -> Tuple[bool, Optional[str]]:
try: try:
# 命令逻辑 # 主要逻辑
result = await self._do_command() result = await self._process_command()
return True, "执行成功" return True, "执行成功"
except ValueError as e: except ValueError as e:
await self.send_text(f"❌ 参数错误: {e}") await self.send_text(f"❌ 参数错误: {e}")
return False, f"参数错误: {e}" return False, f"参数错误: {e}"
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix} 命令执行失败: {e}") await self.send_text(f"❌ 执行失败: {e}")
await self.send_text("❌ 命令执行失败")
return False, f"执行失败: {e}" return False, f"执行失败: {e}"
``` ```
## 🐛 调试技巧 通过新的API格式Command开发变得更加简洁和直观
### 1. 正则测试
```python
import re
pattern = r"^/user\s+(?P<action>add|del)\s+(?P<username>\w+)$"
test_inputs = [
"/user add 张三",
"/user del 李四",
"/user info 王五", # 不匹配
]
for input_text in test_inputs:
match = re.match(pattern, input_text)
print(f"'{input_text}' -> {match.groupdict() if match else 'No match'}")
```
### 2. 参数调试
```python
async def execute(self) -> Tuple[bool, Optional[str]]:
# 调试输出
logger.debug(f"匹配组: {self.matched_groups}")
logger.debug(f"原始消息: {self.message.processed_plain_text}")
# 命令逻辑...
```
### 3. 拦截测试
```python
# 测试不同的拦截设置
intercept_message = True # 测试拦截
intercept_message = False # 测试不拦截
# 观察后续Action是否被触发
```
## 🎯 最佳实践 ## 🎯 最佳实践

View File

@@ -1,255 +0,0 @@
# 🔧 插件配置访问指南
> **💡 阅读须知**
>
> 本文主要介绍如何在插件的 **Action** 或 **Command** 组件中 **访问(读取)** 配置值。
>
> 如果你还不了解如何为插件 **定义** 配置并让系统 **自动生成** 带注释的 `config.toml` 文件,请务必先阅读 ➡️ **[⚙️ 插件配置定义指南](configuration-guide.md)**。
## 问题描述
在插件开发中,你可能遇到这样的问题:
- `get_config`方法只在`BasePlugin`类中
- `BaseAction``BaseCommand`无法直接继承这个方法
- 想要在Action或Command中访问插件配置
## ✅ 解决方案
**直接使用 `self.api.get_config()` 方法!**
系统已经自动为你处理了配置传递,你只需要通过`PluginAPI`访问配置即可。
## 📖 快速示例
### 在Action中访问配置
```python
from src.plugin_system import BaseAction
class MyAction(BaseAction):
async def execute(self):
# 方法1: 获取配置值(带默认值)
api_key = self.api.get_config("api.key", "default_key")
timeout = self.api.get_config("api.timeout", 30)
# 方法2: 检查配置是否存在
if self.api.has_config("features.premium"):
premium_enabled = self.api.get_config("features.premium")
# 使用高级功能
# 方法3: 支持嵌套键访问
log_level = self.api.get_config("advanced.logging.level", "INFO")
# 方法4: 获取所有配置
all_config = self.api.get_all_config()
await self.send_text(f"API密钥: {api_key}")
return True, "配置访问成功"
```
### 在Command中访问配置
```python
from src.plugin_system import BaseCommand
class MyCommand(BaseCommand):
async def execute(self):
# 使用方式与Action完全相同
welcome_msg = self.api.get_config("messages.welcome", "欢迎!")
max_results = self.api.get_config("search.max_results", 10)
# 根据配置执行不同逻辑
if self.api.get_config("features.debug_mode", False):
await self.send_text(f"调试模式已启用,最大结果数: {max_results}")
await self.send_text(welcome_msg)
return True, "命令执行完成"
```
## 🔧 API方法详解
### 1. `get_config(key, default=None)`
获取配置值,支持嵌套键访问:
```python
# 简单键
value = self.api.get_config("timeout", 30)
# 嵌套键(用点号分隔)
value = self.api.get_config("database.connection.host", "localhost")
value = self.api.get_config("features.ai.model", "gpt-3.5-turbo")
```
### 2. `has_config(key)`
检查配置项是否存在:
```python
if self.api.has_config("api.secret_key"):
# 配置存在,可以安全使用
secret = self.api.get_config("api.secret_key")
else:
# 配置不存在,使用默认行为
pass
```
### 3. `get_all_config()`
获取所有配置的副本:
```python
all_config = self.api.get_all_config()
for section, config in all_config.items():
print(f"配置节: {section}, 包含 {len(config)} 项配置")
```
## 📁 配置文件示例
假设你的插件有这样的配置文件 `config.toml`
```toml
[api]
key = "your_api_key"
timeout = 30
base_url = "https://api.example.com"
[features]
enable_cache = true
debug_mode = false
max_retries = 3
[messages]
welcome = "欢迎使用我的插件!"
error = "出现了错误,请稍后重试"
[advanced]
[advanced.logging]
level = "INFO"
file_path = "logs/plugin.log"
[advanced.cache]
ttl_seconds = 3600
max_size = 100
```
## 🎯 实际使用案例
### 案例1API调用配置
```python
class ApiAction(BaseAction):
async def execute(self):
# 获取API配置
api_key = self.api.get_config("api.key")
if not api_key:
await self.send_text("❌ API密钥未配置")
return False, "缺少API密钥"
timeout = self.api.get_config("api.timeout", 30)
base_url = self.api.get_config("api.base_url", "https://api.example.com")
# 使用配置进行API调用
# ... API调用逻辑
return True, "API调用完成"
```
### 案例2功能开关配置
```python
class FeatureCommand(BaseCommand):
async def execute(self):
# 检查功能开关
if not self.api.get_config("features.enable_cache", True):
await self.send_text("缓存功能已禁用")
return True, "功能被禁用"
# 检查调试模式
debug_mode = self.api.get_config("features.debug_mode", False)
if debug_mode:
await self.send_text("🐛 调试模式已启用")
max_retries = self.api.get_config("features.max_retries", 3)
# 使用重试配置
return True, "功能执行完成"
```
### 案例3个性化消息配置
```python
class WelcomeAction(BaseAction):
async def execute(self):
# 获取个性化消息
welcome_msg = self.api.get_config("messages.welcome", "欢迎!")
# 检查是否有自定义问候语列表
if self.api.has_config("messages.custom_greetings"):
greetings = self.api.get_config("messages.custom_greetings", [])
if greetings:
import random
welcome_msg = random.choice(greetings)
await self.send_text(welcome_msg)
return True, "发送了个性化问候"
```
## 🔄 配置传递机制
系统自动处理配置传递,无需手动操作:
1. **插件初始化**`BasePlugin`加载`config.toml``self.config`
2. **组件注册** → 系统记录插件配置
3. **组件实例化** → 自动传递`plugin_config`参数给Action/Command
4. **API初始化** → 配置保存到`PluginAPI`实例中
5. **组件使用** → 通过`self.api.get_config()`访问
## ⚠️ 注意事项
### 1. 总是提供默认值
```python
# ✅ 好的做法
timeout = self.api.get_config("api.timeout", 30)
# ❌ 避免这样做
timeout = self.api.get_config("api.timeout") # 可能返回None
```
### 2. 验证配置类型
```python
# 获取配置后验证类型
max_items = self.api.get_config("list.max_items", 10)
if not isinstance(max_items, int) or max_items <= 0:
max_items = 10 # 使用安全的默认值
```
### 3. 缓存复杂配置解析
```python
class MyAction(BaseAction):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 在初始化时解析复杂配置,避免重复解析
self._api_config = self._parse_api_config()
def _parse_api_config(self):
return {
'key': self.api.get_config("api.key", ""),
'timeout': self.api.get_config("api.timeout", 30),
'retries': self.api.get_config("api.max_retries", 3)
}
```
## 🎉 总结
现在你知道了在Action和Command中访问配置很简单
```python
# 这就是你需要的全部代码!
config_value = self.api.get_config("your.config.key", "default_value")
```
不需要继承`BasePlugin`,不需要复杂的配置传递,`PluginAPI`已经为你准备好了一切!

View File

@@ -1,10 +1,30 @@
# ⚙️ 插件配置定义指南 # ⚙️ 插件配置完整指南
本文档将指导你如何为你的插件定义一个健壮、规范且自带文档的配置文件 本文档将全面指导你如何为你的插件**定义配置**和在组件中**访问配置**,帮助你构建一个健壮、规范且自带文档的配置系统
## 核心理念Schema驱动的配置 > **🚨 重要原则:任何时候都不要手动创建 config.toml 文件!**
>
> 系统会根据你在代码中定义的 `config_schema` 自动生成配置文件。手动创建配置文件会破坏自动化流程,导致配置不一致、缺失注释和文档等问题。
在新版插件系统中,我们引入了一套 **配置Schema模式驱动** 的机制。你不再需要手动创建和维护 `config.toml` 文件,而是通过在插件代码中 **声明配置的结构**,系统将为你完成剩下的工作。 ## 📖 目录
1. [配置定义Schema驱动的配置系统](#配置定义schema驱动的配置系统)
2. [配置访问在Action和Command中使用配置](#配置访问在action和command中使用配置)
3. [完整示例:从定义到使用](#完整示例从定义到使用)
4. [最佳实践与注意事项](#最佳实践与注意事项)
---
## 配置定义Schema驱动的配置系统
### 核心理念Schema驱动的配置
在新版插件系统中,我们引入了一套 **配置Schema模式驱动** 的机制。**你不需要也不应该手动创建和维护 `config.toml` 文件**,而是通过在插件代码中 **声明配置的结构**,系统将为你完成剩下的工作。
> **⚠️ 绝对不要手动创建 config.toml 文件!**
>
> - ❌ **错误做法**:手动在插件目录下创建 `config.toml` 文件
> - ✅ **正确做法**:在插件代码中定义 `config_schema`,让系统自动生成配置文件
**核心优势:** **核心优势:**
@@ -14,9 +34,24 @@
- **健壮性 (Robustness)**: 在代码中直接定义配置的类型和默认值,减少了因配置错误导致的运行时问题。 - **健壮性 (Robustness)**: 在代码中直接定义配置的类型和默认值,减少了因配置错误导致的运行时问题。
- **易于管理 (Easy Management)**: 生成的配置文件可以方便地加入 `.gitignore`避免将个人配置如API Key提交到版本库。 - **易于管理 (Easy Management)**: 生成的配置文件可以方便地加入 `.gitignore`避免将个人配置如API Key提交到版本库。
--- ### 配置生成工作流程
## 如何定义配置 ```mermaid
graph TD
A[编写插件代码] --> B[定义 config_schema]
B --> C[首次加载插件]
C --> D{config.toml 是否存在?}
D -->|不存在| E[系统自动生成 config.toml]
D -->|存在| F[加载现有配置文件]
E --> G[配置完成,插件可用]
F --> G
style E fill:#90EE90
style B fill:#87CEEB
style G fill:#DDA0DD
```
### 如何定义配置
配置的定义在你的插件主类(继承自 `BasePlugin`)中完成,主要通过两个类属性: 配置的定义在你的插件主类(继承自 `BasePlugin`)中完成,主要通过两个类属性:
@@ -41,18 +76,13 @@ class ConfigField:
choices: Optional[List[Any]] = None # 可选值列表 (可选) choices: Optional[List[Any]] = None # 可选值列表 (可选)
``` ```
--- ### 配置定义示例
## 完整示例
让我们以一个功能丰富的 `MutePlugin` 为例,看看如何定义它的配置。 让我们以一个功能丰富的 `MutePlugin` 为例,看看如何定义它的配置。
### 1. 插件代码 (`plugin.py`)
```python ```python
# src/plugins/built_in/mute_plugin/plugin.py # src/plugins/built_in/mute_plugin/plugin.py
# ... 其他导入 ...
from src.plugin_system import BasePlugin, register_plugin from src.plugin_system import BasePlugin, register_plugin
from src.plugin_system.base.config_types import ConfigField from src.plugin_system.base.config_types import ConfigField
from typing import List, Tuple, Type from typing import List, Tuple, Type
@@ -119,13 +149,20 @@ class MutePlugin(BasePlugin):
} }
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
# ... 组件注册逻辑 ...
# 在这里可以通过 self.get_config() 来获取配置值 # 在这里可以通过 self.get_config() 来获取配置值
pass enable_smart_mute = self.get_config("components.enable_smart_mute", True)
enable_mute_command = self.get_config("components.enable_mute_command", False)
components = []
if enable_smart_mute:
components.append((SmartMuteAction.get_action_info(), SmartMuteAction))
if enable_mute_command:
components.append((MuteCommand.get_command_info(), MuteCommand))
return components
``` ```
### 2. 自动生成的配置文件 (`config.toml`) ### 自动生成的配置文件
`mute_plugin` 首次加载且其目录中不存在 `config.toml` 时,系统会自动创建以下文件: `mute_plugin` 首次加载且其目录中不存在 `config.toml` 时,系统会自动创建以下文件:
@@ -190,13 +227,405 @@ level = "INFO"
# 日志记录前缀 # 日志记录前缀
# 示例: [MyMutePlugin] # 示例: [MyMutePlugin]
prefix = "[MutePlugin]" prefix = "[MutePlugin]"
``` ```
## 最佳实践 ---
1. **定义优于创建**: 始终优先在 `plugin.py` 中定义 `config_schema`,而不是手动创建 `config.toml` ## 配置访问在Action和Command中使用配置
2. **描述清晰**: 为每个 `ConfigField``config_section_descriptions` 编写清晰、准确的描述。这会直接成为你的插件文档的一部分。
3. **提供合理默认值**: 确保你的插件在默认配置下就能正常运行(或处于一个安全禁用的状态)。 ### 问题描述
4. **gitignore**: 将 `plugins/*/config.toml``src/plugins/built_in/*/config.toml` 加入 `.gitignore`,以避免提交个人敏感信息。
5. **访问配置**: 在插件的任何地方,统一使用 `self.api.get_config("section.key", "default_value")` 来安全地获取配置值。详细请参考 [**插件配置访问指南**](config-access-guide.md)。 在插件开发中,你可能遇到这样的问题:
- 想要在Action或Command中访问插件配置
### ✅ 解决方案
**直接使用 `self.get_config()` 方法!**
系统已经自动为你处理了配置传递,你只需要通过组件内置的 `get_config` 方法访问配置即可。
### 📖 快速示例
#### 在Action中访问配置
```python
from src.plugin_system import BaseAction
class MyAction(BaseAction):
async def execute(self):
# 方法1: 获取配置值(带默认值)
api_key = self.get_config("api.key", "default_key")
timeout = self.get_config("api.timeout", 30)
# 方法2: 支持嵌套键访问
log_level = self.get_config("advanced.logging.level", "INFO")
# 方法3: 直接访问顶层配置
enable_feature = self.get_config("features.enable_smart", False)
# 使用配置值
if enable_feature:
await self.send_text(f"API密钥: {api_key}")
return True, "配置访问成功"
```
#### 在Command中访问配置
```python
from src.plugin_system import BaseCommand
class MyCommand(BaseCommand):
async def execute(self):
# 使用方式与Action完全相同
welcome_msg = self.get_config("messages.welcome", "欢迎!")
max_results = self.get_config("search.max_results", 10)
# 根据配置执行不同逻辑
if self.get_config("features.debug_mode", False):
await self.send_text(f"调试模式已启用,最大结果数: {max_results}")
await self.send_text(welcome_msg)
return True, "命令执行完成"
```
### 🔧 API方法详解
#### 1. `get_config(key, default=None)`
获取配置值,支持嵌套键访问:
```python
# 简单键
value = self.get_config("timeout", 30)
# 嵌套键(用点号分隔)
value = self.get_config("database.connection.host", "localhost")
value = self.get_config("features.ai.model", "gpt-3.5-turbo")
```
#### 2. 类型安全的配置访问
```python
# 确保正确的类型
max_retries = self.get_config("api.max_retries", 3)
if not isinstance(max_retries, int):
max_retries = 3 # 使用安全的默认值
# 布尔值配置
debug_mode = self.get_config("features.debug_mode", False)
if debug_mode:
# 调试功能逻辑
pass
```
#### 3. 配置驱动的组件行为
```python
class ConfigDrivenAction(BaseAction):
async def execute(self):
# 根据配置决定激活行为
activation_config = {
"use_keywords": self.get_config("activation.use_keywords", True),
"use_llm": self.get_config("activation.use_llm", False),
"keywords": self.get_config("activation.keywords", []),
}
# 根据配置调整功能
features = {
"enable_emoji": self.get_config("features.enable_emoji", True),
"enable_llm_reply": self.get_config("features.enable_llm_reply", False),
"max_length": self.get_config("output.max_length", 200),
}
# 使用配置执行逻辑
if features["enable_llm_reply"]:
# 使用LLM生成回复
pass
else:
# 使用模板回复
pass
return True, "配置驱动执行完成"
```
### 🔄 配置传递机制
系统自动处理配置传递,无需手动操作:
1. **插件初始化**`BasePlugin`加载`config.toml``self.config`
2. **组件注册** → 系统记录插件配置
3. **组件实例化** → 自动传递`plugin_config`参数给Action/Command
4. **配置访问** → 组件通过`self.get_config()`直接访问配置
---
## 完整示例:从定义到使用
### 插件定义
```python
from src.plugin_system.base.config_types import ConfigField
@register_plugin
class GreetingPlugin(BasePlugin):
"""问候插件完整示例"""
plugin_name = "greeting_plugin"
plugin_description = "智能问候插件,展示配置定义和访问的完整流程"
plugin_version = "1.0.0"
config_file_name = "config.toml"
# 配置节描述
config_section_descriptions = {
"plugin": "插件基本信息",
"greeting": "问候功能配置",
"features": "功能开关配置",
"messages": "消息模板配置"
}
# 配置Schema定义
config_schema = {
"plugin": {
"enabled": ConfigField(type=bool, default=True, description="是否启用插件"),
"version": ConfigField(type=str, default="1.0.0", description="插件版本")
},
"greeting": {
"template": ConfigField(
type=str,
default="你好,{username}!欢迎使用问候插件!",
description="问候消息模板"
),
"enable_emoji": ConfigField(type=bool, default=True, description="是否启用表情符号"),
"enable_llm": ConfigField(type=bool, default=False, description="是否使用LLM生成个性化问候")
},
"features": {
"smart_detection": ConfigField(type=bool, default=True, description="是否启用智能检测"),
"random_greeting": ConfigField(type=bool, default=False, description="是否使用随机问候语"),
"max_greetings_per_hour": ConfigField(type=int, default=5, description="每小时最大问候次数")
},
"messages": {
"custom_greetings": ConfigField(
type=list,
default=["你好!", "嗨!", "欢迎!"],
description="自定义问候语列表"
),
"error_message": ConfigField(
type=str,
default="问候功能暂时不可用",
description="错误时显示的消息"
)
}
}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""根据配置动态注册组件"""
components = []
# 根据配置决定是否注册组件
if self.get_config("plugin.enabled", True):
components.append((SmartGreetingAction.get_action_info(), SmartGreetingAction))
components.append((GreetingCommand.get_command_info(), GreetingCommand))
return components
```
### Action组件使用配置
```python
class SmartGreetingAction(BaseAction):
"""智能问候Action - 展示配置访问"""
focus_activation_type = ActionActivationType.KEYWORD
normal_activation_type = ActionActivationType.KEYWORD
activation_keywords = ["你好", "hello", "hi"]
async def execute(self) -> Tuple[bool, str]:
"""执行智能问候,大量使用配置"""
try:
# 检查插件是否启用
if not self.get_config("plugin.enabled", True):
return False, "插件已禁用"
# 获取问候配置
template = self.get_config("greeting.template", "你好,{username}")
enable_emoji = self.get_config("greeting.enable_emoji", True)
enable_llm = self.get_config("greeting.enable_llm", False)
# 获取功能配置
smart_detection = self.get_config("features.smart_detection", True)
random_greeting = self.get_config("features.random_greeting", False)
max_per_hour = self.get_config("features.max_greetings_per_hour", 5)
# 获取消息配置
custom_greetings = self.get_config("messages.custom_greetings", [])
error_message = self.get_config("messages.error_message", "问候功能不可用")
# 根据配置执行不同逻辑
username = self.action_data.get("username", "用户")
if random_greeting and custom_greetings:
# 使用随机自定义问候语
import random
greeting_msg = random.choice(custom_greetings)
elif enable_llm:
# 使用LLM生成个性化问候
greeting_msg = await self._generate_llm_greeting(username)
else:
# 使用模板问候
greeting_msg = template.format(username=username)
# 发送问候消息
await self.send_text(greeting_msg)
# 根据配置发送表情
if enable_emoji:
await self.send_emoji("😊")
return True, f"向{username}发送了问候"
except Exception as e:
# 使用配置的错误消息
await self.send_text(self.get_config("messages.error_message", "出错了"))
return False, f"问候失败: {str(e)}"
async def _generate_llm_greeting(self, username: str) -> str:
"""根据配置使用LLM生成问候语"""
# 这里可以进一步使用配置来定制LLM行为
llm_style = self.get_config("greeting.llm_style", "friendly")
# ... LLM调用逻辑
return f"你好 {username}!很高兴见到你!"
```
### Command组件使用配置
```python
class GreetingCommand(BaseCommand):
"""问候命令 - 展示配置访问"""
command_pattern = r"^/greet(?:\s+(?P<username>\w+))?$"
command_help = "发送问候消息"
command_examples = ["/greet", "/greet Alice"]
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行问候命令"""
# 检查功能是否启用
if not self.get_config("plugin.enabled", True):
await self.send_text("问候功能已禁用")
return False, "功能禁用"
# 获取用户名
username = self.matched_groups.get("username", "用户")
# 根据配置选择问候方式
if self.get_config("features.random_greeting", False):
custom_greetings = self.get_config("messages.custom_greetings", ["你好!"])
import random
greeting = random.choice(custom_greetings)
else:
template = self.get_config("greeting.template", "你好,{username}")
greeting = template.format(username=username)
# 发送问候
await self.send_text(greeting)
# 根据配置发送表情
if self.get_config("greeting.enable_emoji", True):
await self.send_text("😊")
return True, "问候发送成功"
```
---
## 最佳实践与注意事项
### 配置定义最佳实践
> **🚨 核心原则:永远不要手动创建 config.toml 文件!**
1. **🔥 绝不手动创建配置文件**: **任何时候都不要手动创建 `config.toml` 文件**!必须通过在 `plugin.py` 中定义 `config_schema` 让系统自动生成。
-**禁止**`touch config.toml`、手动编写配置文件
-**正确**:定义 `config_schema`,启动插件,让系统自动生成
2. **Schema优先**: 所有配置项都必须在 `config_schema` 中声明,包括类型、默认值和描述。
3. **描述清晰**: 为每个 `ConfigField``config_section_descriptions` 编写清晰、准确的描述。这会直接成为你的插件文档的一部分。
4. **提供合理默认值**: 确保你的插件在默认配置下就能正常运行(或处于一个安全禁用的状态)。
5. **gitignore**: 将 `plugins/*/config.toml``src/plugins/built_in/*/config.toml` 加入 `.gitignore`,以避免提交个人敏感信息。
6. **配置文件只供修改**: 自动生成的 `config.toml` 文件只应该被用户**修改**,而不是从零创建。
### 配置访问最佳实践
#### 1. 总是提供默认值
```python
# ✅ 好的做法
timeout = self.get_config("api.timeout", 30)
# ❌ 避免这样做
timeout = self.get_config("api.timeout") # 可能返回None
```
#### 2. 验证配置类型
```python
# 获取配置后验证类型
max_items = self.get_config("list.max_items", 10)
if not isinstance(max_items, int) or max_items <= 0:
max_items = 10 # 使用安全的默认值
```
#### 3. 缓存复杂配置解析
```python
class MyAction(BaseAction):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 在初始化时解析复杂配置,避免重复解析
self._api_config = self._parse_api_config()
def _parse_api_config(self):
return {
'key': self.get_config("api.key", ""),
'timeout': self.get_config("api.timeout", 30),
'retries': self.get_config("api.max_retries", 3)
}
```
#### 4. 配置驱动的组件注册
```python
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""根据配置动态注册组件"""
components = []
# 从配置获取组件启用状态
enable_action = self.get_config("components.enable_action", True)
enable_command = self.get_config("components.enable_command", True)
if enable_action:
components.append((MyAction.get_action_info(), MyAction))
if enable_command:
components.append((MyCommand.get_command_info(), MyCommand))
return components
```
### 🎉 总结
现在你掌握了插件配置的完整流程:
1. **定义配置**: 在插件中使用 `config_schema` 定义配置结构
2. **访问配置**: 在组件中使用 `self.get_config("key", default_value)` 访问配置
3. **自动生成**: 系统自动生成带注释的配置文件
4. **动态行为**: 根据配置动态调整插件行为
> **🚨 最后强调:任何时候都不要手动创建 config.toml 文件!**
>
> 让系统根据你的 `config_schema` 自动生成配置文件,这是插件系统的核心设计原则。
不需要继承`BasePlugin`,不需要复杂的配置传递,不需要手动创建配置文件,组件内置的`get_config`方法和自动化的配置生成机制已经为你准备好了一切!

View File

@@ -1,412 +0,0 @@
# 📋 开发标准规范
## 🎯 概述
本文档定义了MaiBot插件开发的标准规范包括Action组件、Command组件和Tool组件的开发规范确保代码质量、可维护性和性能。
## 🧩 组件开发规范
### Tool组件开发
**工具基本要求**
- 继承 `BaseTool` 基类
- 定义唯一的工具名称
- 提供清晰的功能描述
- 使用JSONSchema定义参数
- 实现 `execute` 异步方法
- 使用 `register_tool()` 注册
**工具开发模板**
```python
from src.tools.tool_can_use.base_tool import BaseTool, register_tool
class MyTool(BaseTool):
"""工具类文档字符串"""
name = "my_tool"
description = "详细的工具功能描述告诉LLM这个工具的用途"
parameters = {
"type": "object",
"properties": {
"param": {
"type": "string",
"description": "参数详细描述"
}
},
"required": ["param"]
}
async def execute(self, function_args, message_txt=""):
"""执行工具逻辑
Args:
function_args: 工具调用参数
message_txt: 原始消息文本
Returns:
dict: 包含name和content字段的结果
"""
# 实现工具功能逻辑
result = "处理结果"
return {
"name": self.name,
"content": result
}
# 注册工具
register_tool(MyTool)
```
**工具命名规范**
- 使用描述性的英文名称
- 采用下划线命名法snake_case
- 体现工具的核心功能
- 避免过于简短或复杂的名称
**示例**
```python
# ✅ 好的命名
name = "weather_query" # 天气查询
name = "knowledge_search" # 知识搜索
name = "stock_price_check" # 股价检查
# ❌ 避免的命名
name = "tool1" # 无意义
name = "wq" # 过于简短
name = "weather_and_news" # 功能复杂
```
### Action组件开发
**Action必需字段检查表**
**激活控制字段**
-`activation_type`激活类型KEYWORD/LLM_JUDGE/RANDOM/ALWAYS/NEVER
-`activation_config`:激活配置参数
**基本信息字段**
-`name`Action唯一标识名称
-`description`:功能描述
-`usage_tip`:使用提示
**功能定义字段**
-`func`:执行函数
-`llm_function_tips`LLM调用提示
**Action开发模板**
```python
from src.plugin_system.base_actions import BaseAction
class MyAction(BaseAction):
"""Action类文档字符串"""
# 激活控制
activation_type = "KEYWORD" # 或 LLM_JUDGE/RANDOM/ALWAYS/NEVER
activation_config = {
"keywords": ["关键词1", "关键词2"],
"priority": 1
}
# 基本信息
name = "my_action"
description = "Action功能描述"
usage_tip = "使用场景和方法提示"
# 功能定义
func = "执行函数名"
llm_function_tips = "告诉LLM何时以及如何使用这个Action"
async def 执行函数名(self, message_txt, sender_name, chat_stream):
"""Action执行逻辑"""
# 实现Action功能
await chat_stream.send_message("执行结果")
```
**激活类型使用规范**
- `KEYWORD`:适用于有明确关键词的功能,性能最优
- `LLM_JUDGE`:适用于需要智能判断的复杂场景
- `RANDOM`:适用于随机触发的功能
- `ALWAYS`:适用于总是可用的基础功能
- `NEVER`:适用于临时禁用的功能
### Command组件开发
**Command开发模板**
```python
from src.plugin_system.base_commands import BaseCommand
class MyCommand(BaseCommand):
"""Command类文档字符串"""
# 命令基本信息
command_name = "my_command"
description = "命令功能描述"
usage = "/my_command <参数> - 命令使用说明"
# 匹配模式
pattern = r"^/my_command\s+(.*)"
async def execute(self, match, message_txt, sender_name, chat_stream):
"""Command执行逻辑"""
params = match.group(1) if match.group(1) else ""
# 实现命令功能
await chat_stream.send_message(f"命令执行结果: {params}")
```
## 📝 代码结构标准
### 文件组织结构
```
plugins/my_plugin/
├── __init__.py # 插件入口
├── plugin.py # 插件主文件
├── config.toml # 插件配置
├── actions/ # Action组件目录
│ ├── __init__.py
│ └── my_action.py
├── commands/ # Command组件目录
│ ├── __init__.py
│ └── my_command.py
├── utils/ # 工具函数目录
│ ├── __init__.py
│ └── helpers.py
└── README.md # 插件说明文档
```
### 插件主文件模板
```python
"""
插件名称My Plugin
插件描述:插件功能描述
作者:作者名称
版本1.0.0
"""
from src.plugin_system.plugin_interface import PluginInterface
from .actions.my_action import MyAction
from .commands.my_command import MyCommand
class MyPlugin(PluginInterface):
"""插件主类"""
def get_action_info(self):
"""获取Action信息"""
return [MyAction()]
def get_command_info(self):
"""获取Command信息"""
return [MyCommand()]
# 插件实例
plugin_instance = MyPlugin()
```
## 🔧 命名规范
### 类命名
- **Action类**:使用 `Action` 后缀,如 `GreetingAction`
- **Command类**:使用 `Command` 后缀,如 `HelpCommand`
- **Tool类**:使用 `Tool` 后缀,如 `WeatherTool`
- **插件类**:使用 `Plugin` 后缀,如 `ExamplePlugin`
### 变量命名
- 使用小写字母和下划线snake_case
- 布尔变量使用 `is_``has_``can_` 前缀
- 常量使用全大写字母
### 函数命名
- 使用小写字母和下划线snake_case
- 异步函数不需要特殊前缀
- 私有方法使用单下划线前缀
## 📊 性能优化规范
### Action激活类型选择
1. **首选KEYWORD**:明确知道触发关键词时
2. **谨慎使用LLM_JUDGE**:仅在必须智能判断时使用
3. **合理设置优先级**避免过多高优先级Action
### 异步编程规范
- 所有I/O操作必须使用异步
- 避免在异步函数中使用阻塞操作
- 合理使用 `asyncio.gather()` 并发执行
### 资源管理
- 及时关闭文件、网络连接等资源
- 使用上下文管理器(`async with`
- 避免内存泄漏
## 🚨 错误处理规范
### 异常处理模板
```python
async def my_function(self, message_txt, sender_name, chat_stream):
"""函数文档字符串"""
try:
# 核心逻辑
result = await some_operation()
# 成功处理
await chat_stream.send_message(f"操作成功: {result}")
except ValueError as e:
# 具体异常处理
await chat_stream.send_message(f"参数错误: {str(e)}")
except Exception as e:
# 通用异常处理
await chat_stream.send_message(f"操作失败: {str(e)}")
# 记录错误日志
logger.error(f"Function my_function failed: {str(e)}")
```
### 错误信息规范
- 使用用户友好的错误提示
- 避免暴露系统内部信息
- 提供解决建议或替代方案
- 记录详细的错误日志
## 🧪 测试标准
### 单元测试模板
```python
import unittest
import asyncio
from unittest.mock import Mock, AsyncMock
from plugins.my_plugin.actions.my_action import MyAction
class TestMyAction(unittest.TestCase):
"""MyAction测试类"""
def setUp(self):
"""测试前准备"""
self.action = MyAction()
self.mock_chat_stream = AsyncMock()
def test_action_properties(self):
"""测试Action属性"""
self.assertEqual(self.action.name, "my_action")
self.assertIsNotNone(self.action.description)
self.assertIsNotNone(self.action.activation_type)
async def test_action_execution(self):
"""测试Action执行"""
await self.action.执行函数名("测试消息", "测试用户", self.mock_chat_stream)
# 验证消息发送
self.mock_chat_stream.send_message.assert_called()
def test_action_execution_sync(self):
"""同步测试包装器"""
asyncio.run(self.test_action_execution())
if __name__ == '__main__':
unittest.main()
```
### 测试覆盖率要求
- 核心功能必须有测试覆盖
- 异常处理路径需要测试
- 边界条件需要验证
## 📚 文档规范
### 代码文档
- 所有类和函数必须有文档字符串
- 使用Google风格的docstring
- 包含参数说明和返回值说明
### README文档模板
```markdown
# 插件名称
## 📖 插件描述
简要描述插件的功能和用途
## ✨ 功能特性
- 功能1功能描述
- 功能2功能描述
## 🚀 快速开始
### 安装配置
1. 步骤1
2. 步骤2
### 使用方法
具体的使用说明和示例
## 📝 配置说明
配置文件的详细说明
## 🔧 开发信息
- 作者:作者名称
- 版本:版本号
- 许可证:许可证类型
```
## 🔍 代码审查清单
### 基础检查
- [ ] 代码符合命名规范
- [ ] 类和函数有完整文档字符串
- [ ] 异常处理覆盖完整
- [ ] 没有硬编码的配置信息
### Action组件检查
- [ ] 包含所有必需字段
- [ ] 激活类型选择合理
- [ ] LLM函数提示清晰
- [ ] 执行函数实现正确
### Command组件检查
- [ ] 正则表达式模式正确
- [ ] 参数提取和验证完整
- [ ] 使用说明准确
### Tool组件检查
- [ ] 继承BaseTool基类
- [ ] 参数定义遵循JSONSchema
- [ ] 返回值格式正确
- [ ] 工具已正确注册
### 性能检查
- [ ] 避免不必要的LLM_JUDGE激活
- [ ] 异步操作使用正确
- [ ] 资源管理合理
### 安全检查
- [ ] 输入参数验证
- [ ] SQL注入防护
- [ ] 敏感信息保护
## 🎯 最佳实践总结
### 设计原则
1. **单一职责**:每个组件专注单一功能
2. **松耦合**:减少组件间依赖
3. **高内聚**:相关功能聚合在一起
4. **可扩展**:易于添加新功能
### 性能优化
1. **合理选择激活类型**优先使用KEYWORD
2. **避免阻塞操作**:使用异步编程
3. **缓存重复计算**:提高响应速度
4. **资源池化**:复用连接和对象
### 用户体验
1. **友好的错误提示**:帮助用户理解问题
2. **清晰的使用说明**:降低学习成本
3. **一致的交互方式**:统一的命令格式
4. **及时的反馈**:让用户知道操作状态
---
🎉 **遵循这些标准可以确保插件的质量、性能和用户体验!**

View File

@@ -1,414 +0,0 @@
# 📚 完整示例
## 📖 概述
这里收集了各种类型的完整插件示例展示了MaiBot插件系统的最佳实践和高级用法。每个示例都包含完整的代码、配置和说明。
## 🎯 示例列表
### 🌟 基础示例
- [Hello World插件](#hello-world插件) - 快速入门示例
- [简单计算器](#简单计算器) - Command基础用法
- [智能问答](#智能问答) - Action基础用法
### 🔧 实用示例
- [用户管理系统](#用户管理系统) - 数据库操作示例
- [定时提醒插件](#定时提醒插件) - 定时任务示例
- [天气查询插件](#天气查询插件) - 外部API调用示例
### 🛠️ 工具系统示例
- [天气查询工具](#天气查询工具) - Focus模式信息获取工具
- [知识搜索工具](#知识搜索工具) - 百科知识查询工具
### 🚀 高级示例
- [多功能聊天助手](#多功能聊天助手) - 综合功能插件
- [游戏管理插件](#游戏管理插件) - 复杂状态管理
- [数据分析插件](#数据分析插件) - 数据处理和可视化
---
## Hello World插件
最基础的入门插件展示Action和Command的基本用法。
### 功能说明
- **HelloAction**: 响应问候语,展示关键词激活
- **TimeCommand**: 查询当前时间,展示命令处理
### 完整代码
`plugins/hello_world_plugin/plugin.py`:
```python
from typing import List, Tuple, Type
from src.plugin_system import (
BasePlugin, register_plugin, BaseAction, BaseCommand,
ComponentInfo, ActionActivationType, ChatMode
)
class HelloAction(BaseAction):
"""问候Action"""
# ===== 激活控制必须项 =====
focus_activation_type = ActionActivationType.KEYWORD
normal_activation_type = ActionActivationType.KEYWORD
mode_enable = ChatMode.ALL
parallel_action = False
# ===== 基本信息必须项 =====
action_name = "hello_greeting"
action_description = "向用户发送友好的问候消息"
# 关键词配置
activation_keywords = ["你好", "hello", "hi"]
keyword_case_sensitive = False
# ===== 功能定义必须项 =====
action_parameters = {
"greeting_style": "问候风格casual(随意) 或 formal(正式)"
}
action_require = [
"用户发送问候语时使用",
"营造友好的聊天氛围"
]
associated_types = ["text", "emoji"]
async def execute(self) -> Tuple[bool, str]:
style = self.action_data.get("greeting_style", "casual")
if style == "formal":
message = "您好!很高兴为您服务!"
emoji = "🙏"
else:
message = "嗨!很开心见到你!"
emoji = "😊"
await self.send_text(message)
await self.send_type("emoji", emoji)
return True, f"发送了{style}风格的问候"
class TimeCommand(BaseCommand):
"""时间查询Command"""
command_pattern = r"^/time$"
command_help = "查询当前时间"
command_examples = ["/time"]
intercept_message = True
async def execute(self) -> Tuple[bool, str]:
import datetime
now = datetime.datetime.now()
time_str = now.strftime("%Y-%m-%d %H:%M:%S")
await self.send_text(f"⏰ 当前时间:{time_str}")
return True, f"显示了当前时间: {time_str}"
@register_plugin
class HelloWorldPlugin(BasePlugin):
"""Hello World插件"""
plugin_name = "hello_world_plugin"
plugin_description = "Hello World演示插件"
plugin_version = "1.0.0"
plugin_author = "MaiBot Team"
enable_plugin = True
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
return [
(HelloAction.get_action_info(), HelloAction),
(TimeCommand.get_command_info(
name="time_query",
description="查询当前系统时间"
), TimeCommand),
]
```
### 配置文件
`plugins/hello_world_plugin/config.toml`:
```toml
[plugin]
name = "hello_world_plugin"
version = "1.0.0"
enabled = true
[greeting]
default_style = "casual"
enable_emoji = true
[time]
timezone = "Asia/Shanghai"
format = "%Y-%m-%d %H:%M:%S"
```
---
## 天气查询工具
展示如何创建Focus模式下的信息获取工具专门用于扩展麦麦的信息获取能力。
### 功能说明
- **Focus模式专用**:仅在专注聊天模式下工作
- **自动调用**LLM根据用户查询自动判断是否使用
- **信息增强**:为麦麦提供实时天气数据
- **必须启用工具处理器**
### 完整代码
`src/tools/tool_can_use/weather_tool.py`:
```python
from src.tools.tool_can_use.base_tool import BaseTool, register_tool
import aiohttp
import json
class WeatherTool(BaseTool):
"""天气查询工具 - 获取指定城市的实时天气信息"""
# 工具名称,必须唯一
name = "weather_query"
# 工具描述告诉LLM这个工具的用途
description = "查询指定城市的实时天气信息,包括温度、湿度、天气状况等"
# 参数定义遵循JSONSchema格式
parameters = {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "要查询天气的城市名称,如:北京、上海、纽约"
},
"country": {
"type": "string",
"description": "国家代码CN、US可选参数"
}
},
"required": ["city"]
}
async def execute(self, function_args, message_txt=""):
"""执行天气查询"""
try:
city = function_args.get("city")
country = function_args.get("country", "")
# 构建查询参数
location = f"{city},{country}" if country else city
# 调用天气API
weather_data = await self._fetch_weather(location)
# 格式化结果
result = self._format_weather_data(weather_data)
return {
"name": self.name,
"content": result
}
except Exception as e:
return {
"name": self.name,
"content": f"天气查询失败: {str(e)}"
}
async def _fetch_weather(self, location: str) -> dict:
"""获取天气数据"""
# 这里是示例实际需要接入真实的天气API
# 例如OpenWeatherMap、和风天气等
api_url = f"http://api.weather.com/v1/current?q={location}"
async with aiohttp.ClientSession() as session:
async with session.get(api_url) as response:
return await response.json()
def _format_weather_data(self, data: dict) -> str:
"""格式化天气数据"""
if not data:
return "暂无天气数据"
# 提取关键信息
city = data.get("location", {}).get("name", "未知城市")
temp = data.get("current", {}).get("temp_c", "未知")
condition = data.get("current", {}).get("condition", {}).get("text", "未知")
humidity = data.get("current", {}).get("humidity", "未知")
# 格式化输出
return f"""
🌤️ {city} 实时天气
━━━━━━━━━━━━━━━━━━
🌡️ 温度: {temp}°C
☁️ 天气: {condition}
💧 湿度: {humidity}%
━━━━━━━━━━━━━━━━━━
""".strip()
# 注册工具(重要!必须调用)
register_tool(WeatherTool)
```
### 使用说明
1. **部署位置**:将文件放在 `src/tools/tool_can_use/` 目录下
2. **模式要求**仅在Focus模式下可用
3. **配置要求**:必须开启工具处理器 `enable_tool_processor = True`
4. **自动调用**:用户发送"今天北京天气怎么样?"时,麦麦会自动调用此工具
---
## 知识搜索工具
展示如何创建知识查询工具,为麦麦提供百科知识和专业信息。
### 功能说明
- **知识增强**:扩展麦麦的知识获取能力
- **分类搜索**:支持科学、历史、技术等分类
- **多语言支持**:支持中英文结果
- **智能调用**LLM自动判断何时需要知识查询
### 完整代码
`src/tools/tool_can_use/knowledge_search_tool.py`:
```python
from src.tools.tool_can_use.base_tool import BaseTool, register_tool
import aiohttp
import json
class KnowledgeSearchTool(BaseTool):
"""知识搜索工具 - 查询百科知识和专业信息"""
name = "knowledge_search"
description = "搜索百科知识、专业术语解释、历史事件等信息"
parameters = {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "要搜索的知识关键词或问题"
},
"category": {
"type": "string",
"description": "知识分类science(科学)、history(历史)、technology(技术)、general(通用)等",
"enum": ["science", "history", "technology", "general"]
},
"language": {
"type": "string",
"description": "结果语言zh(中文)、en(英文)",
"enum": ["zh", "en"]
}
},
"required": ["query"]
}
async def execute(self, function_args, message_txt=""):
"""执行知识搜索"""
try:
query = function_args.get("query")
category = function_args.get("category", "general")
language = function_args.get("language", "zh")
# 执行搜索逻辑
search_results = await self._search_knowledge(query, category, language)
# 格式化结果
result = self._format_search_results(query, search_results)
return {
"name": self.name,
"content": result
}
except Exception as e:
return {
"name": self.name,
"content": f"知识搜索失败: {str(e)}"
}
async def _search_knowledge(self, query: str, category: str, language: str) -> list:
"""执行知识搜索"""
# 这里实现实际的搜索逻辑
# 可以对接维基百科API、百度百科API等
# 示例API调用
if language == "zh":
api_url = f"https://zh.wikipedia.org/api/rest_v1/page/summary/{query}"
else:
api_url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{query}"
async with aiohttp.ClientSession() as session:
async with session.get(api_url) as response:
if response.status == 200:
data = await response.json()
return [
{
"title": data.get("title", "无标题"),
"summary": data.get("extract", "无摘要"),
"source": "Wikipedia"
}
]
else:
return []
def _format_search_results(self, query: str, results: list) -> str:
"""格式化搜索结果"""
if not results:
return f"未找到关于 '{query}' 的相关信息"
formatted_text = f"📚 关于 '{query}' 的搜索结果:\n\n"
for i, result in enumerate(results[:3], 1): # 限制显示前3条
title = result.get("title", "无标题")
summary = result.get("summary", "无摘要")
source = result.get("source", "未知来源")
formatted_text += f"{i}. **{title}**\n"
formatted_text += f" {summary}\n"
formatted_text += f" 📖 来源: {source}\n\n"
return formatted_text.strip()
# 注册工具
register_tool(KnowledgeSearchTool)
```
### 配置示例
Focus模式配置文件示例
```python
# 在Focus模式配置中
focus_config = {
"enable_tool_processor": True, # 必须启用工具处理器
"tool_timeout": 30, # 工具执行超时时间(秒)
"max_tools_per_message": 3 # 单次消息最大工具调用数
}
```
### 使用流程
1. **用户查询**用户在Focus模式下发送"什么是量子计算?"
2. **LLM判断**:麦麦识别这是知识查询需求
3. **工具调用**:自动调用 `knowledge_search` 工具
4. **信息获取**:工具查询相关知识信息
5. **整合回复**:麦麦将获取的信息整合到回复中
### 工具系统特点
- **🎯 专用性**仅在Focus模式下工作专注信息获取
- **🔍 智能性**LLM自动判断何时需要使用工具
- **📊 丰富性**:为麦麦提供外部数据和实时信息
- **⚡ 高效性**:系统自动发现和注册工具
- **🔧 独立性**:目前需要单独编写,未来将更好融入插件系统
---
🎉 **这些示例展示了MaiBot插件系统的强大功能根据你的需求选择合适的示例作为起点。**

View File

@@ -1,520 +0,0 @@
# 📖 插件配置访问完整示例
> 这个示例展示了如何在Action和Command组件中正确访问插件的配置文件。
## 🎯 问题背景
在插件开发过程中,你可能遇到这样的问题:
- `get_config`方法只在`BasePlugin`类中
- `BaseAction``BaseCommand`无法直接继承这个方法
- 想要在Action或Command中访问插件配置
## ✅ 解决方案
通过`self.api.get_config()`方法访问配置,系统会自动将插件配置传递给组件。
## 📁 完整示例
### 1. 插件配置文件
创建 `config.toml`
```toml
[greeting]
default_style = "casual"
enable_emoji = true
custom_messages = [
"你好呀!",
"嗨!很高兴见到你!",
"哈喽!"
]
[database]
enabled = true
table_prefix = "hello_"
max_records = 1000
[features]
enable_weather = false
enable_jokes = true
api_timeout = 30
[advanced.logging]
level = "INFO"
file_path = "logs/hello_plugin.log"
[advanced.cache]
enabled = true
ttl_seconds = 3600
max_size = 100
```
### 2. 插件主文件
创建 `plugin.py`
```python
"""
配置访问示例插件
展示如何在Action和Command中访问配置
"""
from src.plugin_system import (
BasePlugin,
BaseAction,
BaseCommand,
register_plugin,
ActionInfo,
CommandInfo,
PythonDependency,
ActionActivationType
)
from src.common.logger import get_logger
logger = get_logger("config_example_plugin")
@register_plugin
class ConfigExamplePlugin(BasePlugin):
"""配置访问示例插件"""
plugin_name = "config_example_plugin"
plugin_description = "展示如何在组件中访问配置的示例插件"
plugin_version = "1.0.0"
plugin_author = "MaiBot Team"
config_file_name = "config.toml"
def get_plugin_components(self):
"""返回插件组件"""
return [
(ActionInfo(
name="config_greeting_action",
description="使用配置的问候Action",
focus_activation_type=ActionActivationType.KEYWORD,
normal_activation_type=ActionActivationType.KEYWORD,
activation_keywords=["配置问候", "config hello"],
), ConfigGreetingAction),
(CommandInfo(
name="config_status",
description="显示配置状态",
command_pattern=r"^/config\s*(status|show)?$",
command_help="显示插件配置状态",
command_examples=["/config", "/config status"],
), ConfigStatusCommand),
(CommandInfo(
name="config_test",
description="测试配置访问",
command_pattern=r"^/config\s+test\s+(?P<key>\S+)$",
command_help="测试访问指定配置项",
command_examples=["/config test greeting.default_style"],
), ConfigTestCommand),
]
class ConfigGreetingAction(BaseAction):
"""使用配置的问候Action"""
async def execute(self):
"""执行配置化的问候"""
try:
# 方法1: 直接访问配置项
style = self.api.get_config("greeting.default_style", "casual")
enable_emoji = self.api.get_config("greeting.enable_emoji", True)
# 方法2: 检查配置是否存在
if self.api.has_config("greeting.custom_messages"):
messages = self.api.get_config("greeting.custom_messages", [])
if messages:
# 随机选择一个问候语
import random
message = random.choice(messages)
else:
message = "你好!"
else:
# 使用默认问候语
if style == "formal":
message = "您好!很高兴为您服务!"
else:
message = "嗨!很开心见到你!"
# 添加表情符号
if enable_emoji:
emoji = "😊" if style == "casual" else "🙏"
message += emoji
# 发送问候消息
await self.send_text(message)
# 记录到数据库(如果启用)
await self._save_greeting_record(style, message)
return True, f"发送了{style}风格的配置化问候"
except Exception as e:
logger.error(f"配置问候执行失败: {e}")
await self.send_text("抱歉,问候功能遇到了问题")
return False, f"执行失败: {str(e)}"
async def _save_greeting_record(self, style: str, message: str):
"""保存问候记录到数据库"""
try:
# 检查数据库功能是否启用
if not self.api.get_config("database.enabled", False):
return
# 获取数据库配置
table_prefix = self.api.get_config("database.table_prefix", "hello_")
max_records = self.api.get_config("database.max_records", 1000)
# 构造记录数据
record_data = {
"style": style,
"message": message,
"timestamp": "now", # 实际应用中使用datetime
"user_id": "demo_user" # 从context获取真实用户ID
}
# 这里应该调用数据库API保存记录
logger.info(f"保存问候记录到 {table_prefix}greetings: {record_data}")
except Exception as e:
logger.error(f"保存问候记录失败: {e}")
class ConfigStatusCommand(BaseCommand):
"""显示配置状态Command"""
async def execute(self):
"""显示插件配置状态"""
try:
# 获取所有配置
all_config = self.api.get_all_config()
if not all_config:
await self.send_text("❌ 没有找到配置文件")
return True, "没有配置文件"
# 构建状态报告
status_lines = ["📋 插件配置状态:", ""]
# 问候配置
greeting_config = all_config.get("greeting", {})
if greeting_config:
status_lines.append("🎯 问候配置:")
status_lines.append(f" - 默认风格: {greeting_config.get('default_style', 'N/A')}")
status_lines.append(f" - 启用表情: {'✅' if greeting_config.get('enable_emoji') else '❌'}")
custom_msgs = greeting_config.get('custom_messages', [])
status_lines.append(f" - 自定义消息: {len(custom_msgs)}条")
status_lines.append("")
# 数据库配置
db_config = all_config.get("database", {})
if db_config:
status_lines.append("🗄️ 数据库配置:")
status_lines.append(f" - 状态: {'✅ 启用' if db_config.get('enabled') else '❌ 禁用'}")
status_lines.append(f" - 表前缀: {db_config.get('table_prefix', 'N/A')}")
status_lines.append(f" - 最大记录: {db_config.get('max_records', 'N/A')}")
status_lines.append("")
# 功能配置
features_config = all_config.get("features", {})
if features_config:
status_lines.append("🔧 功能配置:")
for feature, enabled in features_config.items():
if isinstance(enabled, bool):
status_lines.append(f" - {feature}: {'✅' if enabled else '❌'}")
else:
status_lines.append(f" - {feature}: {enabled}")
status_lines.append("")
# 高级配置
advanced_config = all_config.get("advanced", {})
if advanced_config:
status_lines.append("⚙️ 高级配置:")
for section, config in advanced_config.items():
status_lines.append(f" - {section}: {len(config) if isinstance(config, dict) else 1}项")
# 发送状态报告
status_text = "\n".join(status_lines)
await self.send_text(status_text)
return True, "显示了配置状态"
except Exception as e:
logger.error(f"获取配置状态失败: {e}")
await self.send_text(f"❌ 获取配置状态失败: {str(e)}")
return False, f"获取失败: {str(e)}"
class ConfigTestCommand(BaseCommand):
"""测试配置访问Command"""
async def execute(self):
"""测试访问指定的配置项"""
try:
# 获取要测试的配置键
config_key = self.matched_groups.get("key", "")
if not config_key:
await self.send_text("❌ 请指定要测试的配置项\n用法: /config test <key>")
return True, "缺少配置键参数"
# 测试配置访问的不同方法
result_lines = [f"🔍 测试配置项: `{config_key}`", ""]
# 方法1: 检查是否存在
exists = self.api.has_config(config_key)
result_lines.append(f"📋 配置存在: {'✅ 是' if exists else '❌ 否'}")
if exists:
# 方法2: 获取配置值
config_value = self.api.get_config(config_key)
value_type = type(config_value).__name__
result_lines.append(f"📊 数据类型: {value_type}")
# 根据类型显示值
if isinstance(config_value, (str, int, float, bool)):
result_lines.append(f"💾 配置值: {config_value}")
elif isinstance(config_value, list):
result_lines.append(f"📝 列表长度: {len(config_value)}")
if config_value:
result_lines.append(f"📋 首项: {config_value[0]}")
elif isinstance(config_value, dict):
result_lines.append(f"🗂️ 字典大小: {len(config_value)}项")
if config_value:
keys = list(config_value.keys())[:3]
result_lines.append(f"🔑 键示例: {', '.join(keys)}")
else:
result_lines.append(f"💾 配置值: {str(config_value)[:100]}...")
# 方法3: 测试默认值
test_default = self.api.get_config(config_key, "DEFAULT_VALUE")
if test_default != "DEFAULT_VALUE":
result_lines.append("✅ 默认值机制正常")
else:
result_lines.append("⚠️ 配置值为空或等于测试默认值")
else:
# 测试默认值返回
default_value = self.api.get_config(config_key, "NOT_FOUND")
result_lines.append(f"🔄 默认值返回: {default_value}")
# 显示相关配置项
if "." in config_key:
section = config_key.split(".")[0]
all_config = self.api.get_all_config()
section_config = all_config.get(section, {})
if section_config and isinstance(section_config, dict):
related_keys = list(section_config.keys())[:5]
result_lines.append("")
result_lines.append(f"🔗 相关配置项 ({section}):")
for key in related_keys:
full_key = f"{section}.{key}"
status = "✅" if self.api.has_config(full_key) else "❌"
result_lines.append(f" {status} {full_key}")
# 发送测试结果
result_text = "\n".join(result_lines)
await self.send_text(result_text)
return True, f"测试了配置项: {config_key}"
except Exception as e:
logger.error(f"配置测试失败: {e}")
await self.send_text(f"❌ 配置测试失败: {str(e)}")
return False, f"测试失败: {str(e)}"
# 演示代码
async def demo_config_access():
"""演示配置访问功能"""
print("🔧 插件配置访问演示")
print("=" * 50)
# 模拟插件配置
mock_config = {
"greeting": {
"default_style": "casual",
"enable_emoji": True,
"custom_messages": ["你好呀!", "嗨!很高兴见到你!"]
},
"database": {
"enabled": True,
"table_prefix": "hello_",
"max_records": 1000
},
"advanced": {
"logging": {
"level": "INFO",
"file_path": "logs/hello_plugin.log"
}
}
}
# 创建模拟API
from src.plugin_system.apis.plugin_api import PluginAPI
api = PluginAPI(plugin_config=mock_config)
print("\n📋 配置访问测试:")
# 测试1: 基本配置访问
style = api.get_config("greeting.default_style", "unknown")
print(f" 问候风格: {style}")
# 测试2: 布尔值配置
enable_emoji = api.get_config("greeting.enable_emoji", False)
print(f" 启用表情: {enable_emoji}")
# 测试3: 列表配置
messages = api.get_config("greeting.custom_messages", [])
print(f" 自定义消息: {len(messages)}条")
# 测试4: 深层嵌套配置
log_level = api.get_config("advanced.logging.level", "INFO")
print(f" 日志级别: {log_level}")
# 测试5: 不存在的配置
unknown = api.get_config("unknown.config", "default")
print(f" 未知配置: {unknown}")
# 测试6: 配置存在检查
exists1 = api.has_config("greeting.default_style")
exists2 = api.has_config("nonexistent.config")
print(f" greeting.default_style 存在: {exists1}")
print(f" nonexistent.config 存在: {exists2}")
# 测试7: 获取所有配置
all_config = api.get_all_config()
print(f" 总配置节数: {len(all_config)}")
print("\n✅ 配置访问测试完成!")
if __name__ == "__main__":
import asyncio
asyncio.run(demo_config_access())
```
## 🎯 核心要点
### 1. 在Action中访问配置
```python
class MyAction(BaseAction):
async def execute(self):
# 基本配置访问
value = self.api.get_config("section.key", "default")
# 检查配置是否存在
if self.api.has_config("section.key"):
# 配置存在,执行相应逻辑
pass
# 获取所有配置
all_config = self.api.get_all_config()
```
### 2. 在Command中访问配置
```python
class MyCommand(BaseCommand):
async def execute(self):
# 访问配置的方法与Action完全相同
value = self.api.get_config("section.key", "default")
# 支持嵌套键访问
nested_value = self.api.get_config("section.subsection.key")
```
### 3. 配置传递机制
系统会自动处理配置传递:
1. `BasePlugin`加载配置文件到`self.config`
2. 组件注册时,系统通过`component_registry.get_plugin_config()`获取配置
3. Action/Command实例化时配置作为`plugin_config`参数传递
4. `PluginAPI`初始化时保存配置到`self._plugin_config`
5. 组件通过`self.api.get_config()`访问配置
## 🔧 使用这个示例
### 1. 创建插件目录
```bash
mkdir plugins/config_example_plugin
cd plugins/config_example_plugin
```
### 2. 复制文件
- 将配置文件保存为 `config.toml`
- 将插件代码保存为 `plugin.py`
### 3. 测试功能
```bash
# 启动MaiBot后测试以下命令
# 测试配置状态显示
/config status
# 测试特定配置项
/config test greeting.default_style
/config test database.enabled
/config test advanced.logging.level
# 触发配置化问候
配置问候
```
## 💡 最佳实践
### 1. 提供合理的默认值
```python
# 总是提供默认值
timeout = self.api.get_config("api.timeout", 30)
enabled = self.api.get_config("feature.enabled", False)
```
### 2. 验证配置类型
```python
# 验证配置类型
max_items = self.api.get_config("list.max_items", 10)
if not isinstance(max_items, int) or max_items <= 0:
max_items = 10 # 使用安全的默认值
```
### 3. 缓存复杂配置
```python
class MyAction(BaseAction):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 缓存复杂配置避免重复解析
self._cached_config = self._parse_complex_config()
def _parse_complex_config(self):
# 解析复杂配置逻辑
return processed_config
```
### 4. 配置变更检测
```python
# 对于支持热更新的配置
last_config_hash = None
def check_config_changes(self):
current_config = self.api.get_all_config()
current_hash = hash(str(current_config))
if current_hash != self.last_config_hash:
self.last_config_hash = current_hash
self._reload_config()
```
通过这种方式你的Action和Command组件可以灵活地访问插件配置实现更加强大和可定制的功能

View File

@@ -1,477 +0,0 @@
# 📦 依赖管理完整示例
> 这个示例展示了如何在插件中正确使用Python依赖管理功能。
## 🎯 示例插件:智能数据分析插件
这个插件展示了如何处理必需依赖、可选依赖,以及优雅降级处理。
```python
"""
智能数据分析插件
展示依赖管理的完整用法
"""
from src.plugin_system import (
BasePlugin,
BaseAction,
register_plugin,
ActionInfo,
PythonDependency,
ActionActivationType
)
from src.common.logger import get_logger
logger = get_logger("data_analysis_plugin")
@register_plugin
class DataAnalysisPlugin(BasePlugin):
"""智能数据分析插件"""
plugin_name = "data_analysis_plugin"
plugin_description = "提供数据分析和可视化功能的示例插件"
plugin_version = "1.0.0"
plugin_author = "MaiBot Team"
# 声明Python包依赖
python_dependencies = [
# 必需依赖 - 核心功能
PythonDependency(
package_name="requests",
version=">=2.25.0",
description="HTTP库用于获取外部数据"
),
# 可选依赖 - 数据处理
PythonDependency(
package_name="pandas",
version=">=1.3.0",
optional=True,
description="数据处理库,提供高级数据操作功能"
),
# 可选依赖 - 数值计算
PythonDependency(
package_name="numpy",
version=">=1.20.0",
optional=True,
description="数值计算库,用于数学运算"
),
# 可选依赖 - 数据可视化
PythonDependency(
package_name="matplotlib",
version=">=3.3.0",
optional=True,
description="绘图库,用于生成数据图表"
),
# 特殊情况:导入名与安装名不同
PythonDependency(
package_name="PIL",
install_name="Pillow",
version=">=8.0.0",
optional=True,
description="图像处理库,用于图表保存和处理"
),
]
def get_plugin_components(self):
"""返回插件组件"""
return [
# 基础数据获取只依赖requests
(ActionInfo(
name="fetch_data_action",
description="获取外部数据",
focus_activation_type=ActionActivationType.KEYWORD,
normal_activation_type=ActionActivationType.KEYWORD,
activation_keywords=["获取数据", "下载数据"],
), FetchDataAction),
# 数据分析依赖pandas和numpy
(ActionInfo(
name="analyze_data_action",
description="数据分析和统计",
focus_activation_type=ActionActivationType.KEYWORD,
normal_activation_type=ActionActivationType.KEYWORD,
activation_keywords=["分析数据", "数据统计"],
), AnalyzeDataAction),
# 数据可视化依赖matplotlib
(ActionInfo(
name="visualize_data_action",
description="数据可视化",
focus_activation_type=ActionActivationType.KEYWORD,
normal_activation_type=ActionActivationType.KEYWORD,
activation_keywords=["数据图表", "可视化"],
), VisualizeDataAction),
]
class FetchDataAction(BaseAction):
"""数据获取Action - 仅依赖必需的requests库"""
async def execute(self, action_input, context=None):
"""获取外部数据"""
try:
import requests
# 模拟数据获取
url = action_input.get("url", "https://api.github.com/users/octocat")
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
return {
"status": "success",
"message": f"成功获取数据,响应大小: {len(str(data))} 字符",
"data": data,
"capabilities": ["basic_fetch"]
}
except ImportError:
return {
"status": "error",
"message": "缺少必需依赖requests库",
"hint": "请运行: pip install requests>=2.25.0",
"error_code": "MISSING_DEPENDENCY"
}
except Exception as e:
return {
"status": "error",
"message": f"数据获取失败: {str(e)}",
"error_code": "FETCH_ERROR"
}
class AnalyzeDataAction(BaseAction):
"""数据分析Action - 支持多级功能降级"""
async def execute(self, action_input, context=None):
"""分析数据,支持功能降级"""
# 检查可用的依赖
has_pandas = self._check_dependency("pandas")
has_numpy = self._check_dependency("numpy")
# 获取输入数据
data = action_input.get("data", [1, 2, 3, 4, 5])
if has_pandas and has_numpy:
return await self._advanced_analysis(data)
elif has_numpy:
return await self._numpy_analysis(data)
else:
return await self._basic_analysis(data)
def _check_dependency(self, package_name):
"""检查依赖是否可用"""
try:
__import__(package_name)
return True
except ImportError:
return False
async def _advanced_analysis(self, data):
"""高级分析使用pandas + numpy"""
import pandas as pd
import numpy as np
# 转换为DataFrame
df = pd.DataFrame({"values": data})
# 高级统计分析
stats = {
"count": len(df),
"mean": df["values"].mean(),
"median": df["values"].median(),
"std": df["values"].std(),
"min": df["values"].min(),
"max": df["values"].max(),
"quartiles": df["values"].quantile([0.25, 0.5, 0.75]).to_dict(),
"skewness": df["values"].skew(),
"kurtosis": df["values"].kurtosis()
}
return {
"status": "success",
"message": "高级数据分析完成",
"data": stats,
"method": "advanced",
"capabilities": ["pandas", "numpy", "advanced_stats"]
}
async def _numpy_analysis(self, data):
"""中级分析仅使用numpy"""
import numpy as np
arr = np.array(data)
stats = {
"count": len(arr),
"mean": np.mean(arr),
"median": np.median(arr),
"std": np.std(arr),
"min": np.min(arr),
"max": np.max(arr),
"sum": np.sum(arr)
}
return {
"status": "success",
"message": "数值计算分析完成",
"data": stats,
"method": "numpy",
"capabilities": ["numpy", "basic_stats"]
}
async def _basic_analysis(self, data):
"""基础分析纯Python"""
stats = {
"count": len(data),
"mean": sum(data) / len(data) if data else 0,
"min": min(data) if data else None,
"max": max(data) if data else None,
"sum": sum(data)
}
return {
"status": "success",
"message": "基础数据分析完成",
"data": stats,
"method": "basic",
"capabilities": ["pure_python"],
"note": "安装numpy和pandas可获得更多分析功能"
}
class VisualizeDataAction(BaseAction):
"""数据可视化Action - 展示条件功能启用"""
async def execute(self, action_input, context=None):
"""数据可视化"""
# 检查可视化依赖
visualization_available = self._check_visualization_deps()
if not visualization_available:
return {
"status": "unavailable",
"message": "数据可视化功能不可用",
"reason": "缺少matplotlib和PIL依赖",
"install_hint": "pip install matplotlib>=3.3.0 Pillow>=8.0.0",
"alternative": "可以使用基础数据分析功能"
}
return await self._create_visualization(action_input)
def _check_visualization_deps(self):
"""检查可视化所需的依赖"""
try:
import matplotlib
import PIL
return True
except ImportError:
return False
async def _create_visualization(self, action_input):
"""创建数据可视化"""
import matplotlib.pyplot as plt
import io
import base64
from PIL import Image
# 获取数据
data = action_input.get("data", [1, 2, 3, 4, 5])
chart_type = action_input.get("type", "line")
# 创建图表
plt.figure(figsize=(10, 6))
if chart_type == "line":
plt.plot(data)
plt.title("线性图")
elif chart_type == "bar":
plt.bar(range(len(data)), data)
plt.title("柱状图")
elif chart_type == "hist":
plt.hist(data, bins=10)
plt.title("直方图")
else:
plt.plot(data)
plt.title("默认线性图")
plt.xlabel("索引")
plt.ylabel("数值")
plt.grid(True)
# 保存为字节流
buffer = io.BytesIO()
plt.savefig(buffer, format='png', dpi=150, bbox_inches='tight')
buffer.seek(0)
# 转换为base64
image_base64 = base64.b64encode(buffer.getvalue()).decode()
plt.close() # 释放内存
return {
"status": "success",
"message": f"生成{chart_type}图表成功",
"data": {
"chart_type": chart_type,
"data_points": len(data),
"image_base64": image_base64
},
"capabilities": ["matplotlib", "pillow", "visualization"]
}
# 测试和演示代码
async def demo_dependency_management():
"""演示依赖管理功能"""
print("🔍 插件依赖管理演示")
print("=" * 50)
# 创建插件实例
plugin = DataAnalysisPlugin()
print("\n📦 插件依赖信息:")
for dep in plugin.python_dependencies:
status = "✅" if plugin._check_dependency_available(dep.package_name) else "❌"
optional_str = " (可选)" if dep.optional else " (必需)"
print(f" {status} {dep.package_name} {dep.version}{optional_str}")
print(f" {dep.description}")
print("\n🧪 功能测试:")
# 测试数据获取
fetch_action = FetchDataAction()
result = await fetch_action.execute({"url": "https://httpbin.org/json"})
print(f" 数据获取: {result['status']}")
# 测试数据分析
analyze_action = AnalyzeDataAction()
test_data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
result = await analyze_action.execute({"data": test_data})
print(f" 数据分析: {result['status']} (方法: {result.get('method', 'unknown')})")
print(f" 可用功能: {result.get('capabilities', [])}")
# 测试数据可视化
viz_action = VisualizeDataAction()
result = await viz_action.execute({"data": test_data, "type": "line"})
print(f" 数据可视化: {result['status']}")
print("\n💡 依赖管理建议:")
missing_deps = plugin.plugin_info.get_missing_packages()
if missing_deps:
print(" 缺失的必需依赖:")
for dep in missing_deps:
print(f" - {dep.get_pip_requirement()}")
print(f"\n 安装命令:")
print(f" pip install {' '.join([dep.get_pip_requirement() for dep in missing_deps])}")
else:
print(" ✅ 所有必需依赖都已安装")
if __name__ == "__main__":
import asyncio
# 为演示添加依赖检查方法
def _check_dependency_available(package_name):
try:
__import__(package_name)
return True
except ImportError:
return False
DataAnalysisPlugin._check_dependency_available = _check_dependency_available
# 运行演示
asyncio.run(demo_dependency_management())
```
## 🎯 示例说明
### 1. 依赖分层设计
这个示例展示了三层依赖设计:
- **必需依赖**: `requests` - 核心功能必需
- **增强依赖**: `pandas`, `numpy` - 提供更强大的分析能力
- **可选依赖**: `matplotlib`, `PIL` - 提供可视化功能
### 2. 优雅降级策略
```python
# 三级功能降级
if has_pandas and has_numpy:
return await self._advanced_analysis(data) # 最佳体验
elif has_numpy:
return await self._numpy_analysis(data) # 中等体验
else:
return await self._basic_analysis(data) # 基础体验
```
### 3. 条件功能启用
```python
# 只有依赖可用时才提供功能
visualization_available = self._check_visualization_deps()
if not visualization_available:
return {"status": "unavailable", "install_hint": "..."}
```
## 🚀 使用这个示例
### 1. 复制代码
将示例代码保存为 `plugins/data_analysis_plugin/plugin.py`
### 2. 测试依赖检查
```python
from src.plugin_system import plugin_manager
# 检查这个插件的依赖
result = plugin_manager.check_all_dependencies()
print(result['plugin_status']['data_analysis_plugin'])
```
### 3. 安装缺失依赖
```python
# 生成requirements文件
plugin_manager.generate_plugin_requirements("data_plugin_deps.txt")
# 手动安装
# pip install -r data_plugin_deps.txt
```
### 4. 测试功能降级
```bash
# 测试基础功能只安装requests
pip install requests>=2.25.0
# 测试增强功能(添加数据处理)
pip install numpy>=1.20.0 pandas>=1.3.0
# 测试完整功能(添加可视化)
pip install matplotlib>=3.3.0 Pillow>=8.0.0
```
## 💡 最佳实践总结
1. **分层依赖设计**: 区分核心、增强、可选依赖
2. **优雅降级处理**: 提供多级功能体验
3. **明确错误信息**: 告诉用户如何解决依赖问题
4. **条件功能启用**: 根据依赖可用性动态调整功能
5. **详细依赖描述**: 说明每个依赖的用途
这个示例展示了如何构建一个既强大又灵活的插件,即使在依赖不完整的情况下也能提供有用的功能。

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,28 +1,24 @@
# 🚀 快速开始指南 # 🚀 快速开始指南
本指南将带你用5分钟时间从零开始创建一个功能完整的MaiBot插件。 本指南将带你用5分钟时间从零开始创建一个功能完整的MaiCore插件。
> **💡 配置先行**
>
> 在开始之前,强烈建议你先阅读 ➡️ **[⚙️ 插件配置定义指南](configuration-guide.md)**。
>
> 了解如何通过 `config_schema` 定义插件配置,可以让系统为你自动生成带详细注释的 `config.toml` 文件,这是现代插件开发的最佳实践。
## 📖 概述 ## 📖 概述
这个指南将带你在5分钟内创建你的第一个MaiBot插件。我们将创建一个简单的问候插件,展示插件系统的基本概念。 这个指南将带你快速创建你的第一个MaiCore插件。我们将创建一个简单的问候插件,展示插件系统的基本概念。无需阅读其他文档,跟着本指南就能完成!
## 🎯 学习目标 ## 🎯 学习目标
- 理解插件的基本结构 - 理解插件的基本结构
- 创建你的第一个Action组件 - 从最简单的插件开始,循序渐进
- 创建你的第一个Command组件 - 学会创建Action组件智能动作
- 学会配置插件 - 学会创建Command组件命令响应
- 掌握配置Schema定义和配置文件自动生成可选
## 📂 准备工作 ## 📂 准备工作
确保你已经: 确保你已经:
1. 克隆了MaiBot项目
1. 克隆了MaiCore项目
2. 安装了Python依赖 2. 安装了Python依赖
3. 了解基本的Python语法 3. 了解基本的Python语法
@@ -30,92 +26,271 @@
### 1. 创建插件目录 ### 1. 创建插件目录
在项目根目录的 `plugins/` 文件夹下创建你的插件目录: 在项目根目录的 `plugins/` 文件夹下创建你的插件目录,目录名与插件名保持一致
可以用以下命令快速创建:
```bash ```bash
mkdir plugins/hello_world_plugin mkdir plugins/hello_world_plugin
cd plugins/hello_world_plugin cd plugins/hello_world_plugin
``` ```
### 2. 创建插件主文 ### 2. 创建最简单的插件
创建 `plugin.py` 文件: 让我们从最基础的开始!创建 `plugin.py` 文件:
```python
from typing import List, Tuple, Type
from src.plugin_system import BasePlugin, register_plugin, ComponentInfo
# ===== 插件注册 =====
@register_plugin
class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiCore插件"""
# 插件基本信息(必须填写)
plugin_name = "hello_world_plugin"
plugin_description = "我的第一个MaiCore插件"
plugin_version = "1.0.0"
plugin_author = "你的名字"
enable_plugin = True # 启用插件
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""返回插件包含的组件列表(目前是空的)"""
return []
```
🎉 **恭喜你刚刚创建了一个最简单但完整的MaiCore插件**
**解释一下这些代码:**
- 首先我们在plugin.py中定义了一个HelloWorldPulgin插件类继承自 `BasePlugin` ,提供基本功能。
- 通过给类加上,`@register_plugin` 装饰器,我们告诉系统"这是一个插件"
- `plugin_name` 等是插件的基本信息,必须填写
- `get_plugin_components()` 返回插件的功能组件现在我们没有定义任何action动作或者command(指令),是空的
### 3. 测试基础插件
现在就可以测试这个插件了启动MaiCore
直接通过启动器运行MaiCore或者 `python bot.py`
在日志中你应该能看到插件被加载的信息。虽然插件还没有任何功能,但它已经成功运行了!
![1750326700269](image/quick-start/1750326700269.png)
### 4. 添加第一个功能问候Action
现在我们要给插件加入一个有用的功能我们从最好玩的Action做起
Action是一类可以让MaiCore根据自身意愿选择使用的“动作”在MaiCore中不论是“回复”还是“不回复”或者“发送表情”以及“禁言”等等都是通过Action实现的。
你可以通过编写动作来拓展MaiCore的能力包括发送语音截图甚至操作文件编写代码......
现在让我们给插件添加第一个简单的功能。这个Action可以对用户发送一句问候语。
`plugin.py` 文件中添加Action组件完整代码如下
```python ```python
from typing import List, Tuple, Type from typing import List, Tuple, Type
from src.plugin_system import ( from src.plugin_system import (
BasePlugin, register_plugin, BaseAction, BaseCommand, BasePlugin, register_plugin, BaseAction,
ComponentInfo, ActionActivationType, ChatMode ComponentInfo, ActionActivationType, ChatMode
) )
# ===== Action组件 ===== # ===== Action组件 =====
class HelloAction(BaseAction): class HelloAction(BaseAction):
"""问候Action - 展示智能动作的基本用法""" """问候Action - 简单的问候动作"""
# ===== 激活控制必须项 ===== # === 基本信息(必须填写)===
focus_activation_type = ActionActivationType.KEYWORD
normal_activation_type = ActionActivationType.KEYWORD
mode_enable = ChatMode.ALL
parallel_action = False
# ===== 基本信息必须项 =====
action_name = "hello_greeting" action_name = "hello_greeting"
action_description = "向用户发送友好的问候消息" action_description = "向用户发送问候消息"
# 关键词配置 # === 功能描述(必须填写)===
activation_keywords = ["你好", "hello", "hi"]
keyword_case_sensitive = False
# ===== 功能定义必须项 =====
action_parameters = { action_parameters = {
"greeting_style": "问候风格casual(随意) 或 formal(正式)" "greeting_message": "要发送的问候消息"
} }
action_require = [ action_require = [
"用户发送问候时使用", "需要发送友好问候时使用",
"营造友好的聊天氛围" "当有人向你问好时使用",
"当你遇见没有见过的人时使用"
] ]
associated_types = ["text"]
associated_types = ["text", "emoji"]
async def execute(self) -> Tuple[bool, str]: async def execute(self) -> Tuple[bool, str]:
"""执行问候动作""" """执行问候动作 - 这是核心功能"""
# 获取参数 # 发送问候消息
style = self.action_data.get("greeting_style", "casual") greeting_message = self.action_data.get("greeting_message","")
# 根据风格生成问候语 message = "嗨!很开心见到你!😊" + greeting_message
if style == "formal":
message = "您好!很高兴为您服务!"
emoji = "🙏"
else:
message = "嗨!很开心见到你!"
emoji = "😊"
# 发送消息
await self.send_text(message) await self.send_text(message)
await self.send_type("emoji", emoji)
return True, f"发送了{style}风格的问候" return True, "发送了问候消息"
# ===== 插件注册 =====
@register_plugin
class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiCore插件"""
# 插件基本信息
plugin_name = "hello_world_plugin"
plugin_description = "我的第一个MaiCore插件包含问候功能"
plugin_version = "1.0.0"
plugin_author = "你的名字"
enable_plugin = True
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""返回插件包含的组件列表"""
return [
# 添加我们的问候Action
(HelloAction.get_action_info(), HelloAction),
]
```
**新增内容解释:**
- `HelloAction` 是一个Action组件MaiCore可能会选择使用它
- `execute()` 函数是Action的核心定义了当Action被MaiCore选择后具体要做什么
- `self.send_text()` 是发送文本消息的便捷方法
### 5. 测试问候功能
重启MaiCore然后在聊天中发送任意消息比如
```
你好
```
MaiCore可能会选择使用你的问候Action发送回复
```
嗨!很开心见到你!😊
```
![1750332508760](image/quick-start/1750332508760.png)
> **💡 小提示**MaiCore会智能地决定什么时候使用它。如果没有立即看到效果多试几次不同的消息。
🎉 **太棒了!你的插件已经有实际功能了!**
### 5.5. 了解激活系统(重要概念)
Action固然好用简单但是现在有个问题当用户加载了非常多的插件添加了很多自定义ActionLLM需要选择的Action也会变多
而不断增多的Action会加大LLM的消耗和负担降低Action使用的精准度。而且我们并不需要LLM在所有时候都考虑所有Action
例如,当群友只是在进行正常的聊天,就没有必要每次都考虑是否要选择“禁言”动作,这不仅影响决策速度,还会增加消耗。
那有什么办法能够让Action有选择的加入MaiCore的决策池呢
**什么是激活系统?**
激活系统决定了什么时候你的Action会被MaiCore"考虑"使用:
- **`ActionActivationType.ALWAYS`** - 总是可用(默认值)
- **`ActionActivationType.KEYWORD`** - 只有消息包含特定关键词时才可用
- **`ActionActivationType.PROBABILITY`** - 根据概率随机可用
- **`ActionActivationType.NEVER`** - 永不可用(用于调试)
> **💡 使用提示**
>
> - 推荐使用枚举类型(如 `ActionActivationType.ALWAYS`),有代码提示和类型检查
> - 也可以直接使用字符串(如 `"always"`),系统都支持
### 5.6. 进阶:尝试关键词激活(可选)
现在让我们尝试一个更精确的激活方式添加一个只在用户说特定关键词时才激活的Action
```python
# 在HelloAction后面添加这个新Action
class ByeAction(BaseAction):
"""告别Action - 只在用户说再见时激活"""
action_name = "bye_greeting"
action_description = "向用户发送告别消息"
# 使用关键词激活
focus_activation_type = ActionActivationType.KEYWORD
normal_activation_type = ActionActivationType.KEYWORD
# 关键词设置
activation_keywords = ["再见", "bye", "88", "拜拜"]
keyword_case_sensitive = False
action_parameters = {"bye_message": "要发送的告别消息"}
action_require = [
"用户要告别时使用",
"当有人要离开时使用",
"当有人和你说再见时使用",
]
associated_types = ["text"]
async def execute(self) -> Tuple[bool, str]:
bye_message = self.action_data.get("bye_message","")
message = "再见!期待下次聊天!👋" + bye_message
await self.send_text(message)
return True, "发送了告别消息"
```
然后在插件注册中添加这个Action
```python
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
return [
(HelloAction.get_action_info(), HelloAction),
(ByeAction.get_action_info(), ByeAction), # 添加告别Action
]
```
现在测试:发送"再见"应该会触发告别Action
**关键词激活的特点:**
- 更精确:只在包含特定关键词时才会被考虑
- 更可预测:用户知道说什么会触发什么功能
- 更适合:特定场景或命令式的功能
### 6. 添加第二个功能时间查询Command
现在让我们添加一个Command组件。Command和Action不同它是直接响应用户命令的
Command是最简单最直接的相应不由LLM判断选择使用
```python
# 在现有代码基础上添加Command组件
# ===== Command组件 ===== # ===== Command组件 =====
class TimeCommand(BaseCommand): from src.plugin_system import BaseCommand
"""时间查询Command - 展示命令的基本用法""" #导入Command基类
command_pattern = r"^/time$" class TimeCommand(BaseCommand):
"""时间查询Command - 响应/time命令"""
command_name = "time"
command_description = "查询当前时间"
# === 命令设置(必须填写)===
command_pattern = r"^/time$" # 精确匹配 "/time" 命令
command_help = "查询当前时间" command_help = "查询当前时间"
command_examples = ["/time"] command_examples = ["/time"]
intercept_message = True # 拦截消息处理 intercept_message = True # 拦截消息,不让其他组件处理
async def execute(self) -> Tuple[bool, str]: async def execute(self) -> Tuple[bool, str]:
"""执行时间查询""" """执行时间查询"""
import datetime import datetime
# 获取当前时间
time_format = self.get_config("time.format", "%Y-%m-%d %H:%M:%S")
now = datetime.datetime.now() now = datetime.datetime.now()
time_str = now.strftime("%Y-%m-%d %H:%M:%S") time_str = now.strftime(time_format)
await self.send_text(f"⏰ 当前时间:{time_str}") # 发送时间信息
message = f"⏰ 当前时间:{time_str}"
await self.send_text(message)
return True, f"显示了当前时间: {time_str}" return True, f"显示了当前时间: {time_str}"
@@ -123,267 +298,230 @@ class TimeCommand(BaseCommand):
@register_plugin @register_plugin
class HelloWorldPlugin(BasePlugin): class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiBot插件""" """Hello World插件 - 你的第一个MaiCore插件"""
# 插件基本信息
plugin_name = "hello_world_plugin" plugin_name = "hello_world_plugin"
plugin_description = "Hello World演示插件展示基本的Action和Command用法" plugin_description = "我的第一个MaiCore插件包含问候和时间查询功能"
plugin_version = "1.0.0" plugin_version = "1.0.0"
plugin_author = "你的名字" plugin_author = "你的名字"
enable_plugin = True # 默认启用插件 enable_plugin = True
config_file_name = "config.toml"
# Python依赖声明可选
python_dependencies = [
# 如果你的插件需要额外的Python包在这里声明
# PythonDependency(
# package_name="requests",
# version=">=2.25.0",
# description="HTTP请求库"
# ),
]
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""返回插件包含的组件列表"""
return [ return [
# Action组件 - 使用类中定义的所有属性
(HelloAction.get_action_info(), HelloAction), (HelloAction.get_action_info(), HelloAction),
(ByeAction.get_action_info(), ByeAction),
# Command组件 - 需要指定name和description (TimeCommand.get_command_info(), TimeCommand),
(TimeCommand.get_command_info(
name="time_query",
description="查询当前系统时间"
), TimeCommand),
] ]
``` ```
### 3. 创建配置文件 **Command组件解释**
创建 `config.toml` 文件: - Command是直接响应用户命令的组件
- `command_pattern` 使用正则表达式匹配用户输入
- `^/time$` 表示精确匹配 "/time"
- `intercept_message = True` 表示处理完命令后不再让其他组件处理
```toml ### 7. 测试时间查询功能
[plugin]
name = "hello_world_plugin"
version = "1.0.0"
enabled = true
description = "Hello World演示插件"
[greeting] 重启MaiCore发送命令
default_style = "casual"
enable_emoji = true
[time] ```
timezone = "Asia/Shanghai" /time
format = "%Y-%m-%d %H:%M:%S"
[logging]
level = "INFO"
``` ```
### 4. 创建说明文档 你应该会收到回复:
创建 `README.md` 文件: ```
⏰ 当前时间2024-01-01 12:30:45
```
🎉 **太棒了现在你的插件有3个功能了**
### 8. 添加配置文件(可选进阶)
如果你想让插件更加灵活,可以添加配置支持。
> **🚨 重要不要手动创建config.toml文件**
>
> 我们需要在插件代码中定义配置Schema让系统自动生成配置文件。
首先在插件类中定义配置Schema
```python
from src.plugin_system.base.config_types import ConfigField
@register_plugin
class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiCore插件"""
plugin_name = "hello_world_plugin"
plugin_description = "我的第一个MaiCore插件包含问候和时间查询功能"
plugin_version = "1.0.0"
plugin_author = "你的名字"
enable_plugin = True
config_file_name = "config.toml" # 配置文件名
# 配置节描述
config_section_descriptions = {
"plugin": "插件基本信息",
"greeting": "问候功能配置",
"time": "时间查询配置"
}
# 配置Schema定义
config_schema = {
"plugin": {
"name": ConfigField(type=str, default="hello_world_plugin", description="插件名称"),
"version": ConfigField(type=str, default="1.0.0", description="插件版本"),
"enabled": ConfigField(type=bool, default=True, description="是否启用插件")
},
"greeting": {
"message": ConfigField(
type=str,
default="嗨!很开心见到你!😊",
description="默认问候消息"
),
"enable_emoji": ConfigField(type=bool, default=True, description="是否启用表情符号")
},
"time": {
"format": ConfigField(
type=str,
default="%Y-%m-%d %H:%M:%S",
description="时间显示格式"
)
}
}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
return [
(HelloAction.get_action_info(), HelloAction),
(ByeAction.get_action_info(), ByeAction),
(TimeCommand.get_command_info(), TimeCommand),
]
```
然后修改Action和Command代码让它们读取配置
```python
# 在HelloAction的execute方法中
async def execute(self) -> Tuple[bool, str]:
# 从配置文件读取问候消息
greeting_message = self.action_data.get("greeting_message", "")
base_message = self.get_config("greeting.message", "嗨!很开心见到你!😊")
message = base_message + greeting_message
await self.send_text(message)
return True, "发送了问候消息"
# 在TimeCommand的execute方法中
async def execute(self) -> Tuple[bool, str]:
import datetime
# 从配置文件读取时间格式
time_format = self.get_config("time.format", "%Y-%m-%d %H:%M:%S")
now = datetime.datetime.now()
time_str = now.strftime(time_format)
message = f"⏰ 当前时间:{time_str}"
await self.send_text(message)
return True, f"显示了当前时间: {time_str}"
```
**配置系统工作流程:**
1. **定义Schema**: 在插件代码中定义配置结构
2. **自动生成**: 启动插件时,系统会自动生成 `config.toml` 文件
3. **用户修改**: 用户可以修改生成的配置文件
4. **代码读取**: 使用 `self.get_config()` 读取配置值
**配置功能解释:**
- `self.get_config()` 可以读取配置文件中的值
- 第一个参数是配置路径(用点分隔),第二个参数是默认值
- 配置文件会包含详细的注释和说明,用户可以轻松理解和修改
- **绝不要手动创建配置文件**,让系统自动生成
### 9. 创建说明文档(可选)
创建 `README.md` 文件来说明你的插件:
```markdown ```markdown
# Hello World 插件 # Hello World 插件
## 概述 ## 概述
我的第一个MaiCore插件包含问候和时间查询功能。
这是一个简单的Hello World插件演示了MaiBot插件系统的基本用法。
## 功能 ## 功能
- **问候功能**: 当用户说"你好"、"hello"、"hi"时自动回复
- **HelloAction**: 智能问候动作,响应用户的问候语 - **时间查询**: 发送 `/time` 命令查询当前时间
- **TimeCommand**: 时间查询命令,显示当前时间
## 使用方法 ## 使用方法
### 问候功能
发送包含以下关键词的消息:
- "你好"
- "hello"
- "hi"
### Action使用 ### 时间查询
当用户发送包含"你好"、"hello"或"hi"的消息时,插件会自动触发问候动作。 发送命令:`/time`
### Command使用 ## 配置文件
发送 `/time` 查询当前时间。 插件会自动生成 `config.toml` 配置文件,用户可以修改:
- 问候消息内容
- 时间显示格式
- 插件启用状态
## 配置 注意:配置文件是自动生成的,不要手动创建!
可以通过 `config.toml` 调整插件行为。
``` ```
## 🎮 测试插件 ## 🎯 你学会了什么
### 1. 启动MaiBot 恭喜你刚刚从零开始创建了一个完整的MaiCore插件让我们回顾一下
将插件放入 `plugins/` 目录后启动MaiBot ### 核心概念
- **插件Plugin**: 包含多个功能组件的集合
- **Action组件**: 智能动作,由麦麦根据情境自动选择使用
- **Command组件**: 直接响应用户命令的功能
- **配置Schema**: 定义配置结构,系统自动生成配置文件
### 开发流程
1. ✅ 创建最简单的插件框架
2. ✅ 添加Action
3. ✅ 理解激活系统的工作原理
4. ✅ 尝试KEYWORD激活的Action进阶
5. ✅ 添加Command组件
6. ✅ 可选定义配置Schema
7. ✅ 测试完整功能
## 📚 进阶学习
现在你已经掌握了基础,可以继续深入学习:
1. **掌握更多Action功能** 📖 [Action组件详解](action-components.md)
- 学习不同的激活方式
- 了解Action的生命周期
- 掌握参数传递
2. **学会配置管理** ⚙️ [插件配置定义指南](configuration-guide.md)
- 定义配置Schema
- 自动生成配置文件
- 配置验证和类型检查
3. **深入Command系统** 📖 [Command组件详解](command-components.md)
- 复杂正则表达式
- 参数提取和处理
- 错误处理
4. **掌握API系统** 📖 [新API使用指南](examples/replyer_api_usage.md)
- replyer_1智能生成
- 高级消息处理
- 表情和媒体发送
祝你插件开发愉快!🎉
```bash
python main.py
``` ```
### 2. 测试Action
发送消息:
``` ```
你好
```
期望输出:
```
嗨!很开心见到你!😊
```
### 3. 测试Command
发送命令:
```
/time
```
期望输出:
```
⏰ 当前时间2024-01-01 12:00:00
```
## 🔍 解析代码
### Action组件重点
1. **激活控制**: 使用 `KEYWORD` 激活类型,当检测到指定关键词时触发
2. **必须项完整**: 包含所有必须的类属性
3. **智能决策**: 麦麦会根据情境决定是否使用这个Action
### Command组件重点
1. **正则匹配**: 使用 `^/time$` 精确匹配 `/time` 命令
2. **消息拦截**: 设置 `intercept_message = True` 防止命令继续处理
3. **即时响应**: 匹配到命令立即执行
### 插件注册重点
1. **@register_plugin**: 装饰器自动注册插件
2. **组件列表**: `get_plugin_components()` 返回所有组件
3. **配置加载**: 自动加载 `config.toml` 文件
## 📦 添加依赖包(可选)
如果你的插件需要额外的Python包可以声明依赖
```python
from src.plugin_system import PythonDependency
@register_plugin
class HelloWorldPlugin(BasePlugin):
# ... 其他配置 ...
# 声明Python依赖
python_dependencies = [
PythonDependency(
package_name="requests",
version=">=2.25.0",
description="HTTP请求库用于网络功能"
),
PythonDependency(
package_name="numpy",
version=">=1.20.0",
optional=True,
description="数值计算库(可选功能)"
),
]
```
### 依赖检查
系统会自动检查依赖,你也可以手动检查:
```python
from src.plugin_system import plugin_manager
# 检查所有插件依赖
result = plugin_manager.check_all_dependencies()
print(f"缺少依赖的插件: {result['plugins_with_missing_required']}个")
# 生成requirements文件
plugin_manager.generate_plugin_requirements("plugin_deps.txt")
```
📚 **详细了解**: [依赖管理系统](dependency-management.md)
## 🎯 下一步
恭喜你已经创建了第一个MaiBot插件。接下来可以
1. 学习 [Action组件详解](action-components.md) 掌握更复杂的Action开发
2. 学习 [Command组件详解](command-components.md) 创建更强大的命令
3. 了解 [依赖管理系统](dependency-management.md) 管理Python包依赖
4. 查看 [API参考](api/) 了解所有可用的接口
5. 参考 [完整示例](examples/complete-examples.md) 学习最佳实践
## 🐛 常见问题
### Q: 插件没有加载怎么办?
A: 检查:
1. 插件是否放在 `plugins/` 目录下
2. `plugin.py` 文件语法是否正确
3. 查看启动日志中的错误信息
### Q: Action没有触发怎么办
A: 检查:
1. 关键词是否正确配置
2. 消息是否包含激活关键词
3. 聊天模式是否匹配
### Q: Command无响应怎么办
A: 检查:
1. 正则表达式是否正确
2. 命令格式是否精确匹配
3. 是否有其他插件拦截了消息
## 🔧 插件启用状态管理
### 启用状态控制方式
插件可以通过以下两种方式控制启用状态:
1. **类属性控制**
```python
class MyPlugin(BasePlugin):
enable_plugin = True # 在类中设置启用状态
```
2. **配置文件控制**
```toml
[plugin]
enabled = true # 在配置文件中设置启用状态
```
### 启用状态优先级
1. 配置文件中的设置优先级高于类属性
2. 如果配置文件中没有 `[plugin] enabled` 设置,则使用类属性中的值
3. 如果类属性也没有设置,则使用 `BasePlugin` 的默认值 `False`
### 最佳实践
1. 在开发插件时,建议在类中设置 `enable_plugin = True`
2. 在部署插件时,通过配置文件控制启用状态
3. 在文档中明确说明插件的默认启用状态
4. 提供配置示例,说明如何启用/禁用插件
### 常见问题
1. **插件未加载**
- 检查类属性 `enable_plugin` 是否设置为 `True`
- 检查配置文件中的 `[plugin] enabled` 设置
- 查看日志中是否有插件加载相关的错误信息
2. **配置文件不生效**
- 确保配置文件名称正确(默认为 `config.toml`
- 确保配置文件格式正确TOML格式
- 确保配置文件中的 `[plugin]` 部分存在
3. **动态启用/禁用**
- 修改配置文件后需要重启MaiBot才能生效
- 目前不支持运行时动态启用/禁用插件
---
🎉 **成功你已经掌握了MaiBot插件开发的基础**

View File

@@ -1,293 +0,0 @@
# 综合示例插件
## 概述
这是一个展示新插件系统完整功能的综合示例插件,整合了所有旧示例插件的功能,并使用新的架构重写。
## 功能特性
### 🎯 Action组件
#### SmartGreetingAction - 智能问候
- **激活类型**:
- Focus模式: KEYWORD (关键词激活)
- Normal模式: KEYWORD (关键词激活)
- **触发关键词**: 你好、hello、hi、嗨、问候、早上好、晚上好
- **支持模式**: 所有聊天模式
- **并行执行**: 否
- **功能**: 智能问候支持多种风格和LLM个性化生成
- **参数**: username(用户名), greeting_style(问候风格)
- **配置**: 可自定义问候模板、启用表情、LLM生成
#### HelpfulAction - 智能助手
- **激活类型**:
- Focus模式: LLM_JUDGE (LLM智能判断)
- Normal模式: RANDOM (随机激活概率15%)
- **支持模式**: 所有聊天模式
- **并行执行**: 是
- **功能**: 主动提供帮助和建议展示LLM判断激活机制
- **参数**: help_type(帮助类型), topic(主题), complexity(复杂度)
- **特点**:
- 通过LLM智能判断是否需要提供帮助
- 展示两层决策机制的实际应用
- 支持多种帮助类型(解释、建议、指导、提示)
### 📝 Command组件
#### 1. ComprehensiveHelpCommand - 综合帮助系统
```
/help [命令名]
```
- **功能**: 显示所有命令帮助或特定命令详情
- **拦截**: ✅ 拦截消息处理
- **示例**: `/help`, `/help send`
#### 2. MessageSendCommand - 消息发送
```
/send <group|user> <ID> <消息内容>
```
- **功能**: 向指定群聊或私聊发送消息
- **拦截**: ✅ 拦截消息处理
- **示例**: `/send group 123456 大家好`
#### 3. SystemStatusCommand - 系统状态查询
```
/status [类型]
```
- **功能**: 查询系统、插件、内存等状态
- **拦截**: ✅ 拦截消息处理
- **示例**: `/status`, `/status 插件`
#### 4. EchoCommand - 回声命令
```
/echo <消息内容>
```
- **功能**: 重复用户输入的消息
- **拦截**: ✅ 拦截消息处理
- **示例**: `/echo Hello World`
#### 5. MessageInfoCommand - 消息信息查询
```
/info
```
- **功能**: 显示当前消息的详细信息
- **拦截**: ✅ 拦截消息处理
- **示例**: `/info`
#### 6. CustomPrefixCommand - 自定义前缀
```
/prefix <前缀> <内容>
```
- **功能**: 为消息添加自定义前缀
- **拦截**: ✅ 拦截消息处理
- **示例**: `/prefix [公告] 系统维护`
#### 7. LogMonitorCommand - 日志监控
```
/log [级别]
```
- **功能**: 记录消息到日志但不拦截后续处理
- **拦截**: ❌ 不拦截,继续处理消息
- **示例**: `/log`, `/log debug`
## 🔧 拦截控制演示
此插件完美演示了新插件系统的**拦截控制功能**
### 拦截型命令 (intercept_message = True)
- `/help` - 显示帮助后停止处理
- `/send` - 发送消息后停止处理
- `/status` - 查询状态后停止处理
- `/echo` - 回声后停止处理
- `/info` - 显示信息后停止处理
- `/prefix` - 添加前缀后停止处理
### 非拦截型命令 (intercept_message = False)
- `/log` - 记录日志但继续处理,可能触发其他功能
## ⚙️ 配置说明
插件支持通过 `config.toml` 进行详细配置:
### 组件控制
```toml
[components]
enable_greeting = true # 启用智能问候Action
enable_helpful = true # 启用智能助手Action
enable_help = true # 启用帮助系统Command
enable_send = true # 启用消息发送Command
enable_echo = true # 启用回声Command
enable_info = true # 启用消息信息Command
enable_dice = true # 启用骰子Command
```
### Action配置
```toml
[greeting]
template = "你好,{username}" # 问候模板
enable_emoji = true # 启用表情
enable_llm = false # 启用LLM生成
[helpful]
enable_llm = false # 启用LLM生成帮助
enable_emoji = true # 启用鼓励表情
random_activation_probability = 0.15 # 随机激活概率
```
### Command配置
```toml
[send]
max_message_length = 500 # 最大消息长度
[echo]
max_length = 200 # 回声最大长度
enable_formatting = true # 启用格式化
[help]
enable_llm = false # 启用LLM生成帮助内容
enable_emoji = true # 启用帮助表情
```
## 🚀 使用示例
### Action组件示例
#### 智能问候Action (关键词激活)
```
用户: 你好
机器人: 嗨!很开心见到你~ 😊
用户: 早上好
机器人: 早上好!今天也要元气满满哦! ✨
```
#### 智能助手Action (LLM判断激活)
```
用户: 我不太懂怎么使用这个功能
机器人: 关于功能使用我来为你解释一下这是一个simple级别的概念...
这个概念其实很简单,让我用通俗的话来说明。 💡
用户: Python装饰器是什么
机器人: 关于Python装饰器我来为你解释一下这是一个medium级别的概念...
装饰器是一种设计模式,用于在不修改原函数的情况下扩展功能。 🎯
```
### Command组件示例
#### 帮助查询
```
用户: /help
机器人: [显示完整命令帮助列表]
用户: /help send
机器人: [显示send命令的详细帮助]
```
#### 消息发送
```
用户: /send group 123456 大家好!
机器人: ✅ 消息已成功发送到 群聊 123456
```
#### 骰子命令
```
用户: !dice
机器人: 🎲 你投出了: 4
用户: !骰子 3
机器人: 🎲 你投出了3个骰子: 2, 5, 1 (总计: 8)
```
### 两层决策机制展示
#### 第一层:激活控制
```
# SmartGreetingAction - 关键词激活
用户消息包含"你好" → Action被激活 → 进入候选池
# HelpfulAction - LLM判断激活
用户表达困惑 → LLM判断"是" → Action被激活 → 进入候选池
用户正常聊天 → LLM判断"否" → Action不激活 → 不进入候选池
```
#### 第二层:使用决策
```
# 即使Action被激活LLM还会根据action_require判断是否真正使用
# 比如HelpfulAction的条件"避免过度频繁地提供帮助,要恰到好处"
# 如果刚刚已经提供了帮助,可能不会再次选择使用
```
## 📁 文件结构
```
plugins/example_plugin/ # 用户插件目录
├── plugin.py # 主插件文件
├── config.toml # 配置文件
└── README.md # 说明文档
```
> 💡 **目录说明**
> - `plugins/` - 用户自定义插件目录(推荐放置位置)
> - `src/plugins/builtin/` - 系统内置插件目录
## 🔄 架构升级
此插件展示了从旧插件系统到新插件系统的完整升级:
### 新系统特征
- 使用统一的组件注册机制
- 新的 `BaseAction``BaseCommand` 基类
- **拦截控制功能** - 灵活的消息处理流程
- 强大的配置驱动架构
- 统一的API接口
- 完整的错误处理和日志
## 💡 开发指南
此插件可作为开发新插件的完整参考:
### Action开发规范
1. **必须项检查清单**:
- ✅ 激活控制必须项:`focus_activation_type`, `normal_activation_type`, `mode_enable`, `parallel_action`
- ✅ 基本信息必须项:`action_name`, `action_description`
- ✅ 功能定义必须项:`action_parameters`, `action_require`, `associated_types`
2. **激活类型选择**:
- `KEYWORD`: 适合明确触发词的功能(如问候)
- `LLM_JUDGE`: 适合需要智能判断的功能(如帮助)
- `RANDOM`: 适合增加随机性的功能
- `ALWAYS`: 适合总是考虑的功能
- `NEVER`: 用于临时禁用
3. **两层决策设计**:
- 第一层激活控制控制Action是否进入候选池
- 第二层使用决策LLM根据场景智能选择
### Command开发规范
1. **拦截控制**: 根据需要设置 `intercept_message`
2. **正则表达式**: 使用命名组捕获参数
3. **错误处理**: 完整的异常捕获和用户反馈
### 通用开发规范
1. **配置使用**: 通过 `self.api.get_config()` 读取配置
2. **日志记录**: 结构化的日志输出
3. **API调用**: 使用新的统一API接口
4. **注册简化**: Action使用 `get_action_info()` 无参数调用
## 🎉 总结
这个综合示例插件完美展示了新插件系统的强大功能:
### 🚀 核心特性
- **两层决策机制**优化LLM决策压力提升性能
- **完整的Action规范**:所有必须项都在类中统一定义
- **灵活的激活控制**:支持多种激活类型和条件
- **精确的拦截控制**Command可以精确控制消息处理流程
### 📚 学习价值
- **Action vs Command**: 清晰展示两种组件的不同设计理念
- **激活机制**: 实际演示关键词、LLM判断、随机等激活方式
- **配置驱动**: 展示如何通过配置文件控制插件行为
- **错误处理**: 完整的异常处理和用户反馈机制
这个插件是理解和掌握MaiBot插件系统的最佳起点🌟

View File

@@ -1,792 +0,0 @@
"""
综合示例插件
将旧的示例插件功能重写为新插件系统架构,展示完整的插件开发模式。
包含功能:
- 智能问候Action
- 帮助系统Command
- 消息发送Command
- 状态查询Command
- 回声Command
- 自定义前缀Command
- 消息信息查询Command
- 高级消息发送Command
演示新插件系统的完整功能:
- Action和Command组件的定义
- 拦截控制功能
- 配置驱动的行为
- API的多种使用方式
- 日志和错误处理
"""
from typing import List, Tuple, Type, Optional
import time
import random
# 导入新插件系统
from src.plugin_system.base.base_plugin import BasePlugin
from src.plugin_system.base.base_plugin import register_plugin
from src.plugin_system.base.base_action import BaseAction
from src.plugin_system.base.base_command import BaseCommand
from src.plugin_system.base.component_types import ComponentInfo, ActionActivationType, ChatMode
from src.plugin_system.base.config_types import ConfigField
from src.common.logger import get_logger
logger = get_logger("example_comprehensive")
# ===== Action组件 =====
class SmartGreetingAction(BaseAction):
"""智能问候Action - 基于关键词触发的问候系统"""
# ===== 激活控制必须项 =====
focus_activation_type = ActionActivationType.KEYWORD
normal_activation_type = ActionActivationType.KEYWORD
mode_enable = ChatMode.ALL
parallel_action = False
# ===== 基本信息必须项 =====
action_name = "smart_greeting"
action_description = "智能问候系统,基于关键词触发,支持个性化问候消息"
# 关键词配置
activation_keywords = ["你好", "hello", "hi", "", "问候", "早上好", "晚上好"]
keyword_case_sensitive = False
# ===== 功能定义必须项 =====
action_parameters = {
"username": "要问候的用户名(可选)",
"greeting_style": "问候风格casual(随意)、formal(正式)、friendly(友好)默认casual",
}
action_require = [
"用户发送包含问候词汇的消息时使用",
"检测到新用户加入时使用",
"响应友好交流需求时使用",
"避免在短时间内重复问候同一用户",
]
associated_types = ["text", "emoji"]
async def execute(self) -> Tuple[bool, str]:
"""执行智能问候"""
logger.info(f"{self.log_prefix} 执行智能问候动作: {self.reasoning}")
try:
# 获取参数
username = self.action_data.get("username", "")
greeting_style = self.action_data.get("greeting_style", "casual")
# 获取配置
template = self.api.get_config("greeting.template", "你好,{username}欢迎使用MaiBot综合插件系统")
enable_emoji = self.api.get_config("greeting.enable_emoji", True)
enable_llm = self.api.get_config("greeting.enable_llm", False)
# 构建问候消息
if enable_llm:
# 使用LLM生成个性化问候
greeting_message = await self._generate_llm_greeting(username, greeting_style)
else:
# 使用模板生成问候
greeting_message = await self._generate_template_greeting(template, username, greeting_style)
# 发送问候消息
await self.send_text(greeting_message)
# 可选发送表情
if enable_emoji:
emojis = ["😊", "👋", "🎉", "", "🌟"]
selected_emoji = random.choice(emojis)
await self.send_type("emoji", selected_emoji)
logger.info(f"{self.log_prefix} 智能问候执行成功")
return True, f"{username or '用户'}发送了{greeting_style}风格的问候"
except Exception as e:
logger.error(f"{self.log_prefix} 智能问候执行失败: {e}")
return False, f"问候失败: {str(e)}"
async def _generate_template_greeting(self, template: str, username: str, style: str) -> str:
"""使用模板生成问候消息"""
# 根据风格调整问候语
style_templates = {
"casual": "{username}!很开心见到你~",
"formal": "您好{username},很荣幸为您服务!",
"friendly": "你好{username}!欢迎来到这里,希望我们能成为好朋友!😊",
}
selected_template = style_templates.get(style, template)
username_display = f" {username}" if username else ""
return selected_template.format(username=username_display)
async def _generate_llm_greeting(self, username: str, style: str) -> str:
"""使用LLM生成个性化问候"""
try:
# 获取可用模型
models = self.api.get_available_models()
if not models:
logger.warning(f"{self.log_prefix} 无可用LLM模型使用默认问候")
return await self._generate_template_greeting("你好{username}", username, style)
# 构建提示词
prompt = f"""
请生成一个{style}风格的问候消息。
用户名: {username or "用户"}
要求:
- 风格: {style}
- 简洁友好
- 不超过50字
- 符合中文表达习惯
"""
# 调用LLM
model_config = next(iter(models.values()))
success, response, reasoning, model_name = await self.api.generate_with_model(
prompt=prompt,
model_config=model_config,
request_type="plugin.greeting",
temperature=0.7,
max_tokens=100,
)
if success and response:
return response.strip()
else:
logger.warning(f"{self.log_prefix} LLM生成失败使用默认问候")
return await self._generate_template_greeting("你好{username}", username, style)
except Exception as e:
logger.error(f"{self.log_prefix} LLM问候生成异常: {e}")
return await self._generate_template_greeting("你好{username}", username, style)
class HelpfulAction(BaseAction):
"""智能帮助Action - 展示LLM_JUDGE激活类型和随机激活的综合示例"""
# ===== 激活控制必须项 =====
focus_activation_type = ActionActivationType.LLM_JUDGE
normal_activation_type = ActionActivationType.RANDOM
mode_enable = ChatMode.ALL
parallel_action = True
# ===== 基本信息必须项 =====
action_name = "helpful_assistant"
action_description = "智能助手Action主动提供帮助和建议展示LLM判断激活"
# LLM判断提示词
llm_judge_prompt = """
判定是否需要使用智能帮助动作的条件:
1. 用户表达了困惑或需要帮助
2. 用户提出了问题但没有得到满意答案
3. 对话中出现了技术术语或复杂概念
4. 用户似乎在寻找解决方案
5. 适合提供额外信息或建议的场合
不要使用的情况:
1. 用户明确表示不需要帮助
2. 对话进行得很顺利,无需干预
3. 用户只是在闲聊,没有实际需求
请回答""""
"""
# 随机激活概率
random_activation_probability = 0.15
# ===== 功能定义必须项 =====
action_parameters = {
"help_type": "帮助类型explanation(解释)、suggestion(建议)、guidance(指导)、tips(提示)",
"topic": "帮助主题或用户关心的问题",
"complexity": "复杂度simple(简单)、medium(中等)、advanced(高级)",
}
action_require = [
"用户表达困惑或寻求帮助时使用",
"检测到用户遇到技术问题时使用",
"对话中出现知识盲点时主动提供帮助",
"避免过度频繁地提供帮助,要恰到好处",
]
associated_types = ["text", "emoji"]
async def execute(self) -> Tuple[bool, str]:
"""执行智能帮助"""
logger.info(f"{self.log_prefix} 执行智能帮助动作: {self.reasoning}")
try:
# 获取参数
help_type = self.action_data.get("help_type", "suggestion")
topic = self.action_data.get("topic", "")
complexity = self.action_data.get("complexity", "simple")
# 根据帮助类型生成响应
help_message = await self._generate_help_message(help_type, topic, complexity)
# 发送帮助消息
await self.send_text(help_message)
# 可选发送鼓励表情
if self.api.get_config("help.enable_emoji", True):
emojis = ["💡", "🤔", "💪", "🎯", ""]
selected_emoji = random.choice(emojis)
await self.send_type("emoji", selected_emoji)
logger.info(f"{self.log_prefix} 智能帮助执行成功")
return True, f"提供了{help_type}类型的帮助,主题:{topic}"
except Exception as e:
logger.error(f"{self.log_prefix} 智能帮助执行失败: {e}")
return False, f"帮助失败: {str(e)}"
async def _generate_help_message(self, help_type: str, topic: str, complexity: str) -> str:
"""生成帮助消息"""
# 获取配置
enable_llm = self.api.get_config("help.enable_llm", False)
if enable_llm:
return await self._generate_llm_help(help_type, topic, complexity)
else:
return await self._generate_template_help(help_type, topic, complexity)
async def _generate_template_help(self, help_type: str, topic: str, complexity: str) -> str:
"""使用模板生成帮助消息"""
help_templates = {
"explanation": f"关于{topic},我来为你解释一下:这是一个{complexity}级别的概念...",
"suggestion": f"针对{topic},我建议你可以尝试以下方法...",
"guidance": f"{topic}方面,我可以为你提供一些指导...",
"tips": f"关于{topic},这里有一些实用的小贴士...",
}
base_message = help_templates.get(help_type, f"关于{topic},我很乐意为你提供帮助!")
# 根据复杂度调整消息
if complexity == "advanced":
base_message += "\n\n这个话题比较深入,需要一些基础知识。"
elif complexity == "simple":
base_message += "\n\n这个概念其实很简单,让我用通俗的话来说明。"
return base_message
async def _generate_llm_help(self, help_type: str, topic: str, complexity: str) -> str:
"""使用LLM生成个性化帮助"""
try:
models = self.api.get_available_models()
if not models:
return await self._generate_template_help(help_type, topic, complexity)
prompt = f"""
请生成一个{help_type}类型的帮助消息。
主题: {topic}
复杂度: {complexity}
要求:
- 风格友好、耐心
- 内容准确、有用
- 长度适中(100-200字)
- 根据复杂度调整语言难度
"""
model_config = next(iter(models.values()))
success, response, reasoning, model_name = await self.api.generate_with_model(
prompt=prompt, model_config=model_config, request_type="plugin.help", temperature=0.7, max_tokens=300
)
if success and response:
return response.strip()
else:
return await self._generate_template_help(help_type, topic, complexity)
except Exception as e:
logger.error(f"{self.log_prefix} LLM帮助生成异常: {e}")
return await self._generate_template_help(help_type, topic, complexity)
# ===== Command组件 =====
class ComprehensiveHelpCommand(BaseCommand):
"""综合帮助系统 - 显示所有可用命令和Action"""
command_pattern = r"^/help(?:\s+(?P<command>\w+))?$"
command_help = "显示所有命令帮助或特定命令详情,用法:/help [命令名]"
command_examples = ["/help", "/help send", "/help status"]
intercept_message = True # 拦截消息,不继续处理
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行帮助命令"""
try:
command_name = self.matched_groups.get("command")
if command_name:
# 显示特定命令帮助
return await self._show_specific_help(command_name)
else:
# 显示所有命令概览
return await self._show_all_commands()
except Exception as e:
logger.error(f"{self.log_prefix} 帮助命令执行失败: {e}")
await self.send_text(f"❌ 帮助系统错误: {str(e)}")
return False, str(e)
async def _show_specific_help(self, command_name: str) -> Tuple[bool, str]:
"""显示特定命令的详细帮助"""
# 这里可以扩展为动态获取所有注册的Command信息
help_info = {
"help": {"description": "显示帮助信息", "usage": "/help [命令名]", "examples": ["/help", "/help send"]},
"send": {
"description": "发送消息到指定目标",
"usage": "/send <group|user> <ID> <消息内容>",
"examples": ["/send group 123456 你好", "/send user 789456 私聊"],
},
"status": {
"description": "查询系统状态",
"usage": "/status [类型]",
"examples": ["/status", "/status 系统", "/status 插件"],
},
}
info = help_info.get(command_name.lower())
if not info:
response = f"❌ 未找到命令: {command_name}\n使用 /help 查看所有可用命令"
else:
response = f"""
📖 命令帮助: {command_name}
📝 描述: {info["description"]}
⚙️ 用法: {info["usage"]}
💡 示例:
{chr(10).join(f"{example}" for example in info["examples"])}
""".strip()
await self.send_text(response)
return True, response
async def _show_all_commands(self) -> Tuple[bool, str]:
"""显示所有可用命令"""
help_text = """
🤖 综合示例插件 - 命令帮助
📝 可用命令:
• /help [命令] - 显示帮助信息
• /send <目标类型> <ID> <消息> - 发送消息
• /status [类型] - 查询系统状态
• /echo <消息> - 回声重复消息
• /info - 查询当前消息信息
• /prefix <前缀> <内容> - 自定义前缀消息
🎯 智能功能:
• 智能问候 - 关键词触发自动问候
• 状态监控 - 实时系统状态查询
• 消息转发 - 跨群聊/私聊消息发送
⚙️ 拦截控制:
• 部分命令拦截消息处理(如 /help
• 部分命令允许继续处理(如 /log
💡 使用 /help <命令名> 获取特定命令的详细说明
""".strip()
await self.send_text(help_text)
return True, help_text
class MessageSendCommand(BaseCommand):
"""消息发送Command - 向指定群聊或私聊发送消息"""
command_pattern = r"^/send\s+(?P<target_type>group|user)\s+(?P<target_id>\d+)\s+(?P<content>.+)$"
command_help = "向指定群聊或私聊发送消息,用法:/send <group|user> <ID> <消息内容>"
command_examples = [
"/send group 123456789 大家好!",
"/send user 987654321 私聊消息",
"/send group 555666777 这是来自插件的消息",
]
intercept_message = True # 拦截消息处理
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行消息发送"""
try:
target_type = self.matched_groups.get("target_type")
target_id = self.matched_groups.get("target_id")
content = self.matched_groups.get("content")
if not all([target_type, target_id, content]):
await self.send_text("❌ 命令参数不完整,请检查格式")
return False, "参数不完整"
# 长度限制检查
max_length = self.api.get_config("send.max_message_length", 500)
if len(content) > max_length:
await self.send_text(f"❌ 消息过长,最大长度: {max_length} 字符")
return False, "消息过长"
logger.info(f"{self.log_prefix} 发送消息: {target_type}:{target_id} -> {content[:50]}...")
# 根据目标类型发送消息
if target_type == "group":
success = await self.api.send_text_to_group(text=content, group_id=target_id, platform="qq")
target_desc = f"群聊 {target_id}"
elif target_type == "user":
success = await self.api.send_text_to_user(text=content, user_id=target_id, platform="qq")
target_desc = f"用户 {target_id}"
else:
await self.send_text(f"❌ 不支持的目标类型: {target_type}")
return False, f"不支持的目标类型: {target_type}"
# 返回结果
if success:
response = f"✅ 消息已成功发送到 {target_desc}"
await self.send_text(response)
return True, response
else:
response = f"❌ 消息发送失败,目标 {target_desc} 可能不存在"
await self.send_text(response)
return False, response
except Exception as e:
logger.error(f"{self.log_prefix} 消息发送失败: {e}")
error_msg = f"❌ 发送失败: {str(e)}"
await self.send_text(error_msg)
return False, str(e)
class DiceCommand(BaseCommand):
"""骰子命令,使用!前缀而不是/前缀"""
command_pattern = r"^[!](?:dice|骰子)(?:\s+(?P<count>\d+))?$" # 匹配 !dice 或 !骰子,可选参数为骰子数量
command_help = "使用方法: !dice [数量] 或 !骰子 [数量] - 掷骰子默认掷1个"
command_examples = ["!dice", "!骰子", "!dice 3", "!骰子 5"]
intercept_message = True # 拦截消息处理
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行骰子命令
Returns:
Tuple[bool, Optional[str]]: (是否执行成功, 回复消息)
"""
try:
# 获取骰子数量默认为1
count_str = self.matched_groups.get("count")
# 确保count_str不为None
if count_str is None:
count = 1 # 默认值
else:
try:
count = int(count_str)
if count <= 0:
response = "❌ 骰子数量必须大于0"
await self.send_text(response)
return False, response
if count > 10: # 限制最大数量
response = "❌ 一次最多只能掷10个骰子"
await self.send_text(response)
return False, response
except ValueError:
response = "❌ 骰子数量必须是整数"
await self.send_text(response)
return False, response
# 生成随机数
results = [random.randint(1, 6) for _ in range(count)]
# 构建回复消息
if count == 1:
message = f"🎲 掷出了 {results[0]}"
else:
dice_results = ", ".join(map(str, results))
total = sum(results)
message = f"🎲 掷出了 {count} 个骰子: [{dice_results}],总点数: {total}"
await self.send_text(message)
logger.info(f"{self.log_prefix} 执行骰子命令: {message}")
return True, message
except Exception as e:
error_msg = f"❌ 执行命令时出错: {str(e)}"
await self.send_text(error_msg)
logger.error(f"{self.log_prefix} 执行骰子命令时出错: {e}")
return False, error_msg
class EchoCommand(BaseCommand):
"""回声Command - 重复用户输入的消息"""
command_pattern = r"^/echo\s+(?P<message>.+)$"
command_help = "重复你的消息内容,用法:/echo <消息内容>"
command_examples = ["/echo Hello World", "/echo 你好世界", "/echo 测试回声"]
intercept_message = True # 拦截消息处理
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行回声命令"""
try:
message = self.matched_groups.get("message", "")
if not message:
response = "❌ 请提供要重复的消息!用法:/echo <消息内容>"
await self.send_text(response)
return False, response
# 检查消息长度限制
max_length = self.api.get_config("echo.max_length", 200)
if len(message) > max_length:
response = f"❌ 消息过长,最大长度: {max_length} 字符"
await self.send_text(response)
return False, response
# 格式化回声消息
enable_formatting = self.api.get_config("echo.enable_formatting", True)
if enable_formatting:
response = f"🔊 回声: {message}"
else:
response = message
await self.send_text(response)
logger.info(f"{self.log_prefix} 回声消息: {message}")
return True, response
except Exception as e:
logger.error(f"{self.log_prefix} 回声命令失败: {e}")
error_msg = f"❌ 回声失败: {str(e)}"
await self.send_text(error_msg)
return False, str(e)
class MessageInfoCommand(BaseCommand):
"""消息信息Command - 显示当前消息的详细信息"""
command_pattern = r"^/info$"
command_help = "显示当前消息的详细信息"
command_examples = ["/info"]
intercept_message = True # 拦截消息处理
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行消息信息查询"""
try:
message = self.message
# 收集消息信息
user_info = message.message_info.user_info
group_info = message.message_info.group_info
info_parts = [
"📋 消息信息详情",
"",
"👤 用户信息:",
f" • ID: {user_info.user_id}",
f" • 昵称: {user_info.user_nickname}",
f" • 群名片: {getattr(user_info, 'user_cardname', '')}",
f" • 平台: {message.message_info.platform}",
"",
"💬 消息信息:",
f" • 消息ID: {message.message_info.message_id}",
f" • 时间戳: {message.message_info.time}",
f" • 原始内容: {message.processed_plain_text[:100]}{'...' if len(message.processed_plain_text) > 100 else ''}",
f" • 是否表情: {'' if getattr(message, 'is_emoji', False) else ''}",
]
# 群聊信息
if group_info:
info_parts.extend(
[
"",
"👥 群聊信息:",
f" • 群ID: {group_info.group_id}",
f" • 群名: {getattr(group_info, 'group_name', '未知')}",
" • 聊天类型: 群聊",
]
)
else:
info_parts.extend(["", "💭 聊天类型: 私聊"])
# 流信息
if hasattr(message, "chat_stream") and message.chat_stream:
stream = message.chat_stream
info_parts.extend(
[
"",
"🌊 聊天流信息:",
f" • 流ID: {stream.stream_id}",
f" • 创建时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(stream.create_time))}",
f" • 最后活跃: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(stream.last_active_time))}",
]
)
response = "\n".join(info_parts)
await self.send_text(response)
logger.info(f"{self.log_prefix} 显示消息信息: {user_info.user_id}")
return True, response
except Exception as e:
logger.error(f"{self.log_prefix} 消息信息查询失败: {e}")
error_msg = f"❌ 信息查询失败: {str(e)}"
await self.send_text(error_msg)
return False, str(e)
@register_plugin
class ExampleComprehensivePlugin(BasePlugin):
"""综合示例插件
整合了旧示例插件的所有功能,展示新插件系统的完整能力:
- 多种Action和Command组件
- 拦截控制功能演示
- 配置驱动的行为
- 完整的错误处理
- 日志记录和监控
"""
# 插件基本信息
plugin_name = "example_plugin"
plugin_description = "综合示例插件,展示新插件系统的完整功能"
plugin_version = "2.0.0"
plugin_author = "MaiBot开发团队"
enable_plugin = True
config_file_name = "config.toml"
# 配置节描述
config_section_descriptions = {
"plugin": "插件基本信息配置",
"components": "组件启用控制",
"greeting": "智能问候配置",
"helpful": "智能帮助Action配置",
"help": "帮助系统Command配置",
"send": "消息发送命令配置",
"echo": "回声命令配置",
"dice": "骰子命令配置",
"info": "消息信息命令配置",
"logging": "日志记录配置",
}
# 配置Schema定义
config_schema = {
"plugin": {
"name": ConfigField(type=str, default="example_plugin", description="插件名称", required=True),
"version": ConfigField(type=str, default="2.0.0", description="插件版本号"),
"enabled": ConfigField(type=bool, default=False, description="是否启用插件"),
"description": ConfigField(
type=str, default="综合示例插件,展示新插件系统的完整功能", description="插件描述", required=True
),
},
"components": {
"enable_greeting": ConfigField(type=bool, default=True, description="是否启用'智能问候'Action"),
"enable_helpful": ConfigField(type=bool, default=True, description="是否启用'智能帮助'Action"),
"enable_help": ConfigField(type=bool, default=True, description="是否启用'/help'命令"),
"enable_send": ConfigField(type=bool, default=True, description="是否启用'/send'命令"),
"enable_echo": ConfigField(type=bool, default=True, description="是否启用'/echo'命令"),
"enable_info": ConfigField(type=bool, default=True, description="是否启用'/info'命令"),
"enable_dice": ConfigField(type=bool, default=True, description="是否启用'!dice'命令"),
},
"greeting": {
"template": ConfigField(
type=str, default="你好,{username}欢迎使用MaiBot综合插件系统", description="问候消息模板"
),
"enable_emoji": ConfigField(type=bool, default=True, description="问候时是否附带表情"),
"enable_llm": ConfigField(type=bool, default=False, description="是否使用LLM生成个性化问候语"),
},
"helpful": {
"enable_llm": ConfigField(type=bool, default=False, description="是否使用LLM生成帮助内容"),
"enable_emoji": ConfigField(type=bool, default=True, description="提供帮助时是否附带表情"),
"random_activation_probability": ConfigField(
type=float, default=0.15, description="Normal模式下随机触发帮助的概率"
),
},
"help": {
"show_extended_help": ConfigField(type=bool, default=True, description="是否显示扩展帮助信息"),
"include_action_info": ConfigField(type=bool, default=True, description="帮助信息中是否包含Action的信息"),
"include_config_info": ConfigField(type=bool, default=True, description="帮助信息中是否包含配置相关信息"),
"enable_llm": ConfigField(type=bool, default=False, description="是否使用LLM生成帮助摘要"),
"enable_emoji": ConfigField(type=bool, default=True, description="帮助信息中是否使用表情符号"),
},
"send": {
"max_message_length": ConfigField(type=int, default=500, description="发送消息的最大长度限制"),
"enable_length_check": ConfigField(type=bool, default=True, description="是否启用消息长度检查"),
"default_platform": ConfigField(type=str, default="qq", description="默认发送平台"),
},
"echo": {
"max_length": ConfigField(type=int, default=200, description="回声消息的最大长度"),
"enable_formatting": ConfigField(type=bool, default=True, description="是否为回声消息添加'🔊 回声: '前缀"),
},
"dice": {
"enable_dice": ConfigField(type=bool, default=True, description="是否启用骰子功能"),
"max_dice_count": ConfigField(type=int, default=10, description="一次最多可以掷的骰子数量"),
},
"info": {
"show_detailed_info": ConfigField(type=bool, default=True, description="是否显示详细信息"),
"include_stream_info": ConfigField(type=bool, default=True, description="是否包含聊天流信息"),
"max_content_preview": ConfigField(type=int, default=100, description="消息内容预览的最大长度"),
},
"logging": {
"level": ConfigField(
type=str, default="INFO", description="日志级别", choices=["DEBUG", "INFO", "WARNING", "ERROR"]
),
"prefix": ConfigField(type=str, default="[ExampleComprehensive]", description="日志前缀"),
},
}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""返回插件包含的组件列表"""
# 从配置动态设置Action参数
helpful_chance = self.get_config("helpful.random_activation_probability", 0.15)
HelpfulAction.random_activation_probability = helpful_chance
# 从配置获取组件启用状态
enable_greeting = self.get_config("components.enable_greeting", True)
enable_helpful = self.get_config("components.enable_helpful", True)
enable_help = self.get_config("components.enable_help", True)
enable_send = self.get_config("components.enable_send", True)
enable_echo = self.get_config("components.enable_echo", True)
enable_info = self.get_config("components.enable_info", True)
enable_dice = self.get_config("components.enable_dice", True)
components = []
# 添加Action组件 - 使用类中定义的所有属性
if enable_greeting:
components.append((SmartGreetingAction.get_action_info(), SmartGreetingAction))
if enable_helpful:
components.append((HelpfulAction.get_action_info(), HelpfulAction))
# 添加Command组件
if enable_help:
components.append(
(
ComprehensiveHelpCommand.get_command_info(
name="comprehensive_help", description="综合帮助系统,显示所有命令信息"
),
ComprehensiveHelpCommand,
)
)
if enable_send:
components.append(
(
MessageSendCommand.get_command_info(
name="message_send", description="消息发送命令,支持群聊和私聊"
),
MessageSendCommand,
)
)
if enable_echo:
components.append(
(EchoCommand.get_command_info(name="echo", description="回声命令,重复用户输入"), EchoCommand)
)
if enable_info:
components.append(
(
MessageInfoCommand.get_command_info(name="message_info", description="消息信息查询,显示详细信息"),
MessageInfoCommand,
)
)
if enable_dice:
components.append((DiceCommand.get_command_info(name="dice", description="骰子命令,掷骰子"), DiceCommand))
return components

View File

@@ -1183,3 +1183,4 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -366,11 +366,9 @@ class VirtualLogDisplay:
# 应用标签(可选,为了性能可以考虑简化) # 应用标签(可选,为了性能可以考虑简化)
for tag_info in batch_tags: for tag_info in batch_tags:
try:
tag_name = tag_info[3] tag_name = tag_info[3]
self.text_widget.tag_add(tag_name, f"{start_pos}+{tag_info[1]}c", f"{start_pos}+{tag_info[2]}c") self.text_widget.tag_add(tag_name, f"{start_pos}+{tag_info[1]}c", f"{start_pos}+{tag_info[2]}c")
except:
pass
class AsyncLogLoader: class AsyncLogLoader:

View File

@@ -1,534 +0,0 @@
import traceback
from typing import List, Optional, Dict, Any, Tuple
from src.chat.focus_chat.expressors.exprssion_learner import get_expression_learner
from src.chat.message_receive.message import MessageRecv, MessageThinking, MessageSending
from src.chat.message_receive.message import Seg # Local import needed after move
from src.chat.message_receive.message import UserInfo
from src.chat.message_receive.chat_stream import get_chat_manager
from src.common.logger import get_logger
from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config
from src.chat.utils.utils_image import image_path_to_base64 # Local import needed after move
from src.chat.utils.timer_calculator import Timer # <--- Import Timer
from src.chat.emoji_system.emoji_manager import get_emoji_manager
from src.chat.focus_chat.heartFC_sender import HeartFCSender
from src.chat.utils.utils import process_llm_response
from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info
from src.chat.message_receive.chat_stream import ChatStream
from src.chat.focus_chat.hfc_utils import parse_thinking_id_to_timestamp
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
import time
import random
logger = get_logger("expressor")
def init_prompt():
Prompt(
"""
你可以参考你的以下的语言习惯,如果情景合适就使用,不要盲目使用,不要生硬使用,而是结合到表达中:
{style_habbits}
你现在正在群里聊天,以下是群里正在进行的聊天内容:
{chat_info}
以上是聊天内容,你需要了解聊天记录中的内容
{chat_target}
你的名字是{bot_name}{prompt_personality},在这聊天中,"{target_message}"引起了你的注意,对这句话,你想表达:{in_mind_reply},原因是:{reason}。你现在要思考怎么回复
你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。请你修改你想表达的原句,符合你的表达风格和语言习惯
请你根据情景使用以下句法:
{grammar_habbits}
{config_expression_style},你可以完全重组回复,保留最基本的表达含义就好,但重组后保持语意通顺。
不要浮夸,不要夸张修辞,平淡且不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 ),只输出一条回复就好。
现在,你说:
""",
"default_expressor_prompt",
)
Prompt(
"""
你可以参考以下的语言习惯,如果情景合适就使用,不要盲目使用,不要生硬使用,而是结合到表达中:
{style_habbits}
你现在正在群里聊天,以下是群里正在进行的聊天内容:
{chat_info}
以上是聊天内容,你需要了解聊天记录中的内容
{chat_target}
你的名字是{bot_name}{prompt_personality},在这聊天中,"{target_message}"引起了你的注意,对这句话,你想表达:{in_mind_reply},原因是:{reason}。你现在要思考怎么回复
你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。
请你根据情景使用以下句法:
{grammar_habbits}
{config_expression_style},你可以完全重组回复,保留最基本的表达含义就好,但重组后保持语意通顺。
不要浮夸,不要夸张修辞,平淡且不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 ),只输出一条回复就好。
现在,你说:
""",
"default_expressor_private_prompt", # New template for private FOCUSED chat
)
class DefaultExpressor:
def __init__(self, chat_stream: ChatStream):
self.log_prefix = "expressor"
# TODO: API-Adapter修改标记
self.express_model = LLMRequest(
model=global_config.model.replyer_1,
request_type="focus.expressor",
)
self.heart_fc_sender = HeartFCSender()
self.chat_id = chat_stream.stream_id
self.chat_stream = chat_stream
self.is_group_chat, self.chat_target_info = get_chat_type_and_target_info(self.chat_id)
async def _create_thinking_message(self, anchor_message: Optional[MessageRecv], thinking_id: str):
"""创建思考消息 (尝试锚定到 anchor_message)"""
if not anchor_message or not anchor_message.chat_stream:
logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流。")
return None
chat = anchor_message.chat_stream
messageinfo = anchor_message.message_info
thinking_time_point = parse_thinking_id_to_timestamp(thinking_id)
bot_user_info = UserInfo(
user_id=global_config.bot.qq_account,
user_nickname=global_config.bot.nickname,
platform=messageinfo.platform,
)
thinking_message = MessageThinking(
message_id=thinking_id,
chat_stream=chat,
bot_user_info=bot_user_info,
reply=anchor_message, # 回复的是锚点消息
thinking_start_time=thinking_time_point,
)
# logger.debug(f"创建思考消息thinking_message{thinking_message}")
await self.heart_fc_sender.register_thinking(thinking_message)
return None
async def deal_reply(
self,
cycle_timers: dict,
action_data: Dict[str, Any],
reasoning: str,
anchor_message: MessageRecv,
thinking_id: str,
) -> tuple[bool, Optional[List[Tuple[str, str]]]]:
# 创建思考消息
await self._create_thinking_message(anchor_message, thinking_id)
reply = [] # 初始化 reply防止未定义
try:
has_sent_something = False
# 处理文本部分
text_part = action_data.get("text", [])
if text_part:
with Timer("生成回复", cycle_timers):
# 可以保留原有的文本处理逻辑或进行适当调整
reply = await self.express(
in_mind_reply=text_part,
anchor_message=anchor_message,
thinking_id=thinking_id,
reason=reasoning,
action_data=action_data,
)
with Timer("选择表情", cycle_timers):
emoji_keyword = action_data.get("emojis", [])
emoji_base64 = await self._choose_emoji(emoji_keyword)
if emoji_base64:
reply.append(("emoji", emoji_base64))
if reply:
with Timer("发送消息", cycle_timers):
sent_msg_list = await self.send_response_messages(
anchor_message=anchor_message,
thinking_id=thinking_id,
response_set=reply,
)
has_sent_something = True
else:
logger.warning(f"{self.log_prefix} 文本回复生成失败")
if not has_sent_something:
logger.warning(f"{self.log_prefix} 回复动作未包含任何有效内容")
return has_sent_something, sent_msg_list
except Exception as e:
logger.error(f"回复失败: {e}")
traceback.print_exc()
return False, None
# --- 回复器 (Replier) 的定义 --- #
async def express(
self,
in_mind_reply: str,
reason: str,
anchor_message: MessageRecv,
thinking_id: str,
action_data: Dict[str, Any],
) -> Optional[List[str]]:
"""
回复器 (Replier): 核心逻辑,负责生成回复文本。
(已整合原 HeartFCGenerator 的功能)
"""
try:
# --- Determine sender_name for private chat ---
sender_name_for_prompt = "某人" # Default for group or if info unavailable
if not self.is_group_chat and self.chat_target_info:
# Prioritize person_name, then nickname
sender_name_for_prompt = (
self.chat_target_info.get("person_name")
or self.chat_target_info.get("user_nickname")
or sender_name_for_prompt
)
# --- End determining sender_name ---
target_message = action_data.get("target", "")
# 3. 构建 Prompt
with Timer("构建Prompt", {}): # 内部计时器,可选保留
prompt = await self.build_prompt_focus(
chat_stream=self.chat_stream, # Pass the stream object
in_mind_reply=in_mind_reply,
reason=reason,
sender_name=sender_name_for_prompt, # Pass determined name
target_message=target_message,
config_expression_style=global_config.expression.expression_style,
)
# 4. 调用 LLM 生成回复
content = None
reasoning_content = None
model_name = "unknown_model"
if not prompt:
logger.error(f"{self.log_prefix}[Replier-{thinking_id}] Prompt 构建失败,无法生成回复。")
return None
try:
with Timer("LLM生成", {}): # 内部计时器,可选保留
# TODO: API-Adapter修改标记
# logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\nPrompt:\n{prompt}\n")
content, (reasoning_content, model_name) = await self.express_model.generate_response_async(prompt)
logger.info(f"想要表达:{in_mind_reply}||理由:{reason}")
logger.info(f"最终回复: {content}\n")
except Exception as llm_e:
# 精简报错信息
logger.error(f"{self.log_prefix}LLM 生成失败: {llm_e}")
return None # LLM 调用失败则无法生成回复
processed_response = process_llm_response(content)
# 5. 处理 LLM 响应
if not content:
logger.warning(f"{self.log_prefix}LLM 生成了空内容。")
return None
if not processed_response:
logger.warning(f"{self.log_prefix}处理后的回复为空。")
return None
reply_set = []
for str in processed_response:
reply_seg = ("text", str)
reply_set.append(reply_seg)
return reply_set
except Exception as e:
logger.error(f"{self.log_prefix}回复生成意外失败: {e}")
traceback.print_exc()
return None
async def build_prompt_focus(
self,
reason,
chat_stream,
sender_name,
in_mind_reply,
target_message,
config_expression_style,
) -> str:
is_group_chat = bool(chat_stream.group_info)
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_stream.stream_id,
timestamp=time.time(),
limit=global_config.focus_chat.observation_context_size,
)
chat_talking_prompt = build_readable_messages(
message_list_before_now,
replace_bot_name=True,
merge_messages=True,
timestamp_mode="relative",
read_mark=0.0,
truncate=True,
)
expression_learner = get_expression_learner()
(
learnt_style_expressions,
learnt_grammar_expressions,
personality_expressions,
) = await expression_learner.get_expression_by_chat_id(chat_stream.stream_id)
style_habbits = []
grammar_habbits = []
# 1. learnt_expressions加权随机选3条
if learnt_style_expressions:
weights = [expr["count"] for expr in learnt_style_expressions]
selected_learnt = weighted_sample_no_replacement(learnt_style_expressions, weights, 3)
for expr in selected_learnt:
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
style_habbits.append(f"{expr['situation']}时,使用 {expr['style']}")
# 2. learnt_grammar_expressions加权随机选3条
if learnt_grammar_expressions:
weights = [expr["count"] for expr in learnt_grammar_expressions]
selected_learnt = weighted_sample_no_replacement(learnt_grammar_expressions, weights, 3)
for expr in selected_learnt:
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
grammar_habbits.append(f"{expr['situation']}时,使用 {expr['style']}")
# 3. personality_expressions随机选1条
if personality_expressions:
expr = random.choice(personality_expressions)
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
style_habbits.append(f"{expr['situation']}时,使用 {expr['style']}")
style_habbits_str = "\n".join(style_habbits)
grammar_habbits_str = "\n".join(grammar_habbits)
logger.debug("开始构建 focus prompt")
# --- Choose template based on chat type ---
if is_group_chat:
template_name = "default_expressor_prompt"
# Group specific formatting variables (already fetched or default)
chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1")
# chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2")
prompt = await global_prompt_manager.format_prompt(
template_name,
style_habbits=style_habbits_str,
grammar_habbits=grammar_habbits_str,
chat_target=chat_target_1,
chat_info=chat_talking_prompt,
bot_name=global_config.bot.nickname,
prompt_personality="",
reason=reason,
in_mind_reply=in_mind_reply,
target_message=target_message,
config_expression_style=config_expression_style,
)
else: # Private chat
template_name = "default_expressor_private_prompt"
chat_target_1 = "你正在和人私聊"
prompt = await global_prompt_manager.format_prompt(
template_name,
style_habbits=style_habbits_str,
grammar_habbits=grammar_habbits_str,
chat_target=chat_target_1,
chat_info=chat_talking_prompt,
bot_name=global_config.bot.nickname,
prompt_personality="",
reason=reason,
in_mind_reply=in_mind_reply,
target_message=target_message,
config_expression_style=config_expression_style,
)
return prompt
# --- 发送器 (Sender) --- #
async def send_response_messages(
self,
anchor_message: Optional[MessageRecv],
response_set: List[Tuple[str, str]],
thinking_id: str = "",
display_message: str = "",
) -> Optional[MessageSending]:
"""发送回复消息 (尝试锚定到 anchor_message),使用 HeartFCSender"""
chat = self.chat_stream
chat_id = self.chat_id
if chat is None:
logger.error(f"{self.log_prefix} 无法发送回复chat_stream 为空。")
return None
if not anchor_message:
logger.error(f"{self.log_prefix} 无法发送回复anchor_message 为空。")
return None
stream_name = get_chat_manager().get_stream_name(chat_id) or chat_id # 获取流名称用于日志
# 检查思考过程是否仍在进行,并获取开始时间
if thinking_id:
thinking_start_time = await self.heart_fc_sender.get_thinking_start_time(chat_id, thinking_id)
else:
thinking_id = "ds" + str(round(time.time(), 2))
thinking_start_time = time.time()
if thinking_start_time is None:
logger.error(f"[{stream_name}]expressor思考过程未找到或已结束无法发送回复。")
return None
mark_head = False
# first_bot_msg: Optional[MessageSending] = None
reply_message_ids = [] # 记录实际发送的消息ID
sent_msg_list = []
for i, msg_text in enumerate(response_set):
# 为每个消息片段生成唯一ID
type = msg_text[0]
data = msg_text[1]
if global_config.experimental.debug_show_chat_mode and type == "text":
data += ""
part_message_id = f"{thinking_id}_{i}"
message_segment = Seg(type=type, data=data)
if type == "emoji":
is_emoji = True
else:
is_emoji = False
reply_to = not mark_head
bot_message = await self._build_single_sending_message(
anchor_message=anchor_message,
message_id=part_message_id,
message_segment=message_segment,
display_message=display_message,
reply_to=reply_to,
is_emoji=is_emoji,
thinking_id=thinking_id,
thinking_start_time=thinking_start_time,
)
try:
if not mark_head:
mark_head = True
# first_bot_msg = bot_message # 保存第一个成功发送的消息对象
typing = False
else:
typing = True
if type == "emoji":
typing = False
if anchor_message.raw_message:
set_reply = True
else:
set_reply = False
sent_msg = await self.heart_fc_sender.send_message(
bot_message, has_thinking=True, typing=typing, set_reply=set_reply
)
reply_message_ids.append(part_message_id) # 记录我们生成的ID
sent_msg_list.append((type, sent_msg))
except Exception as e:
logger.error(f"{self.log_prefix}发送回复片段 {i} ({part_message_id}) 时失败: {e}")
traceback.print_exc()
# 这里可以选择是继续发送下一个片段还是中止
# 在尝试发送完所有片段后,完成原始的 thinking_id 状态
try:
await self.heart_fc_sender.complete_thinking(chat_id, thinking_id)
except Exception as e:
logger.error(f"{self.log_prefix}完成思考状态 {thinking_id} 时出错: {e}")
return sent_msg_list
async def _choose_emoji(self, send_emoji: str):
"""
选择表情根据send_emoji文本选择表情返回表情base64
"""
emoji_base64 = ""
emoji_raw = await get_emoji_manager().get_emoji_for_text(send_emoji)
if emoji_raw:
emoji_path, _description, _emotion = emoji_raw
emoji_base64 = image_path_to_base64(emoji_path)
return emoji_base64
async def _build_single_sending_message(
self,
anchor_message: MessageRecv,
message_id: str,
message_segment: Seg,
reply_to: bool,
is_emoji: bool,
thinking_id: str,
thinking_start_time: float,
display_message: str,
) -> MessageSending:
"""构建单个发送消息"""
bot_user_info = UserInfo(
user_id=global_config.bot.qq_account,
user_nickname=global_config.bot.nickname,
platform=self.chat_stream.platform,
)
bot_message = MessageSending(
message_id=message_id, # 使用片段的唯一ID
chat_stream=self.chat_stream,
bot_user_info=bot_user_info,
sender_info=anchor_message.message_info.user_info,
message_segment=message_segment,
reply=anchor_message, # 回复原始锚点
is_head=reply_to,
is_emoji=is_emoji,
thinking_start_time=thinking_start_time, # 传递原始思考开始时间
display_message=display_message,
)
return bot_message
def weighted_sample_no_replacement(items, weights, k) -> list:
"""
加权且不放回地随机抽取k个元素。
参数:
items: 待抽取的元素列表
weights: 每个元素对应的权重与items等长且为正数
k: 需要抽取的元素个数
返回:
selected: 按权重加权且不重复抽取的k个元素组成的列表
如果items中的元素不足k就只会返回所有可用的元素
实现思路:
每次从当前池中按权重加权随机选出一个元素选中后将其从池中移除重复k次。
这样保证了:
1. count越大被选中概率越高
2. 不会重复选中同一个元素
"""
selected = []
pool = list(zip(items, weights))
for _ in range(min(k, len(pool))):
total = sum(w for _, w in pool)
r = random.uniform(0, total)
upto = 0
for idx, (item, weight) in enumerate(pool):
upto += weight
if upto >= r:
selected.append(item)
pool.pop(idx)
break
return selected
init_prompt()

View File

@@ -21,8 +21,6 @@ from src.chat.heart_flow.observation.chatting_observation import ChattingObserva
from src.chat.heart_flow.observation.structure_observation import StructureObservation from src.chat.heart_flow.observation.structure_observation import StructureObservation
from src.chat.heart_flow.observation.actions_observation import ActionObservation from src.chat.heart_flow.observation.actions_observation import ActionObservation
from src.chat.focus_chat.info_processors.tool_processor import ToolProcessor from src.chat.focus_chat.info_processors.tool_processor import ToolProcessor
from src.chat.focus_chat.expressors.default_expressor import DefaultExpressor
from src.chat.focus_chat.replyer.default_replyer import DefaultReplyer
from src.chat.focus_chat.memory_activator import MemoryActivator from src.chat.focus_chat.memory_activator import MemoryActivator
from src.chat.focus_chat.info_processors.base_processor import BaseProcessor from src.chat.focus_chat.info_processors.base_processor import BaseProcessor
from src.chat.focus_chat.info_processors.self_processor import SelfProcessor from src.chat.focus_chat.info_processors.self_processor import SelfProcessor
@@ -125,9 +123,6 @@ class HeartFChatting:
self.processors: List[BaseProcessor] = [] self.processors: List[BaseProcessor] = []
self._register_default_processors() self._register_default_processors()
self.expressor = DefaultExpressor(chat_stream=self.chat_stream)
self.replyer = DefaultReplyer(chat_stream=self.chat_stream)
self.action_manager = ActionManager() self.action_manager = ActionManager()
self.action_planner = PlannerFactory.create_planner( self.action_planner = PlannerFactory.create_planner(
log_prefix=self.log_prefix, action_manager=self.action_manager log_prefix=self.log_prefix, action_manager=self.action_manager
@@ -543,6 +538,7 @@ class HeartFChatting:
async def _observe_process_plan_action_loop(self, cycle_timers: dict, thinking_id: str) -> dict: async def _observe_process_plan_action_loop(self, cycle_timers: dict, thinking_id: str) -> dict:
try: try:
loop_start_time = time.time()
with Timer("观察", cycle_timers): with Timer("观察", cycle_timers):
# 执行所有观察器的观察 # 执行所有观察器的观察
for observation in self.observations: for observation in self.observations:
@@ -583,7 +579,7 @@ class HeartFChatting:
} }
with Timer("规划器", cycle_timers): with Timer("规划器", cycle_timers):
plan_result = await self.action_planner.plan(all_plan_info, running_memorys) plan_result = await self.action_planner.plan(all_plan_info, running_memorys, loop_start_time)
loop_plan_info = { loop_plan_info = {
"action_result": plan_result.get("action_result", {}), "action_result": plan_result.get("action_result", {}),
@@ -607,7 +603,7 @@ class HeartFChatting:
logger.debug(f"{self.log_prefix} 麦麦想要:'{action_str}'") logger.debug(f"{self.log_prefix} 麦麦想要:'{action_str}'")
success, reply_text, command = await self._handle_action( success, reply_text, command = await self._handle_action(
action_type, reasoning, action_data, cycle_timers, thinking_id, self.observations action_type, reasoning, action_data, cycle_timers, thinking_id
) )
loop_action_info = { loop_action_info = {
@@ -646,7 +642,6 @@ class HeartFChatting:
action_data: dict, action_data: dict,
cycle_timers: dict, cycle_timers: dict,
thinking_id: str, thinking_id: str,
observations: List[Observation],
) -> tuple[bool, str, str]: ) -> tuple[bool, str, str]:
""" """
处理规划动作,使用动作工厂创建相应的动作处理器 处理规划动作,使用动作工厂创建相应的动作处理器
@@ -670,9 +665,6 @@ class HeartFChatting:
reasoning=reasoning, reasoning=reasoning,
cycle_timers=cycle_timers, cycle_timers=cycle_timers,
thinking_id=thinking_id, thinking_id=thinking_id,
observations=observations,
expressor=self.expressor,
replyer=self.replyer,
chat_stream=self.chat_stream, chat_stream=self.chat_stream,
log_prefix=self.log_prefix, log_prefix=self.log_prefix,
shutting_down=self._shutting_down, shutting_down=self._shutting_down,

View File

@@ -15,7 +15,7 @@ install(extra_lines=3)
logger = get_logger("sender") logger = get_logger("sender")
async def send_message(message: MessageSending) -> str: async def send_message(message: MessageSending) -> bool:
"""合并后的消息发送函数包含WS发送和日志记录""" """合并后的消息发送函数包含WS发送和日志记录"""
message_preview = truncate_message(message.processed_plain_text, max_length=40) message_preview = truncate_message(message.processed_plain_text, max_length=40)
@@ -23,7 +23,7 @@ async def send_message(message: MessageSending) -> str:
# 直接调用API发送消息 # 直接调用API发送消息
await get_global_api().send_message(message) await get_global_api().send_message(message)
logger.info(f"已将消息 '{message_preview}' 发往平台'{message.message_info.platform}'") logger.info(f"已将消息 '{message_preview}' 发往平台'{message.message_info.platform}'")
return message.processed_plain_text return True
except Exception as e: except Exception as e:
logger.error(f"发送消息 '{message_preview}' 发往平台'{message.message_info.platform}' 失败: {str(e)}") logger.error(f"发送消息 '{message_preview}' 发往平台'{message.message_info.platform}' 失败: {str(e)}")
@@ -73,17 +73,15 @@ class HeartFCSender:
thinking_message = self.thinking_messages.get(chat_id, {}).get(message_id) thinking_message = self.thinking_messages.get(chat_id, {}).get(message_id)
return thinking_message.thinking_start_time if thinking_message else None return thinking_message.thinking_start_time if thinking_message else None
async def send_message(self, message: MessageSending, has_thinking=False, typing=False, set_reply=False): async def send_message(self, message: MessageSending, typing=False, set_reply=False, storage_message=True):
""" """
处理、发送并存储一条消息。 处理、发送并存储一条消息。
参数: 参数:
message: MessageSending 对象,待发送的消息。 message: MessageSending 对象,待发送的消息。
has_thinking: 是否管理思考状态,表情包无思考状态(如需调用 register_thinking/complete_thinking typing: 是否模拟打字等待
typing: 是否模拟打字等待(根据 has_thinking 控制等待时长)。
用法: 用法:
- has_thinking=True 时,自动处理思考消息的时间和清理。
- typing=True 时,发送前会有打字等待。 - typing=True 时,发送前会有打字等待。
""" """
if not message.chat_stream: if not message.chat_stream:
@@ -98,40 +96,29 @@ class HeartFCSender:
try: try:
if set_reply: if set_reply:
_ = message.update_thinking_time() message.build_reply()
logger.debug(f"[{chat_id}] 选择回复引用消息: {message.processed_plain_text[:20]}...")
# --- 条件应用 set_reply 逻辑 ---
if (
message.is_head
and not message.is_private_message()
and message.reply.processed_plain_text != "[System Trigger Context]"
):
# message.set_reply(message.reply)
message.set_reply()
logger.debug(f"[{chat_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}...")
# print(f"message.display_message: {message.display_message}")
await message.process() await message.process()
# print(f"message.display_message: {message.display_message}")
if typing: if typing:
if has_thinking:
typing_time = calculate_typing_time( typing_time = calculate_typing_time(
input_string=message.processed_plain_text, input_string=message.processed_plain_text,
thinking_start_time=message.thinking_start_time, thinking_start_time=message.thinking_start_time,
is_emoji=message.is_emoji, is_emoji=message.is_emoji,
) )
await asyncio.sleep(typing_time) await asyncio.sleep(typing_time)
else:
await asyncio.sleep(0.5)
sent_msg = await send_message(message) sent_msg = await send_message(message)
if not sent_msg:
return False
if storage_message:
await self.storage.store_message(message, message.chat_stream) await self.storage.store_message(message, message.chat_stream)
if sent_msg:
return sent_msg return sent_msg
else:
return "发送失败"
except Exception as e: except Exception as e:
logger.error(f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}") logger.error(f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}")

View File

@@ -172,6 +172,7 @@ class HeartFCMessageReceiver:
return return
# 5. 消息存储 # 5. 消息存储
print(f"message: {message.message_info.time}")
await self.storage.store_message(message, chat) await self.storage.store_message(message, chat)
# 6. 兴趣度计算与更新 # 6. 兴趣度计算与更新

View File

@@ -11,7 +11,7 @@ from src.chat.message_receive.chat_stream import get_chat_manager
from .base_processor import BaseProcessor from .base_processor import BaseProcessor
from src.chat.focus_chat.info.info_base import InfoBase from src.chat.focus_chat.info.info_base import InfoBase
from src.chat.focus_chat.info.expression_selection_info import ExpressionSelectionInfo from src.chat.focus_chat.info.expression_selection_info import ExpressionSelectionInfo
from src.chat.focus_chat.expressors.exprssion_learner import get_expression_learner from src.chat.express.exprssion_learner import get_expression_learner
from json_repair import repair_json from json_repair import repair_json
import json import json

View File

@@ -1,13 +1,9 @@
from typing import Dict, List, Optional, Type, Any from typing import Dict, List, Optional, Type, Any
from src.plugin_system.base.base_action import BaseAction from src.plugin_system.base.base_action import BaseAction
from src.chat.heart_flow.observation.observation import Observation
from src.chat.focus_chat.replyer.default_replyer import DefaultReplyer
from src.chat.focus_chat.expressors.default_expressor import DefaultExpressor
from src.chat.message_receive.chat_stream import ChatStream from src.chat.message_receive.chat_stream import ChatStream
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system.core.component_registry import component_registry
# 不再需要导入动作类因为已经在main.py中导入 from src.plugin_system.base.component_types import ComponentType
# import src.chat.actions.default_actions # noqa
logger = get_logger("action_manager") logger = get_logger("action_manager")
@@ -15,87 +11,11 @@ logger = get_logger("action_manager")
ActionInfo = Dict[str, Any] ActionInfo = Dict[str, Any]
class PluginActionWrapper(BaseAction):
"""
新插件系统Action组件的兼容性包装器
将新插件系统的Action组件包装为旧系统兼容的BaseAction接口
"""
def __init__(
self, plugin_action, action_name: str, action_data: dict, reasoning: str, cycle_timers: dict, thinking_id: str
):
"""初始化包装器"""
# 调用旧系统BaseAction初始化只传递它能接受的参数
super().__init__(
action_data=action_data, reasoning=reasoning, cycle_timers=cycle_timers, thinking_id=thinking_id
)
# 存储插件Action实例它已经包含了所有必要的服务对象
self.plugin_action = plugin_action
self.action_name = action_name
# 从插件Action实例复制属性到包装器
self._sync_attributes_from_plugin_action()
def _sync_attributes_from_plugin_action(self):
"""从插件Action实例同步属性到包装器"""
# 基本属性
self.action_name = getattr(self.plugin_action, "action_name", self.action_name)
# 设置兼容的默认值
self.action_description = f"插件Action: {self.action_name}"
self.action_parameters = {}
self.action_require = []
# 激活类型属性(从新插件系统转换)
plugin_focus_type = getattr(self.plugin_action, "focus_activation_type", None)
plugin_normal_type = getattr(self.plugin_action, "normal_activation_type", None)
if plugin_focus_type:
self.focus_activation_type = (
plugin_focus_type.value if hasattr(plugin_focus_type, "value") else str(plugin_focus_type)
)
if plugin_normal_type:
self.normal_activation_type = (
plugin_normal_type.value if hasattr(plugin_normal_type, "value") else str(plugin_normal_type)
)
# 其他属性
self.random_activation_probability = getattr(self.plugin_action, "random_activation_probability", 0.0)
self.llm_judge_prompt = getattr(self.plugin_action, "llm_judge_prompt", "")
self.activation_keywords = getattr(self.plugin_action, "activation_keywords", [])
self.keyword_case_sensitive = getattr(self.plugin_action, "keyword_case_sensitive", False)
# 模式和并行设置
plugin_mode = getattr(self.plugin_action, "mode_enable", None)
if plugin_mode:
self.mode_enable = plugin_mode.value if hasattr(plugin_mode, "value") else str(plugin_mode)
self.parallel_action = getattr(self.plugin_action, "parallel_action", True)
self.enable_plugin = True
async def execute(self) -> tuple[bool, str]:
"""实现抽象方法execute委托给插件Action的execute方法"""
try:
# 调用插件Action的execute方法
success, response = await self.plugin_action.execute()
logger.debug(f"插件Action {self.action_name} 执行{'成功' if success else '失败'}: {response}")
return success, response
except Exception as e:
logger.error(f"插件Action {self.action_name} 执行异常: {e}")
return False, f"插件Action执行失败: {str(e)}"
async def handle_action(self) -> tuple[bool, str]:
"""兼容旧系统的动作处理接口委托给execute方法"""
return await self.execute()
class ActionManager: class ActionManager:
""" """
动作管理器,用于管理各种类型的动作 动作管理器,用于管理各种类型的动作
现在统一使用新插件系统,简化了原有的新旧兼容逻辑。
""" """
# 类常量 # 类常量
@@ -119,23 +39,20 @@ class ActionManager:
# 初始化时将默认动作加载到使用中的动作 # 初始化时将默认动作加载到使用中的动作
self._using_actions = self._default_actions.copy() self._using_actions = self._default_actions.copy()
# 添加系统核心动作
# self._add_system_core_actions()
def _load_plugin_actions(self) -> None: def _load_plugin_actions(self) -> None:
""" """
加载所有插件目录中的动作 加载所有插件系统中的动作
""" """
try: try:
# 从新插件系统获取Action组件 # 从新插件系统获取Action组件
self._load_plugin_system_actions() self._load_plugin_system_actions()
logger.debug("插件系统加载Action组件成功") logger.debug("从插件系统加载Action组件成功")
except Exception as e: except Exception as e:
logger.error(f"加载插件动作失败: {e}") logger.error(f"加载插件动作失败: {e}")
def _load_plugin_system_actions(self) -> None: def _load_plugin_system_actions(self) -> None:
"""插件系统的component_registry加载Action组件""" """从插件系统的component_registry加载Action组件"""
try: try:
from src.plugin_system.core.component_registry import component_registry from src.plugin_system.core.component_registry import component_registry
from src.plugin_system.base.component_types import ComponentType from src.plugin_system.base.component_types import ComponentType
@@ -148,7 +65,7 @@ class ActionManager:
logger.debug(f"Action组件 {action_name} 已存在,跳过") logger.debug(f"Action组件 {action_name} 已存在,跳过")
continue continue
# 将插件系统的ActionInfo转换为旧系统格式 # 将插件系统的ActionInfo转换为ActionManager格式
converted_action_info = { converted_action_info = {
"description": action_info.description, "description": action_info.description,
"parameters": getattr(action_info, "action_parameters", {}), "parameters": getattr(action_info, "action_parameters", {}),
@@ -165,8 +82,7 @@ class ActionManager:
# 模式和并行设置 # 模式和并行设置
"mode_enable": action_info.mode_enable.value, "mode_enable": action_info.mode_enable.value,
"parallel_action": action_info.parallel_action, "parallel_action": action_info.parallel_action,
# 标记这是来自新插件系统的组件 # 插件信息
"_plugin_system_component": True,
"_plugin_name": getattr(action_info, "plugin_name", ""), "_plugin_name": getattr(action_info, "plugin_name", ""),
} }
@@ -180,7 +96,7 @@ class ActionManager:
f"从插件系统加载Action组件: {action_name} (插件: {getattr(action_info, 'plugin_name', 'unknown')})" f"从插件系统加载Action组件: {action_name} (插件: {getattr(action_info, 'plugin_name', 'unknown')})"
) )
logger.info(f"插件系统加载了 {len(action_components)} 个Action组件") logger.info(f"从插件系统加载了 {len(action_components)} 个Action组件")
except Exception as e: except Exception as e:
logger.error(f"从插件系统加载Action组件失败: {e}") logger.error(f"从插件系统加载Action组件失败: {e}")
@@ -195,12 +111,9 @@ class ActionManager:
reasoning: str, reasoning: str,
cycle_timers: dict, cycle_timers: dict,
thinking_id: str, thinking_id: str,
observations: List[Observation],
chat_stream: ChatStream, chat_stream: ChatStream,
log_prefix: str, log_prefix: str,
shutting_down: bool = False, shutting_down: bool = False,
expressor: DefaultExpressor = None,
replyer: DefaultReplyer = None,
) -> Optional[BaseAction]: ) -> Optional[BaseAction]:
""" """
创建动作处理器实例 创建动作处理器实例
@@ -211,9 +124,6 @@ class ActionManager:
reasoning: 执行理由 reasoning: 执行理由
cycle_timers: 计时器字典 cycle_timers: 计时器字典
thinking_id: 思考ID thinking_id: 思考ID
observations: 观察列表
expressor: 表达器
replyer: 回复器
chat_stream: 聊天流 chat_stream: 聊天流
log_prefix: 日志前缀 log_prefix: 日志前缀
shutting_down: 是否正在关闭 shutting_down: 是否正在关闭
@@ -221,122 +131,39 @@ class ActionManager:
Returns: Returns:
Optional[BaseAction]: 创建的动作处理器实例如果动作名称未注册则返回None Optional[BaseAction]: 创建的动作处理器实例如果动作名称未注册则返回None
""" """
# 检查动作是否在当前使用的动作集中
# if action_name not in self._using_actions:
# logger.warning(f"当前不可用的动作类型: {action_name}")
# return None
# 检查是否是新插件系统的Action组件
action_info = self._registered_actions.get(action_name)
if action_info and action_info.get("_plugin_system_component", False):
return self._create_plugin_system_action(
action_name,
action_data,
reasoning,
cycle_timers,
thinking_id,
observations,
chat_stream,
log_prefix,
shutting_down,
expressor,
replyer,
)
# 旧系统的动作创建逻辑
from src.plugin_system.core.component_registry import component_registry
action_registry = component_registry.get_action_registry()
handler_class = action_registry.get(action_name)
if not handler_class:
logger.warning(f"未注册的动作类型: {action_name}")
return None
try: try:
# 创建动作实例 # 获取组件类 - 明确指定查询Action类型
instance = handler_class( component_class = component_registry.get_component_class(action_name, ComponentType.ACTION)
action_data=action_data,
reasoning=reasoning,
cycle_timers=cycle_timers,
thinking_id=thinking_id,
observations=observations,
expressor=expressor,
replyer=replyer,
chat_stream=chat_stream,
log_prefix=log_prefix,
shutting_down=shutting_down,
)
return instance
except Exception as e:
logger.error(f"创建动作处理器实例失败: {e}")
return None
def _create_plugin_system_action(
self,
action_name: str,
action_data: dict,
reasoning: str,
cycle_timers: dict,
thinking_id: str,
observations: List[Observation],
chat_stream: ChatStream,
log_prefix: str,
shutting_down: bool = False,
expressor: DefaultExpressor = None,
replyer: DefaultReplyer = None,
) -> Optional["PluginActionWrapper"]:
"""
创建新插件系统的Action组件实例并包装为兼容旧系统的接口
Returns:
Optional[PluginActionWrapper]: 包装后的Action实例
"""
try:
from src.plugin_system.core.component_registry import component_registry
# 获取组件类
component_class = component_registry.get_component_class(action_name)
if not component_class: if not component_class:
logger.error(f"未找到插件Action组件: {action_name}") logger.warning(f"{log_prefix} 未找到Action组件: {action_name}")
return None
# 获取组件信息
component_info = component_registry.get_component_info(action_name, ComponentType.ACTION)
if not component_info:
logger.warning(f"{log_prefix} 未找到Action组件信息: {action_name}")
return None return None
# 获取插件配置 # 获取插件配置
component_info = component_registry.get_component_info(action_name)
plugin_config = None
if component_info and component_info.plugin_name:
plugin_config = component_registry.get_plugin_config(component_info.plugin_name) plugin_config = component_registry.get_plugin_config(component_info.plugin_name)
# 创建插件Action实例 # 创建动作实例
plugin_action_instance = component_class( instance = component_class(
action_data=action_data, action_data=action_data,
reasoning=reasoning, reasoning=reasoning,
cycle_timers=cycle_timers, cycle_timers=cycle_timers,
thinking_id=thinking_id, thinking_id=thinking_id,
chat_stream=chat_stream, chat_stream=chat_stream,
expressor=expressor,
replyer=replyer,
observations=observations,
log_prefix=log_prefix, log_prefix=log_prefix,
shutting_down=shutting_down,
plugin_config=plugin_config, plugin_config=plugin_config,
) )
# 创建兼容性包装器 logger.debug(f"创建Action实例成功: {action_name}")
wrapper = PluginActionWrapper( return instance
plugin_action=plugin_action_instance,
action_name=action_name,
action_data=action_data,
reasoning=reasoning,
cycle_timers=cycle_timers,
thinking_id=thinking_id,
)
logger.debug(f"创建插件Action实例成功: {action_name}")
return wrapper
except Exception as e: except Exception as e:
logger.error(f"创建插件Action实例失败 {action_name}: {e}") logger.error(f"创建Action实例失败 {action_name}: {e}")
import traceback import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
@@ -366,19 +193,13 @@ class ActionManager:
""" """
filtered_actions = {} filtered_actions = {}
# print(self._using_actions)
for action_name, action_info in self._using_actions.items(): for action_name, action_info in self._using_actions.items():
# print(f"action_info: {action_info}")
# print(f"action_name: {action_name}")
action_mode = action_info.get("mode_enable", "all") action_mode = action_info.get("mode_enable", "all")
# 检查动作是否在当前模式下启用 # 检查动作是否在当前模式下启用
if action_mode == "all" or action_mode == mode: if action_mode == "all" or action_mode == mode:
filtered_actions[action_name] = action_info filtered_actions[action_name] = action_info
logger.debug(f"动作 {action_name} 在模式 {mode} 下可用 (mode_enable: {action_mode})") logger.debug(f"动作 {action_name} 在模式 {mode} 下可用 (mode_enable: {action_mode})")
# else:
# logger.debug(f"动作 {action_name} 在模式 {mode} 下不可用 (mode_enable: {action_mode})")
logger.debug(f"模式 {mode} 下可用动作: {list(filtered_actions.keys())}") logger.debug(f"模式 {mode} 下可用动作: {list(filtered_actions.keys())}")
return filtered_actions return filtered_actions
@@ -474,20 +295,6 @@ class ActionManager:
def restore_default_actions(self) -> None: def restore_default_actions(self) -> None:
"""恢复默认动作集到使用集""" """恢复默认动作集到使用集"""
self._using_actions = self._default_actions.copy() self._using_actions = self._default_actions.copy()
# 添加系统核心动作即使enable_plugin为False的系统动作
# self._add_system_core_actions()
# def _add_system_core_actions(self) -> None:
# """
# 添加系统核心动作到使用集
# 系统核心动作是那些enable_plugin为False但是系统必需的动作
# """
# system_core_actions = ["exit_focus_chat"] # 可以根据需要扩展
# for action_name in system_core_actions:
# if action_name in self._registered_actions and action_name not in self._using_actions:
# self._using_actions[action_name] = self._registered_actions[action_name]
# logger.debug(f"添加系统核心动作到使用集: {action_name}")
def add_system_action_if_needed(self, action_name: str) -> bool: def add_system_action_if_needed(self, action_name: str) -> bool:
""" """
@@ -517,5 +324,4 @@ class ActionManager:
""" """
from src.plugin_system.core.component_registry import component_registry from src.plugin_system.core.component_registry import component_registry
action_registry = component_registry.get_action_registry() return component_registry.get_component_class(action_name)
return action_registry.get(action_name)

View File

@@ -12,14 +12,14 @@ class BasePlanner(ABC):
self.action_manager = action_manager self.action_manager = action_manager
@abstractmethod @abstractmethod
async def plan(self, all_plan_info: List[InfoBase], running_memorys: List[Dict[str, Any]]) -> Dict[str, Any]: async def plan(self, all_plan_info: List[InfoBase], running_memorys: List[Dict[str, Any]], loop_start_time: float) -> Dict[str, Any]:
""" """
规划下一步行动 规划下一步行动
Args: Args:
all_plan_info: 所有计划信息 all_plan_info: 所有计划信息
running_memorys: 回忆信息 running_memorys: 回忆信息
loop_start_time: 循环开始时间
Returns: Returns:
Dict[str, Any]: 规划结果 Dict[str, Any]: 规划结果
""" """

View File

@@ -243,6 +243,8 @@ class ActionModifier:
for action_name, action_info in actions_with_info.items(): for action_name, action_info in actions_with_info.items():
activation_type = action_info.get("focus_activation_type", "always") activation_type = action_info.get("focus_activation_type", "always")
print(f"action_name: {action_name}, activation_type: {activation_type}")
# 现在统一是字符串格式的激活类型值 # 现在统一是字符串格式的激活类型值
if activation_type == "always": if activation_type == "always":
always_actions[action_name] = action_info always_actions[action_name] = action_info

View File

@@ -32,11 +32,7 @@ def init_prompt():
{self_info_block} {self_info_block}
请记住你的性格,身份和特点。 请记住你的性格,身份和特点。
{extra_info_block}
{memory_str}
{time_block} {time_block}
你是群内的一员,你现在正在参与群内的闲聊,以下是群内的聊天内容: 你是群内的一员,你现在正在参与群内的闲聊,以下是群内的聊天内容:
{chat_content_block} {chat_content_block}
@@ -86,13 +82,14 @@ class ActionPlanner(BasePlanner):
request_type="focus.planner", # 用于动作规划 request_type="focus.planner", # 用于动作规划
) )
async def plan(self, all_plan_info: List[InfoBase], running_memorys: List[Dict[str, Any]]) -> Dict[str, Any]: async def plan(self, all_plan_info: List[InfoBase], running_memorys: List[Dict[str, Any]], loop_start_time: float) -> Dict[str, Any]:
""" """
规划器 (Planner): 使用LLM根据上下文决定做出什么动作。 规划器 (Planner): 使用LLM根据上下文决定做出什么动作。
参数: 参数:
all_plan_info: 所有计划信息 all_plan_info: 所有计划信息
running_memorys: 回忆信息 running_memorys: 回忆信息
loop_start_time: 循环开始时间
""" """
action = "no_reply" # 默认动作 action = "no_reply" # 默认动作
@@ -247,6 +244,8 @@ class ActionPlanner(BasePlanner):
action_data["selected_expressions"] = selected_expressions action_data["selected_expressions"] = selected_expressions
logger.debug(f"{self.log_prefix} 传递{len(selected_expressions)}个选中的表达方式到action_data") logger.debug(f"{self.log_prefix} 传递{len(selected_expressions)}个选中的表达方式到action_data")
action_data["loop_start_time"] = loop_start_time
# 对于reply动作不需要额外处理因为相关字段已经在上面的循环中添加到action_data # 对于reply动作不需要额外处理因为相关字段已经在上面的循环中添加到action_data
if extracted_action not in current_available_actions: if extracted_action not in current_available_actions:
@@ -326,7 +325,7 @@ class ActionPlanner(BasePlanner):
chat_content_block = "" chat_content_block = ""
if observed_messages_str: if observed_messages_str:
chat_content_block = f"聊天记录:\n{observed_messages_str}" chat_content_block = f"\n{observed_messages_str}"
else: else:
chat_content_block = "你还未开始聊天" chat_content_block = "你还未开始聊天"
@@ -387,7 +386,7 @@ class ActionPlanner(BasePlanner):
prompt = planner_prompt_template.format( prompt = planner_prompt_template.format(
relation_info_block=relation_info_block, relation_info_block=relation_info_block,
self_info_block=self_info_block, self_info_block=self_info_block,
memory_str=memory_str, # memory_str=memory_str,
time_block=time_block, time_block=time_block,
# bot_name=global_config.bot.nickname, # bot_name=global_config.bot.nickname,
prompt_personality=personality_block, prompt_personality=personality_block,
@@ -397,7 +396,7 @@ class ActionPlanner(BasePlanner):
cycle_info_block=cycle_info, cycle_info_block=cycle_info,
action_options_text=action_options_block, action_options_text=action_options_block,
# action_available_block=action_available_block, # action_available_block=action_available_block,
extra_info_block=extra_info_block, # extra_info_block=extra_info_block,
moderation_prompt=moderation_prompt_block, moderation_prompt=moderation_prompt_block,
) )
return prompt return prompt

View File

@@ -8,9 +8,7 @@ from src.chat.message_receive.chat_stream import get_chat_manager
from src.common.logger import get_logger from src.common.logger import get_logger
from src.llm_models.utils_model import LLMRequest from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config from src.config.config import global_config
from src.chat.utils.utils_image import image_path_to_base64 # Local import needed after move
from src.chat.utils.timer_calculator import Timer # <--- Import Timer from src.chat.utils.timer_calculator import Timer # <--- Import Timer
from src.chat.emoji_system.emoji_manager import get_emoji_manager
from src.chat.focus_chat.heartFC_sender import HeartFCSender from src.chat.focus_chat.heartFC_sender import HeartFCSender
from src.chat.utils.utils import process_llm_response from src.chat.utils.utils import process_llm_response
from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info
@@ -18,6 +16,7 @@ from src.chat.message_receive.chat_stream import ChatStream
from src.chat.focus_chat.hfc_utils import parse_thinking_id_to_timestamp from src.chat.focus_chat.hfc_utils import parse_thinking_id_to_timestamp
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
from src.chat.express.exprssion_learner import get_expression_learner
import time import time
import random import random
from datetime import datetime from datetime import datetime
@@ -50,7 +49,7 @@ def init_prompt():
不要浮夸不要夸张修辞只输出一条回复就好 不要浮夸不要夸张修辞只输出一条回复就好
现在你说 现在你说
""", """,
"default_replyer_prompt", "default_generator_prompt",
) )
Prompt( Prompt(
@@ -70,7 +69,51 @@ def init_prompt():
不要浮夸不要夸张修辞只输出一条回复就好 不要浮夸不要夸张修辞只输出一条回复就好
现在你说 现在你说
""", """,
"default_replyer_private_prompt", "default_generator_private_prompt",
)
Prompt(
"""
你可以参考你的以下的语言习惯如果情景合适就使用不要盲目使用,不要生硬使用而是结合到表达中
{style_habbits}
你现在正在群里聊天以下是群里正在进行的聊天内容
{chat_info}
以上是聊天内容你需要了解聊天记录中的内容
{chat_target}
你的名字是{bot_name}{prompt_personality}在这聊天中"{sender_name}"说的"{target_message}"引起了你的注意对这句话你想表达{raw_reply},原因是{reason}你现在要思考怎么回复
你需要使用合适的语法和句法参考聊天内容组织一条日常且口语化的回复请你修改你想表达的原句符合你的表达风格和语言习惯
请你根据情景使用以下句法
{grammar_habbits}
{config_expression_style}你可以完全重组回复保留最基本的表达含义就好但重组后保持语意通顺
不要浮夸不要夸张修辞平淡且不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 )只输出一条回复就好
现在你说
""",
"default_expressor_prompt",
)
Prompt(
"""
你可以参考以下的语言习惯如果情景合适就使用不要盲目使用,不要生硬使用而是结合到表达中
{style_habbits}
你现在正在群里聊天以下是群里正在进行的聊天内容
{chat_info}
以上是聊天内容你需要了解聊天记录中的内容
{chat_target}
你的名字是{bot_name}{prompt_personality}在这聊天中"{sender_name}"说的"{target_message}"引起了你的注意对这句话你想表达{raw_reply},原因是{reason}你现在要思考怎么回复
你需要使用合适的语法和句法参考聊天内容组织一条日常且口语化的回复
请你根据情景使用以下句法
{grammar_habbits}
{config_expression_style}你可以完全重组回复保留最基本的表达含义就好但重组后保持语意通顺
不要浮夸不要夸张修辞平淡且不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 )只输出一条回复就好
现在你说
""",
"default_expressor_private_prompt", # New template for private FOCUSED chat
) )
@@ -84,9 +127,8 @@ class DefaultReplyer:
) )
self.heart_fc_sender = HeartFCSender() self.heart_fc_sender = HeartFCSender()
self.chat_id = chat_stream.stream_id
self.chat_stream = chat_stream self.chat_stream = chat_stream
self.is_group_chat, self.chat_target_info = get_chat_type_and_target_info(self.chat_id) self.is_group_chat, self.chat_target_info = get_chat_type_and_target_info(self.chat_stream.stream_id)
async def _create_thinking_message(self, anchor_message: Optional[MessageRecv], thinking_id: str): async def _create_thinking_message(self, anchor_message: Optional[MessageRecv], thinking_id: str):
"""创建思考消息 (尝试锚定到 anchor_message)""" """创建思考消息 (尝试锚定到 anchor_message)"""
@@ -115,213 +157,152 @@ class DefaultReplyer:
await self.heart_fc_sender.register_thinking(thinking_message) await self.heart_fc_sender.register_thinking(thinking_message)
return None return None
async def deal_reply( async def generate_reply_with_context(
self, self,
cycle_timers: dict, reply_data: Dict[str, Any],
action_data: Dict[str, Any], ) -> Tuple[bool, Optional[List[str]]]:
reasoning: str,
anchor_message: MessageRecv,
thinking_id: str,
) -> tuple[bool, Optional[List[Tuple[str, str]]]]:
# 创建思考消息
await self._create_thinking_message(anchor_message, thinking_id)
reply = [] # 初始化 reply防止未定义
try:
has_sent_something = False
# 处理文本部分
# text_part = action_data.get("text", [])
# if text_part:
sent_msg_list = []
with Timer("生成回复", cycle_timers):
# 可以保留原有的文本处理逻辑或进行适当调整
reply = await self.reply(
# in_mind_reply=text_part,
anchor_message=anchor_message,
thinking_id=thinking_id,
reason=reasoning,
action_data=action_data,
)
if reply:
with Timer("发送消息", cycle_timers):
sent_msg_list = await self.send_response_messages(
anchor_message=anchor_message,
thinking_id=thinking_id,
response_set=reply,
)
has_sent_something = True
else:
logger.warning(f"{self.log_prefix} 文本回复生成失败")
if not has_sent_something:
logger.warning(f"{self.log_prefix} 回复动作未包含任何有效内容")
return has_sent_something, sent_msg_list
except Exception as e:
logger.error(f"回复失败: {e}")
traceback.print_exc()
return False, None
# --- 回复器 (Replier) 的定义 --- #
async def deal_emoji(
self,
anchor_message: MessageRecv,
thinking_id: str,
action_data: Dict[str, Any],
cycle_timers: dict,
) -> Optional[List[str]]:
"""
表情动作处理类
"""
await self._create_thinking_message(anchor_message, thinking_id)
try:
has_sent_something = False
sent_msg_list = []
reply = []
with Timer("选择表情", cycle_timers):
emoji_keyword = action_data.get("description", [])
emoji_base64, _description, emotion = await self._choose_emoji(emoji_keyword)
if emoji_base64:
# logger.info(f"选择表情: {_description}")
reply.append(("emoji", emoji_base64))
else:
logger.warning(f"{self.log_prefix} 没有找到合适表情")
if reply:
with Timer("发送表情", cycle_timers):
sent_msg_list = await self.send_response_messages(
anchor_message=anchor_message,
thinking_id=thinking_id,
response_set=reply,
)
has_sent_something = True
else:
logger.warning(f"{self.log_prefix} 表情发送失败")
if not has_sent_something:
logger.warning(f"{self.log_prefix} 表情发送失败")
return has_sent_something, sent_msg_list
except Exception as e:
logger.error(f"回复失败: {e}")
traceback.print_exc()
return False, None
async def reply(
self,
# in_mind_reply: str,
reason: str,
anchor_message: MessageRecv,
thinking_id: str,
action_data: Dict[str, Any],
) -> Optional[List[str]]:
""" """
回复器 (Replier): 核心逻辑负责生成回复文本 回复器 (Replier): 核心逻辑负责生成回复文本
(已整合原 HeartFCGenerator 的功能) (已整合原 HeartFCGenerator 的功能)
""" """
try: try:
# 1. 获取情绪影响因子并调整模型温度
# arousal_multiplier = mood_manager.get_arousal_multiplier()
# current_temp = float(global_config.model.normal["temp"]) * arousal_multiplier
# self.express_model.params["temperature"] = current_temp # 动态调整温度
reply_to = action_data.get("reply_to", "none")
sender = ""
targer = ""
if ":" in reply_to or "" in reply_to:
# 使用正则表达式匹配中文或英文冒号
parts = re.split(pattern=r"[:]", string=reply_to, maxsplit=1)
if len(parts) == 2:
sender = parts[0].strip()
targer = parts[1].strip()
identity = action_data.get("identity", "")
extra_info_block = action_data.get("extra_info_block", "")
relation_info_block = action_data.get("relation_info_block", "")
# 3. 构建 Prompt # 3. 构建 Prompt
with Timer("构建Prompt", {}): # 内部计时器,可选保留 with Timer("构建Prompt", {}): # 内部计时器,可选保留
prompt = await self.build_prompt_focus( prompt = await self.build_prompt_reply_context(
chat_stream=self.chat_stream, # Pass the stream object reply_data=reply_data, # 传递action_data
# in_mind_reply=in_mind_reply,
identity=identity,
extra_info_block=extra_info_block,
relation_info_block=relation_info_block,
reason=reason,
sender_name=sender, # Pass determined name
target_message=targer,
config_expression_style=global_config.expression.expression_style,
action_data=action_data, # 传递action_data
) )
# 4. 调用 LLM 生成回复 # 4. 调用 LLM 生成回复
content = None content = None
reasoning_content = None reasoning_content = None
model_name = "unknown_model" model_name = "unknown_model"
if not prompt:
logger.error(f"{self.log_prefix}[Replier-{thinking_id}] Prompt 构建失败,无法生成回复。")
return None
try: try:
with Timer("LLM生成", {}): # 内部计时器,可选保留 with Timer("LLM生成", {}): # 内部计时器,可选保留
logger.info(f"{self.log_prefix}Prompt:\n{prompt}\n") logger.info(f"{self.log_prefix}Prompt:\n{prompt}\n")
content, (reasoning_content, model_name) = await self.express_model.generate_response_async(prompt) content, (reasoning_content, model_name) = await self.express_model.generate_response_async(prompt)
# logger.info(f"prompt: {prompt}")
logger.info(f"最终回复: {content}") logger.info(f"最终回复: {content}")
except Exception as llm_e: except Exception as llm_e:
# 精简报错信息 # 精简报错信息
logger.error(f"{self.log_prefix}LLM 生成失败: {llm_e}") logger.error(f"{self.log_prefix}LLM 生成失败: {llm_e}")
return None # LLM 调用失败则无法生成回复 return False, None # LLM 调用失败则无法生成回复
processed_response = process_llm_response(content) processed_response = process_llm_response(content)
# 5. 处理 LLM 响应 # 5. 处理 LLM 响应
if not content: if not content:
logger.warning(f"{self.log_prefix}LLM 生成了空内容。") logger.warning(f"{self.log_prefix}LLM 生成了空内容。")
return None return False, None
if not processed_response: if not processed_response:
logger.warning(f"{self.log_prefix}处理后的回复为空。") logger.warning(f"{self.log_prefix}处理后的回复为空。")
return None return False, None
reply_set = [] reply_set = []
for str in processed_response: for str in processed_response:
reply_seg = ("text", str) reply_seg = ("text", str)
reply_set.append(reply_seg) reply_set.append(reply_seg)
return reply_set return True , reply_set
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix}回复生成意外失败: {e}") logger.error(f"{self.log_prefix}回复生成意外失败: {e}")
traceback.print_exc() traceback.print_exc()
return None return False, None
async def build_prompt_focus( async def rewrite_reply_with_context(
self, self,
reason, reply_data: Dict[str, Any],
chat_stream, ) -> Tuple[bool, Optional[List[str]]]:
sender_name, """
# in_mind_reply, 表达器 (Expressor): 核心逻辑负责生成回复文本
extra_info_block, """
relation_info_block, try:
identity,
target_message,
config_expression_style, reply_to = reply_data.get("reply_to", "")
action_data=None, raw_reply = reply_data.get("raw_reply", "")
# stuation, reason = reply_data.get("reason", "")
with Timer("构建Prompt", {}): # 内部计时器,可选保留
prompt = await self.build_prompt_rewrite_context(
raw_reply=raw_reply,
reason=reason,
reply_to=reply_to,
)
content = None
reasoning_content = None
model_name = "unknown_model"
if not prompt:
logger.error(f"{self.log_prefix}Prompt 构建失败,无法生成回复。")
return False, None
try:
with Timer("LLM生成", {}): # 内部计时器,可选保留
# TODO: API-Adapter修改标记
content, (reasoning_content, model_name) = await self.express_model.generate_response_async(prompt)
logger.info(f"想要表达:{raw_reply}||理由:{reason}")
logger.info(f"最终回复: {content}\n")
except Exception as llm_e:
# 精简报错信息
logger.error(f"{self.log_prefix}LLM 生成失败: {llm_e}")
return False, None # LLM 调用失败则无法生成回复
processed_response = process_llm_response(content)
# 5. 处理 LLM 响应
if not content:
logger.warning(f"{self.log_prefix}LLM 生成了空内容。")
return False, None
if not processed_response:
logger.warning(f"{self.log_prefix}处理后的回复为空。")
return False, None
reply_set = []
for str in processed_response:
reply_seg = ("text", str)
reply_set.append(reply_seg)
return True, reply_set
except Exception as e:
logger.error(f"{self.log_prefix}回复生成意外失败: {e}")
traceback.print_exc()
return False, None
async def build_prompt_reply_context(
self,
reply_data=None,
) -> str: ) -> str:
chat_stream = self.chat_stream
is_group_chat = bool(chat_stream.group_info) is_group_chat = bool(chat_stream.group_info)
identity = reply_data.get("identity", "")
extra_info_block = reply_data.get("extra_info_block", "")
relation_info_block = reply_data.get("relation_info_block", "")
reply_to = reply_data.get("reply_to", "none")
sender = ""
target = ""
if ":" in reply_to or "" in reply_to:
# 使用正则表达式匹配中文或英文冒号
parts = re.split(pattern=r"[:]", string=reply_to, maxsplit=1)
if len(parts) == 2:
sender = parts[0].strip()
target = parts[1].strip()
message_list_before_now = get_raw_msg_before_timestamp_with_chat( message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_stream.stream_id, chat_id=chat_stream.stream_id,
timestamp=time.time(), timestamp=time.time(),
@@ -341,7 +322,7 @@ class DefaultReplyer:
grammar_habbits = [] grammar_habbits = []
# 使用从处理器传来的选中表达方式 # 使用从处理器传来的选中表达方式
selected_expressions = action_data.get("selected_expressions", []) if action_data else [] selected_expressions = reply_data.get("selected_expressions", []) if reply_data else []
if selected_expressions: if selected_expressions:
logger.info(f"{self.log_prefix} 使用处理器选中的{len(selected_expressions)}个表达方式") logger.info(f"{self.log_prefix} 使用处理器选中的{len(selected_expressions)}个表达方式")
@@ -371,7 +352,7 @@ class DefaultReplyer:
try: try:
# 处理关键词规则 # 处理关键词规则
for rule in global_config.keyword_reaction.keyword_rules: for rule in global_config.keyword_reaction.keyword_rules:
if any(keyword in target_message for keyword in rule.keywords): if any(keyword in target for keyword in rule.keywords):
logger.info(f"检测到关键词规则:{rule.keywords},触发反应:{rule.reaction}") logger.info(f"检测到关键词规则:{rule.keywords},触发反应:{rule.reaction}")
keywords_reaction_prompt += f"{rule.reaction}" keywords_reaction_prompt += f"{rule.reaction}"
@@ -380,7 +361,7 @@ class DefaultReplyer:
for pattern_str in rule.regex: for pattern_str in rule.regex:
try: try:
pattern = re.compile(pattern_str) pattern = re.compile(pattern_str)
if result := pattern.search(target_message): if result := pattern.search(target):
reaction = rule.reaction reaction = rule.reaction
for name, content in result.groupdict().items(): for name, content in result.groupdict().items():
reaction = reaction.replace(f"[{name}]", content) reaction = reaction.replace(f"[{name}]", content)
@@ -397,18 +378,18 @@ class DefaultReplyer:
# logger.debug("开始构建 focus prompt") # logger.debug("开始构建 focus prompt")
if sender_name: if sender:
reply_target_block = ( reply_target_block = (
f"现在{sender_name}说的:{target_message}。引起了你的注意,你想要在群里发言或者回复这条消息。" f"现在{sender}说的:{target}。引起了你的注意,你想要在群里发言或者回复这条消息。"
) )
elif target_message: elif target:
reply_target_block = f"现在{target_message}引起了你的注意,你想要在群里发言或者回复这条消息。" reply_target_block = f"现在{target}引起了你的注意,你想要在群里发言或者回复这条消息。"
else: else:
reply_target_block = "现在,你想要在群里发言或者回复消息。" reply_target_block = "现在,你想要在群里发言或者回复消息。"
# --- Choose template based on chat type --- # --- Choose template based on chat type ---
if is_group_chat: if is_group_chat:
template_name = "default_replyer_prompt" template_name = "default_generator_prompt"
# Group specific formatting variables (already fetched or default) # Group specific formatting variables (already fetched or default)
chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1") chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1")
# chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2") # chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2")
@@ -422,18 +403,14 @@ class DefaultReplyer:
relation_info_block=relation_info_block, relation_info_block=relation_info_block,
time_block=time_block, time_block=time_block,
reply_target_block=reply_target_block, reply_target_block=reply_target_block,
# bot_name=global_config.bot.nickname,
# prompt_personality="",
# reason=reason,
# in_mind_reply=in_mind_reply,
keywords_reaction_prompt=keywords_reaction_prompt, keywords_reaction_prompt=keywords_reaction_prompt,
identity=identity, identity=identity,
target_message=target_message, target_message=target,
sender_name=sender_name, sender_name=sender,
config_expression_style=config_expression_style, config_expression_style=global_config.expression.expression_style,
) )
else: # Private chat else: # Private chat
template_name = "default_replyer_private_prompt" template_name = "default_generator_private_prompt"
chat_target_1 = "你正在和人私聊" chat_target_1 = "你正在和人私聊"
prompt = await global_prompt_manager.format_prompt( prompt = await global_prompt_manager.format_prompt(
template_name, template_name,
@@ -444,20 +421,125 @@ class DefaultReplyer:
relation_info_block=relation_info_block, relation_info_block=relation_info_block,
time_block=time_block, time_block=time_block,
reply_target_block=reply_target_block, reply_target_block=reply_target_block,
# bot_name=global_config.bot.nickname,
# prompt_personality="",
# reason=reason,
# in_mind_reply=in_mind_reply,
keywords_reaction_prompt=keywords_reaction_prompt, keywords_reaction_prompt=keywords_reaction_prompt,
identity=identity, identity=identity,
target_message=target_message, target_message=target,
sender_name=sender_name, sender_name=sender,
config_expression_style=config_expression_style, config_expression_style=global_config.expression.expression_style,
) )
return prompt return prompt
# --- 发送器 (Sender) --- # async def build_prompt_rewrite_context(
self,
reason,
raw_reply,
reply_to,
) -> str:
sender = ""
target = ""
if ":" in reply_to or "" in reply_to:
# 使用正则表达式匹配中文或英文冒号
parts = re.split(pattern=r"[:]", string=reply_to, maxsplit=1)
if len(parts) == 2:
sender = parts[0].strip()
target = parts[1].strip()
chat_stream = self.chat_stream
is_group_chat = bool(chat_stream.group_info)
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_stream.stream_id,
timestamp=time.time(),
limit=global_config.focus_chat.observation_context_size,
)
chat_talking_prompt = build_readable_messages(
message_list_before_now,
replace_bot_name=True,
merge_messages=True,
timestamp_mode="relative",
read_mark=0.0,
truncate=True,
)
expression_learner = get_expression_learner()
(
learnt_style_expressions,
learnt_grammar_expressions,
personality_expressions,
) = await expression_learner.get_expression_by_chat_id(chat_stream.stream_id)
style_habbits = []
grammar_habbits = []
# 1. learnt_expressions加权随机选3条
if learnt_style_expressions:
weights = [expr["count"] for expr in learnt_style_expressions]
selected_learnt = weighted_sample_no_replacement(learnt_style_expressions, weights, 3)
for expr in selected_learnt:
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
style_habbits.append(f"{expr['situation']}时,使用 {expr['style']}")
# 2. learnt_grammar_expressions加权随机选3条
if learnt_grammar_expressions:
weights = [expr["count"] for expr in learnt_grammar_expressions]
selected_learnt = weighted_sample_no_replacement(learnt_grammar_expressions, weights, 3)
for expr in selected_learnt:
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
grammar_habbits.append(f"{expr['situation']}时,使用 {expr['style']}")
# 3. personality_expressions随机选1条
if personality_expressions:
expr = random.choice(personality_expressions)
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
style_habbits.append(f"{expr['situation']}时,使用 {expr['style']}")
style_habbits_str = "\n".join(style_habbits)
grammar_habbits_str = "\n".join(grammar_habbits)
logger.debug("开始构建 focus prompt")
# --- Choose template based on chat type ---
if is_group_chat:
template_name = "default_expressor_prompt"
# Group specific formatting variables (already fetched or default)
chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1")
# chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2")
prompt = await global_prompt_manager.format_prompt(
template_name,
style_habbits=style_habbits_str,
grammar_habbits=grammar_habbits_str,
chat_target=chat_target_1,
chat_info=chat_talking_prompt,
bot_name=global_config.bot.nickname,
prompt_personality="",
reason=reason,
raw_reply=raw_reply,
sender_name=sender,
target_message=target,
config_expression_style=global_config.expression.expression_style,
)
else: # Private chat
template_name = "default_expressor_private_prompt"
chat_target_1 = "你正在和人私聊"
prompt = await global_prompt_manager.format_prompt(
template_name,
style_habbits=style_habbits_str,
grammar_habbits=grammar_habbits_str,
chat_target=chat_target_1,
chat_info=chat_talking_prompt,
bot_name=global_config.bot.nickname,
prompt_personality="",
reason=reason,
raw_reply=raw_reply,
sender_name=sender,
target_message=target,
config_expression_style=global_config.expression.expression_style,
)
return prompt
async def send_response_messages( async def send_response_messages(
self, self,
@@ -468,7 +550,7 @@ class DefaultReplyer:
) -> Optional[MessageSending]: ) -> Optional[MessageSending]:
"""发送回复消息 (尝试锚定到 anchor_message),使用 HeartFCSender""" """发送回复消息 (尝试锚定到 anchor_message),使用 HeartFCSender"""
chat = self.chat_stream chat = self.chat_stream
chat_id = self.chat_id chat_id = self.chat_stream.stream_id
if chat is None: if chat is None:
logger.error(f"{self.log_prefix} 无法发送回复chat_stream 为空。") logger.error(f"{self.log_prefix} 无法发送回复chat_stream 为空。")
return None return None
@@ -514,7 +596,7 @@ class DefaultReplyer:
is_emoji = False is_emoji = False
reply_to = not mark_head reply_to = not mark_head
bot_message = await self._build_single_sending_message( bot_message: MessageSending = await self._build_single_sending_message(
anchor_message=anchor_message, anchor_message=anchor_message,
message_id=part_message_id, message_id=part_message_id,
message_segment=message_segment, message_segment=message_segment,
@@ -526,22 +608,22 @@ class DefaultReplyer:
) )
try: try:
if (bot_message.is_private_message() or
bot_message.reply.processed_plain_text != "[System Trigger Context]" or
mark_head):
set_reply = False
else:
set_reply = True
if not mark_head: if not mark_head:
mark_head = True mark_head = True
# first_bot_msg = bot_message # 保存第一个成功发送的消息对象
typing = False typing = False
else: else:
typing = True typing = True
if type == "emoji":
typing = False
if anchor_message.raw_message:
set_reply = True
else:
set_reply = False
sent_msg = await self.heart_fc_sender.send_message( sent_msg = await self.heart_fc_sender.send_message(
bot_message, has_thinking=True, typing=typing, set_reply=set_reply bot_message, typing=typing, set_reply=set_reply
) )
reply_message_ids.append(part_message_id) # 记录我们生成的ID reply_message_ids.append(part_message_id) # 记录我们生成的ID
@@ -562,30 +644,15 @@ class DefaultReplyer:
return sent_msg_list return sent_msg_list
async def _choose_emoji(self, send_emoji: str):
"""
选择表情根据send_emoji文本选择表情返回表情base64
"""
emoji_base64 = ""
description = ""
emoji_raw = await get_emoji_manager().get_emoji_for_text(send_emoji)
if emoji_raw:
emoji_path, description, _emotion = emoji_raw
emoji_base64 = image_path_to_base64(emoji_path)
return emoji_base64, description, _emotion
else:
return None, None, None
async def _build_single_sending_message( async def _build_single_sending_message(
self, self,
anchor_message: MessageRecv,
message_id: str, message_id: str,
message_segment: Seg, message_segment: Seg,
reply_to: bool, reply_to: bool,
is_emoji: bool, is_emoji: bool,
thinking_id: str,
thinking_start_time: float, thinking_start_time: float,
display_message: str, display_message: str,
anchor_message: MessageRecv = None
) -> MessageSending: ) -> MessageSending:
"""构建单个发送消息""" """构建单个发送消息"""
@@ -596,12 +663,16 @@ class DefaultReplyer:
) )
# await anchor_message.process() # await anchor_message.process()
if anchor_message:
sender_info = anchor_message.message_info.user_info
else:
sender_info = None
bot_message = MessageSending( bot_message = MessageSending(
message_id=message_id, # 使用片段的唯一ID message_id=message_id, # 使用片段的唯一ID
chat_stream=self.chat_stream, chat_stream=self.chat_stream,
bot_user_info=bot_user_info, bot_user_info=bot_user_info,
sender_info=anchor_message.message_info.user_info, sender_info=sender_info,
message_segment=message_segment, message_segment=message_segment,
reply=anchor_message, # 回复原始锚点 reply=anchor_message, # 回复原始锚点
is_head=reply_to, is_head=reply_to,

View File

@@ -14,7 +14,8 @@ from src.chat.message_receive.message import MessageRecv
from src.chat.heart_flow.observation.observation import Observation from src.chat.heart_flow.observation.observation import Observation
from src.common.logger import get_logger from src.common.logger import get_logger
from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info
from src.chat.message_receive.chat_stream import get_chat_manager
from src.person_info.person_info import get_person_info_manager
logger = get_logger("observation") logger = get_logger("observation")
# 定义提示模板 # 定义提示模板
@@ -70,6 +71,8 @@ class ChattingObservation(Observation):
self.oldest_messages = [] self.oldest_messages = []
self.oldest_messages_str = "" self.oldest_messages_str = ""
self.last_observe_time = datetime.now().timestamp() -1
print(f"last_observe_time: {self.last_observe_time}")
initial_messages = get_raw_msg_before_timestamp_with_chat(self.chat_id, self.last_observe_time, 10) initial_messages = get_raw_msg_before_timestamp_with_chat(self.chat_id, self.last_observe_time, 10)
self.last_observe_time = initial_messages[-1]["time"] if initial_messages else self.last_observe_time self.last_observe_time = initial_messages[-1]["time"] if initial_messages else self.last_observe_time
self.talking_message = initial_messages self.talking_message = initial_messages
@@ -92,38 +95,27 @@ class ChattingObservation(Observation):
def get_observe_info(self, ids=None): def get_observe_info(self, ids=None):
return self.talking_message_str return self.talking_message_str
def search_message_by_text(self, text: str) -> Optional[MessageRecv]: def get_recv_message_by_text(self, sender: str, text: str) -> Optional[MessageRecv]:
""" """
根据回复的纯文本 根据回复的纯文本
1. 在talking_message中查找最新的最匹配的消息 1. 在talking_message中查找最新的最匹配的消息
2. 如果找到,则返回消息 2. 如果找到,则返回消息
""" """
msg_list = []
find_msg = None find_msg = None
reverse_talking_message = list(reversed(self.talking_message)) reverse_talking_message = list(reversed(self.talking_message))
for message in reverse_talking_message: for message in reverse_talking_message:
if message["processed_plain_text"] == text: user_id = message["user_id"]
platform = message["platform"]
person_id = get_person_info_manager().get_person_id(platform, user_id)
person_name = get_person_info_manager().get_value(person_id, "person_name")
if person_name == sender:
similarity = difflib.SequenceMatcher(None, text, message["processed_plain_text"]).ratio()
if similarity >= 0.9:
find_msg = message find_msg = message
break break
else:
raw_message = message.get("raw_message")
if raw_message:
similarity = difflib.SequenceMatcher(None, text, raw_message).ratio()
else:
similarity = difflib.SequenceMatcher(None, text, message.get("processed_plain_text", "")).ratio()
msg_list.append({"message": message, "similarity": similarity})
if not find_msg: if not find_msg:
if msg_list:
msg_list.sort(key=lambda x: x["similarity"], reverse=True)
if msg_list[0]["similarity"] >= 0.9:
find_msg = msg_list[0]["message"]
else:
logger.debug("没有找到锚定消息,相似度低")
return None
else:
logger.debug("没有找到锚定消息,没有消息捕获")
return None return None
user_info = { user_info = {
@@ -167,6 +159,10 @@ class ChattingObservation(Observation):
"processed_plain_text": find_msg.get("processed_plain_text"), "processed_plain_text": find_msg.get("processed_plain_text"),
} }
find_rec_msg = MessageRecv(message_dict) find_rec_msg = MessageRecv(message_dict)
find_rec_msg.update_chat_stream(get_chat_manager().get_or_create_stream(self.chat_id))
return find_rec_msg return find_rec_msg
async def observe(self): async def observe(self):
@@ -179,6 +175,8 @@ class ChattingObservation(Observation):
limit_mode="latest", limit_mode="latest",
) )
print(f"new_messages_list: {new_messages_list}")
last_obs_time_mark = self.last_observe_time last_obs_time_mark = self.last_observe_time
if new_messages_list: if new_messages_list:
self.last_observe_time = new_messages_list[-1]["time"] self.last_observe_time = new_messages_list[-1]["time"]

View File

@@ -172,6 +172,15 @@ class ChatManager:
key = "_".join(components) key = "_".join(components)
return hashlib.md5(key.encode()).hexdigest() return hashlib.md5(key.encode()).hexdigest()
def get_stream_id(self, platform: str, chat_id: str, is_group: bool = True) -> str:
"""获取聊天流ID"""
if is_group:
components = [platform, str(chat_id)]
else:
components = [platform, str(chat_id), "private"]
key = "_".join(components)
return hashlib.md5(key.encode()).hexdigest()
async def get_or_create_stream( async def get_or_create_stream(
self, platform: str, user_info: UserInfo, group_info: Optional[GroupInfo] = None self, platform: str, user_info: UserInfo, group_info: Optional[GroupInfo] = None
) -> ChatStream: ) -> ChatStream:

View File

@@ -275,7 +275,7 @@ class MessageSending(MessageProcessBase):
message_id: str, message_id: str,
chat_stream: "ChatStream", chat_stream: "ChatStream",
bot_user_info: UserInfo, bot_user_info: UserInfo,
sender_info: UserInfo | None, # 用来记录发送者信息,用于私聊回复 sender_info: UserInfo | None, # 用来记录发送者信息
message_segment: Seg, message_segment: Seg,
display_message: str = "", display_message: str = "",
reply: Optional["MessageRecv"] = None, reply: Optional["MessageRecv"] = None,
@@ -304,11 +304,8 @@ class MessageSending(MessageProcessBase):
# 用于显示发送内容与显示不一致的情况 # 用于显示发送内容与显示不一致的情况
self.display_message = display_message self.display_message = display_message
def set_reply(self, reply: Optional["MessageRecv"] = None): def build_reply(self):
"""设置回复消息""" """设置回复消息"""
if True:
if reply:
self.reply = reply
if self.reply: if self.reply:
self.reply_to_message_id = self.reply.message_info.message_id self.reply_to_message_id = self.reply.message_info.message_id
self.message_segment = Seg( self.message_segment = Seg(

View File

@@ -230,7 +230,7 @@ class MessageManager:
logger.debug( logger.debug(
f"[{message.chat_stream.stream_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}..." f"[{message.chat_stream.stream_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}..."
) )
message.set_reply(message.reply) message.build_reply()
# --- 结束条件 set_reply --- # --- 结束条件 set_reply ---
await message.process() # 预处理消息内容 await message.process() # 预处理消息内容

View File

@@ -22,7 +22,7 @@ from src.chat.focus_chat.planners.action_manager import ActionManager
from src.chat.normal_chat.normal_chat_planner import NormalChatPlanner from src.chat.normal_chat.normal_chat_planner import NormalChatPlanner
from src.chat.normal_chat.normal_chat_action_modifier import NormalChatActionModifier from src.chat.normal_chat.normal_chat_action_modifier import NormalChatActionModifier
from src.chat.normal_chat.normal_chat_expressor import NormalChatExpressor from src.chat.normal_chat.normal_chat_expressor import NormalChatExpressor
from src.chat.focus_chat.replyer.default_replyer import DefaultReplyer from src.chat.focus_chat.replyer.default_generator import DefaultReplyer
from src.person_info.person_info import PersonInfoManager from src.person_info.person_info import PersonInfoManager
from src.chat.utils.chat_message_builder import ( from src.chat.utils.chat_message_builder import (
get_raw_msg_by_timestamp_with_chat, get_raw_msg_by_timestamp_with_chat,
@@ -1063,9 +1063,6 @@ class NormalChat:
reasoning=action_data.get("reasoning", ""), reasoning=action_data.get("reasoning", ""),
cycle_timers={}, # normal_chat使用空的cycle_timers cycle_timers={}, # normal_chat使用空的cycle_timers
thinking_id=thinking_id, thinking_id=thinking_id,
observations=[], # normal_chat不使用observations
expressor=self.expressor, # 使用normal_chat专用的expressor
replyer=self.replyer,
chat_stream=self.chat_stream, chat_stream=self.chat_stream,
log_prefix=self.stream_name, log_prefix=self.stream_name,
shutting_down=self._disabled, shutting_down=self._disabled,

View File

@@ -1,4 +1,4 @@
from src.chat.focus_chat.expressors.exprssion_learner import get_expression_learner from src.chat.express.exprssion_learner import get_expression_learner
from src.config.config import global_config from src.config.config import global_config
from src.common.logger import get_logger from src.common.logger import get_logger
from src.individuality.individuality import get_individuality from src.individuality.individuality import get_individuality

View File

@@ -2,7 +2,7 @@ import asyncio
import time import time
from maim_message import MessageServer from maim_message import MessageServer
from src.chat.focus_chat.expressors.exprssion_learner import get_expression_learner from src.chat.express.exprssion_learner import get_expression_learner
from src.common.remote import TelemetryHeartBeatTask from src.common.remote import TelemetryHeartBeatTask
from src.manager.async_task_manager import async_task_manager from src.manager.async_task_manager import async_task_manager
from src.chat.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask from src.chat.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask

View File

@@ -8,6 +8,7 @@ MaiBot 插件系统
from src.plugin_system.base.base_plugin import BasePlugin, register_plugin from src.plugin_system.base.base_plugin import BasePlugin, register_plugin
from src.plugin_system.base.base_action import BaseAction from src.plugin_system.base.base_action import BaseAction
from src.plugin_system.base.base_command import BaseCommand from src.plugin_system.base.base_command import BaseCommand
from src.plugin_system.base.config_types import ConfigField
from src.plugin_system.base.component_types import ( from src.plugin_system.base.component_types import (
ComponentType, ComponentType,
ActionActivationType, ActionActivationType,
@@ -18,11 +19,11 @@ from src.plugin_system.base.component_types import (
PluginInfo, PluginInfo,
PythonDependency, PythonDependency,
) )
from src.plugin_system.apis.plugin_api import PluginAPI, create_plugin_api, create_command_api
from src.plugin_system.core.plugin_manager import plugin_manager from src.plugin_system.core.plugin_manager import plugin_manager
from src.plugin_system.core.component_registry import component_registry from src.plugin_system.core.component_registry import component_registry
from src.plugin_system.core.dependency_manager import dependency_manager from src.plugin_system.core.dependency_manager import dependency_manager
__version__ = "1.0.0" __version__ = "1.0.0"
__all__ = [ __all__ = [
@@ -39,14 +40,11 @@ __all__ = [
"CommandInfo", "CommandInfo",
"PluginInfo", "PluginInfo",
"PythonDependency", "PythonDependency",
# API接口
"PluginAPI",
"create_plugin_api",
"create_command_api",
# 管理器 # 管理器
"plugin_manager", "plugin_manager",
"component_registry", "component_registry",
"dependency_manager", "dependency_manager",
# 装饰器 # 装饰器
"register_plugin", "register_plugin",
"ConfigField",
] ]

View File

@@ -1,37 +1,33 @@
""" """
插件API模块 插件系统API模块
提供插件可以使用的各种API接口 提供插件开发所需的各种API
""" """
from src.plugin_system.apis.plugin_api import PluginAPI, create_plugin_api, create_command_api # 导入所有API模块
from src.plugin_system.apis.message_api import MessageAPI from src.plugin_system.apis import (
from src.plugin_system.apis.llm_api import LLMAPI chat_api,
from src.plugin_system.apis.database_api import DatabaseAPI config_api,
from src.plugin_system.apis.config_api import ConfigAPI database_api,
from src.plugin_system.apis.utils_api import UtilsAPI emoji_api,
from src.plugin_system.apis.stream_api import StreamAPI generator_api,
from src.plugin_system.apis.hearflow_api import HearflowAPI llm_api,
message_api,
# 新增分类的API聚合 person_api,
from src.plugin_system.apis.action_apis import ActionAPI send_api,
from src.plugin_system.apis.independent_apis import IndependentAPI, StaticAPI utils_api
)
# 导出所有API模块使它们可以通过 apis.xxx 方式访问
__all__ = [ __all__ = [
# 原有统一API "chat_api",
"PluginAPI", "config_api",
"create_plugin_api", "database_api",
"create_command_api", "emoji_api",
# 原有单独API "generator_api",
"MessageAPI", "llm_api",
"LLMAPI", "message_api",
"DatabaseAPI", "person_api",
"ConfigAPI", "send_api",
"UtilsAPI", "utils_api"
"StreamAPI",
"HearflowAPI",
# 新增分类API
"ActionAPI", # 需要Action依赖的API
"IndependentAPI", # 独立API
"StaticAPI", # 静态API
] ]

View File

@@ -1,88 +0,0 @@
"""
Action相关API聚合模块
聚合了需要Action组件依赖的API这些API需要通过Action初始化时注入的服务对象才能正常工作。
包括MessageAPI、DatabaseAPI等需要chat_stream、expressor等服务的API。
"""
from src.plugin_system.apis.message_api import MessageAPI
from src.plugin_system.apis.database_api import DatabaseAPI
from src.common.logger import get_logger
logger = get_logger("action_apis")
class ActionAPI(MessageAPI, DatabaseAPI):
"""
Action相关API聚合类
聚合了需要Action组件依赖的API功能。这些API需要以下依赖
- _services: 包含chat_stream、expressor、replyer、observations等服务对象
- log_prefix: 日志前缀
- thinking_id: 思考ID
- cycle_timers: 计时器
- action_data: Action数据
使用场景:
- 在Action组件中使用需要发送消息、存储数据等功能
- 需要访问聊天上下文和执行环境的操作
"""
def __init__(
self,
chat_stream=None,
expressor=None,
replyer=None,
observations=None,
log_prefix: str = "[ActionAPI]",
thinking_id: str = "",
cycle_timers: dict = None,
action_data: dict = None,
):
"""
初始化Action相关API
Args:
chat_stream: 聊天流对象
expressor: 表达器对象
replyer: 回复器对象
observations: 观察列表
log_prefix: 日志前缀
thinking_id: 思考ID
cycle_timers: 计时器字典
action_data: Action数据
"""
# 存储依赖对象
self._services = {
"chat_stream": chat_stream,
"expressor": expressor,
"replyer": replyer,
"observations": observations or [],
}
self.log_prefix = log_prefix
self.thinking_id = thinking_id
self.cycle_timers = cycle_timers or {}
self.action_data = action_data or {}
logger.debug(f"{self.log_prefix} ActionAPI 初始化完成")
def set_chat_stream(self, chat_stream):
"""设置聊天流对象"""
self._services["chat_stream"] = chat_stream
logger.debug(f"{self.log_prefix} 设置聊天流")
def set_expressor(self, expressor):
"""设置表达器对象"""
self._services["expressor"] = expressor
logger.debug(f"{self.log_prefix} 设置表达器")
def set_replyer(self, replyer):
"""设置回复器对象"""
self._services["replyer"] = replyer
logger.debug(f"{self.log_prefix} 设置回复器")
def set_observations(self, observations):
"""设置观察列表"""
self._services["observations"] = observations or []
logger.debug(f"{self.log_prefix} 设置观察列表")

View File

@@ -0,0 +1,292 @@
"""
聊天API模块
专门负责聊天信息的查询和管理采用标准Python包设计模式
使用方式:
from src.plugin_system.apis import chat_api
streams = chat_api.get_all_group_streams()
chat_type = chat_api.get_stream_type(stream)
或者:
from src.plugin_system.apis.chat_api import ChatManager as chat
streams = chat.get_all_group_streams()
"""
from typing import List, Dict, Any, Optional
from src.common.logger import get_logger
# 导入依赖
from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager
from src.chat.focus_chat.info.obs_info import ObsInfo
logger = get_logger("chat_api")
class ChatManager:
"""聊天管理器 - 专门负责聊天信息的查询和管理"""
@staticmethod
def get_all_streams(platform: str = "qq") -> List[ChatStream]:
"""获取所有聊天流
Args:
platform: 平台筛选,默认为"qq"
Returns:
List[ChatStream]: 聊天流列表
"""
streams = []
try:
for _, stream in get_chat_manager().streams.items():
if stream.platform == platform:
streams.append(stream)
logger.debug(f"[ChatAPI] 获取到 {len(streams)}{platform} 平台的聊天流")
except Exception as e:
logger.error(f"[ChatAPI] 获取聊天流失败: {e}")
return streams
@staticmethod
def get_group_streams(platform: str = "qq") -> List[ChatStream]:
"""获取所有群聊聊天流
Args:
platform: 平台筛选,默认为"qq"
Returns:
List[ChatStream]: 群聊聊天流列表
"""
streams = []
try:
for _, stream in get_chat_manager().streams.items():
if stream.platform == platform and stream.group_info:
streams.append(stream)
logger.debug(f"[ChatAPI] 获取到 {len(streams)}{platform} 平台的群聊流")
except Exception as e:
logger.error(f"[ChatAPI] 获取群聊流失败: {e}")
return streams
@staticmethod
def get_private_streams(platform: str = "qq") -> List[ChatStream]:
"""获取所有私聊聊天流
Args:
platform: 平台筛选,默认为"qq"
Returns:
List[ChatStream]: 私聊聊天流列表
"""
streams = []
try:
for _, stream in get_chat_manager().streams.items():
if stream.platform == platform and not stream.group_info:
streams.append(stream)
logger.debug(f"[ChatAPI] 获取到 {len(streams)}{platform} 平台的私聊流")
except Exception as e:
logger.error(f"[ChatAPI] 获取私聊流失败: {e}")
return streams
@staticmethod
def get_stream_by_group_id(group_id: str, platform: str = "qq") -> Optional[ChatStream]:
"""根据群ID获取聊天流
Args:
group_id: 群聊ID
platform: 平台,默认为"qq"
Returns:
Optional[ChatStream]: 聊天流对象如果未找到返回None
"""
try:
for _, stream in get_chat_manager().streams.items():
if (
stream.group_info
and str(stream.group_info.group_id) == str(group_id)
and stream.platform == platform
):
logger.debug(f"[ChatAPI] 找到群ID {group_id} 的聊天流")
return stream
logger.warning(f"[ChatAPI] 未找到群ID {group_id} 的聊天流")
except Exception as e:
logger.error(f"[ChatAPI] 查找群聊流失败: {e}")
return None
@staticmethod
def get_stream_by_user_id(user_id: str, platform: str = "qq") -> Optional[ChatStream]:
"""根据用户ID获取私聊流
Args:
user_id: 用户ID
platform: 平台,默认为"qq"
Returns:
Optional[ChatStream]: 聊天流对象如果未找到返回None
"""
try:
for _, stream in get_chat_manager().streams.items():
if (
not stream.group_info
and str(stream.user_info.user_id) == str(user_id)
and stream.platform == platform
):
logger.debug(f"[ChatAPI] 找到用户ID {user_id} 的私聊流")
return stream
logger.warning(f"[ChatAPI] 未找到用户ID {user_id} 的私聊流")
except Exception as e:
logger.error(f"[ChatAPI] 查找私聊流失败: {e}")
return None
@staticmethod
def get_stream_type(chat_stream: ChatStream) -> str:
"""获取聊天流类型
Args:
chat_stream: 聊天流对象
Returns:
str: 聊天类型 ("group", "private", "unknown")
"""
if not chat_stream:
return "unknown"
if hasattr(chat_stream, "group_info"):
return "group" if chat_stream.group_info else "private"
return "unknown"
@staticmethod
def get_stream_info(chat_stream: ChatStream) -> Dict[str, Any]:
"""获取聊天流详细信息
Args:
chat_stream: 聊天流对象
Returns:
Dict[str, Any]: 聊天流信息字典
"""
if not chat_stream:
return {}
try:
info = {
"stream_id": chat_stream.stream_id,
"platform": chat_stream.platform,
"type": ChatManager.get_stream_type(chat_stream),
}
if chat_stream.group_info:
info.update({
"group_id": chat_stream.group_info.group_id,
"group_name": getattr(chat_stream.group_info, "group_name", "未知群聊"),
})
if chat_stream.user_info:
info.update({
"user_id": chat_stream.user_info.user_id,
"user_name": chat_stream.user_info.user_nickname,
})
return info
except Exception as e:
logger.error(f"[ChatAPI] 获取聊天流信息失败: {e}")
return {}
@staticmethod
def get_recent_messages_from_obs(observations: List[Any], count: int = 5) -> List[Dict[str, Any]]:
"""从观察对象获取最近的消息
Args:
observations: 观察对象列表
count: 要获取的消息数量
Returns:
List[Dict]: 消息列表,每个消息包含发送者、内容等信息
"""
messages = []
try:
if observations and len(observations) > 0:
obs = observations[0]
if hasattr(obs, "get_talking_message"):
obs: ObsInfo
raw_messages = obs.get_talking_message()
# 转换为简化格式
for msg in raw_messages[-count:]:
simple_msg = {
"sender": msg.get("sender", "未知"),
"content": msg.get("content", ""),
"timestamp": msg.get("timestamp", 0),
}
messages.append(simple_msg)
logger.debug(f"[ChatAPI] 获取到 {len(messages)} 条最近消息")
except Exception as e:
logger.error(f"[ChatAPI] 获取最近消息失败: {e}")
return messages
@staticmethod
def get_streams_summary() -> Dict[str, int]:
"""获取聊天流统计摘要
Returns:
Dict[str, int]: 包含各种统计信息的字典
"""
try:
all_streams = ChatManager.get_all_streams()
group_streams = ChatManager.get_group_streams()
private_streams = ChatManager.get_private_streams()
summary = {
"total_streams": len(all_streams),
"group_streams": len(group_streams),
"private_streams": len(private_streams),
"qq_streams": len([s for s in all_streams if s.platform == "qq"]),
}
logger.debug(f"[ChatAPI] 聊天流统计: {summary}")
return summary
except Exception as e:
logger.error(f"[ChatAPI] 获取聊天流统计失败: {e}")
return {"total_streams": 0, "group_streams": 0, "private_streams": 0, "qq_streams": 0}
# =============================================================================
# 模块级别的便捷函数 - 类似 requests.get(), requests.post() 的设计
# =============================================================================
def get_all_streams(platform: str = "qq") -> List[ChatStream]:
"""获取所有聊天流的便捷函数"""
return ChatManager.get_all_streams(platform)
def get_group_streams(platform: str = "qq") -> List[ChatStream]:
"""获取群聊聊天流的便捷函数"""
return ChatManager.get_group_streams(platform)
def get_private_streams(platform: str = "qq") -> List[ChatStream]:
"""获取私聊聊天流的便捷函数"""
return ChatManager.get_private_streams(platform)
def get_stream_by_group_id(group_id: str, platform: str = "qq") -> Optional[ChatStream]:
"""根据群ID获取聊天流的便捷函数"""
return ChatManager.get_stream_by_group_id(group_id, platform)
def get_stream_by_user_id(user_id: str, platform: str = "qq") -> Optional[ChatStream]:
"""根据用户ID获取私聊流的便捷函数"""
return ChatManager.get_stream_by_user_id(user_id, platform)
def get_stream_type(chat_stream: ChatStream) -> str:
"""获取聊天流类型的便捷函数"""
return ChatManager.get_stream_type(chat_stream)
def get_stream_info(chat_stream: ChatStream) -> Dict[str, Any]:
"""获取聊天流信息的便捷函数"""
return ChatManager.get_stream_info(chat_stream)
def get_streams_summary() -> Dict[str, int]:
"""获取聊天流统计摘要的便捷函数"""
return ChatManager.get_streams_summary()

View File

@@ -1,3 +1,12 @@
"""配置API模块
提供了配置读取和用户信息获取等功能
使用方式:
from src.plugin_system.apis import config_api
value = config_api.get_global_config("section.key")
platform, user_id = await config_api.get_user_id_by_person_name("用户名")
"""
from typing import Any from typing import Any
from src.common.logger import get_logger from src.common.logger import get_logger
from src.config.config import global_config from src.config.config import global_config
@@ -6,13 +15,11 @@ from src.person_info.person_info import get_person_info_manager
logger = get_logger("config_api") logger = get_logger("config_api")
class ConfigAPI: # =============================================================================
"""配置API模块 # 配置访问API函数
# =============================================================================
提供了配置读取和用户信息获取等功能 def get_global_config(key: str, default: Any = None) -> Any:
"""
def get_global_config(self, key: str, default: Any = None) -> Any:
""" """
安全地从全局配置中获取一个值。 安全地从全局配置中获取一个值。
插件应使用此方法读取全局配置,以保证只读和隔离性。 插件应使用此方法读取全局配置,以保证只读和隔离性。
@@ -36,22 +43,22 @@ class ConfigAPI:
return default return default
return current return current
except Exception as e: except Exception as e:
logger.warning(f"获取全局配置 {key} 失败: {e}") logger.warning(f"[ConfigAPI] 获取全局配置 {key} 失败: {e}")
return default return default
def get_config(self, key: str, default: Any = None) -> Any:
def get_plugin_config(plugin_config: dict, key: str, default: Any = None) -> Any:
""" """
从插件配置中获取值,支持嵌套键访问 从插件配置中获取值,支持嵌套键访问
Args: Args:
plugin_config: 插件配置字典
key: 配置键名,支持嵌套访问如 "section.subsection.key" key: 配置键名,支持嵌套访问如 "section.subsection.key"
default: 如果配置不存在时返回的默认值 default: 如果配置不存在时返回的默认值
Returns: Returns:
Any: 配置值或默认值 Any: 配置值或默认值
""" """
# 获取插件配置
plugin_config = getattr(self, "_plugin_config", {})
if not plugin_config: if not plugin_config:
return default return default
@@ -67,7 +74,12 @@ class ConfigAPI:
return current return current
async def get_user_id_by_person_name(self, person_name: str) -> tuple[str, str]:
# =============================================================================
# 用户信息API函数
# =============================================================================
async def get_user_id_by_person_name(person_name: str) -> tuple[str, str]:
"""根据用户名获取用户ID """根据用户名获取用户ID
Args: Args:
@@ -76,13 +88,18 @@ class ConfigAPI:
Returns: Returns:
tuple[str, str]: (平台, 用户ID) tuple[str, str]: (平台, 用户ID)
""" """
try:
person_info_manager = get_person_info_manager() person_info_manager = get_person_info_manager()
person_id = person_info_manager.get_person_id_by_person_name(person_name) person_id = person_info_manager.get_person_id_by_person_name(person_name)
user_id = await person_info_manager.get_value(person_id, "user_id") user_id = await person_info_manager.get_value(person_id, "user_id")
platform = await person_info_manager.get_value(person_id, "platform") platform = await person_info_manager.get_value(person_id, "platform")
return platform, user_id return platform, user_id
except Exception as e:
logger.error(f"[ConfigAPI] 根据用户名获取用户ID失败: {e}")
return "", ""
async def get_person_info(self, person_id: str, key: str, default: Any = None) -> Any:
async def get_person_info(person_id: str, key: str, default: Any = None) -> Any:
"""获取用户信息 """获取用户信息
Args: Args:
@@ -93,5 +110,9 @@ class ConfigAPI:
Returns: Returns:
Any: 用户信息值或默认值 Any: 用户信息值或默认值
""" """
try:
person_info_manager = get_person_info_manager() person_info_manager = get_person_info_manager()
return await person_info_manager.get_value(person_id, key, default) return await person_info_manager.get_value(person_id, key, default)
except Exception as e:
logger.error(f"[ConfigAPI] 获取用户信息失败: {e}")
return default

View File

@@ -1,68 +1,24 @@
"""数据库API模块
提供数据库操作相关功能采用标准Python包设计模式
使用方式:
from src.plugin_system.apis import database_api
records = await database_api.db_query(ActionRecords, query_type="get")
record = await database_api.db_save(ActionRecords, data={"action_id": "123"})
"""
import traceback import traceback
import time
from typing import Dict, List, Any, Union, Type from typing import Dict, List, Any, Union, Type
from src.common.logger import get_logger from src.common.logger import get_logger
from src.common.database.database_model import ActionRecords
from src.common.database.database import db
from peewee import Model, DoesNotExist from peewee import Model, DoesNotExist
logger = get_logger("database_api") logger = get_logger("database_api")
# =============================================================================
class DatabaseAPI: # 通用数据库查询API函数
"""数据库API模块 # =============================================================================
提供了数据库操作相关的功能
"""
async def store_action_info(
self,
action_build_into_prompt: bool = False,
action_prompt_display: str = "",
action_done: bool = True,
thinking_id: str = "",
action_data: dict = None,
) -> None:
"""存储action信息到数据库
Args:
action_build_into_prompt: 是否构建到提示中
action_prompt_display: 显示的action提示信息
action_done: action是否完成
thinking_id: 思考ID
action_data: action数据如果不提供则使用空字典
"""
try:
chat_stream = self.get_service("chat_stream")
if not chat_stream:
logger.error(f"{self.log_prefix} 无法存储action信息缺少chat_stream服务")
return
action_time = time.time()
action_id = f"{action_time}_{thinking_id}"
ActionRecords.create(
action_id=action_id,
time=action_time,
action_name=self.__class__.__name__,
action_data=str(action_data or {}),
action_done=action_done,
action_build_into_prompt=action_build_into_prompt,
action_prompt_display=action_prompt_display,
chat_id=chat_stream.stream_id,
chat_info_stream_id=chat_stream.stream_id,
chat_info_platform=chat_stream.platform,
user_id=chat_stream.user_info.user_id if chat_stream.user_info else "",
user_nickname=chat_stream.user_info.user_nickname if chat_stream.user_info else "",
user_cardname=chat_stream.user_info.user_cardname if chat_stream.user_info else "",
)
logger.debug(f"{self.log_prefix} 已存储action信息: {action_prompt_display}")
except Exception as e:
logger.error(f"{self.log_prefix} 存储action信息时出错: {e}")
traceback.print_exc()
async def db_query( async def db_query(
self,
model_class: Type[Model], model_class: Type[Model],
query_type: str = "get", query_type: str = "get",
filters: Dict[str, Any] = None, filters: Dict[str, Any] = None,
@@ -94,7 +50,7 @@ class DatabaseAPI:
示例: 示例:
# 查询最近10条消息 # 查询最近10条消息
messages = await self.db_query( messages = await database_api.db_query(
Messages, Messages,
query_type="get", query_type="get",
filters={"chat_id": chat_stream.stream_id}, filters={"chat_id": chat_stream.stream_id},
@@ -103,14 +59,14 @@ class DatabaseAPI:
) )
# 创建一条记录 # 创建一条记录
new_record = await self.db_query( new_record = await database_api.db_query(
ActionRecords, ActionRecords,
query_type="create", query_type="create",
data={"action_id": "123", "time": time.time(), "action_name": "TestAction"} data={"action_id": "123", "time": time.time(), "action_name": "TestAction"}
) )
# 更新记录 # 更新记录
updated_count = await self.db_query( updated_count = await database_api.db_query(
ActionRecords, ActionRecords,
query_type="update", query_type="update",
filters={"action_id": "123"}, filters={"action_id": "123"},
@@ -118,14 +74,14 @@ class DatabaseAPI:
) )
# 删除记录 # 删除记录
deleted_count = await self.db_query( deleted_count = await database_api.db_query(
ActionRecords, ActionRecords,
query_type="delete", query_type="delete",
filters={"action_id": "123"} filters={"action_id": "123"}
) )
# 计数 # 计数
count = await self.db_query( count = await database_api.db_query(
Messages, Messages,
query_type="count", query_type="count",
filters={"chat_id": chat_stream.stream_id} filters={"chat_id": chat_stream.stream_id}
@@ -199,7 +155,7 @@ class DatabaseAPI:
return [] return []
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix} 数据库操作出错: {e}") logger.error(f"[DatabaseAPI] 数据库操作出错: {e}")
traceback.print_exc() traceback.print_exc()
# 根据查询类型返回合适的默认值 # 根据查询类型返回合适的默认值
@@ -209,48 +165,9 @@ class DatabaseAPI:
return None return None
return None return None
async def db_raw_query(
self, sql: str, params: List[Any] = None, fetch_results: bool = True
) -> Union[List[Dict[str, Any]], int, None]:
"""执行原始SQL查询
警告: 使用此方法需要小心确保SQL语句已正确构造以避免SQL注入风险。
Args:
sql: 原始SQL查询字符串
params: 查询参数列表用于替换SQL中的占位符
fetch_results: 是否获取查询结果对于SELECT查询设为True对于
UPDATE/INSERT/DELETE等操作设为False
Returns:
如果fetch_results为True返回查询结果列表
如果fetch_results为False返回受影响的行数
如果出错返回None
"""
try:
cursor = db.execute_sql(sql, params or [])
if fetch_results:
# 获取列名
columns = [col[0] for col in cursor.description]
# 构建结果字典列表
results = []
for row in cursor.fetchall():
results.append(dict(zip(columns, row)))
return results
else:
# 返回受影响的行数
return cursor.rowcount
except Exception as e:
logger.error(f"{self.log_prefix} 执行原始SQL查询出错: {e}")
traceback.print_exc()
return None
async def db_save( async def db_save(
self, model_class: Type[Model], data: Dict[str, Any], key_field: str = None, key_value: Any = None model_class: Type[Model], data: Dict[str, Any], key_field: str = None, key_value: Any = None
) -> Union[Dict[str, Any], None]: ) -> Union[Dict[str, Any], None]:
"""保存数据到数据库(创建或更新) """保存数据到数据库(创建或更新)
@@ -269,7 +186,7 @@ class DatabaseAPI:
示例: 示例:
# 创建或更新一条记录 # 创建或更新一条记录
record = await self.db_save( record = await database_api.db_save(
ActionRecords, ActionRecords,
{ {
"action_id": "123", "action_id": "123",
@@ -308,12 +225,13 @@ class DatabaseAPI:
return created_record return created_record
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix} 保存数据库记录出错: {e}") logger.error(f"[DatabaseAPI] 保存数据库记录出错: {e}")
traceback.print_exc() traceback.print_exc()
return None return None
async def db_get( async def db_get(
self, model_class: Type[Model], filters: Dict[str, Any] = None, order_by: str = None, limit: int = None model_class: Type[Model], filters: Dict[str, Any] = None, order_by: str = None, limit: int = None
) -> Union[List[Dict[str, Any]], Dict[str, Any], None]: ) -> Union[List[Dict[str, Any]], Dict[str, Any], None]:
"""从数据库获取记录 """从数据库获取记录
@@ -331,14 +249,14 @@ class DatabaseAPI:
示例: 示例:
# 获取单个记录 # 获取单个记录
record = await self.db_get( record = await database_api.db_get(
ActionRecords, ActionRecords,
filters={"action_id": "123"}, filters={"action_id": "123"},
limit=1 limit=1
) )
# 获取最近10条记录 # 获取最近10条记录
records = await self.db_get( records = await database_api.db_get(
Messages, Messages,
filters={"chat_id": chat_stream.stream_id}, filters={"chat_id": chat_stream.stream_id},
order_by="-time", order_by="-time",
@@ -374,6 +292,95 @@ class DatabaseAPI:
return results return results
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix} 获取数据库记录出错: {e}") logger.error(f"[DatabaseAPI] 获取数据库记录出错: {e}")
traceback.print_exc() traceback.print_exc()
return None if limit == 1 else [] return None if limit == 1 else []
async def store_action_info(
chat_stream=None,
action_build_into_prompt: bool = False,
action_prompt_display: str = "",
action_done: bool = True,
thinking_id: str = "",
action_data: dict = None,
action_name: str = "",
) -> Union[Dict[str, Any], None]:
"""存储动作信息到数据库
将Action执行的相关信息保存到ActionRecords表中用于后续的记忆和上下文构建。
Args:
chat_stream: 聊天流对象,包含聊天相关信息
action_build_into_prompt: 是否将此动作构建到提示中
action_prompt_display: 动作的提示显示文本
action_done: 动作是否完成
thinking_id: 关联的思考ID
action_data: 动作数据字典
action_name: 动作名称
Returns:
Dict[str, Any]: 保存的记录数据
None: 如果保存失败
示例:
record = await database_api.store_action_info(
chat_stream=chat_stream,
action_build_into_prompt=True,
action_prompt_display="执行了回复动作",
action_done=True,
thinking_id="thinking_123",
action_data={"content": "Hello"},
action_name="reply_action"
)
"""
try:
import time
import json
from src.common.database.database_model import ActionRecords
# 构建动作记录数据
record_data = {
"action_id": thinking_id or str(int(time.time() * 1000000)), # 使用thinking_id或生成唯一ID
"time": time.time(),
"action_name": action_name,
"action_data": json.dumps(action_data or {}, ensure_ascii=False),
"action_done": action_done,
"action_build_into_prompt": action_build_into_prompt,
"action_prompt_display": action_prompt_display,
}
# 从chat_stream获取聊天信息
if chat_stream:
record_data.update({
"chat_id": getattr(chat_stream, 'stream_id', ''),
"chat_info_stream_id": getattr(chat_stream, 'stream_id', ''),
"chat_info_platform": getattr(chat_stream, 'platform', ''),
})
else:
# 如果没有chat_stream设置默认值
record_data.update({
"chat_id": "",
"chat_info_stream_id": "",
"chat_info_platform": "",
})
# 使用已有的db_save函数保存记录
saved_record = await db_save(
ActionRecords,
data=record_data,
key_field="action_id",
key_value=record_data["action_id"]
)
if saved_record:
logger.info(f"[DatabaseAPI] 成功存储动作信息: {action_name} (ID: {record_data['action_id']})")
else:
logger.error(f"[DatabaseAPI] 存储动作信息失败: {action_name}")
return saved_record
except Exception as e:
logger.error(f"[DatabaseAPI] 存储动作信息时发生错误: {e}")
traceback.print_exc()
return None

View File

@@ -0,0 +1,219 @@
"""
表情API模块
提供表情包相关功能采用标准Python包设计模式
使用方式:
from src.plugin_system.apis import emoji_api
result = await emoji_api.get_by_description("开心")
count = emoji_api.get_count()
"""
from typing import Optional, Tuple
from src.common.logger import get_logger
from src.chat.emoji_system.emoji_manager import get_emoji_manager
from src.chat.utils.utils_image import image_path_to_base64
logger = get_logger("emoji_api")
# =============================================================================
# 表情包获取API函数
# =============================================================================
async def get_by_description(description: str) -> Optional[Tuple[str, str, str]]:
"""根据描述选择表情包
Args:
description: 表情包的描述文本,例如"开心""难过""愤怒"
Returns:
Optional[Tuple[str, str, str]]: (base64编码, 表情包描述, 匹配的情感标签) 或 None
"""
try:
logger.info(f"[EmojiAPI] 根据描述获取表情包: {description}")
emoji_manager = get_emoji_manager()
emoji_result = await emoji_manager.get_emoji_for_text(description)
if not emoji_result:
logger.warning(f"[EmojiAPI] 未找到匹配描述 '{description}' 的表情包")
return None
emoji_path, emoji_description, matched_emotion = emoji_result
emoji_base64 = image_path_to_base64(emoji_path)
if not emoji_base64:
logger.error(f"[EmojiAPI] 无法将表情包文件转换为base64: {emoji_path}")
return None
logger.info(f"[EmojiAPI] 成功获取表情包: {emoji_description}, 匹配情感: {matched_emotion}")
return emoji_base64, emoji_description, matched_emotion
except Exception as e:
logger.error(f"[EmojiAPI] 获取表情包失败: {e}")
return None
async def get_random() -> Optional[Tuple[str, str, str]]:
"""随机获取表情包
Returns:
Optional[Tuple[str, str, str]]: (base64编码, 表情包描述, 随机情感标签) 或 None
"""
try:
logger.info("[EmojiAPI] 随机获取表情包")
emoji_manager = get_emoji_manager()
all_emojis = emoji_manager.emoji_objects
if not all_emojis:
logger.warning("[EmojiAPI] 没有可用的表情包")
return None
# 过滤有效表情包
valid_emojis = [emoji for emoji in all_emojis if not emoji.is_deleted]
if not valid_emojis:
logger.warning("[EmojiAPI] 没有有效的表情包")
return None
# 随机选择
import random
selected_emoji = random.choice(valid_emojis)
emoji_base64 = image_path_to_base64(selected_emoji.full_path)
if not emoji_base64:
logger.error(f"[EmojiAPI] 无法转换表情包为base64: {selected_emoji.full_path}")
return None
matched_emotion = random.choice(selected_emoji.emotion) if selected_emoji.emotion else "随机表情"
# 记录使用次数
emoji_manager.record_usage(selected_emoji.hash)
logger.info(f"[EmojiAPI] 成功获取随机表情包: {selected_emoji.description}")
return emoji_base64, selected_emoji.description, matched_emotion
except Exception as e:
logger.error(f"[EmojiAPI] 获取随机表情包失败: {e}")
return None
async def get_by_emotion(emotion: str) -> Optional[Tuple[str, str, str]]:
"""根据情感标签获取表情包
Args:
emotion: 情感标签,如"happy""sad""angry"
Returns:
Optional[Tuple[str, str, str]]: (base64编码, 表情包描述, 匹配的情感标签) 或 None
"""
try:
logger.info(f"[EmojiAPI] 根据情感获取表情包: {emotion}")
emoji_manager = get_emoji_manager()
all_emojis = emoji_manager.emoji_objects
# 筛选匹配情感的表情包
matching_emojis = []
for emoji_obj in all_emojis:
if not emoji_obj.is_deleted and emotion.lower() in [e.lower() for e in emoji_obj.emotion]:
matching_emojis.append(emoji_obj)
if not matching_emojis:
logger.warning(f"[EmojiAPI] 未找到匹配情感 '{emotion}' 的表情包")
return None
# 随机选择匹配的表情包
import random
selected_emoji = random.choice(matching_emojis)
emoji_base64 = image_path_to_base64(selected_emoji.full_path)
if not emoji_base64:
logger.error(f"[EmojiAPI] 无法转换表情包为base64: {selected_emoji.full_path}")
return None
# 记录使用次数
emoji_manager.record_usage(selected_emoji.hash)
logger.info(f"[EmojiAPI] 成功获取情感表情包: {selected_emoji.description}")
return emoji_base64, selected_emoji.description, emotion
except Exception as e:
logger.error(f"[EmojiAPI] 根据情感获取表情包失败: {e}")
return None
# =============================================================================
# 表情包信息查询API函数
# =============================================================================
def get_count() -> int:
"""获取表情包数量
Returns:
int: 当前可用的表情包数量
"""
try:
emoji_manager = get_emoji_manager()
return emoji_manager.emoji_num
except Exception as e:
logger.error(f"[EmojiAPI] 获取表情包数量失败: {e}")
return 0
def get_info() -> dict:
"""获取表情包系统信息
Returns:
dict: 包含表情包数量、最大数量等信息
"""
try:
emoji_manager = get_emoji_manager()
return {
"current_count": emoji_manager.emoji_num,
"max_count": emoji_manager.emoji_num_max,
"available_emojis": len([e for e in emoji_manager.emoji_objects if not e.is_deleted]),
}
except Exception as e:
logger.error(f"[EmojiAPI] 获取表情包信息失败: {e}")
return {"current_count": 0, "max_count": 0, "available_emojis": 0}
def get_emotions() -> list:
"""获取所有可用的情感标签
Returns:
list: 所有表情包的情感标签列表(去重)
"""
try:
emoji_manager = get_emoji_manager()
emotions = set()
for emoji_obj in emoji_manager.emoji_objects:
if not emoji_obj.is_deleted and emoji_obj.emotion:
emotions.update(emoji_obj.emotion)
return sorted(list(emotions))
except Exception as e:
logger.error(f"[EmojiAPI] 获取情感标签失败: {e}")
return []
def get_descriptions() -> list:
"""获取所有表情包描述
Returns:
list: 所有可用表情包的描述列表
"""
try:
emoji_manager = get_emoji_manager()
descriptions = []
for emoji_obj in emoji_manager.emoji_objects:
if not emoji_obj.is_deleted and emoji_obj.description:
descriptions.append(emoji_obj.description)
return descriptions
except Exception as e:
logger.error(f"[EmojiAPI] 获取表情包描述失败: {e}")
return []

View File

@@ -0,0 +1,170 @@
"""
回复器API模块
提供回复器相关功能采用标准Python包设计模式
使用方式:
from src.plugin_system.apis import generator_api
replyer = generator_api.get_replyer(chat_stream)
success, reply_set = await generator_api.generate_reply(chat_stream, action_data, reasoning)
"""
from typing import Tuple, Any, Dict, List
from src.common.logger import get_logger
from src.chat.focus_chat.replyer.default_generator import DefaultReplyer
from src.chat.message_receive.chat_stream import get_chat_manager
logger = get_logger("generator_api")
# =============================================================================
# 回复器获取API函数
# =============================================================================
def get_replyer(chat_stream=None, platform: str = None, chat_id: str = None, is_group: bool = True) -> DefaultReplyer:
"""获取回复器对象
优先使用chat_stream如果没有则使用platform和chat_id组合
Args:
chat_stream: 聊天流对象(优先)
platform: 平台名称,如"qq"
chat_id: 聊天ID群ID或用户ID
is_group: 是否为群聊
Returns:
Optional[Any]: 回复器对象如果获取失败则返回None
"""
try:
# 优先使用聊天流
if chat_stream:
logger.debug("[GeneratorAPI] 使用聊天流获取回复器")
return DefaultReplyer(chat_stream=chat_stream)
# 使用平台和ID组合
if platform and chat_id:
logger.debug("[GeneratorAPI] 使用平台和ID获取回复器")
chat_manager = get_chat_manager()
if not chat_manager:
logger.warning("[GeneratorAPI] 无法获取聊天管理器")
return None
# 查找对应的聊天流
target_stream = None
for _stream_id, stream in chat_manager.streams.items():
if stream.platform == platform:
if is_group and stream.group_info:
if str(stream.group_info.group_id) == str(chat_id):
target_stream = stream
break
elif not is_group and stream.user_info:
if str(stream.user_info.user_id) == str(chat_id):
target_stream = stream
break
return DefaultReplyer(chat_stream=target_stream)
logger.warning("[GeneratorAPI] 缺少必要参数,无法获取回复器")
return None
except Exception as e:
logger.error(f"[GeneratorAPI] 获取回复器失败: {e}")
return None
# =============================================================================
# 回复生成API函数
# =============================================================================
async def generate_reply(
chat_stream=None,
action_data: Dict[str, Any] = None,
platform: str = None,
chat_id: str = None,
is_group: bool = True
) -> Tuple[bool, List[Tuple[str, Any]]]:
"""生成回复
Args:
chat_stream: 聊天流对象(优先)
action_data: 动作数据
reasoning: 推理原因
thinking_id: 思考ID
cycle_timers: 循环计时器
anchor_message: 锚点消息
platform: 平台名称(备用)
chat_id: 聊天ID备用
is_group: 是否为群聊(备用)
Returns:
Tuple[bool, List[Tuple[str, Any]]]: (是否成功, 回复集合)
"""
try:
# 获取回复器
replyer = get_replyer(chat_stream, platform, chat_id, is_group)
if not replyer:
logger.error("[GeneratorAPI] 无法获取回复器")
return False, []
logger.info("[GeneratorAPI] 开始生成回复")
# 调用回复器生成回复
success, reply_set = await replyer.generate_reply_with_context(
reply_data=action_data or {},
)
if success:
logger.info(f"[GeneratorAPI] 回复生成成功,生成了 {len(reply_set)} 个回复项")
else:
logger.warning("[GeneratorAPI] 回复生成失败")
return success, reply_set or []
except Exception as e:
logger.error(f"[GeneratorAPI] 生成回复时出错: {e}")
return False, []
async def rewrite_reply(
chat_stream=None,
reply_data: Dict[str, Any] = None,
platform: str = None,
chat_id: str = None,
is_group: bool = True
) -> Tuple[bool, List[Tuple[str, Any]]]:
"""重写回复
Args:
chat_stream: 聊天流对象(优先)
action_data: 动作数据
platform: 平台名称(备用)
chat_id: 聊天ID备用
is_group: 是否为群聊(备用)
Returns:
Tuple[bool, List[Tuple[str, Any]]]: (是否成功, 回复集合)
"""
try:
# 获取回复器
replyer = get_replyer(chat_stream, platform, chat_id, is_group)
if not replyer:
logger.error("[GeneratorAPI] 无法获取回复器")
return False, []
logger.info("[GeneratorAPI] 开始重写回复")
# 调用回复器重写回复
success, reply_set = await replyer.rewrite_reply_with_context(
reply_data=reply_data or {},
)
if success:
logger.info(f"[GeneratorAPI] 重写回复成功,生成了 {len(reply_set)} 个回复项")
else:
logger.warning("[GeneratorAPI] 重写回复失败")
return success, reply_set or []
except Exception as e:
logger.error(f"[GeneratorAPI] 重写回复时出错: {e}")
return False, []

View File

@@ -1,177 +0,0 @@
from typing import Optional, List, Any, Tuple
from src.common.logger import get_logger
logger = get_logger("hearflow_api")
def _get_heartflow():
"""获取heartflow实例的延迟导入函数"""
from src.chat.heart_flow.heartflow import heartflow
return heartflow
def _get_subheartflow_types():
"""获取SubHeartflow和ChatState类型的延迟导入函数"""
from src.chat.heart_flow.sub_heartflow import SubHeartflow, ChatState
return SubHeartflow, ChatState
class HearflowAPI:
"""心流API模块
提供与心流和子心流相关的操作接口
"""
def __init__(self):
self.log_prefix = "[HearflowAPI]"
async def get_sub_hearflow_by_chat_id(self, chat_id: str) -> Optional[Any]:
"""根据chat_id获取指定的sub_hearflow实例
Args:
chat_id: 聊天ID与sub_hearflow的subheartflow_id相同
Returns:
Optional[SubHeartflow]: sub_hearflow实例如果不存在则返回None
"""
# 使用延迟导入
heartflow = _get_heartflow()
# 直接从subheartflow_manager获取已存在的子心流
# 使用锁来确保线程安全
async with heartflow.subheartflow_manager._lock:
subflow = heartflow.subheartflow_manager.subheartflows.get(chat_id)
if subflow and not subflow.should_stop:
logger.debug(f"{self.log_prefix} 成功获取子心流实例: {chat_id}")
return subflow
else:
logger.debug(f"{self.log_prefix} 子心流不存在或已停止: {chat_id}")
return None
async def get_or_create_sub_hearflow_by_chat_id(self, chat_id: str) -> Optional[Any]:
"""根据chat_id获取或创建sub_hearflow实例
Args:
chat_id: 聊天ID
Returns:
Optional[SubHeartflow]: sub_hearflow实例创建失败时返回None
"""
heartflow = _get_heartflow()
return await heartflow.get_or_create_subheartflow(chat_id)
def get_all_sub_hearflow_ids(self) -> List[str]:
"""获取所有子心流的ID列表
Returns:
List[str]: 所有子心流的ID列表
"""
heartflow = _get_heartflow()
all_subflows = heartflow.subheartflow_manager.get_all_subheartflows()
chat_ids = [subflow.chat_id for subflow in all_subflows if not subflow.should_stop]
logger.debug(f"{self.log_prefix} 获取到 {len(chat_ids)} 个活跃的子心流ID")
return chat_ids
def get_all_sub_hearflows(self) -> List[Any]:
"""获取所有子心流实例
Returns:
List[SubHeartflow]: 所有活跃的子心流实例列表
"""
heartflow = _get_heartflow()
all_subflows = heartflow.subheartflow_manager.get_all_subheartflows()
active_subflows = [subflow for subflow in all_subflows if not subflow.should_stop]
logger.debug(f"{self.log_prefix} 获取到 {len(active_subflows)} 个活跃的子心流实例")
return active_subflows
async def get_sub_hearflow_chat_state(self, chat_id: str) -> Optional[Any]:
"""获取指定子心流的聊天状态
Args:
chat_id: 聊天ID
Returns:
Optional[ChatState]: 聊天状态如果子心流不存在则返回None
"""
subflow = await self.get_sub_hearflow_by_chat_id(chat_id)
if subflow:
return subflow.chat_state.chat_status
return None
async def set_sub_hearflow_chat_state(self, chat_id: str, target_state: Any) -> bool:
"""设置指定子心流的聊天状态
Args:
chat_id: 聊天ID
target_state: 目标状态(ChatState枚举值)
Returns:
bool: 是否设置成功
"""
heartflow = _get_heartflow()
return await heartflow.subheartflow_manager.force_change_state(chat_id, target_state)
async def get_sub_hearflow_replyer_and_expressor(self, chat_id: str) -> Tuple[Optional[Any], Optional[Any]]:
"""根据chat_id获取指定子心流的replyer和expressor实例
Args:
chat_id: 聊天ID
Returns:
Tuple[Optional[Any], Optional[Any]]: (replyer实例, expressor实例)如果子心流不存在或未处于FOCUSED状态返回(None, None)
"""
subflow = await self.get_sub_hearflow_by_chat_id(chat_id)
if not subflow:
logger.debug(f"{self.log_prefix} 子心流不存在: {chat_id}")
return None, None
# 使用延迟导入获取ChatState
_, ChatState = _get_subheartflow_types()
# 检查子心流是否处于FOCUSED状态且有HeartFC实例
if subflow.chat_state.chat_status != ChatState.FOCUSED:
logger.debug(
f"{self.log_prefix} 子心流 {chat_id} 未处于FOCUSED状态当前状态: {subflow.chat_state.chat_status.value}"
)
return None, None
if not subflow.heart_fc_instance:
logger.debug(f"{self.log_prefix} 子心流 {chat_id} 没有HeartFC实例")
return None, None
# 返回replyer和expressor实例
replyer = subflow.heart_fc_instance.replyer
expressor = subflow.heart_fc_instance.expressor
if replyer and expressor:
logger.debug(f"{self.log_prefix} 成功获取子心流 {chat_id} 的replyer和expressor")
else:
logger.warning(f"{self.log_prefix} 子心流 {chat_id} 的replyer或expressor为空")
return replyer, expressor
async def get_sub_hearflow_replyer(self, chat_id: str) -> Optional[Any]:
"""根据chat_id获取指定子心流的replyer实例
Args:
chat_id: 聊天ID
Returns:
Optional[Any]: replyer实例如果不存在则返回None
"""
replyer, _ = await self.get_sub_hearflow_replyer_and_expressor(chat_id)
return replyer
async def get_sub_hearflow_expressor(self, chat_id: str) -> Optional[Any]:
"""根据chat_id获取指定子心流的expressor实例
Args:
chat_id: 聊天ID
Returns:
Optional[Any]: expressor实例如果不存在则返回None
"""
_, expressor = await self.get_sub_hearflow_replyer_and_expressor(chat_id)
return expressor

View File

@@ -1,134 +0,0 @@
"""
独立API聚合模块
聚合了不需要Action组件依赖的API这些API可以独立使用不需要注入服务对象。
包括LLMAPI、ConfigAPI、UtilsAPI、StreamAPI、HearflowAPI等独立功能的API。
"""
from src.plugin_system.apis.llm_api import LLMAPI
from src.plugin_system.apis.config_api import ConfigAPI
from src.plugin_system.apis.utils_api import UtilsAPI
from src.plugin_system.apis.stream_api import StreamAPI
from src.plugin_system.apis.hearflow_api import HearflowAPI
from src.common.logger import get_logger
logger = get_logger("independent_apis")
class IndependentAPI(LLMAPI, ConfigAPI, UtilsAPI, StreamAPI, HearflowAPI):
"""
独立API聚合类
聚合了不需要Action组件依赖的API功能。这些API的特点
- 不需要chat_stream、expressor等服务对象
- 可以独立调用不依赖Action执行上下文
- 主要是工具类方法和配置查询方法
包含的API
- LLMAPI: LLM模型调用仅需要全局配置
- ConfigAPI: 配置读取(使用全局配置)
- UtilsAPI: 工具方法(文件操作、时间处理等)
- StreamAPI: 聊天流查询使用ChatManager
- HearflowAPI: 心流状态控制使用heartflow
使用场景:
- 在Command组件中使用
- 独立的工具函数调用
- 配置查询和系统状态检查
"""
def __init__(self, log_prefix: str = "[IndependentAPI]"):
"""
初始化独立API
Args:
log_prefix: 日志前缀,用于区分不同的调用来源
"""
self.log_prefix = log_prefix
logger.debug(f"{self.log_prefix} IndependentAPI 初始化完成")
# 提供便捷的静态访问方式
class StaticAPI:
"""
静态API类
提供完全静态的API访问方式不需要实例化适合简单的工具调用。
"""
# LLM相关
@staticmethod
def get_available_models():
"""获取可用的LLM模型"""
api = LLMAPI()
return api.get_available_models()
@staticmethod
async def generate_with_model(prompt: str, model_config: dict, **kwargs):
"""使用LLM生成内容"""
api = LLMAPI()
api.log_prefix = "[StaticAPI]"
return await api.generate_with_model(prompt, model_config, **kwargs)
# 配置相关
@staticmethod
def get_global_config(key: str, default=None):
"""获取全局配置"""
api = ConfigAPI()
return api.get_global_config(key, default)
@staticmethod
async def get_user_id_by_name(person_name: str):
"""根据用户名获取用户ID"""
api = ConfigAPI()
return await api.get_user_id_by_person_name(person_name)
# 工具相关
@staticmethod
def get_timestamp():
"""获取当前时间戳"""
api = UtilsAPI()
return api.get_timestamp()
@staticmethod
def format_time(timestamp=None, format_str="%Y-%m-%d %H:%M:%S"):
"""格式化时间"""
api = UtilsAPI()
return api.format_time(timestamp, format_str)
@staticmethod
def generate_unique_id():
"""生成唯一ID"""
api = UtilsAPI()
return api.generate_unique_id()
# 聊天流相关
@staticmethod
def get_chat_stream_by_group_id(group_id: str, platform: str = "qq"):
"""通过群ID获取聊天流"""
api = StreamAPI()
api.log_prefix = "[StaticAPI]"
return api.get_chat_stream_by_group_id(group_id, platform)
@staticmethod
def get_all_group_chat_streams(platform: str = "qq"):
"""获取所有群聊聊天流"""
api = StreamAPI()
api.log_prefix = "[StaticAPI]"
return api.get_all_group_chat_streams(platform)
# 心流相关
@staticmethod
async def get_sub_hearflow_by_chat_id(chat_id: str):
"""获取子心流"""
api = HearflowAPI()
api.log_prefix = "[StaticAPI]"
return await api.get_sub_hearflow_by_chat_id(chat_id)
@staticmethod
async def set_sub_hearflow_chat_state(chat_id: str, target_state):
"""设置子心流状态"""
api = HearflowAPI()
api.log_prefix = "[StaticAPI]"
return await api.set_sub_hearflow_chat_state(chat_id, target_state)

View File

@@ -1,3 +1,12 @@
"""LLM API模块
提供了与LLM模型交互的功能
使用方式:
from src.plugin_system.apis import llm_api
models = llm_api.get_available_models()
success, response, reasoning, model_name = await llm_api.generate_with_model(prompt, model_config)
"""
from typing import Tuple, Dict, Any from typing import Tuple, Dict, Any
from src.common.logger import get_logger from src.common.logger import get_logger
from src.llm_models.utils_model import LLMRequest from src.llm_models.utils_model import LLMRequest
@@ -6,28 +15,30 @@ from src.config.config import global_config
logger = get_logger("llm_api") logger = get_logger("llm_api")
class LLMAPI: # =============================================================================
"""LLM API模块 # LLM模型API函数
# =============================================================================
提供了与LLM模型交互的功能 def get_available_models() -> Dict[str, Any]:
"""
def get_available_models(self) -> Dict[str, Any]:
"""获取所有可用的模型配置 """获取所有可用的模型配置
Returns: Returns:
Dict[str, Any]: 模型配置字典key为模型名称value为模型配置 Dict[str, Any]: 模型配置字典key为模型名称value为模型配置
""" """
try:
if not hasattr(global_config, "model"): if not hasattr(global_config, "model"):
logger.error(f"{self.log_prefix} 无法获取模型列表:全局配置中未找到 model 配置") logger.error("[LLMAPI] 无法获取模型列表:全局配置中未找到 model 配置")
return {} return {}
models = global_config.model models = global_config.model
return models return models
except Exception as e:
logger.error(f"[LLMAPI] 获取可用模型失败: {e}")
return {}
async def generate_with_model( async def generate_with_model(
self, prompt: str, model_config: Dict[str, Any], request_type: str = "plugin.generate", **kwargs prompt: str, model_config: Dict[str, Any], request_type: str = "plugin.generate", **kwargs
) -> Tuple[bool, str, str, str]: ) -> Tuple[bool, str, str, str]:
"""使用指定模型生成内容 """使用指定模型生成内容
@@ -41,7 +52,7 @@ class LLMAPI:
Tuple[bool, str, str, str]: (是否成功, 生成的内容, 推理过程, 模型名称) Tuple[bool, str, str, str]: (是否成功, 生成的内容, 推理过程, 模型名称)
""" """
try: try:
logger.info(f"{self.log_prefix} 使用模型生成内容,提示词: {prompt[:100]}...") logger.info(f"[LLMAPI] 使用模型生成内容,提示词: {prompt[:100]}...")
llm_request = LLMRequest(model=model_config, request_type=request_type, **kwargs) llm_request = LLMRequest(model=model_config, request_type=request_type, **kwargs)
@@ -50,5 +61,5 @@ class LLMAPI:
except Exception as e: except Exception as e:
error_msg = f"生成内容时出错: {str(e)}" error_msg = f"生成内容时出错: {str(e)}"
logger.error(f"{self.log_prefix} {error_msg}") logger.error(f"[LLMAPI] {error_msg}")
return False, error_msg, "", "" return False, error_msg, "", ""

View File

@@ -1,202 +1,329 @@
import traceback """
消息API模块
提供消息查询和构建成字符串的功能采用标准Python包设计模式
使用方式:
from src.plugin_system.apis import message_api
messages = message_api.get_messages_by_time_in_chat(chat_id, start_time, end_time)
readable_text = message_api.build_readable_messages(messages)
"""
from typing import List, Dict, Any, Tuple, Optional
import time import time
from typing import List, Dict, Any from src.chat.utils.chat_message_builder import (
from src.common.logger import get_logger get_raw_msg_by_timestamp,
from src.chat.focus_chat.hfc_utils import create_empty_anchor_message get_raw_msg_by_timestamp_with_chat,
get_raw_msg_by_timestamp_with_chat_inclusive,
# 以下为类型注解需要 get_raw_msg_by_timestamp_with_chat_users,
from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager get_raw_msg_by_timestamp_random,
from src.chat.focus_chat.info.obs_info import ObsInfo get_raw_msg_by_timestamp_with_users,
get_raw_msg_before_timestamp,
# 新增导入 get_raw_msg_before_timestamp_with_chat,
from src.chat.focus_chat.heartFC_sender import HeartFCSender get_raw_msg_before_timestamp_with_users,
from src.chat.message_receive.message import MessageSending num_new_messages_since,
from maim_message import Seg, UserInfo num_new_messages_since_with_users,
from src.config.config import global_config build_readable_messages,
build_readable_messages_with_list,
logger = get_logger("message_api") get_person_id_list,
)
class MessageAPI: # =============================================================================
"""消息API模块 # 消息查询API函数
# =============================================================================
提供了发送消息、获取消息历史等功能 def get_messages_by_time(
start_time: float, end_time: float, limit: int = 0, limit_mode: str = "latest"
) -> List[Dict[str, Any]]:
""" """
获取指定时间范围内的消息
async def send_message_to_target(
self,
message_type: str,
content: str,
platform: str,
target_id: str,
is_group: bool = True,
display_message: str = "",
typing: bool = False,
) -> bool:
"""直接向指定目标发送消息
Args: Args:
message_type: 消息类型,如"text""image""emoji" start_time: 开始时间戳
content: 消息内容 end_time: 结束时间戳
platform: 目标平台,如"qq" limit: 限制返回的消息数量0为不限制
target_id: 目标ID群ID或用户ID limit_mode: 当limit>0时生效'earliest'表示获取最早的记录,'latest'表示获取最新的记录
is_group: 是否为群聊True为群聊False为私聊
display_message: 显示消息(可选)
Returns: Returns:
bool: 是否发送成功 消息列表
""" """
try: return get_raw_msg_by_timestamp(start_time, end_time, limit, limit_mode)
# 构建目标聊天流ID
if is_group:
# 群聊:从数据库查找对应的聊天流
target_stream = None
for _, stream in get_chat_manager().streams.items():
if (
stream.group_info
and str(stream.group_info.group_id) == str(target_id)
and stream.platform == platform
):
target_stream = stream
break
if not target_stream:
logger.error(f"{getattr(self, 'log_prefix', '')} 未找到群ID为 {target_id} 的聊天流")
return False
else:
# 私聊:从数据库查找对应的聊天流
target_stream = None
for _, stream in get_chat_manager().streams.items():
if (
not stream.group_info
and str(stream.user_info.user_id) == str(target_id)
and stream.platform == platform
):
target_stream = stream
break
if not target_stream: def get_messages_by_time_in_chat(
logger.error(f"{getattr(self, 'log_prefix', '')} 未找到用户ID为 {target_id} 的私聊流") chat_id: str, start_time: float, end_time: float, limit: int = 0, limit_mode: str = "latest"
return False ) -> List[Dict[str, Any]]:
"""
# 创建HeartFCSender实例 获取指定聊天中指定时间范围内的消息
heart_fc_sender = HeartFCSender()
# 生成消息ID和thinking_id
current_time = time.time()
message_id = f"plugin_msg_{int(current_time * 1000)}"
# 构建机器人用户信息
bot_user_info = UserInfo(
user_id=global_config.bot.qq_account,
user_nickname=global_config.bot.nickname,
platform=platform,
)
# 创建消息段
message_segment = Seg(type=message_type, data=content)
# 创建空锚点消息(用于回复)
anchor_message = await create_empty_anchor_message(platform, target_stream.group_info, target_stream)
# 构建发送消息对象
bot_message = MessageSending(
message_id=message_id,
chat_stream=target_stream,
bot_user_info=bot_user_info,
sender_info=target_stream.user_info, # 目标用户信息
message_segment=message_segment,
display_message=display_message,
reply=anchor_message,
is_head=True,
is_emoji=(message_type == "emoji"),
thinking_start_time=current_time,
)
# 发送消息
sent_msg = await heart_fc_sender.send_message(
bot_message, has_thinking=False, typing=typing, set_reply=False
)
if sent_msg:
logger.info(f"{getattr(self, 'log_prefix', '')} 成功发送消息到 {platform}:{target_id}")
return True
else:
logger.error(f"{getattr(self, 'log_prefix', '')} 发送消息失败")
return False
except Exception as e:
logger.error(f"{getattr(self, 'log_prefix', '')} 向目标发送消息时出错: {e}")
traceback.print_exc()
return False
async def send_text_to_group(self, text: str, group_id: str, platform: str = "qq") -> bool:
"""便捷方法:向指定群聊发送文本消息
Args: Args:
text: 要发送的文本内容 chat_id: 聊天ID
group_id: 群聊ID start_time: 开始时间戳
platform: 平台,默认为"qq" end_time: 结束时间戳
limit: 限制返回的消息数量0为不限制
limit_mode: 当limit>0时生效'earliest'表示获取最早的记录,'latest'表示获取最新的记录
Returns: Returns:
bool: 是否发送成功 消息列表
""" """
return await self.send_message_to_target( return get_raw_msg_by_timestamp_with_chat(chat_id, start_time, end_time, limit, limit_mode)
message_type="text", content=text, platform=platform, target_id=group_id, is_group=True
)
async def send_text_to_user(self, text: str, user_id: str, platform: str = "qq") -> bool:
"""便捷方法:向指定用户发送私聊文本消息 def get_messages_by_time_in_chat_inclusive(
chat_id: str, start_time: float, end_time: float, limit: int = 0, limit_mode: str = "latest"
) -> List[Dict[str, Any]]:
"""
获取指定聊天中指定时间范围内的消息(包含边界)
Args: Args:
text: 要发送的文本内容 chat_id: 聊天ID
user_id: 用户ID start_time: 开始时间戳(包含)
platform: 平台,默认为"qq" end_time: 结束时间戳(包含)
limit: 限制返回的消息数量0为不限制
limit_mode: 当limit>0时生效'earliest'表示获取最早的记录,'latest'表示获取最新的记录
Returns: Returns:
bool: 是否发送成功 消息列表
""" """
return await self.send_message_to_target( return get_raw_msg_by_timestamp_with_chat_inclusive(chat_id, start_time, end_time, limit, limit_mode)
message_type="text", content=text, platform=platform, target_id=user_id, is_group=False
)
def get_chat_type(self) -> str:
"""获取当前聊天类型
Returns: def get_messages_by_time_in_chat_for_users(
str: 聊天类型 ("group""private") chat_id: str,
start_time: float,
end_time: float,
person_ids: list,
limit: int = 0,
limit_mode: str = "latest",
) -> List[Dict[str, Any]]:
""" """
services = getattr(self, "_services", {}) 获取指定聊天中指定用户在指定时间范围内的消息
chat_stream: ChatStream = services.get("chat_stream")
if chat_stream and hasattr(chat_stream, "group_info"):
return "group" if chat_stream.group_info else "private"
return "unknown"
def get_recent_messages(self, count: int = 5) -> List[Dict[str, Any]]:
"""获取最近的消息
Args: Args:
count: 要获取的消息数量 chat_id: 聊天ID
start_time: 开始时间戳
end_time: 结束时间戳
person_ids: 用户ID列表
limit: 限制返回的消息数量0为不限制
limit_mode: 当limit>0时生效'earliest'表示获取最早的记录,'latest'表示获取最新的记录
Returns: Returns:
List[Dict]: 消息列表,每个消息包含发送者、内容等信息 消息列表
""" """
messages = [] return get_raw_msg_by_timestamp_with_chat_users(chat_id, start_time, end_time, person_ids, limit, limit_mode)
services = getattr(self, "_services", {})
observations = services.get("observations", [])
if observations and len(observations) > 0:
obs = observations[0]
if hasattr(obs, "get_talking_message"):
obs: ObsInfo
raw_messages = obs.get_talking_message()
# 转换为简化格式
for msg in raw_messages[-count:]:
simple_msg = {
"sender": msg.get("sender", "未知"),
"content": msg.get("content", ""),
"timestamp": msg.get("timestamp", 0),
}
messages.append(simple_msg)
return messages def get_random_chat_messages(
start_time: float, end_time: float, limit: int = 0, limit_mode: str = "latest"
) -> List[Dict[str, Any]]:
"""
随机选择一个聊天,返回该聊天在指定时间范围内的消息
Args:
start_time: 开始时间戳
end_time: 结束时间戳
limit: 限制返回的消息数量0为不限制
limit_mode: 当limit>0时生效'earliest'表示获取最早的记录,'latest'表示获取最新的记录
Returns:
消息列表
"""
return get_raw_msg_by_timestamp_random(start_time, end_time, limit, limit_mode)
def get_messages_by_time_for_users(
start_time: float, end_time: float, person_ids: list, limit: int = 0, limit_mode: str = "latest"
) -> List[Dict[str, Any]]:
"""
获取指定用户在所有聊天中指定时间范围内的消息
Args:
start_time: 开始时间戳
end_time: 结束时间戳
person_ids: 用户ID列表
limit: 限制返回的消息数量0为不限制
limit_mode: 当limit>0时生效'earliest'表示获取最早的记录,'latest'表示获取最新的记录
Returns:
消息列表
"""
return get_raw_msg_by_timestamp_with_users(start_time, end_time, person_ids, limit, limit_mode)
def get_messages_before_time(timestamp: float, limit: int = 0) -> List[Dict[str, Any]]:
"""
获取指定时间戳之前的消息
Args:
timestamp: 时间戳
limit: 限制返回的消息数量0为不限制
Returns:
消息列表
"""
return get_raw_msg_before_timestamp(timestamp, limit)
def get_messages_before_time_in_chat(chat_id: str, timestamp: float, limit: int = 0) -> List[Dict[str, Any]]:
"""
获取指定聊天中指定时间戳之前的消息
Args:
chat_id: 聊天ID
timestamp: 时间戳
limit: 限制返回的消息数量0为不限制
Returns:
消息列表
"""
return get_raw_msg_before_timestamp_with_chat(chat_id, timestamp, limit)
def get_messages_before_time_for_users(
timestamp: float, person_ids: list, limit: int = 0
) -> List[Dict[str, Any]]:
"""
获取指定用户在指定时间戳之前的消息
Args:
timestamp: 时间戳
person_ids: 用户ID列表
limit: 限制返回的消息数量0为不限制
Returns:
消息列表
"""
return get_raw_msg_before_timestamp_with_users(timestamp, person_ids, limit)
def get_recent_messages(
chat_id: str,
hours: float = 24.0,
limit: int = 100,
limit_mode: str = "latest"
) -> List[Dict[str, Any]]:
"""
获取指定聊天中最近一段时间的消息
Args:
chat_id: 聊天ID
hours: 最近多少小时默认24小时
limit: 限制返回的消息数量默认100条
limit_mode: 当limit>0时生效'earliest'表示获取最早的记录,'latest'表示获取最新的记录
Returns:
消息列表
"""
now = time.time()
start_time = now - hours * 3600
return get_raw_msg_by_timestamp_with_chat(chat_id, start_time, now, limit, limit_mode)
# =============================================================================
# 消息计数API函数
# =============================================================================
def count_new_messages(
chat_id: str, start_time: float = 0.0, end_time: Optional[float] = None
) -> int:
"""
计算指定聊天中从开始时间到结束时间的新消息数量
Args:
chat_id: 聊天ID
start_time: 开始时间戳
end_time: 结束时间戳如果为None则使用当前时间
Returns:
新消息数量
"""
return num_new_messages_since(chat_id, start_time, end_time)
def count_new_messages_for_users(
chat_id: str, start_time: float, end_time: float, person_ids: list
) -> int:
"""
计算指定聊天中指定用户从开始时间到结束时间的新消息数量
Args:
chat_id: 聊天ID
start_time: 开始时间戳
end_time: 结束时间戳
person_ids: 用户ID列表
Returns:
新消息数量
"""
return num_new_messages_since_with_users(chat_id, start_time, end_time, person_ids)
# =============================================================================
# 消息格式化API函数
# =============================================================================
def build_readable_messages_to_str(
messages: List[Dict[str, Any]],
replace_bot_name: bool = True,
merge_messages: bool = False,
timestamp_mode: str = "relative",
read_mark: float = 0.0,
truncate: bool = False,
show_actions: bool = False,
) -> str:
"""
将消息列表构建成可读的字符串
Args:
messages: 消息列表
replace_bot_name: 是否将机器人的名称替换为""
merge_messages: 是否合并连续消息
timestamp_mode: 时间戳显示模式,'relative''absolute'
read_mark: 已读标记时间戳,用于分割已读和未读消息
truncate: 是否截断长消息
show_actions: 是否显示动作记录
Returns:
格式化后的可读字符串
"""
return build_readable_messages(
messages, replace_bot_name, merge_messages, timestamp_mode, read_mark, truncate, show_actions
)
async def build_readable_messages_with_details(
messages: List[Dict[str, Any]],
replace_bot_name: bool = True,
merge_messages: bool = False,
timestamp_mode: str = "relative",
truncate: bool = False,
) -> Tuple[str, List[Tuple[float, str, str]]]:
"""
将消息列表构建成可读的字符串,并返回详细信息
Args:
messages: 消息列表
replace_bot_name: 是否将机器人的名称替换为""
merge_messages: 是否合并连续消息
timestamp_mode: 时间戳显示模式,'relative''absolute'
truncate: 是否截断长消息
Returns:
格式化后的可读字符串和详细信息元组列表(时间戳, 昵称, 内容)
"""
return await build_readable_messages_with_list(
messages, replace_bot_name, merge_messages, timestamp_mode, truncate
)
async def get_person_ids_from_messages(messages: List[Dict[str, Any]]) -> List[str]:
"""
从消息列表中提取不重复的用户ID列表
Args:
messages: 消息列表
Returns:
用户ID列表
"""
return await get_person_id_list(messages)

View File

@@ -0,0 +1,153 @@
"""个人信息API模块
提供个人信息查询功能,用于插件获取用户相关信息
使用方式:
from src.plugin_system.apis import person_api
person_id = person_api.get_person_id("qq", 123456)
value = await person_api.get_person_value(person_id, "nickname")
"""
from typing import Any
from src.common.logger import get_logger
from src.person_info.person_info import get_person_info_manager, PersonInfoManager
logger = get_logger("person_api")
# =============================================================================
# 个人信息API函数
# =============================================================================
def get_person_id(platform: str, user_id: int) -> str:
"""根据平台和用户ID获取person_id
Args:
platform: 平台名称,如 "qq", "telegram"
user_id: 用户ID
Returns:
str: 唯一的person_idMD5哈希值
示例:
person_id = person_api.get_person_id("qq", 123456)
"""
try:
return PersonInfoManager.get_person_id(platform, user_id)
except Exception as e:
logger.error(f"[PersonAPI] 获取person_id失败: platform={platform}, user_id={user_id}, error={e}")
return ""
async def get_person_value(person_id: str, field_name: str, default: Any = None) -> Any:
"""根据person_id和字段名获取某个值
Args:
person_id: 用户的唯一标识ID
field_name: 要获取的字段名,如 "nickname", "impression"
default: 当字段不存在或获取失败时返回的默认值
Returns:
Any: 字段值或默认值
示例:
nickname = await person_api.get_person_value(person_id, "nickname", "未知用户")
impression = await person_api.get_person_value(person_id, "impression")
"""
try:
person_info_manager = get_person_info_manager()
value = await person_info_manager.get_value(person_id, field_name)
return value if value is not None else default
except Exception as e:
logger.error(f"[PersonAPI] 获取用户信息失败: person_id={person_id}, field={field_name}, error={e}")
return default
async def get_person_values(person_id: str, field_names: list, default_dict: dict = None) -> dict:
"""批量获取用户信息字段值
Args:
person_id: 用户的唯一标识ID
field_names: 要获取的字段名列表
default_dict: 默认值字典,键为字段名,值为默认值
Returns:
dict: 字段名到值的映射字典
示例:
values = await person_api.get_person_values(
person_id,
["nickname", "impression", "know_times"],
{"nickname": "未知用户", "know_times": 0}
)
"""
try:
person_info_manager = get_person_info_manager()
values = await person_info_manager.get_values(person_id, field_names)
# 如果获取成功,返回结果
if values:
return values
# 如果获取失败,构建默认值字典
result = {}
if default_dict:
for field in field_names:
result[field] = default_dict.get(field, None)
else:
for field in field_names:
result[field] = None
return result
except Exception as e:
logger.error(f"[PersonAPI] 批量获取用户信息失败: person_id={person_id}, fields={field_names}, error={e}")
# 返回默认值字典
result = {}
if default_dict:
for field in field_names:
result[field] = default_dict.get(field, None)
else:
for field in field_names:
result[field] = None
return result
async def is_person_known(platform: str, user_id: int) -> bool:
"""判断是否认识某个用户
Args:
platform: 平台名称
user_id: 用户ID
Returns:
bool: 是否认识该用户
示例:
known = await person_api.is_person_known("qq", 123456)
"""
try:
person_info_manager = get_person_info_manager()
return await person_info_manager.is_person_known(platform, user_id)
except Exception as e:
logger.error(f"[PersonAPI] 检查用户是否已知失败: platform={platform}, user_id={user_id}, error={e}")
return False
def get_person_id_by_name(person_name: str) -> str:
"""根据用户名获取person_id
Args:
person_name: 用户名
Returns:
str: person_id如果未找到返回空字符串
示例:
person_id = person_api.get_person_id_by_name("张三")
"""
try:
person_info_manager = get_person_info_manager()
return person_info_manager.get_person_id_by_person_name(person_name)
except Exception as e:
logger.error(f"[PersonAPI] 根据用户名获取person_id失败: person_name={person_name}, error={e}")
return ""

View File

@@ -1,234 +0,0 @@
# -*- coding: utf-8 -*-
"""
统一的插件API聚合模块
提供所有插件API功能的统一访问入口
"""
from src.common.logger import get_logger
# 导入所有API模块
from src.plugin_system.apis.message_api import MessageAPI
from src.plugin_system.apis.llm_api import LLMAPI
from src.plugin_system.apis.database_api import DatabaseAPI
from src.plugin_system.apis.config_api import ConfigAPI
from src.plugin_system.apis.utils_api import UtilsAPI
from src.plugin_system.apis.stream_api import StreamAPI
from src.plugin_system.apis.hearflow_api import HearflowAPI
logger = get_logger("plugin_api")
class PluginAPI(MessageAPI, LLMAPI, DatabaseAPI, ConfigAPI, UtilsAPI, StreamAPI, HearflowAPI):
"""
插件API聚合类
集成了所有可供插件使用的API功能提供统一的访问接口。
插件组件可以直接使用此API实例来访问各种功能。
特性:
- 聚合所有API模块的功能
- 支持依赖注入和配置
- 提供统一的错误处理和日志记录
"""
def __init__(
self,
chat_stream=None,
expressor=None,
replyer=None,
observations=None,
log_prefix: str = "[PluginAPI]",
plugin_config: dict = None,
):
"""
初始化插件API
Args:
chat_stream: 聊天流对象
expressor: 表达器对象
replyer: 回复器对象
observations: 观察列表
log_prefix: 日志前缀
plugin_config: 插件配置字典
"""
# 存储依赖对象
self._services = {
"chat_stream": chat_stream,
"expressor": expressor,
"replyer": replyer,
"observations": observations or [],
}
self.log_prefix = log_prefix
# 存储action上下文信息
self._action_context = {}
# 调用所有父类的初始化
super().__init__()
# 存储插件配置
self._plugin_config = plugin_config or {}
def set_chat_stream(self, chat_stream):
"""设置聊天流对象"""
self._services["chat_stream"] = chat_stream
logger.debug(f"{self.log_prefix} 设置聊天流: {getattr(chat_stream, 'stream_id', 'Unknown')}")
def set_expressor(self, expressor):
"""设置表达器对象"""
self._services["expressor"] = expressor
logger.debug(f"{self.log_prefix} 设置表达器")
def set_replyer(self, replyer):
"""设置回复器对象"""
self._services["replyer"] = replyer
logger.debug(f"{self.log_prefix} 设置回复器")
def set_observations(self, observations):
"""设置观察列表"""
self._services["observations"] = observations or []
logger.debug(f"{self.log_prefix} 设置观察列表,数量: {len(observations or [])}")
def get_service(self, service_name: str):
"""获取指定的服务对象"""
return self._services.get(service_name)
def has_service(self, service_name: str) -> bool:
"""检查是否有指定的服务对象"""
return service_name in self._services and self._services[service_name] is not None
def set_action_context(self, thinking_id: str = None, shutting_down: bool = False, **kwargs):
"""设置action上下文信息"""
if thinking_id:
self._action_context["thinking_id"] = thinking_id
self._action_context["shutting_down"] = shutting_down
self._action_context.update(kwargs)
def get_action_context(self, key: str, default=None):
"""获取action上下文信息"""
return self._action_context.get(key, default)
def get_config(self, key: str, default=None):
"""获取插件配置值,支持嵌套键访问
Args:
key: 配置键名,支持嵌套访问如 "section.subsection.key"
default: 默认值
Returns:
Any: 配置值或默认值
"""
if not self._plugin_config:
return default
# 支持嵌套键访问
keys = key.split(".")
current = self._plugin_config
for k in keys:
if isinstance(current, dict) and k in current:
current = current[k]
else:
return default
return current
def has_config(self, key: str) -> bool:
"""检查是否存在指定的配置项
Args:
key: 配置键名,支持嵌套访问如 "section.subsection.key"
Returns:
bool: 是否存在该配置项
"""
if not self._plugin_config:
return False
keys = key.split(".")
current = self._plugin_config
for k in keys:
if isinstance(current, dict) and k in current:
current = current[k]
else:
return False
return True
def get_all_config(self) -> dict:
"""获取所有插件配置
Returns:
dict: 插件配置字典的副本
"""
return self._plugin_config.copy() if self._plugin_config else {}
# 便捷的工厂函数
def create_plugin_api(
chat_stream=None,
expressor=None,
replyer=None,
observations=None,
log_prefix: str = "[Plugin]",
plugin_config: dict = None,
) -> PluginAPI:
"""
创建插件API实例的便捷函数
Args:
chat_stream: 聊天流对象
expressor: 表达器对象
replyer: 回复器对象
observations: 观察列表
log_prefix: 日志前缀
plugin_config: 插件配置字典
Returns:
PluginAPI: 配置好的插件API实例
"""
return PluginAPI(
chat_stream=chat_stream,
expressor=expressor,
replyer=replyer,
observations=observations,
log_prefix=log_prefix,
plugin_config=plugin_config,
)
def create_command_api(message, log_prefix: str = "[Command]") -> PluginAPI:
"""
为命令创建插件API实例的便捷函数
Args:
message: 消息对象,应该包含 chat_stream 等信息
log_prefix: 日志前缀
Returns:
PluginAPI: 配置好的插件API实例
"""
chat_stream = getattr(message, "chat_stream", None)
api = PluginAPI(chat_stream=chat_stream, log_prefix=log_prefix)
return api
# 导出主要接口
__all__ = [
"PluginAPI",
"create_plugin_api",
"create_command_api",
# 也可以导出各个API类供单独使用
"MessageAPI",
"LLMAPI",
"DatabaseAPI",
"ConfigAPI",
"UtilsAPI",
"StreamAPI",
"HearflowAPI",
]

View File

@@ -0,0 +1,445 @@
"""
发送API模块
专门负责发送各种类型的消息采用标准Python包设计模式
使用方式:
from src.plugin_system.apis import send_api
await send_api.text_to_group("hello", "123456")
await send_api.emoji_to_group(emoji_base64, "123456")
await send_api.custom_message("video", video_data, "123456", True)
"""
import traceback
import time
import difflib
from typing import Optional
from src.common.logger import get_logger
# 导入依赖
from src.chat.message_receive.chat_stream import get_chat_manager
from src.chat.focus_chat.heartFC_sender import HeartFCSender
from src.chat.message_receive.message import MessageSending, MessageRecv
from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat
from src.person_info.person_info import get_person_info_manager
from maim_message import Seg, UserInfo
from src.config.config import global_config
logger = get_logger("send_api")
# =============================================================================
# 内部实现函数(不暴露给外部)
# =============================================================================
async def _send_to_target(
message_type: str,
content: str,
stream_id: str,
display_message: str = "",
typing: bool = False,
reply_to: str = "",
storage_message: bool = True,
) -> bool:
"""向指定目标发送消息的内部实现
Args:
message_type: 消息类型,如"text""image""emoji"
content: 消息内容
stream_id: 目标流ID
display_message: 显示消息
typing: 是否显示正在输入
reply_to: 回复消息的格式,如"发送者:消息内容"
Returns:
bool: 是否发送成功
"""
try:
logger.info(f"[SendAPI] 发送{message_type}消息到 {stream_id}")
# 查找目标聊天流
target_stream = get_chat_manager().get_stream(stream_id)
if not target_stream:
logger.error(f"[SendAPI] 未找到聊天流: {stream_id}")
return False
# 创建发送器
heart_fc_sender = HeartFCSender()
# 生成消息ID
current_time = time.time()
message_id = f"send_api_{int(current_time * 1000)}"
# 构建机器人用户信息
bot_user_info = UserInfo(
user_id=global_config.bot.qq_account,
user_nickname=global_config.bot.nickname,
platform=target_stream.platform,
)
# 创建消息段
message_segment = Seg(type=message_type, data=content)
# 处理回复消息
anchor_message = None
if reply_to:
anchor_message = await _find_reply_message(target_stream, reply_to)
# 构建发送消息对象
bot_message = MessageSending(
message_id=message_id,
chat_stream=target_stream,
bot_user_info=bot_user_info,
sender_info=target_stream.user_info,
message_segment=message_segment,
display_message=display_message,
reply=anchor_message,
is_head=True,
is_emoji=(message_type == "emoji"),
thinking_start_time=current_time,
)
# 发送消息
sent_msg = await heart_fc_sender.send_message(
bot_message, typing=typing, set_reply=(anchor_message is not None), storage_message=storage_message
)
if sent_msg:
logger.info(f"[SendAPI] 成功发送消息到 {stream_id}")
return True
else:
logger.error("[SendAPI] 发送消息失败")
return False
except Exception as e:
logger.error(f"[SendAPI] 发送消息时出错: {e}")
traceback.print_exc()
return False
async def _find_reply_message(target_stream, reply_to: str) -> Optional[MessageRecv]:
"""查找要回复的消息
Args:
target_stream: 目标聊天流
reply_to: 回复格式,如"发送者:消息内容""发送者:消息内容"
Returns:
Optional[MessageRecv]: 找到的消息如果没找到则返回None
"""
try:
# 解析reply_to参数
if ":" in reply_to:
parts = reply_to.split(":", 1)
elif "" in reply_to:
parts = reply_to.split("", 1)
else:
logger.warning(f"[SendAPI] reply_to格式不正确: {reply_to}")
return None
if len(parts) != 2:
logger.warning(f"[SendAPI] reply_to格式不正确: {reply_to}")
return None
sender = parts[0].strip()
text = parts[1].strip()
# 获取聊天流的最新20条消息
reverse_talking_message = get_raw_msg_before_timestamp_with_chat(
target_stream.stream_id,
time.time(), # 当前时间之前的消息
20 # 最新的20条消息
)
# 反转列表,使最新的消息在前面
reverse_talking_message = list(reversed(reverse_talking_message))
find_msg = None
for message in reverse_talking_message:
user_id = message["user_id"]
platform = message["chat_info_platform"]
person_id = get_person_info_manager().get_person_id(platform, user_id)
person_name = await get_person_info_manager().get_value(person_id, "person_name")
if person_name == sender:
similarity = difflib.SequenceMatcher(None, text, message["processed_plain_text"]).ratio()
if similarity >= 0.9:
find_msg = message
break
if not find_msg:
logger.info("[SendAPI] 未找到匹配的回复消息")
return None
# 构建MessageRecv对象
user_info = {
"platform": find_msg.get("user_platform", ""),
"user_id": find_msg.get("user_id", ""),
"user_nickname": find_msg.get("user_nickname", ""),
"user_cardname": find_msg.get("user_cardname", ""),
}
group_info = {}
if find_msg.get("chat_info_group_id"):
group_info = {
"platform": find_msg.get("chat_info_group_platform", ""),
"group_id": find_msg.get("chat_info_group_id", ""),
"group_name": find_msg.get("chat_info_group_name", ""),
}
format_info = {"content_format": "", "accept_format": ""}
template_info = {"template_items": {}}
message_info = {
"platform": target_stream.platform,
"message_id": find_msg.get("message_id"),
"time": find_msg.get("time"),
"group_info": group_info,
"user_info": user_info,
"additional_config": find_msg.get("additional_config"),
"format_info": format_info,
"template_info": template_info,
}
message_dict = {
"message_info": message_info,
"raw_message": find_msg.get("processed_plain_text"),
"detailed_plain_text": find_msg.get("processed_plain_text"),
"processed_plain_text": find_msg.get("processed_plain_text"),
}
find_rec_msg = MessageRecv(message_dict)
find_rec_msg.update_chat_stream(target_stream)
logger.info(f"[SendAPI] 找到匹配的回复消息,发送者: {sender}")
return find_rec_msg
except Exception as e:
logger.error(f"[SendAPI] 查找回复消息时出错: {e}")
traceback.print_exc()
return None
# =============================================================================
# 公共API函数 - 预定义类型的发送函数
# =============================================================================
async def text_to_group(text: str, group_id: str, platform: str = "qq", typing: bool = False, reply_to: str = "", storage_message: bool = True) -> bool:
"""向群聊发送文本消息
Args:
text: 要发送的文本内容
group_id: 群聊ID
platform: 平台,默认为"qq"
typing: 是否显示正在输入
reply_to: 回复消息,格式为"发送者:消息内容"
Returns:
bool: 是否发送成功
"""
stream_id = get_chat_manager().get_stream_id(platform, group_id, True)
return await _send_to_target("text", text, stream_id, "", typing, reply_to, storage_message)
async def text_to_user(text: str, user_id: str, platform: str = "qq", typing: bool = False, reply_to: str = "", storage_message: bool = True) -> bool:
"""向用户发送私聊文本消息
Args:
text: 要发送的文本内容
user_id: 用户ID
platform: 平台,默认为"qq"
typing: 是否显示正在输入
reply_to: 回复消息,格式为"发送者:消息内容"
Returns:
bool: 是否发送成功
"""
stream_id = get_chat_manager().get_stream_id(platform, user_id, False)
return await _send_to_target("text", text, stream_id, "", typing, reply_to, storage_message)
async def emoji_to_group(emoji_base64: str, group_id: str, platform: str = "qq", storage_message: bool = True) -> bool:
"""向群聊发送表情包
Args:
emoji_base64: 表情包的base64编码
group_id: 群聊ID
platform: 平台,默认为"qq"
Returns:
bool: 是否发送成功
"""
stream_id = get_chat_manager().get_stream_id(platform, group_id, True)
return await _send_to_target("emoji", emoji_base64, stream_id, "", typing=False, storage_message=storage_message)
async def emoji_to_user(emoji_base64: str, user_id: str, platform: str = "qq", storage_message: bool = True) -> bool:
"""向用户发送表情包
Args:
emoji_base64: 表情包的base64编码
user_id: 用户ID
platform: 平台,默认为"qq"
Returns:
bool: 是否发送成功
"""
stream_id = get_chat_manager().get_stream_id(platform, user_id, False)
return await _send_to_target("emoji", emoji_base64, stream_id, "", typing=False, storage_message=storage_message)
async def image_to_group(image_base64: str, group_id: str, platform: str = "qq", storage_message: bool = True) -> bool:
"""向群聊发送图片
Args:
image_base64: 图片的base64编码
group_id: 群聊ID
platform: 平台,默认为"qq"
Returns:
bool: 是否发送成功
"""
stream_id = get_chat_manager().get_stream_id(platform, group_id, True)
return await _send_to_target("image", image_base64, stream_id, "", typing=False, storage_message=storage_message)
async def image_to_user(image_base64: str, user_id: str, platform: str = "qq", storage_message: bool = True) -> bool:
"""向用户发送图片
Args:
image_base64: 图片的base64编码
user_id: 用户ID
platform: 平台,默认为"qq"
Returns:
bool: 是否发送成功
"""
stream_id = get_chat_manager().get_stream_id(platform, user_id, False)
return await _send_to_target("image", image_base64, stream_id, "", typing=False)
async def command_to_group(command: str, group_id: str, platform: str = "qq", storage_message: bool = True) -> bool:
"""向群聊发送命令
Args:
command: 命令
group_id: 群聊ID
platform: 平台,默认为"qq"
Returns:
bool: 是否发送成功
"""
stream_id = get_chat_manager().get_stream_id(platform, group_id, True)
return await _send_to_target("command", command, stream_id, "", typing=False, storage_message=storage_message)
async def command_to_user(command: str, user_id: str, platform: str = "qq", storage_message: bool = True) -> bool:
"""向用户发送命令
Args:
command: 命令
user_id: 用户ID
platform: 平台,默认为"qq"
Returns:
bool: 是否发送成功
"""
stream_id = get_chat_manager().get_stream_id(platform, user_id, False)
return await _send_to_target("command", command, stream_id, "", typing=False, storage_message=storage_message)
# =============================================================================
# 通用发送函数 - 支持任意消息类型
# =============================================================================
async def custom_to_group(
message_type: str,
content: str,
group_id: str,
platform: str = "qq",
display_message: str = "",
typing: bool = False,
reply_to: str = "",
storage_message: bool = True
) -> bool:
"""向群聊发送自定义类型消息
Args:
message_type: 消息类型,如"text""image""emoji""video""file"
content: 消息内容通常是base64编码或文本
group_id: 群聊ID
platform: 平台,默认为"qq"
display_message: 显示消息
typing: 是否显示正在输入
reply_to: 回复消息,格式为"发送者:消息内容"
Returns:
bool: 是否发送成功
"""
stream_id = get_chat_manager().get_stream_id(platform, group_id, True)
return await _send_to_target(message_type, content, stream_id, display_message, typing, reply_to, storage_message)
async def custom_to_user(
message_type: str,
content: str,
user_id: str,
platform: str = "qq",
display_message: str = "",
typing: bool = False,
reply_to: str = "",
storage_message: bool = True
) -> bool:
"""向用户发送自定义类型消息
Args:
message_type: 消息类型,如"text""image""emoji""video""file"
content: 消息内容通常是base64编码或文本
user_id: 用户ID
platform: 平台,默认为"qq"
display_message: 显示消息
typing: 是否显示正在输入
reply_to: 回复消息,格式为"发送者:消息内容"
Returns:
bool: 是否发送成功
"""
stream_id = get_chat_manager().get_stream_id(platform, user_id, False)
return await _send_to_target(message_type, content, stream_id, display_message, typing, reply_to, storage_message)
async def custom_message(
message_type: str,
content: str,
target_id: str,
is_group: bool = True,
platform: str = "qq",
display_message: str = "",
typing: bool = False,
reply_to: str = "",
storage_message: bool = True
) -> bool:
"""发送自定义消息的通用接口
Args:
message_type: 消息类型,如"text""image""emoji""video""file""audio"
content: 消息内容
target_id: 目标ID群ID或用户ID
is_group: 是否为群聊True为群聊False为私聊
platform: 平台,默认为"qq"
display_message: 显示消息
typing: 是否显示正在输入
reply_to: 回复消息,格式为"发送者:消息内容"
Returns:
bool: 是否发送成功
示例:
# 发送视频到群聊
await send_api.custom_message("video", video_base64, "123456", True)
# 发送文件到用户
await send_api.custom_message("file", file_base64, "987654", False)
# 发送音频到群聊并回复特定消息
await send_api.custom_message("audio", audio_base64, "123456", True, reply_to="张三:你好")
"""
stream_id = get_chat_manager().get_stream_id(platform, target_id, is_group)
return await _send_to_target(message_type, content, stream_id, display_message, typing, reply_to, storage_message)

View File

@@ -1,220 +0,0 @@
from typing import Optional, List, Dict, Any, Tuple
from src.common.logger import get_logger
from src.chat.message_receive.chat_stream import ChatManager, ChatStream
from src.chat.focus_chat.hfc_utils import parse_thinking_id_to_timestamp
import asyncio
logger = get_logger("stream_api")
class StreamAPI:
"""聊天流API模块
提供了获取聊天流、通过群ID查找聊天流等功能
"""
def get_chat_stream_by_group_id(self, group_id: str, platform: str = "qq") -> Optional[ChatStream]:
"""通过QQ群ID获取聊天流
Args:
group_id: QQ群ID
platform: 平台标识,默认为"qq"
Returns:
Optional[ChatStream]: 找到的聊天流对象如果未找到则返回None
"""
try:
chat_manager = ChatManager()
# 遍历所有已加载的聊天流查找匹配的群ID
for stream_id, stream in chat_manager.streams.items():
if (
stream.group_info
and str(stream.group_info.group_id) == str(group_id)
and stream.platform == platform
):
logger.info(f"{self.log_prefix} 通过群ID {group_id} 找到聊天流: {stream_id}")
return stream
logger.warning(f"{self.log_prefix} 未找到群ID为 {group_id} 的聊天流")
return None
except Exception as e:
logger.error(f"{self.log_prefix} 通过群ID获取聊天流时出错: {e}")
return None
def get_all_group_chat_streams(self, platform: str = "qq") -> List[ChatStream]:
"""获取所有群聊的聊天流
Args:
platform: 平台标识,默认为"qq"
Returns:
List[ChatStream]: 所有群聊的聊天流列表
"""
try:
chat_manager = ChatManager()
group_streams = []
for stream in chat_manager.streams.values():
if stream.group_info and stream.platform == platform:
group_streams.append(stream)
logger.info(f"{self.log_prefix} 找到 {len(group_streams)} 个群聊聊天流")
return group_streams
except Exception as e:
logger.error(f"{self.log_prefix} 获取所有群聊聊天流时出错: {e}")
return []
def get_chat_stream_by_user_id(self, user_id: str, platform: str = "qq") -> Optional[ChatStream]:
"""通过用户ID获取私聊聊天流
Args:
user_id: 用户ID
platform: 平台标识,默认为"qq"
Returns:
Optional[ChatStream]: 找到的私聊聊天流对象如果未找到则返回None
"""
try:
chat_manager = ChatManager()
# 遍历所有已加载的聊天流查找匹配的用户ID私聊
for stream_id, stream in chat_manager.streams.items():
if (
not stream.group_info # 私聊没有群信息
and stream.user_info
and str(stream.user_info.user_id) == str(user_id)
and stream.platform == platform
):
logger.info(f"{self.log_prefix} 通过用户ID {user_id} 找到私聊聊天流: {stream_id}")
return stream
logger.warning(f"{self.log_prefix} 未找到用户ID为 {user_id} 的私聊聊天流")
return None
except Exception as e:
logger.error(f"{self.log_prefix} 通过用户ID获取私聊聊天流时出错: {e}")
return None
def get_chat_streams_info(self) -> List[Dict[str, Any]]:
"""获取所有聊天流的基本信息
Returns:
List[Dict[str, Any]]: 包含聊天流基本信息的字典列表
"""
try:
chat_manager = ChatManager()
streams_info = []
for stream_id, stream in chat_manager.streams.items():
info = {
"stream_id": stream_id,
"platform": stream.platform,
"chat_type": "group" if stream.group_info else "private",
"create_time": stream.create_time,
"last_active_time": stream.last_active_time,
}
if stream.group_info:
info.update({"group_id": stream.group_info.group_id, "group_name": stream.group_info.group_name})
if stream.user_info:
info.update({"user_id": stream.user_info.user_id, "user_nickname": stream.user_info.user_nickname})
streams_info.append(info)
logger.info(f"{self.log_prefix} 获取到 {len(streams_info)} 个聊天流信息")
return streams_info
except Exception as e:
logger.error(f"{self.log_prefix} 获取聊天流信息时出错: {e}")
return []
async def get_chat_stream_by_group_id_async(self, group_id: str, platform: str = "qq") -> Optional[ChatStream]:
"""异步通过QQ群ID获取聊天流包括从数据库搜索
Args:
group_id: QQ群ID
platform: 平台标识,默认为"qq"
Returns:
Optional[ChatStream]: 找到的聊天流对象如果未找到则返回None
"""
try:
# 首先尝试从内存中查找
stream = self.get_chat_stream_by_group_id(group_id, platform)
if stream:
return stream
# 如果内存中没有,尝试从数据库加载所有聊天流后再查找
chat_manager = ChatManager()
await chat_manager.load_all_streams()
# 再次尝试从内存中查找
stream = self.get_chat_stream_by_group_id(group_id, platform)
return stream
except Exception as e:
logger.error(f"{self.log_prefix} 异步通过群ID获取聊天流时出错: {e}")
return None
async def wait_for_new_message(self, timeout: int = 1200) -> Tuple[bool, str]:
"""等待新消息或超时
Args:
timeout: 超时时间默认1200秒
Returns:
Tuple[bool, str]: (是否收到新消息, 空字符串)
"""
try:
# 获取必要的服务对象
observations = self.get_service("observations")
if not observations:
logger.warning(f"{self.log_prefix} 无法获取observations服务无法等待新消息")
return False, ""
# 获取第一个观察对象通常是ChattingObservation
observation = observations[0] if observations else None
if not observation:
logger.warning(f"{self.log_prefix} 无观察对象,无法等待新消息")
return False, ""
# 从action上下文获取thinking_id
thinking_id = self.get_action_context("thinking_id")
if not thinking_id:
logger.warning(f"{self.log_prefix} 无thinking_id无法等待新消息")
return False, ""
logger.info(f"{self.log_prefix} 开始等待新消息... (超时: {timeout}秒)")
wait_start_time = asyncio.get_event_loop().time()
while True:
# 检查关闭标志
shutting_down = self.get_action_context("shutting_down", False)
if shutting_down:
logger.info(f"{self.log_prefix} 等待新消息时检测到关闭信号,中断等待")
return False, ""
# 检查新消息
thinking_id_timestamp = parse_thinking_id_to_timestamp(thinking_id)
if await observation.has_new_messages_since(thinking_id_timestamp):
logger.info(f"{self.log_prefix} 检测到新消息")
return True, ""
# 检查超时
if asyncio.get_event_loop().time() - wait_start_time > timeout:
logger.warning(f"{self.log_prefix} 等待新消息超时({timeout}秒)")
return False, ""
# 短暂休眠
await asyncio.sleep(0.5)
except asyncio.CancelledError:
logger.info(f"{self.log_prefix} 等待新消息被中断 (CancelledError)")
return False, ""
except Exception as e:
logger.error(f"{self.log_prefix} 等待新消息时发生错误: {e}")
return False, f"等待新消息失败: {str(e)}"

View File

@@ -1,31 +1,51 @@
"""工具类API模块
提供了各种辅助功能
使用方式:
from src.plugin_system.apis import utils_api
plugin_path = utils_api.get_plugin_path()
data = utils_api.read_json_file("data.json")
timestamp = utils_api.get_timestamp()
"""
import os import os
import json import json
import time import time
import inspect
import datetime
import uuid
from typing import Any, Optional from typing import Any, Optional
from src.common.logger import get_logger from src.common.logger import get_logger
logger = get_logger("utils_api") logger = get_logger("utils_api")
class UtilsAPI: # =============================================================================
"""工具类API模块 # 文件操作API函数
# =============================================================================
提供了各种辅助功能 def get_plugin_path(caller_frame=None) -> str:
""" """获取调用者插件的路径
def get_plugin_path(self) -> str: Args:
"""获取当前插件的路径 caller_frame: 调用者的栈帧默认为None自动获取
Returns: Returns:
str: 插件目录的绝对路径 str: 插件目录的绝对路径
""" """
import inspect try:
if caller_frame is None:
caller_frame = inspect.currentframe().f_back
plugin_module_path = inspect.getfile(self.__class__) plugin_module_path = inspect.getfile(caller_frame)
plugin_dir = os.path.dirname(plugin_module_path) plugin_dir = os.path.dirname(plugin_module_path)
return plugin_dir return plugin_dir
except Exception as e:
logger.error(f"[UtilsAPI] 获取插件路径失败: {e}")
return ""
def read_json_file(self, file_path: str, default: Any = None) -> Any:
def read_json_file(file_path: str, default: Any = None) -> Any:
"""读取JSON文件 """读取JSON文件
Args: Args:
@@ -36,21 +56,24 @@ class UtilsAPI:
Any: JSON数据或默认值 Any: JSON数据或默认值
""" """
try: try:
# 如果是相对路径,则相对于插件目录 # 如果是相对路径,则相对于调用者的插件目录
if not os.path.isabs(file_path): if not os.path.isabs(file_path):
file_path = os.path.join(self.get_plugin_path(), file_path) caller_frame = inspect.currentframe().f_back
plugin_dir = get_plugin_path(caller_frame)
file_path = os.path.join(plugin_dir, file_path)
if not os.path.exists(file_path): if not os.path.exists(file_path):
logger.warning(f"{self.log_prefix} 文件不存在: {file_path}") logger.warning(f"[UtilsAPI] 文件不存在: {file_path}")
return default return default
with open(file_path, "r", encoding="utf-8") as f: with open(file_path, "r", encoding="utf-8") as f:
return json.load(f) return json.load(f)
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix} 读取JSON文件出错: {e}") logger.error(f"[UtilsAPI] 读取JSON文件出错: {e}")
return default return default
def write_json_file(self, file_path: str, data: Any, indent: int = 2) -> bool:
def write_json_file(file_path: str, data: Any, indent: int = 2) -> bool:
"""写入JSON文件 """写入JSON文件
Args: Args:
@@ -62,9 +85,11 @@ class UtilsAPI:
bool: 是否写入成功 bool: 是否写入成功
""" """
try: try:
# 如果是相对路径,则相对于插件目录 # 如果是相对路径,则相对于调用者的插件目录
if not os.path.isabs(file_path): if not os.path.isabs(file_path):
file_path = os.path.join(self.get_plugin_path(), file_path) caller_frame = inspect.currentframe().f_back
plugin_dir = get_plugin_path(caller_frame)
file_path = os.path.join(plugin_dir, file_path)
# 确保目录存在 # 确保目录存在
os.makedirs(os.path.dirname(file_path), exist_ok=True) os.makedirs(os.path.dirname(file_path), exist_ok=True)
@@ -73,10 +98,15 @@ class UtilsAPI:
json.dump(data, f, ensure_ascii=False, indent=indent) json.dump(data, f, ensure_ascii=False, indent=indent)
return True return True
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix} 写入JSON文件出错: {e}") logger.error(f"[UtilsAPI] 写入JSON文件出错: {e}")
return False return False
def get_timestamp(self) -> int:
# =============================================================================
# 时间相关API函数
# =============================================================================
def get_timestamp() -> int:
"""获取当前时间戳 """获取当前时间戳
Returns: Returns:
@@ -84,7 +114,8 @@ class UtilsAPI:
""" """
return int(time.time()) return int(time.time())
def format_time(self, timestamp: Optional[int] = None, format_str: str = "%Y-%m-%d %H:%M:%S") -> str:
def format_time(timestamp: Optional[int] = None, format_str: str = "%Y-%m-%d %H:%M:%S") -> str:
"""格式化时间 """格式化时间
Args: Args:
@@ -94,13 +125,16 @@ class UtilsAPI:
Returns: Returns:
str: 格式化后的时间字符串 str: 格式化后的时间字符串
""" """
import datetime try:
if timestamp is None: if timestamp is None:
timestamp = time.time() timestamp = time.time()
return datetime.datetime.fromtimestamp(timestamp).strftime(format_str) return datetime.datetime.fromtimestamp(timestamp).strftime(format_str)
except Exception as e:
logger.error(f"[UtilsAPI] 格式化时间失败: {e}")
return ""
def parse_time(self, time_str: str, format_str: str = "%Y-%m-%d %H:%M:%S") -> int:
def parse_time(time_str: str, format_str: str = "%Y-%m-%d %H:%M:%S") -> int:
"""解析时间字符串为时间戳 """解析时间字符串为时间戳
Args: Args:
@@ -110,17 +144,22 @@ class UtilsAPI:
Returns: Returns:
int: 时间戳(秒) int: 时间戳(秒)
""" """
import datetime try:
dt = datetime.datetime.strptime(time_str, format_str) dt = datetime.datetime.strptime(time_str, format_str)
return int(dt.timestamp()) return int(dt.timestamp())
except Exception as e:
logger.error(f"[UtilsAPI] 解析时间失败: {e}")
return 0
def generate_unique_id(self) -> str:
# =============================================================================
# 其他工具函数
# =============================================================================
def generate_unique_id() -> str:
"""生成唯一ID """生成唯一ID
Returns: Returns:
str: 唯一ID str: 唯一ID
""" """
import uuid
return str(uuid.uuid4()) return str(uuid.uuid4())

View File

@@ -1,8 +1,10 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Tuple from typing import Tuple, Optional
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system.apis.plugin_api import PluginAPI
from src.plugin_system.base.component_types import ActionActivationType, ChatMode, ActionInfo, ComponentType from src.plugin_system.base.component_types import ActionActivationType, ChatMode, ActionInfo, ComponentType
from src.plugin_system.apis import send_api, database_api,message_api
import time
import asyncio
logger = get_logger("base_action") logger = get_logger("base_action")
@@ -29,9 +31,6 @@ class BaseAction(ABC):
reasoning: str, reasoning: str,
cycle_timers: dict, cycle_timers: dict,
thinking_id: str, thinking_id: str,
observations: list = None,
expressor=None,
replyer=None,
chat_stream=None, chat_stream=None,
log_prefix: str = "", log_prefix: str = "",
shutting_down: bool = False, shutting_down: bool = False,
@@ -61,6 +60,9 @@ class BaseAction(ABC):
self.log_prefix = log_prefix self.log_prefix = log_prefix
self.shutting_down = shutting_down self.shutting_down = shutting_down
# 保存插件配置
self.plugin_config = plugin_config or {}
# 设置动作基本信息实例属性 # 设置动作基本信息实例属性
self.action_name: str = getattr(self, "action_name", self.__class__.__name__.lower().replace("action", "")) self.action_name: str = getattr(self, "action_name", self.__class__.__name__.lower().replace("action", ""))
self.action_description: str = getattr(self, "action_description", self.__doc__ or "Action组件") self.action_description: str = getattr(self, "action_description", self.__doc__ or "Action组件")
@@ -68,8 +70,8 @@ class BaseAction(ABC):
self.action_require: list[str] = getattr(self.__class__, "action_require", []).copy() self.action_require: list[str] = getattr(self.__class__, "action_require", []).copy()
# 设置激活类型实例属性(从类属性复制,提供默认值) # 设置激活类型实例属性(从类属性复制,提供默认值)
self.focus_activation_type: str = self._get_activation_type_value("focus_activation_type", "never") self.focus_activation_type: str = self._get_activation_type_value("focus_activation_type", "always")
self.normal_activation_type: str = self._get_activation_type_value("normal_activation_type", "never") self.normal_activation_type: str = self._get_activation_type_value("normal_activation_type", "always")
self.random_activation_probability: float = getattr(self.__class__, "random_activation_probability", 0.0) self.random_activation_probability: float = getattr(self.__class__, "random_activation_probability", 0.0)
self.llm_judge_prompt: str = getattr(self.__class__, "llm_judge_prompt", "") self.llm_judge_prompt: str = getattr(self.__class__, "llm_judge_prompt", "")
self.activation_keywords: list[str] = getattr(self.__class__, "activation_keywords", []).copy() self.activation_keywords: list[str] = getattr(self.__class__, "activation_keywords", []).copy()
@@ -77,22 +79,46 @@ class BaseAction(ABC):
self.mode_enable: str = self._get_mode_value("mode_enable", "all") self.mode_enable: str = self._get_mode_value("mode_enable", "all")
self.parallel_action: bool = getattr(self.__class__, "parallel_action", True) self.parallel_action: bool = getattr(self.__class__, "parallel_action", True)
self.associated_types: list[str] = getattr(self.__class__, "associated_types", []).copy() self.associated_types: list[str] = getattr(self.__class__, "associated_types", []).copy()
self.enable_plugin: bool = True # 默认启用
# 创建API实例传递所有服务对象 # =============================================================================
self.api = PluginAPI( # 便捷属性 - 直接在初始化时获取常用聊天信息(带类型注解)
chat_stream=chat_stream or kwargs.get("chat_stream"), # =============================================================================
expressor=expressor or kwargs.get("expressor"),
replyer=replyer or kwargs.get("replyer"),
observations=observations or kwargs.get("observations", []),
log_prefix=log_prefix,
plugin_config=plugin_config or kwargs.get("plugin_config"),
)
# 设置API的action上下文
self.api.set_action_context(thinking_id=thinking_id, shutting_down=shutting_down) # 获取聊天流对象
self.chat_stream = chat_stream or kwargs.get("chat_stream")
self.chat_id = self.chat_stream.stream_id
# 初始化基础信息(带类型注解)
self.is_group: bool = False
self.platform: Optional[str] = None
self.group_id: Optional[str] = None
self.user_id: Optional[str] = None
self.target_id: Optional[str] = None
self.group_name: Optional[str] = None
self.user_nickname: Optional[str] = None
# 如果有聊天流,提取所有信息
if self.chat_stream:
self.platform = getattr(self.chat_stream, 'platform', None)
# 获取群聊信息
# print(self.chat_stream)
# print(self.chat_stream.group_info)
if self.chat_stream.group_info:
self.is_group = True
self.group_id = str(self.chat_stream.group_info.group_id)
self.group_name = getattr(self.chat_stream.group_info, 'group_name', None)
else:
self.is_group = False
self.user_id = str(self.chat_stream.user_info.user_id)
self.user_nickname = getattr(self.chat_stream.user_info, 'user_nickname', None)
# 设置目标ID群聊用群ID私聊用户ID
self.target_id = self.group_id if self.is_group else self.user_id
logger.debug(f"{self.log_prefix} Action组件初始化完成") logger.debug(f"{self.log_prefix} Action组件初始化完成")
logger.debug(f"{self.log_prefix} 聊天信息: 类型={'群聊' if self.is_group else '私聊'}, 平台={self.platform}, 目标={self.target_id}")
def _get_activation_type_value(self, attr_name: str, default: str) -> str: def _get_activation_type_value(self, attr_name: str, default: str) -> str:
"""获取激活类型的字符串值""" """获取激活类型的字符串值"""
@@ -112,67 +138,184 @@ class BaseAction(ABC):
return attr.value return attr.value
return str(attr) return str(attr)
async def send_text(self, content: str) -> bool:
"""发送回复消息 async def wait_for_new_message(self, timeout: int = 1200) -> Tuple[bool, str]:
"""等待新消息或超时
在loop_start_time之后等待新消息如果没有新消息且没有超时就一直等待。
使用message_api检查self.chat_id对应的聊天中是否有新消息。
Args: Args:
content: 回复内容 timeout: 超时时间默认1200秒
Returns:
Tuple[bool, str]: (是否收到新消息, 空字符串)
"""
try:
# 获取循环开始时间,如果没有则使用当前时间
loop_start_time = self.action_data.get("loop_start_time", time.time())
logger.info(f"{self.log_prefix} 开始等待新消息... (最长等待: {timeout}秒, 从时间点: {loop_start_time})")
# 确保有有效的chat_id
if not self.chat_id:
logger.error(f"{self.log_prefix} 等待新消息失败: 没有有效的chat_id")
return False, "没有有效的chat_id"
wait_start_time = asyncio.get_event_loop().time()
while True:
# 检查关闭标志
# shutting_down = self.get_action_context("shutting_down", False)
# if shutting_down:
# logger.info(f"{self.log_prefix} 等待新消息时检测到关闭信号,中断等待")
# return False, ""
# 检查新消息
current_time = time.time()
new_message_count = message_api.count_new_messages(
chat_id=self.chat_id,
start_time=loop_start_time,
end_time=current_time
)
if new_message_count > 0:
logger.info(f"{self.log_prefix} 检测到{new_message_count}条新消息聊天ID: {self.chat_id}")
return True, ""
# 检查超时
elapsed_time = asyncio.get_event_loop().time() - wait_start_time
if elapsed_time > timeout:
logger.warning(f"{self.log_prefix} 等待新消息超时({timeout}秒)聊天ID: {self.chat_id}")
return False, ""
# 每30秒记录一次等待状态
if int(elapsed_time) % 15 == 0 and int(elapsed_time) > 0:
logger.debug(f"{self.log_prefix} 已等待{int(elapsed_time)}秒,继续等待新消息...")
# 短暂休眠
await asyncio.sleep(0.5)
except asyncio.CancelledError:
logger.info(f"{self.log_prefix} 等待新消息被中断 (CancelledError)")
return False, ""
except Exception as e:
logger.error(f"{self.log_prefix} 等待新消息时发生错误: {e}")
return False, f"等待新消息失败: {str(e)}"
async def send_text(self, content: str, reply_to: str = "") -> bool:
"""发送文本消息
Args:
content: 文本内容
Returns: Returns:
bool: 是否发送成功 bool: 是否发送成功
""" """
chat_stream = self.api.get_service("chat_stream") if not self.target_id or not self.platform:
if not chat_stream: logger.error(f"{self.log_prefix} 缺少发送消息所需的信息")
logger.error(f"{self.log_prefix} 没有可用的聊天流发送回复")
return False return False
if chat_stream.group_info: if self.is_group:
# 群聊 return await send_api.text_to_group(
return await self.api.send_text_to_group( text=content, group_id=self.target_id, platform=self.platform, reply_to=reply_to
text=content, group_id=str(chat_stream.group_info.group_id), platform=chat_stream.platform
) )
else: else:
# 私聊 return await send_api.text_to_user(
return await self.api.send_text_to_user( text=content, user_id=self.target_id, platform=self.platform, reply_to=reply_to
text=content, user_id=str(chat_stream.user_info.user_id), platform=chat_stream.platform
) )
async def send_type(self, type: str, text: str, typing: bool = False) -> bool: async def send_emoji(self, emoji_base64: str) -> bool:
"""发送回复消息 """发送表情包
Args: Args:
text: 回复内容 emoji_base64: 表情包的base64编码
Returns: Returns:
bool: 是否发送成功 bool: 是否发送成功
""" """
chat_stream = self.api.get_service("chat_stream") # 导入send_api
if not chat_stream: from src.plugin_system.apis import send_api
logger.error(f"{self.log_prefix} 没有可用的聊天流发送回复")
if not self.target_id or not self.platform:
logger.error(f"{self.log_prefix} 缺少发送消息所需的信息")
return False return False
if chat_stream.group_info: if self.is_group:
# 群聊 return await send_api.emoji_to_group(emoji_base64, self.target_id, self.platform)
return await self.api.send_message_to_target(
message_type=type,
content=text,
platform=chat_stream.platform,
target_id=str(chat_stream.group_info.group_id),
is_group=True,
typing=typing,
)
else: else:
# 私聊 return await send_api.emoji_to_user(emoji_base64, self.target_id, self.platform)
return await self.api.send_message_to_target(
message_type=type, async def send_image(self, image_base64: str) -> bool:
content=text, """发送图片
platform=chat_stream.platform,
target_id=str(chat_stream.user_info.user_id), Args:
is_group=False, image_base64: 图片的base64编码
typing=typing,
Returns:
bool: 是否发送成功
"""
# 导入send_api
from src.plugin_system.apis import send_api
if not self.target_id or not self.platform:
logger.error(f"{self.log_prefix} 缺少发送消息所需的信息")
return False
if self.is_group:
return await send_api.image_to_group(image_base64, self.target_id, self.platform)
else:
return await send_api.image_to_user(image_base64, self.target_id, self.platform)
async def send_custom(self, message_type: str, content: str, typing: bool = False) -> bool:
"""发送自定义类型消息
Args:
message_type: 消息类型,如"video""file""audio"
content: 消息内容
typing: 是否显示正在输入
Returns:
bool: 是否发送成功
"""
# 导入send_api
from src.plugin_system.apis import send_api
if not self.target_id or not self.platform:
logger.error(f"{self.log_prefix} 缺少发送消息所需的信息")
return False
return await send_api.custom_message(
message_type=message_type,
content=content,
target_id=self.target_id,
is_group=self.is_group,
platform=self.platform,
typing=typing
) )
async def send_command(self, command_name: str, args: dict = None, display_message: str = None) -> bool: async def store_action_info(
self,
action_build_into_prompt: bool = False,
action_prompt_display: str = "",
action_done: bool = True,
) -> None:
"""存储动作信息到数据库
Args:
action_build_into_prompt: 是否构建到提示中
action_prompt_display: 显示的action提示信息
action_done: action是否完成
"""
await database_api.store_action_info(
chat_stream=self.chat_stream,
action_build_into_prompt=action_build_into_prompt,
action_prompt_display=action_prompt_display,
action_done=action_done,
thinking_id=self.thinking_id,
action_data=self.action_data,
action_name=self.action_name,
)
async def send_command(self, command_name: str, args: dict = None, display_message: str = None, storage_message: bool = True) -> bool:
"""发送命令消息 """发送命令消息
使用和send_text相同的方式通过MessageAPI发送命令 使用和send_text相同的方式通过MessageAPI发送命令
@@ -189,31 +332,21 @@ class BaseAction(ABC):
# 构造命令数据 # 构造命令数据
command_data = {"name": command_name, "args": args or {}} command_data = {"name": command_name, "args": args or {}}
# 使用send_message_to_target方法发送命令 if self.is_group:
chat_stream = self.api.get_service("chat_stream")
if not chat_stream:
logger.error(f"{self.log_prefix} 没有可用的聊天流发送命令")
return False
if chat_stream.group_info:
# 群聊 # 群聊
success = await self.api.send_message_to_target( success = await send_api.command_to_group(
message_type="command", command=command_data,
content=command_data, group_id=str(self.group_id),
platform=chat_stream.platform, platform=self.platform,
target_id=str(chat_stream.group_info.group_id), storage_message=storage_message
is_group=True,
display_message=display_message or f"执行命令: {command_name}",
) )
else: else:
# 私聊 # 私聊
success = await self.api.send_message_to_target( success = await send_api.command_to_user(
message_type="command", command=command_data,
content=command_data, user_id=str(self.user_id),
platform=chat_stream.platform, platform=self.platform,
target_id=str(chat_stream.user_info.user_id), storage_message=storage_message
is_group=False,
display_message=display_message or f"执行命令: {command_name}",
) )
if success: if success:
@@ -227,142 +360,6 @@ class BaseAction(ABC):
logger.error(f"{self.log_prefix} 发送命令时出错: {e}") logger.error(f"{self.log_prefix} 发送命令时出错: {e}")
return False return False
async def send_message_by_expressor(self, text: str, target: str = "") -> bool:
"""通过expressor发送文本消息的Action专用方法
Args:
text: 要发送的消息文本
target: 目标消息(可选)
Returns:
bool: 是否发送成功
"""
try:
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation
from src.chat.focus_chat.hfc_utils import create_empty_anchor_message
# 获取服务
expressor = self.api.get_service("expressor")
chat_stream = self.api.get_service("chat_stream")
observations = self.api.get_service("observations") or []
if not expressor or not chat_stream:
logger.error(f"{self.log_prefix} 无法通过expressor发送消息缺少必要的服务")
return False
# 构造动作数据
reply_data = {"text": text, "target": target, "emojis": []}
# 查找 ChattingObservation 实例
chatting_observation = None
for obs in observations:
if isinstance(obs, ChattingObservation):
chatting_observation = obs
break
if not chatting_observation:
logger.warning(f"{self.log_prefix} 未找到 ChattingObservation 实例,创建占位符")
anchor_message = await create_empty_anchor_message(
chat_stream.platform, chat_stream.group_info, chat_stream
)
else:
anchor_message = chatting_observation.search_message_by_text(target)
if not anchor_message:
logger.info(f"{self.log_prefix} 未找到锚点消息,创建占位符")
anchor_message = await create_empty_anchor_message(
chat_stream.platform, chat_stream.group_info, chat_stream
)
else:
anchor_message.update_chat_stream(chat_stream)
# 使用Action上下文信息发送消息
success, _ = await expressor.deal_reply(
cycle_timers=self.cycle_timers,
action_data=reply_data,
anchor_message=anchor_message,
reasoning=self.reasoning,
thinking_id=self.thinking_id,
)
if success:
logger.info(f"{self.log_prefix} 成功通过expressor发送消息")
else:
logger.error(f"{self.log_prefix} 通过expressor发送消息失败")
return success
except Exception as e:
logger.error(f"{self.log_prefix} 通过expressor发送消息时出错: {e}")
return False
async def send_message_by_replyer(self, target: str = "", extra_info_block: str = None) -> bool:
"""通过replyer发送消息的Action专用方法
Args:
target: 目标消息(可选)
extra_info_block: 额外信息块(可选)
Returns:
bool: 是否发送成功
"""
try:
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation
from src.chat.focus_chat.hfc_utils import create_empty_anchor_message
# 获取服务
replyer = self.api.get_service("replyer")
chat_stream = self.api.get_service("chat_stream")
observations = self.api.get_service("observations") or []
if not replyer or not chat_stream:
logger.error(f"{self.log_prefix} 无法通过replyer发送消息缺少必要的服务")
return False
# 构造动作数据
reply_data = {"target": target, "extra_info_block": extra_info_block}
# 查找 ChattingObservation 实例
chatting_observation = None
for obs in observations:
if isinstance(obs, ChattingObservation):
chatting_observation = obs
break
if not chatting_observation:
logger.warning(f"{self.log_prefix} 未找到 ChattingObservation 实例,创建占位符")
anchor_message = await create_empty_anchor_message(
chat_stream.platform, chat_stream.group_info, chat_stream
)
else:
anchor_message = chatting_observation.search_message_by_text(target)
if not anchor_message:
logger.info(f"{self.log_prefix} 未找到锚点消息,创建占位符")
anchor_message = await create_empty_anchor_message(
chat_stream.platform, chat_stream.group_info, chat_stream
)
else:
anchor_message.update_chat_stream(chat_stream)
# 使用Action上下文信息发送消息
success, _ = await replyer.deal_reply(
cycle_timers=self.cycle_timers,
action_data=reply_data,
anchor_message=anchor_message,
reasoning=self.reasoning,
thinking_id=self.thinking_id,
)
if success:
logger.info(f"{self.log_prefix} 成功通过replyer发送消息")
else:
logger.error(f"{self.log_prefix} 通过replyer发送消息失败")
return success
except Exception as e:
logger.error(f"{self.log_prefix} 通过replyer发送消息时出错: {e}")
return False
@classmethod @classmethod
def get_action_info(cls) -> "ActionInfo": def get_action_info(cls) -> "ActionInfo":
"""从类属性生成ActionInfo """从类属性生成ActionInfo
@@ -400,8 +397,8 @@ class BaseAction(ABC):
name=name, name=name,
component_type=ComponentType.ACTION, component_type=ComponentType.ACTION,
description=description, description=description,
focus_activation_type=get_enum_value("focus_activation_type", "never"), focus_activation_type=get_enum_value("focus_activation_type", "always"),
normal_activation_type=get_enum_value("normal_activation_type", "never"), normal_activation_type=get_enum_value("normal_activation_type", "always"),
activation_keywords=getattr(cls, "activation_keywords", []).copy(), activation_keywords=getattr(cls, "activation_keywords", []).copy(),
keyword_case_sensitive=getattr(cls, "keyword_case_sensitive", False), keyword_case_sensitive=getattr(cls, "keyword_case_sensitive", False),
mode_enable=get_mode_value("mode_enable", "all"), mode_enable=get_mode_value("mode_enable", "all"),
@@ -433,3 +430,40 @@ class BaseAction(ABC):
Tuple[bool, str]: (是否执行成功, 回复文本) Tuple[bool, str]: (是否执行成功, 回复文本)
""" """
return await self.execute() return await self.execute()
def get_action_context(self, key: str, default=None):
"""获取action上下文信息
Args:
key: 上下文键名
default: 默认值
Returns:
Any: 上下文值或默认值
"""
return self.api.get_action_context(key, default)
def get_config(self, key: str, default=None):
"""获取插件配置值,支持嵌套键访问
Args:
key: 配置键名,支持嵌套访问如 "section.subsection.key"
default: 默认值
Returns:
Any: 配置值或默认值
"""
if not self.plugin_config:
return default
# 支持嵌套键访问
keys = key.split(".")
current = self.plugin_config
for k in keys:
if isinstance(current, dict) and k in current:
current = current[k]
else:
return default
return current

View File

@@ -1,9 +1,9 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, Tuple, Optional, List from typing import Dict, Tuple, Optional, List
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system.apis.plugin_api import PluginAPI
from src.plugin_system.base.component_types import CommandInfo, ComponentType from src.plugin_system.base.component_types import CommandInfo, ComponentType
from src.chat.message_receive.message import MessageRecv from src.chat.message_receive.message import MessageRecv
from src.plugin_system.apis import send_api
logger = get_logger("base_command") logger = get_logger("base_command")
@@ -20,6 +20,9 @@ class BaseCommand(ABC):
- intercept_message: 是否拦截消息处理默认True拦截False继续传递 - intercept_message: 是否拦截消息处理默认True拦截False继续传递
""" """
command_name: str = ""
command_description: str = ""
# 默认命令设置(子类可以覆盖) # 默认命令设置(子类可以覆盖)
command_pattern: str = "" command_pattern: str = ""
command_help: str = "" command_help: str = ""
@@ -35,9 +38,7 @@ class BaseCommand(ABC):
""" """
self.message = message self.message = message
self.matched_groups: Dict[str, str] = {} # 存储正则表达式匹配的命名组 self.matched_groups: Dict[str, str] = {} # 存储正则表达式匹配的命名组
self.plugin_config = plugin_config or {} # 直接存储插件配置字典
# 创建API实例
self.api = PluginAPI(chat_stream=message.chat_stream, log_prefix="[Command]", plugin_config=plugin_config)
self.log_prefix = "[Command]" self.log_prefix = "[Command]"
@@ -60,6 +61,31 @@ class BaseCommand(ABC):
""" """
pass pass
def get_config(self, key: str, default=None):
"""获取插件配置值,支持嵌套键访问
Args:
key: 配置键名,支持嵌套访问如 "section.subsection.key"
default: 默认值
Returns:
Any: 配置值或默认值
"""
if not self.plugin_config:
return default
# 支持嵌套键访问
keys = key.split(".")
current = self.plugin_config
for k in keys:
if isinstance(current, dict) and k in current:
current = current[k]
else:
return default
return current
async def send_text(self, content: str) -> None: async def send_text(self, content: str) -> None:
"""发送回复消息 """发送回复消息
@@ -71,13 +97,19 @@ class BaseCommand(ABC):
if chat_stream.group_info: if chat_stream.group_info:
# 群聊 # 群聊
await self.api.send_text_to_group(
text=content, group_id=str(chat_stream.group_info.group_id), platform=chat_stream.platform await send_api.text_to_group(
text=content,
group_id=str(chat_stream.group_info.group_id),
platform=chat_stream.platform
) )
else: else:
# 私聊 # 私聊
await self.api.send_text_to_user(
text=content, user_id=str(chat_stream.user_info.user_id), platform=chat_stream.platform await send_api.text_to_user(
text=content,
user_id=str(chat_stream.user_info.user_id),
platform=chat_stream.platform
) )
async def send_type( async def send_type(
@@ -98,31 +130,30 @@ class BaseCommand(ABC):
if chat_stream.group_info: if chat_stream.group_info:
# 群聊 # 群聊
return await self.api.send_message_to_target( from src.plugin_system.apis import send_api
return await send_api.custom_message(
message_type=message_type, message_type=message_type,
content=content, content=content,
platform=chat_stream.platform,
target_id=str(chat_stream.group_info.group_id), target_id=str(chat_stream.group_info.group_id),
is_group=True, is_group=True,
display_message=display_message, platform=chat_stream.platform,
typing=typing, typing=typing,
) )
else: else:
# 私聊 # 私聊
return await self.api.send_message_to_target( from src.plugin_system.apis import send_api
return await send_api.custom_message(
message_type=message_type, message_type=message_type,
content=content, content=content,
platform=chat_stream.platform,
target_id=str(chat_stream.user_info.user_id), target_id=str(chat_stream.user_info.user_id),
is_group=False, is_group=False,
display_message=display_message, platform=chat_stream.platform,
typing=typing,
) )
async def send_command(self, command_name: str, args: dict = None, display_message: str = None) -> bool: async def send_command(self, command_name: str, args: dict = None, display_message: str = None) -> bool:
"""发送命令消息 """发送命令消息
使用和send_text相同的方式通过MessageAPI发送命令
Args: Args:
command_name: 命令名称 command_name: 命令名称
args: 命令参数 args: 命令参数
@@ -135,29 +166,28 @@ class BaseCommand(ABC):
# 构造命令数据 # 构造命令数据
command_data = {"name": command_name, "args": args or {}} command_data = {"name": command_name, "args": args or {}}
# 使用send_message_to_target方法发送命令 # 获取聊天流信息
chat_stream = self.message.chat_stream chat_stream = self.message.chat_stream
command_content = command_data
if chat_stream.group_info: if chat_stream.group_info:
# 群聊 # 群聊
success = await self.api.send_message_to_target( from src.plugin_system.apis import send_api
success = await send_api.custom_message(
message_type="command", message_type="command",
content=command_content, content=command_data,
platform=chat_stream.platform,
target_id=str(chat_stream.group_info.group_id), target_id=str(chat_stream.group_info.group_id),
is_group=True, is_group=True,
display_message=display_message or f"执行命令: {command_name}", platform=chat_stream.platform,
) )
else: else:
# 私聊 # 私聊
success = await self.api.send_message_to_target( from src.plugin_system.apis import send_api
success = await send_api.custom_message(
message_type="command", message_type="command",
content=command_content, content=command_data,
platform=chat_stream.platform,
target_id=str(chat_stream.user_info.user_id), target_id=str(chat_stream.user_info.user_id),
is_group=False, is_group=False,
display_message=display_message or f"执行命令: {command_name}", platform=chat_stream.platform,
) )
if success: if success:
@@ -172,7 +202,7 @@ class BaseCommand(ABC):
return False return False
@classmethod @classmethod
def get_command_info(cls, name: str = None, description: str = None) -> "CommandInfo": def get_command_info(cls) -> "CommandInfo":
"""从类属性生成CommandInfo """从类属性生成CommandInfo
Args: Args:
@@ -183,19 +213,10 @@ class BaseCommand(ABC):
CommandInfo: 生成的Command信息对象 CommandInfo: 生成的Command信息对象
""" """
# 优先使用类属性,然后自动生成
if name is None:
name = getattr(cls, "command_name", cls.__name__.lower().replace("command", ""))
if description is None:
description = getattr(cls, "command_description", None)
if description is None:
description = cls.__doc__ or f"{cls.__name__} Command组件"
description = description.strip().split("\n")[0] # 取第一行作为描述
return CommandInfo( return CommandInfo(
name=name, name=cls.command_name,
component_type=ComponentType.COMMAND, component_type=ComponentType.COMMAND,
description=description, description=cls.command_description,
command_pattern=cls.command_pattern, command_pattern=cls.command_pattern,
command_help=cls.command_help, command_help=cls.command_help,
command_examples=cls.command_examples.copy() if cls.command_examples else [], command_examples=cls.command_examples.copy() if cls.command_examples else [],

View File

@@ -54,23 +54,43 @@ class ComponentRegistry:
""" """
component_name = component_info.name component_name = component_info.name
component_type = component_info.component_type component_type = component_info.component_type
plugin_name = getattr(component_info, 'plugin_name', 'unknown')
if component_name in self._components: # 🔥 系统级别自动区分:为不同类型的组件添加命名空间前缀
logger.warning(f"组件 {component_name} 已存在,跳过注册") if component_type == ComponentType.ACTION:
namespaced_name = f"action.{component_name}"
elif component_type == ComponentType.COMMAND:
namespaced_name = f"command.{component_name}"
else:
# 未来扩展的组件类型
namespaced_name = f"{component_type.value}.{component_name}"
# 检查命名空间化的名称是否冲突
if namespaced_name in self._components:
existing_info = self._components[namespaced_name]
existing_plugin = getattr(existing_info, 'plugin_name', 'unknown')
logger.warning(
f"组件冲突: {component_type.value}组件 '{component_name}' "
f"已被插件 '{existing_plugin}' 注册,跳过插件 '{plugin_name}' 的注册"
)
return False return False
# 注册到通用注册表 # 注册到通用注册表(使用命名空间化的名称)
self._components[component_name] = component_info self._components[namespaced_name] = component_info
self._components_by_type[component_type][component_name] = component_info self._components_by_type[component_type][component_name] = component_info # 类型内部仍使用原名
self._component_classes[component_name] = component_class self._component_classes[namespaced_name] = component_class
# 根据组件类型进行特定注册 # 根据组件类型进行特定注册(使用原始名称)
if component_type == ComponentType.ACTION: if component_type == ComponentType.ACTION:
self._register_action_component(component_info, component_class) self._register_action_component(component_info, component_class)
elif component_type == ComponentType.COMMAND: elif component_type == ComponentType.COMMAND:
self._register_command_component(component_info, component_class) self._register_command_component(component_info, component_class)
logger.debug(f"已注册{component_type.value}组件: {component_name} ({component_class.__name__})") logger.debug(
f"已注册{component_type.value}组件: '{component_name}' -> '{namespaced_name}' "
f"({component_class.__name__}) [插件: {plugin_name}]"
)
return True return True
def _register_action_component(self, action_info: ActionInfo, action_class: Type): def _register_action_component(self, action_info: ActionInfo, action_class: Type):
@@ -94,14 +114,104 @@ class ComponentRegistry:
# === 组件查询方法 === # === 组件查询方法 ===
def get_component_info(self, component_name: str) -> Optional[ComponentInfo]: def get_component_info(self, component_name: str, component_type: ComponentType = None) -> Optional[ComponentInfo]:
"""获取组件信息""" """获取组件信息,支持自动命名空间解析
Args:
component_name: 组件名称,可以是原始名称或命名空间化的名称
component_type: 组件类型,如果提供则优先在该类型中查找
Returns:
Optional[ComponentInfo]: 组件信息或None
"""
# 1. 如果已经是命名空间化的名称,直接查找
if '.' in component_name:
return self._components.get(component_name) return self._components.get(component_name)
def get_component_class(self, component_name: str) -> Optional[Type]: # 2. 如果指定了组件类型,构造命名空间化的名称查找
"""获取组件类""" if component_type:
if component_type == ComponentType.ACTION:
namespaced_name = f"action.{component_name}"
elif component_type == ComponentType.COMMAND:
namespaced_name = f"command.{component_name}"
else:
namespaced_name = f"{component_type.value}.{component_name}"
return self._components.get(namespaced_name)
# 3. 如果没有指定类型,尝试在所有命名空间中查找
candidates = []
for namespace_prefix in ["action", "command"]:
namespaced_name = f"{namespace_prefix}.{component_name}"
component_info = self._components.get(namespaced_name)
if component_info:
candidates.append((namespace_prefix, namespaced_name, component_info))
if len(candidates) == 1:
# 只有一个匹配,直接返回
return candidates[0][2]
elif len(candidates) > 1:
# 多个匹配,记录警告并返回第一个
namespaces = [ns for ns, _, _ in candidates]
logger.warning(
f"组件名称 '{component_name}' 在多个命名空间中存在: {namespaces}"
f"使用第一个匹配项: {candidates[0][1]}"
)
return candidates[0][2]
# 4. 都没找到
return None
def get_component_class(self, component_name: str, component_type: ComponentType = None) -> Optional[Type]:
"""获取组件类,支持自动命名空间解析
Args:
component_name: 组件名称,可以是原始名称或命名空间化的名称
component_type: 组件类型,如果提供则优先在该类型中查找
Returns:
Optional[Type]: 组件类或None
"""
# 1. 如果已经是命名空间化的名称,直接查找
if '.' in component_name:
return self._component_classes.get(component_name) return self._component_classes.get(component_name)
# 2. 如果指定了组件类型,构造命名空间化的名称查找
if component_type:
if component_type == ComponentType.ACTION:
namespaced_name = f"action.{component_name}"
elif component_type == ComponentType.COMMAND:
namespaced_name = f"command.{component_name}"
else:
namespaced_name = f"{component_type.value}.{component_name}"
return self._component_classes.get(namespaced_name)
# 3. 如果没有指定类型,尝试在所有命名空间中查找
candidates = []
for namespace_prefix in ["action", "command"]:
namespaced_name = f"{namespace_prefix}.{component_name}"
component_class = self._component_classes.get(namespaced_name)
if component_class:
candidates.append((namespace_prefix, namespaced_name, component_class))
if len(candidates) == 1:
# 只有一个匹配,直接返回
namespace, full_name, cls = candidates[0]
logger.debug(f"自动解析组件: '{component_name}' -> '{full_name}'")
return cls
elif len(candidates) > 1:
# 多个匹配,记录警告并返回第一个
namespaces = [ns for ns, _, _ in candidates]
logger.warning(
f"组件名称 '{component_name}' 在多个命名空间中存在: {namespaces}"
f"使用第一个匹配项: {candidates[0][1]}"
)
return candidates[0][2]
# 4. 都没找到
return None
def get_components_by_type(self, component_type: ComponentType) -> Dict[str, ComponentInfo]: def get_components_by_type(self, component_type: ComponentType) -> Dict[str, ComponentInfo]:
"""获取指定类型的所有组件""" """获取指定类型的所有组件"""
return self._components_by_type.get(component_type, {}).copy() return self._components_by_type.get(component_type, {}).copy()
@@ -123,7 +233,7 @@ class ComponentRegistry:
def get_action_info(self, action_name: str) -> Optional[ActionInfo]: def get_action_info(self, action_name: str) -> Optional[ActionInfo]:
"""获取Action信息""" """获取Action信息"""
info = self.get_component_info(action_name) info = self.get_component_info(action_name, ComponentType.ACTION)
return info if isinstance(info, ActionInfo) else None return info if isinstance(info, ActionInfo) else None
# === Command特定查询方法 === # === Command特定查询方法 ===
@@ -138,7 +248,7 @@ class ComponentRegistry:
def get_command_info(self, command_name: str) -> Optional[CommandInfo]: def get_command_info(self, command_name: str) -> Optional[CommandInfo]:
"""获取Command信息""" """获取Command信息"""
info = self.get_component_info(command_name) info = self.get_component_info(command_name, ComponentType.COMMAND)
return info if isinstance(info, CommandInfo) else None return info if isinstance(info, CommandInfo) else None
def find_command_by_text(self, text: str) -> Optional[tuple[Type, dict, bool, str]]: def find_command_by_text(self, text: str) -> Optional[tuple[Type, dict, bool, str]]:
@@ -150,7 +260,9 @@ class ComponentRegistry:
Returns: Returns:
Optional[tuple[Type, dict, bool, str]]: (命令类, 匹配的命名组, 是否拦截消息, 插件名) 或 None Optional[tuple[Type, dict, bool, str]]: (命令类, 匹配的命名组, 是否拦截消息, 插件名) 或 None
""" """
for pattern, command_class in self._command_patterns.items(): for pattern, command_class in self._command_patterns.items():
match = pattern.match(text) match = pattern.match(text)
if match: if match:
command_name = None command_name = None
@@ -163,7 +275,8 @@ class ComponentRegistry:
# 检查命令是否启用 # 检查命令是否启用
if command_name: if command_name:
command_info = self.get_command_info(command_name) command_info = self.get_command_info(command_name)
if command_info and command_info.enabled: if command_info:
if command_info.enabled:
return ( return (
command_class, command_class,
match.groupdict(), match.groupdict(),
@@ -227,26 +340,51 @@ class ComponentRegistry:
# === 状态管理方法 === # === 状态管理方法 ===
def enable_component(self, component_name: str) -> bool: def enable_component(self, component_name: str, component_type: ComponentType = None) -> bool:
"""启用组件""" """启用组件,支持命名空间解析"""
if component_name in self._components: # 首先尝试找到正确的命名空间化名称
self._components[component_name].enabled = True component_info = self.get_component_info(component_name, component_type)
if not component_info:
return False
# 根据组件类型构造正确的命名空间化名称
if component_info.component_type == ComponentType.ACTION:
namespaced_name = f"action.{component_name}" if '.' not in component_name else component_name
elif component_info.component_type == ComponentType.COMMAND:
namespaced_name = f"command.{component_name}" if '.' not in component_name else component_name
else:
namespaced_name = f"{component_info.component_type.value}.{component_name}" if '.' not in component_name else component_name
if namespaced_name in self._components:
self._components[namespaced_name].enabled = True
# 如果是Action更新默认动作集 # 如果是Action更新默认动作集
component_info = self._components[component_name]
if isinstance(component_info, ActionInfo): if isinstance(component_info, ActionInfo):
self._default_actions[component_name] = component_info.description self._default_actions[component_name] = component_info.description
logger.debug(f"已启用组件: {component_name}") logger.debug(f"已启用组件: {component_name} -> {namespaced_name}")
return True return True
return False return False
def disable_component(self, component_name: str) -> bool: def disable_component(self, component_name: str, component_type: ComponentType = None) -> bool:
"""禁用组件""" """禁用组件,支持命名空间解析"""
if component_name in self._components: # 首先尝试找到正确的命名空间化名称
self._components[component_name].enabled = False component_info = self.get_component_info(component_name, component_type)
if not component_info:
return False
# 根据组件类型构造正确的命名空间化名称
if component_info.component_type == ComponentType.ACTION:
namespaced_name = f"action.{component_name}" if '.' not in component_name else component_name
elif component_info.component_type == ComponentType.COMMAND:
namespaced_name = f"command.{component_name}" if '.' not in component_name else component_name
else:
namespaced_name = f"{component_info.component_type.value}.{component_name}" if '.' not in component_name else component_name
if namespaced_name in self._components:
self._components[namespaced_name].enabled = False
# 如果是Action从默认动作集中移除 # 如果是Action从默认动作集中移除
if component_name in self._default_actions: if component_name in self._default_actions:
del self._default_actions[component_name] del self._default_actions[component_name]
logger.debug(f"已禁用组件: {component_name}") logger.debug(f"已禁用组件: {component_name} -> {namespaced_name}")
return True return True
return False return False

View File

@@ -1,27 +0,0 @@
# 核心动作插件配置文件
[plugin]
name = "core_actions"
description = "系统核心动作插件"
version = "0.2"
author = "built-in"
enabled = true
[no_reply]
# 等待新消息的超时时间(秒)
waiting_timeout = 1200
[emoji]
# 表情动作配置
enabled = true
# 在Normal模式下的随机激活概率
random_probability = 0.1
# 是否启用智能表情选择
smart_selection = true
# LLM判断相关配置
[emoji.llm_judge]
# 是否启用LLM智能判断
enabled = true
# 自定义判断提示词(可选)
custom_prompt = ""

View File

@@ -5,18 +5,18 @@
这是系统的内置插件,提供基础的聊天交互功能 这是系统的内置插件,提供基础的聊天交互功能
""" """
import re import time
from typing import List, Tuple, Type, Optional from typing import List, Tuple, Type
# 导入新插件系统 # 导入新插件系统
from src.plugin_system import BasePlugin, register_plugin, BaseAction, ComponentInfo, ActionActivationType, ChatMode from src.plugin_system import BasePlugin, register_plugin, BaseAction, ComponentInfo, ActionActivationType, ChatMode
from src.plugin_system.base.base_command import BaseCommand
from src.plugin_system.base.config_types import ConfigField from src.plugin_system.base.config_types import ConfigField
# 导入依赖的系统组件 # 导入依赖的系统组件
from src.common.logger import get_logger from src.common.logger import get_logger
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation
from src.chat.focus_chat.hfc_utils import create_empty_anchor_message # 导入API模块 - 标准Python包方式
from src.plugin_system.apis import emoji_api, generator_api, message_api
logger = get_logger("core_actions") logger = get_logger("core_actions")
@@ -35,11 +35,11 @@ class ReplyAction(BaseAction):
# 动作基本信息 # 动作基本信息
action_name = "reply" action_name = "reply"
action_description = "参与聊天回复,处理文本和表情的发送" action_description = "参与聊天回复,发送文本进行表达"
# 动作参数定义 # 动作参数定义
action_parameters = { action_parameters = {
"reply_to": "如果是明确回复某个人的发言请在reply_to参数中指定格式用户名:发言内容如果不是reply_to的值设为none" "reply_to": "你要回复的对方的发言内容,格式:(用户名:发言内容),可以为none"
} }
# 动作使用场景 # 动作使用场景
@@ -52,40 +52,52 @@ class ReplyAction(BaseAction):
"""执行回复动作""" """执行回复动作"""
logger.info(f"{self.log_prefix} 决定回复: {self.reasoning}") logger.info(f"{self.log_prefix} 决定回复: {self.reasoning}")
start_time = self.action_data.get("loop_start_time", time.time())
try: try:
# 获取聊天观察
chatting_observation = self._get_chatting_observation()
if not chatting_observation:
return False, "未找到聊天观察"
# 处理回复目标 success, reply_set = await generator_api.generate_reply(
anchor_message = await self._resolve_reply_target(chatting_observation) chat_stream=self.chat_stream,
# 获取回复器服务
replyer = self.api.get_service("replyer")
if not replyer:
logger.error(f"{self.log_prefix} 未找到回复器服务")
return False, "回复器服务不可用"
# 执行回复
success, reply_set = await replyer.deal_reply(
cycle_timers=self.cycle_timers,
action_data=self.action_data, action_data=self.action_data,
anchor_message=anchor_message, platform=self.platform,
reasoning=self.reasoning, chat_id=self.chat_id,
thinking_id=self.thinking_id, is_group=self.is_group
) )
# 检查从start_time以来的新消息数量
# 获取动作触发时间或使用默认值
current_time = time.time()
new_message_count = message_api.count_new_messages(
chat_id=self.chat_id,
start_time=start_time,
end_time=current_time
)
# 根据新消息数量决定是否使用reply_to
need_reply = new_message_count >= 4
logger.info(f"{self.log_prefix}{start_time}{current_time}共有{new_message_count}条新消息,{'使用' if need_reply else '不使用'}reply_to")
# 构建回复文本 # 构建回复文本
reply_text = self._build_reply_text(reply_set) reply_text = ""
first_reply = False
for reply_seg in reply_set:
data = reply_seg[1]
if not first_reply and need_reply:
await self.send_text(
content=data,
reply_to=self.action_data.get("reply_to", "")
)
else:
await self.send_text(content=data)
first_reply = True
reply_text += data
# 存储动作记录 # 存储动作记录
await self.api.store_action_info( await self.store_action_info(
action_build_into_prompt=False, action_build_into_prompt=False,
action_prompt_display=reply_text, action_prompt_display=reply_text,
action_done=True, action_done=True,
thinking_id=self.thinking_id,
action_data=self.action_data,
) )
# 重置NoReplyAction的连续计数器 # 重置NoReplyAction的连续计数器
@@ -97,47 +109,6 @@ class ReplyAction(BaseAction):
logger.error(f"{self.log_prefix} 回复动作执行失败: {e}") logger.error(f"{self.log_prefix} 回复动作执行失败: {e}")
return False, f"回复失败: {str(e)}" return False, f"回复失败: {str(e)}"
def _get_chatting_observation(self) -> Optional[ChattingObservation]:
"""获取聊天观察对象"""
observations = self.api.get_service("observations") or []
for obs in observations:
if isinstance(obs, ChattingObservation):
return obs
return None
async def _resolve_reply_target(self, chatting_observation: ChattingObservation):
"""解析回复目标消息"""
reply_to = self.action_data.get("reply_to", "none")
if ":" in reply_to or "" in reply_to:
# 解析回复目标格式:用户名:消息内容
parts = re.split(pattern=r"[:]", string=reply_to, maxsplit=1)
if len(parts) == 2:
target = parts[1].strip()
anchor_message = chatting_observation.search_message_by_text(target)
if anchor_message:
chat_stream = self.api.get_service("chat_stream")
if chat_stream:
anchor_message.update_chat_stream(chat_stream)
return anchor_message
# 创建空锚点消息
logger.info(f"{self.log_prefix} 未找到锚点消息,创建占位符")
chat_stream = self.api.get_service("chat_stream")
if chat_stream:
return await create_empty_anchor_message(chat_stream.platform, chat_stream.group_info, chat_stream)
return None
def _build_reply_text(self, reply_set) -> str:
"""构建回复文本"""
reply_text = ""
if reply_set:
for reply in reply_set:
reply_type = reply[0]
data = reply[1]
if reply_type in ["text", "emoji"]:
reply_text += data
return reply_text
class NoReplyAction(BaseAction): class NoReplyAction(BaseAction):
@@ -178,12 +149,19 @@ class NoReplyAction(BaseAction):
count = NoReplyAction._consecutive_count count = NoReplyAction._consecutive_count
# 计算本次等待时间 # 计算本次等待时间
timeout = self._calculate_waiting_time(count) if count <= len(self._waiting_stages):
# 前3次使用预设时间
stage_time = self._waiting_stages[count - 1]
# 如果WAITING_TIME_THRESHOLD更小则使用它
timeout = min(stage_time, self.waiting_timeout)
else:
# 第4次及以后使用WAITING_TIME_THRESHOLD
timeout = self.waiting_timeout
logger.info(f"{self.log_prefix} 选择不回复(第{count}次连续),等待新消息中... (超时: {timeout}秒)") logger.info(f"{self.log_prefix} 选择不回复(第{count}次连续),等待新消息中... (超时: {timeout}秒)")
# 等待新消息或达到时间上限 # 等待新消息或达到时间上限
result = await self.api.wait_for_new_message(timeout) result = await self.wait_for_new_message(timeout)
# 如果有新消息或者超时都不重置计数器因为可能还会继续no_reply # 如果有新消息或者超时都不重置计数器因为可能还会继续no_reply
return result return result
@@ -192,17 +170,6 @@ class NoReplyAction(BaseAction):
logger.error(f"{self.log_prefix} 不回复动作执行失败: {e}") logger.error(f"{self.log_prefix} 不回复动作执行失败: {e}")
return False, f"不回复动作执行失败: {e}" return False, f"不回复动作执行失败: {e}"
def _calculate_waiting_time(self, consecutive_count: int) -> int:
"""根据连续次数计算等待时间"""
if consecutive_count <= len(self._waiting_stages):
# 前3次使用预设时间
stage_time = self._waiting_stages[consecutive_count - 1]
# 如果WAITING_TIME_THRESHOLD更小则使用它
return min(stage_time, self.waiting_timeout)
else:
# 第4次及以后使用WAITING_TIME_THRESHOLD
return self.waiting_timeout
@classmethod @classmethod
def reset_consecutive_count(cls): def reset_consecutive_count(cls):
"""重置连续计数器""" """重置连续计数器"""
@@ -248,56 +215,33 @@ class EmojiAction(BaseAction):
logger.info(f"{self.log_prefix} 决定发送表情") logger.info(f"{self.log_prefix} 决定发送表情")
try: try:
# 创建空锚点消息 # 1. 根据描述选择表情包
anchor_message = await self._create_anchor_message() description = self.action_data.get("description", "")
if not anchor_message: emoji_result = await emoji_api.get_by_description(description)
return False, "无法创建锚点消息"
# 获取回复器服务 if not emoji_result:
replyer = self.api.get_service("replyer") logger.warning(f"{self.log_prefix} 未找到匹配描述 '{description}' 的表情包")
if not replyer: return False, f"未找到匹配 '{description}' 的表情包"
logger.error(f"{self.log_prefix} 未找到回复器服务")
return False, "回复器服务不可用"
# 执行表情处理 emoji_base64, emoji_description, matched_emotion = emoji_result
success, reply_set = await replyer.deal_emoji( logger.info(f"{self.log_prefix} 找到表情包: {emoji_description}, 匹配情感: {matched_emotion}")
cycle_timers=self.cycle_timers,
action_data=self.action_data,
anchor_message=anchor_message,
thinking_id=self.thinking_id,
)
# 构建回复文本 # 使用BaseAction的便捷方法发送表情包
reply_text = self._build_reply_text(reply_set) success = await self.send_emoji(emoji_base64)
if not success:
logger.error(f"{self.log_prefix} 表情包发送失败")
return False, "表情包发送失败"
# 重置NoReplyAction的连续计数器 # 重置NoReplyAction的连续计数器
NoReplyAction.reset_consecutive_count() NoReplyAction.reset_consecutive_count()
return success, reply_text return True, f"发送表情包: {emoji_description}"
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix} 表情动作执行失败: {e}") logger.error(f"{self.log_prefix} 表情动作执行失败: {e}")
return False, f"表情发送失败: {str(e)}" return False, f"表情发送失败: {str(e)}"
async def _create_anchor_message(self):
"""创建锚点消息"""
chat_stream = self.api.get_service("chat_stream")
if chat_stream:
logger.info(f"{self.log_prefix} 为表情包创建占位符")
return await create_empty_anchor_message(chat_stream.platform, chat_stream.group_info, chat_stream)
return None
def _build_reply_text(self, reply_set) -> str:
"""构建回复文本"""
reply_text = ""
if reply_set:
for reply in reply_set:
reply_type = reply[0]
data = reply[1]
if reply_type in ["text", "emoji"]:
reply_text += data
return reply_text
class ChangeToFocusChatAction(BaseAction): class ChangeToFocusChatAction(BaseAction):
"""切换到专注聊天动作 - 从普通模式切换到专注模式""" """切换到专注聊天动作 - 从普通模式切换到专注模式"""
@@ -314,6 +258,7 @@ class ChangeToFocusChatAction(BaseAction):
# 动作参数定义 # 动作参数定义
action_parameters = {} action_parameters = {}
apex = 111
# 动作使用场景 # 动作使用场景
action_require = [ action_require = [
"你想要进入专注聊天模式", "你想要进入专注聊天模式",
@@ -437,8 +382,6 @@ class CoreActionsPlugin(BasePlugin):
"enable_emoji": ConfigField(type=bool, default=True, description="是否启用'表情'动作"), "enable_emoji": ConfigField(type=bool, default=True, description="是否启用'表情'动作"),
"enable_change_to_focus": ConfigField(type=bool, default=True, description="是否启用'切换到专注模式'动作"), "enable_change_to_focus": ConfigField(type=bool, default=True, description="是否启用'切换到专注模式'动作"),
"enable_exit_focus": ConfigField(type=bool, default=True, description="是否启用'退出专注模式'动作"), "enable_exit_focus": ConfigField(type=bool, default=True, description="是否启用'退出专注模式'动作"),
"enable_ping_command": ConfigField(type=bool, default=True, description="是否启用'/ping'测试命令"),
"enable_log_command": ConfigField(type=bool, default=True, description="是否启用'/log'日志命令"),
}, },
"no_reply": { "no_reply": {
"waiting_timeout": ConfigField( "waiting_timeout": ConfigField(
@@ -482,73 +425,137 @@ class CoreActionsPlugin(BasePlugin):
components.append((ExitFocusChatAction.get_action_info(), ExitFocusChatAction)) components.append((ExitFocusChatAction.get_action_info(), ExitFocusChatAction))
if self.get_config("components.enable_change_to_focus", True): if self.get_config("components.enable_change_to_focus", True):
components.append((ChangeToFocusChatAction.get_action_info(), ChangeToFocusChatAction)) components.append((ChangeToFocusChatAction.get_action_info(), ChangeToFocusChatAction))
if self.get_config("components.enable_ping_command", True): # components.append((DeepReplyAction.get_action_info(), DeepReplyAction))
components.append(
(PingCommand.get_command_info(name="ping", description="测试机器人响应,拦截后续处理"), PingCommand)
)
if self.get_config("components.enable_log_command", True):
components.append(
(LogCommand.get_command_info(name="log", description="记录消息到日志,不拦截后续处理"), LogCommand)
)
return components return components
# ===== 示例Command组件 =====
class PingCommand(BaseCommand):
"""Ping命令 - 测试响应,拦截消息处理"""
command_pattern = r"^/ping(\s+(?P<message>.+))?$"
command_help = "测试机器人响应 - 拦截后续处理"
command_examples = ["/ping", "/ping 测试消息"]
intercept_message = True # 拦截消息,不继续处理
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行ping命令"""
try:
message = self.matched_groups.get("message", "")
reply_text = f"🏓 Pong! {message}" if message else "🏓 Pong!"
await self.send_text(reply_text)
return True, f"发送ping响应: {reply_text}"
except Exception as e:
logger.error(f"Ping命令执行失败: {e}")
return False, f"执行失败: {str(e)}"
class LogCommand(BaseCommand): # class DeepReplyAction(BaseAction):
"""日志命令 - 记录消息但不拦截后续处理""" # """回复动作 - 参与聊天回复"""
command_pattern = r"^/log(\s+(?P<level>debug|info|warn|error))?$" # # 激活设置
command_help = "记录当前消息到日志 - 不拦截后续处理" # focus_activation_type = ActionActivationType.ALWAYS
command_examples = ["/log", "/log info", "/log debug"] # normal_activation_type = ActionActivationType.NEVER
intercept_message = False # 不拦截消息,继续后续处理 # mode_enable = ChatMode.FOCUS
# parallel_action = False
async def execute(self) -> Tuple[bool, Optional[str]]: # # 动作基本信息
"""执行日志命令""" # action_name = "deep_reply"
try: # action_description = "参与聊天回复,关注某个话题,对聊天内容进行深度思考,给出回复"
level = self.matched_groups.get("level", "info")
user_nickname = self.message.message_info.user_info.user_nickname
content = self.message.processed_plain_text
log_message = f"[{level.upper()}] 用户 {user_nickname}: {content}" # # 动作参数定义
# action_parameters = {
# "topic": "想要思考的话题"
# }
# 根据级别记录日志 # # 动作使用场景
if level == "debug": # action_require = ["有些问题需要深度思考", "某个问题可能涉及多个方面", "某个问题涉及专业领域或者需要专业知识","这个问题讨论的很激烈,需要深度思考"]
logger.debug(log_message)
elif level == "warn":
logger.warning(log_message)
elif level == "error":
logger.error(log_message)
else:
logger.info(log_message)
# 不发送回复,让消息继续处理 # # 关联类型
return True, f"已记录到{level}级别日志" # associated_types = ["text"]
except Exception as e: # async def execute(self) -> Tuple[bool, str]:
logger.error(f"Log命令执行失败: {e}") # """执行回复动作"""
return False, f"执行失败: {str(e)}" # logger.info(f"{self.log_prefix} 决定深度思考")
# try:
# # 获取聊天观察
# chatting_observation = self._get_chatting_observation()
# if not chatting_observation:
# return False, "未找到聊天观察"
# talking_message_str = chatting_observation.talking_message_str
# # 处理回复目标
# chat_stream = self.api.get_service("chat_stream")
# anchor_message = await create_empty_anchor_message(chat_stream.platform, chat_stream.group_info, chat_stream)
# llm_model = self.api.get_available_models().replyer_1
# prompt = f"""
# {talking_message_str}
# 在上面的聊天中,你对{self.action_data.get("topic", "")}感兴趣,形成深刻观点,请你思考,总结成一份学术论文,APA标准格式
# """
# success, response, reasoning, model_name = await self.api.generate_with_model(prompt, llm_model)
# print(prompt)
# print(f"DeepReplyAction: {response}")
# # prompt = f"""
# # {talking_message_str}
# # 在上面的聊天中,你对{self.action_data.get("topic", "")}感兴趣,请你思考
# # """
# extra_info_block = self.action_data.get("extra_info_block", "")
# extra_info_block += response
# # extra_info_block += f"\n--------------------------------\n注意这是最重要的内容你现在可以用比较长的篇幅来表达你的观点不要只回复一个字或者几个字\n由于你进入了深度思考模式所以下方聊天记录的回复要求不再适用请你自由的表达不论字数长短限制\n\n--------------------------------\n注意你现在可以用比较长的篇幅来表达你的观点不要只回复一个字或者几个字\n由于你进入了深度思考模式所以下方聊天记录的回复要求不再适用请你自由的表达不论字数长短限制\n"
# # extra_info_block += f"\n--------------------------------\n注意优先关注这句你现在可以用比较长的篇幅来表达你的观点不要只回复一个字或者几个字\n由于你进入了深度思考模式所以下方聊天记录的回复要求不再适用请你自由的表达不论字数长短限制\n\n--------------------------------\n注意你现在可以用比较长的篇幅来表达你的观点不要只回复一个字或者几个字\n由于你进入了深度思考模式所以其他的回复要求不再适用请你自由的表达不论字数长短限制\n"
# self.action_data["extra_info_block"] = extra_info_block
# # 获取回复器服务
# # replyer = self.api.get_service("replyer")
# # if not replyer:
# # logger.error(f"{self.log_prefix} 未找到回复器服务")
# # return False, "回复器服务不可用"
# # await self.send_message_by_expressor(extra_info_block)
# await self.send_text(extra_info_block)
# # 执行回复
# # success, reply_set = await replyer.deal_reply(
# # cycle_timers=self.cycle_timers,
# # action_data=self.action_data,
# # anchor_message=anchor_message,
# # reasoning=self.reasoning,
# # thinking_id=self.thinking_id,
# # )
# # 构建回复文本
# reply_text = "self._build_reply_text(reply_set)"
# # 存储动作记录
# await self.api.store_action_info(
# action_build_into_prompt=False,
# action_prompt_display=reply_text,
# action_done=True,
# thinking_id=self.thinking_id,
# action_data=self.action_data,
# )
# # 重置NoReplyAction的连续计数器
# NoReplyAction.reset_consecutive_count()
# return success, reply_text
# except Exception as e:
# logger.error(f"{self.log_prefix} 回复动作执行失败: {e}")
# return False, f"回复失败: {str(e)}"
# def _get_chatting_observation(self) -> Optional[ChattingObservation]:
# """获取聊天观察对象"""
# observations = self.api.get_service("observations") or []
# for obs in observations:
# if isinstance(obs, ChattingObservation):
# return obs
# return None
# def _build_reply_text(self, reply_set) -> str:
# """构建回复文本"""
# reply_text = ""
# if reply_set:
# for reply in reply_set:
# data = reply[1]
# reply_text += data
# return reply_text

View File

@@ -26,6 +26,8 @@ from src.plugin_system.base.base_command import BaseCommand
from src.plugin_system.base.component_types import ComponentInfo, ActionActivationType, ChatMode from src.plugin_system.base.component_types import ComponentInfo, ActionActivationType, ChatMode
from src.plugin_system.base.config_types import ConfigField from src.plugin_system.base.config_types import ConfigField
from src.common.logger import get_logger from src.common.logger import get_logger
# 导入配置API可选的简便方法
from src.plugin_system.apis import person_api, generator_api
logger = get_logger("mute_plugin") logger = get_logger("mute_plugin")
@@ -110,8 +112,8 @@ class MuteAction(BaseAction):
return False, error_msg return False, error_msg
# 获取时长限制配置 # 获取时长限制配置
min_duration = self.api.get_config("mute.min_duration", 60) min_duration = self.get_config("mute.min_duration", 60)
max_duration = self.api.get_config("mute.max_duration", 2592000) max_duration = self.get_config("mute.max_duration", 2592000)
# 验证时长格式并转换 # 验证时长格式并转换
try: try:
@@ -133,18 +135,12 @@ class MuteAction(BaseAction):
except (ValueError, TypeError): except (ValueError, TypeError):
error_msg = f"禁言时长格式无效: {duration}" error_msg = f"禁言时长格式无效: {duration}"
logger.error(f"{self.log_prefix} {error_msg}") logger.error(f"{self.log_prefix} {error_msg}")
await self.send_text("禁言时长必须是数字哦~") # await self.send_text("禁言时长必须是数字哦~")
return False, error_msg return False, error_msg
# 获取用户ID # 获取用户ID
try: person_id = person_api.get_person_id_by_name(target)
platform, user_id = await self.api.get_user_id_by_person_name(target) user_id = await person_api.get_person_value(person_id,"user_id")
except Exception as e:
error_msg = f"查找用户ID时出错: {e}"
logger.error(f"{self.log_prefix} {error_msg}")
await self.send_text("查找用户信息时出现问题~")
return False, error_msg
if not user_id: if not user_id:
error_msg = f"未找到用户 {target} 的ID" error_msg = f"未找到用户 {target} 的ID"
await self.send_text(f"找不到 {target} 这个人呢~") await self.send_text(f"找不到 {target} 这个人呢~")
@@ -152,53 +148,52 @@ class MuteAction(BaseAction):
return False, error_msg return False, error_msg
# 格式化时长显示 # 格式化时长显示
enable_formatting = self.api.get_config("mute.enable_duration_formatting", True) enable_formatting = self.get_config("mute.enable_duration_formatting", True)
time_str = self._format_duration(duration_int) if enable_formatting else f"{duration_int}" time_str = self._format_duration(duration_int) if enable_formatting else f"{duration_int}"
# 获取模板化消息 # 获取模板化消息
message = self._get_template_message(target, time_str, reason) message = self._get_template_message(target, time_str, reason)
# await self.send_text(message)
await self.send_message_by_expressor(message) result_status,result_message = await generator_api.rewrite_reply(
chat_stream=self.chat_stream,
reply_data={
"raw_reply": message,
"reason": reason,
}
)
if result_status:
for reply_seg in result_message:
data = reply_seg[1]
await self.send_text(data)
# 发送群聊禁言命令 # 发送群聊禁言命令
success = await self.send_command( success = await self.send_command(
command_name="GROUP_BAN", command_name="GROUP_BAN",
args={"qq_id": str(user_id), "duration": str(duration_int)}, args={"qq_id": str(user_id), "duration": str(duration_int)},
display_message="发送禁言命令", storage_message=False
) )
if success: if success:
logger.info(f"{self.log_prefix} 成功发送禁言命令,用户 {target}({user_id}),时长 {duration_int}") logger.info(f"{self.log_prefix} 成功发送禁言命令,用户 {target}({user_id}),时长 {duration_int}")
# 存储动作信息 # 存储动作信息
await self.api.store_action_info( await self.store_action_info(
action_build_into_prompt=True, action_build_into_prompt=True,
action_prompt_display=f"尝试禁言了用户 {target},时长 {time_str},原因:{reason}", action_prompt_display=f"尝试禁言了用户 {target},时长 {time_str},原因:{reason}",
action_done=True, action_done=True,
thinking_id=self.thinking_id,
action_data={
"target": target,
"user_id": user_id,
"duration": duration_int,
"duration_str": time_str,
"reason": reason,
},
) )
return True, f"成功禁言 {target},时长 {time_str}" return True, f"成功禁言 {target},时长 {time_str}"
else: else:
error_msg = "发送禁言命令失败" error_msg = "发送禁言命令失败"
logger.error(f"{self.log_prefix} {error_msg}") logger.error(f"{self.log_prefix} {error_msg}")
await self.send_text("执行禁言动作失败") await self.send_text("执行禁言动作失败")
return False, error_msg return False, error_msg
def _get_template_message(self, target: str, duration_str: str, reason: str) -> str: def _get_template_message(self, target: str, duration_str: str, reason: str) -> str:
"""获取模板化的禁言消息""" """获取模板化的禁言消息"""
templates = self.api.get_config( templates = self.get_config(
"mute.templates", "mute.templates"
[
"好的,禁言 {target} {duration},理由:{reason}",
"收到,对 {target} 执行禁言 {duration},因为{reason}",
"明白了,禁言 {target} {duration},原因是{reason}",
],
) )
template = random.choice(templates) template = random.choice(templates)
@@ -258,8 +253,8 @@ class MuteCommand(BaseCommand):
return False, "参数不完整" return False, "参数不完整"
# 获取时长限制配置 # 获取时长限制配置
min_duration = self.api.get_config("mute.min_duration", 60) min_duration = self.get_config("mute.min_duration", 60)
max_duration = self.api.get_config("mute.max_duration", 2592000) max_duration = self.get_config("mute.max_duration", 2592000)
# 验证时长 # 验证时长
try: try:
@@ -281,19 +276,16 @@ class MuteCommand(BaseCommand):
return False, "时长格式错误" return False, "时长格式错误"
# 获取用户ID # 获取用户ID
try: person_id = person_api.get_person_id_by_name(target)
platform, user_id = await self.api.get_user_id_by_person_name(target) user_id = person_api.get_person_value(person_id, "user_id")
except Exception as e:
logger.error(f"{self.log_prefix} 查找用户ID时出错: {e}")
await self.send_text("❌ 查找用户信息时出现问题")
return False, str(e)
if not user_id: if not user_id:
error_msg = f"未找到用户 {target} 的ID"
await self.send_text(f"❌ 找不到用户: {target}") await self.send_text(f"❌ 找不到用户: {target}")
return False, "用户不存在" logger.error(f"{self.log_prefix} {error_msg}")
return False, error_msg
# 格式化时长显示 # 格式化时长显示
enable_formatting = self.api.get_config("mute.enable_duration_formatting", True) enable_formatting = self.get_config("mute.enable_duration_formatting", True)
time_str = self._format_duration(duration_int) if enable_formatting else f"{duration_int}" time_str = self._format_duration(duration_int) if enable_formatting else f"{duration_int}"
logger.info(f"{self.log_prefix} 执行禁言命令: {target}({user_id}) -> {time_str}") logger.info(f"{self.log_prefix} 执行禁言命令: {target}({user_id}) -> {time_str}")
@@ -323,14 +315,7 @@ class MuteCommand(BaseCommand):
def _get_template_message(self, target: str, duration_str: str, reason: str) -> str: def _get_template_message(self, target: str, duration_str: str, reason: str) -> str:
"""获取模板化的禁言消息""" """获取模板化的禁言消息"""
templates = self.api.get_config( templates = self.get_config("mute.templates")
"mute.templates",
[
"✅ 已禁言 {target} {duration},理由:{reason}",
"🔇 对 {target} 执行禁言 {duration},因为{reason}",
"⛔ 禁言 {target} {duration},原因:{reason}",
],
)
template = random.choice(templates) template = random.choice(templates)
return template.format(target=target, duration=duration_str, reason=reason) return template.format(target=target, duration=duration_str, reason=reason)

View File

@@ -1,534 +0,0 @@
"""
拍照插件
功能特性:
- Action: 生成一张自拍照prompt由人设和模板生成
- Command: 展示最近生成的照片
#此插件并不完善
#此插件并不完善
#此插件并不完善
#此插件并不完善
#此插件并不完善
#此插件并不完善
#此插件并不完善
包含组件:
- 拍照Action - 生成自拍照
- 展示照片Command - 展示最近生成的照片
"""
from typing import List, Tuple, Type, Optional
import random
import datetime
import json
import os
import asyncio
import urllib.request
import urllib.error
import base64
import traceback
from src.plugin_system.base.base_plugin import BasePlugin, register_plugin
from src.plugin_system.base.base_action import BaseAction
from src.plugin_system.base.base_command import BaseCommand
from src.plugin_system.base.component_types import ComponentInfo, ActionActivationType, ChatMode
from src.plugin_system.base.config_types import ConfigField
from src.common.logger import get_logger
logger = get_logger("take_picture_plugin")
# 定义数据目录常量
DATA_DIR = os.path.join("data", "take_picture_data")
# 确保数据目录存在
os.makedirs(DATA_DIR, exist_ok=True)
# 创建全局锁
file_lock = asyncio.Lock()
class TakePictureAction(BaseAction):
"""生成一张自拍照"""
focus_activation_type = ActionActivationType.KEYWORD
normal_activation_type = ActionActivationType.KEYWORD
mode_enable = ChatMode.ALL
parallel_action = False
action_name = "take_picture"
action_description = "生成一张用手机拍摄,比如自拍或者近照"
activation_keywords = ["拍张照", "自拍", "发张照片", "看看你", "你的照片"]
keyword_case_sensitive = False
action_parameters = {}
action_require = [
"当用户想看你的照片时使用",
"当用户让你发自拍时使用"
"当想随手拍眼前的场景时使用"
]
associated_types = ["text","image"]
# 内置的Prompt模板如果配置文件中没有定义将使用这些模板
DEFAULT_PROMPT_TEMPLATES = [
"极其频繁无奇的iPhone自拍照没有明确的主体或构图感就是随手一拍的快照照片略带运动模糊阳光或室内打光不均匀导致的轻微曝光过度整体呈现出一种刻意的平庸感就像是从口袋里拿手机时不小心拍到的一张自拍。主角是{name}{personality}"
]
# 简单的请求缓存,避免短时间内重复请求
_request_cache = {}
async def execute(self) -> Tuple[bool, Optional[str]]:
logger.info(f"{self.log_prefix} 执行拍照动作")
try:
# 配置验证
http_base_url = self.api.get_config("api.base_url")
http_api_key = self.api.get_config("api.volcano_generate_api_key")
if not (http_base_url and http_api_key):
error_msg = "抱歉照片生成功能所需的API配置如API地址或密钥不完整无法提供服务。"
await self.send_text(error_msg)
logger.error(f"{self.log_prefix} HTTP调用配置缺失: base_url 或 volcano_generate_api_key.")
return False, "API配置不完整"
# API密钥验证
if http_api_key == "YOUR_DOUBAO_API_KEY_HERE":
error_msg = "照片生成功能尚未配置请设置正确的API密钥。"
await self.send_text(error_msg)
logger.error(f"{self.log_prefix} API密钥未配置")
return False, "API密钥未配置"
# 获取全局配置信息
bot_nickname = self.api.get_global_config("bot.nickname", "麦麦")
bot_personality = self.api.get_global_config("personality.personality_core", "")
personality_sides = self.api.get_global_config("personality.personality_sides", [])
if personality_sides:
bot_personality += random.choice(personality_sides)
# 准备模板变量
template_vars = {
"name": bot_nickname,
"personality": bot_personality
}
logger.info(f"{self.log_prefix} 使用的全局配置: name={bot_nickname}, personality={bot_personality}")
# 尝试从配置文件获取模板,如果没有则使用默认模板
templates = self.api.get_config("picture.prompt_templates", self.DEFAULT_PROMPT_TEMPLATES)
if not templates:
logger.warning(f"{self.log_prefix} 未找到有效的提示词模板,使用默认模板")
templates = self.DEFAULT_PROMPT_TEMPLATES
prompt_template = random.choice(templates)
# 填充模板
final_prompt = prompt_template.format(**template_vars)
logger.info(f"{self.log_prefix} 生成的最终Prompt: {final_prompt}")
# 从配置获取参数
model = self.api.get_config("picture.default_model", "doubao-seedream-3-0-t2i-250415")
style = self.api.get_config("picture.default_style", "动漫")
size = self.api.get_config("picture.default_size", "1024x1024")
watermark = self.api.get_config("picture.default_watermark", True)
guidance_scale = self.api.get_config("picture.default_guidance_scale", 2.5)
seed = self.api.get_config("picture.default_seed", 42)
# 检查缓存
enable_cache = self.api.get_config("storage.enable_cache", True)
if enable_cache:
cache_key = self._get_cache_key(final_prompt, model, size)
if cache_key in self._request_cache:
cached_result = self._request_cache[cache_key]
logger.info(f"{self.log_prefix} 使用缓存的图片结果")
await self.send_text("我之前拍过类似的照片,用之前的结果~")
# 直接发送缓存的结果
send_success = await self._send_image(cached_result)
if send_success:
await self.send_text("这是我的照片,好看吗?")
return True, "照片已发送(缓存)"
else:
# 缓存失败,清除这个缓存项并继续正常流程
del self._request_cache[cache_key]
await self.send_text(f"正在为你拍照,请稍候...")
try:
seed = random.randint(1, 1000000)
success, result = await asyncio.to_thread(
self._make_http_image_request,
prompt=final_prompt,
model=model,
size=size,
seed=seed,
guidance_scale=guidance_scale,
watermark=watermark,
)
except Exception as e:
logger.error(f"{self.log_prefix} (HTTP) 异步请求执行失败: {e!r}", exc_info=True)
traceback.print_exc()
success = False
result = f"照片生成服务遇到意外问题: {str(e)[:100]}"
if success:
image_url = result
logger.info(f"{self.log_prefix} 图片URL获取成功: {image_url[:70]}... 下载并编码.")
try:
encode_success, encode_result = await asyncio.to_thread(self._download_and_encode_base64, image_url)
except Exception as e:
logger.error(f"{self.log_prefix} (B64) 异步下载/编码失败: {e!r}", exc_info=True)
traceback.print_exc()
encode_success = False
encode_result = f"图片下载或编码时发生内部错误: {str(e)[:100]}"
if encode_success:
base64_image_string = encode_result
# 更新缓存
if enable_cache:
self._update_cache(final_prompt, model, size, base64_image_string)
# 发送图片
send_success = await self._send_image(base64_image_string)
if send_success:
# 存储到文件
await self._store_picture_info(final_prompt, image_url)
logger.info(f"{self.log_prefix} 成功生成并存储照片: {image_url}")
await self.send_text("当当当当~这是我刚拍的照片,好看吗?")
return True, f"成功生成照片: {image_url}"
else:
await self.send_text("照片生成了,但发送失败了,可能是格式问题...")
return False, "照片发送失败"
else:
await self.send_text(f"照片下载失败: {encode_result}")
return False, encode_result
else:
await self.send_text(f"哎呀,拍照失败了: {result}")
return False, result
except Exception as e:
logger.error(f"{self.log_prefix} 执行拍照动作失败: {e}", exc_info=True)
traceback.print_exc()
await self.send_text("呜呜,拍照的时候出了一点小问题...")
return False, str(e)
async def _store_picture_info(self, prompt: str, image_url: str):
"""将照片信息存入日志文件"""
log_file = self.api.get_config("storage.log_file", "picture_log.json")
log_path = os.path.join(DATA_DIR, log_file)
max_photos = self.api.get_config("storage.max_photos", 50)
async with file_lock:
try:
if os.path.exists(log_path):
with open(log_path, 'r', encoding='utf-8') as f:
log_data = json.load(f)
else:
log_data = []
except (json.JSONDecodeError, FileNotFoundError):
log_data = []
# 添加新照片
log_data.append({
"prompt": prompt,
"image_url": image_url,
"timestamp": datetime.datetime.now().isoformat()
})
# 如果超过最大数量,删除最旧的
if len(log_data) > max_photos:
log_data = sorted(log_data, key=lambda x: x.get('timestamp', ''), reverse=True)[:max_photos]
try:
with open(log_path, 'w', encoding='utf-8') as f:
json.dump(log_data, f, ensure_ascii=False, indent=4)
except Exception as e:
logger.error(f"{self.log_prefix} 写入照片日志文件失败: {e}", exc_info=True)
def _make_http_image_request(
self, prompt: str, model: str, size: str, seed: int, guidance_scale: float, watermark: bool
) -> Tuple[bool, str]:
"""发送HTTP请求到火山引擎豆包API生成图片"""
try:
base_url = self.api.get_config("api.base_url")
api_key = self.api.get_config("api.volcano_generate_api_key")
# 构建请求URL和头部
endpoint = f"{base_url.rstrip('/')}/images/generations"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
# 构建请求体
request_body = {
"model": model,
"prompt": prompt,
"response_format": "url",
"size": size,
"seed": seed,
"guidance_scale": guidance_scale,
"watermark": watermark,
"api-key": api_key,
}
# 创建请求对象
req = urllib.request.Request(
endpoint,
data=json.dumps(request_body).encode("utf-8"),
headers=headers,
method="POST",
)
# 发送请求并获取响应
with urllib.request.urlopen(req, timeout=60) as response:
response_data = json.loads(response.read().decode("utf-8"))
# 解析响应
image_url = None
if (
isinstance(response_data.get("data"), list)
and response_data["data"]
and isinstance(response_data["data"][0], dict)
):
image_url = response_data["data"][0].get("url")
elif response_data.get("url"):
image_url = response_data.get("url")
if image_url:
return True, image_url
else:
error_msg = response_data.get("error", {}).get("message", "未知错误")
logger.error(f"API返回错误: {error_msg}")
return False, f"API错误: {error_msg}"
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8")
logger.error(f"HTTP错误 {e.code}: {error_body}")
return False, f"HTTP错误 {e.code}: {error_body[:100]}..."
except Exception as e:
logger.error(f"请求异常: {e}", exc_info=True)
return False, f"请求异常: {str(e)}"
def _download_and_encode_base64(self, image_url: str) -> Tuple[bool, str]:
"""下载图片并转换为Base64编码"""
try:
with urllib.request.urlopen(image_url) as response:
image_data = response.read()
base64_encoded = base64.b64encode(image_data).decode('utf-8')
return True, base64_encoded
except Exception as e:
logger.error(f"图片下载编码失败: {e}", exc_info=True)
return False, str(e)
async def _send_image(self, base64_image: str) -> bool:
"""发送图片"""
try:
# 使用聊天流信息确定发送目标
chat_stream = self.api.get_service("chat_stream")
if not chat_stream:
logger.error(f"{self.log_prefix} 没有可用的聊天流发送图片")
return False
if chat_stream.group_info:
# 群聊
return await self.api.send_message_to_target(
message_type="image",
content=base64_image,
platform=chat_stream.platform,
target_id=str(chat_stream.group_info.group_id),
is_group=True,
display_message="发送生成的照片",
)
else:
# 私聊
return await self.api.send_message_to_target(
message_type="image",
content=base64_image,
platform=chat_stream.platform,
target_id=str(chat_stream.user_info.user_id),
is_group=False,
display_message="发送生成的照片",
)
except Exception as e:
logger.error(f"{self.log_prefix} 发送图片时出错: {e}")
return False
@classmethod
def _get_cache_key(cls, description: str, model: str, size: str) -> str:
"""生成缓存键"""
return f"{description}|{model}|{size}"
def _update_cache(self, description: str, model: str, size: str, base64_image: str):
"""更新缓存"""
max_cache_size = self.api.get_config("storage.max_cache_size", 10)
cache_key = self._get_cache_key(description, model, size)
# 添加到缓存
self._request_cache[cache_key] = base64_image
# 如果缓存超过最大大小,删除最旧的项
if len(self._request_cache) > max_cache_size:
oldest_key = next(iter(self._request_cache))
del self._request_cache[oldest_key]
class ShowRecentPicturesCommand(BaseCommand):
"""展示最近生成的照片"""
command_name = "show_recent_pictures"
command_description = "展示最近生成的5张照片"
command_pattern = r"^/show_pics$"
command_help = "用法: /show_pics"
command_examples = ["/show_pics"]
intercept_message = True
async def execute(self) -> Tuple[bool, Optional[str]]:
logger.info(f"{self.log_prefix} 执行展示最近照片命令")
log_file = self.api.get_config("storage.log_file", "picture_log.json")
log_path = os.path.join(DATA_DIR, log_file)
async with file_lock:
try:
if not os.path.exists(log_path):
await self.send_text("最近还没有拍过照片哦,快让我自拍一张吧!")
return True, "没有照片日志文件"
with open(log_path, 'r', encoding='utf-8') as f:
log_data = json.load(f)
if not log_data:
await self.send_text("最近还没有拍过照片哦,快让我自拍一张吧!")
return True, "没有照片"
# 获取最新的5张照片
recent_pics = sorted(log_data, key=lambda x: x['timestamp'], reverse=True)[:5]
# 先发送文本消息
await self.send_text("这是我最近拍的几张照片~")
# 逐个发送图片
for pic in recent_pics:
# 尝试获取图片URL
image_url = pic.get('image_url')
if image_url:
try:
# 下载图片并转换为Base64
with urllib.request.urlopen(image_url) as response:
image_data = response.read()
base64_encoded = base64.b64encode(image_data).decode('utf-8')
# 发送图片
await self.send_type(
message_type="image",
content=base64_encoded,
display_message="发送最近的照片"
)
except Exception as e:
logger.error(f"{self.log_prefix} 下载或发送照片失败: {e}", exc_info=True)
return True, "成功展示最近的照片"
except json.JSONDecodeError:
await self.send_text("照片记录文件好像损坏了...")
return False, "JSON解码错误"
except Exception as e:
logger.error(f"{self.log_prefix} 展示照片失败: {e}", exc_info=True)
await self.send_text("哎呀,查找照片的时候出错了。")
return False, str(e)
@register_plugin
class TakePicturePlugin(BasePlugin):
"""拍照插件"""
plugin_name = "take_picture_plugin"
plugin_description = "提供生成自拍照和展示最近照片的功能"
plugin_version = "1.0.0"
plugin_author = "SengokuCola"
enable_plugin = True
config_file_name = "config.toml"
# 配置节描述
config_section_descriptions = {
"plugin": "插件基本信息配置",
"api": "API相关配置包含火山引擎API的访问信息",
"components": "组件启用控制",
"picture": "拍照功能核心配置",
"storage": "照片存储相关配置",
}
# 配置Schema定义
config_schema = {
"plugin": {
"name": ConfigField(type=str, default="take_picture_plugin", description="插件名称", required=True),
"version": ConfigField(type=str, default="1.3.0", description="插件版本号"),
"enabled": ConfigField(type=bool, default=False, description="是否启用插件"),
"description": ConfigField(type=str, default="提供生成自拍照和展示最近照片的功能", description="插件描述", required=True),
},
"api": {
"base_url": ConfigField(
type=str,
default="https://ark.cn-beijing.volces.com/api/v3",
description="API基础URL",
example="https://api.example.com/v1",
),
"volcano_generate_api_key": ConfigField(
type=str, default="YOUR_DOUBAO_API_KEY_HERE", description="火山引擎豆包API密钥", required=True
),
},
"components": {
"enable_take_picture_action": ConfigField(type=bool, default=True, description="是否启用拍照Action"),
"enable_show_pics_command": ConfigField(type=bool, default=True, description="是否启用展示照片Command"),
},
"picture": {
"default_model": ConfigField(
type=str,
default="doubao-seedream-3-0-t2i-250415",
description="默认使用的文生图模型",
choices=["doubao-seedream-3-0-t2i-250415", "doubao-seedream-2-0-t2i"],
),
"default_style": ConfigField(type=str, default="动漫", description="默认图片风格"),
"default_size": ConfigField(
type=str,
default="1024x1024",
description="默认图片尺寸",
example="1024x1024",
choices=["1024x1024", "1024x1280", "1280x1024", "1024x1536", "1536x1024"],
),
"default_watermark": ConfigField(type=bool, default=True, description="是否默认添加水印"),
"default_guidance_scale": ConfigField(
type=float, default=2.5, description="模型指导强度,影响图片与提示的关联性", example="2.0"
),
"default_seed": ConfigField(type=int, default=42, description="随机种子,用于复现图片"),
"prompt_templates": ConfigField(
type=list,
default=TakePictureAction.DEFAULT_PROMPT_TEMPLATES,
description="用于生成自拍照的prompt模板"
),
},
"storage": {
"max_photos": ConfigField(type=int, default=50, description="最大保存的照片数量"),
"log_file": ConfigField(type=str, default="picture_log.json", description="照片日志文件名"),
"enable_cache": ConfigField(type=bool, default=True, description="是否启用请求缓存"),
"max_cache_size": ConfigField(type=int, default=10, description="最大缓存数量"),
}
}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""返回插件包含的组件列表"""
components = []
if self.get_config("components.enable_take_picture_action", True):
components.append((TakePictureAction.get_action_info(), TakePictureAction))
if self.get_config("components.enable_show_pics_command", True):
components.append((ShowRecentPicturesCommand.get_command_info(), ShowRecentPicturesCommand))
return components

View File

@@ -57,7 +57,7 @@ class TTSAction(BaseAction):
try: try:
# 发送TTS消息 # 发送TTS消息
await self.send_type(type="tts_text", text=processed_text) await self.send_custom(message_type="tts_text", content=processed_text)
logger.info(f"{self.log_prefix} TTS动作执行成功文本长度: {len(processed_text)}") logger.info(f"{self.log_prefix} TTS动作执行成功文本长度: {len(processed_text)}")
return True, "TTS动作执行成功" return True, "TTS动作执行成功"

View File

@@ -62,7 +62,7 @@ class VTBAction(BaseAction):
try: try:
# 发送VTB动作消息 - 使用新版本的send_type方法 # 发送VTB动作消息 - 使用新版本的send_type方法
await self.send_type(type="vtb_text", text=processed_text) await self.send_custom(message_type="vtb_text", content=processed_text)
logger.info(f"{self.log_prefix} VTB动作执行成功文本内容: {processed_text}") logger.info(f"{self.log_prefix} VTB动作执行成功文本内容: {processed_text}")
return True, "VTB动作执行成功" return True, "VTB动作执行成功"