diff --git a/app/api/dispatch.py b/app/api/dispatch.py index 8961426..3a9fa98 100644 --- a/app/api/dispatch.py +++ b/app/api/dispatch.py @@ -20,19 +20,25 @@ # Contact: DLmaster_361@163.com -from fastapi import APIRouter, Body +import uuid +from fastapi import APIRouter, WebSocket, Body, Path -from app.core import Config +from app.core import Config, TaskManager from app.models.schema import * router = APIRouter(prefix="/api/dispatch", tags=["任务调度"]) -@router.post("/add", summary="添加任务", response_model=OutBase, status_code=200) -async def add_plan(plan: DispatchIn = Body(...)) -> OutBase: +@router.post( + "/start", summary="添加任务", response_model=TaskCreateOut, status_code=200 +) +async def add_plan(task: TaskCreateIn = Body(...)) -> TaskCreateOut: - uid, config = await Config.add_plan(plan.type) - return OutBase(code=200, status="success", message="任务添加成功") + try: + task_id = await TaskManager.add_task(task.mode, task.taskId) + except Exception as e: + return TaskCreateOut(code=500, status="error", message=str(e), taskId="") + return TaskCreateOut(taskId=str(task_id)) @router.post("/stop", summary="中止任务", response_model=OutBase, status_code=200) @@ -45,13 +51,18 @@ async def stop_plan(plan: DispatchIn = Body(...)) -> OutBase: return OutBase() -@router.post( - "/order", summary="重新排序计划表", response_model=OutBase, status_code=200 -) -async def reorder_plan(plan: PlanReorderIn = Body(...)) -> OutBase: - +@router.websocket("/ws/{taskId}") +async def websocket_endpoint( + websocket: WebSocket, taskId: str = Path(..., description="要连接的任务ID") +): try: - await Config.reorder_plan(plan.indexList) - except Exception as e: - return OutBase(code=500, status="error", message=str(e)) - return OutBase() + uid = uuid.UUID(taskId) + except ValueError: + await websocket.close(code=1008, reason="无效的任务ID") + return + + if uid in TaskManager.connection_events: + TaskManager.websocket_dict[uid] = websocket + TaskManager.connection_events[uid].set() + else: + await websocket.close(code=1008, reason="任务不存在或已结束") diff --git a/app/core/__init__.py b/app/core/__init__.py index 1e95f41..7d03561 100644 --- a/app/core/__init__.py +++ b/app/core/__init__.py @@ -23,7 +23,15 @@ __version__ = "5.0.0" __author__ = "DLmaster361 " __license__ = "GPL-3.0 license" -from .config import Config +from .config import Config, MaaConfig, GeneralConfig, MaaUserConfig from .timer import MainTimer +from .task_manager import TaskManager -__all__ = ["Config", "MainTimer"] +__all__ = [ + "Config", + "MaaConfig", + "GeneralConfig", + "MainTimer", + "TaskManager", + "MaaUserConfig", +] diff --git a/app/core/config.py b/app/core/config.py index 564ce01..4d2e32e 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -20,20 +20,18 @@ # Contact: DLmaster_361@163.com -import sqlite3 -import json -import sys -import shutil import re -import base64 +import shutil +import asyncio import requests import truststore import calendar from datetime import datetime, timedelta, date from pathlib import Path +from collections import defaultdict -from typing import Union, Dict, List, Literal, Optional, Any, Tuple, Callable, TypeVar +from typing import Dict, List, Literal, Optional, Any from app.utils import get_logger from app.models.ConfigBase import * @@ -546,7 +544,7 @@ class GeneralConfig(ConfigBase): class AppConfig(GlobalConfig): - VERSION = "4.5.0.1" + VERSION = "5.0.0.1" CLASS_BOOK = { "MAA": MaaConfig, @@ -580,15 +578,11 @@ class AppConfig(GlobalConfig): def __init__(self) -> None: super().__init__(if_save_multi_config=False) - self.root_path = Path.cwd() + self.log_path = Path.cwd() / "debug/app.log" + self.database_path = Path.cwd() / "data/data.db" + self.config_path = Path.cwd() / "config" + self.key_path = Path.cwd() / "data/key" - self.log_path = self.root_path / "debug/app.log" - self.database_path = self.root_path / "data/data.db" - self.config_path = self.root_path / "config" - self.key_path = self.root_path / "data/key" - - # self.PASSWORD = "" - self.running_list = [] self.silence_dict: Dict[Path, datetime] = {} self.power_sign = "NoAction" self.if_ignore_silence = False @@ -597,7 +591,7 @@ class AppConfig(GlobalConfig): logger.info("===================================") logger.info("AUTO_MAA 后端应用程序") logger.info(f"版本号: v{self.VERSION}") - logger.info(f"根目录: {self.root_path}") + logger.info(f"工作目录: {Path.cwd()}") logger.info("===================================") # 检查目录 @@ -618,6 +612,10 @@ class AppConfig(GlobalConfig): await self.PlanConfig.connect(self.config_path / "PlanConfig.json") await self.QueueConfig.connect(self.config_path / "QueueConfig.json") + from .task_manager import TaskManager + + self.task_dict = TaskManager.task_dict + # self.check_data() logger.info("程序初始化完成") @@ -653,6 +651,11 @@ class AppConfig(GlobalConfig): uid = uuid.UUID(script_id) + if uid in self.task_dict: + raise RuntimeError( + f"Cannot update script {script_id} while tasks are running." + ) + for group, items in data.items(): for name, value in items.items(): logger.debug(f"更新脚本配置:{script_id} - {group}.{name} = {value}") @@ -665,7 +668,14 @@ class AppConfig(GlobalConfig): logger.info(f"删除脚本配置:{script_id}") - await self.ScriptConfig.remove(uuid.UUID(script_id)) + uid = uuid.UUID(script_id) + + if uid in self.task_dict: + raise RuntimeError( + f"Cannot delete script {script_id} while tasks are running." + ) + + await self.ScriptConfig.remove(uid) async def reorder_script(self, index_list: list[str]) -> None: """重新排序脚本""" @@ -1058,5 +1068,343 @@ class AppConfig(GlobalConfig): "Cannot connect to the notice server. Please check your network connection or try again later." ) + async def save_maa_log(self, log_path: Path, logs: list, maa_result: str) -> bool: + """ + 保存MAA日志并生成对应统计数据 + + :param log_path: 日志文件保存路径 + :type log_path: Path + :param logs: 日志内容列表 + :type logs: list + :param maa_result: MAA 结果 + :type maa_result: str + :return: 是否包含6★招募 + :rtype: bool + """ + + logger.info(f"开始处理 MAA 日志,日志长度: {len(logs)},日志标记:{maa_result}") + + data = { + "recruit_statistics": defaultdict(int), + "drop_statistics": defaultdict(dict), + "maa_result": maa_result, + } + + if_six_star = False + + # 公招统计(仅统计招募到的) + 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 current_star_level == "6★": + if_six_star = True + + 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 + + # 掉落统计 + # 存储所有关卡的掉落统计 + all_stage_drops = {} + + # 查找所有Fight任务的开始和结束位置 + fight_tasks = [] + for i, line in enumerate(logs): + if "开始任务: Fight" in line or "开始任务: 刷理智" in line: + # 查找对应的任务结束位置 + end_index = -1 + for j in range(i + 1, len(logs)): + if "完成任务: Fight" in logs[j] or "完成任务: 刷理智" in logs[j]: + end_index = j + break + # 如果遇到新的Fight任务开始,则当前任务没有正常结束 + if j < len(logs) and ( + "开始任务: Fight" in logs[j] or "开始任务: 刷理智" in logs[j] + ): + break + + # 如果找到了结束位置,记录这个任务的范围 + if end_index != -1: + fight_tasks.append((i, end_index)) + + # 处理每个Fight任务 + for start_idx, end_idx in fight_tasks: + # 提取当前任务的日志 + task_logs = logs[start_idx : end_idx + 1] + + # 查找任务中的最后一次掉落统计 + last_drop_stats = {} + current_stage = None + + for line in task_logs: + # 匹配掉落统计行,如"1-7 掉落统计:" + drop_match = re.search(r"([A-Za-z0-9\-]+) 掉落统计:", line) + if drop_match: + # 发现新的掉落统计,重置当前关卡的掉落数据 + current_stage = drop_match.group(1) + last_drop_stats = {} + continue + + # 如果已经找到了关卡,处理掉落物 + if current_stage: + item_match: List[str] = re.findall( + r"^(?!\[)(\S+?)\s*:\s*([\d,]+)(?:\s*\(\+[\d,]+\))?", + line, + re.M, + ) + for item, total in item_match: + # 解析数值时去掉逗号 (如 2,160 -> 2160) + total = int(total.replace(",", "")) + + # 黑名单 + if item not in [ + "当前次数", + "理智", + "最快截图耗时", + "专精等级", + "剩余时间", + ]: + last_drop_stats[item] = total + + # 如果任务中有掉落统计,更新总统计 + if current_stage and last_drop_stats: + if current_stage not in all_stage_drops: + all_stage_drops[current_stage] = {} + + # 累加掉落数据 + for item, count in last_drop_stats.items(): + all_stage_drops[current_stage].setdefault(item, 0) + all_stage_drops[current_stage][item] += count + + # 将累加后的掉落数据保存到结果中 + data["drop_statistics"] = all_stage_drops + + # 保存日志 + log_path.parent.mkdir(parents=True, exist_ok=True) + 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.success( + f"MAA 日志统计完成,日志路径:{log_path}", + ) + + return if_six_star + + async def save_general_log( + self, log_path: Path, logs: list, general_result: str + ) -> None: + """ + 保存通用日志并生成对应统计数据 + + :param log_path: 日志文件保存路径 + :param logs: 日志内容列表 + :param general_result: 待保存的日志结果信息 + """ + + logger.info( + f"开始处理通用日志,日志长度: {len(logs)},日志标记:{general_result}" + ) + + data: Dict[str, str] = {"general_result": general_result} + + # 保存日志 + log_path.parent.mkdir(parents=True, exist_ok=True) + with log_path.with_suffix(".log").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.success(f"通用日志统计完成,日志路径:{log_path.with_suffix('.log')}") + + def merge_statistic_info(self, statistic_path_list: List[Path]) -> dict: + """ + 合并指定数据统计信息文件 + + :param statistic_path_list: 需要合并的统计信息文件路径列表 + :return: 合并后的统计信息字典 + """ + + logger.info(f"开始合并统计信息文件,共计 {len(statistic_path_list)} 个文件") + + data: Dict[str, Any] = {"index": {}} + + for json_file in statistic_path_list: + + with json_file.open("r", encoding="utf-8") as f: + single_data = json.load(f) + + for key in single_data.keys(): + + if key not in data: + data[key] = {} + + # 合并公招统计 + if key == "recruit_statistics": + + for star_level, count in single_data[key].items(): + if star_level not in data[key]: + data[key][star_level] = 0 + data[key][star_level] += count + + # 合并掉落统计 + elif key == "drop_statistics": + + for stage, drops in single_data[key].items(): + if stage not in data[key]: + data[key][stage] = {} # 初始化关卡 + + for item, count in drops.items(): + + if item not in data[key][stage]: + data[key][stage][item] = 0 + data[key][stage][item] += count + + # 录入运行结果 + elif key in ["maa_result", "general_result"]: + + actual_date = datetime.strptime( + f"{json_file.parent.parent.name} {json_file.stem}", + "%Y-%m-%d %H-%M-%S", + ) + timedelta( + days=( + 1 + if datetime.strptime(json_file.stem, "%H-%M-%S").time() + < datetime.min.time().replace(hour=4) + else 0 + ) + ) + + if single_data[key] != "Success!": + if "error_info" not in data: + data["error_info"] = {} + data["error_info"][actual_date.strftime("%d日 %H:%M:%S")] = ( + single_data[key] + ) + + data["index"][actual_date] = [ + actual_date.strftime("%d日 %H:%M:%S"), + ("完成" if single_data[key] == "Success!" else "异常"), + json_file, + ] + + data["index"] = [data["index"][_] for _ in sorted(data["index"])] + + logger.success( + f"统计信息合并完成,共计 {len(data['index'])} 条记录", + ) + + return {k: v for k, v in data.items() if v} + + def search_history( + self, mode: str, start_date: datetime, end_date: datetime + ) -> dict: + """ + 搜索指定范围内的历史记录 + + :param mode: 合并模式(按日合并、按周合并、按月合并) + :param start_date: 开始日期 + :param end_date: 结束日期 + :return: 搜索到的历史记录字典 + """ + + logger.info( + f"开始搜索历史记录,合并模式:{mode},日期范围:{start_date} 至 {end_date}" + ) + + history_dict = {} + + for date_folder in (Path.cwd() / "history").iterdir(): + if not date_folder.is_dir(): + continue # 只处理日期文件夹 + + try: + + date = datetime.strptime(date_folder.name, "%Y-%m-%d") + + if not (start_date <= date <= end_date): + continue # 只统计在范围内的日期 + + if mode == "按日合并": + date_name = date.strftime("%Y年 %m月 %d日") + elif mode == "按周合并": + year, week, _ = date.isocalendar() + date_name = f"{year}年 第{week}周" + elif mode == "按月合并": + date_name = date.strftime("%Y年 %m月") + + if date_name not in history_dict: + history_dict[date_name] = {} + + for user_folder in date_folder.iterdir(): + if not user_folder.is_dir(): + continue # 只处理用户文件夹 + + if user_folder.stem not in history_dict[date_name]: + history_dict[date_name][user_folder.stem] = list( + user_folder.with_suffix("").glob("*.json") + ) + else: + history_dict[date_name][user_folder.stem] += list( + user_folder.with_suffix("").glob("*.json") + ) + + except ValueError: + logger.warning(f"非日期格式的目录: {date_folder}") + + logger.success(f"历史记录搜索完成,共计 {len(history_dict)} 条记录") + + return { + k: v + for k, v in sorted(history_dict.items(), key=lambda x: x[0], reverse=True) + } + + def clean_old_history(self): + """删除超过用户设定天数的历史记录文件(基于目录日期)""" + + if self.get("Function", "HistoryRetentionTime") == 0: + logger.info("历史记录永久保留,跳过历史记录清理") + return + + logger.info("开始清理超过设定天数的历史记录") + + deleted_count = 0 + + for date_folder in (Path.cwd() / "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=self.get("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.success(f"清理完成: {deleted_count} 个日期目录") + Config = AppConfig() diff --git a/app/core/task_manager.py b/app/core/task_manager.py index adbfd60..cf734b0 100644 --- a/app/core/task_manager.py +++ b/app/core/task_manager.py @@ -20,242 +20,30 @@ import uuid -from datetime import datetime -from packaging import version -from typing import Dict, Union +import asyncio +from fastapi import WebSocket +from typing import Dict, Optional -from .config import Config, MaaConfig, GeneralConfig -from utils import get_logger -from task import * +from .config import Config, MaaConfig, GeneralConfig, QueueConfig +from app.models.schema import TaskMessage +from app.utils import get_logger +from app.task import * logger = get_logger("业务调度") -class Task: - """业务线程""" - - check_maa_version = Signal(str) - push_info_bar = Signal(str, str, str, int) - play_sound = Signal(str) - question = Signal(str, str) - question_response = Signal(bool) - update_maa_user_info = Signal(str, dict) - update_general_sub_info = Signal(str, dict) - create_task_list = Signal(list) - create_user_list = Signal(list) - update_task_list = Signal(list) - update_user_list = Signal(list) - update_log_text = Signal(str) - accomplish = Signal(list) - - def __init__( - self, mode: str, name: str, info: Dict[str, Dict[str, Union[str, int, bool]]] - ): - super(Task, self).__init__() - - self.setObjectName(f"Task-{mode}-{name}") - - self.mode = mode - self.name = name - self.info = info - - self.logs = [] - - self.question_response.connect(lambda: print("response")) - - @logger.catch - def run(self): - - if "设置MAA" in self.mode: - - logger.info(f"任务开始:设置{self.name}", module=f"业务 {self.name}") - self.push_info_bar.emit("info", "设置MAA", self.name, 3000) - - self.task = MaaManager( - self.mode, - Config.script_dict[self.name], - (None if "全局" in self.mode else self.info["SetMaaInfo"]["Path"]), - ) - self.task.check_maa_version.connect(self.check_maa_version.emit) - self.task.push_info_bar.connect(self.push_info_bar.emit) - self.task.play_sound.connect(self.play_sound.emit) - self.task.accomplish.connect(lambda: self.accomplish.emit([])) - - try: - self.task.run() - except Exception as e: - logger.exception( - f"任务异常:{self.name},错误信息:{e}", module=f"业务 {self.name}" - ) - self.push_info_bar.emit("error", "任务异常", self.name, -1) - - elif self.mode == "设置通用脚本": - - logger.info(f"任务开始:设置{self.name}", module=f"业务 {self.name}") - self.push_info_bar.emit("info", "设置通用脚本", self.name, 3000) - - self.task = GeneralManager( - self.mode, - Config.script_dict[self.name], - self.info["SetSubInfo"]["Path"], - ) - self.task.push_info_bar.connect(self.push_info_bar.emit) - self.task.play_sound.connect(self.play_sound.emit) - self.task.accomplish.connect(lambda: self.accomplish.emit([])) - - try: - self.task.run() - except Exception as e: - logger.exception( - f"任务异常:{self.name},错误信息:{e}", module=f"业务 {self.name}" - ) - self.push_info_bar.emit("error", "任务异常", self.name, -1) - - else: - - logger.info(f"任务开始:{self.name}", module=f"业务 {self.name}") - self.task_list = [ - [ - ( - value - if Config.script_dict[value]["Config"].get_name() == "" - else f"{value} - {Config.script_dict[value]["Config"].get_name()}" - ), - "等待", - value, - ] - for _, value in sorted( - self.info["Queue"].items(), key=lambda x: int(x[0][7:]) - ) - if value != "禁用" - ] - - self.create_task_list.emit(self.task_list) - - for task in self.task_list: - - if self.isInterruptionRequested(): - break - - task[1] = "运行" - self.update_task_list.emit(self.task_list) - - # 检查任务是否在运行列表中 - if task[2] in Config.running_list: - - task[1] = "跳过" - self.update_task_list.emit(self.task_list) - logger.info( - f"跳过任务:{task[0]},该任务已在运行列表中", - module=f"业务 {self.name}", - ) - self.push_info_bar.emit("info", "跳过任务", task[0], 3000) - continue - - # 标记为运行中 - Config.running_list.append(task[2]) - logger.info(f"任务开始:{task[0]}", module=f"业务 {self.name}") - self.push_info_bar.emit("info", "任务开始", task[0], 3000) - - if Config.script_dict[task[2]]["Type"] == "Maa": - - self.task = MaaManager( - self.mode[0:4], - Config.script_dict[task[2]], - ) - - self.task.check_maa_version.connect(self.check_maa_version.emit) - self.task.question.connect(self.question.emit) - self.question_response.disconnect() - self.question_response.connect(self.task.question_response.emit) - self.task.push_info_bar.connect(self.push_info_bar.emit) - self.task.play_sound.connect(self.play_sound.emit) - self.task.create_user_list.connect(self.create_user_list.emit) - self.task.update_user_list.connect(self.update_user_list.emit) - self.task.update_log_text.connect(self.update_log_text.emit) - self.task.update_user_info.connect(self.update_maa_user_info.emit) - self.task.accomplish.connect( - lambda log: self.task_accomplish(task[2], log) - ) - - elif Config.script_dict[task[2]]["Type"] == "General": - - self.task = GeneralManager( - self.mode[0:4], - Config.script_dict[task[2]], - ) - - self.task.question.connect(self.question.emit) - self.question_response.disconnect() - self.question_response.connect(self.task.question_response.emit) - self.task.push_info_bar.connect(self.push_info_bar.emit) - self.task.play_sound.connect(self.play_sound.emit) - self.task.create_user_list.connect(self.create_user_list.emit) - self.task.update_user_list.connect(self.update_user_list.emit) - self.task.update_log_text.connect(self.update_log_text.emit) - self.task.update_sub_info.connect(self.update_general_sub_info.emit) - self.task.accomplish.connect( - lambda log: self.task_accomplish(task[2], log) - ) - - try: - self.task.run() # 运行任务业务 - - task[1] = "完成" - self.update_task_list.emit(self.task_list) - logger.info(f"任务完成:{task[0]}", module=f"业务 {self.name}") - self.push_info_bar.emit("info", "任务完成", task[0], 3000) - - except Exception as e: - - self.task_accomplish( - task[2], - { - "Time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "History": f"任务异常,异常简报:{e}", - }, - ) - - task[1] = "异常" - self.update_task_list.emit(self.task_list) - logger.exception( - f"任务异常:{task[0]},错误信息:{e}", - module=f"业务 {self.name}", - ) - self.push_info_bar.emit("error", "任务异常", task[0], -1) - - # 任务结束后从运行列表中移除 - Config.running_list.remove(task[2]) - - self.accomplish.emit(self.logs) - - def task_accomplish(self, name: str, log: dict): - """ - 销毁任务线程并保存任务结果 - - :param name: 任务名称 - :param log: 任务日志记录 - """ - - logger.info( - f"任务完成:{name},日志记录:{list(log.values())}", - module=f"业务 {self.name}", - ) - - self.logs.append([name, log]) - self.task.deleteLater() - - class _TaskManager: """业务调度器""" def __init__(self): - super(_TaskManager, self).__init__() + super().__init__() - self.task_dict: Dict[str, Task] = {} + self.task_dict: Dict[uuid.UUID, asyncio.Task] = {} + self.connection_events: Dict[uuid.UUID, asyncio.Event] = {} + self.websocket_dict: Dict[uuid.UUID, WebSocket] = {} - def add_task(self, mode: str, uid: str): + async def add_task(self, mode: str, uid: str) -> uuid.UUID: """ 添加任务 @@ -268,6 +56,7 @@ class _TaskManager: if mode == "设置脚本": if actual_id in Config.ScriptConfig: task_id = actual_id + actual_id = None else: for script_id, script in Config.ScriptConfig.items(): if ( @@ -280,128 +69,192 @@ class _TaskManager: raise ValueError( f"The task corresponding to UID {uid} could not be found." ) - elif actual_id in Config.QueueConfig or actual_id in Config.ScriptConfig: + elif actual_id in Config.QueueConfig: task_id = actual_id + actual_id = None + elif actual_id in Config.ScriptConfig: + task_id = uuid.uuid4() else: raise ValueError(f"The task corresponding to UID {uid} could not be found.") - if name in Config.running_list or name in self.task_dict: + if task_id in self.task_dict: - logger.warning(f"任务已存在:{name}") - MainInfoBar.push_info_bar("warning", "任务已存在", name, 5000) - return None + raise RuntimeError(f"The task {task_id} is already running.") - logger.info(f"任务开始:{name},模式:{mode}", module="业务调度") - MainInfoBar.push_info_bar("info", "任务开始", name, 3000) - SoundPlayer.play("任务开始") - - # 标记任务为运行中 - Config.running_list.append(name) + logger.info(f"创建任务:{task_id},模式:{mode}") # 创建任务实例并连接信号 - self.task_dict[name] = Task(mode, name, info) - self.task_dict[name].check_maa_version.connect(self.check_maa_version) - self.task_dict[name].question.connect( - lambda title, content: self.push_dialog(name, title, content) + if task_id in self.connection_events: + self.connection_events[task_id].clear() + else: + self.connection_events[task_id] = asyncio.Event() + + await self.connection_events[task_id].wait() + + if task_id not in self.websocket_dict: + raise RuntimeError(f"The task {task_id} is not connected to a WebSocket.") + + logger.info(f"开始运行任务:{task_id},模式:{mode}") + + self.task_dict[task_id] = asyncio.create_task( + self.run_task(mode, task_id, actual_id) ) - self.task_dict[name].push_info_bar.connect(MainInfoBar.push_info_bar) - self.task_dict[name].play_sound.connect(SoundPlayer.play) - self.task_dict[name].update_maa_user_info.connect(Config.change_maa_user_info) - self.task_dict[name].update_general_sub_info.connect( - Config.change_general_sub_info - ) - self.task_dict[name].accomplish.connect( - lambda logs: self.remove_task(mode, name, logs) + self.task_dict[task_id].add_done_callback( + lambda t: asyncio.create_task(self.remove_task(t, mode, task_id)) ) - # 向UI发送信号以创建或连接GUI - if "新调度台" in mode: - self.create_gui.emit(self.task_dict[name]) + return task_id - elif "主调度台" in mode: - self.connect_gui.emit(self.task_dict[name]) + @logger.catch + async def run_task( + self, mode: str, task_id: uuid.UUID, actual_id: Optional[uuid.UUID] + ): - # 启动任务线程 - self.task_dict[name].start() + websocket = self.websocket_dict[task_id] - def stop_task(self, name: str) -> None: + if mode == "设置脚本": + + if isinstance(Config.ScriptConfig[task_id], MaaConfig): + task_item = MaaManager(mode, task_id, actual_id, websocket) + # elif isinstance(Config.ScriptConfig[task_id], GeneralConfig): + # task_item = GeneralManager(mode, task_id, actual_id, websocket) + else: + logger.error( + f"不支持的脚本类型:{Config.ScriptConfig[task_id].__class__.__name__}" + ) + await websocket.send_json( + TaskMessage( + type="Info", data={"Error": "脚本类型不支持"} + ).model_dump() + ) + return + + task = asyncio.create_task(task_item.run()) + task.add_done_callback( + lambda t: asyncio.create_task(task_item.final_task(t)) + ) + + else: + + if task_id in Config.QueueConfig: + + queue = Config.QueueConfig[task_id] + if not isinstance(queue, QueueConfig): + logger.error( + f"不支持的队列类型:{Config.QueueConfig[task_id].__class__.__name__}" + ) + await websocket.send_json( + TaskMessage( + type="Info", data={"Error": "队列类型不支持"} + ).model_dump() + ) + return + + task_list = [] + for queue_item in queue.QueueItem.values(): + uid = uuid.UUID(queue_item.get("Info", "ScriptId")) + task_list.append( + { + "script_id": str(uid), + "status": "等待", + "name": Config.ScriptConfig[uid].get("Info", "Name"), + } + ) + + elif actual_id is not None and actual_id in Config.ScriptConfig: + + task_list = [{"script_id": str(actual_id), "status": "等待"}] + + for task in task_list: + + script_id = uuid.UUID(task["script_id"]) + + # 检查任务是否在运行列表中 + if script_id in self.task_dict: + + task["status"] = "跳过" + await websocket.send_json( + TaskMessage( + type="Update", data={"task_list": task_list} + ).model_dump() + ) + logger.info(f"跳过任务:{script_id},该任务已在运行列表中") + continue + + # 标记为运行中 + task["status"] = "运行" + await websocket.send_json( + TaskMessage( + type="Update", data={"task_list": task_list} + ).model_dump() + ) + logger.info(f"任务开始:{script_id}") + + if isinstance(Config.ScriptConfig[script_id], MaaConfig): + task_item = MaaManager(mode, script_id, None, websocket) + # elif isinstance(Config.ScriptConfig[task_id], GeneralConfig): + # task_item = GeneralManager(mode, task_id, actual_id, websocket) + else: + logger.error( + f"不支持的脚本类型:{Config.ScriptConfig[script_id].__class__.__name__}" + ) + await websocket.send_json( + TaskMessage( + type="Info", data={"Error": "脚本类型不支持"} + ).model_dump() + ) + continue + + task = asyncio.create_task(task_item.run()) + task.add_done_callback( + lambda t: asyncio.create_task(task_item.final_task(t)) + ) + + async def stop_task(self, task_id: uuid.UUID) -> None: """ 中止任务 - :param name: 任务名称 + :param task_id: 任务ID """ - logger.info(f"中止任务:{name}", module="业务调度") - MainInfoBar.push_info_bar("info", "中止任务", name, 3000) + logger.info(f"中止任务:{task_id}") - if name == "ALL": + if task_id not in self.task_dict: + raise ValueError(f"The task {task_id} is not running.") - for name in self.task_dict: + self.task_dict[task_id].cancel() - self.task_dict[name].task.requestInterruption() - self.task_dict[name].requestInterruption() - self.task_dict[name].quit() - self.task_dict[name].wait() - - elif name in self.task_dict: - - self.task_dict[name].task.requestInterruption() - self.task_dict[name].requestInterruption() - self.task_dict[name].quit() - self.task_dict[name].wait() - - def remove_task(self, mode: str, name: str, logs: list) -> None: + async def remove_task( + self, task: asyncio.Task, mode: str, task_id: uuid.UUID + ) -> None: """ 处理任务结束后的收尾工作 - :param mode: 任务模式 - :param name: 任务名称 - :param logs: 任务日志 + Parameters + ---------- + task : asyncio.Task + 任务对象 + mode : str + 任务模式 + task_id : uuid.UUID + 任务ID """ - logger.info(f"任务结束:{name}", module="业务调度") - MainInfoBar.push_info_bar("info", "任务结束", name, 3000) - SoundPlayer.play("任务结束") + logger.info(f"任务结束:{task_id}") - # 删除任务线程,移除运行中标记 - self.task_dict[name].deleteLater() - self.task_dict.pop(name) - Config.running_list.remove(name) + # 从任务字典中移除任务 + try: + await task + except asyncio.CancelledError: + logger.info(f"任务 {task_id} 已结束") + self.task_dict.pop(task_id) - if "调度队列" in name and "人工排查" not in mode: - - # 保存调度队列历史记录 - if len(logs) > 0: - time = logs[0][1]["Time"] - history = "" - for log in logs: - history += f"任务名称:{log[0]},{log[1]["History"].replace("\n","\n ")}\n" - Config.save_history(name, {"Time": time, "History": history}) - else: - Config.save_history( - name, - { - "Time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "History": "没有任务被执行", - }, - ) - - # 根据调度队列情况设置电源状态 - if ( - Config.queue_dict[name]["Config"].get( - Config.queue_dict[name]["Config"].QueueSet_AfterAccomplish - ) - != "NoAction" - and Config.power_sign == "NoAction" - ): - Config.set_power_sign( - Config.queue_dict[name]["Config"].get( - Config.queue_dict[name]["Config"].QueueSet_AfterAccomplish - ) - ) - - if Config.args.mode == "cli" and Config.power_sign == "NoAction": - Config.set_power_sign("KillSelf") + websocket = self.websocket_dict.pop(task_id, None) + if websocket: + await websocket.send_json( + TaskMessage(type="Signal", data={"Accomplish": "无描述"}).model_dump() + ) + await websocket.close() TaskManager = _TaskManager() diff --git a/app/models/ConfigBase.py b/app/models/ConfigBase.py index e11edaf..cd4b49e 100644 --- a/app/models/ConfigBase.py +++ b/app/models/ConfigBase.py @@ -193,6 +193,9 @@ class ConfigItem: self.name = name self.value: Any = default self.validator = validator or ConfigValidator() + self.is_locked = False + + self.setValue(default) def setValue(self, value: Any): """ @@ -211,6 +214,11 @@ class ConfigItem: ) == value: return + if self.is_locked: + raise ValueError( + f"Config item '{self.group}.{self.name}' is locked and cannot be modified." + ) + # deepcopy new value try: self.value = deepcopy(value) @@ -232,6 +240,18 @@ class ConfigItem: return dpapi_decrypt(self.value) return self.value + def lock(self): + """ + 锁定配置项,锁定后无法修改配置项值 + """ + self.is_locked = True + + def unlock(self): + """ + 解锁配置项,解锁后可以修改配置项值 + """ + self.is_locked = False + class ConfigBase: """ @@ -249,6 +269,7 @@ class ConfigBase: self.file: None | Path = None self.if_save_multi_config = if_save_multi_config + self.is_locked = False async def connect(self, path: Path): """ @@ -265,6 +286,9 @@ class ConfigBase: "The config file must be a JSON file with '.json' extension." ) + if self.is_locked: + raise ValueError("Config is locked and cannot be modified.") + self.file = path if not self.file.exists(): @@ -292,6 +316,9 @@ class ConfigBase: 配置数据字典 """ + if self.is_locked: + raise ValueError("Config is locked and cannot be modified.") + # update the value of config item if data.get("SubConfigsInfo"): for k, v in data["SubConfigsInfo"].items(): @@ -395,6 +422,32 @@ class ConfigBase: indent=4, ) + async def lock(self): + """ + 锁定配置项,锁定后无法修改配置项值 + """ + + self.is_locked = True + + for name in dir(self): + item = getattr(self, name) + if isinstance(item, ConfigItem | MultipleConfig): + item.lock() + + async def unlock(self): + """ + 解锁配置项,解锁后可以修改配置项值 + """ + + self.is_locked = False + + for name in dir(self): + item = getattr(self, name) + if isinstance(item, ConfigItem): + item.unlock() + elif isinstance(item, MultipleConfig): + await item.unlock() + class MultipleConfig: """ @@ -424,6 +477,7 @@ class MultipleConfig: self.file: None | Path = None self.order: List[uuid.UUID] = [] self.data: Dict[uuid.UUID, ConfigBase] = {} + self.is_locked = False def __getitem__(self, key: uuid.UUID) -> ConfigBase: """允许通过 config[uuid] 访问配置项""" @@ -462,6 +516,9 @@ class MultipleConfig: "The config file must be a JSON file with '.json' extension." ) + if self.is_locked: + raise ValueError("Config is locked and cannot be modified.") + self.file = path if not self.file.exists(): @@ -490,6 +547,9 @@ class MultipleConfig: 配置数据字典 """ + if self.is_locked: + raise ValueError("Config is locked and cannot be modified.") + if not data.get("instances"): self.order = [] self.data = {} @@ -614,9 +674,17 @@ class MultipleConfig: 要移除的配置项的唯一标识符 """ + if self.is_locked: + raise ValueError("Config is locked and cannot be modified.") + if uid not in self.data: raise ValueError(f"Config item with uid {uid} does not exist.") + if self.data[uid].is_locked: + raise ValueError( + f"Config item with uid {uid} is locked and cannot be removed." + ) + self.data.pop(uid) self.order.remove(uid) @@ -641,6 +709,26 @@ class MultipleConfig: if self.file: await self.save() + async def lock(self): + """ + 锁定配置项,锁定后无法修改配置项值 + """ + + self.is_locked = True + + for item in self.values(): + await item.lock() + + async def unlock(self): + """ + 解锁配置项,解锁后可以修改配置项值 + """ + + self.is_locked = False + + for item in self.values(): + await item.unlock() + def keys(self): """返回配置项的所有唯一标识符""" diff --git a/app/models/schema.py b/app/models/schema.py index 6e939ad..71f9e26 100644 --- a/app/models/schema.py +++ b/app/models/schema.py @@ -192,12 +192,24 @@ class DispatchIn(BaseModel): ) -class DispatchCreateIn(DispatchIn): +class TaskCreateIn(DispatchIn): mode: Literal["自动代理", "人工排查", "设置脚本"] = Field( ..., description="任务模式" ) +class TaskCreateOut(OutBase): + taskId: str = Field(..., description="新创建的任务ID") + + +class TaskMessage(BaseModel): + type: Literal["Update", "Message", "Info", "Signal"] = Field( + ..., + description="消息类型 Update: 更新数据, Message: 请求弹出对话框, Info: 需要在UI显示的消息, Signal: 程序信号", + ) + data: Dict[str, Any] = Field(..., description="消息数据,具体内容根据type类型而定") + + class SettingGetOut(OutBase): data: Dict[str, Dict[str, Any]] = Field(..., description="全局设置数据") diff --git a/app/services/system.py b/app/services/system.py index 9f4f89b..bd926aa 100644 --- a/app/services/system.py +++ b/app/services/system.py @@ -303,7 +303,7 @@ class _SystemHandler: win32gui.EnumWindows(callback, window_info) return window_info - def kill_process(self, path: Path) -> None: + async def kill_process(self, path: Path) -> None: """ 根据路径中止进程 @@ -312,7 +312,7 @@ class _SystemHandler: logger.info(f"开始中止进程: {path}") - for pid in self.search_pids(path): + for pid in await self.search_pids(path): killprocess = subprocess.Popen( f"taskkill /F /T /PID {pid}", shell=True, @@ -322,7 +322,7 @@ class _SystemHandler: logger.success(f"进程已中止: {path}") - def search_pids(self, path: Path) -> list: + async def search_pids(self, path: Path) -> list: """ 根据路径查找进程PID diff --git a/app/task/MAA.py b/app/task/MAA.py new file mode 100644 index 0000000..d10be38 --- /dev/null +++ b/app/task/MAA.py @@ -0,0 +1,1998 @@ +# AUTO_MAA:A MAA Multi Account Management and Automation Tool +# Copyright © 2024-2025 DLmaster361 + +# This file is part of AUTO_MAA. + +# AUTO_MAA is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, +# or (at your option) any later version. + +# AUTO_MAA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty +# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +# the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with AUTO_MAA. If not, see . + +# Contact: DLmaster_361@163.com + + +import json +import asyncio +import subprocess +import shutil +import uuid +import win32com.client +from fastapi import WebSocket +from functools import partial +from datetime import datetime, timedelta +from pathlib import Path +from jinja2 import Environment, FileSystemLoader +from typing import Union, List, Dict, Optional + +from app.core import Config, MaaConfig, MaaUserConfig +from app.models.schema import TaskMessage +from app.models.ConfigBase import MultipleConfig +from app.services import Notify, System +from app.utils import get_logger, LogMonitor, ProcessManager +from .skland import skland_sign_in + + +logger = get_logger("MAA调度器") + + +METHOD_BOOK = {"NoAction": "8", "ExitGame": "9", "ExitEmulator": "12"} +MOOD_BOOK = {"Annihilation": "剿灭", "Routine": "日常"} + + +class MaaManager: + """MAA控制器""" + + def __init__( + self, + mode: str, + script_id: uuid.UUID, + user_id: Optional[uuid.UUID], + websocket: WebSocket, + ): + super().__init__() + + self.mode = mode + self.script_id = script_id + self.user_id = user_id + self.websocket = websocket + + self.emulator_process_manager = ProcessManager() + self.maa_process_manager = ProcessManager() + self.wait_event = asyncio.Event() + + self.maa_logs = [] + self.maa_result = "Wait" + self.maa_update_package = "" + + async def configure(self): + """提取配置信息""" + + await Config.ScriptConfig[self.script_id].lock() + + self.script_config = Config.ScriptConfig[self.script_id] + if isinstance(self.script_config, MaaConfig): + self.user_config = MultipleConfig([MaaUserConfig]) + await self.user_config.load(await self.script_config.UserData.toDict()) + + self.maa_root_path = Path(self.script_config.get("Info", "Path")) + self.maa_set_path = self.maa_root_path / "config/gui.json" + self.maa_log_path = self.maa_root_path / "debug/gui.log" + self.maa_exe_path = self.maa_root_path / "MAA.exe" + self.maa_tasks_path = self.maa_root_path / "resource/tasks/tasks.json" + self.port_range = [0] + [ + (i // 2 + 1) * (-1 if i % 2 else 1) + for i in range(0, 2 * self.script_config.get("Run", "ADBSearchRange")) + ] + self.maa_log_monitor = LogMonitor( + (1, 20), "%Y-%m-%d %H:%M:%S", self.check_maa_log + ) + + logger.success(f"{self.script_id}已锁定,MAA配置提取完成") + + def check_config(self) -> str: + """检查配置是否可用""" + + if not self.maa_exe_path.exists(): + return "MAA.exe文件不存在,请检查MAA路径设置!" + if not self.maa_set_path.exists(): + return "MAA配置文件不存在,请检查MAA路径设置!" + if (self.mode != "设置脚本" or self.user_id is not None) and not ( + Path.cwd() / f"data/{self.script_id}/Default/gui.json" + ).exists(): + return "未完成 MAA 全局设置,请先设置 MAA!" + return "Success!" + + async def run(self): + """主进程,运行MAA代理进程""" + + self.current_date = datetime.now().strftime("%m-%d") + curdate = Config.server_date().strftime("%Y-%m-%d") + self.begin_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + await self.configure() + check_result = self.check_config() + if check_result != "Success!": + logger.error(f"未通过配置检查:{check_result}") + await self.websocket.send_json( + TaskMessage(type="Info", data={"Error": check_result}).model_dump() + ) + return + + # 记录 MAA 配置文件 + logger.info(f"记录 MAA 配置文件:{self.maa_set_path}") + (Path.cwd() / f"data/{self.script_id}/Temp").mkdir(parents=True, exist_ok=True) + if self.maa_set_path.exists(): + shutil.copy( + self.maa_set_path, Path.cwd() / f"data/{self.script_id}/Temp/gui.json" + ) + + # 整理用户数据,筛选需代理的用户 + if self.mode != "设置脚本": + + self.user_list: List[Dict[str, str]] = [ + { + "user_id": str(uid), + "status": "等待", + "name": config.get("Info", "Name"), + } + for uid, config in self.user_config.items() + if config.get("Info", "Status") + and config.get("Info", "RemainedDay") > 0 + ] + self.user_list = sorted( + self.user_list, + key=lambda x: ( + self.user_config[uuid.UUID(x["user_id"])].get("Info", "Mode") + ), + ) + + logger.info(f"用户列表创建完成,已筛选用户数:{len(self.user_list)}") + + # 自动代理模式 + if self.mode == "自动代理": + + # 标记是否需要重启模拟器 + self.if_open_emulator = True + # # 执行情况预处理 + for _ in self.user_list: + if ( + self.user_config[uuid.UUID(_["user_id"])].get( + "Data", "LastProxyDate" + ) + != curdate + ): + await self.user_config[uuid.UUID(_["user_id"])].set( + "Data", "LastProxyDate", curdate + ) + await self.user_config[uuid.UUID(_["user_id"])].set( + "Data", "ProxyTimes", 0 + ) + + # 开始代理 + for index, user in enumerate(self.user_list): + + user_data = self.user_config[uuid.UUID(user["user_id"])] + + if self.script_config.get( + "Run", "ProxyTimesLimit" + ) == 0 or user_data.get("Data", "ProxyTimes") < self.script_config.get( + "RunSet", "ProxyTimesLimit" + ): + user["status"] = "运行" + await self.websocket.send_json( + TaskMessage( + type="Update", data={"user_list": self.user_list} + ).model_dump() + ) + else: + user["status"] = "跳过" + await self.websocket.send_json( + TaskMessage( + type="Update", data={"user_list": self.user_list} + ).model_dump() + ) + continue + + logger.info( + f"开始代理用户: {user['user_id']}", + ) + + # 详细模式用户首次代理需打开模拟器 + if user_data.get("Info", "Mode") == "详细": + self.if_open_emulator = True + + # 初始化代理情况记录和模式替换表 + run_book = { + "Annihilation": bool( + user_data.get("Info", "Annihilation") == "Close" + ), + "Routine": user_data.get("Info", "Mode") == "简洁" + or not user_data.get("Info", "Routine"), + } + + user_logs_list = [] + user_start_time = datetime.now() + + if user_data.get("Info", "IfSkland") and user_data.get( + "Info", "SklandToken" + ): + + if user_data.get( + "Data", "LastSklandDate" + ) != datetime.now().strftime("%Y-%m-%d"): + + await self.websocket.send_json( + TaskMessage( + type="Update", + data={"log": "正在执行森空岛签到中\n请稍候~"}, + ).model_dump() + ) + + skland_result = await skland_sign_in( + user_data.get("Info", "SklandToken") + ) + + for type, user_list in skland_result.items(): + + if type != "总计" and len(user_list) > 0: + + logger.info( + f"用户: {user['user_id']} - 森空岛签到{type}: {'、'.join(user_list)}", + ) + await self.websocket.send_json( + TaskMessage( + type="Info", + data={ + ( + "Info" if type != "失败" else "Error" + ): f"用户 {user['name']} 森空岛签到{type}: {'、'.join(user_list)}" + }, + ).model_dump() + ) + if skland_result["总计"] == 0: + logger.info(f"用户: {user['user_id']} - 森空岛签到失败") + await self.websocket.send_json( + TaskMessage( + type="Info", + data={ + "Error": f"用户 {user['name']} 森空岛签到失败", + }, + ).model_dump() + ) + + if ( + skland_result["总计"] > 0 + and len(skland_result["失败"]) == 0 + ): + await user_data.set( + "Data", + "LastSklandDate", + datetime.now().strftime("%Y-%m-%d"), + ) + + elif user_data.get("Info", "IfSkland"): + logger.warning( + f"用户: {user['user_id']} - 未配置森空岛签到Token,跳过森空岛签到" + ) + await self.websocket.send_json( + TaskMessage( + type="Info", + data={ + "Warning": f"用户 {user['name']} 未配置森空岛签到Token,跳过森空岛签到" + }, + ).model_dump() + ) + + # 剿灭-日常模式循环 + for mode in ["Annihilation", "Routine"]: + + if run_book[mode]: + continue + + # 剿灭模式;满足条件跳过剿灭 + if ( + mode == "Annihilation" + and self.script_config.get("Run", "AnnihilationWeeklyLimit") + and datetime.strptime( + user_data.get("Data", "LastAnnihilationDate"), "%Y-%m-%d" + ).isocalendar()[:2] + == datetime.strptime(curdate, "%Y-%m-%d").isocalendar()[:2] + ): + logger.info( + f"用户: {user['user_id']} - 本周剿灭模式已达上限,跳过执行剿灭任务" + ) + run_book[mode] = True + continue + else: + self.weekly_annihilation_limit_reached = False + + if ( + user_data.get("Info", "Mode") == "详细" + and not ( + Path.cwd() + / f"data/{self.script_id}/{user['user_id']}/ConfigFile/gui.json" + ).exists() + ): + logger.error( + f"用户: {user['user_id']} - 未找到日常详细配置文件" + ) + await self.websocket.send_json( + TaskMessage( + type="Info", + data={"Error": f"未找到 {user['name']} 的详细配置文件"}, + ).model_dump() + ) + run_book[mode] = False + break + + # 更新当前模式到界面 + await self.websocket.send_json( + TaskMessage( + type="Update", + data={ + "user_status": { + "user_id": user["user_id"], + "type": mode, + } + }, + ).model_dump() + ) + + # 解析任务构成 + if mode == "Routine": + + self.task_dict = { + "WakeUp": str(user_data.get("Task", "IfWakeUp")), + "Recruiting": str(user_data.get("Task", "IfRecruiting")), + "Base": str(user_data.get("Task", "IfBase")), + "Combat": str(user_data.get("Task", "IfCombat")), + "Mission": str(user_data.get("Task", "IfMission")), + "Mall": str(user_data.get("Task", "IfMall")), + "AutoRoguelike": str( + user_data.get("Task", "IfAutoRoguelike") + ), + "Reclamation": str(user_data.get("Task", "IfReclamation")), + } + + elif mode == "Annihilation": + + self.task_dict = { + "WakeUp": "True", + "Recruiting": "False", + "Base": "False", + "Combat": "True", + "Mission": "False", + "Mall": "False", + "AutoRoguelike": "False", + "Reclamation": "False", + } + + logger.info( + f"用户 {user['name']} - 模式: {mode} - 任务列表: {self.task_dict.values()}" + ) + + # 尝试次数循环 + for i in range(user_data.get("Run", "RunTimesLimit")): + + if run_book[mode]: + break + + logger.info( + f"用户 {user['name']} - 模式: {mode} - 尝试次数: {i + 1}/{user_data.get('Run','RunTimesLimit')}", + ) + + # 配置MAA + if isinstance(user_data, MaaUserConfig): + set = await self.set_maa(mode, index, user_data) + # 记录当前时间 + self.log_start_time = datetime.now() + + # 记录模拟器与ADB路径 + self.emulator_path = Path( + set["Configurations"]["Default"]["Start.EmulatorPath"] + ) + self.emulator_arguments = set["Configurations"]["Default"][ + "Start.EmulatorAddCommand" + ].split() + # 如果是快捷方式,进行解析 + if ( + self.emulator_path.suffix == ".lnk" + and self.emulator_path.exists() + ): + try: + shell = win32com.client.Dispatch("WScript.Shell") + shortcut = shell.CreateShortcut(str(self.emulator_path)) + self.emulator_path = Path(shortcut.TargetPath) + self.emulator_arguments = shortcut.Arguments.split() + except Exception as e: + logger.exception(f"解析快捷方式时出现异常:{e}") + await self.websocket.send_json( + TaskMessage( + type="Info", + data={ + "Error": f"解析快捷方式时出现异常:{e}", + }, + ).model_dump() + ) + self.if_open_emulator = True + break + elif not self.emulator_path.exists(): + logger.error(f"模拟器快捷方式不存在:{self.emulator_path}") + await self.websocket.send_json( + TaskMessage( + type="Info", + data={ + "Error": f"模拟器快捷方式 {self.emulator_path} 不存在", + }, + ).model_dump() + ) + self.if_open_emulator = True + break + + self.wait_time = int( + set["Configurations"]["Default"][ + "Start.EmulatorWaitSeconds" + ] + ) + + self.ADB_path = Path( + set["Configurations"]["Default"]["Connect.AdbPath"] + ) + self.ADB_path = ( + self.ADB_path + if self.ADB_path.is_absolute() + else self.maa_root_path / self.ADB_path + ) + self.ADB_address = set["Configurations"]["Default"][ + "Connect.Address" + ] + self.if_kill_emulator = bool( + set["Configurations"]["Default"]["MainFunction.PostActions"] + == "12" + ) + self.if_open_emulator_process = bool( + set["Configurations"]["Default"][ + "Start.OpenEmulatorAfterLaunch" + ] + == "True" + ) + + # 任务开始前释放ADB + try: + logger.info(f"释放ADB:{self.ADB_address}") + subprocess.run( + [self.ADB_path, "disconnect", self.ADB_address], + creationflags=subprocess.CREATE_NO_WINDOW, + ) + except subprocess.CalledProcessError as e: + # 忽略错误,因为可能本来就没有连接 + logger.warning(f"释放ADB时出现异常:{e}") + except Exception as e: + logger.exception(f"释放ADB时出现异常:{e}") + await self.websocket.send_json( + TaskMessage( + type="Info", + data={"Warning": f"释放ADB时出现异常:{e}"}, + ).model_dump() + ) + + if self.if_open_emulator_process: + try: + logger.info( + f"启动模拟器:{self.emulator_path},参数:{self.emulator_arguments}" + ) + await self.emulator_process_manager.open_process( + self.emulator_path, self.emulator_arguments, 0 + ) + except Exception as e: + logger.exception(f"启动模拟器时出现异常:{e}") + await self.websocket.send_json( + TaskMessage( + type="Info", + data={ + "Error": "启动模拟器时出现异常,请检查MAA中模拟器路径设置" + }, + ).model_dump() + ) + self.if_open_emulator = True + break + + # 更新静默进程标记有效时间 + logger.info( + f"更新静默进程标记:{self.emulator_path},标记有效时间:{datetime.now() + timedelta(seconds=self.wait_time + 10)}" + ) + Config.silence_dict[self.emulator_path] = ( + datetime.now() + timedelta(seconds=self.wait_time + 10) + ) + + await self.search_ADB_address() + + # 创建MAA任务 + logger.info(f"启动MAA进程:{self.maa_exe_path}") + await self.maa_process_manager.open_process( + self.maa_exe_path, [], 0 + ) + + # 监测MAA运行状态 + self.log_check_mode = mode + await self.maa_log_monitor.start( + self.maa_log_path, self.log_start_time + ) + + self.wait_event.clear() + await self.wait_event.wait() + + # 处理MAA结果 + if self.maa_result == "Success!": + + # 标记任务完成 + run_book[mode] = True + + logger.info( + f"用户: {user['user_id']} - MAA进程完成代理任务" + ) + await self.websocket.send_json( + TaskMessage( + type="Update", + data={ + "log": "检测到MAA进程完成代理任务\n正在等待相关程序结束\n请等待10s" + }, + ).model_dump() + ) + + else: + logger.error( + f"用户: {user['user_id']} - 代理任务异常: {self.maa_result}", + ) + # 打印中止信息 + # 此时,log变量内存储的就是出现异常的日志信息,可以保存或发送用于问题排查 + await self.websocket.send_json( + TaskMessage( + type="Update", + data={ + "log": f"{self.maa_result}\n正在中止相关程序\n请等待10s" + }, + ).model_dump() + ) + # 无命令行中止MAA与其子程序 + logger.info(f"中止MAA进程:{self.maa_exe_path}") + await self.maa_process_manager.kill(if_force=True) + await System.kill_process(self.maa_exe_path) + + # 中止模拟器进程 + logger.info( + f"中止模拟器进程:{list(self.emulator_process_manager.tracked_pids)}" + ) + await self.emulator_process_manager.kill() + + self.if_open_emulator = True + + # 推送异常通知 + Notify.push_plyer( + "用户自动代理出现异常!", + f"用户 {user['name']} 的{MOOD_BOOK[mode]}部分出现一次异常", + f"{user['name']}的{MOOD_BOOK[mode]}出现异常", + 3, + ) + + await self.maa_log_monitor.stop() + await asyncio.sleep(10) + + # 任务结束后释放ADB + try: + logger.info(f"释放ADB:{self.ADB_address}") + subprocess.run( + [self.ADB_path, "disconnect", self.ADB_address], + creationflags=subprocess.CREATE_NO_WINDOW, + ) + except subprocess.CalledProcessError as e: + # 忽略错误,因为可能本来就没有连接 + logger.warning(f"释放ADB时出现异常:{e}") + except Exception as e: + logger.exception(f"释放ADB时出现异常:{e}") + await self.websocket.send_json( + TaskMessage( + type="Info", + data={"Error": f"释放ADB时出现异常:{e}"}, + ).model_dump() + ) + # 任务结束后再次手动中止模拟器进程,防止退出不彻底 + if self.if_kill_emulator: + logger.info( + f"任务结束后再次中止模拟器进程:{list(self.emulator_process_manager.tracked_pids)}" + ) + await self.emulator_process_manager.kill() + self.if_open_emulator = True + + # 从配置文件中解析所需信息 + with self.maa_set_path.open(mode="r", encoding="utf-8") as f: + data = json.load(f) + + # 记录自定义基建索引 + await user_data.set( + "Data", + "CustomInfrastPlanIndex", + data["Configurations"]["Default"][ + "Infrast.CustomInfrastPlanIndex" + ], + ) + + # 记录更新包路径 + if ( + data["Global"]["VersionUpdate.package"] + and ( + self.maa_root_path + / data["Global"]["VersionUpdate.package"] + ).exists() + ): + self.maa_update_package = data["Global"][ + "VersionUpdate.package" + ] + + # 记录剿灭情况 + if ( + mode == "Annihilation" + and self.weekly_annihilation_limit_reached + ): + await user_data.set("Data", "LastAnnihilationDate", curdate) + # 保存运行日志以及统计信息 + if_six_star = await Config.save_maa_log( + Path.cwd() + / f"history/{curdate}/{user['name']}/{self.log_start_time.strftime('%H-%M-%S')}.log", + self.maa_logs, + self.maa_result, + ) + user_logs_list.append( + Path.cwd() + / f"history/{curdate}/{user['name']}/{self.log_start_time.strftime('%H-%M-%S')}.json" + ) + if if_six_star: + await self.push_notification( + "公招六星", + f"喜报:用户 {user['name']} 公招出六星啦!", + { + "user_name": user["name"], + }, + ( + user_data + if isinstance(user_data, MaaUserConfig) + else None + ), + ) + + # 执行MAA解压更新动作 + if self.maa_update_package: + + logger.info(f"检测到MAA更新,正在执行更新动作") + + await self.websocket.send_json( + TaskMessage( + type="Update", + data={ + "log": "检测到MAA存在更新\nMAA正在执行更新动作\n请等待10s" + }, + ).model_dump() + ) + subprocess.Popen( + [self.maa_exe_path], + creationflags=subprocess.CREATE_NO_WINDOW, + ) + await asyncio.sleep(10) + await System.kill_process(self.maa_exe_path) + + self.maa_update_package = "" + + logger.info(f"更新动作结束") + + # 发送统计信息 + statistics = Config.merge_statistic_info(user_logs_list) + statistics["user_info"] = user["name"] + statistics["start_time"] = user_start_time.strftime("%Y-%m-%d %H:%M:%S") + statistics["end_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + statistics["maa_result"] = ( + "代理任务全部完成" + if (run_book["Annihilation"] and run_book["Routine"]) + else "代理任务未全部完成" + ) + await self.push_notification( + "统计信息", + f"{self.current_date} | 用户 {user['name']} 的自动代理统计报告", + statistics, + user_data if isinstance(user_data, MaaUserConfig) else None, + ) + + if run_book["Annihilation"] and run_book["Routine"]: + # 成功完成代理的用户修改相关参数 + if ( + user_data.get("Data", "ProxyTimes") == 0 + and user_data.get("Info", "RemainedDay") != -1 + ): + await user_data.set( + "Info", + "RemainedDay", + user_data.get("Info", "RemainedDay") - 1, + ) + await user_data.set( + "Data", "ProxyTimes", user_data.get("Data", "ProxyTimes") + 1 + ) + user["status"] = "完成" + logger.success(f"用户 {user['name']} 的自动代理任务已完成") + Notify.push_plyer( + "成功完成一个自动代理任务!", + f"已完成用户 {user['name']} 的自动代理任务", + f"已完成 {user['name']} 的自动代理任务", + 3, + ) + else: + # 录入代理失败的用户 + logger.error(f"用户 {user['name']} 的自动代理任务未完成") + user["status"] = "异常" + + await self.websocket.send_json( + TaskMessage( + type="Update", data={"user_list": self.user_list} + ).model_dump() + ) + + # # 人工排查模式 + # elif self.mode == "人工排查": + + # # 人工排查时,屏蔽静默操作 + # logger.info( + # "人工排查任务开始,屏蔽静默操作", + # ) + # Config.if_ignore_silence = True + + # # 标记是否需要启动模拟器 + # self.if_open_emulator = True + # # 标识排查模式 + # for _ in self.user_list: + # _[0] += "_排查模式" + + # # 开始排查 + # for user in self.user_list: + + # user_data = self.data[user[2]]["Config"] + + # if self.isInterruptionRequested: + # break + + # logger.info(f"开始排查用户: {user[0]}", ) + + # user[1] = "运行" + # self.update_user_list.emit(self.user_list) + + # if user_data["Info"]["Mode"] == "详细": + # self.if_open_emulator = True + + # run_book = [False for _ in range(2)] + + # # 启动重试循环 + # while not self.isInterruptionRequested: + + # # 配置MAA + # self.set_maa("人工排查", user[2]) + + # # 记录当前时间 + # self.log_start_time = datetime.now() + # # 创建MAA任务 + # logger.info( + # f"启动MAA进程:{self.maa_exe_path}", + # , + # ) + # self.maa_process_manager.open_process(self.maa_exe_path, [], 0) + + # # 监测MAA运行状态 + # self.log_check_mode = "人工排查" + # self.start_monitor() + + # if self.maa_result == "Success!": + # logger.info( + # f"用户: {user[0]} - MAA进程成功登录PRTS", + # , + # ) + # run_book[0] = True + # self.update_log_text.emit("检测到MAA进程成功登录PRTS") + # else: + # logger.error( + # f"用户: {user[0]} - MAA未能正确登录到PRTS: {self.maa_result}", + # , + # ) + # self.update_log_text.emit( + # f"{self.maa_result}\n正在中止相关程序\n请等待10s" + # ) + # # 无命令行中止MAA与其子程序 + # logger.info( + # f"中止MAA进程:{self.maa_exe_path}", + # , + # ) + # self.maa_process_manager.kill(if_force=True) + # System.kill_process(self.maa_exe_path) + # self.if_open_emulator = True + # self.sleep(10) + + # # 登录成功,结束循环 + # if run_book[0]: + # break + # # 登录失败,询问是否结束循环 + # elif not self.isInterruptionRequested: + + # self.play_sound.emit("排查重试") + # if not self.push_question( + # "操作提示", "MAA未能正确登录到PRTS,是否重试?" + # ): + # break + + # # 登录成功,录入人工排查情况 + # if run_book[0] and not self.isInterruptionRequested: + + # self.play_sound.emit("排查录入") + # if self.push_question( + # "操作提示", "请检查用户代理情况,该用户是否正确完成代理任务?" + # ): + # run_book[1] = True + + # # 结果录入 + # if run_book[0] and run_book[1]: + # logger.info( + # f"用户 {user[0]} 通过人工排查", + # ) + # user_data["Data"]["IfPassCheck"] = True + # user[1] = "完成" + # else: + # logger.info( + # f"用户 {user[0]} 未通过人工排查", + # , + # ) + # user_data["Data"]["IfPassCheck"] = False + # user[1] = "异常" + + # self.update_user_list.emit(self.user_list) + + # # 解除静默操作屏蔽 + # logger.info( + # "人工排查任务结束,解除静默操作屏蔽", + # ) + # Config.if_ignore_silence = False + + # # 设置MAA模式 + # elif "设置MAA" in self.mode: + + # # 配置MAA + # self.set_maa(self.mode, "") + # # 创建MAA任务 + # logger.info( + # f"启动MAA进程:{self.maa_exe_path}", + # ) + # self.maa_process_manager.open_process(self.maa_exe_path, [], 0) + # # 记录当前时间 + # self.log_start_time = datetime.now() + + # # 监测MAA运行状态 + # self.log_check_mode = "设置MAA" + # self.start_monitor() + + # if "全局" in self.mode: + # (self.config_path / "Default").mkdir(parents=True, exist_ok=True) + # shutil.copy(self.maa_set_path, self.config_path / "Default") + # logger.success( + # f"全局MAA配置文件已保存到 {self.config_path / 'Default/gui.json'}", + # , + # ) + + # elif "用户" in self.mode: + # self.user_config_path.mkdir(parents=True, exist_ok=True) + # shutil.copy(self.maa_set_path, self.user_config_path) + # logger.success( + # f"用户MAA配置文件已保存到 {self.user_config_path}", + # , + # ) + + # result_text = "" + + async def final_task(self, task: asyncio.Task): + + await Config.ScriptConfig[self.script_id].unlock() + + # 导出结果 + if self.mode in ["自动代理", "人工排查"]: + + # 结束各子任务 + await self.maa_process_manager.kill(if_force=True) + await System.kill_process(self.maa_exe_path) + await self.emulator_process_manager.kill() + await self.maa_log_monitor.stop() + + # 更新用户数据 + sc = Config.ScriptConfig[self.script_id] + if isinstance(sc, MaaConfig): + await sc.UserData.load(await self.user_config.toDict()) + await Config.ScriptConfig.save() + + error_user = [_["name"] for _ in self.user_list if _["status"] == "异常"] + over_user = [_["name"] for _ in self.user_list if _["status"] == "完成"] + wait_user = [_["name"] for _ in self.user_list if _["status"] == "等待"] + + # 保存运行日志 + title = ( + f"{self.current_date} | {self.script_config.get("Info", "Name")}的{self.mode}任务报告" + if self.script_config.get("Info", "Name") != "" + else f"{self.current_date} | {self.mode[:4]}任务报告" + ) + result = { + "title": f"{self.mode}任务报告", + "script_name": ( + self.script_config.get("Info", "Name") + if self.script_config.get("Info", "Name") != "" + else "空白" + ), + "start_time": self.begin_time, + "end_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "completed_count": len(over_user), + "uncompleted_count": len(error_user) + len(wait_user), + "failed_user": error_user, + "waiting_user": wait_user, + } + + # 生成结果文本 + result_text = ( + f"任务开始时间:{result["start_time"]},结束时间:{result["end_time"]}\n" + f"已完成数:{result["completed_count"]},未完成数:{result["uncompleted_count"]}\n\n" + ) + if len(result["failed_user"]) > 0: + result_text += f"{self.mode[2:4]}未成功的用户:\n{"\n".join(result["failed_user"])}\n" + if len(result["waiting_user"]) > 0: + result_text += f"\n未开始{self.mode[2:4]}的用户:\n{"\n".join(result["waiting_user"])}\n" + + # 推送代理结果通知 + Notify.push_plyer( + title.replace("报告", "已完成!"), + f"已完成用户数:{len(over_user)},未完成用户数:{len(error_user) + len(wait_user)}", + f"已完成用户数:{len(over_user)},未完成用户数:{len(error_user) + len(wait_user)}", + 10, + ) + await self.push_notification("代理结果", title, result) + + # 复原 MAA 配置文件 + logger.info(f"复原 MAA 配置文件:{Path.cwd() / f'data/{self.script_id}/Temp'}") + if (Path.cwd() / f"data/{self.script_id}/Temp").exists(): + shutil.copy( + Path.cwd() / f"data/{self.script_id}/Temp/gui.json", self.maa_set_path + ) + shutil.rmtree(Path.cwd() / f"data/{self.script_id}/Temp") + + self.agree_bilibili(False) + return result_text + + async def search_ADB_address(self) -> None: + """搜索ADB实际地址""" + + await self.websocket.send_json( + TaskMessage( + type="Update", + data={ + "log": f"即将搜索ADB实际地址\n正在等待模拟器完成启动\n请等待{self.wait_time}s" + }, + ) + ) + + await asyncio.sleep(self.wait_time) + + if "-" in self.ADB_address: + ADB_ip = f"{self.ADB_address.split("-")[0]}-" + ADB_port = int(self.ADB_address.split("-")[1]) + + elif ":" in self.ADB_address: + ADB_ip = f"{self.ADB_address.split(':')[0]}:" + ADB_port = int(self.ADB_address.split(":")[1]) + + logger.info( + f"正在搜索ADB实际地址,ADB前缀:{ADB_ip},初始端口:{ADB_port},搜索范围:{self.port_range}" + ) + + for port in self.port_range: + + ADB_address = f"{ADB_ip}{ADB_port + port}" + + # 尝试通过ADB连接到指定地址 + connect_result = subprocess.run( + [self.ADB_path, "connect", ADB_address], + creationflags=subprocess.CREATE_NO_WINDOW, + stdin=subprocess.DEVNULL, + capture_output=True, + text=True, + encoding="utf-8", + ) + + if "connected" in connect_result.stdout: + + # 检查连接状态 + devices_result = subprocess.run( + [self.ADB_path, "devices"], + creationflags=subprocess.CREATE_NO_WINDOW, + stdin=subprocess.DEVNULL, + capture_output=True, + text=True, + encoding="utf-8", + ) + if ADB_address in devices_result.stdout: + + logger.info(f"ADB实际地址:{ADB_address}") + + # 断开连接 + logger.info(f"断开ADB连接:{ADB_address}") + subprocess.run( + [self.ADB_path, "disconnect", ADB_address], + creationflags=subprocess.CREATE_NO_WINDOW, + ) + + self.ADB_address = ADB_address + + # 覆写当前ADB地址 + logger.info(f"开始使用实际 ADB 地址覆写:{self.ADB_address}") + await self.maa_process_manager.kill(if_force=True) + await System.kill_process(self.maa_exe_path) + with self.maa_set_path.open(mode="r", encoding="utf-8") as f: + data = json.load(f) + data["Configurations"]["Default"][ + "Connect.Address" + ] = self.ADB_address + data["Configurations"]["Default"]["Start.EmulatorWaitSeconds"] = "0" + with self.maa_set_path.open(mode="w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=4) + + return None + + else: + logger.info(f"无法连接到ADB地址:{ADB_address}") + else: + logger.info(f"无法连接到ADB地址:{ADB_address}") + + async def check_maa_log(self, log_content: List[str]) -> None: + """获取MAA日志并检查以判断MAA程序运行状态""" + + self.maa_logs = log_content + log = "".join(log_content) + + # 更新MAA日志 + if self.maa_process_manager.is_running(): + + await self.websocket.send_json( + TaskMessage(type="Update", data={"log": log}).model_dump() + ) + + if self.mode == "自动代理": + + # 获取最近一条日志的时间 + latest_time = self.log_start_time + for _ in self.maa_logs[::-1]: + try: + if "如果长时间无进一步日志更新,可能需要手动干预。" in _: + continue + latest_time = datetime.strptime(_[1:20], "%Y-%m-%d %H:%M:%S") + break + except ValueError: + pass + + logger.info(f"MAA最近一条日志时间:{latest_time}") + + if self.mode == "Annihilation" and "剿灭任务失败" in log: + self.weekly_annihilation_limit_reached = True + else: + self.weekly_annihilation_limit_reached = False + + if "任务出错: StartUp" in log or "任务出错: 开始唤醒" in log: + self.maa_result = "MAA未能正确登录PRTS" + + elif "任务已全部完成!" in log: + + if "完成任务: StartUp" in log or "完成任务: 开始唤醒" in log: + self.task_dict["WakeUp"] = "False" + if "完成任务: Recruit" in log or "完成任务: 自动公招" in log: + self.task_dict["Recruiting"] = "False" + if "完成任务: Infrast" in log or "完成任务: 基建换班" in log: + self.task_dict["Base"] = "False" + if ( + "完成任务: Fight" in log + or "完成任务: 刷理智" in log + or "剿灭任务失败" in log + ): + self.task_dict["Combat"] = "False" + if "完成任务: Mall" in log or "完成任务: 获取信用及购物" in log: + self.task_dict["Mall"] = "False" + if "完成任务: Award" in log or "完成任务: 领取奖励" in log: + self.task_dict["Mission"] = "False" + if "完成任务: Roguelike" in log or "完成任务: 自动肉鸽" in log: + self.task_dict["AutoRoguelike"] = "False" + if "完成任务: Reclamation" in log or "完成任务: 生息演算" in log: + self.task_dict["Reclamation"] = "False" + + if all(v == "False" for v in self.task_dict.values()): + self.maa_result = "Success!" + else: + self.maa_result = "MAA部分任务执行失败" + + elif "请 「检查连接设置」 → 「尝试重启模拟器与 ADB」 → 「重启电脑」" in log: + self.maa_result = "MAA的ADB连接异常" + + elif "未检测到任何模拟器" in log: + self.maa_result = "MAA未检测到任何模拟器" + + elif "已停止" in log: + self.maa_result = "MAA在完成任务前中止" + + elif ( + "MaaAssistantArknights GUI exited" in log + or not self.maa_process_manager.is_running() + ): + self.maa_result = "MAA在完成任务前退出" + + elif datetime.now() - latest_time > timedelta( + minutes=self.script_config.get( + "RunSet", f"{self.log_check_mode}TimeLimit" + ) + ): + self.maa_result = "MAA进程超时" + + else: + self.maa_result = "Wait" + + elif self.mode == "人工排查": + if "完成任务: StartUp" in log or "完成任务: 开始唤醒" in log: + self.maa_result = "Success!" + elif "请 「检查连接设置」 → 「尝试重启模拟器与 ADB」 → 「重启电脑」" in log: + self.maa_result = "MAA的ADB连接异常" + elif "未检测到任何模拟器" in log: + self.maa_result = "MAA未检测到任何模拟器" + elif "已停止" in log: + self.maa_result = "MAA在完成任务前中止" + elif ( + "MaaAssistantArknights GUI exited" in log + or not self.maa_process_manager.is_running() + ): + self.maa_result = "MAA在完成任务前退出" + else: + self.maa_result = "Wait" + + elif self.mode == "设置脚本": + if ( + "MaaAssistantArknights GUI exited" in log + or not self.maa_process_manager.is_running() + ): + self.maa_result = "Success!" + else: + self.maa_result = "Wait" + + logger.debug(f"MAA 日志分析结果:{self.maa_result}") + + if self.maa_result != "Wait": + + self.wait_event.set() + + # def start_monitor(self) -> None: + # """开始监视MAA日志""" + + # logger.info( + # f"开始监视MAA日志,路径:{self.maa_log_path},日志起始时间:{self.log_start_time},模式:{self.log_check_mode}", + # , + # ) + # self.log_monitor.addPath(str(self.maa_log_path)) + # self.log_monitor_timer.start(1000) + # self.last_check_time = datetime.now() + # self.monitor_loop.exec() + + # def quit_monitor(self) -> None: + # """退出MAA日志监视进程""" + + # if len(self.log_monitor.files()) != 0: + + # logger.info( + # f"MAA日志监视器移除路径:{self.maa_log_path}", + # , + # ) + # self.log_monitor.removePath(str(self.maa_log_path)) + + # else: + # logger.warning( + # f"MAA日志监视器没有正在监看的路径:{self.log_monitor.files()}", + # , + # ) + + # self.log_monitor_timer.stop() + # self.last_check_time = None + # self.monitor_loop.quit() + + # logger.info("MAA日志监视锁已释放", ) + + async def set_maa(self, mode: str, index: int, user_data: MaaUserConfig) -> dict: + """配置MAA运行参数""" + logger.info(f"开始配置MAA运行参数: {mode}/{index}") + + if "设置脚本" not in self.mode and mode != "Update": + + if user_data.get("Info", "Server") == "Bilibili": + self.agree_bilibili(True) + else: + self.agree_bilibili(False) + + # 配置MAA前关闭可能未正常退出的MAA进程 + await self.maa_process_manager.kill(if_force=True) + await System.kill_process(self.maa_exe_path) + + # 预导入MAA配置文件 + if self.mode in ["自动代理", "人工排查"]: + if user_data.get("Info", "Mode") == "简洁": + shutil.copy( + (Path.cwd() / f"data/{self.script_id}/Default/ConfigFile/gui.json"), + self.maa_set_path, + ) + elif user_data.get("Info", "Mode") == "详细": + shutil.copy( + ( + Path.cwd() + / f"data/{self.script_id}/{self.user_id}/ConfigFile/gui.json" + ), + self.maa_set_path, + ) + elif self.mode == "设置脚本": + if ( + self.user_id is None + and ( + Path.cwd() / f"data/{self.script_id}/Default/ConfigFile/gui.json" + ).exists() + ): + shutil.copy( + (Path.cwd() / f"data/{self.script_id}/Default/ConfigFile/gui.json"), + self.maa_set_path, + ) + elif self.user_id is not None: + if ( + Path.cwd() + / f"data/{self.script_id}/{self.user_id}/ConfigFile/gui.json" + ).exists(): + shutil.copy( + ( + Path.cwd() + / f"data/{self.script_id}/{self.user_id}/ConfigFile/gui.json" + ), + self.maa_set_path, + ) + else: + shutil.copy( + ( + Path.cwd() + / f"data/{self.script_id}/Default/ConfigFile/gui.json" + ), + self.maa_set_path, + ) + + with self.maa_set_path.open(mode="r", encoding="utf-8") as f: + data = json.load(f) + + # 切换配置 + if data["Current"] != "Default": + + data["Configurations"]["Default"] = data["Configurations"][data["Current"]] + data["Current"] = "Default" + + # 时间设置 + for i in range(1, 9): + data["Global"][f"Timer.Timer{i}"] = "False" + + # 自动代理配置 + if self.mode == "自动代理": + + if (index == len(self.user_list) - 1) or ( + self.user_config[uuid.UUID(self.user_list[index + 1]["user_id"])].get( + "Info", "Mode" + ) + == "详细" + ): + data["Configurations"]["Default"][ + "MainFunction.PostActions" + ] = "12" # 完成后退出MAA和模拟器 + else: + + data["Configurations"]["Default"]["MainFunction.PostActions"] = ( + METHOD_BOOK[self.script_config.get("Run", "TaskTransitionMethod")] + ) # 完成后行为 + + data["Configurations"]["Default"][ + "Start.RunDirectly" + ] = "True" # 启动MAA后直接运行 + data["Configurations"]["Default"]["Start.OpenEmulatorAfterLaunch"] = str( + self.if_open_emulator + ) # 启动MAA后自动开启模拟器 + + data["Global"][ + "VersionUpdate.ScheduledUpdateCheck" + ] = "False" # 定时检查更新 + data["Global"][ + "VersionUpdate.AutoDownloadUpdatePackage" + ] = "True" # 自动下载更新包 + data["Global"][ + "VersionUpdate.AutoInstallUpdatePackage" + ] = "False" # 自动安装更新包 + + if Config.get("Function", "IfSilence"): + data["Global"]["Start.MinimizeDirectly"] = "True" # 启动MAA后直接最小化 + data["Global"]["GUI.UseTray"] = "True" # 显示托盘图标 + data["Global"]["GUI.MinimizeToTray"] = "True" # 最小化时隐藏至托盘 + + # 客户端类型 + data["Configurations"]["Default"]["Start.ClientType"] = user_data.get( + "Info", "Server" + ) + + # 账号切换 + if user_data.get("Info", "Server") == "Official": + data["Configurations"]["Default"]["Start.AccountName"] = ( + f"{user_data.get("Info", "Id")[:3]}****{user_data.get("Info", "Id")[7:]}" + if len(user_data.get("Info", "Id")) == 11 + else user_data.get("Info", "Id") + ) + elif user_data.get("Info", "Server") == "Bilibili": + data["Configurations"]["Default"]["Start.AccountName"] = user_data.get( + "Info", "Id" + ) + + # 按预设设定任务 + data["Configurations"]["Default"][ + "TaskQueue.WakeUp.IsChecked" + ] = "True" # 开始唤醒 + data["Configurations"]["Default"]["TaskQueue.Recruiting.IsChecked"] = ( + self.task_dict["Recruiting"] + ) # 自动公招 + data["Configurations"]["Default"]["TaskQueue.Base.IsChecked"] = ( + self.task_dict["Base"] + ) # 基建换班 + data["Configurations"]["Default"]["TaskQueue.Combat.IsChecked"] = ( + self.task_dict["Combat"] + ) # 刷理智 + data["Configurations"]["Default"]["TaskQueue.Mission.IsChecked"] = ( + self.task_dict["Mission"] + ) # 领取奖励 + data["Configurations"]["Default"]["TaskQueue.Mall.IsChecked"] = ( + self.task_dict["Mall"] + ) # 获取信用及购物 + data["Configurations"]["Default"]["TaskQueue.AutoRoguelike.IsChecked"] = ( + self.task_dict["AutoRoguelike"] + ) # 自动肉鸽 + data["Configurations"]["Default"]["TaskQueue.Reclamation.IsChecked"] = ( + self.task_dict["Reclamation"] + ) # 生息演算 + + # 整理任务顺序 + if mode == "Annihilation" or user_data.get("Info", "Mode") == "简洁": + + data["Configurations"]["Default"]["TaskQueue.Order.WakeUp"] = "0" + data["Configurations"]["Default"]["TaskQueue.Order.Recruiting"] = "1" + data["Configurations"]["Default"]["TaskQueue.Order.Base"] = "2" + data["Configurations"]["Default"]["TaskQueue.Order.Combat"] = "3" + data["Configurations"]["Default"]["TaskQueue.Order.Mall"] = "4" + data["Configurations"]["Default"]["TaskQueue.Order.Mission"] = "5" + data["Configurations"]["Default"]["TaskQueue.Order.AutoRoguelike"] = "6" + data["Configurations"]["Default"]["TaskQueue.Order.Reclamation"] = "7" + + data["Configurations"]["Default"]["MainFunction.UseMedicine"] = ( + "False" if user_data.get("Info", "MedicineNumb") == 0 else "True" + ) # 吃理智药 + data["Configurations"]["Default"]["MainFunction.UseMedicine.Quantity"] = ( + str(user_data.get("Info", "MedicineNumb")) + ) # 吃理智药数量 + data["Configurations"]["Default"]["MainFunction.Series.Quantity"] = ( + user_data.get("Info", "SeriesNumb") + ) # 连战次数 + + if "剿灭" in mode: + + data["Configurations"]["Default"][ + "MainFunction.Stage1" + ] = "Annihilation" # 主关卡 + data["Configurations"]["Default"][ + "MainFunction.Stage2" + ] = "" # 备选关卡1 + data["Configurations"]["Default"][ + "MainFunction.Stage3" + ] = "" # 备选关卡2 + data["Configurations"]["Default"][ + "Fight.RemainingSanityStage" + ] = "" # 剩余理智关卡 + data["Configurations"]["Default"][ + "MainFunction.Series.Quantity" + ] = "1" # 连战次数 + data["Configurations"]["Default"][ + "MainFunction.Annihilation.UseCustom" + ] = "True" # 自定义剿灭关卡 + data["Configurations"]["Default"]["MainFunction.Annihilation.Stage"] = ( + user_data.get("Info", "Annihilation") + ) # 自定义剿灭关卡号 + data["Configurations"]["Default"][ + "Penguin.IsDrGrandet" + ] = "False" # 博朗台模式 + data["Configurations"]["Default"][ + "GUI.CustomStageCode" + ] = "True" # 手动输入关卡名 + data["Configurations"]["Default"][ + "GUI.UseAlternateStage" + ] = "False" # 使用备选关卡 + data["Configurations"]["Default"][ + "Fight.UseRemainingSanityStage" + ] = "False" # 使用剩余理智 + data["Configurations"]["Default"][ + "Fight.UseExpiringMedicine" + ] = "True" # 无限吃48小时内过期的理智药 + data["Configurations"]["Default"][ + "GUI.HideSeries" + ] = "False" # 隐藏连战次数 + + elif "日常" in mode: + + data["Configurations"]["Default"]["MainFunction.Stage1"] = ( + user_data.get("Info", "Stage") + if user_data.get("Info", "Stage") != "-" + else "" + ) # 主关卡 + data["Configurations"]["Default"]["MainFunction.Stage2"] = ( + user_data.get("Info", "Stage_1") + if user_data.get("Info", "Stage_1") != "-" + else "" + ) # 备选关卡1 + data["Configurations"]["Default"]["MainFunction.Stage3"] = ( + user_data.get("Info", "Stage_2") + if user_data.get("Info", "Stage_2") != "-" + else "" + ) # 备选关卡2 + data["Configurations"]["Default"]["MainFunction.Stage4"] = ( + user_data.get("Info", "Stage_3") + if user_data.get("Info", "Stage_3") != "-" + else "" + ) # 备选关卡3 + data["Configurations"]["Default"]["Fight.RemainingSanityStage"] = ( + user_data.get("Info", "Stage_Remain") + if user_data.get("Info", "Stage_Remain") != "-" + else "" + ) # 剩余理智关卡 + data["Configurations"]["Default"][ + "GUI.UseAlternateStage" + ] = "True" # 备选关卡 + data["Configurations"]["Default"]["Fight.UseRemainingSanityStage"] = ( + "True" if user_data.get("Info", "Stage_Remain") != "-" else "False" + ) # 使用剩余理智 + + if user_data.get("Info", "Mode") == "简洁": + + data["Configurations"]["Default"][ + "Penguin.IsDrGrandet" + ] = "False" # 博朗台模式 + data["Configurations"]["Default"][ + "GUI.CustomStageCode" + ] = "True" # 手动输入关卡名 + data["Configurations"]["Default"][ + "Fight.UseExpiringMedicine" + ] = "True" # 无限吃48小时内过期的理智药 + # 自定义基建配置 + if user_data.get("Info", "InfrastMode") == "Custom": + + if ( + Path.cwd() + / f"data/{self.script_id}/{self.user_id}/Infrastructure/infrastructure.json" + ).exists(): + + data["Configurations"]["Default"][ + "Infrast.InfrastMode" + ] = "Custom" # 基建模式 + data["Configurations"]["Default"][ + "Infrast.CustomInfrastPlanIndex" + ] = user_data.get( + "Data", "CustomInfrastPlanIndex" + ) # 自定义基建配置索引 + data["Configurations"]["Default"][ + "Infrast.DefaultInfrast" + ] = "user_defined" # 内置配置 + data["Configurations"]["Default"][ + "Infrast.IsCustomInfrastFileReadOnly" + ] = "False" # 自定义基建配置文件只读 + data["Configurations"]["Default"][ + "Infrast.CustomInfrastFile" + ] = str( + Path.cwd() + / f"data/{self.script_id}/{self.user_id}/Infrastructure/infrastructure.json" + ) # 自定义基建配置文件地址 + else: + logger.warning( + f"未选择用户 {user_data.get('Info', 'Name')} 的自定义基建配置文件" + ) + await self.websocket.send_json( + TaskMessage( + type="Info", + data={ + "warning": f"未选择用户 {user_data.get('Info', 'Name')} 的自定义基建配置文件" + }, + ) + ) + data["Configurations"]["Default"][ + "Infrast.CustomInfrastEnabled" + ] = "Normal" # 基建模式 + else: + data["Configurations"]["Default"]["Infrast.InfrastMode"] = ( + user_data.get("Info", "InfrastMode") + ) # 基建模式 + + elif user_data.get("Info", "Mode") == "详细": + + # 基建模式 + if ( + data["Configurations"]["Default"]["Infrast.InfrastMode"] + == "Custom" + ): + data["Configurations"]["Default"][ + "Infrast.CustomInfrastPlanIndex" + ] = user_data.get( + "Data", "CustomInfrastPlanIndex" + ) # 自定义基建配置索引 + + # 人工排查配置 + elif "人工排查" in mode: + + data["Configurations"]["Default"][ + "MainFunction.PostActions" + ] = "8" # 完成后退出MAA + data["Configurations"]["Default"][ + "Start.RunDirectly" + ] = "True" # 启动MAA后直接运行 + data["Global"]["Start.MinimizeDirectly"] = "True" # 启动MAA后直接最小化 + data["Global"]["GUI.UseTray"] = "True" # 显示托盘图标 + data["Global"]["GUI.MinimizeToTray"] = "True" # 最小化时隐藏至托盘 + data["Configurations"]["Default"]["Start.OpenEmulatorAfterLaunch"] = str( + self.if_open_emulator + ) # 启动MAA后自动开启模拟器 + data["Global"][ + "VersionUpdate.ScheduledUpdateCheck" + ] = "False" # 定时检查更新 + data["Global"][ + "VersionUpdate.AutoDownloadUpdatePackage" + ] = "False" # 自动下载更新包 + data["Global"][ + "VersionUpdate.AutoInstallUpdatePackage" + ] = "False" # 自动安装更新包 + + # 客户端类型 + data["Configurations"]["Default"]["Start.ClientType"] = user_data.get( + "Info", "Server" + ) + + # 账号切换 + if user_data.get("Info", "Server") == "Official": + data["Configurations"]["Default"]["Start.AccountName"] = ( + f"{user_data.get('Info', 'Id')[:3]}****{user_data.get('Info', 'Id')[7:]}" + if len(user_data.get("Info", "Id")) == 11 + else user_data.get("Info", "Id") + ) + elif user_data.get("Info", "Server") == "Bilibili": + data["Configurations"]["Default"]["Start.AccountName"] = user_data.get( + "Info", "Id" + ) + + data["Configurations"]["Default"][ + "TaskQueue.WakeUp.IsChecked" + ] = "True" # 开始唤醒 + data["Configurations"]["Default"][ + "TaskQueue.Recruiting.IsChecked" + ] = "False" # 自动公招 + data["Configurations"]["Default"][ + "TaskQueue.Base.IsChecked" + ] = "False" # 基建换班 + data["Configurations"]["Default"][ + "TaskQueue.Combat.IsChecked" + ] = "False" # 刷理智 + data["Configurations"]["Default"][ + "TaskQueue.Mission.IsChecked" + ] = "False" # 领取奖励 + data["Configurations"]["Default"][ + "TaskQueue.Mall.IsChecked" + ] = "False" # 获取信用及购物 + data["Configurations"]["Default"][ + "TaskQueue.AutoRoguelike.IsChecked" + ] = "False" # 自动肉鸽 + data["Configurations"]["Default"][ + "TaskQueue.Reclamation.IsChecked" + ] = "False" # 生息演算 + + # 设置MAA配置 + elif "设置MAA" in mode: + + data["Configurations"]["Default"][ + "MainFunction.PostActions" + ] = "0" # 完成后无动作 + data["Configurations"]["Default"][ + "Start.RunDirectly" + ] = "False" # 启动MAA后直接运行 + data["Configurations"]["Default"][ + "Start.OpenEmulatorAfterLaunch" + ] = "False" # 启动MAA后自动开启模拟器 + data["Global"][ + "VersionUpdate.ScheduledUpdateCheck" + ] = "False" # 定时检查更新 + data["Global"][ + "VersionUpdate.AutoDownloadUpdatePackage" + ] = "False" # 自动下载更新包 + data["Global"][ + "VersionUpdate.AutoInstallUpdatePackage" + ] = "False" # 自动安装更新包 + + if Config.get("Function", "IfSilence"): + data["Global"][ + "Start.MinimizeDirectly" + ] = "False" # 启动MAA后直接最小化 + + data["Configurations"]["Default"][ + "TaskQueue.WakeUp.IsChecked" + ] = "False" # 开始唤醒 + data["Configurations"]["Default"][ + "TaskQueue.Recruiting.IsChecked" + ] = "False" # 自动公招 + data["Configurations"]["Default"][ + "TaskQueue.Base.IsChecked" + ] = "False" # 基建换班 + data["Configurations"]["Default"][ + "TaskQueue.Combat.IsChecked" + ] = "False" # 刷理智 + data["Configurations"]["Default"][ + "TaskQueue.Mission.IsChecked" + ] = "False" # 领取奖励 + data["Configurations"]["Default"][ + "TaskQueue.Mall.IsChecked" + ] = "False" # 获取信用及购物 + data["Configurations"]["Default"][ + "TaskQueue.AutoRoguelike.IsChecked" + ] = "False" # 自动肉鸽 + data["Configurations"]["Default"][ + "TaskQueue.Reclamation.IsChecked" + ] = "False" # 生息演算 + + elif mode == "更新MAA": + + data["Configurations"]["Default"][ + "MainFunction.PostActions" + ] = "0" # 完成后无动作 + data["Configurations"]["Default"][ + "Start.RunDirectly" + ] = "False" # 启动MAA后直接运行 + data["Configurations"]["Default"][ + "Start.OpenEmulatorAfterLaunch" + ] = "False" # 启动MAA后自动开启模拟器 + data["Global"]["Start.MinimizeDirectly"] = "True" # 启动MAA后直接最小化 + data["Global"]["GUI.UseTray"] = "True" # 显示托盘图标 + data["Global"]["GUI.MinimizeToTray"] = "True" # 最小化时隐藏至托盘 + data["Global"][ + "VersionUpdate.package" + ] = self.maa_update_package # 更新包路径 + + data["Global"][ + "VersionUpdate.ScheduledUpdateCheck" + ] = "False" # 定时检查更新 + data["Global"][ + "VersionUpdate.AutoDownloadUpdatePackage" + ] = "False" # 自动下载更新包 + data["Global"][ + "VersionUpdate.AutoInstallUpdatePackage" + ] = "True" # 自动安装更新包 + data["Configurations"]["Default"][ + "TaskQueue.WakeUp.IsChecked" + ] = "False" # 开始唤醒 + data["Configurations"]["Default"][ + "TaskQueue.Recruiting.IsChecked" + ] = "False" # 自动公招 + data["Configurations"]["Default"][ + "TaskQueue.Base.IsChecked" + ] = "False" # 基建换班 + data["Configurations"]["Default"][ + "TaskQueue.Combat.IsChecked" + ] = "False" # 刷理智 + data["Configurations"]["Default"][ + "TaskQueue.Mission.IsChecked" + ] = "False" # 领取奖励 + data["Configurations"]["Default"][ + "TaskQueue.Mall.IsChecked" + ] = "False" # 获取信用及购物 + data["Configurations"]["Default"][ + "TaskQueue.AutoRoguelike.IsChecked" + ] = "False" # 自动肉鸽 + data["Configurations"]["Default"][ + "TaskQueue.Reclamation.IsChecked" + ] = "False" # 生息演算 + + # 启动模拟器仅生效一次 + if "设置MAA" not in mode and "更新MAA" not in mode and self.if_open_emulator: + self.if_open_emulator = False + + # 覆写配置文件 + with self.maa_set_path.open(mode="w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=4) + + logger.success( + f"MAA运行参数配置完成: {mode}/{index}", + ) + + return data + + def agree_bilibili(self, if_agree): + """向MAA写入Bilibili协议相关任务""" + logger.info(f"Bilibili协议相关任务状态: {'启用' if if_agree else '禁用'}") + + with self.maa_tasks_path.open(mode="r", encoding="utf-8") as f: + data = json.load(f) + + if if_agree and Config.get("Function", "IfAgreeBilibili"): + data["BilibiliAgreement_AUTO"] = { + "algorithm": "OcrDetect", + "action": "ClickSelf", + "text": ["同意"], + "maxTimes": 5, + "Doc": "关闭B服用户协议", + "next": ["StartUpThemes#next"], + } + if "BilibiliAgreement_AUTO" not in data["StartUpThemes"]["next"]: + data["StartUpThemes"]["next"].insert(0, "BilibiliAgreement_AUTO") + else: + if "BilibiliAgreement_AUTO" in data: + data.pop("BilibiliAgreement_AUTO") + if "BilibiliAgreement_AUTO" in data["StartUpThemes"]["next"]: + data["StartUpThemes"]["next"].remove("BilibiliAgreement_AUTO") + + with self.maa_tasks_path.open(mode="w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=4) + + async def push_notification( + self, + mode: str, + title: str, + message, + user_data: Optional[MaaUserConfig] = None, + ) -> None: + """通过所有渠道推送通知""" + logger.info(f"开始推送通知,模式:{mode},标题:{title}") + + env = Environment(loader=FileSystemLoader(str(Path.cwd() / "resources/html"))) + + if mode == "代理结果" and ( + Config.get("Notify", "SendTaskResultTime") == "任何时刻" + or ( + Config.get("Notify", "SendTaskResultTime") == "仅失败时" + and message["uncompleted_count"] != 0 + ) + ): + # 生成文本通知内容 + message_text = ( + f"任务开始时间:{message['start_time']},结束时间:{message['end_time']}\n" + f"已完成数:{message['completed_count']},未完成数:{message['uncompleted_count']}\n\n" + ) + + if len(message["failed_user"]) > 0: + message_text += f"{self.mode[2:4]}未成功的用户:\n{"\n".join(message["failed_user"])}\n" + if len(message["waiting_user"]) > 0: + message_text += f"\n未开始{self.mode[2:4]}的用户:\n{"\n".join(message["waiting_user"])}\n" + + # 生成HTML通知内容 + message["failed_user"] = "、".join(message["failed_user"]) + message["waiting_user"] = "、".join(message["waiting_user"]) + + template = env.get_template("MAA_result.html") + message_html = template.render(message) + + # ServerChan的换行是两个换行符。故而将\n替换为\n\n + serverchan_message = message_text.replace("\n", "\n\n") + + # 发送全局通知 + + if Config.get("Notify", "IfSendMail"): + Notify.send_mail( + "网页", title, message_html, Config.get("Notify", "ToAddress") + ) + + if Config.get("Notify", "IfServerChan"): + Notify.ServerChanPush( + title, + f"{serverchan_message}\n\nAUTO_MAA 敬上", + Config.get("Notify", "ServerChanKey"), + ) + + if Config.get("Notify", "IfCompanyWebHookBot"): + Notify.WebHookPush( + title, + f"{message_text}\n\nAUTO_MAA 敬上", + Config.get("Notify", "CompanyWebHookBotUrl"), + ) + + elif mode == "统计信息": + + # 生成文本通知内容 + formatted = [] + if "drop_statistics" in message: + for stage, items in message["drop_statistics"].items(): + formatted.append(f"掉落统计({stage}):") + for item, quantity in items.items(): + formatted.append(f" {item}: {quantity}") + drop_text = "\n".join(formatted) + + formatted = ["招募统计:"] + if "recruit_statistics" in message: + for star, count in message["recruit_statistics"].items(): + formatted.append(f" {star}: {count}") + recruit_text = "\n".join(formatted) + + message_text = ( + f"开始时间: {message['start_time']}\n" + f"结束时间: {message['end_time']}\n" + f"MAA执行结果: {message['maa_result']}\n\n" + f"{recruit_text}\n" + f"{drop_text}" + ) + + # 生成HTML通知内容 + template = env.get_template("MAA_statistics.html") + message_html = template.render(message) + + # ServerChan的换行是两个换行符。故而将\n替换为\n\n + serverchan_message = message_text.replace("\n", "\n\n") + + # 发送全局通知 + if Config.get("Notify", "IfSendStatistic"): + + if Config.get("Notify", "IfSendMail"): + Notify.send_mail( + "网页", title, message_html, Config.get("Notify", "ToAddress") + ) + + if Config.get("Notify", "IfServerChan"): + Notify.ServerChanPush( + title, + f"{serverchan_message}\n\nAUTO_MAA 敬上", + Config.get("Notify", "ServerChanKey"), + ) + + if Config.get("Notify", "IfCompanyWebHookBot"): + Notify.WebHookPush( + title, + f"{message_text}\n\nAUTO_MAA 敬上", + Config.get("Notify", "CompanyWebHookBotUrl"), + ) + + # 发送用户单独通知 + if ( + isinstance(user_data, MaaUserConfig) + and user_data.get("Notify", "Enabled") + and user_data.get("Notify", "IfSendStatistic") + ): + + # 发送邮件通知 + if user_data.get("Notify", "IfSendMail"): + if user_data.get("Notify", "ToAddress"): + Notify.send_mail( + "网页", + title, + message_html, + user_data.get("Notify", "ToAddress"), + ) + else: + logger.error(f"用户邮箱地址为空,无法发送用户单独的邮件通知") + + # 发送ServerChan通知 + if user_data.get("Notify", "IfServerChan"): + if user_data.get("Notify", "ServerChanKey"): + Notify.ServerChanPush( + title, + f"{serverchan_message}\n\nAUTO_MAA 敬上", + user_data.get("Notify", "ServerChanKey"), + ) + else: + logger.error( + "用户ServerChan密钥为空,无法发送用户单独的ServerChan通知" + ) + + # 推送CompanyWebHookBot通知 + if user_data.get("Notify", "IfCompanyWebHookBot"): + if user_data.get("Notify", "CompanyWebHookBotUrl"): + Notify.WebHookPush( + title, + f"{message_text}\n\nAUTO_MAA 敬上", + user_data.get("Notify", "CompanyWebHookBotUrl"), + ) + else: + logger.error( + f"用户CompanyWebHookBot密钥为空,无法发送用户单独的CompanyWebHookBot通知" + ) + + elif mode == "公招六星": + + # 生成HTML通知内容 + template = env.get_template("MAA_six_star.html") + + message_html = template.render(message) + + # 发送全局通知 + if Config.get("Notify", "IfSendSixStar"): + + if Config.get("Notify", "IfSendMail"): + Notify.send_mail( + "网页", title, message_html, Config.get("Notify", "ToAddress") + ) + + if Config.get("Notify", "IfServerChan"): + Notify.ServerChanPush( + title, + "好羡慕~\n\nAUTO_MAA 敬上", + Config.get("Notify", "ServerChanKey"), + ) + + if Config.get("Notify", "IfCompanyWebHookBot"): + Notify.WebHookPush( + title, + "好羡慕~\n\nAUTO_MAA 敬上", + Config.get("Notify", "CompanyWebHookBotUrl"), + ) + Notify.CompanyWebHookBotPushImage( + Path.cwd() / "resources/images/notification/six_star.png", + Config.get("Notify", "CompanyWebHookBotUrl"), + ) + + # 发送用户单独通知 + if ( + isinstance(user_data, MaaUserConfig) + and user_data.get("Notify", "Enabled") + and user_data.get("Notify", "IfSendSixStar") + ): + + # 发送邮件通知 + if user_data.get("Notify", "IfSendMail"): + if user_data.get("Notify", "ToAddress"): + Notify.send_mail( + "网页", + title, + message_html, + user_data.get("Notify", "ToAddress"), + ) + else: + logger.error("用户邮箱地址为空,无法发送用户单独的邮件通知") + + # 发送ServerChan通知 + if user_data.get("Notify", "IfServerChan"): + + if user_data.get("Notify", "ServerChanKey"): + Notify.ServerChanPush( + title, + "好羡慕~\n\nAUTO_MAA 敬上", + user_data.get("Notify", "ServerChanKey"), + ) + else: + logger.error( + "用户ServerChan密钥为空,无法发送用户单独的ServerChan通知" + ) + + # 推送CompanyWebHookBot通知 + if user_data.get("Notify", "IfCompanyWebHookBot"): + if user_data.get("Notify", "CompanyWebHookBotUrl"): + Notify.WebHookPush( + title, + "好羡慕~\n\nAUTO_MAA 敬上", + user_data.get("Notify", "CompanyWebHookBotUrl"), + ) + Notify.CompanyWebHookBotPushImage( + Path.cwd() / "resources/images/notification/six_star.png", + user_data.get("Notify", "CompanyWebHookBotUrl"), + ) + else: + logger.error( + "用户CompanyWebHookBot密钥为空,无法发送用户单独的CompanyWebHookBot通知" + ) + + return None diff --git a/app/task/__init__.py b/app/task/__init__.py index 8c25728..8edcb2d 100644 --- a/app/task/__init__.py +++ b/app/task/__init__.py @@ -25,5 +25,6 @@ __license__ = "GPL-3.0 license" from .skland import skland_sign_in +from .MAA import MaaManager -__all__ = ["skland_sign_in"] +__all__ = ["skland_sign_in", "MaaManager"] diff --git a/app/task/skland.py b/app/task/skland.py index 467a504..9e4afb8 100644 --- a/app/task/skland.py +++ b/app/task/skland.py @@ -34,8 +34,8 @@ import hashlib import requests from urllib import parse -from core import Config -from utils.logger import get_logger +from app.core import Config +from app.utils.logger import get_logger logger = get_logger("森空岛签到任务") diff --git a/app/utils/LogMonitor.py b/app/utils/LogMonitor.py new file mode 100644 index 0000000..4442fc5 --- /dev/null +++ b/app/utils/LogMonitor.py @@ -0,0 +1,89 @@ +import asyncio +import aiofiles +import os +from datetime import datetime +from pathlib import Path +from typing import Callable, Optional, List, Awaitable + +from loguru import logger + + +class LogMonitor: + def __init__( + self, + time_stamp_range: tuple[int, int], + time_format: str, + callback: Callable[[List[str]], Awaitable[None]], + encoding: str = "utf-8", + ): + self.time_stamp_range = time_stamp_range + self.time_format = time_format + self.callback = callback + self.encoding = encoding + self.log_file_path: Optional[Path] = None + self.log_start_time: datetime = datetime.now() + self.log_contents: List[str] = [] + self.task: Optional[asyncio.Task] = None + + async def monitor_log(self): + + if self.log_file_path is None or not self.log_file_path.exists(): + raise ValueError("Log file path is not set or does not exist.") + + while True: + + log_contents = [] + if_log_start = False + + async with aiofiles.open( + self.log_file_path, "r", encoding=self.encoding + ) as f: + + async for line in f: + if not if_log_start: + try: + entry_time = datetime.strptime( + line[ + self.time_stamp_range[0] : self.time_stamp_range[1] + ], + self.time_format, + ) + if entry_time > self.log_start_time: + if_log_start = True + log_contents.append(line) + except ValueError: + pass + else: + log_contents.append(line) + + # 调用回调 + if log_contents != self.log_contents: + self.log_contents = log_contents + await self.callback(log_contents) + + await asyncio.sleep(1) + + async def start(self, log_file_path: Path, start_time: datetime) -> None: + """启动监控""" + + if log_file_path.is_dir(): + raise ValueError(f"Log file cannot be a directory: {log_file_path}") + + if self.task is not None and not self.task.done(): + await self.stop() + + self.log_file_path = log_file_path + self.log_start_time = start_time + self.task = asyncio.create_task(self.monitor_log()) + + logger.info(f"开始监控文件: {self.log_file_path}") + + async def stop(self): + """停止监控""" + + if self.task is not None and not self.task.done(): + self.task.cancel() + + self.log_contents = [] + logger.info(f"停止监控文件: {self.log_file_path}") + self.log_file_path = None diff --git a/app/utils/__init__.py b/app/utils/__init__.py index d59c26f..82e63c0 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -26,12 +26,14 @@ __license__ = "GPL-3.0 license" from .logger import get_logger from .ImageUtils import ImageUtils +from .LogMonitor import LogMonitor from .ProcessManager import ProcessManager from .security import dpapi_encrypt, dpapi_decrypt __all__ = [ "get_logger", "ImageUtils", + "LogMonitor", "ProcessManager", "dpapi_encrypt", "dpapi_decrypt", diff --git a/pyproject.toml b/pyproject.toml index 03d1498..9e08ffc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "uvicorn==0.35.0", "plyer==2.1.0", "psutil==7.0.0", + "jinja2==3.1.6", "pywin32==310", "keyboard==0.13.5", "truststore==0.10.1", diff --git a/requirements.txt b/requirements.txt index ce83496..53a3a8b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,10 @@ loguru==0.7.3 fastapi==0.116.1 pydantic==2.11.7 uvicorn==0.35.0 +aiofiles==24.1.0 plyer==2.1.0 psutil==7.0.0 +jinja2==3.1.6 pywin32==310 keyboard==0.13.5 truststore==0.10.1 diff --git a/resources/docs/ChineseSimplified.isl b/resources/docs/ChineseSimplified.isl new file mode 100644 index 0000000..855da1a --- /dev/null +++ b/resources/docs/ChineseSimplified.isl @@ -0,0 +1,403 @@ +; *** Inno Setup version 6.4.0+ Chinese Simplified messages *** +; +; To download user-contributed translations of this file, go to: +; https://jrsoftware.org/files/istrans/ +; +; Note: When translating this text, do not add periods (.) to the end of +; messages that didn't have them already, because on those messages Inno +; Setup adds the periods automatically (appending a period would result in +; two periods being displayed). +; +; Maintained by Zhenghan Yang +; Email: 847320916@QQ.com +; Translation based on network resource +; The latest Translation is on https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation +; + +[LangOptions] +; The following three entries are very important. Be sure to read and +; understand the '[LangOptions] section' topic in the help file. +LanguageName=简体中文 +; If Language Name display incorrect, uncomment next line +; LanguageName=<7B80><4F53><4E2D><6587> +; About LanguageID, to reference link: +; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c +LanguageID=$0804 +; About CodePage, to reference link: +; https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers +LanguageCodePage=936 +; If the language you are translating to requires special font faces or +; sizes, uncomment any of the following entries and change them accordingly. +;DialogFontName= +;DialogFontSize=8 +;WelcomeFontName=Verdana +;WelcomeFontSize=12 +;TitleFontName=Arial +;TitleFontSize=29 +;CopyrightFontName=Arial +;CopyrightFontSize=8 + +[Messages] + +; *** 应用程序标题 +SetupAppTitle=安装 +SetupWindowTitle=安装 - %1 +UninstallAppTitle=卸载 +UninstallAppFullTitle=%1 卸载 + +; *** Misc. common +InformationTitle=信息 +ConfirmTitle=确认 +ErrorTitle=错误 + +; *** SetupLdr messages +SetupLdrStartupMessage=现在将安装 %1。您想要继续吗? +LdrCannotCreateTemp=无法创建临时文件。安装程序已中止 +LdrCannotExecTemp=无法执行临时目录中的文件。安装程序已中止 +HelpTextNote= + +; *** 启动错误消息 +LastErrorMessage=%1。%n%n错误 %2: %3 +SetupFileMissing=安装目录中缺少文件 %1。请修正这个问题或者获取程序的新副本。 +SetupFileCorrupt=安装文件已损坏。请获取程序的新副本。 +SetupFileCorruptOrWrongVer=安装文件已损坏,或是与这个安装程序的版本不兼容。请修正这个问题或获取新的程序副本。 +InvalidParameter=无效的命令行参数:%n%n%1 +SetupAlreadyRunning=安装程序正在运行。 +WindowsVersionNotSupported=此程序不支持当前计算机运行的 Windows 版本。 +WindowsServicePackRequired=此程序需要 %1 服务包 %2 或更高版本。 +NotOnThisPlatform=此程序不能在 %1 上运行。 +OnlyOnThisPlatform=此程序只能在 %1 上运行。 +OnlyOnTheseArchitectures=此程序只能安装到为下列处理器架构设计的 Windows 版本中:%n%n%1 +WinVersionTooLowError=此程序需要 %1 版本 %2 或更高。 +WinVersionTooHighError=此程序不能安装于 %1 版本 %2 或更高。 +AdminPrivilegesRequired=在安装此程序时您必须以管理员身份登录。 +PowerUserPrivilegesRequired=在安装此程序时您必须以管理员身份或有权限的用户组身份登录。 +SetupAppRunningError=安装程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。 +UninstallAppRunningError=卸载程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。 + +; *** 启动问题 +PrivilegesRequiredOverrideTitle=选择安装程序模式 +PrivilegesRequiredOverrideInstruction=选择安装模式 +PrivilegesRequiredOverrideText1=%1 可以为所有用户安装(需要管理员权限),或仅为您安装。 +PrivilegesRequiredOverrideText2=%1 只能为您安装,或为所有用户安装(需要管理员权限)。 +PrivilegesRequiredOverrideAllUsers=为所有用户安装(&A) +PrivilegesRequiredOverrideAllUsersRecommended=为所有用户安装(&A) (建议选项) +PrivilegesRequiredOverrideCurrentUser=只为我安装(&M) +PrivilegesRequiredOverrideCurrentUserRecommended=只为我安装(&M) (建议选项) + +; *** 其他错误 +ErrorCreatingDir=安装程序无法创建目录“%1” +ErrorTooManyFilesInDir=无法在目录“%1”中创建文件,因为里面包含太多文件 + +; *** 安装程序公共消息 +ExitSetupTitle=退出安装程序 +ExitSetupMessage=安装程序尚未完成。如果现在退出,将不会安装该程序。%n%n您之后可以再次运行安装程序完成安装。%n%n现在退出安装程序吗? +AboutSetupMenuItem=关于安装程序(&A)... +AboutSetupTitle=关于安装程序 +AboutSetupMessage=%1 版本 %2%n%3%n%n%1 主页:%n%4 +AboutSetupNote= +TranslatorNote=简体中文翻译由Kira(847320916@qq.com)维护。项目地址:https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation + +; *** 按钮 +ButtonBack=< 上一步(&B) +ButtonNext=下一步(&N) > +ButtonInstall=安装(&I) +ButtonOK=确定 +ButtonCancel=取消 +ButtonYes=是(&Y) +ButtonYesToAll=全是(&A) +ButtonNo=否(&N) +ButtonNoToAll=全否(&O) +ButtonFinish=完成(&F) +ButtonBrowse=浏览(&B)... +ButtonWizardBrowse=浏览(&R)... +ButtonNewFolder=新建文件夹(&M) + +; *** “选择语言”对话框消息 +SelectLanguageTitle=选择安装语言 +SelectLanguageLabel=选择安装时使用的语言。 + +; *** 公共向导文字 +ClickNext=点击“下一步”继续,或点击“取消”退出安装程序。 +BeveledLabel= +BrowseDialogTitle=浏览文件夹 +BrowseDialogLabel=在下面的列表中选择一个文件夹,然后点击“确定”。 +NewFolderName=新建文件夹 + +; *** “欢迎”向导页 +WelcomeLabel1=欢迎使用 [name] 安装向导 +WelcomeLabel2=现在将安装 [name/ver] 到您的电脑中。%n%n建议您在继续安装前关闭所有其他应用程序。 + +; *** “密码”向导页 +WizardPassword=密码 +PasswordLabel1=这个安装程序有密码保护。 +PasswordLabel3=请输入密码,然后点击“下一步”继续。密码区分大小写。 +PasswordEditLabel=密码(&P): +IncorrectPassword=您输入的密码不正确,请重新输入。 + +; *** “许可协议”向导页 +WizardLicense=许可协议 +LicenseLabel=请在继续安装前阅读以下重要信息。 +LicenseLabel3=请仔细阅读下列许可协议。在继续安装前您必须同意这些协议条款。 +LicenseAccepted=我同意此协议(&A) +LicenseNotAccepted=我不同意此协议(&D) + +; *** “信息”向导页 +WizardInfoBefore=信息 +InfoBeforeLabel=请在继续安装前阅读以下重要信息。 +InfoBeforeClickLabel=准备好继续安装后,点击“下一步”。 +WizardInfoAfter=信息 +InfoAfterLabel=请在继续安装前阅读以下重要信息。 +InfoAfterClickLabel=准备好继续安装后,点击“下一步”。 + +; *** “用户信息”向导页 +WizardUserInfo=用户信息 +UserInfoDesc=请输入您的信息。 +UserInfoName=用户名(&U): +UserInfoOrg=组织(&O): +UserInfoSerial=序列号(&S): +UserInfoNameRequired=您必须输入用户名。 + +; *** “选择目标目录”向导页 +WizardSelectDir=选择目标位置 +SelectDirDesc=您想将 [name] 安装在哪里? +SelectDirLabel3=安装程序将安装 [name] 到下面的文件夹中。 +SelectDirBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。 +DiskSpaceGBLabel=至少需要有 [gb] GB 的可用磁盘空间。 +DiskSpaceMBLabel=至少需要有 [mb] MB 的可用磁盘空间。 +CannotInstallToNetworkDrive=安装程序无法安装到一个网络驱动器。 +CannotInstallToUNCPath=安装程序无法安装到一个 UNC 路径。 +InvalidPath=您必须输入一个带驱动器卷标的完整路径,例如:%n%nC:\APP%n%n或UNC路径:%n%n\\server\share +InvalidDrive=您选定的驱动器或 UNC 共享不存在或不能访问。请选择其他位置。 +DiskSpaceWarningTitle=磁盘空间不足 +DiskSpaceWarning=安装程序至少需要 %1 KB 的可用空间才能安装,但选定驱动器只有 %2 KB 的可用空间。%n%n您一定要继续吗? +DirNameTooLong=文件夹名称或路径太长。 +InvalidDirName=文件夹名称无效。 +BadDirName32=文件夹名称不能包含下列任何字符:%n%n%1 +DirExistsTitle=文件夹已存在 +DirExists=文件夹:%n%n%1%n%n已经存在。您一定要安装到这个文件夹中吗? +DirDoesntExistTitle=文件夹不存在 +DirDoesntExist=文件夹:%n%n%1%n%n不存在。您想要创建此文件夹吗? + +; *** “选择组件”向导页 +WizardSelectComponents=选择组件 +SelectComponentsDesc=您想安装哪些程序组件? +SelectComponentsLabel2=选中您想安装的组件;取消您不想安装的组件。然后点击“下一步”继续。 +FullInstallation=完全安装 +; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language) +CompactInstallation=简洁安装 +CustomInstallation=自定义安装 +NoUninstallWarningTitle=组件已存在 +NoUninstallWarning=安装程序检测到下列组件已安装在您的电脑中:%n%n%1%n%n取消选中这些组件不会卸载它们。%n%n确定要继续吗? +ComponentSize1=%1 KB +ComponentSize2=%1 MB +ComponentsDiskSpaceGBLabel=当前选择的组件需要至少 [gb] GB 的磁盘空间。 +ComponentsDiskSpaceMBLabel=当前选择的组件需要至少 [mb] MB 的磁盘空间。 + +; *** “选择附加任务”向导页 +WizardSelectTasks=选择附加任务 +SelectTasksDesc=您想要安装程序执行哪些附加任务? +SelectTasksLabel2=选择您想要安装程序在安装 [name] 时执行的附加任务,然后点击“下一步”。 + +; *** “选择开始菜单文件夹”向导页 +WizardSelectProgramGroup=选择开始菜单文件夹 +SelectStartMenuFolderDesc=安装程序应该在哪里放置程序的快捷方式? +SelectStartMenuFolderLabel3=安装程序将在下列“开始”菜单文件夹中创建程序的快捷方式。 +SelectStartMenuFolderBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。 +MustEnterGroupName=您必须输入一个文件夹名。 +GroupNameTooLong=文件夹名或路径太长。 +InvalidGroupName=无效的文件夹名字。 +BadGroupName=文件夹名不能包含下列任何字符:%n%n%1 +NoProgramGroupCheck2=不创建开始菜单文件夹(&D) + +; *** “准备安装”向导页 +WizardReady=准备安装 +ReadyLabel1=安装程序准备就绪,现在可以开始安装 [name] 到您的电脑。 +ReadyLabel2a=点击“安装”继续此安装程序。如果您想重新考虑或修改任何设置,点击“上一步”。 +ReadyLabel2b=点击“安装”继续此安装程序。 +ReadyMemoUserInfo=用户信息: +ReadyMemoDir=目标位置: +ReadyMemoType=安装类型: +ReadyMemoComponents=已选择组件: +ReadyMemoGroup=开始菜单文件夹: +ReadyMemoTasks=附加任务: + +; *** TExtractionWizardPage wizard page and Extract7ZipArchive +ExtractionLabel=正在提取附加文件... +ButtonStopExtraction=停止提取(&S) +StopExtraction=您确定要停止提取吗? +ErrorExtractionAborted=提取已中止 +ErrorExtractionFailed=提取失败:%1 + +; *** TDownloadWizardPage wizard page and DownloadTemporaryFile +DownloadingLabel=正在下载附加文件... +ButtonStopDownload=停止下载(&S) +StopDownload=您确定要停止下载吗? +ErrorDownloadAborted=下载已中止 +ErrorDownloadFailed=下载失败:%1 %2 +ErrorDownloadSizeFailed=获取下载大小失败:%1 %2 +ErrorFileHash1=校验文件哈希失败:%1 +ErrorFileHash2=无效的文件哈希:预期 %1,实际 %2 +ErrorProgress=无效的进度:%1 / %2 +ErrorFileSize=文件大小错误:预期 %1,实际 %2 + +; *** “正在准备安装”向导页 +WizardPreparing=正在准备安装 +PreparingDesc=安装程序正在准备安装 [name] 到您的电脑。 +PreviousInstallNotCompleted=先前的程序安装或卸载未完成,您需要重启您的电脑以完成。%n%n在重启电脑后,再次运行安装程序以完成 [name] 的安装。 +CannotContinue=安装程序不能继续。请点击“取消”退出。 +ApplicationsFound=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。 +ApplicationsFound2=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。安装完成后,安装程序将尝试重新启动这些应用程序。 +CloseApplications=自动关闭应用程序(&A) +DontCloseApplications=不要关闭应用程序(&D) +ErrorCloseApplications=安装程序无法自动关闭所有应用程序。建议您在继续之前,关闭所有在使用需要由安装程序更新的文件的应用程序。 +PrepareToInstallNeedsRestart=安装程序必须重启您的计算机。计算机重启后,请再次运行安装程序以完成 [name] 的安装。%n%n是否立即重新启动? + +; *** “正在安装”向导页 +WizardInstalling=正在安装 +InstallingLabel=安装程序正在安装 [name] 到您的电脑,请稍候。 + +; *** “安装完成”向导页 +FinishedHeadingLabel=[name] 安装完成 +FinishedLabelNoIcons=安装程序已在您的电脑中安装了 [name]。 +FinishedLabel=安装程序已在您的电脑中安装了 [name]。您可以通过已安装的快捷方式运行此应用程序。 +ClickFinish=点击“完成”退出安装程序。 +FinishedRestartLabel=为完成 [name] 的安装,安装程序必须重新启动您的电脑。要立即重启吗? +FinishedRestartMessage=为完成 [name] 的安装,安装程序必须重新启动您的电脑。%n%n要立即重启吗? +ShowReadmeCheck=是,我想查阅自述文件 +YesRadio=是,立即重启电脑(&Y) +NoRadio=否,稍后重启电脑(&N) +; used for example as 'Run MyProg.exe' +RunEntryExec=运行 %1 +; used for example as 'View Readme.txt' +RunEntryShellExec=查阅 %1 + +; *** “安装程序需要下一张磁盘”提示 +ChangeDiskTitle=安装程序需要下一张磁盘 +SelectDiskLabel2=请插入磁盘 %1 并点击“确定”。%n%n如果这个磁盘中的文件可以在下列文件夹之外的文件夹中找到,请输入正确的路径或点击“浏览”。 +PathLabel=路径(&P): +FileNotInDir2=“%2”中找不到文件“%1”。请插入正确的磁盘或选择其他文件夹。 +SelectDirectoryLabel=请指定下一张磁盘的位置。 + +; *** 安装状态消息 +SetupAborted=安装程序未完成安装。%n%n请修正这个问题并重新运行安装程序。 +AbortRetryIgnoreSelectAction=选择操作 +AbortRetryIgnoreRetry=重试(&T) +AbortRetryIgnoreIgnore=忽略错误并继续(&I) +AbortRetryIgnoreCancel=关闭安装程序 + +; *** 安装状态消息 +StatusClosingApplications=正在关闭应用程序... +StatusCreateDirs=正在创建目录... +StatusExtractFiles=正在解压缩文件... +StatusCreateIcons=正在创建快捷方式... +StatusCreateIniEntries=正在创建 INI 条目... +StatusCreateRegistryEntries=正在创建注册表条目... +StatusRegisterFiles=正在注册文件... +StatusSavingUninstall=正在保存卸载信息... +StatusRunProgram=正在完成安装... +StatusRestartingApplications=正在重启应用程序... +StatusRollback=正在撤销更改... + +; *** 其他错误 +ErrorInternal2=内部错误:%1 +ErrorFunctionFailedNoCode=%1 失败 +ErrorFunctionFailed=%1 失败;错误代码 %2 +ErrorFunctionFailedWithMessage=%1 失败;错误代码 %2.%n%3 +ErrorExecutingProgram=无法执行文件:%n%1 + +; *** 注册表错误 +ErrorRegOpenKey=打开注册表项时出错:%n%1\%2 +ErrorRegCreateKey=创建注册表项时出错:%n%1\%2 +ErrorRegWriteKey=写入注册表项时出错:%n%1\%2 + +; *** INI 错误 +ErrorIniEntry=在文件“%1”中创建 INI 条目时出错。 + +; *** 文件复制错误 +FileAbortRetryIgnoreSkipNotRecommended=跳过此文件(&S) (不推荐) +FileAbortRetryIgnoreIgnoreNotRecommended=忽略错误并继续(&I) (不推荐) +SourceIsCorrupted=源文件已损坏 +SourceDoesntExist=源文件“%1”不存在 +ExistingFileReadOnly2=无法替换现有文件,它是只读的。 +ExistingFileReadOnlyRetry=移除只读属性并重试(&R) +ExistingFileReadOnlyKeepExisting=保留现有文件(&K) +ErrorReadingExistingDest=尝试读取现有文件时出错: +FileExistsSelectAction=选择操作 +FileExists2=文件已经存在。 +FileExistsOverwriteExisting=覆盖已存在的文件(&O) +FileExistsKeepExisting=保留现有的文件(&K) +FileExistsOverwriteOrKeepAll=为所有冲突文件执行此操作(&D) +ExistingFileNewerSelectAction=选择操作 +ExistingFileNewer2=现有的文件比安装程序将要安装的文件还要新。 +ExistingFileNewerOverwriteExisting=覆盖已存在的文件(&O) +ExistingFileNewerKeepExisting=保留现有的文件(&K) (推荐) +ExistingFileNewerOverwriteOrKeepAll=为所有冲突文件执行此操作(&D) +ErrorChangingAttr=尝试更改下列现有文件的属性时出错: +ErrorCreatingTemp=尝试在目标目录创建文件时出错: +ErrorReadingSource=尝试读取下列源文件时出错: +ErrorCopying=尝试复制下列文件时出错: +ErrorReplacingExistingFile=尝试替换现有文件时出错: +ErrorRestartReplace=重启并替换失败: +ErrorRenamingTemp=尝试重命名下列目标目录中的一个文件时出错: +ErrorRegisterServer=无法注册 DLL/OCX:%1 +ErrorRegSvr32Failed=RegSvr32 失败;退出代码 %1 +ErrorRegisterTypeLib=无法注册类库:%1 + +; *** 卸载显示名字标记 +; used for example as 'My Program (32-bit)' +UninstallDisplayNameMark=%1 (%2) +; used for example as 'My Program (32-bit, All users)' +UninstallDisplayNameMarks=%1 (%2, %3) +UninstallDisplayNameMark32Bit=32 位 +UninstallDisplayNameMark64Bit=64 位 +UninstallDisplayNameMarkAllUsers=所有用户 +UninstallDisplayNameMarkCurrentUser=当前用户 + +; *** 安装后错误 +ErrorOpeningReadme=尝试打开自述文件时出错。 +ErrorRestartingComputer=安装程序无法重启电脑,请手动重启。 + +; *** 卸载消息 +UninstallNotFound=文件“%1”不存在。无法卸载。 +UninstallOpenError=文件“%1”不能被打开。无法卸载。 +UninstallUnsupportedVer=此版本的卸载程序无法识别卸载日志文件“%1”的格式。无法卸载 +UninstallUnknownEntry=卸载日志中遇到一个未知条目 (%1) +ConfirmUninstall=您确认要完全移除 %1 及其所有组件吗? +UninstallOnlyOnWin64=仅允许在 64 位 Windows 中卸载此程序。 +OnlyAdminCanUninstall=仅使用管理员权限的用户能完成此卸载。 +UninstallStatusLabel=正在从您的电脑中移除 %1,请稍候。 +UninstalledAll=已顺利从您的电脑中移除 %1。 +UninstalledMost=%1 卸载完成。%n%n有部分内容未能被删除,但您可以手动删除它们。 +UninstalledAndNeedsRestart=为完成 %1 的卸载,需要重启您的电脑。%n%n立即重启电脑吗? +UninstallDataCorrupted=文件“%1”已损坏。无法卸载 + +; *** 卸载状态消息 +ConfirmDeleteSharedFileTitle=删除共享的文件吗? +ConfirmDeleteSharedFile2=系统表示下列共享的文件已不有其他程序使用。您希望卸载程序删除这些共享的文件吗?%n%n如果删除这些文件,但仍有程序在使用这些文件,则这些程序可能出现异常。如果您不能确定,请选择“否”,在系统中保留这些文件以免引发问题。 +SharedFileNameLabel=文件名: +SharedFileLocationLabel=位置: +WizardUninstalling=卸载状态 +StatusUninstalling=正在卸载 %1... + +; *** Shutdown block reasons +ShutdownBlockReasonInstallingApp=正在安装 %1。 +ShutdownBlockReasonUninstallingApp=正在卸载 %1。 + +; The custom messages below aren't used by Setup itself, but if you make +; use of them in your scripts, you'll want to translate them. + +[CustomMessages] + +NameAndVersion=%1 版本 %2 +AdditionalIcons=附加快捷方式: +CreateDesktopIcon=创建桌面快捷方式(&D) +CreateQuickLaunchIcon=创建快速启动栏快捷方式(&Q) +ProgramOnTheWeb=%1 网站 +UninstallProgram=卸载 %1 +LaunchProgram=运行 %1 +AssocFileExtension=将 %2 文件扩展名与 %1 建立关联(&A) +AssocingFileExtension=正在将 %2 文件扩展名与 %1 建立关联... +AutoStartProgramGroupDescription=启动: +AutoStartProgram=自动启动 %1 +AddonHostProgramNotFound=您选择的文件夹中无法找到 %1。%n%n您要继续吗? \ No newline at end of file diff --git a/resources/docs/MAA_config_info.txt b/resources/docs/MAA_config_info.txt new file mode 100644 index 0000000..a19847f --- /dev/null +++ b/resources/docs/MAA_config_info.txt @@ -0,0 +1,65 @@ +#主界面 +"MainFunction.PostActions": "8" #完成后退出MAA +"MainFunction.PostActions": "9" #完成后退出MAA和游戏 +"MainFunction.PostActions": "12" #完成后退出MAA和模拟器 +"TaskQueue.WakeUp.IsChecked": "True" #开始唤醒 +"TaskQueue.Recruiting.IsChecked": "True" #自动公招 +"TaskQueue.Base.IsChecked": "True" #基建换班 +"TaskQueue.Combat.IsChecked": "True" #刷理智 +"TaskQueue.Mall.IsChecked": "True" #获取信用及购物 +"TaskQueue.Mission.IsChecked": "True" #领取奖励 +"TaskQueue.AutoRoguelike.IsChecked": "False" #自动肉鸽 +"TaskQueue.Reclamation.IsChecked": "False" #生息演算 +"TaskQueue.Order.WakeUp": "0" +"TaskQueue.Order.Recruiting": "1" +"TaskQueue.Order.Base": "2" +"TaskQueue.Order.Combat": "3" +"TaskQueue.Order.Mall": "4" +"TaskQueue.Order.Mission": "5" +"TaskQueue.Order.AutoRoguelike": "6" +"TaskQueue.Order.Reclamation": "7" +#刷理智 +"MainFunction.UseMedicine": "True" #吃理智药 +"MainFunction.UseMedicine.Quantity": "999" #吃理智药数量 +"MainFunction.Stage1": "" #主关卡 +"MainFunction.Stage2": "" #备选关卡1 +"MainFunction.Stage3": "" #备选关卡2 +"MainFunction.Stage4": "" #备选关卡3 +"Fight.RemainingSanityStage": "Annihilation" #剩余理智关卡 +"MainFunction.Series.Quantity": "1" #连战次数 +"MainFunction.Annihilation.UseCustom": "True" #自定义剿灭关卡 +"MainFunction.Annihilation.Stage": "Annihilation"、"Chernobog@Annihilation"、"LungmenOutskirts@Annihilation"、"LungmenDowntown@Annihilation" #自定义剿灭关卡号 +"Penguin.IsDrGrandet": "True" #博朗台模式 +"GUI.CustomStageCode": "False" #手动输入关卡名 +"GUI.UseAlternateStage": "False" #使用备选关卡 +"Fight.UseRemainingSanityStage": "True" #使用剩余理智 +"GUI.AllowUseStoneSave": "False" #允许吃源石保持状态 +"Fight.UseExpiringMedicine": "False" #无限吃48小时内过期的理智药 +"GUI.HideUnavailableStage": "False" #隐藏当日不开放关卡 +"GUI.HideSeries": "False" #隐藏连战次数 +"Infrast.CustomInfrastPlanShowInFightSettings": "False" #显示基建计划 +"Penguin.EnablePenguin": "True" #上报企鹅物流 +"Yituliu.EnableYituliu": "True" #上报一图流 +#基建换班 +"Infrast.InfrastMode": "Normal"、"Rotation"、"Custom" #基建模式 +"Infrast.CustomInfrastPlanIndex": "1" #自定义基建配置索引号 +"Infrast.DefaultInfrast": "user_defined" #内置配置 +"Infrast.IsCustomInfrastFileReadOnly": "False" #自定义基建配置文件只读 +"Infrast.CustomInfrastFile": "" #自定义基建配置文件地址 +#设置 +"Start.ClientType": "Official"、"Bilibili"、"YoStarEN"、"YoStarJP"、"YoStarKR"、"txwy" #服务器 +G"Timer.Timer1": "False" #时间设置1 +"Connect.AdbPath" #ADB路径 +"Connect.Address": "127.0.0.1:16448" #连接地址 +G"VersionUpdate.ScheduledUpdateCheck": "True" #定时检查更新 +G"VersionUpdate.AutoDownloadUpdatePackage": "True" #自动下载更新包 +G"VersionUpdate.AutoInstallUpdatePackage": "True" #自动安装更新包 +G"Start.MinimizeDirectly": "True" #启动MAA后直接最小化 +"Start.RunDirectly": "True" #启动MAA后直接运行 +"Start.OpenEmulatorAfterLaunch": "True" #启动MAA后自动开启模拟器 +G"GUI.UseTray": "True" #显示托盘图标 +G"GUI.MinimizeToTray": "False" #最小化时隐藏至托盘 +"Start.EmulatorPath" #模拟器路径 +"Start.EmulatorAddCommand": "-v 2" #附加命令 +"Start.EmulatorWaitSeconds": "10" #等待模拟器启动时间 +G"VersionUpdate.package": "MirrorChyanAppv5.15.6.zip" #更新包标识 \ No newline at end of file diff --git a/resources/html/MAA_result.html b/resources/html/MAA_result.html new file mode 100644 index 0000000..78328d2 --- /dev/null +++ b/resources/html/MAA_result.html @@ -0,0 +1,160 @@ + + + + + + + + +
+
+

