♻️重构, 实现基础框架-日志 AUTO_MAA配置 用户配置基类

This commit is contained in:
MoeSnowyFox
2025-08-03 03:43:57 +08:00
parent 908da0bc47
commit f1859a5877
146 changed files with 1220 additions and 30467 deletions

0
app/core/MAA.py Normal file
View File

View File

@@ -1,63 +0,0 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
# This file is part of AUTO_MAA.
# AUTO_MAA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
# AUTO_MAA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
# the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA核心组件包
v4.4
作者DLmaster_361
"""
__version__ = "4.2.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .config import (
QueueConfig,
MaaConfig,
MaaUserConfig,
MaaPlanConfig,
GeneralConfig,
GeneralSubConfig,
Config,
)
from .logger import logger
from .main_info_bar import MainInfoBar
from .network import Network
from .sound_player import SoundPlayer
from .task_manager import Task, TaskManager
from .timer import MainTimer
__all__ = [
"Config",
"QueueConfig",
"MaaConfig",
"MaaUserConfig",
"MaaPlanConfig",
"GeneralConfig",
"GeneralSubConfig",
"logger",
"MainInfoBar",
"Network",
"SoundPlayer",
"Task",
"TaskManager",
"MainTimer",
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +0,0 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
# This file is part of AUTO_MAA.
# AUTO_MAA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
# AUTO_MAA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
# the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA日志组件
v4.4
作者DLmaster_361
"""
from loguru import logger as _logger
# 设置日志 module 字段默认值
logger = _logger.patch(
lambda record: record["extra"].setdefault("module", "未知模块") or True
)
logger.remove(0)

View File

