├── .github └── workflows │ ├── build-app.yml │ ├── mirrorchyan.yml │ └── mirrorchyan_release_note.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── __init__.py ├── core │ ├── __init__.py │ ├── config.py │ ├── main_info_bar.py │ ├── network.py │ ├── sound_player.py │ ├── task_manager.py │ └── timer.py ├── models │ ├── MAA.py │ └── __init__.py ├── services │ ├── __init__.py │ ├── notification.py │ ├── security.py │ └── system.py ├── ui │ ├── Widget.py │ ├── __init__.py │ ├── dispatch_center.py │ ├── downloader.py │ ├── history.py │ ├── home.py │ ├── main_window.py │ ├── member_manager.py │ ├── plan_manager.py │ ├── queue_manager.py │ └── setting.py └── utils │ ├── AUTO_MAA.iss │ ├── __init__.py │ └── package.py ├── main.py ├── requirements.txt └── resources ├── docs ├── ChineseSimplified.isl └── MAA_config_info.txt ├── html ├── MAA_result.html ├── MAA_six_star.html └── MAA_statistics.html ├── icons ├── AUTO_MAA.ico ├── AUTO_MAA_Updater.ico └── MirrorChyan.ico ├── images ├── AUTO_MAA.png ├── Home │ └── BannerDefault.png └── README │ └── payid.png ├── sounds ├── both │ ├── 删除用户.wav │ ├── 删除脚本实例.wav │ ├── 删除计划表.wav │ ├── 删除调度队列.wav │ ├── 欢迎回来.wav │ ├── 添加用户.wav │ ├── 添加脚本实例.wav │ ├── 添加计划表.wav │ └── 添加调度队列.wav ├── noisy │ ├── ADB失败.wav │ ├── ADB成功.wav │ ├── MAA在完成任务前中止.wav │ ├── MAA在完成任务前退出.wav │ ├── MAA更新.wav │ ├── MAA未检测到任何模拟器.wav │ ├── MAA未能正确登录PRTS.wav │ ├── MAA的ADB连接异常.wav │ ├── MAA进程超时.wav │ ├── MAA部分任务执行失败.wav │ ├── 任务开始.wav │ ├── 任务结束.wav │ ├── 公告展示.wav │ ├── 公告通知.wav │ ├── 六星喜报.wav │ ├── 历史记录查询.wav │ ├── 发生异常.wav │ ├── 发生错误.wav │ ├── 子任务失败.wav │ ├── 排查录入.wav │ ├── 排查重试.wav │ ├── 无新版本.wav │ └── 有新版本.wav └── simple │ ├── 任务开始.wav │ ├── 任务结束.wav │ ├── 公告展示.wav │ ├── 公告通知.wav │ ├── 历史记录查询.wav │ ├── 发生异常.wav │ ├── 发生错误.wav │ ├── 无新版本.wav │ └── 有新版本.wav └── version.json /.github/workflows/build-app.yml: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | name: Build AUTO_MAA 22 | 23 | on: 24 | workflow_dispatch: 25 | 26 | permissions: 27 | contents: write 28 | actions: write 29 | 30 | jobs: 31 | pre_check: 32 | name: Pre Checks 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Repo Check 36 | id: repo_check 37 | run: | 38 | if [[ "$GITHUB_REPOSITORY" != "DLmaster361/AUTO_MAA" ]]; then 39 | echo "When forking this repository to make your own builds, you have to adjust this check." 40 | exit 1 41 | fi 42 | exit 0 43 | build_AUTO_MAA: 44 | runs-on: windows-latest 45 | needs: pre_check 46 | steps: 47 | - name: Checkout code 48 | uses: actions/checkout@v4 49 | - name: Set up Python 3.12 50 | uses: actions/setup-python@v5 51 | with: 52 | python-version: "3.12" 53 | - name: Install dependencies 54 | run: | 55 | python -m pip install --upgrade pip 56 | pip install flake8 pytest 57 | pip install -r requirements.txt 58 | choco install innosetup 59 | echo "C:\Program Files (x86)\Inno Setup 6" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append 60 | - name: Lint with flake8 61 | run: | 62 | # stop the build if there are Python syntax errors or undefined names 63 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 64 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 65 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 66 | - name: Package 67 | id: package 68 | run: | 69 | copy app\utils\package.py .\ 70 | python package.py 71 | - name: Read version 72 | id: read_version 73 | run: | 74 | $MAIN_VERSION=(Get-Content -Path "version_info.txt" -TotalCount 1).Trim() 75 | "AUTO_MAA_version=$MAIN_VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append 76 | - name: Upload Artifact 77 | uses: actions/upload-artifact@v4 78 | with: 79 | name: AUTO_MAA_${{ env.AUTO_MAA_version }} 80 | path: AUTO_MAA_${{ env.AUTO_MAA_version }}.zip 81 | - name: Upload Version_Info Artifact 82 | uses: actions/upload-artifact@v4 83 | with: 84 | name: version_info 85 | path: version_info.txt 86 | publish_release: 87 | name: Publish release 88 | needs: build_AUTO_MAA 89 | runs-on: ubuntu-latest 90 | steps: 91 | - name: Checkout 92 | uses: actions/checkout@v4 93 | - name: Download artifacts 94 | uses: actions/download-artifact@v4 95 | with: 96 | pattern: AUTO_MAA_* 97 | merge-multiple: true 98 | path: artifacts 99 | - name: Download Version_Info 100 | uses: actions/download-artifact@v4 101 | with: 102 | name: version_info 103 | path: ./ 104 | - name: Create release 105 | id: create_release 106 | run: | 107 | set -xe 108 | shopt -s nullglob 109 | NAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))" 110 | TAGNAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))" 111 | NOTES_MAIN="$(sed 's/\r$//g' <(tail -n +3 version_info.txt))" 112 | NOTES="$NOTES_MAIN 113 | 114 | [已有 Mirror酱 CDK ?前往 Mirror酱 高速下载](https://mirrorchyan.com/zh/projects?rid=AUTO_MAA) 115 | 116 | \`\`\`本release通过GitHub Actions自动构建\`\`\`" 117 | if [ "${{ github.ref_name }}" == "main" ]; then 118 | PRERELEASE_FLAG="" 119 | else 120 | PRERELEASE_FLAG="--prerelease" 121 | fi 122 | gh release create "$TAGNAME" --target "main" --title "$NAME" --notes "$NOTES" $PRERELEASE_FLAG artifacts/* 123 | env: 124 | GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }} 125 | - name: Trigger MirrorChyanUploading 126 | run: | 127 | gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan 128 | gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan_release_note 129 | env: 130 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 131 | -------------------------------------------------------------------------------- /.github/workflows/mirrorchyan.yml: -------------------------------------------------------------------------------- 1 | name: mirrorchyan 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | mirrorchyan: 8 | runs-on: macos-latest 9 | 10 | steps: 11 | - id: uploading 12 | uses: MirrorChyan/uploading-action@v1 13 | with: 14 | filetype: latest-release 15 | filename: "AUTO_MAA*.zip" 16 | mirrorchyan_rid: AUTO_MAA 17 | 18 | owner: DLmaster361 19 | repo: AUTO_MAA 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | upload_token: ${{ secrets.MirrorChyanUploadToken }} 22 | -------------------------------------------------------------------------------- /.github/workflows/mirrorchyan_release_note.yml: -------------------------------------------------------------------------------- 1 | name: mirrorchyan_release_note 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [edited] 7 | 8 | jobs: 9 | mirrorchyan: 10 | runs-on: macos-latest 11 | 12 | steps: 13 | - id: uploading 14 | uses: MirrorChyan/release-note-action@v1 15 | with: 16 | mirrorchyan_rid: AUTO_MAA 17 | 18 | upload_token: ${{ secrets.MirrorChyanUploadToken }} 19 | github_token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | config/ 3 | data/ 4 | debug/ 5 | history/ 6 | resources/notice.json 7 | resources/theme_image.json 8 | resources/images/Home/BannerTheme.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

AUTO_MAA

2 |

3 | MAA多账号管理与自动化软件

4 | 软件图标 5 |

6 | 7 | --- 8 | 9 |

10 | GitHub Stars 11 | GitHub Forks 12 | GitHub Downloads 13 | GitHub Issues 14 | GitHub Contributors 15 | GitHub License 16 | DeepWiki 17 | mirrorc 18 |