{{ title }}

+ + +
+ +
+

脚本实例名称:{{ script_name }}

+

任务开始时间:{{ start_time }}

+

任务结束时间:{{ end_time }}

+

已完成数:{{ completed_count }}

+ {% if uncompleted_count %} +

未完成数:{{ uncompleted_count }}

+ {% endif %} + {% if failed_user %} +

代理未成功的用户: {{ failed_user }}

+ {% endif %} + {% if waiting_user %} +

未开始代理的用户: {{ waiting_user }}

+ {% endif %} +
+ +

AUTO_MAA 敬上

+ + + +
+ + + \ No newline at end of file diff --git a/resources/html/MAA_six_star.html b/resources/html/MAA_six_star.html new file mode 100644 index 0000000..ef38222 --- /dev/null +++ b/resources/html/MAA_six_star.html @@ -0,0 +1,26 @@ + + + + + + + + + Base64 Image + + + \ No newline at end of file diff --git a/resources/html/MAA_statistics.html b/resources/html/MAA_statistics.html new file mode 100644 index 0000000..561faa4 --- /dev/null +++ b/resources/html/MAA_statistics.html @@ -0,0 +1,233 @@ + + + + + + + + +
+
+

自动代理统计报告

+ + +
+ +
+

用户代理信息:{{ user_info }}