@@ -1,109 +0,0 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
# This file is part of AUTO_MAA.
# AUTO_MAA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
# AUTO_MAA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
# the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA信息通知栏
v4.4
作者DLmaster_361
"""
from PySide6.QtCore import Qt
from qfluentwidgets import InfoBar, InfoBarPosition
from .logger import logger
from .config import Config
from .sound_player import SoundPlayer
class _MainInfoBar:
"""信息通知栏"""
# 模式到 InfoBar 方法的映射
mode_mapping = {
"success": InfoBar.success,
"warning": InfoBar.warning,
"error": InfoBar.error,
"info": InfoBar.info,
}
def push_info_bar(
self, mode: str, title: str, content: str, time: int, if_force: bool = False
) -> None:
"""
推送消息到吐司通知栏
:param mode: 通知栏模式,支持 "success", "warning", "error", "info"
:param title: 通知栏标题
:type title: str
:param content: 通知栏内容
:type content: str
:param time: 显示时长,单位为毫秒
:type time: int
:param if_force: 是否强制推送
:type if_force: bool
"""
if Config.main_window is None:
logger.error("信息通知栏未设置父窗口", module="吐司通知栏")
return None
# 根据 mode 获取对应的 InfoBar 方法
info_bar_method = self.mode_mapping.get(mode)
if not info_bar_method:
logger.error(f"未知的通知栏模式: {mode}", module="吐司通知栏")
return None
if Config.main_window.isVisible():
# 主窗口可见时直接推送通知
info_bar_method(
title=title,
content=content,
orient=Qt.Horizontal,
isClosable=True,
position=InfoBarPosition.TOP_RIGHT,
duration=time,
parent=Config.main_window,
)
elif if_force:
# 如果主窗口不可见且强制推送,则录入消息队列等待窗口显示后推送
info_bar_item = {
"mode": mode,
"title": title,
"content": content,
"time": time,
}
if info_bar_item not in Config.info_bar_list:
Config.info_bar_list.append(info_bar_item)
logger.info(
f"主窗口不可见,已将通知栏消息录入队列: {info_bar_item}",
module="吐司通知栏",
)
if mode == "warning":
SoundPlayer.play("发生异常")
if mode == "error":
SoundPlayer.play("发生错误")
MainInfoBar = _MainInfoBar()

View File

@@ -1,308 +0,0 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
# This file is part of AUTO_MAA.
# AUTO_MAA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
# AUTO_MAA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
# the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA网络请求线程
v4.4
作者DLmaster_361
"""
from PySide6.QtCore import QObject, QThread, QEventLoop
import re
import time
import requests
import truststore
from pathlib import Path
from typing import Dict
from .logger import logger
class NetworkThread(QThread):
"""网络请求线程类"""
max_retries = 3
timeout = 10
backoff_factor = 0.1
def __init__(
self,
mode: str,
url: str,
path: Path = None,
files: Dict = None,
data: Dict = None,
) -> None:
super().__init__()
self.setObjectName(
f"NetworkThread-{mode}-{re.sub(r'(&cdk=)[^&]+(&)', r'\1******\2', url)}"
)
logger.info(f"创建网络请求线程: {self.objectName()}", module="网络请求子线程")
self.mode = mode
self.url = url
self.path = path
self.files = files
self.data = data
from .config import Config
self.proxies = {
"http": Config.get(Config.update_ProxyAddress),
"https": Config.get(Config.update_ProxyAddress),
}
self.status_code = None
self.response_json = None
self.error_message = None
self.loop = QEventLoop()
truststore.inject_into_ssl() # 信任系统证书
@logger.catch
def run(self) -> None:
"""运行网络请求线程"""
if self.mode == "get":
self.get_json(self.url)
elif self.mode == "get_file":
self.get_file(self.url, self.path)
elif self.mode == "upload_file":
self.upload_file(self.url, self.files, self.data)
def get_json(self, url: str) -> None:
"""
通过get方法获取json数据
:param url: 请求的URL
"""
logger.info(f"子线程 {self.objectName()} 开始网络请求", module="网络请求子线程")
response = None
for _ in range(self.max_retries):
try:
response = requests.get(url, timeout=self.timeout, proxies=self.proxies)
self.status_code = response.status_code
self.response_json = response.json()
self.error_message = None
break
except Exception as e:
self.status_code = response.status_code if response else None
self.response_json = None
self.error_message = str(e)
logger.exception(
f"子线程 {self.objectName()} 网络请求失败:{e},第{_+1}次尝试",
module="网络请求子线程",
)
time.sleep(self.backoff_factor)
self.loop.quit()
def get_file(self, url: str, path: Path) -> None:
"""
通过get方法下载文件到指定路径
:param url: 请求的URL
:param path: 下载文件的保存路径
"""
logger.info(f"子线程 {self.objectName()} 开始下载文件", module="网络请求子线程")
response = None
try:
response = requests.get(url, timeout=self.timeout, proxies=self.proxies)
if response.status_code == 200:
with open(path, "wb") as file:
file.write(response.content)
self.status_code = response.status_code
self.error_message = None
else:
self.status_code = response.status_code
self.error_message = f"下载失败,状态码: {response.status_code}"
except Exception as e:
self.status_code = response.status_code if response else None
self.error_message = str(e)
logger.exception(
f"子线程 {self.objectName()} 网络请求失败:{e}", module="网络请求子线程"
)
self.loop.quit()
def upload_file(self, url: str, files: Dict, data: Dict = None) -> None:
"""
通过POST方法上传文件
:param url: 请求的URL
:param files: 文件字典,格式为 {'file': ('filename', file_obj, 'content_type')}
:param data: 表单数据字典
"""
logger.info(f"子线程 {self.objectName()} 开始上传文件", module="网络请求子线程")
response = None
for _ in range(self.max_retries):
try:
response = requests.post(
url,
files=files,
data=data,
timeout=self.timeout,
proxies=self.proxies,
)
self.status_code = response.status_code
# 尝试解析JSON响应
try:
self.response_json = response.json()
except ValueError:
# 如果不是JSON格式保存文本内容
self.response_json = {"text": response.text}
self.error_message = None
break
except Exception as e:
self.status_code = response.status_code if response else None
self.response_json = None
self.error_message = str(e)
logger.exception(
f"子线程 {self.objectName()} 文件上传失败:{e},第{_+1}次尝试",
module="网络请求子线程",
)
time.sleep(self.backoff_factor)
self.loop.quit()
class _Network(QObject):
"""网络请求线程管理类"""
def __init__(self) -> None:
super().__init__()
self.task_queue = []
def add_task(
self,
mode: str,
url: str,
path: Path = None,
files: Dict = None,
data: Dict = None,
) -> NetworkThread:
"""
添加网络请求任务
:param mode: 请求模式,支持 "get", "get_file", "upload_file"
:param url: 请求的URL
:param path: 下载文件的保存路径,仅在 mode 为 "get_file" 时有效
:param files: 上传文件字典,仅在 mode 为 "upload_file" 时有效
:param data: 表单数据字典,仅在 mode 为 "upload_file" 时有效
:return: 返回创建的 NetworkThread 实例
"""
logger.info(f"添加网络请求任务: {mode} {url} {path}", module="网络请求")
network_thread = NetworkThread(mode, url, path, files, data)
self.task_queue.append(network_thread)
network_thread.start()
return network_thread
def upload_config_file(
self, file_path: Path, username: str = "", description: str = ""
) -> NetworkThread:
"""
上传配置文件到分享服务器
:param file_path: 要上传的文件路径
:param username: 用户名(可选)
:param description: 文件描述(必填)
:return: 返回创建的 NetworkThread 实例
"""
if not file_path.exists():
raise FileNotFoundError(f"文件不存在: {file_path}")
if not description:
raise ValueError("文件描述不能为空")
# 准备上传的文件
with open(file_path, "rb") as f:
files = {"file": (file_path.name, f.read(), "application/json")}
# 准备表单数据
data = {"description": description}
if username:
data["username"] = username
url = "http://221.236.27.82:10023/api/upload/share"
logger.info(
f"准备上传配置文件: {file_path.name},用户: {username or '匿名'},描述: {description}",
extra={"module": "网络请求"},
)
return self.add_task("upload_file", url, files=files, data=data)
def get_result(self, network_thread: NetworkThread) -> dict:
"""
获取网络请求结果
:param network_thread: 网络请求线程实例
:return: 包含状态码、响应JSON和错误信息的字典
"""
result = {
"status_code": network_thread.status_code,
"response_json": network_thread.response_json,
"error_message": (
re.sub(r"(&cdk=)[^&]+(&)", r"\1******\2", network_thread.error_message)
if network_thread.error_message
else None
),
}
network_thread.quit()
network_thread.wait()
self.task_queue.remove(network_thread)
network_thread.deleteLater()
logger.info(
f"网络请求结果: {result['status_code']},请求子线程已结束",
module="网络请求",
)
return result
Network = _Network()

