feat: MAA完整功能上线

This commit is contained in:
DLmaster361
2025-08-10 21:45:35 +08:00
parent 409a7a2d03
commit 6b729be34e
9 changed files with 526 additions and 391 deletions

View File

@@ -24,7 +24,7 @@ import uuid
import asyncio import asyncio
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Body, Path from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Body, Path
from app.core import Config, TaskManager from app.core import TaskManager, Broadcast
from app.models.schema import * from app.models.schema import *
router = APIRouter(prefix="/api/dispatch", tags=["任务调度"]) router = APIRouter(prefix="/api/dispatch", tags=["任务调度"])
@@ -69,7 +69,7 @@ async def websocket_endpoint(
while True: while True:
try: try:
data = await asyncio.wait_for(websocket.receive_json(), timeout=30.0) data = await asyncio.wait_for(websocket.receive_json(), timeout=30.0)
await Config.message_queue.put({"task_id": uid, "message": data}) await Broadcast.put(data)
except asyncio.TimeoutError: except asyncio.TimeoutError:
await websocket.send_json( await websocket.send_json(
TaskMessage(type="Signal", data={"Ping": "无描述"}).model_dump() TaskMessage(type="Signal", data={"Ping": "无描述"}).model_dump()

View File

@@ -23,11 +23,13 @@ __version__ = "5.0.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>" __author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license" __license__ = "GPL-3.0 license"
from .broadcast import Broadcast
from .config import Config, MaaConfig, GeneralConfig, MaaUserConfig from .config import Config, MaaConfig, GeneralConfig, MaaUserConfig
from .timer import MainTimer from .timer import MainTimer
from .task_manager import TaskManager from .task_manager import TaskManager
__all__ = [ __all__ = [
"Broadcast",
"Config", "Config",
"MaaConfig", "MaaConfig",
"GeneralConfig", "GeneralConfig",

51
app/core/broadcast.py Normal file
View File

@@ -0,0 +1,51 @@
# 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 <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
import asyncio
from copy import deepcopy
from typing import Set
from app.utils import get_logger
logger = get_logger("消息广播")
class _Broadcast:
def __init__(self):
self.__subscribers: Set[asyncio.Queue] = set()
async def subscribe(self, queue: asyncio.Queue):
"""订阅者注册"""
self.__subscribers.add(queue)
async def unsubscribe(self, queue: asyncio.Queue):
"""取消订阅"""
self.__subscribers.remove(queue)
async def put(self, item):
"""向所有订阅者广播消息"""
for subscriber in self.__subscribers:
await subscriber.put(deepcopy(item))
Broadcast = _Broadcast()

View File

@@ -593,10 +593,9 @@ class AppConfig(GlobalConfig):
self.log_path.parent.mkdir(parents=True, exist_ok=True) self.log_path.parent.mkdir(parents=True, exist_ok=True)
self.config_path.mkdir(parents=True, exist_ok=True) self.config_path.mkdir(parents=True, exist_ok=True)
self.message_queue = asyncio.Queue()
self.silence_dict: Dict[Path, datetime] = {} self.silence_dict: Dict[Path, datetime] = {}
self.power_sign = "NoAction" self.power_sign = "NoAction"
self.if_ignore_silence = False self.if_ignore_silence: List[uuid.UUID] = []
self.ScriptConfig = MultipleConfig([MaaConfig, GeneralConfig]) self.ScriptConfig = MultipleConfig([MaaConfig, GeneralConfig])
self.PlanConfig = MultipleConfig([MaaPlanConfig]) self.PlanConfig = MultipleConfig([MaaPlanConfig])

View File

@@ -78,7 +78,9 @@ class _TaskManager:
else: else:
raise ValueError(f"The task corresponding to UID {uid} could not be found.") raise ValueError(f"The task corresponding to UID {uid} could not be found.")
if task_id in self.task_dict: if task_id in self.task_dict or (
actual_id is not None and actual_id in self.task_dict
):
raise RuntimeError(f"The task {task_id} is already running.") raise RuntimeError(f"The task {task_id} is already running.")

View File

@@ -50,8 +50,8 @@ class _MainTimer:
if ( if (
not Config.if_ignore_silence not Config.if_ignore_silence
and await Config.get("Function", "IfSilence") and Config.get("Function", "IfSilence")
and await Config.get("Function", "BossKey") != "" and Config.get("Function", "BossKey") != ""
): ):
pass pass

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@ class LogMonitor:
self.last_callback_time: datetime = datetime.now() self.last_callback_time: datetime = datetime.now()
self.log_contents: List[str] = [] self.log_contents: List[str] = []
self.task: Optional[asyncio.Task] = None self.task: Optional[asyncio.Task] = None
self.__is_running = False
async def monitor_log(self): async def monitor_log(self):
"""监控日志文件的主循环""" """监控日志文件的主循环"""
@@ -35,77 +36,63 @@ class LogMonitor:
logger.info(f"开始监控日志文件: {self.log_file_path}") logger.info(f"开始监控日志文件: {self.log_file_path}")
consecutive_errors = 0 while self.__is_running:
logger.debug("正在检查日志文件...")
log_contents = []
if_log_start = False
while True: # 检查文件是否仍然存在
try: if not self.log_file_path.exists():
log_contents = [] logger.warning(f"日志文件不存在: {self.log_file_path}")
if_log_start = False
# 检查文件是否仍然存在
if not self.log_file_path.exists():
logger.warning(f"日志文件不存在: {self.log_file_path}")
await asyncio.sleep(1)
continue
# 尝试读取文件
try:
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, IndexError):
continue
else:
log_contents.append(line)
except (FileNotFoundError, PermissionError) as e:
logger.warning(f"文件访问错误: {e}")
await asyncio.sleep(5)
continue
except UnicodeDecodeError as e:
logger.error(f"文件编码错误: {e}")
await asyncio.sleep(10)
continue
# 调用回调
if (
log_contents != self.log_contents
or datetime.now() - self.last_callback_time > timedelta(minutes=1)
):
self.log_contents = log_contents
self.last_callback_time = datetime.now()
# 安全调用回调函数
try:
await self.callback(log_contents)
except Exception as e:
logger.error(f"回调函数执行失败: {e}")
except asyncio.CancelledError:
logger.info("日志监控任务被取消")
break
except Exception as e:
logger.error(f"监控日志时发生未知错误:{e}")
consecutive_errors += 1
wait_time = min(60, 2**consecutive_errors)
await asyncio.sleep(wait_time)
continue continue
# 尝试读取文件
try:
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, IndexError):
continue
else:
log_contents.append(line)
except (FileNotFoundError, PermissionError) as e:
logger.warning(f"文件访问错误: {e}")
await asyncio.sleep(5)
continue
except UnicodeDecodeError as e:
logger.error(f"文件编码错误: {e}")
await asyncio.sleep(10)
continue
# 调用回调
if (
log_contents != self.log_contents
or datetime.now() - self.last_callback_time > timedelta(minutes=1)
):
self.log_contents = log_contents
self.last_callback_time = datetime.now()
# 安全调用回调函数
try:
await self.callback(log_contents)
except Exception as e:
logger.error(f"回调函数执行失败: {e}")
await asyncio.sleep(1) await asyncio.sleep(1)
async def start(self, log_file_path: Path, start_time: datetime) -> None: async def start(self, log_file_path: Path, start_time: datetime) -> None:
@@ -117,6 +104,8 @@ class LogMonitor:
if self.task is not None and not self.task.done(): if self.task is not None and not self.task.done():
await self.stop() await self.stop()
self.__is_running = True
self.log_contents = []
self.log_file_path = log_file_path self.log_file_path = log_file_path
self.log_start_time = start_time self.log_start_time = start_time
self.task = asyncio.create_task(self.monitor_log()) self.task = asyncio.create_task(self.monitor_log())
@@ -125,14 +114,15 @@ class LogMonitor:
async def stop(self): async def stop(self):
"""停止监控""" """停止监控"""
logger.info("请求取消日志监控任务")
if self.task is not None and not self.task.done(): if self.task is not None and not self.task.done():
self.task.cancel() self.task.cancel()
try: try:
await self.task await self.task
except asyncio.CancelledError: except asyncio.CancelledError:
pass logger.info("日志监控任务已中止")
self.log_contents = [] logger.success("日志监控任务已停止")
self.log_file_path = None
self.task = None self.task = None
logger.info(f"日志监控已停止: {self.log_file_path}")

View File

@@ -22,6 +22,7 @@
import asyncio import asyncio
import psutil import psutil
import subprocess import subprocess
from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
@@ -34,6 +35,7 @@ class ProcessManager:
self.main_pid = None self.main_pid = None
self.tracked_pids = set() self.tracked_pids = set()
self.check_task = None self.check_task = None
self.track_end_time = datetime.now()
async def open_process( async def open_process(
self, path: Path, args: list = [], tracking_time: int = 60 self, path: Path, args: list = [], tracking_time: int = 60
@@ -88,14 +90,13 @@ class ProcessManager:
# 启动持续追踪任务 # 启动持续追踪任务
if tracking_time > 0: if tracking_time > 0:
self.track_end_time = datetime.now() + timedelta(seconds=tracking_time)
self.check_task = asyncio.create_task(self.track_processes()) self.check_task = asyncio.create_task(self.track_processes())
await asyncio.sleep(tracking_time)
await self.stop_tracking()
async def track_processes(self) -> None: async def track_processes(self) -> None:
"""更新子进程列表""" """更新子进程列表"""
while True: while datetime.now() < self.track_end_time:
current_pids = set(self.tracked_pids) current_pids = set(self.tracked_pids)
for pid in current_pids: for pid in current_pids:
try: try:
@@ -108,16 +109,6 @@ class ProcessManager:
continue continue
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
async def stop_tracking(self) -> None:
"""停止更新子进程列表"""
if self.check_task and not self.check_task.done():
self.check_task.cancel()
try:
await self.check_task
except asyncio.CancelledError:
pass
async def is_running(self) -> bool: async def is_running(self) -> bool:
"""检查所有跟踪的进程是否还在运行""" """检查所有跟踪的进程是否还在运行"""
@@ -152,6 +143,13 @@ class ProcessManager:
async def clear(self) -> None: async def clear(self) -> None:
"""清空跟踪的进程列表""" """清空跟踪的进程列表"""
await self.stop_tracking() if self.check_task is not None and not self.check_task.done():
self.check_task.cancel()
try:
await self.check_task
except asyncio.CancelledError:
pass
self.main_pid = None self.main_pid = None
self.tracked_pids.clear() self.tracked_pids.clear()