diff --git a/app/core/config.py b/app/core/config.py index 02febe3..f642cfe 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -582,6 +582,12 @@ 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 0dc7b88..df74fe5 100644 --- a/app/models/MAA.py +++ b/app/models/MAA.py @@ -38,6 +38,7 @@ 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): @@ -637,6 +638,70 @@ 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 new file mode 100644 index 0000000..9ae8e3a --- /dev/null +++ b/app/services/maa_log_analyzer.py @@ -0,0 +1,123 @@ +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/setting.py b/app/ui/setting.py index d77e775..a9caa1f 100644 --- a/app/ui/setting.py +++ b/app/ui/setting.py @@ -77,6 +77,7 @@ 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) @@ -92,6 +93,7 @@ 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) @@ -797,6 +799,35 @@ 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: """将版本号列表转为可读的文本信息"""