From 1b6796ec43497fa79925e05d39beb430c7ef9403 Mon Sep 17 00:00:00 2001 From: "feifei.xu" <307327147@qq.com> Date: Tue, 21 Jul 2026 17:04:34 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=AE=8C=E5=96=84README=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E4=B8=8E=E6=9E=B6=E6=9E=84=E5=9B=BE=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=85=A8=E9=87=8F=E6=B3=A8=E9=87=8A=E4=B8=8E=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README: 补充项目概述、技术栈、架构图(Mermaid)、插件生命周期时序图 - build.py: 补充模块和函数 docstring - core/__init__.py: 修正模块说明(工具箱→插件系统) - log.py: 修复过时注释,清理死代码,抑制第三方库日志刷屏 - plugin_registry.py: 修正矛盾注释,补充缺失日志,隐藏远程路径 - plugins.py: 修正过时注释,清理死代码和无用导入 - plugins_card.py: 修正错误类名注释,补充全量 docstring - main.py: 修正错误注释,补充启动/退出/插件加载日志 --- README.md | 155 ++++++++++++++++++++++++++++++++- build.py | 25 ++++-- core/__init__.py | 2 +- core/logging/log.py | 21 ++--- core/plugin/plugin_registry.py | 32 +++++-- core/plugin/plugins.py | 94 ++++---------------- core/plugin/plugins_card.py | 52 +++++++---- main.py | 39 ++++----- 8 files changed, 282 insertions(+), 138 deletions(-) diff --git a/README.md b/README.md index c7d5374..f30e59b 100644 --- a/README.md +++ b/README.md @@ -1 +1,154 @@ -# Model_Team_Tools +# Model Team Tools + +面向模型团队的桌面插件化工具平台,基于 PySide6 + Nuitka 构建。 + +## 功能特性 + +- **插件热管理**:自动发现本地/远程插件,支持一键安装、更新、卸载,重启即生效 +- **统一日志系统**:三路输出(文件、控制台、UI 面板),实时查看运行状态 +- **暗色主题**:基于 qt-material dark_teal 主题,支持自定义 QSS 样式 +- **独立分发**:Nuitka 编译为 Windows 独立可执行文件,无需安装 Python 环境 + +## 软件架构 + +```mermaid +graph TB + subgraph UI["GUI 层 (PySide6)"] + MW[MainWindow
主窗口] + PLUG[Plugins
插件管理窗口] + CARD[PluginsCard
插件卡片] + LOG[FrameLog
日志面板] + end + + subgraph CORE["Core 层"] + REG[PluginRegistry
插件注册中心
主线程] + WK[PluginWorker
I/O 工作线程] + LH[LogHandler
日志处理器] + end + + subgraph HOOK["启动层"] + RH[runtime_hook
sys.path 注入] + end + + subgraph EXT["外部资源"] + LOCAL[本地 plugins/] + REMOTE[远程 Y:/.../plugins/*.zip] + WS[workspace/ 配置] + end + + RH -->|1.注入路径| LOCAL + MW -->|2.初始化| REG + MW --> PLUG + MW --> LOG + PLUG --> CARD + CARD -->|Event| REG + REG -->|信号槽| WK + WK -->|扫描/下载| LOCAL + WK -->|扫描| REMOTE + REG -->|create_plugin| LOCAL + LH -->|写入| LOG +``` + +## 技术栈 + +| 组件 | 选型 | +|------|------| +| GUI | PySide6 (Qt6) | +| 主题 | qt-material | +| 打包 | Nuitka standalone (mingw64) | +| 插件格式 | .pyd + manifest.json (.zip 分发) | +| Python | 3.11 | + +## 项目结构 + +``` +Model_Team_Tools/ +├── main.py # 程序入口,主窗口 +├── main.ui / main_ui.py # 主界面定义 +├── runtime_hook.py # 启动钩子:将 plugins/ 注入 sys.path +├── build.py # Nuitka 打包脚本 +├── requirements.txt # 依赖清单 +├── resources/ # 图标、QSS 样式 +├── core/ +│ ├── logging/ # 日志系统(文件+控制台+UI) +│ └── plugin/ # 插件注册中心 + 管理窗口 + 卡片组件 +├── plugins/ # 本地已安装插件目录 +└── workspace/ # 插件工作区配置文件 +``` + +## 快速开始 + +```bash +# 安装依赖 +pip install -r requirements.txt + +# 运行(开发模式) +python main.py + +# 打包(发布模式) +python build.py +# 产物位于 output/windows-amd64/main.dist/main.exe +``` + +## 插件系统 + +### 架构 + +- **PluginRegistry**(主线程):管理插件实例、QWidget 创建、信号转发 +- **PluginWorker**(QThread):扫描本地/远程插件、下载解压、读写清单文件 +- **PluginsCard**(UI 组件):展示插件信息,提供安装/更新/卸载按钮 + +### 生命周期 + +```mermaid +sequenceDiagram + participant App as 应用启动 + participant RH as runtime_hook + participant REG as PluginRegistry + participant WK as PluginWorker + participant UI as Plugins/PluginsCard + + App->>RH: 启动时注入 sys.path + App->>REG: 创建 PluginRegistry + REG->>REG: 处理 .uninstall_pending 清单 + REG->>WK: 启动 Worker 线程 + REG->>WK: 扫描本地 plugins/ + WK->>WK: 加载 .pyd → 读元数据 + WK-->>REG: local_discovered + REG->>REG: import_module + create_plugin + REG-->>UI: plugins_loader_signal + REG->>WK: 扫描远程 *.zip + WK->>WK: 读 manifest.json + WK-->>REG: remote_discovered + REG-->>UI: update_plugins_card_signal + UI->>UI: 刷新卡片列表 +``` + +### 远程插件源 + +默认路径:`Y:/SE/xufeifei/plugins` + +远程插件以 `.zip` 格式分发,内含: +- `.pyd`:Nuitka 编译的插件主体 +- `manifest.json`:元数据(名称、版本、描述) +- 插件依赖的 C 扩展和 stdlib 子包 + +### 安装/更新/卸载 + +| 操作 | 运行时行为 | 重启后行为 | +|------|-----------|-----------| +| 安装 | 下载 zip → 解压到 `plugins/` | 自动发现并加载 | +| 更新 | 写入 `.uninstall_pending` 清单 | 删旧目录 → 解压新版本 | +| 卸载 | 写入 `.uninstall_pending` 清单 | 删除插件目录 | + +## 配置 + +| 配置项 | 位置 | 说明 | +|--------|------|------| +| 远程插件源 | `core/plugin/plugin_registry.py` | `REMOTE_PLUGINS_PATH` | +| 日志级别 | `core/logging/log.py` | 默认 DEBUG | +| 延迟启动间隔 | `PluginRegistry._PENDING_CLEANUP_DELAY` | 默认 1500ms | + +## 版本 + +当前版本:**0.0.1** diff --git a/build.py b/build.py index 75f82bf..43cd10f 100644 --- a/build.py +++ b/build.py @@ -1,3 +1,11 @@ +"""Nuitka 打包构建脚本。 + +将 main.py 编译为 Windows 独立可执行文件,并打包为便携式 zip。 +需要安装 Nuitka 和 mingw64 编译器。 + +用法: + python build.py +""" import os import sys import glob @@ -8,15 +16,25 @@ from zipfile import ZIP_DEFLATED, ZipFile from main import APP_NAME, APP_VERSION +# 编译器选择 COMPILER = "mingw64" + +# 输出路径 OUTPUT_PATH = Path('output') RESOURCES_PATH = Path("resources") RELEASE_PATH = OUTPUT_PATH / APP_NAME - BUILD_PATH = OUTPUT_PATH / f'{platform.system().lower()}-{platform.machine().lower()}' def build_main(): + """使用 Nuitka 编译 main.py 为独立可执行文件。 + + 编译选项: + - --standalone:不依赖本地 Python 环境 + - --plugin-enable=pyside6:启用 PySide6 支持 + - --include-module=runtime_hook:将启动钩子编入 exe + - --windows-console-mode=disable:不显示控制台窗口 + """ nuitka_cmd = [ sys.executable, '-m', @@ -26,10 +44,6 @@ def build_main(): '--show-progress', '--plugin-enable=pyside6', '--include-module=qt_material', - # 把插件 SDK 的 runtime_hook 编进 exe,启动时把 plugins/*/ 加进 sys.path。 - # Nuitka 无 --runtime-hook 选项,用 --include-module + main.py 顶部 import 替代。 - # 宿主不需要预知插件依赖哪些 stdlib——纯 Python 依赖编进 mil.pyd, - # C 扩展由插件自带,runtime_hook 把 plugins/*/ 加进 sys.path 后能加载。 '--include-module=runtime_hook', f"--include-data-dir={RESOURCES_PATH}=resources", ] @@ -54,6 +68,7 @@ def build_main(): def create_zip(): + """将 Nuitka 编译产物打包为便携式 zip 文件。""" file_list = glob.glob(f'{BUILD_PATH / APP_NAME / "dist"}', recursive=True) file_list.sort() diff --git a/core/__init__.py b/core/__init__.py index 6afbdb4..f5336cf 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -3,7 +3,7 @@ Core 模块 - 核心功能模块包 本包包含应用程序的核心功能组件: - 日志系统(logging 包) -- 工具箱系统(tools 包) +- 插件系统(plugin 包) 提供统一的接口导出,方便其他模块调用。 diff --git a/core/logging/log.py b/core/logging/log.py index 881bca1..dfe512a 100644 --- a/core/logging/log.py +++ b/core/logging/log.py @@ -20,7 +20,7 @@ from pathlib import Path from typing import Callable, Optional from PySide6.QtCore import QObject, Signal -from PySide6.QtWidgets import QFrame, QTextEdit +from PySide6.QtWidgets import QFrame from .log_ui import Ui_FrameLog @@ -115,6 +115,10 @@ class FrameLog(QFrame, Ui_FrameLog): logger = logging.getLogger() logger.setLevel(logging.DEBUG) + # 抑制第三方库 DEBUG 日志刷屏 + logging.getLogger("PySide6").setLevel(logging.WARNING) + logging.getLogger("qt_material").setLevel(logging.WARNING) + # 创建日志格式化器:时间 [级别] 消息 formatter = logging.Formatter( "%(asctime)s [%(levelname)s] %(message)s", @@ -190,20 +194,11 @@ class FrameLog(QFrame, Ui_FrameLog): self.log_signal.connect(self._update_log_display) def _update_log_display(self, message: str) -> None: - """ - 更新 UI 的日志显示组件 + """更新 UI 的日志显示组件。 - 将日志消息追加到 TextEdit 组件中显示 + 将日志消息追加到 TextEdit 组件中显示。 Args: message: 格式化后的日志消息字符串 - - 实现细节: - - 使用 findChild 查找 TextEdit 组件 - - 使用 append 方法追加日志消息,自动换行 """ - self.textEdit.append(message) - - # text_edit = self.findChild(QTextEdit, "textEdit") - # if text_edit: - # text_edit.append(message) \ No newline at end of file + self.textEdit.append(message) \ No newline at end of file diff --git a/core/plugin/plugin_registry.py b/core/plugin/plugin_registry.py index 46930da..ec7b47d 100644 --- a/core/plugin/plugin_registry.py +++ b/core/plugin/plugin_registry.py @@ -40,8 +40,9 @@ def _load_module_from_path(module_name: str, file_path: Path): 用 spec_from_file_location 直接从 .pyd 文件构建模块,避免 importlib.import_module 命中同名旧缓存。注意:.pyd 的入口函数 - PyInit_ 与文件名绑定,module_name 必须等于文件 stem, - 不能用别名。 + PyInit_ 与文件名绑定,module_name 必须等于 _pyd_module_name() + 提取的模块名(第一个点号之前的部分),不能直接用完整 stem。 + 例如 mil.cp311-win_amd64.pyd 的 module_name 必须是 "mil"。 """ spec = importlib.util.spec_from_file_location(module_name, file_path) if spec is None or spec.loader is None: @@ -91,9 +92,9 @@ class PluginWorker(QObject): def do_discover_remote(self, plugins_dir: str) -> None: """扫描远程 *.zip,读 zip 内 manifest.json 取插件元数据。""" if not os.path.exists(plugins_dir): - logger.error(f"服务器链接错误!路径不存在: {plugins_dir}") + logger.error(f"远程插件路径不可达") return - logger.info(f"开始扫描远程插件 zip: {plugins_dir}") + logger.info(f"开始扫描远程插件 zip") self.remote_discovered.emit( self._scan_remote_zips(Path(plugins_dir)) ) @@ -125,7 +126,7 @@ class PluginWorker(QObject): tmp_zip.unlink() self.install_finished.emit(True, f"安装成功: {local_dir}") except Exception as e: - logger.error(f"{remote_zip}安装失败{e.args}") + logger.error(f"{remote_zip} 安装失败: {e}") self.install_finished.emit(False, str(e)) @Slot(str, str) @@ -166,6 +167,7 @@ class PluginWorker(QObject): def _ensure_dir(plugins_dir: Path) -> bool: if not os.path.exists(plugins_dir): os.mkdir(plugins_dir) + logger.info(f"插件目录已创建: {plugins_dir}") return False return True @@ -231,18 +233,21 @@ class PluginRegistry(QObject): 主线程。所有跨线程通信经 Qt 信号槽(Queued)。 """ + # --- 对外信号:通知主窗口 UI 更新 --- plugins_loader_signal = Signal(dict) update_plugins_card_signal = Signal(dict) uninstall_completed_signal = Signal(str) install_progress = Signal(str, int, int) # name, copied_bytes, total_bytes install_finished = Signal(bool, str) + # --- 内部信号:主线程 → Worker 线程(跨线程请求) --- _discover_local_requested = Signal(str) _discover_remote_requested = Signal(str) _install_requested = Signal(str, str) _uninstall_requested = Signal(str, str) _update_requested = Signal(str, str, str) + # 自重启后延迟启动,等待旧进程释放 .pyd 文件锁(毫秒) _PENDING_CLEANUP_DELAY = 1500 def __init__(self, parent: Optional[QObject] = None) -> None: @@ -284,6 +289,7 @@ class PluginRegistry(QObject): """ self._process_pending_uninstalls() self._thread.start() + logger.info("插件系统初始化完成,开始发现插件") QTimer.singleShot(0, self._start_discovery) def shutdown(self) -> None: @@ -292,8 +298,10 @@ class PluginRegistry(QObject): 必须在 QThread 析构前调用,否则触发 "QThread: Destroyed while thread is still running" 警告。 """ + logger.info("PluginRegistry 正在关闭...") self._thread.quit() self._thread.wait(3000) + logger.info("PluginRegistry 已关闭") @staticmethod def _process_pending_uninstalls() -> None: @@ -309,6 +317,7 @@ class PluginRegistry(QObject): """ if not UNINSTALL_PENDING_FILE.exists(): return + logger.info("检测到待处理卸载/更新清单,开始清理...") lines = UNINSTALL_PENDING_FILE.read_text(encoding='utf-8').splitlines() failed: list = [] for line in lines: @@ -354,11 +363,18 @@ class PluginRegistry(QObject): UNINSTALL_PENDING_FILE.unlink() def _start_discovery(self) -> None: + """启动插件发现:扫描本地 pyd 和远程 zip。""" 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: + """处理本地扫描结果:import_module + create_plugin,注册到 plugins 字典。 + + 本地扫描在远程扫描之前完成,因此先发射 plugins_loader_signal + 让主窗口把已安装插件加入工具栏,再发射 update_plugins_card_signal + 刷新卡片列表(安装后 local version 已更新)。 + """ for item in results: name = item["name"] try: @@ -367,6 +383,7 @@ class PluginRegistry(QObject): except Exception as e: logger.error(f"本地插件 {name} 实例化失败,跳过: {e}") continue + logger.info(f"本地插件已加载: {name} V{item['version']}") if name not in self.plugins: self.plugins[name] = {} self.plugins[name]["obj"] = obj @@ -378,6 +395,7 @@ class PluginRegistry(QObject): @Slot(list) def _on_remote_discovered(self, results: list) -> None: + """处理远程扫描结果:填充远程版本/描述/路径,刷新卡片列表。""" for item in results: name = item["name"] if name not in self.plugins: @@ -392,6 +410,7 @@ class PluginRegistry(QObject): @Slot(bool, str) def _on_install_finished(self, success: bool, message: str) -> None: + """安装完成回调:成功则把新解压目录加入 sys.path 并重新扫描本地。""" if success: logger.info(message) for sub in LOCAL_PLUGINS_PATH.iterdir(): @@ -417,6 +436,7 @@ class PluginRegistry(QObject): @Slot(str, bool, str) def _on_uninstall_finished(self, name: str, success: bool, message: str) -> None: + """卸载/更新完成回调:清除 plugins 字典中对应项的 obj 和版本,通知主窗口。""" if not success: logger.error(message) return @@ -442,8 +462,10 @@ class PluginRegistry(QObject): remote_path = self.plugins[name]['remote path'] self._install_requested.emit(local_dir, remote_path) elif event == Event.Uninstall: + logger.info(f"[{name}] 开始卸载...") self._uninstall_requested.emit(name, local_dir) elif event == Event.Update: self._installing_name = name + logger.info(f"[{name}] 开始更新...") remote_path = self.plugins[name]['remote path'] self._update_requested.emit(name, local_dir, remote_path) diff --git a/core/plugin/plugins.py b/core/plugin/plugins.py index bdbc5c4..13a2972 100644 --- a/core/plugin/plugins.py +++ b/core/plugin/plugins.py @@ -1,64 +1,47 @@ """ -工具箱模块 +插件管理模块 -提供工具箱主窗口,包含各种实用工具的入口。 +提供插件管理主窗口,包含插件卡片列表和安装/更新/卸载入口。 使用 PySide6 构建的窗口框架。 主要功能: -- 工具箱主窗口界面 -- 窗口居中显示功能 -- 工具分类和导航 +- 插件管理窗口界面 +- 插件卡片列表渲染与状态刷新 +- 插件事件的转发与处理 Author: Model Team Version: 0.0.1 """ -import os -import sys import logging -import threading -import importlib from qt_material import apply_stylesheet -from pathlib import Path from .plugins_ui import Ui_FormPlugins from PySide6.QtGui import QIcon from PySide6.QtWidgets import QWidget, QListWidgetItem -from PySide6.QtCore import QSize, Signal +from PySide6.QtCore import QSize from .plugins_card import PluginsCard from .plugin_registry import PluginRegistry logger = logging.getLogger(__name__) -LOCAL_PLUGINS_PATH = Path("./plugins") -REMOTE_PLUGINS_PATH = Path('Y:/SE/xufeifei/plugins') - class Plugins(QWidget, Ui_FormPlugins): - """工具箱主窗口类 - - 继承自 QWidget(Qt 窗口基类)和 Ui_FormTools(Qt Designer 生成的 UI 界面) - 负责显示工具箱的主界面,提供工具的分类和导航功能。 - - 窗口特性: - - 默认尺寸: 400x293 像素 - - 支持窗口居中显示 - - 可作为独立窗口或嵌入其他窗口使用 - - Attributes: - parent: 父窗口对象,默认为 None(顶级窗口) + """插件管理窗口。 + + 继承自 QWidget 和 Ui_FormPlugins(Qt Designer 生成的 UI 界面), + 负责展示插件卡片列表,处理插件安装/更新/卸载事件的转发。 + + 可作为独立窗口或嵌入其他窗口使用。 """ - update_tool_card = Signal(dict) + + def __init__(self, plugin_registry: PluginRegistry, parent: QWidget = None) -> None: + """初始化插件管理窗口。 - def __init__(self, plugin_registry:PluginRegistry, parent: QWidget = None) -> None: - """初始化工具箱窗口 - - 创建工具箱窗口实例,初始化 UI 界面。 - Args: - parent: 父窗口对象,用于建立父子关系。 - 默认为 None,表示这是顶级窗口。 + plugin_registry: 插件注册中心实例,用于事件转发 + parent: 父窗口对象,默认为 None(顶级窗口) """ super().__init__(parent) self.setupUi(self) @@ -72,16 +55,10 @@ class Plugins(QWidget, Ui_FormPlugins): self.plugin_registry.uninstall_completed_signal.connect(self.on_plugin_uninstalled) def initUI(self) -> None: - """ - 初始化用户界面 - - 配置窗口标题、图标、大小和居中显示。 - """ + """初始化窗口标题和图标。""" self.setWindowTitle("工具箱") self.setWindowIcon(QIcon('resources/tools.ico')) - # self.update_tool_card.connect(self.on_update_tool_card) - def load_stylesheet(self) -> None: """ 加载 Qt 样式表和自定义样式文件 @@ -103,40 +80,7 @@ class Plugins(QWidget, Ui_FormPlugins): except Exception as e: logger.warning(f"样式加载失败,使用默认主题: {e}") - # def start_load_tools(self): - # self.sub_thread = threading.Thread(target= self.load_tool_list,args=(self.tools,)) - # self.sub_thread.start() - - # def load_tool_list(self, tools:dict): - # if not os.path.exists(REMOTE_PLUGINS_PATH): - # logger.error("服务器链接错误!") - # return - # #读取服务器工具信息 - # sys.path.append(str(REMOTE_PLUGINS_PATH)) - # for tool in Path(REMOTE_PLUGINS_PATH).glob("*.pyd"): - # tool_name = tool.stem - # try: - # module = importlib.import_module(tool_name) - - # name = module.read_tool_name() - # version = module.read_tool_version() - # description = module.read_tool_description() - # if name in tools.keys(): - # pass - # else: - # tools[name] = {} - # tools[name]["name"] = tool_name - # tools[name]["local version"] = None - # tools[name]["local description"] = None - # tools[name]["remote version"] = version - # tools[name]["remote description"] = description - # tools[name]['remote path'] = REMOTE_PLUGINS_PATH - # except Exception as e: - # logger.error(f"服务器{tool_name}无法加载:{str(e)}") - # sys.path.remove(str(REMOTE_PLUGINS_PATH)) - # self.update_tool_card.emit(tools) - - def on_update_plugins_card(self, plugins:dict): + def on_update_plugins_card(self, plugins: dict) -> None: for name in plugins.keys(): if name in self.cards: # 卡片已存在,只刷新状态(避免重复创建叠加) diff --git a/core/plugin/plugins_card.py b/core/plugin/plugins_card.py index 9cc72e2..5deedbf 100644 --- a/core/plugin/plugins_card.py +++ b/core/plugin/plugins_card.py @@ -1,15 +1,8 @@ -import os -import time -import logging -import threading - from .plugin_registry import Event from PySide6.QtWidgets import QFrame from PySide6.QtCore import Signal from .plugins_card_ui import Ui_FramePluginsCard -logger = logging.getLogger(__name__) - def parse_version(version: str) -> tuple: """将版本字符串解析为可比较的元组,如 '1.10.0' -> (1, 10, 0)。 @@ -34,13 +27,27 @@ def parse_version(version: str) -> tuple: class PluginsCard(QFrame, Ui_FramePluginsCard): - """工具卡片类 + """插件卡片组件。 - 继承自 QFrame(Qt 帧基类)和 Ui_FrameToolCard(Qt Designer 生成的 UI 界面) - 负责显示工具的卡片界面,包含工具的图标、名称和描述。 + 继承自 QFrame 和 Ui_FramePluginsCard(Qt Designer 生成的 UI 界面), + 负责展示单个插件的图标、名称、版本和描述,并提供安装/更新/卸载按钮。 + + Attributes: + name: 插件名称 + info: 插件元数据字典,含 local version / remote version / description 等 """ - send_event_signal = Signal(Event,str) - def __init__(self, name: str, info:dict, parent=None) -> None: + + # 按钮点击信号,携带事件类型和插件名称 + send_event_signal = Signal(Event, str) + + def __init__(self, name: str, info: dict, parent=None) -> None: + """初始化插件卡片。 + + Args: + name: 插件名称 + info: 插件元数据字典 + parent: 父组件,默认为 None + """ super().__init__(parent) self.setupUi(self) @@ -49,7 +56,8 @@ class PluginsCard(QFrame, Ui_FramePluginsCard): self.initUI() - def initUI(self): + def initUI(self) -> None: + """初始化卡片 UI:刷新显示内容并绑定按钮事件。""" self.refresh() self.textBrowser.setReadOnly(True) self.pushButtonUpdate.clicked.connect(self.on_update_event) @@ -69,7 +77,12 @@ class PluginsCard(QFrame, Ui_FramePluginsCard): else: self.pushButtonUpdate.setEnabled(False) - def to_card(self): + def to_card(self) -> str: + """将插件信息渲染为 Markdown 卡片文本。 + + Returns: + 格式化的 Markdown 字符串,包含名称、版本和描述。 + """ remote_ver = self.info.get('remote version') local_ver = self.info.get('local version') desc = self.info.get('remote description') or self.info.get('local description') or '暂无' @@ -83,11 +96,14 @@ class PluginsCard(QFrame, Ui_FramePluginsCard): card += f"**描述:** {desc}" return card - def on_install_event(self): + def on_install_event(self) -> None: + """触发安装事件。""" self.send_event_signal.emit(Event.Install, self.name) - def on_uninstall_event(self): + def on_uninstall_event(self) -> None: + """触发卸载事件。""" self.send_event_signal.emit(Event.Uninstall, self.name) - - def on_update_event(self): + + def on_update_event(self) -> None: + """触发更新事件。""" self.send_event_signal.emit(Event.Update, self.name) \ No newline at end of file diff --git a/main.py b/main.py index 1d80269..d316780 100644 --- a/main.py +++ b/main.py @@ -17,9 +17,6 @@ import subprocess import runtime_hook # noqa: F401 启动时把 plugins/*/ 加进 sys.path,必须在 core 之前 import core import traceback -import importlib - -from pathlib import Path from typing import Optional from PySide6.QtCore import QTimer @@ -38,7 +35,6 @@ MICRO_VER: int = 1 # 修订版本号 APP_NAME: str = "Model Team Tools" APP_VERSION: str = f"{MAJOR_VER}.{MINOR_VER}.{MICRO_VER}" -LOCAL_PLUGINS_PATH = Path("./plugins") class MainWindow(QMainWindow, Ui_MainWindow): """ @@ -57,7 +53,9 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.plugin_registry.plugins_loader_signal.connect(self.on_plugins_loader) self.plugin_registry.uninstall_completed_signal.connect(self.on_plugin_uninstalled) + # 工具栏:插件名称 → QAction 映射 self.tool_actions: dict = {} + # 内容区:插件名称 → QWidget 映射 self.plugin_widgets: dict = {} self.initUI() @@ -106,19 +104,9 @@ class MainWindow(QMainWindow, Ui_MainWindow): logger.warning(f"样式加载失败,使用默认主题: {e}") def on_tools(self) -> None: - """打开工具箱窗口 - - 创建并显示工具箱窗口实例。包含完整的错误处理机制, - 确保任何异常都不会导致主程序崩溃。 - - 异常处理: - - 捕获所有异常并记录到日志 - - 显示详细的错误堆栈信息 - - 不影响主窗口的正常使用 - - 日志记录: - - 成功打开工具箱时记录 INFO 级别日志 - - 打开失败时记录 ERROR 级别日志 + """打开插件管理窗口。 + + 包含完整的错误处理机制,确保任何异常都不会导致主程序崩溃。 """ try: self.plugins.show() @@ -126,7 +114,14 @@ class MainWindow(QMainWindow, Ui_MainWindow): logger.error(f"无法打开工具箱窗口: {e}") logger.error(traceback.format_exc()) - def on_plugins_loader(self, plugins:dict): + def on_plugins_loader(self, plugins: dict) -> None: + """加载已安装插件到主窗口工具栏和堆栈页面。 + + 为每个插件创建 QAction 并添加到工具栏,同时将插件 QWidget + 添加到 stackedWidget 中,点击 action 时切换对应页面。 + """ + if not plugins: + return for name in plugins.keys(): action = QAction(name,self) self.toolBar.addAction(action) @@ -136,9 +131,10 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.stackedWidget.addWidget(widget) self.tool_actions[name] = action self.plugin_widgets[name] = widget + logger.info(f"已加载 {len(plugins)} 个插件到工具栏: {list(plugins.keys())}") - def on_action_triggered(self, action:QAction): - print(action.text()) + def on_action_triggered(self, action: QAction) -> None: + """工具栏按钮点击:切换到对应插件的 stack 页面。""" self.stackedWidget.setCurrentWidget(self.plugin_registry.plugins[action.text()]['obj']) def on_plugin_uninstalled(self, name: str) -> None: @@ -147,6 +143,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): .pyd 已被进程加载,运行时无法删除(Windows 文件锁),因此重启进程: 新进程启动时会先执行待删清单,在 import 之前删除 .pyd 文件。 """ + logger.info(f"从主窗口移除插件: {name}") action = self.tool_actions.pop(name, None) if action is not None: self.toolBar.removeAction(action) @@ -197,8 +194,10 @@ class MainWindow(QMainWindow, Ui_MainWindow): if __name__ == '__main__': + print(f"{APP_NAME} v{APP_VERSION} 启动中...") app = QApplication(sys.argv) main: MainWindow = MainWindow() + app.aboutToQuit.connect(lambda: logger.info(f"{APP_NAME} 退出")) app.aboutToQuit.connect(main.plugin_registry.shutdown) main.show() sys.exit(app.exec()) \ No newline at end of file