Compare commits

...

73 Commits

Author SHA1 Message Date
DLmaster361
4cbd921ab6 Merge branch 'dev' 2025-07-15 18:16:23 +08:00
DLmaster361
2c915161d5 Merge commit '75b06ca770ebcee68bf05ab95f069c6e8fd0cae8' into dev 2025-07-15 18:15:48 +08:00
DLmaster361
75b06ca770 feat(general): 通用配置模式接入日志系统 2025-07-15 18:15:26 +08:00
DLmaster361
c3468a3387 Merge branch 'generic_dev' into dev 2025-07-15 16:20:26 +08:00
DLmaster361
a2f4adb647 fix: 适配 MAA 任务及基建设施日志翻译 2025-07-15 16:20:09 +08:00
DLmaster361
403f69df8b refactor(ui): 重构历史记录保存与载入逻辑 2025-07-15 16:08:11 +08:00
DLmaster361
12cf10f97a fix((ui): 修复MAA用户仪表盘备选1字段错误 2025-07-14 19:12:59 +08:00
DLmaster361
6084befe2c Merge branch 'dev' into generic_dev 2025-07-13 23:25:02 +08:00
DLmaster361
1aa99ea613 Merge branch 'network_dev' into dev 2025-07-13 23:00:59 +08:00
DLmaster361
d539c0f808 fix(core): 信任系统证书,并添加网络代理地址配置项 #50 2025-07-13 22:50:04 +08:00
DLmaster361
bc509806fb Merge branch 'generic_dev' into dev 2025-07-12 23:11:35 +08:00
DLmaster361
c52820550f feat(ui): 添加导入导出通用配置功能 2025-07-12 21:54:40 +08:00
DLmaster361
98b30f90a1 Merge commit '4efbafc174504bd2fbd5b22075e3b5429406691b' into generic_dev 2025-07-12 19:41:10 +08:00
DLmaster361
4efbafc174 fix(system): 修复开机自启相关功能 2025-07-12 16:08:07 +08:00
DLmaster361
6d3fda50d3 feat(security): 添加重置管理密钥功能 2025-07-12 01:27:27 +08:00
DLmaster361
70b936012f Merge commit '54917fbe6de3ca5ac4a07bc88cdf0178a23f88dc' into dev 2025-07-11 18:42:40 +08:00
DLmaster361
54917fbe6d fix(general): 修复无成功日志时的脚本判定逻辑 2025-07-11 18:38:58 +08:00
DLmaster361
abeb9f054d fix(maa): 适配 MAA 备选关卡字段修改 2025-07-11 14:48:23 +08:00
DLmaster361
c6d6c5fb2a Merge branch 'generic_dev' into dev 2025-07-10 17:35:22 +08:00
DLmaster361
5b0d7f0012 feat(ui): 修改版本号到v4.4
- 进一步适配三月七相关配置项
2025-07-10 17:34:41 +08:00
DLmaster361
d9043aab0a fix(ui): 适配 Mirror 酱 平台下载策略调整 2025-07-10 13:29:17 +08:00
DLmaster361
5c6a20be4e Merge branch 'generic_dev' into dev 2025-07-10 02:30:02 +08:00
DLmaster361
1c0a65957d feat(core): 初步完成通用调度模块 2025-07-10 02:29:08 +08:00
DLmaster361
7c315624b1 Merge commit '0572caa528deb72bc97c42e0b169292823b1e8a8' into dev 2025-06-17 19:55:55 +08:00
DLmaster361
0572caa528 fix: 固定certifi版本号 2025-06-17 03:18:55 +08:00
DLmaster361
4233040585 fix: 尝试固定certifi版本号 2025-06-17 02:46:27 +08:00
DLmaster361
c27dc8e380 fix: 移除无用依赖项 2025-06-17 02:00:32 +08:00
DLmaster361
e746756e56 ci: 临时固定Nuitka打包版本号 2025-06-17 01:14:29 +08:00
DLmaster361
1829d1cd0b fix(ui): 修复删除计划表引发的错误 2025-06-17 00:34:07 +08:00
DLmaster361
fb979e5639 docs: 补充代码签名策略 2025-06-16 21:38:25 +08:00
DLmaster361
e7d0a85ad5 ci: 修正project-slug 2025-06-16 21:24:05 +08:00
DLmaster361
a384711327 Merge branch 'dev' 2025-06-16 14:33:49 +08:00
DLmaster361
3fd4778a48 feat(maa): 适配 MAA 无Default配置情况 2025-06-16 14:26:29 +08:00
DLmaster361
4841dc09b3 refactor(res): 更新软件主页用图 2025-06-14 17:20:03 +08:00
DLmaster361
b3aa4fc776 fix(maa): 修复森空岛签到信息特定情况下不弹出通知的问题 2025-06-13 10:02:43 +08:00
DLmaster361
a9b3b8b6f4 Merge branch 'Clozy_dev' into dev 2025-06-11 23:05:59 +08:00
DLmaster361
56ef196695 refactor(models/services): 简化测试用图 2025-06-11 23:05:46 +08:00
242238d341 refactor(models/services): 优化代码结构和可读性
-移除了不必要的变量声明
-简化了图像路径的构建方式
- 统一了代码格式
2025-06-11 22:51:26 +08:00
f66f6d38fe feat(notification): 用户单独通知六星喜报 2025-06-11 22:31:51 +08:00
d58077f58b fix(app): 修复企业微信机器人图片推送异常
- 移除了不必要的变量 final_image_path
-直接使用 image_path 进行图片存在性检查
- 更新了图片 base64 和 md5计算的逻辑
2025-06-11 22:26:44 +08:00
4d4d6dbedf refactor(app): 重构图片压缩功能并优化类型注解
- 使用 Path 对象替换字符串表示文件路径,提高代码可读性和功能
- 优化类型注解,包括函数参数和返回值- 重构 ImageUtils.compress_image_if_needed 方法,简化逻辑并提高性能
- 更新 notification.py 中使用该方法的代码,直接返回压缩后的 Path 对象
2025-06-11 22:14:09 +08:00
f60b276916 refactor(notify): 重构企业微信群机器人图片推送功能
-将图片压缩、存在性检查、Base64编码和MD5计算移至 CompanyWebHookBotPushImage 方法内部
- 优化错误处理和日志记录
- 简化调用接口,提高代码可读性和维护性
2025-06-11 20:54:45 +08:00
87857fd499 feat(notification): 新增图片压缩处理
- 新增 ImageUtils.compress_image_if_needed 方法,用于压缩图片大小
- 在 MAA.py 和 notification.py 中集成图片压缩功能
- 添加对不同图片格式(JPEG、PNG)的压缩支持
- 优化图片路径处理,确保压缩后图片正确发送
- 更新 requirements.txt,添加 pillow 依赖
2025-06-11 19:50:58 +08:00
3c371cd079 feat(notification): 企业微信群机器人支持图片推送
- 新增 ImageUtils 类,提供图像处理相关工具方法
- 在 MAA.py 中集成 ImageUtils,用于获取和处理通知图片
- 在 notification.py 中实现 CompanyWebHookBotPushImage 方法,支持企业微信群机器人推送图片
- 修改测试通知方法,增加图片推送测试
2025-06-11 17:36:11 +08:00
DLmaster361
428b849bcc fix(maa): 静默模式控制时段延长至模拟器完成启动的10s后 2025-06-11 01:13:57 +08:00
DLmaster361
85f3b4f607 Merge branch 'dev' 2025-06-10 18:49:00 +08:00
DLmaster361
916396f855 refactor: 使用 keyboard 模块替代 pyautogui 模块 2025-06-10 18:48:41 +08:00
DLmaster361
211c8d2b04 Merge branch 'dev' 2025-06-10 14:10:01 +08:00
DLmaster361
92e274d3fd ci: 移除pyscreeze 2025-06-10 14:09:40 +08:00
DLmaster361
d511ea48d5 Merge branch 'main' into dev 2025-06-09 23:45:03 +08:00
DLmaster361
1aa4da1adf feat: 支持使用命令行调用 2025-06-09 23:43:36 +08:00
DLmaster361
0e8b6b0b6b feat: 添加用户守则 2025-06-08 23:41:18 +08:00
DLmaster361
1a2c1b976f fix(maa): 更新动作执行后移除相应标记 2025-06-07 15:59:06 +08:00
DLmaster361
1cc242fa51 feat: 优化下载器测速中止条件 2025-06-06 22:38:37 +08:00
DLmaster361
18dfdba15d ci: 测试完整工作流 2025-06-06 00:09:01 +08:00
DLmaster361
b04ac4eec6 ci: 使用预设配置 2025-06-05 23:50:56 +08:00
DLmaster361
c009f0c891 ci: 测试证书注册 2025-06-05 23:39:09 +08:00
DLmaster361
d2dc0bd295 ci: 上传测试之二 2025-06-05 23:28:15 +08:00
DLmaster361
ddbb5b7f19 ci: 名字作出区分 2025-06-05 23:25:48 +08:00
DLmaster361
954c25090b ci: 测试上传工作 2025-06-05 23:25:08 +08:00
DLmaster361
0b6cc59de1 ci: 构建部分测试流程 2025-06-05 22:38:24 +08:00
DLmaster361
2271b5741d ci: 移除不支持的参数 2025-06-05 22:08:41 +08:00
DLmaster361
8a438b041f ci: 修正signpath证书参数 2025-06-05 21:09:03 +08:00
DLmaster361
dd92fcc4d8 ci: 添加证书测试工作流 2025-06-05 20:02:17 +08:00
DLmaster361
8f66ca0e16 Merge branch 'dev' 2025-06-05 19:10:49 +08:00
DLmaster361
895ba1d24a feat(res): 公招喜报模板优化 2025-06-04 11:21:34 +08:00
e49b807bef feat(ui): 完善森空岛签到功能的提示信息 2025-06-02 14:26:25 +08:00
DLmaster361
73c15b5e93 refactor(skland): 森空岛签到功能拆分独立 2025-06-02 14:05:31 +08:00
DLmaster361
e505ea8c51 feat(maa): 森空岛签到功能上线 2025-06-02 02:35:01 +08:00
DLmaster361
21e7df7c3e Merge branch 'dev' 2025-06-01 20:04:16 +08:00
DLmaster361
2d72ca66a4 fix(core): 修复网络模块子线程未及时销毁导致的程序崩溃 2025-06-01 19:52:22 +08:00
DLmaster361
4725a30165 fix(ui): 修复语音包禁忌二重奏 2025-06-01 04:31:29 +08:00
DLmaster361
f3c977f1b3 Merge branch 'main' into dev 2025-06-01 03:18:39 +08:00
46 changed files with 5284 additions and 1096 deletions

View File

@@ -28,9 +28,11 @@ permissions:
actions: write
jobs:
pre_check:
name: Pre Checks
runs-on: ubuntu-latest
steps:
- name: Repo Check
id: repo_check
@@ -40,67 +42,199 @@ jobs:
exit 1
fi
exit 0
build_AUTO_MAA:
runs-on: windows-latest
needs: pre_check
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python 3.12
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
pip install -r requirements.txt
choco install innosetup
echo "C:\Program Files (x86)\Inno Setup 6" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Package
id: package
- name: Get version
id: get_version
run: |
copy app\utils\package.py .\
python package.py
- name: Read version
id: read_version
$version = (Get-Content resources/version.json | ConvertFrom-Json).main_version
echo "main_version=$version" >> $env:GITHUB_OUTPUT
- name: Nuitka build main program
uses: Nuitka/Nuitka-Action@main
with:
script-name: main.py
mode: app
enable-plugins: pyside6
onefile-tempdir-spec: "{TEMP}/AUTO_MAA"
windows-console-mode: attach
windows-icon-from-ico: resources/icons/AUTO_MAA.ico
windows-uac-admin: true
company-name: AUTO_MAA Team
product-name: AUTO_MAA
file-version: ${{ steps.get_version.outputs.main_version }}
product-version: ${{ steps.get_version.outputs.main_version }}
file-description: AUTO_MAA Component
copyright: Copyright © 2024-2025 DLmaster361
assume-yes-for-downloads: true
output-file: AUTO_MAA
output-dir: AUTO_MAA
- name: Upload unsigned main program
id: upload-unsigned-main-program
uses: actions/upload-artifact@v4
with:
name: AUTO_MAA
path: AUTO_MAA/AUTO_MAA.exe
- name: Sign main program
id: sign_main_program
uses: signpath/github-action-submit-signing-request@v1.2
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '787a1d5f-6177-4f30-9559-d2646473584a'
project-slug: 'AUTO_MAA'
signing-policy-slug: 'release-signing'
artifact-configuration-slug: "AUTO_MAA"
github-artifact-id: '${{ steps.upload-unsigned-main-program.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: 'AUTO_MAA'
- name: Add other resources
shell: pwsh
run: |
$MAIN_VERSION=(Get-Content -Path "version_info.txt" -TotalCount 1).Trim()
"AUTO_MAA_version=$MAIN_VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append
$root = "${{ github.workspace }}"
$ver = "${{ steps.get_version.outputs.main_version }}"
Copy-Item "$root/app" "$root/AUTO_MAA/app" -Recurse
Copy-Item "$root/resources" "$root/AUTO_MAA/resources" -Recurse
Copy-Item "$root/main.py" "$root/AUTO_MAA/"
Copy-Item "$root/requirements.txt" "$root/AUTO_MAA/"
Copy-Item "$root/README.md" "$root/AUTO_MAA/"
Copy-Item "$root/LICENSE" "$root/AUTO_MAA/"
- name: Create Inno Setup script
shell: pwsh
run: |
$root = "${{ github.workspace }}"
$ver = "${{ steps.get_version.outputs.main_version }}"
$iss = Get-Content "$root/app/utils/AUTO_MAA.iss" -Raw
$iss = $iss -replace '#define MyAppVersion ""', "#define MyAppVersion `"$ver`""
$iss = $iss -replace '#define MyAppPath ""', "#define MyAppPath `"$root/AUTO_MAA`""
$iss = $iss -replace '#define OutputDir ""', "#define OutputDir `"$root`""
Set-Content -Path "$root/AUTO_MAA.iss" -Value $iss
- name: Build setup program
uses: Minionguyjpro/Inno-Setup-Action@v1.2.5
with:
path: AUTO_MAA.iss
- name: Upload unsigned setup program
id: upload-unsigned-setup-program
uses: actions/upload-artifact@v4
with:
name: AUTO_MAA-Setup
path: AUTO_MAA-Setup.exe
- name: Sign setup program
id: sign_setup_program
uses: signpath/github-action-submit-signing-request@v1.2
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '787a1d5f-6177-4f30-9559-d2646473584a'
project-slug: 'AUTO_MAA'
signing-policy-slug: 'release-signing'
artifact-configuration-slug: "AUTO_MAA-Setup"
github-artifact-id: '${{ steps.upload-unsigned-setup-program.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: 'AUTO_MAA_Setup'
- name: Compress setup exe
shell: pwsh
run: Compress-Archive -Path AUTO_MAA_Setup/* -DestinationPath AUTO_MAA_${{ steps.get_version.outputs.main_version }}.zip
- name: Generate version info
shell: python
run: |
import json
from pathlib import Path
def version_text(version_numb):
while len(version_numb) < 4:
version_numb.append(0)
if version_numb[3] == 0:
return f"v{'.'.join(str(_) for _ in version_numb[0:3])}"
else:
return f"v{'.'.join(str(_) for _ in version_numb[0:3])}-beta.{version_numb[3]}"
def version_info_markdown(info):
version_info = ""
for key, value in info.items():
version_info += f"## {key}\n"
for v in value:
version_info += f"- {v}\n"
return version_info
root_path = Path(".")
version = json.loads((root_path / "resources/version.json").read_text(encoding="utf-8"))
main_version_numb = list(map(int, version["main_version"].split(".")))
all_version_info = {}
for v_i in version["version_info"].values():
for key, value in v_i.items():
if key in all_version_info:
all_version_info[key] += value.copy()
else:
all_version_info[key] = value.copy()
(root_path / "version_info.txt").write_text(
f"{version_text(main_version_numb)}\n\n<!--{json.dumps(version['version_info'], ensure_ascii=False)}-->\n{version_info_markdown(all_version_info)}",
encoding="utf-8",
)
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: AUTO_MAA_${{ env.AUTO_MAA_version }}
path: AUTO_MAA_${{ env.AUTO_MAA_version }}.zip
name: AUTO_MAA_${{ steps.get_version.outputs.main_version }}
path: AUTO_MAA_${{ steps.get_version.outputs.main_version }}.zip
- name: Upload Version_Info Artifact
uses: actions/upload-artifact@v4
with:
name: version_info
path: version_info.txt
publish_release:
name: Publish release
needs: build_AUTO_MAA
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
pattern: AUTO_MAA_*
merge-multiple: true
path: artifacts
- name: Download Version_Info
uses: actions/download-artifact@v4
with:
name: version_info
path: ./
- name: Create release
id: create_release
run: |
@@ -110,8 +244,20 @@ jobs:
TAGNAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
NOTES_MAIN="$(sed 's/\r$//g' <(tail -n +3 version_info.txt))"
NOTES="$NOTES_MAIN
[已有 Mirror酱 CDK ?前往 Mirror酱 高速下载](https://mirrorchyan.com/zh/projects?rid=AUTO_MAA)
## 代码签名策略Code signing policy
Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
- 审批人Approvers: [DLmaster (@DLmaster361)](https://github.com/DLmaster361)
## 隐私政策Privacy policy
除非用户、安装者或使用者特别要求,否则本程序不会将任何信息传输到其他网络系统。
This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it.
[已有 Mirror酱 CDK ?前往 Mirror酱 高速下载](https://mirrorchyan.com/zh/projects?rid=AUTO_MAA&source=auto_maa-release)
\`\`\`本release通过GitHub Actions自动构建\`\`\`"
if [ "${{ github.ref_name }}" == "main" ]; then
@@ -122,6 +268,7 @@ jobs:
gh release create "$TAGNAME" --target "main" --title "$NAME" --notes "$NOTES" $PRERELEASE_FLAG artifacts/*
env:
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
- name: Trigger MirrorChyanUploading
run: |
gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ config/
data/
debug/
history/
script/
resources/notice.json
resources/theme_image.json
resources/images/Home/BannerTheme.jpg

View File

@@ -14,7 +14,7 @@
<a href="https://github.com/DLmaster361/AUTO_MAA/graphs/contributors"><img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/DLmaster361/AUTO_MAA?style=flat-square"></a>
<a href="https://github.com/DLmaster361/AUTO_MAA/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/DLmaster361/AUTO_MAA?style=flat-square"></a>
<a href="https://deepwiki.com/DLmaster361/AUTO_MAA"><img alt="DeepWiki" src="https://deepwiki.com/badge.svg"></a>
<a href="https://mirrorchyan.com/zh/projects?rid=AUTO_MAA"><img alt="mirrorc" src="https://img.shields.io/badge/Mirror%E9%85%B1-%239af3f6?logo=countingworkspro&logoColor=4f46e5"></a>
<a href="https://mirrorchyan.com/zh/projects?rid=AUTO_MAA&source=auto_maa-readme"><img alt="mirrorc" src="https://img.shields.io/badge/Mirror%E9%85%B1-%239af3f6?logo=countingworkspro&logoColor=4f46e5"></a>
</p>
## 软件介绍
@@ -52,13 +52,10 @@
- **传播:** AUTO_MAA原则上允许传播者自由传播本软件但无论在何种传播过程中不得删除项目作者与开发者所留版权声明不得隐瞒项目作者与相关开发者的存在。由于软件性质项目组不希望发现任何人在明日方舟官方媒体包括官方媒体账号与森空岛社区等或明日方舟游戏相关内容包括同好群、线下活动与游戏内容讨论等下提及AUTO_MAA或MAA希望各位理解。
- **衍生:** AUTO_MAA允许任何人对软件本体或软件部分代码进行二次开发或利用。但依据GPL相关成果再次分发时也必须使用GPL或兼容的协议开源。
- **贡献:** 不论是直接参与软件的维护编写或是撰写文档、测试、反馈BUG、给出建议、参与讨论都为AUTO_MAA项目的发展完善做出了不可忽视的贡献。项目组提倡各位贡献者遵照GitHub开源社区惯例发布Issues参与项目。避免私信或私发邮件安全性漏洞或敏感问题除外以帮助更多用户。
- **图像:** `AUTO_MAA主页默认图像` 并不适用开源协议,著作权归 [NARINpopo](https://space.bilibili.com/1877154) 画师所有,商业使用权归 [DLmaster (@DLmaster361)](https://github.com/DLmaster361) 所有,软件用户仅拥有非商业使用权。不得以开源协议已授权为由在未经授权的情况下使用 `AUTO_MAA主页默认图像`,不得在未经授权的情况下将 `AUTO_MAA主页默认图像` 用于任何商业用途。
以上细则是本项目对GPL的相关补充与强调。未提及的以GPL为准发生冲突的以本细则为准。如有不清楚的部分请发Issues询问。若发生纠纷相关内容也没有在Issues上提及的项目组拥有最终解释权。
**注意**
- 由于本软件有修改其它目录JSON文件等行为使用前请将AUTO_MAA添加入Windows Defender信任区以及防病毒软件的信任区或开发者目录避免被误杀。
---
# 使用方法
@@ -75,6 +72,24 @@
可在[《AUTO_MAA开发者协作文档》](https://docs.qq.com/aio/DQ3Z5eHNxdmxFQmZX)的`开发任务`页面中查看开发进度。
## 代码签名策略Code signing policy
Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
- 审批人Approvers: [DLmaster (@DLmaster361)](https://github.com/DLmaster361)
## 隐私政策Privacy policy
除非用户、安装者或使用者特别要求,否则本程序不会将任何信息传输到其他网络系统。
This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it.
## 特别鸣谢
- 下载服务器:由[AoXuan (@ClozyA)](https://github.com/ClozyA) 个人为项目赞助。
- EXE签名: 由 [SignPath.io](https://signpath.io/)提供免费代码签名,签名来自[SignPath Foundation](https://signpath.org/)。
## 贡献者
感谢以下贡献者对本项目做出的贡献
@@ -87,8 +102,6 @@
![Alt](https://repobeats.axiom.co/api/embed/6c2f834141eff1ac297db70d12bd11c6236a58a5.svg "Repobeats analytics image")
感谢 [AoXuan (@ClozyA)](https://github.com/ClozyA) 为本项目提供的下载服务器
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=DLmaster361/AUTO_MAA&type=Date)](https://star-history.com/#DLmaster361/AUTO_MAA&Date)

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA主程序包
v4.3
v4.4
作者DLmaster_361
"""

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA核心组件包
v4.3
v4.4
作者DLmaster_361
"""
@@ -29,7 +29,15 @@ __version__ = "4.2.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .config import QueueConfig, MaaConfig, MaaUserConfig, MaaPlanConfig, Config
from .config import (
QueueConfig,
MaaConfig,
MaaUserConfig,
MaaPlanConfig,
GeneralConfig,
GeneralSubConfig,
Config,
)
from .main_info_bar import MainInfoBar
from .network import Network
from .sound_player import SoundPlayer
@@ -42,6 +50,8 @@ __all__ = [
"MaaConfig",
"MaaUserConfig",
"MaaPlanConfig",
"GeneralConfig",
"GeneralSubConfig",
"MainInfoBar",
"Network",
"SoundPlayer",

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA信息通知栏
v4.3
v4.4
作者DLmaster_361
"""

View File

@@ -21,14 +21,16 @@
"""
AUTO_MAA
AUTO_MAA网络请求线程
v4.3
v4.4
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtCore import QObject, QThread, QEventLoop
import re
import time
import requests
import truststore
from pathlib import Path
@@ -42,16 +44,29 @@ class NetworkThread(QThread):
def __init__(self, mode: str, url: str, path: Path = None) -> None:
super().__init__()
self.setObjectName(
f"NetworkThread-{mode}-{re.sub(r'(&cdk=)[^&]+(&)', r'\1******\2', url)}"
)
self.mode = mode
self.url = url
self.path = path
from .config import Config
self.proxies = {
"http": Config.get(Config.update_ProxyAddress),
"https": Config.get(Config.update_ProxyAddress),
}
self.status_code = None
self.response_json = None
self.error_message = None
self.loop = QEventLoop()
truststore.inject_into_ssl()
@logger.catch
def run(self) -> None:
"""运行网络请求线程"""
@@ -68,7 +83,7 @@ class NetworkThread(QThread):
for _ in range(self.max_retries):
try:
response = requests.get(url, timeout=self.timeout)
response = requests.get(url, timeout=self.timeout, proxies=self.proxies)
self.status_code = response.status_code
self.response_json = response.json()
self.error_message = None
@@ -87,7 +102,7 @@ class NetworkThread(QThread):
response = None
try:
response = requests.get(url, timeout=10)
response = requests.get(url, timeout=10, proxies=self.proxies)
if response.status_code == 200:
with open(path, "wb") as file:
file.write(response.content)
@@ -125,13 +140,22 @@ class _Network(QObject):
def get_result(self, network_thread: NetworkThread) -> dict:
"""获取网络请求结果"""
self.task_queue.remove(network_thread)
return {
result = {
"status_code": network_thread.status_code,
"response_json": network_thread.response_json,
"error_message": network_thread.error_message,
"error_message": (
re.sub(r"(&cdk=)[^&]+(&)", r"\1******\2", network_thread.error_message)
if network_thread.error_message
else None
),
}
network_thread.quit()
network_thread.wait()
self.task_queue.remove(network_thread)
network_thread.deleteLater()
return result
Network = _Network()

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA音效播放器
v4.3
v4.4
作者DLmaster_361
"""
@@ -41,27 +41,26 @@ class _SoundPlayer(QObject):
self.sounds_path = Config.app_path / "resources/sounds"
def play(self, sound_name: str, parent: QObject = None):
def play(self, sound_name: str):
if not Config.get(Config.voice_Enabled):
return
if (self.sounds_path / f"both/{sound_name}.wav").exists():
self.play_voice(self.sounds_path / f"both/{sound_name}.wav", parent)
self.play_voice(self.sounds_path / f"both/{sound_name}.wav")
elif (
self.sounds_path / Config.get(Config.voice_Type) / f"{sound_name}.wav"
).exists():
self.play_voice(
self.sounds_path / Config.get(Config.voice_Type) / f"{sound_name}.wav",
parent,
self.sounds_path / Config.get(Config.voice_Type) / f"{sound_name}.wav"
)
def play_voice(self, sound_path: Path, parent: QObject):
def play_voice(self, sound_path: Path):
effect = QSoundEffect(self if parent is None else parent)
effect = QSoundEffect(self)
effect.setVolume(1)
effect.setSource(QUrl.fromLocalFile(sound_path))
effect.play()

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA业务调度器
v4.3
v4.4
作者DLmaster_361
"""
@@ -36,7 +36,7 @@ from .config import Config
from .main_info_bar import MainInfoBar
from .network import Network
from .sound_player import SoundPlayer
from app.models import MaaManager
from app.models import MaaManager, GeneralManager
from app.services import System
@@ -45,9 +45,11 @@ class Task(QThread):
check_maa_version = Signal(str)
push_info_bar = Signal(str, str, str, int)
play_sound = Signal(str)
question = Signal(str, str)
question_response = Signal(bool)
update_user_info = Signal(str, dict)
update_maa_user_info = Signal(str, dict)
update_general_sub_info = Signal(str, dict)
create_task_list = Signal(list)
create_user_list = Signal(list)
update_task_list = Signal(list)
@@ -60,6 +62,8 @@ class Task(QThread):
):
super(Task, self).__init__()
self.setObjectName(f"Task-{mode}-{name}")
self.mode = mode
self.name = name
self.info = info
@@ -83,9 +87,34 @@ class Task(QThread):
)
self.task.check_maa_version.connect(self.check_maa_version.emit)
self.task.push_info_bar.connect(self.push_info_bar.emit)
self.task.play_sound.connect(self.play_sound.emit)
self.task.accomplish.connect(lambda: self.accomplish.emit([]))
self.task.run()
try:
self.task.run()
except Exception as e:
logger.exception(f"任务异常:{self.name},错误信息:{e}")
self.push_info_bar.emit("error", "任务异常", self.name, -1)
elif self.mode == "设置通用脚本":
logger.info(f"任务开始:设置{self.name}")
self.push_info_bar.emit("info", "设置通用脚本", self.name, 3000)
self.task = GeneralManager(
self.mode,
Config.member_dict[self.name],
self.info["SetSubInfo"]["Path"],
)
self.task.push_info_bar.connect(self.push_info_bar.emit)
self.task.play_sound.connect(self.play_sound.emit)
self.task.accomplish.connect(lambda: self.accomplish.emit([]))
try:
self.task.run()
except Exception as e:
logger.exception(f"任务异常:{self.name},错误信息:{e}")
self.push_info_bar.emit("error", "任务异常", self.name, -1)
else:
@@ -93,11 +122,8 @@ class Task(QThread):
[
(
value
if Config.member_dict[value]["Config"].get(
Config.member_dict[value]["Config"].MaaSet_Name
)
== ""
else f"{value} - {Config.member_dict[value]["Config"].get(Config.member_dict[value]["Config"].MaaSet_Name)}"
if Config.member_dict[value]["Config"].get_name() == ""
else f"{value} - {Config.member_dict[value]["Config"].get_name()}"
),
"等待",
value,
@@ -142,27 +168,64 @@ class Task(QThread):
self.question_response.disconnect()
self.question_response.connect(self.task.question_response.emit)
self.task.push_info_bar.connect(self.push_info_bar.emit)
self.task.play_sound.connect(self.play_sound.emit)
self.task.create_user_list.connect(self.create_user_list.emit)
self.task.update_user_list.connect(self.update_user_list.emit)
self.task.update_log_text.connect(self.update_log_text.emit)
self.task.update_user_info.connect(self.update_user_info.emit)
self.task.update_user_info.connect(self.update_maa_user_info.emit)
self.task.accomplish.connect(
lambda log: self.task_accomplish(task[2], log)
)
elif Config.member_dict[task[2]]["Type"] == "General":
self.task = GeneralManager(
self.mode[0:4],
Config.member_dict[task[2]],
)
self.task.question.connect(self.question.emit)
self.question_response.disconnect()
self.question_response.connect(self.task.question_response.emit)
self.task.push_info_bar.connect(self.push_info_bar.emit)
self.task.play_sound.connect(self.play_sound.emit)
self.task.create_user_list.connect(self.create_user_list.emit)
self.task.update_user_list.connect(self.update_user_list.emit)
self.task.update_log_text.connect(self.update_log_text.emit)
self.task.update_sub_info.connect(self.update_general_sub_info.emit)
self.task.accomplish.connect(
lambda log: self.task_accomplish(task[2], log)
)
try:
self.task.run()
Config.running_list.remove(task[2])
task[1] = "完成"
self.update_task_list.emit(self.task_list)
logger.info(f"任务完成:{task[0]}")
self.push_info_bar.emit("info", "任务完成", task[0], 3000)
task[1] = "完成"
self.update_task_list.emit(self.task_list)
logger.info(f"任务完成:{task[0]}")
self.push_info_bar.emit("info", "任务完成", task[0], 3000)
except Exception as e:
self.task_accomplish(
task[2],
{
"Time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"History": f"任务异常,异常简报:{e}",
},
)
task[1] = "异常"
self.update_task_list.emit(self.task_list)
logger.exception(f"任务异常:{task[0]},错误信息:{e}")
self.push_info_bar.emit("error", "任务异常", task[0], -1)
Config.running_list.remove(task[2])
self.accomplish.emit(self.logs)
def task_accomplish(self, name: str, log: dict):
"""保存保存任务结果"""
"""保存任务结果"""
self.logs.append([name, log])
self.task.deleteLater()
@@ -201,7 +264,11 @@ class _TaskManager(QObject):
lambda title, content: self.push_dialog(name, title, content)
)
self.task_dict[name].push_info_bar.connect(MainInfoBar.push_info_bar)
self.task_dict[name].update_user_info.connect(Config.change_user_info)
self.task_dict[name].play_sound.connect(SoundPlayer.play)
self.task_dict[name].update_maa_user_info.connect(Config.change_maa_user_info)
self.task_dict[name].update_general_sub_info.connect(
Config.change_general_sub_info
)
self.task_dict[name].accomplish.connect(
lambda logs: self.remove_task(mode, name, logs)
)
@@ -277,6 +344,9 @@ class _TaskManager(QObject):
)
)
if Config.args.mode == "cli" and Config.power_sign == "NoAction":
Config.set_power_sign("KillSelf")
def check_maa_version(self, v: str):
"""检查MAA版本"""

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA主业务定时器
v4.3
v4.4
作者DLmaster_361
"""
@@ -29,7 +29,7 @@ from loguru import logger
from PySide6.QtCore import QObject, QTimer
from datetime import datetime
from pathlib import Path
import pyautogui
import keyboard
from .config import Config
from .task_manager import TaskManager
@@ -41,8 +41,6 @@ class _MainTimer(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self.if_FailSafeException = False
self.Timer = QTimer()
self.Timer.timeout.connect(self.timed_start)
self.Timer.timeout.connect(self.set_silence)
@@ -55,7 +53,7 @@ class _MainTimer(QObject):
def long_timed_task(self):
"""长时间定期检定任务"""
Config.get_gameid()
Config.get_stage()
Config.main_window.setting.show_notice()
if Config.get(Config.update_IfAutoUpdate):
Config.main_window.setting.check_update()
@@ -98,31 +96,21 @@ class _MainTimer(QObject):
windows = System.get_window_info()
# 排除雷电名为新通知的窗口
windows = [
window
for window in windows
if not (
window[0] == "新通知" and Path(window[1]) in Config.silence_list
)
]
# 此处排除雷电名为新通知的窗口
if any(
str(emulator_path) in window
str(emulator_path) in window and window[0] != "新通知"
for window in windows
for emulator_path in Config.silence_list
):
try:
pyautogui.hotkey(
*[
keyboard.press_and_release(
"+".join(
_.strip().lower()
for _ in Config.get(Config.function_BossKey).split("+")
]
)
)
except pyautogui.FailSafeException as e:
if not self.if_FailSafeException:
logger.warning(f"FailSafeException: {e}")
self.if_FailSafeException = True
except Exception as e:
logger.error(f"模拟按键时出错:{e}")
def check_power(self):

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
MAA功能组件
v4.3
v4.4
作者DLmaster_361
"""
@@ -30,16 +30,17 @@ from PySide6.QtCore import QObject, Signal, QEventLoop, QFileSystemWatcher, QTim
import json
import subprocess
import shutil
import time
import re
import win32com.client
from functools import partial
from datetime import datetime, timedelta
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
from typing import Union, List, Dict
from app.core import Config, MaaConfig, MaaUserConfig, SoundPlayer
from app.services import Notify, System
from app.core import Config, MaaConfig, MaaUserConfig
from app.services import Notify, Crypto, System, skland_sign_in
from app.utils import ProcessManager
class MaaManager(QObject):
@@ -50,14 +51,13 @@ class MaaManager(QObject):
question_response = Signal(bool)
update_user_info = Signal(str, dict)
push_info_bar = Signal(str, str, str, int)
play_sound = Signal(str)
create_user_list = Signal(list)
update_user_list = Signal(list)
update_log_text = Signal(str)
interrupt = Signal()
accomplish = Signal(dict)
isInterruptionRequested = False
def __init__(
self,
mode: str,
@@ -79,17 +79,25 @@ class MaaManager(QObject):
self.config_path = config["Path"]
self.user_config_path = user_config_path
self.emulator_process_manager = ProcessManager()
self.maa_process_manager = ProcessManager()
self.log_monitor = QFileSystemWatcher()
self.log_monitor_timer = QTimer()
self.log_monitor_timer.timeout.connect(self.refresh_maa_log)
self.monitor_loop = QEventLoop()
self.maa_process_manager.processClosed.connect(
lambda: self.log_monitor.fileChanged.emit("进程结束检查")
)
self.question_loop = QEventLoop()
self.question_response.connect(self.__capture_response)
self.question_response.connect(self.question_loop.quit)
self.wait_loop = QEventLoop()
self.isInterruptionRequested = False
self.interrupt.connect(self.quit_monitor)
self.maa_version = None
@@ -219,6 +227,59 @@ class MaaManager(QObject):
user_logs_list = []
user_start_time = datetime.now()
if user_data["Info"]["IfSkland"] and user_data["Info"]["SklandToken"]:
if user_data["Data"]["LastSklandDate"] != datetime.now().strftime(
"%Y-%m-%d"
):
self.update_log_text.emit("正在执行森空岛签到中\n请稍候~")
skland_result = skland_sign_in(
Crypto.win_decryptor(user_data["Info"]["SklandToken"])
)
for type, user_list in skland_result.items():
if type != "总计" and len(user_list) > 0:
logger.info(
f"{self.name} | 用户: {user[0]} - 森空岛签到{type}: {''.join(user_list)}"
)
self.push_info_bar.emit(
"info",
f"森空岛签到{type}",
"".join(user_list),
-1 if type == "失败" else 5000,
)
if skland_result["总计"] == 0:
self.push_info_bar.emit(
"info",
"森空岛签到失败",
user[0],
-1,
)
if (
skland_result["总计"] > 0
and len(skland_result["失败"]) == 0
):
user_data["Data"][
"LastSklandDate"
] = datetime.now().strftime("%Y-%m-%d")
self.play_sound.emit("森空岛签到成功")
else:
self.play_sound.emit("森空岛签到失败")
elif user_data["Info"]["IfSkland"]:
logger.warning(
f"{self.name} | 用户: {user[0]} - 未配置森空岛签到Token跳过森空岛签到"
)
self.push_info_bar.emit(
"warning", "森空岛签到失败", "未配置鹰角网络通行证登录凭证", -1
)
# 剿灭-日常模式循环
for mode in ["Annihilation", "Routine"]:
@@ -450,9 +511,8 @@ class MaaManager(QObject):
logger.info(
f"{self.name} | 启动模拟器:{self.emulator_path},参数:{self.emulator_arguments}"
)
self.emulator_process = subprocess.Popen(
[self.emulator_path, *self.emulator_arguments],
creationflags=subprocess.CREATE_NO_WINDOW,
self.emulator_process_manager.open_process(
self.emulator_path, self.emulator_arguments, 0
)
except Exception as e:
logger.error(f"{self.name} | 启动模拟器时出现异常:{e}")
@@ -471,10 +531,7 @@ class MaaManager(QObject):
self.search_ADB_address()
# 创建MAA任务
maa = subprocess.Popen(
[self.maa_exe_path],
creationflags=subprocess.CREATE_NO_WINDOW,
)
self.maa_process_manager.open_process(self.maa_exe_path, [], 10)
# 监测MAA运行状态
self.start_monitor(start_time, mode_book[mode])
@@ -524,11 +581,11 @@ class MaaManager(QObject):
f"{self.maa_result}\n正在中止相关程序\n请等待10s"
)
# 无命令行中止MAA与其子程序
self.maa_process_manager.kill(if_force=True)
System.kill_process(self.maa_exe_path)
# 中止模拟器进程
self.emulator_process.terminate()
self.emulator_process.wait()
self.emulator_process_manager.kill()
self.if_open_emulator = True
@@ -564,9 +621,9 @@ class MaaManager(QObject):
1,
)
if i == self.set["RunSet"]["RunTimesLimit"] - 1:
SoundPlayer.play("子任务失败", self)
self.play_sound.emit("子任务失败")
else:
SoundPlayer.play(self.maa_result, self)
self.play_sound.emit(self.maa_result)
self.sleep(10)
# 任务结束后释放ADB
@@ -589,8 +646,7 @@ class MaaManager(QObject):
)
# 任务结束后再次手动中止模拟器进程,防止退出不彻底
if self.if_kill_emulator:
self.emulator_process.terminate()
self.emulator_process.wait()
self.emulator_process_manager.kill()
self.if_open_emulator = True
# 记录剿灭情况
@@ -619,7 +675,7 @@ class MaaManager(QObject):
},
user_data,
)
SoundPlayer.play("六星喜报", self)
self.play_sound.emit("六星喜报")
# 执行MAA解压更新动作
if self.maa_update_package:
@@ -631,7 +687,7 @@ class MaaManager(QObject):
self.update_log_text.emit(
f"检测到MAA存在更新\nMAA正在执行更新动作\n请等待10s"
)
SoundPlayer.play("MAA更新", self)
self.play_sound.emit("MAA更新")
self.set_maa("更新MAA", None)
subprocess.Popen(
[self.maa_exe_path],
@@ -640,10 +696,12 @@ class MaaManager(QObject):
self.sleep(10)
System.kill_process(self.maa_exe_path)
self.maa_update_package = ""
logger.info(f"{self.name} | 更新动作结束")
# 发送统计信息
statistics = Config.merge_maa_logs("指定项", user_logs_list)
statistics = Config.merge_statistic_info(user_logs_list)
statistics["user_index"] = user[2]
statistics["user_info"] = user[0]
statistics["start_time"] = user_start_time.strftime("%Y-%m-%d %H:%M:%S")
@@ -720,10 +778,7 @@ class MaaManager(QObject):
# 记录当前时间
start_time = datetime.now()
# 创建MAA任务
maa = subprocess.Popen(
[self.maa_exe_path],
creationflags=subprocess.CREATE_NO_WINDOW,
)
self.maa_process_manager.open_process(self.maa_exe_path, [], 10)
# 监测MAA运行状态
self.start_monitor(start_time, "人工排查")
@@ -742,6 +797,7 @@ class MaaManager(QObject):
f"{self.maa_result}\n正在中止相关程序\n请等待10s"
)
# 无命令行中止MAA与其子程序
self.maa_process_manager.kill(if_force=True)
System.kill_process(self.maa_exe_path)
self.if_open_emulator = True
self.sleep(10)
@@ -752,7 +808,7 @@ class MaaManager(QObject):
# 登录失败,询问是否结束循环
elif not self.isInterruptionRequested:
SoundPlayer.play("排查重试", self)
self.play_sound.emit("排查重试")
if not self.push_question(
"操作提示", "MAA未能正确登录到PRTS是否重试"
):
@@ -761,7 +817,7 @@ class MaaManager(QObject):
# 登录成功,录入人工排查情况
if run_book[0] and not self.isInterruptionRequested:
SoundPlayer.play("排查录入", self)
self.play_sound.emit("排查录入")
if self.push_question(
"操作提示", "请检查用户代理情况,该用户是否正确完成代理任务?"
):
@@ -788,10 +844,7 @@ class MaaManager(QObject):
# 配置MAA
self.set_maa(self.mode, "")
# 创建MAA任务
maa = subprocess.Popen(
[self.maa_exe_path],
creationflags=subprocess.CREATE_NO_WINDOW,
)
self.maa_process_manager.open_process(self.maa_exe_path, [], 10)
# 记录当前时间
start_time = datetime.now()
@@ -813,6 +866,7 @@ class MaaManager(QObject):
# 关闭可能未正常退出的MAA进程
if self.isInterruptionRequested:
self.maa_process_manager.kill(if_force=True)
System.kill_process(self.maa_exe_path)
# 复原MAA配置文件
@@ -909,8 +963,13 @@ class MaaManager(QObject):
self.sleep(self.wait_time)
# 移除静默进程标记
Config.silence_list.remove(self.emulator_path)
if self.isInterruptionRequested:
return None
# 10s后移除静默进程标记
QTimer.singleShot(
10000, partial(Config.silence_list.remove, self.emulator_path)
)
if "-" in self.ADB_address:
ADB_ip = f"{self.ADB_address.split("-")[0]}-"
@@ -932,6 +991,7 @@ class MaaManager(QObject):
connect_result = subprocess.run(
[self.ADB_path, "connect", ADB_address],
creationflags=subprocess.CREATE_NO_WINDOW,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
encoding="utf-8",
@@ -943,6 +1003,7 @@ class MaaManager(QObject):
devices_result = subprocess.run(
[self.ADB_path, "devices"],
creationflags=subprocess.CREATE_NO_WINDOW,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
encoding="utf-8",
@@ -960,6 +1021,7 @@ class MaaManager(QObject):
self.ADB_address = ADB_address
# 覆写当前ADB地址
self.maa_process_manager.kill(if_force=True)
System.kill_process(self.maa_exe_path)
with self.maa_set_path.open(mode="r", encoding="utf-8") as f:
data = json.load(f)
@@ -970,7 +1032,7 @@ class MaaManager(QObject):
with self.maa_set_path.open(mode="w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
SoundPlayer.play("ADB成功", self)
self.play_sound.emit("ADB成功")
return None
else:
@@ -978,7 +1040,8 @@ class MaaManager(QObject):
else:
logger.info(f"{self.name} | 无法连接到ADB地址{ADB_address}")
SoundPlayer.play("ADB失败", self)
if not self.isInterruptionRequested:
self.play_sound.emit("ADB失败")
def refresh_maa_log(self) -> None:
"""刷新MAA日志"""
@@ -988,7 +1051,7 @@ class MaaManager(QObject):
# 一分钟内未执行日志变化检查,强制检查一次
if datetime.now() - self.last_check_time > timedelta(minutes=1):
self.log_monitor.fileChanged.emit(self.log_monitor.files()[0])
self.log_monitor.fileChanged.emit("1分钟超时检查")
def check_maa_log(self, start_time: datetime, mode: str) -> list:
"""获取MAA日志并检查以判断MAA程序运行状态"""
@@ -1059,21 +1122,25 @@ class MaaManager(QObject):
elif "任务已全部完成!" in log:
if "完成任务: StartUp" in log:
if "完成任务: StartUp" in log or "完成任务: 开始唤醒" in log:
self.task_dict["WakeUp"] = "False"
if "完成任务: Recruit" in log:
if "完成任务: Recruit" in log or "完成任务: 自动公招" in log:
self.task_dict["Recruiting"] = "False"
if "完成任务: Infrast" in log:
if "完成任务: Infrast" in log or "完成任务: 基建换班" in log:
self.task_dict["Base"] = "False"
if "完成任务: Fight" in log or "剿灭任务失败" in log:
if (
"完成任务: Fight" in log
or "完成任务: 刷理智" in log
or "剿灭任务失败" in log
):
self.task_dict["Combat"] = "False"
if "完成任务: Mall" in log:
if "完成任务: Mall" in log or "完成任务: 获取信用及购物" in log:
self.task_dict["Mall"] = "False"
if "完成任务: Award" in log:
if "完成任务: Award" in log or "完成任务: 领取奖励" in log:
self.task_dict["Mission"] = "False"
if "完成任务: Roguelike" in log:
if "完成任务: Roguelike" in log or "完成任务: 自动肉鸽" in log:
self.task_dict["AutoRoguelike"] = "False"
if "完成任务: Reclamation" in log:
if "完成任务: Reclamation" in log or "完成任务: 生息演算" in log:
self.task_dict["Reclamation"] = "False"
if all(v == "False" for v in self.task_dict.values()):
@@ -1090,7 +1157,10 @@ class MaaManager(QObject):
elif "已停止" in log:
self.maa_result = "MAA在完成任务前中止"
elif "MaaAssistantArknights GUI exited" in log:
elif (
"MaaAssistantArknights GUI exited" in log
or not self.maa_process_manager.is_running()
):
self.maa_result = "MAA在完成任务前退出"
elif datetime.now() - latest_time > timedelta(
@@ -1105,7 +1175,7 @@ class MaaManager(QObject):
self.maa_result = "Wait"
elif mode == "人工排查":
if "完成任务: StartUp" in log:
if "完成任务: StartUp" in log or "完成任务: 开始唤醒" in log:
self.maa_result = "Success!"
elif "请 「检查连接设置」 → 「尝试重启模拟器与 ADB」 → 「重启电脑」" in log:
self.maa_result = "MAA的ADB连接异常"
@@ -1113,7 +1183,10 @@ class MaaManager(QObject):
self.maa_result = "MAA未检测到任何模拟器"
elif "已停止" in log:
self.maa_result = "MAA在完成任务前中止"
elif "MaaAssistantArknights GUI exited" in log:
elif (
"MaaAssistantArknights GUI exited" in log
or not self.maa_process_manager.is_running()
):
self.maa_result = "MAA在完成任务前退出"
elif self.isInterruptionRequested:
self.maa_result = "任务被手动中止"
@@ -1121,7 +1194,10 @@ class MaaManager(QObject):
self.maa_result = "Wait"
elif mode == "设置MAA":
if "MaaAssistantArknights GUI exited" in log:
if (
"MaaAssistantArknights GUI exited" in log
or not self.maa_process_manager.is_running()
):
self.maa_result = "Success!"
else:
self.maa_result = "Wait"
@@ -1164,6 +1240,7 @@ class MaaManager(QObject):
user_data = self.data[index]["Config"]
# 配置MAA前关闭可能未正常退出的MAA进程
self.maa_process_manager.kill(if_force=True)
System.kill_process(self.maa_exe_path)
# 预导入MAA配置文件
@@ -1216,10 +1293,15 @@ class MaaManager(QObject):
else:
self.agree_bilibili(False)
# 切换配置
if data["Current"] != "Default":
data["Configurations"]["Default"] = data["Configurations"][data["Current"]]
data["Current"] = "Default"
# 自动代理配置
if "自动代理" in mode:
data["Current"] = "Default" # 切换配置
for i in range(1, 9):
data["Global"][f"Timer.Timer{i}"] = "False" # 时间设置
@@ -1372,23 +1454,28 @@ class MaaManager(QObject):
user_data["Info"]["MedicineNumb"]
) # 吃理智药数量
data["Configurations"]["Default"]["MainFunction.Stage1"] = (
user_data["Info"]["GameId"]
if user_data["Info"]["GameId"] != "-"
user_data["Info"]["Stage"]
if user_data["Info"]["Stage"] != "-"
else ""
) # 主关卡
data["Configurations"]["Default"]["MainFunction.Stage2"] = (
user_data["Info"]["GameId_1"]
if user_data["Info"]["GameId_1"] != "-"
user_data["Info"]["Stage_1"]
if user_data["Info"]["Stage_1"] != "-"
else ""
) # 备选关卡1
data["Configurations"]["Default"]["MainFunction.Stage3"] = (
user_data["Info"]["GameId_2"]
if user_data["Info"]["GameId_2"] != "-"
user_data["Info"]["Stage_2"]
if user_data["Info"]["Stage_2"] != "-"
else ""
) # 备选关卡2
data["Configurations"]["Default"]["MainFunction.Stage4"] = (
user_data["Info"]["Stage_3"]
if user_data["Info"]["Stage_3"] != "-"
else ""
) # 备选关卡3
data["Configurations"]["Default"]["Fight.RemainingSanityStage"] = (
user_data["Info"]["GameId_Remain"]
if user_data["Info"]["GameId_Remain"] != "-"
user_data["Info"]["Stage_Remain"]
if user_data["Info"]["Stage_Remain"] != "-"
else ""
) # 剩余理智关卡
data["Configurations"]["Default"][
@@ -1408,7 +1495,7 @@ class MaaManager(QObject):
data["Configurations"]["Default"][
"Fight.UseRemainingSanityStage"
] = (
"True" if user_data["Info"]["GameId_Remain"] != "-" else "False"
"True" if user_data["Info"]["Stage_Remain"] != "-" else "False"
) # 使用剩余理智
data["Configurations"]["Default"][
"Fight.UseExpiringMedicine"
@@ -1478,23 +1565,28 @@ class MaaManager(QObject):
user_data["Info"]["MedicineNumb"]
) # 吃理智药数量
data["Configurations"]["Default"]["MainFunction.Stage1"] = (
user_data["Info"]["GameId"]
if user_data["Info"]["GameId"] != "-"
user_data["Info"]["Stage"]
if user_data["Info"]["Stage"] != "-"
else ""
) # 主关卡
data["Configurations"]["Default"]["MainFunction.Stage2"] = (
user_data["Info"]["GameId_1"]
if user_data["Info"]["GameId_1"] != "-"
user_data["Info"]["Stage_1"]
if user_data["Info"]["Stage_1"] != "-"
else ""
) # 备选关卡1
data["Configurations"]["Default"]["MainFunction.Stage3"] = (
user_data["Info"]["GameId_2"]
if user_data["Info"]["GameId_2"] != "-"
user_data["Info"]["Stage_2"]
if user_data["Info"]["Stage_2"] != "-"
else ""
) # 备选关卡2
data["Configurations"]["Default"]["MainFunction.Stage4"] = (
user_data["Info"]["Stage_3"]
if user_data["Info"]["Stage_3"] != "-"
else ""
) # 备选关卡3
data["Configurations"]["Default"]["Fight.RemainingSanityStage"] = (
user_data["Info"]["GameId_Remain"]
if user_data["Info"]["GameId_Remain"] != "-"
user_data["Info"]["Stage_Remain"]
if user_data["Info"]["Stage_Remain"] != "-"
else ""
) # 剩余理智关卡
data["Configurations"]["Default"][
@@ -1508,7 +1600,7 @@ class MaaManager(QObject):
data["Configurations"]["Default"][
"Fight.UseRemainingSanityStage"
] = (
"True" if user_data["Info"]["GameId_Remain"] != "-" else "False"
"True" if user_data["Info"]["Stage_Remain"] != "-" else "False"
) # 使用剩余理智
# 基建模式
@@ -1525,7 +1617,6 @@ class MaaManager(QObject):
# 人工排查配置
elif "人工排查" in mode:
data["Current"] = "Default" # 切换配置
for i in range(1, 9):
data["Global"][f"Timer.Timer{i}"] = "False" # 时间设置
data["Configurations"]["Default"][
@@ -1598,7 +1689,6 @@ class MaaManager(QObject):
# 设置MAA配置
elif "设置MAA" in mode:
data["Current"] = "Default" # 切换配置
for i in range(1, 9):
data["Global"][f"Timer.Timer{i}"] = "False" # 时间设置
data["Configurations"]["Default"][
@@ -1654,7 +1744,6 @@ class MaaManager(QObject):
elif mode == "更新MAA":
data["Current"] = "Default" # 切换配置
for i in range(1, 9):
data["Global"][f"Timer.Timer{i}"] = "False" # 时间设置
data["Configurations"]["Default"][
@@ -1814,15 +1903,17 @@ class MaaManager(QObject):
# 生成文本通知内容
formatted = []
for stage, items in message["drop_statistics"].items():
formatted.append(f"掉落统计({stage}:")
for item, quantity in items.items():
formatted.append(f" {item}: {quantity}")
if "drop_statistics" in message:
for stage, items in message["drop_statistics"].items():
formatted.append(f"掉落统计({stage}:")
for item, quantity in items.items():
formatted.append(f" {item}: {quantity}")
drop_text = "\n".join(formatted)
formatted = ["招募统计:"]
for star, count in message["recruit_statistics"].items():
formatted.append(f" {star}: {count}")
if "recruit_statistics" in message:
for star, count in message["recruit_statistics"].items():
formatted.append(f" {star}: {count}")
recruit_text = "\n".join(formatted)
message_text = (
@@ -1942,6 +2033,10 @@ class MaaManager(QObject):
"好羡慕~\n\nAUTO_MAA 敬上",
Config.get(Config.notify_CompanyWebHookBotUrl),
)
Notify.CompanyWebHookBotPushImage(
Config.app_path / "resources/images/notification/six_star.png",
Config.get(Config.notify_CompanyWebHookBotUrl),
)
# 发送用户单独通知
if user_data["Notify"]["Enabled"] and user_data["Notify"]["IfSendSixStar"]:
@@ -1984,8 +2079,14 @@ class MaaManager(QObject):
"好羡慕~\n\nAUTO_MAA 敬上",
user_data["Notify"]["CompanyWebHookBotUrl"],
)
Notify.CompanyWebHookBotPushImage(
Config.app_path
/ "resources/images/notification/six_star.png",
Config.get(Config.notify_CompanyWebHookBotUrl),
)
else:
logger.error(
f"{self.name} |用户CompanyWebHookBot密钥为空无法发送用户单独的CompanyWebHookBot通知"
)
return None

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA模组包
v4.3
v4.4
作者DLmaster_361
"""
@@ -29,6 +29,7 @@ __version__ = "4.2.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
from .general import GeneralManager
from .MAA import MaaManager
__all__ = ["MaaManager"]
__all__ = ["GeneralManager", "MaaManager"]

937
app/models/general.py Normal file
View File

@@ -0,0 +1,937 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
# This file is part of AUTO_MAA.
# AUTO_MAA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
# AUTO_MAA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
# the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
"""
AUTO_MAA
通用功能组件
v4.4
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtCore import QObject, Signal, QEventLoop, QFileSystemWatcher, QTimer
import os
import sys
import shutil
import subprocess
from functools import partial
from datetime import datetime, timedelta
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
from typing import Union, List, Dict
from app.core import Config, GeneralConfig, GeneralSubConfig
from app.services import Notify, System
from app.utils import ProcessManager
class GeneralManager(QObject):
"""通用脚本通用控制器"""
question = Signal(str, str)
question_response = Signal(bool)
update_sub_info = Signal(str, dict)
push_info_bar = Signal(str, str, str, int)
play_sound = Signal(str)
create_user_list = Signal(list)
update_user_list = Signal(list)
update_log_text = Signal(str)
interrupt = Signal()
accomplish = Signal(dict)
def __init__(
self,
mode: str,
config: Dict[
str,
Union[
str,
Path,
GeneralConfig,
Dict[str, Dict[str, Union[Path, GeneralSubConfig]]],
],
],
sub_config_path: Path = None,
):
super(GeneralManager, self).__init__()
self.sub_list = []
self.mode = mode
self.config_path = config["Path"]
self.sub_config_path = sub_config_path
self.game_process_manager = ProcessManager()
self.script_process_manager = ProcessManager()
self.log_monitor = QFileSystemWatcher()
self.log_monitor_timer = QTimer()
self.log_monitor_timer.timeout.connect(self.refresh_log)
self.monitor_loop = QEventLoop()
self.script_process_manager.processClosed.connect(
lambda: self.log_monitor.fileChanged.emit("进程结束检查")
)
self.question_loop = QEventLoop()
self.question_response.connect(self.__capture_response)
self.question_response.connect(self.question_loop.quit)
self.wait_loop = QEventLoop()
self.isInterruptionRequested = False
self.interrupt.connect(self.quit_monitor)
self.task_dict = {}
self.set = config["Config"].toDict()
self.data: Dict[str, Dict[str, Union[Path, dict]]] = {}
if self.mode != "设置通用脚本":
for name, info in config["SubData"].items():
self.data[name] = {
"Path": info["Path"],
"Config": info["Config"].toDict(),
}
self.data = dict(sorted(self.data.items(), key=lambda x: int(x[0][3:])))
def check_config_info(self) -> bool:
"""检查配置完整性"""
if not (
Path(self.set["Script"]["RootPath"]).exists()
and Path(self.set["Script"]["ScriptPath"]).exists()
and Path(self.set["Script"]["ConfigPath"]).exists()
and Path(self.set["Script"]["LogPath"]).parent.exists()
and self.set["Script"]["LogTimeFormat"]
and self.set["Script"]["ErrorLog"]
) or (
self.set["Game"]["Enabled"] and not Path(self.set["Game"]["Path"]).exists()
):
logger.error("脚本配置缺失")
self.push_info_bar.emit("error", "脚本配置缺失", "请检查脚本配置!", -1)
return False
return True
def configure(self):
"""提取配置信息"""
self.name = self.set["Script"]["Name"]
self.script_root_path = Path(self.set["Script"]["RootPath"])
self.script_exe_path = Path(self.set["Script"]["ScriptPath"])
self.script_config_path = Path(self.set["Script"]["ConfigPath"])
self.script_log_path = (
Path(self.set["Script"]["LogPath"]).with_stem(
datetime.now().strftime(self.set["Script"]["LogPathFormat"])
)
if self.set["Script"]["LogPathFormat"]
else Path(self.set["Script"]["LogPath"])
)
if not self.script_log_path.exists():
self.script_log_path.parent.mkdir(parents=True, exist_ok=True)
self.script_log_path.touch(exist_ok=True)
self.game_path = Path(self.set["Game"]["Path"])
self.log_time_range = [
self.set["Script"]["LogTimeStart"] - 1,
self.set["Script"]["LogTimeEnd"],
]
self.success_log = (
[_.strip() for _ in self.set["Script"]["SuccessLog"].split("|")]
if self.set["Script"]["SuccessLog"]
else []
)
self.error_log = [_.strip() for _ in self.set["Script"]["ErrorLog"].split("|")]
def run(self):
"""主进程,运行通用脚本代理进程"""
current_date = datetime.now().strftime("%m-%d")
curdate = Config.server_date().strftime("%Y-%m-%d")
begin_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 检查配置完整性
if not self.check_config_info():
self.accomplish.emit(
{"Time": begin_time, "History": "由于配置不完整,通用代理进程中止"}
)
return None
self.configure()
# 整理用户数据,筛选需代理的用户
if self.mode != "设置通用脚本":
self.data = dict(sorted(self.data.items(), key=lambda x: int(x[0][3:])))
self.sub_list: List[List[str, str, str]] = [
[_["Config"]["Info"]["Name"], "等待", index]
for index, _ in self.data.items()
if (
_["Config"]["Info"]["RemainedDay"] != 0
and _["Config"]["Info"]["Status"]
)
]
self.create_user_list.emit(self.sub_list)
# 自动代理模式
if self.mode == "自动代理":
# 执行情况预处理
for _ in self.sub_list:
if self.data[_[2]]["Config"]["Data"]["LastProxyDate"] != curdate:
self.data[_[2]]["Config"]["Data"]["LastProxyDate"] = curdate
self.data[_[2]]["Config"]["Data"]["ProxyTimes"] = 0
_[
0
] += f" - 第{self.data[_[2]]['Config']['Data']['ProxyTimes'] + 1}次代理"
# 开始代理
for sub in self.sub_list:
sub_data = self.data[sub[2]]["Config"]
if self.isInterruptionRequested:
break
if (
self.set["Run"]["ProxyTimesLimit"] == 0
or sub_data["Data"]["ProxyTimes"]
< self.set["Run"]["ProxyTimesLimit"]
):
sub[1] = "运行"
self.update_user_list.emit(self.sub_list)
else:
sub[1] = "跳过"
self.update_user_list.emit(self.sub_list)
continue
logger.info(f"{self.name} | 开始代理配置: {sub[0]}")
sub_start_time = datetime.now()
run_book = False
if not (self.data[sub[2]]["Path"] / "ConfigFiles").exists():
logger.error(f"{self.name} | 配置: {sub[0]} - 未找到配置文件")
self.push_info_bar.emit(
"error",
"启动通用代理进程失败",
f"未找到{sub[0]}的配置文件!",
-1,
)
run_book = False
continue
# 尝试次数循环
for i in range(self.set["Run"]["RunTimesLimit"]):
if self.isInterruptionRequested or run_book:
break
logger.info(
f"{self.name} | 用户: {sub[0]} - 尝试次数: {i + 1}/{self.set['Run']['RunTimesLimit']}"
)
# 记录当前时间
start_time = datetime.now()
# 配置脚本
self.set_sub(sub[2])
# 执行任务前脚本
if (
sub_data["Info"]["IfScriptBeforeTask"]
and Path(sub_data["Info"]["ScriptBeforeTask"]).exists()
):
self.execute_script_task(
Path(sub_data["Info"]["ScriptBeforeTask"]), "脚本前任务"
)
# 启动游戏/模拟器
if self.set["Game"]["Enabled"]:
try:
logger.info(
f"{self.name} | 启动游戏/模拟器:{self.game_path},参数:{self.set['Game']['Arguments']}"
)
self.game_process_manager.open_process(
self.game_path,
str(self.set["Game"]["Arguments"]).split(" "),
0,
)
except Exception as e:
logger.error(
f"{self.name} | 启动游戏/模拟器时出现异常:{e}"
)
self.push_info_bar.emit(
"error",
"启动游戏/模拟器时出现异常",
"请检查游戏/模拟器路径设置",
-1,
)
self.script_result = "游戏/模拟器启动失败"
break
# 添加静默进程标记
if self.set["Game"]["Style"] == "Emulator":
Config.silence_list.append(self.game_path)
self.update_log_text.emit(
f"正在等待游戏/模拟器完成启动\n请等待{self.set['Game']['WaitTime']}s"
)
self.sleep(self.set["Game"]["WaitTime"])
# 10s后移除静默进程标记
if self.set["Game"]["Style"] == "Emulator":
QTimer.singleShot(
10000,
partial(Config.silence_list.remove, self.game_path),
)
# 运行脚本任务
logger.info(
f"{self.name} | 运行脚本任务:{self.script_exe_path},参数:{self.set['Script']['Arguments']}"
)
self.script_process_manager.open_process(
self.script_exe_path,
str(self.set["Script"]["Arguments"]).split(" "),
tracking_time=60 if self.set["Script"]["IfTrackProcess"] else 0,
)
# 监测运行状态
self.start_monitor(start_time)
if self.script_result == "Success!":
# 标记任务完成
run_book = True
# 中止相关程序
self.script_process_manager.kill()
System.kill_process(self.script_exe_path)
if self.set["Game"]["Enabled"]:
self.game_process_manager.kill()
if self.set["Game"]["IfForceClose"]:
System.kill_process(self.game_path)
logger.info(
f"{self.name} | 配置: {sub[0]} - 通用脚本进程完成代理任务"
)
self.update_log_text.emit(
"检测到通用脚本进程完成代理任务\n正在等待相关程序结束\n请等待10s"
)
self.sleep(10)
else:
logger.error(
f"{self.name} | 配置: {sub[0]} - 代理任务异常: {self.script_result}"
)
# 打印中止信息
# 此时log变量内存储的就是出现异常的日志信息可以保存或发送用于问题排查
self.update_log_text.emit(
f"{self.script_result}\n正在中止相关程序\n请等待10s"
)
# 中止相关程序
self.script_process_manager.kill()
if self.set["Game"]["Enabled"]:
self.game_process_manager.kill()
if self.set["Game"]["IfForceClose"]:
System.kill_process(self.game_path)
# 推送异常通知
Notify.push_plyer(
"用户自动代理出现异常!",
f"用户 {sub[0].replace("_", " 今天的")}出现一次异常",
f"{sub[0].replace("_", " ")}出现异常",
1,
)
if i == self.set["Run"]["RunTimesLimit"] - 1:
self.play_sound.emit("子任务失败")
else:
self.play_sound.emit(self.script_result)
self.sleep(10)
# 执行任务后脚本
if (
sub_data["Info"]["IfScriptAfterTask"]
and Path(sub_data["Info"]["ScriptAfterTask"]).exists()
):
self.execute_script_task(
Path(sub_data["Info"]["ScriptAfterTask"]), "脚本后任务"
)
# 保存运行日志以及统计信息
Config.save_general_log(
Config.app_path
/ f"history/{curdate}/{sub_data['Info']['Name']}/{start_time.strftime("%H-%M-%S")}.log",
self.check_script_log(start_time),
self.script_result,
)
# 发送统计信息
statistics = {
"sub_index": sub[2],
"sub_info": sub[0],
"start_time": sub_start_time.strftime("%Y-%m-%d %H:%M:%S"),
"end_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"sub_result": "代理成功" if run_book else self.script_result,
}
self.push_notification(
"统计信息",
f"{current_date} | 配置 {sub[0]} 的自动代理统计报告",
statistics,
sub_data,
)
if run_book:
# 成功完成代理的用户修改相关参数
if (
sub_data["Data"]["ProxyTimes"] == 0
and sub_data["Info"]["RemainedDay"] != -1
):
sub_data["Info"]["RemainedDay"] -= 1
sub_data["Data"]["ProxyTimes"] += 1
sub[1] = "完成"
Notify.push_plyer(
"成功完成一个自动代理任务!",
f"已完成配置 {sub[0].replace("_", " 今天的")}任务",
f"已完成 {sub[0].replace("_", "")}",
3,
)
else:
# 录入代理失败的用户
sub[1] = "异常"
self.update_user_list.emit(self.sub_list)
# 设置通用脚本模式
elif self.mode == "设置通用脚本":
# 配置通用脚本
self.set_sub()
try:
# 创建通用脚本任务
logger.info(f"{self.name} | 无参数启动通用脚本:{self.script_exe_path}")
self.script_process_manager.open_process(
self.script_exe_path,
tracking_time=60 if self.set["Script"]["IfTrackProcess"] else 0,
)
# 记录当前时间
start_time = datetime.now()
# 监测通用脚本运行状态
self.start_monitor(start_time)
self.sub_config_path.mkdir(parents=True, exist_ok=True)
if self.set["Script"]["ConfigPathMode"] == "文件夹":
shutil.copytree(
self.script_config_path,
self.sub_config_path,
dirs_exist_ok=True,
)
else:
shutil.copy(self.script_config_path, self.sub_config_path)
except Exception as e:
logger.error(f"{self.name} | 启动通用脚本时出现异常:{e}")
self.push_info_bar.emit(
"error",
"启动通用脚本时出现异常",
"请检查相关设置",
-1,
)
result_text = ""
# 导出结果
if self.mode in ["自动代理"]:
# 关闭可能未正常退出的通用脚本进程
if self.isInterruptionRequested:
self.script_process_manager.kill(if_force=True)
System.kill_process(self.script_exe_path)
if self.set["Game"]["Enabled"]:
self.game_process_manager.kill(if_force=True)
if self.set["Game"]["IfForceClose"]:
System.kill_process(self.game_path)
# 更新用户数据
updated_info = {_[2]: self.data[_[2]] for _ in self.sub_list}
self.update_sub_info.emit(self.config_path.name, updated_info)
error_index = [_[2] for _ in self.sub_list if _[1] == "异常"]
over_index = [_[2] for _ in self.sub_list if _[1] == "完成"]
wait_index = [_[2] for _ in self.sub_list if _[1] == "等待"]
# 保存运行日志
title = (
f"{current_date} | {self.name}{self.mode[:4]}任务报告"
if self.name != ""
else f"{current_date} | {self.mode[:4]}任务报告"
)
result = {
"title": f"{self.mode[:4]}任务报告",
"script_name": (self.name if self.name != "" else "空白"),
"start_time": begin_time,
"end_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"completed_count": len(over_index),
"uncompleted_count": len(error_index) + len(wait_index),
"failed_sub": [
self.data[_]["Config"]["Info"]["Name"] for _ in error_index
],
"waiting_sub": [
self.data[_]["Config"]["Info"]["Name"] for _ in wait_index
],
}
# 生成结果文本
result_text = (
f"任务开始时间:{result['start_time']},结束时间:{result['end_time']}\n"
f"已完成数:{result['completed_count']},未完成数:{result['uncompleted_count']}\n\n"
)
if len(result["failed_sub"]) > 0:
result_text += f"{self.mode[2:4]}未成功的配置:\n{"\n".join(result['failed_sub'])}\n"
if len(result["waiting_sub"]) > 0:
result_text += f"\n未开始{self.mode[2:4]}的配置:\n{"\n".join(result['waiting_sub'])}\n"
# 推送代理结果通知
Notify.push_plyer(
title.replace("报告", "已完成!"),
f"已完成配置数:{len(over_index)},未完成配置数:{len(error_index) + len(wait_index)}",
f"已完成配置数:{len(over_index)},未完成配置数:{len(error_index) + len(wait_index)}",
10,
)
self.push_notification("代理结果", title, result)
self.log_monitor.deleteLater()
self.log_monitor_timer.deleteLater()
self.accomplish.emit({"Time": begin_time, "History": result_text})
def requestInterruption(self) -> None:
logger.info(f"{self.name} | 收到任务中止申请")
if len(self.log_monitor.files()) != 0:
self.interrupt.emit()
self.script_result = "任务被手动中止"
self.isInterruptionRequested = True
self.wait_loop.quit()
def push_question(self, title: str, message: str) -> bool:
self.question.emit(title, message)
self.question_loop.exec()
return self.response
def __capture_response(self, response: bool) -> None:
self.response = response
def sleep(self, time: int) -> None:
"""非阻塞型等待"""
QTimer.singleShot(time * 1000, self.wait_loop.quit)
self.wait_loop.exec()
def refresh_log(self) -> None:
"""刷新脚本日志"""
with self.script_log_path.open(mode="r", encoding="utf-8") as f:
pass
# 一分钟内未执行日志变化检查,强制检查一次
if (datetime.now() - self.last_check_time).total_seconds() > 60:
self.log_monitor.fileChanged.emit("1分钟超时检查")
def strptime(
self, date_string: str, format: str, default_date: datetime
) -> datetime:
"""根据指定格式解析日期字符串"""
# 时间字段映射表
time_fields = {
"%Y": "year",
"%m": "month",
"%d": "day",
"%H": "hour",
"%M": "minute",
"%S": "second",
"%f": "microsecond",
}
date = datetime.strptime(date_string, format)
# 构建参数字典
datetime_kwargs = {}
for format_code, field_name in time_fields.items():
if format_code in format:
datetime_kwargs[field_name] = getattr(date, field_name)
else:
datetime_kwargs[field_name] = getattr(default_date, field_name)
return datetime(**datetime_kwargs)
def check_script_log(self, start_time: datetime) -> list:
"""获取脚本日志并检查以判断脚本程序运行状态"""
self.last_check_time = datetime.now()
# 获取日志
logs = []
if_log_start = False
with self.script_log_path.open(mode="r", encoding="utf-8") as f:
for entry in f:
if not if_log_start:
try:
entry_time = self.strptime(
entry[self.log_time_range[0] : self.log_time_range[1]],
self.set["Script"]["LogTimeFormat"],
self.last_check_time,
)
if entry_time > start_time:
if_log_start = True
logs.append(entry)
except ValueError:
pass
else:
logs.append(entry)
log = "".join(logs)
# 更新日志
if len(logs) > 100:
self.update_log_text.emit("".join(logs[-100:]))
else:
self.update_log_text.emit("".join(logs))
if "自动代理" in self.mode:
# 获取最近一条日志的时间
latest_time = start_time
for _ in logs[::-1]:
try:
latest_time = self.strptime(
_[self.log_time_range[0] : self.log_time_range[1]],
self.set["Script"]["LogTimeFormat"],
self.last_check_time,
)
break
except ValueError:
pass
for success_sign in self.success_log:
if success_sign in log:
self.script_result = "Success!"
break
else:
if self.isInterruptionRequested:
self.script_result = "任务被手动中止"
elif datetime.now() - latest_time > timedelta(
minutes=self.set["Run"]["RunTimeLimit"]
):
self.script_result = "脚本进程超时"
else:
for error_sign in self.error_log:
if error_sign in log:
self.script_result = f"异常日志:{error_sign}"
break
else:
if self.script_process_manager.is_running():
self.script_result = "Wait"
elif self.success_log:
self.script_result = "脚本在完成任务前退出"
else:
self.script_result = "Success!"
elif self.mode == "设置通用脚本":
if self.script_process_manager.is_running():
self.script_result = "Wait"
else:
self.script_result = "Success!"
if self.script_result != "Wait":
self.quit_monitor()
return logs
def start_monitor(self, start_time: datetime) -> None:
"""开始监视通用脚本日志"""
logger.info(f"{self.name} | 开始监视通用脚本日志")
self.log_monitor.addPath(str(self.script_log_path))
self.log_monitor.fileChanged.connect(lambda: self.check_script_log(start_time))
self.log_monitor_timer.start(1000)
self.last_check_time = datetime.now()
self.monitor_loop.exec()
def quit_monitor(self) -> None:
"""退出通用脚本日志监视进程"""
if len(self.log_monitor.files()) != 0:
logger.info(f"{self.name} | 退出通用脚本日志监视")
self.log_monitor.removePath(str(self.script_log_path))
self.log_monitor.fileChanged.disconnect()
self.log_monitor_timer.stop()
self.last_check_time = None
self.monitor_loop.quit()
def set_sub(self, index: str = "") -> dict:
"""配置通用脚本运行参数"""
logger.info(f"{self.name} | 配置脚本运行参数: {index}")
# 配置前关闭可能未正常退出的脚本进程
System.kill_process(self.script_exe_path)
# 预导入配置文件
if self.mode == "设置通用脚本":
if self.sub_config_path.exists():
if self.set["Script"]["ConfigPathMode"] == "文件夹":
shutil.copytree(
self.sub_config_path,
self.script_config_path,
dirs_exist_ok=True,
)
elif (self.sub_config_path / self.script_config_path.name).exists():
shutil.copy(
self.sub_config_path / self.script_config_path.name,
self.script_config_path,
)
else:
if self.set["Script"]["ConfigPathMode"] == "文件夹":
shutil.copytree(
self.data[index]["Path"] / "ConfigFiles",
self.script_config_path,
dirs_exist_ok=True,
)
else:
shutil.copy(
self.data[index]["Path"]
/ "ConfigFiles"
/ self.script_config_path.name,
self.script_config_path,
)
def execute_script_task(self, script_path: Path, task_name: str) -> bool:
"""执行脚本任务并等待结束"""
try:
logger.info(f"{self.name} | 开始执行{task_name}: {script_path}")
# 根据文件类型选择执行方式
if script_path.suffix.lower() == ".py":
cmd = [sys.executable, script_path]
elif script_path.suffix.lower() in [".bat", ".cmd", ".exe"]:
cmd = [str(script_path)]
elif script_path.suffix.lower() == "":
logger.warning(f"{self.name} | {task_name}脚本没有指定后缀名,无法执行")
return False
else:
# 使用系统默认程序打开
os.startfile(str(script_path))
return True
# 执行脚本并等待结束
result = subprocess.run(
cmd,
cwd=script_path.parent,
stdin=subprocess.DEVNULL,
creationflags=(
subprocess.CREATE_NO_WINDOW
if Config.get(Config.function_IfSilence)
else 0
),
timeout=600,
capture_output=True,
errors="ignore",
)
if result.returncode == 0:
logger.info(f"{self.name} | {task_name}执行成功")
if result.stdout.strip():
logger.info(f"{self.name} | {task_name}输出: {result.stdout}")
return True
else:
logger.error(
f"{self.name} | {task_name}执行失败,返回码: {result.returncode}"
)
if result.stderr.strip():
logger.error(f"{self.name} | {task_name}错误输出: {result.stderr}")
return False
except subprocess.TimeoutExpired:
logger.error(f"{self.name} | {task_name}执行超时")
return False
except Exception as e:
logger.exception(f"{self.name} | 执行{task_name}时出现异常: {e}")
return False
def push_notification(
self,
mode: str,
title: str,
message: Union[str, dict],
sub_data: Dict[str, Dict[str, Union[str, int, bool]]] = None,
) -> None:
"""通过所有渠道推送通知"""
env = Environment(
loader=FileSystemLoader(str(Config.app_path / "resources/html"))
)
if mode == "代理结果" and (
Config.get(Config.notify_SendTaskResultTime) == "任何时刻"
or (
Config.get(Config.notify_SendTaskResultTime) == "仅失败时"
and message["uncompleted_count"] != 0
)
):
# 生成文本通知内容
message_text = (
f"任务开始时间:{message['start_time']},结束时间:{message['end_time']}\n"
f"已完成数:{message['completed_count']},未完成数:{message['uncompleted_count']}\n\n"
)
if len(message["failed_sub"]) > 0:
message_text += f"{self.mode[2:4]}未成功的配置:\n{"\n".join(message['failed_sub'])}\n"
if len(message["waiting_sub"]) > 0:
message_text += f"\n未开始{self.mode[2:4]}的配置:\n{"\n".join(message['waiting_sub'])}\n"
# 生成HTML通知内容
message["failed_sub"] = "".join(message["failed_sub"])
message["waiting_sub"] = "".join(message["waiting_sub"])
template = env.get_template("general_result.html")
message_html = template.render(message)
# ServerChan的换行是两个换行符。故而将\n替换为\n\n
serverchan_message = message_text.replace("\n", "\n\n")
# 发送全局通知
if Config.get(Config.notify_IfSendMail):
Notify.send_mail(
"网页", title, message_html, Config.get(Config.notify_ToAddress)
)
if Config.get(Config.notify_IfServerChan):
Notify.ServerChanPush(
title,
f"{serverchan_message}\n\nAUTO_MAA 敬上",
Config.get(Config.notify_ServerChanKey),
Config.get(Config.notify_ServerChanTag),
Config.get(Config.notify_ServerChanChannel),
)
if Config.get(Config.notify_IfCompanyWebHookBot):
Notify.CompanyWebHookBotPush(
title,
f"{message_text}\n\nAUTO_MAA 敬上",
Config.get(Config.notify_CompanyWebHookBotUrl),
)
elif mode == "统计信息":
message_text = (
f"开始时间: {message['start_time']}\n"
f"结束时间: {message['end_time']}\n"
f"通用脚本执行结果: {message['sub_result']}\n\n"
)
# 生成HTML通知内容
template = env.get_template("general_statistics.html")
message_html = template.render(message)
# ServerChan的换行是两个换行符。故而将\n替换为\n\n
serverchan_message = message_text.replace("\n", "\n\n")
# 发送全局通知
if Config.get(Config.notify_IfSendStatistic):
if Config.get(Config.notify_IfSendMail):
Notify.send_mail(
"网页", title, message_html, Config.get(Config.notify_ToAddress)
)
if Config.get(Config.notify_IfServerChan):
Notify.ServerChanPush(
title,
f"{serverchan_message}\n\nAUTO_MAA 敬上",
Config.get(Config.notify_ServerChanKey),
Config.get(Config.notify_ServerChanTag),
Config.get(Config.notify_ServerChanChannel),
)
if Config.get(Config.notify_IfCompanyWebHookBot):
Notify.CompanyWebHookBotPush(
title,
f"{message_text}\n\nAUTO_MAA 敬上",
Config.get(Config.notify_CompanyWebHookBotUrl),
)
# 发送用户单独通知
if sub_data["Notify"]["Enabled"] and sub_data["Notify"]["IfSendStatistic"]:
# 发送邮件通知
if sub_data["Notify"]["IfSendMail"]:
if sub_data["Notify"]["ToAddress"]:
Notify.send_mail(
"网页",
title,
message_html,
sub_data["Notify"]["ToAddress"],
)
else:
logger.error(
f"{self.name} | 用户邮箱地址为空,无法发送用户单独的邮件通知"
)
# 发送ServerChan通知
if sub_data["Notify"]["IfServerChan"]:
if sub_data["Notify"]["ServerChanKey"]:
Notify.ServerChanPush(
title,
f"{serverchan_message}\n\nAUTO_MAA 敬上",
sub_data["Notify"]["ServerChanKey"],
sub_data["Notify"]["ServerChanTag"],
sub_data["Notify"]["ServerChanChannel"],
)
else:
logger.error(
f"{self.name} |用户ServerChan密钥为空无法发送用户单独的ServerChan通知"
)
# 推送CompanyWebHookBot通知
if sub_data["Notify"]["IfCompanyWebHookBot"]:
if sub_data["Notify"]["CompanyWebHookBotUrl"]:
Notify.CompanyWebHookBotPush(
title,
f"{message_text}\n\nAUTO_MAA 敬上",
sub_data["Notify"]["CompanyWebHookBotUrl"],
)
else:
logger.error(
f"{self.name} |用户CompanyWebHookBot密钥为空无法发送用户单独的CompanyWebHookBot通知"
)
return None

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA服务包
v4.3
v4.4
作者DLmaster_361
"""
@@ -32,5 +32,6 @@ __license__ = "GPL-3.0 license"
from .notification import Notify
from .security import Crypto
from .system import System
from .skland import skland_sign_in
__all__ = ["Notify", "Crypto", "System"]
__all__ = ["Notify", "Crypto", "System", "skland_sign_in"]

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA通知服务
v4.3
v4.4
作者DLmaster_361
"""
@@ -32,6 +32,7 @@ from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formataddr
from pathlib import Path
import requests
from PySide6.QtCore import QObject, Signal
@@ -40,6 +41,7 @@ from plyer import notification
from app.core import Config
from app.services.security import Crypto
from app.utils.ImageUtils import ImageUtils
class Notification(QObject):
@@ -197,7 +199,16 @@ class Notification(QObject):
params = {"title": title, "desp": content, **options}
headers = {"Content-Type": "application/json;charset=utf-8"}
response = requests.post(url, json=params, headers=headers, timeout=10)
response = requests.post(
url,
json=params,
headers=headers,
timeout=10,
proxies={
"http": Config.get(Config.update_ProxyAddress),
"https": Config.get(Config.update_ProxyAddress),
},
)
result = response.json()
if result.get("code") == 0:
@@ -242,6 +253,10 @@ class Notification(QObject):
url=webhook_url,
json=data,
timeout=10,
proxies={
"http": Config.get(Config.update_ProxyAddress),
"https": Config.get(Config.update_ProxyAddress),
},
)
info = response.json()
break
@@ -271,6 +286,104 @@ class Notification(QObject):
)
return f"使用企业微信群机器人推送通知时出错:{err}"
def CompanyWebHookBotPushImage(self, image_path: Path, webhook_url: str) -> bool:
"""使用企业微信群机器人推送图片通知"""
try:
# 压缩图片
ImageUtils.compress_image_if_needed(image_path)
# 检查图片是否存在
if not image_path.exists():
logger.error(
"图片推送异常 | 图片不存在或者压缩失败,请检查图片路径是否正确"
)
self.push_info_bar.emit(
"error",
"企业微信群机器人通知推送异常",
"图片不存在或者压缩失败,请检查图片路径是否正确",
-1,
)
return False
if not webhook_url:
logger.error("请正确设置企业微信群机器人的WebHook地址")
self.push_info_bar.emit(
"error",
"企业微信群机器人通知推送异常",
"请正确设置企业微信群机器人的WebHook地址",
-1,
)
return False
# 获取图片base64和md5
try:
image_base64 = ImageUtils.get_base64_from_file(str(image_path))
image_md5 = ImageUtils.calculate_md5_from_file(str(image_path))
except Exception as e:
logger.exception(f"图片编码或MD5计算失败{e}")
self.push_info_bar.emit(
"error",
"企业微信群机器人通知推送异常",
f"图片编码或MD5计算失败{e}",
-1,
)
return False
data = {
"msgtype": "image",
"image": {"base64": image_base64, "md5": image_md5},
}
for _ in range(3):
try:
response = requests.post(
url=webhook_url,
json=data,
timeout=10,
proxies={
"http": Config.get(Config.update_ProxyAddress),
"https": Config.get(Config.update_ProxyAddress),
},
)
info = response.json()
break
except requests.RequestException as e:
err = e
logger.warning(f"推送企业微信群机器人图片第{_+1}次失败:{e}")
time.sleep(0.1)
else:
logger.error(f"推送企业微信群机器人图片时出错:{err}")
self.push_info_bar.emit(
"error",
"企业微信群机器人图片推送失败",
f"使用企业微信群机器人推送图片时出错:{err}",
-1,
)
return False
if info.get("errcode") == 0:
logger.info("企业微信群机器人推送图片成功")
return True
else:
logger.error(f"企业微信群机器人推送图片失败:{info}")
self.push_info_bar.emit(
"error",
"企业微信群机器人图片推送失败",
f"使用企业微信群机器人推送图片时出错:{info}",
-1,
)
return False
except Exception as e:
logger.error(f"推送企业微信群机器人图片时发生未知异常:{e}")
self.push_info_bar.emit(
"error",
"企业微信群机器人图片推送失败",
f"发生未知异常:{e}",
-1,
)
return False
def send_test_notification(self):
"""发送测试通知到所有已启用的通知渠道"""
# 发送系统通知
@@ -307,6 +420,10 @@ class Notification(QObject):
"这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
Config.get(Config.notify_CompanyWebHookBotUrl),
)
Notify.CompanyWebHookBotPushImage(
Config.app_path / "resources/images/notification/test_notify.png",
Config.get(Config.notify_CompanyWebHookBotUrl),
)
return True

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA安全服务
v4.3
v4.4
作者DLmaster_361
"""
@@ -147,22 +147,36 @@ class CryptoHandler:
for member in Config.member_dict.values():
# 使用旧管理密钥解密
for user in member["UserData"].values():
user["Password"] = self.AUTO_decryptor(
user["Config"].get(user["Config"].Info_Password), PASSWORD_old
)
if member["Type"] == "Maa":
for user in member["UserData"].values():
user["Password"] = self.AUTO_decryptor(
user["Config"].get(user["Config"].Info_Password), PASSWORD_old
)
self.get_PASSWORD(PASSWORD_new)
for member in Config.member_dict.values():
# 使用新管理密钥重新加密
for user in member["UserData"].values():
user["Config"].set(
user["Config"].Info_Password, self.AUTO_encryptor(user["Password"])
)
user["Password"] = None
del user["Password"]
if member["Type"] == "Maa":
for user in member["UserData"].values():
user["Config"].set(
user["Config"].Info_Password,
self.AUTO_encryptor(user["Password"]),
)
user["Password"] = None
del user["Password"]
def reset_PASSWORD(self, PASSWORD_new: str) -> None:
"""重置管理密钥"""
self.get_PASSWORD(PASSWORD_new)
for member in Config.member_dict.values():
if member["Type"] == "Maa":
for user in member["UserData"].values():
user["Config"].set(user["Config"].Info_Password, "")
def win_encryptor(
self, note: str, description: str = None, entropy: bytes = None

261
app/services/skland.py Normal file
View File

@@ -0,0 +1,261 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
# This file incorporates work covered by the following copyright and
# permission notice:
#
# skland-checkin-ghaction Copyright © 2023 Yanstory
# https://github.com/Yanstory/skland-checkin-ghaction
# This file is part of AUTO_MAA.
# AUTO_MAA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
# AUTO_MAA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
# the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA森空岛服务
v4.4
作者DLmaster_361、ClozyA
"""
from loguru import logger
import time
import json
import hmac
import hashlib
import requests
from urllib import parse
from app.core import Config
def skland_sign_in(token) -> dict:
"""森空岛签到"""
app_code = "4ca99fa6b56cc2ba"
# 用于获取grant code
grant_code_url = "https://as.hypergryph.com/user/oauth2/v2/grant"
# 用于获取cred
cred_code_url = "https://zonai.skland.com/api/v1/user/auth/generate_cred_by_code"
# 查询角色绑定
binding_url = "https://zonai.skland.com/api/v1/game/player/binding"
# 签到接口
sign_url = "https://zonai.skland.com/api/v1/game/attendance"
# 基础请求头
header = {
"cred": "",
"User-Agent": "Skland/1.5.1 (com.hypergryph.skland; build:100501001; Android 34;) Okhttp/4.11.0",
"Accept-Encoding": "gzip",
"Connection": "close",
}
header_login = header.copy()
header_for_sign = {
"platform": "1",
"timestamp": "",
"dId": "",
"vName": "1.5.1",
}
# 生成签名
def generate_signature(token_for_sign: str, path, body_or_query):
"""
生成请求签名
:param token_for_sign: 用于加密的token
:param path: 请求路径(如 /api/v1/game/player/binding
:param body_or_query: GET用query字符串POST用body字符串
:return: (sign, 新的header_for_sign字典)
"""
t = str(int(time.time()) - 2) # 时间戳,-2秒以防服务器时间不一致
token_bytes = token_for_sign.encode("utf-8")
header_ca = dict(header_for_sign)
header_ca["timestamp"] = t
header_ca_str = json.dumps(header_ca, separators=(",", ":"))
s = path + body_or_query + t + header_ca_str # 拼接原始字符串
# HMAC-SHA256 + MD5得到最终sign
hex_s = hmac.new(token_bytes, s.encode("utf-8"), hashlib.sha256).hexdigest()
md5 = hashlib.md5(hex_s.encode("utf-8")).hexdigest()
return md5, header_ca
# 获取带签名的header
def get_sign_header(url: str, method, body, old_header, sign_token):
"""
获取带签名的请求头
:param url: 请求完整url
:param method: 请求方式 GET/POST
:param body: POST请求体或GET时为None
:param old_header: 原始请求头
:param sign_token: 当前会话的签名token
:return: 新请求头
"""
h = json.loads(json.dumps(old_header))
p = parse.urlparse(url)
if method.lower() == "get":
sign, header_ca = generate_signature(sign_token, p.path, p.query)
else:
sign, header_ca = generate_signature(
sign_token, p.path, json.dumps(body) if body else ""
)
h["sign"] = sign
for i in header_ca:
h[i] = header_ca[i]
return h
# 复制请求头并添加cred
def copy_header(cred):
v = json.loads(json.dumps(header))
v["cred"] = cred
return v
# 使用token一步步拿到cred和sign_token
def login_by_token(token_code):
"""
:param token_code: 你的skyland token
:return: (cred, sign_token)
"""
try:
# token为json对象时提取data.content
t = json.loads(token_code)
token_code = t["data"]["content"]
except:
pass
grant_code = get_grant_code(token_code)
return get_cred(grant_code)
# 通过grant code换cred和sign_token
def get_cred(grant):
rsp = requests.post(
cred_code_url,
json={"code": grant, "kind": 1},
headers=header_login,
proxies={
"http": Config.get(Config.update_ProxyAddress),
"https": Config.get(Config.update_ProxyAddress),
},
).json()
if rsp["code"] != 0:
raise Exception(f'获得cred失败{rsp.get("messgae")}')
sign_token = rsp["data"]["token"]
cred = rsp["data"]["cred"]
return cred, sign_token
# 通过token换grant code
def get_grant_code(token):
rsp = requests.post(
grant_code_url,
json={"appCode": app_code, "token": token, "type": 0},
headers=header_login,
proxies={
"http": Config.get(Config.update_ProxyAddress),
"https": Config.get(Config.update_ProxyAddress),
},
).json()
if rsp["status"] != 0:
raise Exception(
f'使用token: {token[:3]}******{token[-3:]} 获得认证代码失败:{rsp.get("msg")}'
)
return rsp["data"]["code"]
# 获取已绑定的角色列表
def get_binding_list(cred, sign_token):
"""
查询绑定的角色
:param cred: 当前cred
:param sign_token: 当前sign_token
:return: 角色列表
"""
v = []
rsp = requests.get(
binding_url,
headers=get_sign_header(
binding_url, "get", None, copy_header(cred), sign_token
),
proxies={
"http": Config.get(Config.update_ProxyAddress),
"https": Config.get(Config.update_ProxyAddress),
},
).json()
if rsp["code"] != 0:
logger.error(f"森空岛服务 | 请求角色列表出现问题:{rsp['message']}")
if rsp.get("message") == "用户未登录":
logger.error(f"森空岛服务 | 用户登录可能失效了,请重新登录!")
return v
# 只取明日方舟arknights的绑定账号
for i in rsp["data"]["list"]:
if i.get("appCode") != "arknights":
continue
v.extend(i.get("bindingList"))
return v
# 执行签到
def do_sign(cred, sign_token) -> dict:
"""
对所有绑定的角色进行签到
:param cred: 当前cred
:param sign_token: 当前sign_token
:return: 签到结果字典
"""
characters = get_binding_list(cred, sign_token)
result = {"成功": [], "重复": [], "失败": [], "总计": len(characters)}
for character in characters:
body = {
"uid": character.get("uid"),
"gameId": character.get("channelMasterId"),
}
rsp = requests.post(
sign_url,
headers=get_sign_header(
sign_url, "post", body, copy_header(cred), sign_token
),
json=body,
proxies={
"http": Config.get(Config.update_ProxyAddress),
"https": Config.get(Config.update_ProxyAddress),
},
).json()
if rsp["code"] != 0:
result[
"重复" if rsp.get("message") == "请勿重复签到!" else "失败"
].append(
f"{character.get("nickName")}{character.get("channelName")}"
)
else:
result["成功"].append(
f"{character.get("nickName")}{character.get("channelName")}"
)
time.sleep(3)
return result
# 主流程
try:
# 拿到cred和sign_token
cred, sign_token = login_by_token(token)
time.sleep(1)
# 依次签到
return do_sign(cred, sign_token)
except Exception as e:
logger.error(f"森空岛服务 | 森空岛签到失败: {e}")
return {"成功": [], "重复": [], "失败": [], "总计": 0}

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA系统服务
v4.3
v4.4
作者DLmaster_361
"""
@@ -31,9 +31,11 @@ import sys
import ctypes
import win32gui
import win32process
import winreg
import psutil
import subprocess
import tempfile
import getpass
from datetime import datetime
from pathlib import Path
from app.core import Config
@@ -65,23 +67,119 @@ class _SystemHandler:
"""同步开机自启"""
if Config.get(Config.start_IfSelfStart) and not self.is_startup():
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Run",
winreg.KEY_SET_VALUE,
winreg.KEY_ALL_ACCESS | winreg.KEY_WRITE | winreg.KEY_CREATE_SUB_KEY,
)
winreg.SetValueEx(key, "AUTO_MAA", 0, winreg.REG_SZ, Config.app_path_sys)
winreg.CloseKey(key)
# 创建任务计划
try:
# 获取当前用户和时间
current_user = getpass.getuser()
current_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
# XML 模板
xml_content = f"""<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Date>{current_time}</Date>
<Author>{current_user}</Author>
<Description>AUTO_MAA自启动服务</Description>
<URI>\\AUTO_MAA_AutoStart</URI>
</RegistrationInfo>
<Triggers>
<LogonTrigger>
<StartBoundary>{current_time}</StartBoundary>
<Enabled>true</Enabled>
</LogonTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<LogonType>InteractiveToken</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>false</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>false</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>"{Config.app_path_sys}"</Command>
</Exec>
</Actions>
</Task>"""
# 创建临时 XML 文件并执行
with tempfile.NamedTemporaryFile(
mode="w", suffix=".xml", delete=False, encoding="utf-16"
) as f:
f.write(xml_content)
xml_file = f.name
try:
result = subprocess.run(
[
"schtasks",
"/create",
"/tn",
"AUTO_MAA_AutoStart",
"/xml",
xml_file,
"/f",
],
creationflags=subprocess.CREATE_NO_WINDOW,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
)
if result.returncode == 0:
logger.info(f"任务计划程序自启动已创建: {Config.app_path_sys}")
else:
logger.error(f"创建任务计划失败: {result.stderr}")
finally:
# 删除临时文件
try:
Path(xml_file).unlink()
except:
pass
except Exception as e:
logger.exception(f"设置任务计划程序自启动失败: {e}")
elif not Config.get(Config.start_IfSelfStart) and self.is_startup():
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Run",
winreg.KEY_SET_VALUE,
winreg.KEY_ALL_ACCESS | winreg.KEY_WRITE | winreg.KEY_CREATE_SUB_KEY,
)
winreg.DeleteValue(key, "AUTO_MAA")
winreg.CloseKey(key)
try:
result = subprocess.run(
["schtasks", "/delete", "/tn", "AUTO_MAA_AutoStart", "/f"],
creationflags=subprocess.CREATE_NO_WINDOW,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
)
if result.returncode == 0:
logger.info("任务计划程序自启动已删除")
else:
logger.error(f"删除任务计划失败: {result.stderr}")
except Exception as e:
logger.exception(f"删除任务计划程序自启动失败: {e}")
def set_power(self, mode) -> None:
@@ -112,6 +210,7 @@ class _SystemHandler:
Config.main_window.close()
QApplication.quit()
sys.exit(0)
elif sys.platform.startswith("linux"):
@@ -138,23 +237,22 @@ class _SystemHandler:
Config.main_window.close()
QApplication.quit()
sys.exit(0)
def is_startup(self) -> bool:
"""判断程序是否已经开机自启"""
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Run",
0,
winreg.KEY_READ,
)
try:
value, _ = winreg.QueryValueEx(key, "AUTO_MAA")
winreg.CloseKey(key)
return True
except FileNotFoundError:
winreg.CloseKey(key)
result = subprocess.run(
["schtasks", "/query", "/tn", "AUTO_MAA_AutoStart"],
creationflags=subprocess.CREATE_NO_WINDOW,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
)
return result.returncode == 0
except Exception as e:
logger.error(f"检查任务计划程序失败: {e}")
return False
def get_window_info(self) -> list:

View File

@@ -1,6 +1,12 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
# This file incorporates work covered by the following copyright and
# permission notice:
#
# ZenlessZoneZero-OneDragon Copyright © 2024-2025 DoctorReid
# https://github.com/DoctorReid/ZenlessZoneZero-OneDragon
# This file is part of AUTO_MAA.
# AUTO_MAA is free software: you can redistribute it and/or modify
@@ -21,12 +27,14 @@
"""
AUTO_MAA
AUTO_MAA组件
v4.3
v4.4
作者DLmaster_361
"""
import os
import re
import win32com.client
from pathlib import Path
from datetime import datetime
from functools import partial
from typing import Optional, Union, List, Dict
@@ -42,6 +50,7 @@ from PySide6.QtWidgets import (
QHBoxLayout,
QVBoxLayout,
QSizePolicy,
QFileDialog,
)
from qfluentwidgets import (
LineEdit,
@@ -156,7 +165,7 @@ class ProgressRingMessageBox(MessageBoxBase):
super().__init__(parent)
self.title = SubtitleLabel(title)
self.time = 100
self.time = 100 if Config.args.mode == "gui" else 1
Widget = QWidget()
Layout = QHBoxLayout(Widget)
self.ring = ProgressRing()
@@ -560,6 +569,100 @@ class PasswordLineEditSettingCard(SettingCard):
self.LineEdit.textChanged.connect(self.__textChanged)
class PathSettingCard(PushSettingCard):
pathChanged = Signal(Path, Path)
def __init__(
self,
icon: Union[str, QIcon, FluentIconBase],
title: str,
mode: Union[str, OptionsConfigItem],
text: str,
qconfig: QConfig,
configItem: ConfigItem,
parent=None,
):
super().__init__(text, icon, title, "未设置", parent)
self.title = title
self.mode = mode
self.qconfig = qconfig
self.configItem = configItem
if isinstance(mode, OptionsConfigItem):
self.ComboBox = ComboBox(self)
self.hBoxLayout.insertWidget(5, self.ComboBox, 0, Qt.AlignRight)
for option in mode.options:
self.ComboBox.addItem(option, userData=option)
self.ComboBox.setCurrentText(self.qconfig.get(mode))
self.ComboBox.currentIndexChanged.connect(self._onCurrentIndexChanged)
mode.valueChanged.connect(self.setValue)
self.setContent(self.qconfig.get(self.configItem))
self.clicked.connect(self.ChoosePath)
self.configItem.valueChanged.connect(
lambda: self.setContent(self.qconfig.get(self.configItem))
)
def ChoosePath(self):
"""选择文件或文件夹路径"""
old_path = Path(self.qconfig.get(self.configItem))
if self.get_mode() == "文件夹":
folder = QFileDialog.getExistingDirectory(
self, "选择文件夹", self.qconfig.get(self.configItem)
)
if folder:
self.qconfig.set(self.configItem, folder)
self.pathChanged.emit(old_path, Path(folder))
else:
file_path, _ = QFileDialog.getOpenFileName(
self, "打开文件", self.qconfig.get(self.configItem), self.get_mode()
)
if file_path:
file_path = self.analysis_lnk(file_path)
self.qconfig.set(self.configItem, str(file_path))
self.pathChanged.emit(old_path, file_path)
def analysis_lnk(self, path: str) -> Path:
"""快捷方式解析"""
lnk_path = Path(path)
if lnk_path.suffix == ".lnk":
try:
shell = win32com.client.Dispatch("WScript.Shell")
shortcut = shell.CreateShortcut(str(lnk_path))
return Path(shortcut.TargetPath)
except Exception as e:
return lnk_path
else:
return lnk_path
def get_mode(self) -> str:
"""获取当前模式"""
if isinstance(self.mode, OptionsConfigItem):
return self.qconfig.get(self.mode)
return self.mode
def _onCurrentIndexChanged(self, index: int):
self.qconfig.set(self.mode, self.ComboBox.itemData(index))
def setValue(self, value):
self.ComboBox.setCurrentText(value)
self.qconfig.set(self.mode, value)
class PushAndSwitchButtonSettingCard(SettingCard):
"""Setting card with push & switch button"""
@@ -608,6 +711,83 @@ class PushAndSwitchButtonSettingCard(SettingCard):
self.switchButton.setText("" if isChecked else "")
class PasswordLineAndSwitchButtonSettingCard(SettingCard):
"""Setting card with PasswordLineEdit and SwitchButton"""
textChanged = Signal()
def __init__(
self,
icon: Union[str, QIcon, FluentIconBase],
title: str,
content: Union[str, None],
text: str,
algorithm: str,
qconfig: QConfig,
configItem_bool: ConfigItem,
configItem_info: ConfigItem,
parent=None,
):
super().__init__(icon, title, content, parent)
self.algorithm = algorithm
self.qconfig = qconfig
self.configItem_bool = configItem_bool
self.configItem_info = configItem_info
self.LineEdit = PasswordLineEdit(self)
self.LineEdit.setMinimumWidth(200)
self.LineEdit.setPlaceholderText(text)
if algorithm == "AUTO":
self.LineEdit.setViewPasswordButtonVisible(False)
self.SwitchButton = SwitchButton(self)
self.hBoxLayout.addWidget(self.LineEdit, 0, Qt.AlignRight)
self.hBoxLayout.addSpacing(16)
self.hBoxLayout.addWidget(self.SwitchButton, 0, Qt.AlignRight)
self.hBoxLayout.addSpacing(16)
self.configItem_info.valueChanged.connect(self.setInfo)
self.LineEdit.textChanged.connect(self.__textChanged)
self.configItem_bool.valueChanged.connect(self.SwitchButton.setChecked)
self.SwitchButton.checkedChanged.connect(
lambda isChecked: self.qconfig.set(self.configItem_bool, isChecked)
)
self.setInfo(self.qconfig.get(configItem_info))
self.SwitchButton.setChecked(self.qconfig.get(configItem_bool))
def __textChanged(self, content: str):
self.configItem_info.valueChanged.disconnect(self.setInfo)
if self.algorithm == "DPAPI":
self.qconfig.set(self.configItem_info, Crypto.win_encryptor(content))
elif self.algorithm == "AUTO":
self.qconfig.set(self.configItem_info, Crypto.AUTO_encryptor(content))
self.configItem_info.valueChanged.connect(self.setInfo)
self.textChanged.emit()
def setInfo(self, content: str):
self.LineEdit.textChanged.disconnect(self.__textChanged)
if self.algorithm == "DPAPI":
self.LineEdit.setText(Crypto.win_decryptor(content))
elif self.algorithm == "AUTO":
if Crypto.check_PASSWORD(Config.PASSWORD):
self.LineEdit.setText(Crypto.AUTO_decryptor(content, Config.PASSWORD))
self.LineEdit.setPasswordVisible(True)
self.LineEdit.setReadOnly(False)
elif Config.PASSWORD:
self.LineEdit.setText("管理密钥错误")
self.LineEdit.setPasswordVisible(True)
self.LineEdit.setReadOnly(True)
else:
self.LineEdit.setText("************")
self.LineEdit.setPasswordVisible(False)
self.LineEdit.setReadOnly(True)
self.LineEdit.textChanged.connect(self.__textChanged)
class PushAndComboBoxSettingCard(SettingCard):
"""Setting card with push & combo box"""
@@ -1080,6 +1260,48 @@ class TimeEditSettingCard(SettingCard):
self.TimeEdit.setTime(QTime.fromString(value, "HH:mm"))
class SubLableSettingCard(SettingCard):
"""Setting card with Sub's Lable"""
def __init__(
self,
icon: Union[str, QIcon, FluentIconBase],
title: str,
content: Union[str, None],
qconfig: QConfig,
configItems: Dict[str, ConfigItem],
parent=None,
):
super().__init__(icon, title, content, parent)
self.qconfig = qconfig
self.configItems = configItems
self.Lable = SubtitleLabel(self)
if configItems:
for configItem in configItems.values():
configItem.valueChanged.connect(self.setValue)
self.setValue()
self.hBoxLayout.addWidget(self.Lable, 0, Qt.AlignRight)
self.hBoxLayout.addSpacing(16)
def setValue(self):
text_list = []
if self.configItems:
text_list.append(
f"今日已代理{self.qconfig.get(self.configItems["ProxyTimes"])}"
if Config.server_date().strftime("%Y-%m-%d")
== self.qconfig.get(self.configItems["LastProxyDate"])
else "今日未进行代理"
)
self.Lable.setText(" | ".join(text_list))
class UserLableSettingCard(SettingCard):
"""Setting card with User's Lable"""
@@ -1129,6 +1351,13 @@ class UserLableSettingCard(SettingCard):
== Config.server_date().isocalendar()[:2]
else "本周剿灭未完成"
)
if self.qconfig.get(self.configItems["IfSkland"]):
text_list.append(
"森空岛已签到"
if datetime.now().strftime("%Y-%m-%d")
== self.qconfig.get(self.configItems["LastSklandDate"])
else "森空岛未签到"
)
self.Lable.setText(" | ".join(text_list))
@@ -1251,13 +1480,18 @@ class UserNoticeSettingCard(PushAndSwitchButtonSettingCard):
if not (
self.qconfig.get(self.configItems["IfSendStatistic"])
or self.qconfig.get(self.configItems["IfSendSixStar"])
or (
"IfSendSixStar" in self.configItems
and self.qconfig.get(self.configItems["IfSendSixStar"])
)
):
text_list.append("未启用任何通知项")
if self.qconfig.get(self.configItems["IfSendStatistic"]):
text_list.append("统计信息已启用")
if self.qconfig.get(self.configItems["IfSendSixStar"]):
if "IfSendSixStar" in self.configItems and self.qconfig.get(
self.configItems["IfSendSixStar"]
):
text_list.append("六星喜报已启用")
if self.qconfig.get(self.configItems["IfSendMail"]):

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA图形化界面包
v4.3
v4.4
作者DLmaster_361
"""

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA调度中枢界面
v4.3
v4.4
作者DLmaster_361
"""
@@ -210,8 +210,8 @@ class DispatchCenter(QWidget):
self.script_list["主调度台"].top_bar.object.addItem(
(
f"实例 - {info['Type']}"
if info["Config"].get(info["Config"].MaaSet_Name) == ""
else f"实例 - {info['Type']} - {info["Config"].get(info["Config"].MaaSet_Name)}"
if info["Config"].get_name() == ""
else f"实例 - {info['Type']} - {info['Config'].get_name()}"
),
userData=name,
)
@@ -284,8 +284,8 @@ class DispatchCenter(QWidget):
continue
text_list.append(
f"实例 - {info['Type']}"
if info["Config"].get(info["Config"].MaaSet_Name) == ""
else f"实例 - {info['Type']} - {info["Config"].get(info["Config"].MaaSet_Name)}"
if info["Config"].get_name() == ""
else f"实例 - {info['Type']} - {info['Config'].get_name()}"
)
data_list.append(name)
@@ -317,14 +317,12 @@ class DispatchCenter(QWidget):
elif "脚本" in choice.input[0].currentData():
if Config.member_dict[choice.input[0].currentData()]["Type"] == "Maa":
logger.info(f"用户添加任务:{choice.input[0].currentData()}")
TaskManager.add_task(
"自动代理_新调度台",
f"自定义队列 - {choice.input[0].currentData()}",
{"Queue": {"Member_1": choice.input[0].currentData()}},
)
logger.info(f"用户添加任务:{choice.input[0].currentData()}")
TaskManager.add_task(
"自动代理_新调度台",
f"自定义队列 - {choice.input[0].currentData()}",
{"Queue": {"Member_1": choice.input[0].currentData()}},
)
class DispatchBox(QWidget):
@@ -409,6 +407,18 @@ class DispatchCenter(QWidget):
)
return None
if (
"脚本" in self.object.currentData()
and Config.member_dict[self.object.currentData()]["Type"]
== "General"
and self.mode.currentData() == "人工排查"
):
logger.warning("通用脚本类型不存在人工排查功能")
MainInfoBar.push_info_bar(
"warning", "不支持的任务", "通用脚本无人工排查功能", 5000
)
return None
if "调度队列" in self.object.currentData():
logger.info(f"用户添加任务:{self.object.currentData()}")
@@ -420,14 +430,12 @@ class DispatchCenter(QWidget):
elif "脚本" in self.object.currentData():
if Config.member_dict[self.object.currentData()]["Type"] == "Maa":
logger.info(f"用户添加任务:{self.object.currentData()}")
TaskManager.add_task(
f"{self.mode.currentText()}_主调度台",
"自定义队列",
{"Queue": {"Member_1": self.object.currentData()}},
)
logger.info(f"用户添加任务:{self.object.currentData()}")
TaskManager.add_task(
f"{self.mode.currentText()}_主调度台",
"自定义队列",
{"Queue": {"Member_1": self.object.currentData()}},
)
class DispatchInfoCard(HeaderCardWidget):

View File

@@ -21,15 +21,15 @@
"""
AUTO_MAA
AUTO_MAA更新器
v4.3
v4.4
作者DLmaster_361
"""
from loguru import logger
import zipfile
import requests
import subprocess
import time
import psutil
from functools import partial
from pathlib import Path
@@ -46,6 +46,9 @@ from PySide6.QtCore import QThread, Signal, QTimer, QEventLoop
from typing import List, Dict, Union
from app.core import Config
from app.services import System
def version_text(version_numb: list) -> str:
"""将版本号列表转为可读的文本信息"""
@@ -78,19 +81,30 @@ class DownloadProcess(QThread):
) -> None:
super(DownloadProcess, self).__init__()
self.setObjectName(f"DownloadProcess-{url}-{start_byte}-{end_byte}")
self.url = url
self.start_byte = start_byte
self.end_byte = end_byte
self.download_path = download_path
self.check_times = check_times
@logger.catch
def run(self) -> None:
# 清理可能存在的临时文件
if self.download_path.exists():
self.download_path.unlink()
headers = {"Range": f"bytes={self.start_byte}-{self.end_byte}"}
logger.info(
f"开始下载:{self.url},范围:{self.start_byte}-{self.end_byte},存储地址:{self.download_path}"
)
headers = (
{"Range": f"bytes={self.start_byte}-{self.end_byte}"}
if not (self.start_byte == -1 or self.end_byte == -1)
else None
)
while not self.isInterruptionRequested() and self.check_times != 0:
@@ -99,17 +113,30 @@ class DownloadProcess(QThread):
start_time = time.time()
response = requests.get(
self.url, headers=headers, timeout=10, stream=True
self.url,
headers=headers,
timeout=10,
stream=True,
proxies={
"http": Config.get(Config.update_ProxyAddress),
"https": Config.get(Config.update_ProxyAddress),
},
)
if response.status_code != 206:
if response.status_code not in [200, 206]:
if self.check_times != -1:
self.check_times -= 1
logger.error(
f"连接失败:{self.url},状态码:{response.status_code},剩余重试次数:{self.check_times}"
)
time.sleep(1)
continue
logger.info(f"连接成功:{self.url},状态码:{response.status_code}")
downloaded_size = 0
with self.download_path.open(mode="wb") as f:
@@ -128,10 +155,14 @@ class DownloadProcess(QThread):
if self.download_path.exists():
self.download_path.unlink()
self.accomplish.emit(0)
logger.info(f"下载中止:{self.url}")
else:
self.accomplish.emit(time.time() - start_time)
logger.success(
f"下载完成:{self.url},实际下载大小:{downloaded_size} 字节,耗时:{time.time() - start_time:.2f}"
)
break
@@ -139,6 +170,10 @@ class DownloadProcess(QThread):
if self.check_times != -1:
self.check_times -= 1
logger.exception(
f"下载出错:{self.url},错误信息:{e},剩余重试次数:{self.check_times}"
)
time.sleep(1)
else:
@@ -146,6 +181,7 @@ class DownloadProcess(QThread):
if self.download_path.exists():
self.download_path.unlink()
self.accomplish.emit(0)
logger.error(f"下载失败:{self.url}")
class ZipExtractProcess(QThread):
@@ -157,14 +193,19 @@ class ZipExtractProcess(QThread):
def __init__(self, name: str, app_path: Path, download_path: Path) -> None:
super(ZipExtractProcess, self).__init__()
self.setObjectName(f"ZipExtractProcess-{name}")
self.name = name
self.app_path = app_path
self.download_path = download_path
@logger.catch
def run(self) -> None:
try:
logger.info(f"开始解压:{self.download_path}{self.app_path}")
while True:
if self.isInterruptionRequested():
@@ -174,13 +215,15 @@ class ZipExtractProcess(QThread):
with zipfile.ZipFile(self.download_path, "r") as zip_ref:
zip_ref.extractall(self.app_path)
self.accomplish.emit()
logger.success(f"解压完成:{self.download_path}{self.app_path}")
break
except PermissionError:
if self.name == "AUTO_MAA":
self.info.emit(f"解压出错AUTO_MAA正在运行正在尝试将其关闭")
self.kill_process(self.app_path / "AUTO_MAA.exe")
System.kill_process(self.app_path / "AUTO_MAA.exe")
else:
self.info.emit(f"解压出错:{self.name}正在运行,正在等待其关闭")
logger.warning(f"解压出错:{self.name}正在运行,正在等待其关闭")
time.sleep(1)
except Exception as e:
@@ -188,32 +231,9 @@ class ZipExtractProcess(QThread):
e = str(e)
e = "\n".join([e[_ : _ + 75] for _ in range(0, len(e), 75)])
self.info.emit(f"解压更新时出错:\n{e}")
logger.exception(f"解压更新时出错:{e}")
return None
def kill_process(self, path: Path) -> None:
"""根据路径中止进程"""
for pid in self.search_pids(path):
killprocess = subprocess.Popen(
f"taskkill /F /PID {pid}",
shell=True,
creationflags=subprocess.CREATE_NO_WINDOW,
)
killprocess.wait()
def search_pids(self, path: Path) -> list:
"""根据路径查找进程PID"""
pids = []
for proc in psutil.process_iter(["pid", "exe"]):
try:
if proc.info["exe"] and proc.info["exe"].lower() == str(path).lower():
pids.append(proc.info["pid"])
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
# 进程可能在此期间已结束或无法访问,忽略这些异常
pass
return pids
class DownloadManager(QDialog):
"""下载管理器"""
@@ -234,6 +254,7 @@ class DownloadManager(QDialog):
self.download_path = app_path / "DOWNLOAD_TEMP.zip" # 临时下载文件的路径
self.download_process_dict: Dict[str, DownloadProcess] = {}
self.timer_dict: Dict[str, QTimer] = {}
self.if_speed_test_accomplish = False
self.resize(700, 70)
@@ -318,6 +339,10 @@ class DownloadManager(QDialog):
allow_redirects=True,
timeout=10,
stream=True,
proxies={
"http": Config.get(Config.update_ProxyAddress),
"https": Config.get(Config.update_ProxyAddress),
},
) as response:
if response.status_code == 200:
return response.url
@@ -325,7 +350,14 @@ class DownloadManager(QDialog):
elif self.config["mode"] == "MirrorChyan":
with requests.get(
self.config["url"], allow_redirects=True, timeout=10, stream=True
self.config["url"],
allow_redirects=True,
timeout=10,
stream=True,
proxies={
"http": Config.get(Config.update_ProxyAddress),
"https": Config.get(Config.update_ProxyAddress),
},
) as response:
if response.status_code == 200:
return response.url
@@ -338,6 +370,8 @@ class DownloadManager(QDialog):
url_dict = self.get_download_url("测速")
self.test_speed_result: Dict[str, float] = {}
logger.info(f"测速链接:{url_dict}")
for name, url in url_dict.items():
if self.isInterruptionRequested:
@@ -405,11 +439,21 @@ class DownloadManager(QDialog):
if not self.download_process_dict:
self.download_process_clear.emit()
# 当有速度大于1 MB/s的链接或存在3个即以上链接测速完成时停止其他测速
if not self.if_speed_test_accomplish and (
sum(1 for speed in self.test_speed_result.values() if speed > 0) >= 3
or any(speed > 1 for speed in self.test_speed_result.values())
):
self.if_speed_test_accomplish = True
for timer in self.timer_dict.values():
timer.timeout.emit()
if any(speed == -1 for _, speed in self.test_speed_result.items()):
return None
# 保存测速结果
self.config["speed_result"] = self.test_speed_result
logger.info(f"测速结果:{self.test_speed_result}")
self.update_info("测速完成!")
self.speed_test_accomplish.emit()
@@ -422,7 +466,14 @@ class DownloadManager(QDialog):
url = self.get_download_url("下载")
self.downloaded_size_list: List[List[int, bool]] = []
response = requests.head(url, timeout=10)
response = requests.head(
url,
timeout=10,
proxies={
"http": Config.get(Config.update_ProxyAddress),
"https": Config.get(Config.update_ProxyAddress),
},
)
self.file_size = int(response.headers.get("content-length", 0))
part_size = self.file_size // self.config["thread_numb"]
@@ -448,8 +499,8 @@ class DownloadManager(QDialog):
# 创建下载子线程
self.download_process_dict[f"part{i}"] = DownloadProcess(
url,
start_byte,
end_byte,
-1 if self.config["mode"] == "MirrorChyan" else start_byte,
-1 if self.config["mode"] == "MirrorChyan" else end_byte,
self.download_path.with_suffix(f".part{i}"),
1 if self.config["mode"] == "MirrorChyan" else -1,
)
@@ -508,6 +559,9 @@ class DownloadManager(QDialog):
return None
# 合并下载的分段文件
logger.info(
f"所有分段下载完成:{self.name},开始合并分段文件到 {self.download_path}"
)
with self.download_path.open(mode="wb") as outfile:
for i in range(self.config["thread_numb"]):
with self.download_path.with_suffix(f".part{i}").open(
@@ -516,6 +570,10 @@ class DownloadManager(QDialog):
outfile.write(infile.read())
self.download_path.with_suffix(f".part{i}").unlink()
logger.success(
f"合并完成:{self.name},下载文件大小:{self.download_path.stat().st_size} 字节"
)
self.update_info("正在解压更新文件")
self.update_progress(0, 0, 0)
@@ -566,6 +624,9 @@ class DownloadManager(QDialog):
self.progress_2.setValue(current)
def requestInterruption(self) -> None:
"""请求中断下载任务"""
logger.info("收到下载任务中止请求")
self.isInterruptionRequested = True

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA历史记录界面
v4.3
v4.4
作者DLmaster_361
"""
@@ -48,7 +48,7 @@ import subprocess
from datetime import datetime, timedelta
from functools import partial
from pathlib import Path
from typing import Union, List, Dict
from typing import List, Dict
from app.core import Config, SoundPlayer
@@ -100,9 +100,9 @@ class History(QWidget):
datetime(end_date.year(), end_date.month(), end_date.day()),
)
for date, user in history_dict.items():
for date, user_dict in history_dict.items():
self.history_card_list.append(self.HistoryCard(mode, date, user, self))
self.history_card_list.append(self.HistoryCard(date, user_dict, self))
self.content_layout.addWidget(self.history_card_list[-1])
self.content_layout.addStretch(1)
@@ -172,14 +172,9 @@ class History(QWidget):
self.search.clicked.emit()
class HistoryCard(QuickExpandGroupCard):
"""历史记录卡片"""
def __init__(
self,
mode: str,
date: str,
user: Union[List[Path], Dict[str, List[Path]]],
parent=None,
):
def __init__(self, date: str, user_dict: Dict[str, List[Path]], parent=None):
super().__init__(
FluentIcon.HISTORY, date, f"{date}的历史运行记录与统计信息", parent
)
@@ -192,64 +187,28 @@ class History(QWidget):
self.user_history_card_list = []
if mode == "按日合并":
for user_path in user:
self.user_history_card_list.append(
self.UserHistoryCard(mode, user_path.stem, user_path, self)
)
Layout.addWidget(self.user_history_card_list[-1])
elif mode in ["按周合并", "按月合并"]:
for user, info in user.items():
self.user_history_card_list.append(
self.UserHistoryCard(mode, user, info, self)
)
Layout.addWidget(self.user_history_card_list[-1])
for user, info in user_dict.items():
self.user_history_card_list.append(
self.UserHistoryCard(user, info, self)
)
Layout.addWidget(self.user_history_card_list[-1])
class UserHistoryCard(HeaderCardWidget):
"""用户历史记录卡片"""
def __init__(
self,
mode: str,
name: str,
user_history: Union[Path, List[Path]],
parent=None,
):
def __init__(self, name: str, user_history: List[Path], parent=None):
super().__init__(parent)
self.setTitle(name)
if mode == "按日合并":
self.user_history = user_history
self.user_history_path = user_history
self.main_history = Config.load_maa_logs("总览", user_history)
self.index_card = self.IndexCard(
self.main_history["条目索引"], self
)
self.index_card.index_changed.connect(self.update_info)
self.viewLayout.addWidget(self.index_card)
elif mode in ["按周合并", "按月合并"]:
history = Config.merge_maa_logs("指定项", user_history)
self.main_history = {}
self.main_history["统计数据"] = {
"公招统计": list(history["recruit_statistics"].items())
}
for game_id, drops in history["drop_statistics"].items():
self.main_history["统计数据"][f"掉落统计:{game_id}"] = list(
drops.items()
)
self.index_card = self.IndexCard(self.user_history, self)
self.index_card.index_changed.connect(self.update_info)
self.statistics_card = QHBoxLayout()
self.log_card = self.LogCard(self)
self.viewLayout.addWidget(self.index_card)
self.viewLayout.addLayout(self.statistics_card)
self.viewLayout.addWidget(self.log_card)
self.viewLayout.setContentsMargins(0, 0, 0, 0)
@@ -259,19 +218,45 @@ class History(QWidget):
self.update_info("数据总览")
def get_statistics(self, mode: str) -> dict:
"""生成GUI相应结构化统计数据"""
history_info = Config.merge_statistic_info(
self.user_history if mode == "数据总览" else [Path(mode)]
)
statistics_info = {}
if "recruit_statistics" in history_info:
statistics_info["公招统计"] = list(
history_info["recruit_statistics"].items()
)
if "drop_statistics" in history_info:
for game_id, drops in history_info["drop_statistics"].items():
statistics_info[f"掉落统计:{game_id}"] = list(drops.items())
if mode == "数据总览" and "error_info" in history_info:
statistics_info["报错汇总"] = list(
history_info["error_info"].items()
)
return statistics_info
def update_info(self, index: str) -> None:
"""更新信息"""
# 移除已有统计信息UI组件
while self.statistics_card.count() > 0:
item = self.statistics_card.takeAt(0)
if item.spacerItem():
self.statistics_card.removeItem(item.spacerItem())
elif item.widget():
item.widget().deleteLater()
if index == "数据总览":
while self.statistics_card.count() > 0:
item = self.statistics_card.takeAt(0)
if item.spacerItem():
self.statistics_card.removeItem(item.spacerItem())
elif item.widget():
item.widget().deleteLater()
for name, item_list in self.main_history["统计数据"].items():
for name, item_list in self.get_statistics("数据总览").items():
statistics_card = self.StatisticsCard(name, item_list, self)
self.statistics_card.addWidget(statistics_card)
@@ -280,44 +265,24 @@ class History(QWidget):
else:
single_history = Config.load_maa_logs(
"单项",
self.user_history_path.with_suffix("")
/ f"{index.replace(":","-")}.json",
)
while self.statistics_card.count() > 0:
item = self.statistics_card.takeAt(0)
if item.spacerItem():
self.statistics_card.removeItem(item.spacerItem())
elif item.widget():
item.widget().deleteLater()
for name, item_list in single_history["统计数据"].items():
single_history = self.get_statistics(index)
log_path = Path(index).with_suffix(".log")
for name, item_list in single_history.items():
statistics_card = self.StatisticsCard(name, item_list, self)
self.statistics_card.addWidget(statistics_card)
self.log_card.text.setText(single_history["日志信息"])
with log_path.open("r", encoding="utf-8") as f:
log = f.read()
self.log_card.text.setText(log)
self.log_card.open_file.clicked.disconnect()
self.log_card.open_file.clicked.connect(
lambda: os.startfile(
self.user_history_path.with_suffix("")
/ f"{index.replace(":","-")}.log"
)
lambda: os.startfile(log_path)
)
self.log_card.open_dir.clicked.disconnect()
self.log_card.open_dir.clicked.connect(
lambda: subprocess.Popen(
[
"explorer",
"/select,",
str(
self.user_history_path.with_suffix("")
/ f"{index.replace(":","-")}.log"
),
]
)
lambda: subprocess.Popen(["explorer", "/select,", log_path])
)
self.log_card.show()
@@ -329,7 +294,7 @@ class History(QWidget):
index_changed = Signal(str)
def __init__(self, index_list: list, parent=None):
def __init__(self, history_list: List[Path], parent=None):
super().__init__(parent)
self.setTitle("记录条目")
@@ -339,11 +304,14 @@ class History(QWidget):
self.index_cards: List[StatefulItemCard] = []
index_list = Config.merge_statistic_info(history_list)["index"]
index_list.insert(0, ["数据总览", "运行", "数据总览"])
for index in index_list:
self.index_cards.append(StatefulItemCard(index))
self.index_cards.append(StatefulItemCard(index[:2]))
self.index_cards[-1].clicked.connect(
partial(self.index_changed.emit, index[0])
partial(self.index_changed.emit, str(index[2]))
)
self.Layout.addWidget(self.index_cards[-1])

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA主界面
v4.3
v4.4
作者DLmaster_361
"""
@@ -405,4 +405,6 @@ class ButtonGroup(SimpleCardWidget):
def open_sales(self):
"""打开 MirrorChyan 链接"""
QDesktopServices.openUrl(QUrl("https://mirrorchyan.com/"))
QDesktopServices.openUrl(
QUrl("https://mirrorchyan.com/zh/get-start?source=auto_maa-home")
)

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA主界面
v4.3
v4.4
作者DLmaster_361
"""
@@ -184,7 +184,7 @@ class AUTO_MAA(MSFluentWindow):
self.set_min_method()
Config.user_info_changed.connect(self.member_manager.refresh_dashboard)
Config.sub_info_changed.connect(self.member_manager.refresh_dashboard)
Config.power_sign_changed.connect(self.dispatch_center.update_power_sign)
TaskManager.create_gui.connect(self.dispatch_center.add_board)
TaskManager.connect_gui.connect(self.dispatch_center.connect_main_board)
@@ -257,6 +257,9 @@ class AUTO_MAA(MSFluentWindow):
) -> None:
"""配置窗口状态"""
if Config.args.mode != "gui":
return None
self.switch_theme()
if mode == "显示主窗口":
@@ -358,6 +361,9 @@ class AUTO_MAA(MSFluentWindow):
# 检查密码
self.setting.check_PASSWORD()
# 获取关卡号信息
Config.get_stage()
# 获取主题图像
if Config.get(Config.function_HomeImageMode) == "主题图像":
self.home.get_home_image()
@@ -379,7 +385,40 @@ class AUTO_MAA(MSFluentWindow):
self.titleBar.minBtn.click()
SoundPlayer.play("MAA在完成任务前退出")
if Config.args.config:
for config in [_ for _ in Config.args.config if _ in Config.queue_dict]:
TaskManager.add_task(
"自动代理_新调度台",
config,
Config.queue_dict["调度队列_1"]["Config"].toDict(),
)
for config in [_ for _ in Config.args.config if _ in Config.member_dict]:
TaskManager.add_task(
"自动代理_新调度台",
"自定义队列",
{"Queue": {"Member_1": config}},
)
if not any(
_ in (list(Config.member_dict.keys()) + list(Config.queue_dict.keys()))
for _ in Config.args.config
):
logger.warning(
"当前运行模式为命令行模式,由于您使用了错误的 --config 参数进行配置,程序自动退出"
)
System.set_power("KillSelf")
elif Config.args.mode == "cli":
logger.warning(
"当前运行模式为命令行模式,由于您未使用 --config 参数进行配置,程序自动退出"
)
System.set_power("KillSelf")
def clean_old_logs(self):
"""
@@ -433,7 +472,7 @@ class AUTO_MAA(MSFluentWindow):
logger.warning("启动主任务失败:未找到有效的主任务配置文件")
MainInfoBar.push_info_bar(
"warning", "启动主任务失败", "调度队列_1”与“脚本_1均不存在", -1
"warning", "启动主任务失败", "调度队列_1」与「脚本_1均不存在", -1
)
def __currentChanged(self, index: int) -> None:

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA计划管理界面
v4.3
v4.4
作者DLmaster_361
"""
@@ -156,7 +156,7 @@ class PlanManager(QWidget):
self.plan_manager.clear_SettingBox()
shutil.rmtree(Config.plan_dict[name]["Path"])
Config.change_plan(name, "禁用")
Config.change_plan(name, "固定")
for i in range(int(name[3:]) + 1, len(Config.plan_dict) + 1):
if Config.plan_dict[f"计划_{i}"]["Path"].exists():
Config.plan_dict[f"计划_{i}"]["Path"].rename(
@@ -324,7 +324,7 @@ class PlanManager(QWidget):
"""清空所有子界面"""
for sub_interface in self.script_list:
Config.gameid_refreshed.disconnect(sub_interface.refresh_gameid)
Config.stage_refreshed.disconnect(sub_interface.refresh_stage)
self.stackedWidget.removeWidget(sub_interface)
sub_interface.deleteLater()
self.script_list.clear()
@@ -372,7 +372,7 @@ class PlanManager(QWidget):
self.table = TableWidget(self)
self.table.setColumnCount(8)
self.table.setRowCount(6)
self.table.setRowCount(7)
self.table.setHorizontalHeaderLabels(
["全局", "周一", "周二", "周三", "周四", "周五", "周六", "周日"]
)
@@ -383,6 +383,7 @@ class PlanManager(QWidget):
"关卡选择",
"备选 - 1",
"备选 - 2",
"备选 - 3",
"剩余理智",
]
)
@@ -392,7 +393,7 @@ class PlanManager(QWidget):
self.table.horizontalHeader().setSectionResizeMode(
col, QHeaderView.ResizeMode.Stretch
)
for row in range(6):
for row in range(7):
self.table.verticalHeader().setSectionResizeMode(
row, QHeaderView.ResizeMode.ResizeToContents
)
@@ -427,21 +428,21 @@ class PlanManager(QWidget):
configItem=configItem,
parent=self,
)
elif name == "GameId_Remain":
elif name == "Stage_Remain":
self.item_dict[group][name] = EditableComboBoxSetting(
value=Config.gameid_dict[group]["value"],
value=Config.stage_dict[group]["value"],
texts=[
"不使用" if _ == "当前/上次" else _
for _ in Config.gameid_dict[group]["text"]
for _ in Config.stage_dict[group]["text"]
],
qconfig=self.config,
configItem=configItem,
parent=self,
)
elif "GameId" in name:
elif "Stage" in name:
self.item_dict[group][name] = EditableComboBoxSetting(
value=Config.gameid_dict[group]["value"],
texts=Config.gameid_dict[group]["text"],
value=Config.stage_dict[group]["value"],
texts=Config.stage_dict[group]["text"],
qconfig=self.config,
configItem=configItem,
parent=self,
@@ -459,7 +460,7 @@ class PlanManager(QWidget):
self.viewLayout.setContentsMargins(3, 0, 3, 3)
self.card_Mode.comboBox.currentIndexChanged.connect(self.switch_mode)
Config.gameid_refreshed.connect(self.refresh_gameid)
Config.stage_refreshed.connect(self.refresh_stage)
self.switch_mode()
@@ -473,25 +474,25 @@ class PlanManager(QWidget):
== (self.config.get(self.config.Info_Mode) == "ALL")
)
def refresh_gameid(self):
def refresh_stage(self):
for group, name_dict in self.item_dict.items():
for name, setting_item in name_dict.items():
if name == "GameId_Remain":
if name == "Stage_Remain":
setting_item.reLoadOptions(
Config.gameid_dict[group]["value"],
Config.stage_dict[group]["value"],
[
"不使用" if _ == "当前/上次" else _
for _ in Config.gameid_dict[group]["text"]
for _ in Config.stage_dict[group]["text"]
],
)
elif "GameId" in name:
elif "Stage" in name:
setting_item.reLoadOptions(
Config.gameid_dict[group]["value"],
Config.gameid_dict[group]["text"],
Config.stage_dict[group]["value"],
Config.stage_dict[group]["text"],
)

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA调度队列界面
v4.3
v4.4
作者DLmaster_361
"""
@@ -111,7 +111,7 @@ class QueueManager(QWidget):
"Config": queue_config,
}
self.queue_manager.add_QueueSettingBox(index)
self.queue_manager.add_SettingBox(index)
self.queue_manager.switch_SettingBox(index)
logger.success(f"调度队列_{index} 添加成功")
@@ -256,8 +256,8 @@ class QueueManager(QWidget):
+ [
(
k
if v["Config"].get(v["Config"].MaaSet_Name) == ""
else f"{k} - {v["Config"].get(v["Config"].MaaSet_Name)}"
if v["Config"].get_name() == ""
else f"{k} - {v["Config"].get_name()}"
)
for k, v in Config.member_dict.items()
],
@@ -332,7 +332,7 @@ class QueueManager(QWidget):
Config.search_queue()
for name in Config.queue_dict.keys():
self.add_QueueSettingBox(int(name[5:]))
self.add_SettingBox(int(name[5:]))
self.switch_SettingBox(index)
@@ -358,12 +358,12 @@ class QueueManager(QWidget):
self.script_list.clear()
self.pivot.clear()
def add_QueueSettingBox(self, uid: int) -> None:
def add_SettingBox(self, uid: int) -> None:
"""添加一个调度队列设置界面"""
maa_setting_box = self.QueueMemberSettingBox(uid, self)
setting_box = self.QueueMemberSettingBox(uid, self)
self.script_list.append(maa_setting_box)
self.script_list.append(setting_box)
self.stackedWidget.addWidget(self.script_list[-1])
@@ -586,8 +586,8 @@ class QueueManager(QWidget):
+ [
(
k
if v["Config"].get(v["Config"].MaaSet_Name) == ""
else f"{k} - {v["Config"].get(v["Config"].MaaSet_Name)}"
if v["Config"].get_name() == ""
else f"{k} - {v["Config"].get_name()}"
)
for k, v in Config.member_dict.items()
],

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA设置界面
v4.3
v4.4
作者DLmaster_361
"""
@@ -86,6 +86,7 @@ class Setting(QWidget):
)
self.start.card_IfSelfStart.checkedChanged.connect(System.set_SelfStart)
self.security.card_changePASSWORD.clicked.connect(self.change_PASSWORD)
self.security.card_resetPASSWORD.clicked.connect(self.reset_PASSWORD)
self.updater.card_CheckUpdate.clicked.connect(
lambda: self.check_update(if_show=True)
)
@@ -119,7 +120,7 @@ class Setting(QWidget):
choice = MessageBox(
"授权声明",
"开启托管bilibili游戏隐私政策功能即代表您已完整阅读并同意《哔哩哔哩弹幕网用户使用协议》、《哔哩哔哩隐私政策》和《哔哩哔哩游戏中心用户协议》并授权AUTO_MAA在其认定需要时以其认定合适的方法替您处理相关弹窗\n\n是否同意授权?",
"开启托管bilibili游戏隐私政策功能即代表您已完整阅读并同意《哔哩哔哩弹幕网用户使用协议》、《哔哩哔哩隐私政策》和《哔哩哔哩游戏中心用户协议》并授权AUTO_MAA在其认定需要时以其认定合适的方法替您处理相关弹窗\n\n是否同意授权?",
self.window(),
)
if choice.exec():
@@ -147,7 +148,7 @@ class Setting(QWidget):
choice = MessageBox(
"风险声明",
"开启跳过MuMu启动广告功能即代表您已安装MuMu模拟器-12且允许AUTO_MAA以其认定合适的方法屏蔽MuMu启动广告并接受此操作带来的风险\n\n此功能即时生效,是否仍要开启此功能?",
"开启跳过MuMu启动广告功能即代表您已安装MuMu模拟器-12且允许AUTO_MAA以其认定合适的方法屏蔽MuMu启动广告并接受此操作带来的风险\n\n此功能即时生效,是否仍要开启此功能?",
self.window(),
)
if choice.exec():
@@ -259,6 +260,61 @@ class Setting(QWidget):
if choice.exec():
break
def reset_PASSWORD(self) -> None:
"""重置管理密钥"""
choice = MessageBox(
"确认",
"重置管理密钥将清空所有使用管理密钥加密的数据,您确认要重置管理密钥吗?",
self.window(),
)
if choice.exec():
choice = LineEditMessageBox(
self.window(),
"请输入文本提示框内的验证信息",
"AUTO_MAA绝赞DeBug中",
"明文",
)
if choice.exec() and choice.input.text() in [
"AUTO_MAA绝赞DeBug中",
"AUTO_MAA绝赞DeBug中!",
]:
# 获取新的管理密钥
while True:
choice = LineEditMessageBox(
self.window(), "请输入新的管理密钥", "新管理密钥", "密码"
)
if choice.exec() and choice.input.text() != "":
# 重置管理密钥
Crypto.reset_PASSWORD(choice.input.text())
MainInfoBar.push_info_bar(
"success", "操作成功", "管理密钥重置成功", 3000
)
break
else:
choice = MessageBox(
"确认",
"您没有输入新的管理密钥,是否取消修改管理密钥?",
self.window(),
)
if choice.exec():
break
else:
MainInfoBar.push_info_bar(
"info",
"验证未通过",
"请输入「AUTO_MAA绝赞DeBug中」后单击确认键",
3000,
)
def check_update(self, if_show: bool = False, if_first: bool = False) -> None:
"""检查版本更新,调起文件下载进程"""
@@ -541,6 +597,32 @@ class Setting(QWidget):
mode="w", encoding="utf-8"
) as f:
json.dump(notice, f, ensure_ascii=False, indent=4)
else:
import random
if random.random() < 0.1:
cc = NoticeMessageBox(
self.window(),
"用户守则",
{
"用户守则 - 第一版": """
0. 用户守则的每一条都应该清晰可读、不含任何语法错误。如果发现任何一条不符合以上描述,请忽视它。
1. AUTO_MAA 的所有版本均包含完整源代码与 LICENSE 文件,若发现此内容缺失,请立即关闭软件,并联系最近的 AUTO_MAA 开发者。
2. AUTO_MAA 不会对您许下任何承诺,请自行保护好自己的数据,若软件运行过程中发生了数据损坏,项目组不负任何责任。
3. AUTO_MAA 只会注册一个启动项,若发现两个 AUTO_MAA 同时自启动,请立即使用系统或杀软的 **启动项管理** 功能删除所有名为 AUTO_MAA 的启动项后重启软件。
4. AUTO_MAA 正式版不应该包含命令行窗口,如果您看到了它,请立即关闭软件,通过 AUTO_MAA.exe 文件重新打开软件。
5. 深色模式是危险的,但并非无法使用。
6. 第 0 条规则不存在。如果你看到了,请忘记它,并正常使用软件
7. **Mirror 酱** 是善良的,你只要付出小小的代价,就能得到祂的庇护。
8. AUTO_MAA 没有实时合成语音的能力,软件所有语音都存储在本地。如果听到本地不存在的语音,立即关闭扬声器,并检查是否有未知脚本在运行。
9. AUTO_MAA 不会在周六凌晨更新。如果收到更新提示,请忽略,不要查看更新内容,直到第二天天亮。
10. 用户守则仅有一页""",
"--- 标记文档中止 ---": "xdfv-serfcx-jiol,m: !1 $bad food of do $5b 9630-300 $daad 100-1\n\n// 0 == o //\n\n∠( °ω°)/",
},
)
cc.button_cancel.hide()
cc.button_layout.insertStretch(0, 1)
cc.exec()
elif (
datetime.now()
@@ -644,7 +726,7 @@ class FunctionSettingCard(HeaderCardWidget):
self.card_BossKey = LineEditSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="模拟器老板键",
content="请输入对应的模拟器老板键,请直接输入文字,多个键位之间请用“+”隔开。如:Alt+Q",
content="请输入对应的模拟器老板键,请直接输入文字,多个键位之间请用「+」隔开。如:Alt+Q",
text="请以文字形式输入模拟器老板快捷键",
qconfig=Config,
configItem=Config.function_BossKey,
@@ -955,7 +1037,7 @@ class NotifySettingCard(HeaderCardWidget):
self.card_ServerChanChannel = LineEditSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="ServerChanChannel代码",
content="可以留空,留空则默认。可以多个,请使用“|”隔开",
content="可以留空,留空则默认。可以多个,请使用「|」隔开",
text="请输入需要推送的Channel代码SCT生效",
qconfig=Config,
configItem=Config.notify_ServerChanChannel,
@@ -964,7 +1046,7 @@ class NotifySettingCard(HeaderCardWidget):
self.card_ServerChanTag = LineEditSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="Tag内容",
content="可以留空,留空则默认。可以多个,请使用“|”隔开",
content="可以留空,留空则默认。可以多个,请使用「|」隔开",
text="请输入加入推送的TagSC3生效",
qconfig=Config,
configItem=Config.notify_ServerChanTag,
@@ -1030,9 +1112,17 @@ class SecuritySettingCard(HeaderCardWidget):
content="修改用于解密用户密码的管理密钥",
parent=self,
)
self.card_resetPASSWORD = PushSettingCard(
text="重置",
icon=FluentIcon.VPN,
title="重置管理密钥",
content="重置用于解密用户密码的管理密钥",
parent=self,
)
Layout = QVBoxLayout()
Layout.addWidget(self.card_changePASSWORD)
Layout.addWidget(self.card_resetPASSWORD)
self.viewLayout.addLayout(Layout)
@@ -1074,6 +1164,15 @@ class UpdaterSettingCard(HeaderCardWidget):
configItem=Config.update_ThreadNumb,
parent=self,
)
self.card_ProxyAddress = LineEditSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="网络代理地址",
content="使用网络代理软件时,若出现网络连接问题,请尝试设置代理地址,此设置全局生效",
text="请输入代理地址",
qconfig=Config,
configItem=Config.update_ProxyAddress,
parent=self,
)
self.card_ProxyUrlList = UrlListSettingCard(
icon=FluentIcon.SETTING,
title="代理地址列表",
@@ -1093,7 +1192,9 @@ class UpdaterSettingCard(HeaderCardWidget):
parent=self,
)
mirrorchyan_url = HyperlinkButton(
"https://mirrorchyan.com/", "获取Mirror酱CDK", self
"https://mirrorchyan.com/zh/get-start?source=auto_maa-setting_card",
"获取Mirror酱CDK",
self,
)
self.card_MirrorChyanCDK.hBoxLayout.insertWidget(
5, mirrorchyan_url, 0, Qt.AlignRight
@@ -1104,6 +1205,7 @@ class UpdaterSettingCard(HeaderCardWidget):
Layout.addWidget(self.card_IfAutoUpdate)
Layout.addWidget(self.card_UpdateType)
Layout.addWidget(self.card_ThreadNumb)
Layout.addWidget(self.card_ProxyAddress)
Layout.addWidget(self.card_ProxyUrlList)
Layout.addWidget(self.card_MirrorChyanCDK)
self.viewLayout.addLayout(Layout)

95
app/utils/ImageUtils.py Normal file
View File

@@ -0,0 +1,95 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2025 ClozyA
# This file is part of AUTO_MAA.
# AUTO_MAA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
# AUTO_MAA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
# the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA图像组件
v4.4
作者ClozyA
"""
import base64
import hashlib
from pathlib import Path
from PIL import Image
class ImageUtils:
@staticmethod
def get_base64_from_file(image_path):
"""从本地文件读取并返回base64编码字符串"""
with open(image_path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
@staticmethod
def calculate_md5_from_file(image_path):
"""从本地文件读取并返回md5值hex字符串"""
with open(image_path, "rb") as f:
return hashlib.md5(f.read()).hexdigest()
@staticmethod
def calculate_md5_from_base64(base64_content):
"""从base64字符串计算md5"""
image_data = base64.b64decode(base64_content)
return hashlib.md5(image_data).hexdigest()
@staticmethod
def compress_image_if_needed(image_path: Path, max_size_mb=2) -> Path:
"""
如果图片大于max_size_mb则压缩并覆盖原文件返回原始路径Path对象
"""
if hasattr(Image, "Resampling"): # Pillow 9.1.0及以后
RESAMPLE = Image.Resampling.LANCZOS
else:
RESAMPLE = Image.ANTIALIAS
max_size = max_size_mb * 1024 * 1024
if image_path.stat().st_size <= max_size:
return image_path
img = Image.open(image_path)
suffix = image_path.suffix.lower()
quality = 90 if suffix in [".jpg", ".jpeg"] else None
step = 5
if suffix in [".jpg", ".jpeg"]:
while True:
img.save(image_path, quality=quality, optimize=True)
if image_path.stat().st_size <= max_size or quality <= 10:
break
quality -= step
elif suffix == ".png":
width, height = img.size
while True:
img.save(image_path, optimize=True)
if (
image_path.stat().st_size <= max_size
or width <= 200
or height <= 200
):
break
width = int(width * 0.95)
height = int(height * 0.95)
img = img.resize((width, height), RESAMPLE)
else:
raise ValueError("仅支持JPG/JPEG和PNG格式图片的压缩。")
return image_path

169
app/utils/ProcessManager.py Normal file
View File

@@ -0,0 +1,169 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
# This file is part of AUTO_MAA.
# AUTO_MAA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
# AUTO_MAA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
# the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA进程管理组件
v4.4
作者DLmaster_361
"""
import psutil
import subprocess
from pathlib import Path
from datetime import datetime
from PySide6.QtCore import QTimer, QObject, Signal
class ProcessManager(QObject):
"""进程监视器类,用于跟踪主进程及其所有子进程的状态"""
processClosed = Signal()
def __init__(self):
super().__init__()
self.main_pid = None
self.tracked_pids = set()
self.check_timer = QTimer()
self.check_timer.timeout.connect(self.check_processes)
def open_process(
self,
path: Path,
args: list = [],
tracking_time: int = 60,
) -> int:
"""
启动一个新进程并返回其pid并开始监视该进程
:param path: 可执行文件的路径
:param args: 启动参数列表
:param tracking_time: 子进程追踪持续时间(秒)
:return: 新进程的PID
"""
process = subprocess.Popen(
[path, *args],
cwd=path.parent,
creationflags=subprocess.CREATE_NO_WINDOW,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
self.start_monitoring(process.pid, tracking_time)
def start_monitoring(self, pid: int, tracking_time: int = 60) -> None:
"""
启动进程监视器,跟踪指定的主进程及其子进程
:param pid: 被监视进程的PID
:param tracking_time: 子进程追踪持续时间(秒)
"""
self.clear()
self.main_pid = pid
self.tracking_time = tracking_time
# 扫描并记录所有相关进程
try:
# 获取主进程及其子进程
main_proc = psutil.Process(self.main_pid)
self.tracked_pids.add(self.main_pid)
# 递归获取所有子进程
if tracking_time:
for child in main_proc.children(recursive=True):
self.tracked_pids.add(child.pid)
except psutil.NoSuchProcess:
pass
# 启动持续追踪机制
self.start_time = datetime.now()
self.check_timer.start(100)
def check_processes(self) -> None:
"""检查跟踪的进程是否仍在运行,并更新子进程列表"""
# 仅在时限内持续更新跟踪的进程列表,发现新的子进程
if (datetime.now() - self.start_time).total_seconds() < self.tracking_time:
current_pids = set(self.tracked_pids)
for pid in current_pids:
try:
proc = psutil.Process(pid)
for child in proc.children():
if child.pid not in self.tracked_pids:
# 新发现的子进程
self.tracked_pids.add(child.pid)
except psutil.NoSuchProcess:
continue
if not self.is_running():
self.clear()
self.processClosed.emit()
def is_running(self) -> bool:
"""检查所有跟踪的进程是否还在运行"""
for pid in self.tracked_pids:
try:
proc = psutil.Process(pid)
if proc.is_running():
return True
except psutil.NoSuchProcess:
continue
return False
def kill(self, if_force: bool = False) -> None:
"""停止监视器并中止所有跟踪的进程"""
self.check_timer.stop()
for pid in self.tracked_pids:
try:
proc = psutil.Process(pid)
if if_force:
kill_process = subprocess.Popen(
["taskkill", "/F", "/T", "/PID", str(pid)],
creationflags=subprocess.CREATE_NO_WINDOW,
)
kill_process.wait()
proc.terminate()
except psutil.NoSuchProcess:
continue
if self.main_pid:
self.processClosed.emit()
self.clear()
def clear(self) -> None:
"""清空跟踪的进程列表"""
self.main_pid = None
self.check_timer.stop()
self.tracked_pids.clear()

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA工具包
v4.3
v4.4
作者DLmaster_361
"""
@@ -29,4 +29,7 @@ __version__ = "4.2.0"
__author__ = "DLmaster361 <DLmaster_361@163.com>"
__license__ = "GPL-3.0 license"
__all__ = []
from .ImageUtils import ImageUtils
from .ProcessManager import ProcessManager
__all__ = ["ImageUtils", "ProcessManager"]

View File

@@ -21,7 +21,7 @@
"""
AUTO_MAA
AUTO_MAA打包程序
v4.3
v4.4
作者DLmaster_361
"""
@@ -70,13 +70,13 @@ if __name__ == "__main__":
print("Packaging AUTO_MAA main program ...")
os.system(
"powershell -Command python -m nuitka --standalone --onefile --mingw64"
" --enable-plugins=pyside6 --windows-console-mode=disable"
"powershell -Command python -m nuitka --standalone --onefile --mingw64 --windows-uac-admin"
" --enable-plugins=pyside6 --windows-console-mode=attach"
" --onefile-tempdir-spec='{TEMP}\\AUTO_MAA'"
" --windows-icon-from-ico=resources\\icons\\AUTO_MAA.ico"
" --company-name='AUTO_MAA Team' --product-name=AUTO_MAA"
f" --file-version={version["main_version"]}"
f" --product-version={version["main_version"]}"
f" --file-version={version['main_version']}"
f" --product-version={version['main_version']}"
" --file-description='AUTO_MAA Component'"
" --copyright='Copyright © 2024-2025 DLmaster361'"
" --assume-yes-for-downloads --output-filename=AUTO_MAA"

41
main.py
View File

@@ -21,14 +21,43 @@
"""
AUTO_MAA
AUTO_MAA主程序
v4.3
v4.4
作者DLmaster_361
"""
# 屏蔽广告
import builtins
original_print = builtins.print
def no_print(*args, **kwargs):
if (
args
and isinstance(args[0], str)
and "QFluentWidgets Pro is now released." in args[0]
):
return
return original_print(*args, **kwargs)
builtins.print = no_print
from loguru import logger
import os
import sys
import ctypes
from PySide6.QtWidgets import QApplication
from qfluentwidgets import FluentTranslator
import sys
def is_admin() -> bool:
"""检查当前程序是否以管理员身份运行"""
try:
return ctypes.windll.shell32.IsUserAnAdmin()
except:
return False
@logger.catch
@@ -49,4 +78,10 @@ def main():
if __name__ == "__main__":
main()
if is_admin():
main()
else:
ctypes.windll.shell32.ShellExecuteW(
None, "runas", sys.executable, os.path.realpath(sys.argv[0]), None, 1
)
sys.exit(0)

View File

@@ -1,13 +1,15 @@
loguru
plyer
PySide6
loguru==0.7.3
plyer==2.1.0
PySide6==6.9.1
PySide6-Fluent-Widgets[full]
psutil
opencv-python
pywin32
pyautogui
pycryptodome
requests
markdown
Jinja2
nuitka
psutil==7.0.0
pywin32==310
keyboard==0.13.5
pycryptodome==3.23.0
certifi==2025.4.26
truststore==0.10.1
requests==2.32.4
markdown==3.8.2
Jinja2==3.1.6
nuitka==2.7.12
pillow==11.3.0

View File

@@ -24,6 +24,7 @@
"MainFunction.Stage1": "" #主关卡
"MainFunction.Stage2": "" #备选关卡1
"MainFunction.Stage3": "" #备选关卡2
"MainFunction.Stage4": "" #备选关卡3
"Fight.RemainingSanityStage": "Annihilation" #剩余理智关卡
"MainFunction.Series.Quantity": "1" #连战次数
"Penguin.IsDrGrandet": "True" #博朗台模式

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -1,16 +1,60 @@
{
"main_version": "4.3.9.1",
"main_version": "4.4.0.0",
"version_info": {
"4.3.9.1": {
"4.4.0.0": {
"新增功能": [
"语音功能上线"
"通用配置模式接入日志系统"
],
"修复bug": [
"网络模块支持并发请求",
"修复中止任务时程序异常卡顿"
"修复BUG": [
"信任系统证书,并添加网络代理地址配置项 #50",
"适配 MAA 任务及基建设施日志翻译"
],
"程序优化": [
"非UI组件转为QObject类"
"重构历史记录保存与载入逻辑"
]
},
"4.4.0.5": {
"新增功能": [
"添加导入导出通用配置功能"
],
"修复BUG": [
"修复开机自启相关功能"
]
},
"4.4.0.4": {
"新增功能": [
"添加重置管理密钥功能"
],
"修复BUG": [
"修复无计划表时数据系统无法正常升级到v1.7的问题"
]
},
"4.4.0.3": {
"修复BUG": [
"适配 MAA 备选关卡字段修改",
"修复无成功日志时的脚本判定逻辑"
],
"程序优化": [
"`GameId`字段改为 `Stage`,与 MAA 保持一致"
]
},
"4.4.0.2": {
"新增功能": [
"进一步适配三月七相关配置项"
],
"修复BUG": [
"适配 Mirror 酱 平台下载策略调整"
]
},
"4.4.0.1": {
"新增功能": [
"初步完成通用调度模块"
],
"修复BUG": [
"修复了程序BUG较少的BUG"
],
"程序优化": [
"子线程卡死不再阻塞调度任务"
]
}
}