From 6898e548a52a7dd04fa40dabc0580e31697959d4 Mon Sep 17 00:00:00 2001 From: DLmaster361 Date: Tue, 5 Aug 2025 22:50:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=BD=BD=E5=85=A5=E5=90=84=E7=A7=8D?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/config.py | 135 +++++------ app/core/timer.py | 7 +- app/services/notification.py | 452 ++++++++++------------------------- app/services/system.py | 62 +++-- app/task/skland.py | 62 ++--- app/utils/ImageUtils.py | 14 +- app/utils/ProcessManager.py | 74 +++--- app/utils/__init__.py | 10 +- app/utils/logger.py | 1 + 9 files changed, 290 insertions(+), 527 deletions(-) diff --git a/app/core/config.py b/app/core/config.py index c695e0f..7a5019a 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -39,6 +39,9 @@ from utils import get_logger from models.ConfigBase import * +logger = get_logger("配置管理") + + class GlobalConfig(ConfigBase): """全局配置""" @@ -99,8 +102,6 @@ class GlobalConfig(ConfigBase): Notify_ToAddress = ConfigItem("Notify", "ToAddress", "") Notify_IfServerChan = ConfigItem("Notify", "IfServerChan", False, BoolValidator()) Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") - Notify_ServerChanChannel = ConfigItem("Notify", "ServerChanChannel", "") - Notify_ServerChanTag = ConfigItem("Notify", "ServerChanTag", "") Notify_IfCompanyWebHookBot = ConfigItem( "Notify", "IfCompanyWebHookBot", False, BoolValidator() ) @@ -284,8 +285,6 @@ class MaaUserConfig(ConfigBase): "Notify", "IfServerChan", False, BoolValidator() ) self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") - self.Notify_ServerChanChannel = ConfigItem("Notify", "ServerChanChannel", "") - self.Notify_ServerChanTag = ConfigItem("Notify", "ServerChanTag", "") self.Notify_IfCompanyWebHookBot = ConfigItem( "Notify", "IfCompanyWebHookBot", False, BoolValidator() ) @@ -471,8 +470,6 @@ class GeneralUserConfig(ConfigBase): "Notify", "IfServerChan", False, BoolValidator() ) self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") - self.Notify_ServerChanChannel = ConfigItem("Notify", "ServerChanChannel", "") - self.Notify_ServerChanTag = ConfigItem("Notify", "ServerChanTag", "") self.Notify_IfCompanyWebHookBot = ConfigItem( "Notify", "IfCompanyWebHookBot", False, BoolValidator() ) @@ -596,13 +593,12 @@ class AppConfig(GlobalConfig): self.power_sign = "NoAction" self.if_ignore_silence = False - self.logger = get_logger("配置管理") - self.logger.info("") - self.logger.info("===================================") - self.logger.info("AUTO_MAA 后端应用程序") - self.logger.info(f"版本号: v{self.VERSION}") - self.logger.info(f"根目录: {self.root_path}") - self.logger.info("===================================") + logger.info("") + logger.info("===================================") + logger.info("AUTO_MAA 后端应用程序") + logger.info(f"版本号: v{self.VERSION}") + logger.info(f"根目录: {self.root_path}") + logger.info("===================================") # 检查目录 self.log_path.parent.mkdir(parents=True, exist_ok=True) @@ -623,21 +619,21 @@ class AppConfig(GlobalConfig): await self.QueueConfig.connect(self.config_path / "QueueConfig.json") # self.check_data() - self.logger.info("程序初始化完成") + logger.info("程序初始化完成") async def add_script( self, script: Literal["MAA", "General"] ) -> tuple[uuid.UUID, ConfigBase]: """添加脚本配置""" - self.logger.info(f"添加脚本配置:{script}") + logger.info(f"添加脚本配置:{script}") return await self.ScriptConfig.add(self.CLASS_BOOK[script]) async def get_script(self, script_id: Optional[str]) -> tuple[list, dict]: """获取脚本配置""" - self.logger.info(f"获取脚本配置:{script_id}") + logger.info(f"获取脚本配置:{script_id}") if script_id is None: data = await self.ScriptConfig.toDict() @@ -653,15 +649,13 @@ class AppConfig(GlobalConfig): ) -> None: """更新脚本配置""" - self.logger.info(f"更新脚本配置:{script_id}") + logger.info(f"更新脚本配置:{script_id}") uid = uuid.UUID(script_id) for group, items in data.items(): for name, value in items.items(): - self.logger.debug( - f"更新脚本配置:{script_id} - {group}.{name} = {value}" - ) + logger.debug(f"更新脚本配置:{script_id} - {group}.{name} = {value}") await self.ScriptConfig[uid].set(group, name, value) await self.ScriptConfig.save() @@ -669,21 +663,21 @@ class AppConfig(GlobalConfig): async def del_script(self, script_id: str) -> None: """删除脚本配置""" - self.logger.info(f"删除脚本配置:{script_id}") + logger.info(f"删除脚本配置:{script_id}") await self.ScriptConfig.remove(uuid.UUID(script_id)) async def reorder_script(self, index_list: list[str]) -> None: """重新排序脚本""" - self.logger.info(f"重新排序脚本:{index_list}") + logger.info(f"重新排序脚本:{index_list}") await self.ScriptConfig.setOrder([uuid.UUID(_) for _ in index_list]) async def add_user(self, script_id: str) -> tuple[uuid.UUID, ConfigBase]: """添加用户配置""" - self.logger.info(f"{script_id} 添加用户配置") + logger.info(f"{script_id} 添加用户配置") script_config = self.ScriptConfig[uuid.UUID(script_id)] @@ -702,16 +696,14 @@ class AppConfig(GlobalConfig): ) -> None: """更新用户配置""" - self.logger.info(f"{script_id} 更新用户配置:{user_id}") + logger.info(f"{script_id} 更新用户配置:{user_id}") script_config = self.ScriptConfig[uuid.UUID(script_id)] uid = uuid.UUID(user_id) for group, items in data.items(): for name, value in items.items(): - self.logger.debug( - f"更新脚本配置:{script_id} - {group}.{name} = {value}" - ) + logger.debug(f"更新脚本配置:{script_id} - {group}.{name} = {value}") if isinstance(script_config, (MaaConfig | GeneralConfig)): await script_config.UserData[uid].set(group, name, value) @@ -720,7 +712,7 @@ class AppConfig(GlobalConfig): async def del_user(self, script_id: str, user_id: str) -> None: """删除用户配置""" - self.logger.info(f"{script_id} 删除用户配置:{user_id}") + logger.info(f"{script_id} 删除用户配置:{user_id}") script_config = self.ScriptConfig[uuid.UUID(script_id)] uid = uuid.UUID(user_id) @@ -732,7 +724,7 @@ class AppConfig(GlobalConfig): async def reorder_user(self, script_id: str, index_list: list[str]) -> None: """重新排序用户""" - self.logger.info(f"{script_id} 重新排序用户:{index_list}") + logger.info(f"{script_id} 重新排序用户:{index_list}") script_config = self.ScriptConfig[uuid.UUID(script_id)] @@ -743,14 +735,14 @@ class AppConfig(GlobalConfig): async def add_queue(self) -> tuple[uuid.UUID, ConfigBase]: """添加调度队列""" - self.logger.info("添加调度队列") + logger.info("添加调度队列") return await self.QueueConfig.add(QueueConfig) async def get_queue(self, queue_id: Optional[str]) -> tuple[list, dict]: """获取调度队列配置""" - self.logger.info(f"获取调度队列配置:{queue_id}") + logger.info(f"获取调度队列配置:{queue_id}") if queue_id is None: data = await self.QueueConfig.toDict() @@ -766,15 +758,13 @@ class AppConfig(GlobalConfig): ) -> None: """更新调度队列配置""" - self.logger.info(f"更新调度队列配置:{queue_id}") + logger.info(f"更新调度队列配置:{queue_id}") uid = uuid.UUID(queue_id) for group, items in data.items(): for name, value in items.items(): - self.logger.debug( - f"更新调度队列配置:{queue_id} - {group}.{name} = {value}" - ) + logger.debug(f"更新调度队列配置:{queue_id} - {group}.{name} = {value}") await self.QueueConfig[uid].set(group, name, value) await self.QueueConfig.save() @@ -782,21 +772,21 @@ class AppConfig(GlobalConfig): async def del_queue(self, queue_id: str) -> None: """删除调度队列配置""" - self.logger.info(f"删除调度队列配置:{queue_id}") + logger.info(f"删除调度队列配置:{queue_id}") await self.QueueConfig.remove(uuid.UUID(queue_id)) async def reorder_queue(self, index_list: list[str]) -> None: """重新排序调度队列""" - self.logger.info(f"重新排序调度队列:{index_list}") + logger.info(f"重新排序调度队列:{index_list}") await self.QueueConfig.setOrder([uuid.UUID(_) for _ in index_list]) async def add_time_set(self, queue_id: str) -> tuple[uuid.UUID, ConfigBase]: """添加时间设置配置""" - self.logger.info(f"{queue_id} 添加时间设置配置") + logger.info(f"{queue_id} 添加时间设置配置") queue_config = self.QueueConfig[uuid.UUID(queue_id)] @@ -813,16 +803,14 @@ class AppConfig(GlobalConfig): ) -> None: """更新时间设置配置""" - self.logger.info(f"{queue_id} 更新时间设置配置:{time_set_id}") + logger.info(f"{queue_id} 更新时间设置配置:{time_set_id}") queue_config = self.QueueConfig[uuid.UUID(queue_id)] uid = uuid.UUID(time_set_id) for group, items in data.items(): for name, value in items.items(): - self.logger.debug( - f"更新时间设置配置:{queue_id} - {group}.{name} = {value}" - ) + logger.debug(f"更新时间设置配置:{queue_id} - {group}.{name} = {value}") if isinstance(queue_config, QueueConfig): await queue_config.TimeSet[uid].set(group, name, value) @@ -831,7 +819,7 @@ class AppConfig(GlobalConfig): async def del_time_set(self, queue_id: str, time_set_id: str) -> None: """删除时间设置配置""" - self.logger.info(f"{queue_id} 删除时间设置配置:{time_set_id}") + logger.info(f"{queue_id} 删除时间设置配置:{time_set_id}") queue_config = self.QueueConfig[uuid.UUID(queue_id)] uid = uuid.UUID(time_set_id) @@ -843,7 +831,7 @@ class AppConfig(GlobalConfig): async def reorder_time_set(self, queue_id: str, index_list: list[str]) -> None: """重新排序时间设置""" - self.logger.info(f"{queue_id} 重新排序时间设置:{index_list}") + logger.info(f"{queue_id} 重新排序时间设置:{index_list}") queue_config = self.QueueConfig[uuid.UUID(queue_id)] @@ -856,14 +844,14 @@ class AppConfig(GlobalConfig): ) -> tuple[uuid.UUID, ConfigBase]: """添加计划表""" - self.logger.info(f"添加计划表:{script}") + logger.info(f"添加计划表:{script}") return await self.PlanConfig.add(self.CLASS_BOOK[script]) async def get_plan(self, plan_id: Optional[str]) -> tuple[list, dict]: """获取计划表配置""" - self.logger.info(f"获取计划表配置:{plan_id}") + logger.info(f"获取计划表配置:{plan_id}") if plan_id is None: data = await self.PlanConfig.toDict() @@ -877,15 +865,13 @@ class AppConfig(GlobalConfig): async def update_plan(self, plan_id: str, data: Dict[str, Dict[str, Any]]) -> None: """更新计划表配置""" - self.logger.info(f"更新计划表配置:{plan_id}") + logger.info(f"更新计划表配置:{plan_id}") uid = uuid.UUID(plan_id) for group, items in data.items(): for name, value in items.items(): - self.logger.debug( - f"更新计划表配置:{plan_id} - {group}.{name} = {value}" - ) + logger.debug(f"更新计划表配置:{plan_id} - {group}.{name} = {value}") await self.PlanConfig[uid].set(group, name, value) await self.PlanConfig.save() @@ -893,21 +879,21 @@ class AppConfig(GlobalConfig): async def del_plan(self, plan_id: str) -> None: """删除计划表配置""" - self.logger.info(f"删除计划表配置:{plan_id}") + logger.info(f"删除计划表配置:{plan_id}") await self.PlanConfig.remove(uuid.UUID(plan_id)) async def reorder_plan(self, index_list: list[str]) -> None: """重新排序计划表""" - self.logger.info(f"重新排序计划表:{index_list}") + logger.info(f"重新排序计划表:{index_list}") await self.PlanConfig.setOrder([uuid.UUID(_) for _ in index_list]) async def add_queue_item(self, queue_id: str) -> tuple[uuid.UUID, ConfigBase]: """添加队列项配置""" - self.logger.info(f"{queue_id} 添加队列项配置") + logger.info(f"{queue_id} 添加队列项配置") queue_config = self.QueueConfig[uuid.UUID(queue_id)] @@ -924,7 +910,7 @@ class AppConfig(GlobalConfig): ) -> None: """更新队列项配置""" - self.logger.info(f"{queue_id} 更新队列项配置:{queue_item_id}") + logger.info(f"{queue_id} 更新队列项配置:{queue_item_id}") queue_config = self.QueueConfig[uuid.UUID(queue_id)] uid = uuid.UUID(queue_item_id) @@ -933,9 +919,7 @@ class AppConfig(GlobalConfig): for name, value in items.items(): if uuid.UUID(value) not in self.ScriptConfig: raise ValueError(f"Script with uid {value} does not exist.") - self.logger.debug( - f"更新队列项配置:{queue_id} - {group}.{name} = {value}" - ) + logger.debug(f"更新队列项配置:{queue_id} - {group}.{name} = {value}") if isinstance(queue_config, QueueConfig): await queue_config.QueueItem[uid].set(group, name, value) @@ -944,7 +928,7 @@ class AppConfig(GlobalConfig): async def del_queue_item(self, queue_id: str, queue_item_id: str) -> None: """删除队列项配置""" - self.logger.info(f"{queue_id} 删除队列项配置:{queue_item_id}") + logger.info(f"{queue_id} 删除队列项配置:{queue_item_id}") queue_config = self.QueueConfig[uuid.UUID(queue_id)] uid = uuid.UUID(queue_item_id) @@ -956,7 +940,7 @@ class AppConfig(GlobalConfig): async def reorder_queue_item(self, queue_id: str, index_list: list[str]) -> None: """重新排序队列项""" - self.logger.info(f"{queue_id} 重新排序队列项:{index_list}") + logger.info(f"{queue_id} 重新排序队列项:{index_list}") queue_config = self.QueueConfig[uuid.UUID(queue_id)] @@ -967,18 +951,18 @@ class AppConfig(GlobalConfig): async def get_setting(self) -> Dict[str, Any]: """获取全局设置""" - self.logger.info("获取全局设置") + logger.info("获取全局设置") return await self.toDict(ignore_multi_config=True) async def update_setting(self, data: Dict[str, Dict[str, Any]]) -> None: """更新全局设置""" - self.logger.info(f"更新全局设置") + logger.info(f"更新全局设置") for group, items in data.items(): for name, value in items.items(): - self.logger.debug(f"更新全局设置 - {group}.{name} = {value}") + logger.debug(f"更新全局设置 - {group}.{name} = {value}") await self.set(group, name, value) def server_date(self) -> date: @@ -994,25 +978,29 @@ class AppConfig(GlobalConfig): dt = dt - timedelta(days=1) return dt.date() + def get_proxies(self) -> Dict[str, str]: + """获取代理设置""" + return { + "http": self.get("Update", "ProxyAddress"), + "https": self.get("Update", "ProxyAddress"), + } + async def get_stage(self) -> tuple[bool, Dict[str, Dict[str, list]]]: """从MAA服务器更新活动关卡信息""" - self.logger.info("开始获取活动关卡信息") + logger.info("开始获取活动关卡信息") response = requests.get( "https://api.maa.plus/MaaAssistantArknights/api/gui/StageActivity.json", timeout=10, - proxies={ - "http": self.get("Update", "ProxyAddress"), - "https": self.get("Update", "ProxyAddress"), - }, + proxies=self.get_proxies(), ) if response.status_code == 200: stage_infos = response.json()["Official"]["sideStoryStage"] if_get_maa_stage = True else: - self.logger.warning(f"无法从MAA服务器获取活动关卡信息:{response.text}") + logger.warning(f"无法从MAA服务器获取活动关卡信息:{response.text}") if_get_maa_stage = False stage_infos = [] @@ -1054,23 +1042,18 @@ class AppConfig(GlobalConfig): async def get_server_info(self, type: str) -> Dict[str, Any]: """获取公告信息""" - self.logger.info(f"开始从 AUTO_MAA 服务器获取 {type} 信息") + logger.info(f"开始从 AUTO_MAA 服务器获取 {type} 信息") response = requests.get( url=f"http://221.236.27.82:10197/d/AUTO_MAA/Server/{type}.json", timeout=10, - proxies={ - "http": self.get("Update", "ProxyAddress"), - "https": self.get("Update", "ProxyAddress"), - }, + proxies=self.get_proxies(), ) if response.status_code == 200: return response.json() else: - self.logger.warning( - f"无法从 AUTO_MAA 服务器获取 {type} 信息:{response.text}" - ) + logger.warning(f"无法从 AUTO_MAA 服务器获取 {type} 信息:{response.text}") raise ConnectionError( "Cannot connect to the notice server. Please check your network connection or try again later." ) diff --git a/app/core/timer.py b/app/core/timer.py index 1963299..c04b226 100644 --- a/app/core/timer.py +++ b/app/core/timer.py @@ -26,16 +26,17 @@ from utils import get_logger from .config import Config +logger = get_logger("主业务定时器") + + class _MainTimer: def __init__(self): super().__init__() - self.logger = get_logger("主业务定时器") - async def second_task(self): """每秒定期任务""" - self.logger.info("每秒定期任务启动") + logger.info("每秒定期任务启动") while True: diff --git a/app/services/notification.py b/app/services/notification.py index 0ce78fd..9ef2f14 100644 --- a/app/services/notification.py +++ b/app/services/notification.py @@ -18,39 +18,28 @@ # Contact: DLmaster_361@163.com -""" -AUTO_MAA -AUTO_MAA通知服务 -v4.4 -作者:DLmaster_361 -""" import re import smtplib -import time +import requests from email.header import Header from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import formataddr from pathlib import Path -from typing import Union - -import requests -from PySide6.QtCore import QObject, Signal from plyer import notification -from app.core import Config, logger -from app.utils.security import Crypto -from app.utils.ImageUtils import ImageUtils +from core import Config +from utils import get_logger, ImageUtils + +logger = get_logger("通知服务") -class Notification(QObject): +class Notification: - push_info_bar = Signal(str, str, str, int) - - def __init__(self, parent=None): - super().__init__(parent) + def __init__(self): + super().__init__() def push_plyer(self, title, message, ticker, t) -> bool: """ @@ -63,19 +52,22 @@ class Notification(QObject): :return: bool """ - if Config.get(Config.notify_IfPushPlyer): + if Config.get("Notify", "IfPushPlyer"): - logger.info(f"推送系统通知:{title}", module="通知服务") + logger.info(f"推送系统通知:{title}") - notification.notify( - title=title, - message=message, - app_name="AUTO_MAA", - app_icon=str(Config.app_path / "resources/icons/AUTO_MAA.ico"), - timeout=t, - ticker=ticker, - toast=True, - ) + if notification.notify is not None: + notification.notify( + title=title, + message=message, + app_name="AUTO_MAA", + app_icon=(Path.cwd() / "resources/icons/AUTO_MAA.ico").as_posix(), + timeout=t, + ticker=ticker, + toast=True, + ) + else: + logger.error("plyer.notification 未正确导入,无法推送系统通知") return True @@ -90,12 +82,12 @@ class Notification(QObject): """ if ( - Config.get(Config.notify_SMTPServerAddress) == "" - or Config.get(Config.notify_AuthorizationCode) == "" + Config.get("Notify", "SMTPServerAddress") == "" + or Config.get("Notify", "AuthorizationCode") == "" or not bool( re.match( r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", - Config.get(Config.notify_FromAddress), + Config.get("Notify", "FromAddress"), ) ) or not bool( @@ -106,336 +98,146 @@ class Notification(QObject): ) ): logger.error( - "请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址", - module="通知服务", + "请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址" ) - self.push_info_bar.emit( - "error", - "邮件通知推送异常", - "请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址", - -1, + raise ValueError( + "The SMTP server address, authorization code, sender address, or recipient address is not set correctly." ) - return None - try: - # 定义邮件正文 - if mode == "文本": - message = MIMEText(content, "plain", "utf-8") - elif mode == "网页": - message = MIMEMultipart("alternative") - message["From"] = formataddr( - ( - Header("AUTO_MAA通知服务", "utf-8").encode(), - Config.get(Config.notify_FromAddress), - ) - ) # 发件人显示的名字 - message["To"] = formataddr( - (Header("AUTO_MAA用户", "utf-8").encode(), to_address) - ) # 收件人显示的名字 - message["Subject"] = Header(title, "utf-8") - - if mode == "网页": - message.attach(MIMEText(content, "html", "utf-8")) - - smtpObj = smtplib.SMTP_SSL(Config.get(Config.notify_SMTPServerAddress), 465) - smtpObj.login( - Config.get(Config.notify_FromAddress), - Crypto.win_decryptor(Config.get(Config.notify_AuthorizationCode)), + # 定义邮件正文 + if mode == "文本": + message = MIMEText(content, "plain", "utf-8") + elif mode == "网页": + message = MIMEMultipart("alternative") + message["From"] = formataddr( + ( + Header("AUTO_MAA通知服务", "utf-8").encode(), + Config.get("Notify", "FromAddress"), ) - smtpObj.sendmail( - Config.get(Config.notify_FromAddress), to_address, message.as_string() - ) - smtpObj.quit() - logger.success(f"邮件发送成功:{title}", module="通知服务") - except Exception as e: - logger.exception(f"发送邮件时出错:{e}", module="通知服务") - self.push_info_bar.emit("error", "发送邮件时出错", f"{e}", -1) + ) # 发件人显示的名字 + message["To"] = formataddr( + (Header("AUTO_MAA用户", "utf-8").encode(), to_address) + ) # 收件人显示的名字 + message["Subject"] = str(Header(title, "utf-8")) - def ServerChanPush( - self, title, content, send_key, tag, channel - ) -> Union[bool, str]: + if mode == "网页": + message.attach(MIMEText(content, "html", "utf-8")) + + smtpObj = smtplib.SMTP_SSL(Config.get("Notify", "SMTPServerAddress"), 465) + smtpObj.login( + Config.get("Notify", "FromAddress"), + Config.get("Notify", "AuthorizationCode"), + ) + smtpObj.sendmail( + Config.get("Notify", "FromAddress"), to_address, message.as_string() + ) + smtpObj.quit() + logger.success(f"邮件发送成功:{title}") + + def ServerChanPush(self, title, content, send_key) -> None: """ 使用Server酱推送通知 :param title: 通知标题 :param content: 通知内容 :param send_key: Server酱的SendKey - :param tag: 通知标签 - :param channel: 通知频道 - :return: bool or str """ if not send_key: - logger.error("请正确设置Server酱的SendKey", module="通知服务") - self.push_info_bar.emit( - "error", "Server酱通知推送异常", "请正确设置Server酱的SendKey", -1 - ) - return None + raise ValueError("The ServerChan SendKey can not be empty.") - try: - # 构造 URL - if send_key.startswith("sctp"): - match = re.match(r"^sctp(\d+)t", send_key) - if match: - url = f"https://{match.group(1)}.push.ft07.com/send/{send_key}.send" - else: - raise ValueError("SendKey 格式错误(sctp)") + # 构造 URL + if send_key.startswith("sctp"): + match = re.match(r"^sctp(\d+)t", send_key) + if match: + url = f"https://{match.group(1)}.push.ft07.com/send/{send_key}.send" else: - url = f"https://sctapi.ftqq.com/{send_key}.send" + raise ValueError("SendKey format is incorrect (sctp).") + else: + url = f"https://sctapi.ftqq.com/{send_key}.send" - # 构建 tags 和 channel - def is_valid(s): - return s == "" or ( - s == "|".join(s.split("|")) - and (s.count("|") == 0 or all(s.split("|"))) - ) + # 请求发送 + params = {"title": title, "desp": content} + headers = {"Content-Type": "application/json;charset=utf-8"} - tags = "|".join(_.strip() for _ in tag.split("|")) - channels = "|".join(_.strip() for _ in channel.split("|")) + response = requests.post( + url, json=params, headers=headers, timeout=10, proxies=Config.get_proxies() + ) + result = response.json() - options = {} - if is_valid(tags): - options["tags"] = tags - else: - logger.warning("Server酱 Tag 配置不正确,将被忽略", module="通知服务") - self.push_info_bar.emit( - "warning", - "Server酱通知推送异常", - "请正确设置 ServerChan 的 Tag", - -1, - ) + if result.get("code") == 0: + logger.success(f"Server酱推送通知成功:{title}") + else: + raise Exception(f"ServerChan failed to send notification: {response.text}") - if is_valid(channels): - options["channel"] = channels - else: - logger.warning( - "Server酱 Channel 配置不正确,将被忽略", module="通知服务" - ) - self.push_info_bar.emit( - "warning", - "Server酱通知推送异常", - "请正确设置 ServerChan 的 Channel", - -1, - ) - - # 请求发送 - params = {"title": title, "desp": content, **options} - headers = {"Content-Type": "application/json;charset=utf-8"} - - response = requests.post( - url, - json=params, - headers=headers, - timeout=10, - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, - ) - result = response.json() - - if result.get("code") == 0: - logger.success(f"Server酱推送通知成功:{title}", module="通知服务") - return True - else: - error_code = result.get("code", "-1") - logger.exception( - f"Server酱通知推送失败:响应码:{error_code}", module="通知服务" - ) - self.push_info_bar.emit( - "error", "Server酱通知推送失败", f"响应码:{error_code}", -1 - ) - return f"Server酱通知推送失败:{error_code}" - - except Exception as e: - logger.exception(f"Server酱通知推送异常:{e}", module="通知服务") - self.push_info_bar.emit( - "error", - "Server酱通知推送异常", - "请检查相关设置和网络连接。如全部配置正确,请稍后再试。", - -1, - ) - return f"Server酱通知推送异常:{str(e)}" - - def CompanyWebHookBotPush(self, title, content, webhook_url) -> Union[bool, str]: + def WebHookPush(self, title, content, webhook_url) -> None: """ - 使用企业微信群机器人推送通知 + WebHook 推送通知 :param title: 通知标题 :param content: 通知内容 - :param webhook_url: 企业微信群机器人的WebHook地址 - :return: bool or str + :param webhook_url: WebHook地址 """ - if webhook_url == "": - logger.error("请正确设置企业微信群机器人的WebHook地址", module="通知服务") - self.push_info_bar.emit( - "error", - "企业微信群机器人通知推送异常", - "请正确设置企业微信群机器人的WebHook地址", - -1, - ) - return None + if not webhook_url: + raise ValueError("The webhook URL can not be empty.") content = f"{title}\n{content}" data = {"msgtype": "text", "text": {"content": content}} - for _ in range(3): - try: - response = requests.post( - url=webhook_url, - json=data, - timeout=10, - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, - ) - info = response.json() - break - except Exception as e: - err = e - time.sleep(0.1) - else: - logger.error(f"推送企业微信群机器人时出错:{err}", module="通知服务") - self.push_info_bar.emit( - "error", - "企业微信群机器人通知推送失败", - f"使用企业微信群机器人推送通知时出错:{err}", - -1, - ) - return None + response = requests.post( + url=webhook_url, json=data, timeout=10, proxies=Config.get_proxies() + ) + info = response.json() if info["errcode"] == 0: - logger.success(f"企业微信群机器人推送通知成功:{title}", module="通知服务") - return True + logger.success(f"WebHook 推送通知成功:{title}") else: - logger.error(f"企业微信群机器人推送通知失败:{info}", module="通知服务") - self.push_info_bar.emit( - "error", - "企业微信群机器人通知推送失败", - f"使用企业微信群机器人推送通知时出错:{err}", - -1, - ) - return f"使用企业微信群机器人推送通知时出错:{err}" + raise Exception(f"WebHook failed to send notification: {response.text}") - def CompanyWebHookBotPushImage(self, image_path: Path, webhook_url: str) -> bool: + def CompanyWebHookBotPushImage(self, image_path: Path, webhook_url: str) -> None: """ 使用企业微信群机器人推送图片通知 :param image_path: 图片文件路径 :param webhook_url: 企业微信群机器人的WebHook地址 - :return: bool """ - try: - # 压缩图片 - ImageUtils.compress_image_if_needed(image_path) + if not webhook_url: + raise ValueError("The webhook URL can not be empty.") - # 检查图片是否存在 - if not image_path.exists(): - logger.error( - "图片推送异常 | 图片不存在或者压缩失败,请检查图片路径是否正确", - module="通知服务", - ) - self.push_info_bar.emit( - "error", - "企业微信群机器人通知推送异常", - "图片不存在或者压缩失败,请检查图片路径是否正确", - -1, - ) - return False + # 压缩图片 + ImageUtils.compress_image_if_needed(image_path) - if not webhook_url: - logger.error( - "请正确设置企业微信群机器人的WebHook地址", module="通知服务" - ) - self.push_info_bar.emit( - "error", - "企业微信群机器人通知推送异常", - "请正确设置企业微信群机器人的WebHook地址", - -1, - ) - return False + # 检查图片是否存在 + if not image_path.exists(): + raise FileNotFoundError(f"File not found: {image_path}") - # 获取图片base64和md5 - try: - image_base64 = ImageUtils.get_base64_from_file(str(image_path)) - image_md5 = ImageUtils.calculate_md5_from_file(str(image_path)) - except Exception as e: - logger.exception(f"图片编码或MD5计算失败:{e}", module="通知服务") - self.push_info_bar.emit( - "error", - "企业微信群机器人通知推送异常", - f"图片编码或MD5计算失败:{e}", - -1, - ) - return False + # 获取图片base64和md5 + image_base64 = ImageUtils.get_base64_from_file(str(image_path)) + image_md5 = ImageUtils.calculate_md5_from_file(str(image_path)) - data = { - "msgtype": "image", - "image": {"base64": image_base64, "md5": image_md5}, - } + data = { + "msgtype": "image", + "image": {"base64": image_base64, "md5": image_md5}, + } - for _ in range(3): - try: - response = requests.post( - url=webhook_url, - json=data, - timeout=10, - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, - ) - info = response.json() - break - except requests.RequestException as e: - err = e - logger.exception( - f"推送企业微信群机器人图片第{_+1}次失败:{e}", module="通知服务" - ) - time.sleep(0.1) - else: - logger.error("推送企业微信群机器人图片时出错", module="通知服务") - self.push_info_bar.emit( - "error", - "企业微信群机器人图片推送失败", - f"使用企业微信群机器人推送图片时出错:{err}", - -1, - ) - return False + response = requests.post( + url=webhook_url, json=data, timeout=10, proxies=Config.get_proxies() + ) + info = response.json() - if info.get("errcode") == 0: - logger.success( - f"企业微信群机器人推送图片成功:{image_path.name}", - module="通知服务", - ) - return True - else: - logger.error(f"企业微信群机器人推送图片失败:{info}", module="通知服务") - self.push_info_bar.emit( - "error", - "企业微信群机器人图片推送失败", - f"使用企业微信群机器人推送图片时出错:{info}", - -1, - ) - return False - - except Exception as e: - logger.error(f"推送企业微信群机器人图片时发生未知异常:{e}") - self.push_info_bar.emit( - "error", - "企业微信群机器人图片推送失败", - f"发生未知异常:{e}", - -1, + if info.get("errcode") == 0: + logger.success(f"企业微信群机器人推送图片成功:{image_path.name}") + else: + raise Exception( + f"Company WebHook Bot failed to send image: {response.text}" ) - return False - def send_test_notification(self): + def send_test_notification(self) -> None: """发送测试通知到所有已启用的通知渠道""" - logger.info("发送测试通知到所有已启用的通知渠道", module="通知服务") + logger.info("发送测试通知到所有已启用的通知渠道") # 发送系统通知 self.push_plyer( @@ -446,39 +248,35 @@ class Notification(QObject): ) # 发送邮件通知 - if Config.get(Config.notify_IfSendMail): + if Config.get("Notify", "IfSendMail"): self.send_mail( "文本", "AUTO_MAA测试通知", "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!", - Config.get(Config.notify_ToAddress), + Config.get("Notify", "ToAddress"), ) # 发送Server酱通知 - if Config.get(Config.notify_IfServerChan): + if Config.get("Notify", "IfServerChan"): self.ServerChanPush( "AUTO_MAA测试通知", "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!", - Config.get(Config.notify_ServerChanKey), - Config.get(Config.notify_ServerChanTag), - Config.get(Config.notify_ServerChanChannel), + Config.get("Notify", "ServerChanKey"), ) - # 发送企业微信机器人通知 - if Config.get(Config.notify_IfCompanyWebHookBot): - self.CompanyWebHookBotPush( + # 发送WebHook通知 + if Config.get("Notify", "IfCompanyWebHookBot"): + self.WebHookPush( "AUTO_MAA测试通知", "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!", - Config.get(Config.notify_CompanyWebHookBotUrl), + Config.get("Notify", "CompanyWebHookBotUrl"), ) Notify.CompanyWebHookBotPushImage( - Config.app_path / "resources/images/notification/test_notify.png", - Config.get(Config.notify_CompanyWebHookBotUrl), + Path.cwd() / "resources/images/notification/test_notify.png", + Config.get("Notify", "CompanyWebHookBotUrl"), ) - logger.info("测试通知发送完成", module="通知服务") - - return True + logger.success("测试通知发送完成") Notify = Notification() diff --git a/app/services/system.py b/app/services/system.py index 7e2ef90..a3a00df 100644 --- a/app/services/system.py +++ b/app/services/system.py @@ -18,14 +18,7 @@ # Contact: DLmaster_361@163.com -""" -AUTO_MAA -AUTO_MAA系统服务 -v4.4 -作者:DLmaster_361 -""" -from PySide6.QtWidgets import QApplication import sys import ctypes import win32gui @@ -37,7 +30,10 @@ import getpass from datetime import datetime from pathlib import Path -from app.core import Config +from core import Config +from utils.logger import get_logger + +logger = get_logger("系统服务") class _SystemHandler: @@ -53,7 +49,7 @@ class _SystemHandler: def set_Sleep(self) -> None: """同步系统休眠状态""" - if Config.get(Config.function_IfAllowSleep): + if Config.get("Function", "IfAllowSleep"): # 设置系统电源状态 ctypes.windll.kernel32.SetThreadExecutionState( self.ES_CONTINUOUS | self.ES_SYSTEM_REQUIRED @@ -65,7 +61,9 @@ class _SystemHandler: def set_SelfStart(self) -> None: """同步开机自启""" - if Config.get(Config.start_IfSelfStart) and not self.is_startup(): + return None # 目前不支持开机自启 + + if Config.get("Function", "IfSelfStart") and not self.is_startup(): # 创建任务计划 try: @@ -164,7 +162,7 @@ class _SystemHandler: pass except Exception as e: - logger.exception(f"程序自启动任务计划创建失败: {e}", module="系统服务") + logger.exception(f"程序自启动任务计划创建失败: {e}") elif not Config.get(Config.start_IfSelfStart) and self.is_startup(): @@ -179,7 +177,7 @@ class _SystemHandler: ) if result.returncode == 0: - logger.success("程序自启动任务计划已删除", module="系统服务") + logger.success("程序自启动任务计划已删除") else: logger.error( f"程序自启动任务计划删除失败: {result.stderr}", @@ -187,7 +185,7 @@ class _SystemHandler: ) except Exception as e: - logger.exception(f"程序自启动任务计划删除失败: {e}", module="系统服务") + logger.exception(f"程序自启动任务计划删除失败: {e}") def set_power(self, mode) -> None: """ @@ -200,69 +198,65 @@ class _SystemHandler: if mode == "NoAction": - logger.info("不执行系统电源操作", module="系统服务") + logger.info("不执行系统电源操作") elif mode == "Shutdown": self.kill_emulator_processes() - logger.info("执行关机操作", module="系统服务") + logger.info("执行关机操作") subprocess.run(["shutdown", "/s", "/t", "0"]) elif mode == "ShutdownForce": - logger.info("执行强制关机操作", module="系统服务") + logger.info("执行强制关机操作") subprocess.run(["shutdown", "/s", "/t", "0", "/f"]) elif mode == "Hibernate": - logger.info("执行休眠操作", module="系统服务") + logger.info("执行休眠操作") subprocess.run(["shutdown", "/h"]) elif mode == "Sleep": - logger.info("执行睡眠操作", module="系统服务") + logger.info("执行睡眠操作") subprocess.run( ["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"] ) elif mode == "KillSelf": - logger.info("执行退出主程序操作", module="系统服务") - Config.main_window.close() - QApplication.quit() + logger.info("执行退出主程序操作") sys.exit(0) elif sys.platform.startswith("linux"): if mode == "NoAction": - logger.info("不执行系统电源操作", module="系统服务") + logger.info("不执行系统电源操作") elif mode == "Shutdown": - logger.info("执行关机操作", module="系统服务") + logger.info("执行关机操作") subprocess.run(["shutdown", "-h", "now"]) elif mode == "Hibernate": - logger.info("执行休眠操作", module="系统服务") + logger.info("执行休眠操作") subprocess.run(["systemctl", "hibernate"]) elif mode == "Sleep": - logger.info("执行睡眠操作", module="系统服务") + logger.info("执行睡眠操作") subprocess.run(["systemctl", "suspend"]) elif mode == "KillSelf": - logger.info("执行退出主程序操作", module="系统服务") - Config.main_window.close() - QApplication.quit() + logger.info("执行退出主程序操作") sys.exit(0) def kill_emulator_processes(self): """这里暂时仅支持 MuMu 模拟器""" - logger.info("正在清除模拟器进程", module="系统服务") + logger.info("正在清除模拟器进程") keywords = ["Nemu", "nemu", "emulator", "MuMu"] for proc in psutil.process_iter(["pid", "name"]): @@ -277,7 +271,7 @@ class _SystemHandler: except (psutil.NoSuchProcess, psutil.AccessDenied): continue - logger.success("模拟器进程清除完成", module="系统服务") + logger.success("模拟器进程清除完成") def is_startup(self) -> bool: """判断程序是否已经开机自启""" @@ -292,7 +286,7 @@ class _SystemHandler: ) return result.returncode == 0 except Exception as e: - logger.exception(f"检查任务计划程序失败: {e}", module="系统服务") + logger.exception(f"检查任务计划程序失败: {e}") return False def get_window_info(self) -> list: @@ -316,7 +310,7 @@ class _SystemHandler: :param path: 进程路径 """ - logger.info(f"开始中止进程: {path}", module="系统服务") + logger.info(f"开始中止进程: {path}") for pid in self.search_pids(path): killprocess = subprocess.Popen( @@ -326,7 +320,7 @@ class _SystemHandler: ) killprocess.wait() - logger.success(f"进程已中止: {path}", module="系统服务") + logger.success(f"进程已中止: {path}") def search_pids(self, path: Path) -> list: """ @@ -336,7 +330,7 @@ class _SystemHandler: :return: 匹配的进程PID列表 """ - logger.info(f"开始查找进程 PID: {path}", module="系统服务") + logger.info(f"开始查找进程 PID: {path}") pids = [] for proc in psutil.process_iter(["pid", "exe"]): diff --git a/app/task/skland.py b/app/task/skland.py index 901a352..831e9a6 100644 --- a/app/task/skland.py +++ b/app/task/skland.py @@ -35,14 +35,18 @@ v4.4 import time import json import hmac +import asyncio import hashlib import requests from urllib import parse -from app.core import Config, logger +from core import Config +from utils.logger import get_logger + +logger = get_logger("森空岛签到任务") -def skland_sign_in(token) -> dict: +async def skland_sign_in(token) -> dict: """森空岛签到""" app_code = "4ca99fa6b56cc2ba" @@ -127,7 +131,7 @@ def skland_sign_in(token) -> dict: v["cred"] = cred return v - def login_by_token(token_code): + async def login_by_token(token_code): """ 使用token一步步拿到cred和sign_token @@ -140,10 +144,10 @@ def skland_sign_in(token) -> dict: token_code = t["data"]["content"] except: pass - grant_code = get_grant_code(token_code) - return get_cred(grant_code) + grant_code = await get_grant_code(token_code) + return await get_cred(grant_code) - def get_cred(grant): + async def get_cred(grant): """ 通过grant code获取cred和sign_token @@ -155,10 +159,7 @@ def skland_sign_in(token) -> dict: cred_code_url, json={"code": grant, "kind": 1}, headers=header_login, - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, + proxies=Config.get_proxies(), ).json() if rsp["code"] != 0: raise Exception(f'获得cred失败:{rsp.get("messgae")}') @@ -166,7 +167,7 @@ def skland_sign_in(token) -> dict: cred = rsp["data"]["cred"] return cred, sign_token - def get_grant_code(token): + async def get_grant_code(token): """ 通过token获取grant code @@ -177,10 +178,7 @@ def skland_sign_in(token) -> dict: grant_code_url, json={"appCode": app_code, "token": token, "type": 0}, headers=header_login, - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, + proxies=Config.get_proxies(), ).json() if rsp["status"] != 0: raise Exception( @@ -188,7 +186,7 @@ def skland_sign_in(token) -> dict: ) return rsp["data"]["code"] - def get_binding_list(cred, sign_token): + async def get_binding_list(cred, sign_token): """ 查询已绑定的角色列表 @@ -202,21 +200,12 @@ def skland_sign_in(token) -> dict: headers=get_sign_header( binding_url, "get", None, copy_header(cred), sign_token ), - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, + proxies=Config.get_proxies(), ).json() if rsp["code"] != 0: - logger.error( - f"森空岛服务 | 请求角色列表出现问题:{rsp['message']}", - module="森空岛签到", - ) + logger.error(f"请求角色列表出现问题:{rsp['message']}") if rsp.get("message") == "用户未登录": - logger.error( - f"森空岛服务 | 用户登录可能失效了,请重新登录!", - module="森空岛签到", - ) + logger.error(f"用户登录可能失效了,请重新登录!") return v # 只取明日方舟(arknights)的绑定账号 for i in rsp["data"]["list"]: @@ -225,7 +214,7 @@ def skland_sign_in(token) -> dict: v.extend(i.get("bindingList")) return v - def do_sign(cred, sign_token) -> dict: + async def do_sign(cred, sign_token) -> dict: """ 对所有绑定的角色进行签到 @@ -234,7 +223,7 @@ def skland_sign_in(token) -> dict: :return: 签到结果字典 """ - characters = get_binding_list(cred, sign_token) + characters = await get_binding_list(cred, sign_token) result = {"成功": [], "重复": [], "失败": [], "总计": len(characters)} for character in characters: @@ -249,10 +238,7 @@ def skland_sign_in(token) -> dict: sign_url, "post", body, copy_header(cred), sign_token ), json=body, - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, + proxies=Config.get_proxies(), ).json() if rsp["code"] != 0: @@ -276,10 +262,10 @@ def skland_sign_in(token) -> dict: # 主流程 try: # 拿到cred和sign_token - cred, sign_token = login_by_token(token) - time.sleep(1) + cred, sign_token = await login_by_token(token) + await asyncio.sleep(1) # 依次签到 - return do_sign(cred, sign_token) + return await do_sign(cred, sign_token) except Exception as e: - logger.exception(f"森空岛服务 | 森空岛签到失败: {e}", module="森空岛签到") + logger.exception(f"森空岛签到失败: {e}") return {"成功": [], "重复": [], "失败": [], "总计": 0} diff --git a/app/utils/ImageUtils.py b/app/utils/ImageUtils.py index 2631dc9..523475f 100644 --- a/app/utils/ImageUtils.py +++ b/app/utils/ImageUtils.py @@ -18,12 +18,6 @@ # Contact: DLmaster_361@163.com -""" -AUTO_MAA -AUTO_MAA图像组件 -v4.4 -作者:ClozyA -""" import base64 import hashlib @@ -56,10 +50,8 @@ class ImageUtils: """ 如果图片大于max_size_mb,则压缩并覆盖原文件,返回原始路径(Path对象) """ - if hasattr(Image, "Resampling"): # Pillow 9.1.0及以后 - RESAMPLE = Image.Resampling.LANCZOS - else: - RESAMPLE = Image.ANTIALIAS + + RESAMPLE = Image.Resampling.LANCZOS # Pillow 9.1.0及以后 max_size = max_size_mb * 1024 * 1024 if image_path.stat().st_size <= max_size: @@ -70,7 +62,7 @@ class ImageUtils: quality = 90 if suffix in [".jpg", ".jpeg"] else None step = 5 - if suffix in [".jpg", ".jpeg"]: + if quality is not None: while True: img.save(image_path, quality=quality, optimize=True) if image_path.stat().st_size <= max_size or quality <= 10: diff --git a/app/utils/ProcessManager.py b/app/utils/ProcessManager.py index d29528f..0ea1a11 100644 --- a/app/utils/ProcessManager.py +++ b/app/utils/ProcessManager.py @@ -19,36 +19,33 @@ # Contact: DLmaster_361@163.com +import asyncio import psutil import subprocess from pathlib import Path -from datetime import datetime - -from PySide6.QtCore import QTimer, QObject, Signal -class ProcessManager(QObject): +class ProcessManager: """进程监视器类,用于跟踪主进程及其所有子进程的状态""" - processClosed = Signal() - def __init__(self): super().__init__() self.main_pid = None self.tracked_pids = set() + self.check_task = None - self.check_timer = QTimer() - self.check_timer.timeout.connect(self.check_processes) - - def open_process(self, path: Path, args: list = [], tracking_time: int = 60) -> int: + async def open_process( + self, path: Path, args: list = [], tracking_time: int = 60 + ) -> None: """ 启动一个新进程并返回其pid,并开始监视该进程 - :param path: 可执行文件的路径 - :param args: 启动参数列表 - :param tracking_time: 子进程追踪持续时间(秒) - :return: 新进程的PID + Parameters + ---------- + path: 可执行文件的路径 + args: 启动参数列表 + tracking_time: 子进程追踪持续时间(秒) """ process = subprocess.Popen( @@ -60,9 +57,9 @@ class ProcessManager(QObject): stderr=subprocess.DEVNULL, ) - self.start_monitoring(process.pid, tracking_time) + await self.start_monitoring(process.pid, tracking_time) - def start_monitoring(self, pid: int, tracking_time: int = 60) -> None: + async def start_monitoring(self, pid: int, tracking_time: int = 60) -> None: """ 启动进程监视器,跟踪指定的主进程及其子进程 @@ -70,7 +67,7 @@ class ProcessManager(QObject): :param tracking_time: 子进程追踪持续时间(秒) """ - self.clear() + await self.clear() self.main_pid = pid self.tracking_time = tracking_time @@ -89,16 +86,16 @@ class ProcessManager(QObject): except psutil.NoSuchProcess: pass - # 启动持续追踪机制 - self.start_time = datetime.now() - self.check_timer.start(100) + # 启动持续追踪任务 + if tracking_time > 0: + self.check_task = asyncio.create_task(self.track_processes()) + await asyncio.sleep(tracking_time) + await self.stop_tracking() - def check_processes(self) -> None: - """检查跟踪的进程是否仍在运行,并更新子进程列表""" - - # 仅在时限内持续更新跟踪的进程列表,发现新的子进程 - if (datetime.now() - self.start_time).total_seconds() < self.tracking_time: + async def track_processes(self) -> None: + """更新子进程列表""" + while True: current_pids = set(self.tracked_pids) for pid in current_pids: try: @@ -109,12 +106,19 @@ class ProcessManager(QObject): self.tracked_pids.add(child.pid) except psutil.NoSuchProcess: continue + await asyncio.sleep(0.1) - if not self.is_running(): - self.clear() - self.processClosed.emit() + async def stop_tracking(self) -> None: + """停止更新子进程列表""" - def is_running(self) -> bool: + if self.check_task and not self.check_task.done(): + self.check_task.cancel() + try: + await self.check_task + except asyncio.CancelledError: + pass + + async def is_running(self) -> bool: """检查所有跟踪的进程是否还在运行""" for pid in self.tracked_pids: @@ -127,11 +131,9 @@ class ProcessManager(QObject): return False - def kill(self, if_force: bool = False) -> None: + async def kill(self, if_force: bool = False) -> None: """停止监视器并中止所有跟踪的进程""" - self.check_timer.stop() - for pid in self.tracked_pids: try: proc = psutil.Process(pid) @@ -145,13 +147,11 @@ class ProcessManager(QObject): except psutil.NoSuchProcess: continue - if self.main_pid: - self.processClosed.emit() - self.clear() + await self.clear() - def clear(self) -> None: + async def clear(self) -> None: """清空跟踪的进程列表""" + await self.stop_tracking() self.main_pid = None - self.check_timer.stop() self.tracked_pids.clear() diff --git a/app/utils/__init__.py b/app/utils/__init__.py index cd7286d..d59c26f 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -25,6 +25,14 @@ __license__ = "GPL-3.0 license" from .logger import get_logger +from .ImageUtils import ImageUtils +from .ProcessManager import ProcessManager from .security import dpapi_encrypt, dpapi_decrypt -__all__ = ["get_logger", "dpapi_encrypt", "dpapi_decrypt"] +__all__ = [ + "get_logger", + "ImageUtils", + "ProcessManager", + "dpapi_encrypt", + "dpapi_decrypt", +] diff --git a/app/utils/logger.py b/app/utils/logger.py index 81d548c..61594c7 100644 --- a/app/utils/logger.py +++ b/app/utils/logger.py @@ -59,6 +59,7 @@ _logger = _logger.patch(lambda record: record["extra"].setdefault("module", "未 def get_logger(module_name: str): """ 获取一个绑定 module 名的日志器 + :param module_name: 模块名称,如 "用户管理" :return: 绑定后的 logger """