Files
abot/plugins/xiuren_image/main.py
2026-01-16 13:35:45 +08:00

273 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from pathlib import Path
from loguru import logger
import os
import base64
from typing import Dict, Any, List, Optional, Tuple
from db.connection import DBConnectionManager
from base.plugin_common.message_plugin_interface import MessagePluginInterface
from base.plugin_common.plugin_interface import PluginStatus
from plugins.xiuren_image.images_cache import ImageCacheManager
from utils.decorator.plugin_decorators import plugin_stats_decorator
from utils.decorator.rate_limit_decorator import group_feature_rate_limit
from utils.revoke.message_auto_revoke import MessageAutoRevoke
from utils.robot_cmd.robot_command import Feature, PermissionStatus, GroupBotManager
from utils.decorator.points_decorator import plugin_points_cost
from wechat_ipad import WechatAPIClient
class XiurenImagePlugin(MessagePluginInterface):
"""秀人图片插件"""
# 功能权限常量
FEATURE_KEY = "XIUREN_IMAGE"
FEATURE_DESCRIPTION = "🖼️ 图来能力 [图来, 秀人]"
@property
def name(self) -> str:
return "秀人图片"
@property
def version(self) -> str:
return "1.0.0"
@property
def description(self) -> str:
return "提供随机秀人网图片功能"
@property
def author(self) -> str:
return "liu.wei"
@property
def command_prefix(self) -> Optional[str]:
return "" # 不需要前缀,直接匹配命令
@property
def commands(self) -> List[str]:
return self._commands
@property
def feature_key(self) -> Optional[str]:
return self.FEATURE_KEY
@property
def feature_description(self) -> Optional[str]:
return self.FEATURE_DESCRIPTION
def __init__(self):
super().__init__()
# 使用Path对象处理路径自动适应不同操作系统
self.image_folder = str(Path(Path(__file__).parent.parent.parent, "xiuren"))
# 注册功能权限
self.feature = self.register_feature()
# 初始化图片缓存管理器
self.image_cache_manager = None
def initialize(self, context: Dict[str, Any]) -> bool:
"""初始化插件"""
self.LOG = logger
self.LOG.debug(f"正在初始化 {self.name} 插件...")
# 保存上下文对象
self.event_system = context.get("event_system")
self._commands = self._config.get("XiurenImage", {}).get("command", ["图来", "秀人"])
self.command_format = self._config.get("XiurenImage", {}).get("command-format", "图来")
self.enable = self._config.get("XiurenImage", {}).get("enable", True)
# 从配置获取图片文件夹,如果配置中有值则使用配置值
config_image_folder = self._config.get("XiurenImage", {}).get("image_folder")
if config_image_folder:
self.image_folder = config_image_folder
# 从配置获取缓存大小默认5张
cache_size = self._config.get("XiurenImage", {}).get("cache_size", 5)
# 检查图片文件夹是否存在
try:
if not os.path.exists(self.image_folder):
self.LOG.warning(f"图片文件夹不存在: {self.image_folder}")
os.makedirs(self.image_folder, exist_ok=True)
except Exception as e:
self.LOG.error(f"创建图片文件夹失败: {e}")
# 初始化图片缓存管理器
self.image_cache_manager = ImageCacheManager(self.image_folder, cache_size)
self.LOG.debug(
f"[{self.name}] 插件初始化完成,指令:{self._commands},图片目录:{self.image_folder},缓存大小:{cache_size}")
return True
def start(self) -> bool:
"""启动插件"""
self.LOG.debug(f"[{self.name}] 插件已启动")
self.status = PluginStatus.RUNNING
return True
def stop(self) -> bool:
"""停止插件"""
self.LOG.info(f"[{self.name}] 插件已停止")
self.status = PluginStatus.STOPPED
return True
def can_process(self, message: Dict[str, Any]) -> bool:
"""检查是否可以处理该消息"""
if not self.enable:
return False
content = str(message.get("content", "")).strip()
command = content.split(" ")[0]
return command in self._commands
@plugin_stats_decorator(plugin_name="秀人网图片")
@plugin_points_cost(5, "秀人网图片消耗积分", FEATURE_KEY)
@group_feature_rate_limit(max_per_minute=5, feature_key=FEATURE_KEY)
async def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
"""处理消息"""
content = str(message.get("content", "")).strip()
self.LOG.debug(f"插件执行: {self.name}{content}")
sender = message.get("sender")
roomid = message.get("roomid", "")
gbm: GroupBotManager = message.get("gbm")
bot: WechatAPIClient = message.get("bot")
revoke: MessageAutoRevoke = message.get("revoke")
# 检查权限
if roomid and gbm.get_group_permission(roomid, self.feature) == PermissionStatus.DISABLED:
return False, "没有权限"
try:
# 从缓存获取图片bytes数据
cached_image = self._get_cached_image()
if not cached_image:
client_msg_id, create_time, new_msg_id = await bot.send_text_message((roomid if roomid else sender),
f"❌未找到图片资源",
sender)
revoke.add_message_to_revoke(roomid, client_msg_id, create_time, new_msg_id, 5)
return False, "未找到图片资源"
# 发送图片
image_data = cached_image['bytes']
image_path = cached_image['path']
# 记录缓存状态
cache_count = self.image_cache_manager.get_cached_image_count()
self.LOG.info(f"从缓存获取图片成功,路径:{image_path},当前缓存数量:{cache_count}")
# 发送图片支持bytes格式
client_msg_id, create_time, new_msg_id = await bot.send_image_message((roomid if roomid else sender),
image_data)
# revoke.add_message_to_revoke(roomid, client_msg_id, create_time, new_msg_id, 90)
self.LOG.info(
f"发送图片结果,client_msg_id= {client_msg_id},create_time={create_time},new_msg_id={new_msg_id}")
return True, "发送成功"
except Exception as e:
self.LOG.error(f"处理图片请求出错: {e}")
return False, f"处理出错: {e}"
def _get_cached_image(self) -> Optional[Dict[str, any]]:
"""
从缓存获取图片数据
返回格式: {'path': str, 'bytes': bytes}
"""
try:
# 优先从内存缓存获取
cached_image = self.image_cache_manager.get_cached_image_bytes()
if cached_image:
return cached_image
# 如果缓存中没有,回退到原来的方式
self.LOG.warning("缓存中没有图片,回退到磁盘读取")
pic_path = self._get_random_pic()
if pic_path:
# 读取图片bytes
try:
with open(pic_path, 'rb') as f:
image_bytes = f.read()
return {
'path': pic_path,
'bytes': image_bytes
}
except Exception as e:
self.LOG.error(f"读取图片文件失败: {e}")
return None
return None
except Exception as e:
self.LOG.error(f"获取缓存图片失败: {e}")
return None
def _get_random_pic(self) -> Optional[str]:
"""
从 Redis 随机获取图片路径
"""
redis_key = ImageCacheManager.IMAGE_KEY_PREFIX + "all"
try:
img = DBConnectionManager.get_instance().get_redis_connection().srandmember(redis_key)
if img:
# redis 返回 bytes转字符串
return img.decode('utf-8') if isinstance(img, bytes) else img
else:
self.LOG.warning("Redis 中没有图片数据")
return None
except Exception as e:
self.LOG.error(f"从 Redis 获取随机图片失败: {e}")
return None
# def _get_random_pic(self) -> Optional[str]:
# """获取随机图片路径"""
# try:
# # 获取图片文件夹中的所有图片
# if not os.path.exists(self.image_folder):
# self.LOG.error(f"图片文件夹不存在: {self.image_folder}")
# return None
#
# # 检查读取权限
# if not os.access(self.image_folder, os.R_OK):
# self.LOG.error(f"没有图片文件夹的读取权限: {self.image_folder}")
# return None
#
# self.LOG.debug(f"扫描图片目录: {self.image_folder} (包括子目录)")
#
# image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'}
# image_files = []
#
# # 使用 os.walk() 递归遍历所有子目录
# try:
# for root, _, files in os.walk(self.image_folder):
# for file in files:
# try:
# _, ext = os.path.splitext(file)
# if ext.lower() in image_extensions:
# full_path = os.path.join(root, file)
# # 检查文件是否可读
# if os.access(full_path, os.R_OK):
# image_files.append(full_path)
# else:
# self.LOG.warning(f"文件无法读取: {full_path}")
# except Exception as file_err:
# self.LOG.warning(f"处理文件时出错: {file} - {file_err}")
# except Exception as walk_err:
# self.LOG.error(f"遍历目录时出错: {walk_err}")
# return None
#
# if not image_files:
# self.LOG.warning("在目录中未找到图片文件(包括子目录)")
# return None
#
# # 随机选择一张图片
# try:
# random_pic = random.choice(image_files)
# return random_pic # 直接返回路径不需要再调用os.path.abspath
# except Exception as choice_err:
# self.LOG.error(f"选择随机图片时出错: {choice_err}")
# return None
#
# except Exception as e:
# self.LOG.error(f"获取随机图片出错: {e}")
# return None