diff --git a/MaiBot插件开发文档.md b/MaiBot插件开发文档.md deleted file mode 100644 index 71ed553af..000000000 --- a/MaiBot插件开发文档.md +++ /dev/null @@ -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插件开发之旅吧!** 🚀 \ No newline at end of file diff --git a/docs/plugins/README.md b/docs/plugins/README.md new file mode 100644 index 000000000..1ae26bb6f --- /dev/null +++ b/docs/plugins/README.md @@ -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生态系统! diff --git a/docs/plugins/action-components.md b/docs/plugins/action-components.md index 192b60ba2..3b541f14f 100644 --- a/docs/plugins/action-components.md +++ b/docs/plugins/action-components.md @@ -23,27 +23,28 @@ Action采用**两层决策机制**来优化性能和决策质量: #### 激活类型说明 -| 激活类型 | 说明 | 使用场景 | -|---------|-----|---------| -| `NEVER` | 从不激活,Action对麦麦不可见 | 临时禁用某个Action | -| `ALWAYS` | 永远激活,Action总是在麦麦的候选池中 | 核心功能,如回复、表情 | -| `LLM_JUDGE` | 通过LLM智能判断当前情境是否需要激活此Action | 需要智能判断的复杂场景 | -| `RANDOM` | 基于随机概率决定是否激活 | 增加行为随机性的功能 | -| `KEYWORD` | 当检测到特定关键词时激活 | 明确触发条件的功能 | +| 激活类型 | 说明 | 使用场景 | +| ------------- | ------------------------------------------- | ------------------------ | +| `NEVER` | 从不激活,Action对麦麦不可见 | 临时禁用某个Action | +| `ALWAYS` | 永远激活,Action总是在麦麦的候选池中 | 核心功能,如回复、不回复 | +| `LLM_JUDGE` | 通过LLM智能判断当前情境是否需要激活此Action | 需要智能判断的复杂场景 | +| `RANDOM` | 基于随机概率决定是否激活 | 增加行为随机性的功能 | +| `KEYWORD` | 当检测到特定关键词时激活 | 明确触发条件的功能 | #### 聊天模式控制 -| 模式 | 说明 | -|-----|-----| -| `ChatMode.FOCUS` | 仅在专注聊天模式下可激活 | +| 模式 | 说明 | +| ------------------- | ------------------------ | +| `ChatMode.FOCUS` | 仅在专注聊天模式下可激活 | | `ChatMode.NORMAL` | 仅在普通聊天模式下可激活 | -| `ChatMode.ALL` | 所有模式下都可激活 | +| `ChatMode.ALL` | 所有模式下都可激活 | ### 第二层:使用决策(Usage Decision) **在Action被激活后,使用条件决定麦麦什么时候会"选择"使用这个Action**。 这一层由以下因素综合决定: + - `action_require`:使用场景描述,帮助LLM判断何时选择 - `action_parameters`:所需参数,影响Action的可执行性 - 当前聊天上下文和麦麦的决策逻辑 @@ -58,7 +59,7 @@ class EmojiAction(BaseAction): focus_activation_type = ActionActivationType.RANDOM # 专注模式下随机激活 normal_activation_type = ActionActivationType.KEYWORD # 普通模式下关键词激活 activation_keywords = ["表情", "emoji", "😊"] - + # 第二层:使用决策 action_require = [ "表达情绪时可以选择使用", @@ -68,12 +69,14 @@ class EmojiAction(BaseAction): ``` **决策流程**: + 1. **第一层激活判断**: + - 普通模式:只有当用户消息包含"表情"、"emoji"或"😊"时,麦麦才"知道"可以使用这个Action - 专注模式:随机激活,有概率让麦麦"看到"这个Action - 2. **第二层使用决策**: - - 即使Action被激活,麦麦还会根据`action_require`中的条件判断是否真正选择使用 + + - 即使Action被激活,麦麦还会根据 `action_require`中的条件判断是否真正选择使用 - 例如:如果刚刚已经发过表情,根据"不要连续发送多个表情"的要求,麦麦可能不会选择这个Action ## 📋 Action必须项清单 @@ -125,38 +128,147 @@ action_require = [ associated_types = ["text", "emoji", "image"] ``` -### 4. 动作记录必须项 +### 4. 新API导入必须项 -每个 Action 在执行完成后,**必须**使用 `store_action_info` 记录动作信息。这是非常重要的,因为: - -1. **记忆连续性**:让麦麦记住自己执行过的动作,避免重复执行 -2. **上下文理解**:帮助麦麦理解自己的行为历史,做出更合理的决策 -3. **行为追踪**:便于后续查询和分析麦麦的行为模式 +使用新插件系统时,必须导入所需的API模块: ```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: - # 存储动作信息 - await self.api.store_action_info( + # 存储动作信息 - 使用新API格式 + await self.store_action_info( action_build_into_prompt=True, # 让麦麦知道这个动作 action_prompt_display=f"执行了xxx动作,参数:{param}", # 动作描述 action_done=True, # 动作是否完成 - thinking_id=self.thinking_id, # 关联的思考ID - action_data={ # 动作的详细数据 - "param1": value1, - "param2": value2 - } ) 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, "数据存储完成" +``` ## 🔧 激活类型详解 @@ -168,10 +280,34 @@ async def execute(self) -> Tuple[bool, Optional[str]]: class GreetingAction(BaseAction): focus_activation_type = ActionActivationType.KEYWORD normal_activation_type = ActionActivationType.KEYWORD - + # 关键词配置 activation_keywords = ["你好", "hello", "hi", "嗨"] 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激活 @@ -182,16 +318,38 @@ class GreetingAction(BaseAction): class HelpAction(BaseAction): focus_activation_type = ActionActivationType.LLM_JUDGE normal_activation_type = ActionActivationType.LLM_JUDGE - + # LLM判断提示词 llm_judge_prompt = """ 判定是否需要使用帮助动作的条件: 1. 用户表达了困惑或需要帮助 2. 用户提出了问题但没有得到满意答案 3. 对话中出现了技术术语或复杂概念 - + 请回答"是"或"否"。 """ + + 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激活 @@ -202,466 +360,210 @@ class HelpAction(BaseAction): class SurpriseAction(BaseAction): focus_activation_type = ActionActivationType.RANDOM normal_activation_type = ActionActivationType.RANDOM - + # 随机激活概率 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]: - """执行智能问候""" - # 获取参数 - username = self.action_data.get("username", "") - greeting_style = self.action_data.get("greeting_style", "casual") - - # 根据风格生成问候消息 - if greeting_style == "formal": - message = f"您好{username}!很荣幸为您服务!" - emoji = "🙏" - elif greeting_style == "friendly": - message = f"你好{username}!欢迎来到这里,希望我们能成为好朋友!" - emoji = "😊" - else: # casual - message = f"嗨{username}!很开心见到你~" - emoji = "👋" - - # 发送消息 - await self.send_text(message) - await self.send_type("emoji", emoji) - - return True, f"向{username or '用户'}发送了{greeting_style}风格的问候" + import random + + surprises = ["🎉", "✨", "🌟", "💝", "🎈"] + selected = random.choice(surprises) + + await self.send_emoji(selected) + return True, f"发送了惊喜表情: {selected}" ``` -### 智能禁言Action +## 💡 完整示例 -以下是一个真实的群管理禁言Action示例,展示了LLM判断、参数验证、配置管理等高级功能: +### 智能聊天Action ```python -from typing import Optional -import random -from src.plugin_system.base.base_action import BaseAction -from src.plugin_system.base.component_types import ActionActivationType, ChatMode +from src.plugin_system.apis import generator_api, emoji_api -class MuteAction(BaseAction): - """智能禁言Action - 基于LLM智能判断是否需要禁言""" - - # ===== 激活控制必须项 ===== - focus_activation_type = ActionActivationType.LLM_JUDGE # Focus模式使用LLM判定 - normal_activation_type = ActionActivationType.KEYWORD # Normal模式使用关键词 +class IntelligentChatAction(BaseAction): + """智能聊天Action - 展示新API的完整用法""" + + # 激活设置 + focus_activation_type = ActionActivationType.ALWAYS + normal_activation_type = ActionActivationType.LLM_JUDGE mode_enable = ChatMode.ALL parallel_action = False - - # ===== 基本信息必须项 ===== - action_name = "mute" - action_description = "智能禁言系统,基于LLM判断是否需要禁言" - - # ===== 激活配置 ===== - # 关键词设置(用于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 = "智能助手,主动提供帮助和建议" - + + # 基本信息 + action_name = "intelligent_chat" + action_description = "使用replyer_1进行智能聊天回复,支持表情包和个性化回复" + # LLM判断提示词 llm_judge_prompt = """ - 判定是否需要提供智能帮助的条件: - 1. 用户表达了困惑或需要帮助 - 2. 对话中出现了技术问题 - 3. 用户寻求解决方案或建议 - 4. 适合提供额外信息的场合 - - 不要使用的情况: - 1. 用户明确表示不需要帮助 - 2. 对话进行得很顺利 - 3. 刚刚已经提供过帮助 - + 判断是否需要进行智能聊天回复: + 1. 用户提出了有趣的话题 + 2. 需要更加个性化的回复 + 3. 适合发送表情包的情况 + 请回答"是"或"否"。 """ - - # 随机激活概率 - random_activation_probability = 0.15 - - # ===== 功能定义必须项 ===== + + # 功能定义 action_parameters = { - "help_type": "帮助类型:explanation(解释)、suggestion(建议)、guidance(指导)", - "topic": "帮助主题或用户关心的问题", - "urgency": "紧急程度:low(低)、medium(中)、high(高)" + "topic": "聊天话题", + "mood": "当前氛围(happy/sad/excited/calm)", + "include_emoji": "是否包含表情包(true/false)" } - + action_require = [ - "用户表达困惑或寻求帮助时使用", - "检测到用户遇到技术问题时使用", - "对话中出现知识盲点时主动提供帮助", - "避免过度频繁地提供帮助,要恰到好处" + "需要更个性化回复时使用", + "聊天氛围适合发送表情时使用", + "避免在正式场合使用" ] - + associated_types = ["text", "emoji"] - + async def execute(self) -> Tuple[bool, str]: - """执行智能帮助""" # 获取参数 - help_type = self.action_data.get("help_type", "suggestion") - topic = self.action_data.get("topic", "") - urgency = self.action_data.get("urgency", "medium") - - # 根据帮助类型和紧急程度生成消息 - if help_type == "explanation": - message = f"关于{topic},让我来为你解释一下..." - elif help_type == "guidance": - message = f"在{topic}方面,我可以为你提供一些指导..." - else: # suggestion - message = f"针对{topic},我建议你可以尝试以下方法..." - - # 根据紧急程度调整表情 - if urgency == "high": - emoji = "🚨" - elif urgency == "low": - emoji = "💡" + topic = self.action_data.get("topic", "日常聊天") + mood = self.action_data.get("mood", "happy") + include_emoji = self.action_data.get("include_emoji", "true") == "true" + + # 构建智能回复数据 + chat_data = { + "text": f"请针对{topic}话题进行回复,当前氛围是{mood}", + "topic": topic, + "mood": mood, + "style": "conversational", + "replyer_name": "replyer_1" # 使用replyer_1 + } + + # 生成智能回复 + success, reply_set = await generator_api.generate_reply( + chat_stream=self.chat_stream, + action_data=chat_data, + 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: - emoji = "🤔" - - # 发送帮助消息 - await self.send_text(message) - await self.send_type("emoji", emoji) - - return True, f"提供了{help_type}类型的帮助,主题:{topic}" + return False, "智能回复生成失败" ``` -## 📊 性能优化建议 +## 🛠️ 调试技巧 -### 1. 合理使用激活类型 - -- **ALWAYS**: 仅用于核心功能 -- **LLM_JUDGE**: 适度使用,避免过多LLM调用 -- **KEYWORD**: 优选,性能最好 -- **RANDOM**: 控制概率,避免过于频繁 - -### 2. 优化execute方法 +### 开发调试Action ```python -async def execute(self) -> Tuple[bool, str]: - try: - # 快速参数验证 - if not self._validate_parameters(): - return False, "参数验证失败" - - # 核心逻辑 - result = await self._core_logic() - - # 成功返回 - return True, "执行成功" - - except Exception as e: - logger.error(f"{self.log_prefix} 执行失败: {e}") - return False, f"执行失败: {str(e)}" +class DebugAction(BaseAction): + """调试Action - 展示如何调试新API""" + + focus_activation_type = ActionActivationType.KEYWORD + normal_activation_type = ActionActivationType.KEYWORD + activation_keywords = ["debug", "调试"] + mode_enable = ChatMode.ALL + parallel_action = True + + action_name = "debug_helper" + action_description = "调试助手,显示当前状态信息" + + action_parameters = {} + action_require = ["需要调试信息时使用"] + associated_types = ["text"] + + async def execute(self) -> Tuple[bool, str]: + # 收集调试信息 + 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, + }) + + # 格式化调试信息 + 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, "调试信息已发送" ``` -### 3. 合理设置并行执行 +## 📚 最佳实践 -```python -# 轻量级Action可以并行 -parallel_action = True # 如:发送表情、记录日志 +1. **总是导入所需的API模块**: -# 重要Action应该独占 -parallel_action = False # 如:回复消息、状态切换 -``` + ```python + from src.plugin_system.apis import generator_api, send_api, emoji_api + ``` +2. **在生成内容时指定replyer_1**: -## 🐛 调试技巧 + ```python + action_data = { + "text": "生成内容的请求", + "replyer_name": "replyer_1" + } + ``` +3. **使用便捷发送方法**: -### 1. 日志记录 + ```python + await self.send_text("文本") # 自动处理群聊/私聊 + await self.send_emoji(emoji_base64) + ``` +4. **合理使用配置**: -```python -from src.common.logger import get_logger + ```python + enable_feature = self.get_config("section.key", default_value) + ``` +5. **总是记录动作信息**: -logger = get_logger("my_action") + ```python + await self.store_action_info( + action_build_into_prompt=True, + action_prompt_display="动作描述", + action_done=True + ) + ``` -async def execute(self) -> Tuple[bool, str]: - logger.info(f"{self.log_prefix} 开始执行: {self.reasoning}") - logger.debug(f"{self.log_prefix} 参数: {self.action_data}") - - # 执行逻辑... - - logger.info(f"{self.log_prefix} 执行完成") -``` - -### 2. 激活状态检查 - -```python -# 在execute方法中检查激活原因 -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}") -``` - -### 3. 参数验证 - -```python -def _validate_parameters(self) -> bool: - required_params = ["param1", "param2"] - for param in required_params: - if param not in self.action_data: - logger.warning(f"{self.log_prefix} 缺少必需参数: {param}") - return False - return True -``` - -## 🎯 最佳实践 - -### 1. 清晰的Action命名 - -- 使用描述性的类名:`SmartGreetingAction` 而不是 `Action1` -- action_name要简洁明确:`"smart_greeting"` 而不是 `"action_1"` - -### 2. 完整的文档字符串 - -```python -class MyAction(BaseAction): - """ - 我的Action - 一句话描述功能 - - 详细描述Action的用途、激活条件、执行逻辑等。 - - 激活条件: - - Focus模式:关键词激活 - - Normal模式:LLM判断激活 - - 执行逻辑: - 1. 验证参数 - 2. 生成响应 - 3. 发送消息 - """ -``` - -### 3. 错误处理 - -```python -async def execute(self) -> Tuple[bool, str]: - 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}" -``` - -### 4. 配置驱动 - -```python -# 从配置文件读取设置 -enable_feature = self.api.get_config("my_action.enable_feature", True) -max_retries = self.api.get_config("my_action.max_retries", 3) -``` - ---- - -🎉 **现在你已经掌握了Action组件开发的完整知识!继续学习 [Command组件详解](command-components.md) 来了解命令开发。** \ No newline at end of file +通过使用新的API格式,Action的开发变得更加简洁和强大! diff --git a/docs/plugins/api/message-api.md b/docs/plugins/api/message-api.md index 0d84708df..c83473ad4 100644 --- a/docs/plugins/api/message-api.md +++ b/docs/plugins/api/message-api.md @@ -2,14 +2,14 @@ ## 📖 概述 -消息API提供了发送各种类型消息的接口,支持文本、表情、图片等多种消息类型,以及向不同目标发送消息的功能。 +消息API提供了发送各种类型消息的接口,支持文本、表情、图片等多种消息类型。新版API格式更加简洁直观,自动处理群聊/私聊判断。 ## 🔄 基础消息发送 ### 发送文本消息 ```python -# 发送普通文本消息 +# 新API格式 - 自动判断群聊/私聊 await self.send_text("这是一条文本消息") # 发送多行文本 @@ -21,60 +21,73 @@ message = """ await self.send_text(message.strip()) ``` +### 发送表情消息 + +```python +# 新API格式 - 发送表情 +await self.send_emoji("😊") +await self.send_emoji("🎉") +await self.send_emoji("👋") +``` + ### 发送特定类型消息 ```python -# 发送表情 -await self.send_type("emoji", "😊") - # 发送图片 await self.send_type("image", "https://example.com/image.jpg") # 发送音频 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 -# 发送命令类型的消息 -await self.send_command("system_command", {"param": "value"}) -``` +# 导入send_api +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="这是发送到群聊的消息", group_id="123456789", platform="qq" ) -if success: - print("消息发送成功") -else: - print("消息发送失败") -``` - -### 向指定用户发送私聊消息 - -```python # 向指定用户发送私聊消息 -success = await self.api.send_text_to_user( +success = await send_api.text_to_user( text="这是私聊消息", user_id="987654321", 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 # 向任意目标发送任意类型消息 -success = await self.api.send_message_to_target( +success = await send_api.message_to_target( message_type="text", # 消息类型 content="消息内容", # 消息内容 platform="qq", # 平台 @@ -88,76 +101,83 @@ success = await self.api.send_message_to_target( ### 支持的消息类型 -| 类型 | 说明 | 示例 | -|-----|------|------| -| `text` | 普通文本消息 | "Hello World" | -| `emoji` | 表情消息 | "😊" | -| `image` | 图片消息 | 图片URL或路径 | -| `audio` | 音频消息 | 音频文件路径 | -| `video` | 视频消息 | 视频文件路径 | -| `file` | 文件消息 | 文件路径 | +| 类型 | 说明 | 新API方法 | send_api方法 | +|-----|------|----------|-------------| +| `text` | 普通文本消息 | `await self.send_text()` | `await send_api.text_to_group()` | +| `emoji` | 表情消息 | `await self.send_emoji()` | `await send_api.emoji_to_group()` | +| `image` | 图片消息 | `await self.send_type("image", url)` | `await send_api.message_to_target()` | +| `audio` | 音频消息 | `await self.send_type("audio", path)` | `await send_api.message_to_target()` | +| `video` | 视频消息 | `await self.send_type("video", path)` | `await send_api.message_to_target()` | +| `file` | 文件消息 | `await self.send_type("file", path)` | `await send_api.message_to_target()` | -### 消息类型示例 +### 新API格式示例 ```python -# 文本消息 -await self.send_type("text", "普通文本") - -# 表情消息 -await self.send_type("emoji", "🎉") - -# 图片消息 -await self.send_type("image", "/path/to/image.jpg") - -# 音频消息 -await self.send_type("audio", "/path/to/audio.mp3") - -# 文件消息 -await self.send_type("file", "/path/to/document.pdf") +class ExampleAction(BaseAction): + async def execute(self) -> Tuple[bool, str]: + # 文本消息 - 最常用 + await self.send_text("普通文本消息") + + # 表情消息 - 直接方法 + await self.send_emoji("🎉") + + # 图片消息 + await self.send_type("image", "/path/to/image.jpg") + + # 音频消息 + await self.send_type("audio", "/path/to/audio.mp3") + + # 文件消息 + 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 -# 在Action或Command中获取当前处理的消息 -current_message = self.message +# 新API格式 - 直接属性访问 +class ExampleCommand(BaseCommand): + async def execute(self) -> Tuple[bool, str]: + # 用户信息 + user_id = self.user_id + user_nickname = self.user_nickname + + # 聊天信息 + is_group_chat = self.is_group + chat_id = self.chat_id + platform = self.platform + + # 消息内容 + message_text = self.message.processed_plain_text + + # 构建信息显示 + info = f""" +👤 用户: {user_nickname}({user_id}) +💬 类型: {'群聊' if is_group_chat else '私聊'} +📱 平台: {platform} +📝 内容: {message_text} + """.strip() + + await self.send_text(info) + return True, "显示了消息信息" +``` -# 消息基本信息 -user_id = current_message.message_info.user_info.user_id -user_nickname = current_message.message_info.user_info.user_nickname -message_content = current_message.processed_plain_text -timestamp = current_message.timestamp +### 获取群聊信息(如果适用) -# 群聊信息(如果是群聊) -if current_message.message_info.group_info: - group_id = current_message.message_info.group_info.group_id - group_name = current_message.message_info.group_info.group_name +```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` | 微信聊天平台 | | Discord | `discord` | Discord聊天平台 | -### 平台特定功能 +### 平台适配示例 ```python -# 获取当前平台 -current_platform = self.api.get_current_platform() - -# 根据平台调整消息格式 -if current_platform == "qq": - # QQ平台特定处理 - await self.send_text("[QQ] 消息内容") -elif current_platform == "wechat": - # 微信平台特定处理 - await self.send_text("【微信】消息内容") +class PlatformAdaptiveAction(BaseAction): + async def execute(self) -> Tuple[bool, str]: + # 获取当前平台 + current_platform = self.platform + + # 根据平台调整消息格式 + if current_platform == "qq": + await self.send_text("[QQ] 这是QQ平台的消息") + await self.send_emoji("🐧") # QQ企鹅表情 + elif current_platform == "wechat": + 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 -# 发送Markdown格式的消息(如果平台支持) -markdown_message = """ -**粗体文本** -*斜体文本* -`代码块` -[链接](https://example.com) -""" +# 自动处理长消息分割 +long_message = "这是一条很长的消息..." * 100 -await self.send_text(markdown_message) +# 新API会自动处理长消息分割 +await self.send_text(long_message) ``` -### 消息模板 +### 消息模板和格式化 ```python -# 使用模板生成消息 -def format_user_info(username: str, level: int, points: int) -> str: - return f""" -👤 用户信息 -━━━━━━━━━━━━━━━━━━ -📛 用户名: {username} -⭐ 等级: Lv.{level} -💰 积分: {points:,} -━━━━━━━━━━━━━━━━━━ - """.strip() - -# 使用模板 -user_info = format_user_info("张三", 15, 12580) -await self.send_text(user_info) -``` - -### 表情和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" +class TemplateMessageAction(BaseAction): + async def execute(self) -> Tuple[bool, str]: + # 使用配置中的消息模板 + template = self.get_config("messages.greeting_template", "你好 {username}!") + + # 格式化消息 + formatted_message = template.format( + username=self.user_nickname, + time=datetime.now().strftime("%H:%M"), + platform=self.platform ) - if not success: - logger.warning(f"向群聊 {group_id} 发送消息失败") - - return success + await self.send_text(formatted_message) - except Exception as e: - logger.error(f"向群聊发送消息异常: {e}") - return False + # 根据配置决定是否发送表情 + if self.get_config("messages.include_emoji", True): + await self.send_emoji("😊") + + return True, "发送了模板化消息" ``` -## 📊 最佳实践 - -### 1. 消息长度控制 +### 条件消息发送 ```python -async def send_long_message(self, content: str, max_length: int = 500): - """发送长消息,自动分段""" - if len(content) <= max_length: - await self.send_text(content) - else: - # 分段发送 - 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}") +class ConditionalMessageAction(BaseAction): + async def execute(self) -> Tuple[bool, str]: + # 根据用户类型发送不同消息 + 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) + elif msg_type == "emoji": + await self.send_emoji(content) - # 避免发送过快 - if i < len(parts) - 1: - await asyncio.sleep(0.5) + # 可选:添加延迟避免消息发送过快 + import asyncio + await asyncio.sleep(0.5) + + return True, "发送了批量消息" ``` -### 2. 消息格式规范 +### 错误处理和重试 ```python -class MessageFormatter: - """消息格式化工具类""" - - @staticmethod - def success(message: str) -> str: - return f"✅ {message}" - - @staticmethod - def error(message: str) -> str: - return f"❌ {message}" - - @staticmethod - 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("操作失败,请重试")) +class ReliableMessageAction(BaseAction): + async def execute(self) -> Tuple[bool, str]: + max_retries = 3 + retry_count = 0 + + while retry_count < max_retries: + try: + await self.send_text("重要消息") + return True, "消息发送成功" + except Exception as e: + retry_count += 1 + logger.warning(f"消息发送失败 (尝试 {retry_count}/{max_retries}): {e}") + + if retry_count < max_retries: + import asyncio + await asyncio.sleep(1) # 等待1秒后重试 + + return False, "消息发送失败,已达到最大重试次数" ``` -### 3. 异步消息处理 +## 📝 最佳实践 + +### 1. 消息发送最佳实践 ```python -async def batch_send_messages(self, messages: List[str]): - """批量发送消息""" - tasks = [] - - for message in messages: - task = self.send_text(message) - tasks.append(task) - - # 并发发送,但控制并发数 - semaphore = asyncio.Semaphore(3) # 最多3个并发 - - async def send_with_limit(message): - async with semaphore: +# ✅ 好的做法 +class GoodMessageAction(BaseAction): + async def execute(self) -> Tuple[bool, str]: + # 1. 检查配置 + if not self.get_config("features.enable_messages", True): + return True, "消息功能已禁用" + + # 2. 简洁的消息发送 + await self.send_text("简洁明了的消息") + + # 3. 适当的表情使用 + if self.get_config("features.enable_emoji", True): + await self.send_emoji("😊") + + return True, "消息发送完成" + +# ❌ 避免的做法 +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 asyncio.gather(*[send_with_limit(msg) for msg in messages]) + return True, "消息发送成功" + except Exception as e: + logger.error(f"消息发送失败: {e}") + # 可选:发送备用消息 + await self.send_text("消息发送遇到问题,请稍后再试") + return False, f"发送失败: {str(e)}" ``` -### 4. 消息缓存 +### 3. 性能优化 ```python -class MessageCache: - """消息缓存管理""" - - def __init__(self): - self._cache = {} - self._max_size = 100 - - def get_cached_message(self, key: str) -> Optional[str]: - 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] +# ✅ 性能友好的消息发送 +class OptimizedMessageAction(BaseAction): + async def execute(self) -> Tuple[bool, str]: + # 合并多个短消息为一条长消息 + parts = [ + "第一部分信息", + "第二部分信息", + "第三部分信息" + ] - 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) + combined_message = "\n".join(parts) + await self.send_text(combined_message) + + return True, "发送了优化的消息" ``` ---- - -🎉 **现在你已经掌握了消息API的完整用法!继续学习其他API接口。** \ No newline at end of file +通过新的API格式,消息发送变得更加简洁高效! \ No newline at end of file diff --git a/docs/plugins/command-components.md b/docs/plugins/command-components.md index 659156f45..ae2239671 100644 --- a/docs/plugins/command-components.md +++ b/docs/plugins/command-components.md @@ -73,7 +73,7 @@ class SimpleCommand(BaseCommand): ### 参数捕获 -使用命名组 `(?Ppattern)` 捕获参数: +使用命名组 `(?Ppattern)` 捕获参数: ```python class UserCommand(BaseCommand): @@ -336,7 +336,7 @@ class SystemInfoCommand(BaseCommand): async def _show_plugin_info(self): """显示插件信息""" - # 通过API获取插件信息 + # 通过配置获取插件信息 plugins = await self._get_loaded_plugins() plugin_info = f""" @@ -349,7 +349,7 @@ class SystemInfoCommand(BaseCommand): async def _get_loaded_plugins(self) -> list: """获取已加载的插件列表""" - # 这里可以通过self.api获取实际的插件信息 + # 这里可以通过配置或API获取实际的插件信息 return [ {"name": "core_actions", "active": True}, {"name": "example_plugin", "active": True}, @@ -386,6 +386,55 @@ class CustomPrefixCommand(BaseCommand): 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. 正则表达式优化 @@ -398,83 +447,39 @@ command_pattern = r"^/ping$" 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. 参数验证 ```python +# ✅ 好的做法 - 早期验证 async def execute(self) -> Tuple[bool, Optional[str]]: - # 快速参数验证 username = self.matched_groups.get("username") - if not username or len(username) < 2: - await self.send_text("❌ 用户名不合法") - return False, "参数验证失败" + if not username: + await self.send_text("❌ 请提供用户名") + return False, "缺少参数" - # 主要逻辑 - ... + # 继续处理... ``` -### 3. 异常处理 +### 3. 错误处理 ```python +# ✅ 好的做法 - 完整错误处理 async def execute(self) -> Tuple[bool, Optional[str]]: try: - # 命令逻辑 - result = await self._do_command() + # 主要逻辑 + result = await self._process_command() return True, "执行成功" except ValueError as e: await self.send_text(f"❌ 参数错误: {e}") return False, f"参数错误: {e}" except Exception as e: - logger.error(f"{self.log_prefix} 命令执行失败: {e}") - await self.send_text("❌ 命令执行失败") + await self.send_text(f"❌ 执行失败: {e}") return False, f"执行失败: {e}" ``` -## 🐛 调试技巧 - -### 1. 正则测试 - -```python -import re - -pattern = r"^/user\s+(?Padd|del)\s+(?P\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是否被触发 -``` +通过新的API格式,Command开发变得更加简洁和直观! ## 🎯 最佳实践 diff --git a/docs/plugins/config-access-guide.md b/docs/plugins/config-access-guide.md deleted file mode 100644 index 119684886..000000000 --- a/docs/plugins/config-access-guide.md +++ /dev/null @@ -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`已经为你准备好了一切! \ No newline at end of file diff --git a/docs/plugins/configuration-guide.md b/docs/plugins/configuration-guide.md index 5b1b3f4df..a681922d5 100644 --- a/docs/plugins/configuration-guide.md +++ b/docs/plugins/configuration-guide.md @@ -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)**: 在代码中直接定义配置的类型和默认值,减少了因配置错误导致的运行时问题。 - **易于管理 (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`)中完成,主要通过两个类属性: @@ -41,18 +76,13 @@ class ConfigField: choices: Optional[List[Any]] = None # 可选值列表 (可选) ``` ---- - -## 完整示例 +### 配置定义示例 让我们以一个功能丰富的 `MutePlugin` 为例,看看如何定义它的配置。 -### 1. 插件代码 (`plugin.py`) - ```python # src/plugins/built_in/mute_plugin/plugin.py -# ... 其他导入 ... from src.plugin_system import BasePlugin, register_plugin from src.plugin_system.base.config_types import ConfigField from typing import List, Tuple, Type @@ -119,13 +149,20 @@ class MutePlugin(BasePlugin): } def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: - # ... 组件注册逻辑 ... # 在这里可以通过 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` 时,系统会自动创建以下文件: @@ -190,13 +227,405 @@ level = "INFO" # 日志记录前缀 # 示例: [MyMutePlugin] prefix = "[MutePlugin]" - ``` -## 最佳实践 +--- -1. **定义优于创建**: 始终优先在 `plugin.py` 中定义 `config_schema`,而不是手动创建 `config.toml`。 -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)。 \ No newline at end of file +## 配置访问:在Action和Command中使用配置 + +### 问题描述 + +在插件开发中,你可能遇到这样的问题: +- 想要在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\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`方法和自动化的配置生成机制已经为你准备好了一切! \ No newline at end of file diff --git a/docs/plugins/development-standards.md b/docs/plugins/development-standards.md deleted file mode 100644 index 0ef18fcf7..000000000 --- a/docs/plugins/development-standards.md +++ /dev/null @@ -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. **及时的反馈**:让用户知道操作状态 - ---- - -🎉 **遵循这些标准可以确保插件的质量、性能和用户体验!** \ No newline at end of file diff --git a/docs/plugins/examples/complete-examples.md b/docs/plugins/examples/complete-examples.md deleted file mode 100644 index d54eb078b..000000000 --- a/docs/plugins/examples/complete-examples.md +++ /dev/null @@ -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插件系统的强大功能!根据你的需求选择合适的示例作为起点。** \ No newline at end of file diff --git a/docs/plugins/examples/config-access-example.md b/docs/plugins/examples/config-access-example.md deleted file mode 100644 index f79adeb38..000000000 --- a/docs/plugins/examples/config-access-example.md +++ /dev/null @@ -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\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 ") - 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组件可以灵活地访问插件配置,实现更加强大和可定制的功能! \ No newline at end of file diff --git a/docs/plugins/examples/dependency-example.md b/docs/plugins/examples/dependency-example.md deleted file mode 100644 index 26b7fa0ff..000000000 --- a/docs/plugins/examples/dependency-example.md +++ /dev/null @@ -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. **详细依赖描述**: 说明每个依赖的用途 - -这个示例展示了如何构建一个既强大又灵活的插件,即使在依赖不完整的情况下也能提供有用的功能。 \ No newline at end of file diff --git a/docs/plugins/image/quick-start/1750326700269.png b/docs/plugins/image/quick-start/1750326700269.png new file mode 100644 index 000000000..1dc4f19b5 Binary files /dev/null and b/docs/plugins/image/quick-start/1750326700269.png differ diff --git a/docs/plugins/image/quick-start/1750332444690.png b/docs/plugins/image/quick-start/1750332444690.png new file mode 100644 index 000000000..aefbbb3e0 Binary files /dev/null and b/docs/plugins/image/quick-start/1750332444690.png differ diff --git a/docs/plugins/image/quick-start/1750332508760.png b/docs/plugins/image/quick-start/1750332508760.png new file mode 100644 index 000000000..924b9b6b0 Binary files /dev/null and b/docs/plugins/image/quick-start/1750332508760.png differ diff --git a/docs/plugins/quick-start.md b/docs/plugins/quick-start.md index 79d596601..9386d6920 100644 --- a/docs/plugins/quick-start.md +++ b/docs/plugins/quick-start.md @@ -1,28 +1,24 @@ # 🚀 快速开始指南 -本指南将带你用5分钟时间,从零开始创建一个功能完整的MaiBot插件。 - -> **💡 配置先行** -> -> 在开始之前,强烈建议你先阅读 ➡️ **[⚙️ 插件配置定义指南](configuration-guide.md)**。 -> -> 了解如何通过 `config_schema` 定义插件配置,可以让系统为你自动生成带详细注释的 `config.toml` 文件,这是现代插件开发的最佳实践。 +本指南将带你用5分钟时间,从零开始创建一个功能完整的MaiCore插件。 ## 📖 概述 -这个指南将带你在5分钟内创建你的第一个MaiBot插件。我们将创建一个简单的问候插件,展示插件系统的基本概念。 +这个指南将带你快速创建你的第一个MaiCore插件。我们将创建一个简单的问候插件,展示插件系统的基本概念。无需阅读其他文档,跟着本指南就能完成! ## 🎯 学习目标 - 理解插件的基本结构 -- 创建你的第一个Action组件 -- 创建你的第一个Command组件 -- 学会配置插件 +- 从最简单的插件开始,循序渐进 +- 学会创建Action组件(智能动作) +- 学会创建Command组件(命令响应) +- 掌握配置Schema定义和配置文件自动生成(可选) ## 📂 准备工作 确保你已经: -1. 克隆了MaiBot项目 + +1. 克隆了MaiCore项目 2. 安装了Python依赖 3. 了解基本的Python语法 @@ -30,360 +26,502 @@ ### 1. 创建插件目录 -在项目根目录的 `plugins/` 文件夹下创建你的插件目录: +在项目根目录的 `plugins/` 文件夹下创建你的插件目录,目录名与插件名保持一致: + +可以用以下命令快速创建: ```bash mkdir plugins/hello_world_plugin cd plugins/hello_world_plugin ``` -### 2. 创建插件主文件 +### 2. 创建最简单的插件 -创建 `plugin.py` 文件: +让我们从最基础的开始!创建 `plugin.py` 文件: + +```python +from typing import List, Tuple, Type +from src.plugin_system import BasePlugin, register_plugin, ComponentInfo + +# ===== 插件注册 ===== + +@register_plugin +class HelloWorldPlugin(BasePlugin): + """Hello World插件 - 你的第一个MaiCore插件""" + + # 插件基本信息(必须填写) + plugin_name = "hello_world_plugin" + plugin_description = "我的第一个MaiCore插件" + plugin_version = "1.0.0" + plugin_author = "你的名字" + enable_plugin = True # 启用插件 + + def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: + """返回插件包含的组件列表(目前是空的)""" + return [] +``` + +🎉 **恭喜!你刚刚创建了一个最简单但完整的MaiCore插件!** + +**解释一下这些代码:** + +- 首先,我们在plugin.py中定义了一个HelloWorldPulgin插件类,继承自 `BasePlugin` ,提供基本功能。 +- 通过给类加上,`@register_plugin` 装饰器,我们告诉系统"这是一个插件" +- `plugin_name` 等是插件的基本信息,必须填写 +- `get_plugin_components()` 返回插件的功能组件,现在我们没有定义任何action(动作)或者command(指令),是空的 + +### 3. 测试基础插件 + +现在就可以测试这个插件了!启动MaiCore: + +直接通过启动器运行MaiCore或者 `python bot.py` + +在日志中你应该能看到插件被加载的信息。虽然插件还没有任何功能,但它已经成功运行了! + +![1750326700269](image/quick-start/1750326700269.png) + +### 4. 添加第一个功能:问候Action + +现在我们要给插件加入一个有用的功能,我们从最好玩的Action做起 + +Action是一类可以让MaiCore根据自身意愿选择使用的“动作”,在MaiCore中,不论是“回复”还是“不回复”,或者“发送表情”以及“禁言”等等,都是通过Action实现的。 + +你可以通过编写动作,来拓展MaiCore的能力,包括发送语音,截图,甚至操作文件,编写代码...... + +现在让我们给插件添加第一个简单的功能。这个Action可以对用户发送一句问候语。 + +在 `plugin.py` 文件中添加Action组件,完整代码如下: ```python from typing import List, Tuple, Type from src.plugin_system import ( - BasePlugin, register_plugin, BaseAction, BaseCommand, + BasePlugin, register_plugin, BaseAction, ComponentInfo, ActionActivationType, ChatMode ) # ===== Action组件 ===== 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_description = "向用户发送友好的问候消息" + action_description = "向用户发送问候消息" - # 关键词配置 - activation_keywords = ["你好", "hello", "hi"] - keyword_case_sensitive = False - - # ===== 功能定义必须项 ===== + # === 功能描述(必须填写)=== action_parameters = { - "greeting_style": "问候风格:casual(随意) 或 formal(正式)" + "greeting_message": "要发送的问候消息" } - action_require = [ - "用户发送问候语时使用", - "营造友好的聊天氛围" - ] - - associated_types = ["text", "emoji"] + "需要发送友好问候时使用", + "当有人向你问好时使用", + "当你遇见没有见过的人时使用" + ] + associated_types = ["text"] async def execute(self) -> Tuple[bool, str]: - """执行问候动作""" - # 获取参数 - style = self.action_data.get("greeting_style", "casual") - - # 根据风格生成问候语 - if style == "formal": - message = "您好!很高兴为您服务!" - emoji = "🙏" - else: - message = "嗨!很开心见到你!" - emoji = "😊" - - # 发送消息 + """执行问候动作 - 这是核心功能""" + # 发送问候消息 + greeting_message = self.action_data.get("greeting_message","") + + message = "嗨!很开心见到你!😊" + greeting_message await self.send_text(message) - await self.send_type("emoji", emoji) - - return True, f"发送了{style}风格的问候" + + return True, "发送了问候消息" + +# ===== 插件注册 ===== + +@register_plugin +class HelloWorldPlugin(BasePlugin): + """Hello World插件 - 你的第一个MaiCore插件""" + + # 插件基本信息 + plugin_name = "hello_world_plugin" + plugin_description = "我的第一个MaiCore插件,包含问候功能" + plugin_version = "1.0.0" + plugin_author = "你的名字" + enable_plugin = True + + def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: + """返回插件包含的组件列表""" + return [ + # 添加我们的问候Action + (HelloAction.get_action_info(), HelloAction), + ] +``` + +**新增内容解释:** + +- `HelloAction` 是一个Action组件,MaiCore可能会选择使用它 +- `execute()` 函数是Action的核心,定义了当Action被MaiCore选择后,具体要做什么 +- `self.send_text()` 是发送文本消息的便捷方法 + +### 5. 测试问候功能 + +重启MaiCore,然后在聊天中发送任意消息,比如: + +``` +你好 +``` + +MaiCore可能会选择使用你的问候Action,发送回复: + +``` +嗨!很开心见到你!😊 +``` + +![1750332508760](image/quick-start/1750332508760.png) + +> **💡 小提示**:MaiCore会智能地决定什么时候使用它。如果没有立即看到效果,多试几次不同的消息。 + +🎉 **太棒了!你的插件已经有实际功能了!** + +### 5.5. 了解激活系统(重要概念) + +Action固然好用简单,但是现在有个问题,当用户加载了非常多的插件,添加了很多自定义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组件 ===== -class TimeCommand(BaseCommand): - """时间查询Command - 展示命令的基本用法""" +from src.plugin_system import BaseCommand +#导入Command基类 - command_pattern = r"^/time$" +class TimeCommand(BaseCommand): + """时间查询Command - 响应/time命令""" + + command_name = "time" + command_description = "查询当前时间" + + # === 命令设置(必须填写)=== + command_pattern = r"^/time$" # 精确匹配 "/time" 命令 command_help = "查询当前时间" command_examples = ["/time"] - intercept_message = True # 拦截消息处理 + intercept_message = True # 拦截消息,不让其他组件处理 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("%Y-%m-%d %H:%M:%S") - - await self.send_text(f"⏰ 当前时间:{time_str}") - + time_str = now.strftime(time_format) + + # 发送时间信息 + message = f"⏰ 当前时间:{time_str}" + await self.send_text(message) + return True, f"显示了当前时间: {time_str}" # ===== 插件注册 ===== @register_plugin class HelloWorldPlugin(BasePlugin): - """Hello World插件 - 你的第一个MaiBot插件""" + """Hello World插件 - 你的第一个MaiCore插件""" - # 插件基本信息 plugin_name = "hello_world_plugin" - plugin_description = "Hello World演示插件,展示基本的Action和Command用法" + plugin_description = "我的第一个MaiCore插件,包含问候和时间查询功能" plugin_version = "1.0.0" plugin_author = "你的名字" - enable_plugin = True # 默认启用插件 - config_file_name = "config.toml" - - # Python依赖声明(可选) - python_dependencies = [ - # 如果你的插件需要额外的Python包,在这里声明 - # PythonDependency( - # package_name="requests", - # version=">=2.25.0", - # description="HTTP请求库" - # ), - ] + enable_plugin = True def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: - """返回插件包含的组件列表""" return [ - # Action组件 - 使用类中定义的所有属性 (HelloAction.get_action_info(), HelloAction), - - # Command组件 - 需要指定name和description - (TimeCommand.get_command_info( - name="time_query", - description="查询当前系统时间" - ), TimeCommand), + (ByeAction.get_action_info(), ByeAction), + (TimeCommand.get_command_info(), TimeCommand), ] ``` -### 3. 创建配置文件 +**Command组件解释:** -创建 `config.toml` 文件: +- Command是直接响应用户命令的组件 +- `command_pattern` 使用正则表达式匹配用户输入 +- `^/time$` 表示精确匹配 "/time" +- `intercept_message = True` 表示处理完命令后不再让其他组件处理 -```toml -[plugin] -name = "hello_world_plugin" -version = "1.0.0" -enabled = true -description = "Hello World演示插件" +### 7. 测试时间查询功能 -[greeting] -default_style = "casual" -enable_emoji = true +重启MaiCore,发送命令: -[time] -timezone = "Asia/Shanghai" -format = "%Y-%m-%d %H:%M:%S" - -[logging] -level = "INFO" +``` +/time ``` -### 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 # Hello World 插件 ## 概述 - -这是一个简单的Hello World插件,演示了MaiBot插件系统的基本用法。 +我的第一个MaiCore插件,包含问候和时间查询功能。 ## 功能 - -- **HelloAction**: 智能问候动作,响应用户的问候语 -- **TimeCommand**: 时间查询命令,显示当前时间 +- **问候功能**: 当用户说"你好"、"hello"、"hi"时自动回复 +- **时间查询**: 发送 `/time` 命令查询当前时间 ## 使用方法 +### 问候功能 +发送包含以下关键词的消息: +- "你好" +- "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插件开发的基础!** \ No newline at end of file diff --git a/plugins/example_plugin/README.md b/plugins/example_plugin/README.md deleted file mode 100644 index 97be5f980..000000000 --- a/plugins/example_plugin/README.md +++ /dev/null @@ -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 <消息内容> -``` -- **功能**: 向指定群聊或私聊发送消息 -- **拦截**: ✅ 拦截消息处理 -- **示例**: `/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插件系统的最佳起点!🌟 \ No newline at end of file diff --git a/plugins/example_plugin/plugin.py b/plugins/example_plugin/plugin.py deleted file mode 100644 index db7478e87..000000000 --- a/plugins/example_plugin/plugin.py +++ /dev/null @@ -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\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 <消息内容>", - "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 <目标类型> <消息> - 发送消息 -• /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+(?Pgroup|user)\s+(?P\d+)\s+(?P.+)$" - command_help = "向指定群聊或私聊发送消息,用法:/send <消息内容>" - 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\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.+)$" - 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 diff --git a/scripts/log_viewer.py b/scripts/log_viewer.py index 248919fa8..30f9cebf0 100644 --- a/scripts/log_viewer.py +++ b/scripts/log_viewer.py @@ -1183,3 +1183,4 @@ def main(): if __name__ == "__main__": main() + diff --git a/scripts/log_viewer_optimized.py b/scripts/log_viewer_optimized.py index bb1954515..b4ae6e066 100644 --- a/scripts/log_viewer_optimized.py +++ b/scripts/log_viewer_optimized.py @@ -366,11 +366,9 @@ class VirtualLogDisplay: # 应用标签(可选,为了性能可以考虑简化) for tag_info in batch_tags: - try: - 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") - except: - pass + 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") + class AsyncLogLoader: diff --git a/src/chat/focus_chat/expressors/exprssion_learner.py b/src/chat/express/exprssion_learner.py similarity index 100% rename from src/chat/focus_chat/expressors/exprssion_learner.py rename to src/chat/express/exprssion_learner.py diff --git a/src/chat/focus_chat/expressors/default_expressor.py b/src/chat/focus_chat/expressors/default_expressor.py deleted file mode 100644 index 9be412c83..000000000 --- a/src/chat/focus_chat/expressors/default_expressor.py +++ /dev/null @@ -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() diff --git a/src/chat/focus_chat/heartFC_chat.py b/src/chat/focus_chat/heartFC_chat.py index bf70a2e41..f8cd5dfe3 100644 --- a/src/chat/focus_chat/heartFC_chat.py +++ b/src/chat/focus_chat/heartFC_chat.py @@ -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.actions_observation import ActionObservation 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.info_processors.base_processor import BaseProcessor from src.chat.focus_chat.info_processors.self_processor import SelfProcessor @@ -125,9 +123,6 @@ class HeartFChatting: self.processors: List[BaseProcessor] = [] 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_planner = PlannerFactory.create_planner( 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: try: + loop_start_time = time.time() with Timer("观察", cycle_timers): # 执行所有观察器的观察 for observation in self.observations: @@ -583,7 +579,7 @@ class HeartFChatting: } 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 = { "action_result": plan_result.get("action_result", {}), @@ -607,7 +603,7 @@ class HeartFChatting: logger.debug(f"{self.log_prefix} 麦麦想要:'{action_str}'") 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 = { @@ -646,7 +642,6 @@ class HeartFChatting: action_data: dict, cycle_timers: dict, thinking_id: str, - observations: List[Observation], ) -> tuple[bool, str, str]: """ 处理规划动作,使用动作工厂创建相应的动作处理器 @@ -670,9 +665,6 @@ class HeartFChatting: reasoning=reasoning, cycle_timers=cycle_timers, thinking_id=thinking_id, - observations=observations, - expressor=self.expressor, - replyer=self.replyer, chat_stream=self.chat_stream, log_prefix=self.log_prefix, shutting_down=self._shutting_down, diff --git a/src/chat/focus_chat/heartFC_sender.py b/src/chat/focus_chat/heartFC_sender.py index 772e086d3..3cb4e3a2e 100644 --- a/src/chat/focus_chat/heartFC_sender.py +++ b/src/chat/focus_chat/heartFC_sender.py @@ -15,7 +15,7 @@ install(extra_lines=3) logger = get_logger("sender") -async def send_message(message: MessageSending) -> str: +async def send_message(message: MessageSending) -> bool: """合并后的消息发送函数,包含WS发送和日志记录""" message_preview = truncate_message(message.processed_plain_text, max_length=40) @@ -23,7 +23,7 @@ async def send_message(message: MessageSending) -> str: # 直接调用API发送消息 await get_global_api().send_message(message) logger.info(f"已将消息 '{message_preview}' 发往平台'{message.message_info.platform}'") - return message.processed_plain_text + return True except Exception as 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) 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 对象,待发送的消息。 - has_thinking: 是否管理思考状态,表情包无思考状态(如需调用 register_thinking/complete_thinking)。 - typing: 是否模拟打字等待(根据 has_thinking 控制等待时长)。 + typing: 是否模拟打字等待。 用法: - - has_thinking=True 时,自动处理思考消息的时间和清理。 - typing=True 时,发送前会有打字等待。 """ if not message.chat_stream: @@ -98,40 +96,29 @@ class HeartFCSender: try: 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() - # print(f"message.display_message: {message.display_message}") if typing: - if has_thinking: - typing_time = calculate_typing_time( - input_string=message.processed_plain_text, - thinking_start_time=message.thinking_start_time, - is_emoji=message.is_emoji, - ) - await asyncio.sleep(typing_time) - else: - await asyncio.sleep(0.5) + typing_time = calculate_typing_time( + input_string=message.processed_plain_text, + thinking_start_time=message.thinking_start_time, + is_emoji=message.is_emoji, + ) + await asyncio.sleep(typing_time) + sent_msg = await send_message(message) - await self.storage.store_message(message, message.chat_stream) + if not sent_msg: + return False - if sent_msg: - return sent_msg - else: - return "发送失败" + if storage_message: + await self.storage.store_message(message, message.chat_stream) + + return sent_msg except Exception as e: logger.error(f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}") diff --git a/src/chat/focus_chat/heartflow_message_processor.py b/src/chat/focus_chat/heartflow_message_processor.py index cf0b70a26..5060c5f0c 100644 --- a/src/chat/focus_chat/heartflow_message_processor.py +++ b/src/chat/focus_chat/heartflow_message_processor.py @@ -172,6 +172,7 @@ class HeartFCMessageReceiver: return # 5. 消息存储 + print(f"message: {message.message_info.time}") await self.storage.store_message(message, chat) # 6. 兴趣度计算与更新 diff --git a/src/chat/focus_chat/info_processors/expression_selector_processor.py b/src/chat/focus_chat/info_processors/expression_selector_processor.py index cd82afcdc..ac68b3e08 100644 --- a/src/chat/focus_chat/info_processors/expression_selector_processor.py +++ b/src/chat/focus_chat/info_processors/expression_selector_processor.py @@ -11,7 +11,7 @@ from src.chat.message_receive.chat_stream import get_chat_manager from .base_processor import BaseProcessor 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.expressors.exprssion_learner import get_expression_learner +from src.chat.express.exprssion_learner import get_expression_learner from json_repair import repair_json import json diff --git a/src/chat/focus_chat/planners/action_manager.py b/src/chat/focus_chat/planners/action_manager.py index 555a6df4f..ec53227f6 100644 --- a/src/chat/focus_chat/planners/action_manager.py +++ b/src/chat/focus_chat/planners/action_manager.py @@ -1,13 +1,9 @@ from typing import Dict, List, Optional, Type, Any 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.common.logger import get_logger - -# 不再需要导入动作类,因为已经在main.py中导入 -# import src.chat.actions.default_actions # noqa +from src.plugin_system.core.component_registry import component_registry +from src.plugin_system.base.component_types import ComponentType logger = get_logger("action_manager") @@ -15,87 +11,11 @@ logger = get_logger("action_manager") 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: """ 动作管理器,用于管理各种类型的动作 + + 现在统一使用新插件系统,简化了原有的新旧兼容逻辑。 """ # 类常量 @@ -119,23 +39,20 @@ class ActionManager: # 初始化时将默认动作加载到使用中的动作 self._using_actions = self._default_actions.copy() - # 添加系统核心动作 - # self._add_system_core_actions() - def _load_plugin_actions(self) -> None: """ - 加载所有插件目录中的动作 + 加载所有插件系统中的动作 """ try: # 从新插件系统获取Action组件 self._load_plugin_system_actions() - logger.debug("从新插件系统加载Action组件成功") + logger.debug("从插件系统加载Action组件成功") except Exception as e: logger.error(f"加载插件动作失败: {e}") def _load_plugin_system_actions(self) -> None: - """从新插件系统的component_registry加载Action组件""" + """从插件系统的component_registry加载Action组件""" try: from src.plugin_system.core.component_registry import component_registry from src.plugin_system.base.component_types import ComponentType @@ -148,7 +65,7 @@ class ActionManager: logger.debug(f"Action组件 {action_name} 已存在,跳过") continue - # 将新插件系统的ActionInfo转换为旧系统格式 + # 将插件系统的ActionInfo转换为ActionManager格式 converted_action_info = { "description": action_info.description, "parameters": getattr(action_info, "action_parameters", {}), @@ -165,8 +82,7 @@ class ActionManager: # 模式和并行设置 "mode_enable": action_info.mode_enable.value, "parallel_action": action_info.parallel_action, - # 标记这是来自新插件系统的组件 - "_plugin_system_component": True, + # 插件信息 "_plugin_name": getattr(action_info, "plugin_name", ""), } @@ -180,7 +96,7 @@ class ActionManager: 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: logger.error(f"从插件系统加载Action组件失败: {e}") @@ -195,12 +111,9 @@ class ActionManager: 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[BaseAction]: """ 创建动作处理器实例 @@ -211,9 +124,6 @@ class ActionManager: reasoning: 执行理由 cycle_timers: 计时器字典 thinking_id: 思考ID - observations: 观察列表 - expressor: 表达器 - replyer: 回复器 chat_stream: 聊天流 log_prefix: 日志前缀 shutting_down: 是否正在关闭 @@ -221,122 +131,39 @@ class ActionManager: Returns: 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: - # 创建动作实例 - instance = handler_class( - 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) + # 获取组件类 - 明确指定查询Action类型 + component_class = component_registry.get_component_class(action_name, ComponentType.ACTION) 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 # 获取插件配置 - 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, reasoning=reasoning, cycle_timers=cycle_timers, thinking_id=thinking_id, chat_stream=chat_stream, - expressor=expressor, - replyer=replyer, - observations=observations, log_prefix=log_prefix, + shutting_down=shutting_down, plugin_config=plugin_config, ) - # 创建兼容性包装器 - wrapper = PluginActionWrapper( - 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 + logger.debug(f"创建Action实例成功: {action_name}") + return instance except Exception as e: - logger.error(f"创建插件Action实例失败 {action_name}: {e}") + logger.error(f"创建Action实例失败 {action_name}: {e}") import traceback logger.error(traceback.format_exc()) @@ -366,19 +193,13 @@ class ActionManager: """ filtered_actions = {} - # print(self._using_actions) - 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") # 检查动作是否在当前模式下启用 if action_mode == "all" or action_mode == mode: filtered_actions[action_name] = action_info 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())}") return filtered_actions @@ -474,20 +295,6 @@ class ActionManager: def restore_default_actions(self) -> None: """恢复默认动作集到使用集""" 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: """ @@ -517,5 +324,4 @@ class ActionManager: """ from src.plugin_system.core.component_registry import component_registry - action_registry = component_registry.get_action_registry() - return action_registry.get(action_name) + return component_registry.get_component_class(action_name) diff --git a/src/chat/focus_chat/planners/base_planner.py b/src/chat/focus_chat/planners/base_planner.py index eea4859b6..0201da63d 100644 --- a/src/chat/focus_chat/planners/base_planner.py +++ b/src/chat/focus_chat/planners/base_planner.py @@ -12,14 +12,14 @@ class BasePlanner(ABC): self.action_manager = action_manager @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: all_plan_info: 所有计划信息 running_memorys: 回忆信息 - + loop_start_time: 循环开始时间 Returns: Dict[str, Any]: 规划结果 """ diff --git a/src/chat/focus_chat/planners/modify_actions.py b/src/chat/focus_chat/planners/modify_actions.py index ada8e13e4..1c0bb72bf 100644 --- a/src/chat/focus_chat/planners/modify_actions.py +++ b/src/chat/focus_chat/planners/modify_actions.py @@ -242,6 +242,8 @@ class ActionModifier: for action_name, action_info in actions_with_info.items(): activation_type = action_info.get("focus_activation_type", "always") + + print(f"action_name: {action_name}, activation_type: {activation_type}") # 现在统一是字符串格式的激活类型值 if activation_type == "always": diff --git a/src/chat/focus_chat/planners/planner_simple.py b/src/chat/focus_chat/planners/planner_simple.py index 6aa2a7e86..95c5a0d8c 100644 --- a/src/chat/focus_chat/planners/planner_simple.py +++ b/src/chat/focus_chat/planners/planner_simple.py @@ -32,11 +32,7 @@ def init_prompt(): {self_info_block} 请记住你的性格,身份和特点。 -{extra_info_block} -{memory_str} - {time_block} - 你是群内的一员,你现在正在参与群内的闲聊,以下是群内的聊天内容: {chat_content_block} @@ -86,13 +82,14 @@ class ActionPlanner(BasePlanner): 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根据上下文决定做出什么动作。 参数: all_plan_info: 所有计划信息 running_memorys: 回忆信息 + loop_start_time: 循环开始时间 """ action = "no_reply" # 默认动作 @@ -246,6 +243,8 @@ class ActionPlanner(BasePlanner): if selected_expressions: action_data["selected_expressions"] = selected_expressions logger.debug(f"{self.log_prefix} 传递{len(selected_expressions)}个选中的表达方式到action_data") + + action_data["loop_start_time"] = loop_start_time # 对于reply动作不需要额外处理,因为相关字段已经在上面的循环中添加到action_data @@ -326,7 +325,7 @@ class ActionPlanner(BasePlanner): chat_content_block = "" if observed_messages_str: - chat_content_block = f"聊天记录:\n{observed_messages_str}" + chat_content_block = f"\n{observed_messages_str}" else: chat_content_block = "你还未开始聊天" @@ -387,7 +386,7 @@ class ActionPlanner(BasePlanner): prompt = planner_prompt_template.format( relation_info_block=relation_info_block, self_info_block=self_info_block, - memory_str=memory_str, + # memory_str=memory_str, time_block=time_block, # bot_name=global_config.bot.nickname, prompt_personality=personality_block, @@ -397,7 +396,7 @@ class ActionPlanner(BasePlanner): cycle_info_block=cycle_info, action_options_text=action_options_block, # action_available_block=action_available_block, - extra_info_block=extra_info_block, + # extra_info_block=extra_info_block, moderation_prompt=moderation_prompt_block, ) return prompt diff --git a/src/chat/focus_chat/replyer/default_replyer.py b/src/chat/focus_chat/replyer/default_generator.py similarity index 60% rename from src/chat/focus_chat/replyer/default_replyer.py rename to src/chat/focus_chat/replyer/default_generator.py index 24715ecd3..6f2f13673 100644 --- a/src/chat/focus_chat/replyer/default_replyer.py +++ b/src/chat/focus_chat/replyer/default_generator.py @@ -8,9 +8,7 @@ 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 @@ -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.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.express.exprssion_learner import get_expression_learner import time import random from datetime import datetime @@ -50,7 +49,7 @@ def init_prompt(): 不要浮夸,不要夸张修辞,只输出一条回复就好。 现在,你说: """, - "default_replyer_prompt", + "default_generator_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.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) + 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): """创建思考消息 (尝试锚定到 anchor_message)""" @@ -114,213 +156,152 @@ class DefaultReplyer: await self.heart_fc_sender.register_thinking(thinking_message) return None - - async def deal_reply( + + async def generate_reply_with_context( 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: - 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]]: + reply_data: Dict[str, Any], + ) -> Tuple[bool, Optional[List[str]]]: """ 回复器 (Replier): 核心逻辑,负责生成回复文本。 (已整合原 HeartFCGenerator 的功能) """ 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 with Timer("构建Prompt", {}): # 内部计时器,可选保留 - prompt = await self.build_prompt_focus( - chat_stream=self.chat_stream, # Pass the stream object - # 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 + prompt = await self.build_prompt_reply_context( + reply_data=reply_data, # 传递action_data ) # 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生成", {}): # 内部计时器,可选保留 logger.info(f"{self.log_prefix}Prompt:\n{prompt}\n") content, (reasoning_content, model_name) = await self.express_model.generate_response_async(prompt) - # logger.info(f"prompt: {prompt}") logger.info(f"最终回复: {content}") except Exception as llm_e: # 精简报错信息 logger.error(f"{self.log_prefix}LLM 生成失败: {llm_e}") - return None # LLM 调用失败则无法生成回复 + return False, None # LLM 调用失败则无法生成回复 processed_response = process_llm_response(content) # 5. 处理 LLM 响应 if not content: logger.warning(f"{self.log_prefix}LLM 生成了空内容。") - return None + return False, None if not processed_response: logger.warning(f"{self.log_prefix}处理后的回复为空。") - return None + return False, None reply_set = [] for str in processed_response: reply_seg = ("text", str) reply_set.append(reply_seg) - return reply_set + return True , reply_set except Exception as e: logger.error(f"{self.log_prefix}回复生成意外失败: {e}") traceback.print_exc() - return None - - async def build_prompt_focus( + return False, None + + async def rewrite_reply_with_context( self, - reason, - chat_stream, - sender_name, - # in_mind_reply, - extra_info_block, - relation_info_block, - identity, - target_message, - config_expression_style, - action_data=None, - # stuation, + reply_data: Dict[str, Any], + ) -> Tuple[bool, Optional[List[str]]]: + """ + 表达器 (Expressor): 核心逻辑,负责生成回复文本。 + """ + try: + + + reply_to = reply_data.get("reply_to", "") + raw_reply = reply_data.get("raw_reply", "") + 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: + chat_stream = self.chat_stream + 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( chat_id=chat_stream.stream_id, @@ -341,7 +322,7 @@ class DefaultReplyer: 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: logger.info(f"{self.log_prefix} 使用处理器选中的{len(selected_expressions)}个表达方式") @@ -371,7 +352,7 @@ class DefaultReplyer: try: # 处理关键词规则 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}") keywords_reaction_prompt += f"{rule.reaction}," @@ -380,7 +361,7 @@ class DefaultReplyer: for pattern_str in rule.regex: try: pattern = re.compile(pattern_str) - if result := pattern.search(target_message): + if result := pattern.search(target): reaction = rule.reaction for name, content in result.groupdict().items(): reaction = reaction.replace(f"[{name}]", content) @@ -397,18 +378,18 @@ class DefaultReplyer: # logger.debug("开始构建 focus prompt") - if sender_name: + if sender: reply_target_block = ( - f"现在{sender_name}说的:{target_message}。引起了你的注意,你想要在群里发言或者回复这条消息。" + f"现在{sender}说的:{target}。引起了你的注意,你想要在群里发言或者回复这条消息。" ) - elif target_message: - reply_target_block = f"现在{target_message}引起了你的注意,你想要在群里发言或者回复这条消息。" + elif target: + reply_target_block = f"现在{target}引起了你的注意,你想要在群里发言或者回复这条消息。" else: reply_target_block = "现在,你想要在群里发言或者回复消息。" # --- Choose template based on chat type --- if is_group_chat: - template_name = "default_replyer_prompt" + template_name = "default_generator_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") @@ -422,18 +403,14 @@ class DefaultReplyer: relation_info_block=relation_info_block, time_block=time_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, identity=identity, - target_message=target_message, - sender_name=sender_name, - config_expression_style=config_expression_style, + target_message=target, + sender_name=sender, + config_expression_style=global_config.expression.expression_style, ) else: # Private chat - template_name = "default_replyer_private_prompt" + template_name = "default_generator_private_prompt" chat_target_1 = "你正在和人私聊" prompt = await global_prompt_manager.format_prompt( template_name, @@ -444,20 +421,125 @@ class DefaultReplyer: relation_info_block=relation_info_block, time_block=time_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, identity=identity, - target_message=target_message, - sender_name=sender_name, - config_expression_style=config_expression_style, + target_message=target, + sender_name=sender, + config_expression_style=global_config.expression.expression_style, ) 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( self, @@ -468,7 +550,7 @@ class DefaultReplyer: ) -> Optional[MessageSending]: """发送回复消息 (尝试锚定到 anchor_message),使用 HeartFCSender""" chat = self.chat_stream - chat_id = self.chat_id + chat_id = self.chat_stream.stream_id if chat is None: logger.error(f"{self.log_prefix} 无法发送回复,chat_stream 为空。") return None @@ -514,7 +596,7 @@ class DefaultReplyer: is_emoji = False 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, message_id=part_message_id, message_segment=message_segment, @@ -526,22 +608,22 @@ class DefaultReplyer: ) 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: 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 + bot_message, typing=typing, set_reply=set_reply ) reply_message_ids.append(part_message_id) # 记录我们生成的ID @@ -562,30 +644,15 @@ class DefaultReplyer: 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( 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, + anchor_message: MessageRecv = None ) -> MessageSending: """构建单个发送消息""" @@ -596,12 +663,16 @@ class DefaultReplyer: ) # await anchor_message.process() + if anchor_message: + sender_info = anchor_message.message_info.user_info + else: + sender_info = None 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, + sender_info=sender_info, message_segment=message_segment, reply=anchor_message, # 回复原始锚点 is_head=reply_to, diff --git a/src/chat/heart_flow/observation/chatting_observation.py b/src/chat/heart_flow/observation/chatting_observation.py index 213cafbc3..d88e5ad6e 100644 --- a/src/chat/heart_flow/observation/chatting_observation.py +++ b/src/chat/heart_flow/observation/chatting_observation.py @@ -14,7 +14,8 @@ from src.chat.message_receive.message import MessageRecv from src.chat.heart_flow.observation.observation import Observation from src.common.logger import get_logger 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") # 定义提示模板 @@ -70,6 +71,8 @@ class ChattingObservation(Observation): self.oldest_messages = [] 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) self.last_observe_time = initial_messages[-1]["time"] if initial_messages else self.last_observe_time self.talking_message = initial_messages @@ -92,39 +95,28 @@ class ChattingObservation(Observation): def get_observe_info(self, ids=None): 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中查找最新的,最匹配的消息 2. 如果找到,则返回消息 """ - msg_list = [] find_msg = None reverse_talking_message = list(reversed(self.talking_message)) for message in reverse_talking_message: - if message["processed_plain_text"] == text: - find_msg = message - 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}) + 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 + break 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 = { "platform": find_msg.get("user_platform", ""), @@ -167,6 +159,10 @@ class ChattingObservation(Observation): "processed_plain_text": find_msg.get("processed_plain_text"), } 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 async def observe(self): @@ -179,6 +175,8 @@ class ChattingObservation(Observation): limit_mode="latest", ) + print(f"new_messages_list: {new_messages_list}") + last_obs_time_mark = self.last_observe_time if new_messages_list: self.last_observe_time = new_messages_list[-1]["time"] diff --git a/src/chat/message_receive/chat_stream.py b/src/chat/message_receive/chat_stream.py index abd3f0afd..bf6ea3c8a 100644 --- a/src/chat/message_receive/chat_stream.py +++ b/src/chat/message_receive/chat_stream.py @@ -171,6 +171,15 @@ class ChatManager: # 使用MD5生成唯一ID key = "_".join(components) 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( self, platform: str, user_info: UserInfo, group_info: Optional[GroupInfo] = None diff --git a/src/chat/message_receive/message.py b/src/chat/message_receive/message.py index 13a238cc8..5798eb512 100644 --- a/src/chat/message_receive/message.py +++ b/src/chat/message_receive/message.py @@ -275,7 +275,7 @@ class MessageSending(MessageProcessBase): message_id: str, chat_stream: "ChatStream", bot_user_info: UserInfo, - sender_info: UserInfo | None, # 用来记录发送者信息,用于私聊回复 + sender_info: UserInfo | None, # 用来记录发送者信息 message_segment: Seg, display_message: str = "", reply: Optional["MessageRecv"] = None, @@ -304,20 +304,17 @@ class MessageSending(MessageProcessBase): # 用于显示发送内容与显示不一致的情况 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: - self.reply_to_message_id = self.reply.message_info.message_id - self.message_segment = Seg( - type="seglist", - data=[ - Seg(type="reply", data=self.reply.message_info.message_id), - self.message_segment, - ], - ) + if self.reply: + self.reply_to_message_id = self.reply.message_info.message_id + self.message_segment = Seg( + type="seglist", + data=[ + Seg(type="reply", data=self.reply.message_info.message_id), + self.message_segment, + ], + ) async def process(self) -> None: """处理消息内容,生成纯文本和详细文本""" diff --git a/src/chat/message_receive/message_sender.py b/src/chat/message_receive/message_sender.py index 74039935b..030b8b3f2 100644 --- a/src/chat/message_receive/message_sender.py +++ b/src/chat/message_receive/message_sender.py @@ -230,7 +230,7 @@ class MessageManager: logger.debug( f"[{message.chat_stream.stream_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}..." ) - message.set_reply(message.reply) + message.build_reply() # --- 结束条件 set_reply --- await message.process() # 预处理消息内容 diff --git a/src/chat/normal_chat/normal_chat.py b/src/chat/normal_chat/normal_chat.py index be2435c3a..3c5a587a0 100644 --- a/src/chat/normal_chat/normal_chat.py +++ b/src/chat/normal_chat/normal_chat.py @@ -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_action_modifier import NormalChatActionModifier 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.chat.utils.chat_message_builder import ( get_raw_msg_by_timestamp_with_chat, @@ -1063,9 +1063,6 @@ class NormalChat: reasoning=action_data.get("reasoning", ""), cycle_timers={}, # normal_chat使用空的cycle_timers thinking_id=thinking_id, - observations=[], # normal_chat不使用observations - expressor=self.expressor, # 使用normal_chat专用的expressor - replyer=self.replyer, chat_stream=self.chat_stream, log_prefix=self.stream_name, shutting_down=self._disabled, diff --git a/src/chat/normal_chat/normal_prompt.py b/src/chat/normal_chat/normal_prompt.py index 4a6d54b91..540793115 100644 --- a/src/chat/normal_chat/normal_prompt.py +++ b/src/chat/normal_chat/normal_prompt.py @@ -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.common.logger import get_logger from src.individuality.individuality import get_individuality diff --git a/src/main.py b/src/main.py index 9a9a436c5..3857df0dc 100644 --- a/src/main.py +++ b/src/main.py @@ -2,7 +2,7 @@ import asyncio import time 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.manager.async_task_manager import async_task_manager from src.chat.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask diff --git a/src/plugin_system/__init__.py b/src/plugin_system/__init__.py index 7067b399b..7a49c6f62 100644 --- a/src/plugin_system/__init__.py +++ b/src/plugin_system/__init__.py @@ -8,6 +8,7 @@ MaiBot 插件系统 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.config_types import ConfigField from src.plugin_system.base.component_types import ( ComponentType, ActionActivationType, @@ -18,11 +19,11 @@ from src.plugin_system.base.component_types import ( PluginInfo, 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.component_registry import component_registry from src.plugin_system.core.dependency_manager import dependency_manager + __version__ = "1.0.0" __all__ = [ @@ -39,14 +40,11 @@ __all__ = [ "CommandInfo", "PluginInfo", "PythonDependency", - # API接口 - "PluginAPI", - "create_plugin_api", - "create_command_api", # 管理器 "plugin_manager", "component_registry", "dependency_manager", # 装饰器 "register_plugin", + "ConfigField", ] diff --git a/src/plugin_system/apis/__init__.py b/src/plugin_system/apis/__init__.py index 8557aa974..eb3930279 100644 --- a/src/plugin_system/apis/__init__.py +++ b/src/plugin_system/apis/__init__.py @@ -1,37 +1,33 @@ """ -插件API模块 +插件系统API模块 -提供插件可以使用的各种API接口 +提供了插件开发所需的各种API """ -from src.plugin_system.apis.plugin_api import PluginAPI, create_plugin_api, create_command_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 - -# 新增:分类的API聚合 -from src.plugin_system.apis.action_apis import ActionAPI -from src.plugin_system.apis.independent_apis import IndependentAPI, StaticAPI +# 导入所有API模块 +from src.plugin_system.apis import ( + chat_api, + config_api, + database_api, + emoji_api, + generator_api, + llm_api, + message_api, + person_api, + send_api, + utils_api +) +# 导出所有API模块,使它们可以通过 apis.xxx 方式访问 __all__ = [ - # 原有统一API - "PluginAPI", - "create_plugin_api", - "create_command_api", - # 原有单独API - "MessageAPI", - "LLMAPI", - "DatabaseAPI", - "ConfigAPI", - "UtilsAPI", - "StreamAPI", - "HearflowAPI", - # 新增分类API - "ActionAPI", # 需要Action依赖的API - "IndependentAPI", # 独立API - "StaticAPI", # 静态API + "chat_api", + "config_api", + "database_api", + "emoji_api", + "generator_api", + "llm_api", + "message_api", + "person_api", + "send_api", + "utils_api" ] diff --git a/src/plugin_system/apis/action_apis.py b/src/plugin_system/apis/action_apis.py deleted file mode 100644 index 84b750dad..000000000 --- a/src/plugin_system/apis/action_apis.py +++ /dev/null @@ -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} 设置观察列表") diff --git a/src/plugin_system/apis/chat_api.py b/src/plugin_system/apis/chat_api.py new file mode 100644 index 000000000..f07613f23 --- /dev/null +++ b/src/plugin_system/apis/chat_api.py @@ -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() \ No newline at end of file diff --git a/src/plugin_system/apis/config_api.py b/src/plugin_system/apis/config_api.py index fba31c10f..a2f8870dd 100644 --- a/src/plugin_system/apis/config_api.py +++ b/src/plugin_system/apis/config_api.py @@ -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 src.common.logger import get_logger from src.config.config import global_config @@ -6,92 +15,104 @@ from src.person_info.person_info import get_person_info_manager 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: - """ - 安全地从全局配置中获取一个值。 - 插件应使用此方法读取全局配置,以保证只读和隔离性。 - - Args: - key: 配置键名,支持嵌套访问如 "section.subsection.key" - default: 如果配置不存在时返回的默认值 - - Returns: - Any: 配置值或默认值 - """ - # 支持嵌套键访问 - keys = key.split(".") - current = global_config - - try: - for k in keys: - if hasattr(current, k): - current = getattr(current, k) - else: - return default - return current - except Exception as e: - logger.warning(f"获取全局配置 {key} 失败: {e}") - return default - - def get_config(self, key: str, default: Any = None) -> Any: - """ - 从插件配置中获取值,支持嵌套键访问 - - Args: - key: 配置键名,支持嵌套访问如 "section.subsection.key" - default: 如果配置不存在时返回的默认值 - - Returns: - Any: 配置值或默认值 - """ - # 获取插件配置 - plugin_config = getattr(self, "_plugin_config", {}) - if not plugin_config: - return default - - # 支持嵌套键访问 - keys = key.split(".") - current = plugin_config + Args: + key: 配置键名,支持嵌套访问如 "section.subsection.key" + default: 如果配置不存在时返回的默认值 + Returns: + Any: 配置值或默认值 + """ + # 支持嵌套键访问 + keys = key.split(".") + current = global_config + + try: for k in keys: - if isinstance(current, dict) and k in current: - current = current[k] + if hasattr(current, k): + current = getattr(current, k) else: return default - return current + except Exception as e: + logger.warning(f"[ConfigAPI] 获取全局配置 {key} 失败: {e}") + return default - async def get_user_id_by_person_name(self, person_name: str) -> tuple[str, str]: - """根据用户名获取用户ID - Args: - person_name: 用户名 +def get_plugin_config(plugin_config: dict, key: str, default: Any = None) -> Any: + """ + 从插件配置中获取值,支持嵌套键访问 - Returns: - tuple[str, str]: (平台, 用户ID) - """ + Args: + plugin_config: 插件配置字典 + key: 配置键名,支持嵌套访问如 "section.subsection.key" + default: 如果配置不存在时返回的默认值 + + Returns: + Any: 配置值或默认值 + """ + if not plugin_config: + return default + + # 支持嵌套键访问 + keys = key.split(".") + current = plugin_config + + for k in keys: + if isinstance(current, dict) and k in current: + current = current[k] + else: + return default + + return current + + +# ============================================================================= +# 用户信息API函数 +# ============================================================================= + +async def get_user_id_by_person_name(person_name: str) -> tuple[str, str]: + """根据用户名获取用户ID + + Args: + person_name: 用户名 + + Returns: + tuple[str, str]: (平台, 用户ID) + """ + try: person_info_manager = get_person_info_manager() 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") platform = await person_info_manager.get_value(person_id, "platform") 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: - """获取用户信息 - Args: - person_id: 用户ID - key: 信息键名 - default: 默认值 +async def get_person_info(person_id: str, key: str, default: Any = None) -> Any: + """获取用户信息 - Returns: - Any: 用户信息值或默认值 - """ + Args: + person_id: 用户ID + key: 信息键名 + default: 默认值 + + Returns: + Any: 用户信息值或默认值 + """ + try: person_info_manager = get_person_info_manager() return await person_info_manager.get_value(person_id, key, default) + except Exception as e: + logger.error(f"[ConfigAPI] 获取用户信息失败: {e}") + return default diff --git a/src/plugin_system/apis/database_api.py b/src/plugin_system/apis/database_api.py index 5239fb9ac..ccde040bf 100644 --- a/src/plugin_system/apis/database_api.py +++ b/src/plugin_system/apis/database_api.py @@ -1,352 +1,97 @@ +"""数据库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 time from typing import Dict, List, Any, Union, Type 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 logger = get_logger("database_api") +# ============================================================================= +# 通用数据库查询API函数 +# ============================================================================= -class DatabaseAPI: - """数据库API模块 +async def db_query( + model_class: Type[Model], + query_type: str = "get", + filters: Dict[str, Any] = None, + data: Dict[str, Any] = None, + limit: int = None, + order_by: List[str] = None, + single_result: bool = False, +) -> Union[List[Dict[str, Any]], Dict[str, Any], None]: + """执行数据库查询操作 - 提供了数据库操作相关的功能 + 这个方法提供了一个通用接口来执行数据库操作,包括查询、创建、更新和删除记录。 + + Args: + model_class: Peewee 模型类,例如 ActionRecords, Messages 等 + query_type: 查询类型,可选值: "get", "create", "update", "delete", "count" + filters: 过滤条件字典,键为字段名,值为要匹配的值 + data: 用于创建或更新的数据字典 + limit: 限制结果数量 + order_by: 排序字段列表,使用字段名,前缀'-'表示降序 + single_result: 是否只返回单个结果 + + Returns: + 根据查询类型返回不同的结果: + - "get": 返回查询结果列表或单个结果(如果 single_result=True) + - "create": 返回创建的记录 + - "update": 返回受影响的行数 + - "delete": 返回受影响的行数 + - "count": 返回记录数量 + + 示例: + # 查询最近10条消息 + messages = await database_api.db_query( + Messages, + query_type="get", + filters={"chat_id": chat_stream.stream_id}, + limit=10, + order_by=["-time"] + ) + + # 创建一条记录 + new_record = await database_api.db_query( + ActionRecords, + query_type="create", + data={"action_id": "123", "time": time.time(), "action_name": "TestAction"} + ) + + # 更新记录 + updated_count = await database_api.db_query( + ActionRecords, + query_type="update", + filters={"action_id": "123"}, + data={"action_done": True} + ) + + # 删除记录 + deleted_count = await database_api.db_query( + ActionRecords, + query_type="delete", + filters={"action_id": "123"} + ) + + # 计数 + count = await database_api.db_query( + Messages, + query_type="count", + filters={"chat_id": chat_stream.stream_id} + ) """ - - 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( - self, - model_class: Type[Model], - query_type: str = "get", - filters: Dict[str, Any] = None, - data: Dict[str, Any] = None, - limit: int = None, - order_by: List[str] = None, - single_result: bool = False, - ) -> Union[List[Dict[str, Any]], Dict[str, Any], None]: - """执行数据库查询操作 - - 这个方法提供了一个通用接口来执行数据库操作,包括查询、创建、更新和删除记录。 - - Args: - model_class: Peewee 模型类,例如 ActionRecords, Messages 等 - query_type: 查询类型,可选值: "get", "create", "update", "delete", "count" - filters: 过滤条件字典,键为字段名,值为要匹配的值 - data: 用于创建或更新的数据字典 - limit: 限制结果数量 - order_by: 排序字段列表,使用字段名,前缀'-'表示降序 - single_result: 是否只返回单个结果 - - Returns: - 根据查询类型返回不同的结果: - - "get": 返回查询结果列表或单个结果(如果 single_result=True) - - "create": 返回创建的记录 - - "update": 返回受影响的行数 - - "delete": 返回受影响的行数 - - "count": 返回记录数量 - - 示例: - # 查询最近10条消息 - messages = await self.db_query( - Messages, - query_type="get", - filters={"chat_id": chat_stream.stream_id}, - limit=10, - order_by=["-time"] - ) - - # 创建一条记录 - new_record = await self.db_query( - ActionRecords, - query_type="create", - data={"action_id": "123", "time": time.time(), "action_name": "TestAction"} - ) - - # 更新记录 - updated_count = await self.db_query( - ActionRecords, - query_type="update", - filters={"action_id": "123"}, - data={"action_done": True} - ) - - # 删除记录 - deleted_count = await self.db_query( - ActionRecords, - query_type="delete", - filters={"action_id": "123"} - ) - - # 计数 - count = await self.db_query( - Messages, - query_type="count", - filters={"chat_id": chat_stream.stream_id} - ) - """ - try: - if query_type not in ["get", "create", "update", "delete", "count"]: - raise ValueError("query_type must be 'get' or 'create' or 'update' or 'delete' or 'count'") - # 构建基本查询 - if query_type in ["get", "update", "delete", "count"]: - query = model_class.select() - - # 应用过滤条件 - if filters: - for field, value in filters.items(): - query = query.where(getattr(model_class, field) == value) - - # 执行查询 - if query_type == "get": - # 应用排序 - if order_by: - for field in order_by: - if field.startswith("-"): - query = query.order_by(getattr(model_class, field[1:]).desc()) - else: - query = query.order_by(getattr(model_class, field)) - - # 应用限制 - if limit: - query = query.limit(limit) - - # 执行查询 - results = list(query.dicts()) - - # 返回结果 - if single_result: - return results[0] if results else None - return results - - elif query_type == "create": - if not data: - raise ValueError("创建记录需要提供data参数") - - # 创建记录 - record = model_class.create(**data) - # 返回创建的记录 - return model_class.select().where(model_class.id == record.id).dicts().get() - - elif query_type == "update": - if not data: - raise ValueError("更新记录需要提供data参数") - - # 更新记录 - return query.update(**data).execute() - - elif query_type == "delete": - # 删除记录 - return query.delete().execute() - - elif query_type == "count": - # 计数 - return query.count() - - else: - raise ValueError(f"不支持的查询类型: {query_type}") - - except DoesNotExist: - # 记录不存在 - if query_type == "get" and single_result: - return None - return [] - - except Exception as e: - logger.error(f"{self.log_prefix} 数据库操作出错: {e}") - traceback.print_exc() - - # 根据查询类型返回合适的默认值 - if query_type == "get": - return None if single_result else [] - elif query_type in ["create", "update", "delete", "count"]: - 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( - self, model_class: Type[Model], data: Dict[str, Any], key_field: str = None, key_value: Any = None - ) -> Union[Dict[str, Any], None]: - """保存数据到数据库(创建或更新) - - 如果提供了key_field和key_value,会先尝试查找匹配的记录进行更新; - 如果没有找到匹配记录,或未提供key_field和key_value,则创建新记录。 - - Args: - model_class: Peewee模型类,如ActionRecords, Messages等 - data: 要保存的数据字典 - key_field: 用于查找现有记录的字段名,例如"action_id" - key_value: 用于查找现有记录的字段值 - - Returns: - Dict[str, Any]: 保存后的记录数据 - None: 如果操作失败 - - 示例: - # 创建或更新一条记录 - record = await self.db_save( - ActionRecords, - { - "action_id": "123", - "time": time.time(), - "action_name": "TestAction", - "action_done": True - }, - key_field="action_id", - key_value="123" - ) - """ - try: - # 如果提供了key_field和key_value,尝试更新现有记录 - if key_field and key_value is not None: - # 查找现有记录 - existing_records = list( - model_class.select().where(getattr(model_class, key_field) == key_value).limit(1) - ) - - if existing_records: - # 更新现有记录 - existing_record = existing_records[0] - for field, value in data.items(): - setattr(existing_record, field, value) - existing_record.save() - - # 返回更新后的记录 - updated_record = model_class.select().where(model_class.id == existing_record.id).dicts().get() - return updated_record - - # 如果没有找到现有记录或未提供key_field和key_value,创建新记录 - new_record = model_class.create(**data) - - # 返回创建的记录 - created_record = model_class.select().where(model_class.id == new_record.id).dicts().get() - return created_record - - except Exception as e: - logger.error(f"{self.log_prefix} 保存数据库记录出错: {e}") - traceback.print_exc() - return None - - async def db_get( - self, 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]: - """从数据库获取记录 - - 这是db_query方法的简化版本,专注于数据检索操作。 - - Args: - model_class: Peewee模型类 - filters: 过滤条件,字段名和值的字典 - order_by: 排序字段,前缀'-'表示降序,例如'-time'表示按时间降序 - limit: 结果数量限制,如果为1则返回单个记录而不是列表 - - Returns: - 如果limit=1,返回单个记录字典或None; - 否则返回记录字典列表或空列表。 - - 示例: - # 获取单个记录 - record = await self.db_get( - ActionRecords, - filters={"action_id": "123"}, - limit=1 - ) - - # 获取最近10条记录 - records = await self.db_get( - Messages, - filters={"chat_id": chat_stream.stream_id}, - order_by="-time", - limit=10 - ) - """ - try: - # 构建查询 + try: + if query_type not in ["get", "create", "update", "delete", "count"]: + raise ValueError("query_type must be 'get' or 'create' or 'update' or 'delete' or 'count'") + # 构建基本查询 + if query_type in ["get", "update", "delete", "count"]: query = model_class.select() # 应用过滤条件 @@ -354,12 +99,15 @@ class DatabaseAPI: for field, value in filters.items(): query = query.where(getattr(model_class, field) == value) + # 执行查询 + if query_type == "get": # 应用排序 if order_by: - if order_by.startswith("-"): - query = query.order_by(getattr(model_class, order_by[1:]).desc()) - else: - query = query.order_by(getattr(model_class, order_by)) + for field in order_by: + if field.startswith("-"): + query = query.order_by(getattr(model_class, field[1:]).desc()) + else: + query = query.order_by(getattr(model_class, field)) # 应用限制 if limit: @@ -369,11 +117,270 @@ class DatabaseAPI: results = list(query.dicts()) # 返回结果 - if limit == 1: + if single_result: return results[0] if results else None return results - except Exception as e: - logger.error(f"{self.log_prefix} 获取数据库记录出错: {e}") - traceback.print_exc() - return None if limit == 1 else [] + elif query_type == "create": + if not data: + raise ValueError("创建记录需要提供data参数") + + # 创建记录 + record = model_class.create(**data) + # 返回创建的记录 + return model_class.select().where(model_class.id == record.id).dicts().get() + + elif query_type == "update": + if not data: + raise ValueError("更新记录需要提供data参数") + + # 更新记录 + return query.update(**data).execute() + + elif query_type == "delete": + # 删除记录 + return query.delete().execute() + + elif query_type == "count": + # 计数 + return query.count() + + else: + raise ValueError(f"不支持的查询类型: {query_type}") + + except DoesNotExist: + # 记录不存在 + if query_type == "get" and single_result: + return None + return [] + + except Exception as e: + logger.error(f"[DatabaseAPI] 数据库操作出错: {e}") + traceback.print_exc() + + # 根据查询类型返回合适的默认值 + if query_type == "get": + return None if single_result else [] + elif query_type in ["create", "update", "delete", "count"]: + return None + return None + + +async def db_save( + model_class: Type[Model], data: Dict[str, Any], key_field: str = None, key_value: Any = None +) -> Union[Dict[str, Any], None]: + """保存数据到数据库(创建或更新) + + 如果提供了key_field和key_value,会先尝试查找匹配的记录进行更新; + 如果没有找到匹配记录,或未提供key_field和key_value,则创建新记录。 + + Args: + model_class: Peewee模型类,如ActionRecords, Messages等 + data: 要保存的数据字典 + key_field: 用于查找现有记录的字段名,例如"action_id" + key_value: 用于查找现有记录的字段值 + + Returns: + Dict[str, Any]: 保存后的记录数据 + None: 如果操作失败 + + 示例: + # 创建或更新一条记录 + record = await database_api.db_save( + ActionRecords, + { + "action_id": "123", + "time": time.time(), + "action_name": "TestAction", + "action_done": True + }, + key_field="action_id", + key_value="123" + ) + """ + try: + # 如果提供了key_field和key_value,尝试更新现有记录 + if key_field and key_value is not None: + # 查找现有记录 + existing_records = list( + model_class.select().where(getattr(model_class, key_field) == key_value).limit(1) + ) + + if existing_records: + # 更新现有记录 + existing_record = existing_records[0] + for field, value in data.items(): + setattr(existing_record, field, value) + existing_record.save() + + # 返回更新后的记录 + updated_record = model_class.select().where(model_class.id == existing_record.id).dicts().get() + return updated_record + + # 如果没有找到现有记录或未提供key_field和key_value,创建新记录 + new_record = model_class.create(**data) + + # 返回创建的记录 + created_record = model_class.select().where(model_class.id == new_record.id).dicts().get() + return created_record + + except Exception as e: + logger.error(f"[DatabaseAPI] 保存数据库记录出错: {e}") + traceback.print_exc() + return None + + +async def db_get( + 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]: + """从数据库获取记录 + + 这是db_query方法的简化版本,专注于数据检索操作。 + + Args: + model_class: Peewee模型类 + filters: 过滤条件,字段名和值的字典 + order_by: 排序字段,前缀'-'表示降序,例如'-time'表示按时间降序 + limit: 结果数量限制,如果为1则返回单个记录而不是列表 + + Returns: + 如果limit=1,返回单个记录字典或None; + 否则返回记录字典列表或空列表。 + + 示例: + # 获取单个记录 + record = await database_api.db_get( + ActionRecords, + filters={"action_id": "123"}, + limit=1 + ) + + # 获取最近10条记录 + records = await database_api.db_get( + Messages, + filters={"chat_id": chat_stream.stream_id}, + order_by="-time", + limit=10 + ) + """ + try: + # 构建查询 + query = model_class.select() + + # 应用过滤条件 + if filters: + for field, value in filters.items(): + query = query.where(getattr(model_class, field) == value) + + # 应用排序 + if order_by: + if order_by.startswith("-"): + query = query.order_by(getattr(model_class, order_by[1:]).desc()) + else: + query = query.order_by(getattr(model_class, order_by)) + + # 应用限制 + if limit: + query = query.limit(limit) + + # 执行查询 + results = list(query.dicts()) + + # 返回结果 + if limit == 1: + return results[0] if results else None + return results + + except Exception as e: + logger.error(f"[DatabaseAPI] 获取数据库记录出错: {e}") + traceback.print_exc() + 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 \ No newline at end of file diff --git a/src/plugin_system/apis/emoji_api.py b/src/plugin_system/apis/emoji_api.py new file mode 100644 index 000000000..5cf2d6d2b --- /dev/null +++ b/src/plugin_system/apis/emoji_api.py @@ -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 [] diff --git a/src/plugin_system/apis/generator_api.py b/src/plugin_system/apis/generator_api.py new file mode 100644 index 000000000..fdc29a06d --- /dev/null +++ b/src/plugin_system/apis/generator_api.py @@ -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, [] + + diff --git a/src/plugin_system/apis/hearflow_api.py b/src/plugin_system/apis/hearflow_api.py deleted file mode 100644 index 86739c887..000000000 --- a/src/plugin_system/apis/hearflow_api.py +++ /dev/null @@ -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 diff --git a/src/plugin_system/apis/independent_apis.py b/src/plugin_system/apis/independent_apis.py deleted file mode 100644 index d094b3889..000000000 --- a/src/plugin_system/apis/independent_apis.py +++ /dev/null @@ -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) diff --git a/src/plugin_system/apis/llm_api.py b/src/plugin_system/apis/llm_api.py index c4a9aeb92..1fb2f11a2 100644 --- a/src/plugin_system/apis/llm_api.py +++ b/src/plugin_system/apis/llm_api.py @@ -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 src.common.logger import get_logger from src.llm_models.utils_model import LLMRequest @@ -6,49 +15,51 @@ from src.config.config import global_config logger = get_logger("llm_api") -class LLMAPI: - """LLM API模块 +# ============================================================================= +# LLM模型API函数 +# ============================================================================= - 提供了与LLM模型交互的功能 +def get_available_models() -> Dict[str, Any]: + """获取所有可用的模型配置 + + Returns: + Dict[str, Any]: 模型配置字典,key为模型名称,value为模型配置 """ - - def get_available_models(self) -> Dict[str, Any]: - """获取所有可用的模型配置 - - Returns: - Dict[str, Any]: 模型配置字典,key为模型名称,value为模型配置 - """ + try: if not hasattr(global_config, "model"): - logger.error(f"{self.log_prefix} 无法获取模型列表:全局配置中未找到 model 配置") + logger.error("[LLMAPI] 无法获取模型列表:全局配置中未找到 model 配置") return {} models = global_config.model - return models + except Exception as e: + logger.error(f"[LLMAPI] 获取可用模型失败: {e}") + return {} - async def generate_with_model( - self, prompt: str, model_config: Dict[str, Any], request_type: str = "plugin.generate", **kwargs - ) -> Tuple[bool, str, str, str]: - """使用指定模型生成内容 - Args: - prompt: 提示词 - model_config: 模型配置(从 get_available_models 获取的模型配置) - request_type: 请求类型标识 - **kwargs: 其他模型特定参数,如temperature、max_tokens等 +async def generate_with_model( + prompt: str, model_config: Dict[str, Any], request_type: str = "plugin.generate", **kwargs +) -> Tuple[bool, str, str, str]: + """使用指定模型生成内容 - Returns: - Tuple[bool, str, str, str]: (是否成功, 生成的内容, 推理过程, 模型名称) - """ - try: - logger.info(f"{self.log_prefix} 使用模型生成内容,提示词: {prompt[:100]}...") + Args: + prompt: 提示词 + model_config: 模型配置(从 get_available_models 获取的模型配置) + request_type: 请求类型标识 + **kwargs: 其他模型特定参数,如temperature、max_tokens等 - llm_request = LLMRequest(model=model_config, request_type=request_type, **kwargs) + Returns: + Tuple[bool, str, str, str]: (是否成功, 生成的内容, 推理过程, 模型名称) + """ + try: + logger.info(f"[LLMAPI] 使用模型生成内容,提示词: {prompt[:100]}...") - response, (reasoning, model_name) = await llm_request.generate_response_async(prompt) - return True, response, reasoning, model_name + llm_request = LLMRequest(model=model_config, request_type=request_type, **kwargs) - except Exception as e: - error_msg = f"生成内容时出错: {str(e)}" - logger.error(f"{self.log_prefix} {error_msg}") - return False, error_msg, "", "" + response, (reasoning, model_name) = await llm_request.generate_response_async(prompt) + return True, response, reasoning, model_name + + except Exception as e: + error_msg = f"生成内容时出错: {str(e)}" + logger.error(f"[LLMAPI] {error_msg}") + return False, error_msg, "", "" diff --git a/src/plugin_system/apis/message_api.py b/src/plugin_system/apis/message_api.py index ab72915cb..106fa56bc 100644 --- a/src/plugin_system/apis/message_api.py +++ b/src/plugin_system/apis/message_api.py @@ -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 -from typing import List, Dict, Any -from src.common.logger import get_logger -from src.chat.focus_chat.hfc_utils import create_empty_anchor_message - -# 以下为类型注解需要 -from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager -from src.chat.focus_chat.info.obs_info import ObsInfo - -# 新增导入 -from src.chat.focus_chat.heartFC_sender import HeartFCSender -from src.chat.message_receive.message import MessageSending -from maim_message import Seg, UserInfo -from src.config.config import global_config - -logger = get_logger("message_api") +from src.chat.utils.chat_message_builder import ( + get_raw_msg_by_timestamp, + get_raw_msg_by_timestamp_with_chat, + get_raw_msg_by_timestamp_with_chat_inclusive, + get_raw_msg_by_timestamp_with_chat_users, + get_raw_msg_by_timestamp_random, + get_raw_msg_by_timestamp_with_users, + get_raw_msg_before_timestamp, + get_raw_msg_before_timestamp_with_chat, + get_raw_msg_before_timestamp_with_users, + num_new_messages_since, + num_new_messages_since_with_users, + build_readable_messages, + build_readable_messages_with_list, + 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]]: """ + 获取指定时间范围内的消息 + + Args: + start_time: 开始时间戳 + end_time: 结束时间戳 + limit: 限制返回的消息数量,0为不限制 + limit_mode: 当limit>0时生效,'earliest'表示获取最早的记录,'latest'表示获取最新的记录 + + Returns: + 消息列表 + """ + return get_raw_msg_by_timestamp(start_time, end_time, limit, limit_mode) - 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: - message_type: 消息类型,如"text"、"image"、"emoji"等 - content: 消息内容 - platform: 目标平台,如"qq" - target_id: 目标ID(群ID或用户ID) - is_group: 是否为群聊,True为群聊,False为私聊 - display_message: 显示消息(可选) +def get_messages_by_time_in_chat( + chat_id: str, start_time: float, end_time: float, limit: int = 0, limit_mode: str = "latest" +) -> List[Dict[str, Any]]: + """ + 获取指定聊天中指定时间范围内的消息 + + Args: + chat_id: 聊天ID + start_time: 开始时间戳 + end_time: 结束时间戳 + limit: 限制返回的消息数量,0为不限制 + limit_mode: 当limit>0时生效,'earliest'表示获取最早的记录,'latest'表示获取最新的记录 + + Returns: + 消息列表 + """ + return get_raw_msg_by_timestamp_with_chat(chat_id, start_time, end_time, limit, limit_mode) - Returns: - bool: 是否发送成功 - """ - try: - # 构建目标聊天流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 +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: + chat_id: 聊天ID + start_time: 开始时间戳(包含) + end_time: 结束时间戳(包含) + limit: 限制返回的消息数量,0为不限制 + limit_mode: 当limit>0时生效,'earliest'表示获取最早的记录,'latest'表示获取最新的记录 + + Returns: + 消息列表 + """ + return get_raw_msg_by_timestamp_with_chat_inclusive(chat_id, start_time, end_time, limit, limit_mode) - if not target_stream: - logger.error(f"{getattr(self, 'log_prefix', '')} 未找到用户ID为 {target_id} 的私聊流") - return False - # 创建HeartFCSender实例 - heart_fc_sender = HeartFCSender() +def get_messages_by_time_in_chat_for_users( + chat_id: str, + start_time: float, + end_time: float, + person_ids: list, + limit: int = 0, + limit_mode: str = "latest", +) -> List[Dict[str, Any]]: + """ + 获取指定聊天中指定用户在指定时间范围内的消息 + + Args: + chat_id: 聊天ID + start_time: 开始时间戳 + end_time: 结束时间戳 + person_ids: 用户ID列表 + limit: 限制返回的消息数量,0为不限制 + limit_mode: 当limit>0时生效,'earliest'表示获取最早的记录,'latest'表示获取最新的记录 + + Returns: + 消息列表 + """ + return get_raw_msg_by_timestamp_with_chat_users(chat_id, start_time, end_time, person_ids, limit, limit_mode) - # 生成消息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, - ) +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) - # 创建消息段 - message_segment = Seg(type=message_type, data=content) - # 创建空锚点消息(用于回复) - anchor_message = await create_empty_anchor_message(platform, target_stream.group_info, target_stream) +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) - # 构建发送消息对象 - 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 - ) +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) - 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 +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) - async def send_text_to_group(self, text: str, group_id: str, platform: str = "qq") -> bool: - """便捷方法:向指定群聊发送文本消息 - Args: - text: 要发送的文本内容 - group_id: 群聊ID - platform: 平台,默认为"qq" +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) - Returns: - bool: 是否发送成功 - """ - return await self.send_message_to_target( - 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_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) - Args: - text: 要发送的文本内容 - user_id: 用户ID - platform: 平台,默认为"qq" - Returns: - bool: 是否发送成功 - """ - return await self.send_message_to_target( - message_type="text", content=text, platform=platform, target_id=user_id, is_group=False - ) +# ============================================================================= +# 消息计数API函数 +# ============================================================================= - def get_chat_type(self) -> str: - """获取当前聊天类型 +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) - Returns: - str: 聊天类型 ("group" 或 "private") - """ - 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]]: - """获取最近的消息 +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) - Args: - count: 要获取的消息数量 - Returns: - List[Dict]: 消息列表,每个消息包含发送者、内容等信息 - """ - messages = [] - services = getattr(self, "_services", {}) - observations = services.get("observations", []) +# ============================================================================= +# 消息格式化API函数 +# ============================================================================= - 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) +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 + ) - return messages + +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) diff --git a/src/plugin_system/apis/person_api.py b/src/plugin_system/apis/person_api.py new file mode 100644 index 000000000..85ad8a70f --- /dev/null +++ b/src/plugin_system/apis/person_api.py @@ -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 "" diff --git a/src/plugin_system/apis/plugin_api.py b/src/plugin_system/apis/plugin_api.py deleted file mode 100644 index 951cafa2c..000000000 --- a/src/plugin_system/apis/plugin_api.py +++ /dev/null @@ -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", -] diff --git a/src/plugin_system/apis/send_api.py b/src/plugin_system/apis/send_api.py new file mode 100644 index 000000000..66734c7c7 --- /dev/null +++ b/src/plugin_system/apis/send_api.py @@ -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) \ No newline at end of file diff --git a/src/plugin_system/apis/stream_api.py b/src/plugin_system/apis/stream_api.py deleted file mode 100644 index 881a4bb1a..000000000 --- a/src/plugin_system/apis/stream_api.py +++ /dev/null @@ -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)}" diff --git a/src/plugin_system/apis/utils_api.py b/src/plugin_system/apis/utils_api.py index 61efec821..f29b6ee85 100644 --- a/src/plugin_system/apis/utils_api.py +++ b/src/plugin_system/apis/utils_api.py @@ -1,126 +1,165 @@ +"""工具类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 json import time +import inspect +import datetime +import uuid from typing import Any, Optional from src.common.logger import get_logger logger = get_logger("utils_api") -class UtilsAPI: - """工具类API模块 +# ============================================================================= +# 文件操作API函数 +# ============================================================================= - 提供了各种辅助功能 +def get_plugin_path(caller_frame=None) -> str: + """获取调用者插件的路径 + + Args: + caller_frame: 调用者的栈帧,默认为None(自动获取) + + Returns: + str: 插件目录的绝对路径 """ + try: + if caller_frame is None: + caller_frame = inspect.currentframe().f_back - def get_plugin_path(self) -> str: - """获取当前插件的路径 - - Returns: - str: 插件目录的绝对路径 - """ - import inspect - - plugin_module_path = inspect.getfile(self.__class__) + plugin_module_path = inspect.getfile(caller_frame) plugin_dir = os.path.dirname(plugin_module_path) 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: - """读取JSON文件 - Args: - file_path: 文件路径,可以是相对于插件目录的路径 - default: 如果文件不存在或读取失败时返回的默认值 +def read_json_file(file_path: str, default: Any = None) -> Any: + """读取JSON文件 - Returns: - Any: JSON数据或默认值 - """ - try: - # 如果是相对路径,则相对于插件目录 - if not os.path.isabs(file_path): - file_path = os.path.join(self.get_plugin_path(), file_path) + Args: + file_path: 文件路径,可以是相对于插件目录的路径 + default: 如果文件不存在或读取失败时返回的默认值 - if not os.path.exists(file_path): - logger.warning(f"{self.log_prefix} 文件不存在: {file_path}") - return default + Returns: + Any: JSON数据或默认值 + """ + try: + # 如果是相对路径,则相对于调用者的插件目录 + if not os.path.isabs(file_path): + caller_frame = inspect.currentframe().f_back + plugin_dir = get_plugin_path(caller_frame) + file_path = os.path.join(plugin_dir, file_path) - with open(file_path, "r", encoding="utf-8") as f: - return json.load(f) - except Exception as e: - logger.error(f"{self.log_prefix} 读取JSON文件出错: {e}") + if not os.path.exists(file_path): + logger.warning(f"[UtilsAPI] 文件不存在: {file_path}") return default - def write_json_file(self, file_path: str, data: Any, indent: int = 2) -> bool: - """写入JSON文件 + with open(file_path, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + logger.error(f"[UtilsAPI] 读取JSON文件出错: {e}") + return default - Args: - file_path: 文件路径,可以是相对于插件目录的路径 - data: 要写入的数据 - indent: JSON缩进 - Returns: - bool: 是否写入成功 - """ - try: - # 如果是相对路径,则相对于插件目录 - if not os.path.isabs(file_path): - file_path = os.path.join(self.get_plugin_path(), file_path) +def write_json_file(file_path: str, data: Any, indent: int = 2) -> bool: + """写入JSON文件 - # 确保目录存在 - os.makedirs(os.path.dirname(file_path), exist_ok=True) + Args: + file_path: 文件路径,可以是相对于插件目录的路径 + data: 要写入的数据 + indent: JSON缩进 - with open(file_path, "w", encoding="utf-8") as f: - json.dump(data, f, ensure_ascii=False, indent=indent) - return True - except Exception as e: - logger.error(f"{self.log_prefix} 写入JSON文件出错: {e}") - return False + Returns: + bool: 是否写入成功 + """ + try: + # 如果是相对路径,则相对于调用者的插件目录 + if not os.path.isabs(file_path): + caller_frame = inspect.currentframe().f_back + plugin_dir = get_plugin_path(caller_frame) + file_path = os.path.join(plugin_dir, file_path) - def get_timestamp(self) -> int: - """获取当前时间戳 + # 确保目录存在 + os.makedirs(os.path.dirname(file_path), exist_ok=True) - Returns: - int: 当前时间戳(秒) - """ - return int(time.time()) + with open(file_path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=indent) + return True + except Exception as e: + logger.error(f"[UtilsAPI] 写入JSON文件出错: {e}") + return False - def format_time(self, timestamp: Optional[int] = None, format_str: str = "%Y-%m-%d %H:%M:%S") -> str: - """格式化时间 - Args: - timestamp: 时间戳,如果为None则使用当前时间 - format_str: 时间格式字符串 +# ============================================================================= +# 时间相关API函数 +# ============================================================================= - Returns: - str: 格式化后的时间字符串 - """ - import datetime +def get_timestamp() -> int: + """获取当前时间戳 + Returns: + int: 当前时间戳(秒) + """ + return int(time.time()) + + +def format_time(timestamp: Optional[int] = None, format_str: str = "%Y-%m-%d %H:%M:%S") -> str: + """格式化时间 + + Args: + timestamp: 时间戳,如果为None则使用当前时间 + format_str: 时间格式字符串 + + Returns: + str: 格式化后的时间字符串 + """ + try: if timestamp is None: timestamp = time.time() 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: - """解析时间字符串为时间戳 - Args: - time_str: 时间字符串 - format_str: 时间格式字符串 +def parse_time(time_str: str, format_str: str = "%Y-%m-%d %H:%M:%S") -> int: + """解析时间字符串为时间戳 - Returns: - int: 时间戳(秒) - """ - import datetime + Args: + time_str: 时间字符串 + format_str: 时间格式字符串 + Returns: + int: 时间戳(秒) + """ + try: dt = datetime.datetime.strptime(time_str, format_str) return int(dt.timestamp()) + except Exception as e: + logger.error(f"[UtilsAPI] 解析时间失败: {e}") + return 0 - def generate_unique_id(self) -> str: - """生成唯一ID - Returns: - str: 唯一ID - """ - import uuid +# ============================================================================= +# 其他工具函数 +# ============================================================================= - return str(uuid.uuid4()) +def generate_unique_id() -> str: + """生成唯一ID + + Returns: + str: 唯一ID + """ + return str(uuid.uuid4()) diff --git a/src/plugin_system/base/base_action.py b/src/plugin_system/base/base_action.py index f12723a8c..c25893742 100644 --- a/src/plugin_system/base/base_action.py +++ b/src/plugin_system/base/base_action.py @@ -1,435 +1,469 @@ -from abc import ABC, abstractmethod -from typing import Tuple -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 - -logger = get_logger("base_action") - - -class BaseAction(ABC): - """Action组件基类 - - Action是插件的一种组件类型,用于处理聊天中的动作逻辑 - - 子类可以通过类属性定义激活条件,这些会在实例化时转换为实例属性: - - focus_activation_type: 专注模式激活类型 - - normal_activation_type: 普通模式激活类型 - - activation_keywords: 激活关键词列表 - - keyword_case_sensitive: 关键词是否区分大小写 - - mode_enable: 启用的聊天模式 - - parallel_action: 是否允许并行执行 - - random_activation_probability: 随机激活概率 - - llm_judge_prompt: LLM判断提示词 - """ - - def __init__( - self, - action_data: dict, - reasoning: str, - cycle_timers: dict, - thinking_id: str, - observations: list = None, - expressor=None, - replyer=None, - chat_stream=None, - log_prefix: str = "", - shutting_down: bool = False, - plugin_config: dict = None, - **kwargs, - ): - """初始化Action组件 - - Args: - action_data: 动作数据 - reasoning: 执行该动作的理由 - cycle_timers: 计时器字典 - thinking_id: 思考ID - observations: 观察列表 - expressor: 表达器对象 - replyer: 回复器对象 - chat_stream: 聊天流对象 - log_prefix: 日志前缀 - shutting_down: 是否正在关闭 - plugin_config: 插件配置字典 - **kwargs: 其他参数 - """ - self.action_data = action_data - self.reasoning = reasoning - self.cycle_timers = cycle_timers - self.thinking_id = thinking_id - self.log_prefix = log_prefix - self.shutting_down = shutting_down - - # 设置动作基本信息实例属性 - 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_parameters: dict = getattr(self.__class__, "action_parameters", {}).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.normal_activation_type: str = self._get_activation_type_value("normal_activation_type", "never") - 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.activation_keywords: list[str] = getattr(self.__class__, "activation_keywords", []).copy() - self.keyword_case_sensitive: bool = getattr(self.__class__, "keyword_case_sensitive", False) - self.mode_enable: str = self._get_mode_value("mode_enable", "all") - self.parallel_action: bool = getattr(self.__class__, "parallel_action", True) - 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) - - logger.debug(f"{self.log_prefix} Action组件初始化完成") - - def _get_activation_type_value(self, attr_name: str, default: str) -> str: - """获取激活类型的字符串值""" - attr = getattr(self.__class__, attr_name, None) - if attr is None: - return default - if hasattr(attr, "value"): - return attr.value - return str(attr) - - def _get_mode_value(self, attr_name: str, default: str) -> str: - """获取模式的字符串值""" - attr = getattr(self.__class__, attr_name, None) - if attr is None: - return default - if hasattr(attr, "value"): - return attr.value - return str(attr) - - async def send_text(self, content: str) -> bool: - """发送回复消息 - - Args: - content: 回复内容 - - Returns: - bool: 是否发送成功 - """ - 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_text_to_group( - text=content, group_id=str(chat_stream.group_info.group_id), platform=chat_stream.platform - ) - else: - # 私聊 - return await self.api.send_text_to_user( - 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: - """发送回复消息 - - Args: - text: 回复内容 - - Returns: - bool: 是否发送成功 - """ - 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=type, - content=text, - platform=chat_stream.platform, - target_id=str(chat_stream.group_info.group_id), - is_group=True, - typing=typing, - ) - else: - # 私聊 - return await self.api.send_message_to_target( - message_type=type, - content=text, - platform=chat_stream.platform, - target_id=str(chat_stream.user_info.user_id), - is_group=False, - typing=typing, - ) - - async def send_command(self, command_name: str, args: dict = None, display_message: str = None) -> bool: - """发送命令消息 - - 使用和send_text相同的方式通过MessageAPI发送命令 - - Args: - command_name: 命令名称 - args: 命令参数 - display_message: 显示消息 - - Returns: - bool: 是否发送成功 - """ - try: - # 构造命令数据 - command_data = {"name": command_name, "args": args or {}} - - # 使用send_message_to_target方法发送命令 - 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( - message_type="command", - content=command_data, - platform=chat_stream.platform, - target_id=str(chat_stream.group_info.group_id), - is_group=True, - display_message=display_message or f"执行命令: {command_name}", - ) - else: - # 私聊 - success = await self.api.send_message_to_target( - message_type="command", - content=command_data, - platform=chat_stream.platform, - target_id=str(chat_stream.user_info.user_id), - is_group=False, - display_message=display_message or f"执行命令: {command_name}", - ) - - if success: - logger.info(f"{self.log_prefix} 成功发送命令: {command_name}") - else: - logger.error(f"{self.log_prefix} 发送命令失败: {command_name}") - - return success - - except Exception as e: - logger.error(f"{self.log_prefix} 发送命令时出错: {e}") - 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 - def get_action_info(cls) -> "ActionInfo": - """从类属性生成ActionInfo - - 所有信息都从类属性中读取,确保一致性和完整性。 - Action类必须定义所有必要的类属性。 - - Returns: - ActionInfo: 生成的Action信息对象 - """ - - # 从类属性读取名称,如果没有定义则使用类名自动生成 - name = getattr(cls, "action_name", cls.__name__.lower().replace("action", "")) - - # 从类属性读取描述,如果没有定义则使用文档字符串的第一行 - description = getattr(cls, "action_description", None) - if description is None: - description = "Action动作" - - # 安全获取激活类型值 - def get_enum_value(attr_name, default): - attr = getattr(cls, attr_name, None) - if attr is None: - # 如果没有定义,返回默认的枚举值 - return getattr(ActionActivationType, default.upper(), ActionActivationType.NEVER) - return attr - - def get_mode_value(attr_name, default): - attr = getattr(cls, attr_name, None) - if attr is None: - return getattr(ChatMode, default.upper(), ChatMode.ALL) - return attr - - return ActionInfo( - name=name, - component_type=ComponentType.ACTION, - description=description, - focus_activation_type=get_enum_value("focus_activation_type", "never"), - normal_activation_type=get_enum_value("normal_activation_type", "never"), - activation_keywords=getattr(cls, "activation_keywords", []).copy(), - keyword_case_sensitive=getattr(cls, "keyword_case_sensitive", False), - mode_enable=get_mode_value("mode_enable", "all"), - parallel_action=getattr(cls, "parallel_action", True), - random_activation_probability=getattr(cls, "random_activation_probability", 0.0), - llm_judge_prompt=getattr(cls, "llm_judge_prompt", ""), - # 使用正确的字段名 - action_parameters=getattr(cls, "action_parameters", {}).copy(), - action_require=getattr(cls, "action_require", []).copy(), - associated_types=getattr(cls, "associated_types", []).copy(), - ) - - @abstractmethod - async def execute(self) -> Tuple[bool, str]: - """执行Action的抽象方法,子类必须实现 - - Returns: - Tuple[bool, str]: (是否执行成功, 回复文本) - """ - pass - - async def handle_action(self) -> Tuple[bool, str]: - """兼容旧系统的handle_action接口,委托给execute方法 - - 为了保持向后兼容性,旧系统的代码可能会调用handle_action方法。 - 此方法将调用委托给新的execute方法。 - - Returns: - Tuple[bool, str]: (是否执行成功, 回复文本) - """ - return await self.execute() +from abc import ABC, abstractmethod +from typing import Tuple, Optional +from src.common.logger import get_logger +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") + + +class BaseAction(ABC): + """Action组件基类 + + Action是插件的一种组件类型,用于处理聊天中的动作逻辑 + + 子类可以通过类属性定义激活条件,这些会在实例化时转换为实例属性: + - focus_activation_type: 专注模式激活类型 + - normal_activation_type: 普通模式激活类型 + - activation_keywords: 激活关键词列表 + - keyword_case_sensitive: 关键词是否区分大小写 + - mode_enable: 启用的聊天模式 + - parallel_action: 是否允许并行执行 + - random_activation_probability: 随机激活概率 + - llm_judge_prompt: LLM判断提示词 + """ + + def __init__( + self, + action_data: dict, + reasoning: str, + cycle_timers: dict, + thinking_id: str, + chat_stream=None, + log_prefix: str = "", + shutting_down: bool = False, + plugin_config: dict = None, + **kwargs, + ): + """初始化Action组件 + + Args: + action_data: 动作数据 + reasoning: 执行该动作的理由 + cycle_timers: 计时器字典 + thinking_id: 思考ID + observations: 观察列表 + expressor: 表达器对象 + replyer: 回复器对象 + chat_stream: 聊天流对象 + log_prefix: 日志前缀 + shutting_down: 是否正在关闭 + plugin_config: 插件配置字典 + **kwargs: 其他参数 + """ + self.action_data = action_data + self.reasoning = reasoning + self.cycle_timers = cycle_timers + self.thinking_id = thinking_id + self.log_prefix = log_prefix + 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_description: str = getattr(self, "action_description", self.__doc__ or "Action组件") + self.action_parameters: dict = getattr(self.__class__, "action_parameters", {}).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", "always") + 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.llm_judge_prompt: str = getattr(self.__class__, "llm_judge_prompt", "") + self.activation_keywords: list[str] = getattr(self.__class__, "activation_keywords", []).copy() + self.keyword_case_sensitive: bool = getattr(self.__class__, "keyword_case_sensitive", False) + self.mode_enable: str = self._get_mode_value("mode_enable", "all") + self.parallel_action: bool = getattr(self.__class__, "parallel_action", True) + self.associated_types: list[str] = getattr(self.__class__, "associated_types", []).copy() + + # ============================================================================= + # 便捷属性 - 直接在初始化时获取常用聊天信息(带类型注解) + # ============================================================================= + + + # 获取聊天流对象 + 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} 聊天信息: 类型={'群聊' if self.is_group else '私聊'}, 平台={self.platform}, 目标={self.target_id}") + + def _get_activation_type_value(self, attr_name: str, default: str) -> str: + """获取激活类型的字符串值""" + attr = getattr(self.__class__, attr_name, None) + if attr is None: + return default + if hasattr(attr, "value"): + return attr.value + return str(attr) + + def _get_mode_value(self, attr_name: str, default: str) -> str: + """获取模式的字符串值""" + attr = getattr(self.__class__, attr_name, None) + if attr is None: + return default + if hasattr(attr, "value"): + return attr.value + return str(attr) + + + async def wait_for_new_message(self, timeout: int = 1200) -> Tuple[bool, str]: + """等待新消息或超时 + + 在loop_start_time之后等待新消息,如果没有新消息且没有超时,就一直等待。 + 使用message_api检查self.chat_id对应的聊天中是否有新消息。 + + Args: + 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: + bool: 是否发送成功 + """ + 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.text_to_group( + text=content, group_id=self.target_id, platform=self.platform, reply_to=reply_to + ) + else: + return await send_api.text_to_user( + text=content, user_id=self.target_id, platform=self.platform, reply_to=reply_to + ) + + async def send_emoji(self, emoji_base64: str) -> bool: + """发送表情包 + + Args: + emoji_base64: 表情包的base64编码 + + 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.emoji_to_group(emoji_base64, self.target_id, self.platform) + else: + return await send_api.emoji_to_user(emoji_base64, self.target_id, self.platform) + + async def send_image(self, image_base64: str) -> bool: + """发送图片 + + Args: + image_base64: 图片的base64编码 + + 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 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发送命令 + + Args: + command_name: 命令名称 + args: 命令参数 + display_message: 显示消息 + + Returns: + bool: 是否发送成功 + """ + try: + # 构造命令数据 + command_data = {"name": command_name, "args": args or {}} + + if self.is_group: + # 群聊 + success = await send_api.command_to_group( + command=command_data, + group_id=str(self.group_id), + platform=self.platform, + storage_message=storage_message + ) + else: + # 私聊 + success = await send_api.command_to_user( + command=command_data, + user_id=str(self.user_id), + platform=self.platform, + storage_message=storage_message + ) + + if success: + logger.info(f"{self.log_prefix} 成功发送命令: {command_name}") + else: + logger.error(f"{self.log_prefix} 发送命令失败: {command_name}") + + return success + + except Exception as e: + logger.error(f"{self.log_prefix} 发送命令时出错: {e}") + return False + + @classmethod + def get_action_info(cls) -> "ActionInfo": + """从类属性生成ActionInfo + + 所有信息都从类属性中读取,确保一致性和完整性。 + Action类必须定义所有必要的类属性。 + + Returns: + ActionInfo: 生成的Action信息对象 + """ + + # 从类属性读取名称,如果没有定义则使用类名自动生成 + name = getattr(cls, "action_name", cls.__name__.lower().replace("action", "")) + + # 从类属性读取描述,如果没有定义则使用文档字符串的第一行 + description = getattr(cls, "action_description", None) + if description is None: + description = "Action动作" + + # 安全获取激活类型值 + def get_enum_value(attr_name, default): + attr = getattr(cls, attr_name, None) + if attr is None: + # 如果没有定义,返回默认的枚举值 + return getattr(ActionActivationType, default.upper(), ActionActivationType.NEVER) + return attr + + def get_mode_value(attr_name, default): + attr = getattr(cls, attr_name, None) + if attr is None: + return getattr(ChatMode, default.upper(), ChatMode.ALL) + return attr + + return ActionInfo( + name=name, + component_type=ComponentType.ACTION, + description=description, + focus_activation_type=get_enum_value("focus_activation_type", "always"), + normal_activation_type=get_enum_value("normal_activation_type", "always"), + activation_keywords=getattr(cls, "activation_keywords", []).copy(), + keyword_case_sensitive=getattr(cls, "keyword_case_sensitive", False), + mode_enable=get_mode_value("mode_enable", "all"), + parallel_action=getattr(cls, "parallel_action", True), + random_activation_probability=getattr(cls, "random_activation_probability", 0.0), + llm_judge_prompt=getattr(cls, "llm_judge_prompt", ""), + # 使用正确的字段名 + action_parameters=getattr(cls, "action_parameters", {}).copy(), + action_require=getattr(cls, "action_require", []).copy(), + associated_types=getattr(cls, "associated_types", []).copy(), + ) + + @abstractmethod + async def execute(self) -> Tuple[bool, str]: + """执行Action的抽象方法,子类必须实现 + + Returns: + Tuple[bool, str]: (是否执行成功, 回复文本) + """ + pass + + async def handle_action(self) -> Tuple[bool, str]: + """兼容旧系统的handle_action接口,委托给execute方法 + + 为了保持向后兼容性,旧系统的代码可能会调用handle_action方法。 + 此方法将调用委托给新的execute方法。 + + Returns: + Tuple[bool, str]: (是否执行成功, 回复文本) + """ + 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 diff --git a/src/plugin_system/base/base_command.py b/src/plugin_system/base/base_command.py index 3ee342922..63550b52b 100644 --- a/src/plugin_system/base/base_command.py +++ b/src/plugin_system/base/base_command.py @@ -1,9 +1,9 @@ from abc import ABC, abstractmethod from typing import Dict, Tuple, Optional, List 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.chat.message_receive.message import MessageRecv +from src.plugin_system.apis import send_api logger = get_logger("base_command") @@ -20,6 +20,9 @@ class BaseCommand(ABC): - intercept_message: 是否拦截消息处理(默认True拦截,False继续传递) """ + command_name: str = "" + command_description: str = "" + # 默认命令设置(子类可以覆盖) command_pattern: str = "" command_help: str = "" @@ -35,9 +38,7 @@ class BaseCommand(ABC): """ self.message = message self.matched_groups: Dict[str, str] = {} # 存储正则表达式匹配的命名组 - - # 创建API实例 - self.api = PluginAPI(chat_stream=message.chat_stream, log_prefix="[Command]", plugin_config=plugin_config) + self.plugin_config = plugin_config or {} # 直接存储插件配置字典 self.log_prefix = "[Command]" @@ -60,6 +61,31 @@ class BaseCommand(ABC): """ 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: """发送回复消息 @@ -71,13 +97,19 @@ class BaseCommand(ABC): 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: # 私聊 - 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( @@ -98,31 +130,30 @@ class BaseCommand(ABC): 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, content=content, - platform=chat_stream.platform, target_id=str(chat_stream.group_info.group_id), is_group=True, - display_message=display_message, + platform=chat_stream.platform, typing=typing, ) 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, content=content, - platform=chat_stream.platform, target_id=str(chat_stream.user_info.user_id), 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: """发送命令消息 - 使用和send_text相同的方式通过MessageAPI发送命令 - Args: command_name: 命令名称 args: 命令参数 @@ -135,29 +166,28 @@ class BaseCommand(ABC): # 构造命令数据 command_data = {"name": command_name, "args": args or {}} - # 使用send_message_to_target方法发送命令 + # 获取聊天流信息 chat_stream = self.message.chat_stream - command_content = command_data 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", - content=command_content, - platform=chat_stream.platform, + content=command_data, target_id=str(chat_stream.group_info.group_id), is_group=True, - display_message=display_message or f"执行命令: {command_name}", + platform=chat_stream.platform, ) 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", - content=command_content, - platform=chat_stream.platform, + content=command_data, target_id=str(chat_stream.user_info.user_id), is_group=False, - display_message=display_message or f"执行命令: {command_name}", + platform=chat_stream.platform, ) if success: @@ -172,7 +202,7 @@ class BaseCommand(ABC): return False @classmethod - def get_command_info(cls, name: str = None, description: str = None) -> "CommandInfo": + def get_command_info(cls) -> "CommandInfo": """从类属性生成CommandInfo Args: @@ -183,19 +213,10 @@ class BaseCommand(ABC): 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( - name=name, + name=cls.command_name, component_type=ComponentType.COMMAND, - description=description, + description=cls.command_description, command_pattern=cls.command_pattern, command_help=cls.command_help, command_examples=cls.command_examples.copy() if cls.command_examples else [], diff --git a/src/plugin_system/core/component_registry.py b/src/plugin_system/core/component_registry.py index fba04e8d5..47611817c 100644 --- a/src/plugin_system/core/component_registry.py +++ b/src/plugin_system/core/component_registry.py @@ -54,23 +54,43 @@ class ComponentRegistry: """ component_name = component_info.name 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 - # 注册到通用注册表 - self._components[component_name] = component_info - self._components_by_type[component_type][component_name] = component_info - self._component_classes[component_name] = component_class + # 注册到通用注册表(使用命名空间化的名称) + self._components[namespaced_name] = component_info + self._components_by_type[component_type][component_name] = component_info # 类型内部仍使用原名 + self._component_classes[namespaced_name] = component_class - # 根据组件类型进行特定注册 + # 根据组件类型进行特定注册(使用原始名称) if component_type == ComponentType.ACTION: self._register_action_component(component_info, component_class) elif component_type == ComponentType.COMMAND: 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 def _register_action_component(self, action_info: ActionInfo, action_class: Type): @@ -94,13 +114,103 @@ class ComponentRegistry: # === 组件查询方法 === - def get_component_info(self, component_name: str) -> Optional[ComponentInfo]: - """获取组件信息""" - return self._components.get(component_name) + 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) + + # 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) -> Optional[Type]: - """获取组件类""" - return self._component_classes.get(component_name) + 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) + + # 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]: """获取指定类型的所有组件""" @@ -123,7 +233,7 @@ class ComponentRegistry: def get_action_info(self, action_name: str) -> Optional[ActionInfo]: """获取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 # === Command特定查询方法 === @@ -138,7 +248,7 @@ class ComponentRegistry: def get_command_info(self, command_name: str) -> Optional[CommandInfo]: """获取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 def find_command_by_text(self, text: str) -> Optional[tuple[Type, dict, bool, str]]: @@ -150,7 +260,9 @@ class ComponentRegistry: Returns: Optional[tuple[Type, dict, bool, str]]: (命令类, 匹配的命名组, 是否拦截消息, 插件名) 或 None """ + for pattern, command_class in self._command_patterns.items(): + match = pattern.match(text) if match: command_name = None @@ -159,17 +271,18 @@ class ComponentRegistry: if cls == command_class: command_name = name break - + # 检查命令是否启用 if command_name: command_info = self.get_command_info(command_name) - if command_info and command_info.enabled: - return ( - command_class, - match.groupdict(), - command_info.intercept_message, - command_info.plugin_name, - ) + if command_info: + if command_info.enabled: + return ( + command_class, + match.groupdict(), + command_info.intercept_message, + command_info.plugin_name, + ) return None # === 插件管理方法 === @@ -227,26 +340,51 @@ class ComponentRegistry: # === 状态管理方法 === - def enable_component(self, component_name: str) -> bool: - """启用组件""" - if component_name in self._components: - self._components[component_name].enabled = True + def enable_component(self, component_name: str, component_type: ComponentType = None) -> bool: + """启用组件,支持命名空间解析""" + # 首先尝试找到正确的命名空间化名称 + 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,更新默认动作集 - component_info = self._components[component_name] if isinstance(component_info, ActionInfo): self._default_actions[component_name] = component_info.description - logger.debug(f"已启用组件: {component_name}") + logger.debug(f"已启用组件: {component_name} -> {namespaced_name}") return True return False - def disable_component(self, component_name: str) -> bool: - """禁用组件""" - if component_name in self._components: - self._components[component_name].enabled = False + def disable_component(self, component_name: str, component_type: ComponentType = None) -> bool: + """禁用组件,支持命名空间解析""" + # 首先尝试找到正确的命名空间化名称 + 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,从默认动作集中移除 if component_name in self._default_actions: del self._default_actions[component_name] - logger.debug(f"已禁用组件: {component_name}") + logger.debug(f"已禁用组件: {component_name} -> {namespaced_name}") return True return False diff --git a/src/plugins/built_in/core_actions/1config.toml b/src/plugins/built_in/core_actions/1config.toml deleted file mode 100644 index 7eaf98f29..000000000 --- a/src/plugins/built_in/core_actions/1config.toml +++ /dev/null @@ -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 = "" \ No newline at end of file diff --git a/src/plugins/built_in/core_actions/plugin.py b/src/plugins/built_in/core_actions/plugin.py index e854a0b2a..5c1b048c9 100644 --- a/src/plugins/built_in/core_actions/plugin.py +++ b/src/plugins/built_in/core_actions/plugin.py @@ -5,18 +5,18 @@ 这是系统的内置插件,提供基础的聊天交互功能 """ -import re -from typing import List, Tuple, Type, Optional +import time +from typing import List, Tuple, Type # 导入新插件系统 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.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") @@ -35,11 +35,11 @@ class ReplyAction(BaseAction): # 动作基本信息 action_name = "reply" - action_description = "参与聊天回复,处理文本和表情的发送" + action_description = "参与聊天回复,发送文本进行表达" # 动作参数定义 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}") + start_time = self.action_data.get("loop_start_time", time.time()) + try: - # 获取聊天观察 - chatting_observation = self._get_chatting_observation() - if not chatting_observation: - return False, "未找到聊天观察" - - # 处理回复目标 - anchor_message = await self._resolve_reply_target(chatting_observation) - - # 获取回复器服务 - 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, + + success, reply_set = await generator_api.generate_reply( + chat_stream=self.chat_stream, action_data=self.action_data, - anchor_message=anchor_message, - reasoning=self.reasoning, - thinking_id=self.thinking_id, + platform=self.platform, + chat_id=self.chat_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_prompt_display=reply_text, action_done=True, - thinking_id=self.thinking_id, - action_data=self.action_data, ) # 重置NoReplyAction的连续计数器 @@ -97,47 +109,6 @@ class ReplyAction(BaseAction): 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 - - 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): @@ -178,30 +149,26 @@ class NoReplyAction(BaseAction): 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}秒)") # 等待新消息或达到时间上限 - result = await self.api.wait_for_new_message(timeout) + result = await self.wait_for_new_message(timeout) # 如果有新消息或者超时,都不重置计数器,因为可能还会继续no_reply return result except Exception as e: logger.error(f"{self.log_prefix} 不回复动作执行失败: {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 + return False, f"不回复动作执行失败: {e}" @classmethod def reset_consecutive_count(cls): @@ -248,56 +215,33 @@ class EmojiAction(BaseAction): logger.info(f"{self.log_prefix} 决定发送表情") try: - # 创建空锚点消息 - anchor_message = await self._create_anchor_message() - if not anchor_message: - return False, "无法创建锚点消息" - - # 获取回复器服务 - replyer = self.api.get_service("replyer") - if not replyer: - logger.error(f"{self.log_prefix} 未找到回复器服务") - return False, "回复器服务不可用" - - # 执行表情处理 - success, reply_set = await replyer.deal_emoji( - cycle_timers=self.cycle_timers, - action_data=self.action_data, - anchor_message=anchor_message, - thinking_id=self.thinking_id, - ) - - # 构建回复文本 - reply_text = self._build_reply_text(reply_set) + # 1. 根据描述选择表情包 + description = self.action_data.get("description", "") + emoji_result = await emoji_api.get_by_description(description) + + if not emoji_result: + logger.warning(f"{self.log_prefix} 未找到匹配描述 '{description}' 的表情包") + return False, f"未找到匹配 '{description}' 的表情包" + + emoji_base64, emoji_description, matched_emotion = emoji_result + logger.info(f"{self.log_prefix} 找到表情包: {emoji_description}, 匹配情感: {matched_emotion}") + + # 使用BaseAction的便捷方法发送表情包 + success = await self.send_emoji(emoji_base64) + + if not success: + logger.error(f"{self.log_prefix} 表情包发送失败") + return False, "表情包发送失败" # 重置NoReplyAction的连续计数器 NoReplyAction.reset_consecutive_count() - return success, reply_text + return True, f"发送表情包: {emoji_description}" except Exception as e: logger.error(f"{self.log_prefix} 表情动作执行失败: {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): """切换到专注聊天动作 - 从普通模式切换到专注模式""" @@ -314,6 +258,7 @@ class ChangeToFocusChatAction(BaseAction): # 动作参数定义 action_parameters = {} + apex = 111 # 动作使用场景 action_require = [ "你想要进入专注聊天模式", @@ -437,8 +382,6 @@ class CoreActionsPlugin(BasePlugin): "enable_emoji": 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_ping_command": ConfigField(type=bool, default=True, description="是否启用'/ping'测试命令"), - "enable_log_command": ConfigField(type=bool, default=True, description="是否启用'/log'日志命令"), }, "no_reply": { "waiting_timeout": ConfigField( @@ -482,73 +425,137 @@ class CoreActionsPlugin(BasePlugin): components.append((ExitFocusChatAction.get_action_info(), ExitFocusChatAction)) if self.get_config("components.enable_change_to_focus", True): components.append((ChangeToFocusChatAction.get_action_info(), ChangeToFocusChatAction)) - if self.get_config("components.enable_ping_command", True): - 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) - ) + # components.append((DeepReplyAction.get_action_info(), DeepReplyAction)) return components -# ===== 示例Command组件 ===== -class PingCommand(BaseCommand): - """Ping命令 - 测试响应,拦截消息处理""" - - command_pattern = r"^/ping(\s+(?P.+))?$" - 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+(?Pdebug|info|warn|error))?$" - command_help = "记录当前消息到日志 - 不拦截后续处理" - command_examples = ["/log", "/log info", "/log debug"] - intercept_message = False # 不拦截消息,继续后续处理 +# # 激活设置 +# focus_activation_type = ActionActivationType.ALWAYS +# normal_activation_type = ActionActivationType.NEVER +# mode_enable = ChatMode.FOCUS +# parallel_action = False - async def execute(self) -> Tuple[bool, Optional[str]]: - """执行日志命令""" - try: - level = self.matched_groups.get("level", "info") - user_nickname = self.message.message_info.user_info.user_nickname - content = self.message.processed_plain_text +# # 动作基本信息 +# action_name = "deep_reply" +# action_description = "参与聊天回复,关注某个话题,对聊天内容进行深度思考,给出回复" - log_message = f"[{level.upper()}] 用户 {user_nickname}: {content}" +# # 动作参数定义 +# action_parameters = { +# "topic": "想要思考的话题" +# } - # 根据级别记录日志 - if level == "debug": - logger.debug(log_message) - elif level == "warn": - logger.warning(log_message) - elif level == "error": - logger.error(log_message) - else: - logger.info(log_message) +# # 动作使用场景 +# action_require = ["有些问题需要深度思考", "某个问题可能涉及多个方面", "某个问题涉及专业领域或者需要专业知识","这个问题讨论的很激烈,需要深度思考"] - # 不发送回复,让消息继续处理 - return True, f"已记录到{level}级别日志" +# # 关联类型 +# associated_types = ["text"] - except Exception as e: - logger.error(f"Log命令执行失败: {e}") - return False, f"执行失败: {str(e)}" +# async def execute(self) -> Tuple[bool, str]: +# """执行回复动作""" +# 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 \ No newline at end of file diff --git a/src/plugins/built_in/mute_plugin/plugin.py b/src/plugins/built_in/mute_plugin/plugin.py index 90822fb9c..3c2036897 100644 --- a/src/plugins/built_in/mute_plugin/plugin.py +++ b/src/plugins/built_in/mute_plugin/plugin.py @@ -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.config_types import ConfigField from src.common.logger import get_logger +# 导入配置API(可选的简便方法) +from src.plugin_system.apis import person_api, generator_api logger = get_logger("mute_plugin") @@ -110,8 +112,8 @@ class MuteAction(BaseAction): return False, error_msg # 获取时长限制配置 - min_duration = self.api.get_config("mute.min_duration", 60) - max_duration = self.api.get_config("mute.max_duration", 2592000) + min_duration = self.get_config("mute.min_duration", 60) + max_duration = self.get_config("mute.max_duration", 2592000) # 验证时长格式并转换 try: @@ -133,72 +135,65 @@ class MuteAction(BaseAction): except (ValueError, TypeError): error_msg = f"禁言时长格式无效: {duration}" logger.error(f"{self.log_prefix} {error_msg}") - await self.send_text("禁言时长必须是数字哦~") + # await self.send_text("禁言时长必须是数字哦~") return False, error_msg # 获取用户ID - try: - platform, user_id = await self.api.get_user_id_by_person_name(target) - 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 - + person_id = person_api.get_person_id_by_name(target) + user_id = await person_api.get_person_value(person_id,"user_id") if not user_id: error_msg = f"未找到用户 {target} 的ID" await self.send_text(f"找不到 {target} 这个人呢~") 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}秒" # 获取模板化消息 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( command_name="GROUP_BAN", args={"qq_id": str(user_id), "duration": str(duration_int)}, - display_message="发送禁言命令", + storage_message=False ) if success: 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_prompt_display=f"尝试禁言了用户 {target},时长 {time_str},原因:{reason}", 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}" else: error_msg = "发送禁言命令失败" logger.error(f"{self.log_prefix} {error_msg}") + await self.send_text("执行禁言动作失败") return False, error_msg 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}", - ], + templates = self.get_config( + "mute.templates" ) template = random.choice(templates) @@ -258,8 +253,8 @@ class MuteCommand(BaseCommand): return False, "参数不完整" # 获取时长限制配置 - min_duration = self.api.get_config("mute.min_duration", 60) - max_duration = self.api.get_config("mute.max_duration", 2592000) + min_duration = self.get_config("mute.min_duration", 60) + max_duration = self.get_config("mute.max_duration", 2592000) # 验证时长 try: @@ -281,19 +276,16 @@ class MuteCommand(BaseCommand): return False, "时长格式错误" # 获取用户ID - try: - platform, user_id = await self.api.get_user_id_by_person_name(target) - except Exception as e: - logger.error(f"{self.log_prefix} 查找用户ID时出错: {e}") - await self.send_text("❌ 查找用户信息时出现问题") - return False, str(e) - + person_id = person_api.get_person_id_by_name(target) + user_id = person_api.get_person_value(person_id, "user_id") if not user_id: + error_msg = f"未找到用户 {target} 的ID" 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}秒" 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: """获取模板化的禁言消息""" - templates = self.api.get_config( - "mute.templates", - [ - "✅ 已禁言 {target} {duration},理由:{reason}", - "🔇 对 {target} 执行禁言 {duration},因为{reason}", - "⛔ 禁言 {target} {duration},原因:{reason}", - ], - ) + templates = self.get_config("mute.templates") template = random.choice(templates) return template.format(target=target, duration=duration_str, reason=reason) diff --git a/src/plugins/built_in/take_picture_plugin/plugin.py b/src/plugins/built_in/take_picture_plugin/plugin.py deleted file mode 100644 index d5ab16bcc..000000000 --- a/src/plugins/built_in/take_picture_plugin/plugin.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/plugins/built_in/tts_plugin/plugin.py b/src/plugins/built_in/tts_plugin/plugin.py index 34eedfa21..bce8b48f4 100644 --- a/src/plugins/built_in/tts_plugin/plugin.py +++ b/src/plugins/built_in/tts_plugin/plugin.py @@ -57,7 +57,7 @@ class TTSAction(BaseAction): try: # 发送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)}") return True, "TTS动作执行成功" diff --git a/src/plugins/built_in/vtb_plugin/plugin.py b/src/plugins/built_in/vtb_plugin/plugin.py index c290687b9..25e5cc9bf 100644 --- a/src/plugins/built_in/vtb_plugin/plugin.py +++ b/src/plugins/built_in/vtb_plugin/plugin.py @@ -62,7 +62,7 @@ class VTBAction(BaseAction): try: # 发送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}") return True, "VTB动作执行成功"