feat: 没测过的MAA调度方案

This commit is contained in:
DLmaster361
2025-08-09 01:50:38 +08:00
parent ba1fcd1f26
commit 9a87a62353
73 changed files with 4081 additions and 358 deletions

View File

@@ -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="任务不存在或已结束")

View File

@@ -23,7 +23,15 @@ __version__ = "5.0.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__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",
]

View File

@@ -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()

View File

@@ -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()

View File

@@ -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):
"""返回配置项的所有唯一标识符"""

View File

@@ -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="全局设置数据")

View File

@@ -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

1998
app/task/MAA.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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"]

View File

@@ -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("森空岛签到任务")

89
app/utils/LogMonitor.py Normal file
View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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您要继续吗

View File

@@ -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" #更新包标识

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

63
resources/version.json Normal file
View File

@@ -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组件忽视滚轮事件"
]
}
}
}