Merge branch 'master' into migrate-windpicker-changes

This commit is contained in:
Windpicker-owo
2025-08-25 17:47:50 +08:00
9 changed files with 512 additions and 103 deletions

View File

@@ -9,7 +9,6 @@ from typing import List
from src.plugin_system.apis.plugin_register_api import register_plugin from src.plugin_system.apis.plugin_register_api import register_plugin
from src.plugin_system.base.base_plugin import BasePlugin from src.plugin_system.base.base_plugin import BasePlugin
from src.plugin_system.base.base_command import BaseCommand from src.plugin_system.base.base_command import BaseCommand
from src.plugin_system.apis.permission_api import permission_api
from src.plugin_system.apis.logging_api import get_logger from src.plugin_system.apis.logging_api import get_logger
from src.plugin_system.base.config_types import ConfigField from src.plugin_system.base.config_types import ConfigField
from src.plugin_system.utils.permission_decorators import require_permission, require_master, PermissionChecker from src.plugin_system.utils.permission_decorators import require_permission, require_master, PermissionChecker

View File

@@ -20,8 +20,6 @@ from src.mais4u.mais4u_chat.s4u_msg_processor import S4UMessageProcessor
# 导入反注入系统 # 导入反注入系统
from src.chat.antipromptinjector import initialize_anti_injector from src.chat.antipromptinjector import initialize_anti_injector
# 定义日志配置
# 获取项目根目录假设本文件在src/chat/message_receive/下,根目录为上上上级目录) # 获取项目根目录假设本文件在src/chat/message_receive/下,根目录为上上上级目录)
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
@@ -242,6 +240,20 @@ class ChatBot:
- 性能计时 - 性能计时
""" """
try: try:
# 首先处理可能的切片消息重组
from src.utils.message_chunker import reassembler
# 尝试重组切片消息
reassembled_message = await reassembler.process_chunk(message_data)
if reassembled_message is None:
# 这是一个切片,但还未完整,等待更多切片
logger.debug("等待更多切片,跳过此次处理")
return
elif reassembled_message != message_data:
# 消息已被重组,使用重组后的消息
logger.info("使用重组后的完整消息进行处理")
message_data = reassembled_message
# 确保所有任务已启动 # 确保所有任务已启动
await self._ensure_started() await self._ensure_started()

View File

