fix:修复禁言插件和豆包画图插件

This commit is contained in:
SengokuCola
2025-06-11 00:18:48 +08:00
parent 6455dab5b8
commit 22aae4d1cd
17 changed files with 271 additions and 685 deletions

View File

@@ -159,6 +159,13 @@ class HeartFChatting:
for name, (observation_class, param_name) in OBSERVATION_CLASSES.items(): for name, (observation_class, param_name) in OBSERVATION_CLASSES.items():
try: 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} kwargs = {param_name: self.stream_id}
observation = observation_class(**kwargs) observation = observation_class(**kwargs)

View File

@@ -2,6 +2,7 @@ from typing import List, Any, Optional
import asyncio import asyncio
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from src.chat.focus_chat.working_memory.memory_manager import MemoryManager, MemoryItem from src.chat.focus_chat.working_memory.memory_manager import MemoryManager, MemoryItem
from src.config.config import global_config
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -33,8 +34,11 @@ class WorkingMemory:
# 衰减任务 # 衰减任务
self.decay_task = None self.decay_task = None
# 启动自动衰减任务 # 只有在工作记忆处理器启用时才启动自动衰减任务
if global_config.focus_chat_processor.working_memory_processor:
self._start_auto_decay() self._start_auto_decay()
else:
logger.debug(f"工作记忆处理器已禁用,跳过启动自动衰减任务 (chat_id: {chat_id})")
def _start_auto_decay(self): def _start_auto_decay(self):
"""启动自动衰减任务""" """启动自动衰减任务"""

View File

@@ -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迁移为系统插件
- **工具插件** - 常用的工具和实用功能
- **示例插件** - 帮助开发者学习的示例代码
这个重构保持了向后兼容性,同时提供了更清晰、更易维护的架构。

View File

@@ -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<param>\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避免不必要的依赖注入。

View File