19 | 20 | ## 软件介绍 21 | 22 | ### 性质 23 | 24 | 本软件是明日方舟第三方软件`MAA`的第三方工具,即第33方软件。旨在优化MAA多账号功能体验,并通过一些方法解决MAA项目未能解决的部分问题,提高代理的稳定性。 25 | 26 | - **集中管理**:一站式管理多个MAA脚本与多个用户配置,和凌乱的散装脚本窗口说再见! 27 | - **无人值守**:自动处理MAA相关报错,再也不用为代理任务卡死时自己不在电脑旁烦恼啦! 28 | - **配置灵活**:通过调度队列与脚本的组合,自由实现您能想到的所有调度需求! 29 | - **信息统计**:自动统计用户的公招与关卡掉落物,看看这个月您的收益是多少! 30 | 31 | ### 原理 32 | 33 | 本软件可以存储多个明日方舟账号数据,并通过以下流程实现代理功能: 34 | 35 | 1. **配置:** 根据对应用户的配置信息,生成配置文件并将其导入MAA。 36 | 2. **监测:** 在MAA开始代理后,持续读取MAA的日志以判断其运行状态。当软件认定MAA出现异常时,通过重启MAA使之仍能继续完成任务。 37 | 3. **循环:** 重复上述步骤,使MAA依次完成各个用户的自动代理任务。 38 | 39 | ### 优势 40 | 41 | - **高效稳定**:通过日志监测、异常处理等机制,保障代理任务顺利完成。 42 | - **简洁易用**:无需手动修改配置文件,实现自动化调度与多开管理。 43 | - **兼容扩展**:支持 MAA 几乎所有的配置选项,满足不同用户需求。 44 | 45 | ## 重要声明 46 | 47 | 本开发团队承诺,不会修改明日方舟游戏本体与相关配置文件。本项目使用GPL开源,相关细则如下: 48 | 49 | - **作者:** AUTO_MAA软件作者为DLmaster、DLmaster361或DLmaster_361,以上均指代同一人。 50 | - **使用:** AUTO_MAA使用者可以按自己的意愿自由使用本软件。依据GPL,对于由此可能产生的损失,AUTO_MAA项目组不负任何责任。 51 | - **分发:** AUTO_MAA允许任何人自由分发本软件,包括进行商业活动牟利。若为直接分发本软件,必须遵循GPL向接收者提供本软件项目地址、完整的软件源码与GPL协议原文(件);若为修改软件后进行分发,必须遵循GPL向接收者提供本软件项目地址、修改前的完整软件源码副本与GPL协议原文(件),违反者可能会被追究法律责任。 52 | - **传播:** AUTO_MAA原则上允许传播者自由传播本软件,但无论在何种传播过程中,不得删除项目作者与开发者所留版权声明,不得隐瞒项目作者与相关开发者的存在。由于软件性质,项目组不希望发现任何人在明日方舟官方媒体(包括官方媒体账号与森空岛社区等)或明日方舟游戏相关内容(包括同好群、线下活动与游戏内容讨论等)下提及AUTO_MAA或MAA,希望各位理解。 53 | - **衍生:** AUTO_MAA允许任何人对软件本体或软件部分代码进行二次开发或利用。但依据GPL,相关成果再次分发时也必须使用GPL或兼容的协议开源。 54 | - **贡献:** 不论是直接参与软件的维护编写,或是撰写文档、测试、反馈BUG、给出建议、参与讨论,都为AUTO_MAA项目的发展完善做出了不可忽视的贡献。项目组提倡各位贡献者遵照GitHub开源社区惯例,发布Issues参与项目。避免私信或私发邮件(安全性漏洞或敏感问题除外),以帮助更多用户。 55 | 56 | 以上细则是本项目对GPL的相关补充与强调。未提及的以GPL为准,发生冲突的以本细则为准。如有不清楚的部分,请发Issues询问。若发生纠纷,相关内容也没有在Issues上提及的,项目组拥有最终解释权。 57 | 58 | **注意** 59 | 60 | - 由于本软件有修改其它目录JSON文件等行为,使用前请将AUTO_MAA添加入Windows Defender信任区以及防病毒软件的信任区或开发者目录,避免被误杀。 61 | 62 | --- 63 | 64 | # 使用方法 65 | 66 | 访问AUTO_MAA官方文档站以获取使用指南和项目相关信息 67 | 68 | - [AUTO_MAA官方文档站](https://clozya.github.io/AUTOMAA_docs) 69 | 70 | --- 71 | 72 | # 关于 73 | 74 | ## 项目开发情况 75 | 76 | 可在[《AUTO_MAA开发者协作文档》](https://docs.qq.com/aio/DQ3Z5eHNxdmxFQmZX)的`开发任务`页面中查看开发进度。 77 | 78 | ## 贡献者 79 | 80 | 感谢以下贡献者对本项目做出的贡献 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | ![Alt](https://repobeats.axiom.co/api/embed/6c2f834141eff1ac297db70d12bd11c6236a58a5.svg "Repobeats analytics image") 89 | 90 | 感谢 [AoXuan (@ClozyA)](https://github.com/ClozyA) 为本项目提供的下载服务器 91 | 92 | ## Star History 93 | 94 | [![Star History Chart](https://api.star-history.com/svg?repos=DLmaster361/AUTO_MAA&type=Date)](https://star-history.com/#DLmaster361/AUTO_MAA&Date) 95 | 96 | ## 交流与赞助 97 | 98 | 欢迎加入AUTO_MAA项目组,欢迎反馈bug 99 | 100 | - QQ交流群:[957750551](https://qm.qq.com/q/bd9fISNoME) 101 | 102 | --- 103 | 104 | 如果喜欢这个项目的话,给作者来杯咖啡吧! 105 | 106 | ![payid](resources/images/README/payid.png "payid") 107 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA主程序包 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | __version__ = "4.2.0" 29 | __author__ = "DLmaster361 " 30 | __license__ = "GPL-3.0 license" 31 | 32 | from .core import QueueConfig, MaaConfig, MaaUserConfig, Task, TaskManager, MainTimer 33 | from .models import MaaManager 34 | from .services import Notify, Crypto, System 35 | from .ui import AUTO_MAA 36 | 37 | __all__ = [ 38 | "QueueConfig", 39 | "MaaConfig", 40 | "MaaUserConfig", 41 | "Task", 42 | "TaskManager", 43 | "MainTimer", 44 | "MaaManager", 45 | "Notify", 46 | "Crypto", 47 | "System", 48 | "AUTO_MAA", 49 | ] 50 | -------------------------------------------------------------------------------- /app/core/__init__.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA核心组件包 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | __version__ = "4.2.0" 29 | __author__ = "DLmaster361 " 30 | __license__ = "GPL-3.0 license" 31 | 32 | from .config import QueueConfig, MaaConfig, MaaUserConfig, MaaPlanConfig, Config 33 | from .main_info_bar import MainInfoBar 34 | from .network import Network 35 | from .sound_player import SoundPlayer 36 | from .task_manager import Task, TaskManager 37 | from .timer import MainTimer 38 | 39 | __all__ = [ 40 | "Config", 41 | "QueueConfig", 42 | "MaaConfig", 43 | "MaaUserConfig", 44 | "MaaPlanConfig", 45 | "MainInfoBar", 46 | "Network", 47 | "SoundPlayer", 48 | "Task", 49 | "TaskManager", 50 | "MainTimer", 51 | ] 52 | -------------------------------------------------------------------------------- /app/core/main_info_bar.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA信息通知栏 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | from loguru import logger 29 | from PySide6.QtCore import Qt 30 | from qfluentwidgets import InfoBar, InfoBarPosition 31 | 32 | from .config import Config 33 | from .sound_player import SoundPlayer 34 | 35 | 36 | class _MainInfoBar: 37 | """信息通知栏""" 38 | 39 | # 模式到 InfoBar 方法的映射 40 | mode_mapping = { 41 | "success": InfoBar.success, 42 | "warning": InfoBar.warning, 43 | "error": InfoBar.error, 44 | "info": InfoBar.info, 45 | } 46 | 47 | def push_info_bar( 48 | self, mode: str, title: str, content: str, time: int, if_force: bool = False 49 | ): 50 | """推送到信息通知栏""" 51 | if Config.main_window is None: 52 | logger.error("信息通知栏未设置父窗口") 53 | return None 54 | 55 | # 根据 mode 获取对应的 InfoBar 方法 56 | info_bar_method = self.mode_mapping.get(mode) 57 | 58 | if not info_bar_method: 59 | logger.error(f"未知的通知栏模式: {mode}") 60 | return None 61 | 62 | if Config.main_window.isVisible(): 63 | info_bar_method( 64 | title=title, 65 | content=content, 66 | orient=Qt.Horizontal, 67 | isClosable=True, 68 | position=InfoBarPosition.TOP_RIGHT, 69 | duration=time, 70 | parent=Config.main_window, 71 | ) 72 | elif if_force: 73 | # 如果主窗口不可见且强制推送,则录入消息队列等待窗口显示后推送 74 | info_bar_item = { 75 | "mode": mode, 76 | "title": title, 77 | "content": content, 78 | "time": time, 79 | } 80 | if info_bar_item not in Config.info_bar_list: 81 | Config.info_bar_list.append(info_bar_item) 82 | 83 | if mode == "warning": 84 | SoundPlayer.play("发生异常") 85 | if mode == "error": 86 | SoundPlayer.play("发生错误") 87 | 88 | 89 | MainInfoBar = _MainInfoBar() 90 | -------------------------------------------------------------------------------- /app/core/network.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA网络请求线程 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | from loguru import logger 29 | from PySide6.QtCore import QObject, QThread, QEventLoop 30 | import re 31 | import time 32 | import requests 33 | from pathlib import Path 34 | 35 | 36 | class NetworkThread(QThread): 37 | """网络请求线程类""" 38 | 39 | max_retries = 3 40 | timeout = 10 41 | backoff_factor = 0.1 42 | 43 | def __init__(self, mode: str, url: str, path: Path = None) -> None: 44 | super().__init__() 45 | 46 | self.setObjectName( 47 | f"NetworkThread-{mode}-{re.sub(r'(&cdk=)[^&]+(&)', r'\1******\2', url)}" 48 | ) 49 | 50 | self.mode = mode 51 | self.url = url 52 | self.path = path 53 | 54 | self.status_code = None 55 | self.response_json = None 56 | self.error_message = None 57 | 58 | self.loop = QEventLoop() 59 | 60 | @logger.catch 61 | def run(self) -> None: 62 | """运行网络请求线程""" 63 | 64 | if self.mode == "get": 65 | self.get_json(self.url) 66 | elif self.mode == "get_file": 67 | self.get_file(self.url, self.path) 68 | 69 | def get_json(self, url: str) -> None: 70 | """通过get方法获取json数据""" 71 | 72 | response = None 73 | 74 | for _ in range(self.max_retries): 75 | try: 76 | response = requests.get(url, timeout=self.timeout) 77 | self.status_code = response.status_code 78 | self.response_json = response.json() 79 | self.error_message = None 80 | break 81 | except Exception as e: 82 | self.status_code = response.status_code if response else None 83 | self.response_json = None 84 | self.error_message = str(e) 85 | time.sleep(self.backoff_factor) 86 | 87 | self.loop.quit() 88 | 89 | def get_file(self, url: str, path: Path) -> None: 90 | """通过get方法下载文件""" 91 | 92 | response = None 93 | 94 | try: 95 | response = requests.get(url, timeout=10) 96 | if response.status_code == 200: 97 | with open(path, "wb") as file: 98 | file.write(response.content) 99 | self.status_code = response.status_code 100 | else: 101 | self.status_code = response.status_code 102 | self.error_message = "下载失败" 103 | 104 | except Exception as e: 105 | self.status_code = response.status_code if response else None 106 | self.error_message = str(e) 107 | 108 | self.loop.quit() 109 | 110 | 111 | class _Network(QObject): 112 | """网络请求线程类""" 113 | 114 | def __init__(self) -> None: 115 | super().__init__() 116 | 117 | self.task_queue = [] 118 | 119 | def add_task(self, mode: str, url: str, path: Path = None) -> NetworkThread: 120 | """添加网络请求任务""" 121 | 122 | network_thread = NetworkThread(mode, url, path) 123 | 124 | self.task_queue.append(network_thread) 125 | 126 | network_thread.start() 127 | 128 | return network_thread 129 | 130 | def get_result(self, network_thread: NetworkThread) -> dict: 131 | """获取网络请求结果""" 132 | 133 | result = { 134 | "status_code": network_thread.status_code, 135 | "response_json": network_thread.response_json, 136 | "error_message": ( 137 | re.sub(r"(&cdk=)[^&]+(&)", r"\1******\2", network_thread.error_message) 138 | if network_thread.error_message 139 | else None 140 | ), 141 | } 142 | 143 | network_thread.quit() 144 | network_thread.wait() 145 | self.task_queue.remove(network_thread) 146 | network_thread.deleteLater() 147 | 148 | return result 149 | 150 | 151 | Network = _Network() 152 | -------------------------------------------------------------------------------- /app/core/sound_player.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA音效播放器 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | from loguru import logger 29 | from PySide6.QtCore import QObject, QUrl 30 | from PySide6.QtMultimedia import QSoundEffect 31 | from pathlib import Path 32 | 33 | 34 | from .config import Config 35 | 36 | 37 | class _SoundPlayer(QObject): 38 | 39 | def __init__(self): 40 | super().__init__() 41 | 42 | self.sounds_path = Config.app_path / "resources/sounds" 43 | 44 | def play(self, sound_name: str): 45 | 46 | if not Config.get(Config.voice_Enabled): 47 | return 48 | 49 | if (self.sounds_path / f"both/{sound_name}.wav").exists(): 50 | 51 | self.play_voice(self.sounds_path / f"both/{sound_name}.wav") 52 | 53 | elif ( 54 | self.sounds_path / Config.get(Config.voice_Type) / f"{sound_name}.wav" 55 | ).exists(): 56 | 57 | self.play_voice( 58 | self.sounds_path / Config.get(Config.voice_Type) / f"{sound_name}.wav" 59 | ) 60 | 61 | def play_voice(self, sound_path: Path): 62 | 63 | effect = QSoundEffect(self) 64 | effect.setVolume(1) 65 | effect.setSource(QUrl.fromLocalFile(sound_path)) 66 | effect.play() 67 | 68 | 69 | SoundPlayer = _SoundPlayer() 70 | -------------------------------------------------------------------------------- /app/core/task_manager.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA业务调度器 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | from loguru import logger 29 | from PySide6.QtCore import QThread, QObject, Signal 30 | from qfluentwidgets import MessageBox 31 | from datetime import datetime 32 | from packaging import version 33 | from typing import Dict, Union 34 | 35 | from .config import Config 36 | from .main_info_bar import MainInfoBar 37 | from .network import Network 38 | from .sound_player import SoundPlayer 39 | from app.models import MaaManager 40 | from app.services import System 41 | 42 | 43 | class Task(QThread): 44 | """业务线程""" 45 | 46 | check_maa_version = Signal(str) 47 | push_info_bar = Signal(str, str, str, int) 48 | play_sound = Signal(str) 49 | question = Signal(str, str) 50 | question_response = Signal(bool) 51 | update_user_info = Signal(str, dict) 52 | create_task_list = Signal(list) 53 | create_user_list = Signal(list) 54 | update_task_list = Signal(list) 55 | update_user_list = Signal(list) 56 | update_log_text = Signal(str) 57 | accomplish = Signal(list) 58 | 59 | def __init__( 60 | self, mode: str, name: str, info: Dict[str, Dict[str, Union[str, int, bool]]] 61 | ): 62 | super(Task, self).__init__() 63 | 64 | self.setObjectName(f"Task-{mode}-{name}") 65 | 66 | self.mode = mode 67 | self.name = name 68 | self.info = info 69 | 70 | self.logs = [] 71 | 72 | self.question_response.connect(lambda: print("response")) 73 | 74 | @logger.catch 75 | def run(self): 76 | 77 | if "设置MAA" in self.mode: 78 | 79 | logger.info(f"任务开始:设置{self.name}") 80 | self.push_info_bar.emit("info", "设置MAA", self.name, 3000) 81 | 82 | self.task = MaaManager( 83 | self.mode, 84 | Config.member_dict[self.name], 85 | (None if "全局" in self.mode else self.info["SetMaaInfo"]["Path"]), 86 | ) 87 | self.task.check_maa_version.connect(self.check_maa_version.emit) 88 | self.task.push_info_bar.connect(self.push_info_bar.emit) 89 | self.task.play_sound.connect(self.play_sound.emit) 90 | self.task.accomplish.connect(lambda: self.accomplish.emit([])) 91 | 92 | self.task.run() 93 | 94 | else: 95 | 96 | self.task_list = [ 97 | [ 98 | ( 99 | value 100 | if Config.member_dict[value]["Config"].get( 101 | Config.member_dict[value]["Config"].MaaSet_Name 102 | ) 103 | == "" 104 | else f"{value} - {Config.member_dict[value]["Config"].get(Config.member_dict[value]["Config"].MaaSet_Name)}" 105 | ), 106 | "等待", 107 | value, 108 | ] 109 | for _, value in sorted( 110 | self.info["Queue"].items(), key=lambda x: int(x[0][7:]) 111 | ) 112 | if value != "禁用" 113 | ] 114 | 115 | self.create_task_list.emit(self.task_list) 116 | 117 | for task in self.task_list: 118 | 119 | if self.isInterruptionRequested(): 120 | break 121 | 122 | task[1] = "运行" 123 | self.update_task_list.emit(self.task_list) 124 | 125 | if task[2] in Config.running_list: 126 | 127 | task[1] = "跳过" 128 | self.update_task_list.emit(self.task_list) 129 | logger.info(f"跳过任务:{task[0]}") 130 | self.push_info_bar.emit("info", "跳过任务", task[0], 3000) 131 | continue 132 | 133 | Config.running_list.append(task[2]) 134 | logger.info(f"任务开始:{task[0]}") 135 | self.push_info_bar.emit("info", "任务开始", task[0], 3000) 136 | 137 | if Config.member_dict[task[2]]["Type"] == "Maa": 138 | 139 | self.task = MaaManager( 140 | self.mode[0:4], 141 | Config.member_dict[task[2]], 142 | ) 143 | 144 | self.task.check_maa_version.connect(self.check_maa_version.emit) 145 | self.task.question.connect(self.question.emit) 146 | self.question_response.disconnect() 147 | self.question_response.connect(self.task.question_response.emit) 148 | self.task.push_info_bar.connect(self.push_info_bar.emit) 149 | self.task.play_sound.connect(self.play_sound.emit) 150 | self.task.create_user_list.connect(self.create_user_list.emit) 151 | self.task.update_user_list.connect(self.update_user_list.emit) 152 | self.task.update_log_text.connect(self.update_log_text.emit) 153 | self.task.update_user_info.connect(self.update_user_info.emit) 154 | self.task.accomplish.connect( 155 | lambda log: self.task_accomplish(task[2], log) 156 | ) 157 | 158 | self.task.run() 159 | 160 | Config.running_list.remove(task[2]) 161 | 162 | task[1] = "完成" 163 | self.update_task_list.emit(self.task_list) 164 | logger.info(f"任务完成:{task[0]}") 165 | self.push_info_bar.emit("info", "任务完成", task[0], 3000) 166 | 167 | self.accomplish.emit(self.logs) 168 | 169 | def task_accomplish(self, name: str, log: dict): 170 | """保存保存任务结果""" 171 | 172 | self.logs.append([name, log]) 173 | self.task.deleteLater() 174 | 175 | 176 | class _TaskManager(QObject): 177 | """业务调度器""" 178 | 179 | create_gui = Signal(Task) 180 | connect_gui = Signal(Task) 181 | 182 | def __init__(self): 183 | super(_TaskManager, self).__init__() 184 | 185 | self.task_dict: Dict[str, Task] = {} 186 | 187 | def add_task( 188 | self, mode: str, name: str, info: Dict[str, Dict[str, Union[str, int, bool]]] 189 | ): 190 | """添加任务""" 191 | 192 | if name in Config.running_list or name in self.task_dict: 193 | 194 | logger.warning(f"任务已存在:{name}") 195 | MainInfoBar.push_info_bar("warning", "任务已存在", name, 5000) 196 | return None 197 | 198 | logger.info(f"任务开始:{name}") 199 | MainInfoBar.push_info_bar("info", "任务开始", name, 3000) 200 | SoundPlayer.play("任务开始") 201 | 202 | Config.running_list.append(name) 203 | self.task_dict[name] = Task(mode, name, info) 204 | self.task_dict[name].check_maa_version.connect(self.check_maa_version) 205 | self.task_dict[name].question.connect( 206 | lambda title, content: self.push_dialog(name, title, content) 207 | ) 208 | self.task_dict[name].push_info_bar.connect(MainInfoBar.push_info_bar) 209 | self.task_dict[name].play_sound.connect(SoundPlayer.play) 210 | self.task_dict[name].update_user_info.connect(Config.change_user_info) 211 | self.task_dict[name].accomplish.connect( 212 | lambda logs: self.remove_task(mode, name, logs) 213 | ) 214 | 215 | if "新调度台" in mode: 216 | self.create_gui.emit(self.task_dict[name]) 217 | 218 | elif "主调度台" in mode: 219 | self.connect_gui.emit(self.task_dict[name]) 220 | 221 | self.task_dict[name].start() 222 | 223 | def stop_task(self, name: str): 224 | """中止任务""" 225 | 226 | logger.info(f"中止任务:{name}") 227 | MainInfoBar.push_info_bar("info", "中止任务", name, 3000) 228 | 229 | if name == "ALL": 230 | 231 | for name in self.task_dict: 232 | 233 | self.task_dict[name].task.requestInterruption() 234 | self.task_dict[name].requestInterruption() 235 | self.task_dict[name].quit() 236 | self.task_dict[name].wait() 237 | 238 | elif name in self.task_dict: 239 | 240 | self.task_dict[name].task.requestInterruption() 241 | self.task_dict[name].requestInterruption() 242 | self.task_dict[name].quit() 243 | self.task_dict[name].wait() 244 | 245 | def remove_task(self, mode: str, name: str, logs: list): 246 | """任务结束后的处理""" 247 | 248 | logger.info(f"任务结束:{name}") 249 | MainInfoBar.push_info_bar("info", "任务结束", name, 3000) 250 | SoundPlayer.play("任务结束") 251 | 252 | self.task_dict[name].deleteLater() 253 | self.task_dict.pop(name) 254 | Config.running_list.remove(name) 255 | 256 | if "调度队列" in name and "人工排查" not in mode: 257 | 258 | if len(logs) > 0: 259 | time = logs[0][1]["Time"] 260 | history = "" 261 | for log in logs: 262 | history += f"任务名称:{log[0]},{log[1]["History"].replace("\n","\n ")}\n" 263 | Config.save_history(name, {"Time": time, "History": history}) 264 | else: 265 | Config.save_history( 266 | name, 267 | { 268 | "Time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 269 | "History": "没有任务被执行", 270 | }, 271 | ) 272 | 273 | if ( 274 | Config.queue_dict[name]["Config"].get( 275 | Config.queue_dict[name]["Config"].queueSet_AfterAccomplish 276 | ) 277 | != "NoAction" 278 | and Config.power_sign == "NoAction" 279 | ): 280 | Config.set_power_sign( 281 | Config.queue_dict[name]["Config"].get( 282 | Config.queue_dict[name]["Config"].queueSet_AfterAccomplish 283 | ) 284 | ) 285 | 286 | def check_maa_version(self, v: str): 287 | """检查MAA版本""" 288 | 289 | network = Network.add_task( 290 | mode="get", 291 | url="https://mirrorchyan.com/api/resources/MAA/latest?user_agent=AutoMaaGui&os=win&arch=x64&channel=stable", 292 | ) 293 | network.loop.exec() 294 | network_result = Network.get_result(network) 295 | if network_result["status_code"] == 200: 296 | maa_info = network_result["response_json"] 297 | else: 298 | logger.warning(f"获取MAA版本信息时出错:{network_result['error_message']}") 299 | MainInfoBar.push_info_bar( 300 | "warning", 301 | "获取MAA版本信息时出错", 302 | f"网络错误:{network_result['status_code']}", 303 | 5000, 304 | ) 305 | return None 306 | 307 | if version.parse(maa_info["data"]["version_name"]) > version.parse(v): 308 | 309 | logger.info( 310 | f"检测到MAA版本过低:{v},最新版本:{maa_info['data']['version_name']}" 311 | ) 312 | MainInfoBar.push_info_bar( 313 | "info", 314 | "MAA版本过低", 315 | f"当前版本:{v},最新稳定版:{maa_info['data']['version_name']}", 316 | -1, 317 | ) 318 | 319 | def push_dialog(self, name: str, title: str, content: str): 320 | """推送对话框""" 321 | 322 | choice = MessageBox(title, content, Config.main_window) 323 | choice.yesButton.setText("是") 324 | choice.cancelButton.setText("否") 325 | 326 | self.task_dict[name].question_response.emit(bool(choice.exec())) 327 | 328 | 329 | TaskManager = _TaskManager() 330 | -------------------------------------------------------------------------------- /app/core/timer.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA主业务定时器 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | from loguru import logger 29 | from PySide6.QtCore import QObject, QTimer 30 | from datetime import datetime 31 | from pathlib import Path 32 | import pyautogui 33 | 34 | from .config import Config 35 | from .task_manager import TaskManager 36 | from app.services import System 37 | 38 | 39 | class _MainTimer(QObject): 40 | 41 | def __init__(self, parent=None): 42 | super().__init__(parent) 43 | 44 | self.if_FailSafeException = False 45 | 46 | self.Timer = QTimer() 47 | self.Timer.timeout.connect(self.timed_start) 48 | self.Timer.timeout.connect(self.set_silence) 49 | self.Timer.timeout.connect(self.check_power) 50 | self.Timer.start(1000) 51 | self.LongTimer = QTimer() 52 | self.LongTimer.timeout.connect(self.long_timed_task) 53 | self.LongTimer.start(3600000) 54 | 55 | def long_timed_task(self): 56 | """长时间定期检定任务""" 57 | 58 | Config.get_gameid() 59 | Config.main_window.setting.show_notice() 60 | if Config.get(Config.update_IfAutoUpdate): 61 | Config.main_window.setting.check_update() 62 | 63 | def timed_start(self): 64 | """定时启动代理任务""" 65 | 66 | for name, info in Config.queue_dict.items(): 67 | 68 | if not info["Config"].get(info["Config"].queueSet_Enabled): 69 | continue 70 | 71 | data = info["Config"].toDict() 72 | 73 | time_set = [ 74 | data["Time"][f"TimeSet_{_}"] 75 | for _ in range(10) 76 | if data["Time"][f"TimeEnabled_{_}"] 77 | ] 78 | # 按时间调起代理任务 79 | curtime = datetime.now().strftime("%Y-%m-%d %H:%M") 80 | if ( 81 | curtime[11:16] in time_set 82 | and curtime 83 | != info["Config"].get(info["Config"].Data_LastProxyTime)[:16] 84 | and name not in Config.running_list 85 | ): 86 | 87 | logger.info(f"定时任务:{name}") 88 | TaskManager.add_task("自动代理_新调度台", name, data) 89 | 90 | def set_silence(self): 91 | """设置静默模式""" 92 | 93 | if ( 94 | not Config.if_ignore_silence 95 | and Config.get(Config.function_IfSilence) 96 | and Config.get(Config.function_BossKey) != "" 97 | ): 98 | 99 | windows = System.get_window_info() 100 | 101 | # 排除雷电名为新通知的窗口 102 | windows = [ 103 | window 104 | for window in windows 105 | if not ( 106 | window[0] == "新通知" and Path(window[1]) in Config.silence_list 107 | ) 108 | ] 109 | 110 | if any( 111 | str(emulator_path) in window 112 | for window in windows 113 | for emulator_path in Config.silence_list 114 | ): 115 | try: 116 | pyautogui.hotkey( 117 | *[ 118 | _.strip().lower() 119 | for _ in Config.get(Config.function_BossKey).split("+") 120 | ] 121 | ) 122 | except pyautogui.FailSafeException as e: 123 | if not self.if_FailSafeException: 124 | logger.warning(f"FailSafeException: {e}") 125 | self.if_FailSafeException = True 126 | 127 | def check_power(self): 128 | 129 | if Config.power_sign != "NoAction" and not Config.running_list: 130 | 131 | from app.ui import ProgressRingMessageBox 132 | 133 | mode_book = { 134 | "KillSelf": "退出软件", 135 | "Sleep": "睡眠", 136 | "Hibernate": "休眠", 137 | "Shutdown": "关机", 138 | } 139 | 140 | choice = ProgressRingMessageBox( 141 | Config.main_window, f"{mode_book[Config.power_sign]}倒计时" 142 | ) 143 | if choice.exec(): 144 | System.set_power(Config.power_sign) 145 | Config.set_power_sign("NoAction") 146 | else: 147 | Config.set_power_sign("NoAction") 148 | 149 | 150 | MainTimer = _MainTimer() 151 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA模组包 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | __version__ = "4.2.0" 29 | __author__ = "DLmaster361 " 30 | __license__ = "GPL-3.0 license" 31 | 32 | from .MAA import MaaManager 33 | 34 | __all__ = ["MaaManager"] 35 | -------------------------------------------------------------------------------- /app/services/__init__.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA服务包 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | __version__ = "4.2.0" 29 | __author__ = "DLmaster361 " 30 | __license__ = "GPL-3.0 license" 31 | 32 | from .notification import Notify 33 | from .security import Crypto 34 | from .system import System 35 | 36 | __all__ = ["Notify", "Crypto", "System"] 37 | -------------------------------------------------------------------------------- /app/services/notification.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA通知服务 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | import re 29 | import smtplib 30 | import time 31 | from email.header import Header 32 | from email.mime.multipart import MIMEMultipart 33 | from email.mime.text import MIMEText 34 | from email.utils import formataddr 35 | 36 | import requests 37 | from PySide6.QtCore import QObject, Signal 38 | from loguru import logger 39 | from plyer import notification 40 | 41 | from app.core import Config 42 | from app.services.security import Crypto 43 | 44 | 45 | class Notification(QObject): 46 | 47 | push_info_bar = Signal(str, str, str, int) 48 | 49 | def __init__(self, parent=None): 50 | super().__init__(parent) 51 | 52 | def push_plyer(self, title, message, ticker, t): 53 | """推送系统通知""" 54 | 55 | if Config.get(Config.notify_IfPushPlyer): 56 | 57 | notification.notify( 58 | title=title, 59 | message=message, 60 | app_name="AUTO_MAA", 61 | app_icon=str(Config.app_path / "resources/icons/AUTO_MAA.ico"), 62 | timeout=t, 63 | ticker=ticker, 64 | toast=True, 65 | ) 66 | 67 | return True 68 | 69 | def send_mail(self, mode, title, content, to_address) -> None: 70 | """推送邮件通知""" 71 | if ( 72 | Config.get(Config.notify_SMTPServerAddress) == "" 73 | or Config.get(Config.notify_AuthorizationCode) == "" 74 | or not bool( 75 | re.match( 76 | r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", 77 | Config.get(Config.notify_FromAddress), 78 | ) 79 | ) 80 | or not bool( 81 | re.match( 82 | r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", 83 | to_address, 84 | ) 85 | ) 86 | ): 87 | logger.error( 88 | "请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址" 89 | ) 90 | self.push_info_bar.emit( 91 | "error", 92 | "邮件通知推送异常", 93 | "请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址", 94 | -1, 95 | ) 96 | return None 97 | 98 | try: 99 | # 定义邮件正文 100 | if mode == "文本": 101 | message = MIMEText(content, "plain", "utf-8") 102 | elif mode == "网页": 103 | message = MIMEMultipart("alternative") 104 | message["From"] = formataddr( 105 | ( 106 | Header("AUTO_MAA通知服务", "utf-8").encode(), 107 | Config.get(Config.notify_FromAddress), 108 | ) 109 | ) # 发件人显示的名字 110 | message["To"] = formataddr( 111 | ( 112 | Header("AUTO_MAA用户", "utf-8").encode(), 113 | to_address, 114 | ) 115 | ) # 收件人显示的名字 116 | message["Subject"] = Header(title, "utf-8") 117 | 118 | if mode == "网页": 119 | message.attach(MIMEText(content, "html", "utf-8")) 120 | 121 | smtpObj = smtplib.SMTP_SSL( 122 | Config.get(Config.notify_SMTPServerAddress), 123 | 465, 124 | ) 125 | smtpObj.login( 126 | Config.get(Config.notify_FromAddress), 127 | Crypto.win_decryptor(Config.get(Config.notify_AuthorizationCode)), 128 | ) 129 | smtpObj.sendmail( 130 | Config.get(Config.notify_FromAddress), 131 | to_address, 132 | message.as_string(), 133 | ) 134 | smtpObj.quit() 135 | logger.success("邮件发送成功") 136 | return None 137 | except Exception as e: 138 | logger.error(f"发送邮件时出错:\n{e}") 139 | self.push_info_bar.emit("error", "发送邮件时出错", f"{e}", -1) 140 | return None 141 | return None 142 | 143 | def ServerChanPush(self, title, content, send_key, tag, channel): 144 | """使用Server酱推送通知""" 145 | if not send_key: 146 | logger.error("请正确设置Server酱的SendKey") 147 | self.push_info_bar.emit( 148 | "error", "Server酱通知推送异常", "请正确设置Server酱的SendKey", -1 149 | ) 150 | return None 151 | 152 | try: 153 | # 构造 URL 154 | if send_key.startswith("sctp"): 155 | match = re.match(r"^sctp(\d+)t", send_key) 156 | if match: 157 | url = f"https://{match.group(1)}.push.ft07.com/send/{send_key}.send" 158 | else: 159 | raise ValueError("SendKey 格式错误(sctp)") 160 | else: 161 | url = f"https://sctapi.ftqq.com/{send_key}.send" 162 | 163 | # 构建 tags 和 channel 164 | def is_valid(s): 165 | return s == "" or ( 166 | s == "|".join(s.split("|")) 167 | and (s.count("|") == 0 or all(s.split("|"))) 168 | ) 169 | 170 | tags = "|".join(_.strip() for _ in tag.split("|")) 171 | channels = "|".join(_.strip() for _ in channel.split("|")) 172 | 173 | options = {} 174 | if is_valid(tags): 175 | options["tags"] = tags 176 | else: 177 | logger.warning("Server酱 Tag 配置不正确,将被忽略") 178 | self.push_info_bar.emit( 179 | "warning", 180 | "Server酱通知推送异常", 181 | "请正确设置 ServerChan 的 Tag", 182 | -1, 183 | ) 184 | 185 | if is_valid(channels): 186 | options["channel"] = channels 187 | else: 188 | logger.warning("Server酱 Channel 配置不正确,将被忽略") 189 | self.push_info_bar.emit( 190 | "warning", 191 | "Server酱通知推送异常", 192 | "请正确设置 ServerChan 的 Channel", 193 | -1, 194 | ) 195 | 196 | # 请求发送 197 | params = {"title": title, "desp": content, **options} 198 | headers = {"Content-Type": "application/json;charset=utf-8"} 199 | 200 | response = requests.post(url, json=params, headers=headers, timeout=10) 201 | result = response.json() 202 | 203 | if result.get("code") == 0: 204 | logger.info("Server酱推送通知成功") 205 | return True 206 | else: 207 | error_code = result.get("code", "-1") 208 | logger.error(f"Server酱通知推送失败:响应码:{error_code}") 209 | self.push_info_bar.emit( 210 | "error", "Server酱通知推送失败", f"响应码:{error_code}", -1 211 | ) 212 | return f"Server酱通知推送失败:{error_code}" 213 | 214 | except Exception as e: 215 | logger.exception("Server酱通知推送异常") 216 | self.push_info_bar.emit( 217 | "error", 218 | "Server酱通知推送异常", 219 | "请检查相关设置和网络连接。如全部配置正确,请稍后再试。", 220 | -1, 221 | ) 222 | return f"Server酱通知推送异常:{str(e)}" 223 | 224 | def CompanyWebHookBotPush(self, title, content, webhook_url): 225 | """使用企业微信群机器人推送通知""" 226 | if webhook_url == "": 227 | logger.error("请正确设置企业微信群机器人的WebHook地址") 228 | self.push_info_bar.emit( 229 | "error", 230 | "企业微信群机器人通知推送异常", 231 | "请正确设置企业微信群机器人的WebHook地址", 232 | -1, 233 | ) 234 | return None 235 | 236 | content = f"{title}\n{content}" 237 | data = {"msgtype": "text", "text": {"content": content}} 238 | 239 | for _ in range(3): 240 | try: 241 | response = requests.post( 242 | url=webhook_url, 243 | json=data, 244 | timeout=10, 245 | ) 246 | info = response.json() 247 | break 248 | except Exception as e: 249 | err = e 250 | time.sleep(0.1) 251 | else: 252 | logger.error(f"推送企业微信群机器人时出错:{err}") 253 | self.push_info_bar.emit( 254 | "error", 255 | "企业微信群机器人通知推送失败", 256 | f"使用企业微信群机器人推送通知时出错:{err}", 257 | -1, 258 | ) 259 | return None 260 | 261 | if info["errcode"] == 0: 262 | logger.info("企业微信群机器人推送通知成功") 263 | return True 264 | else: 265 | logger.error(f"企业微信群机器人推送通知失败:{info}") 266 | self.push_info_bar.emit( 267 | "error", 268 | "企业微信群机器人通知推送失败", 269 | f"使用企业微信群机器人推送通知时出错:{err}", 270 | -1, 271 | ) 272 | return f"使用企业微信群机器人推送通知时出错:{err}" 273 | 274 | def send_test_notification(self): 275 | """发送测试通知到所有已启用的通知渠道""" 276 | # 发送系统通知 277 | self.push_plyer( 278 | "测试通知", 279 | "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!", 280 | "测试通知", 281 | 3, 282 | ) 283 | 284 | # 发送邮件通知 285 | if Config.get(Config.notify_IfSendMail): 286 | self.send_mail( 287 | "文本", 288 | "AUTO_MAA测试通知", 289 | "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!", 290 | Config.get(Config.notify_ToAddress), 291 | ) 292 | 293 | # 发送Server酱通知 294 | if Config.get(Config.notify_IfServerChan): 295 | self.ServerChanPush( 296 | "AUTO_MAA测试通知", 297 | "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!", 298 | Config.get(Config.notify_ServerChanKey), 299 | Config.get(Config.notify_ServerChanTag), 300 | Config.get(Config.notify_ServerChanChannel), 301 | ) 302 | 303 | # 发送企业微信机器人通知 304 | if Config.get(Config.notify_IfCompanyWebHookBot): 305 | self.CompanyWebHookBotPush( 306 | "AUTO_MAA测试通知", 307 | "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!", 308 | Config.get(Config.notify_CompanyWebHookBotUrl), 309 | ) 310 | 311 | return True 312 | 313 | 314 | Notify = Notification() 315 | -------------------------------------------------------------------------------- /app/services/security.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA安全服务 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | from loguru import logger 29 | import hashlib 30 | import random 31 | import secrets 32 | import base64 33 | import win32crypt 34 | from pathlib import Path 35 | from Crypto.Cipher import AES 36 | from Crypto.PublicKey import RSA 37 | from Crypto.Cipher import PKCS1_OAEP 38 | from Crypto.Util.Padding import pad, unpad 39 | from typing import List, Dict, Union 40 | 41 | from app.core import Config 42 | 43 | 44 | class CryptoHandler: 45 | 46 | def get_PASSWORD(self, PASSWORD: str) -> None: 47 | """配置管理密钥""" 48 | 49 | # 生成目录 50 | Config.key_path.mkdir(parents=True, exist_ok=True) 51 | 52 | # 生成RSA密钥对 53 | key = RSA.generate(2048) 54 | public_key_local = key.publickey() 55 | private_key = key 56 | # 保存RSA公钥 57 | (Config.app_path / "data/key/public_key.pem").write_bytes( 58 | public_key_local.exportKey() 59 | ) 60 | # 生成密钥转换与校验随机盐 61 | PASSWORD_salt = secrets.token_hex(random.randint(32, 1024)) 62 | (Config.app_path / "data/key/PASSWORDsalt.txt").write_text( 63 | PASSWORD_salt, 64 | encoding="utf-8", 65 | ) 66 | verify_salt = secrets.token_hex(random.randint(32, 1024)) 67 | (Config.app_path / "data/key/verifysalt.txt").write_text( 68 | verify_salt, 69 | encoding="utf-8", 70 | ) 71 | # 将管理密钥转化为AES-256密钥 72 | AES_password = hashlib.sha256( 73 | (PASSWORD + PASSWORD_salt).encode("utf-8") 74 | ).digest() 75 | # 生成AES-256密钥校验哈希值并保存 76 | AES_password_verify = hashlib.sha256( 77 | AES_password + verify_salt.encode("utf-8") 78 | ).digest() 79 | (Config.app_path / "data/key/AES_password_verify.bin").write_bytes( 80 | AES_password_verify 81 | ) 82 | # AES-256加密RSA私钥并保存密文 83 | AES_key = AES.new(AES_password, AES.MODE_ECB) 84 | private_key_local = AES_key.encrypt(pad(private_key.exportKey(), 32)) 85 | (Config.app_path / "data/key/private_key.bin").write_bytes(private_key_local) 86 | 87 | def AUTO_encryptor(self, note: str) -> str: 88 | """使用AUTO_MAA的算法加密数据""" 89 | 90 | if note == "": 91 | return "" 92 | 93 | # 读取RSA公钥 94 | public_key_local = RSA.import_key( 95 | (Config.app_path / "data/key/public_key.pem").read_bytes() 96 | ) 97 | # 使用RSA公钥对数据进行加密 98 | cipher = PKCS1_OAEP.new(public_key_local) 99 | encrypted = cipher.encrypt(note.encode("utf-8")) 100 | return base64.b64encode(encrypted).decode("utf-8") 101 | 102 | def AUTO_decryptor(self, note: str, PASSWORD: str) -> str: 103 | """使用AUTO_MAA的算法解密数据""" 104 | 105 | if note == "": 106 | return "" 107 | 108 | # 读入RSA私钥密文、盐与校验哈希值 109 | private_key_local = ( 110 | (Config.app_path / "data/key/private_key.bin").read_bytes().strip() 111 | ) 112 | PASSWORD_salt = ( 113 | (Config.app_path / "data/key/PASSWORDsalt.txt") 114 | .read_text(encoding="utf-8") 115 | .strip() 116 | ) 117 | verify_salt = ( 118 | (Config.app_path / "data/key/verifysalt.txt") 119 | .read_text(encoding="utf-8") 120 | .strip() 121 | ) 122 | AES_password_verify = ( 123 | (Config.app_path / "data/key/AES_password_verify.bin").read_bytes().strip() 124 | ) 125 | # 将管理密钥转化为AES-256密钥并验证 126 | AES_password = hashlib.sha256( 127 | (PASSWORD + PASSWORD_salt).encode("utf-8") 128 | ).digest() 129 | AES_password_SHA = hashlib.sha256( 130 | AES_password + verify_salt.encode("utf-8") 131 | ).digest() 132 | if AES_password_SHA != AES_password_verify: 133 | return "管理密钥错误" 134 | else: 135 | # AES解密RSA私钥 136 | AES_key = AES.new(AES_password, AES.MODE_ECB) 137 | private_key_pem = unpad(AES_key.decrypt(private_key_local), 32) 138 | private_key = RSA.import_key(private_key_pem) 139 | # 使用RSA私钥解密数据 140 | decrypter = PKCS1_OAEP.new(private_key) 141 | note = decrypter.decrypt(base64.b64decode(note)).decode("utf-8") 142 | return note 143 | 144 | def change_PASSWORD(self, PASSWORD_old: str, PASSWORD_new: str) -> None: 145 | """修改管理密钥""" 146 | 147 | for member in Config.member_dict.values(): 148 | 149 | # 使用旧管理密钥解密 150 | for user in member["UserData"].values(): 151 | user["Password"] = self.AUTO_decryptor( 152 | user["Config"].get(user["Config"].Info_Password), PASSWORD_old 153 | ) 154 | 155 | self.get_PASSWORD(PASSWORD_new) 156 | 157 | for member in Config.member_dict.values(): 158 | 159 | # 使用新管理密钥重新加密 160 | for user in member["UserData"].values(): 161 | user["Config"].set( 162 | user["Config"].Info_Password, self.AUTO_encryptor(user["Password"]) 163 | ) 164 | user["Password"] = None 165 | del user["Password"] 166 | 167 | def win_encryptor( 168 | self, note: str, description: str = None, entropy: bytes = None 169 | ) -> str: 170 | """使用Windows DPAPI加密数据""" 171 | 172 | if note == "": 173 | return "" 174 | 175 | encrypted = win32crypt.CryptProtectData( 176 | note.encode("utf-8"), description, entropy, None, None, 0 177 | ) 178 | return base64.b64encode(encrypted).decode("utf-8") 179 | 180 | def win_decryptor(self, note: str, entropy: bytes = None) -> str: 181 | """使用Windows DPAPI解密数据""" 182 | 183 | if note == "": 184 | return "" 185 | 186 | decrypted = win32crypt.CryptUnprotectData( 187 | base64.b64decode(note), entropy, None, None, 0 188 | ) 189 | return decrypted[1].decode("utf-8") 190 | 191 | def search_member(self) -> List[Dict[str, Union[Path, list]]]: 192 | """搜索所有脚本实例及其用户数据库路径""" 193 | 194 | member_list = [] 195 | 196 | if (Config.app_path / "config/MaaConfig").exists(): 197 | for subdir in (Config.app_path / "config/MaaConfig").iterdir(): 198 | if subdir.is_dir(): 199 | 200 | member_list.append({"Path": subdir / "user_data.db"}) 201 | 202 | return member_list 203 | 204 | def check_PASSWORD(self, PASSWORD: str) -> bool: 205 | """验证管理密钥""" 206 | 207 | return bool( 208 | self.AUTO_decryptor(self.AUTO_encryptor("-"), PASSWORD) != "管理密钥错误" 209 | ) 210 | 211 | 212 | Crypto = CryptoHandler() 213 | -------------------------------------------------------------------------------- /app/services/system.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA系统服务 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | from loguru import logger 29 | from PySide6.QtWidgets import QApplication 30 | import sys 31 | import ctypes 32 | import win32gui 33 | import win32process 34 | import winreg 35 | import psutil 36 | import subprocess 37 | from pathlib import Path 38 | 39 | from app.core import Config 40 | 41 | 42 | class _SystemHandler: 43 | 44 | ES_CONTINUOUS = 0x80000000 45 | ES_SYSTEM_REQUIRED = 0x00000001 46 | 47 | def __init__(self): 48 | 49 | self.set_Sleep() 50 | self.set_SelfStart() 51 | 52 | def set_Sleep(self) -> None: 53 | """同步系统休眠状态""" 54 | 55 | if Config.get(Config.function_IfAllowSleep): 56 | # 设置系统电源状态 57 | ctypes.windll.kernel32.SetThreadExecutionState( 58 | self.ES_CONTINUOUS | self.ES_SYSTEM_REQUIRED 59 | ) 60 | else: 61 | # 恢复系统电源状态 62 | ctypes.windll.kernel32.SetThreadExecutionState(self.ES_CONTINUOUS) 63 | 64 | def set_SelfStart(self) -> None: 65 | """同步开机自启""" 66 | 67 | if Config.get(Config.start_IfSelfStart) and not self.is_startup(): 68 | key = winreg.OpenKey( 69 | winreg.HKEY_CURRENT_USER, 70 | r"Software\Microsoft\Windows\CurrentVersion\Run", 71 | winreg.KEY_SET_VALUE, 72 | winreg.KEY_ALL_ACCESS | winreg.KEY_WRITE | winreg.KEY_CREATE_SUB_KEY, 73 | ) 74 | winreg.SetValueEx(key, "AUTO_MAA", 0, winreg.REG_SZ, Config.app_path_sys) 75 | winreg.CloseKey(key) 76 | elif not Config.get(Config.start_IfSelfStart) and self.is_startup(): 77 | key = winreg.OpenKey( 78 | winreg.HKEY_CURRENT_USER, 79 | r"Software\Microsoft\Windows\CurrentVersion\Run", 80 | winreg.KEY_SET_VALUE, 81 | winreg.KEY_ALL_ACCESS | winreg.KEY_WRITE | winreg.KEY_CREATE_SUB_KEY, 82 | ) 83 | winreg.DeleteValue(key, "AUTO_MAA") 84 | winreg.CloseKey(key) 85 | 86 | def set_power(self, mode) -> None: 87 | 88 | if sys.platform.startswith("win"): 89 | 90 | if mode == "NoAction": 91 | 92 | logger.info("不执行系统电源操作") 93 | 94 | elif mode == "Shutdown": 95 | 96 | logger.info("执行关机操作") 97 | subprocess.run(["shutdown", "/s", "/t", "0"]) 98 | 99 | elif mode == "Hibernate": 100 | 101 | logger.info("执行休眠操作") 102 | subprocess.run(["shutdown", "/h"]) 103 | 104 | elif mode == "Sleep": 105 | 106 | logger.info("执行睡眠操作") 107 | subprocess.run( 108 | ["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"] 109 | ) 110 | 111 | elif mode == "KillSelf": 112 | 113 | Config.main_window.close() 114 | QApplication.quit() 115 | 116 | elif sys.platform.startswith("linux"): 117 | 118 | if mode == "NoAction": 119 | 120 | logger.info("不执行系统电源操作") 121 | 122 | elif mode == "Shutdown": 123 | 124 | logger.info("执行关机操作") 125 | subprocess.run(["shutdown", "-h", "now"]) 126 | 127 | elif mode == "Hibernate": 128 | 129 | logger.info("执行休眠操作") 130 | subprocess.run(["systemctl", "hibernate"]) 131 | 132 | elif mode == "Sleep": 133 | 134 | logger.info("执行睡眠操作") 135 | subprocess.run(["systemctl", "suspend"]) 136 | 137 | elif mode == "KillSelf": 138 | 139 | Config.main_window.close() 140 | QApplication.quit() 141 | 142 | def is_startup(self) -> bool: 143 | """判断程序是否已经开机自启""" 144 | 145 | key = winreg.OpenKey( 146 | winreg.HKEY_CURRENT_USER, 147 | r"Software\Microsoft\Windows\CurrentVersion\Run", 148 | 0, 149 | winreg.KEY_READ, 150 | ) 151 | 152 | try: 153 | value, _ = winreg.QueryValueEx(key, "AUTO_MAA") 154 | winreg.CloseKey(key) 155 | return True 156 | except FileNotFoundError: 157 | winreg.CloseKey(key) 158 | return False 159 | 160 | def get_window_info(self) -> list: 161 | """获取当前窗口信息""" 162 | 163 | def callback(hwnd, window_info): 164 | if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowText(hwnd): 165 | _, pid = win32process.GetWindowThreadProcessId(hwnd) 166 | process = psutil.Process(pid) 167 | window_info.append((win32gui.GetWindowText(hwnd), process.exe())) 168 | return True 169 | 170 | window_info = [] 171 | win32gui.EnumWindows(callback, window_info) 172 | return window_info 173 | 174 | def kill_process(self, path: Path) -> None: 175 | """根据路径中止进程""" 176 | 177 | for pid in self.search_pids(path): 178 | killprocess = subprocess.Popen( 179 | f"taskkill /F /T /PID {pid}", 180 | shell=True, 181 | creationflags=subprocess.CREATE_NO_WINDOW, 182 | ) 183 | killprocess.wait() 184 | 185 | def search_pids(self, path: Path) -> list: 186 | """根据路径查找进程PID""" 187 | 188 | pids = [] 189 | for proc in psutil.process_iter(["pid", "exe"]): 190 | try: 191 | if proc.info["exe"] and proc.info["exe"].lower() == str(path).lower(): 192 | pids.append(proc.info["pid"]) 193 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 194 | # 进程可能在此期间已结束或无法访问,忽略这些异常 195 | pass 196 | return pids 197 | 198 | 199 | System = _SystemHandler() 200 | -------------------------------------------------------------------------------- /app/ui/__init__.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA图形化界面包 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | __version__ = "4.2.0" 29 | __author__ = "DLmaster361 " 30 | __license__ = "GPL-3.0 license" 31 | 32 | from .main_window import AUTO_MAA 33 | from .Widget import ProgressRingMessageBox 34 | 35 | __all__ = ["AUTO_MAA", "ProgressRingMessageBox"] 36 | -------------------------------------------------------------------------------- /app/ui/dispatch_center.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA调度中枢界面 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | from loguru import logger 29 | from PySide6.QtWidgets import ( 30 | QWidget, 31 | QVBoxLayout, 32 | QStackedWidget, 33 | QHBoxLayout, 34 | ) 35 | from qfluentwidgets import ( 36 | BodyLabel, 37 | CardWidget, 38 | ScrollArea, 39 | FluentIcon, 40 | HeaderCardWidget, 41 | FluentIcon, 42 | TextBrowser, 43 | ComboBox, 44 | SubtitleLabel, 45 | PushButton, 46 | ) 47 | from PySide6.QtGui import QTextCursor 48 | from typing import List, Dict 49 | 50 | 51 | from app.core import Config, TaskManager, Task, MainInfoBar, SoundPlayer 52 | from .Widget import StatefulItemCard, ComboBoxMessageBox, PivotArea 53 | 54 | 55 | class DispatchCenter(QWidget): 56 | 57 | def __init__(self, parent=None): 58 | super().__init__(parent) 59 | 60 | self.setObjectName("调度中枢") 61 | 62 | self.multi_button = PushButton(FluentIcon.ADD, "添加任务", self) 63 | self.multi_button.setToolTip("添加任务") 64 | self.multi_button.clicked.connect(self.start_multi_task) 65 | 66 | self.power_combox = ComboBox() 67 | self.power_combox.addItem("无动作", userData="NoAction") 68 | self.power_combox.addItem("退出软件", userData="KillSelf") 69 | self.power_combox.addItem("睡眠", userData="Sleep") 70 | self.power_combox.addItem("休眠", userData="Hibernate") 71 | self.power_combox.addItem("关机", userData="Shutdown") 72 | self.power_combox.setCurrentText("无动作") 73 | self.power_combox.currentIndexChanged.connect(self.set_power_sign) 74 | 75 | self.pivotArea = PivotArea(self) 76 | self.pivot = self.pivotArea.pivot 77 | 78 | self.stackedWidget = QStackedWidget(self) 79 | self.stackedWidget.setContentsMargins(0, 0, 0, 0) 80 | self.stackedWidget.setStyleSheet("background: transparent; border: none;") 81 | 82 | self.script_list: Dict[str, DispatchCenter.DispatchBox] = {} 83 | 84 | dispatch_box = self.DispatchBox("主调度台", self) 85 | self.script_list["主调度台"] = dispatch_box 86 | self.stackedWidget.addWidget(self.script_list["主调度台"]) 87 | self.pivot.addItem( 88 | routeKey="主调度台", 89 | text="主调度台", 90 | onClick=self.update_top_bar, 91 | icon=FluentIcon.CAFE, 92 | ) 93 | 94 | h_layout = QHBoxLayout() 95 | h_layout.addWidget(self.multi_button) 96 | h_layout.addWidget(self.pivotArea) 97 | h_layout.addWidget(BodyLabel("全部完成后", self)) 98 | h_layout.addWidget(self.power_combox) 99 | h_layout.setContentsMargins(11, 5, 11, 0) 100 | 101 | self.Layout = QVBoxLayout(self) 102 | self.Layout.addLayout(h_layout) 103 | self.Layout.addWidget(self.stackedWidget) 104 | self.Layout.setContentsMargins(0, 0, 0, 0) 105 | 106 | self.pivot.currentItemChanged.connect( 107 | lambda index: self.stackedWidget.setCurrentWidget(self.script_list[index]) 108 | ) 109 | 110 | def add_board(self, task: Task) -> None: 111 | """添加一个调度台界面""" 112 | 113 | dispatch_box = self.DispatchBox(task.name, self) 114 | 115 | dispatch_box.top_bar.main_button.clicked.connect( 116 | lambda: TaskManager.stop_task(task.name) 117 | ) 118 | 119 | task.create_task_list.connect(dispatch_box.info.task.create_task) 120 | task.create_user_list.connect(dispatch_box.info.user.create_user) 121 | task.update_task_list.connect(dispatch_box.info.task.update_task) 122 | task.update_user_list.connect(dispatch_box.info.user.update_user) 123 | task.update_log_text.connect(dispatch_box.info.log_text.text.setText) 124 | task.accomplish.connect(lambda: self.del_board(f"调度台_{task.name}")) 125 | 126 | self.script_list[f"调度台_{task.name}"] = dispatch_box 127 | 128 | self.stackedWidget.addWidget(self.script_list[f"调度台_{task.name}"]) 129 | 130 | self.pivot.addItem(routeKey=f"调度台_{task.name}", text=f"调度台 {task.name}") 131 | 132 | def del_board(self, name: str) -> None: 133 | """删除指定子界面""" 134 | 135 | self.pivot.setCurrentItem("主调度台") 136 | self.stackedWidget.removeWidget(self.script_list[name]) 137 | self.script_list[name].deleteLater() 138 | self.pivot.removeWidget(name) 139 | 140 | def connect_main_board(self, task: Task) -> None: 141 | """连接主调度台""" 142 | 143 | self.script_list["主调度台"].top_bar.Lable.setText( 144 | f"{task.name} - {task.mode.replace("_主调度台","")}模式" 145 | ) 146 | self.script_list["主调度台"].top_bar.Lable.show() 147 | self.script_list["主调度台"].top_bar.object.hide() 148 | self.script_list["主调度台"].top_bar.mode.hide() 149 | self.script_list["主调度台"].top_bar.main_button.clicked.disconnect() 150 | self.script_list["主调度台"].top_bar.main_button.setText("中止任务") 151 | self.script_list["主调度台"].top_bar.main_button.clicked.connect( 152 | lambda: TaskManager.stop_task(task.name) 153 | ) 154 | task.create_task_list.connect( 155 | self.script_list["主调度台"].info.task.create_task 156 | ) 157 | task.create_user_list.connect( 158 | self.script_list["主调度台"].info.user.create_user 159 | ) 160 | task.update_task_list.connect( 161 | self.script_list["主调度台"].info.task.update_task 162 | ) 163 | task.update_user_list.connect( 164 | self.script_list["主调度台"].info.user.update_user 165 | ) 166 | task.update_log_text.connect( 167 | self.script_list["主调度台"].info.log_text.text.setText 168 | ) 169 | task.accomplish.connect( 170 | lambda logs: self.disconnect_main_board(task.name, logs) 171 | ) 172 | 173 | def disconnect_main_board(self, name: str, logs: list) -> None: 174 | """断开主调度台""" 175 | 176 | self.script_list["主调度台"].top_bar.Lable.hide() 177 | self.script_list["主调度台"].top_bar.object.show() 178 | self.script_list["主调度台"].top_bar.mode.show() 179 | self.script_list["主调度台"].top_bar.main_button.clicked.disconnect() 180 | self.script_list["主调度台"].top_bar.main_button.setText("开始任务") 181 | self.script_list["主调度台"].top_bar.main_button.clicked.connect( 182 | self.script_list["主调度台"].top_bar.start_main_task 183 | ) 184 | if len(logs) > 0: 185 | history = "" 186 | for log in logs: 187 | history += ( 188 | f"任务名称:{log[0]},{log[1]["History"].replace("\n","\n ")}\n" 189 | ) 190 | self.script_list["主调度台"].info.log_text.text.setText(history) 191 | else: 192 | self.script_list["主调度台"].info.log_text.text.setText("没有任务被执行") 193 | 194 | def update_top_bar(self): 195 | """更新顶栏""" 196 | 197 | self.script_list["主调度台"].top_bar.object.clear() 198 | 199 | for name, info in Config.queue_dict.items(): 200 | self.script_list["主调度台"].top_bar.object.addItem( 201 | ( 202 | "队列" 203 | if info["Config"].get(info["Config"].queueSet_Name) == "" 204 | else f"队列 - {info["Config"].get(info["Config"].queueSet_Name)}" 205 | ), 206 | userData=name, 207 | ) 208 | 209 | for name, info in Config.member_dict.items(): 210 | self.script_list["主调度台"].top_bar.object.addItem( 211 | ( 212 | f"实例 - {info['Type']}" 213 | if info["Config"].get(info["Config"].MaaSet_Name) == "" 214 | else f"实例 - {info['Type']} - {info["Config"].get(info["Config"].MaaSet_Name)}" 215 | ), 216 | userData=name, 217 | ) 218 | 219 | if len(Config.queue_dict) == 1: 220 | self.script_list["主调度台"].top_bar.object.setCurrentIndex(0) 221 | elif len(Config.member_dict) == 1: 222 | self.script_list["主调度台"].top_bar.object.setCurrentIndex( 223 | len(Config.queue_dict) 224 | ) 225 | else: 226 | self.script_list["主调度台"].top_bar.object.setCurrentIndex(-1) 227 | 228 | self.script_list["主调度台"].top_bar.mode.clear() 229 | self.script_list["主调度台"].top_bar.mode.addItems(["自动代理", "人工排查"]) 230 | self.script_list["主调度台"].top_bar.mode.setCurrentIndex(0) 231 | 232 | def update_power_sign(self) -> None: 233 | """更新电源设置""" 234 | 235 | mode_book = { 236 | "NoAction": "无动作", 237 | "KillSelf": "退出软件", 238 | "Sleep": "睡眠", 239 | "Hibernate": "休眠", 240 | "Shutdown": "关机", 241 | } 242 | self.power_combox.currentIndexChanged.disconnect() 243 | self.power_combox.setCurrentText(mode_book[Config.power_sign]) 244 | self.power_combox.currentIndexChanged.connect(self.set_power_sign) 245 | 246 | def set_power_sign(self) -> None: 247 | """设置所有任务完成后动作""" 248 | 249 | if not Config.running_list: 250 | 251 | self.power_combox.currentIndexChanged.disconnect() 252 | self.power_combox.setCurrentText("无动作") 253 | self.power_combox.currentIndexChanged.connect(self.set_power_sign) 254 | logger.warning("没有正在运行的任务,无法设置任务完成后动作") 255 | MainInfoBar.push_info_bar( 256 | "warning", 257 | "没有正在运行的任务", 258 | "无法设置任务完成后动作", 259 | 5000, 260 | ) 261 | 262 | else: 263 | 264 | Config.set_power_sign(self.power_combox.currentData()) 265 | 266 | def start_multi_task(self) -> None: 267 | """开始任务""" 268 | 269 | # 获取所有可用的队列和实例 270 | text_list = [] 271 | data_list = [] 272 | for name, info in Config.queue_dict.items(): 273 | if name in Config.running_list: 274 | continue 275 | text_list.append( 276 | "队列" 277 | if info["Config"].get(info["Config"].queueSet_Name) == "" 278 | else f"队列 - {info["Config"].get(info["Config"].queueSet_Name)}" 279 | ) 280 | data_list.append(name) 281 | 282 | for name, info in Config.member_dict.items(): 283 | if name in Config.running_list: 284 | continue 285 | text_list.append( 286 | f"实例 - {info['Type']}" 287 | if info["Config"].get(info["Config"].MaaSet_Name) == "" 288 | else f"实例 - {info['Type']} - {info["Config"].get(info["Config"].MaaSet_Name)}" 289 | ) 290 | data_list.append(name) 291 | 292 | choice = ComboBoxMessageBox( 293 | self.window(), 294 | "选择一个对象以添加相应多开任务", 295 | ["选择调度对象"], 296 | [text_list], 297 | [data_list], 298 | ) 299 | 300 | if choice.exec() and choice.input[0].currentIndex() != -1: 301 | 302 | if choice.input[0].currentData() in Config.running_list: 303 | logger.warning(f"任务已存在:{choice.input[0].currentData()}") 304 | MainInfoBar.push_info_bar( 305 | "warning", "任务已存在", choice.input[0].currentData(), 5000 306 | ) 307 | return None 308 | 309 | if "调度队列" in choice.input[0].currentData(): 310 | 311 | logger.info(f"用户添加任务:{choice.input[0].currentData()}") 312 | TaskManager.add_task( 313 | "自动代理_新调度台", 314 | choice.input[0].currentData(), 315 | Config.queue_dict[choice.input[0].currentData()]["Config"].toDict(), 316 | ) 317 | 318 | elif "脚本" in choice.input[0].currentData(): 319 | 320 | if Config.member_dict[choice.input[0].currentData()]["Type"] == "Maa": 321 | 322 | logger.info(f"用户添加任务:{choice.input[0].currentData()}") 323 | TaskManager.add_task( 324 | "自动代理_新调度台", 325 | f"自定义队列 - {choice.input[0].currentData()}", 326 | {"Queue": {"Member_1": choice.input[0].currentData()}}, 327 | ) 328 | 329 | class DispatchBox(QWidget): 330 | 331 | def __init__(self, name: str, parent=None): 332 | super().__init__(parent) 333 | 334 | self.setObjectName(name) 335 | 336 | self.top_bar = self.DispatchTopBar(self, name) 337 | self.info = self.DispatchInfoCard(self) 338 | 339 | content_widget = QWidget() 340 | content_layout = QVBoxLayout(content_widget) 341 | content_layout.setContentsMargins(0, 0, 0, 0) 342 | content_layout.addWidget(self.top_bar) 343 | content_layout.addWidget(self.info) 344 | 345 | scrollArea = ScrollArea() 346 | scrollArea.setWidgetResizable(True) 347 | scrollArea.setContentsMargins(0, 0, 0, 0) 348 | scrollArea.setStyleSheet("background: transparent; border: none;") 349 | scrollArea.setWidget(content_widget) 350 | 351 | layout = QVBoxLayout(self) 352 | layout.addWidget(scrollArea) 353 | 354 | class DispatchTopBar(CardWidget): 355 | 356 | def __init__(self, parent=None, name: str = None): 357 | super().__init__(parent) 358 | 359 | Layout = QHBoxLayout(self) 360 | 361 | if name == "主调度台": 362 | 363 | self.Lable = SubtitleLabel("", self) 364 | self.Lable.hide() 365 | self.object = ComboBox() 366 | self.object.setPlaceholderText("请选择调度对象") 367 | self.mode = ComboBox() 368 | self.mode.setPlaceholderText("请选择调度模式") 369 | 370 | self.main_button = PushButton("开始任务") 371 | self.main_button.clicked.connect(self.start_main_task) 372 | 373 | Layout.addWidget(self.Lable) 374 | Layout.addWidget(self.object) 375 | Layout.addWidget(self.mode) 376 | Layout.addStretch(1) 377 | Layout.addWidget(self.main_button) 378 | 379 | else: 380 | 381 | self.Lable = SubtitleLabel(name, self) 382 | self.main_button = PushButton("中止任务") 383 | 384 | Layout.addWidget(self.Lable) 385 | Layout.addStretch(1) 386 | Layout.addWidget(self.main_button) 387 | 388 | def start_main_task(self): 389 | """开始任务""" 390 | 391 | if self.object.currentIndex() == -1: 392 | logger.warning("未选择调度对象") 393 | MainInfoBar.push_info_bar( 394 | "warning", "未选择调度对象", "请选择后再开始任务", 5000 395 | ) 396 | return None 397 | 398 | if self.mode.currentIndex() == -1: 399 | logger.warning("未选择调度模式") 400 | MainInfoBar.push_info_bar( 401 | "warning", "未选择调度模式", "请选择后再开始任务", 5000 402 | ) 403 | return None 404 | 405 | if self.object.currentData() in Config.running_list: 406 | logger.warning(f"任务已存在:{self.object.currentData()}") 407 | MainInfoBar.push_info_bar( 408 | "warning", "任务已存在", self.object.currentData(), 5000 409 | ) 410 | return None 411 | 412 | if "调度队列" in self.object.currentData(): 413 | 414 | logger.info(f"用户添加任务:{self.object.currentData()}") 415 | TaskManager.add_task( 416 | f"{self.mode.currentText()}_主调度台", 417 | self.object.currentData(), 418 | Config.queue_dict[self.object.currentData()]["Config"].toDict(), 419 | ) 420 | 421 | elif "脚本" in self.object.currentData(): 422 | 423 | if Config.member_dict[self.object.currentData()]["Type"] == "Maa": 424 | 425 | logger.info(f"用户添加任务:{self.object.currentData()}") 426 | TaskManager.add_task( 427 | f"{self.mode.currentText()}_主调度台", 428 | "自定义队列", 429 | {"Queue": {"Member_1": self.object.currentData()}}, 430 | ) 431 | 432 | class DispatchInfoCard(HeaderCardWidget): 433 | 434 | def __init__(self, parent=None): 435 | super().__init__(parent) 436 | 437 | self.setTitle("调度信息") 438 | 439 | self.task = self.TaskInfoCard(self) 440 | self.user = self.UserInfoCard(self) 441 | self.log_text = self.LogCard(self) 442 | 443 | self.viewLayout.addWidget(self.task) 444 | self.viewLayout.addWidget(self.user) 445 | self.viewLayout.addWidget(self.log_text) 446 | 447 | self.viewLayout.setStretch(0, 1) 448 | self.viewLayout.setStretch(1, 1) 449 | self.viewLayout.setStretch(2, 5) 450 | 451 | def update_board(self, task_list: list, user_list: list, log: str): 452 | """更新调度信息""" 453 | 454 | self.task.update_task(task_list) 455 | self.user.update_user(user_list) 456 | self.log_text.text.setText(log) 457 | 458 | class TaskInfoCard(HeaderCardWidget): 459 | 460 | def __init__(self, parent=None): 461 | super().__init__(parent) 462 | self.setTitle("任务队列") 463 | 464 | self.Layout = QVBoxLayout() 465 | self.viewLayout.addLayout(self.Layout) 466 | self.viewLayout.setContentsMargins(3, 0, 3, 3) 467 | 468 | self.task_cards: List[StatefulItemCard] = [] 469 | 470 | def create_task(self, task_list: list): 471 | """创建任务队列""" 472 | 473 | while self.Layout.count() > 0: 474 | item = self.Layout.takeAt(0) 475 | if item.spacerItem(): 476 | self.Layout.removeItem(item.spacerItem()) 477 | elif item.widget(): 478 | item.widget().deleteLater() 479 | 480 | self.task_cards = [] 481 | 482 | for task in task_list: 483 | 484 | self.task_cards.append(StatefulItemCard(task)) 485 | self.Layout.addWidget(self.task_cards[-1]) 486 | 487 | self.Layout.addStretch(1) 488 | 489 | def update_task(self, task_list: list): 490 | """更新任务队列""" 491 | 492 | for i in range(len(task_list)): 493 | 494 | self.task_cards[i].update_status(task_list[i][1]) 495 | 496 | class UserInfoCard(HeaderCardWidget): 497 | 498 | def __init__(self, parent=None): 499 | super().__init__(parent) 500 | self.setTitle("用户队列") 501 | 502 | self.Layout = QVBoxLayout() 503 | self.viewLayout.addLayout(self.Layout) 504 | self.viewLayout.setContentsMargins(3, 0, 3, 3) 505 | 506 | self.user_cards: List[StatefulItemCard] = [] 507 | 508 | def create_user(self, user_list: list): 509 | """创建用户队列""" 510 | 511 | while self.Layout.count() > 0: 512 | item = self.Layout.takeAt(0) 513 | if item.spacerItem(): 514 | self.Layout.removeItem(item.spacerItem()) 515 | elif item.widget(): 516 | item.widget().deleteLater() 517 | 518 | self.user_cards = [] 519 | 520 | for user in user_list: 521 | 522 | self.user_cards.append(StatefulItemCard(user)) 523 | self.Layout.addWidget(self.user_cards[-1]) 524 | 525 | self.Layout.addStretch(1) 526 | 527 | def update_user(self, user_list: list): 528 | """更新用户队列""" 529 | 530 | for i in range(len(user_list)): 531 | 532 | self.user_cards[i].Label.setText(user_list[i][0]) 533 | self.user_cards[i].update_status(user_list[i][1]) 534 | 535 | class LogCard(HeaderCardWidget): 536 | 537 | def __init__(self, parent=None): 538 | super().__init__(parent) 539 | self.setTitle("日志") 540 | 541 | self.text = TextBrowser() 542 | self.viewLayout.setContentsMargins(3, 0, 3, 3) 543 | self.viewLayout.addWidget(self.text) 544 | 545 | self.text.textChanged.connect(self.to_end) 546 | 547 | def to_end(self): 548 | """滚动到底部""" 549 | 550 | self.text.moveCursor(QTextCursor.End) 551 | self.text.ensureCursorVisible() 552 | -------------------------------------------------------------------------------- /app/ui/downloader.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA更新器 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | import zipfile 29 | import requests 30 | import subprocess 31 | import time 32 | import psutil 33 | from functools import partial 34 | from pathlib import Path 35 | 36 | from PySide6.QtWidgets import QDialog, QVBoxLayout 37 | from qfluentwidgets import ( 38 | ProgressBar, 39 | IndeterminateProgressBar, 40 | BodyLabel, 41 | setTheme, 42 | Theme, 43 | ) 44 | from PySide6.QtGui import QCloseEvent 45 | from PySide6.QtCore import QThread, Signal, QTimer, QEventLoop 46 | 47 | from typing import List, Dict, Union 48 | 49 | 50 | def version_text(version_numb: list) -> str: 51 | """将版本号列表转为可读的文本信息""" 52 | 53 | while len(version_numb) < 4: 54 | version_numb.append(0) 55 | 56 | if version_numb[3] == 0: 57 | version = f"v{'.'.join(str(_) for _ in version_numb[0:3])}" 58 | else: 59 | version = ( 60 | f"v{'.'.join(str(_) for _ in version_numb[0:3])}-beta.{version_numb[3]}" 61 | ) 62 | return version 63 | 64 | 65 | class DownloadProcess(QThread): 66 | """分段下载子线程""" 67 | 68 | progress = Signal(int) 69 | accomplish = Signal(float) 70 | 71 | def __init__( 72 | self, 73 | url: str, 74 | start_byte: int, 75 | end_byte: int, 76 | download_path: Path, 77 | check_times: int = -1, 78 | ) -> None: 79 | super(DownloadProcess, self).__init__() 80 | 81 | self.setObjectName(f"DownloadProcess-{url}-{start_byte}-{end_byte}") 82 | 83 | self.url = url 84 | self.start_byte = start_byte 85 | self.end_byte = end_byte 86 | self.download_path = download_path 87 | self.check_times = check_times 88 | 89 | def run(self) -> None: 90 | 91 | # 清理可能存在的临时文件 92 | if self.download_path.exists(): 93 | self.download_path.unlink() 94 | 95 | headers = {"Range": f"bytes={self.start_byte}-{self.end_byte}"} 96 | 97 | while not self.isInterruptionRequested() and self.check_times != 0: 98 | 99 | try: 100 | 101 | start_time = time.time() 102 | 103 | response = requests.get( 104 | self.url, headers=headers, timeout=10, stream=True 105 | ) 106 | 107 | if response.status_code != 206: 108 | 109 | if self.check_times != -1: 110 | self.check_times -= 1 111 | 112 | time.sleep(1) 113 | continue 114 | 115 | downloaded_size = 0 116 | with self.download_path.open(mode="wb") as f: 117 | 118 | for chunk in response.iter_content(chunk_size=8192): 119 | 120 | if self.isInterruptionRequested(): 121 | break 122 | 123 | f.write(chunk) 124 | downloaded_size += len(chunk) 125 | 126 | self.progress.emit(downloaded_size) 127 | 128 | if self.isInterruptionRequested(): 129 | 130 | if self.download_path.exists(): 131 | self.download_path.unlink() 132 | self.accomplish.emit(0) 133 | 134 | else: 135 | 136 | self.accomplish.emit(time.time() - start_time) 137 | 138 | break 139 | 140 | except Exception as e: 141 | 142 | if self.check_times != -1: 143 | self.check_times -= 1 144 | time.sleep(1) 145 | 146 | else: 147 | 148 | if self.download_path.exists(): 149 | self.download_path.unlink() 150 | self.accomplish.emit(0) 151 | 152 | 153 | class ZipExtractProcess(QThread): 154 | """解压子线程""" 155 | 156 | info = Signal(str) 157 | accomplish = Signal() 158 | 159 | def __init__(self, name: str, app_path: Path, download_path: Path) -> None: 160 | super(ZipExtractProcess, self).__init__() 161 | 162 | self.setObjectName(f"ZipExtractProcess-{name}") 163 | 164 | self.name = name 165 | self.app_path = app_path 166 | self.download_path = download_path 167 | 168 | def run(self) -> None: 169 | 170 | try: 171 | 172 | while True: 173 | 174 | if self.isInterruptionRequested(): 175 | self.download_path.unlink() 176 | return None 177 | try: 178 | with zipfile.ZipFile(self.download_path, "r") as zip_ref: 179 | zip_ref.extractall(self.app_path) 180 | self.accomplish.emit() 181 | break 182 | except PermissionError: 183 | if self.name == "AUTO_MAA": 184 | self.info.emit(f"解压出错:AUTO_MAA正在运行,正在尝试将其关闭") 185 | self.kill_process(self.app_path / "AUTO_MAA.exe") 186 | else: 187 | self.info.emit(f"解压出错:{self.name}正在运行,正在等待其关闭") 188 | time.sleep(1) 189 | 190 | except Exception as e: 191 | 192 | e = str(e) 193 | e = "\n".join([e[_ : _ + 75] for _ in range(0, len(e), 75)]) 194 | self.info.emit(f"解压更新时出错:\n{e}") 195 | return None 196 | 197 | def kill_process(self, path: Path) -> None: 198 | """根据路径中止进程""" 199 | 200 | for pid in self.search_pids(path): 201 | killprocess = subprocess.Popen( 202 | f"taskkill /F /PID {pid}", 203 | shell=True, 204 | creationflags=subprocess.CREATE_NO_WINDOW, 205 | ) 206 | killprocess.wait() 207 | 208 | def search_pids(self, path: Path) -> list: 209 | """根据路径查找进程PID""" 210 | 211 | pids = [] 212 | for proc in psutil.process_iter(["pid", "exe"]): 213 | try: 214 | if proc.info["exe"] and proc.info["exe"].lower() == str(path).lower(): 215 | pids.append(proc.info["pid"]) 216 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 217 | # 进程可能在此期间已结束或无法访问,忽略这些异常 218 | pass 219 | return pids 220 | 221 | 222 | class DownloadManager(QDialog): 223 | """下载管理器""" 224 | 225 | speed_test_accomplish = Signal() 226 | download_accomplish = Signal() 227 | download_process_clear = Signal() 228 | 229 | isInterruptionRequested = False 230 | 231 | def __init__(self, app_path: Path, name: str, version: list, config: dict) -> None: 232 | super().__init__() 233 | 234 | self.app_path = app_path 235 | self.name = name 236 | self.version = version 237 | self.config = config 238 | self.download_path = app_path / "DOWNLOAD_TEMP.zip" # 临时下载文件的路径 239 | self.download_process_dict: Dict[str, DownloadProcess] = {} 240 | self.timer_dict: Dict[str, QTimer] = {} 241 | 242 | self.resize(700, 70) 243 | 244 | setTheme(Theme.AUTO, lazy=True) 245 | 246 | # 创建垂直布局 247 | self.Layout = QVBoxLayout(self) 248 | 249 | self.info = BodyLabel("正在初始化", self) 250 | self.progress_1 = IndeterminateProgressBar(self) 251 | self.progress_2 = ProgressBar(self) 252 | 253 | self.update_progress(0, 0, 0) 254 | 255 | self.Layout.addWidget(self.info) 256 | self.Layout.addStretch(1) 257 | self.Layout.addWidget(self.progress_1) 258 | self.Layout.addWidget(self.progress_2) 259 | self.Layout.addStretch(1) 260 | 261 | def run(self) -> None: 262 | 263 | if self.name == "AUTO_MAA": 264 | if self.config["mode"] == "Proxy": 265 | self.test_speed_task1() 266 | self.speed_test_accomplish.connect(self.download_task1) 267 | elif self.config["mode"] == "MirrorChyan": 268 | self.download_task1() 269 | elif self.config["mode"] == "MirrorChyan": 270 | self.download_task1() 271 | 272 | def get_download_url(self, mode: str) -> Union[str, Dict[str, str]]: 273 | """获取下载链接""" 274 | 275 | url_dict = {} 276 | 277 | if mode == "测速": 278 | 279 | url_dict["GitHub站"] = ( 280 | f"https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip" 281 | ) 282 | url_dict["官方镜像站"] = ( 283 | f"https://gitee.com/DLmaster_361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip" 284 | ) 285 | for name, download_url_head in self.config["download_dict"].items(): 286 | url_dict[name] = ( 287 | f"{download_url_head}AUTO_MAA_{version_text(self.version)}.zip" 288 | ) 289 | for proxy_url in self.config["proxy_list"]: 290 | url_dict[proxy_url] = ( 291 | f"{proxy_url}https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip" 292 | ) 293 | return url_dict 294 | 295 | elif mode == "下载": 296 | 297 | if self.name == "AUTO_MAA": 298 | 299 | if self.config["mode"] == "Proxy": 300 | 301 | if "selected" in self.config: 302 | selected_url = self.config["selected"] 303 | elif "speed_result" in self.config: 304 | selected_url = max( 305 | self.config["speed_result"], 306 | key=self.config["speed_result"].get, 307 | ) 308 | 309 | if selected_url == "GitHub站": 310 | return f"https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip" 311 | elif selected_url == "官方镜像站": 312 | return f"https://gitee.com/DLmaster_361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip" 313 | elif selected_url in self.config["download_dict"].keys(): 314 | return f"{self.config["download_dict"][selected_url]}AUTO_MAA_{version_text(self.version)}.zip" 315 | else: 316 | return f"{selected_url}https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip" 317 | 318 | elif self.config["mode"] == "MirrorChyan": 319 | 320 | with requests.get( 321 | self.config["url"], 322 | allow_redirects=True, 323 | timeout=10, 324 | stream=True, 325 | ) as response: 326 | if response.status_code == 200: 327 | return response.url 328 | 329 | elif self.config["mode"] == "MirrorChyan": 330 | 331 | with requests.get( 332 | self.config["url"], allow_redirects=True, timeout=10, stream=True 333 | ) as response: 334 | if response.status_code == 200: 335 | return response.url 336 | 337 | def test_speed_task1(self) -> None: 338 | 339 | if self.isInterruptionRequested: 340 | return None 341 | 342 | url_dict = self.get_download_url("测速") 343 | self.test_speed_result: Dict[str, float] = {} 344 | 345 | for name, url in url_dict.items(): 346 | 347 | if self.isInterruptionRequested: 348 | break 349 | 350 | # 创建测速线程,下载4MB文件以测试下载速度 351 | self.download_process_dict[name] = DownloadProcess( 352 | url, 353 | 0, 354 | 4194304, 355 | self.app_path / f"{name.replace('/','').replace(':','')}.zip", 356 | 10, 357 | ) 358 | self.test_speed_result[name] = -1 359 | self.download_process_dict[name].accomplish.connect( 360 | partial(self.test_speed_task2, name) 361 | ) 362 | 363 | self.download_process_dict[name].start() 364 | timer = QTimer(self) 365 | timer.setSingleShot(True) 366 | timer.timeout.connect(partial(self.kill_speed_test, name)) 367 | timer.start(30000) 368 | self.timer_dict[name] = timer 369 | 370 | self.update_info("正在测速,预计用时30秒") 371 | self.update_progress(0, 1, 0) 372 | 373 | def kill_speed_test(self, name: str) -> None: 374 | 375 | if name in self.download_process_dict: 376 | self.download_process_dict[name].requestInterruption() 377 | 378 | def test_speed_task2(self, name: str, t: float) -> None: 379 | 380 | # 计算下载速度 381 | if self.isInterruptionRequested: 382 | self.update_info(f"已中止测速进程:{name}") 383 | self.test_speed_result[name] = 0 384 | elif t != 0: 385 | self.update_info(f"{name}:{ 4 / t:.2f} MB/s") 386 | self.test_speed_result[name] = 4 / t 387 | else: 388 | self.update_info(f"{name}:{ 0:.2f} MB/s") 389 | self.test_speed_result[name] = 0 390 | self.update_progress( 391 | 0, 392 | len(self.test_speed_result), 393 | sum(1 for speed in self.test_speed_result.values() if speed != -1), 394 | ) 395 | 396 | # 删除临时文件 397 | if (self.app_path / f"{name.replace('/','').replace(':','')}.zip").exists(): 398 | (self.app_path / f"{name.replace('/','').replace(':','')}.zip").unlink() 399 | 400 | # 清理下载线程 401 | self.timer_dict[name].stop() 402 | self.timer_dict[name].deleteLater() 403 | self.timer_dict.pop(name) 404 | self.download_process_dict[name].requestInterruption() 405 | self.download_process_dict[name].quit() 406 | self.download_process_dict[name].wait() 407 | self.download_process_dict[name].deleteLater() 408 | self.download_process_dict.pop(name) 409 | if not self.download_process_dict: 410 | self.download_process_clear.emit() 411 | 412 | if any(speed == -1 for _, speed in self.test_speed_result.items()): 413 | return None 414 | 415 | # 保存测速结果 416 | self.config["speed_result"] = self.test_speed_result 417 | 418 | self.update_info("测速完成!") 419 | self.speed_test_accomplish.emit() 420 | 421 | def download_task1(self) -> None: 422 | 423 | if self.isInterruptionRequested: 424 | return None 425 | 426 | url = self.get_download_url("下载") 427 | self.downloaded_size_list: List[List[int, bool]] = [] 428 | 429 | response = requests.head(url, timeout=10) 430 | 431 | self.file_size = int(response.headers.get("content-length", 0)) 432 | part_size = self.file_size // self.config["thread_numb"] 433 | self.downloaded_size = 0 434 | self.last_download_size = 0 435 | self.last_time = time.time() 436 | self.speed = 0 437 | 438 | # 拆分下载任务,启用多线程下载 439 | for i in range(self.config["thread_numb"]): 440 | 441 | if self.isInterruptionRequested: 442 | break 443 | 444 | # 计算单任务下载范围 445 | start_byte = i * part_size 446 | end_byte = ( 447 | (i + 1) * part_size - 1 448 | if (i != self.config["thread_numb"] - 1) 449 | else self.file_size - 1 450 | ) 451 | 452 | # 创建下载子线程 453 | self.download_process_dict[f"part{i}"] = DownloadProcess( 454 | url, 455 | start_byte, 456 | end_byte, 457 | self.download_path.with_suffix(f".part{i}"), 458 | 1 if self.config["mode"] == "MirrorChyan" else -1, 459 | ) 460 | self.downloaded_size_list.append([0, False]) 461 | self.download_process_dict[f"part{i}"].progress.connect( 462 | partial(self.download_task2, i) 463 | ) 464 | self.download_process_dict[f"part{i}"].accomplish.connect( 465 | partial(self.download_task3, i) 466 | ) 467 | self.download_process_dict[f"part{i}"].start() 468 | 469 | def download_task2(self, index: str, current: int) -> None: 470 | """更新下载进度""" 471 | 472 | self.downloaded_size_list[index][0] = current 473 | self.downloaded_size = sum([_[0] for _ in self.downloaded_size_list]) 474 | self.update_progress(0, self.file_size, self.downloaded_size) 475 | 476 | if time.time() - self.last_time >= 1.0: 477 | self.speed = ( 478 | (self.downloaded_size - self.last_download_size) 479 | / (time.time() - self.last_time) 480 | / 1024 481 | ) 482 | self.last_download_size = self.downloaded_size 483 | self.last_time = time.time() 484 | 485 | if self.speed >= 1024: 486 | self.update_info( 487 | f"正在下载:{self.name} 已下载:{self.downloaded_size / 1048576:.2f}/{self.file_size / 1048576:.2f} MB ({self.downloaded_size / self.file_size * 100:.2f}%) 下载速度:{self.speed / 1024:.2f} MB/s", 488 | ) 489 | else: 490 | self.update_info( 491 | f"正在下载:{self.name} 已下载:{self.downloaded_size / 1048576:.2f}/{self.file_size / 1048576:.2f} MB ({self.downloaded_size / self.file_size * 100:.2f}%) 下载速度:{self.speed:.2f} KB/s", 492 | ) 493 | 494 | def download_task3(self, index: str, t: float) -> None: 495 | 496 | # 标记下载线程完成 497 | self.downloaded_size_list[index][1] = True 498 | 499 | # 清理下载线程 500 | self.download_process_dict[f"part{index}"].requestInterruption() 501 | self.download_process_dict[f"part{index}"].quit() 502 | self.download_process_dict[f"part{index}"].wait() 503 | self.download_process_dict[f"part{index}"].deleteLater() 504 | self.download_process_dict.pop(f"part{index}") 505 | if not self.download_process_dict: 506 | self.download_process_clear.emit() 507 | 508 | if ( 509 | any([not _[1] for _ in self.downloaded_size_list]) 510 | or self.isInterruptionRequested 511 | ): 512 | return None 513 | 514 | # 合并下载的分段文件 515 | with self.download_path.open(mode="wb") as outfile: 516 | for i in range(self.config["thread_numb"]): 517 | with self.download_path.with_suffix(f".part{i}").open( 518 | mode="rb" 519 | ) as infile: 520 | outfile.write(infile.read()) 521 | self.download_path.with_suffix(f".part{i}").unlink() 522 | 523 | self.update_info("正在解压更新文件") 524 | self.update_progress(0, 0, 0) 525 | 526 | # 创建解压线程 527 | self.zip_extract = ZipExtractProcess( 528 | self.name, self.app_path, self.download_path 529 | ) 530 | self.zip_loop = QEventLoop() 531 | self.zip_extract.info.connect(self.update_info) 532 | self.zip_extract.accomplish.connect(self.zip_loop.quit) 533 | self.zip_extract.start() 534 | self.zip_loop.exec() 535 | 536 | self.update_info("正在删除临时文件") 537 | self.update_progress(0, 0, 0) 538 | if (self.app_path / "changes.json").exists(): 539 | (self.app_path / "changes.json").unlink() 540 | if self.download_path.exists(): 541 | self.download_path.unlink() 542 | 543 | # 下载完成后打开对应程序 544 | if not self.isInterruptionRequested and self.name == "MAA": 545 | subprocess.Popen( 546 | [self.app_path / "MAA.exe"], 547 | creationflags=subprocess.CREATE_NEW_PROCESS_GROUP 548 | | subprocess.DETACHED_PROCESS 549 | | subprocess.CREATE_NO_WINDOW, 550 | ) 551 | if self.name == "AUTO_MAA": 552 | self.update_info(f"即将安装{self.name}") 553 | else: 554 | self.update_info(f"{self.name}下载成功!") 555 | self.update_progress(0, 100, 100) 556 | self.download_accomplish.emit() 557 | 558 | def update_info(self, text: str) -> None: 559 | self.info.setText(text) 560 | 561 | def update_progress(self, begin: int, end: int, current: int) -> None: 562 | 563 | if begin == 0 and end == 0: 564 | self.progress_2.setVisible(False) 565 | self.progress_1.setVisible(True) 566 | else: 567 | self.progress_1.setVisible(False) 568 | self.progress_2.setVisible(True) 569 | self.progress_2.setRange(begin, end) 570 | self.progress_2.setValue(current) 571 | 572 | def requestInterruption(self) -> None: 573 | 574 | self.isInterruptionRequested = True 575 | 576 | if hasattr(self, "zip_extract") and self.zip_extract: 577 | self.zip_extract.requestInterruption() 578 | 579 | if hasattr(self, "zip_loop") and self.zip_loop: 580 | self.zip_loop.quit() 581 | 582 | for process in self.download_process_dict.values(): 583 | process.requestInterruption() 584 | 585 | if self.download_process_dict: 586 | loop = QEventLoop() 587 | self.download_process_clear.connect(loop.quit) 588 | loop.exec() 589 | 590 | def closeEvent(self, event: QCloseEvent): 591 | """清理残余进程""" 592 | 593 | self.requestInterruption() 594 | 595 | event.accept() 596 | -------------------------------------------------------------------------------- /app/ui/history.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA历史记录界面 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | from loguru import logger 29 | from PySide6.QtWidgets import ( 30 | QWidget, 31 | QVBoxLayout, 32 | QHBoxLayout, 33 | ) 34 | from qfluentwidgets import ( 35 | ScrollArea, 36 | FluentIcon, 37 | HeaderCardWidget, 38 | PushButton, 39 | TextBrowser, 40 | CardWidget, 41 | ComboBox, 42 | ZhDatePicker, 43 | SubtitleLabel, 44 | ) 45 | from PySide6.QtCore import Signal, QDate 46 | import os 47 | import subprocess 48 | from datetime import datetime, timedelta 49 | from functools import partial 50 | from pathlib import Path 51 | from typing import Union, List, Dict 52 | 53 | 54 | from app.core import Config, SoundPlayer 55 | from .Widget import StatefulItemCard, QuantifiedItemCard, QuickExpandGroupCard 56 | 57 | 58 | class History(QWidget): 59 | 60 | def __init__(self, parent=None): 61 | super().__init__(parent) 62 | self.setObjectName("历史记录") 63 | 64 | self.history_top_bar = self.HistoryTopBar(self) 65 | self.history_top_bar.search_history.connect(self.reload_history) 66 | 67 | content_widget = QWidget() 68 | self.content_layout = QVBoxLayout(content_widget) 69 | self.content_layout.setContentsMargins(0, 0, 11, 0) 70 | 71 | scrollArea = ScrollArea() 72 | scrollArea.setWidgetResizable(True) 73 | scrollArea.setContentsMargins(0, 0, 0, 0) 74 | scrollArea.setStyleSheet("background: transparent; border: none;") 75 | scrollArea.setWidget(content_widget) 76 | 77 | layout = QVBoxLayout(self) 78 | layout.addWidget(self.history_top_bar) 79 | layout.addWidget(scrollArea) 80 | 81 | self.history_card_list = [] 82 | 83 | def reload_history(self, mode: str, start_date: QDate, end_date: QDate) -> None: 84 | """加载历史记录界面""" 85 | 86 | SoundPlayer.play("历史记录查询") 87 | 88 | while self.content_layout.count() > 0: 89 | item = self.content_layout.takeAt(0) 90 | if item.spacerItem(): 91 | self.content_layout.removeItem(item.spacerItem()) 92 | elif item.widget(): 93 | item.widget().deleteLater() 94 | 95 | self.history_card_list = [] 96 | 97 | history_dict = Config.search_history( 98 | mode, 99 | datetime(start_date.year(), start_date.month(), start_date.day()), 100 | datetime(end_date.year(), end_date.month(), end_date.day()), 101 | ) 102 | 103 | for date, user in history_dict.items(): 104 | 105 | self.history_card_list.append(self.HistoryCard(mode, date, user, self)) 106 | self.content_layout.addWidget(self.history_card_list[-1]) 107 | 108 | self.content_layout.addStretch(1) 109 | 110 | class HistoryTopBar(CardWidget): 111 | """历史记录顶部工具栏""" 112 | 113 | search_history = Signal(str, QDate, QDate) 114 | 115 | def __init__(self, parent=None): 116 | super().__init__(parent) 117 | 118 | Layout = QHBoxLayout(self) 119 | 120 | self.lable_1 = SubtitleLabel("查询范围:") 121 | self.start_date = ZhDatePicker() 122 | self.start_date.setDate(QDate(2019, 5, 1)) 123 | self.lable_2 = SubtitleLabel("→") 124 | self.end_date = ZhDatePicker() 125 | server_date = Config.server_date() 126 | self.end_date.setDate( 127 | QDate(server_date.year, server_date.month, server_date.day) 128 | ) 129 | self.mode = ComboBox() 130 | self.mode.setPlaceholderText("请选择查询模式") 131 | self.mode.addItems(["按日合并", "按周合并", "按月合并"]) 132 | 133 | self.select_month = PushButton(FluentIcon.TAG, "最近一月") 134 | self.select_week = PushButton(FluentIcon.TAG, "最近一周") 135 | self.search = PushButton(FluentIcon.SEARCH, "查询") 136 | self.select_month.clicked.connect(lambda: self.select_date("month")) 137 | self.select_week.clicked.connect(lambda: self.select_date("week")) 138 | self.search.clicked.connect( 139 | lambda: self.search_history.emit( 140 | self.mode.currentText(), 141 | self.start_date.getDate(), 142 | self.end_date.getDate(), 143 | ) 144 | ) 145 | 146 | Layout.addWidget(self.lable_1) 147 | Layout.addWidget(self.start_date) 148 | Layout.addWidget(self.lable_2) 149 | Layout.addWidget(self.end_date) 150 | Layout.addWidget(self.mode) 151 | Layout.addStretch(1) 152 | Layout.addWidget(self.select_month) 153 | Layout.addWidget(self.select_week) 154 | Layout.addWidget(self.search) 155 | 156 | def select_date(self, date: str) -> None: 157 | """选中最近一段时间并启动查询""" 158 | 159 | server_date = Config.server_date() 160 | if date == "week": 161 | begin_date = server_date - timedelta(weeks=1) 162 | elif date == "month": 163 | begin_date = server_date - timedelta(days=30) 164 | 165 | self.start_date.setDate( 166 | QDate(begin_date.year, begin_date.month, begin_date.day) 167 | ) 168 | self.end_date.setDate( 169 | QDate(server_date.year, server_date.month, server_date.day) 170 | ) 171 | 172 | self.search.clicked.emit() 173 | 174 | class HistoryCard(QuickExpandGroupCard): 175 | 176 | def __init__( 177 | self, 178 | mode: str, 179 | date: str, 180 | user: Union[List[Path], Dict[str, List[Path]]], 181 | parent=None, 182 | ): 183 | super().__init__( 184 | FluentIcon.HISTORY, date, f"{date}的历史运行记录与统计信息", parent 185 | ) 186 | 187 | widget = QWidget() 188 | Layout = QVBoxLayout(widget) 189 | self.viewLayout.setContentsMargins(0, 0, 0, 0) 190 | self.viewLayout.setSpacing(0) 191 | self.addGroupWidget(widget) 192 | 193 | self.user_history_card_list = [] 194 | 195 | if mode == "按日合并": 196 | 197 | for user_path in user: 198 | self.user_history_card_list.append( 199 | self.UserHistoryCard(mode, user_path.stem, user_path, self) 200 | ) 201 | Layout.addWidget(self.user_history_card_list[-1]) 202 | 203 | elif mode in ["按周合并", "按月合并"]: 204 | 205 | for user, info in user.items(): 206 | self.user_history_card_list.append( 207 | self.UserHistoryCard(mode, user, info, self) 208 | ) 209 | Layout.addWidget(self.user_history_card_list[-1]) 210 | 211 | class UserHistoryCard(HeaderCardWidget): 212 | """用户历史记录卡片""" 213 | 214 | def __init__( 215 | self, 216 | mode: str, 217 | name: str, 218 | user_history: Union[Path, List[Path]], 219 | parent=None, 220 | ): 221 | super().__init__(parent) 222 | 223 | self.setTitle(name) 224 | 225 | if mode == "按日合并": 226 | 227 | self.user_history_path = user_history 228 | self.main_history = Config.load_maa_logs("总览", user_history) 229 | 230 | self.index_card = self.IndexCard( 231 | self.main_history["条目索引"], self 232 | ) 233 | self.index_card.index_changed.connect(self.update_info) 234 | self.viewLayout.addWidget(self.index_card) 235 | 236 | elif mode in ["按周合并", "按月合并"]: 237 | 238 | history = Config.merge_maa_logs("指定项", user_history) 239 | 240 | self.main_history = {} 241 | self.main_history["统计数据"] = { 242 | "公招统计": list(history["recruit_statistics"].items()) 243 | } 244 | 245 | for game_id, drops in history["drop_statistics"].items(): 246 | self.main_history["统计数据"][f"掉落统计:{game_id}"] = list( 247 | drops.items() 248 | ) 249 | 250 | self.statistics_card = QHBoxLayout() 251 | self.log_card = self.LogCard(self) 252 | 253 | self.viewLayout.addLayout(self.statistics_card) 254 | self.viewLayout.addWidget(self.log_card) 255 | self.viewLayout.setContentsMargins(0, 0, 0, 0) 256 | self.viewLayout.setSpacing(0) 257 | self.viewLayout.setStretch(0, 1) 258 | self.viewLayout.setStretch(2, 4) 259 | 260 | self.update_info("数据总览") 261 | 262 | def update_info(self, index: str) -> None: 263 | """更新信息""" 264 | 265 | if index == "数据总览": 266 | 267 | while self.statistics_card.count() > 0: 268 | item = self.statistics_card.takeAt(0) 269 | if item.spacerItem(): 270 | self.statistics_card.removeItem(item.spacerItem()) 271 | elif item.widget(): 272 | item.widget().deleteLater() 273 | 274 | for name, item_list in self.main_history["统计数据"].items(): 275 | 276 | statistics_card = self.StatisticsCard(name, item_list, self) 277 | self.statistics_card.addWidget(statistics_card) 278 | 279 | self.log_card.hide() 280 | 281 | else: 282 | 283 | single_history = Config.load_maa_logs( 284 | "单项", 285 | self.user_history_path.with_suffix("") 286 | / f"{index.replace(":","-")}.json", 287 | ) 288 | 289 | while self.statistics_card.count() > 0: 290 | item = self.statistics_card.takeAt(0) 291 | if item.spacerItem(): 292 | self.statistics_card.removeItem(item.spacerItem()) 293 | elif item.widget(): 294 | item.widget().deleteLater() 295 | 296 | for name, item_list in single_history["统计数据"].items(): 297 | 298 | statistics_card = self.StatisticsCard(name, item_list, self) 299 | self.statistics_card.addWidget(statistics_card) 300 | 301 | self.log_card.text.setText(single_history["日志信息"]) 302 | self.log_card.open_file.clicked.disconnect() 303 | self.log_card.open_file.clicked.connect( 304 | lambda: os.startfile( 305 | self.user_history_path.with_suffix("") 306 | / f"{index.replace(":","-")}.log" 307 | ) 308 | ) 309 | self.log_card.open_dir.clicked.disconnect() 310 | self.log_card.open_dir.clicked.connect( 311 | lambda: subprocess.Popen( 312 | [ 313 | "explorer", 314 | "/select,", 315 | str( 316 | self.user_history_path.with_suffix("") 317 | / f"{index.replace(":","-")}.log" 318 | ), 319 | ] 320 | ) 321 | ) 322 | self.log_card.show() 323 | 324 | self.viewLayout.setStretch(1, self.statistics_card.count()) 325 | 326 | self.setMinimumHeight(300) 327 | 328 | class IndexCard(HeaderCardWidget): 329 | 330 | index_changed = Signal(str) 331 | 332 | def __init__(self, index_list: list, parent=None): 333 | super().__init__(parent) 334 | self.setTitle("记录条目") 335 | 336 | self.Layout = QVBoxLayout() 337 | self.viewLayout.addLayout(self.Layout) 338 | self.viewLayout.setContentsMargins(3, 0, 3, 3) 339 | 340 | self.index_cards: List[StatefulItemCard] = [] 341 | 342 | for index in index_list: 343 | 344 | self.index_cards.append(StatefulItemCard(index)) 345 | self.index_cards[-1].clicked.connect( 346 | partial(self.index_changed.emit, index[0]) 347 | ) 348 | self.Layout.addWidget(self.index_cards[-1]) 349 | 350 | self.Layout.addStretch(1) 351 | 352 | class StatisticsCard(HeaderCardWidget): 353 | 354 | def __init__(self, name: str, item_list: list, parent=None): 355 | super().__init__(parent) 356 | self.setTitle(name) 357 | 358 | self.Layout = QVBoxLayout() 359 | self.viewLayout.addLayout(self.Layout) 360 | self.viewLayout.setContentsMargins(3, 0, 3, 3) 361 | 362 | self.item_cards: List[QuantifiedItemCard] = [] 363 | 364 | for item in item_list: 365 | 366 | self.item_cards.append(QuantifiedItemCard(item)) 367 | self.Layout.addWidget(self.item_cards[-1]) 368 | 369 | if len(item_list) == 0: 370 | self.Layout.addWidget(QuantifiedItemCard(["暂无记录", ""])) 371 | 372 | self.Layout.addStretch(1) 373 | 374 | class LogCard(HeaderCardWidget): 375 | 376 | def __init__(self, parent=None): 377 | super().__init__(parent) 378 | self.setTitle("日志") 379 | 380 | self.text = TextBrowser(self) 381 | self.open_file = PushButton("打开日志文件", self) 382 | self.open_file.clicked.connect(lambda: print("打开日志文件")) 383 | self.open_dir = PushButton("打开所在目录", self) 384 | self.open_dir.clicked.connect(lambda: print("打开所在文件")) 385 | 386 | Layout = QVBoxLayout() 387 | h_layout = QHBoxLayout() 388 | h_layout.addWidget(self.open_file) 389 | h_layout.addWidget(self.open_dir) 390 | Layout.addWidget(self.text) 391 | Layout.addLayout(h_layout) 392 | self.viewLayout.setContentsMargins(3, 0, 3, 3) 393 | self.viewLayout.addLayout(Layout) 394 | -------------------------------------------------------------------------------- /app/ui/home.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA主界面 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | from loguru import logger 29 | from PySide6.QtWidgets import ( 30 | QWidget, 31 | QVBoxLayout, 32 | QHBoxLayout, 33 | QSpacerItem, 34 | QSizePolicy, 35 | QFileDialog, 36 | ) 37 | from PySide6.QtCore import Qt, QSize, QUrl 38 | from PySide6.QtGui import QDesktopServices, QColor 39 | from qfluentwidgets import ( 40 | FluentIcon, 41 | ScrollArea, 42 | SimpleCardWidget, 43 | PrimaryToolButton, 44 | TextBrowser, 45 | ) 46 | import re 47 | import shutil 48 | import json 49 | from datetime import datetime 50 | from pathlib import Path 51 | 52 | from app.core import Config, MainInfoBar, Network 53 | from .Widget import Banner, IconButton 54 | 55 | 56 | class Home(QWidget): 57 | 58 | def __init__(self, parent=None): 59 | super().__init__(parent) 60 | self.setObjectName("主页") 61 | 62 | self.banner = Banner() 63 | self.banner_text = TextBrowser() 64 | 65 | v_layout = QVBoxLayout(self.banner) 66 | v_layout.setContentsMargins(0, 0, 0, 15) 67 | v_layout.setSpacing(5) 68 | v_layout.setAlignment(Qt.AlignmentFlag.AlignTop) 69 | 70 | # 空白占位符 71 | v_layout.addItem( 72 | QSpacerItem(10, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) 73 | ) 74 | 75 | # 顶部部分 (按钮组) 76 | h1_layout = QHBoxLayout() 77 | h1_layout.setAlignment(Qt.AlignmentFlag.AlignTop) 78 | 79 | # 左边留白区域 80 | h1_layout.addStretch() 81 | 82 | # 按钮组 83 | buttonGroup = ButtonGroup() 84 | buttonGroup.setMaximumHeight(320) 85 | h1_layout.addWidget(buttonGroup) 86 | 87 | # 空白占位符 88 | h1_layout.addItem( 89 | QSpacerItem(20, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) 90 | ) 91 | 92 | # 将顶部水平布局添加到垂直布局 93 | v_layout.addLayout(h1_layout) 94 | 95 | # 中间留白区域 96 | v_layout.addItem( 97 | QSpacerItem(10, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) 98 | ) 99 | v_layout.addStretch() 100 | 101 | # 中间留白区域 102 | v_layout.addItem( 103 | QSpacerItem(10, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) 104 | ) 105 | v_layout.addStretch() 106 | 107 | # 底部部分 (图片切换按钮) 108 | h2_layout = QHBoxLayout() 109 | h2_layout.setAlignment(Qt.AlignmentFlag.AlignTop) 110 | 111 | # 左边留白区域 112 | h2_layout.addItem( 113 | QSpacerItem(20, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) 114 | ) 115 | 116 | # # 公告卡片 117 | # noticeCard = NoticeCard() 118 | # h2_layout.addWidget(noticeCard) 119 | 120 | h2_layout.addStretch() 121 | 122 | # 自定义图像按钮布局 123 | self.imageButton = PrimaryToolButton(FluentIcon.IMAGE_EXPORT) 124 | self.imageButton.setFixedSize(56, 56) 125 | self.imageButton.setIconSize(QSize(32, 32)) 126 | self.imageButton.clicked.connect(self.get_home_image) 127 | 128 | v1_layout = QVBoxLayout() 129 | v1_layout.addWidget(self.imageButton, alignment=Qt.AlignmentFlag.AlignBottom) 130 | 131 | h2_layout.addLayout(v1_layout) 132 | 133 | # 空白占位符 134 | h2_layout.addItem( 135 | QSpacerItem(25, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) 136 | ) 137 | 138 | # 将底部水平布局添加到垂直布局 139 | v_layout.addLayout(h2_layout) 140 | 141 | content_widget = QWidget() 142 | content_layout = QVBoxLayout(content_widget) 143 | content_layout.setContentsMargins(0, 0, 0, 0) 144 | content_layout.addWidget(self.banner) 145 | content_layout.addWidget(self.banner_text) 146 | content_layout.setStretch(0, 2) 147 | content_layout.setStretch(1, 3) 148 | 149 | scrollArea = ScrollArea() 150 | scrollArea.setWidgetResizable(True) 151 | scrollArea.setContentsMargins(0, 0, 0, 0) 152 | scrollArea.setStyleSheet("background: transparent; border: none;") 153 | scrollArea.setWidget(content_widget) 154 | 155 | layout = QVBoxLayout(self) 156 | layout.addWidget(scrollArea) 157 | 158 | self.set_banner() 159 | 160 | def get_home_image(self) -> None: 161 | """获取主页图片""" 162 | 163 | if Config.get(Config.function_HomeImageMode) == "默认": 164 | pass 165 | elif Config.get(Config.function_HomeImageMode) == "自定义": 166 | 167 | file_path, _ = QFileDialog.getOpenFileName( 168 | self, "打开自定义主页图片", "", "图片文件 (*.png *.jpg *.bmp)" 169 | ) 170 | if file_path: 171 | 172 | for file in Config.app_path.glob( 173 | "resources/images/Home/BannerCustomize.*" 174 | ): 175 | file.unlink() 176 | 177 | shutil.copy( 178 | file_path, 179 | Config.app_path 180 | / f"resources/images/Home/BannerCustomize{Path(file_path).suffix}", 181 | ) 182 | 183 | logger.info(f"自定义主页图片更换成功:{file_path}") 184 | MainInfoBar.push_info_bar( 185 | "success", 186 | "主页图片更换成功", 187 | "自定义主页图片更换成功!", 188 | 3000, 189 | ) 190 | 191 | else: 192 | logger.warning("自定义主页图片更换失败:未选择图片文件") 193 | MainInfoBar.push_info_bar( 194 | "warning", 195 | "主页图片更换失败", 196 | "未选择图片文件!", 197 | 5000, 198 | ) 199 | elif Config.get(Config.function_HomeImageMode) == "主题图像": 200 | 201 | # 从远程服务器获取最新主题图像 202 | network = Network.add_task( 203 | mode="get", 204 | url="https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/theme_image.json", 205 | ) 206 | network.loop.exec() 207 | network_result = Network.get_result(network) 208 | if network_result["status_code"] == 200: 209 | theme_image = network_result["response_json"] 210 | else: 211 | logger.warning( 212 | f"获取最新主题图像时出错:{network_result['error_message']}" 213 | ) 214 | MainInfoBar.push_info_bar( 215 | "warning", 216 | "获取最新主题图像时出错", 217 | f"网络错误:{network_result['status_code']}", 218 | 5000, 219 | ) 220 | return None 221 | 222 | if (Config.app_path / "resources/theme_image.json").exists(): 223 | with (Config.app_path / "resources/theme_image.json").open( 224 | mode="r", encoding="utf-8" 225 | ) as f: 226 | theme_image_local = json.load(f) 227 | time_local = datetime.strptime( 228 | theme_image_local["time"], "%Y-%m-%d %H:%M" 229 | ) 230 | else: 231 | time_local = datetime.strptime("2000-01-01 00:00", "%Y-%m-%d %H:%M") 232 | 233 | if not ( 234 | Config.app_path / "resources/images/Home/BannerTheme.jpg" 235 | ).exists() or ( 236 | datetime.now() 237 | > datetime.strptime(theme_image["time"], "%Y-%m-%d %H:%M") 238 | > time_local 239 | ): 240 | 241 | network = Network.add_task( 242 | mode="get_file", 243 | url=theme_image["url"], 244 | path=Config.app_path / "resources/images/Home/BannerTheme.jpg", 245 | ) 246 | network.loop.exec() 247 | network_result = Network.get_result(network) 248 | 249 | if network_result["status_code"] == 200: 250 | 251 | with (Config.app_path / "resources/theme_image.json").open( 252 | mode="w", encoding="utf-8" 253 | ) as f: 254 | json.dump(theme_image, f, ensure_ascii=False, indent=4) 255 | 256 | logger.success(f"主题图像「{theme_image["name"]}」下载成功") 257 | MainInfoBar.push_info_bar( 258 | "success", 259 | "主题图像下载成功", 260 | f"「{theme_image["name"]}」下载成功!", 261 | 3000, 262 | ) 263 | 264 | else: 265 | 266 | logger.warning( 267 | f"下载最新主题图像时出错:{network_result['error_message']}" 268 | ) 269 | MainInfoBar.push_info_bar( 270 | "warning", 271 | "下载最新主题图像时出错", 272 | f"网络错误:{network_result['status_code']}", 273 | 5000, 274 | ) 275 | 276 | else: 277 | 278 | logger.info("主题图像已是最新") 279 | MainInfoBar.push_info_bar( 280 | "info", 281 | "主题图像已是最新", 282 | "主题图像已是最新!", 283 | 3000, 284 | ) 285 | 286 | self.set_banner() 287 | 288 | def set_banner(self): 289 | """设置主页图像""" 290 | if Config.get(Config.function_HomeImageMode) == "默认": 291 | self.banner.set_banner_image( 292 | str(Config.app_path / "resources/images/Home/BannerDefault.png") 293 | ) 294 | self.imageButton.hide() 295 | self.banner_text.setVisible(False) 296 | elif Config.get(Config.function_HomeImageMode) == "自定义": 297 | for file in Config.app_path.glob("resources/images/Home/BannerCustomize.*"): 298 | self.banner.set_banner_image(str(file)) 299 | break 300 | self.imageButton.show() 301 | self.banner_text.setVisible(False) 302 | elif Config.get(Config.function_HomeImageMode) == "主题图像": 303 | self.banner.set_banner_image( 304 | str(Config.app_path / "resources/images/Home/BannerTheme.jpg") 305 | ) 306 | self.imageButton.show() 307 | self.banner_text.setVisible(True) 308 | 309 | if (Config.app_path / "resources/theme_image.json").exists(): 310 | with (Config.app_path / "resources/theme_image.json").open( 311 | mode="r", encoding="utf-8" 312 | ) as f: 313 | theme_image = json.load(f) 314 | html_content = theme_image["html"] 315 | else: 316 | html_content = "

