diff --git a/src/chat/focus_chat/heartFC_chat.py b/src/chat/focus_chat/heartFC_chat.py index 9dee93d6e..4509826b3 100644 --- a/src/chat/focus_chat/heartFC_chat.py +++ b/src/chat/focus_chat/heartFC_chat.py @@ -159,6 +159,13 @@ class HeartFChatting: for name, (observation_class, param_name) in OBSERVATION_CLASSES.items(): try: + # 检查是否需要跳过WorkingMemoryObservation + if name == "WorkingMemoryObservation": + # 如果工作记忆处理器被禁用,则跳过WorkingMemoryObservation + if not global_config.focus_chat_processor.working_memory_processor: + logger.debug(f"{self.log_prefix} 工作记忆处理器已禁用,跳过注册观察器 {name}") + continue + # 根据参数名使用正确的参数 kwargs = {param_name: self.stream_id} observation = observation_class(**kwargs) diff --git a/src/chat/focus_chat/working_memory/working_memory.py b/src/chat/focus_chat/working_memory/working_memory.py index 496fdc37e..190dc9362 100644 --- a/src/chat/focus_chat/working_memory/working_memory.py +++ b/src/chat/focus_chat/working_memory/working_memory.py @@ -2,6 +2,7 @@ from typing import List, Any, Optional import asyncio from src.common.logger_manager import get_logger from src.chat.focus_chat.working_memory.memory_manager import MemoryManager, MemoryItem +from src.config.config import global_config logger = get_logger(__name__) @@ -33,8 +34,11 @@ class WorkingMemory: # 衰减任务 self.decay_task = None - # 启动自动衰减任务 - self._start_auto_decay() + # 只有在工作记忆处理器启用时才启动自动衰减任务 + if global_config.focus_chat_processor.working_memory_processor: + self._start_auto_decay() + else: + logger.debug(f"工作记忆处理器已禁用,跳过启动自动衰减任务 (chat_id: {chat_id})") def _start_auto_decay(self): """启动自动衰减任务""" diff --git a/src/plugin_system/README.md b/src/plugin_system/README.md deleted file mode 100644 index b8e943892..000000000 --- a/src/plugin_system/README.md +++ /dev/null @@ -1,169 +0,0 @@ -# MaiBot 插件系统 - 重构版 - -## 目录结构说明 - -经过重构,插件系统现在采用清晰的**系统核心**与**插件内容**分离的架构: - -``` -src/ -├── plugin_system/ # 🔧 系统核心 - 插件框架本身 -│ ├── __init__.py # 统一导出接口 -│ ├── core/ # 核心管理 -│ │ ├── plugin_manager.py -│ │ ├── component_registry.py -│ │ └── __init__.py -│ ├── apis/ # API接口 -│ │ ├── plugin_api.py # 统一API聚合 -│ │ ├── message_api.py -│ │ ├── llm_api.py -│ │ ├── database_api.py -│ │ ├── config_api.py -│ │ ├── utils_api.py -│ │ ├── stream_api.py -│ │ ├── hearflow_api.py -│ │ └── __init__.py -│ ├── base/ # 基础类 -│ │ ├── base_plugin.py -│ │ ├── base_action.py -│ │ ├── base_command.py -│ │ ├── component_types.py -│ │ └── __init__.py -│ └── registry/ # 注册相关(预留) -└── plugins/ # 🔌 插件内容 - 具体的插件实现 - ├── built_in/ # 内置插件 - │ ├── system_actions/ # 系统内置Action - │ └── system_commands/# 系统内置Command - └── examples/ # 示例插件 - └── simple_plugin/ - ├── plugin.py - └── config.toml -``` - -## 架构优势 - -### 1. 职责清晰 -- **`src/plugin_system/`** - 系统提供的框架、API和基础设施 -- **`src/plugins/`** - 用户开发或使用的具体插件 - -### 2. 导入简化 -```python -# 统一导入接口 -from src.plugin_system import ( - BasePlugin, register_plugin, BaseAction, BaseCommand, - ActionInfo, CommandInfo, PluginAPI -) -``` - -### 3. 模块化设计 -- 各个子模块都有清晰的职责和接口 -- 支持按需导入特定功能 -- 便于维护和扩展 - -## 快速开始 - -### 创建简单插件 - -```python -from src.plugin_system import BasePlugin, register_plugin, BaseAction, ActionInfo - -class MyAction(BaseAction): - async def execute(self): - return True, "Hello from my plugin!" - -@register_plugin -class MyPlugin(BasePlugin): - plugin_name = "my_plugin" - plugin_description = "我的第一个插件" - - def get_plugin_components(self): - return [( - ActionInfo(name="my_action", description="我的动作"), - MyAction - )] -``` - -### 使用系统API - -```python -class MyAction(BaseAction): - async def execute(self): - # 发送消息 - await self.api.send_text_to_group( - self.api.get_service("chat_stream"), - "Hello World!" - ) - - # 数据库操作 - data = await self.api.db_get("table", "key") - - # LLM调用 - response = await self.api.llm_text_request("你好") - - return True, response -``` - -## 兼容性迁移 - -### 现有Action迁移 -```python -# 旧方式 -from src.chat.actions.base_action import BaseAction, register_action - -# 新方式 -from src.plugin_system import BaseAction, register_plugin -from src.plugin_system.base.component_types import ActionInfo - -# 将Action封装到Plugin中 -@register_plugin -class MyActionPlugin(BasePlugin): - plugin_name = "my_action_plugin" - - def get_plugin_components(self): - return [(ActionInfo(...), MyAction)] -``` - -### 现有Command迁移 -```python -# 旧方式 -from src.chat.command.command_handler import BaseCommand, register_command - -# 新方式 -from src.plugin_system import BaseCommand, register_plugin -from src.plugin_system.base.component_types import CommandInfo - -# 将Command封装到Plugin中 -@register_plugin -class MyCommandPlugin(BasePlugin): - plugin_name = "my_command_plugin" - - def get_plugin_components(self): - return [(CommandInfo(...), MyCommand)] -``` - -## 扩展指南 - -### 添加新的组件类型 -1. 在 `component_types.py` 中定义新的组件类型 -2. 在 `component_registry.py` 中添加对应的注册逻辑 -3. 创建对应的基类 - -### 添加新的API -1. 在 `apis/` 目录下创建新的API模块 -2. 在 `plugin_api.py` 中集成新API -3. 更新 `__init__.py` 导出接口 - -## 最佳实践 - -1. **单一插件包含相关组件** - 一个插件可以包含多个相关的Action和Command -2. **使用配置文件** - 通过TOML配置文件管理插件行为 -3. **合理的组件命名** - 使用描述性的组件名称 -4. **充分的错误处理** - 在组件中妥善处理异常 -5. **详细的文档** - 为插件和组件编写清晰的文档 - -## 内置插件规划 - -- **系统核心插件** - 将现有的内置Action/Command迁移为系统插件 -- **工具插件** - 常用的工具和实用功能 -- **示例插件** - 帮助开发者学习的示例代码 - -这个重构保持了向后兼容性,同时提供了更清晰、更易维护的架构。 \ No newline at end of file diff --git a/src/plugin_system/apis/API使用指南.md b/src/plugin_system/apis/API使用指南.md deleted file mode 100644 index c34f7d6da..000000000 --- a/src/plugin_system/apis/API使用指南.md +++ /dev/null @@ -1,172 +0,0 @@ -# API使用指南 - -插件系统提供了多种API访问方式,根据使用场景选择合适的API类。 - -## 📊 API分类 - -### 🔗 ActionAPI - 需要Action依赖 -**适用场景**:在Action组件中使用,需要访问聊天上下文 -```python -from src.plugin_system.apis import ActionAPI - -class MyAction(BaseAction): - async def execute(self): - # Action已内置ActionAPI,可以直接使用 - await self.api.send_message("text", "Hello") - await self.api.store_action_info(action_prompt_display="执行了动作") -``` - -**包含功能**: -- ✅ 发送消息(需要chat_stream、expressor等) -- ✅ 数据库操作(需要thinking_id、action_data等) - -### 🔧 IndependentAPI - 独立功能 -**适用场景**:在Command组件中使用,或需要独立工具功能 -```python -from src.plugin_system.apis import IndependentAPI - -class MyCommand(BaseCommand): - async def execute(self): - # 创建独立API实例 - api = IndependentAPI(log_prefix="[MyCommand]") - - # 使用独立功能 - models = api.get_available_models() - config = api.get_global_config("some_key") - timestamp = api.get_timestamp() -``` - -**包含功能**: -- ✅ LLM模型调用 -- ✅ 配置读取 -- ✅ 工具函数(时间、文件、ID生成等) -- ✅ 聊天流查询 -- ✅ 心流状态控制 - -### ⚡ StaticAPI - 静态访问 -**适用场景**:简单工具调用,不需要实例化 -```python -from src.plugin_system.apis import StaticAPI - -# 直接调用静态方法 -models = StaticAPI.get_available_models() -config = StaticAPI.get_global_config("bot.nickname") -timestamp = StaticAPI.get_timestamp() -unique_id = StaticAPI.generate_unique_id() - -# 异步方法 -result = await StaticAPI.generate_with_model(prompt, model_config) -chat_stream = StaticAPI.get_chat_stream_by_group_id("123456") -``` - -## 🎯 使用建议 - -### Action组件开发 -```python -class MyAction(BaseAction): - # 激活条件直接在类中定义 - focus_activation_type = ActionActivationType.KEYWORD - activation_keywords = ["测试"] - - async def execute(self): - # 使用内置的ActionAPI - success = await self.api.send_message("text", "处理中...") - - # 存储执行记录 - await self.api.store_action_info( - action_prompt_display="执行了测试动作" - ) - - return True, "完成" -``` - -### Command组件开发 -```python -class MyCommand(BaseCommand): - # 命令模式直接在类中定义 - command_pattern = r"^/test\s+(?P\w+)$" - command_help = "测试命令" - - async def execute(self): - # 使用独立API - api = IndependentAPI(log_prefix="[TestCommand]") - - # 获取配置 - max_length = api.get_global_config("test.max_length", 100) - - # 生成内容(如果需要) - if api.get_available_models(): - models = api.get_available_models() - first_model = list(models.values())[0] - - success, response, _, _ = await api.generate_with_model( - "生成测试回复", first_model - ) - - if success: - await self.send_reply(response) -``` - -### 独立工具使用 -```python -# 不在插件环境中的独立使用 -from src.plugin_system.apis import StaticAPI - -def some_utility_function(): - # 获取配置 - bot_name = StaticAPI.get_global_config("bot.nickname", "Bot") - - # 生成ID - request_id = StaticAPI.generate_unique_id() - - # 格式化时间 - current_time = StaticAPI.format_time() - - return f"{bot_name}_{request_id}_{current_time}" -``` - -## 🔄 迁移指南 - -### 从原PluginAPI迁移 - -**原来的用法**: -```python -# 原来需要导入完整PluginAPI -from src.plugin_system.apis import PluginAPI - -api = PluginAPI(chat_stream=..., expressor=...) -await api.send_message("text", "Hello") -config = api.get_global_config("key") -``` - -**新的用法**: -```python -# 方式1:继续使用原PluginAPI(不变) -from src.plugin_system.apis import PluginAPI - -# 方式2:使用分类API(推荐) -from src.plugin_system.apis import ActionAPI, IndependentAPI - -# Action相关功能 -action_api = ActionAPI(chat_stream=..., expressor=...) -await action_api.send_message("text", "Hello") - -# 独立功能 -config = IndependentAPI().get_global_config("key") -# 或者 -config = StaticAPI.get_global_config("key") -``` - -## 📋 API对照表 - -| 功能类别 | 原PluginAPI | ActionAPI | IndependentAPI | StaticAPI | -|---------|-------------|-----------|----------------|-----------| -| 发送消息 | ✅ | ✅ | ❌ | ❌ | -| 数据库操作 | ✅ | ✅ | ❌ | ❌ | -| LLM调用 | ✅ | ❌ | ✅ | ✅ | -| 配置读取 | ✅ | ❌ | ✅ | ✅ | -| 工具函数 | ✅ | ❌ | ✅ | ✅ | -| 聊天流查询 | ✅ | ❌ | ✅ | ✅ | -| 心流控制 | ✅ | ❌ | ✅ | ✅ | - -这样的分类让插件开发者可以更明确地知道需要什么样的API,避免不必要的依赖注入。 \ No newline at end of file diff --git a/src/plugin_system/base/base_action.py b/src/plugin_system/base/base_action.py index b5d841728..120ab7873 100644 --- a/src/plugin_system/base/base_action.py +++ b/src/plugin_system/base/base_action.py @@ -166,14 +166,13 @@ class BaseAction(ABC): if not chat_stream: logger.error(f"{self.log_prefix} 没有可用的聊天流发送命令") return False - - command_content = str(command_data) + if chat_stream.group_info: # 群聊 success = await self.api.send_message_to_target( message_type="command", - content=command_content, + content=command_data, platform=chat_stream.platform, target_id=str(chat_stream.group_info.group_id), is_group=True, @@ -183,7 +182,7 @@ class BaseAction(ABC): # 私聊 success = await self.api.send_message_to_target( message_type="command", - content=command_content, + content=command_data, platform=chat_stream.platform, target_id=str(chat_stream.user_info.user_id), is_group=False, @@ -213,7 +212,7 @@ class BaseAction(ABC): """ try: from src.chat.heart_flow.observation.chatting_observation import ChattingObservation - from src.chat.message_receive.message import create_empty_anchor_message + from src.chat.focus_chat.hfc_utils import create_empty_anchor_message # 获取服务 expressor = self.api.get_service("expressor") @@ -281,7 +280,7 @@ class BaseAction(ABC): """ try: from src.chat.heart_flow.observation.chatting_observation import ChattingObservation - from src.chat.message_receive.message import create_empty_anchor_message + from src.chat.focus_chat.hfc_utils import create_empty_anchor_message # 获取服务 replyer = self.api.get_service("replyer") diff --git a/src/plugin_system/base/base_command.py b/src/plugin_system/base/base_command.py index 251b8ce8b..42dd5328a 100644 --- a/src/plugin_system/base/base_command.py +++ b/src/plugin_system/base/base_command.py @@ -102,7 +102,7 @@ class BaseCommand(ABC): # 使用send_message_to_target方法发送命令 chat_stream = self.message.chat_stream - command_content = str(command_data) + command_content = command_data if chat_stream.group_info: # 群聊 diff --git a/src/plugin_system/base/base_plugin.py b/src/plugin_system/base/base_plugin.py index bafe471d7..99c4335bb 100644 --- a/src/plugin_system/base/base_plugin.py +++ b/src/plugin_system/base/base_plugin.py @@ -167,16 +167,26 @@ class BasePlugin(ABC): return True def get_config(self, key: str, default: Any = None) -> Any: - """获取插件配置值 + """获取插件配置值,支持嵌套键访问 Args: - key: 配置键名 + key: 配置键名,支持嵌套访问如 "section.subsection.key" default: 默认值 Returns: Any: 配置值或默认值 """ - return self.config.get(key, default) + # 支持嵌套键访问 + keys = key.split('.') + current = self.config + + for k in keys: + if isinstance(current, dict) and k in current: + current = current[k] + else: + return default + + return current def register_plugin(cls): diff --git a/src/plugins/built_in/doubao_pic/actions/__init__.py b/src/plugins/built_in/doubao_pic/actions/__init__.py deleted file mode 100644 index 249d25223..000000000 --- a/src/plugins/built_in/doubao_pic/actions/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""测试插件动作模块""" - -# 导入所有动作模块以确保装饰器被执行 -from . import pic_action # noqa diff --git a/src/plugins/built_in/doubao_pic/actions/generate_pic_config.py b/src/plugins/built_in/doubao_pic/actions/generate_pic_config.py deleted file mode 100644 index d9f689783..000000000 --- a/src/plugins/built_in/doubao_pic/actions/generate_pic_config.py +++ /dev/null @@ -1,122 +0,0 @@ -import os -import toml -from src.common.logger_manager import get_logger - -logger = get_logger("pic_config") - -CONFIG_CONTENT = """\ -# 火山方舟 API 的基础 URL -base_url = "https://ark.cn-beijing.volces.com/api/v3" -# 用于图片生成的API密钥 -volcano_generate_api_key = "YOUR_VOLCANO_GENERATE_API_KEY_HERE" -# 默认图片生成模型 -default_model = "doubao-seedream-3-0-t2i-250415" -# 默认图片尺寸 -default_size = "1024x1024" - - -# 是否默认开启水印 -default_watermark = true -# 默认引导强度 -default_guidance_scale = 2.5 -# 默认随机种子 -default_seed = 42 - -# 缓存设置 -cache_enabled = true -cache_max_size = 10 - -# 更多插件特定配置可以在此添加... -# custom_parameter = "some_value" -""" - -# 默认配置字典,用于验证和修复 -DEFAULT_CONFIG = { - "base_url": "https://ark.cn-beijing.volces.com/api/v3", - "volcano_generate_api_key": "YOUR_VOLCANO_GENERATE_API_KEY_HERE", - "default_model": "doubao-seedream-3-0-t2i-250415", - "default_size": "1024x1024", - "default_watermark": True, - "default_guidance_scale": 2.5, - "default_seed": 42, - "cache_enabled": True, - "cache_max_size": 10, -} - - -def validate_and_fix_config(config_path: str) -> bool: - """验证并修复配置文件""" - try: - with open(config_path, "r", encoding="utf-8") as f: - config = toml.load(f) - - # 检查缺失的配置项 - missing_keys = [] - fixed = False - - for key, default_value in DEFAULT_CONFIG.items(): - if key not in config: - missing_keys.append(key) - config[key] = default_value - fixed = True - logger.info(f"添加缺失的配置项: {key} = {default_value}") - - # 验证配置值的类型和范围 - if isinstance(config.get("default_guidance_scale"), (int, float)): - if not 0.1 <= config["default_guidance_scale"] <= 20.0: - config["default_guidance_scale"] = 2.5 - fixed = True - logger.info("修复无效的 default_guidance_scale 值") - - if isinstance(config.get("default_seed"), (int, float)): - config["default_seed"] = int(config["default_seed"]) - else: - config["default_seed"] = 42 - fixed = True - logger.info("修复无效的 default_seed 值") - - if config.get("cache_max_size") and not isinstance(config["cache_max_size"], int): - config["cache_max_size"] = 10 - fixed = True - logger.info("修复无效的 cache_max_size 值") - - # 如果有修复,写回文件 - if fixed: - # 创建备份 - backup_path = config_path + ".backup" - if os.path.exists(config_path): - os.rename(config_path, backup_path) - logger.info(f"已创建配置备份: {backup_path}") - - # 写入修复后的配置 - with open(config_path, "w", encoding="utf-8") as f: - toml.dump(config, f) - logger.info(f"配置文件已修复: {config_path}") - - return True - - except Exception as e: - logger.error(f"验证配置文件时出错: {e}") - return False - - -def generate_config(): - # 获取当前脚本所在的目录 - current_dir = os.path.dirname(os.path.abspath(__file__)) - config_file_path = os.path.join(current_dir, "pic_action_config.toml") - - if not os.path.exists(config_file_path): - try: - with open(config_file_path, "w", encoding="utf-8") as f: - f.write(CONFIG_CONTENT) - logger.info(f"配置文件已生成: {config_file_path}") - logger.info("请记得编辑该文件,填入您的火山引擎API 密钥。") - except IOError as e: - logger.error(f"错误:无法写入配置文件 {config_file_path}。原因: {e}") - else: - # 验证并修复现有配置 - validate_and_fix_config(config_file_path) - - -if __name__ == "__main__": - generate_config() diff --git a/src/plugins/built_in/doubao_pic/actions/pic_action_config.toml.backup b/src/plugins/built_in/doubao_pic/actions/pic_action_config.toml.backup deleted file mode 100644 index f0ca91ab3..000000000 --- a/src/plugins/built_in/doubao_pic/actions/pic_action_config.toml.backup +++ /dev/null @@ -1,19 +0,0 @@ -# 火山方舟 API 的基础 URL -base_url = "https://ark.cn-beijing.volces.com/api/v3" -# 用于图片生成的API密钥 -volcano_generate_api_key = "YOUR_VOLCANO_GENERATE_API_KEY_HERE" -# 默认图片生成模型 -default_model = "doubao-seedream-3-0-t2i-250415" -# 默认图片尺寸 -default_size = "1024x1024" - - -# 是否默认开启水印 -default_watermark = true -# 默认引导强度 -default_guidance_scale = 2.5 -# 默认随机种子 -default_seed = 42 - -# 更多插件特定配置可以在此添加... -# custom_parameter = "some_value" diff --git a/src/plugins/built_in/doubao_pic/__init__.py b/src/plugins/built_in/doubao_pic_plugin/__init__.py similarity index 87% rename from src/plugins/built_in/doubao_pic/__init__.py rename to src/plugins/built_in/doubao_pic_plugin/__init__.py index 90745b78f..8c1c55abb 100644 --- a/src/plugins/built_in/doubao_pic/__init__.py +++ b/src/plugins/built_in/doubao_pic_plugin/__init__.py @@ -4,7 +4,7 @@ 这是一个测试插件,用于测试图片发送功能 """ -"""豆包图片生成插件 +"""豆包图片生成插件包 这是一个基于火山引擎豆包模型的AI图片生成插件。 @@ -21,7 +21,7 @@ - 将文字描述转换为视觉图像 - 创意图片和艺术作品生成 -配置文件:src/plugins/doubao_pic/actions/pic_action_config.toml +配置文件:config.toml 配置要求: 1. 设置火山引擎API密钥 (volcano_generate_api_key) @@ -30,3 +30,7 @@ 注意:需要有效的火山引擎API访问权限才能正常使用。 """ + +from .plugin import DoubaoImagePlugin + +__all__ = ["DoubaoImagePlugin"] diff --git a/src/plugins/built_in/doubao_pic/actions/pic_action_config.toml b/src/plugins/built_in/doubao_pic_plugin/config.toml similarity index 52% rename from src/plugins/built_in/doubao_pic/actions/pic_action_config.toml rename to src/plugins/built_in/doubao_pic_plugin/config.toml index 26bb8aa39..f5f8a7e3d 100644 --- a/src/plugins/built_in/doubao_pic/actions/pic_action_config.toml +++ b/src/plugins/built_in/doubao_pic_plugin/config.toml @@ -1,9 +1,20 @@ +# 豆包图片生成插件配置文件 + +# API配置 base_url = "https://ark.cn-beijing.volces.com/api/v3" -volcano_generate_api_key = "YOUR_VOLCANO_GENERATE_API_KEY_HERE" +volcano_generate_api_key = "9481fe36-8db7-4353-b53d-eae8c74b6b96" + +# 生成参数配置 default_model = "doubao-seedream-3-0-t2i-250415" default_size = "1024x1024" default_watermark = true default_guidance_scale = 2.5 default_seed = 42 + +# 缓存配置 cache_enabled = true cache_max_size = 10 + +# 组件启用配置 +[components] +enable_image_generation = true \ No newline at end of file diff --git a/src/plugins/built_in/doubao_pic/actions/pic_action.py b/src/plugins/built_in/doubao_pic_plugin/plugin.py similarity index 58% rename from src/plugins/built_in/doubao_pic/actions/pic_action.py rename to src/plugins/built_in/doubao_pic_plugin/plugin.py index 193eeed73..e6f0bc76c 100644 --- a/src/plugins/built_in/doubao_pic/actions/pic_action.py +++ b/src/plugins/built_in/doubao_pic_plugin/plugin.py @@ -1,51 +1,56 @@ +""" +豆包图片生成插件 + +基于火山引擎豆包模型的AI图片生成插件。 + +功能特性: +- 智能LLM判定:根据聊天内容智能判断是否需要生成图片 +- 高质量图片生成:使用豆包Seed Dream模型生成图片 +- 结果缓存:避免重复生成相同内容的图片 +- 配置验证:自动验证和修复配置文件 +- 参数验证:完整的输入参数验证和错误处理 +- 多尺寸支持:支持多种图片尺寸生成 + +包含组件: +- 图片生成Action - 根据描述使用火山引擎API生成图片 +""" + import asyncio import json import urllib.request import urllib.error -import base64 # 新增:用于Base64编码 -import traceback # 新增:用于打印堆栈跟踪 -from typing import Tuple -from src.chat.actions.plugin_action import PluginAction, register_action -from src.chat.actions.base_action import ActionActivationType, ChatMode +import base64 +import traceback +import random +from typing import List, Tuple, Type, Optional + +# 导入新插件系统 +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.component_types import ComponentInfo, ActionActivationType, ChatMode from src.common.logger_manager import get_logger -from .generate_pic_config import generate_config -logger = get_logger("pic_action") - -# 当此模块被加载时,尝试生成配置文件(如果它不存在) -# 注意:在某些插件加载机制下,这可能会在每次机器人启动或插件重载时执行 -# 考虑是否需要更复杂的逻辑来决定何时运行 (例如,仅在首次安装时) -generate_config() +logger = get_logger("doubao_pic_plugin") -@register_action -class PicAction(PluginAction): - """根据描述使用火山引擎HTTP API生成图片的动作处理类""" +# ===== Action组件 ===== - action_name = "pic_action" - action_description = ( - "可以根据特定的描述,生成并发送一张图片,如果没提供描述,就根据聊天内容生成,你可以立刻画好,不用等待" - ) - action_parameters = { - "description": "图片描述,输入你想要生成并发送的图片的描述,必填", - "size": "图片尺寸,例如 '1024x1024' (可选, 默认从配置或 '1024x1024')", - } - action_require = [ - "当有人让你画东西时使用,你可以立刻画好,不用等待", - "当有人要求你生成并发送一张图片时使用", - "当有人让你画一张图时使用", - ] - enable_plugin = False - action_config_file_name = "pic_action_config.toml" - - # 激活类型设置 +class DoubaoImageGenerationAction(BaseAction): + """豆包图片生成Action - 根据描述使用火山引擎API生成图片""" + + # Action基本信息 + action_name = "doubao_image_generation" + action_description = "可以根据特定的描述,生成并发送一张图片,如果没提供描述,就根据聊天内容生成,你可以立刻画好,不用等待" + + # 激活设置 focus_activation_type = ActionActivationType.LLM_JUDGE # Focus模式使用LLM判定,精确理解需求 - normal_activation_type = ActionActivationType.KEYWORD # Normal模式使用关键词激活,快速响应 - + normal_activation_type = ActionActivationType.KEYWORD # Normal模式使用关键词激活,快速响应 + # 关键词设置(用于Normal模式) activation_keywords = ["画", "绘制", "生成图片", "画图", "draw", "paint", "图片生成"] keyword_case_sensitive = False - + # LLM判定提示词(用于Focus模式) llm_judge_prompt = """ 判定是否需要使用图片生成动作的条件: @@ -67,77 +72,45 @@ class PicAction(PluginAction): 4. 技术讨论中提到绘图概念但无生成需求 5. 用户明确表示不需要图片时 """ - - # Random激活概率(备用) - random_activation_probability = 0.15 # 适中概率,图片生成比较有趣 - + + mode_enable = ChatMode.ALL + parallel_action = True + + # Action参数定义 + action_parameters = { + "description": "图片描述,输入你想要生成并发送的图片的描述,必填", + "size": "图片尺寸,例如 '1024x1024' (可选, 默认从配置或 '1024x1024')", + } + + # Action使用场景 + action_require = [ + "当有人让你画东西时使用,你可以立刻画好,不用等待", + "当有人要求你生成并发送一张图片时使用", + "当有人让你画一张图时使用", + ] + # 简单的请求缓存,避免短时间内重复请求 _request_cache = {} _cache_max_size = 10 - # 模式启用设置 - 图片生成在所有模式下可用 - mode_enable = ChatMode.ALL - - # 并行执行设置 - 图片生成可以与回复并行执行,不覆盖回复内容 - parallel_action = False - - @classmethod - def _get_cache_key(cls, description: str, model: str, size: str) -> str: - """生成缓存键""" - return f"{description[:100]}|{model}|{size}" # 限制描述长度避免键过长 - - @classmethod - def _cleanup_cache(cls): - """清理缓存,保持大小在限制内""" - if len(cls._request_cache) > cls._cache_max_size: - # 简单的FIFO策略,移除最旧的条目 - keys_to_remove = list(cls._request_cache.keys())[: -cls._cache_max_size // 2] - for key in keys_to_remove: - del cls._request_cache[key] - - def __init__( - self, - action_data: dict, - reasoning: str, - cycle_timers: dict, - thinking_id: str, - global_config: dict = None, - **kwargs, - ): - super().__init__(action_data, reasoning, cycle_timers, thinking_id, global_config, **kwargs) - - logger.info(f"{self.log_prefix} 开始绘图!原因是:{self.reasoning}") - - http_base_url = self.config.get("base_url") - http_api_key = self.config.get("volcano_generate_api_key") - - if not (http_base_url and http_api_key): - logger.error( - f"{self.log_prefix} PicAction初始化, 但HTTP配置 (base_url 或 volcano_generate_api_key) 缺失. HTTP图片生成将失败." - ) - else: - logger.info(f"{self.log_prefix} HTTP方式初始化完成. Base URL: {http_base_url}, API Key已配置.") - - # _restore_env_vars 方法不再需要,已移除 - - async def process(self) -> Tuple[bool, str]: - """处理图片生成动作(通过HTTP API)""" - logger.info(f"{self.log_prefix} 执行 pic_action (HTTP): {self.reasoning}") - + async def execute(self) -> Tuple[bool, Optional[str]]: + """执行图片生成动作""" + logger.info(f"{self.log_prefix} 执行豆包图片生成动作") + # 配置验证 - http_base_url = self.config.get("base_url") - http_api_key = self.config.get("volcano_generate_api_key") + http_base_url = self.api.get_config("base_url") + http_api_key = self.api.get_config("volcano_generate_api_key") if not (http_base_url and http_api_key): error_msg = "抱歉,图片生成功能所需的HTTP配置(如API地址或密钥)不完整,无法提供服务。" - await self.send_message_by_expressor(error_msg) + await self.send_reply(error_msg) logger.error(f"{self.log_prefix} HTTP调用配置缺失: base_url 或 volcano_generate_api_key.") return False, "HTTP配置不完整" # API密钥验证 - if http_api_key == "YOUR_VOLCANO_GENERATE_API_KEY_HERE": + if http_api_key == "YOUR_DOUBAO_API_KEY_HERE": error_msg = "图片生成功能尚未配置,请设置正确的API密钥。" - await self.send_message_by_expressor(error_msg) + await self.send_reply(error_msg) logger.error(f"{self.log_prefix} API密钥未配置") return False, "API密钥未配置" @@ -145,7 +118,7 @@ class PicAction(PluginAction): description = self.action_data.get("description") if not description or not description.strip(): logger.warning(f"{self.log_prefix} 图片描述为空,无法生成图片。") - await self.send_message_by_expressor("你需要告诉我想要画什么样的图片哦~ 比如说'画一只可爱的小猫'") + await self.send_reply("你需要告诉我想要画什么样的图片哦~ 比如说'画一只可爱的小猫'") return False, "图片描述为空" # 清理和验证描述 @@ -155,8 +128,8 @@ class PicAction(PluginAction): logger.info(f"{self.log_prefix} 图片描述过长,已截断") # 获取配置 - default_model = self.config.get("default_model", "doubao-seedream-3-0-t2i-250415") - image_size = self.action_data.get("size", self.config.get("default_size", "1024x1024")) + default_model = self.api.get_config("default_model", "doubao-seedream-3-0-t2i-250415") + image_size = self.action_data.get("size", self.api.get_config("default_size", "1024x1024")) # 验证图片尺寸格式 if not self._validate_image_size(image_size): @@ -168,58 +141,23 @@ class PicAction(PluginAction): if cache_key in self._request_cache: cached_result = self._request_cache[cache_key] logger.info(f"{self.log_prefix} 使用缓存的图片结果") - await self.send_message_by_expressor("我之前画过类似的图片,用之前的结果~") - + await self.send_reply("我之前画过类似的图片,用之前的结果~") + # 直接发送缓存的结果 - send_success = await self.send_message(type="image", data=cached_result) + send_success = await self._send_image(cached_result) if send_success: - await self.send_message_by_expressor("图片表情已发送!") - return True, "图片表情已发送(缓存)" + await self.send_reply("图片已发送!") + return True, "图片已发送(缓存)" else: # 缓存失败,清除这个缓存项并继续正常流程 del self._request_cache[cache_key] - # guidance_scale 现在完全由配置文件控制 - guidance_scale_input = self.config.get("default_guidance_scale", 2.5) # 默认2.5 - guidance_scale_val = 2.5 # Fallback default - try: - guidance_scale_val = float(guidance_scale_input) - except (ValueError, TypeError): - logger.warning( - f"{self.log_prefix} 配置文件中的 default_guidance_scale 值 '{guidance_scale_input}' 无效 (应为浮点数),使用默认值 2.5。" - ) - guidance_scale_val = 2.5 + # 获取其他配置参数 + guidance_scale_val = self._get_guidance_scale() + seed_val = self._get_seed() + watermark_val = self._get_watermark() - # Seed parameter - ensure it's always an integer - seed_config_value = self.config.get("default_seed") - seed_val = 42 # Default seed if not configured or invalid - if seed_config_value is not None: - try: - seed_val = int(seed_config_value) - except (ValueError, TypeError): - logger.warning( - f"{self.log_prefix} 配置文件中的 default_seed ('{seed_config_value}') 无效,将使用默认种子 42。" - ) - # seed_val is already 42 - else: - logger.info( - f"{self.log_prefix} 未在配置中找到 default_seed,将使用默认种子 42。建议在配置文件中添加 default_seed。" - ) - # seed_val is already 42 - - # Watermark 现在完全由配置文件控制 - effective_watermark_source = self.config.get("default_watermark", True) # 默认True - if isinstance(effective_watermark_source, bool): - watermark_val = effective_watermark_source - elif isinstance(effective_watermark_source, str): - watermark_val = effective_watermark_source.lower() == "true" - else: - logger.warning( - f"{self.log_prefix} 配置文件中的 default_watermark 值 '{effective_watermark_source}' 无效 (应为布尔值或 'true'/'false'),使用默认值 True。" - ) - watermark_val = True - - await self.send_message_by_expressor( + await self.send_reply( f"收到!正在为您生成关于 '{description}' 的图片,请稍候...(模型: {default_model}, 尺寸: {image_size})" ) @@ -253,17 +191,17 @@ class PicAction(PluginAction): if encode_success: base64_image_string = encode_result - send_success = await self.send_message(type="image", data=base64_image_string) + send_success = await self._send_image(base64_image_string) if send_success: # 缓存成功的结果 self._request_cache[cache_key] = base64_image_string self._cleanup_cache() - - await self.send_message_by_expressor("图片表情已发送!") - return True, "图片表情已发送" + + await self.send_message_by_expressor("图片已发送!") + return True, "图片已发送" else: - await self.send_message_by_expressor("图片已处理为Base64,但作为表情发送失败了。") - return False, "图片表情发送失败 (Base64)" + await self.send_message_by_expressor("图片已处理为Base64,但发送失败了。") + return False, "图片发送失败 (Base64)" else: await self.send_message_by_expressor(f"获取到图片URL,但在处理图片时失败了:{encode_result}") return False, f"图片处理失败(Base64): {encode_result}" @@ -272,6 +210,90 @@ class PicAction(PluginAction): await self.send_message_by_expressor(f"哎呀,生成图片时遇到问题:{error_message}") return False, f"图片生成失败: {error_message}" + def _get_guidance_scale(self) -> float: + """获取guidance_scale配置值""" + guidance_scale_input = self.api.get_config("default_guidance_scale", 2.5) + try: + return float(guidance_scale_input) + except (ValueError, TypeError): + logger.warning(f"{self.log_prefix} default_guidance_scale 值无效,使用默认值 2.5") + return 2.5 + + def _get_seed(self) -> int: + """获取seed配置值""" + seed_config_value = self.api.get_config("default_seed") + if seed_config_value is not None: + try: + return int(seed_config_value) + except (ValueError, TypeError): + logger.warning(f"{self.log_prefix} default_seed 值无效,使用默认值 42") + return 42 + + def _get_watermark(self) -> bool: + """获取watermark配置值""" + watermark_source = self.api.get_config("default_watermark", True) + if isinstance(watermark_source, bool): + return watermark_source + elif isinstance(watermark_source, str): + return watermark_source.lower() == "true" + else: + logger.warning(f"{self.log_prefix} default_watermark 值无效,使用默认值 True") + return True + + 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[:100]}|{model}|{size}" + + @classmethod + def _cleanup_cache(cls): + """清理缓存,保持大小在限制内""" + if len(cls._request_cache) > cls._cache_max_size: + keys_to_remove = list(cls._request_cache.keys())[:-cls._cache_max_size//2] + for key in keys_to_remove: + del cls._request_cache[key] + + def _validate_image_size(self, image_size: str) -> bool: + """验证图片尺寸格式""" + try: + width, height = map(int, image_size.split('x')) + return 100 <= width <= 10000 and 100 <= height <= 10000 + except (ValueError, TypeError): + return False + def _download_and_encode_base64(self, image_url: str) -> Tuple[bool, str]: """下载图片并将其编码为Base64字符串""" logger.info(f"{self.log_prefix} (B64) 下载并编码图片: {image_url[:70]}...") @@ -286,16 +308,17 @@ class PicAction(PluginAction): error_msg = f"下载图片失败 (状态: {response.status})" logger.error(f"{self.log_prefix} (B64) {error_msg} URL: {image_url}") return False, error_msg - except Exception as e: # Catches all exceptions from urlopen, b64encode, etc. + except Exception as e: logger.error(f"{self.log_prefix} (B64) 下载或编码时错误: {e!r}", exc_info=True) traceback.print_exc() return False, f"下载或编码图片时发生错误: {str(e)[:100]}" def _make_http_image_request( - self, prompt: str, model: str, size: str, seed: int | None, guidance_scale: float, watermark: bool + self, prompt: str, model: str, size: str, seed: int, guidance_scale: float, watermark: bool ) -> Tuple[bool, str]: - base_url = self.config.get("base_url") - generate_api_key = self.config.get("volcano_generate_api_key") + """发送HTTP请求生成图片""" + base_url = self.api.get_config("base_url") + generate_api_key = self.api.get_config("volcano_generate_api_key") endpoint = f"{base_url.rstrip('/')}/images/generations" @@ -306,11 +329,9 @@ class PicAction(PluginAction): "size": size, "guidance_scale": guidance_scale, "watermark": watermark, - "seed": seed, # seed is now always an int from process() + "seed": seed, "api-key": generate_api_key, } - # if seed is not None: # No longer needed, seed is always an int - # payload_dict["seed"] = seed data = json.dumps(payload_dict).encode("utf-8") headers = { @@ -320,12 +341,6 @@ class PicAction(PluginAction): } logger.info(f"{self.log_prefix} (HTTP) 发起图片请求: {model}, Prompt: {prompt[:30]}... To: {endpoint}") - logger.debug( - f"{self.log_prefix} (HTTP) Request Headers: {{...Authorization: Bearer {generate_api_key[:10]}...}}" - ) - logger.debug( - f"{self.log_prefix} (HTTP) Request Body (api-key omitted): {json.dumps({k: v for k, v in payload_dict.items() if k != 'api-key'})}" - ) req = urllib.request.Request(endpoint, data=data, headers=headers, method="POST") @@ -353,24 +368,48 @@ class PicAction(PluginAction): logger.info(f"{self.log_prefix} (HTTP) 图片生成成功,URL: {image_url[:70]}...") return True, image_url else: - logger.error( - f"{self.log_prefix} (HTTP) API成功但无图片URL. 响应预览: {response_body_str[:300]}..." - ) + logger.error(f"{self.log_prefix} (HTTP) API成功但无图片URL") return False, "图片生成API响应成功但未找到图片URL" else: - logger.error( - f"{self.log_prefix} (HTTP) API请求失败. 状态: {response.status}. 正文: {response_body_str[:300]}..." - ) + logger.error(f"{self.log_prefix} (HTTP) API请求失败. 状态: {response.status}") return False, f"图片API请求失败(状态码 {response.status})" except Exception as e: logger.error(f"{self.log_prefix} (HTTP) 图片生成时意外错误: {e!r}", exc_info=True) traceback.print_exc() return False, f"图片生成HTTP请求时发生意外错误: {str(e)[:100]}" - def _validate_image_size(self, image_size: str) -> bool: - """验证图片尺寸格式""" - try: - width, height = map(int, image_size.split("x")) - return 100 <= width <= 10000 and 100 <= height <= 10000 - except (ValueError, TypeError): - return False + +# ===== 插件主类 ===== + +@register_plugin +class DoubaoImagePlugin(BasePlugin): + """豆包图片生成插件 + + 基于火山引擎豆包模型的AI图片生成插件: + - 图片生成Action:根据描述使用火山引擎API生成图片 + """ + + # 插件基本信息 + plugin_name = "doubao_pic_plugin" + plugin_description = "基于火山引擎豆包模型的AI图片生成插件" + plugin_version = "2.0.0" + plugin_author = "MaiBot开发团队" + enable_plugin = True + config_file_name = "config.toml" + + def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: + """返回插件包含的组件列表""" + + # 从配置获取组件启用状态 + enable_image_generation = self.get_config("components.enable_image_generation", True) + + components = [] + + # 添加图片生成Action + if enable_image_generation: + components.append(( + DoubaoImageGenerationAction.get_action_info(), + DoubaoImageGenerationAction + )) + + return components \ No newline at end of file diff --git a/src/plugins/built_in/mute_plugin/config.toml b/src/plugins/built_in/mute_plugin/config.toml index 4e8ccf440..a9bccdaa3 100644 --- a/src/plugins/built_in/mute_plugin/config.toml +++ b/src/plugins/built_in/mute_plugin/config.toml @@ -9,7 +9,7 @@ description = "群聊禁言管理插件,提供智能禁言功能" # 组件启用控制 [components] enable_smart_mute = true # 启用智能禁言Action -enable_mute_command = true # 启用禁言命令Command +enable_mute_command = false # 启用禁言命令Command # 禁言配置 [mute] @@ -29,9 +29,9 @@ templates = [ "好的,禁言 {target} {duration},理由:{reason}", "收到,对 {target} 执行禁言 {duration},因为{reason}", "明白了,禁言 {target} {duration},原因是{reason}", - "✅ 已禁言 {target} {duration},理由:{reason}", - "🔇 对 {target} 执行禁言 {duration},因为{reason}", - "⛔ 禁言 {target} {duration},原因:{reason}" + "哇哈哈哈哈哈,已禁言 {target} {duration},理由:{reason}", + "哎呦我去,对 {target} 执行禁言 {duration},因为{reason}", + "{target},你完蛋了,我要禁言你 {duration} 秒,原因:{reason}" ] # 错误消息模板 @@ -57,9 +57,6 @@ allow_parallel = false # 禁言命令配置 [mute_command] -# 是否需要管理员权限 -require_admin = true - # 最大批量禁言数量 max_batch_size = 5 diff --git a/src/plugins/built_in/mute_plugin/plugin.py b/src/plugins/built_in/mute_plugin/plugin.py index 8ced269be..987e71948 100644 --- a/src/plugins/built_in/mute_plugin/plugin.py +++ b/src/plugins/built_in/mute_plugin/plugin.py @@ -153,7 +153,8 @@ class MuteAction(BaseAction): # 获取模板化消息 message = self._get_template_message(target, time_str, reason) - await self.send_reply(message) + # await self.send_reply(message) + await self.send_message_by_expressor(message) # 发送群聊禁言命令 success = await self.send_command( diff --git a/src/plugins/examples/example_plugin/config.toml b/src/plugins/examples/example_plugin/config.toml index 981b480e1..011b216a0 100644 --- a/src/plugins/examples/example_plugin/config.toml +++ b/src/plugins/examples/example_plugin/config.toml @@ -1,10 +1,10 @@ # 综合示例插件配置文件 [plugin] -name = "example_comprehensive" +name = "example_plugin" version = "2.0.0" enabled = true -description = "展示新插件系统完整功能的综合示例插件" +description = "展示新插件系统完整功能的示例插件" # 组件启用控制 [components] diff --git a/src/plugins/examples/example_plugin/plugin.py b/src/plugins/examples/example_plugin/plugin.py index 8a91e7c82..373988db2 100644 --- a/src/plugins/examples/example_plugin/plugin.py +++ b/src/plugins/examples/example_plugin/plugin.py @@ -408,7 +408,7 @@ class ExampleComprehensivePlugin(BasePlugin): """ # 插件基本信息 - plugin_name = "example_comprehensive" + plugin_name = "example_plugin" plugin_description = "综合示例插件,展示新插件系统的完整功能" plugin_version = "2.0.0" plugin_author = "MaiBot开发团队"