├── .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 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 | 
89 |
90 | 感谢 [AoXuan (@ClozyA)](https://github.com/ClozyA) 为本项目提供的下载服务器
91 |
92 | ## Star History
93 |
94 | [](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 | 
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 | }
--------------------------------------------------------------------------------