@@ -167,13 +167,12 @@ class BaseAction(ABC):
logger.error(f"{self.log_prefix} 没有可用的聊天流发送命令") logger.error(f"{self.log_prefix} 没有可用的聊天流发送命令")
return False return False
command_content = str(command_data)
if chat_stream.group_info: if chat_stream.group_info:
# 群聊 # 群聊
success = await self.api.send_message_to_target( success = await self.api.send_message_to_target(
message_type="command", message_type="command",
content=command_content, content=command_data,
platform=chat_stream.platform, platform=chat_stream.platform,
target_id=str(chat_stream.group_info.group_id), target_id=str(chat_stream.group_info.group_id),
is_group=True, is_group=True,
@@ -183,7 +182,7 @@ class BaseAction(ABC):
# 私聊 # 私聊
success = await self.api.send_message_to_target( success = await self.api.send_message_to_target(
message_type="command", message_type="command",
content=command_content, content=command_data,
platform=chat_stream.platform, platform=chat_stream.platform,
target_id=str(chat_stream.user_info.user_id), target_id=str(chat_stream.user_info.user_id),
is_group=False, is_group=False,
@@ -213,7 +212,7 @@ class BaseAction(ABC):
""" """
try: try:
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation 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") expressor = self.api.get_service("expressor")
@@ -281,7 +280,7 @@ class BaseAction(ABC):
""" """
try: try:
from src.chat.heart_flow.observation.chatting_observation import ChattingObservation 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") replyer = self.api.get_service("replyer")

View File

@@ -102,7 +102,7 @@ class BaseCommand(ABC):
# 使用send_message_to_target方法发送命令 # 使用send_message_to_target方法发送命令
chat_stream = self.message.chat_stream chat_stream = self.message.chat_stream
command_content = str(command_data) command_content = command_data
if chat_stream.group_info: if chat_stream.group_info:
# 群聊 # 群聊

View File

@@ -167,16 +167,26 @@ class BasePlugin(ABC):
return True return True
def get_config(self, key: str, default: Any = None) -> Any: def get_config(self, key: str, default: Any = None) -> Any:
"""获取插件配置值 """获取插件配置值,支持嵌套键访问
Args: Args:
key: 配置键名 key: 配置键名,支持嵌套访问如 "section.subsection.key"
default: 默认值 default: 默认值
Returns: Returns:
Any: 配置值或默认值 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): def register_plugin(cls):

View File

@@ -1,4 +0,0 @@
"""测试插件动作模块"""
# 导入所有动作模块以确保装饰器被执行
from . import pic_action # noqa

View File

@@ -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()

View File

@@ -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"

View File

@@ -4,7 +4,7 @@
这是一个测试插件用于测试图片发送功能 这是一个测试插件用于测试图片发送功能
""" """
"""豆包图片生成插件 """豆包图片生成插件
这是一个基于火山引擎豆包模型的AI图片生成插件 这是一个基于火山引擎豆包模型的AI图片生成插件
@@ -21,7 +21,7 @@
- 将文字描述转换为视觉图像 - 将文字描述转换为视觉图像
- 创意图片和艺术作品生成 - 创意图片和艺术作品生成
配置文件src/plugins/doubao_pic/actions/pic_action_config.toml 配置文件config.toml
配置要求 配置要求
1. 设置火山引擎API密钥 (volcano_generate_api_key) 1. 设置火山引擎API密钥 (volcano_generate_api_key)
@@ -30,3 +30,7 @@
注意需要有效的火山引擎API访问权限才能正常使用 注意需要有效的火山引擎API访问权限才能正常使用
""" """
from .plugin import DoubaoImagePlugin
__all__ = ["DoubaoImagePlugin"]

View File

@@ -1,9 +1,20 @@
# 豆包图片生成插件配置文件
# API配置
base_url = "https://ark.cn-beijing.volces.com/api/v3" 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_model = "doubao-seedream-3-0-t2i-250415"
default_size = "1024x1024" default_size = "1024x1024"
default_watermark = true default_watermark = true
default_guidance_scale = 2.5 default_guidance_scale = 2.5
default_seed = 42 default_seed = 42
# 缓存配置
cache_enabled = true cache_enabled = true
cache_max_size = 10 cache_max_size = 10
# 组件启用配置
[components]
enable_image_generation = true

View File

@@ -1,44 +1,49 @@
"""
豆包图片生成插件
基于火山引擎豆包模型的AI图片生成插件
功能特性
- 智能LLM判定根据聊天内容智能判断是否需要生成图片
- 高质量图片生成使用豆包Seed Dream模型生成图片
- 结果缓存避免重复生成相同内容的图片
- 配置验证自动验证和修复配置文件
- 参数验证完整的输入参数验证和错误处理
- 多尺寸支持支持多种图片尺寸生成
包含组件
- 图片生成Action - 根据描述使用火山引擎API生成图片
"""
import asyncio import asyncio
import json import json
import urllib.request import urllib.request
import urllib.error import urllib.error
import base64 # 新增用于Base64编码 import base64
import traceback # 新增:用于打印堆栈跟踪 import traceback
from typing import Tuple import random
from src.chat.actions.plugin_action import PluginAction, register_action from typing import List, Tuple, Type, Optional
from src.chat.actions.base_action import ActionActivationType, ChatMode
# 导入新插件系统
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 src.common.logger_manager import get_logger
from .generate_pic_config import generate_config
logger = get_logger("pic_action") logger = get_logger("doubao_pic_plugin")
# 当此模块被加载时,尝试生成配置文件(如果它不存在)
# 注意:在某些插件加载机制下,这可能会在每次机器人启动或插件重载时执行
# 考虑是否需要更复杂的逻辑来决定何时运行 (例如,仅在首次安装时)
generate_config()
@register_action # ===== Action组件 =====
class PicAction(PluginAction):
"""根据描述使用火山引擎HTTP API生成图片的动作处理类"""
action_name = "pic_action" class DoubaoImageGenerationAction(BaseAction):
action_description = ( """豆包图片生成Action - 根据描述使用火山引擎API生成图片"""
"可以根据特定的描述,生成并发送一张图片,如果没提供描述,就根据聊天内容生成,你可以立刻画好,不用等待"
)
action_parameters = {
"description": "图片描述,输入你想要生成并发送的图片的描述,必填",
"size": "图片尺寸,例如 '1024x1024' (可选, 默认从配置或 '1024x1024')",
}
action_require = [
"当有人让你画东西时使用,你可以立刻画好,不用等待",
"当有人要求你生成并发送一张图片时使用",
"当有人让你画一张图时使用",
]
enable_plugin = False
action_config_file_name = "pic_action_config.toml"
# 激活类型设置 # Action基本信息
action_name = "doubao_image_generation"
action_description = "可以根据特定的描述,生成并发送一张图片,如果没提供描述,就根据聊天内容生成,你可以立刻画好,不用等待"
# 激活设置
focus_activation_type = ActionActivationType.LLM_JUDGE # Focus模式使用LLM判定精确理解需求 focus_activation_type = ActionActivationType.LLM_JUDGE # Focus模式使用LLM判定精确理解需求
normal_activation_type = ActionActivationType.KEYWORD # Normal模式使用关键词激活快速响应 normal_activation_type = ActionActivationType.KEYWORD # Normal模式使用关键词激活快速响应
@@ -68,76 +73,44 @@ class PicAction(PluginAction):
5. 用户明确表示不需要图片时 5. 用户明确表示不需要图片时
""" """
# Random激活概率备用 mode_enable = ChatMode.ALL
random_activation_probability = 0.15 # 适中概率,图片生成比较有趣 parallel_action = True
# Action参数定义
action_parameters = {
"description": "图片描述,输入你想要生成并发送的图片的描述,必填",
"size": "图片尺寸,例如 '1024x1024' (可选, 默认从配置或 '1024x1024')",
}
# Action使用场景
action_require = [
"当有人让你画东西时使用,你可以立刻画好,不用等待",
"当有人要求你生成并发送一张图片时使用",
"当有人让你画一张图时使用",
]
# 简单的请求缓存,避免短时间内重复请求 # 简单的请求缓存,避免短时间内重复请求
_request_cache = {} _request_cache = {}
_cache_max_size = 10 _cache_max_size = 10
# 模式启用设置 - 图片生成在所有模式下可用 async def execute(self) -> Tuple[bool, Optional[str]]:
mode_enable = ChatMode.ALL """执行图片生成动作"""
logger.info(f"{self.log_prefix} 执行豆包图片生成动作")
# 并行执行设置 - 图片生成可以与回复并行执行,不覆盖回复内容
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}")
# 配置验证 # 配置验证
http_base_url = self.config.get("base_url") http_base_url = self.api.get_config("base_url")
http_api_key = self.config.get("volcano_generate_api_key") http_api_key = self.api.get_config("volcano_generate_api_key")
if not (http_base_url and http_api_key): if not (http_base_url and http_api_key):
error_msg = "抱歉图片生成功能所需的HTTP配置如API地址或密钥不完整无法提供服务。" 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.") logger.error(f"{self.log_prefix} HTTP调用配置缺失: base_url 或 volcano_generate_api_key.")
return False, "HTTP配置不完整" return False, "HTTP配置不完整"
# API密钥验证 # API密钥验证
if http_api_key == "YOUR_VOLCANO_GENERATE_API_KEY_HERE": if http_api_key == "YOUR_DOUBAO_API_KEY_HERE":
error_msg = "图片生成功能尚未配置请设置正确的API密钥。" error_msg = "图片生成功能尚未配置请设置正确的API密钥。"
await self.send_message_by_expressor(error_msg) await self.send_reply(error_msg)
logger.error(f"{self.log_prefix} API密钥未配置") logger.error(f"{self.log_prefix} API密钥未配置")
return False, "API密钥未配置" return False, "API密钥未配置"
@@ -145,7 +118,7 @@ class PicAction(PluginAction):
description = self.action_data.get("description") description = self.action_data.get("description")
if not description or not description.strip(): if not description or not description.strip():
logger.warning(f"{self.log_prefix} 图片描述为空,无法生成图片。") logger.warning(f"{self.log_prefix} 图片描述为空,无法生成图片。")
await self.send_message_by_expressor("你需要告诉我想要画什么样的图片哦~ 比如说'画一只可爱的小猫'") await self.send_reply("你需要告诉我想要画什么样的图片哦~ 比如说'画一只可爱的小猫'")
return False, "图片描述为空" return False, "图片描述为空"
# 清理和验证描述 # 清理和验证描述
@@ -155,8 +128,8 @@ class PicAction(PluginAction):
logger.info(f"{self.log_prefix} 图片描述过长,已截断") logger.info(f"{self.log_prefix} 图片描述过长,已截断")
# 获取配置 # 获取配置
default_model = self.config.get("default_model", "doubao-seedream-3-0-t2i-250415") default_model = self.api.get_config("default_model", "doubao-seedream-3-0-t2i-250415")
image_size = self.action_data.get("size", self.config.get("default_size", "1024x1024")) image_size = self.action_data.get("size", self.api.get_config("default_size", "1024x1024"))
# 验证图片尺寸格式 # 验证图片尺寸格式
if not self._validate_image_size(image_size): if not self._validate_image_size(image_size):
@@ -168,58 +141,23 @@ class PicAction(PluginAction):
if cache_key in self._request_cache: if cache_key in self._request_cache:
cached_result = self._request_cache[cache_key] cached_result = self._request_cache[cache_key]
logger.info(f"{self.log_prefix} 使用缓存的图片结果") 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: if send_success:
await self.send_message_by_expressor("图片表情已发送!") await self.send_reply("图片已发送!")
return True, "图片表情已发送(缓存)" return True, "图片已发送(缓存)"
else: else:
# 缓存失败,清除这个缓存项并继续正常流程 # 缓存失败,清除这个缓存项并继续正常流程
del self._request_cache[cache_key] del self._request_cache[cache_key]
# guidance_scale 现在完全由配置文件控制 # 获取其他配置参数
guidance_scale_input = self.config.get("default_guidance_scale", 2.5) # 默认2.5 guidance_scale_val = self._get_guidance_scale()
guidance_scale_val = 2.5 # Fallback default seed_val = self._get_seed()
try: watermark_val = self._get_watermark()
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
# Seed parameter - ensure it's always an integer await self.send_reply(
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(
f"收到!正在为您生成关于 '{description}' 的图片,请稍候...(模型: {default_model}, 尺寸: {image_size}" f"收到!正在为您生成关于 '{description}' 的图片,请稍候...(模型: {default_model}, 尺寸: {image_size}"
) )
@@ -253,17 +191,17 @@ class PicAction(PluginAction):
if encode_success: if encode_success:
base64_image_string = encode_result 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: if send_success:
# 缓存成功的结果 # 缓存成功的结果
self._request_cache[cache_key] = base64_image_string self._request_cache[cache_key] = base64_image_string
self._cleanup_cache() self._cleanup_cache()
await self.send_message_by_expressor("图片表情已发送!") await self.send_message_by_expressor("图片已发送!")
return True, "图片表情已发送" return True, "图片已发送"
else: else:
await self.send_message_by_expressor("图片已处理为Base64作为表情发送失败了。") await self.send_message_by_expressor("图片已处理为Base64但发送失败了。")
return False, "图片表情发送失败 (Base64)" return False, "图片发送失败 (Base64)"
else: else:
await self.send_message_by_expressor(f"获取到图片URL但在处理图片时失败了{encode_result}") await self.send_message_by_expressor(f"获取到图片URL但在处理图片时失败了{encode_result}")
return False, f"图片处理失败(Base64): {encode_result}" return False, f"图片处理失败(Base64): {encode_result}"
@@ -272,6 +210,90 @@ class PicAction(PluginAction):
await self.send_message_by_expressor(f"哎呀,生成图片时遇到问题:{error_message}") await self.send_message_by_expressor(f"哎呀,生成图片时遇到问题:{error_message}")
return False, 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]: def _download_and_encode_base64(self, image_url: str) -> Tuple[bool, str]:
"""下载图片并将其编码为Base64字符串""" """下载图片并将其编码为Base64字符串"""
logger.info(f"{self.log_prefix} (B64) 下载并编码图片: {image_url[:70]}...") logger.info(f"{self.log_prefix} (B64) 下载并编码图片: {image_url[:70]}...")
@@ -286,16 +308,17 @@ class PicAction(PluginAction):
error_msg = f"下载图片失败 (状态: {response.status})" error_msg = f"下载图片失败 (状态: {response.status})"
logger.error(f"{self.log_prefix} (B64) {error_msg} URL: {image_url}") logger.error(f"{self.log_prefix} (B64) {error_msg} URL: {image_url}")
return False, error_msg 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) logger.error(f"{self.log_prefix} (B64) 下载或编码时错误: {e!r}", exc_info=True)
traceback.print_exc() traceback.print_exc()
return False, f"下载或编码图片时发生错误: {str(e)[:100]}" return False, f"下载或编码图片时发生错误: {str(e)[:100]}"
def _make_http_image_request( 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]: ) -> Tuple[bool, str]:
base_url = self.config.get("base_url") """发送HTTP请求生成图片"""
generate_api_key = self.config.get("volcano_generate_api_key") 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" endpoint = f"{base_url.rstrip('/')}/images/generations"
@@ -306,11 +329,9 @@ class PicAction(PluginAction):
"size": size, "size": size,
"guidance_scale": guidance_scale, "guidance_scale": guidance_scale,
"watermark": watermark, "watermark": watermark,
"seed": seed, # seed is now always an int from process() "seed": seed,
"api-key": generate_api_key, "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") data = json.dumps(payload_dict).encode("utf-8")
headers = { headers = {
@@ -320,12 +341,6 @@ class PicAction(PluginAction):
} }
logger.info(f"{self.log_prefix} (HTTP) 发起图片请求: {model}, Prompt: {prompt[:30]}... To: {endpoint}") 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") 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]}...") logger.info(f"{self.log_prefix} (HTTP) 图片生成成功URL: {image_url[:70]}...")
return True, image_url return True, image_url
else: else:
logger.error( logger.error(f"{self.log_prefix} (HTTP) API成功但无图片URL")
f"{self.log_prefix} (HTTP) API成功但无图片URL. 响应预览: {response_body_str[:300]}..."
)
return False, "图片生成API响应成功但未找到图片URL" return False, "图片生成API响应成功但未找到图片URL"
else: else:
logger.error( logger.error(f"{self.log_prefix} (HTTP) API请求失败. 状态: {response.status}")
f"{self.log_prefix} (HTTP) API请求失败. 状态: {response.status}. 正文: {response_body_str[:300]}..."
)
return False, f"图片API请求失败(状态码 {response.status})" return False, f"图片API请求失败(状态码 {response.status})"
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix} (HTTP) 图片生成时意外错误: {e!r}", exc_info=True) logger.error(f"{self.log_prefix} (HTTP) 图片生成时意外错误: {e!r}", exc_info=True)
traceback.print_exc() traceback.print_exc()
return False, f"图片生成HTTP请求时发生意外错误: {str(e)[:100]}" 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")) @register_plugin
return 100 <= width <= 10000 and 100 <= height <= 10000 class DoubaoImagePlugin(BasePlugin):
except (ValueError, TypeError): """豆包图片生成插件
return False
基于火山引擎豆包模型的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

View File

@@ -9,7 +9,7 @@ description = "群聊禁言管理插件,提供智能禁言功能"
# 组件启用控制 # 组件启用控制
[components] [components]
enable_smart_mute = true # 启用智能禁言Action enable_smart_mute = true # 启用智能禁言Action
enable_mute_command = true # 启用禁言命令Command enable_mute_command = false # 启用禁言命令Command
# 禁言配置 # 禁言配置
[mute] [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}", "哎呦我去,对 {target} 执行禁言 {duration},因为{reason}",
"⛔ 禁言 {target} {duration},原因:{reason}" "{target},你完蛋了,我要禁言你 {duration},原因:{reason}"
] ]
# 错误消息模板 # 错误消息模板
@@ -57,9 +57,6 @@ allow_parallel = false
# 禁言命令配置 # 禁言命令配置
[mute_command] [mute_command]
# 是否需要管理员权限
require_admin = true
# 最大批量禁言数量 # 最大批量禁言数量
max_batch_size = 5 max_batch_size = 5

View File

@@ -153,7 +153,8 @@ class MuteAction(BaseAction):
# 获取模板化消息 # 获取模板化消息
message = self._get_template_message(target, time_str, reason) 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( success = await self.send_command(

View File

@@ -1,10 +1,10 @@
# 综合示例插件配置文件 # 综合示例插件配置文件
[plugin] [plugin]
name = "example_comprehensive" name = "example_plugin"
version = "2.0.0" version = "2.0.0"
enabled = true enabled = true
description = "展示新插件系统完整功能的综合示例插件" description = "展示新插件系统完整功能的示例插件"
# 组件启用控制 # 组件启用控制
[components] [components]

View File

@@ -408,7 +408,7 @@ class ExampleComprehensivePlugin(BasePlugin):
""" """
# 插件基本信息 # 插件基本信息
plugin_name = "example_comprehensive" plugin_name = "example_plugin"
plugin_description = "综合示例插件,展示新插件系统的完整功能" plugin_description = "综合示例插件,展示新插件系统的完整功能"
plugin_version = "2.0.0" plugin_version = "2.0.0"
plugin_author = "MaiBot开发团队" plugin_author = "MaiBot开发团队"