View File

@@ -1,79 +0,0 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
# This file is part of AUTO_MAA.
# AUTO_MAA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
# AUTO_MAA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
# the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA音效播放器
v4.4
作者DLmaster_361
"""
from PySide6.QtCore import QObject, QUrl
from PySide6.QtMultimedia import QSoundEffect
from pathlib import Path
from .logger import logger
from .config import Config
class _SoundPlayer(QObject):
def __init__(self):
super().__init__()
self.sounds_path = Config.app_path / "resources/sounds"
def play(self, sound_name: str):
"""
播放指定名称的音效
:param sound_name: 音效文件名(不带扩展名)
"""
if not Config.get(Config.voice_Enabled):
return
if (self.sounds_path / f"both/{sound_name}.wav").exists():
self.play_voice(self.sounds_path / f"both/{sound_name}.wav")
elif (
self.sounds_path / Config.get(Config.voice_Type) / f"{sound_name}.wav"
).exists():
self.play_voice(
self.sounds_path / Config.get(Config.voice_Type) / f"{sound_name}.wav"
)
def play_voice(self, sound_path: Path):
"""
播放音效文件
:param sound_path: 音效文件的完整路径
"""
effect = QSoundEffect(self)
effect.setVolume(1)
effect.setSource(QUrl.fromLocalFile(sound_path))
effect.play()
SoundPlayer = _SoundPlayer()

View File

@@ -1,460 +0,0 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
# This file is part of AUTO_MAA.
# AUTO_MAA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
# AUTO_MAA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
# the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA业务调度器
v4.4
作者DLmaster_361
"""
from PySide6.QtCore import QThread, QObject, Signal
from qfluentwidgets import MessageBox
from datetime import datetime
from packaging import version
from typing import Dict, Union
from .logger import logger
from .config import Config
from .main_info_bar import MainInfoBar
from .network import Network
from .sound_player import SoundPlayer
from app.models import MaaManager, GeneralManager
class Task(QThread):
"""业务线程"""
check_maa_version = Signal(str)
push_info_bar = Signal(str, str, str, int)
play_sound = Signal(str)
question = Signal(str, str)
question_response = Signal(bool)
update_maa_user_info = Signal(str, dict)
update_general_sub_info = Signal(str, dict)
create_task_list = Signal(list)
create_user_list = Signal(list)
update_task_list = Signal(list)
update_user_list = Signal(list)
update_log_text = Signal(str)
accomplish = Signal(list)
def __init__(
self, mode: str, name: str, info: Dict[str, Dict[str, Union[str, int, bool]]]
):
super(Task, self).__init__()
self.setObjectName(f"Task-{mode}-{name}")
self.mode = mode
self.name = name
self.info = info
self.logs = []
self.question_response.connect(lambda: print("response"))
@logger.catch
def run(self):
if "设置MAA" in self.mode:
logger.info(f"任务开始:设置{self.name}", module=f"业务 {self.name}")
self.push_info_bar.emit("info", "设置MAA", self.name, 3000)
self.task = MaaManager(
self.mode,
Config.script_dict[self.name],
(None if "全局" in self.mode else self.info["SetMaaInfo"]["Path"]),
)
self.task.check_maa_version.connect(self.check_maa_version.emit)
self.task.push_info_bar.connect(self.push_info_bar.emit)
self.task.play_sound.connect(self.play_sound.emit)
self.task.accomplish.connect(lambda: self.accomplish.emit([]))
try:
self.task.run()
except Exception as e:
logger.exception(
f"任务异常:{self.name},错误信息:{e}", module=f"业务 {self.name}"
)
self.push_info_bar.emit("error", "任务异常", self.name, -1)
elif self.mode == "设置通用脚本":
logger.info(f"任务开始:设置{self.name}", module=f"业务 {self.name}")
self.push_info_bar.emit("info", "设置通用脚本", self.name, 3000)
self.task = GeneralManager(
self.mode,
Config.script_dict[self.name],
self.info["SetSubInfo"]["Path"],
)
self.task.push_info_bar.connect(self.push_info_bar.emit)
self.task.play_sound.connect(self.play_sound.emit)
self.task.accomplish.connect(lambda: self.accomplish.emit([]))
try:
self.task.run()
except Exception as e:
logger.exception(
f"任务异常:{self.name},错误信息:{e}", module=f"业务 {self.name}"
)
self.push_info_bar.emit("error", "任务异常", self.name, -1)
else:
logger.info(f"任务开始:{self.name}", module=f"业务 {self.name}")
self.task_list = [
[
(
value
if Config.script_dict[value]["Config"].get_name() == ""
else f"{value} - {Config.script_dict[value]["Config"].get_name()}"
),
"等待",
value,
]
for _, value in sorted(
self.info["Queue"].items(), key=lambda x: int(x[0][7:])
)
if value != "禁用"
]
self.create_task_list.emit(self.task_list)
for task in self.task_list:
if self.isInterruptionRequested():
break
task[1] = "运行"
self.update_task_list.emit(self.task_list)
# 检查任务是否在运行列表中
if task[2] in Config.running_list:
task[1] = "跳过"
self.update_task_list.emit(self.task_list)
logger.info(
f"跳过任务:{task[0]},该任务已在运行列表中",
module=f"业务 {self.name}",
)
self.push_info_bar.emit("info", "跳过任务", task[0], 3000)
continue
# 标记为运行中
Config.running_list.append(task[2])
logger.info(f"任务开始:{task[0]}", module=f"业务 {self.name}")
self.push_info_bar.emit("info", "任务开始", task[0], 3000)
if Config.script_dict[task[2]]["Type"] == "Maa":
self.task = MaaManager(
self.mode[0:4],
Config.script_dict[task[2]],
)
self.task.check_maa_version.connect(self.check_maa_version.emit)
self.task.question.connect(self.question.emit)
self.question_response.disconnect()
self.question_response.connect(self.task.question_response.emit)
self.task.push_info_bar.connect(self.push_info_bar.emit)
self.task.play_sound.connect(self.play_sound.emit)
self.task.create_user_list.connect(self.create_user_list.emit)
self.task.update_user_list.connect(self.update_user_list.emit)
self.task.update_log_text.connect(self.update_log_text.emit)
self.task.update_user_info.connect(self.update_maa_user_info.emit)
self.task.accomplish.connect(
lambda log: self.task_accomplish(task[2], log)
)
elif Config.script_dict[task[2]]["Type"] == "General":
self.task = GeneralManager(
self.mode[0:4],
Config.script_dict[task[2]],
)
self.task.question.connect(self.question.emit)
self.question_response.disconnect()
self.question_response.connect(self.task.question_response.emit)
self.task.push_info_bar.connect(self.push_info_bar.emit)
self.task.play_sound.connect(self.play_sound.emit)
self.task.create_user_list.connect(self.create_user_list.emit)
self.task.update_user_list.connect(self.update_user_list.emit)
self.task.update_log_text.connect(self.update_log_text.emit)
self.task.update_sub_info.connect(self.update_general_sub_info.emit)
self.task.accomplish.connect(
lambda log: self.task_accomplish(task[2], log)
)
try:
self.task.run() # 运行任务业务
task[1] = "完成"
self.update_task_list.emit(self.task_list)
logger.info(f"任务完成:{task[0]}", module=f"业务 {self.name}")
self.push_info_bar.emit("info", "任务完成", task[0], 3000)
except Exception as e:
self.task_accomplish(
task[2],
{
"Time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"History": f"任务异常,异常简报:{e}",
},
)
task[1] = "异常"
self.update_task_list.emit(self.task_list)
logger.exception(
f"任务异常:{task[0]},错误信息:{e}",
module=f"业务 {self.name}",
)
self.push_info_bar.emit("error", "任务异常", task[0], -1)
# 任务结束后从运行列表中移除
Config.running_list.remove(task[2])
self.accomplish.emit(self.logs)
def task_accomplish(self, name: str, log: dict):
"""
销毁任务线程并保存任务结果
:param name: 任务名称
:param log: 任务日志记录
"""
logger.info(
f"任务完成:{name},日志记录:{list(log.values())}",
module=f"业务 {self.name}",
)
self.logs.append([name, log])
self.task.deleteLater()
class _TaskManager(QObject):
"""业务调度器"""
create_gui = Signal(Task)
connect_gui = Signal(Task)
def __init__(self):
super(_TaskManager, self).__init__()
self.task_dict: Dict[str, Task] = {}
def add_task(
self, mode: str, name: str, info: Dict[str, Dict[str, Union[str, int, bool]]]
):
"""
添加任务
:param mode: 任务模式
:param name: 任务名称
:param info: 任务信息
"""
if name in Config.running_list or name in self.task_dict:
logger.warning(f"任务已存在:{name}")
MainInfoBar.push_info_bar("warning", "任务已存在", name, 5000)
return None
logger.info(f"任务开始:{name},模式:{mode}", module="业务调度")
MainInfoBar.push_info_bar("info", "任务开始", name, 3000)
SoundPlayer.play("任务开始")
# 标记任务为运行中
Config.running_list.append(name)
# 创建任务实例并连接信号
self.task_dict[name] = Task(mode, name, info)
self.task_dict[name].check_maa_version.connect(self.check_maa_version)
self.task_dict[name].question.connect(
lambda title, content: self.push_dialog(name, title, content)
)
self.task_dict[name].push_info_bar.connect(MainInfoBar.push_info_bar)
self.task_dict[name].play_sound.connect(SoundPlayer.play)
self.task_dict[name].update_maa_user_info.connect(Config.change_maa_user_info)
self.task_dict[name].update_general_sub_info.connect(
Config.change_general_sub_info
)
self.task_dict[name].accomplish.connect(
lambda logs: self.remove_task(mode, name, logs)
)
# 向UI发送信号以创建或连接GUI
if "新调度台" in mode:
self.create_gui.emit(self.task_dict[name])
elif "主调度台" in mode:
self.connect_gui.emit(self.task_dict[name])
# 启动任务线程
self.task_dict[name].start()
def stop_task(self, name: str) -> None:
"""
中止任务
:param name: 任务名称
"""
logger.info(f"中止任务:{name}", module="业务调度")
MainInfoBar.push_info_bar("info", "中止任务", name, 3000)
if name == "ALL":
for name in self.task_dict:
self.task_dict[name].task.requestInterruption()
self.task_dict[name].requestInterruption()
self.task_dict[name].quit()
self.task_dict[name].wait()
elif name in self.task_dict:
self.task_dict[name].task.requestInterruption()
self.task_dict[name].requestInterruption()
self.task_dict[name].quit()
self.task_dict[name].wait()
def remove_task(self, mode: str, name: str, logs: list) -> None:
"""
处理任务结束后的收尾工作
:param mode: 任务模式
:param name: 任务名称
:param logs: 任务日志
"""
logger.info(f"任务结束:{name}", module="业务调度")
MainInfoBar.push_info_bar("info", "任务结束", name, 3000)
SoundPlayer.play("任务结束")
# 删除任务线程,移除运行中标记
self.task_dict[name].deleteLater()
self.task_dict.pop(name)
Config.running_list.remove(name)
if "调度队列" in name and "人工排查" not in mode:
# 保存调度队列历史记录
if len(logs) > 0:
time = logs[0][1]["Time"]
history = ""
for log in logs:
history += f"任务名称:{log[0]}{log[1]["History"].replace("\n","\n ")}\n"
Config.save_history(name, {"Time": time, "History": history})
else:
Config.save_history(
name,
{
"Time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"History": "没有任务被执行",
},
)
# 根据调度队列情况设置电源状态
if (
Config.queue_dict[name]["Config"].get(
Config.queue_dict[name]["Config"].QueueSet_AfterAccomplish
)
!= "NoAction"
and Config.power_sign == "NoAction"
):
Config.set_power_sign(
Config.queue_dict[name]["Config"].get(
Config.queue_dict[name]["Config"].QueueSet_AfterAccomplish
)
)
if Config.args.mode == "cli" and Config.power_sign == "NoAction":
Config.set_power_sign("KillSelf")
def check_maa_version(self, v: str) -> None:
"""
检查MAA版本如果版本过低则推送通知
:param v: 当前MAA版本
"""
logger.info(f"检查MAA版本{v}", module="业务调度")
network = Network.add_task(
mode="get",
url="https://mirrorchyan.com/api/resources/MAA/latest?user_agent=AutoMaaGui&os=win&arch=x64&channel=stable",
)
network.loop.exec()
network_result = Network.get_result(network)
if network_result["status_code"] == 200:
maa_info = network_result["response_json"]
else:
logger.warning(
f"获取MAA版本信息时出错{network_result['error_message']}",
module="业务调度",
)
MainInfoBar.push_info_bar(
"warning",
"获取MAA版本信息时出错",
f"网络错误:{network_result['status_code']}",
5000,
)
return None
if version.parse(maa_info["data"]["version_name"]) > version.parse(v):
logger.info(
f"检测到MAA版本过低{v},最新版本:{maa_info['data']['version_name']}",
module="业务调度",
)
MainInfoBar.push_info_bar(
"info",
"MAA版本过低",
f"当前版本:{v},最新稳定版:{maa_info['data']['version_name']}",
-1,
)
logger.success(
f"MAA版本检查完成{v},最新版本:{maa_info['data']['version_name']}",
module="业务调度",
)
def push_dialog(self, name: str, title: str, content: str):
"""
推送来自任务线程的对话框
:param name: 任务名称
:param title: 对话框标题
:param content: 对话框内容
"""
choice = MessageBox(title, content, Config.main_window)
choice.yesButton.setText("")
choice.cancelButton.setText("")
self.task_dict[name].question_response.emit(bool(choice.exec()))
TaskManager = _TaskManager()

