a13rollcall/plugin_manager.py
2026-10-06 18:38:55 +08:00

1865 行
69 KiB
Python

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

import os
import json
import sys
import traceback
from datetime import datetime
from collections import defaultdict
class PluginLogger:
def __init__(self, plugin_name, log_dir):
self.plugin_name = plugin_name
self.log_dir = log_dir
self.log_file = os.path.join(log_dir, f"{plugin_name}.log")
self._ensure_dir()
def _ensure_dir(self):
if not os.path.exists(self.log_dir):
os.makedirs(self.log_dir)
def _write(self, level, message):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_line = f"[{timestamp}] [{level}] {message}\n"
try:
with open(self.log_file, "a", encoding="utf-8") as f:
f.write(log_line)
except Exception:
pass
def info(self, message):
self._write("INFO", message)
def warning(self, message):
self._write("WARNING", message)
def error(self, message):
self._write("ERROR", message)
def debug(self, message):
self._write("DEBUG", message)
def get_logs(self, lines=100):
if not os.path.exists(self.log_file):
return []
try:
with open(self.log_file, "r", encoding="utf-8") as f:
all_lines = f.readlines()
return all_lines[-lines:]
except Exception:
return []
def clear(self):
if os.path.exists(self.log_file):
os.remove(self.log_file)
class PluginDataManager:
def __init__(self, plugin_name, data_dir):
self.plugin_name = plugin_name
self.data_dir = data_dir
self.config_file = os.path.join(data_dir, "config.json")
self._ensure_dir()
def _ensure_dir(self):
if not os.path.exists(self.data_dir):
os.makedirs(self.data_dir)
def load_config(self):
if not os.path.exists(self.config_file):
return {}
try:
with open(self.config_file, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return {}
def save_config(self, config):
try:
with open(self.config_file, "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=2)
return True
except Exception:
return False
def get(self, key, default=None):
config = self.load_config()
return config.get(key, default)
def set(self, key, value):
config = self.load_config()
config[key] = value
return self.save_config(config)
def get_data_file(self, filename):
return os.path.join(self.data_dir, filename)
def read_data(self, filename):
file_path = self.get_data_file(filename)
if not os.path.exists(file_path):
return None
try:
with open(file_path, "r", encoding="utf-8") as f:
return f.read()
except Exception:
return None
def write_data(self, filename, content):
file_path = self.get_data_file(filename)
try:
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
return True
except Exception:
return False
def read_json(self, filename):
content = self.read_data(filename)
if content:
try:
return json.loads(content)
except Exception:
return None
return None
def write_json(self, filename, data):
try:
content = json.dumps(data, ensure_ascii=False, indent=2)
return self.write_data(filename, content)
except Exception:
return False
class ArcxParser:
MAGIC = "ARCX"
VERSION = 1
@staticmethod
def parse(file_path):
if not os.path.exists(file_path):
return None, "文件不存在"
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
if content.startswith("ARCX"):
return ArcxParser._parse_binary(content)
else:
data = json.loads(content)
return ArcxParser._validate(data), None
except json.JSONDecodeError as e:
return None, f"JSON 解析错误:{str(e)}"
except Exception as e:
return None, f"解析失败:{str(e)}"
@staticmethod
def _parse_binary(content):
try:
lines = content.split("\n")
meta = {}
code_start = -1
for i, line in enumerate(lines):
if line.startswith("META:"):
meta_part = line[5:].strip()
meta = json.loads(meta_part)
elif line.startswith("CODE:"):
code_start = i + 1
break
code = "\n".join(lines[code_start:]) if code_start > 0 else ""
data = {
"meta": meta,
"code": code,
"config": meta.get("config", {})
}
return ArcxParser._validate(data), None
except Exception as e:
return None, f"二进制格式解析失败:{str(e)}"
@staticmethod
def _validate(data):
required = ["meta", "code"]
for key in required:
if key not in data:
data[key] = {} if key == "meta" else ""
meta = data.get("meta", {})
meta.setdefault("name", "未命名插件")
meta.setdefault("version", "1.0.0")
meta.setdefault("author", "未知")
meta.setdefault("description", "")
meta.setdefault("type", "general")
meta.setdefault("api_version", 1)
meta.setdefault("has_settings_page", False)
meta.setdefault("settings_page_title", "")
data["meta"] = meta
data.setdefault("config", {})
data.setdefault("enabled", True)
data.setdefault("installed_at", datetime.now().isoformat())
return data
@staticmethod
def create_arcx(name, description, code, config=None, author="自定义", version="1.0.0", has_settings_page=False):
data = {
"meta": {
"name": name,
"version": version,
"author": author,
"description": description,
"type": "general",
"api_version": 1,
"has_settings_page": has_settings_page,
"settings_page_title": name,
"config": config or {}
},
"code": code,
"config": config or {}
}
return json.dumps(data, ensure_ascii=False, indent=2)
class PluginContext:
def __init__(self, plugin, app, plugin_mgr):
self.plugin = plugin
self.app = app
self.plugin_mgr = plugin_mgr
self._variables = {}
self._ui_elements = []
self._event_handlers = defaultdict(list)
self._loaded_libs = []
plugin_name = plugin["meta"]["name"]
base_dir = os.path.dirname(os.path.abspath(__file__))
self.data_dir = os.path.join(base_dir, "plugins", "data", plugin_name)
self.libs_dir = os.path.join(self.data_dir, "libs")
self.log_dir = os.path.join(base_dir, "plugins", "logs")
self.logger = PluginLogger(plugin_name, self.log_dir)
self.data = PluginDataManager(plugin_name, self.data_dir)
self._load_plugin_libs()
self.logger.info(f"插件上下文初始化完成,数据目录:{self.data_dir}")
def _load_plugin_libs(self):
"""加载插件自带的依赖库到sys.path"""
if os.path.exists(self.libs_dir):
if self.libs_dir not in sys.path:
sys.path.insert(0, self.libs_dir)
self._loaded_libs.append(self.libs_dir)
self.logger.info(f"已加载插件依赖库目录:{self.libs_dir}")
# 检查libs目录下的内容
try:
lib_items = os.listdir(self.libs_dir)
self.logger.info(f"插件依赖库包含 {len(lib_items)} 个项目:{', '.join(lib_items[:10])}")
except Exception as e:
self.logger.warning(f"读取依赖库目录失败:{e}")
def get_libs_dir(self):
"""获取插件依赖库目录"""
return self.libs_dir
def get_icons_dir(self):
"""获取插件图标资源目录"""
return os.path.join(self.data_dir, "icons")
def has_icon(self, icon_name):
"""检查插件是否包含某个图标"""
icons_dir = self.get_icons_dir()
for ext in ['.ico', '.png', '.svg']:
icon_path = os.path.join(icons_dir, icon_name + ext)
if os.path.exists(icon_path):
return True
return os.path.exists(os.path.join(icons_dir, icon_name))
def get_icon_path(self, icon_name):
"""获取插件图标的完整路径"""
icons_dir = self.get_icons_dir()
for ext in ['.ico', '.png', '.svg']:
icon_path = os.path.join(icons_dir, icon_name + ext)
if os.path.exists(icon_path):
return icon_path
direct_path = os.path.join(icons_dir, icon_name)
if os.path.exists(direct_path):
return direct_path
return None
def list_icons(self):
"""列出插件自带的所有图标"""
icons_dir = self.get_icons_dir()
if not os.path.exists(icons_dir):
return []
try:
return [f for f in os.listdir(icons_dir) if f.endswith(('.ico', '.png', '.svg'))]
except Exception:
return []
def load_icon(self, icon_name):
"""加载插件图标为 QIcon"""
try:
from PyQt5.QtGui import QIcon
icon_path = self.get_icon_path(icon_name)
if icon_path:
return QIcon(icon_path)
except Exception as e:
self.logger.error(f"加载图标失败: {e}")
return None
def has_lib(self, lib_name):
"""检查插件是否自带某个依赖库"""
lib_path = os.path.join(self.libs_dir, lib_name)
return os.path.exists(lib_path) or os.path.exists(lib_path + ".py")
def list_libs(self):
"""列出插件自带的所有依赖库"""
if not os.path.exists(self.libs_dir):
return []
try:
return os.listdir(self.libs_dir)
except Exception:
return []
def set(self, key, value):
self._variables[key] = value
self.logger.debug(f"设置变量 {key} = {value}")
def get(self, key, default=None):
return self._variables.get(key, default)
def show_message(self, title, message):
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtCore import Qt
self.logger.info(f"显示消息框:{title}")
try:
box = QMessageBox(self._app.activeWindow() if getattr(self, "_app", None) else None)
box.setWindowTitle(title)
box.setText(message)
box.setIcon(QMessageBox.Information)
box.setWindowFlag(Qt.WindowStaysOnTopHint, True)
box.show()
except Exception:
QMessageBox.information(None, title, message)
def show_custom_dialog(self, title, content, buttons=None):
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton, QHBoxLayout
self.logger.info(f"显示自定义对话框:{title}")
dlg = QDialog()
dlg.setWindowTitle(title)
dlg.setMinimumSize(400, 300)
layout = QVBoxLayout(dlg)
if isinstance(content, str):
label = QLabel(content)
label.setWordWrap(True)
layout.addWidget(label)
btn_layout = QHBoxLayout()
buttons = buttons or ["确定"]
for btn_text in buttons:
btn = QPushButton(btn_text)
btn.clicked.connect(dlg.accept)
btn_layout.addWidget(btn)
layout.addLayout(btn_layout)
dlg.exec_()
return True
def get_config(self, key, default=None):
return self.data.get(key, default)
def set_config(self, key, value):
self.logger.info(f"保存配置 {key} = {value}")
return self.data.set(key, value)
def is_frozen(self):
"""检测是否在打包环境(PyInstaller)中运行"""
return getattr(sys, 'frozen', False)
def get_app_dir(self):
"""获取应用程序目录(兼容打包环境和源码运行)"""
if self.is_frozen():
return os.path.dirname(sys.executable)
return os.path.dirname(os.path.abspath(__file__))
def get_python_version(self):
"""获取Python版本信息"""
return {
"version": sys.version,
"version_info": {
"major": sys.version_info.major,
"minor": sys.version_info.minor,
"micro": sys.version_info.micro,
},
"executable": sys.executable,
"frozen": self.is_frozen()
}
def check_dependency(self, module_name):
"""检查依赖模块是否可用"""
try:
__import__(module_name)
return True
except ImportError:
return False
def get_available_modules(self):
"""获取常用模块的可用状态"""
common_modules = [
"os", "sys", "json", "re", "math", "random", "datetime",
"subprocess", "threading", "socket", "urllib", "http",
"PyQt5", "PyQt5.QtCore", "PyQt5.QtGui", "PyQt5.QtWidgets",
"numpy", "requests", "PIL", "cv2", "pandas", "matplotlib",
"websockets", "flask", "serial", "pygame"
]
available = {}
for module in common_modules:
available[module] = self.check_dependency(module)
return available
def get_resource_path(self, relative_path):
"""获取资源文件路径(兼容打包环境)"""
if self.is_frozen():
base_path = sys._MEIPASS if hasattr(sys, '_MEIPASS') else self.get_app_dir()
return os.path.join(base_path, relative_path)
return os.path.join(self.get_app_dir(), relative_path)
def get_students(self):
if hasattr(self.app, '_students'):
return self.app._students
return []
def get_current_student(self):
if hasattr(self.app, '_current_student'):
return self.app._current_student
return None
def get_draw_state(self):
is_drawing = False
drawn_count = 0
remaining = 0
if hasattr(self.app, '_is_drawing'):
is_drawing = self.app._is_drawing
if hasattr(self.app, '_drawn_students'):
drawn_count = len(self.app._drawn_students)
students = self.get_students()
remaining = max(0, len(students) - drawn_count)
return {
"is_drawing": is_drawing,
"drawn_count": drawn_count,
"remaining": remaining,
"total": len(students),
"current": self.get_current_student(),
"last_result": self.get_last_draw_result(),
}
def get_all_stats(self):
stats = {
"已背过": 0,
"未背熟": 0,
"未背过": 0,
"未标记": 0,
}
try:
if hasattr(self.app, 'data_mgr') and self.app.data_mgr:
if hasattr(self.app.data_mgr, 'get_status_stats'):
data_stats = self.app.data_mgr.get_status_stats()
if data_stats:
stats.update(data_stats)
elif hasattr(self.app.data_mgr, 'status_stats'):
stats.update(self.app.data_mgr.status_stats)
except Exception as e:
self.logger.error(f"获取统计失败: {e}")
return stats
def get_drawn_students(self):
if hasattr(self.app, '_drawn_students'):
return list(self.app._drawn_students)
return []
def get_remaining_students(self):
all_students = self.get_students()
drawn = self.get_drawn_students()
return [s for s in all_students if s not in drawn]
def get_last_draw_result(self):
if hasattr(self.app, '_last_draw_result') and self.app._last_draw_result:
return self.app._last_draw_result
return None
def get_window_state(self):
is_drawing = False
is_minimized = False
is_quick_mode = False
state = "main"
current_window = "主界面"
if hasattr(self.app, '_is_drawing'):
is_drawing = self.app._is_drawing
if hasattr(self.app, 'isMinimized'):
try:
is_minimized = self.app.isMinimized()
except Exception:
pass
if hasattr(self.app, '_quick_mode'):
is_quick_mode = self.app._quick_mode
if is_minimized:
state = "minimized"
current_window = "最小化"
elif is_drawing:
state = "drawing"
current_window = "正在抽取"
elif is_quick_mode:
state = "quick"
current_window = "快捷抽取"
else:
state = "main"
current_window = "主界面"
return {
"state": state,
"is_drawing": is_drawing,
"is_minimized": is_minimized,
"is_quick_mode": is_quick_mode,
"current_window": current_window
}
def trigger_draw(self):
self.logger.info("触发抽取")
if hasattr(self.app, '_start_draw'):
self.app._start_draw()
def add_status_bar_text(self, text):
self.logger.debug(f"状态栏文本:{text}")
if hasattr(self.app, 'statusBar'):
self.app.statusBar().showMessage(text, 5000)
def log(self, message):
self.logger.info(message)
def log_info(self, message):
self.logger.info(message)
def log_error(self, message):
self.logger.error(message)
def log_warning(self, message):
self.logger.warning(message)
def log_debug(self, message):
self.logger.debug(message)
def log_success(self, message):
self.logger.info(f"✅ {message}")
def log_failure(self, message):
self.logger.error(f"❌ {message}")
def register_event_handler(self, event_name, handler):
self._event_handlers[event_name].append(handler)
self.logger.info(f"注册事件处理器:{event_name}")
def emit_event(self, event_name, *args, **kwargs):
self.logger.debug(f"触发事件:{event_name}")
for handler in self._event_handlers.get(event_name, []):
try:
handler(*args, **kwargs)
except Exception as e:
self.logger.error(f"事件处理器执行错误:{str(e)}")
def get_logs(self, lines=100):
return self.logger.get_logs(lines)
def clear_logs(self):
self.logger.clear()
self.logger.info("日志已清空")
def get_app_config(self, key, default=None):
if hasattr(self.app, 'config_mgr') and self.app.config_mgr:
try:
value = self.app.config_mgr.get(key)
if value is not None:
return value
except Exception:
pass
return default
def set_app_config(self, key, value):
if hasattr(self.app, 'config_mgr') and self.app.config_mgr:
self.app.config_mgr.set(key, value)
self.logger.info(f"修改应用配置 {key} = {value}")
return True
return False
def restart_application(self, delay=1000):
self.logger.info("请求重启应用")
if hasattr(self.app, '_restart_application'):
from PyQt5.QtCore import QTimer
QTimer.singleShot(delay, self.app._restart_application)
return True
return False
def get_theme(self):
if hasattr(self.app, 'theme_mgr') and self.app.theme_mgr:
if hasattr(self.app.theme_mgr, 'current_theme'):
return self.app.theme_mgr.current_theme
if hasattr(self.app.theme_mgr, '_current_theme'):
return self.app.theme_mgr._current_theme
return "light"
def set_theme(self, theme_name):
if hasattr(self.app, 'theme_mgr') and self.app.theme_mgr:
self.app.theme_mgr.set_theme(theme_name)
self.logger.info(f"切换主题到 {theme_name}")
return True
return False
def get_class_name(self):
if hasattr(self.app, '_get_class_name'):
return self.app._get_class_name()
return "课堂点名"
def show_toast(self, message, duration=2000):
self.logger.info(f"显示提示:{message}")
if hasattr(self.app, '_show_toast'):
self.app._show_toast(message, duration)
return True
return False
def render_markdown(self, markdown_text):
try:
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from main_qt import MarkdownRenderer
return MarkdownRenderer.render(markdown_text)
except Exception as e:
self.logger.error(f"Markdown 渲染失败:{str(e)}")
return markdown_text
def create_markdown_view(self, parent=None):
try:
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from main_qt import MarkdownView
return MarkdownView(parent)
except Exception as e:
self.logger.error(f"创建 Markdown 视图失败:{str(e)}")
from PyQt5.QtWidgets import QTextEdit
view = QTextEdit(parent)
view.setReadOnly(True)
return view
def get_plugin_pages(self):
meta = self.plugin.get("meta", {})
return meta.get("pages", [])
def build_plugin_page(self, page_key, parent=None):
try:
code = self.plugin.get("code", "")
if not code.strip():
self.logger.error(f"插件代码为空")
return None
exec_globals = self._get_exec_globals()
try:
exec(code, exec_globals)
except Exception as e:
self.logger.error(f"执行插件代码失败:{str(e)}\n{traceback.format_exc()}")
return None
func_name = f"build_page_{page_key}"
if func_name not in exec_globals:
available = [k for k in exec_globals.keys() if k.startswith('build_')]
self.logger.error(f"未找到函数 {func_name},可用函数:{available}")
return None
func = exec_globals[func_name]
if not callable(func):
self.logger.error(f"{func_name} 不是可调用对象")
return None
try:
if parent:
return func(self, parent)
return func(self)
except Exception as e:
self.logger.error(f"调用 {func_name} 失败:{str(e)}\n{traceback.format_exc()}")
return None
except Exception as e:
self.logger.error(f"构建插件页面 {page_key} 失败:{str(e)}\n{traceback.format_exc()}")
return None
def _get_exec_globals(self):
import datetime as _dt_module
from datetime import datetime, timedelta, date
import os
import sys
import json
import time
import random
import math
import re
import traceback
import hashlib
import csv
import collections
import urllib.request
from pathlib import Path
from typing import List, Dict, Optional, Tuple, Any, Callable
try:
from PyQt5.QtCore import (Qt, QTimer, QUrl, QThread, pyqtSignal,
QObject, QSize, QPoint, QRect)
from PyQt5.QtWidgets import (QWidget, QDialog, QMessageBox, QInputDialog,
QLineEdit, QTextEdit, QLabel, QPushButton,
QVBoxLayout, QHBoxLayout, QFormLayout,
QListWidget, QListWidgetItem, QComboBox,
QCheckBox, QSpinBox, QDoubleSpinBox,
QTableWidget, QTableWidgetItem, QHeaderView,
QFileDialog, QProgressBar, QGroupBox,
QTabWidget, QScrollArea, QFrame, QApplication)
from PyQt5.QtGui import QIcon, QPixmap, QColor, QFont, QPalette
_qt_ok = True
except Exception:
_qt_ok = False
def _plugin_print(*args, **kwargs):
message = " ".join(str(a) for a in args)
self.logger.info(message)
g = {
'context': self,
'plugin': self.plugin,
'app': self.app,
'print': _plugin_print,
'__name__': 'plugin_exec',
'datetime': datetime,
'timedelta': timedelta,
'date': date,
'datetime_module': _dt_module,
'os': os,
'sys': sys,
'json': json,
'time': time,
'random': random,
'math': math,
're': re,
'traceback': traceback,
'hashlib': hashlib,
'csv': csv,
'urllib_request': urllib.request,
'collections': collections,
'Path': Path,
'List': List,
'Dict': Dict,
'Optional': Optional,
'Tuple': Tuple,
'Any': Any,
'Callable': Callable,
}
if _qt_ok:
g.update({
'Qt': Qt, 'QTimer': QTimer, 'QUrl': QUrl, 'QThread': QThread,
'QObject': QObject, 'pyqtSignal': pyqtSignal, 'QSize': QSize,
'QPoint': QPoint, 'QRect': QRect,
'QWidget': QWidget, 'QDialog': QDialog, 'QMessageBox': QMessageBox,
'QInputDialog': QInputDialog, 'QLineEdit': QLineEdit,
'QTextEdit': QTextEdit, 'QLabel': QLabel, 'QPushButton': QPushButton,
'QVBoxLayout': QVBoxLayout, 'QHBoxLayout': QHBoxLayout,
'QFormLayout': QFormLayout, 'QListWidget': QListWidget,
'QListWidgetItem': QListWidgetItem, 'QComboBox': QComboBox,
'QCheckBox': QCheckBox, 'QSpinBox': QSpinBox,
'QDoubleSpinBox': QDoubleSpinBox, 'QTableWidget': QTableWidget,
'QTableWidgetItem': QTableWidgetItem, 'QHeaderView': QHeaderView,
'QFileDialog': QFileDialog, 'QProgressBar': QProgressBar,
'QGroupBox': QGroupBox, 'QTabWidget': QTabWidget,
'QScrollArea': QScrollArea, 'QFrame': QFrame,
'QApplication': QApplication,
'QIcon': QIcon, 'QPixmap': QPixmap, 'QColor': QColor,
'QFont': QFont, 'QPalette': QPalette,
})
return g
# ==================== 插件间通信(新增) ====================
def call_plugin(self, plugin_name, function_name, *args, **kwargs):
if not self.plugin_mgr:
return None, "插件管理器不可用"
target_plugin = self.plugin_mgr.get_plugin(plugin_name)
if not target_plugin:
return None, f"插件 {plugin_name} 不存在"
if not target_plugin.get("enabled", True):
return None, f"插件 {plugin_name} 已禁用"
try:
target_context = self.plugin_mgr.get_context(plugin_name, self.app)
if not target_context:
return None, f"无法获取插件 {plugin_name} 的上下文"
code = target_plugin.get("code", "")
exec_globals = target_context._get_exec_globals()
exec(code, exec_globals)
if function_name not in exec_globals:
return None, f"插件 {plugin_name} 中没有函数 {function_name}"
func = exec_globals[function_name]
if not callable(func):
return None, f"{function_name} 不是可调用对象"
result = func(target_context, *args, **kwargs)
self.logger.info(f"调用插件 {plugin_name}.{function_name} 成功")
return result, None
except Exception as e:
error_msg = f"调用插件 {plugin_name}.{function_name} 失败:{str(e)}"
self.logger.error(error_msg)
return None, error_msg
def get_plugin_list(self):
if not self.plugin_mgr:
return []
plugins = self.plugin_mgr.get_all_plugins()
return [{
"name": p["meta"].get("name", ""),
"version": p["meta"].get("version", "0.0.0"),
"author": p["meta"].get("author", ""),
"description": p["meta"].get("description", ""),
"enabled": p.get("enabled", True),
"has_settings": p["meta"].get("has_settings_page", False),
} for p in plugins]
def is_plugin_enabled(self, plugin_name):
if not self.plugin_mgr:
return False
plugin = self.plugin_mgr.get_plugin(plugin_name)
return plugin.get("enabled", True) if plugin else False
def share_data(self, key, value, global_scope=False):
if global_scope and self.plugin_mgr:
if not hasattr(self.plugin_mgr, '_shared_data'):
self.plugin_mgr._shared_data = {}
self.plugin_mgr._shared_data[key] = value
self.logger.info(f"共享数据(全局): {key}")
else:
self.set(f"shared_{key}", value)
self.logger.info(f"共享数据(插件内): {key}")
def get_shared_data(self, key, default=None, global_scope=False):
if global_scope and self.plugin_mgr:
if hasattr(self.plugin_mgr, '_shared_data'):
return self.plugin_mgr._shared_data.get(key, default)
return default
return self.get(f"shared_{key}", default)
def broadcast_event(self, event_name, data=None):
if not self.plugin_mgr:
return 0
if not hasattr(self.plugin_mgr, '_event_bus'):
self.plugin_mgr._event_bus = {}
handlers = self.plugin_mgr._event_bus.get(event_name, [])
count = 0
for handler in handlers:
try:
handler(data)
count += 1
except Exception as e:
self.logger.error(f"事件处理器执行失败:{str(e)}")
self.logger.info(f"广播事件 {event_name},触发 {count} 个处理器")
return count
def listen_event(self, event_name, handler):
if not self.plugin_mgr:
return False
if not hasattr(self.plugin_mgr, '_event_bus'):
self.plugin_mgr._event_bus = {}
if event_name not in self.plugin_mgr._event_bus:
self.plugin_mgr._event_bus[event_name] = []
self.plugin_mgr._event_bus[event_name].append(handler)
self.logger.info(f"监听事件 {event_name}")
return True
def unlisten_event(self, event_name, handler=None):
if not self.plugin_mgr or not hasattr(self.plugin_mgr, '_event_bus'):
return False
if event_name not in self.plugin_mgr._event_bus:
return False
if handler:
try:
self.plugin_mgr._event_bus[event_name].remove(handler)
return True
except ValueError:
return False
else:
del self.plugin_mgr._event_bus[event_name]
return True
# ==================== 核心功能扩展(新增) ====================
def register_draw_mode(self, mode_name, mode_info):
if not hasattr(self, '_custom_draw_modes'):
self._custom_draw_modes = {}
self._custom_draw_modes[mode_name] = mode_info
self.logger.info(f"注册抽取模式: {mode_name}")
return True
def get_draw_modes(self):
return getattr(self, '_custom_draw_modes', {})
def register_mark_status(self, status_name, status_info):
if not hasattr(self, '_custom_mark_statuses'):
self._custom_mark_statuses = {}
self._custom_mark_statuses[status_name] = status_info
self.logger.info(f"注册标记状态: {status_name}")
return True
def get_mark_statuses(self):
return getattr(self, '_custom_mark_statuses', {})
def register_shortcut(self, key_sequence, callback, description=""):
try:
from PyQt5.QtWidgets import QShortcut
from PyQt5.QtGui import QKeySequence
shortcut = QShortcut(QKeySequence(key_sequence), self.app)
shortcut.activated.connect(callback)
if not hasattr(self, '_custom_shortcuts'):
self._custom_shortcuts = []
self._custom_shortcuts.append((shortcut, key_sequence, description))
self.logger.info(f"注册快捷键: {key_sequence} - {description}")
return True
except Exception as e:
self.logger.error(f"注册快捷键失败: {str(e)}")
return False
def get_shortcuts(self):
return getattr(self, '_custom_shortcuts', [])
def set_draw_algorithm(self, algorithm_func):
self._custom_draw_algorithm = algorithm_func
self.logger.info("设置自定义抽取算法")
return True
def get_draw_algorithm(self):
return getattr(self, '_custom_draw_algorithm', None)
def add_student(self, student_name):
if hasattr(self.app, '_students'):
if student_name not in self.app._students:
self.app._students.append(student_name)
self.logger.info(f"添加学生: {student_name}")
return True
return False
def remove_student(self, student_name):
if hasattr(self.app, '_students'):
if student_name in self.app._students:
self.app._students.remove(student_name)
self.logger.info(f"移除学生: {student_name}")
return True
return False
def add_text(self, title, content):
if hasattr(self.app, '_texts'):
self.app._texts.append({"title": title, "content": content})
self.logger.info(f"添加课文: {title}")
return True
return False
# ==================== 设置项集成增强(新增) ====================
def register_setting_category(self, category_name, category_info):
if not hasattr(self, '_custom_setting_categories'):
self._custom_setting_categories = {}
self._custom_setting_categories[category_name] = category_info
self.logger.info(f"注册设置分类: {category_name}")
return True
def get_setting_categories(self):
return getattr(self, '_custom_setting_categories', {})
def register_setting_item(self, category, item_key, item_info):
if not hasattr(self, '_custom_setting_items'):
self._custom_setting_items = {}
if category not in self._custom_setting_items:
self._custom_setting_items[category] = {}
self._custom_setting_items[category][item_key] = item_info
self.logger.info(f"注册设置项: {category}.{item_key}")
return True
def get_setting_items(self, category=None):
items = getattr(self, '_custom_setting_items', {})
if category:
return items.get(category, {})
return items
def get_app_config(self, section, key, default=None):
if hasattr(self.app, 'config_mgr'):
return self.app.config_mgr.get(section, key, default)
return default
def set_app_config(self, section, key, value):
if hasattr(self.app, 'config_mgr'):
self.app.config_mgr.set(section, key, value)
self.logger.info(f"设置应用配置: {section}.{key} = {value}")
return True
return False
# ==================== 热重载(新增) ====================
def reload_self(self):
plugin_name = self.plugin["meta"]["name"]
if self.plugin_mgr:
try:
self.plugin_mgr.unload_plugin(plugin_name)
self.plugin_mgr.load_plugin(plugin_name)
self.logger.info(f"插件 {plugin_name} 热重载成功")
return True
except Exception as e:
self.logger.error(f"热重载失败: {str(e)}")
return False
return False
def reload_plugin(self, plugin_name):
if self.plugin_mgr:
try:
self.plugin_mgr.unload_plugin(plugin_name)
self.plugin_mgr.load_plugin(plugin_name)
self.logger.info(f"插件 {plugin_name} 已重载")
return True
except Exception as e:
self.logger.error(f"重载插件 {plugin_name} 失败: {str(e)}")
return False
return False
def unload_self(self):
plugin_name = self.plugin["meta"]["name"]
if self.plugin_mgr:
return self.plugin_mgr.unload_plugin(plugin_name)
return False
def load_plugin_by_path(self, file_path):
if self.plugin_mgr:
return self.plugin_mgr.load_plugin_from_file(file_path)
return False, "插件管理器不可用"
# ==================== 网络和文件API增强(新增) ====================
def http_get(self, url, headers=None, timeout=10):
try:
import urllib.request
import urllib.error
req = urllib.request.Request(url, headers=headers or {})
with urllib.request.urlopen(req, timeout=timeout) as response:
return response.read().decode('utf-8'), response.status, None
except Exception as e:
return None, None, str(e)
def http_post(self, url, data=None, headers=None, timeout=10):
try:
import urllib.request
import urllib.parse
if data and isinstance(data, dict):
data = urllib.parse.urlencode(data).encode('utf-8')
req = urllib.request.Request(url, data=data, headers=headers or {})
with urllib.request.urlopen(req, timeout=timeout) as response:
return response.read().decode('utf-8'), response.status, None
except Exception as e:
return None, None, str(e)
def http_download(self, url, save_path, timeout=30):
try:
import urllib.request
urllib.request.urlretrieve(url, save_path)
return True, None
except Exception as e:
return False, str(e)
def read_file(self, file_path, encoding='utf-8'):
try:
with open(file_path, 'r', encoding=encoding) as f:
return f.read(), None
except Exception as e:
return None, str(e)
def write_file(self, file_path, content, encoding='utf-8'):
try:
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, 'w', encoding=encoding) as f:
f.write(content)
return True, None
except Exception as e:
return False, str(e)
def append_file(self, file_path, content, encoding='utf-8'):
try:
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, 'a', encoding=encoding) as f:
f.write(content)
return True, None
except Exception as e:
return False, str(e)
def file_exists(self, file_path):
return os.path.exists(file_path)
def delete_file(self, file_path):
try:
if os.path.exists(file_path):
os.remove(file_path)
return True
return False
except Exception as e:
return False
def list_directory(self, dir_path):
try:
return os.listdir(dir_path), None
except Exception as e:
return None, str(e)
def create_directory(self, dir_path):
try:
os.makedirs(dir_path, exist_ok=True)
return True
except Exception as e:
return False
def get_file_info(self, file_path):
try:
stat = os.stat(file_path)
return {
"size": stat.st_size,
"created": datetime.fromtimestamp(stat.st_ctime).strftime("%Y-%m-%d %H:%M:%S"),
"modified": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
"is_dir": os.path.isdir(file_path),
"is_file": os.path.isfile(file_path),
}, None
except Exception as e:
return None, str(e)
def execute_command(self, command, timeout=30, shell=True):
try:
import subprocess
result = subprocess.run(command, shell=shell, capture_output=True, text=True, timeout=timeout)
return {
"returncode": result.returncode,
"stdout": result.stdout,
"stderr": result.stderr,
}, None
except Exception as e:
return None, str(e)
# ==================== 数据库API(新增) ====================
def init_database(self, db_name="plugin_data"):
try:
import sqlite3
db_path = os.path.join(self.data_dir, f"{db_name}.db")
conn = sqlite3.connect(db_path)
if not hasattr(self, '_db_connections'):
self._db_connections = {}
self._db_connections[db_name] = conn
self.logger.info(f"数据库初始化: {db_name}")
return True, None
except Exception as e:
return False, str(e)
def db_execute(self, db_name, sql, params=None):
try:
if not hasattr(self, '_db_connections') or db_name not in self._db_connections:
return None, "数据库未初始化"
conn = self._db_connections[db_name]
cursor = conn.cursor()
cursor.execute(sql, params or ())
conn.commit()
return cursor.fetchall(), None
except Exception as e:
return None, str(e)
def db_query(self, db_name, sql, params=None):
return self.db_execute(db_name, sql, params)
def close_database(self, db_name):
try:
if hasattr(self, '_db_connections') and db_name in self._db_connections:
self._db_connections[db_name].close()
del self._db_connections[db_name]
return True
return False
except Exception as e:
return False
# ==================== 调试和性能监控(新增) ====================
def start_performance_monitor(self):
import time
self._perf_start_time = time.time()
self._perf_counter = 0
self.logger.info("性能监控已启动")
return True
def log_performance(self, operation_name):
import time
if not hasattr(self, '_perf_start_time'):
return None
elapsed = time.time() - self._perf_start_time
self._perf_counter += 1
self.logger.info(f"[性能] {operation_name}: {elapsed*1000:.2f}ms (第{self._perf_counter}次)")
return elapsed
def get_memory_usage(self):
try:
import psutil
process = psutil.Process()
return process.memory_info().rss / 1024 / 1024
except ImportError:
try:
import resource
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
except Exception:
return None
def get_cpu_usage(self):
try:
import psutil
return psutil.cpu_percent(interval=0.1)
except ImportError:
return None
def set_log_level(self, level):
self._log_level = level
self.logger.info(f"日志级别设置为: {level}")
def get_logs(self, lines=100, level=None):
all_logs = self.logger.get_logs(lines * 2)
if level:
all_logs = [l for l in all_logs if f"[{level}]" in l]
return all_logs[-lines:]
def clear_logs(self):
try:
if os.path.exists(self.logger.log_file):
with open(self.logger.log_file, 'w') as f:
f.write("")
return True
except Exception:
pass
return False
# ==================== 资源访问增强(新增) ====================
def get_main_window(self):
return self.app
def get_central_widget(self):
if hasattr(self.app, 'centralWidget'):
return self.app.centralWidget()
return None
def get_all_widgets(self):
if hasattr(self.app, 'findChildren'):
from PyQt5.QtWidgets import QWidget
return self.app.findChildren(QWidget)
return []
def get_widget_by_name(self, name):
if hasattr(self.app, 'findChild'):
from PyQt5.QtWidgets import QWidget
return self.app.findChild(QWidget, name)
return None
def get_widgets_by_class(self, class_name):
widgets = self.get_all_widgets()
return [w for w in widgets if type(w).__name__ == class_name]
def get_ui_injector(self):
try:
from plugin_ui_injector import UIInjector
return UIInjector(self.app)
except Exception as e:
self.logger.error(f"获取UI注入器失败: {str(e)}")
return None
def load_resource(self, relative_path):
resource_path = os.path.join(self.get_app_dir(), relative_path)
if os.path.exists(resource_path):
with open(resource_path, 'rb') as f:
return f.read()
return None
def save_resource(self, relative_path, data):
resource_path = os.path.join(self.data_dir, relative_path)
try:
os.makedirs(os.path.dirname(resource_path), exist_ok=True)
with open(resource_path, 'wb') as f:
f.write(data)
return True
except Exception as e:
self.logger.error(f"保存资源失败: {str(e)}")
return False
def get_resource_path(self, relative_path):
return os.path.join(self.data_dir, relative_path)
# ==================== 应用控制(新增) ====================
def restart_application(self):
if hasattr(self.app, '_restart_application'):
self.app._restart_application()
return True
return False
def quit_application(self):
if hasattr(self.app, 'close'):
self.app.close()
return True
return False
def minimize_application(self):
if hasattr(self.app, 'showMinimized'):
self.app.showMinimized()
return True
return False
def maximize_application(self):
if hasattr(self.app, 'showMaximized'):
self.app.showMaximized()
return True
return False
def set_window_title(self, title):
if hasattr(self.app, 'setWindowTitle'):
self.app.setWindowTitle(title)
return True
return False
def set_window_size(self, width, height):
if hasattr(self.app, 'resize'):
self.app.resize(width, height)
return True
return False
def set_window_on_top(self, on_top=True):
if hasattr(self.app, 'setWindowFlags'):
from PyQt5.QtCore import Qt
flags = self.app.windowFlags()
if on_top:
flags |= Qt.WindowStaysOnTopHint
else:
flags &= ~Qt.WindowStaysOnTopHint
self.app.setWindowFlags(flags)
self.app.show()
return True
return False
def show_toast(self, message, duration=2500, color="#0067c0"):
try:
from PyQt5.QtWidgets import QLabel
from PyQt5.QtCore import QTimer, Qt
toast = QLabel(message, self.app)
toast.setStyleSheet(f"background: {color}; color: white; padding: 12px 24px; border-radius: 8px; font-size: 13px; font-weight: 600;")
toast.setAlignment(Qt.AlignCenter)
toast.adjustSize()
x = (self.app.width() - toast.width()) // 2
y = 100
toast.move(x, y)
toast.show()
QTimer.singleShot(duration, toast.close)
return True
except Exception as e:
self.logger.error(f"显示Toast失败: {str(e)}")
return False
class PluginManager:
def __init__(self, config_mgr):
self.config_mgr = config_mgr
self.base_dir = os.path.dirname(os.path.abspath(__file__))
self.plugins_dir = os.path.join(self.base_dir, "plugins")
self.plugins_data_dir = os.path.join(self.plugins_dir, "data")
self.plugins_log_dir = os.path.join(self.plugins_dir, "logs")
self.plugins = {}
self.contexts = {}
self._ensure_dirs()
self.load_all_plugins()
def _ensure_dirs(self):
for d in [self.plugins_dir, self.plugins_data_dir, self.plugins_log_dir]:
if not os.path.exists(d):
os.makedirs(d)
def _get_state_file(self):
return os.path.join(self.plugins_dir, "plugin_states.json")
def _save_plugin_state(self, name, enabled):
try:
states = {}
state_file = self._get_state_file()
if os.path.exists(state_file):
with open(state_file, "r", encoding="utf-8") as f:
states = json.load(f)
states[name] = {"enabled": enabled, "updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
with open(state_file, "w", encoding="utf-8") as f:
json.dump(states, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"保存插件状态失败:{e}")
def _load_plugin_states(self):
try:
state_file = self._get_state_file()
if not os.path.exists(state_file):
return {}
with open(state_file, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"加载插件状态失败:{e}")
return {}
def load_all_plugins(self):
self.plugins = {}
if not os.path.exists(self.plugins_dir):
return
states = self._load_plugin_states()
for filename in os.listdir(self.plugins_dir):
if filename.endswith(".arcx"):
file_path = os.path.join(self.plugins_dir, filename)
plugin, error = ArcxParser.parse(file_path)
if plugin:
plugin["file_path"] = file_path
plugin["filename"] = filename
plugin_name = plugin["meta"]["name"]
if plugin_name in states:
plugin["enabled"] = states[plugin_name].get("enabled", True)
else:
plugin["enabled"] = True
plugin["plugin_dir"] = os.path.join(self.plugins_dir, plugin_name)
plugin["icons_dir"] = os.path.join(plugin["plugin_dir"], "icons")
plugin["resources_dir"] = os.path.join(plugin["plugin_dir"], "resources")
meta = plugin.get("meta", {})
icon_name = meta.get("icon", "")
if icon_name:
icon_path = os.path.join(plugin["icons_dir"], icon_name)
if os.path.exists(icon_path):
plugin["icon_path"] = icon_path
else:
plugin["icon_path"] = None
else:
plugin["icon_path"] = None
self.plugins[plugin_name] = plugin
else:
print(f"插件加载失败 {filename}: {error}")
def get_plugin(self, name):
return self.plugins.get(name)
def get_all_plugins(self):
return list(self.plugins.values())
def get_enabled_plugins(self):
return [p for p in self.plugins.values() if p.get("enabled", True)]
def get_plugins_with_settings(self):
return [p for p in self.get_enabled_plugins() if p["meta"].get("has_settings_page", False)]
def get_plugin_pages(self, plugin_name):
plugin = self.get_plugin(plugin_name)
if not plugin:
return []
meta = plugin.get("meta", {})
pages = meta.get("pages", [])
if not pages and meta.get("has_settings_page", False):
pages = [{"key": "settings", "title": "设置", "icon": "settings"}]
return pages
def build_plugin_page(self, plugin_name, page_key, app, parent=None):
context = self.get_context(plugin_name, app)
if not context:
return None
return context.build_plugin_page(page_key, parent)
def enable_plugin(self, name):
if name in self.plugins:
self.plugins[name]["enabled"] = True
self._call_lifecycle(name, "on_enable")
return True
return False
def disable_plugin(self, name):
if name in self.plugins:
self._call_lifecycle(name, "on_disable")
self.plugins[name]["enabled"] = False
return True
return False
def install_plugin(self, source_path):
if not os.path.exists(source_path):
return False, "源文件不存在"
plugin, error = ArcxParser.parse(source_path)
if not plugin:
return False, error
filename = os.path.basename(source_path)
dest_path = os.path.join(self.plugins_dir, filename)
import shutil
shutil.copy2(source_path, dest_path)
plugin["file_path"] = dest_path
plugin["filename"] = filename
self.plugins[plugin["meta"]["name"]] = plugin
self._call_lifecycle(plugin["meta"]["name"], "on_install")
return True, f"插件 {plugin['meta']['name']} 安装成功"
def uninstall_plugin(self, name):
if name not in self.plugins:
return False, "插件不存在"
self._call_lifecycle(name, "on_uninstall")
plugin = self.plugins[name]
file_path = plugin.get("file_path")
if file_path and os.path.exists(file_path):
os.remove(file_path)
data_dir = os.path.join(self.plugins_data_dir, name)
if os.path.exists(data_dir):
import shutil
shutil.rmtree(data_dir, ignore_errors=True)
del self.plugins[name]
if name in self.contexts:
del self.contexts[name]
return True, f"插件 {name} 已卸载"
def get_context(self, name, app):
if name not in self.plugins:
return None
if name not in self.contexts:
self.contexts[name] = PluginContext(self.plugins[name], app, self)
return self.contexts[name]
def _build_plugin_globals(self, context, plugin, app=None):
"""构建插件执行环境,预注入常用模块"""
import datetime as _dt_module
from datetime import datetime
import os
import sys
import json
import time
import random
import math
import re
import collections
from pathlib import Path
from typing import List, Dict, Optional, Tuple, Any, Callable
def _plugin_print(*args, **kwargs):
message = " ".join(str(a) for a in args)
context.logger.info(message)
exec_globals = {
"context": context,
"plugin": plugin,
"print": _plugin_print,
"__name__": f"plugin_{plugin.get('id', 'unknown')}",
"datetime": datetime,
"datetime_module": _dt_module,
"os": os,
"sys": sys,
"json": json,
"time": time,
"random": random,
"math": math,
"re": re,
"collections": collections,
"Path": Path,
"List": List,
"Dict": Dict,
"Optional": Optional,
"Tuple": Tuple,
"Any": Any,
"Callable": Callable,
}
if app is not None:
exec_globals["app"] = app
exec_globals["config"] = plugin.get("config", {})
return exec_globals
def execute_plugin(self, name, app, entry_point="main"):
plugin = self.get_plugin(name)
if not plugin:
return None, "插件不存在"
if not plugin.get("enabled", True):
return None, "插件已禁用"
context = self.get_context(name, app)
try:
code = plugin.get("code", "")
if not code.strip():
return None, "插件代码为空"
exec_globals = self._build_plugin_globals(context, plugin, app)
context.logger.info(f"开始执行插件,入口:{entry_point}")
exec(code, exec_globals)
if entry_point in exec_globals and callable(exec_globals[entry_point]):
result = exec_globals[entry_point](context)
context.logger.info(f"插件执行完成,返回:{result}")
return result, None
context.logger.info("插件执行完成(无入口函数)")
return None, None
except Exception as e:
error_msg = f"插件执行错误:{str(e)}\n{traceback.format_exc()}"
context.logger.error(error_msg)
return None, error_msg
def _call_lifecycle(self, name, method_name):
plugin = self.get_plugin(name)
if not plugin or not plugin.get("enabled", True):
return
context = self.contexts.get(name)
if not context:
return
try:
code = plugin.get("code", "")
exec_globals = self._build_plugin_globals(context, plugin)
exec(code, exec_globals)
if method_name in exec_globals and callable(exec_globals[method_name]):
context.logger.info(f"调用生命周期方法:{method_name}")
exec_globals[method_name](context)
except Exception as e:
context.logger.error(f"生命周期方法 {method_name} 执行错误:{str(e)}")
def build_plugin_settings_page(self, name, app):
plugin = self.get_plugin(name)
if not plugin:
return None
context = self.get_context(name, app)
try:
code = plugin.get("code", "")
exec_globals = self._build_plugin_globals(context, plugin, app)
exec(code, exec_globals)
if "build_settings_page" in exec_globals and callable(exec_globals["build_settings_page"]):
context.logger.info("构建设置页面")
return exec_globals["build_settings_page"](context)
except Exception as e:
context.logger.error(f"构建设置页面错误:{str(e)}")
return None
def create_sample_plugin(self):
sample_code = '''
def main(context):
context.show_message("示例插件", "这是一个示例插件!\\n\\n你可以在这里编写自己的插件逻辑。")
students = context.get_students()
context.log(f"当前有 {len(students)} 名学生")
context.set_config("last_run", str(datetime.now()))
return {"status": "success", "student_count": len(students)}
def build_settings_page(context):
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QLineEdit
page = QWidget()
layout = QVBoxLayout(page)
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(12)
title = QLabel("示例插件设置")
title.setStyleSheet("font-size: 18px; font-weight: 700; color: #0067c0;")
layout.addWidget(title)
desc = QLabel("在这里可以配置示例插件的各项参数。\\n配置会自动保存到插件数据目录的 config.json 中。")
desc.setWordWrap(True)
desc.setStyleSheet("color: #666666; font-size: 12px;")
layout.addWidget(desc)
input_label = QLabel("欢迎语:")
input_label.setStyleSheet("font-size: 13px; font-weight: 600;")
layout.addWidget(input_label)
welcome_input = QLineEdit()
welcome_input.setText(context.get_config("welcome_message", "欢迎使用示例插件!"))
layout.addWidget(welcome_input)
def save_config():
context.set_config("welcome_message", welcome_input.text())
context.show_message("保存成功", "配置已保存!")
save_btn = QPushButton("保存配置")
save_btn.setStyleSheet("QPushButton { background: #0067c0; color: white; border: none; border-radius: 6px; padding: 10px 20px; font-weight: 600; }")
save_btn.clicked.connect(save_config)
layout.addWidget(save_btn)
layout.addStretch()
return page
def on_enable(context):
context.log("插件已启用")
def on_disable(context):
context.log("插件已禁用")
def on_install(context):
context.log("插件已安装")
def on_uninstall(context):
context.log("插件已卸载")
'''
return ArcxParser.create_arcx(
name="示例插件",
description="这是一个示例插件,展示插件的基本结构、设置页面、日志系统和数据存储。",
code=sample_code,
config={"welcome_message": "欢迎使用示例插件!", "show_student_count": True},
author="A13",
version="1.0.0",
has_settings_page=True
)
def load_plugin(self, name):
plugin = self.get_plugin(name)
if not plugin:
return False, "插件不存在"
if name in self.contexts:
del self.contexts[name]
self.plugins[name]["enabled"] = True
self._save_plugin_state(name, True)
self._call_lifecycle(name, "on_enable")
return True, None
def unload_plugin(self, name):
plugin = self.get_plugin(name)
if not plugin:
return False, "插件不存在"
self._call_lifecycle(name, "on_disable")
if name in self.contexts:
del self.contexts[name]
self.plugins[name]["enabled"] = False
self._save_plugin_state(name, False)
return True, None
def reload_plugin(self, name):
success, error = self.unload_plugin(name)
if not success:
return False, error
success, error = self.load_plugin(name)
if not success:
return False, error
return True, None
def load_plugin_from_file(self, file_path):
if not os.path.exists(file_path):
return False, "文件不存在"
plugin, error = ArcxParser.parse(file_path)
if not plugin:
return False, error
plugin_name = plugin["meta"]["name"]
filename = os.path.basename(file_path)
dest_path = os.path.join(self.plugins_dir, filename)
import shutil
shutil.copy2(file_path, dest_path)
plugin["file_path"] = dest_path
plugin["filename"] = filename
plugin["enabled"] = True
self.plugins[plugin_name] = plugin
self._save_plugin_state(plugin_name, True)
self._call_lifecycle(plugin_name, "on_install")
self._call_lifecycle(plugin_name, "on_enable")
return True, plugin_name
def reload_all_plugins(self):
results = {}
for name in list(self.plugins.keys()):
success, error = self.reload_plugin(name)
results[name] = {"success": success, "error": error}
return results
def get_loaded_plugins(self):
return [name for name, plugin in self.plugins.items() if plugin.get("enabled", True)]
def get_unloaded_plugins(self):
return [name for name, plugin in self.plugins.items() if not plugin.get("enabled", True)]
def call_plugin_function(self, source_plugin, target_plugin, function_name, *args, **kwargs):
target = self.get_plugin(target_plugin)
if not target:
return None, f"插件 {target_plugin} 不存在"
if not target.get("enabled", True):
return None, f"插件 {target_plugin} 已禁用"
try:
context = self.get_context(target_plugin, None)
if not context:
return None, f"无法获取插件 {target_plugin} 的上下文"
code = target.get("code", "")
exec_globals = context._get_exec_globals()
exec(code, exec_globals)
if function_name not in exec_globals:
return None, f"插件 {target_plugin} 中没有函数 {function_name}"
func = exec_globals[function_name]
if not callable(func):
return None, f"{function_name} 不是可调用对象"
result = func(context, *args, **kwargs)
return result, None
except Exception as e:
return None, str(e)
def broadcast_to_all_plugins(self, event_name, data=None, exclude=None):
count = 0
for name, plugin in self.plugins.items():
if not plugin.get("enabled", True):
continue
if exclude and name in exclude:
continue
try:
context = self.get_context(name, None)
if context:
context.broadcast_event(event_name, data)
count += 1
except Exception:
pass
return count
def get_plugin_stats(self):
stats = {
"total": len(self.plugins),
"enabled": len(self.get_loaded_plugins()),
"disabled": len(self.get_unloaded_plugins()),
"plugins": []
}
for name, plugin in self.plugins.items():
plugin_stat = {
"name": name,
"version": plugin["meta"].get("version", "0.0.0"),
"author": plugin["meta"].get("author", ""),
"enabled": plugin.get("enabled", True),
"has_settings": plugin["meta"].get("has_settings_page", False),
"pages": len(plugin["meta"].get("pages", [])),
}
context = self.contexts.get(name)
if context:
plugin_stat["data_dir"] = context.data_dir
plugin_stat["libs_count"] = len(context.list_libs())
stats["plugins"].append(plugin_stat)
return stats
def get_plugin_logs(self, name, lines=100):
context = self.contexts.get(name)
if context:
return context.logger.get_logs(lines)
return []
def clear_plugin_logs(self, name):
context = self.contexts.get(name)
if context and os.path.exists(context.logger.log_file):
with open(context.logger.log_file, 'w') as f:
f.write("")
return True
return False
def clear_all_logs(self):
count = 0
for name in self.plugins:
if self.clear_plugin_logs(name):
count += 1
return count
def check_plugin_dependencies(self, name):
plugin = self.get_plugin(name)
if not plugin:
return None, "插件不存在"
meta = plugin.get("meta", {})
dependencies = meta.get("dependencies", [])
results = {}
for dep in dependencies:
dep_plugin = self.get_plugin(dep)
results[dep] = {
"installed": dep_plugin is not None,
"enabled": dep_plugin.get("enabled", False) if dep_plugin else False,
}
return results, None
def get_plugin_dependents(self, name):
dependents = []
for plugin_name, plugin in self.plugins.items():
if plugin_name == name:
continue
dependencies = plugin.get("meta", {}).get("dependencies", [])
if name in dependencies:
dependents.append(plugin_name)
return dependents