diff --git a/.gitignore b/.gitignore
index b0bfc9b..380d827 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,7 +7,7 @@ __pycache__/
# C extensions
*.so
-
+*.exe
# Distribution / packaging
.Python
build/
@@ -165,4 +165,5 @@ toollib/frame.csv
*.evf
*.mvf
old/
-发布流程.txt
\ No newline at end of file
+发布流程.txt
+src/plugins/*/*.json
diff --git a/requirements.txt b/requirements.txt
index f436824..b3a8370 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,8 @@
pyQt5==5.15.11
ms-toollib==1.5.1
-setuptools==78.1.1
+setuptools==80.9.0
pyinstaller==6.16.0
-
+msgspec>=0.20.0
+zmq>=0.0.0
diff --git a/src/main.py b/src/main.py
index 424b349..5ffb0d7 100644
--- a/src/main.py
+++ b/src/main.py
@@ -12,9 +12,50 @@
import ms_toollib as ms
import ctypes
from ctypes import wintypes
+from mp_plugins.context import AppContext
+from mp_plugins.events import *
+from mp_plugins import PluginManager
+from pathlib import Path
+import os
+
os.environ["QT_FONT_DPI"] = "96"
+# def patch_env():
+# import os
+
+
+# env = os.environ.copy()
+# root = os.path.dirname(os.path.abspath(__file__)) # 你的项目根目录
+# env["PYTHONPATH"] = root
+# return env
+def get_paths():
+ if getattr(sys, "frozen", False):
+ # 打包成 exe
+ dir = os.path.dirname(sys.executable) # exe 所在目录
+ else:
+ dir = os.path.dirname(os.path.abspath(__file__))
+
+ return dir
+
+
+def patch_env():
+ import os
+ import sys
+
+ env = os.environ.copy()
+
+ if getattr(sys, "frozen", False):
+ # 打包成 exe,库解压到 _MEIPASS
+ root = getattr(sys, "_MEIPASS", None)
+ else:
+ # 调试模式,库在项目目录
+ root = os.path.dirname(os.path.abspath(__file__))
+
+ env["PYTHONPATH"] = root
+ return env
+
+
def on_new_connection(localServer: QLocalServer):
"""当新连接进来时,接受连接并将文件路径传递给主窗口"""
socket = localServer.nextPendingConnection()
@@ -53,7 +94,7 @@ def find_window(class_name, window_name):
"""
- user32 = ctypes.WinDLL('user32', use_last_error=True)
+ user32 = ctypes.WinDLL("user32", use_last_error=True)
user32.FindWindowW.argtypes = [wintypes.LPCWSTR, wintypes.LPCWSTR]
user32.FindWindowW.restype = wintypes.HWND
@@ -63,36 +104,39 @@ def find_window(class_name, window_name):
return hwnd
-
def cli_check_file(file_path: str) -> int:
if not os.path.exists(file_path):
print("ERROR: file not found")
return 2
-
+
# 搜集目录或文件下的所有evf和evfs文件
evf_evfs_files = []
- if os.path.isfile(file_path) and (file_path.endswith('.evf') or file_path.endswith('.evfs')):
+ if os.path.isfile(file_path) and (
+ file_path.endswith(".evf") or file_path.endswith(".evfs")
+ ):
evf_evfs_files = [os.path.abspath(file_path)]
elif os.path.isdir(file_path):
- evf_evfs_files = [os.path.abspath(os.path.join(root, file))
- for root, dirs, files in os.walk(file_path)
- for file in files if file.endswith('.evf') or file.endswith('.evfs')]
+ evf_evfs_files = [
+ os.path.abspath(os.path.join(root, file))
+ for root, dirs, files in os.walk(file_path)
+ for file in files
+ if file.endswith(".evf") or file.endswith(".evfs")
+ ]
if not evf_evfs_files:
print("ERROR: must be evf or evfs files or directory")
return 2
-
-
+
# 实例化一个MineSweeperGUI出来
app = QtWidgets.QApplication(sys.argv)
mainWindow = mainWindowGUI.MainWindow()
ui = mineSweeperGUI.MineSweeperGUI(mainWindow, sys.argv)
-
+
for ide, e in enumerate(evf_evfs_files):
if not ui.checksum_module_ok():
print("ERROR: ???")
return 2
- if e.endswith('.evf'):
+ if e.endswith(".evf"):
# 检验evf文件是否合法
video = ms.EvfVideo(e)
try:
@@ -101,12 +145,13 @@ def cli_check_file(file_path: str) -> int:
evf_evfs_files[ide] = (e, 2)
else:
checksum = ui.checksum_guard.get_checksum(
- video.raw_data[:-(len(video.checksum) + 2)])
+ video.raw_data[: -(len(video.checksum) + 2)]
+ )
if video.checksum == checksum:
evf_evfs_files[ide] = (e, 0)
else:
evf_evfs_files[ide] = (e, 1)
- elif e.endswith('.evfs'):
+ elif e.endswith(".evfs"):
# 检验evfs文件是否合法
videos = ms.Evfs(e)
try:
@@ -116,21 +161,22 @@ def cli_check_file(file_path: str) -> int:
else:
if videos.len() <= 0:
evf_evfs_files[ide] = (e, 2)
- checksum = ui.checksum_guard.get_checksum(
- videos[0].evf_video.raw_data)
+ checksum = ui.checksum_guard.get_checksum(videos[0].evf_video.raw_data)
if video.checksum != checksum:
evf_evfs_files[ide] = (e, 1)
continue
for idcell, cell in enumerate(videos[1:]):
checksum = ui.checksum_guard.get_checksum(
- cell.evf_video.raw_data + videos[idcell - 1].checksum)
+ cell.evf_video.raw_data + videos[idcell - 1].checksum
+ )
if cell.evf_file.checksum != checksum:
evf_evfs_files[ide] = (e, 1)
continue
evf_evfs_files[ide] = (e, 0)
print(evf_evfs_files)
return 0
-
+
+
if __name__ == "__main__":
# metaminesweeper.exe -c filename.evf用法,检查文件的合法性
# metaminesweeper.exe -c filename.evfs
@@ -142,7 +188,11 @@ def cli_check_file(file_path: str) -> int:
if args.check:
exit_code = cli_check_file(args.check)
sys.exit(exit_code)
-
+ env = patch_env()
+ context = AppContext("Metasweeper", "1.0.0", "元扫雷")
+ PluginManager.instance().context = context
+
+ PluginManager.instance().start(Path(get_paths()) / "plugins", env)
app = QtWidgets.QApplication(sys.argv)
serverName = "MineSweeperServer"
@@ -159,7 +209,8 @@ def cli_check_file(file_path: str) -> int:
localServer = QLocalServer()
localServer.listen(serverName)
localServer.newConnection.connect(
- lambda: on_new_connection(localServer=localServer))
+ lambda: on_new_connection(localServer=localServer)
+ )
mainWindow = mainWindowGUI.MainWindow()
ui = mineSweeperGUI.MineSweeperGUI(mainWindow, sys.argv)
ui.mainWindow.show()
@@ -169,16 +220,21 @@ def cli_check_file(file_path: str) -> int:
hwnd = find_window(None, _translate("MainWindow", "元扫雷"))
SetWindowDisplayAffinity = ctypes.windll.user32.SetWindowDisplayAffinity
- ui.disable_screenshot = lambda: ... if SetWindowDisplayAffinity(
- hwnd, 0x00000011) else 1/0
- ui.enable_screenshot = lambda: ... if SetWindowDisplayAffinity(
- hwnd, 0x00000000) else 1/0
- ui.disable_screenshot = lambda: ... if SetWindowDisplayAffinity(
- hwnd, 0x00000011) else 1/0
- ui.enable_screenshot = lambda: ... if SetWindowDisplayAffinity(
- hwnd, 0x00000000) else 1/0
+ ui.disable_screenshot = lambda: (
+ ... if SetWindowDisplayAffinity(hwnd, 0x00000011) else 1 / 0
+ )
+ ui.enable_screenshot = lambda: (
+ ... if SetWindowDisplayAffinity(hwnd, 0x00000000) else 1 / 0
+ )
+ ui.disable_screenshot = lambda: (
+ ... if SetWindowDisplayAffinity(hwnd, 0x00000011) else 1 / 0
+ )
+ ui.enable_screenshot = lambda: (
+ ... if SetWindowDisplayAffinity(hwnd, 0x00000000) else 1 / 0
+ )
sys.exit(app.exec_())
+ PluginManager.instance().stop()
...
# except:
# pass
diff --git a/src/mineSweeperGUI.py b/src/mineSweeperGUI.py
index 816eaf2..4357a91 100644
--- a/src/mineSweeperGUI.py
+++ b/src/mineSweeperGUI.py
@@ -26,7 +26,9 @@
from mainWindowGUI import MainWindow
from datetime import datetime
from mineSweeperVideoPlayer import MineSweeperVideoPlayer
-
+from pluginDialog import PluginManagerUI
+from mp_plugins import PluginManager, PluginContext
+from mp_plugins.events import GameEndEvent
class MineSweeperGUI(MineSweeperVideoPlayer):
def __init__(self, MainWindow: MainWindow, args):
@@ -37,7 +39,6 @@ def __init__(self, MainWindow: MainWindow, args):
self.time_10ms: int = 0 # 已毫秒为单位的游戏时间,全局统一的
self.showTime(self.time_10ms // 100)
-
self.timer_10ms = QTimer()
self.timer_10ms.setInterval(10) # 10毫秒回调一次的定时器
self.timer_10ms.timeout.connect(self.timeCount)
@@ -69,6 +70,7 @@ def save_evf_file_integrated():
self.actiongaun_yv.triggered.connect(self.action_AEvent)
self.actionauto_update.triggered.connect(self.auto_Update)
self.actionopen.triggered.connect(self.action_OpenFile)
+ self.actionchajian.triggered.connect(self.action_OpenPluginDialog)
self.english_action.triggered.connect(
lambda: self.trans_language("en_US"))
self.chinese_action.triggered.connect(
@@ -134,7 +136,7 @@ def save_evf_file_integrated():
self.mainWindow.closeEvent_.connect(self.closeEvent_)
self.mainWindow.dropFileSignal.connect(self.action_OpenFile)
-
+
# 播放录像时,记录上一个鼠标状态用。
# 这是一个补丁,因为工具箱里只有UpDown和UpDownNotFlag,
# 也有DownUpAfterChording,但是没有UpDownAfterChording
@@ -247,8 +249,7 @@ def game_state(self, game_state: str):
case "study":
self.num_bar_ui.QWidget.close()
self._game_state = game_state
-
-
+
@property
def row(self):
return self._row
@@ -259,7 +260,7 @@ def row(self, row):
"row": row,
})
self._row = row
-
+
@property
def column(self):
return self._column
@@ -270,7 +271,7 @@ def column(self, column):
"column": column,
})
self._column = column
-
+
@property
def minenum(self):
return self._minenum
@@ -281,7 +282,6 @@ def minenum(self, minenum):
"minenum": minenum,
})
self._minenum = minenum
-
def layMine(self, i, j):
xx = self.row
@@ -452,8 +452,6 @@ def chording_ai(self, i, j):
not_mine_round + is_mine_round)
self.label.ms_board.board = board
-
-
def mineNumWheel(self, i):
'''
在雷上滚轮,调雷数
@@ -564,6 +562,8 @@ def gameFinished(self):
self.score_board_manager.show(self.label.ms_board, index_type=2)
self.enable_screenshot()
self.unlimit_cursor()
+ event = GameEndEvent()
+ PluginManager.instance().send_event(event, response_count=0)
def gameWin(self): # 成功后改脸和状态变量,停时间
self.timer_10ms.stop()
@@ -580,7 +580,7 @@ def gameWin(self): # 成功后改脸和状态变量,停时间
if self.autosave_video and self.checksum_module_ok():
self.dump_evf_file_data()
self.save_evf_file()
-
+
self.gameFinished()
# 尝试弹窗,没有破纪录则不弹
@@ -601,10 +601,10 @@ def dump_evf_file_data(self):
self.label.ms_board.use_question = False # 禁用问号是共识
self.label.ms_board.use_cursor_pos_lim = self.cursor_limit
self.label.ms_board.use_auto_replay = self.auto_replay > 0
-
+
self.label.ms_board.is_fair = self.is_fair()
self.label.ms_board.is_official = self.is_official()
-
+
self.label.ms_board.software = superGUI.version
self.label.ms_board.mode = self.gameMode
self.label.ms_board.player_identifier = self.player_identifier
@@ -614,7 +614,7 @@ def dump_evf_file_data(self):
country_name[self.country].upper()
self.label.ms_board.device_uuid = hashlib.md5(
bytes(str(uuid.getnode()).encode())).hexdigest().encode("UTF-8")
-
+
self.label.ms_board.generate_evf_v4_raw_data()
# 补上校验值
checksum = self.checksum_guard.get_checksum(
@@ -641,8 +641,7 @@ def save_evf_file(self):
os.mkdir(self.replay_path)
self.label.ms_board.save_to_evf_file(self.cal_evf_filename())
-
-
+
# 拼接evf录像的文件名,无后缀
def cal_evf_filename(self, absolute=True) -> str:
if (self.label.ms_board.row, self.label.ms_board.column, self.label.ms_board.mine_num) == (8, 8, 10):
@@ -676,8 +675,6 @@ def cal_evf_filename(self, absolute=True) -> str:
file_name += "_fake"
return file_name
-
-
# 保存evfs文件。先保存后一个文件,再删除前一个文件。
def save_evfs_file(self):
# 文件名包含秒为单位的时间戳,理论上不会重复
@@ -698,7 +695,6 @@ def save_evfs_file(self):
self.evfs.save_evfs_file(file_name + "1")
self.old_evfs_filename = file_name
-
def gameFailed(self): # 失败后改脸和状态变量
self.timer_10ms.stop()
self.score_board_manager.editing_row = -1
@@ -876,8 +872,7 @@ def try_record_pop(self):
ui.label_16.setText(mode_text)
ui.Dialog.show()
ui.Dialog.exec_()
-
-
+
# 根据条件是否满足,尝试追加evfs文件
# 当且仅当game_state发生变化,且旧状态为"playing"时调用(即使点一下就获胜也会经过"playing")
# 加入evfs是空的,且当前游戏状态不是"win",则不追加
@@ -901,10 +896,10 @@ def try_append_evfs(self, new_game_state):
self.label.ms_board.use_question = False # 禁用问号是共识
self.label.ms_board.use_cursor_pos_lim = self.cursor_limit
self.label.ms_board.use_auto_replay = self.auto_replay > 0
-
+
self.label.ms_board.is_fair = self.is_fair()
self.label.ms_board.is_official = self.is_official()
-
+
self.label.ms_board.software = superGUI.version
self.label.ms_board.mode = self.gameMode
self.label.ms_board.player_identifier = self.player_identifier
@@ -914,7 +909,7 @@ def try_append_evfs(self, new_game_state):
country_name[self.country].upper()
self.label.ms_board.device_uuid = hashlib.md5(
bytes(str(uuid.getnode()).encode())).hexdigest().encode("UTF-8")
-
+
self.label.ms_board.generate_evf_v4_raw_data()
# 补上校验值
checksum = self.checksum_guard.get_checksum(
@@ -937,7 +932,6 @@ def try_append_evfs(self, new_game_state):
self.cal_evf_filename(absolute=False), checksum)
self.evfs.generate_evfs_v0_raw_data()
self.save_evfs_file()
-
def showMineNum(self, n):
# 显示剩余雷数,雷数大于等于0,小于等于999,整数
@@ -1005,7 +999,6 @@ def predefined_Board(self, k):
self.board_constraint = self.predefinedBoardPara[k]['board_constraint']
self.attempt_times_limit = self.predefinedBoardPara[k]['attempt_times_limit']
-
# 菜单回放的回调
def replay_game(self):
if not isinstance(self.label.ms_board, ms.BaseVideo):
@@ -1374,8 +1367,6 @@ def refreshSettingsDefault(self):
self.game_setting.set_value("DEFAULT/minenum", str(self.minenum))
self.game_setting.sync()
-
-
def is_official(self) -> bool:
# 局面开始时,判断一下局面是设置是否正式。
# 极端小的3BV依然是合法的,而网站是否认同不关软件的事。
@@ -1423,7 +1414,6 @@ def limit_cursor(self):
rect = QRect(widget_pos, widget_size)
self._clip_mouse(rect)
-
def unlimit_cursor(self):
'''
取消将鼠标区域限制在游戏界面中。
@@ -1473,3 +1463,8 @@ def closeEvent_(self):
self.game_setting.sync()
self.record_setting.sync()
+
+ def action_OpenPluginDialog(self):
+ contexts = list(PluginManager.instance().plugin_contexts)
+ dialog = PluginManagerUI(contexts)
+ dialog.exec()
diff --git a/src/mp_plugins/__init__.py b/src/mp_plugins/__init__.py
new file mode 100644
index 0000000..80e2a28
--- /dev/null
+++ b/src/mp_plugins/__init__.py
@@ -0,0 +1,13 @@
+from .plugin_process import PluginProcess
+from .plugin_manager import PluginManager
+from .base import BaseContext, BaseEvent, BasePlugin, BaseConfig
+from .base.context import PluginContext
+
+__all__ = [
+ "PluginManager",
+ "BaseContext",
+ "BaseEvent",
+ "BasePlugin",
+ "BaseConfig",
+ "PluginContext",
+]
diff --git a/src/mp_plugins/base/__init__.py b/src/mp_plugins/base/__init__.py
new file mode 100644
index 0000000..50f7b84
--- /dev/null
+++ b/src/mp_plugins/base/__init__.py
@@ -0,0 +1,33 @@
+from .context import BaseContext, PluginContext
+from .error import Error
+from .mode import PluginStatus, MessageMode, ValueEnum
+from .event import BaseEvent
+from .plugin import BasePlugin
+from .message import Message
+from ._data import get_subclass_by_name
+from .config import (
+ BaseConfig,
+ BaseSetting,
+ BoolSetting,
+ NumberSetting,
+ SelectSetting,
+ TextSetting,
+)
+
+__all__ = [
+ "BaseContext",
+ "PluginContext",
+ "Error",
+ "PluginStatus",
+ "MessageMode",
+ "BaseEvent",
+ "BasePlugin",
+ "Message",
+ "ValueEnum",
+ "BaseConfig",
+ "BaseSetting",
+ "BoolSetting",
+ "NumberSetting",
+ "SelectSetting",
+ "TextSetting",
+]
diff --git a/src/mp_plugins/base/_data.py b/src/mp_plugins/base/_data.py
new file mode 100644
index 0000000..a3386c8
--- /dev/null
+++ b/src/mp_plugins/base/_data.py
@@ -0,0 +1,36 @@
+from typing import Type
+from msgspec import Struct, json
+
+
+class _BaseData(Struct):
+ """
+ 数据基类
+ """
+
+ def copy(self):
+ new_data = json.decode(json.encode(self), type=type(self))
+ return new_data
+
+
+_subclass_cache = {}
+
+
+def get_subclass_by_name(name: str) -> Type[_BaseData] | None:
+ """
+ 根据类名获取 BaseEvent 的派生类(支持多级继承),带缓存
+ """
+ global _subclass_cache
+ if name in _subclass_cache:
+ return _subclass_cache[name]
+
+ def _iter_subclasses(cls):
+ for sub in cls.__subclasses__():
+ yield sub
+ yield from _iter_subclasses(sub)
+
+ for subcls in _iter_subclasses(_BaseData):
+ _subclass_cache[subcls.__name__] = subcls
+ if subcls.__name__ == name:
+ return subcls
+
+ return None
diff --git a/src/mp_plugins/base/config.py b/src/mp_plugins/base/config.py
new file mode 100644
index 0000000..9fbd271
--- /dev/null
+++ b/src/mp_plugins/base/config.py
@@ -0,0 +1,52 @@
+from typing import Any, Dict, List, Sequence
+from ._data import _BaseData, get_subclass_by_name
+import msgspec
+
+
+class BaseSetting(_BaseData):
+ name: str = ""
+ value: Any = None
+ setting_type: str = "BaseSetting"
+
+
+class TextSetting(BaseSetting):
+ value: str = ""
+ placeholder: str = ""
+ setting_type: str = "TextSetting"
+
+
+class NumberSetting(BaseSetting):
+ value: float = 0.0
+ min_value: float = 0.0
+ max_value: float = 100.0
+ step: float = 1.0
+ setting_type: str = "NumberSetting"
+
+
+class BoolSetting(BaseSetting):
+ value: bool = False
+ description: str = ""
+ setting_type: str = "BoolSetting"
+
+
+class SelectSetting(BaseSetting):
+ value: str = ""
+ options: List[str] = []
+ setting_type: str = "SelectSetting"
+
+
+class BaseConfig(_BaseData):
+ """ """
+
+ pass
+
+
+def Get_settings(data: Dict[str, Dict[str, Any]]) -> Dict[str, BaseSetting]:
+ settings = {}
+ for key, value in data.items():
+ if settings_type := value.get("setting_type"):
+ setting: BaseSetting = msgspec.json.decode(
+ msgspec.json.encode(value), type=get_subclass_by_name(settings_type)
+ )
+ settings[key] = setting
+ return settings
diff --git a/src/mp_plugins/base/context.py b/src/mp_plugins/base/context.py
new file mode 100644
index 0000000..178305c
--- /dev/null
+++ b/src/mp_plugins/base/context.py
@@ -0,0 +1,31 @@
+from ._data import _BaseData
+from .mode import PluginStatus
+from datetime import datetime
+from typing import List
+
+
+class BaseContext(_BaseData):
+ """
+ 上下文基类
+ """
+
+ name: str = ""
+ version: str = ""
+
+
+class PluginContext(BaseContext):
+ """
+ 插件上下文
+ """
+
+ pid: int = 0
+ name: str = ""
+ display_name: str = ""
+ description: str = ""
+ version: str = ""
+ author: str = ""
+ author_email: str = ""
+ url: str = ""
+ status: PluginStatus = PluginStatus.Stopped
+ heartbeat: float = datetime.now().timestamp()
+ subscribers: List[str] = []
diff --git a/src/mp_plugins/base/error.py b/src/mp_plugins/base/error.py
new file mode 100644
index 0000000..1e94acb
--- /dev/null
+++ b/src/mp_plugins/base/error.py
@@ -0,0 +1,10 @@
+from ._data import _BaseData
+
+
+class Error(_BaseData):
+ """
+ 错误信息
+ """
+
+ type: str
+ message: str
diff --git a/src/mp_plugins/base/event.py b/src/mp_plugins/base/event.py
new file mode 100644
index 0000000..55565df
--- /dev/null
+++ b/src/mp_plugins/base/event.py
@@ -0,0 +1,11 @@
+from ._data import _BaseData
+from datetime import datetime
+import uuid
+
+
+class BaseEvent(_BaseData):
+ """
+ 事件基类
+ """
+
+ timestamp: float = datetime.now().timestamp()
diff --git a/src/mp_plugins/base/message.py b/src/mp_plugins/base/message.py
new file mode 100644
index 0000000..43daa5c
--- /dev/null
+++ b/src/mp_plugins/base/message.py
@@ -0,0 +1,32 @@
+import uuid
+from msgspec import Struct, json
+from typing import Any, Union, Type, TypeVar, Generic, Optional
+from ._data import _BaseData, get_subclass_by_name
+from datetime import datetime
+from .mode import MessageMode
+
+BaseData = TypeVar("BaseData", bound=_BaseData)
+
+
+class Message(_BaseData):
+ """
+ 一个消息类用于包装事件及消息的基本信息
+ """
+
+ id = str(uuid.uuid4())
+ data: Any = None
+ timestamp: datetime = datetime.now()
+ mode: MessageMode = MessageMode.Unknown
+ Source: str = "main" # 来源,也就是消息的发送者
+ class_name: str = ""
+
+ def copy(self):
+ new_message = json.decode(json.encode(self), type=Message)
+ return new_message
+
+ def __post_init__(self):
+ cls = get_subclass_by_name(self.class_name)
+ if cls:
+ # 将原始 dict 解析成对应的 Struct
+ if isinstance(self.data, dict):
+ self.data = cls(**self.data)
diff --git a/src/mp_plugins/base/mode.py b/src/mp_plugins/base/mode.py
new file mode 100644
index 0000000..0e92a59
--- /dev/null
+++ b/src/mp_plugins/base/mode.py
@@ -0,0 +1,26 @@
+from enum import Enum
+
+
+class ValueEnum(Enum):
+ def __eq__(self, value: object) -> bool:
+ if isinstance(value, Enum):
+ return self.value == value.value
+ return self.value == value # 支持直接比较 value
+
+
+class PluginStatus(ValueEnum):
+ """
+ 插件状态
+ """
+
+ Running = "running"
+ Stopped = "stopped"
+ Dead = "dead"
+
+
+class MessageMode(ValueEnum):
+ Event = "event"
+ Context = "context"
+ Error = "error"
+ Unknown = "unknown"
+ Heartbeat = "heartbeat"
diff --git a/src/mp_plugins/base/plugin.py b/src/mp_plugins/base/plugin.py
new file mode 100644
index 0000000..a7f6edc
--- /dev/null
+++ b/src/mp_plugins/base/plugin.py
@@ -0,0 +1,188 @@
+from abc import ABC, abstractmethod
+import inspect
+from msgspec import json
+from datetime import datetime, timedelta
+from .error import Error
+import os
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ List,
+ Optional,
+ Type,
+ Union,
+ TypeVar,
+ Generic,
+ ParamSpec,
+)
+from .message import Message, BaseData, MessageMode
+from ._data import _BaseData, get_subclass_by_name
+from .event import BaseEvent
+from .context import BaseContext, PluginContext
+from queue import Queue
+import zmq
+from .mode import PluginStatus
+from .config import BaseConfig, BaseSetting
+
+P = ParamSpec("P") # 捕获参数
+R = TypeVar("R") # 捕获返回值
+
+
+class BasePlugin(ABC):
+ """
+ 插件基类
+ """
+
+ _context: BaseContext
+ _plugin_context: PluginContext = PluginContext()
+ _config: BaseConfig
+
+ @abstractmethod
+ def build_plugin_context(self) -> None: ...
+
+ @staticmethod
+ def event_handler(
+ event: Type[BaseEvent],
+ ) -> Callable[[Callable[P, BaseEvent]], Callable[P, BaseEvent]]:
+ """
+ 装饰器:标记方法为事件 handler
+ """
+
+ def decorator(func: Callable[P, BaseEvent]) -> Callable[P, BaseEvent]:
+ func.__event_handler__ = event
+ return func
+
+ return decorator
+
+ def __init_subclass__(cls) -> None:
+ super().__init_subclass__()
+ cls._event_handlers: Dict[
+ Type[BaseEvent], List[Callable[[BasePlugin, BaseEvent], BaseEvent]]
+ ] = {}
+ event_set: set[str] = set()
+ for _, value in cls.__dict__.items():
+ if hasattr(value, "__event_handler__"):
+ if value.__event_handler__ not in cls._event_handlers:
+ cls._event_handlers[value.__event_handler__] = []
+ event_set.add(value.__event_handler__.__name__)
+ cls._event_handlers[value.__event_handler__].append(value)
+ cls._plugin_context.subscribers = list(event_set)
+ cls._plugin_context.pid = os.getpid()
+
+ def __init__(self) -> None:
+ super().__init__()
+ context = zmq.Context()
+ self.dealer = context.socket(zmq.DEALER)
+ self.__message_queue: Queue[Message] = Queue()
+ self.__heartbeat_time = datetime.now()
+ self.build_plugin_context()
+
+ @abstractmethod
+ def initialize(self) -> None:
+ self._plugin_context.status = PluginStatus.Running
+ self.refresh_context()
+
+ @abstractmethod
+ def shutdown(self) -> None:
+ self._plugin_context.status = PluginStatus.Stopped
+ self.refresh_context()
+
+ def run(self, host: str, port: int) -> None:
+ self.initialize()
+ self.dealer.setsockopt_string(zmq.IDENTITY, str(self._plugin_context.pid))
+ self.dealer.connect(f"tcp://{host}:{port}")
+ self.refresh_context()
+ while datetime.now() - self.__heartbeat_time < timedelta(seconds=30):
+ while not self.__message_queue.empty():
+ message = self.__message_queue.get()
+ self.dealer.send_multipart([b"", json.encode(message)])
+ try:
+ data = self.dealer.recv(flags=zmq.NOBLOCK)
+ except zmq.Again:
+ continue
+ if data:
+ message = json.decode(data, type=Message)
+ self.__message_dispatching(message)
+ self.shutdown()
+ self.dealer.close()
+
+ def __message_dispatching(self, message: Message) -> None:
+ """
+ 消息分发
+ """
+ new_message = message.copy()
+ new_message.Source = self.__class__.__name__
+ self.__heartbeat_time = datetime.now()
+ if message.mode == MessageMode.Event:
+ if isinstance(message.data, BaseEvent) and message.data is not None:
+ if message.data.__class__ not in self._event_handlers:
+ self.send_error(
+ type="Event Subscribe",
+ error=f"{self.__class__.__name__} not Subscribe {message.data.__class__.__name__}",
+ )
+ return
+ for handler in self._event_handlers[message.data.__class__]:
+ event = handler(self, message.data)
+ new_message.data = event
+ self.__message_queue.put(new_message)
+ else:
+ self.send_error(
+ type="Event Validation",
+ error=f"{message.data} is not a valid event",
+ )
+ elif message.mode == MessageMode.Context:
+ if isinstance(message.data, BaseContext) and message.data is not None:
+ self._context = message.data
+ elif message.mode == MessageMode.Error:
+ pass
+ elif message.mode == MessageMode.Unknown:
+ pass
+ elif message.mode == MessageMode.Heartbeat:
+ self.__message_queue.put(new_message)
+
+ def refresh_context(self):
+ self._plugin_context.heartbeat = datetime.now().timestamp()
+ self.__message_queue.put(
+ Message(
+ data=self._plugin_context,
+ mode=MessageMode.Context,
+ Source=self.__class__.__name__,
+ class_name=self._plugin_context.__class__.__name__,
+ )
+ )
+
+ @property
+ def context(self):
+ return self._context
+
+ def send_error(self, type: str, error: str):
+ self.__message_queue.put(
+ Message(
+ data=Error(type=type, message=error),
+ mode=MessageMode.Error,
+ Source=self.__class__.__name__,
+ )
+ )
+
+ def init_config(self, config: BaseConfig):
+ self._config = config
+ old_config = self.config
+ dir = os.path.dirname(inspect.getfile(self.__class__))
+ config_path = os.path.join(dir, f"{self.__class__.__name__}.json")
+ if old_config is None:
+ with open(config_path, "w", encoding="utf-8") as f:
+ b = json.encode(config)
+ f.write(b.decode("utf-8"))
+
+ @property
+ def config(self):
+ dir = os.path.dirname(inspect.getfile(self.__class__))
+ config_path = os.path.join(dir, f"{self.__class__.__name__}.json")
+ if os.path.exists(config_path):
+ try:
+ with open(config_path, "r", encoding="utf-8") as f:
+ config = json.decode(f.read(), type=self._config.__class__)
+ return config
+ except:
+ return None
diff --git a/src/mp_plugins/context.py b/src/mp_plugins/context.py
new file mode 100644
index 0000000..88ffd8b
--- /dev/null
+++ b/src/mp_plugins/context.py
@@ -0,0 +1,5 @@
+from .base import BaseContext
+
+
+class AppContext(BaseContext):
+ display_name: str = "元扫雷"
diff --git a/src/mp_plugins/events.py b/src/mp_plugins/events.py
new file mode 100644
index 0000000..0a23bcf
--- /dev/null
+++ b/src/mp_plugins/events.py
@@ -0,0 +1,5 @@
+from .base import BaseEvent
+
+
+class GameEndEvent(BaseEvent):
+ pass
diff --git a/src/mp_plugins/plugin_manager.py b/src/mp_plugins/plugin_manager.py
new file mode 100644
index 0000000..f4595eb
--- /dev/null
+++ b/src/mp_plugins/plugin_manager.py
@@ -0,0 +1,255 @@
+from datetime import datetime, timedelta
+import sys
+import threading
+from typing import Callable, Dict, List, Optional, TypeVar, Generic, Sequence
+from threading import RLock
+from .base import (
+ PluginContext,
+ BaseContext,
+ BaseEvent,
+ MessageMode,
+ Message,
+ Error,
+ PluginStatus,
+)
+import zmq
+from .base._data import _BaseData
+from queue import Queue
+import time
+import msgspec
+from .plugin_process import PluginProcess
+from .base.config import BaseConfig, BaseSetting, Get_settings
+import pathlib
+import json
+
+_Event = TypeVar("_Event", bound=BaseEvent)
+
+
+class PluginManager(object):
+
+ __instance: Optional["PluginManager"] = None
+ __lock = RLock()
+ __plugins_context: Dict[str, PluginContext] = {}
+ __plugins_process: Dict[str, PluginProcess] = {}
+ __plugins_config_path: Dict[str, pathlib.Path] = {}
+ __context: BaseContext
+ __event_dict: Dict[str, List[BaseEvent]] = {}
+ __message_queue: Queue[Message] = Queue()
+ __running: bool = False
+
+ def __init__(self) -> None:
+ context = zmq.Context()
+
+ self.__router = context.socket(zmq.ROUTER)
+ self.__port = self.__router.bind_to_random_port("tcp://127.0.0.1")
+ self.__error_callback = None
+
+ @property
+ def port(self):
+ return self.__port
+
+ @classmethod
+ def instance(cls) -> "PluginManager":
+ if cls.__instance is None:
+ with cls.__lock:
+ if cls.__instance is None:
+ cls.__instance = cls()
+ return cls.__instance
+
+ def start(self, plugin_dir: pathlib.Path, env: Dict[str, str]):
+ with self.__lock:
+ if self.__running:
+ return
+ self.__running = True
+ plugin_dir.mkdir(parents=True, exist_ok=True)
+ for plugin_path in plugin_dir.iterdir():
+ if plugin_path.is_dir():
+ if plugin_path.name.startswith("__"):
+ continue
+ self.__plugins_config_path[plugin_path.name] = plugin_path / (
+ plugin_path.name + ".json"
+ )
+ # 先判断是否有exe
+ if (plugin_path / (plugin_path.name + ".exe")).exists():
+ plugin_path = plugin_path / (plugin_path.name + ".exe")
+ else:
+ plugin_path = plugin_path / (plugin_path.name + ".py")
+ process = PluginProcess(plugin_path)
+ process.start("127.0.0.1", self.__port, env=env)
+ self.__plugins_process[str(process.pid)] = process
+
+ t = threading.Thread(target=self.__message_dispatching, daemon=True)
+ self.__request_thread = t
+ t2 = threading.Thread(target=self.__async_send_message, daemon=True)
+ self.__response_thread = t2
+ t3 = threading.Thread(target=self.__send_heartbeat, daemon=True)
+ self.__heartbeat_thread = t3
+ t3.start()
+ t.start()
+ t2.start()
+ print("Plugin manager started")
+
+ def stop(self):
+ with self.__lock:
+ if not self.__running:
+ return
+ self.__running = False
+ for process in self.__plugins_process.values():
+ process.stop()
+ self.__request_thread.join(10)
+ self.__response_thread.join(10)
+ self.__heartbeat_thread.join(10)
+ self.__router.close()
+ print("Plugin manager stopped")
+
+ @property
+ def plugin_contexts(self):
+ for context in self.__plugins_context.values():
+ yield context.copy()
+
+ @property
+ def context(self) -> BaseContext:
+ return self.__context
+
+ @context.setter
+ def context(self, context: BaseContext):
+ self.__context = context
+
+ def send_event(
+ self, event: _Event, timeout: int = 10, response_count: int = 1
+ ) -> Sequence[_Event]:
+ message = Message(
+ data=event, mode=MessageMode.Event, class_name=event.__class__.__name__
+ )
+ self.__event_dict[message.id] = []
+ self.__send_message(message)
+ current_time = datetime.now()
+ while datetime.now() - current_time < timedelta(seconds=timeout) and (
+ len(self.__event_dict.get(message.id, [])) < response_count
+ ):
+ time.sleep(0.1)
+ result = []
+ if message.id in self.__event_dict:
+ for response in self.__event_dict[message.id]:
+ result.append(response)
+ with self.__lock:
+ del self.__event_dict[message.id]
+ return result
+
+ def __send_heartbeat(self):
+ while self.__running:
+ time.sleep(5)
+ for context in self.__plugins_context.values():
+ if datetime.now().timestamp() - context.heartbeat > 3000:
+ context.status = PluginStatus.Dead
+ if context.status == PluginStatus.Running:
+ self.__send_message(Message(mode=MessageMode.Heartbeat))
+
+ def __send_message(self, message: Message):
+ self.__message_queue.put(message)
+
+ def bind_error(self, func: Callable[[Error], None]):
+ self.__error_callback = func
+
+ def __message_dispatching(self) -> None:
+ while self.__running:
+ time.sleep(0.1)
+ try:
+ identity, _, data = self.__router.recv_multipart(flags=zmq.NOBLOCK)
+ except zmq.Again:
+ continue
+ if not data:
+ continue
+ if identity.decode() in self.__plugins_context:
+ self.__plugins_context[identity.decode()].heartbeat = (
+ datetime.now().timestamp()
+ )
+ message = msgspec.json.decode(data, type=Message)
+ if message.mode == MessageMode.Event:
+ with self.__lock:
+ if message.id in self.__event_dict and isinstance(
+ message.data, BaseEvent
+ ):
+ self.__event_dict[message.id].append(message.data)
+ elif message.mode == MessageMode.Error:
+ if self.__error_callback is not None and isinstance(
+ message.data, Error
+ ):
+ self.__error_callback(message.data)
+ elif message.mode == MessageMode.Heartbeat:
+ pass
+ elif message.mode == MessageMode.Context:
+ if isinstance(message.data, PluginContext):
+ with self.__lock:
+ self.__plugins_context[identity.decode()] = message.data
+ self.__send_message(
+ Message(
+ data=self.context,
+ mode=MessageMode.Context,
+ class_name=self.context.__class__.__name__,
+ )
+ )
+
+ def __async_send_message(self):
+ while self.__running:
+ time.sleep(0.1)
+ if self.__message_queue.empty():
+ continue
+ message = self.__message_queue.get()
+ if message.mode == MessageMode.Event:
+ for context in self.__plugins_context.values():
+ event_name = message.data.__class__.__name__
+ if (
+ event_name in context.subscribers
+ and context.status == PluginStatus.Running
+ ):
+ self.__router.send_multipart(
+ [
+ str(context.pid).encode(),
+ b"",
+ msgspec.json.encode(message),
+ ]
+ )
+ elif message.mode == MessageMode.Heartbeat:
+ for context in self.__plugins_context.values():
+ if context.status == PluginStatus.Running:
+ self.__router.send_multipart(
+ [
+ str(context.pid).encode(),
+ b"",
+ msgspec.json.encode(message),
+ ]
+ )
+ elif message.mode == MessageMode.Context:
+ for context in self.__plugins_context.values():
+ if context.status == PluginStatus.Running:
+ self.__router.send_multipart(
+ [
+ str(context.pid).encode(),
+ b"",
+ msgspec.json.encode(message),
+ ]
+ )
+
+ def Get_Settings(self, plugin_name: str):
+ if plugin_name not in self.__plugins_config_path:
+ return {}
+ if not self.__plugins_config_path[plugin_name].exists():
+ return {}
+
+ data = json.loads(self.__plugins_config_path[plugin_name].read_text("utf-8"))
+
+ return Get_settings(data)
+
+ def Set_Settings(self, plugin_name: str, name: str, setting: BaseSetting):
+ data = self.Get_Settings(plugin_name)
+
+ new_data = {}
+ data[name] = setting
+ for key, value in data.items():
+ if isinstance(value, BaseSetting):
+ new_data[key] = msgspec.structs.asdict(value)
+
+ self.__plugins_config_path[plugin_name].write_text(
+ json.dumps(new_data, indent=4)
+ )
diff --git a/src/mp_plugins/plugin_process.py b/src/mp_plugins/plugin_process.py
new file mode 100644
index 0000000..287798f
--- /dev/null
+++ b/src/mp_plugins/plugin_process.py
@@ -0,0 +1,58 @@
+import sys
+import subprocess
+import os
+from pathlib import Path
+from typing import List, Optional, Dict, Any
+
+
+class PluginProcess(object):
+
+ def __init__(self, plugin_path: Path) -> None:
+ self.__plugin_path = plugin_path
+ self._process: Optional[subprocess.Popen] = None
+ self.__pid: int
+ self.plugin_dir_name = plugin_path.parent.parent.name
+ self.plugin_name = plugin_path.name.split(".")[0]
+
+ @property
+ def pid(self):
+ return self.__pid
+
+ @property
+ def plugin_path(self):
+ return self.__plugin_path
+
+ def start(self, host: str, port: int, env: Dict[str, str]):
+ if self._process is not None:
+ return
+ module = f"{self.plugin_dir_name}.{self.plugin_name}.{self.plugin_name}"
+ if self.__plugin_path.suffix == ".py":
+ self._process = subprocess.Popen(
+ [
+ sys.executable,
+ "-m",
+ module,
+ host,
+ str(port),
+ ],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=env,
+ )
+ else:
+ self._process = subprocess.Popen(
+ [
+ self.__plugin_path,
+ host,
+ str(port),
+ ],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ self.__pid = self._process.pid
+
+ def stop(self):
+ if self._process is None:
+ return
+ if self._process.poll() is None:
+ self._process.terminate()
diff --git a/src/pluginDialog.py b/src/pluginDialog.py
new file mode 100644
index 0000000..e2250aa
--- /dev/null
+++ b/src/pluginDialog.py
@@ -0,0 +1,204 @@
+from PyQt5.QtWidgets import (
+ QApplication,
+ QDialog,
+ QListWidget,
+ QFormLayout,
+ QLabel,
+ QPushButton,
+ QVBoxLayout,
+ QHBoxLayout,
+ QGroupBox,
+ QMessageBox,
+ QScrollArea,
+ QWidget,
+ QLineEdit,
+ QDoubleSpinBox,
+ QCheckBox,
+ QComboBox,
+)
+from PyQt5.QtCore import Qt
+import sys
+from typing import Dict
+from mp_plugins import PluginContext
+
+# 你的 Setting 类
+from mp_plugins.base import (
+ BaseSetting,
+ TextSetting,
+ NumberSetting,
+ BoolSetting,
+ SelectSetting,
+)
+from mp_plugins import PluginManager
+
+
+class PluginManagerUI(QDialog):
+
+ def __init__(self, plugin_contexts: list[PluginContext]):
+ """
+ plugin_contexts: 你传入的 plugin 列表
+ get_settings_func(plugin_ctx) -> Dict[str, BaseSetting]
+ 你自己的函数,用来根据 PluginContext 拿到 settings
+ """
+ super().__init__()
+
+ self.plugin_contexts = plugin_contexts
+ self.current_settings_widgets: Dict[str, QWidget] = {}
+
+ self.setWindowTitle("插件管理")
+ self.resize(1000, 650)
+
+ root_layout = QHBoxLayout(self)
+
+ # ================= 左侧插件列表 =================
+ self.list_widget = QListWidget()
+ for ctx in self.plugin_contexts:
+ self.list_widget.addItem(ctx.display_name or ctx.name)
+
+ self.list_widget.currentRowChanged.connect(self.on_plugin_selected)
+ root_layout.addWidget(self.list_widget, 1)
+
+ # ================= 右侧 =================
+ right_layout = QVBoxLayout()
+
+ # -------- 插件详情 --------
+ details_group = QGroupBox("插件详情")
+ self.details_layout = QFormLayout()
+
+ self.detail_labels = {
+ "pid": QLabel(),
+ "name": QLabel(),
+ "display_name": QLabel(),
+ "description": QLabel(),
+ "version": QLabel(),
+ "author": QLabel(),
+ "author_email": QLabel(),
+ "url": QLabel(),
+ "status": QLabel(),
+ "heartbeat": QLabel(),
+ "subscribers": QLabel(),
+ }
+
+ for key, widget in self.detail_labels.items():
+ self.details_layout.addRow(key.replace("_", " ").title() + ":", widget)
+
+ details_group.setLayout(self.details_layout)
+ right_layout.addWidget(details_group, 1)
+
+ # -------- 设置面板(滚动) --------
+ settings_group = QGroupBox("设置")
+ vbox = QVBoxLayout()
+
+ self.scroll_area = QScrollArea()
+ self.scroll_area.setWidgetResizable(True)
+
+ self.scroll_content = QWidget()
+ self.scroll_layout = QFormLayout(self.scroll_content)
+ self.scroll_area.setWidget(self.scroll_content)
+
+ vbox.addWidget(self.scroll_area, 1)
+
+ # 保存按钮
+ self.btn_save = QPushButton("保存")
+ self.btn_save.clicked.connect(self.on_save_clicked)
+ vbox.addWidget(self.btn_save)
+
+ settings_group.setLayout(vbox)
+ right_layout.addWidget(settings_group, 2)
+
+ root_layout.addLayout(right_layout, 2)
+
+ if plugin_contexts:
+ self.list_widget.setCurrentRow(0)
+
+ # ===================================================================
+ # 左侧切换插件
+ # ===================================================================
+ def on_plugin_selected(self, index: int):
+ if index < 0:
+ return
+
+ ctx = self.plugin_contexts[index]
+
+ # --- PluginContext 原样填充 ---
+ for key, label in self.detail_labels.items():
+ value = getattr(ctx, key)
+ if isinstance(value, list):
+ value = ", ".join(value)
+ label.setText(str(value))
+
+ # --- 你自己的获取 settings 的函数 ---
+ settings_dict = PluginManager.instance().Get_Settings(ctx.name)
+
+ # --- 动态加载设置控件 ---
+ self.load_settings(settings_dict)
+
+ # ===================================================================
+ # 加载 settings
+ # ===================================================================
+ def load_settings(self, settings: Dict[str, BaseSetting]):
+ # 清空原控件
+ while self.scroll_layout.count():
+ item = self.scroll_layout.takeAt(0)
+ if item.widget():
+ item.widget().deleteLater()
+
+ self.current_settings_widgets.clear()
+
+ for key, setting in settings.items():
+ widget = None
+
+ if isinstance(setting, TextSetting):
+ widget = QLineEdit()
+ widget.setText(setting.value)
+
+ elif isinstance(setting, NumberSetting):
+ widget = QDoubleSpinBox()
+ widget.setRange(setting.min_value, setting.max_value)
+ widget.setSingleStep(setting.step)
+ widget.setValue(setting.value)
+
+ elif isinstance(setting, BoolSetting):
+ widget = QCheckBox(setting.description)
+ widget.setChecked(setting.value)
+
+ elif isinstance(setting, SelectSetting):
+ widget = QComboBox()
+ widget.addItems(setting.options)
+ widget.setCurrentText(setting.value)
+
+ else:
+ continue
+
+ self.current_settings_widgets[key] = widget
+ self.scroll_layout.addRow(setting.name + ":", widget)
+
+ # ===================================================================
+ # 保存按钮
+ # ===================================================================
+ def on_save_clicked(self):
+ ctx = self.plugin_contexts[self.list_widget.currentRow()]
+
+ # --- PluginContext 原样填充 ---
+ for key, label in self.detail_labels.items():
+ value = getattr(ctx, key)
+ if isinstance(value, list):
+ value = ", ".join(value)
+ label.setText(str(value))
+
+ # --- 你自己的获取 settings 的函数 ---
+ settings_dict = PluginManager.instance().Get_Settings(ctx.name)
+ for key, widget in self.current_settings_widgets.items():
+ setting = settings_dict[key]
+ if isinstance(widget, QLineEdit) and isinstance(setting, TextSetting):
+ setting.value = widget.text()
+ elif isinstance(widget, QDoubleSpinBox) and isinstance(
+ setting, NumberSetting
+ ):
+ setting.value = widget.value()
+ elif isinstance(widget, QCheckBox) and isinstance(setting, BoolSetting):
+ setting.value = widget.isChecked()
+ elif isinstance(widget, QComboBox) and isinstance(setting, SelectSetting):
+ setting.value = widget.currentText()
+ PluginManager.instance().Set_Settings(ctx.name, key, setting)
+ QMessageBox.information(self, "保存成功", "设置已保存")
diff --git a/src/plugins/UpLoadVideo/UpLoadVideo.py b/src/plugins/UpLoadVideo/UpLoadVideo.py
new file mode 100644
index 0000000..9808c89
--- /dev/null
+++ b/src/plugins/UpLoadVideo/UpLoadVideo.py
@@ -0,0 +1,70 @@
+import sys
+import os
+import msgspec
+import zmq
+
+if getattr(sys, "frozen", False): # 检查是否为pyInstaller生成的EXE
+ application_path = os.path.dirname(sys.executable)
+ sys.path.append(application_path + "/../../")
+ print(application_path + "/../../")
+else:
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/../../")
+from mp_plugins import BasePlugin, BaseConfig
+from mp_plugins.base.config import *
+from mp_plugins.context import AppContext
+from mp_plugins.events import GameEndEvent
+
+
+class UpLoadVideoConfig(BaseConfig):
+ user: TextSetting
+ passwd: TextSetting
+ upload_circle: NumberSetting
+ auto_upload: BoolSetting
+ upload_type: SelectSetting
+
+
+class UpLoadVideo(BasePlugin):
+ def __init__(
+ self,
+ ) -> None:
+ super().__init__()
+ self._context: AppContext
+ self._config = UpLoadVideoConfig(
+ TextSetting("用户名", "user"),
+ TextSetting("密码", "passwd"),
+ NumberSetting(name="上传周期", value=0, min_value=1, max_value=10, step=1),
+ BoolSetting("自动上传", True),
+ SelectSetting("上传类型", "自动上传", options=["自动上传", "手动上传"]),
+ )
+ self.init_config(self._config)
+
+ def build_plugin_context(self) -> None:
+ self._plugin_context.name = "UpLoadVideo"
+ self._plugin_context.display_name = "上传录像"
+ self._plugin_context.version = "1.0.0"
+ self._plugin_context.description = "上传录像"
+ self._plugin_context.author = "LjzLoser"
+
+ def initialize(self) -> None:
+ return super().initialize()
+
+ def shutdown(self) -> None:
+ return super().shutdown()
+
+ @BasePlugin.event_handler(GameEndEvent)
+ def on_game_end(self, event: GameEndEvent) -> GameEndEvent:
+ return event
+
+
+if __name__ == "__main__":
+ try:
+ import sys
+
+ args = sys.argv[1:]
+ host = args[0]
+ port = int(args[1])
+ plugin = UpLoadVideo()
+ plugin.run(host, port)
+ except Exception as e:
+ with open("UpLoadVideo_error.log", "w", encoding="utf-8") as f:
+ f.write(str(e))
diff --git a/src/ui/ui_main_board.py b/src/ui/ui_main_board.py
index d402449..bdc96aa 100644
--- a/src/ui/ui_main_board.py
+++ b/src/ui/ui_main_board.py
@@ -229,7 +229,7 @@ def setupUi(self, MainWindow):
self.verticalLayout_2.addLayout(self.horizontalLayout_6)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
- self.menubar.setGeometry(QtCore.QRect(0, 0, 968, 33))
+ self.menubar.setGeometry(QtCore.QRect(0, 0, 968, 27))
font = QtGui.QFont()
font.setFamily("微软雅黑")
font.setPointSize(12)
diff --git a/uiFiles/main_board.ui b/uiFiles/main_board.ui
index ca9327f..751e3cb 100644
--- a/uiFiles/main_board.ui
+++ b/uiFiles/main_board.ui
@@ -609,7 +609,7 @@
0
0
968
- 33
+ 27
@@ -678,6 +678,20 @@
+
+
+