+

任务开始时间:{{ start_time }}

+

任务结束时间:{{ end_time }}

+

MAA执行结果: + {% if maa_result == '代理任务全部完成' %} + {{ maa_result }} + {% elif maa_result == '代理任务未全部完成' %} + {{ maa_result }} + {% else %} + {{ maa_result }} + {% endif %} +

+ + {% if recruit_statistics %} +

公招统计

+ + + + + + {% for star, count in recruit_statistics.items() %} + + + + + {% endfor %} +
星级数量
{{ star }}{{ count }}
+ {% endif %} + + {% if drop_statistics %} + {% for stage, items in drop_statistics.items() %} +

掉落统计({{ stage }})

+ + + + + + {% for item, amount in items.items() %} + + + + + {% endfor %} +
物品数量
{{ item }}{{ amount }}
+ {% endfor %} + {% endif %} +
+ +

AUTO_MAA 敬上

+ + + +
+ + + \ No newline at end of file diff --git a/resources/html/general_result.html b/resources/html/general_result.html new file mode 100644 index 0000000..1a1c842 --- /dev/null +++ b/resources/html/general_result.html @@ -0,0 +1,160 @@ + + + + + + + + +
+
+

{{ title }}

+ + +
+ +
+

脚本实例名称:{{ script_name }}