主题图像

主题图像信息未知

" 317 | 318 | self.banner_text.setHtml(re.sub(r"]*>", "", html_content)) 319 | 320 | 321 | class ButtonGroup(SimpleCardWidget): 322 | """显示主页和 GitHub 按钮的竖直按钮组""" 323 | 324 | def __init__(self, parent=None): 325 | super().__init__(parent=parent) 326 | 327 | self.setFixedSize(56, 180) 328 | 329 | layout = QVBoxLayout(self) 330 | layout.setAlignment(Qt.AlignmentFlag.AlignTop) 331 | 332 | # 创建主页按钮 333 | home_button = IconButton( 334 | FluentIcon.HOME.icon(color=QColor("#fff")), 335 | tip_title="AUTO_MAA官网", 336 | tip_content="AUTO_MAA官方文档站", 337 | isTooltip=True, 338 | ) 339 | home_button.setIconSize(QSize(32, 32)) 340 | home_button.clicked.connect(self.open_home) 341 | layout.addWidget(home_button) 342 | 343 | # 创建 GitHub 按钮 344 | github_button = IconButton( 345 | FluentIcon.GITHUB.icon(color=QColor("#fff")), 346 | tip_title="Github仓库", 347 | tip_content="如果本项目有帮助到您~\n不妨给项目点一个Star⭐", 348 | isTooltip=True, 349 | ) 350 | github_button.setIconSize(QSize(32, 32)) 351 | github_button.clicked.connect(self.open_github) 352 | layout.addWidget(github_button) 353 | 354 | # # 创建 文档 按钮 355 | # doc_button = IconButton( 356 | # FluentIcon.DICTIONARY.icon(color=QColor("#fff")), 357 | # tip_title="自助排障文档", 358 | # tip_content="点击打开自助排障文档,好孩子都能看懂", 359 | # isTooltip=True, 360 | # ) 361 | # doc_button.setIconSize(QSize(32, 32)) 362 | # doc_button.clicked.connect(self.open_doc) 363 | # layout.addWidget(doc_button) 364 | 365 | # 创建 Q群 按钮 366 | doc_button = IconButton( 367 | FluentIcon.CHAT.icon(color=QColor("#fff")), 368 | tip_title="官方社群", 369 | tip_content="加入官方群聊【AUTO_MAA绝赞DeBug中!】", 370 | isTooltip=True, 371 | ) 372 | doc_button.setIconSize(QSize(32, 32)) 373 | doc_button.clicked.connect(self.open_chat) 374 | layout.addWidget(doc_button) 375 | 376 | # 创建 MirrorChyan 按钮 377 | doc_button = IconButton( 378 | FluentIcon.SHOPPING_CART.icon(color=QColor("#fff")), 379 | tip_title="非官方店铺", 380 | tip_content="获取 MirrorChyan CDK,更新快人一步", 381 | isTooltip=True, 382 | ) 383 | doc_button.setIconSize(QSize(32, 32)) 384 | doc_button.clicked.connect(self.open_sales) 385 | layout.addWidget(doc_button) 386 | 387 | def _normalBackgroundColor(self): 388 | return QColor(0, 0, 0, 96) 389 | 390 | def open_home(self): 391 | """打开主页链接""" 392 | QDesktopServices.openUrl(QUrl("https://clozya.github.io/AUTOMAA_docs")) 393 | 394 | def open_github(self): 395 | """打开 GitHub 链接""" 396 | QDesktopServices.openUrl(QUrl("https://github.com/DLmaster361/AUTO_MAA")) 397 | 398 | def open_chat(self): 399 | """打开 Q群 链接""" 400 | QDesktopServices.openUrl(QUrl("https://qm.qq.com/q/bd9fISNoME")) 401 | 402 | def open_doc(self): 403 | """打开 文档 链接""" 404 | QDesktopServices.openUrl(QUrl("https://clozya.github.io/AUTOMAA_docs")) 405 | 406 | def open_sales(self): 407 | """打开 MirrorChyan 链接""" 408 | QDesktopServices.openUrl(QUrl("https://mirrorchyan.com/")) 409 | -------------------------------------------------------------------------------- /app/ui/main_window.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA主界面 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | from loguru import logger 29 | from PySide6.QtWidgets import QApplication, QSystemTrayIcon 30 | from qfluentwidgets import ( 31 | Action, 32 | SystemTrayMenu, 33 | SplashScreen, 34 | FluentIcon, 35 | setTheme, 36 | isDarkTheme, 37 | SystemThemeListener, 38 | Theme, 39 | MSFluentWindow, 40 | NavigationItemPosition, 41 | ) 42 | from PySide6.QtGui import QIcon, QCloseEvent 43 | from PySide6.QtCore import QTimer 44 | from datetime import datetime, timedelta 45 | import shutil 46 | import darkdetect 47 | 48 | from app.core import Config, TaskManager, MainTimer, MainInfoBar, SoundPlayer 49 | from app.services import Notify, Crypto, System 50 | from .home import Home 51 | from .member_manager import MemberManager 52 | from .plan_manager import PlanManager 53 | from .queue_manager import QueueManager 54 | from .dispatch_center import DispatchCenter 55 | from .history import History 56 | from .setting import Setting 57 | 58 | 59 | class AUTO_MAA(MSFluentWindow): 60 | 61 | def __init__(self): 62 | super().__init__() 63 | 64 | self.setWindowIcon(QIcon(str(Config.app_path / "resources/icons/AUTO_MAA.ico"))) 65 | 66 | version_numb = list(map(int, Config.VERSION.split("."))) 67 | version_text = ( 68 | f"v{'.'.join(str(_) for _ in version_numb[0:3])}" 69 | if version_numb[3] == 0 70 | else f"v{'.'.join(str(_) for _ in version_numb[0:3])}-beta.{version_numb[3]}" 71 | ) 72 | 73 | self.setWindowTitle(f"AUTO_MAA - {version_text}") 74 | 75 | self.switch_theme() 76 | 77 | self.splashScreen = SplashScreen(self.windowIcon(), self) 78 | self.show_ui("显示主窗口", if_quick=True) 79 | 80 | Config.main_window = self.window() 81 | 82 | # 创建主窗口 83 | self.home = Home(self) 84 | self.plan_manager = PlanManager(self) 85 | self.member_manager = MemberManager(self) 86 | self.queue_manager = QueueManager(self) 87 | self.dispatch_center = DispatchCenter(self) 88 | self.history = History(self) 89 | self.setting = Setting(self) 90 | 91 | self.addSubInterface( 92 | self.home, 93 | FluentIcon.HOME, 94 | "主页", 95 | FluentIcon.HOME, 96 | NavigationItemPosition.TOP, 97 | ) 98 | self.addSubInterface( 99 | self.member_manager, 100 | FluentIcon.ROBOT, 101 | "脚本管理", 102 | FluentIcon.ROBOT, 103 | NavigationItemPosition.TOP, 104 | ) 105 | self.addSubInterface( 106 | self.plan_manager, 107 | FluentIcon.CALENDAR, 108 | "计划管理", 109 | FluentIcon.CALENDAR, 110 | NavigationItemPosition.TOP, 111 | ) 112 | self.addSubInterface( 113 | self.queue_manager, 114 | FluentIcon.BOOK_SHELF, 115 | "调度队列", 116 | FluentIcon.BOOK_SHELF, 117 | NavigationItemPosition.TOP, 118 | ) 119 | self.addSubInterface( 120 | self.dispatch_center, 121 | FluentIcon.IOT, 122 | "调度中心", 123 | FluentIcon.IOT, 124 | NavigationItemPosition.TOP, 125 | ) 126 | self.addSubInterface( 127 | self.history, 128 | FluentIcon.HISTORY, 129 | "历史记录", 130 | FluentIcon.HISTORY, 131 | NavigationItemPosition.BOTTOM, 132 | ) 133 | self.addSubInterface( 134 | self.setting, 135 | FluentIcon.SETTING, 136 | "设置", 137 | FluentIcon.SETTING, 138 | NavigationItemPosition.BOTTOM, 139 | ) 140 | self.stackedWidget.currentChanged.connect(self.__currentChanged) 141 | 142 | # 创建系统托盘及其菜单 143 | self.tray = QSystemTrayIcon( 144 | QIcon(str(Config.app_path / "resources/icons/AUTO_MAA.ico")), self 145 | ) 146 | self.tray.setToolTip("AUTO_MAA") 147 | self.tray_menu = SystemTrayMenu("AUTO_MAA", self) 148 | 149 | # 显示主界面菜单项 150 | self.tray_menu.addAction( 151 | Action( 152 | FluentIcon.CAFE, 153 | "显示主界面", 154 | triggered=lambda: self.show_ui("显示主窗口"), 155 | ) 156 | ) 157 | self.tray_menu.addSeparator() 158 | 159 | # 开始任务菜单项 160 | self.tray_menu.addActions( 161 | [ 162 | Action(FluentIcon.PLAY, "运行自动代理", triggered=self.start_main_task), 163 | Action( 164 | FluentIcon.PAUSE, 165 | "中止所有任务", 166 | triggered=lambda: TaskManager.stop_task("ALL"), 167 | ), 168 | ] 169 | ) 170 | self.tray_menu.addSeparator() 171 | 172 | # 退出主程序菜单项 173 | self.tray_menu.addAction( 174 | Action( 175 | FluentIcon.POWER_BUTTON, 176 | "退出主程序", 177 | triggered=lambda: System.set_power("KillSelf"), 178 | ) 179 | ) 180 | 181 | # 设置托盘菜单 182 | self.tray.setContextMenu(self.tray_menu) 183 | self.tray.activated.connect(self.on_tray_activated) 184 | 185 | self.set_min_method() 186 | 187 | Config.user_info_changed.connect(self.member_manager.refresh_dashboard) 188 | Config.power_sign_changed.connect(self.dispatch_center.update_power_sign) 189 | TaskManager.create_gui.connect(self.dispatch_center.add_board) 190 | TaskManager.connect_gui.connect(self.dispatch_center.connect_main_board) 191 | Notify.push_info_bar.connect(MainInfoBar.push_info_bar) 192 | self.setting.ui.card_IfShowTray.checkedChanged.connect( 193 | lambda: self.show_ui("配置托盘") 194 | ) 195 | self.setting.ui.card_IfToTray.checkedChanged.connect(self.set_min_method) 196 | self.setting.function.card_HomeImageMode.comboBox.currentIndexChanged.connect( 197 | lambda index: ( 198 | self.home.get_home_image() if index == 2 else self.home.set_banner() 199 | ) 200 | ) 201 | 202 | self.splashScreen.finish() 203 | 204 | self.themeListener = SystemThemeListener(self) 205 | self.themeListener.systemThemeChanged.connect(self.switch_theme) 206 | self.themeListener.start() 207 | 208 | def switch_theme(self) -> None: 209 | """切换主题""" 210 | 211 | setTheme( 212 | Theme(darkdetect.theme()) if darkdetect.theme() else Theme.LIGHT, lazy=True 213 | ) 214 | QTimer.singleShot(300, lambda: setTheme(Theme.AUTO, lazy=True)) 215 | 216 | # 云母特效启用时需要增加重试机制 217 | # 云母特效不兼容Win10,如果True则通过云母进行主题转换,False则根据当前主题设置背景颜色 218 | if self.isMicaEffectEnabled(): 219 | QTimer.singleShot( 220 | 300, 221 | lambda: self.windowEffect.setMicaEffect(self.winId(), isDarkTheme()), 222 | ) 223 | 224 | else: 225 | # 根据当前主题设置背景颜色 226 | if isDarkTheme(): 227 | self.setStyleSheet( 228 | """ 229 | CardWidget {background-color: #313131;} 230 | HeaderCardWidget {background-color: #313131;} 231 | background-color: #313131; 232 | """ 233 | ) 234 | else: 235 | self.setStyleSheet("background-color: #ffffff;") 236 | 237 | def set_min_method(self) -> None: 238 | """设置最小化方法""" 239 | 240 | if Config.get(Config.ui_IfToTray): 241 | 242 | self.titleBar.minBtn.clicked.disconnect() 243 | self.titleBar.minBtn.clicked.connect(lambda: self.show_ui("隐藏到托盘")) 244 | 245 | else: 246 | 247 | self.titleBar.minBtn.clicked.disconnect() 248 | self.titleBar.minBtn.clicked.connect(self.window().showMinimized) 249 | 250 | def on_tray_activated(self, reason): 251 | """双击返回主界面""" 252 | if reason == QSystemTrayIcon.DoubleClick: 253 | self.show_ui("显示主窗口") 254 | 255 | def show_ui( 256 | self, mode: str, if_quick: bool = False, if_start: bool = False 257 | ) -> None: 258 | """配置窗口状态""" 259 | 260 | self.switch_theme() 261 | 262 | if mode == "显示主窗口": 263 | 264 | # 配置主窗口 265 | if not self.window().isVisible(): 266 | size = list( 267 | map( 268 | int, 269 | Config.get(Config.ui_size).split("x"), 270 | ) 271 | ) 272 | location = list( 273 | map( 274 | int, 275 | Config.get(Config.ui_location).split("x"), 276 | ) 277 | ) 278 | if self.window().isMaximized(): 279 | self.window().showNormal() 280 | self.window().setGeometry(location[0], location[1], size[0], size[1]) 281 | self.window().show() 282 | if not if_quick: 283 | if ( 284 | Config.get(Config.ui_maximized) 285 | and not self.window().isMaximized() 286 | ): 287 | self.titleBar.maxBtn.click() 288 | SoundPlayer.play("欢迎回来") 289 | self.show_ui("配置托盘") 290 | elif if_start: 291 | if Config.get(Config.ui_maximized) and not self.window().isMaximized(): 292 | self.titleBar.maxBtn.click() 293 | self.show_ui("配置托盘") 294 | 295 | # 如果窗口不在屏幕内,则重置窗口位置 296 | if not any( 297 | self.window().geometry().intersects(screen.availableGeometry()) 298 | for screen in QApplication.screens() 299 | ): 300 | self.window().showNormal() 301 | self.window().setGeometry(100, 100, 1200, 700) 302 | 303 | self.window().raise_() 304 | self.window().activateWindow() 305 | 306 | while Config.info_bar_list: 307 | info_bar_item = Config.info_bar_list.pop(0) 308 | MainInfoBar.push_info_bar( 309 | info_bar_item["mode"], 310 | info_bar_item["title"], 311 | info_bar_item["content"], 312 | info_bar_item["time"], 313 | ) 314 | 315 | elif mode == "配置托盘": 316 | 317 | if Config.get(Config.ui_IfShowTray): 318 | self.tray.show() 319 | else: 320 | self.tray.hide() 321 | 322 | elif mode == "隐藏到托盘": 323 | 324 | # 保存窗口相关属性 325 | if not self.window().isMaximized(): 326 | 327 | Config.set( 328 | Config.ui_size, 329 | f"{self.geometry().width()}x{self.geometry().height()}", 330 | ) 331 | Config.set( 332 | Config.ui_location, 333 | f"{self.geometry().x()}x{self.geometry().y()}", 334 | ) 335 | 336 | Config.set(Config.ui_maximized, self.window().isMaximized()) 337 | Config.save() 338 | 339 | # 隐藏主窗口 340 | if not if_quick: 341 | 342 | self.window().hide() 343 | self.tray.show() 344 | 345 | def start_up_task(self) -> None: 346 | """启动时任务""" 347 | 348 | # 清理旧日志 349 | self.clean_old_logs() 350 | 351 | # 清理安装包 352 | if (Config.app_path / "AUTO_MAA-Setup.exe").exists(): 353 | try: 354 | (Config.app_path / "AUTO_MAA-Setup.exe").unlink() 355 | except Exception: 356 | pass 357 | 358 | # 检查密码 359 | self.setting.check_PASSWORD() 360 | 361 | # 获取主题图像 362 | if Config.get(Config.function_HomeImageMode) == "主题图像": 363 | self.home.get_home_image() 364 | 365 | # 直接运行主任务 366 | if Config.get(Config.start_IfRunDirectly): 367 | 368 | self.start_main_task() 369 | 370 | # 获取公告 371 | self.setting.show_notice(if_first=True) 372 | 373 | # 检查更新 374 | if Config.get(Config.update_IfAutoUpdate): 375 | self.setting.check_update(if_first=True) 376 | 377 | # 直接最小化 378 | if Config.get(Config.start_IfMinimizeDirectly): 379 | 380 | self.titleBar.minBtn.click() 381 | 382 | def clean_old_logs(self): 383 | """ 384 | 删除超过用户设定天数的日志文件(基于目录日期) 385 | """ 386 | 387 | if Config.get(Config.function_HistoryRetentionTime) == 0: 388 | logger.info("由于用户设置日志永久保留,跳过日志清理") 389 | return 390 | 391 | deleted_count = 0 392 | 393 | for date_folder in (Config.app_path / "history").iterdir(): 394 | if not date_folder.is_dir(): 395 | continue # 只处理日期文件夹 396 | 397 | try: 398 | # 只检查 `YYYY-MM-DD` 格式的文件夹 399 | folder_date = datetime.strptime(date_folder.name, "%Y-%m-%d") 400 | if datetime.now() - folder_date > timedelta( 401 | days=Config.get(Config.function_HistoryRetentionTime) 402 | ): 403 | shutil.rmtree(date_folder, ignore_errors=True) 404 | deleted_count += 1 405 | logger.info(f"已删除超期日志目录: {date_folder}") 406 | except ValueError: 407 | logger.warning(f"非日期格式的目录: {date_folder}") 408 | 409 | logger.info(f"清理完成: {deleted_count} 个日期目录") 410 | 411 | def start_main_task(self) -> None: 412 | """启动主任务""" 413 | 414 | if "调度队列_1" in Config.queue_dict: 415 | 416 | logger.info("自动添加任务:调度队列_1") 417 | TaskManager.add_task( 418 | "自动代理_主调度台", 419 | "调度队列_1", 420 | Config.queue_dict["调度队列_1"]["Config"].toDict(), 421 | ) 422 | 423 | elif "脚本_1" in Config.member_dict: 424 | 425 | logger.info("自动添加任务:脚本_1") 426 | TaskManager.add_task( 427 | "自动代理_主调度台", "自定义队列", {"Queue": {"Member_1": "脚本_1"}} 428 | ) 429 | 430 | else: 431 | 432 | logger.warning("启动主任务失败:未找到有效的主任务配置文件") 433 | MainInfoBar.push_info_bar( 434 | "warning", "启动主任务失败", "“调度队列_1”与“脚本_1”均不存在", -1 435 | ) 436 | 437 | def __currentChanged(self, index: int) -> None: 438 | """切换界面时任务""" 439 | 440 | if index == 1: 441 | self.member_manager.reload_plan_name() 442 | elif index == 3: 443 | self.queue_manager.reload_member_name() 444 | elif index == 4: 445 | self.dispatch_center.pivot.setCurrentItem("主调度台") 446 | self.dispatch_center.update_top_bar() 447 | 448 | def closeEvent(self, event: QCloseEvent): 449 | """清理残余进程""" 450 | 451 | self.show_ui("隐藏到托盘", if_quick=True) 452 | 453 | # 清理各功能线程 454 | MainTimer.Timer.stop() 455 | MainTimer.Timer.deleteLater() 456 | MainTimer.LongTimer.stop() 457 | MainTimer.LongTimer.deleteLater() 458 | TaskManager.stop_task("ALL") 459 | 460 | # 关闭主题监听 461 | self.themeListener.terminate() 462 | self.themeListener.deleteLater() 463 | 464 | logger.info("AUTO_MAA主程序关闭") 465 | logger.info("----------------END----------------") 466 | 467 | event.accept() 468 | -------------------------------------------------------------------------------- /app/ui/plan_manager.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA计划管理界面 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | from loguru import logger 29 | from PySide6.QtWidgets import ( 30 | QWidget, 31 | QVBoxLayout, 32 | QStackedWidget, 33 | QHeaderView, 34 | ) 35 | from qfluentwidgets import ( 36 | Action, 37 | FluentIcon, 38 | MessageBox, 39 | HeaderCardWidget, 40 | CommandBar, 41 | TableWidget, 42 | ) 43 | from typing import List, Dict, Union 44 | import shutil 45 | 46 | from app.core import Config, MainInfoBar, MaaPlanConfig, SoundPlayer 47 | from .Widget import ( 48 | ComboBoxMessageBox, 49 | LineEditSettingCard, 50 | ComboBoxSettingCard, 51 | SpinBoxSetting, 52 | EditableComboBoxSetting, 53 | ComboBoxSetting, 54 | PivotArea, 55 | ) 56 | 57 | 58 | class PlanManager(QWidget): 59 | """计划管理父界面""" 60 | 61 | def __init__(self, parent=None): 62 | super().__init__(parent) 63 | 64 | self.setObjectName("计划管理") 65 | 66 | layout = QVBoxLayout(self) 67 | 68 | self.tools = CommandBar() 69 | 70 | self.plan_manager = self.PlanSettingBox(self) 71 | 72 | # 逐个添加动作 73 | self.tools.addActions( 74 | [ 75 | Action(FluentIcon.ADD_TO, "新建计划表", triggered=self.add_setting_box), 76 | Action( 77 | FluentIcon.REMOVE_FROM, "删除计划表", triggered=self.del_setting_box 78 | ), 79 | ] 80 | ) 81 | self.tools.addSeparator() 82 | self.tools.addActions( 83 | [ 84 | Action( 85 | FluentIcon.LEFT_ARROW, "向左移动", triggered=self.left_setting_box 86 | ), 87 | Action( 88 | FluentIcon.RIGHT_ARROW, "向右移动", triggered=self.right_setting_box 89 | ), 90 | ] 91 | ) 92 | self.tools.addSeparator() 93 | 94 | layout.addWidget(self.tools) 95 | layout.addWidget(self.plan_manager) 96 | 97 | def add_setting_box(self): 98 | """添加一个计划表""" 99 | 100 | choice = ComboBoxMessageBox( 101 | self.window(), 102 | "选择一个计划类型以添加相应计划表", 103 | ["选择计划类型"], 104 | [["MAA"]], 105 | ) 106 | if choice.exec() and choice.input[0].currentIndex() != -1: 107 | 108 | if choice.input[0].currentText() == "MAA": 109 | 110 | index = len(Config.plan_dict) + 1 111 | 112 | maa_plan_config = MaaPlanConfig() 113 | maa_plan_config.load( 114 | Config.app_path / f"config/MaaPlanConfig/计划_{index}/config.json", 115 | maa_plan_config, 116 | ) 117 | maa_plan_config.save() 118 | 119 | Config.plan_dict[f"计划_{index}"] = { 120 | "Type": "Maa", 121 | "Path": Config.app_path / f"config/MaaPlanConfig/计划_{index}", 122 | "Config": maa_plan_config, 123 | } 124 | 125 | self.plan_manager.add_MaaPlanSettingBox(index) 126 | self.plan_manager.switch_SettingBox(index) 127 | 128 | logger.success(f"计划管理 计划_{index} 添加成功") 129 | MainInfoBar.push_info_bar( 130 | "success", "操作成功", f"添加计划表 计划_{index}", 3000 131 | ) 132 | SoundPlayer.play("添加计划表") 133 | 134 | def del_setting_box(self): 135 | """删除一个计划表""" 136 | 137 | name = self.plan_manager.pivot.currentRouteKey() 138 | 139 | if name is None: 140 | logger.warning("删除计划表时未选择计划表") 141 | MainInfoBar.push_info_bar( 142 | "warning", "未选择计划表", "请选择一个计划表", 5000 143 | ) 144 | return None 145 | 146 | if len(Config.running_list) > 0: 147 | logger.warning("删除计划表时调度队列未停止运行") 148 | MainInfoBar.push_info_bar( 149 | "warning", "调度中心正在执行任务", "请等待或手动中止任务", 5000 150 | ) 151 | return None 152 | 153 | choice = MessageBox("确认", f"确定要删除 {name} 吗?", self.window()) 154 | if choice.exec(): 155 | 156 | self.plan_manager.clear_SettingBox() 157 | 158 | shutil.rmtree(Config.plan_dict[name]["Path"]) 159 | Config.change_plan(name, "禁用") 160 | for i in range(int(name[3:]) + 1, len(Config.plan_dict) + 1): 161 | if Config.plan_dict[f"计划_{i}"]["Path"].exists(): 162 | Config.plan_dict[f"计划_{i}"]["Path"].rename( 163 | Config.plan_dict[f"计划_{i}"]["Path"].with_name(f"计划_{i-1}") 164 | ) 165 | Config.change_plan(f"计划_{i}", f"计划_{i-1}") 166 | 167 | self.plan_manager.show_SettingBox(max(int(name[3:]) - 1, 1)) 168 | 169 | logger.success(f"计划表 {name} 删除成功") 170 | MainInfoBar.push_info_bar("success", "操作成功", f"删除计划表 {name}", 3000) 171 | SoundPlayer.play("删除计划表") 172 | 173 | def left_setting_box(self): 174 | """向左移动计划表""" 175 | 176 | name = self.plan_manager.pivot.currentRouteKey() 177 | 178 | if name is None: 179 | logger.warning("向左移动计划表时未选择计划表") 180 | MainInfoBar.push_info_bar( 181 | "warning", "未选择计划表", "请选择一个计划表", 5000 182 | ) 183 | return None 184 | 185 | index = int(name[3:]) 186 | 187 | if index == 1: 188 | logger.warning("向左移动计划表时已到达最左端") 189 | MainInfoBar.push_info_bar( 190 | "warning", "已经是第一个计划表", "无法向左移动", 5000 191 | ) 192 | return None 193 | 194 | if len(Config.running_list) > 0: 195 | logger.warning("向左移动计划表时调度队列未停止运行") 196 | MainInfoBar.push_info_bar( 197 | "warning", "调度中心正在执行任务", "请等待或手动中止任务", 5000 198 | ) 199 | return None 200 | 201 | self.plan_manager.clear_SettingBox() 202 | 203 | Config.plan_dict[name]["Path"].rename( 204 | Config.plan_dict[name]["Path"].with_name("计划_0") 205 | ) 206 | Config.change_plan(name, "计划_0") 207 | Config.plan_dict[f"计划_{index-1}"]["Path"].rename( 208 | Config.plan_dict[name]["Path"] 209 | ) 210 | Config.change_plan(f"计划_{index-1}", name) 211 | Config.plan_dict[name]["Path"].with_name("计划_0").rename( 212 | Config.plan_dict[f"计划_{index-1}"]["Path"] 213 | ) 214 | Config.change_plan("计划_0", f"计划_{index-1}") 215 | 216 | self.plan_manager.show_SettingBox(index - 1) 217 | 218 | logger.success(f"计划表 {name} 左移成功") 219 | MainInfoBar.push_info_bar("success", "操作成功", f"左移计划表 {name}", 3000) 220 | 221 | def right_setting_box(self): 222 | """向右移动计划表""" 223 | 224 | name = self.plan_manager.pivot.currentRouteKey() 225 | 226 | if name is None: 227 | logger.warning("向右移动计划表时未选择计划表") 228 | MainInfoBar.push_info_bar( 229 | "warning", "未选择计划表", "请选择一个计划表", 5000 230 | ) 231 | return None 232 | 233 | index = int(name[3:]) 234 | 235 | if index == len(Config.plan_dict): 236 | logger.warning("向右移动计划表时已到达最右端") 237 | MainInfoBar.push_info_bar( 238 | "warning", "已经是最后一个计划表", "无法向右移动", 5000 239 | ) 240 | return None 241 | 242 | if len(Config.running_list) > 0: 243 | logger.warning("向右移动计划表时调度队列未停止运行") 244 | MainInfoBar.push_info_bar( 245 | "warning", "调度中心正在执行任务", "请等待或手动中止任务", 5000 246 | ) 247 | return None 248 | 249 | self.plan_manager.clear_SettingBox() 250 | 251 | Config.plan_dict[name]["Path"].rename( 252 | Config.plan_dict[name]["Path"].with_name("计划_0") 253 | ) 254 | Config.change_plan(name, "计划_0") 255 | Config.plan_dict[f"计划_{index+1}"]["Path"].rename( 256 | Config.plan_dict[name]["Path"] 257 | ) 258 | Config.change_plan(f"计划_{index+1}", name) 259 | Config.plan_dict[name]["Path"].with_name("计划_0").rename( 260 | Config.plan_dict[f"计划_{index+1}"]["Path"] 261 | ) 262 | Config.change_plan("计划_0", f"计划_{index+1}") 263 | 264 | self.plan_manager.show_SettingBox(index + 1) 265 | 266 | logger.success(f"计划表 {name} 右移成功") 267 | MainInfoBar.push_info_bar("success", "操作成功", f"右移计划表 {name}", 3000) 268 | 269 | class PlanSettingBox(QWidget): 270 | """计划管理子页面组""" 271 | 272 | def __init__(self, parent=None): 273 | super().__init__(parent) 274 | 275 | self.setObjectName("计划管理页面组") 276 | 277 | self.pivotArea = PivotArea(self) 278 | self.pivot = self.pivotArea.pivot 279 | 280 | self.stackedWidget = QStackedWidget(self) 281 | self.stackedWidget.setContentsMargins(0, 0, 0, 0) 282 | self.stackedWidget.setStyleSheet("background: transparent; border: none;") 283 | 284 | self.script_list: List[PlanManager.PlanSettingBox.MaaPlanSettingBox] = [] 285 | 286 | self.Layout = QVBoxLayout(self) 287 | self.Layout.addWidget(self.pivotArea) 288 | self.Layout.addWidget(self.stackedWidget) 289 | self.Layout.setContentsMargins(0, 0, 0, 0) 290 | 291 | self.pivot.currentItemChanged.connect( 292 | lambda index: self.switch_SettingBox( 293 | int(index[3:]), if_chang_pivot=False 294 | ) 295 | ) 296 | 297 | self.show_SettingBox(1) 298 | 299 | def show_SettingBox(self, index) -> None: 300 | """加载所有子界面""" 301 | 302 | Config.search_plan() 303 | 304 | for name, info in Config.plan_dict.items(): 305 | if info["Type"] == "Maa": 306 | self.add_MaaPlanSettingBox(int(name[3:])) 307 | 308 | self.switch_SettingBox(index) 309 | 310 | def switch_SettingBox(self, index: int, if_chang_pivot: bool = True) -> None: 311 | """切换到指定的子界面""" 312 | 313 | if len(Config.plan_dict) == 0: 314 | return None 315 | 316 | if index > len(Config.plan_dict): 317 | return None 318 | 319 | if if_chang_pivot: 320 | self.pivot.setCurrentItem(self.script_list[index - 1].objectName()) 321 | self.stackedWidget.setCurrentWidget(self.script_list[index - 1]) 322 | 323 | def clear_SettingBox(self) -> None: 324 | """清空所有子界面""" 325 | 326 | for sub_interface in self.script_list: 327 | Config.gameid_refreshed.disconnect(sub_interface.refresh_gameid) 328 | self.stackedWidget.removeWidget(sub_interface) 329 | sub_interface.deleteLater() 330 | self.script_list.clear() 331 | self.pivot.clear() 332 | 333 | def add_MaaPlanSettingBox(self, uid: int) -> None: 334 | """添加一个MAA设置界面""" 335 | 336 | maa_plan_setting_box = self.MaaPlanSettingBox(uid, self) 337 | 338 | self.script_list.append(maa_plan_setting_box) 339 | 340 | self.stackedWidget.addWidget(self.script_list[-1]) 341 | 342 | self.pivot.addItem(routeKey=f"计划_{uid}", text=f"计划 {uid}") 343 | 344 | class MaaPlanSettingBox(HeaderCardWidget): 345 | """MAA类计划设置界面""" 346 | 347 | def __init__(self, uid: int, parent=None): 348 | super().__init__(parent) 349 | 350 | self.setObjectName(f"计划_{uid}") 351 | self.setTitle("MAA计划表") 352 | self.config = Config.plan_dict[f"计划_{uid}"]["Config"] 353 | 354 | self.card_Name = LineEditSettingCard( 355 | icon=FluentIcon.EDIT, 356 | title="计划表名称", 357 | content="用于标识计划表的名称", 358 | text="请输入计划表名称", 359 | qconfig=self.config, 360 | configItem=self.config.Info_Name, 361 | parent=self, 362 | ) 363 | self.card_Mode = ComboBoxSettingCard( 364 | icon=FluentIcon.DICTIONARY, 365 | title="计划模式", 366 | content="全局模式下计划内容固定,周计划模式下计划按周一到周日切换", 367 | texts=["全局", "周计划"], 368 | qconfig=self.config, 369 | configItem=self.config.Info_Mode, 370 | parent=self, 371 | ) 372 | 373 | self.table = TableWidget(self) 374 | self.table.setColumnCount(8) 375 | self.table.setRowCount(6) 376 | self.table.setHorizontalHeaderLabels( 377 | ["全局", "周一", "周二", "周三", "周四", "周五", "周六", "周日"] 378 | ) 379 | self.table.setVerticalHeaderLabels( 380 | [ 381 | "吃理智药", 382 | "连战次数", 383 | "关卡选择", 384 | "备选 - 1", 385 | "备选 - 2", 386 | "剩余理智", 387 | ] 388 | ) 389 | self.table.setAlternatingRowColors(False) 390 | self.table.setEditTriggers(TableWidget.NoEditTriggers) 391 | for col in range(8): 392 | self.table.horizontalHeader().setSectionResizeMode( 393 | col, QHeaderView.ResizeMode.Stretch 394 | ) 395 | for row in range(6): 396 | self.table.verticalHeader().setSectionResizeMode( 397 | row, QHeaderView.ResizeMode.ResizeToContents 398 | ) 399 | 400 | self.item_dict: Dict[ 401 | str, 402 | Dict[ 403 | str, 404 | Union[SpinBoxSetting, ComboBoxSetting, EditableComboBoxSetting], 405 | ], 406 | ] = {} 407 | 408 | for col, (group, name_dict) in enumerate( 409 | self.config.config_item_dict.items() 410 | ): 411 | 412 | self.item_dict[group] = {} 413 | 414 | for row, (name, configItem) in enumerate(name_dict.items()): 415 | 416 | if name == "MedicineNumb": 417 | self.item_dict[group][name] = SpinBoxSetting( 418 | range=(0, 1024), 419 | qconfig=self.config, 420 | configItem=configItem, 421 | parent=self, 422 | ) 423 | elif name == "SeriesNumb": 424 | self.item_dict[group][name] = ComboBoxSetting( 425 | texts=["AUTO", "6", "5", "4", "3", "2", "1", "不选择"], 426 | qconfig=self.config, 427 | configItem=configItem, 428 | parent=self, 429 | ) 430 | elif name == "GameId_Remain": 431 | self.item_dict[group][name] = EditableComboBoxSetting( 432 | value=Config.gameid_dict[group]["value"], 433 | texts=[ 434 | "不使用" if _ == "当前/上次" else _ 435 | for _ in Config.gameid_dict[group]["text"] 436 | ], 437 | qconfig=self.config, 438 | configItem=configItem, 439 | parent=self, 440 | ) 441 | elif "GameId" in name: 442 | self.item_dict[group][name] = EditableComboBoxSetting( 443 | value=Config.gameid_dict[group]["value"], 444 | texts=Config.gameid_dict[group]["text"], 445 | qconfig=self.config, 446 | configItem=configItem, 447 | parent=self, 448 | ) 449 | 450 | self.table.setCellWidget(row, col, self.item_dict[group][name]) 451 | 452 | Layout = QVBoxLayout() 453 | Layout.addWidget(self.card_Name) 454 | Layout.addWidget(self.card_Mode) 455 | Layout.addWidget(self.table) 456 | 457 | self.viewLayout.addLayout(Layout) 458 | self.viewLayout.setSpacing(3) 459 | self.viewLayout.setContentsMargins(3, 0, 3, 3) 460 | 461 | self.card_Mode.comboBox.currentIndexChanged.connect(self.switch_mode) 462 | Config.gameid_refreshed.connect(self.refresh_gameid) 463 | 464 | self.switch_mode() 465 | 466 | def switch_mode(self) -> None: 467 | """切换计划模式""" 468 | 469 | for group, name_dict in self.item_dict.items(): 470 | for name, setting_item in name_dict.items(): 471 | setting_item.setEnabled( 472 | (group == "ALL") 473 | == (self.config.get(self.config.Info_Mode) == "ALL") 474 | ) 475 | 476 | def refresh_gameid(self): 477 | 478 | for group, name_dict in self.item_dict.items(): 479 | 480 | for name, setting_item in name_dict.items(): 481 | 482 | if name == "GameId_Remain": 483 | 484 | setting_item.reLoadOptions( 485 | Config.gameid_dict[group]["value"], 486 | [ 487 | "不使用" if _ == "当前/上次" else _ 488 | for _ in Config.gameid_dict[group]["text"] 489 | ], 490 | ) 491 | 492 | elif "GameId" in name: 493 | 494 | setting_item.reLoadOptions( 495 | Config.gameid_dict[group]["value"], 496 | Config.gameid_dict[group]["text"], 497 | ) 498 | -------------------------------------------------------------------------------- /app/utils/AUTO_MAA.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "AUTO_MAA" 5 | #define MyAppVersion "" 6 | #define MyAppPublisher "AUTO_MAA Team" 7 | #define MyAppURL "https://doc.automaa.xyz/" 8 | #define MyAppExeName "AUTO_MAA.exe" 9 | #define MyAppPath "" 10 | #define OutputDir "" 11 | 12 | [Setup] 13 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 14 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 15 | AppId={{D116A92A-E174-4699-B777-61C5FD837B19} 16 | AppName={#MyAppName} 17 | AppVersion={#MyAppVersion} 18 | AppVerName={#MyAppName} 19 | AppPublisher={#MyAppPublisher} 20 | AppPublisherURL={#MyAppURL} 21 | AppSupportURL={#MyAppURL} 22 | AppUpdatesURL={#MyAppURL} 23 | DefaultDirName=D:\{#MyAppName} 24 | UninstallDisplayIcon={app}\{#MyAppExeName} 25 | ; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run 26 | ; on anything but x64 and Windows 11 on Arm. 27 | ArchitecturesAllowed=x64compatible 28 | ; "ArchitecturesInstallIn64BitMode=x64compatible" requests that the 29 | ; install be done in "64-bit mode" on x64 or Windows 11 on Arm, 30 | ; meaning it should use the native 64-bit Program Files directory and 31 | ; the 64-bit view of the registry. 32 | ArchitecturesInstallIn64BitMode=x64compatible 33 | DisableProgramGroupPage=yes 34 | LicenseFile={#MyAppPath}\LICENSE 35 | ; Remove the following line to run in administrative install mode (install for all users). 36 | PrivilegesRequired=lowest 37 | OutputDir={#OutputDir} 38 | OutputBaseFilename=AUTO_MAA-Setup 39 | SetupIconFile={#MyAppPath}\resources\icons\AUTO_MAA.ico 40 | SolidCompression=yes 41 | WizardStyle=modern 42 | 43 | [Languages] 44 | Name: "Chinese"; MessagesFile: "{#MyAppPath}\resources\docs\ChineseSimplified.isl" 45 | Name: "English"; MessagesFile: "compiler:Default.isl" 46 | 47 | [Tasks] 48 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked 49 | 50 | [Files] 51 | Source: "{#MyAppPath}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion 52 | Source: "{#MyAppPath}\app\*"; DestDir: "{app}\app"; Flags: ignoreversion recursesubdirs createallsubdirs 53 | Source: "{#MyAppPath}\resources\*"; DestDir: "{app}\resources"; Flags: ignoreversion recursesubdirs createallsubdirs 54 | Source: "{#MyAppPath}\main.py"; DestDir: "{app}"; Flags: ignoreversion 55 | Source: "{#MyAppPath}\requirements.txt"; DestDir: "{app}"; Flags: ignoreversion 56 | Source: "{#MyAppPath}\README.md"; DestDir: "{app}"; Flags: ignoreversion 57 | Source: "{#MyAppPath}\LICENSE"; DestDir: "{app}"; Flags: ignoreversion 58 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 59 | 60 | [Icons] 61 | Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 62 | Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon 63 | 64 | [Run] 65 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall 66 | 67 | [Code] 68 | var 69 | DeleteDataQuestion: Boolean; 70 | 71 | function InitializeUninstall: Boolean; 72 | begin 73 | DeleteDataQuestion := MsgBox('您确认要完全移除 AUTO_MAA 的所有用户数据文件与子组件吗?', mbConfirmation, MB_YESNO) = IDYES; 74 | Result := True; 75 | end; 76 | 77 | procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); 78 | begin 79 | if CurUninstallStep = usPostUninstall then 80 | begin 81 | DelTree(ExpandConstant('{app}\app'), True, True, True); 82 | DelTree(ExpandConstant('{app}\resources'), True, True, True); 83 | if DeleteDataQuestion then 84 | begin 85 | DelTree(ExpandConstant('{app}'), True, True, True); 86 | end; 87 | end; 88 | end; 89 | -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA工具包 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | __version__ = "4.2.0" 29 | __author__ = "DLmaster361 " 30 | __license__ = "GPL-3.0 license" 31 | 32 | __all__ = [] 33 | -------------------------------------------------------------------------------- /app/utils/package.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA打包程序 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | import os 29 | import sys 30 | import json 31 | import shutil 32 | from pathlib import Path 33 | 34 | 35 | def version_text(version_numb: list) -> str: 36 | """将版本号列表转为可读的文本信息""" 37 | 38 | while len(version_numb) < 4: 39 | version_numb.append(0) 40 | 41 | if version_numb[3] == 0: 42 | version = f"v{'.'.join(str(_) for _ in version_numb[0:3])}" 43 | else: 44 | version = ( 45 | f"v{'.'.join(str(_) for _ in version_numb[0:3])}-beta.{version_numb[3]}" 46 | ) 47 | return version 48 | 49 | 50 | def version_info_markdown(info: dict) -> str: 51 | """将版本信息字典转为markdown信息""" 52 | 53 | version_info = "" 54 | for key, value in info.items(): 55 | version_info += f"## {key}\n" 56 | for v in value: 57 | version_info += f"- {v}\n" 58 | return version_info 59 | 60 | 61 | if __name__ == "__main__": 62 | 63 | root_path = Path(sys.argv[0]).resolve().parent 64 | 65 | with (root_path / "resources/version.json").open(mode="r", encoding="utf-8") as f: 66 | version = json.load(f) 67 | 68 | main_version_numb = list(map(int, version["main_version"].split("."))) 69 | 70 | print("Packaging AUTO_MAA main program ...") 71 | 72 | os.system( 73 | "powershell -Command python -m nuitka --standalone --onefile --mingw64" 74 | " --enable-plugins=pyside6 --windows-console-mode=disable" 75 | " --onefile-tempdir-spec='{TEMP}\\AUTO_MAA'" 76 | " --windows-icon-from-ico=resources\\icons\\AUTO_MAA.ico" 77 | " --company-name='AUTO_MAA Team' --product-name=AUTO_MAA" 78 | f" --file-version={version["main_version"]}" 79 | f" --product-version={version["main_version"]}" 80 | " --file-description='AUTO_MAA Component'" 81 | " --copyright='Copyright © 2024-2025 DLmaster361'" 82 | " --assume-yes-for-downloads --output-filename=AUTO_MAA" 83 | " --remove-output main.py" 84 | ) 85 | 86 | print("AUTO_MAA main program packaging completed !") 87 | 88 | print("start to create setup program ...") 89 | 90 | (root_path / "AUTO_MAA").mkdir(parents=True, exist_ok=True) 91 | shutil.move(root_path / "AUTO_MAA.exe", root_path / "AUTO_MAA/") 92 | shutil.copytree(root_path / "app", root_path / "AUTO_MAA/app") 93 | shutil.copytree(root_path / "resources", root_path / "AUTO_MAA/resources") 94 | shutil.copy(root_path / "main.py", root_path / "AUTO_MAA/") 95 | shutil.copy(root_path / "requirements.txt", root_path / "AUTO_MAA/") 96 | shutil.copy(root_path / "README.md", root_path / "AUTO_MAA/") 97 | shutil.copy(root_path / "LICENSE", root_path / "AUTO_MAA/") 98 | 99 | with (root_path / "app/utils/AUTO_MAA.iss").open(mode="r", encoding="utf-8") as f: 100 | iss = f.read() 101 | iss = ( 102 | iss.replace( 103 | '#define MyAppVersion ""', 104 | f'#define MyAppVersion "{version["main_version"]}"', 105 | ) 106 | .replace( 107 | '#define MyAppPath ""', f'#define MyAppPath "{root_path / "AUTO_MAA"}"' 108 | ) 109 | .replace('#define OutputDir ""', f'#define OutputDir "{root_path}"') 110 | ) 111 | with (root_path / "AUTO_MAA.iss").open(mode="w", encoding="utf-8") as f: 112 | f.write(iss) 113 | 114 | os.system(f'ISCC "{root_path / "AUTO_MAA.iss"}"') 115 | 116 | (root_path / "AUTO_MAA_Setup").mkdir(parents=True, exist_ok=True) 117 | shutil.move(root_path / "AUTO_MAA-Setup.exe", root_path / "AUTO_MAA_Setup") 118 | 119 | shutil.make_archive( 120 | base_name=root_path / f"AUTO_MAA_{version_text(main_version_numb)}", 121 | format="zip", 122 | root_dir=root_path / "AUTO_MAA_Setup", 123 | base_dir=".", 124 | ) 125 | 126 | print("setup program created !") 127 | 128 | (root_path / "AUTO_MAA.iss").unlink(missing_ok=True) 129 | shutil.rmtree(root_path / "AUTO_MAA") 130 | shutil.rmtree(root_path / "AUTO_MAA_Setup") 131 | 132 | all_version_info = {} 133 | for v_i in version["version_info"].values(): 134 | for key, value in v_i.items(): 135 | if key in all_version_info: 136 | all_version_info[key] += value.copy() 137 | else: 138 | all_version_info[key] = value.copy() 139 | 140 | (root_path / "version_info.txt").write_text( 141 | f"{version_text(main_version_numb)}\n\n\n{version_info_markdown(all_version_info)}", 142 | encoding="utf-8", 143 | ) 144 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # AUTO_MAA:A MAA Multi Account Management and Automation Tool 2 | # Copyright © 2024-2025 DLmaster361 3 | 4 | # This file is part of AUTO_MAA. 5 | 6 | # AUTO_MAA is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, 9 | # or (at your option) any later version. 10 | 11 | # AUTO_MAA is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty 13 | # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See 14 | # the GNU General Public License for more details. 15 | 16 | # You should have received a copy of the GNU General Public License 17 | # along with AUTO_MAA. If not, see . 18 | 19 | # Contact: DLmaster_361@163.com 20 | 21 | """ 22 | AUTO_MAA 23 | AUTO_MAA主程序 24 | v4.3 25 | 作者:DLmaster_361 26 | """ 27 | 28 | from loguru import logger 29 | from PySide6.QtWidgets import QApplication 30 | from qfluentwidgets import FluentTranslator 31 | import sys 32 | 33 | 34 | @logger.catch 35 | def main(): 36 | 37 | application = QApplication(sys.argv) 38 | 39 | translator = FluentTranslator() 40 | application.installTranslator(translator) 41 | 42 | from app.ui.main_window import AUTO_MAA 43 | 44 | window = AUTO_MAA() 45 | window.show_ui("显示主窗口", if_start=True) 46 | window.start_up_task() 47 | sys.exit(application.exec()) 48 | 49 | 50 | if __name__ == "__main__": 51 | 52 | main() 53 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | loguru 2 | plyer 3 | PySide6 4 | PySide6-Fluent-Widgets[full] 5 | psutil 6 | opencv-python 7 | pywin32 8 | pyautogui 9 | pycryptodome 10 | requests 11 | markdown 12 | Jinja2 13 | nuitka -------------------------------------------------------------------------------- /resources/docs/ChineseSimplified.isl: -------------------------------------------------------------------------------- 1 | ; *** Inno Setup version 6.4.0+ Chinese Simplified messages *** 2 | ; 3 | ; To download user-contributed translations of this file, go to: 4 | ; https://jrsoftware.org/files/istrans/ 5 | ; 6 | ; Note: When translating this text, do not add periods (.) to the end of 7 | ; messages that didn't have them already, because on those messages Inno 8 | ; Setup adds the periods automatically (appending a period would result in 9 | ; two periods being displayed). 10 | ; 11 | ; Maintained by Zhenghan Yang 12 | ; Email: 847320916@QQ.com 13 | ; Translation based on network resource 14 | ; The latest Translation is on https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation 15 | ; 16 | 17 | [LangOptions] 18 | ; The following three entries are very important. Be sure to read and 19 | ; understand the '[LangOptions] section' topic in the help file. 20 | LanguageName=简体中文 21 | ; If Language Name display incorrect, uncomment next line 22 | ; LanguageName=<7B80><4F53><4E2D><6587> 23 | ; About LanguageID, to reference link: 24 | ; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c 25 | LanguageID=$0804 26 | ; About CodePage, to reference link: 27 | ; https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers 28 | LanguageCodePage=936 29 | ; If the language you are translating to requires special font faces or 30 | ; sizes, uncomment any of the following entries and change them accordingly. 31 | ;DialogFontName= 32 | ;DialogFontSize=8 33 | ;WelcomeFontName=Verdana 34 | ;WelcomeFontSize=12 35 | ;TitleFontName=Arial 36 | ;TitleFontSize=29 37 | ;CopyrightFontName=Arial 38 | ;CopyrightFontSize=8 39 | 40 | [Messages] 41 | 42 | ; *** 应用程序标题 43 | SetupAppTitle=安装 44 | SetupWindowTitle=安装 - %1 45 | UninstallAppTitle=卸载 46 | UninstallAppFullTitle=%1 卸载 47 | 48 | ; *** Misc. common 49 | InformationTitle=信息 50 | ConfirmTitle=确认 51 | ErrorTitle=错误 52 | 53 | ; *** SetupLdr messages 54 | SetupLdrStartupMessage=现在将安装 %1。您想要继续吗? 55 | LdrCannotCreateTemp=无法创建临时文件。安装程序已中止 56 | LdrCannotExecTemp=无法执行临时目录中的文件。安装程序已中止 57 | HelpTextNote= 58 | 59 | ; *** 启动错误消息 60 | LastErrorMessage=%1。%n%n错误 %2: %3 61 | SetupFileMissing=安装目录中缺少文件 %1。请修正这个问题或者获取程序的新副本。 62 | SetupFileCorrupt=安装文件已损坏。请获取程序的新副本。 63 | SetupFileCorruptOrWrongVer=安装文件已损坏,或是与这个安装程序的版本不兼容。请修正这个问题或获取新的程序副本。 64 | InvalidParameter=无效的命令行参数:%n%n%1 65 | SetupAlreadyRunning=安装程序正在运行。 66 | WindowsVersionNotSupported=此程序不支持当前计算机运行的 Windows 版本。 67 | WindowsServicePackRequired=此程序需要 %1 服务包 %2 或更高版本。 68 | NotOnThisPlatform=此程序不能在 %1 上运行。 69 | OnlyOnThisPlatform=此程序只能在 %1 上运行。 70 | OnlyOnTheseArchitectures=此程序只能安装到为下列处理器架构设计的 Windows 版本中:%n%n%1 71 | WinVersionTooLowError=此程序需要 %1 版本 %2 或更高。 72 | WinVersionTooHighError=此程序不能安装于 %1 版本 %2 或更高。 73 | AdminPrivilegesRequired=在安装此程序时您必须以管理员身份登录。 74 | PowerUserPrivilegesRequired=在安装此程序时您必须以管理员身份或有权限的用户组身份登录。 75 | SetupAppRunningError=安装程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。 76 | UninstallAppRunningError=卸载程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。 77 | 78 | ; *** 启动问题 79 | PrivilegesRequiredOverrideTitle=选择安装程序模式 80 | PrivilegesRequiredOverrideInstruction=选择安装模式 81 | PrivilegesRequiredOverrideText1=%1 可以为所有用户安装(需要管理员权限),或仅为您安装。 82 | PrivilegesRequiredOverrideText2=%1 只能为您安装,或为所有用户安装(需要管理员权限)。 83 | PrivilegesRequiredOverrideAllUsers=为所有用户安装(&A) 84 | PrivilegesRequiredOverrideAllUsersRecommended=为所有用户安装(&A) (建议选项) 85 | PrivilegesRequiredOverrideCurrentUser=只为我安装(&M) 86 | PrivilegesRequiredOverrideCurrentUserRecommended=只为我安装(&M) (建议选项) 87 | 88 | ; *** 其他错误 89 | ErrorCreatingDir=安装程序无法创建目录“%1” 90 | ErrorTooManyFilesInDir=无法在目录“%1”中创建文件,因为里面包含太多文件 91 | 92 | ; *** 安装程序公共消息 93 | ExitSetupTitle=退出安装程序 94 | ExitSetupMessage=安装程序尚未完成。如果现在退出,将不会安装该程序。%n%n您之后可以再次运行安装程序完成安装。%n%n现在退出安装程序吗? 95 | AboutSetupMenuItem=关于安装程序(&A)... 96 | AboutSetupTitle=关于安装程序 97 | AboutSetupMessage=%1 版本 %2%n%3%n%n%1 主页:%n%4 98 | AboutSetupNote= 99 | TranslatorNote=简体中文翻译由Kira(847320916@qq.com)维护。项目地址:https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation 100 | 101 | ; *** 按钮 102 | ButtonBack=< 上一步(&B) 103 | ButtonNext=下一步(&N) > 104 | ButtonInstall=安装(&I) 105 | ButtonOK=确定 106 | ButtonCancel=取消 107 | ButtonYes=是(&Y) 108 | ButtonYesToAll=全是(&A) 109 | ButtonNo=否(&N) 110 | ButtonNoToAll=全否(&O) 111 | ButtonFinish=完成(&F) 112 | ButtonBrowse=浏览(&B)... 113 | ButtonWizardBrowse=浏览(&R)... 114 | ButtonNewFolder=新建文件夹(&M) 115 | 116 | ; *** “选择语言”对话框消息 117 | SelectLanguageTitle=选择安装语言 118 | SelectLanguageLabel=选择安装时使用的语言。 119 | 120 | ; *** 公共向导文字 121 | ClickNext=点击“下一步”继续,或点击“取消”退出安装程序。 122 | BeveledLabel= 123 | BrowseDialogTitle=浏览文件夹 124 | BrowseDialogLabel=在下面的列表中选择一个文件夹,然后点击“确定”。 125 | NewFolderName=新建文件夹 126 | 127 | ; *** “欢迎”向导页 128 | WelcomeLabel1=欢迎使用 [name] 安装向导 129 | WelcomeLabel2=现在将安装 [name/ver] 到您的电脑中。%n%n建议您在继续安装前关闭所有其他应用程序。 130 | 131 | ; *** “密码”向导页 132 | WizardPassword=密码 133 | PasswordLabel1=这个安装程序有密码保护。 134 | PasswordLabel3=请输入密码,然后点击“下一步”继续。密码区分大小写。 135 | PasswordEditLabel=密码(&P): 136 | IncorrectPassword=您输入的密码不正确,请重新输入。 137 | 138 | ; *** “许可协议”向导页 139 | WizardLicense=许可协议 140 | LicenseLabel=请在继续安装前阅读以下重要信息。 141 | LicenseLabel3=请仔细阅读下列许可协议。在继续安装前您必须同意这些协议条款。 142 | LicenseAccepted=我同意此协议(&A) 143 | LicenseNotAccepted=我不同意此协议(&D) 144 | 145 | ; *** “信息”向导页 146 | WizardInfoBefore=信息 147 | InfoBeforeLabel=请在继续安装前阅读以下重要信息。 148 | InfoBeforeClickLabel=准备好继续安装后,点击“下一步”。 149 | WizardInfoAfter=信息 150 | InfoAfterLabel=请在继续安装前阅读以下重要信息。 151 | InfoAfterClickLabel=准备好继续安装后,点击“下一步”。 152 | 153 | ; *** “用户信息”向导页 154 | WizardUserInfo=用户信息 155 | UserInfoDesc=请输入您的信息。 156 | UserInfoName=用户名(&U): 157 | UserInfoOrg=组织(&O): 158 | UserInfoSerial=序列号(&S): 159 | UserInfoNameRequired=您必须输入用户名。 160 | 161 | ; *** “选择目标目录”向导页 162 | WizardSelectDir=选择目标位置 163 | SelectDirDesc=您想将 [name] 安装在哪里? 164 | SelectDirLabel3=安装程序将安装 [name] 到下面的文件夹中。 165 | SelectDirBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。 166 | DiskSpaceGBLabel=至少需要有 [gb] GB 的可用磁盘空间。 167 | DiskSpaceMBLabel=至少需要有 [mb] MB 的可用磁盘空间。 168 | CannotInstallToNetworkDrive=安装程序无法安装到一个网络驱动器。 169 | CannotInstallToUNCPath=安装程序无法安装到一个 UNC 路径。 170 | InvalidPath=您必须输入一个带驱动器卷标的完整路径,例如:%n%nC:\APP%n%n或UNC路径:%n%n\\server\share 171 | InvalidDrive=您选定的驱动器或 UNC 共享不存在或不能访问。请选择其他位置。 172 | DiskSpaceWarningTitle=磁盘空间不足 173 | DiskSpaceWarning=安装程序至少需要 %1 KB 的可用空间才能安装,但选定驱动器只有 %2 KB 的可用空间。%n%n您一定要继续吗? 174 | DirNameTooLong=文件夹名称或路径太长。 175 | InvalidDirName=文件夹名称无效。 176 | BadDirName32=文件夹名称不能包含下列任何字符:%n%n%1 177 | DirExistsTitle=文件夹已存在 178 | DirExists=文件夹:%n%n%1%n%n已经存在。您一定要安装到这个文件夹中吗? 179 | DirDoesntExistTitle=文件夹不存在 180 | DirDoesntExist=文件夹:%n%n%1%n%n不存在。您想要创建此文件夹吗? 181 | 182 | ; *** “选择组件”向导页 183 | WizardSelectComponents=选择组件 184 | SelectComponentsDesc=您想安装哪些程序组件? 185 | SelectComponentsLabel2=选中您想安装的组件;取消您不想安装的组件。然后点击“下一步”继续。 186 | FullInstallation=完全安装 187 | ; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language) 188 | CompactInstallation=简洁安装 189 | CustomInstallation=自定义安装 190 | NoUninstallWarningTitle=组件已存在 191 | NoUninstallWarning=安装程序检测到下列组件已安装在您的电脑中:%n%n%1%n%n取消选中这些组件不会卸载它们。%n%n确定要继续吗? 192 | ComponentSize1=%1 KB 193 | ComponentSize2=%1 MB 194 | ComponentsDiskSpaceGBLabel=当前选择的组件需要至少 [gb] GB 的磁盘空间。 195 | ComponentsDiskSpaceMBLabel=当前选择的组件需要至少 [mb] MB 的磁盘空间。 196 | 197 | ; *** “选择附加任务”向导页 198 | WizardSelectTasks=选择附加任务 199 | SelectTasksDesc=您想要安装程序执行哪些附加任务? 200 | SelectTasksLabel2=选择您想要安装程序在安装 [name] 时执行的附加任务,然后点击“下一步”。 201 | 202 | ; *** “选择开始菜单文件夹”向导页 203 | WizardSelectProgramGroup=选择开始菜单文件夹 204 | SelectStartMenuFolderDesc=安装程序应该在哪里放置程序的快捷方式? 205 | SelectStartMenuFolderLabel3=安装程序将在下列“开始”菜单文件夹中创建程序的快捷方式。 206 | SelectStartMenuFolderBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。 207 | MustEnterGroupName=您必须输入一个文件夹名。 208 | GroupNameTooLong=文件夹名或路径太长。 209 | InvalidGroupName=无效的文件夹名字。 210 | BadGroupName=文件夹名不能包含下列任何字符:%n%n%1 211 | NoProgramGroupCheck2=不创建开始菜单文件夹(&D) 212 | 213 | ; *** “准备安装”向导页 214 | WizardReady=准备安装 215 | ReadyLabel1=安装程序准备就绪,现在可以开始安装 [name] 到您的电脑。 216 | ReadyLabel2a=点击“安装”继续此安装程序。如果您想重新考虑或修改任何设置,点击“上一步”。 217 | ReadyLabel2b=点击“安装”继续此安装程序。 218 | ReadyMemoUserInfo=用户信息: 219 | ReadyMemoDir=目标位置: 220 | ReadyMemoType=安装类型: 221 | ReadyMemoComponents=已选择组件: 222 | ReadyMemoGroup=开始菜单文件夹: 223 | ReadyMemoTasks=附加任务: 224 | 225 | ; *** TExtractionWizardPage wizard page and Extract7ZipArchive 226 | ExtractionLabel=正在提取附加文件... 227 | ButtonStopExtraction=停止提取(&S) 228 | StopExtraction=您确定要停止提取吗? 229 | ErrorExtractionAborted=提取已中止 230 | ErrorExtractionFailed=提取失败:%1 231 | 232 | ; *** TDownloadWizardPage wizard page and DownloadTemporaryFile 233 | DownloadingLabel=正在下载附加文件... 234 | ButtonStopDownload=停止下载(&S) 235 | StopDownload=您确定要停止下载吗? 236 | ErrorDownloadAborted=下载已中止 237 | ErrorDownloadFailed=下载失败:%1 %2 238 | ErrorDownloadSizeFailed=获取下载大小失败:%1 %2 239 | ErrorFileHash1=校验文件哈希失败:%1 240 | ErrorFileHash2=无效的文件哈希:预期 %1,实际 %2 241 | ErrorProgress=无效的进度:%1 / %2 242 | ErrorFileSize=文件大小错误:预期 %1,实际 %2 243 | 244 | ; *** “正在准备安装”向导页 245 | WizardPreparing=正在准备安装 246 | PreparingDesc=安装程序正在准备安装 [name] 到您的电脑。 247 | PreviousInstallNotCompleted=先前的程序安装或卸载未完成,您需要重启您的电脑以完成。%n%n在重启电脑后,再次运行安装程序以完成 [name] 的安装。 248 | CannotContinue=安装程序不能继续。请点击“取消”退出。 249 | ApplicationsFound=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。 250 | ApplicationsFound2=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。安装完成后,安装程序将尝试重新启动这些应用程序。 251 | CloseApplications=自动关闭应用程序(&A) 252 | DontCloseApplications=不要关闭应用程序(&D) 253 | ErrorCloseApplications=安装程序无法自动关闭所有应用程序。建议您在继续之前,关闭所有在使用需要由安装程序更新的文件的应用程序。 254 | PrepareToInstallNeedsRestart=安装程序必须重启您的计算机。计算机重启后,请再次运行安装程序以完成 [name] 的安装。%n%n是否立即重新启动? 255 | 256 | ; *** “正在安装”向导页 257 | WizardInstalling=正在安装 258 | InstallingLabel=安装程序正在安装 [name] 到您的电脑,请稍候。 259 | 260 | ; *** “安装完成”向导页 261 | FinishedHeadingLabel=[name] 安装完成 262 | FinishedLabelNoIcons=安装程序已在您的电脑中安装了 [name]。 263 | FinishedLabel=安装程序已在您的电脑中安装了 [name]。您可以通过已安装的快捷方式运行此应用程序。 264 | ClickFinish=点击“完成”退出安装程序。 265 | FinishedRestartLabel=为完成 [name] 的安装,安装程序必须重新启动您的电脑。要立即重启吗? 266 | FinishedRestartMessage=为完成 [name] 的安装,安装程序必须重新启动您的电脑。%n%n要立即重启吗? 267 | ShowReadmeCheck=是,我想查阅自述文件 268 | YesRadio=是,立即重启电脑(&Y) 269 | NoRadio=否,稍后重启电脑(&N) 270 | ; used for example as 'Run MyProg.exe' 271 | RunEntryExec=运行 %1 272 | ; used for example as 'View Readme.txt' 273 | RunEntryShellExec=查阅 %1 274 | 275 | ; *** “安装程序需要下一张磁盘”提示 276 | ChangeDiskTitle=安装程序需要下一张磁盘 277 | SelectDiskLabel2=请插入磁盘 %1 并点击“确定”。%n%n如果这个磁盘中的文件可以在下列文件夹之外的文件夹中找到,请输入正确的路径或点击“浏览”。 278 | PathLabel=路径(&P): 279 | FileNotInDir2=“%2”中找不到文件“%1”。请插入正确的磁盘或选择其他文件夹。 280 | SelectDirectoryLabel=请指定下一张磁盘的位置。 281 | 282 | ; *** 安装状态消息 283 | SetupAborted=安装程序未完成安装。%n%n请修正这个问题并重新运行安装程序。 284 | AbortRetryIgnoreSelectAction=选择操作 285 | AbortRetryIgnoreRetry=重试(&T) 286 | AbortRetryIgnoreIgnore=忽略错误并继续(&I) 287 | AbortRetryIgnoreCancel=关闭安装程序 288 | 289 | ; *** 安装状态消息 290 | StatusClosingApplications=正在关闭应用程序... 291 | StatusCreateDirs=正在创建目录... 292 | StatusExtractFiles=正在解压缩文件... 293 | StatusCreateIcons=正在创建快捷方式... 294 | StatusCreateIniEntries=正在创建 INI 条目... 295 | StatusCreateRegistryEntries=正在创建注册表条目... 296 | StatusRegisterFiles=正在注册文件... 297 | StatusSavingUninstall=正在保存卸载信息... 298 | StatusRunProgram=正在完成安装... 299 | StatusRestartingApplications=正在重启应用程序... 300 | StatusRollback=正在撤销更改... 301 | 302 | ; *** 其他错误 303 | ErrorInternal2=内部错误:%1 304 | ErrorFunctionFailedNoCode=%1 失败 305 | ErrorFunctionFailed=%1 失败;错误代码 %2 306 | ErrorFunctionFailedWithMessage=%1 失败;错误代码 %2.%n%3 307 | ErrorExecutingProgram=无法执行文件:%n%1 308 | 309 | ; *** 注册表错误 310 | ErrorRegOpenKey=打开注册表项时出错:%n%1\%2 311 | ErrorRegCreateKey=创建注册表项时出错:%n%1\%2 312 | ErrorRegWriteKey=写入注册表项时出错:%n%1\%2 313 | 314 | ; *** INI 错误 315 | ErrorIniEntry=在文件“%1”中创建 INI 条目时出错。 316 | 317 | ; *** 文件复制错误 318 | FileAbortRetryIgnoreSkipNotRecommended=跳过此文件(&S) (不推荐) 319 | FileAbortRetryIgnoreIgnoreNotRecommended=忽略错误并继续(&I) (不推荐) 320 | SourceIsCorrupted=源文件已损坏 321 | SourceDoesntExist=源文件“%1”不存在 322 | ExistingFileReadOnly2=无法替换现有文件,它是只读的。 323 | ExistingFileReadOnlyRetry=移除只读属性并重试(&R) 324 | ExistingFileReadOnlyKeepExisting=保留现有文件(&K) 325 | ErrorReadingExistingDest=尝试读取现有文件时出错: 326 | FileExistsSelectAction=选择操作 327 | FileExists2=文件已经存在。 328 | FileExistsOverwriteExisting=覆盖已存在的文件(&O) 329 | FileExistsKeepExisting=保留现有的文件(&K) 330 | FileExistsOverwriteOrKeepAll=为所有冲突文件执行此操作(&D) 331 | ExistingFileNewerSelectAction=选择操作 332 | ExistingFileNewer2=现有的文件比安装程序将要安装的文件还要新。 333 | ExistingFileNewerOverwriteExisting=覆盖已存在的文件(&O) 334 | ExistingFileNewerKeepExisting=保留现有的文件(&K) (推荐) 335 | ExistingFileNewerOverwriteOrKeepAll=为所有冲突文件执行此操作(&D) 336 | ErrorChangingAttr=尝试更改下列现有文件的属性时出错: 337 | ErrorCreatingTemp=尝试在目标目录创建文件时出错: 338 | ErrorReadingSource=尝试读取下列源文件时出错: 339 | ErrorCopying=尝试复制下列文件时出错: 340 | ErrorReplacingExistingFile=尝试替换现有文件时出错: 341 | ErrorRestartReplace=重启并替换失败: 342 | ErrorRenamingTemp=尝试重命名下列目标目录中的一个文件时出错: 343 | ErrorRegisterServer=无法注册 DLL/OCX:%1 344 | ErrorRegSvr32Failed=RegSvr32 失败;退出代码 %1 345 | ErrorRegisterTypeLib=无法注册类库:%1 346 | 347 | ; *** 卸载显示名字标记 348 | ; used for example as 'My Program (32-bit)' 349 | UninstallDisplayNameMark=%1 (%2) 350 | ; used for example as 'My Program (32-bit, All users)' 351 | UninstallDisplayNameMarks=%1 (%2, %3) 352 | UninstallDisplayNameMark32Bit=32 位 353 | UninstallDisplayNameMark64Bit=64 位 354 | UninstallDisplayNameMarkAllUsers=所有用户 355 | UninstallDisplayNameMarkCurrentUser=当前用户 356 | 357 | ; *** 安装后错误 358 | ErrorOpeningReadme=尝试打开自述文件时出错。 359 | ErrorRestartingComputer=安装程序无法重启电脑,请手动重启。 360 | 361 | ; *** 卸载消息 362 | UninstallNotFound=文件“%1”不存在。无法卸载。 363 | UninstallOpenError=文件“%1”不能被打开。无法卸载。 364 | UninstallUnsupportedVer=此版本的卸载程序无法识别卸载日志文件“%1”的格式。无法卸载 365 | UninstallUnknownEntry=卸载日志中遇到一个未知条目 (%1) 366 | ConfirmUninstall=您确认要完全移除 %1 及其所有组件吗? 367 | UninstallOnlyOnWin64=仅允许在 64 位 Windows 中卸载此程序。 368 | OnlyAdminCanUninstall=仅使用管理员权限的用户能完成此卸载。 369 | UninstallStatusLabel=正在从您的电脑中移除 %1,请稍候。 370 | UninstalledAll=已顺利从您的电脑中移除 %1。 371 | UninstalledMost=%1 卸载完成。%n%n有部分内容未能被删除,但您可以手动删除它们。 372 | UninstalledAndNeedsRestart=为完成 %1 的卸载,需要重启您的电脑。%n%n立即重启电脑吗? 373 | UninstallDataCorrupted=文件“%1”已损坏。无法卸载 374 | 375 | ; *** 卸载状态消息 376 | ConfirmDeleteSharedFileTitle=删除共享的文件吗? 377 | ConfirmDeleteSharedFile2=系统表示下列共享的文件已不有其他程序使用。您希望卸载程序删除这些共享的文件吗?%n%n如果删除这些文件,但仍有程序在使用这些文件,则这些程序可能出现异常。如果您不能确定,请选择“否”,在系统中保留这些文件以免引发问题。 378 | SharedFileNameLabel=文件名: 379 | SharedFileLocationLabel=位置: 380 | WizardUninstalling=卸载状态 381 | StatusUninstalling=正在卸载 %1... 382 | 383 | ; *** Shutdown block reasons 384 | ShutdownBlockReasonInstallingApp=正在安装 %1。 385 | ShutdownBlockReasonUninstallingApp=正在卸载 %1。 386 | 387 | ; The custom messages below aren't used by Setup itself, but if you make 388 | ; use of them in your scripts, you'll want to translate them. 389 | 390 | [CustomMessages] 391 | 392 | NameAndVersion=%1 版本 %2 393 | AdditionalIcons=附加快捷方式: 394 | CreateDesktopIcon=创建桌面快捷方式(&D) 395 | CreateQuickLaunchIcon=创建快速启动栏快捷方式(&Q) 396 | ProgramOnTheWeb=%1 网站 397 | UninstallProgram=卸载 %1 398 | LaunchProgram=运行 %1 399 | AssocFileExtension=将 %2 文件扩展名与 %1 建立关联(&A) 400 | AssocingFileExtension=正在将 %2 文件扩展名与 %1 建立关联... 401 | AutoStartProgramGroupDescription=启动: 402 | AutoStartProgram=自动启动 %1 403 | AddonHostProgramNotFound=您选择的文件夹中无法找到 %1。%n%n您要继续吗? -------------------------------------------------------------------------------- /resources/docs/MAA_config_info.txt: -------------------------------------------------------------------------------- 1 | #主界面 2 | "MainFunction.PostActions": "8" #完成后退出MAA 3 | "MainFunction.PostActions": "9" #完成后退出MAA和游戏 4 | "MainFunction.PostActions": "12" #完成后退出MAA和模拟器 5 | "TaskQueue.WakeUp.IsChecked": "True" #开始唤醒 6 | "TaskQueue.Recruiting.IsChecked": "True" #自动公招 7 | "TaskQueue.Base.IsChecked": "True" #基建换班 8 | "TaskQueue.Combat.IsChecked": "True" #刷理智 9 | "TaskQueue.Mall.IsChecked": "True" #获取信用及购物 10 | "TaskQueue.Mission.IsChecked": "True" #领取奖励 11 | "TaskQueue.AutoRoguelike.IsChecked": "False" #自动肉鸽 12 | "TaskQueue.Reclamation.IsChecked": "False" #生息演算 13 | "TaskQueue.Order.WakeUp": "0" 14 | "TaskQueue.Order.Recruiting": "1" 15 | "TaskQueue.Order.Base": "2" 16 | "TaskQueue.Order.Combat": "3" 17 | "TaskQueue.Order.Mall": "4" 18 | "TaskQueue.Order.Mission": "5" 19 | "TaskQueue.Order.AutoRoguelike": "6" 20 | "TaskQueue.Order.Reclamation": "7" 21 | #刷理智 22 | "MainFunction.UseMedicine": "True" #吃理智药 23 | "MainFunction.UseMedicine.Quantity": "999" #吃理智药数量 24 | "MainFunction.Stage1": "" #主关卡 25 | "MainFunction.Stage2": "" #备选关卡1 26 | "MainFunction.Stage3": "" #备选关卡2 27 | "Fight.RemainingSanityStage": "Annihilation" #剩余理智关卡 28 | "MainFunction.Series.Quantity": "1" #连战次数 29 | "Penguin.IsDrGrandet": "True" #博朗台模式 30 | "GUI.CustomStageCode": "False" #手动输入关卡名 31 | "GUI.UseAlternateStage": "False" #使用备选关卡 32 | "Fight.UseRemainingSanityStage": "True" #使用剩余理智 33 | "GUI.AllowUseStoneSave": "False" #允许吃源石保持状态 34 | "Fight.UseExpiringMedicine": "False" #无限吃48小时内过期的理智药 35 | "GUI.HideUnavailableStage": "False" #隐藏当日不开放关卡 36 | "GUI.HideSeries": "False" #隐藏连战次数 37 | "Infrast.CustomInfrastPlanShowInFightSettings": "False" #显示基建计划 38 | "Penguin.EnablePenguin": "True" #上报企鹅物流 39 | "Yituliu.EnableYituliu": "True" #上报一图流 40 | #基建换班 41 | "Infrast.InfrastMode": "Normal"、"Rotation"、"Custom" #基建模式 42 | "Infrast.CustomInfrastPlanIndex": "1" #自定义基建配置索引号 43 | "Infrast.DefaultInfrast": "user_defined" #内置配置 44 | "Infrast.IsCustomInfrastFileReadOnly": "False" #自定义基建配置文件只读 45 | "Infrast.CustomInfrastFile": "" #自定义基建配置文件地址 46 | #设置 47 | "Start.ClientType": "Bilibili"、 "Official" #服务器 48 | G"Timer.Timer1": "False" #时间设置1 49 | "Connect.AdbPath" #ADB路径 50 | "Connect.Address": "127.0.0.1:16448" #连接地址 51 | G"VersionUpdate.ScheduledUpdateCheck": "True" #定时检查更新 52 | G"VersionUpdate.AutoDownloadUpdatePackage": "True" #自动下载更新包 53 | G"VersionUpdate.AutoInstallUpdatePackage": "True" #自动安装更新包 54 | G"Start.MinimizeDirectly": "True" #启动MAA后直接最小化 55 | "Start.RunDirectly": "True" #启动MAA后直接运行 56 | "Start.OpenEmulatorAfterLaunch": "True" #启动MAA后自动开启模拟器 57 | G"GUI.UseTray": "True" #显示托盘图标 58 | G"GUI.MinimizeToTray": "False" #最小化时隐藏至托盘 59 | "Start.EmulatorPath" #模拟器路径 60 | "Start.EmulatorAddCommand": "-v 2" #附加命令 61 | "Start.EmulatorWaitSeconds": "10" #等待模拟器启动时间 62 | G"VersionUpdate.package": "MirrorChyanAppv5.15.6.zip" #更新包标识 -------------------------------------------------------------------------------- /resources/icons/AUTO_MAA.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/icons/AUTO_MAA.ico -------------------------------------------------------------------------------- /resources/icons/AUTO_MAA_Updater.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/icons/AUTO_MAA_Updater.ico -------------------------------------------------------------------------------- /resources/icons/MirrorChyan.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/icons/MirrorChyan.ico -------------------------------------------------------------------------------- /resources/images/AUTO_MAA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/images/AUTO_MAA.png -------------------------------------------------------------------------------- /resources/images/Home/BannerDefault.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/images/Home/BannerDefault.png -------------------------------------------------------------------------------- /resources/images/README/payid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/images/README/payid.png -------------------------------------------------------------------------------- /resources/sounds/both/删除用户.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/both/删除用户.wav -------------------------------------------------------------------------------- /resources/sounds/both/删除脚本实例.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/both/删除脚本实例.wav -------------------------------------------------------------------------------- /resources/sounds/both/删除计划表.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/both/删除计划表.wav -------------------------------------------------------------------------------- /resources/sounds/both/删除调度队列.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/both/删除调度队列.wav -------------------------------------------------------------------------------- /resources/sounds/both/欢迎回来.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/both/欢迎回来.wav -------------------------------------------------------------------------------- /resources/sounds/both/添加用户.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/both/添加用户.wav -------------------------------------------------------------------------------- /resources/sounds/both/添加脚本实例.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/both/添加脚本实例.wav -------------------------------------------------------------------------------- /resources/sounds/both/添加计划表.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/both/添加计划表.wav -------------------------------------------------------------------------------- /resources/sounds/both/添加调度队列.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/both/添加调度队列.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/ADB失败.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/ADB失败.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/ADB成功.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/ADB成功.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/MAA在完成任务前中止.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/MAA在完成任务前中止.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/MAA在完成任务前退出.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/MAA在完成任务前退出.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/MAA更新.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/MAA更新.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/MAA未检测到任何模拟器.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/MAA未检测到任何模拟器.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/MAA未能正确登录PRTS.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/MAA未能正确登录PRTS.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/MAA的ADB连接异常.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/MAA的ADB连接异常.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/MAA进程超时.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/MAA进程超时.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/MAA部分任务执行失败.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/MAA部分任务执行失败.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/任务开始.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/任务开始.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/任务结束.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/任务结束.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/公告展示.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/公告展示.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/公告通知.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/公告通知.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/六星喜报.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/六星喜报.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/历史记录查询.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/历史记录查询.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/发生异常.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/发生异常.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/发生错误.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/发生错误.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/子任务失败.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/子任务失败.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/排查录入.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/排查录入.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/排查重试.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/排查重试.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/无新版本.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/无新版本.wav -------------------------------------------------------------------------------- /resources/sounds/noisy/有新版本.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/noisy/有新版本.wav -------------------------------------------------------------------------------- /resources/sounds/simple/任务开始.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/simple/任务开始.wav -------------------------------------------------------------------------------- /resources/sounds/simple/任务结束.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/simple/任务结束.wav -------------------------------------------------------------------------------- /resources/sounds/simple/公告展示.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/simple/公告展示.wav -------------------------------------------------------------------------------- /resources/sounds/simple/公告通知.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/simple/公告通知.wav -------------------------------------------------------------------------------- /resources/sounds/simple/历史记录查询.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/simple/历史记录查询.wav -------------------------------------------------------------------------------- /resources/sounds/simple/发生异常.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/simple/发生异常.wav -------------------------------------------------------------------------------- /resources/sounds/simple/发生错误.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/simple/发生错误.wav -------------------------------------------------------------------------------- /resources/sounds/simple/无新版本.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/simple/无新版本.wav -------------------------------------------------------------------------------- /resources/sounds/simple/有新版本.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DLmaster361/AUTO_MAA/21e7df7c3e480d0d714b1d09d6c6f27dafec6cb9/resources/sounds/simple/有新版本.wav -------------------------------------------------------------------------------- /resources/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "main_version": "4.3.9.0", 3 | "version_info": { 4 | "4.3.9.0": { 5 | "修复bug": [ 6 | "修复网络模块子线程未及时销毁导致的程序崩溃" 7 | ] 8 | }, 9 | "4.3.9.2": { 10 | "修复bug": [ 11 | "修复语音包禁忌二重奏" 12 | ] 13 | }, 14 | "4.3.9.1": { 15 | "新增功能": [ 16 | "语音功能上线" 17 | ], 18 | "修复bug": [ 19 | "网络模块支持并发请求", 20 | "修复中止任务时程序异常卡顿" 21 | ], 22 | "程序优化": [ 23 | "非UI组件转为QObject类" 24 | ] 25 | } 26 | } 27 | } --------------------------------------------------------------------------------