refc:重构插件api,补全文档,合并expressor和replyer,分离reply和sender,新log浏览器
This commit is contained in:
@@ -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
158
docs/plugins/README.md
Normal 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生态系统!
|
||||||
@@ -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) 来了解命令开发。**
|
|
||||||
|
|||||||
@@ -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接口。**
|
|
||||||
@@ -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是否被触发
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 最佳实践
|
## 🎯 最佳实践
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 实际使用案例
|
|
||||||
|
|
||||||
### 案例1:API调用配置
|
|
||||||
|
|
||||||
```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`已经为你准备好了一切!
|
|
||||||
@@ -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`方法和自动化的配置生成机制已经为你准备好了一切!
|
||||||
@@ -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. **及时的反馈**:让用户知道操作状态
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
🎉 **遵循这些标准可以确保插件的质量、性能和用户体验!**
|
|
||||||
@@ -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插件系统的强大功能!根据你的需求选择合适的示例作为起点。**
|
|
||||||
@@ -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组件可以灵活地访问插件配置,实现更加强大和可定制的功能!
|
|
||||||
@@ -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. **详细依赖描述**: 说明每个依赖的用途
|
|
||||||
|
|
||||||
这个示例展示了如何构建一个既强大又灵活的插件,即使在依赖不完整的情况下也能提供有用的功能。
|
|
||||||
BIN
docs/plugins/image/quick-start/1750326700269.png
Normal file
BIN
docs/plugins/image/quick-start/1750326700269.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
BIN
docs/plugins/image/quick-start/1750332444690.png
Normal file
BIN
docs/plugins/image/quick-start/1750332444690.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/plugins/image/quick-start/1750332508760.png
Normal file
BIN
docs/plugins/image/quick-start/1750332508760.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -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`
|
||||||
|
|
||||||
|
在日志中你应该能看到插件被加载的信息。虽然插件还没有任何功能,但它已经成功运行了!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 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,发送回复:
|
||||||
|
|
||||||
|
```
|
||||||
|
嗨!很开心见到你!😊
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> **💡 小提示**:MaiCore会智能地决定什么时候使用它。如果没有立即看到效果,多试几次不同的消息。
|
||||||
|
|
||||||
|
🎉 **太棒了!你的插件已经有实际功能了!**
|
||||||
|
|
||||||
|
### 5.5. 了解激活系统(重要概念)
|
||||||
|
|
||||||
|
Action固然好用简单,但是现在有个问题,当用户加载了非常多的插件,添加了很多自定义Action,LLM需要选择的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插件开发的基础!**
|
|
||||||
@@ -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插件系统的最佳起点!🌟
|
|
||||||
@@ -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
|
|
||||||
@@ -1183,3 +1183,4 @@ def main():
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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. 兴趣度计算与更新
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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]: 规划结果
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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() # 预处理消息内容
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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} 设置观察列表")
|
|
||||||
292
src/plugin_system/apis/chat_api.py
Normal file
292
src/plugin_system/apis/chat_api.py
Normal 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()
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
219
src/plugin_system/apis/emoji_api.py
Normal file
219
src/plugin_system/apis/emoji_api.py
Normal 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 []
|
||||||
170
src/plugin_system/apis/generator_api.py
Normal file
170
src/plugin_system/apis/generator_api.py
Normal 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, []
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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, "", ""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
153
src/plugin_system/apis/person_api.py
Normal file
153
src/plugin_system/apis/person_api.py
Normal 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_id(MD5哈希值)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
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 ""
|
||||||
@@ -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",
|
|
||||||
]
|
|
||||||
445
src/plugin_system/apis/send_api.py
Normal file
445
src/plugin_system/apis/send_api.py
Normal 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)
|
||||||
@@ -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)}"
|
|
||||||
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 [],
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 = ""
|
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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动作执行成功"
|
||||||
|
|||||||
@@ -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动作执行成功"
|
||||||
|
|||||||
Reference in New Issue
Block a user