feat(core): 新增日志管理功能
- 在配置文件中添加日志保存和保留天数设置项 - 实现日志保存功能,每次运行后保存日志到指定目录 - 添加日志分析功能,掉落信息并保存为 JSON 文件 - 在设置界面新增日志管理相关配置选项 todo: 日志清理可能有问题、多账号日志可能会保存为上一个账号的日志(加了time.sleep还没测)
This commit is contained in:
@@ -582,6 +582,12 @@ class GlobalConfig(QConfig):
|
|||||||
update_UpdateType = OptionsConfigItem(
|
update_UpdateType = OptionsConfigItem(
|
||||||
"Update", "UpdateType", "main", OptionsValidator(["main", "dev"])
|
"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):
|
class QueueConfig(QConfig):
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from typing import List
|
|||||||
|
|
||||||
from app.core import Config
|
from app.core import Config
|
||||||
from app.services import Notify, System
|
from app.services import Notify, System
|
||||||
|
from app.services.maa_log_analyzer import analyze_maa_logs
|
||||||
|
|
||||||
|
|
||||||
class MaaManager(QObject):
|
class MaaManager(QObject):
|
||||||
@@ -637,6 +638,70 @@ class MaaManager(QObject):
|
|||||||
self.log_monitor_timer.stop()
|
self.log_monitor_timer.stop()
|
||||||
self.monitor_loop.quit()
|
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):
|
def set_maa(self, mode, index):
|
||||||
"""配置MAA运行参数"""
|
"""配置MAA运行参数"""
|
||||||
logger.info(f"{self.name} | 配置MAA运行参数: {mode}/{index}")
|
logger.info(f"{self.name} | 配置MAA运行参数: {mode}/{index}")
|
||||||
|
|||||||
123
app/services/maa_log_analyzer.py
Normal file
123
app/services/maa_log_analyzer.py
Normal file
@@ -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)
|
||||||
@@ -77,6 +77,7 @@ class Setting(QWidget):
|
|||||||
self.function = FunctionSettingCard(self)
|
self.function = FunctionSettingCard(self)
|
||||||
self.start = StartSettingCard(self)
|
self.start = StartSettingCard(self)
|
||||||
self.ui = UiSettingCard(self)
|
self.ui = UiSettingCard(self)
|
||||||
|
self.log_settings = LogSettingCard(self)
|
||||||
self.notification = NotifySettingCard(self)
|
self.notification = NotifySettingCard(self)
|
||||||
self.security = SecuritySettingCard(self)
|
self.security = SecuritySettingCard(self)
|
||||||
self.updater = UpdaterSettingCard(self)
|
self.updater = UpdaterSettingCard(self)
|
||||||
@@ -92,6 +93,7 @@ class Setting(QWidget):
|
|||||||
content_layout.addWidget(self.function)
|
content_layout.addWidget(self.function)
|
||||||
content_layout.addWidget(self.start)
|
content_layout.addWidget(self.start)
|
||||||
content_layout.addWidget(self.ui)
|
content_layout.addWidget(self.ui)
|
||||||
|
content_layout.addWidget(self.log_settings)
|
||||||
content_layout.addWidget(self.notification)
|
content_layout.addWidget(self.notification)
|
||||||
content_layout.addWidget(self.security)
|
content_layout.addWidget(self.security)
|
||||||
content_layout.addWidget(self.updater)
|
content_layout.addWidget(self.updater)
|
||||||
@@ -797,6 +799,35 @@ class OtherSettingCard(HeaderCardWidget):
|
|||||||
self.viewLayout.setSpacing(0)
|
self.viewLayout.setSpacing(0)
|
||||||
self.addGroupWidget(widget)
|
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:
|
def version_text(version_numb: list) -> str:
|
||||||
"""将版本号列表转为可读的文本信息"""
|
"""将版本号列表转为可读的文本信息"""
|
||||||
|
|||||||
Reference in New Issue
Block a user