@@ -12,10 +12,14 @@ import asyncio
import base64 import base64
import hashlib import hashlib
import time import time
import numpy as np
from PIL import Image from PIL import Image
from pathlib import Path from pathlib import Path
from typing import List, Tuple, Optional, Dict from typing import List, Tuple, Optional, Dict
import io import io
from concurrent.futures import ThreadPoolExecutor
from functools import partial
import numpy as np
from src.llm_models.utils_model import LLMRequest from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config, model_config from src.config.config import global_config, model_config
@@ -33,6 +37,110 @@ video_events = {}
video_lock_manager = asyncio.Lock() video_lock_manager = asyncio.Lock()
def _extract_frames_worker(video_path: str,
max_frames: int,
frame_quality: int,
max_image_size: int,
frame_extraction_mode: str,
frame_interval_seconds: Optional[float]) -> List[Tuple[str, float]]:
"""线程池中提取视频帧的工作函数"""
frames = []
try:
cap = cv2.VideoCapture(video_path)
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
duration = total_frames / fps if fps > 0 else 0
if frame_extraction_mode == "time_interval":
# 新模式:按时间间隔抽帧
time_interval = frame_interval_seconds
next_frame_time = 0.0
extracted_count = 0 # 初始化提取帧计数器
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
current_time = cap.get(cv2.CAP_PROP_POS_MSEC) / 1000.0
if current_time >= next_frame_time:
# 转换为PIL图像并压缩
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_image = Image.fromarray(frame_rgb)
# 调整图像大小
if max(pil_image.size) > max_image_size:
ratio = max_image_size / max(pil_image.size)
new_size = tuple(int(dim * ratio) for dim in pil_image.size)
pil_image = pil_image.resize(new_size, Image.Resampling.LANCZOS)
# 转换为base64
buffer = io.BytesIO()
pil_image.save(buffer, format='JPEG', quality=frame_quality)
frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
frames.append((frame_base64, current_time))
extracted_count += 1
# 注意这里不能使用logger因为在线程池中
# logger.debug(f"提取第{extracted_count}帧 (时间: {current_time:.2f}s)")
next_frame_time += time_interval
else:
# 使用numpy优化帧间隔计算
if duration > 0:
frame_interval = max(1, int(duration / max_frames * fps))
else:
frame_interval = 30 # 默认间隔
# 使用numpy计算目标帧位置
target_frames = np.arange(0, min(max_frames, total_frames // frame_interval + 1)) * frame_interval
target_frames = target_frames[target_frames < total_frames].astype(int)
for target_frame in target_frames:
# 跳转到目标帧
cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
ret, frame = cap.read()
if not ret:
continue
# 使用numpy优化图像处理
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 转换为PIL图像并使用numpy进行尺寸计算
height, width = frame_rgb.shape[:2]
max_dim = max(height, width)
if max_dim > max_image_size:
# 使用numpy计算缩放比例
ratio = max_image_size / max_dim
new_width = int(width * ratio)
new_height = int(height * ratio)
# 使用opencv进行高效缩放
frame_resized = cv2.resize(frame_rgb, (new_width, new_height), interpolation=cv2.INTER_LANCZOS4)
pil_image = Image.fromarray(frame_resized)
else:
pil_image = Image.fromarray(frame_rgb)
# 转换为base64
buffer = io.BytesIO()
pil_image.save(buffer, format='JPEG', quality=frame_quality)
frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
# 计算时间戳
timestamp = target_frame / fps if fps > 0 else 0
frames.append((frame_base64, timestamp))
cap.release()
return frames
except Exception as e:
# 返回错误信息
return [("ERROR", str(e))]
class VideoAnalyzer: class VideoAnalyzer:
"""优化的视频分析器类""" """优化的视频分析器类"""
@@ -54,49 +162,14 @@ class VideoAnalyzer:
logger.warning(f"video_analysis配置不可用({e})回退使用vlm配置") logger.warning(f"video_analysis配置不可用({e})回退使用vlm配置")
# 从配置文件读取参数,如果配置不存在则使用默认值 # 从配置文件读取参数,如果配置不存在则使用默认值
try: config = global_config.video_analysis
config = global_config.video_analysis
self.max_frames = config.max_frames # 使用 getattr 统一获取配置参数,如果配置不存在则使用默认值
self.frame_quality = config.frame_quality self.max_frames = getattr(config, 'max_frames', 6)
self.max_image_size = config.max_image_size self.frame_quality = getattr(config, 'frame_quality', 85)
self.enable_frame_timing = config.enable_frame_timing self.max_image_size = getattr(config, 'max_image_size', 600)
self.batch_analysis_prompt = config.batch_analysis_prompt self.enable_frame_timing = getattr(config, 'enable_frame_timing', True)
self.frame_extraction_mode = config.frame_extraction_mode self.batch_analysis_prompt = getattr(config, 'batch_analysis_prompt', """请分析这个视频的内容。这些图片是从视频中按时间顺序提取的关键帧。
self.frame_interval_seconds = config.frame_interval_seconds
# 将配置文件中的模式映射到内部使用的模式名称
config_mode = config.analysis_mode
if config_mode == "batch_frames":
self.analysis_mode = "batch"
elif config_mode == "frame_by_frame":
self.analysis_mode = "sequential"
elif config_mode == "auto":
self.analysis_mode = "auto"
else:
logger.warning(f"无效的分析模式: {config_mode}使用默认的auto模式")
self.analysis_mode = "auto"
self.frame_analysis_delay = 0.3 # API调用间隔
self.frame_interval = 1.0 # 抽帧时间间隔(秒)
self.batch_size = 3 # 批处理时每批处理的帧数
self.timeout = 60.0 # 分析超时时间(秒)
logger.info("✅ 从配置文件读取视频分析参数")
except AttributeError as e:
# 如果配置不存在,使用代码中的默认值
logger.warning(f"配置文件中缺少video_analysis配置({e}),使用默认值")
self.max_frames = 6
self.frame_quality = 85
self.max_image_size = 600
self.analysis_mode = "auto"
self.frame_analysis_delay = 0.3
self.frame_interval = 1.0 # 抽帧时间间隔(秒)
self.batch_size = 3 # 批处理时每批处理的帧数
self.timeout = 60.0 # 分析超时时间(秒)
self.enable_frame_timing = True
self.frame_extraction_mode = "fixed_number"
self.frame_interval_seconds = 2.0
self.batch_analysis_prompt = """请分析这个视频的内容。这些图片是从视频中按时间顺序提取的关键帧。
请提供详细的分析,包括: 请提供详细的分析,包括:
1. 视频的整体内容和主题 1. 视频的整体内容和主题
@@ -106,12 +179,40 @@ class VideoAnalyzer:
5. 整体氛围和情感表达 5. 整体氛围和情感表达
6. 任何特殊的视觉效果或文字内容 6. 任何特殊的视觉效果或文字内容
请用中文回答,分析要详细准确。""" 请用中文回答,分析要详细准确。""")
# 新增的线程池配置
self.use_multiprocessing = getattr(config, 'use_multiprocessing', True)
self.max_workers = getattr(config, 'max_workers', 2)
self.frame_extraction_mode = getattr(config, 'frame_extraction_mode', 'fixed_number')
self.frame_interval_seconds = getattr(config, 'frame_interval_seconds', 2.0)
# 将配置文件中的模式映射到内部使用的模式名称
config_mode = getattr(config, 'analysis_mode', 'auto')
if config_mode == "batch_frames":
self.analysis_mode = "batch"
elif config_mode == "frame_by_frame":
self.analysis_mode = "sequential"
elif config_mode == "auto":
self.analysis_mode = "auto"
else:
logger.warning(f"无效的分析模式: {config_mode}使用默认的auto模式")
self.analysis_mode = "auto"
self.frame_analysis_delay = 0.3 # API调用间隔
self.frame_interval = 1.0 # 抽帧时间间隔(秒)
self.batch_size = 3 # 批处理时每批处理的帧数
self.timeout = 60.0 # 分析超时时间(秒)
if config:
logger.info("✅ 从配置文件读取视频分析参数")
else:
logger.warning("配置文件中缺少video_analysis配置使用默认值")
# 系统提示词 # 系统提示词
self.system_prompt = "你是一个专业的视频内容分析助手。请仔细观察用户提供的视频关键帧,详细描述视频内容。" self.system_prompt = "你是一个专业的视频内容分析助手。请仔细观察用户提供的视频关键帧,详细描述视频内容。"
logger.info(f"✅ 视频分析器初始化完成,分析模式: {self.analysis_mode}") logger.info(f"✅ 视频分析器初始化完成,分析模式: {self.analysis_mode}, 线程池: {self.use_multiprocessing}")
def _calculate_video_hash(self, video_data: bytes) -> str: def _calculate_video_hash(self, video_data: bytes) -> str:
"""计算视频文件的hash值""" """计算视频文件的hash值"""
@@ -186,8 +287,70 @@ class VideoAnalyzer:
logger.warning(f"无效的分析模式: {mode}") logger.warning(f"无效的分析模式: {mode}")
async def extract_frames(self, video_path: str) -> List[Tuple[str, float]]: async def extract_frames(self, video_path: str) -> List[Tuple[str, float]]:
"""提取视频帧""" """提取视频帧 - 支持多进程和单线程模式"""
# 先获取视频信息
cap = cv2.VideoCapture(video_path)
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
duration = total_frames / fps if fps > 0 else 0
cap.release()
logger.info(f"视频信息: {total_frames}帧, {fps:.2f}FPS, {duration:.2f}")
# 估算提取帧数
if duration > 0:
frame_interval = max(1, int(duration / self.max_frames * fps))
estimated_frames = min(self.max_frames, total_frames // frame_interval + 1)
else:
estimated_frames = self.max_frames
logger.info(f"计算得出帧间隔: {frame_interval} (将提取约{estimated_frames}帧)")
# 根据配置选择处理方式
if self.use_multiprocessing:
return await self._extract_frames_multiprocess(video_path)
else:
return await self._extract_frames_fallback(video_path)
async def _extract_frames_multiprocess(self, video_path: str) -> List[Tuple[str, float]]:
"""线程池版本的帧提取"""
loop = asyncio.get_event_loop()
try:
logger.info("🔄 启动线程池帧提取...")
# 使用线程池,避免进程间的导入问题
with ThreadPoolExecutor(max_workers=1) as executor:
frames = await loop.run_in_executor(
executor,
_extract_frames_worker,
video_path,
self.max_frames,
self.frame_quality,
self.max_image_size,
self.frame_extraction_mode,
self.frame_interval_seconds
)
# 检查是否有错误
if frames and frames[0][0] == "ERROR":
logger.error(f"线程池帧提取失败: {frames[0][1]}")
# 降级到单线程模式
logger.info("🔄 降级到单线程模式...")
return await self._extract_frames_fallback(video_path)
logger.info(f"✅ 成功提取{len(frames)}帧 (线程池模式)")
return frames
except Exception as e:
logger.error(f"线程池帧提取失败: {e}")
# 降级到原始方法
logger.info("🔄 降级到单线程模式...")
return await self._extract_frames_fallback(video_path)
async def _extract_frames_fallback(self, video_path: str) -> List[Tuple[str, float]]:
"""帧提取的降级方法 - 原始异步版本"""
frames = [] frames = []
extracted_count = 0
cap = cv2.VideoCapture(video_path) cap = cv2.VideoCapture(video_path)
fps = cap.get(cv2.CAP_PROP_FPS) fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
@@ -195,9 +358,7 @@ class VideoAnalyzer:
logger.info(f"视频信息: {total_frames}帧, {fps:.2f}FPS, {duration:.2f}") logger.info(f"视频信息: {total_frames}帧, {fps:.2f}FPS, {duration:.2f}")
frame_count = 0
extracted_count = 0
if self.frame_extraction_mode == "time_interval": if self.frame_extraction_mode == "time_interval":
# 新模式:按时间间隔抽帧 # 新模式:按时间间隔抽帧
time_interval = self.frame_interval_seconds time_interval = self.frame_interval_seconds
@@ -233,42 +394,61 @@ class VideoAnalyzer:
next_frame_time += time_interval next_frame_time += time_interval
else: else:
# 旧模式:固定总帧数 # 使用numpy优化帧间隔计算
if duration > 0: if duration > 0:
frame_interval = max(1, int(total_frames / self.max_frames)) frame_interval = max(1, int(duration / self.max_frames * fps))
else: else:
frame_interval = 1 # 如果无法获取时长则逐帧提取直到达到max_frames frame_interval = 30 # 默认间隔
logger.info(f"计算得出帧间隔: {frame_interval} (将提取约{min(self.max_frames, total_frames // frame_interval + 1)}帧)")
while cap.isOpened() and extracted_count < self.max_frames: # 使用numpy计算目标帧位置
target_frames = np.arange(0, min(self.max_frames, total_frames // frame_interval + 1)) * frame_interval
target_frames = target_frames[target_frames < total_frames].astype(int)
extracted_count = 0
for target_frame in target_frames:
# 跳转到目标帧
cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
ret, frame = cap.read() ret, frame = cap.read()
if not ret: if not ret:
break continue
# 使用numpy优化图像处理
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
if frame_count % frame_interval == 0: # 转换为PIL图像并使用numpy进行尺寸计算
# 转换为PIL图像并压缩 height, width = frame_rgb.shape[:2]
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) max_dim = max(height, width)
if max_dim > self.max_image_size:
# 使用numpy计算缩放比例
ratio = self.max_image_size / max_dim
new_width = int(width * ratio)
new_height = int(height * ratio)
# 使用opencv进行高效缩放
frame_resized = cv2.resize(frame_rgb, (new_width, new_height), interpolation=cv2.INTER_LANCZOS4)
pil_image = Image.fromarray(frame_resized)
else:
pil_image = Image.fromarray(frame_rgb) pil_image = Image.fromarray(frame_rgb)
# 调整图像大小
if max(pil_image.size) > self.max_image_size:
ratio = self.max_image_size / max(pil_image.size)
new_size = tuple(int(dim * ratio) for dim in pil_image.size)
pil_image = pil_image.resize(new_size, Image.Resampling.LANCZOS)
# 转换为base64 # 转换为base64
buffer = io.BytesIO() buffer = io.BytesIO()
pil_image.save(buffer, format='JPEG', quality=self.frame_quality) pil_image.save(buffer, format='JPEG', quality=self.frame_quality)
frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8') frame_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
# 计算时间戳 # 计算时间戳
timestamp = frame_count / fps if fps > 0 else 0 timestamp = target_frame / fps if fps > 0 else 0
frames.append((frame_base64, timestamp)) frames.append((frame_base64, timestamp))
extracted_count += 1 extracted_count += 1
logger.debug(f"提取第{extracted_count}帧 (时间: {timestamp:.2f}s)") logger.debug(f"提取第{extracted_count}帧 (时间: {timestamp:.2f}s, 帧号: {target_frame})")
frame_count += 1 # 每提取一帧让步一次
await asyncio.sleep(0.001)
cap.release() cap.release()
logger.info(f"✅ 成功提取{len(frames)}") logger.info(f"✅ 成功提取{len(frames)}")
return frames return frames

View File

@@ -73,6 +73,19 @@ class MainSystem:
def _cleanup(self): def _cleanup(self):
"""清理资源""" """清理资源"""
try:
# 停止消息重组器
from src.utils.message_chunker import reassembler
import asyncio
loop = asyncio.get_event_loop()
if loop.is_running():
asyncio.create_task(reassembler.stop_cleanup_task())
else:
loop.run_until_complete(reassembler.stop_cleanup_task())
logger.info("🛑 消息重组器已停止")
except Exception as e:
logger.error(f"停止消息重组器时出错: {e}")
try: try:
# 停止插件热重载系统 # 停止插件热重载系统
hot_reload_manager.stop() hot_reload_manager.stop()
@@ -215,6 +228,11 @@ MaiMbot-Pro-Max(第三方修改版)
# 将bot.py中的chat_bot.message_process消息处理函数注册到api.py的消息处理基类中 # 将bot.py中的chat_bot.message_process消息处理函数注册到api.py的消息处理基类中
self.app.register_message_handler(chat_bot.message_process) self.app.register_message_handler(chat_bot.message_process)
# 启动消息重组器的清理任务
from src.utils.message_chunker import reassembler
await reassembler.start_cleanup_task()
logger.info("消息重组器已启动")
# 初始化个体特征 # 初始化个体特征
await self.individuality.initialize() await self.individuality.initialize()
# 初始化日程管理器 # 初始化日程管理器

View File

@@ -0,0 +1,39 @@
{
"manifest_version": 1,
"name": "插件和组件管理 (Plugin and Component Management)",
"version": "1.0.0",
"description": "通过系统API管理插件和组件的生命周期包括加载、卸载、启用和禁用等操作。",
"author": {
"name": "MaiBot团队",
"url": "https://github.com/MaiM-with-u"
},
"license": "GPL-v3.0-or-later",
"host_application": {
"min_version": "0.9.1"
},
"homepage_url": "https://github.com/MaiM-with-u/maibot",
"repository_url": "https://github.com/MaiM-with-u/maibot",
"keywords": [
"plugins",
"components",
"management",
"built-in"
],
"categories": [
"Core System",
"Plugin Management"
],
"default_locale": "zh-CN",
"locales_path": "_locales",
"plugin_info": {
"is_built_in": true,
"plugin_type": "permission",
"components": [
{
"type": "command",
"name": "permission_management",
"description": "管理用户权限,包括添加、删除和修改权限等操作。"
}
]
}
}

View File

@@ -12,7 +12,6 @@ from src.plugin_system.base.base_plugin import BasePlugin
from src.plugin_system.base.base_command import BaseCommand from src.plugin_system.base.base_command import BaseCommand
from src.plugin_system.apis.permission_api import permission_api from src.plugin_system.apis.permission_api import permission_api
from src.plugin_system.apis.logging_api import get_logger from src.plugin_system.apis.logging_api import get_logger
from src.common.message import ChatStream, Message
from src.plugin_system.base.component_types import CommandInfo from src.plugin_system.base.component_types import CommandInfo
from src.plugin_system.base.config_types import ConfigField from src.plugin_system.base.config_types import ConfigField
@@ -25,7 +24,7 @@ class PermissionCommand(BaseCommand):
command_name = "permission" command_name = "permission"
command_description = "权限管理命令" command_description = "权限管理命令"
command_pattern = r"^/permission(\s[a-zA-Z0-9_]+)*\s*$)" command_pattern = r"^/permission"
command_help = "/permission <子命令> [参数...]" command_help = "/permission <子命令> [参数...]"
intercept_message = True intercept_message = True
@@ -44,33 +43,33 @@ class PermissionCommand(BaseCommand):
True True
) )
def can_execute(self, message: Message, chat_stream: ChatStream) -> bool: def can_execute(self) -> bool:
"""检查命令是否可以执行""" """检查命令是否可以执行"""
# 基本权限检查由权限系统处理 # 基本权限检查由权限系统处理
return True return True
async def execute(self, message: Message, chat_stream: ChatStream, args: List[str]) -> None: async def execute(self, args: List[str]) -> None:
"""执行权限管理命令""" """执行权限管理命令"""
if not args: if not args:
await self._show_help(chat_stream) await self._show_help()
return return
subcommand = args[0].lower() subcommand = args[0].lower()
remaining_args = args[1:] remaining_args = args[1:]
chat_stream = self.message.chat_stream
# 检查基本查看权限 # 检查基本查看权限
can_view = permission_api.check_permission( can_view = permission_api.check_permission(
chat_stream.user_platform, chat_stream.platform,
chat_stream.user_id, chat_stream.user_info.user_id,
"plugin.permission.view" "plugin.permission.view"
) or permission_api.is_master(chat_stream.user_platform, chat_stream.user_id) ) or permission_api.is_master(chat_stream.platform, chat_stream.user_info.user_id)
# 检查管理权限 # 检查管理权限
can_manage = permission_api.check_permission( can_manage = permission_api.check_permission(
chat_stream.user_platform, chat_stream.platform,
chat_stream.user_id, chat_stream.user_info.user_id,
"plugin.permission.manage" "plugin.permission.manage"
) or permission_api.is_master(chat_stream.user_platform, chat_stream.user_id) ) or permission_api.is_master(chat_stream.platform, chat_stream.user_info.user_id)
if subcommand in ["grant", "授权", "give"]: if subcommand in ["grant", "授权", "give"]:
if not can_manage: if not can_manage:
@@ -108,7 +107,7 @@ class PermissionCommand(BaseCommand):
else: else:
await self.send_text(f"❌ 未知的子命令: {subcommand}\n使用 /permission help 查看帮助") await self.send_text(f"❌ 未知的子命令: {subcommand}\n使用 /permission help 查看帮助")
async def _show_help(self, chat_stream: ChatStream): async def _show_help(self):
"""显示帮助信息""" """显示帮助信息"""
help_text = """📋 权限管理命令帮助 help_text = """📋 权限管理命令帮助
@@ -143,8 +142,8 @@ class PermissionCommand(BaseCommand):
return mention return mention
return None return None
async def _grant_permission(self, chat_stream: ChatStream, args: List[str]): async def _grant_permission(self, chat_stream , args: List[str]):
"""授权用户权限""" """授权用户权限"""
if len(args) < 2: if len(args) < 2:
await self.send_text("❌ 用法: /permission grant <@用户|QQ号> <权限节点>") await self.send_text("❌ 用法: /permission grant <@用户|QQ号> <权限节点>")
@@ -160,14 +159,14 @@ class PermissionCommand(BaseCommand):
return return
# 执行授权 # 执行授权
success = permission_api.grant_permission(chat_stream.user_platform, user_id, permission_node) success = permission_api.grant_permission(chat_stream.platform, user_id, permission_node)
if success: if success:
await self.send_text(f"✅ 已授权用户 {user_id} 权限节点 {permission_node}") await self.send_text(f"✅ 已授权用户 {user_id} 权限节点 {permission_node}")
else: else:
await self.send_text("❌ 授权失败,请检查权限节点是否存在") await self.send_text("❌ 授权失败,请检查权限节点是否存在")
async def _revoke_permission(self, chat_stream: ChatStream, args: List[str]): async def _revoke_permission(self, chat_stream, args: List[str]):
"""撤销用户权限""" """撤销用户权限"""
if len(args) < 2: if len(args) < 2:
await self.send_text("❌ 用法: /permission revoke <@用户|QQ号> <权限节点>") await self.send_text("❌ 用法: /permission revoke <@用户|QQ号> <权限节点>")
@@ -183,14 +182,14 @@ class PermissionCommand(BaseCommand):
return return
# 执行撤销 # 执行撤销
success = permission_api.revoke_permission(chat_stream.user_platform, user_id, permission_node) success = permission_api.revoke_permission(chat_stream.platform, user_id, permission_node)
if success: if success:
await self.send_text(f"✅ 已撤销用户 {user_id} 权限节点 {permission_node}") await self.send_text(f"✅ 已撤销用户 {user_id} 权限节点 {permission_node}")
else: else:
await self.send_text("❌ 撤销失败,请检查权限节点是否存在") await self.send_text("❌ 撤销失败,请检查权限节点是否存在")
async def _list_permissions(self, chat_stream: ChatStream, args: List[str]): async def _list_permissions(self, chat_stream, args: List[str]):
"""列出用户权限""" """列出用户权限"""
target_user_id = None target_user_id = None
@@ -203,13 +202,13 @@ class PermissionCommand(BaseCommand):
return return
else: else:
# 查看自己的权限 # 查看自己的权限
target_user_id = chat_stream.user_id target_user_id = chat_stream.user_info.user_id
# 检查是否为Master用户 # 检查是否为Master用户
is_master = permission_api.is_master(chat_stream.user_platform, target_user_id) is_master = permission_api.is_master(chat_stream.platform, target_user_id)
# 获取用户权限 # 获取用户权限
permissions = permission_api.get_user_permissions(chat_stream.user_platform, target_user_id) permissions = permission_api.get_user_permissions(chat_stream.platform, target_user_id)
if is_master: if is_master:
response = f"👑 用户 {target_user_id} 是Master用户拥有所有权限" response = f"👑 用户 {target_user_id} 是Master用户拥有所有权限"
@@ -221,8 +220,8 @@ class PermissionCommand(BaseCommand):
response = f"📋 用户 {target_user_id} 没有任何权限" response = f"📋 用户 {target_user_id} 没有任何权限"
await self.send_text(response) await self.send_text(response)
async def _check_permission(self, chat_stream: ChatStream, args: List[str]): async def _check_permission(self, chat_stream, args: List[str]):
"""检查用户权限""" """检查用户权限"""
if len(args) < 2: if len(args) < 2:
await self.send_text("❌ 用法: /permission check <@用户|QQ号> <权限节点>") await self.send_text("❌ 用法: /permission check <@用户|QQ号> <权限节点>")
@@ -238,8 +237,8 @@ class PermissionCommand(BaseCommand):
return return
# 检查权限 # 检查权限
has_permission = permission_api.check_permission(chat_stream.user_platform, user_id, permission_node) has_permission = permission_api.check_permission(chat_stream.platform, user_id, permission_node)
is_master = permission_api.is_master(chat_stream.user_platform, user_id) is_master = permission_api.is_master(chat_stream.platform, user_id)
if has_permission: if has_permission:
if is_master: if is_master:
@@ -250,8 +249,8 @@ class PermissionCommand(BaseCommand):
response = f"❌ 用户 {user_id} 没有权限 {permission_node}" response = f"❌ 用户 {user_id} 没有权限 {permission_node}"
await self.send_text(response) await self.send_text(response)
async def _list_nodes(self, chat_stream: ChatStream, args: List[str]): async def _list_nodes(self, chat_stream, args: List[str]):
"""列出权限节点""" """列出权限节点"""
plugin_name = args[0] if args else None plugin_name = args[0] if args else None

View File

@@ -433,13 +433,13 @@ class ManagementCommand(BaseCommand):
@register_plugin @register_plugin
class PluginManagementPlugin(BasePlugin): class PluginManagementPlugin(BasePlugin):
plugin_name: str = "plugin_management_plugin" plugin_name: str = "plugin_management_plugin"
enable_plugin: bool = False enable_plugin: bool = True
dependencies: list[str] = [] dependencies: list[str] = []
python_dependencies: list[str] = [] python_dependencies: list[str] = []
config_file_name: str = "config.toml" config_file_name: str = "config.toml"
config_schema: dict = { config_schema: dict = {
"plugin": { "plugin": {
"enabled": ConfigField(bool, default=False, description="是否启用插件"), "enabled": ConfigField(bool, default=True, description="是否启用插件"),
"config_version": ConfigField(type=str, default="1.1.0", description="配置文件版本"), "config_version": ConfigField(type=str, default="1.1.0", description="配置文件版本"),
"permission": ConfigField( "permission": ConfigField(
list, default=[], description="有权限使用插件管理命令的用户列表请填写字符串形式的用户ID" list, default=[], description="有权限使用插件管理命令的用户列表请填写字符串形式的用户ID"

View File

@@ -0,0 +1,160 @@
"""
MaiBot 端的消息切片处理模块
用于接收和重组来自 Napcat-Adapter 的切片消息
"""
import json
import time
import asyncio
from typing import Dict, Any, Optional
from src.common.logger import get_logger
logger = get_logger("message_chunker")
class MessageReassembler:
"""消息重组器,用于重组来自 Ada 的切片消息"""
def __init__(self, timeout: int = 30):
self.timeout = timeout
self.chunk_buffers: Dict[str, Dict[str, Any]] = {}
self._cleanup_task = None
async def start_cleanup_task(self):
"""启动清理任务"""
if self._cleanup_task is None:
self._cleanup_task = asyncio.create_task(self._cleanup_expired_chunks())
logger.info("消息重组器清理任务已启动")
async def stop_cleanup_task(self):
"""停止清理任务"""
if self._cleanup_task:
self._cleanup_task.cancel()
try:
await self._cleanup_task
except asyncio.CancelledError:
pass
self._cleanup_task = None
logger.info("消息重组器清理任务已停止")
async def _cleanup_expired_chunks(self):
"""清理过期的切片缓冲区"""
while True:
try:
await asyncio.sleep(10) # 每10秒检查一次
current_time = time.time()
expired_chunks = []
for chunk_id, buffer_info in self.chunk_buffers.items():
if current_time - buffer_info['timestamp'] > self.timeout:
expired_chunks.append(chunk_id)
for chunk_id in expired_chunks:
logger.warning(f"清理过期的切片缓冲区: {chunk_id}")
del self.chunk_buffers[chunk_id]
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"清理过期切片时出错: {e}")
def is_chunk_message(self, message: Dict[str, Any]) -> bool:
"""检查是否是来自 Ada 的切片消息"""
return (
isinstance(message, dict) and
"__mmc_chunk_info__" in message and
"__mmc_chunk_data__" in message and
"__mmc_is_chunked__" in message
)
async def process_chunk(self, message: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""
处理切片消息,如果切片完整则返回重组后的消息
Args:
message: 可能的切片消息
Returns:
如果切片完整则返回重组后的原始消息否则返回None
"""
# 如果不是切片消息,直接返回
if not self.is_chunk_message(message):
return message
try:
chunk_info = message["__mmc_chunk_info__"]
chunk_content = message["__mmc_chunk_data__"]
chunk_id = chunk_info["chunk_id"]
chunk_index = chunk_info["chunk_index"]
total_chunks = chunk_info["total_chunks"]
chunk_timestamp = chunk_info.get("timestamp", time.time())
# 初始化缓冲区
if chunk_id not in self.chunk_buffers:
self.chunk_buffers[chunk_id] = {
"chunks": {},
"total_chunks": total_chunks,
"received_chunks": 0,
"timestamp": chunk_timestamp
}
logger.debug(f"初始化切片缓冲区: {chunk_id} (总计 {total_chunks} 个切片)")
buffer = self.chunk_buffers[chunk_id]
# 检查切片是否已经接收过
if chunk_index in buffer["chunks"]:
logger.warning(f"重复接收切片: {chunk_id}#{chunk_index}")
return None
# 添加切片
buffer["chunks"][chunk_index] = chunk_content
buffer["received_chunks"] += 1
buffer["timestamp"] = time.time() # 更新时间戳
logger.debug(f"接收切片: {chunk_id}#{chunk_index} ({buffer['received_chunks']}/{total_chunks})")
# 检查是否接收完整
if buffer["received_chunks"] == total_chunks:
# 重组消息
reassembled_message = ""
for i in range(total_chunks):
if i not in buffer["chunks"]:
logger.error(f"切片 {chunk_id}#{i} 缺失,无法重组")
return None
reassembled_message += buffer["chunks"][i]
# 清理缓冲区
del self.chunk_buffers[chunk_id]
logger.info(f"消息重组完成: {chunk_id} ({len(reassembled_message)} chars)")
# 尝试反序列化重组后的消息
try:
return json.loads(reassembled_message)
except json.JSONDecodeError as e:
logger.error(f"重组消息反序列化失败: {e}")
return None
# 还没收集完所有切片返回None表示继续等待
return None
except (KeyError, TypeError, ValueError) as e:
logger.error(f"处理切片消息时出错: {e}")
return None
def get_pending_chunks_info(self) -> Dict[str, Any]:
"""获取待处理切片信息"""
info = {}
for chunk_id, buffer in self.chunk_buffers.items():
info[chunk_id] = {
"received": buffer["received_chunks"],
"total": buffer["total_chunks"],
"progress": f"{buffer['received_chunks']}/{buffer['total_chunks']}",
"age_seconds": time.time() - buffer["timestamp"]
}
return info
# 全局重组器实例
reassembler = MessageReassembler()

View File

@@ -1,5 +1,5 @@
[inner] [inner]
version = "6.4.4" version = "6.4.6"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件请递增version的值 #如果你想要修改配置文件请递增version的值
@@ -387,6 +387,8 @@ max_frames = 16 # 最大分析帧数(仅在 mode = "fixed_number" 时生效)
frame_quality = 80 # 帧图像JPEG质量 (1-100) frame_quality = 80 # 帧图像JPEG质量 (1-100)
max_image_size = 800 # 单帧最大图像尺寸(像素) max_image_size = 800 # 单帧最大图像尺寸(像素)
enable_frame_timing = true # 是否在分析中包含帧的时间信息 enable_frame_timing = true # 是否在分析中包含帧的时间信息
use_multiprocessing = true # 是否使用线程池处理视频帧提取(推荐开启,可防止卡死)
max_workers = 2 # 最大线程数建议1-2个避免过度消耗资源
# 批量分析时使用的提示词 # 批量分析时使用的提示词
batch_analysis_prompt = """请分析这个视频的内容。这些图片是从视频中按时间顺序提取的关键帧。 batch_analysis_prompt = """请分析这个视频的内容。这些图片是从视频中按时间顺序提取的关键帧。