View File

@@ -1,175 +0,0 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
# This file is part of AUTO_MAA.
# AUTO_MAA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
# AUTO_MAA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
# the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA主业务定时器
v4.4
作者DLmaster_361
"""
from PySide6.QtCore import QObject, QTimer
from datetime import datetime
import keyboard
from .logger import logger
from .config import Config
from .task_manager import TaskManager
from app.services import System
class _MainTimer(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self.Timer = QTimer()
self.Timer.timeout.connect(self.timed_start)
self.Timer.timeout.connect(self.set_silence)
self.Timer.timeout.connect(self.check_power)
self.LongTimer = QTimer()
self.LongTimer.timeout.connect(self.long_timed_task)
def start(self):
"""启动定时器"""
logger.info("启动主定时器", module="主业务定时器")
self.Timer.start(1000)
self.LongTimer.start(3600000)
def stop(self):
"""停止定时器"""
logger.info("停止主定时器", module="主业务定时器")
self.Timer.stop()
self.Timer.deleteLater()
self.LongTimer.stop()
self.LongTimer.deleteLater()
def long_timed_task(self):
"""长时间定期检定任务"""
logger.info("执行长时间定期检定任务", module="主业务定时器")
Config.get_stage()
Config.main_window.setting.show_notice()
if Config.get(Config.update_IfAutoUpdate):
Config.main_window.setting.check_update()
def timed_start(self):
"""定时启动代理任务"""
for name, info in Config.queue_dict.items():
if not info["Config"].get(info["Config"].QueueSet_TimeEnabled):
continue
data = info["Config"].toDict()
time_set = [
data["Time"][f"Set_{_}"]
for _ in range(10)
if data["Time"][f"Enabled_{_}"]
]
# 按时间调起代理任务
curtime = datetime.now().strftime("%Y-%m-%d %H:%M")
if (
curtime[11:16] in time_set
and curtime
!= info["Config"].get(info["Config"].Data_LastProxyTime)[:16]
and name not in Config.running_list
):
logger.info(f"定时唤起任务:{name}", module="主业务定时器")
TaskManager.add_task("自动代理_新调度台", name, data)
def set_silence(self):
"""设置静默模式"""
if (
not Config.if_ignore_silence
and Config.get(Config.function_IfSilence)
and Config.get(Config.function_BossKey) != ""
):
windows = System.get_window_info()
emulator_windows = []
for window in windows:
for emulator_path, endtime in Config.silence_dict.items():
if (
datetime.now() < endtime
and str(emulator_path) in window
and window[0] != "新通知" # 此处排除雷电名为新通知的窗口
):
emulator_windows.append(window)
if emulator_windows:
logger.info(
f"检测到模拟器窗口:{emulator_windows}", module="主业务定时器"
)
try:
keyboard.press_and_release(
"+".join(
_.strip().lower()
for _ in Config.get(Config.function_BossKey).split("+")
)
)
logger.info(
f"模拟按键:{Config.get(Config.function_BossKey)}",
module="主业务定时器",
)
except Exception as e:
logger.exception(f"模拟按键时出错:{e}", module="主业务定时器")
def check_power(self):
"""检查电源操作"""
if Config.power_sign != "NoAction" and not Config.running_list:
logger.info(f"触发电源操作:{Config.power_sign}", module="主业务定时器")
from app.ui import ProgressRingMessageBox
mode_book = {
"KillSelf": "退出软件",
"Sleep": "睡眠",
"Hibernate": "休眠",
"Shutdown": "关机",
"ShutdownForce": "关机(强制)",
}
choice = ProgressRingMessageBox(
Config.main_window, f"{mode_book[Config.power_sign]}倒计时"
)
if choice.exec():
logger.info(
f"确认执行电源操作:{Config.power_sign}", module="主业务定时器"
)
System.set_power(Config.power_sign)
Config.set_power_sign("NoAction")
else:
logger.info(f"取消电源操作:{Config.power_sign}", module="主业务定时器")
Config.set_power_sign("NoAction")
MainTimer = _MainTimer()

358
app/core/user_config.py Normal file
View File

@@ -0,0 +1,358 @@
import json
import secrets
import string
import asyncio
import aiofiles
from pathlib import Path
from typing import Any, TypeVar, Generic, cast
from pydantic import BaseModel
from app.utils.logger import get_logger
T = TypeVar('T', bound=BaseModel)
class ConfigManager(Generic[T]):
"""
异步配置管理基类支持自动保存和Pydantic数据验证
子类需定义具体的配置模型类型
"""
def __init__(self, file_path: str, log_name: str, model_type: type[T]):
"""
初始化配置管理器
Args:
file_path: 配置文件路径
log_name: 日志名称
model_type: 配置项的Pydantic模型类型
"""
self.file_path = Path(file_path)
self.logger = get_logger(log_name)
self.model_type = model_type
self.data: dict[str, Any] = {
"instance_order": [],
"instances": {}
}
self._lock = asyncio.Lock()
self._save_task: asyncio.Task|None = None
self._pending_save = False
self._load_task = asyncio.create_task(self._load_async())
async def _load_async(self) -> None:
"""异步加载配置文件 - 带健壮的错误处理"""
async with self._lock:
try:
# 检查文件是否存在
if not self.file_path.exists():
self.logger.info(f"配置文件 {self.file_path} 不存在,创建新配置")
self.file_path.parent.mkdir(parents=True, exist_ok=True)
# 初始化空配置
self.data = {
"instance_order": [],
"instances": {}
}
return
# 检查文件是否为空
if self.file_path.stat().st_size == 0:
self.logger.warning(f"配置文件 {self.file_path} 为空,初始化新配置")
self.data = {
"instance_order": [],
"instances": {}
}
return
# 读取并解析配置
async with aiofiles.open(self.file_path, 'r', encoding='utf-8') as f:
content = await f.read()
# 尝试解析JSON
try:
raw_data = json.loads(content)
except json.JSONDecodeError as e:
self.logger.error(f"配置文件 {self.file_path} JSON解析失败: {e}")
# 尝试备份损坏的配置文件
await self._backup_corrupted_config()
# 初始化空配置
self.data = {
"instance_order": [],
"instances": {}
}
return
# 验证并加载实例
instance_order = raw_data.get("instance_order", [])
instances_raw = raw_data.get("instances", {})
instances = {}
for uid, config_data in instances_raw.items():
try:
# 使用Pydantic验证配置数据
instances[uid] = self.model_type(**config_data)
except Exception as e:
self.logger.error(f"配置项 {uid} 验证失败: {e}")
# 不中断整个加载过程,跳过无效配置
continue
# 确保instance_order与现有实例匹配
valid_order = [uid for uid in instance_order if uid in instances]
self.data = {
"instance_order": valid_order,
"instances": instances
}
self.logger.info(f"成功加载 {len(instances)} 个配置实例")
except Exception as e:
self.logger.error(f"配置加载失败: {e}", exc_info=True)
# 初始化空配置作为安全措施
self.data = {
"instance_order": [],
"instances": {}
}
# 尝试备份损坏的配置文件
await self._backup_corrupted_config()
async def _backup_corrupted_config(self) -> None:
"""备份损坏的配置文件"""
try:
backup_path = self.file_path.with_suffix(f"{self.file_path.suffix}.bak")
counter = 1
while backup_path.exists():
backup_path = self.file_path.with_suffix(f"{self.file_path.suffix}.bak{counter}")
counter += 1
if self.file_path.exists():
async with aiofiles.open(self.file_path, 'rb') as src, \
aiofiles.open(backup_path, 'wb') as dst:
content = await src.read()
await dst.write(content)
self.logger.warning(f"已备份损坏的配置文件到: {backup_path}")
except Exception as e:
self.logger.error(f"备份损坏配置失败: {e}")
async def _save_async(self) -> None:
"""异步保存配置到文件"""
async with self._lock:
try:
serializable = {
"instance_order": self.data["instance_order"],
"instances": {
uid: instance.model_dump(mode='json')
for uid, instance in self.data["instances"].items()
}
}
# 确保目录存在
self.file_path.parent.mkdir(parents=True, exist_ok=True)
async with aiofiles.open(self.file_path, 'w', encoding='utf-8') as f:
await f.write(json.dumps(serializable, indent=2, ensure_ascii=False))
self.logger.debug(f"配置已异步保存到: {self.file_path}")
except Exception as e:
self.logger.error(f"配置保存失败: {e}", exc_info=True)
raise
finally:
self._save_task = None
self._pending_save = False
def _schedule_save(self) -> None:
"""
调度配置保存(避免频繁保存)
使用防抖技术,确保短时间内多次修改只保存一次
"""
if self._save_task and not self._save_task.done():
# 已有保存任务在运行,标记需要再次保存
self._pending_save = True
return
async def save_with_debounce():
# 等待短暂时间,合并多次修改
await asyncio.sleep(0.1)
# 如果有新的保存请求,递归处理
if self._pending_save:
self._pending_save = False
await save_with_debounce()
return
await self._save_async()
self._save_task = asyncio.create_task(save_with_debounce())
@staticmethod
def generate_uid(length: int = 8) -> str:
"""生成8位随机UID"""
alphabet = string.ascii_letters + string.digits
return ''.join(secrets.choice(alphabet) for _ in range(length))
async def create(self, **kwargs) -> str:
"""创建新的配置实例"""
async with self._lock:
# 确保配置已加载完成
if not self._load_task.done():
await self._load_task
# 生成唯一UID
uid = self.generate_uid()
while uid in self.data["instances"]:
uid = self.generate_uid()
try:
# 使用Pydantic模型验证数据
new_config = self.model_type(**kwargs)
except Exception as e:
self.logger.error(f"无效的配置数据: {e}")
raise
self.data["instances"][uid] = new_config
self.data["instance_order"].append(uid)
self._schedule_save()
self.logger.info(f"创建新的配置实例: {uid}")
return uid
# 实现所需魔法方法(同步方法)
def __getitem__(self, uid: str) -> T:
"""获取配置项(同步)"""
# 确保配置已加载完成
if not self._load_task.done():
raise RuntimeError("配置尚未加载完成,请等待初始化完成")
return cast(T, self.data["instances"][uid])
def __setitem__(self, uid: str, value: T | dict[str, Any]) -> None:
"""
设置配置项(同步)
注意:此方法是同步的,但会触发异步保存
支持两种用法:
1. config[uid] = config_model_instance
2. config[uid] = {"name": "value", ...} # 字典形式
"""
# 确保配置已加载完成
if not self._load_task.done():
raise RuntimeError("配置尚未加载完成,请等待初始化完成")
# 如果传入的是字典,转换为模型实例
if isinstance(value, dict):
try:
value = self.model_type(**value)
except Exception as e:
self.logger.error(f"配置数据转换失败: {e}")
raise ValueError("无效的配置数据") from e
if not isinstance(value, self.model_type):
raise TypeError(f"值必须是 {self.model_type.__name__} 类型或字典")
# 更新内存数据
if uid not in self.data["instances"]:
self.data["instance_order"].append(uid)
self.data["instances"][uid] = value
self._schedule_save()
def __delitem__(self, uid: str) -> None:
"""删除配置项(同步)"""
# 确保配置已加载完成
if not self._load_task.done():
raise RuntimeError("配置尚未加载完成,请等待初始化完成")
if uid in self.data["instances"]:
del self.data["instances"][uid]
if uid in self.data["instance_order"]:
self.data["instance_order"].remove(uid)
self._schedule_save()
else:
raise KeyError(uid)
def __contains__(self, uid: str) -> bool:
"""检查UID是否存在同步"""
# 确保配置已加载完成
if not self._load_task.done():
return False # 配置未加载完成时,认为不存在
return uid in self.data["instances"]
def __len__(self) -> int:
"""返回配置实例数量(同步)"""
# 确保配置已加载完成
if not self._load_task.done():
return 0
return len(self.data["instance_order"])
def get_instance_order(self) -> list[str]:
"""获取实例顺序列表(同步)"""
# 确保配置已加载完成
if not self._load_task.done():
return []
return self.data["instance_order"].copy()
def get_all_instances(self) -> dict[str, T]:
"""获取所有配置实例(同步)"""
# 确保配置已加载完成
if not self._load_task.done():
return {}
return cast(dict[str, T], self.data["instances"].copy())
async def wait_until_ready(self) -> None:
"""等待配置加载完成"""
await self._load_task
async def save_now(self) -> None:
"""立即保存配置(等待保存完成)"""
if self._save_task:
await self._save_task
else:
await self._save_async()
def is_ready(self) -> bool:
"""检查配置是否已加载完成"""
return self._load_task.done() and not self._load_task.cancelled()
'''
初始化
ConfigManager(file_path: str, log_name: str, model_type: type[T])
file_path: 配置文件路径
log_name: 日志记录器名称
model_type: 配置模型类型(继承自 Pydantic BaseModel
主要方法
create(**kwargs) -> str
异步创建新的配置实例,返回唯一标识符(UID)
wait_until_ready() -> None
异步等待配置加载完成
save_now() -> None
立即保存配置到文件
is_ready() -> bool
检查配置是否已加载完成
get_instance_order() -> list[str]
获取配置实例的顺序列表
get_all_instances() -> dict[str, T]
获取所有配置实例
魔法方法
getitem(uid: str) -> T
通过 UID 获取配置实例
setitem(uid: str, value: T | dict[str, Any]) -> None
通过 UID 设置配置实例
delitem(uid: str) -> None
通过 UID 删除配置实例
contains(uid: str) -> bool
检查是否存在指定 UID 的配置实例
len() -> int
获取配置实例数量
'''