Lib.core.PluginManager
插件管理器
1""" 2插件管理器 3""" 4 5import dataclasses 6import importlib 7import inspect 8import sys 9import traceback 10 11from Lib.common import save_exc_dump 12from Lib.constants import * 13from Lib.core import ConfigManager 14from Lib.core.EventManager import event_listener 15from Lib.core.ListenerServer import EscalationEvent 16from Lib.utils.Logger import get_logger 17 18logger = get_logger() 19 20plugins: list[dict] = [] 21found_plugins: list[dict] = [] 22has_main_func_plugins: list[dict] = [] 23 24if not os.path.exists(PLUGINS_PATH): 25 os.makedirs(PLUGINS_PATH) 26 27 28class NotEnabledPluginException(Exception): 29 """ 30 插件未启用的异常 31 """ 32 pass 33 34 35def load_plugin(plugin): 36 """ 37 加载插件 38 Args: 39 plugin: 插件信息 40 """ 41 name = plugin["name"] 42 full_path = plugin["path"] 43 is_package = os.path.isdir(full_path) and os.path.exists(os.path.join(full_path, "__init__.py")) 44 45 # 计算导入路径 46 # 获取相对于 WORK_PATH 的路径,例如 "plugins/AIChat" 或 "plugins/single_file_plugin.py" 47 relative_plugin_path = os.path.relpath(full_path, start=WORK_PATH) 48 49 # 将路径分隔符替换为点,例如 "plugins.AIChat" 或 "plugins.single_file_plugin" 50 import_path = relative_plugin_path.replace(os.sep, '.') 51 if not is_package and import_path.endswith('.py'): 52 import_path = import_path[:-3] # 去掉 .py 后缀 53 54 logger.debug(f"计算 {name} 得到的导入路径: {import_path}") 55 56 if WORK_PATH not in sys.path: 57 logger.warning(f"项目根目录 {WORK_PATH} 不在 sys.path 中,正在添加。请检查执行环境。") 58 sys.path.insert(0, WORK_PATH) # 插入到前面,优先查找 59 60 try: 61 logger.debug(f"尝试加载: {import_path}") 62 module = importlib.import_module(import_path) 63 except ImportError as e: 64 logger.error(f"加载 {import_path} 失败: {repr(e)}\n" 65 f"{traceback.format_exc()}") 66 raise 67 68 plugin_info = None 69 try: 70 if isinstance(module.plugin_info, PluginInfo): 71 plugin_info = module.plugin_info 72 else: 73 logger.warning(f"插件 {name} 的 plugin_info 并非 PluginInfo 类型,无法获取插件信息") 74 except AttributeError: 75 logger.warning(f"插件 {name} 未定义 plugin_info 属性,无法获取插件信息") 76 77 return module, plugin_info 78 79 80def load_plugins(): 81 """ 82 加载插件 83 """ 84 global plugins, found_plugins 85 86 found_plugins = [] 87 # 获取插件目录下的所有文件 88 for plugin in os.listdir(PLUGINS_PATH): 89 if plugin == "__pycache__": 90 continue 91 full_path = os.path.join(PLUGINS_PATH, plugin) 92 if ( 93 os.path.isdir(full_path) and 94 os.path.exists(os.path.join(full_path, "__init__.py")) and 95 os.path.isfile(os.path.join(full_path, "__init__.py")) 96 ): 97 file_path = os.path.join(os.path.join(full_path, "__init__.py")) 98 name = plugin 99 elif os.path.isfile(full_path) and full_path.endswith(".py"): 100 file_path = full_path 101 name = os.path.split(file_path)[1] 102 else: 103 logger.warning(f"{full_path} 不是一个有效的插件") 104 continue 105 logger.debug(f"找到插件 {file_path} 待加载") 106 plugin = {"name": name, "plugin": None, "info": None, "file_path": file_path, "path": full_path} 107 found_plugins.append(plugin) 108 109 plugins = [] 110 111 for plugin in found_plugins: 112 name = plugin["name"] 113 full_path = plugin["path"] 114 115 if plugin["plugin"] is not None: 116 # 由于其他原因已被加载(例如插件依赖) 117 logger.debug(f"插件 {name} 已被加载,跳过加载") 118 continue 119 120 logger.debug(f"开始尝试加载插件 {full_path}") 121 122 try: 123 module, plugin_info = load_plugin(plugin) 124 125 plugin["info"] = plugin_info 126 plugin["plugin"] = module 127 plugins.append(plugin) 128 except NotEnabledPluginException: 129 logger.warning(f"插件 {name}({full_path}) 已被禁用,将不会被加载") 130 continue 131 except Exception as e: 132 if ConfigManager.GlobalConfig().debug.save_dump: 133 dump_path = save_exc_dump(f"尝试加载插件 {full_path} 时失败") 134 else: 135 dump_path = None 136 logger.error(f"尝试加载插件 {full_path} 时失败! 原因:{repr(e)}\n" 137 f"{"".join(traceback.format_exc())}" 138 f"{f"\n已保存异常到 {dump_path}" if dump_path else ""}") 139 continue 140 141 logger.debug(f"插件 {name}({full_path}) 加载成功!") 142 143 144@dataclasses.dataclass 145class PluginInfo: 146 """ 147 插件信息 148 """ 149 NAME: str # 插件名称 150 AUTHOR: str # 插件作者 151 VERSION: str # 插件版本 152 DESCRIPTION: str # 插件描述 153 HELP_MSG: str # 插件帮助 154 ENABLED: bool = True # 插件是否启用 155 IS_HIDDEN: bool = False # 插件是否隐藏(在/help命令中) 156 extra: dict | None = None # 一个字典,可以用于存储任意信息。其他插件可以通过约定 extra 字典的键名来达成收集某些特殊信息的目的。 157 158 def __post_init__(self): 159 if self.ENABLED is not True: 160 raise NotEnabledPluginException 161 if self.extra is None: 162 self.extra = {} 163 164 165def requirement_plugin(plugin_name: str): 166 """ 167 插件依赖 168 Args: 169 plugin_name: 插件的名称,如果依赖的是库形式的插件则是库文件夹的名称,如果依赖的是文件形式则是插件文件的名称(文件名称包含后缀) 170 171 Returns: 172 依赖的插件的信息 173 """ 174 logger.debug(f"由于插件依赖,正在尝试加载插件 {plugin_name}") 175 for plugin in found_plugins: 176 if plugin["name"] == plugin_name: 177 if plugin not in plugins: 178 try: 179 module, plugin_info = load_plugin(plugin) 180 plugin["info"] = plugin_info 181 plugin["plugin"] = module 182 plugins.append(plugin) 183 except NotEnabledPluginException: 184 logger.error(f"被依赖的插件 {plugin_name} 已被禁用,无法加载依赖") 185 raise Exception(f"被依赖的插件 {plugin_name} 已被禁用,无法加载依赖") 186 except Exception as e: 187 if ConfigManager.GlobalConfig().debug.save_dump: 188 dump_path = save_exc_dump(f"尝试加载被依赖的插件 {plugin_name} 时失败!") 189 else: 190 dump_path = None 191 logger.error(f"尝试加载被依赖的插件 {plugin_name} 时失败! 原因:{repr(e)}\n" 192 f"{"".join(traceback.format_exc())}" 193 f"{f"\n已保存异常到 {dump_path}" if dump_path else ""}") 194 raise e 195 logger.debug(f"由于插件依赖,插件 {plugin_name} 加载成功!") 196 else: 197 logger.debug(f"由于插件依赖,插件 {plugin_name} 已被加载,跳过加载") 198 return plugin 199 else: 200 raise FileNotFoundError(f"插件 {plugin_name} 不存在或不符合要求,无法加载依赖") 201 202 203# 该方法已被弃用 204def run_plugin_main(event_data): 205 """ 206 运行插件的main函数 207 Args: 208 event_data: 事件数据 209 """ 210 global has_main_func_plugins 211 for plugin in has_main_func_plugins: 212 logger.debug(f"执行插件: {plugin['name']}") 213 try: 214 plugin["plugin"].main(event_data, WORK_PATH) 215 except Exception as e: 216 logger.error(f"执行插件{plugin['name']}时发生错误: {repr(e)}") 217 continue 218 219 220@event_listener(EscalationEvent) 221def run_plugin_main_wrapper(event): 222 """ 223 运行插件的main函数 224 Args: 225 event: 事件 226 """ 227 run_plugin_main(event.event_data) 228 229 230def get_caller_plugin_data(): 231 """ 232 获取调用者的插件数据 233 :return: 234 plugin_data: dict | None 235 """ 236 237 stack = inspect.stack()[1:] 238 for frame_info in stack: 239 filename = frame_info.filename 240 241 normalized_filename = os.path.normpath(filename) 242 normalized_plugins_path = os.path.normpath(PLUGINS_PATH) 243 244 if normalized_filename.startswith(normalized_plugins_path): 245 for plugin in found_plugins: 246 normalized_plugin_file_path = os.path.normpath(plugin["file_path"]) 247 plugin_dir, plugin_file = os.path.split(normalized_plugin_file_path) 248 249 if plugin_dir == normalized_plugins_path: 250 if normalized_plugin_file_path == normalized_filename: 251 return plugin 252 else: 253 if normalized_filename.startswith(plugin_dir): 254 return plugin 255 return None
logger =
<RootLogger root (INFO)>
plugins: list[dict] =
[]
found_plugins: list[dict] =
[]
has_main_func_plugins: list[dict] =
[]
class
NotEnabledPluginException(builtins.Exception):
插件未启用的异常
def
load_plugin(plugin):
36def load_plugin(plugin): 37 """ 38 加载插件 39 Args: 40 plugin: 插件信息 41 """ 42 name = plugin["name"] 43 full_path = plugin["path"] 44 is_package = os.path.isdir(full_path) and os.path.exists(os.path.join(full_path, "__init__.py")) 45 46 # 计算导入路径 47 # 获取相对于 WORK_PATH 的路径,例如 "plugins/AIChat" 或 "plugins/single_file_plugin.py" 48 relative_plugin_path = os.path.relpath(full_path, start=WORK_PATH) 49 50 # 将路径分隔符替换为点,例如 "plugins.AIChat" 或 "plugins.single_file_plugin" 51 import_path = relative_plugin_path.replace(os.sep, '.') 52 if not is_package and import_path.endswith('.py'): 53 import_path = import_path[:-3] # 去掉 .py 后缀 54 55 logger.debug(f"计算 {name} 得到的导入路径: {import_path}") 56 57 if WORK_PATH not in sys.path: 58 logger.warning(f"项目根目录 {WORK_PATH} 不在 sys.path 中,正在添加。请检查执行环境。") 59 sys.path.insert(0, WORK_PATH) # 插入到前面,优先查找 60 61 try: 62 logger.debug(f"尝试加载: {import_path}") 63 module = importlib.import_module(import_path) 64 except ImportError as e: 65 logger.error(f"加载 {import_path} 失败: {repr(e)}\n" 66 f"{traceback.format_exc()}") 67 raise 68 69 plugin_info = None 70 try: 71 if isinstance(module.plugin_info, PluginInfo): 72 plugin_info = module.plugin_info 73 else: 74 logger.warning(f"插件 {name} 的 plugin_info 并非 PluginInfo 类型,无法获取插件信息") 75 except AttributeError: 76 logger.warning(f"插件 {name} 未定义 plugin_info 属性,无法获取插件信息") 77 78 return module, plugin_info
加载插件
Arguments:
- plugin: 插件信息
def
load_plugins():
81def load_plugins(): 82 """ 83 加载插件 84 """ 85 global plugins, found_plugins 86 87 found_plugins = [] 88 # 获取插件目录下的所有文件 89 for plugin in os.listdir(PLUGINS_PATH): 90 if plugin == "__pycache__": 91 continue 92 full_path = os.path.join(PLUGINS_PATH, plugin) 93 if ( 94 os.path.isdir(full_path) and 95 os.path.exists(os.path.join(full_path, "__init__.py")) and 96 os.path.isfile(os.path.join(full_path, "__init__.py")) 97 ): 98 file_path = os.path.join(os.path.join(full_path, "__init__.py")) 99 name = plugin 100 elif os.path.isfile(full_path) and full_path.endswith(".py"): 101 file_path = full_path 102 name = os.path.split(file_path)[1] 103 else: 104 logger.warning(f"{full_path} 不是一个有效的插件") 105 continue 106 logger.debug(f"找到插件 {file_path} 待加载") 107 plugin = {"name": name, "plugin": None, "info": None, "file_path": file_path, "path": full_path} 108 found_plugins.append(plugin) 109 110 plugins = [] 111 112 for plugin in found_plugins: 113 name = plugin["name"] 114 full_path = plugin["path"] 115 116 if plugin["plugin"] is not None: 117 # 由于其他原因已被加载(例如插件依赖) 118 logger.debug(f"插件 {name} 已被加载,跳过加载") 119 continue 120 121 logger.debug(f"开始尝试加载插件 {full_path}") 122 123 try: 124 module, plugin_info = load_plugin(plugin) 125 126 plugin["info"] = plugin_info 127 plugin["plugin"] = module 128 plugins.append(plugin) 129 except NotEnabledPluginException: 130 logger.warning(f"插件 {name}({full_path}) 已被禁用,将不会被加载") 131 continue 132 except Exception as e: 133 if ConfigManager.GlobalConfig().debug.save_dump: 134 dump_path = save_exc_dump(f"尝试加载插件 {full_path} 时失败") 135 else: 136 dump_path = None 137 logger.error(f"尝试加载插件 {full_path} 时失败! 原因:{repr(e)}\n" 138 f"{"".join(traceback.format_exc())}" 139 f"{f"\n已保存异常到 {dump_path}" if dump_path else ""}") 140 continue 141 142 logger.debug(f"插件 {name}({full_path}) 加载成功!")
加载插件
@dataclasses.dataclass
class
PluginInfo:
145@dataclasses.dataclass 146class PluginInfo: 147 """ 148 插件信息 149 """ 150 NAME: str # 插件名称 151 AUTHOR: str # 插件作者 152 VERSION: str # 插件版本 153 DESCRIPTION: str # 插件描述 154 HELP_MSG: str # 插件帮助 155 ENABLED: bool = True # 插件是否启用 156 IS_HIDDEN: bool = False # 插件是否隐藏(在/help命令中) 157 extra: dict | None = None # 一个字典,可以用于存储任意信息。其他插件可以通过约定 extra 字典的键名来达成收集某些特殊信息的目的。 158 159 def __post_init__(self): 160 if self.ENABLED is not True: 161 raise NotEnabledPluginException 162 if self.extra is None: 163 self.extra = {}
插件信息
def
requirement_plugin(plugin_name: str):
166def requirement_plugin(plugin_name: str): 167 """ 168 插件依赖 169 Args: 170 plugin_name: 插件的名称,如果依赖的是库形式的插件则是库文件夹的名称,如果依赖的是文件形式则是插件文件的名称(文件名称包含后缀) 171 172 Returns: 173 依赖的插件的信息 174 """ 175 logger.debug(f"由于插件依赖,正在尝试加载插件 {plugin_name}") 176 for plugin in found_plugins: 177 if plugin["name"] == plugin_name: 178 if plugin not in plugins: 179 try: 180 module, plugin_info = load_plugin(plugin) 181 plugin["info"] = plugin_info 182 plugin["plugin"] = module 183 plugins.append(plugin) 184 except NotEnabledPluginException: 185 logger.error(f"被依赖的插件 {plugin_name} 已被禁用,无法加载依赖") 186 raise Exception(f"被依赖的插件 {plugin_name} 已被禁用,无法加载依赖") 187 except Exception as e: 188 if ConfigManager.GlobalConfig().debug.save_dump: 189 dump_path = save_exc_dump(f"尝试加载被依赖的插件 {plugin_name} 时失败!") 190 else: 191 dump_path = None 192 logger.error(f"尝试加载被依赖的插件 {plugin_name} 时失败! 原因:{repr(e)}\n" 193 f"{"".join(traceback.format_exc())}" 194 f"{f"\n已保存异常到 {dump_path}" if dump_path else ""}") 195 raise e 196 logger.debug(f"由于插件依赖,插件 {plugin_name} 加载成功!") 197 else: 198 logger.debug(f"由于插件依赖,插件 {plugin_name} 已被加载,跳过加载") 199 return plugin 200 else: 201 raise FileNotFoundError(f"插件 {plugin_name} 不存在或不符合要求,无法加载依赖")
插件依赖
Arguments:
- plugin_name: 插件的名称,如果依赖的是库形式的插件则是库文件夹的名称,如果依赖的是文件形式则是插件文件的名称(文件名称包含后缀)
Returns:
依赖的插件的信息
def
run_plugin_main(event_data):
205def run_plugin_main(event_data): 206 """ 207 运行插件的main函数 208 Args: 209 event_data: 事件数据 210 """ 211 global has_main_func_plugins 212 for plugin in has_main_func_plugins: 213 logger.debug(f"执行插件: {plugin['name']}") 214 try: 215 plugin["plugin"].main(event_data, WORK_PATH) 216 except Exception as e: 217 logger.error(f"执行插件{plugin['name']}时发生错误: {repr(e)}") 218 continue
运行插件的main函数
Arguments:
- event_data: 事件数据
@event_listener(EscalationEvent)
def
run_plugin_main_wrapper(event):
221@event_listener(EscalationEvent) 222def run_plugin_main_wrapper(event): 223 """ 224 运行插件的main函数 225 Args: 226 event: 事件 227 """ 228 run_plugin_main(event.event_data)
运行插件的main函数
Arguments:
- event: 事件
def
get_caller_plugin_data():
231def get_caller_plugin_data(): 232 """ 233 获取调用者的插件数据 234 :return: 235 plugin_data: dict | None 236 """ 237 238 stack = inspect.stack()[1:] 239 for frame_info in stack: 240 filename = frame_info.filename 241 242 normalized_filename = os.path.normpath(filename) 243 normalized_plugins_path = os.path.normpath(PLUGINS_PATH) 244 245 if normalized_filename.startswith(normalized_plugins_path): 246 for plugin in found_plugins: 247 normalized_plugin_file_path = os.path.normpath(plugin["file_path"]) 248 plugin_dir, plugin_file = os.path.split(normalized_plugin_file_path) 249 250 if plugin_dir == normalized_plugins_path: 251 if normalized_plugin_file_path == normalized_filename: 252 return plugin 253 else: 254 if normalized_filename.startswith(plugin_dir): 255 return plugin 256 return None
获取调用者的插件数据
Returns
plugin_data: dict | None