+

任务开始时间:{{ start_time }}

+

任务结束时间:{{ end_time }}

+

已完成数:{{ completed_count }}

+ {% if uncompleted_count %} +

未完成数:{{ uncompleted_count }}

+ {% endif %} + {% if failed_sub %} +

代理未成功的配置: {{ failed_sub }}

+ {% endif %} + {% if waiting_sub %} +

未开始代理的配置: {{ waiting_sub }}

+ {% endif %} +
+ +

AUTO_MAA 敬上

+ + + +
+ + + \ No newline at end of file diff --git a/resources/html/general_statistics.html b/resources/html/general_statistics.html new file mode 100644 index 0000000..9e577e0 --- /dev/null +++ b/resources/html/general_statistics.html @@ -0,0 +1,200 @@ + + + + + + + + +
+
+

自动代理统计报告

+ + +
+ +
+

用户代理信息:{{ sub_info }}

+

任务开始时间:{{ start_time }}

+

任务结束时间:{{ end_time }}

+

脚本执行结果: + {% if sub_result == '代理成功' %} + {{ sub_result }} + {% elif sub_result == '代理失败' %} + {{ sub_result }} + {% else %} + {{ sub_result }} + {% endif %} +

+ +
+ +

AUTO_MAA 敬上

+ + + +
+ + + \ No newline at end of file diff --git a/resources/icons/AUTO_MAA.ico b/resources/icons/AUTO_MAA.ico new file mode 100644 index 0000000..6f9c7ae Binary files /dev/null and b/resources/icons/AUTO_MAA.ico differ diff --git a/resources/icons/AUTO_MAA_Updater.ico b/resources/icons/AUTO_MAA_Updater.ico new file mode 100644 index 0000000..120746a Binary files /dev/null and b/resources/icons/AUTO_MAA_Updater.ico differ diff --git a/resources/icons/MirrorChyan.ico b/resources/icons/MirrorChyan.ico new file mode 100644 index 0000000..2223251 Binary files /dev/null and b/resources/icons/MirrorChyan.ico differ diff --git a/resources/images/AUTO_MAA.png b/resources/images/AUTO_MAA.png new file mode 100644 index 0000000..669770c Binary files /dev/null and b/resources/images/AUTO_MAA.png differ diff --git a/resources/images/Home/BannerDefault.png b/resources/images/Home/BannerDefault.png new file mode 100644 index 0000000..ac4c489 Binary files /dev/null and b/resources/images/Home/BannerDefault.png differ diff --git a/resources/images/README/payid.png b/resources/images/README/payid.png new file mode 100644 index 0000000..91b671e Binary files /dev/null and b/resources/images/README/payid.png differ diff --git a/resources/images/notification/six_star.png b/resources/images/notification/six_star.png new file mode 100644 index 0000000..2ef8c38 Binary files /dev/null and b/resources/images/notification/six_star.png differ diff --git a/resources/images/notification/test_notify.png b/resources/images/notification/test_notify.png new file mode 100644 index 0000000..6429431 Binary files /dev/null and b/resources/images/notification/test_notify.png differ diff --git a/resources/sounds/both/删除用户.wav b/resources/sounds/both/删除用户.wav new file mode 100644 index 0000000..779c63d Binary files /dev/null and b/resources/sounds/both/删除用户.wav differ diff --git a/resources/sounds/both/删除脚本实例.wav b/resources/sounds/both/删除脚本实例.wav new file mode 100644 index 0000000..5bfad34 Binary files /dev/null and b/resources/sounds/both/删除脚本实例.wav differ diff --git a/resources/sounds/both/删除计划表.wav b/resources/sounds/both/删除计划表.wav new file mode 100644 index 0000000..5a58ad6 Binary files /dev/null and b/resources/sounds/both/删除计划表.wav differ diff --git a/resources/sounds/both/删除调度队列.wav b/resources/sounds/both/删除调度队列.wav new file mode 100644 index 0000000..3abe7e1 Binary files /dev/null and b/resources/sounds/both/删除调度队列.wav differ diff --git a/resources/sounds/both/欢迎回来.wav b/resources/sounds/both/欢迎回来.wav new file mode 100644 index 0000000..47e410b Binary files /dev/null and b/resources/sounds/both/欢迎回来.wav differ diff --git a/resources/sounds/both/添加用户.wav b/resources/sounds/both/添加用户.wav new file mode 100644 index 0000000..96eafea Binary files /dev/null and b/resources/sounds/both/添加用户.wav differ diff --git a/resources/sounds/both/添加脚本实例.wav b/resources/sounds/both/添加脚本实例.wav new file mode 100644 index 0000000..223170b Binary files /dev/null and b/resources/sounds/both/添加脚本实例.wav differ diff --git a/resources/sounds/both/添加计划表.wav b/resources/sounds/both/添加计划表.wav new file mode 100644 index 0000000..1059282 Binary files /dev/null and b/resources/sounds/both/添加计划表.wav differ diff --git a/resources/sounds/both/添加调度队列.wav b/resources/sounds/both/添加调度队列.wav new file mode 100644 index 0000000..c1159d9 Binary files /dev/null and b/resources/sounds/both/添加调度队列.wav differ diff --git a/resources/sounds/noisy/ADB失败.wav b/resources/sounds/noisy/ADB失败.wav new file mode 100644 index 0000000..a432d67 Binary files /dev/null and b/resources/sounds/noisy/ADB失败.wav differ diff --git a/resources/sounds/noisy/ADB成功.wav b/resources/sounds/noisy/ADB成功.wav new file mode 100644 index 0000000..39f5022 Binary files /dev/null and b/resources/sounds/noisy/ADB成功.wav differ diff --git a/resources/sounds/noisy/MAA在完成任务前中止.wav b/resources/sounds/noisy/MAA在完成任务前中止.wav new file mode 100644 index 0000000..23c1b8f Binary files /dev/null and b/resources/sounds/noisy/MAA在完成任务前中止.wav differ diff --git a/resources/sounds/noisy/MAA在完成任务前退出.wav b/resources/sounds/noisy/MAA在完成任务前退出.wav new file mode 100644 index 0000000..9aed847 Binary files /dev/null and b/resources/sounds/noisy/MAA在完成任务前退出.wav differ diff --git a/resources/sounds/noisy/MAA更新.wav b/resources/sounds/noisy/MAA更新.wav new file mode 100644 index 0000000..f14afeb Binary files /dev/null and b/resources/sounds/noisy/MAA更新.wav differ diff --git a/resources/sounds/noisy/MAA未检测到任何模拟器.wav b/resources/sounds/noisy/MAA未检测到任何模拟器.wav new file mode 100644 index 0000000..675361d Binary files /dev/null and b/resources/sounds/noisy/MAA未检测到任何模拟器.wav differ diff --git a/resources/sounds/noisy/MAA未能正确登录PRTS.wav b/resources/sounds/noisy/MAA未能正确登录PRTS.wav new file mode 100644 index 0000000..e0b55fe Binary files /dev/null and b/resources/sounds/noisy/MAA未能正确登录PRTS.wav differ diff --git a/resources/sounds/noisy/MAA的ADB连接异常.wav b/resources/sounds/noisy/MAA的ADB连接异常.wav new file mode 100644 index 0000000..0db037b Binary files /dev/null and b/resources/sounds/noisy/MAA的ADB连接异常.wav differ diff --git a/resources/sounds/noisy/MAA进程超时.wav b/resources/sounds/noisy/MAA进程超时.wav new file mode 100644 index 0000000..cf5b5b4 Binary files /dev/null and b/resources/sounds/noisy/MAA进程超时.wav differ diff --git a/resources/sounds/noisy/MAA部分任务执行失败.wav b/resources/sounds/noisy/MAA部分任务执行失败.wav new file mode 100644 index 0000000..563fd32 Binary files /dev/null and b/resources/sounds/noisy/MAA部分任务执行失败.wav differ diff --git a/resources/sounds/noisy/任务开始.wav b/resources/sounds/noisy/任务开始.wav new file mode 100644 index 0000000..acda117 Binary files /dev/null and b/resources/sounds/noisy/任务开始.wav differ diff --git a/resources/sounds/noisy/任务结束.wav b/resources/sounds/noisy/任务结束.wav new file mode 100644 index 0000000..d1e8eac Binary files /dev/null and b/resources/sounds/noisy/任务结束.wav differ diff --git a/resources/sounds/noisy/公告展示.wav b/resources/sounds/noisy/公告展示.wav new file mode 100644 index 0000000..91910f2 Binary files /dev/null and b/resources/sounds/noisy/公告展示.wav differ diff --git a/resources/sounds/noisy/公告通知.wav b/resources/sounds/noisy/公告通知.wav new file mode 100644 index 0000000..e3b833e Binary files /dev/null and b/resources/sounds/noisy/公告通知.wav differ diff --git a/resources/sounds/noisy/六星喜报.wav b/resources/sounds/noisy/六星喜报.wav new file mode 100644 index 0000000..f6732f8 Binary files /dev/null and b/resources/sounds/noisy/六星喜报.wav differ diff --git a/resources/sounds/noisy/历史记录查询.wav b/resources/sounds/noisy/历史记录查询.wav new file mode 100644 index 0000000..7f1e7b5 Binary files /dev/null and b/resources/sounds/noisy/历史记录查询.wav differ diff --git a/resources/sounds/noisy/发生异常.wav b/resources/sounds/noisy/发生异常.wav new file mode 100644 index 0000000..dca513c Binary files /dev/null and b/resources/sounds/noisy/发生异常.wav differ diff --git a/resources/sounds/noisy/发生错误.wav b/resources/sounds/noisy/发生错误.wav new file mode 100644 index 0000000..da8733a Binary files /dev/null and b/resources/sounds/noisy/发生错误.wav differ diff --git a/resources/sounds/noisy/子任务失败.wav b/resources/sounds/noisy/子任务失败.wav new file mode 100644 index 0000000..1b798f7 Binary files /dev/null and b/resources/sounds/noisy/子任务失败.wav differ diff --git a/resources/sounds/noisy/排查录入.wav b/resources/sounds/noisy/排查录入.wav new file mode 100644 index 0000000..9c40147 Binary files /dev/null and b/resources/sounds/noisy/排查录入.wav differ diff --git a/resources/sounds/noisy/排查重试.wav b/resources/sounds/noisy/排查重试.wav new file mode 100644 index 0000000..185f190 Binary files /dev/null and b/resources/sounds/noisy/排查重试.wav differ diff --git a/resources/sounds/noisy/无新版本.wav b/resources/sounds/noisy/无新版本.wav new file mode 100644 index 0000000..d03ed3c Binary files /dev/null and b/resources/sounds/noisy/无新版本.wav differ diff --git a/resources/sounds/noisy/有新版本.wav b/resources/sounds/noisy/有新版本.wav new file mode 100644 index 0000000..7de578f Binary files /dev/null and b/resources/sounds/noisy/有新版本.wav differ diff --git a/resources/sounds/noisy/森空岛签到失败.wav b/resources/sounds/noisy/森空岛签到失败.wav new file mode 100644 index 0000000..4ad31d5 Binary files /dev/null and b/resources/sounds/noisy/森空岛签到失败.wav differ diff --git a/resources/sounds/noisy/森空岛签到成功.wav b/resources/sounds/noisy/森空岛签到成功.wav new file mode 100644 index 0000000..fd6319f Binary files /dev/null and b/resources/sounds/noisy/森空岛签到成功.wav differ diff --git a/resources/sounds/simple/任务开始.wav b/resources/sounds/simple/任务开始.wav new file mode 100644 index 0000000..37b86cc Binary files /dev/null and b/resources/sounds/simple/任务开始.wav differ diff --git a/resources/sounds/simple/任务结束.wav b/resources/sounds/simple/任务结束.wav new file mode 100644 index 0000000..3821fad Binary files /dev/null and b/resources/sounds/simple/任务结束.wav differ diff --git a/resources/sounds/simple/公告展示.wav b/resources/sounds/simple/公告展示.wav new file mode 100644 index 0000000..fc61355 Binary files /dev/null and b/resources/sounds/simple/公告展示.wav differ diff --git a/resources/sounds/simple/公告通知.wav b/resources/sounds/simple/公告通知.wav new file mode 100644 index 0000000..7dd88e9 Binary files /dev/null and b/resources/sounds/simple/公告通知.wav differ diff --git a/resources/sounds/simple/历史记录查询.wav b/resources/sounds/simple/历史记录查询.wav new file mode 100644 index 0000000..ac45b0d Binary files /dev/null and b/resources/sounds/simple/历史记录查询.wav differ diff --git a/resources/sounds/simple/发生异常.wav b/resources/sounds/simple/发生异常.wav new file mode 100644 index 0000000..8822c69 Binary files /dev/null and b/resources/sounds/simple/发生异常.wav differ diff --git a/resources/sounds/simple/发生错误.wav b/resources/sounds/simple/发生错误.wav new file mode 100644 index 0000000..633c5a9 Binary files /dev/null and b/resources/sounds/simple/发生错误.wav differ diff --git a/resources/sounds/simple/无新版本.wav b/resources/sounds/simple/无新版本.wav new file mode 100644 index 0000000..37509d9 Binary files /dev/null and b/resources/sounds/simple/无新版本.wav differ diff --git a/resources/sounds/simple/有新版本.wav b/resources/sounds/simple/有新版本.wav new file mode 100644 index 0000000..c49d409 Binary files /dev/null and b/resources/sounds/simple/有新版本.wav differ diff --git a/resources/version.json b/resources/version.json new file mode 100644 index 0000000..3d6eb9a --- /dev/null +++ b/resources/version.json @@ -0,0 +1,63 @@ +{ + "main_version": "4.4.1.0", + "version_info": { + "4.4.1.0": { + "新增功能": [ + "启动时支持直接运行复数调度队列" + ], + "修复BUG": [ + "修复计划表未能按照鹰历获取关卡号的问题" + ] + }, + "4.4.1.5": { + "新增功能": [ + "适配 MAA 长期开放剿灭关卡", + "新增完成任务后自动复原脚本配置", + "通用脚本启动附加命令添加额外的语法以适应UI可执行文件与任务可执行文件不同的情况", + "新增 Go_Updater 独立更新器" + ], + "程序优化": [ + "优化调度队列配置逻辑", + "优化静默进程标记逻辑,避免未能及时移除导致相关功能持续开启", + "MAA 代理时更新改为强制开启", + "移除 MAA 详细配置模式中的剿灭项" + ] + }, + "4.4.1.4": { + "修复BUG": [ + "添加强制关机功能并优化关机流程" + ] + }, + "4.4.1.3": { + "修复BUG": [ + "移除崩溃弹窗机制" + ] + }, + "4.4.1.2": { + "新增功能": [ + "AUTO_MAA 配置分享中心上线" + ], + "修复BUG": [ + "日志读取添加兜底机制", + "修复 QTimer.singleShot 参数问题" + ], + "程序优化": [ + "小文件配置信息转移至AUTO_MAA自建服务" + ] + }, + "4.4.1.1": { + "新增功能": [ + "通用脚本支持在选定的时机自动更新配置文件" + ], + "修复BUG": [ + "修复MAA掉落物统计功能", + "修复模拟器界面被异常关闭且无法重新打开的问题" + ], + "程序优化": [ + "重构日志记录,载入更多日志记录项", + "优化日志监看启停逻辑", + "SpinBox和TimeEdit组件忽视滚轮事件" + ] + } + } +} \ No newline at end of file