diff --git a/app/core/config.py b/app/core/config.py index f642cfe..6da3346 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -30,6 +30,9 @@ import sqlite3 import json import sys import shutil +import re +from datetime import datetime, timedelta +from collections import defaultdict from pathlib import Path from qfluentwidgets import ( QConfig, @@ -42,6 +45,7 @@ from qfluentwidgets import ( OptionsValidator, qconfig, ) +from typing import Union, Dict, List class AppConfig: @@ -54,7 +58,7 @@ class AppConfig: self.log_path = self.app_path / "debug/AUTO_MAA.log" self.database_path = self.app_path / "data/data.db" self.config_path = self.app_path / "config/config.json" - self.history_path = self.app_path / "config/history.json" + self.history_path = self.app_path / "history/main.json" self.key_path = self.app_path / "data/key" self.gameid_path = self.app_path / "data/gameid.txt" self.version_path = self.app_path / "resources/version.json" @@ -74,6 +78,7 @@ class AppConfig: (self.app_path / "config").mkdir(parents=True, exist_ok=True) (self.app_path / "data").mkdir(parents=True, exist_ok=True) (self.app_path / "debug").mkdir(parents=True, exist_ok=True) + (self.app_path / "history").mkdir(parents=True, exist_ok=True) # 生成版本信息文件 if not self.version_path.exists(): @@ -461,6 +466,206 @@ class AppConfig: cur.close() db.close() + def save_maa_log(self, log_path: Path, logs: list, maa_result: str) -> None: + """保存MAA日志""" + + log_path.parent.mkdir(parents=True, exist_ok=True) + + data: Dict[str, Union[str, Dict[str, Union[int, dict]]]] = { + "recruit_statistics": defaultdict(int), + "drop_statistics": defaultdict(dict), + "maa_result": maa_result, + } + + # 公招统计(仅统计招募到的) + confirmed_recruit = False + current_star_level = None + i = 0 + while i < len(logs): + if "公招识别结果:" in logs[i]: + current_star_level = None # 每次识别公招时清空之前的星级 + i += 1 + while i < len(logs) and "Tags" not in logs[i]: # 读取所有公招标签 + i += 1 + + if i < len(logs) and "Tags" in logs[i]: # 识别星级 + star_match = re.search(r"(\d+)\s*★ Tags", logs[i]) + if star_match: + current_star_level = f"{star_match.group(1)}★" + + if "已确认招募" in logs[i]: # 只有确认招募后才统计 + confirmed_recruit = True + + if confirmed_recruit and current_star_level: + data["recruit_statistics"][current_star_level] += 1 + confirmed_recruit = False # 重置,等待下一次公招 + current_star_level = None # 清空已处理的星级 + + i += 1 + + # 掉落统计 + current_stage = None + stage_drops = {} + + for i, line in enumerate(logs): + drop_match = re.search(r"([A-Za-z0-9\-]+) 掉落统计:", line) + if drop_match: + # 发现新关卡,保存前一个关卡数据 + if current_stage and stage_drops: + data["drop_statistics"][current_stage] = stage_drops + + current_stage = drop_match.group(1) + stage_drops = {} + continue + + if current_stage: + item_match: List[str] = re.findall( + r"([\u4e00-\u9fa5]+)\s*:\s*([\d,]+)(?:\s*\(\+[\d,]+\))?", line + ) + for item, total in item_match: + # 解析数值时去掉逗号 (如 2,160 -> 2160) + total = int(total.replace(",", "")) + + # 黑名单 + if item not in [ + "当前次数", + "理智", + "最快截图耗时", + "专精等级", + "剩余时间", + ]: + stage_drops[item] = total + + # 处理最后一个关卡的掉落数据 + if current_stage and stage_drops: + data["drop_statistics"][current_stage] = stage_drops + + # 保存日志 + with log_path.open("w", encoding="utf-8") as f: + f.writelines(logs) + with log_path.with_suffix(".json").open("w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=4) + + logger.info(f"处理完成:{log_path}") + + self.merge_maa_logs(log_path.parent) + + def merge_maa_logs(self, logs_path: Path) -> None: + """合并所有 .log 文件""" + + data = { + "recruit_statistics": defaultdict(int), + "drop_statistics": defaultdict(dict), + "maa_result": defaultdict(str), + } + + for json_file in logs_path.glob("*.json"): + + with json_file.open("r", encoding="utf-8") as f: + single_data: Dict[str, Union[str, Dict[str, Union[int, dict]]]] = ( + json.load(f) + ) + + # 合并公招统计 + for star_level, count in single_data["recruit_statistics"].items(): + data["recruit_statistics"][star_level] += count + + # 合并掉落统计 + for stage, drops in single_data["drop_statistics"].items(): + if stage not in data["drop_statistics"]: + data["drop_statistics"][stage] = {} # 初始化关卡 + + for item, count in drops.items(): + + if item in data["drop_statistics"][stage]: + data["drop_statistics"][stage][item] += count + else: + data["drop_statistics"][stage][item] = count + + # 合并MAA结果 + data["maa_result"][ + json_file.name.replace(".json", "").replace("-", ":") + ] = single_data["maa_result"] + + # 生成汇总 JSON 文件 + with logs_path.with_suffix(".json").open("w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=4) + + logger.info(f"统计完成:{logs_path.with_suffix(".json")}") + + def load_maa_logs( + self, mode: str, json_path: Path + ) -> Dict[str, Union[str, list, Dict[str, list]]]: + """加载MAA日志统计信息""" + + if mode == "总览": + + with json_path.open("r", encoding="utf-8") as f: + info: Dict[str, Dict[str, Union[int, dict]]] = json.load(f) + + data = {} + data["条目索引"] = [ + [k, "完成" if v == "Success!" else "异常"] + for k, v in info["maa_result"].items() + ] + data["条目索引"].insert(0, ["数据总览", "运行"]) + data["统计数据"] = {"公招统计": list(info["recruit_statistics"].items())} + + for game_id, drops in info["drop_statistics"].items(): + data["统计数据"][f"掉落统计:{game_id}"] = list(drops.items()) + + data["统计数据"]["报错汇总"] = [ + [k, v] for k, v in info["maa_result"].items() if v != "Success!" + ] + + elif mode == "单项": + + with json_path.open("r", encoding="utf-8") as f: + info: Dict[str, Union[str, Dict[str, Union[int, dict]]]] = json.load(f) + + data = {} + + data["统计数据"] = {"公招统计": list(info["recruit_statistics"].items())} + + for game_id, drops in info["drop_statistics"].items(): + data["统计数据"][f"掉落统计:{game_id}"] = list(drops.items()) + + with json_path.with_suffix(".log").open("r", encoding="utf-8") as f: + log = f.read() + + data["日志信息"] = log + + return data + + def search_history(self) -> dict: + """搜索所有历史记录""" + + history_dict = {} + + for date_folder in (Config.app_path / "history").iterdir(): + if not date_folder.is_dir(): + continue # 只处理日期文件夹 + + try: + + date = datetime.strptime(date_folder.name, "%Y-%m-%d") + + history_dict[date.strftime("%Y年 %m月 %d日")] = list( + date_folder.glob("*.json") + ) + + except ValueError: + logger.warning(f"非日期格式的目录: {date_folder}") + + return { + k: v + for k, v in sorted( + history_dict.items(), + key=lambda x: datetime.strptime(x[0], "%Y年 %m月 %d日"), + reverse=True, + ) + } + def save_history(self, key: str, content: dict) -> None: """保存历史记录""" @@ -540,6 +745,9 @@ class AppConfig: class GlobalConfig(QConfig): """全局配置""" + function_HistoryRetentionTime = OptionsConfigItem( + "Function", "HistoryRetentionTime", 7, OptionsValidator([7, 15, 30, 60, 0]) + ) function_IfAllowSleep = ConfigItem( "Function", "IfAllowSleep", False, BoolValidator() ) @@ -582,12 +790,6 @@ class GlobalConfig(QConfig): update_UpdateType = OptionsConfigItem( "Update", "UpdateType", "main", OptionsValidator(["main", "dev"]) ) - # 日志管理 - function_IfEnableLog = ConfigItem("Function", "IfEnableLog", False, BoolValidator()) - function_LogRetentionDays = OptionsConfigItem( - "Function", "LogRetentionDays", "7 天", - OptionsValidator(["7 天", "15 天", "30 天", "60 天", "永不清理"]) - ) class QueueConfig(QConfig): diff --git a/app/models/MAA.py b/app/models/MAA.py index df74fe5..f95ea2c 100644 --- a/app/models/MAA.py +++ b/app/models/MAA.py @@ -38,7 +38,6 @@ from typing import List from app.core import Config from app.services import Notify, System -from app.services.maa_log_analyzer import analyze_maa_logs class MaaManager(QObject): @@ -288,6 +287,14 @@ class MaaManager(QObject): # 移除静默进程标记 Config.silence_list.remove(self.emulator_path) + # 保存运行日志 + Config.save_maa_log( + Config.app_path + / f"history/{curdate}/{self.data[user[2]][0]}/{start_time.strftime("%H-%M-%S")}.log", + self.check_maa_log(start_time, mode_book[j]), + self.maa_result, + ) + # 成功完成代理的用户修改相关参数 if run_book[0] and run_book[1]: if self.data[user[2]][14] == 0 and self.data[user[2]][3] != -1: @@ -531,8 +538,8 @@ class MaaManager(QObject): with self.maa_log_path.open(mode="r", encoding="utf-8") as f: pass - def check_maa_log(self, start_time: datetime, mode: str) -> None: - """检查MAA日志以判断MAA程序运行状态""" + def check_maa_log(self, start_time: datetime, mode: str) -> list: + """获取MAA日志并检查以判断MAA程序运行状态""" # 获取日志 logs = [] @@ -616,6 +623,8 @@ class MaaManager(QObject): self.quit_monitor() + return logs + def start_monitor(self, start_time: datetime, mode: str) -> None: """开始监视MAA日志""" @@ -638,70 +647,6 @@ class MaaManager(QObject): self.log_monitor_timer.stop() self.monitor_loop.quit() - # 检查用户是否开启日志保存功能 - if not Config.global_config.get(Config.global_config.function_IfEnableLog): - logger.info(f"{self.name} | 用户未启用日志保存功能,跳过保存") - return - - # 获取当前运行的用户 - current_user = next((user[0].split(" - ")[0] for user in self.user_list if user[1] == "运行"), "UnknownUser") - - # 新增日志保存功能 - try: - # 获取当前日期和时间 - now = datetime.now() - date_str = now.strftime("%Y-%m-%d") - time_str = now.strftime("%H-%M-%S") - - # 停三秒,保证日志完全写入 - time.sleep(3) - - # 设定日志保存路径:/maa_run_history/{date}/{实例}/{用户}/{time}.log - base_path = Path(f"./maa_run_history/{date_str}/{self.name}/{current_user}") - base_path.mkdir(parents=True, exist_ok=True) # 确保目录存在 - - log_file_path = base_path / f"{time_str}.log" - - # 读取 MAA 运行日志 - with self.maa_log_path.open(mode="r", encoding="utf-8") as f: - logs = f.readlines() - - # **只获取最后一次 MAA 运行日志** - last_start_idx = None - last_exit_idx = None - - # 反向查找最后一个 "MaaAssistantArknights GUI exited" - for i in range(len(logs) - 1, -1, -1): - if "MaaAssistantArknights GUI exited" in logs[i]: - last_exit_idx = i - break - - # 反向查找最近的 "MaaAssistantArknights GUI started" - if last_exit_idx is not None: - for i in range(last_exit_idx, -1, -1): - if "MaaAssistantArknights GUI started" in logs[i]: - last_start_idx = i - break - - # 确保找到了完整的日志片段 - if last_start_idx is not None and last_exit_idx is not None: - relevant_logs = logs[last_start_idx: last_exit_idx + 1] - - # 只保存最后一次的完整日志 - with log_file_path.open(mode="w", encoding="utf-8") as f: - f.writelines(relevant_logs) - - logger.info(f"{self.name} | 运行日志已保存: {log_file_path}") - - # ========== **调用分析函数** ========== - analyze_maa_logs(base_path) - - else: - logger.warning(f"{self.name} | 未找到完整的 MAA 运行日志片段,跳过保存") - - except Exception as e: - logger.error(f"{self.name} | 日志保存失败: {str(e)}") - def set_maa(self, mode, index): """配置MAA运行参数""" logger.info(f"{self.name} | 配置MAA运行参数: {mode}/{index}") diff --git a/app/services/maa_log_analyzer.py b/app/services/maa_log_analyzer.py deleted file mode 100644 index 9ae8e3a..0000000 --- a/app/services/maa_log_analyzer.py +++ /dev/null @@ -1,123 +0,0 @@ -import json -import os -import re -from datetime import datetime, timedelta -from pathlib import Path -from collections import defaultdict -from loguru import logger - -from app.core import Config - - -def analyze_maa_logs(logs_directory: Path): - """ - 遍历 logs_directory 下所有 .log 文件,解析公招和掉落信息,并保存为 JSON 文件 - """ - if not logs_directory.exists(): - logger.error(f"目录不存在: {logs_directory}") - return - - # **检查并删除超期日志** - clean_old_logs(logs_directory) - - # 设定 JSON 输出路径 - json_output_path = logs_directory / f"{logs_directory.name}.json" if logs_directory.parent.name == "maa_run_history" else logs_directory.parent / f"{logs_directory.name}.json" - - aggregated_data = { - # "recruit_statistics": defaultdict(int), - "drop_statistics": defaultdict(lambda: defaultdict(int)), - } - - log_files = list(logs_directory.rglob("*.log")) - if not log_files: - logger.error(f"没有找到 .log 文件: {logs_directory}") - return - - for log_file in log_files: - analyze_single_log(log_file, aggregated_data) - - # 生成 JSON 文件 - with open(json_output_path, "w", encoding="utf-8") as json_file: - json.dump(aggregated_data, json_file, ensure_ascii=False, indent=4) - - logger.info(f"统计完成:{json_output_path}") - -def analyze_single_log(log_file_path: Path, aggregated_data): - """ - 解析单个 .log 文件,提取公招结果 & 关卡掉落数据 - """ - # recruit_data = aggregated_data["recruit_statistics"] - drop_data = aggregated_data["drop_statistics"] - - with open(log_file_path, "r", encoding="utf-8") as f: - logs = f.readlines() - - # # **公招统计** - # i = 0 - # while i < len(logs): - # if "公招识别结果:" in logs[i]: - # tags = [] - # i += 1 - # while i < len(logs) and "Tags" not in logs[i]: # 读取所有公招标签 - # tags.append(logs[i].strip()) - # i += 1 - # - # if i < len(logs) and "Tags" in logs[i]: # 确保 Tags 行存在 - # star_match = re.search(r"(\d+)\s*Tags", logs[i]) # 提取 3,4,5,6 星 - # if star_match: - # star_level = f"{star_match.group(1)}★" - # recruit_data[star_level] += 1 - # i += 1 - - # **掉落统计** - current_stage = None - for i, line in enumerate(logs): - drop_match = re.search(r"(\d+-\d+) 掉落统计:", line) - if drop_match: - current_stage = drop_match.group(1) - continue - - if current_stage and re.search(r"(\S+)\s*:\s*(\d+)\s*\(\+\d+\)", line): - item_match = re.findall(r"(\S+)\s*:\s*(\d+)\s*\(\+(\d+)\)", line) - for item, total, increment in item_match: - drop_data[current_stage][item] += int(increment) - - logger.info(f"处理完成:{log_file_path}") - - -def clean_old_logs(logs_directory: Path): - """ - 删除超过用户设定天数的日志文件 - """ - retention_setting = Config.global_config.get(Config.global_config.function_LogRetentionDays) - retention_days_mapping = { - "7 天": 7, - "15 天": 15, - "30 天": 30, - "60 天": 60, - "永不清理": None - } - - retention_days = retention_days_mapping.get(retention_setting, None) - if retention_days is None: - logger.info("🔵 用户设置日志保留时间为【永不清理】,跳过清理") - return - - cutoff_time = datetime.now() - timedelta(days=retention_days) - - deleted_count = 0 - for log_file in logs_directory.rglob("*.log"): - file_time = datetime.fromtimestamp(log_file.stat().st_mtime) # 获取文件的修改时间 - if file_time < cutoff_time: - try: - os.remove(log_file) - deleted_count += 1 - logger.info(f"🗑️ 已删除超期日志: {log_file}") - except Exception as e: - logger.error(f"❌ 删除日志失败: {log_file}, 错误: {e}") - - logger.info(f"✅ 清理完成: {deleted_count} 个日志文件") - -# # 运行代码 -# logs_directory = Path("") -# analyze_maa_logs(logs_directory) diff --git a/app/ui/Widget.py b/app/ui/Widget.py index 6ac1d6b..620d44c 100644 --- a/app/ui/Widget.py +++ b/app/ui/Widget.py @@ -39,6 +39,10 @@ from qfluentwidgets import ( Signal, ComboBox, CheckBox, + IconWidget, + FluentIcon, + CardWidget, + BodyLabel, qconfig, ConfigItem, TimeEdit, @@ -319,3 +323,53 @@ class TimeEditSettingCard(SettingCard): qconfig.set(self.configItem_time, value) self.TimeEdit.setTime(QTime.fromString(value, "HH:mm")) + + +class StatefulItemCard(CardWidget): + + def __init__(self, item: list, parent=None): + super().__init__(parent) + + self.Layout = QHBoxLayout(self) + + self.Label = BodyLabel(item[0], self) + self.icon = IconWidget(FluentIcon.MORE, self) + self.icon.setFixedSize(16, 16) + self.update_status(item[1]) + + self.Layout.addWidget(self.icon) + self.Layout.addWidget(self.Label) + self.Layout.addStretch(1) + + def update_status(self, status: str): + + if status == "完成": + self.icon.setIcon(FluentIcon.ACCEPT) + self.Label.setTextColor("#0eb840", "#0eb840") + elif status == "等待": + self.icon.setIcon(FluentIcon.MORE) + self.Label.setTextColor("#161823", "#e3f9fd") + elif status == "运行": + self.icon.setIcon(FluentIcon.PLAY) + self.Label.setTextColor("#177cb0", "#70f3ff") + elif status == "跳过": + self.icon.setIcon(FluentIcon.REMOVE) + self.Label.setTextColor("#75878a", "#7397ab") + elif status == "异常": + self.icon.setIcon(FluentIcon.CLOSE) + self.Label.setTextColor("#ff2121", "#ff2121") + + +class QuantifiedItemCard(CardWidget): + + def __init__(self, item: list, parent=None): + super().__init__(parent) + + self.Layout = QHBoxLayout(self) + + self.Name = BodyLabel(item[0], self) + self.Numb = BodyLabel(str(item[1]), self) + + self.Layout.addWidget(self.Name) + self.Layout.addStretch(1) + self.Layout.addWidget(self.Numb) diff --git a/app/ui/dispatch_center.py b/app/ui/dispatch_center.py index fd9b5ec..8810568 100644 --- a/app/ui/dispatch_center.py +++ b/app/ui/dispatch_center.py @@ -34,8 +34,6 @@ from PySide6.QtWidgets import ( ) from qfluentwidgets import ( CardWidget, - IconWidget, - BodyLabel, Pivot, ScrollArea, FluentIcon, @@ -53,6 +51,7 @@ import json from app.core import Config, TaskManager, Task, MainInfoBar +from .Widget import StatefulItemCard class DispatchCenter(QWidget): @@ -335,7 +334,7 @@ class DispatchBox(QWidget): self.viewLayout.addLayout(self.Layout) self.viewLayout.setContentsMargins(3, 0, 3, 3) - self.task_cards: List[ItemCard] = [] + self.task_cards: List[StatefulItemCard] = [] def create_task(self, task_list: list): """创建任务队列""" @@ -351,7 +350,7 @@ class DispatchBox(QWidget): for task in task_list: - self.task_cards.append(ItemCard(task)) + self.task_cards.append(StatefulItemCard(task)) self.Layout.addWidget(self.task_cards[-1]) self.Layout.addStretch(1) @@ -373,7 +372,7 @@ class DispatchBox(QWidget): self.viewLayout.addLayout(self.Layout) self.viewLayout.setContentsMargins(3, 0, 3, 3) - self.user_cards: List[ItemCard] = [] + self.user_cards: List[StatefulItemCard] = [] def create_user(self, user_list: list): """创建用户队列""" @@ -389,7 +388,7 @@ class DispatchBox(QWidget): for user in user_list: - self.user_cards.append(ItemCard(user)) + self.user_cards.append(StatefulItemCard(user)) self.Layout.addWidget(self.user_cards[-1]) self.Layout.addStretch(1) @@ -419,38 +418,3 @@ class DispatchBox(QWidget): self.text.moveCursor(QTextCursor.End) self.text.ensureCursorVisible() - - -class ItemCard(CardWidget): - - def __init__(self, task_item: list, parent=None): - super().__init__(parent) - - self.Layout = QHBoxLayout(self) - - self.Label = BodyLabel(task_item[0], self) - self.icon = IconWidget(FluentIcon.MORE, self) - self.icon.setFixedSize(16, 16) - self.update_status(task_item[1]) - - self.Layout.addWidget(self.icon) - self.Layout.addWidget(self.Label) - self.Layout.addStretch(1) - - def update_status(self, status: str): - - if status == "完成": - self.icon.setIcon(FluentIcon.ACCEPT) - self.Label.setTextColor("#0eb840", "#0eb840") - elif status == "等待": - self.icon.setIcon(FluentIcon.MORE) - self.Label.setTextColor("#7397ab", "#7397ab") - elif status == "运行": - self.icon.setIcon(FluentIcon.PLAY) - self.Label.setTextColor("#2e4e7e", "#2e4e7e") - elif status == "跳过": - self.icon.setIcon(FluentIcon.REMOVE) - self.Label.setTextColor("#606060", "#d2d2d2") - elif status == "异常": - self.icon.setIcon(FluentIcon.CLOSE) - self.Label.setTextColor("#ff2121", "#ff2121") diff --git a/app/ui/history.py b/app/ui/history.py new file mode 100644 index 0000000..f4d7bf3 --- /dev/null +++ b/app/ui/history.py @@ -0,0 +1,264 @@ +# +# Copyright © <2024> + +# 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 . + +# DLmaster_361@163.com + +""" +AUTO_MAA +AUTO_MAA历史记录界面 +v4.2 +作者:DLmaster_361 +""" + +from loguru import logger +from PySide6.QtWidgets import ( + QWidget, + QVBoxLayout, + QHBoxLayout, +) +from qfluentwidgets import ( + ScrollArea, + FluentIcon, + HeaderCardWidget, + PushButton, + ExpandGroupSettingCard, + TextBrowser, +) +from PySide6.QtCore import Signal +import os +from functools import partial +from pathlib import Path +from typing import List + + +from app.core import Config +from .Widget import StatefulItemCard, QuantifiedItemCard + + +class History(QWidget): + + def __init__( + self, + parent=None, + ): + super().__init__(parent) + self.setObjectName("历史记录") + + content_widget = QWidget() + self.content_layout = QVBoxLayout(content_widget) + + scrollArea = ScrollArea() + scrollArea.setWidgetResizable(True) + scrollArea.setWidget(content_widget) + layout = QVBoxLayout() + layout.addWidget(scrollArea) + self.setLayout(layout) + + self.history_card_list = [] + + self.refresh() + + def refresh(self): + """刷新脚本实例界面""" + + while self.content_layout.count() > 0: + item = self.content_layout.takeAt(0) + if item.spacerItem(): + self.content_layout.removeItem(item.spacerItem()) + elif item.widget(): + item.widget().deleteLater() + + self.history_card_list = [] + + history_dict = Config.search_history() + + for date, user_list in history_dict.items(): + + self.history_card_list.append(HistoryCard(date, user_list, self)) + self.content_layout.addWidget(self.history_card_list[-1]) + + self.content_layout.addStretch(1) + + +class HistoryCard(ExpandGroupSettingCard): + + def __init__(self, date: str, user_list: List[Path], parent=None): + super().__init__( + FluentIcon.HISTORY, + date, + f"{date}的历史运行记录与统计信息", + parent, + ) + + widget = QWidget() + Layout = QVBoxLayout(widget) + self.viewLayout.setContentsMargins(0, 0, 0, 0) + self.viewLayout.setSpacing(0) + self.addGroupWidget(widget) + + self.user_history_card_list = [] + + for user_path in user_list: + + self.user_history_card_list.append(self.UserHistoryCard(user_path, self)) + Layout.addWidget(self.user_history_card_list[-1]) + + class UserHistoryCard(HeaderCardWidget): + + def __init__( + self, + user_history_path: Path, + parent=None, + ): + super().__init__(parent) + + self.setTitle(user_history_path.name.replace(".json", "")) + + self.user_history_path = user_history_path + self.main_history = Config.load_maa_logs("总览", user_history_path) + + self.index_card = self.IndexCard(self.main_history["条目索引"], self) + self.statistics_card = QHBoxLayout() + self.log_card = self.LogCard(self) + + self.index_card.index_changed.connect(self.update_info) + + self.viewLayout.addWidget(self.index_card) + self.viewLayout.addLayout(self.statistics_card) + self.viewLayout.addWidget(self.log_card) + self.viewLayout.setContentsMargins(0, 0, 0, 0) + self.viewLayout.setSpacing(0) + self.viewLayout.setStretch(0, 1) + self.viewLayout.setStretch(2, 4) + + self.update_info("数据总览") + + def update_info(self, index: str) -> None: + """更新信息""" + + if index == "数据总览": + + while self.statistics_card.count() > 0: + item = self.statistics_card.takeAt(0) + if item.spacerItem(): + self.statistics_card.removeItem(item.spacerItem()) + elif item.widget(): + item.widget().deleteLater() + + for name, item_list in self.main_history["统计数据"].items(): + + statistics_card = self.StatisticsCard(name, item_list, self) + self.statistics_card.addWidget(statistics_card) + + self.log_card.hide() + + else: + + single_history = Config.load_maa_logs( + "单项", + self.user_history_path.with_suffix("") + / f"{index.replace(":","-")}.json", + ) + + while self.statistics_card.count() > 0: + item = self.statistics_card.takeAt(0) + if item.spacerItem(): + self.statistics_card.removeItem(item.spacerItem()) + elif item.widget(): + item.widget().deleteLater() + + for name, item_list in single_history["统计数据"].items(): + + statistics_card = self.StatisticsCard(name, item_list, self) + self.statistics_card.addWidget(statistics_card) + + self.log_card.text.setText(single_history["日志信息"]) + self.log_card.button.clicked.disconnect() + self.log_card.button.clicked.connect( + lambda: os.startfile( + self.user_history_path.with_suffix("") + / f"{index.replace(":","-")}.log" + ) + ) + self.log_card.show() + + self.viewLayout.setStretch(1, self.statistics_card.count()) + + self.setMinimumHeight(300) + + class IndexCard(HeaderCardWidget): + + index_changed = Signal(str) + + def __init__(self, index_list: list, parent=None): + super().__init__(parent) + self.setTitle("记录条目") + + self.Layout = QVBoxLayout() + self.viewLayout.addLayout(self.Layout) + self.viewLayout.setContentsMargins(3, 0, 3, 3) + + self.index_cards: List[StatefulItemCard] = [] + + for index in index_list: + + self.index_cards.append(StatefulItemCard(index)) + self.index_cards[-1].clicked.connect( + partial(self.index_changed.emit, index[0]) + ) + self.Layout.addWidget(self.index_cards[-1]) + + self.Layout.addStretch(1) + + class StatisticsCard(HeaderCardWidget): + + def __init__(self, name: str, item_list: list, parent=None): + super().__init__(parent) + self.setTitle(name) + + self.Layout = QVBoxLayout() + self.viewLayout.addLayout(self.Layout) + self.viewLayout.setContentsMargins(3, 0, 3, 3) + + self.item_cards: List[QuantifiedItemCard] = [] + + for item in item_list: + + self.item_cards.append(QuantifiedItemCard(item)) + self.Layout.addWidget(self.item_cards[-1]) + + if len(item_list) == 0: + self.Layout.addWidget(QuantifiedItemCard(["暂无记录", ""])) + + self.Layout.addStretch(1) + + class LogCard(HeaderCardWidget): + + def __init__(self, parent=None): + super().__init__(parent) + self.setTitle("日志") + + self.text = TextBrowser(self) + self.button = PushButton("打开日志文件", self) + self.button.clicked.connect(lambda: print("打开日志文件")) + + Layout = QVBoxLayout() + Layout.addWidget(self.text) + Layout.addWidget(self.button) + self.viewLayout.setContentsMargins(3, 0, 3, 3) + self.viewLayout.addLayout(Layout) diff --git a/app/ui/main_window.py b/app/ui/main_window.py index 227db62..a367f43 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -46,6 +46,8 @@ from qfluentwidgets import ( from PySide6.QtGui import QIcon, QCloseEvent from PySide6.QtCore import Qt, QTimer import json +from datetime import datetime, timedelta +import shutil from app.core import Config, TaskManager, MainTimer, MainInfoBar from app.services import Notify, Crypto, System @@ -53,6 +55,7 @@ from .setting import Setting from .member_manager import MemberManager from .queue_manager import QueueManager from .dispatch_center import DispatchCenter +from .history import History class AUTO_MAA(MSFluentWindow): @@ -76,6 +79,7 @@ class AUTO_MAA(MSFluentWindow): self.member_manager = MemberManager(self) self.queue_manager = QueueManager(self) self.dispatch_center = DispatchCenter(self) + self.history = History(self) self.addSubInterface( self.setting, @@ -105,6 +109,13 @@ class AUTO_MAA(MSFluentWindow): FluentIcon.IOT, NavigationItemPosition.TOP, ) + self.addSubInterface( + self.history, + FluentIcon.HISTORY, + "历史记录", + FluentIcon.HISTORY, + NavigationItemPosition.BOTTOM, + ) self.stackedWidget.currentChanged.connect( lambda index: (self.member_manager.refresh() if index == 1 else None) ) @@ -123,6 +134,9 @@ class AUTO_MAA(MSFluentWindow): self.dispatch_center.update_top_bar() if index == 3 else None ) ) + self.stackedWidget.currentChanged.connect( + lambda index: (self.history.refresh() if index == 4 else None) + ) # 创建系统托盘及其菜单 self.tray = QSystemTrayIcon( @@ -197,6 +211,9 @@ class AUTO_MAA(MSFluentWindow): qconfig.load(Config.config_path, Config.global_config) Config.global_config.save() + # 清理旧日志 + self.clean_old_logs() + # 检查密码 self.setting.check_PASSWORD() @@ -247,6 +264,40 @@ class AUTO_MAA(MSFluentWindow): if reason == QSystemTrayIcon.DoubleClick: self.show_ui("显示主窗口") + def clean_old_logs(self): + """ + 删除超过用户设定天数的日志文件(基于目录日期) + """ + + if ( + Config.global_config.get(Config.global_config.function_HistoryRetentionTime) + == 0 + ): + logger.info("由于用户设置日志永久保留,跳过日志清理") + return + + deleted_count = 0 + + for date_folder in (Config.app_path / "history").iterdir(): + if not date_folder.is_dir(): + continue # 只处理日期文件夹 + + try: + # 只检查 `YYYY-MM-DD` 格式的文件夹 + folder_date = datetime.strptime(date_folder.name, "%Y-%m-%d") + if datetime.now() - folder_date > timedelta( + days=Config.global_config.get( + Config.global_config.function_HistoryRetentionTime + ) + ): + shutil.rmtree(date_folder, ignore_errors=True) + deleted_count += 1 + logger.info(f"已删除超期日志目录: {date_folder}") + except ValueError: + logger.warning(f"非日期格式的目录: {date_folder}") + + logger.info(f"清理完成: {deleted_count} 个日期目录") + def start_main_task(self) -> None: """启动主任务""" diff --git a/app/ui/setting.py b/app/ui/setting.py index a9caa1f..dc8eb4f 100644 --- a/app/ui/setting.py +++ b/app/ui/setting.py @@ -77,7 +77,6 @@ class Setting(QWidget): self.function = FunctionSettingCard(self) self.start = StartSettingCard(self) self.ui = UiSettingCard(self) - self.log_settings = LogSettingCard(self) self.notification = NotifySettingCard(self) self.security = SecuritySettingCard(self) self.updater = UpdaterSettingCard(self) @@ -93,7 +92,6 @@ class Setting(QWidget): content_layout.addWidget(self.function) content_layout.addWidget(self.start) content_layout.addWidget(self.ui) - content_layout.addWidget(self.log_settings) content_layout.addWidget(self.notification) content_layout.addWidget(self.security) content_layout.addWidget(self.updater) @@ -419,6 +417,13 @@ class FunctionSettingCard(HeaderCardWidget): super().__init__(parent) self.setTitle("功能") + self.card_HistoryRetentionTime = ComboBoxSettingCard( + configItem=Config.global_config.function_HistoryRetentionTime, + icon=FluentIcon.PAGE_RIGHT, + title="历史记录保留时间", + content="选择历史记录的保留时间,超期自动清理", + texts=["7 天", "15 天", "30 天", "60 天", "永久"], + ) self.card_IfAllowSleep = SwitchSettingCard( icon=FluentIcon.PAGE_RIGHT, title="启动时阻止系统休眠", @@ -434,6 +439,7 @@ class FunctionSettingCard(HeaderCardWidget): ) Layout = QVBoxLayout() + Layout.addWidget(self.card_HistoryRetentionTime) Layout.addWidget(self.card_IfAllowSleep) Layout.addWidget(self.card_IfSilence) Layout.addWidget(self.card_IfAgreeBilibili) @@ -799,35 +805,6 @@ class OtherSettingCard(HeaderCardWidget): self.viewLayout.setSpacing(0) self.addGroupWidget(widget) -class LogSettingCard(HeaderCardWidget): - """日志管理设置""" - - def __init__(self, parent=None): - super().__init__(parent) - self.setTitle("日志管理") - - # 日志存储开关 - self.card_IfEnableLog = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="启用日志存储", - content="记录并存储每次运行的日志,用于多账号日常掉落统计", - configItem=Config.global_config.function_IfEnableLog, - ) - - # 日志保留天数设置 - self.card_LogRetentionDays = ComboBoxSettingCard( - configItem=Config.global_config.function_LogRetentionDays, - icon=FluentIcon.PAGE_RIGHT, - title="日志保留天数", - content="选择日志的保留时间,超期自动清理", - texts=["7 天", "15 天", "30 天", "60 天", "永不清理"], - ) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_IfEnableLog) - Layout.addWidget(self.card_LogRetentionDays) - self.viewLayout.addLayout(Layout) - def version_text(version_numb: list) -> str: """将版本号列表转为可读的文本信息""" diff --git a/main.py b/main.py index 6e557e4..ddc8e64 100644 --- a/main.py +++ b/main.py @@ -32,7 +32,7 @@ from qfluentwidgets import FluentTranslator import sys -@logger.catch +# @logger.catch def main(): application = QApplication(sys.argv) diff --git a/resources/version.json b/resources/version.json index 459304c..167e750 100644 --- a/resources/version.json +++ b/resources/version.json @@ -1,7 +1,7 @@ { - "main_version": "4.2.4.1", + "main_version": "4.2.4.2", "updater_version": "1.1.2.0", - "announcement": "\n## 新增功能\n- 暂无\n## 修复BUG\n- 更新器修正`channel`\n## 程序优化\n- 添加MAA监测字段:`未检测到任何模拟器`\n- 取消MAA运行中自动更新", + "announcement": "\n## 新增功能\n- 历史记录统计功能上线\n## 修复BUG\n- 更新器修正`channel`\n## 程序优化\n- 添加MAA监测字段:`未检测到任何模拟器`\n- 取消MAA运行中自动更新", "proxy_list": [ "", "https://gitproxy.click/",