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