diff --git a/app/core/config.py b/app/core/config.py index fc12c83..ae3d21e 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -116,6 +116,10 @@ class GlobalConfig(ConfigBase): "Update", "MirrorChyanCDK", "", EncryptValidator() ) + Data_UID = ConfigItem("Data", "UID", str(uuid.uuid4()), UUIDValidator()) + Data_LastStatisticsUpload = ConfigItem( + "Data", "LastStatisticsUpload", "2000-01-01 00:00:00" + ) Data_LastStageUpdated = ConfigItem( "Data", "LastStageUpdated", "2000-01-01 00:00:00" ) @@ -190,7 +194,7 @@ class MaaUserConfig(ConfigBase): def __init__(self) -> None: super().__init__() - self.Info_Name = ConfigItem("Info", "Name", "新用户") + self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator()) self.Info_Id = ConfigItem("Info", "Id", "") self.Info_Mode = ConfigItem( "Info", "Mode", "简洁", OptionsValidator(["简洁", "详细"]) @@ -451,7 +455,7 @@ class GeneralUserConfig(ConfigBase): def __init__(self) -> None: super().__init__() - self.Info_Name = ConfigItem("Info", "Name", "新用户") + self.Info_Name = ConfigItem("Info", "Name", "新用户", UserNameValidator()) self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator()) self.Info_RemainedDay = ConfigItem( "Info", "RemainedDay", -1, RangeValidator(-1, 9999) @@ -460,13 +464,13 @@ class GeneralUserConfig(ConfigBase): "Info", "IfScriptBeforeTask", False, BoolValidator() ) self.Info_ScriptBeforeTask = ConfigItem( - "Info", "ScriptBeforeTask", "", FileValidator() + "Info", "ScriptBeforeTask", str(Path.cwd()), FileValidator() ) self.Info_IfScriptAfterTask = ConfigItem( "Info", "IfScriptAfterTask", False, BoolValidator() ) self.Info_ScriptAfterTask = ConfigItem( - "Info", "ScriptAfterTask", "", FileValidator() + "Info", "ScriptAfterTask", str(Path.cwd()), FileValidator() ) self.Info_Notes = ConfigItem("Info", "Notes", "无") @@ -571,7 +575,7 @@ TYPE_BOOK = {"MaaConfig": "MAA", "GeneralConfig": "通用"} class AppConfig(GlobalConfig): - VERSION = "5.0.0.1" + VERSION = [5, 0, 0, 1] def __init__(self) -> None: super().__init__(if_save_multi_config=False) @@ -579,7 +583,7 @@ class AppConfig(GlobalConfig): logger.info("") logger.info("===================================") logger.info("AUTO_MAA 后端应用程序") - logger.info(f"版本号: v{self.VERSION}") + logger.info(f"版本号: {self.version()}") logger.info(f"工作目录: {Path.cwd()}") logger.info("===================================") @@ -605,6 +609,16 @@ class AppConfig(GlobalConfig): truststore.inject_into_ssl() + def version(self) -> str: + """获取版本号字符串""" + + if self.VERSION[3] == 0: + return f"v{'.'.join(str(_) for _ in self.VERSION[0:3])}" + else: + return ( + f"v{'.'.join(str(_) for _ in self.VERSION[0:3])}-beta.{self.VERSION[3]}" + ) + async def init_config(self) -> None: """初始化配置管理""" diff --git a/app/core/task_manager.py b/app/core/task_manager.py index fec90e0..e69a560 100644 --- a/app/core/task_manager.py +++ b/app/core/task_manager.py @@ -212,6 +212,14 @@ class _TaskManager: partial(self.task_dict.pop, script_id) ) await self.task_dict[script_id] + task["status"] = "完成" + await Config.send_json( + WebSocketMessage( + id=str(task_id), + type="Update", + data={"task_list": task_list}, + ).model_dump() + ) async def stop_task(self, task_id: str) -> None: """ diff --git a/app/core/timer.py b/app/core/timer.py index 55ad9a6..3995f8c 100644 --- a/app/core/timer.py +++ b/app/core/timer.py @@ -22,7 +22,7 @@ import asyncio import keyboard from datetime import datetime -from app.services import System +from app.services import Matomo, System from app.utils import get_logger from .config import Config @@ -42,6 +42,33 @@ class _MainTimer: await asyncio.sleep(1) + async def hour_task(self): + """每小时定期任务""" + + logger.info("每小时定期任务启动") + + while True: + + if ( + datetime.strptime( + Config.get("Data", "LastStatisticsUpload"), "%Y-%m-%d %H:%M:%S" + ).date() + != datetime.now().date() + ): + await Matomo.send_event( + "App", + "Version", + Config.version(), + 1 if "beta" in Config.version() else 0, + ) + await Config.set( + "Data", + "LastStatisticsUpload", + datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + ) + + await asyncio.sleep(3600) + async def set_silence(self): """静默模式通过模拟老板键来隐藏模拟器窗口""" diff --git a/app/models/ConfigBase.py b/app/models/ConfigBase.py index 2367372..2bd977b 100644 --- a/app/models/ConfigBase.py +++ b/app/models/ConfigBase.py @@ -25,10 +25,11 @@ import uuid import win32com.client from copy import deepcopy from pathlib import Path -from typing import List, Any, Dict, Union +from typing import List, Any, Dict, Union, Optional from app.utils import dpapi_encrypt, dpapi_decrypt +from app.utils.constants import RESERVED_NAMES, ILLEGAL_CHARS class ConfigValidator: @@ -97,8 +98,22 @@ class UidValidator(ConfigValidator): return value if self.validate(value) else None +class UUIDValidator(ConfigValidator): + """UUID验证器""" + + def validate(self, value: Any) -> bool: + try: + uuid.UUID(value) + return True + except (TypeError, ValueError): + return False + + def correct(self, value: Any) -> Any: + return value if self.validate(value) else str(uuid.uuid4()) + + class EncryptValidator(ConfigValidator): - """加数据验证器""" + """加密数据验证器""" def validate(self, value: Any) -> bool: if not isinstance(value, str): @@ -163,6 +178,46 @@ class FolderValidator(ConfigValidator): return Path(value).resolve().as_posix() +class UserNameValidator(ConfigValidator): + """用户名验证器""" + + def validate(self, value: Any) -> bool: + if not isinstance(value, str): + return False + + if not value or not value.strip(): + return False + + if value != value.strip() or value != value.strip("."): + return False + + if any(char in ILLEGAL_CHARS for char in value): + return False + + if value.upper() in RESERVED_NAMES: + return False + if len(value) > 255: + return False + + return True + + def correct(self, value: Any) -> str: + if not isinstance(value, str): + value = "默认用户名" + + value = value.strip().strip(".") + + value = "".join(char for char in value if char not in ILLEGAL_CHARS) + + if value.upper() in RESERVED_NAMES or not value: + value = "默认用户名" + + if len(value) > 255: + value = value[:255] + + return value + + class ConfigItem: """配置项""" @@ -171,7 +226,7 @@ class ConfigItem: group: str, name: str, default: Any, - validator: None | ConfigValidator = None, + validator: Optional[ConfigValidator] = None, ): """ Parameters @@ -195,7 +250,10 @@ class ConfigItem: self.validator = validator or ConfigValidator() self.is_locked = False - self.setValue(default) + if not self.validator.validate(self.value): + raise ValueError( + f"配置项 '{self.group}.{self.name}' 的默认值 '{self.value}' 不合法" + ) def setValue(self, value: Any): """ diff --git a/app/services/__init__.py b/app/services/__init__.py index bab395d..c55fcd6 100644 --- a/app/services/__init__.py +++ b/app/services/__init__.py @@ -22,6 +22,7 @@ __version__ = "5.0.0" __author__ = "DLmaster361 " __license__ = "GPL-3.0 license" +from .matomo import Matomo from .notification import Notify from .system import System diff --git a/app/services/matomo.py b/app/services/matomo.py new file mode 100644 index 0000000..701f5b1 --- /dev/null +++ b/app/services/matomo.py @@ -0,0 +1,125 @@ +# 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 asyncio +import aiohttp +import json +import uuid +import psutil +import platform +import time +from typing import Dict, Any, Optional + +from app.core import Config +from app.utils.logger import get_logger + +logger = get_logger("信息上报") + + +class _MatomoHandler: + """Matomo统计上报服务""" + + base_url = "https://statistics.auto-mas.top/matomo.php" + site_id = "3" + + def __init__(self): + + self.session = None + + async def _get_session(self): + """获取HTTP会话""" + + if self.session is None or self.session.closed: + timeout = aiohttp.ClientTimeout(total=10) + self.session = aiohttp.ClientSession(timeout=timeout) + return self.session + + async def close(self): + """关闭HTTP会话""" + if self.session and not self.session.closed: + await self.session.close() + + def _build_base_params(self, custom_vars: Optional[Dict[str, Any]] = None): + """构建基础参数""" + params = { + "idsite": self.site_id, + "rec": "1", + "action_name": "AUTO-MAS后端", + "_id": Config.get("Data", "UID")[:16], + "uid": Config.get("Data", "UID"), + "rand": str(uuid.uuid4().int)[:10], + "apiv": "1", + "h": time.strftime("%H"), + "m": time.strftime("%M"), + "s": time.strftime("%S"), + "ua": f"AUTO-MAS/{Config.version()} ({platform.system()} {platform.release()})", + } + + # 添加自定义变量 + if custom_vars is not None: + cvar = {} + for i, (key, value) in enumerate(custom_vars.items(), 1): + if i <= 5: + cvar[str(i)] = [str(key), str(value)] + if cvar: + params["_cvar"] = json.dumps(cvar) + + return params + + async def send_event( + self, + category: str, + action: str, + name: Optional[str] = None, + value: Optional[float] = None, + custom_vars: Optional[Dict[str, Any]] = None, + ): + """发送事件数据到Matomo + + Args: + category: 事件类别,如 "Script", "Config", "User" + action: 事件动作,如 "Execute", "Update", "Login" + name: 事件名称,如具体的脚本名称 + value: 事件值,如执行时长、文件大小等数值 + custom_vars: 自定义变量字典 + """ + try: + session = await self._get_session() + if session is None: + return + + params = self._build_base_params(custom_vars) + params.update({"e_c": category, "e_a": action, "e_n": name, "e_v": value}) + params = {k: v for k, v in params.items() if v is not None} + + async with session.get(self.base_url, params=params) as response: + if response.status == 200: + logger.debug(f"Matomo事件上报成功: {category}/{action}") + else: + logger.warning(f"Matomo事件上报失败: {response.status}") + + except asyncio.TimeoutError: + logger.warning("Matomo事件上报超时") + except Exception as e: + logger.error(f"Matomo事件上报错误: {e}") + + +Matomo = _MatomoHandler() diff --git a/app/utils/constants.py b/app/utils/constants.py index 25a323e..a54186e 100644 --- a/app/utils/constants.py +++ b/app/utils/constants.py @@ -226,3 +226,32 @@ MATERIALS_MAP = { "PR-D": "近卫/特种芯片", } """掉落物索引表""" + +RESERVED_NAMES = { + "CON", + "PRN", + "AUX", + "NUL", + "COM1", + "COM2", + "COM3", + "COM4", + "COM5", + "COM6", + "COM7", + "COM8", + "COM9", + "LPT1", + "LPT2", + "LPT3", + "LPT4", + "LPT5", + "LPT6", + "LPT7", + "LPT8", + "LPT9", +} +"""Windows保留名称列表""" + +ILLEGAL_CHARS = set('<>:"/\\|?*') +"""文件名非法字符集合""" diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index c634dd3..4af05d3 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -490,6 +490,26 @@ ipcMain.handle('open-url', async (_event, url: string) => { } }) +// 打开文件 +ipcMain.handle('open-file', async (_event, filePath: string) => { + try { + await shell.openPath(filePath) + } catch (error) { + console.error('打开文件失败:', error) + throw error + } +}) + +// 显示文件所在目录并选中文件 +ipcMain.handle('show-item-in-folder', async (_event, filePath: string) => { + try { + shell.showItemInFolder(filePath) + } catch (error) { + console.error('显示文件所在目录失败:', error) + throw error + } +}) + // 环境检查 ipcMain.handle('check-environment', async () => { const appRoot = getAppRoot() diff --git a/frontend/electron/preload.ts b/frontend/electron/preload.ts index 2f19132..4e64079 100644 --- a/frontend/electron/preload.ts +++ b/frontend/electron/preload.ts @@ -50,6 +50,10 @@ contextBridge.exposeInMainWorld('electronAPI', { saveLogsToFile: (logs: string) => ipcRenderer.invoke('save-logs-to-file', logs), loadLogsFromFile: () => ipcRenderer.invoke('load-logs-from-file'), + // 文件系统操作 + openFile: (filePath: string) => ipcRenderer.invoke('open-file', filePath), + showItemInFolder: (filePath: string) => ipcRenderer.invoke('show-item-in-folder', filePath), + // 监听下载进度 onDownloadProgress: (callback: (progress: any) => void) => { ipcRenderer.on('download-progress', (_, progress) => callback(progress)) diff --git a/frontend/package.json b/frontend/package.json index e380476..f3c0b5e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,8 @@ "electron-dev": "wait-on http://localhost:5173 && cross-env VITE_DEV_SERVER_URL=http://localhost:5173 electron .", "build:main": "tsc -p tsconfig.electron.json", "build": "vite build && yarn build:main && electron-builder", - "web": "vite" + "web": "vite", + "release": "vite build && yarn build:main && electron-builder --win --publish always" }, "build": { "asar": true, @@ -20,29 +21,37 @@ "appId": "xyz.automaa.frontend", "productName": "AUTO_MAA", "files": [ - "dist", - "dist-electron", - "public", - "!src/assets/*" + "dist/**", + "dist-electron/**", + "public/**", + "!src/**", + "!**/*.map" + ], + "publish": [ + { + "provider": "github", + "owner": "DLmaster_361", + "repo": "AUTO_MAA" + } ], "extraResources": [ - { - "from": "src/assets", - "to": "assets", - "filter": [] - } + { "from": "src/assets", "to": "assets", "filter": ["**/*"] } ], "win": { "requestedExecutionLevel": "requireAdministrator", - "target": "dir", + "target": [ + { "target": "nsis", "arch": ["x64"] } + ], "icon": "public/AUTO-MAS.ico", - "artifactName": "AUTO_MAA.exe" + "artifactName": "AUTO_MAA-Setup-${version}-${arch}.${ext}" }, - "mac": { - "icon": "public/AUTO-MAS.ico" - }, - "linux": { - "icon": "public/AUTO-MAS.ico" + "nsis": { + "oneClick": false, + "perMachine": true, + "allowToChangeInstallationDirectory": true, + "createDesktopShortcut": true, + "shortcutName": "AUTO_MAA", + "differentialPackage": true } }, "dependencies": { diff --git a/frontend/src/components/AppLayout.vue b/frontend/src/components/AppLayout.vue index f605f22..f4d7327 100644 --- a/frontend/src/components/AppLayout.vue +++ b/frontend/src/components/AppLayout.vue @@ -1,78 +1,31 @@