基础功能完成

This commit is contained in:
2026-07-20 10:31:10 +08:00
parent 207cbde4bc
commit aaad6b8aa6
4 changed files with 175 additions and 92 deletions
+1 -1
View File
@@ -15,5 +15,5 @@ from .logging.log import FrameLog
from .plugin.plugins import Plugins from .plugin.plugins import Plugins
from .plugin.plugin_registry import PluginRegistry from .plugin.plugin_registry import PluginRegistry
__all__ = ["FrameLog", "ToolsWindow","PluginRegistry"] __all__ = ["FrameLog", "Plugins","PluginRegistry"]
+155 -86
View File
@@ -1,17 +1,25 @@
"""插件注册中心。
架构说明:
- PluginWorker 运行在独立 QThread 中,仅做纯 I/O(扫描 .pyd、importlib 读元数据、
文件复制),不创建 QObject、不操作 QWidget。
- PluginRegistry 留在主线程,负责 create_plugin()(返回 QWidget,必须在 GUI 线程
创建)与信号转发。
- 跨线程通信全部走 Qt 信号槽(自动 Queued),不再使用 threading.Thread。
"""
import os import os
import sys import sys
import time import time
import logging import logging
import importlib import importlib
import threading
from pathlib import Path from pathlib import Path
from enum import Enum, auto from enum import Enum, auto
from PySide6.QtCore import QObject,Signal from typing import Optional
from dataclasses import dataclass, field, fields
LOCAL_PLUGINS_PATH = Path("./plugins") from PySide6.QtCore import QObject, Signal, Slot, QThread, QTimer
LOCAL_PLUGINS_PATH = Path("./plugins")
REMOTE_PLUGINS_PATH = Path('Y:/SE/xufeifei/plugins') REMOTE_PLUGINS_PATH = Path('Y:/SE/xufeifei/plugins')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -22,106 +30,167 @@ class Event(Enum):
Install = auto() Install = auto()
Uninstall = auto() Uninstall = auto()
class PluginRegistry(QObject):
"""插件注册中心。"""
plugins_loader_signal = Signal(dict)
update_plugins_card_signal = Signal(dict)
def __init__(self, parent = None) -> None:
super().__init__(parent)
self.plugins = dict() class PluginWorker(QObject):
"""插件发现与安装 worker,运行在独立 QThread 中。
self.start_discover_local(LOCAL_PLUGINS_PATH, self.plugins) 仅做纯 I/O,结果以纯数据(list[dict])形式经信号回传主线程。
self.start_discover_remote(REMOTE_PLUGINS_PATH, self.plugins) """
def start_discover_local(self,plugins_dir: Path, plugins:dict): local_discovered = Signal(list)
self.thread_local = threading.Thread(target= self.discover_local_event,args=(plugins_dir, plugins,)) remote_discovered = Signal(list)
self.thread_local.start() install_progress = Signal(int, int)
install_finished = Signal(bool, str)
def discover_local_event(self, plugins_dir: Path, plugins:dict): @Slot(str)
if not os.path.exists(plugins_dir): def do_discover_local(self, plugins_dir: str) -> None:
os.mkdir(plugins_dir) plugins_dir = Path(plugins_dir)
if not self._ensure_dir(plugins_dir):
return return
self.local_discovered.emit(self._scan_plugins(plugins_dir))
sys.path.append(str(plugins_dir))
for tool in Path(plugins_dir).glob("*.pyd"):
tool_name = tool.stem
try:
module = importlib.import_module(tool_name)
obj = module.create_plugin()
name = module.read_plugin_name()
version = module.read_plugin_version()
if name in plugins.keys():
pass
else:
plugins[name] = {}
plugins[name]['obj'] = obj
plugins[name]['local version'] = version
except Exception as e:
logger.error(f"插件{tool_name}注册失败,{str(e)}")
sys.path.remove(str(LOCAL_PLUGINS_PATH))
self.plugins_loader_signal.emit(plugins)
@Slot(str)
def start_discover_remote(self,plugins_dir: Path, plugins:dict): def do_discover_remote(self, plugins_dir: str) -> None:
self.thread_remote = threading.Thread(target= self.discover_remote_event,args=(plugins_dir, plugins,))
self.thread_remote.start()
def discover_remote_event(self, plugins_dir: Path, plugins:dict):
if not os.path.exists(plugins_dir): if not os.path.exists(plugins_dir):
logger.error("服务器链接错误!") logger.error("服务器链接错误!")
return return
#读取服务器工具信息 self.remote_discovered.emit(
time.sleep(0.01) self._scan_plugins(Path(plugins_dir), with_remote_meta=True)
sys.path.append(str(plugins_dir)) )
for tool in Path(plugins_dir).glob("*.pyd"):
tool_name = tool.stem
try:
module = importlib.import_module(tool_name)
name = module.read_plugin_name() @Slot(str, str)
version = module.read_plugin_version() def do_install(self, local_path: str, remote_path: str) -> None:
description = module.read_plugin_description()
if name in plugins.keys():
pass
else:
print("discover_remote_event")
plugins[name] = {}
plugins[name]["tool_name"] = tool_name
plugins[name]["local version"] = None
plugins[name]["local description"] = None
plugins[name]["remote version"] = version
plugins[name]["remote description"] = description
plugins[name]['remote path'] = plugins_dir
except Exception as e:
logger.error(f"服务器{tool_name}无法加载:{str(e)}")
sys.path.remove(str(plugins_dir))
self.update_plugins_card_signal.emit(plugins)
def start_plugins_event(self, event:Event, name:str):
if event == Event.Install:
local_path = str(LOCAL_PLUGINS_PATH) + '\\' + f"{self.plugins[name]['tool_name']}.pyd"
remote_path = str(REMOTE_PLUGINS_PATH) + '\\' + f"{self.plugins[name]['tool_name']}.pyd"
self.sub_thread = threading.Thread(target= self.install_plugins_event,args=(local_path,remote_path,))
self.sub_thread.start()
pass
def install_plugins_event(self, local_path, remote_path):
try: try:
total_size = os.path.getsize(remote_path) total_size = os.path.getsize(remote_path)
copied_size = 0 copied_size = 0
with open(remote_path, 'rb') as fsrc, open(local_path, 'wb') as fdst: with open(remote_path, 'rb') as fsrc, open(local_path, 'wb') as fdst:
while True: while True:
buf = fsrc.read(1024*1024) buf = fsrc.read(1024 * 1024)
if not buf: if not buf:
break break
fdst.write(buf) fdst.write(buf)
copied_size += len(buf) copied_size += len(buf)
logger.info(f"安装进度: {copied_size/1024/1024:.2f} MB / {total_size/1024/1024:.2f} MB") self.install_progress.emit(copied_size, total_size)
time.sleep(0.1) time.sleep(0.1)
logger.info(f"{local_path}安装成功!!!") self.install_finished.emit(True, f"{local_path}安装成功!!!")
except Exception as e: except Exception as e:
logger.error(f"{remote_path}安装失败{e.args}") logger.error(f"{remote_path}安装失败{e.args}")
return self.install_finished.emit(False, str(e))
@staticmethod
def _ensure_dir(plugins_dir: Path) -> bool:
if not os.path.exists(plugins_dir):
os.mkdir(plugins_dir)
return False
return True
@staticmethod
def _scan_plugins(plugins_dir: Path, with_remote_meta: bool = False) -> list:
results: list = []
sys.path.append(str(plugins_dir))
try:
for tool in plugins_dir.glob("*.pyd"):
tool_name = tool.stem
try:
module = importlib.import_module(tool_name)
item = {
"tool_name": tool_name,
"name": module.read_plugin_name(),
"version": module.read_plugin_version(),
}
if with_remote_meta:
item["description"] = module.read_plugin_description()
item["remote_path"] = str(plugins_dir)
results.append(item)
except Exception as e:
logger.error(f"插件{tool_name}加载失败:{e}")
finally:
sys.path.remove(str(plugins_dir))
return results
class PluginRegistry(QObject):
"""插件注册中心。
主线程负责创建插件实例(create_plugin 返回 QWidget,必须在 GUI 线程创建);
发现与安装的 I/O 由 PluginWorker 在独立 QThread 中执行,结果通过信号回传
主线程。所有跨线程通信经 Qt 信号槽(Queued)。
"""
plugins_loader_signal = Signal(dict)
update_plugins_card_signal = Signal(dict)
_discover_local_requested = Signal(str)
_discover_remote_requested = Signal(str)
_install_requested = Signal(str, str)
def __init__(self, parent: Optional[QObject] = None) -> None:
super().__init__(parent)
self.plugins: dict = {}
self._worker = PluginWorker()
self._thread = QThread()
self._worker.moveToThread(self._thread)
self._discover_local_requested.connect(self._worker.do_discover_local)
self._discover_remote_requested.connect(self._worker.do_discover_remote)
self._install_requested.connect(self._worker.do_install)
self._worker.local_discovered.connect(self._on_local_discovered)
self._worker.remote_discovered.connect(self._on_remote_discovered)
self._worker.install_finished.connect(self._on_install_finished)
self._thread.start()
# 延迟到事件循环启动后触发,保证所有接收方先 connect 后 emit
QTimer.singleShot(0, self._start_discovery)
def _start_discovery(self) -> None:
self._discover_local_requested.emit(str(LOCAL_PLUGINS_PATH))
self._discover_remote_requested.emit(str(REMOTE_PLUGINS_PATH))
@Slot(list)
def _on_local_discovered(self, results: list) -> None:
for item in results:
name = item["name"]
if name not in self.plugins:
self.plugins[name] = {}
module = importlib.import_module(item["tool_name"])
self.plugins[name]["obj"] = module.create_plugin()
self.plugins[name]["local version"] = item["version"]
self.plugins_loader_signal.emit(self.plugins)
@Slot(list)
def _on_remote_discovered(self, results: list) -> None:
for item in results:
name = item["name"]
if name not in self.plugins:
self.plugins[name] = {}
self.plugins[name]["local version"] = None
self.plugins[name]["local description"] = None
self.plugins[name]["tool_name"] = item["tool_name"]
self.plugins[name]["remote version"] = item["version"]
self.plugins[name]["remote description"] = item["description"]
self.plugins[name]["remote path"] = item["remote_path"]
self.update_plugins_card_signal.emit(self.plugins)
@Slot(bool, str)
def _on_install_finished(self, success: bool, message: str) -> None:
if success:
logger.info(message)
self._start_discovery()
# self._discover_local_requested.emit(str(LOCAL_PLUGINS_PATH))
else:
logger.error(message)
def start_plugins_event(self, event: Event, name: str) -> None:
if event == Event.Install:
tool_name = self.plugins[name]['tool_name']
local_path = str(LOCAL_PLUGINS_PATH) + '\\' + f"{tool_name}.pyd"
remote_path = str(REMOTE_PLUGINS_PATH) + '\\' + f"{tool_name}.pyd"
self._install_requested.emit(local_path, remote_path)
elif event == Event.Uninstall:
tool_name = self.plugins[name]['tool_name']
local_path = str(LOCAL_PLUGINS_PATH) + '\\' + f"{tool_name}.pyd"
# os.
pass
+8 -3
View File
@@ -36,7 +36,8 @@ class PluginsCard(QFrame, Ui_FramePluginsCard):
self.pushButtonInstall.setEnabled(False) self.pushButtonInstall.setEnabled(False)
if self.info['remote version'] <= self.info['local version']: if self.info['remote version'] <= self.info['local version']:
self.pushButtonUpdate.setEnabled(False) self.pushButtonUpdate.setEnabled(False)
self.pushButtonInstall.clicked.connect(self.on_intsall_event) self.pushButtonInstall.clicked.connect(self.on_install_event)
self.pushButtonUninstall.clicked.connect(self.on_uninstall_event)
def to_card(self): def to_card(self):
card = f"# {self.name}\n\n" card = f"# {self.name}\n\n"
@@ -48,5 +49,9 @@ class PluginsCard(QFrame, Ui_FramePluginsCard):
card += f"**描述:** {self.info['remote description']}" card += f"**描述:** {self.info['remote description']}"
return card return card
def on_intsall_event(self): def on_install_event(self):
self.send_event_signal.emit(Event.Install, self.name) self.send_event_signal.emit(Event.Install, self.name)
def on_uninstall_event(self):
self.send_event_signal.emit(Event.Uninstall, self.name)
pass
+11 -2
View File
@@ -20,8 +20,9 @@ import importlib
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from PySide6.QtGui import QIcon # from PySide6.QtCore import QAction
from PySide6.QtWidgets import QApplication, QMainWindow, QMenuBar, QStatusBar,QFileDialog from PySide6.QtGui import QIcon,QAction
from PySide6.QtWidgets import QApplication, QMainWindow, QMenuBar, QStatusBar
from qt_material import apply_stylesheet from qt_material import apply_stylesheet
from main_ui import Ui_MainWindow from main_ui import Ui_MainWindow
# 获取模块日志记录器 # 获取模块日志记录器
@@ -121,8 +122,16 @@ class MainWindow(QMainWindow, Ui_MainWindow):
def on_plugins_loader(self, plugins:dict): def on_plugins_loader(self, plugins:dict):
for name in plugins.keys(): for name in plugins.keys():
action = QAction(name,self)
self.toolBar.addAction(action)
action.triggered.connect(lambda checked, action=action: self.on_action_triggered(action))
self.stackedWidget.addWidget(plugins[name]['obj']) self.stackedWidget.addWidget(plugins[name]['obj'])
def on_action_triggered(self, action:QAction):
print(action.text())
self.stackedWidget.setCurrentWidget(self.plugin_registry.plugins[action.text()]['obj'])
if __name__ == '__main__': if __name__ == '__main__':
app = QApplication(sys.argv) app = QApplication(sys.argv)