docs: 完善README文档与架构图,修复全量注释与日志问题

- README: 补充项目概述、技术栈、架构图(Mermaid)、插件生命周期时序图
- build.py: 补充模块和函数 docstring
- core/__init__.py: 修正模块说明(工具箱→插件系统)
- log.py: 修复过时注释,清理死代码,抑制第三方库日志刷屏
- plugin_registry.py: 修正矛盾注释,补充缺失日志,隐藏远程路径
- plugins.py: 修正过时注释,清理死代码和无用导入
- plugins_card.py: 修正错误类名注释,补充全量 docstring
- main.py: 修正错误注释,补充启动/退出/插件加载日志
This commit is contained in:
2026-07-21 17:04:34 +08:00
parent 0efaf2946b
commit 1b6796ec43
8 changed files with 282 additions and 138 deletions
+154 -1
View File
@@ -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<br/>主窗口]
PLUG[Plugins<br/>插件管理窗口]
CARD[PluginsCard<br/>插件卡片]
LOG[FrameLog<br/>日志面板]
end
subgraph CORE["Core 层"]
REG[PluginRegistry<br/>插件注册中心<br/>主线程]
WK[PluginWorker<br/>I/O 工作线程]
LH[LogHandler<br/>日志处理器]
end
subgraph HOOK["启动层"]
RH[runtime_hook<br/>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` 格式分发,内含:
- `<tool_name>.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**
+20 -5
View File
@@ -1,3 +1,11 @@
"""Nuitka 打包构建脚本。
将 main.py 编译为 Windows 独立可执行文件,并打包为便携式 zip。
需要安装 Nuitka 和 mingw64 编译器。
用法:
python build.py
"""
import os import os
import sys import sys
import glob import glob
@@ -8,15 +16,25 @@ from zipfile import ZIP_DEFLATED, ZipFile
from main import APP_NAME, APP_VERSION from main import APP_NAME, APP_VERSION
# 编译器选择
COMPILER = "mingw64" COMPILER = "mingw64"
# 输出路径
OUTPUT_PATH = Path('output') OUTPUT_PATH = Path('output')
RESOURCES_PATH = Path("resources") RESOURCES_PATH = Path("resources")
RELEASE_PATH = OUTPUT_PATH / APP_NAME RELEASE_PATH = OUTPUT_PATH / APP_NAME
BUILD_PATH = OUTPUT_PATH / f'{platform.system().lower()}-{platform.machine().lower()}' BUILD_PATH = OUTPUT_PATH / f'{platform.system().lower()}-{platform.machine().lower()}'
def build_main(): def build_main():
"""使用 Nuitka 编译 main.py 为独立可执行文件。
编译选项:
- --standalone:不依赖本地 Python 环境
- --plugin-enable=pyside6:启用 PySide6 支持
- --include-module=runtime_hook:将启动钩子编入 exe
- --windows-console-mode=disable:不显示控制台窗口
"""
nuitka_cmd = [ nuitka_cmd = [
sys.executable, sys.executable,
'-m', '-m',
@@ -26,10 +44,6 @@ def build_main():
'--show-progress', '--show-progress',
'--plugin-enable=pyside6', '--plugin-enable=pyside6',
'--include-module=qt_material', '--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', '--include-module=runtime_hook',
f"--include-data-dir={RESOURCES_PATH}=resources", f"--include-data-dir={RESOURCES_PATH}=resources",
] ]
@@ -54,6 +68,7 @@ def build_main():
def create_zip(): def create_zip():
"""将 Nuitka 编译产物打包为便携式 zip 文件。"""
file_list = glob.glob(f'{BUILD_PATH / APP_NAME / "dist"}', recursive=True) file_list = glob.glob(f'{BUILD_PATH / APP_NAME / "dist"}', recursive=True)
file_list.sort() file_list.sort()
+1 -1
View File
@@ -3,7 +3,7 @@ Core 模块 - 核心功能模块包
本包包含应用程序的核心功能组件: 本包包含应用程序的核心功能组件:
- 日志系统(logging 包) - 日志系统(logging 包)
- 工具箱系统(tools 包) - 插件系统(plugin 包)
提供统一的接口导出,方便其他模块调用。 提供统一的接口导出,方便其他模块调用。
+7 -12
View File
@@ -20,7 +20,7 @@ from pathlib import Path
from typing import Callable, Optional from typing import Callable, Optional
from PySide6.QtCore import QObject, Signal from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QFrame, QTextEdit from PySide6.QtWidgets import QFrame
from .log_ui import Ui_FrameLog from .log_ui import Ui_FrameLog
@@ -115,6 +115,10 @@ class FrameLog(QFrame, Ui_FrameLog):
logger = logging.getLogger() logger = logging.getLogger()
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
# 抑制第三方库 DEBUG 日志刷屏
logging.getLogger("PySide6").setLevel(logging.WARNING)
logging.getLogger("qt_material").setLevel(logging.WARNING)
# 创建日志格式化器:时间 [级别] 消息 # 创建日志格式化器:时间 [级别] 消息
formatter = logging.Formatter( formatter = logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s", "%(asctime)s [%(levelname)s] %(message)s",
@@ -190,20 +194,11 @@ class FrameLog(QFrame, Ui_FrameLog):
self.log_signal.connect(self._update_log_display) self.log_signal.connect(self._update_log_display)
def _update_log_display(self, message: str) -> None: def _update_log_display(self, message: str) -> None:
""" """更新 UI 的日志显示组件。
更新 UI 的日志显示组件
将日志消息追加到 TextEdit 组件中显示 将日志消息追加到 TextEdit 组件中显示
Args: Args:
message: 格式化后的日志消息字符串 message: 格式化后的日志消息字符串
实现细节:
- 使用 findChild 查找 TextEdit 组件
- 使用 append 方法追加日志消息,自动换行
""" """
self.textEdit.append(message) self.textEdit.append(message)
# text_edit = self.findChild(QTextEdit, "textEdit")
# if text_edit:
# text_edit.append(message)
+27 -5
View File
@@ -40,8 +40,9 @@ def _load_module_from_path(module_name: str, file_path: Path):
用 spec_from_file_location 直接从 .pyd 文件构建模块,避免 用 spec_from_file_location 直接从 .pyd 文件构建模块,避免
importlib.import_module 命中同名旧缓存。注意:.pyd 的入口函数 importlib.import_module 命中同名旧缓存。注意:.pyd 的入口函数
PyInit_<name> 与文件名绑定,module_name 必须等于文件 stem PyInit_<name> 与文件名绑定,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) spec = importlib.util.spec_from_file_location(module_name, file_path)
if spec is None or spec.loader is None: 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: def do_discover_remote(self, plugins_dir: str) -> None:
"""扫描远程 *.zip,读 zip 内 manifest.json 取插件元数据。""" """扫描远程 *.zip,读 zip 内 manifest.json 取插件元数据。"""
if not os.path.exists(plugins_dir): if not os.path.exists(plugins_dir):
logger.error(f"服务器链接错误!路径不存在: {plugins_dir}") logger.error(f"远程插件路径不可达")
return return
logger.info(f"开始扫描远程插件 zip: {plugins_dir}") logger.info(f"开始扫描远程插件 zip")
self.remote_discovered.emit( self.remote_discovered.emit(
self._scan_remote_zips(Path(plugins_dir)) self._scan_remote_zips(Path(plugins_dir))
) )
@@ -125,7 +126,7 @@ class PluginWorker(QObject):
tmp_zip.unlink() tmp_zip.unlink()
self.install_finished.emit(True, f"安装成功: {local_dir}") self.install_finished.emit(True, f"安装成功: {local_dir}")
except Exception as e: except Exception as e:
logger.error(f"{remote_zip}安装失败{e.args}") logger.error(f"{remote_zip} 安装失败: {e}")
self.install_finished.emit(False, str(e)) self.install_finished.emit(False, str(e))
@Slot(str, str) @Slot(str, str)
@@ -166,6 +167,7 @@ class PluginWorker(QObject):
def _ensure_dir(plugins_dir: Path) -> bool: def _ensure_dir(plugins_dir: Path) -> bool:
if not os.path.exists(plugins_dir): if not os.path.exists(plugins_dir):
os.mkdir(plugins_dir) os.mkdir(plugins_dir)
logger.info(f"插件目录已创建: {plugins_dir}")
return False return False
return True return True
@@ -231,18 +233,21 @@ class PluginRegistry(QObject):
主线程。所有跨线程通信经 Qt 信号槽(Queued)。 主线程。所有跨线程通信经 Qt 信号槽(Queued)。
""" """
# --- 对外信号:通知主窗口 UI 更新 ---
plugins_loader_signal = Signal(dict) plugins_loader_signal = Signal(dict)
update_plugins_card_signal = Signal(dict) update_plugins_card_signal = Signal(dict)
uninstall_completed_signal = Signal(str) uninstall_completed_signal = Signal(str)
install_progress = Signal(str, int, int) # name, copied_bytes, total_bytes install_progress = Signal(str, int, int) # name, copied_bytes, total_bytes
install_finished = Signal(bool, str) install_finished = Signal(bool, str)
# --- 内部信号:主线程 → Worker 线程(跨线程请求) ---
_discover_local_requested = Signal(str) _discover_local_requested = Signal(str)
_discover_remote_requested = Signal(str) _discover_remote_requested = Signal(str)
_install_requested = Signal(str, str) _install_requested = Signal(str, str)
_uninstall_requested = Signal(str, str) _uninstall_requested = Signal(str, str)
_update_requested = Signal(str, str, str) _update_requested = Signal(str, str, str)
# 自重启后延迟启动,等待旧进程释放 .pyd 文件锁(毫秒)
_PENDING_CLEANUP_DELAY = 1500 _PENDING_CLEANUP_DELAY = 1500
def __init__(self, parent: Optional[QObject] = None) -> None: def __init__(self, parent: Optional[QObject] = None) -> None:
@@ -284,6 +289,7 @@ class PluginRegistry(QObject):
""" """
self._process_pending_uninstalls() self._process_pending_uninstalls()
self._thread.start() self._thread.start()
logger.info("插件系统初始化完成,开始发现插件")
QTimer.singleShot(0, self._start_discovery) QTimer.singleShot(0, self._start_discovery)
def shutdown(self) -> None: def shutdown(self) -> None:
@@ -292,8 +298,10 @@ class PluginRegistry(QObject):
必须在 QThread 析构前调用,否则触发 必须在 QThread 析构前调用,否则触发
"QThread: Destroyed while thread is still running" 警告。 "QThread: Destroyed while thread is still running" 警告。
""" """
logger.info("PluginRegistry 正在关闭...")
self._thread.quit() self._thread.quit()
self._thread.wait(3000) self._thread.wait(3000)
logger.info("PluginRegistry 已关闭")
@staticmethod @staticmethod
def _process_pending_uninstalls() -> None: def _process_pending_uninstalls() -> None:
@@ -309,6 +317,7 @@ class PluginRegistry(QObject):
""" """
if not UNINSTALL_PENDING_FILE.exists(): if not UNINSTALL_PENDING_FILE.exists():
return return
logger.info("检测到待处理卸载/更新清单,开始清理...")
lines = UNINSTALL_PENDING_FILE.read_text(encoding='utf-8').splitlines() lines = UNINSTALL_PENDING_FILE.read_text(encoding='utf-8').splitlines()
failed: list = [] failed: list = []
for line in lines: for line in lines:
@@ -354,11 +363,18 @@ class PluginRegistry(QObject):
UNINSTALL_PENDING_FILE.unlink() UNINSTALL_PENDING_FILE.unlink()
def _start_discovery(self) -> None: def _start_discovery(self) -> None:
"""启动插件发现:扫描本地 pyd 和远程 zip。"""
self._discover_local_requested.emit(str(LOCAL_PLUGINS_PATH)) self._discover_local_requested.emit(str(LOCAL_PLUGINS_PATH))
self._discover_remote_requested.emit(str(REMOTE_PLUGINS_PATH)) self._discover_remote_requested.emit(str(REMOTE_PLUGINS_PATH))
@Slot(list) @Slot(list)
def _on_local_discovered(self, results: list) -> None: 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: for item in results:
name = item["name"] name = item["name"]
try: try:
@@ -367,6 +383,7 @@ class PluginRegistry(QObject):
except Exception as e: except Exception as e:
logger.error(f"本地插件 {name} 实例化失败,跳过: {e}") logger.error(f"本地插件 {name} 实例化失败,跳过: {e}")
continue continue
logger.info(f"本地插件已加载: {name} V{item['version']}")
if name not in self.plugins: if name not in self.plugins:
self.plugins[name] = {} self.plugins[name] = {}
self.plugins[name]["obj"] = obj self.plugins[name]["obj"] = obj
@@ -378,6 +395,7 @@ class PluginRegistry(QObject):
@Slot(list) @Slot(list)
def _on_remote_discovered(self, results: list) -> None: def _on_remote_discovered(self, results: list) -> None:
"""处理远程扫描结果:填充远程版本/描述/路径,刷新卡片列表。"""
for item in results: for item in results:
name = item["name"] name = item["name"]
if name not in self.plugins: if name not in self.plugins:
@@ -392,6 +410,7 @@ class PluginRegistry(QObject):
@Slot(bool, str) @Slot(bool, str)
def _on_install_finished(self, success: bool, message: str) -> None: def _on_install_finished(self, success: bool, message: str) -> None:
"""安装完成回调:成功则把新解压目录加入 sys.path 并重新扫描本地。"""
if success: if success:
logger.info(message) logger.info(message)
for sub in LOCAL_PLUGINS_PATH.iterdir(): for sub in LOCAL_PLUGINS_PATH.iterdir():
@@ -417,6 +436,7 @@ class PluginRegistry(QObject):
@Slot(str, bool, str) @Slot(str, bool, str)
def _on_uninstall_finished(self, name: str, success: bool, message: str) -> None: def _on_uninstall_finished(self, name: str, success: bool, message: str) -> None:
"""卸载/更新完成回调:清除 plugins 字典中对应项的 obj 和版本,通知主窗口。"""
if not success: if not success:
logger.error(message) logger.error(message)
return return
@@ -442,8 +462,10 @@ class PluginRegistry(QObject):
remote_path = self.plugins[name]['remote path'] remote_path = self.plugins[name]['remote path']
self._install_requested.emit(local_dir, remote_path) self._install_requested.emit(local_dir, remote_path)
elif event == Event.Uninstall: elif event == Event.Uninstall:
logger.info(f"[{name}] 开始卸载...")
self._uninstall_requested.emit(name, local_dir) self._uninstall_requested.emit(name, local_dir)
elif event == Event.Update: elif event == Event.Update:
self._installing_name = name self._installing_name = name
logger.info(f"[{name}] 开始更新...")
remote_path = self.plugins[name]['remote path'] remote_path = self.plugins[name]['remote path']
self._update_requested.emit(name, local_dir, remote_path) self._update_requested.emit(name, local_dir, remote_path)
+16 -72
View File
@@ -1,64 +1,47 @@
""" """
工具箱模块 插件管理模块
提供工具箱主窗口,包含各种实用工具的入口。 提供插件管理主窗口,包含插件卡片列表和安装/更新/卸载入口。
使用 PySide6 构建的窗口框架。 使用 PySide6 构建的窗口框架。
主要功能: 主要功能:
- 工具箱主窗口界面 - 插件管理窗口界面
- 窗口居中显示功能 - 插件卡片列表渲染与状态刷新
- 工具分类和导航 - 插件事件的转发与处理
Author: Model Team Author: Model Team
Version: 0.0.1 Version: 0.0.1
""" """
import os
import sys
import logging import logging
import threading
import importlib
from qt_material import apply_stylesheet from qt_material import apply_stylesheet
from pathlib import Path
from .plugins_ui import Ui_FormPlugins from .plugins_ui import Ui_FormPlugins
from PySide6.QtGui import QIcon from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QWidget, QListWidgetItem from PySide6.QtWidgets import QWidget, QListWidgetItem
from PySide6.QtCore import QSize, Signal from PySide6.QtCore import QSize
from .plugins_card import PluginsCard from .plugins_card import PluginsCard
from .plugin_registry import PluginRegistry from .plugin_registry import PluginRegistry
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
LOCAL_PLUGINS_PATH = Path("./plugins")
REMOTE_PLUGINS_PATH = Path('Y:/SE/xufeifei/plugins')
class Plugins(QWidget, Ui_FormPlugins): class Plugins(QWidget, Ui_FormPlugins):
"""工具箱主窗口 """插件管理窗口
继承自 QWidgetQt 窗口基类)和 Ui_FormToolsQt Designer 生成的 UI 界面) 继承自 QWidget 和 Ui_FormPluginsQt Designer 生成的 UI 界面)
负责显示工具箱的主界面,提供工具的分类和导航功能 负责展示插件卡片列表,处理插件安装/更新/卸载事件的转发
窗口特性: 可作为独立窗口或嵌入其他窗口使用。
- 默认尺寸: 400x293 像素
- 支持窗口居中显示
- 可作为独立窗口或嵌入其他窗口使用
Attributes:
parent: 父窗口对象,默认为 None(顶级窗口)
""" """
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: Args:
parent: 父窗口对象,用于建立父子关系。 plugin_registry: 插件注册中心实例,用于事件转发
默认为 None,表示这是顶级窗口 parent: 父窗口对象,默认为 None顶级窗口
""" """
super().__init__(parent) super().__init__(parent)
self.setupUi(self) self.setupUi(self)
@@ -72,16 +55,10 @@ class Plugins(QWidget, Ui_FormPlugins):
self.plugin_registry.uninstall_completed_signal.connect(self.on_plugin_uninstalled) self.plugin_registry.uninstall_completed_signal.connect(self.on_plugin_uninstalled)
def initUI(self) -> None: def initUI(self) -> None:
""" """初始化窗口标题和图标。"""
初始化用户界面
配置窗口标题、图标、大小和居中显示。
"""
self.setWindowTitle("工具箱") self.setWindowTitle("工具箱")
self.setWindowIcon(QIcon('resources/tools.ico')) self.setWindowIcon(QIcon('resources/tools.ico'))
# self.update_tool_card.connect(self.on_update_tool_card)
def load_stylesheet(self) -> None: def load_stylesheet(self) -> None:
""" """
加载 Qt 样式表和自定义样式文件 加载 Qt 样式表和自定义样式文件
@@ -103,40 +80,7 @@ class Plugins(QWidget, Ui_FormPlugins):
except Exception as e: except Exception as e:
logger.warning(f"样式加载失败,使用默认主题: {e}") logger.warning(f"样式加载失败,使用默认主题: {e}")
# def start_load_tools(self): def on_update_plugins_card(self, plugins: dict) -> None:
# 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):
for name in plugins.keys(): for name in plugins.keys():
if name in self.cards: if name in self.cards:
# 卡片已存在,只刷新状态(避免重复创建叠加) # 卡片已存在,只刷新状态(避免重复创建叠加)
+33 -17
View File
@@ -1,15 +1,8 @@
import os
import time
import logging
import threading
from .plugin_registry import Event from .plugin_registry import Event
from PySide6.QtWidgets import QFrame from PySide6.QtWidgets import QFrame
from PySide6.QtCore import Signal from PySide6.QtCore import Signal
from .plugins_card_ui import Ui_FramePluginsCard from .plugins_card_ui import Ui_FramePluginsCard
logger = logging.getLogger(__name__)
def parse_version(version: str) -> tuple: def parse_version(version: str) -> tuple:
"""将版本字符串解析为可比较的元组,如 '1.10.0' -> (1, 10, 0)。 """将版本字符串解析为可比较的元组,如 '1.10.0' -> (1, 10, 0)。
@@ -34,13 +27,27 @@ def parse_version(version: str) -> tuple:
class PluginsCard(QFrame, Ui_FramePluginsCard): class PluginsCard(QFrame, Ui_FramePluginsCard):
"""工具卡片类 """插件卡片组件。
继承自 QFrameQt 帧基类)和 Ui_FrameToolCardQt Designer 生成的 UI 界面) 继承自 QFrame 和 Ui_FramePluginsCardQt 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) super().__init__(parent)
self.setupUi(self) self.setupUi(self)
@@ -49,7 +56,8 @@ class PluginsCard(QFrame, Ui_FramePluginsCard):
self.initUI() self.initUI()
def initUI(self): def initUI(self) -> None:
"""初始化卡片 UI:刷新显示内容并绑定按钮事件。"""
self.refresh() self.refresh()
self.textBrowser.setReadOnly(True) self.textBrowser.setReadOnly(True)
self.pushButtonUpdate.clicked.connect(self.on_update_event) self.pushButtonUpdate.clicked.connect(self.on_update_event)
@@ -69,7 +77,12 @@ class PluginsCard(QFrame, Ui_FramePluginsCard):
else: else:
self.pushButtonUpdate.setEnabled(False) self.pushButtonUpdate.setEnabled(False)
def to_card(self): def to_card(self) -> str:
"""将插件信息渲染为 Markdown 卡片文本。
Returns:
格式化的 Markdown 字符串,包含名称、版本和描述。
"""
remote_ver = self.info.get('remote version') remote_ver = self.info.get('remote version')
local_ver = self.info.get('local version') local_ver = self.info.get('local version')
desc = self.info.get('remote description') or self.info.get('local description') or '暂无' 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}" card += f"**描述:** {desc}"
return card return card
def on_install_event(self): def on_install_event(self) -> None:
"""触发安装事件。"""
self.send_event_signal.emit(Event.Install, self.name) 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) 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) self.send_event_signal.emit(Event.Update, self.name)
+18 -19
View File
@@ -17,9 +17,6 @@ import subprocess
import runtime_hook # noqa: F401 启动时把 plugins/*/ 加进 sys.path,必须在 core 之前 import runtime_hook # noqa: F401 启动时把 plugins/*/ 加进 sys.path,必须在 core 之前
import core import core
import traceback import traceback
import importlib
from pathlib import Path
from typing import Optional from typing import Optional
from PySide6.QtCore import QTimer from PySide6.QtCore import QTimer
@@ -38,7 +35,6 @@ MICRO_VER: int = 1 # 修订版本号
APP_NAME: str = "Model Team Tools" APP_NAME: str = "Model Team Tools"
APP_VERSION: str = f"{MAJOR_VER}.{MINOR_VER}.{MICRO_VER}" APP_VERSION: str = f"{MAJOR_VER}.{MINOR_VER}.{MICRO_VER}"
LOCAL_PLUGINS_PATH = Path("./plugins")
class MainWindow(QMainWindow, Ui_MainWindow): 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.plugins_loader_signal.connect(self.on_plugins_loader)
self.plugin_registry.uninstall_completed_signal.connect(self.on_plugin_uninstalled) self.plugin_registry.uninstall_completed_signal.connect(self.on_plugin_uninstalled)
# 工具栏:插件名称 → QAction 映射
self.tool_actions: dict = {} self.tool_actions: dict = {}
# 内容区:插件名称 → QWidget 映射
self.plugin_widgets: dict = {} self.plugin_widgets: dict = {}
self.initUI() self.initUI()
@@ -106,19 +104,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
logger.warning(f"样式加载失败,使用默认主题: {e}") logger.warning(f"样式加载失败,使用默认主题: {e}")
def on_tools(self) -> None: def on_tools(self) -> None:
"""打开工具箱窗口 """打开插件管理窗口
创建并显示工具箱窗口实例。包含完整的错误处理机制, 包含完整的错误处理机制,确保任何异常都不会导致主程序崩溃。
确保任何异常都不会导致主程序崩溃。
异常处理:
- 捕获所有异常并记录到日志
- 显示详细的错误堆栈信息
- 不影响主窗口的正常使用
日志记录:
- 成功打开工具箱时记录 INFO 级别日志
- 打开失败时记录 ERROR 级别日志
""" """
try: try:
self.plugins.show() self.plugins.show()
@@ -126,7 +114,14 @@ class MainWindow(QMainWindow, Ui_MainWindow):
logger.error(f"无法打开工具箱窗口: {e}") logger.error(f"无法打开工具箱窗口: {e}")
logger.error(traceback.format_exc()) 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(): for name in plugins.keys():
action = QAction(name,self) action = QAction(name,self)
self.toolBar.addAction(action) self.toolBar.addAction(action)
@@ -136,9 +131,10 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.stackedWidget.addWidget(widget) self.stackedWidget.addWidget(widget)
self.tool_actions[name] = action self.tool_actions[name] = action
self.plugin_widgets[name] = widget self.plugin_widgets[name] = widget
logger.info(f"已加载 {len(plugins)} 个插件到工具栏: {list(plugins.keys())}")
def on_action_triggered(self, action:QAction): def on_action_triggered(self, action: QAction) -> None:
print(action.text()) """工具栏按钮点击:切换到对应插件的 stack 页面。"""
self.stackedWidget.setCurrentWidget(self.plugin_registry.plugins[action.text()]['obj']) self.stackedWidget.setCurrentWidget(self.plugin_registry.plugins[action.text()]['obj'])
def on_plugin_uninstalled(self, name: str) -> None: def on_plugin_uninstalled(self, name: str) -> None:
@@ -147,6 +143,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
.pyd 已被进程加载,运行时无法删除(Windows 文件锁),因此重启进程: .pyd 已被进程加载,运行时无法删除(Windows 文件锁),因此重启进程:
新进程启动时会先执行待删清单,在 import 之前删除 .pyd 文件。 新进程启动时会先执行待删清单,在 import 之前删除 .pyd 文件。
""" """
logger.info(f"从主窗口移除插件: {name}")
action = self.tool_actions.pop(name, None) action = self.tool_actions.pop(name, None)
if action is not None: if action is not None:
self.toolBar.removeAction(action) self.toolBar.removeAction(action)
@@ -197,8 +194,10 @@ class MainWindow(QMainWindow, Ui_MainWindow):
if __name__ == '__main__': if __name__ == '__main__':
print(f"{APP_NAME} v{APP_VERSION} 启动中...")
app = QApplication(sys.argv) app = QApplication(sys.argv)
main: MainWindow = MainWindow() main: MainWindow = MainWindow()
app.aboutToQuit.connect(lambda: logger.info(f"{APP_NAME} 退出"))
app.aboutToQuit.connect(main.plugin_registry.shutdown) app.aboutToQuit.connect(main.plugin_registry.shutdown)
main.show() main.show()
sys.exit(app.exec()) sys.exit(app.exec())